@bastani/atomic 0.9.14-alpha.3 → 0.9.14-alpha.5

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 (150) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  5. package/dist/builtin/subagents/package.json +1 -1
  6. package/dist/builtin/subagents/src/extension/schemas.ts +5 -0
  7. package/dist/builtin/subagents/src/runs/shared/long-running-guard.ts +3 -1
  8. package/dist/builtin/subagents/src/runs/shared/progress-trend.ts +69 -0
  9. package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +12 -1
  10. package/dist/builtin/subagents/src/shared/types-results.ts +2 -0
  11. package/dist/builtin/web-access/CHANGELOG.md +10 -0
  12. package/dist/builtin/web-access/README.md +3 -6
  13. package/dist/builtin/web-access/index.ts +1 -2
  14. package/dist/builtin/web-access/package.json +1 -1
  15. package/dist/builtin/web-access/web-search-config.ts +2 -8
  16. package/dist/builtin/web-access/web-search-tool.ts +2 -7
  17. package/dist/builtin/web-access/web-search-workflow.ts +10 -0
  18. package/dist/builtin/workflows/CHANGELOG.md +46 -0
  19. package/dist/builtin/workflows/README.md +6 -5
  20. package/dist/builtin/workflows/builtin/adversarial-verification-prompts.ts +13 -5
  21. package/dist/builtin/workflows/builtin/adversarial-verification-runner.ts +376 -89
  22. package/dist/builtin/workflows/builtin/adversarial-verification.d.ts +30 -6
  23. package/dist/builtin/workflows/builtin/adversarial-verification.ts +14 -9
  24. package/dist/builtin/workflows/builtin/generate-and-filter-prompts.ts +26 -3
  25. package/dist/builtin/workflows/builtin/generate-and-filter-runner.ts +18 -14
  26. package/dist/builtin/workflows/builtin/goal-artifacts.ts +9 -8
  27. package/dist/builtin/workflows/builtin/goal-convergence.ts +87 -0
  28. package/dist/builtin/workflows/builtin/goal-ledger.ts +4 -0
  29. package/dist/builtin/workflows/builtin/goal-prompts.ts +2 -0
  30. package/dist/builtin/workflows/builtin/goal-reducer.ts +6 -1
  31. package/dist/builtin/workflows/builtin/goal-reverify.ts +305 -0
  32. package/dist/builtin/workflows/builtin/goal-runner.ts +75 -10
  33. package/dist/builtin/workflows/builtin/goal-schemas.ts +7 -0
  34. package/dist/builtin/workflows/builtin/goal-types.ts +6 -0
  35. package/dist/builtin/workflows/builtin/loop-until-done-runner.ts +94 -6
  36. package/dist/builtin/workflows/builtin/loop-until-done.d.ts +8 -0
  37. package/dist/builtin/workflows/builtin/loop-until-done.ts +15 -0
  38. package/dist/builtin/workflows/builtin/progress-scoring.ts +230 -0
  39. package/dist/builtin/workflows/builtin/ralph-core.ts +11 -0
  40. package/dist/builtin/workflows/builtin/ralph-review-gate.ts +1 -0
  41. package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +2 -0
  42. package/dist/builtin/workflows/builtin/ralph-runner.ts +60 -10
  43. package/dist/builtin/workflows/builtin/selection-math.ts +156 -0
  44. package/dist/builtin/workflows/builtin/shared-prompts.ts +5 -0
  45. package/dist/builtin/workflows/builtin/tournament-prompts.ts +57 -75
  46. package/dist/builtin/workflows/builtin/tournament-runner.ts +384 -178
  47. package/dist/builtin/workflows/builtin/tournament.d.ts +46 -17
  48. package/dist/builtin/workflows/builtin/tournament.ts +66 -32
  49. package/dist/builtin/workflows/builtin/verification-criteria.ts +330 -0
  50. package/dist/builtin/workflows/builtin/verification-prompts.ts +206 -0
  51. package/dist/builtin/workflows/builtin/verification-usage.ts +44 -0
  52. package/dist/builtin/workflows/package.json +1 -1
  53. package/dist/builtin/workflows/skills/create-spec/SKILL.md +90 -30
  54. package/dist/builtin/workflows/skills/show-me/LICENSE.txt +21 -0
  55. package/dist/builtin/workflows/skills/show-me/SKILL.md +143 -0
  56. package/dist/builtin/workflows/src/authoring/workflow.ts +8 -0
  57. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  58. package/dist/builtin/workflows/src/durable/completed-catalog.ts +5 -2
  59. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +1 -1
  60. package/dist/builtin/workflows/src/durable/resume-eligibility.ts +5 -3
  61. package/dist/builtin/workflows/src/durable/run-timing.ts +41 -10
  62. package/dist/builtin/workflows/src/durable/tool-primitive.ts +24 -2
  63. package/dist/builtin/workflows/src/engine/options.ts +1 -0
  64. package/dist/builtin/workflows/src/engine/primitives/workflow.ts +12 -3
  65. package/dist/builtin/workflows/src/engine/run-budget.ts +308 -0
  66. package/dist/builtin/workflows/src/engine/run-returned-status.ts +8 -0
  67. package/dist/builtin/workflows/src/engine/run-tool-node-lifecycle.ts +6 -0
  68. package/dist/builtin/workflows/src/engine/run.ts +124 -2
  69. package/dist/builtin/workflows/src/engine/runtime.ts +9 -0
  70. package/dist/builtin/workflows/src/extension/config-file-loader.ts +6 -0
  71. package/dist/builtin/workflows/src/extension/config-loader.ts +24 -1
  72. package/dist/builtin/workflows/src/extension/dispatcher.ts +6 -5
  73. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +2 -0
  74. package/dist/builtin/workflows/src/extension/index.bundle.mjs +3033 -872
  75. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +51 -4
  76. package/dist/builtin/workflows/src/extension/public-types.ts +3 -1
  77. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +7 -1
  78. package/dist/builtin/workflows/src/extension/runtime.ts +22 -10
  79. package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +5 -0
  80. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +1 -0
  81. package/dist/builtin/workflows/src/extension/workflow-schema.ts +16 -0
  82. package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +44 -1
  83. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +10 -1
  84. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +21 -9
  85. package/dist/builtin/workflows/src/runs/foreground/executor-continuation.ts +14 -0
  86. package/dist/builtin/workflows/src/runs/foreground/executor-lifecycle.ts +15 -4
  87. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +62 -5
  88. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +4 -0
  89. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +2 -0
  90. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +3 -1
  91. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -1
  92. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +1 -0
  93. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +1 -0
  94. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +7 -0
  95. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +7 -0
  96. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +1 -0
  97. package/dist/builtin/workflows/src/shared/budget-meter.ts +34 -0
  98. package/dist/builtin/workflows/src/shared/budget.d.ts +67 -0
  99. package/dist/builtin/workflows/src/shared/budget.ts +127 -0
  100. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +92 -8
  101. package/dist/builtin/workflows/src/shared/persistence-restore.ts +11 -1
  102. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +15 -3
  103. package/dist/builtin/workflows/src/shared/returned-run-status.ts +35 -2
  104. package/dist/builtin/workflows/src/shared/store-public-types.ts +4 -1
  105. package/dist/builtin/workflows/src/shared/store-run-methods.ts +8 -1
  106. package/dist/builtin/workflows/src/shared/store-stage-methods.ts +1 -0
  107. package/dist/builtin/workflows/src/shared/store-types.ts +24 -0
  108. package/dist/builtin/workflows/src/shared/types.ts +3 -0
  109. package/dist/builtin/workflows/src/shared/workflow-artifacts.ts +1 -0
  110. package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +3 -0
  111. package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +3 -0
  112. package/dist/builtin/workflows/src/tui/graph-theme.ts +11 -0
  113. package/dist/builtin/workflows/src/tui/graph-view-render.ts +19 -10
  114. package/dist/builtin/workflows/src/tui/tool-detail.ts +45 -26
  115. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  116. package/dist/core/atomic-guide-command.js +1 -0
  117. package/dist/core/atomic-guide-command.js.map +1 -1
  118. package/dist/core/extensions/ui-types.d.ts +13 -3
  119. package/dist/core/extensions/ui-types.d.ts.map +1 -1
  120. package/dist/core/extensions/ui-types.js +15 -3
  121. package/dist/core/extensions/ui-types.js.map +1 -1
  122. package/dist/core/slash-commands.d.ts.map +1 -1
  123. package/dist/core/slash-commands.js +33 -3
  124. package/dist/core/slash-commands.js.map +1 -1
  125. package/dist/main-deferred-startup.d.ts.map +1 -1
  126. package/dist/main-deferred-startup.js +6 -2
  127. package/dist/main-deferred-startup.js.map +1 -1
  128. package/dist/modes/interactive/interactive-startup.js +4 -0
  129. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  130. package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
  131. package/dist/modes/interactive/interactive-tui.js +19 -1
  132. package/dist/modes/interactive/interactive-tui.js.map +1 -1
  133. package/dist/modes/interactive-engine/isolated-runtime.d.ts +7 -0
  134. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  135. package/dist/modes/interactive-engine/isolated-runtime.js +94 -37
  136. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  137. package/dist/modes/rpc/rpc-client.d.ts +1 -0
  138. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  139. package/dist/modes/rpc/rpc-client.js +15 -2
  140. package/dist/modes/rpc/rpc-client.js.map +1 -1
  141. package/dist/modes/rpc/rpc-input-scheduler.d.ts +3 -2
  142. package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
  143. package/dist/modes/rpc/rpc-input-scheduler.js +5 -2
  144. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  145. package/docs/extensions.md +1 -1
  146. package/docs/quickstart.md +1 -0
  147. package/docs/skills.md +4 -0
  148. package/docs/workflows.md +75 -11
  149. package/npm-shrinkwrap.json +29 -29
  150. package/package.json +2 -2
@@ -1,5 +1,11 @@
1
1
  import { join } from "node:path";
2
2
  import type { WorkflowParallelOptions, WorkflowTaskOptions, WorkflowTaskResult, WorkflowTaskStep } from "../src/shared/types.js";
3
+ import { fold_usage } from "./verification-usage.js";
4
+ import {
5
+ convergence_escalation_evidence,
6
+ record_convergence,
7
+ type ConvergenceEntry,
8
+ } from "./goal-convergence.js";
3
9
  import { orchestratorModelConfig, reviewerModelConfig } from "./goal-models.js";
4
10
  import {
5
11
  DEFAULT_BLOCKER_THRESHOLD,
@@ -22,6 +28,8 @@ import {
22
28
  reviewDecisionToRecord,
23
29
  } from "./goal-review.js";
24
30
  import { reviewerFailureText } from "./review-convergence.js";
31
+ import { consolidateFindingsBatch } from "./review-convergence.js";
32
+ import { reverify_consolidated_batch } from "./goal-reverify.js";
25
33
  import {
26
34
  renderForkedGoalOrchestratorPrompt,
27
35
  renderGoalOrchestratorPrompt,
@@ -79,11 +87,13 @@ function reviewerExecutionFailedDecision(input: {
79
87
  readonly reviewQuorum: number;
80
88
  readonly reviews: readonly ReviewRecord[];
81
89
  readonly reason: string;
90
+ readonly convergence?: readonly ConvergenceEntry[];
82
91
  }): ReducerDecision {
92
+ const evidence = convergence_escalation_evidence(input.convergence ?? []);
83
93
  return {
84
94
  turn: input.turn,
85
95
  decision: "needs_human",
86
- reason: input.reason,
96
+ reason: [input.reason, ...evidence].join("\n"),
87
97
  complete_votes: input.reviews.filter((review) => review.decision === "complete").length,
88
98
  review_quorum: input.reviewQuorum,
89
99
  parsed: input.reviews.every((review) => review.parsed),
@@ -150,7 +160,9 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
150
160
  });
151
161
  } catch (err) {
152
162
  const message = err instanceof Error ? err.message : String(err);
153
- terminalRemainingWork = `Orchestrator failed before producing a receipt: ${message}`;
163
+ const baseReason = `Orchestrator failed before producing a receipt: ${message}`;
164
+ terminalRemainingWork = baseReason;
165
+ const reason = [baseReason, ...convergence_escalation_evidence(ledger.convergence ?? [])].join("\n");
154
166
  latestReviews = [];
155
167
  latestReviewArtifactPaths = [];
156
168
  latestReviewReportPath = undefined;
@@ -159,7 +171,7 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
159
171
  ledger.decisions.push({
160
172
  turn,
161
173
  decision: "needs_human",
162
- reason: terminalRemainingWork,
174
+ reason,
163
175
  complete_votes: 0,
164
176
  review_quorum: reviewQuorum,
165
177
  parsed: false,
@@ -167,9 +179,9 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
167
179
  stopReviewLoop: false,
168
180
  nextAction: "needs_human",
169
181
  finalActionRemaining: false,
170
- diagnostics: [terminalRemainingWork],
182
+ diagnostics: [baseReason],
171
183
  });
172
- appendLifecycleEvent(ledger, "status_decided", terminalRemainingWork, turn);
184
+ appendLifecycleEvent(ledger, "status_decided", reason, turn);
173
185
  await writeGoalLedger(ledgerPath, ledger);
174
186
  break;
175
187
  }
@@ -269,10 +281,60 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
269
281
  );
270
282
  return record;
271
283
  }));
272
- latestReviewReportPath = await writeReviewRoundArtifact(artifactDir, latestReviews);
284
+ const consolidatedFindings = consolidateFindingsBatch(
285
+ latestReviews.map((review) => ({
286
+ reviewer: review.reviewer,
287
+ findings: review.findings,
288
+ })),
289
+ );
290
+ const roundProducedDecisions = latestReviews.some((review) => review.parsed);
291
+ const reverifyResults: WorkflowTaskResult[] = [];
292
+ const reverifyContext = {
293
+ task: async (name: string, taskOptions: WorkflowTaskOptions): Promise<WorkflowTaskResult> => {
294
+ const result = await ctx.task(name, taskOptions);
295
+ reverifyResults.push(result);
296
+ return result;
297
+ },
298
+ };
299
+ const reverified = await reverify_consolidated_batch(reverifyContext, {
300
+ batch: consolidatedFindings,
301
+ context: {
302
+ objective,
303
+ candidateRefs: [ledgerPath, orchestratorReceiptPath],
304
+ },
305
+ });
306
+ latestReviewReportPath = await writeReviewRoundArtifact(
307
+ artifactDir,
308
+ latestReviews,
309
+ reverified.batch,
310
+ reverified.audits,
311
+ );
312
+ if (reverified.audits.length > 0) {
313
+ ledger.reverification ??= [];
314
+ ledger.reverification.push(...reverified.audits);
315
+ }
316
+ const findings = latestReviews.flatMap((review) => review.findings);
317
+ const traceability = latestReviews.flatMap((review) => review.requirements_traceability);
318
+ ledger.convergence ??= [];
319
+ // A thrown reviewer batch or an all-unparsed reviewer batch produced no
320
+ // decisions, so recording a zero-blocker round would fabricate progress
321
+ // and can suppress the escalation evidence on the very escalation it triggers.
322
+ if (!reviewerBatchFailed && roundProducedDecisions) {
323
+ ledger.convergence.push(record_convergence({
324
+ unresolvedBlockingCount: reverified.batch.filter((entry) => entry.blocking).length,
325
+ meanFindingConfidence: findings.length === 0
326
+ ? null
327
+ : findings.reduce((total, finding) => total + finding.confidence_score, 0) / findings.length,
328
+ fractionProven: traceability.length === 0
329
+ ? 0
330
+ : traceability.filter((entry) => entry.status === "proven").length / traceability.length,
331
+ demotions: reverified.audits.filter((audit) => audit.verdict === "demoted").length,
332
+ usage: fold_usage([orchestrator, ...reviewResults, ...reverifyResults]),
333
+ }));
334
+ }
335
+ ledger.reviews.push(...latestReviews);
273
336
  // Consolidated round artifact leads so the next orchestrator turn plans the full findings batch first.
274
337
  latestReviewArtifactPaths = [latestReviewReportPath, ...latestReviews.map((review) => review.artifact_path)];
275
- ledger.reviews.push(...latestReviews);
276
338
  appendLifecycleEvent(
277
339
  ledger,
278
340
  "reviews_recorded",
@@ -282,14 +344,16 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
282
344
  if (reviewerBatchFailed) {
283
345
  terminalRemainingWork = collectRemainingWork(latestReviews);
284
346
  const reason = `Reviewer execution failed before quorum could be established. Remaining work: ${terminalRemainingWork}`;
285
- ledger.decisions.push(reviewerExecutionFailedDecision({
347
+ const decision = reviewerExecutionFailedDecision({
286
348
  turn,
287
349
  reviewQuorum,
288
350
  reviews: latestReviews,
289
351
  reason,
290
- }));
352
+ convergence: ledger.convergence,
353
+ });
354
+ ledger.decisions.push(decision);
291
355
  ledger.status = "needs_human";
292
- appendLifecycleEvent(ledger, "status_decided", reason, turn);
356
+ appendLifecycleEvent(ledger, "status_decided", decision.reason, turn);
293
357
  await writeGoalLedger(ledgerPath, ledger);
294
358
  break;
295
359
  }
@@ -300,6 +364,7 @@ export async function runGoalWorkflow(ctx: GoalRunnerContext, options: GoalWorkf
300
364
  reviewQuorum,
301
365
  blockerThreshold,
302
366
  nextActionOnComplete: createPr ? "pull-request" : "finish",
367
+ convergence: ledger.convergence,
303
368
  });
304
369
  if (reducerOutcome.blockerObservation !== undefined) {
305
370
  ledger.blockers.push(reducerOutcome.blockerObservation);
@@ -1,4 +1,5 @@
1
1
  import { Type } from "typebox";
2
+ import { VERIFICATION_SCALE } from "./verification-criteria.js";
2
3
 
3
4
  const reviewFindingSchema = Type.Object(
4
5
  {
@@ -74,6 +75,12 @@ export const reviewDecisionSchema = Type.Object(
74
75
  receipt_assessment: Type.String(),
75
76
  verification_remaining: Type.String(),
76
77
  stop_review_loop: Type.Boolean(),
78
+ criterion_scores: Type.Optional(
79
+ Type.Array(Type.Object({
80
+ criterion_id: Type.String(),
81
+ score: VERIFICATION_SCALE.schema,
82
+ }, { additionalProperties: false })),
83
+ ),
77
84
  reviewer_error: Type.Optional(
78
85
  Type.Union([Type.Null(), reviewerErrorSchema]),
79
86
  ),
@@ -1,4 +1,6 @@
1
1
  import type { ReviewConvergenceSummary } from "./review-convergence.js";
2
+ import type { ReverifyAuditEntry } from "./goal-reverify.js";
3
+ import type { ConvergenceEntry } from "./goal-convergence.js";
2
4
 
3
5
  export const DEFAULT_MAX_TURNS = 10;
4
6
  // Goal Runner runs three independent reviewer personas; two approvals form a majority.
@@ -55,6 +57,7 @@ export type ReviewerError = {
55
57
 
56
58
  export type ReviewDecision = {
57
59
  readonly findings: readonly ReviewFinding[];
60
+ readonly criterion_scores?: readonly { readonly criterion_id: string; readonly score: number }[];
58
61
  readonly overall_correctness: "patch is correct" | "patch is incorrect";
59
62
  readonly overall_explanation: string;
60
63
  readonly overall_confidence_score: number;
@@ -123,6 +126,9 @@ export type GoalLedger = {
123
126
  blockers: BlockerObservation[];
124
127
  decisions: ReducerDecision[];
125
128
  lifecycle: GoalLifecycleEvent[];
129
+ /** V9 audit records; the original findings remain in `reviews`. */
130
+ reverification?: ReverifyAuditEntry<ReviewFinding>[];
131
+ convergence?: ConvergenceEntry[];
126
132
  };
127
133
 
128
134
  export type ReducerOutcome = {
@@ -7,6 +7,7 @@ import type {
7
7
  WorkflowSerializableValue,
8
8
  WorkflowTaskResult,
9
9
  } from "../src/shared/types.js";
10
+ import { classify_trend, score_progress, type Trend } from "./progress-scoring.js";
10
11
  import {
11
12
  renderCompletionPrompt,
12
13
  renderEvaluationPrompt,
@@ -14,6 +15,8 @@ import {
14
15
  } from "./loop-until-done-prompts.js";
15
16
  import { stableArtifactRoot } from "./pattern-artifact-root.js";
16
17
 
18
+ const PROGRESS_DISCLAIMER = "Progress scores are a monitoring signal; VOC separation +0.079; never authoritative.";
19
+
17
20
  const evaluationSchema = Type.Object({
18
21
  done: Type.Boolean(),
19
22
  summary: Type.String(),
@@ -26,6 +29,8 @@ const evaluationSchema = Type.Object({
26
29
  type LoopInputs = {
27
30
  readonly prompt: string;
28
31
  readonly max_iterations: number;
32
+ readonly progress_scoring?: boolean;
33
+ readonly progress_repeats?: number;
29
34
  } & Record<string, WorkflowSerializableValue>;
30
35
 
31
36
  type Evaluation = {
@@ -36,6 +41,12 @@ type Evaluation = {
36
41
  readonly validationEvidence: readonly string[];
37
42
  readonly remainingWork: string;
38
43
  };
44
+ type LedgerProgress = {
45
+ readonly score: number;
46
+ readonly perRepeat: (number | null)[][];
47
+ readonly trend: Trend;
48
+ readonly window: number;
49
+ };
39
50
  type LedgerEntry = {
40
51
  readonly iteration: number;
41
52
  readonly artifact_path: string;
@@ -46,7 +57,57 @@ type LedgerEntry = {
46
57
  readonly validation_evidence: readonly string[];
47
58
  readonly done: boolean;
48
59
  readonly remaining_work: string;
60
+ readonly progress?: LedgerProgress;
49
61
  };
62
+
63
+ function progressCurve(entries: readonly LedgerEntry[]): number[] {
64
+ return entries.flatMap((entry) => (entry.progress === undefined ? [] : [entry.progress.score]));
65
+ }
66
+
67
+ function progressReport(entries: readonly LedgerEntry[]): { curve: number[]; trend: Trend } {
68
+ const curve = progressCurve(entries);
69
+ return { curve, trend: classify_trend(curve).trend };
70
+ }
71
+
72
+ function formatProgressReport(report: { curve: readonly number[]; trend: Trend }): string {
73
+ return [
74
+ `Progress curve: ${JSON.stringify(report.curve)}`,
75
+ `Final trend: ${report.trend}`,
76
+ PROGRESS_DISCLAIMER,
77
+ ].join("\n");
78
+ }
79
+
80
+ function repeatCount(input: LoopInputs): number {
81
+ const repeats = input.progress_repeats;
82
+ return typeof repeats === "number" && Number.isInteger(repeats) && repeats > 0 ? repeats : 1;
83
+ }
84
+
85
+ async function scoreIteration(
86
+ ctx: WorkflowRunContext<LoopInputs>,
87
+ task: string,
88
+ entries: readonly LedgerEntry[],
89
+ repeats: number,
90
+ ): Promise<LedgerProgress | undefined> {
91
+ try {
92
+ const curve = await score_progress(ctx, {
93
+ problem: task,
94
+ steps: entries.map((entry) => entry.summary),
95
+ checkpoints: [entries.length],
96
+ repeats,
97
+ });
98
+ const score = curve.scores[0];
99
+ if (score === null || score === undefined) return undefined;
100
+ const trend = classify_trend([...progressCurve(entries), score]);
101
+ return {
102
+ score,
103
+ perRepeat: curve.perRepeat,
104
+ trend: trend.trend,
105
+ window: trend.evidence.window,
106
+ };
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }
50
111
  function serializableObject(
51
112
  value: WorkflowSerializableValue | undefined,
52
113
  ): WorkflowSerializableObject | undefined {
@@ -79,14 +140,23 @@ function evaluationFrom(result: WorkflowTaskResult): Evaluation {
79
140
  return { done, summary, newFindings, failures, validationEvidence, remainingWork };
80
141
  }
81
142
 
82
- async function writeLedger(path: string, task: string, maxIterations: number, status: string,
83
- entries: readonly LedgerEntry[]): Promise<void> {
143
+ async function writeLedger(
144
+ path: string,
145
+ task: string,
146
+ maxIterations: number,
147
+ status: string,
148
+ entries: readonly LedgerEntry[],
149
+ ): Promise<void> {
150
+ const report = progressReport(entries);
84
151
  await writeFile(path, `${JSON.stringify({
85
152
  task,
86
153
  max_iterations: maxIterations,
87
154
  status,
88
155
  iterations_completed: entries.length,
89
156
  entries,
157
+ progress_curve: report.curve,
158
+ final_trend: report.trend,
159
+ progress_disclaimer: PROGRESS_DISCLAIMER,
90
160
  }, null, 2)}\n`);
91
161
  }
92
162
 
@@ -135,7 +205,7 @@ export async function runLoopUntilDone(ctx: WorkflowRunContext<LoopInputs>) {
135
205
  // runner persists the structured decision itself so evaluation-N.json
136
206
  // stays schema-shaped JSON rather than stage prose.
137
207
  await writeFile(evaluationPath, `${JSON.stringify(evaluator.structured, null, 2)}\n`);
138
- entries.push({
208
+ const entry: LedgerEntry = {
139
209
  iteration,
140
210
  artifact_path: iterationPath,
141
211
  evaluation_artifact_path: evaluationPath,
@@ -145,7 +215,12 @@ export async function runLoopUntilDone(ctx: WorkflowRunContext<LoopInputs>) {
145
215
  validation_evidence: decision.validationEvidence,
146
216
  done: decision.done,
147
217
  remaining_work: decision.remainingWork,
148
- });
218
+ };
219
+ entries.push(entry);
220
+ if (ctx.inputs.progress_scoring !== false) {
221
+ const progress = await scoreIteration(ctx, ctx.inputs.prompt, entries, repeatCount(ctx.inputs));
222
+ if (progress !== undefined) entries[entries.length - 1] = { ...entry, progress };
223
+ }
149
224
  await writeLedger(
150
225
  ledgerPath,
151
226
  ctx.inputs.prompt,
@@ -154,6 +229,7 @@ export async function runLoopUntilDone(ctx: WorkflowRunContext<LoopInputs>) {
154
229
  entries,
155
230
  );
156
231
  if (decision.done) {
232
+ const report = progressReport(entries);
157
233
  const resultPath = join(artifactDir, "result.md");
158
234
  const final = await ctx.task("completion-summary", {
159
235
  prompt: renderCompletionPrompt({ task: ctx.inputs.prompt, ledgerPath, iterationPath }),
@@ -164,8 +240,9 @@ export async function runLoopUntilDone(ctx: WorkflowRunContext<LoopInputs>) {
164
240
  // `result_path` below carries it for callers that want the contents.
165
241
  outputMode: "file-only",
166
242
  });
243
+ const result = `${final.text ? `${final.text.trimEnd()}\n\n` : ""}${formatProgressReport(report)}\n`;
167
244
  return {
168
- result: final.text,
245
+ result,
169
246
  status: "complete" as const,
170
247
  iterations_completed: iteration,
171
248
  ledger_path: ledgerPath,
@@ -174,14 +251,22 @@ export async function runLoopUntilDone(ctx: WorkflowRunContext<LoopInputs>) {
174
251
  result_path: resultPath,
175
252
  remaining_work: "",
176
253
  artifact_dir: artifactDir,
254
+ progress_curve: report.curve,
255
+ final_trend: report.trend,
256
+ progress_disclaimer: PROGRESS_DISCLAIMER,
177
257
  };
178
258
  }
179
259
  }
180
260
 
181
261
  const last = entries.at(-1)!;
262
+ const report = progressReport(entries);
182
263
  await writeLedger(ledgerPath, ctx.inputs.prompt, ctx.inputs.max_iterations, "failed", entries);
264
+ const result = [
265
+ `Iteration limit exhausted after ${ctx.inputs.max_iterations} iterations. Inspect ${ledgerPath}.`,
266
+ formatProgressReport(report),
267
+ ].join("\n\n");
183
268
  return {
184
- result: `Iteration limit exhausted after ${ctx.inputs.max_iterations} iterations. Inspect ${ledgerPath}.`,
269
+ result,
185
270
  status: "failed" as const,
186
271
  iterations_completed: ctx.inputs.max_iterations,
187
272
  ledger_path: ledgerPath,
@@ -190,5 +275,8 @@ export async function runLoopUntilDone(ctx: WorkflowRunContext<LoopInputs>) {
190
275
  result_path: ledgerPath,
191
276
  remaining_work: last.remaining_work,
192
277
  artifact_dir: artifactDir,
278
+ progress_curve: report.curve,
279
+ final_trend: report.trend,
280
+ progress_disclaimer: PROGRESS_DISCLAIMER,
193
281
  };
194
282
  }
@@ -1,15 +1,20 @@
1
1
  import type { WorkflowDefinition, WorkflowInputValues, WorkflowOutputValues } from "../src/authoring.js";
2
2
 
3
3
  export type LoopUntilDoneWorkflowStatus = "complete" | "failed";
4
+ export type LoopUntilDoneProgressTrend = "rising" | "flat" | "regressing";
4
5
 
5
6
  export type LoopUntilDoneWorkflowInputs = WorkflowInputValues & {
6
7
  readonly prompt: string;
7
8
  readonly max_iterations: number;
9
+ readonly progress_scoring: boolean;
10
+ readonly progress_repeats: number;
8
11
  };
9
12
 
10
13
  export type LoopUntilDoneWorkflowRunInputs = WorkflowInputValues & {
11
14
  readonly prompt: string;
12
15
  readonly max_iterations?: number;
16
+ readonly progress_scoring?: boolean;
17
+ readonly progress_repeats?: number;
13
18
  };
14
19
 
15
20
  export type LoopUntilDoneWorkflowOutputs = WorkflowOutputValues & {
@@ -22,6 +27,9 @@ export type LoopUntilDoneWorkflowOutputs = WorkflowOutputValues & {
22
27
  readonly result_path: string;
23
28
  readonly remaining_work: string;
24
29
  readonly artifact_dir: string;
30
+ readonly progress_curve: number[];
31
+ readonly final_trend: LoopUntilDoneProgressTrend;
32
+ readonly progress_disclaimer: string;
25
33
  };
26
34
 
27
35
  export type LoopUntilDoneWorkflowDefinition = WorkflowDefinition<
@@ -18,6 +18,15 @@ export default workflow({
18
18
  default: 5,
19
19
  description: "Maximum work/evaluation iterations before returning an inspectable failed status (1-20).",
20
20
  }),
21
+ progress_scoring: Type.Boolean({
22
+ default: true,
23
+ description: "Enable advisory progress scoring after each completed iteration.",
24
+ }),
25
+ progress_repeats: Type.Integer({
26
+ minimum: 1,
27
+ default: 1,
28
+ description: "Number of advisory progress-scoring repeats per iteration.",
29
+ }),
21
30
  },
22
31
  outputs: {
23
32
  result: Type.String({ description: "Compact reference to the evidence-backed completion report, or the deterministic exhaustion report; read `result_path` for the full report." }),
@@ -31,6 +40,12 @@ export default workflow({
31
40
  result_path: Type.String({ description: "Path to the final report, or the ledger on exhausted failure." }),
32
41
  remaining_work: Type.String({ description: "Actionable remaining work; empty only after proven completion." }),
33
42
  artifact_dir: Type.String({ description: "Run-specific directory containing loop artifacts." }),
43
+ progress_curve: Type.Array(Type.Number(), { description: "Advisory progress scores in iteration order." }),
44
+ final_trend: Type.Union(
45
+ [Type.Literal("rising"), Type.Literal("flat"), Type.Literal("regressing")],
46
+ { description: "Final advisory trend classification; never a stop decision." },
47
+ ),
48
+ progress_disclaimer: Type.String({ description: "Calibration disclaimer for the advisory progress signal." }),
34
49
  },
35
50
  run: async (ctx) => await runLoopUntilDone(withSteeringPropagationContext(ctx)),
36
51
  });
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Observation-grounded trajectory scoring and pure trend evidence.
3
+ *
4
+ * Progress is a monitoring magnitude only. The prompt asks about the supplied
5
+ * current state, and the trend result carries evidence without an action.
6
+ */
7
+ import { Type } from "typebox";
8
+ import type { WorkflowRunContext, WorkflowSerializableObject, WorkflowSerializableValue } from "../src/shared/types.js";
9
+ import { VERIFICATION_SCALE } from "./verification-criteria.js";
10
+
11
+ export const DEFAULT_TREND_WINDOW = 3;
12
+ export const DEFAULT_RISE_DELTA = 1.5;
13
+ export const DEFAULT_FALL_DELTA = -1.5;
14
+
15
+ const DEFAULT_REPEATS = 1;
16
+ const CALIBRATION_RULES = [
17
+ "Trust observed output, not the agent's narration.",
18
+ "Effort and step count are NOT progress.",
19
+ 'Agent declarations of success ("done!", "all tests pass") are ZERO evidence.',
20
+ "Scores may plateau or fall; wrong approaches plateau, and regressions decrease.",
21
+ ] as const;
22
+ const PROGRESS_SCALE_ORIENTATION = `${VERIFICATION_SCALE.min} = certainly would not satisfy the acceptance criteria … ${VERIFICATION_SCALE.max} = verified satisfaction with observed output`;
23
+
24
+ export interface ProgressPromptInput {
25
+ readonly problem: string;
26
+ readonly steps: readonly string[];
27
+ readonly checkpoints: readonly number[];
28
+ }
29
+
30
+ export interface ProgressScoreInput {
31
+ readonly problem: string;
32
+ readonly steps: readonly string[];
33
+ readonly checkpoints?: readonly number[];
34
+ readonly repeats?: number;
35
+ }
36
+
37
+ export type ProgressScoringContext = Pick<WorkflowRunContext, "task">;
38
+
39
+ export type ProgressCurve = {
40
+ checkpoints: number[];
41
+ scores: (number | null)[];
42
+ perRepeat: (number | null)[][];
43
+ };
44
+
45
+ export type TrendConfig = {
46
+ window?: number;
47
+ riseDelta?: number;
48
+ fallDelta?: number;
49
+ };
50
+
51
+ export type Trend = "rising" | "flat" | "regressing";
52
+
53
+ export type TrendResult = {
54
+ trend: Trend;
55
+ evidence: {
56
+ series: readonly number[];
57
+ window: number;
58
+ delta: number;
59
+ };
60
+ };
61
+
62
+ const progressSchema = Type.Object({
63
+ scores: Type.Array(Type.Object({
64
+ checkpoint: Type.Integer({ minimum: 1 }),
65
+ score: VERIFICATION_SCALE.schema,
66
+ }, { additionalProperties: false })),
67
+ }, { additionalProperties: false });
68
+
69
+ type ProgressStructuredOutput = {
70
+ readonly scores: readonly WorkflowSerializableValue[];
71
+ };
72
+
73
+ function isRecord(value: WorkflowSerializableValue | undefined): value is WorkflowSerializableObject {
74
+ return value !== null && typeof value === "object" && !Array.isArray(value);
75
+ }
76
+
77
+ function isProgressStructuredOutput(value: WorkflowSerializableValue | undefined): value is ProgressStructuredOutput {
78
+ if (!isRecord(value) || Object.keys(value).length !== 1 || !Object.hasOwn(value, "scores")) return false;
79
+ return Array.isArray(value.scores);
80
+ }
81
+
82
+ function nullScores(checkpointCount: number): (number | null)[] {
83
+ return Array.from({ length: checkpointCount }, () => null);
84
+ }
85
+
86
+ function defaultCheckpoints(stepCount: number): number[] {
87
+ const result: number[] = [];
88
+ for (let checkpoint = 2; checkpoint <= stepCount - 1; checkpoint += 1) result.push(checkpoint);
89
+ return result;
90
+ }
91
+
92
+ function validateCheckpoints(checkpoints: readonly number[], stepCount: number): void {
93
+ for (const checkpoint of checkpoints) {
94
+ if (!Number.isInteger(checkpoint) || checkpoint < 1 || checkpoint > stepCount) {
95
+ throw new RangeError(`Progress checkpoint ${checkpoint} is outside the step range 1..${stepCount}.`);
96
+ }
97
+ }
98
+ }
99
+
100
+ function scoresFromStructured(
101
+ value: WorkflowSerializableValue | undefined,
102
+ checkpoints: readonly number[],
103
+ ): (number | null)[] {
104
+ if (!isProgressStructuredOutput(value)) return nullScores(checkpoints.length);
105
+ const requested = new Set(checkpoints);
106
+ const seen = new Set<number>();
107
+ const scoresByCheckpoint = new Map<number, number>();
108
+ for (const candidate of value.scores) {
109
+ if (!isRecord(candidate) || Object.keys(candidate).length !== 2 ||
110
+ !Object.hasOwn(candidate, "checkpoint") || !Object.hasOwn(candidate, "score")) continue;
111
+ const checkpoint = candidate.checkpoint;
112
+ const score = candidate.score;
113
+ if (typeof checkpoint !== "number" || !Number.isInteger(checkpoint) || seen.has(checkpoint)) continue;
114
+ if (!requested.has(checkpoint) || typeof score !== "number" || !Number.isInteger(score) ||
115
+ score < VERIFICATION_SCALE.min || score > VERIFICATION_SCALE.max) continue;
116
+ seen.add(checkpoint);
117
+ scoresByCheckpoint.set(checkpoint, score);
118
+ }
119
+ return checkpoints.map((checkpoint) => scoresByCheckpoint.get(checkpoint) ?? null);
120
+ }
121
+
122
+ /**
123
+ * Score all requested checkpoints in one structured stage call for one repeat.
124
+ * A failed or malformed call is an invalid repeat and leaves every checkpoint
125
+ * in that repeat null.
126
+ */
127
+ async function scoreRepeat(
128
+ ctx: ProgressScoringContext,
129
+ input: ProgressScoreInput,
130
+ checkpoints: readonly number[],
131
+ repeat: number,
132
+ ): Promise<(number | null)[]> {
133
+ try {
134
+ const result = await ctx.task(`progress-score-${repeat + 1}`, {
135
+ prompt: build_progress_prompt({ ...input, checkpoints }),
136
+ context: "fresh",
137
+ schema: progressSchema,
138
+ });
139
+ return scoresFromStructured(result.structured, checkpoints);
140
+ } catch {
141
+ return nullScores(checkpoints.length);
142
+ }
143
+ }
144
+
145
+ /** Score every requested checkpoint once per repeat, returning null-safe means. */
146
+ export async function score_progress(
147
+ ctx: ProgressScoringContext,
148
+ input: ProgressScoreInput,
149
+ ): Promise<ProgressCurve> {
150
+ if (input.steps.length === 0) throw new RangeError("Progress scoring requires a non-empty step prefix.");
151
+ const checkpoints = Array.from(input.checkpoints ?? defaultCheckpoints(input.steps.length));
152
+ validateCheckpoints(checkpoints, input.steps.length);
153
+ const repeats = input.repeats ?? DEFAULT_REPEATS;
154
+ const perRepeat: (number | null)[][] = [];
155
+ for (let repeat = 0; repeat < repeats; repeat += 1) {
156
+ perRepeat.push(await scoreRepeat(ctx, input, checkpoints, repeat));
157
+ }
158
+ const scores = checkpoints.map((_, checkpointIndex) => {
159
+ const valid = perRepeat
160
+ .map((repeat) => repeat[checkpointIndex])
161
+ .filter((score): score is number => score !== null);
162
+ if (valid.length === 0) return null;
163
+ return valid.reduce((total, score) => total + score, 0) / valid.length;
164
+ });
165
+ return { checkpoints, scores, perRepeat };
166
+ }
167
+
168
+ /**
169
+ * Build a progress prompt with V3's shared-head/varying-tail layout. Steps are
170
+ * part of the cacheable head; only the requested checkpoint list varies at the
171
+ * tail.
172
+ */
173
+ export function build_progress_prompt(input: ProgressPromptInput): string {
174
+ const numberedSteps = input.steps.map((step, index) => `${index + 1}. ${step}`);
175
+ const sharedHead = [
176
+ "<progress_head>",
177
+ "<problem>",
178
+ input.problem,
179
+ "</problem>",
180
+ "<steps>",
181
+ ...numberedSteps,
182
+ "</steps>",
183
+ "<calibration>",
184
+ ...CALIBRATION_RULES,
185
+ "</calibration>",
186
+ "<scale>",
187
+ `Use VERIFICATION_SCALE ${VERIFICATION_SCALE.min}..${VERIFICATION_SCALE.max} to answer: would the CURRENT state satisfy the acceptance criteria?`,
188
+ PROGRESS_SCALE_ORIENTATION,
189
+ "Score only the supplied current state from observed output.",
190
+ "</scale>",
191
+ "</progress_head>",
192
+ ].join("\n");
193
+ const checkpointTail = [
194
+ "<checkpoints>",
195
+ "Score each listed 1-indexed checkpoint.",
196
+ ...input.checkpoints.map((checkpoint) => `- ${checkpoint}`),
197
+ "</checkpoints>",
198
+ "<output_format>",
199
+ `Return structured_output with scores: [{ checkpoint, score }], using integer scores from ${VERIFICATION_SCALE.min} through ${VERIFICATION_SCALE.max}.`,
200
+ "</output_format>",
201
+ ].join("\n");
202
+ return `${sharedHead}\n\n${checkpointTail}`;
203
+ }
204
+
205
+ function mean(values: readonly number[]): number {
206
+ if (values.length === 0) return 0;
207
+ return values.reduce((total, value) => total + value, 0) / values.length;
208
+ }
209
+
210
+ /**
211
+ * Classify a score series using a deterministic hysteresis delta. The trailing
212
+ * 2*window values are split into equal leading/trailing halves; an odd sample
213
+ * drops its middle value. Thresholds are inclusive at +riseDelta and -fallDelta.
214
+ */
215
+ export function classify_trend(series: readonly number[], config: TrendConfig = {}): TrendResult {
216
+ const window = config.window ?? DEFAULT_TREND_WINDOW;
217
+ const riseDelta = config.riseDelta ?? DEFAULT_RISE_DELTA;
218
+ const fallDelta = config.fallDelta ?? DEFAULT_FALL_DELTA;
219
+ if (series.length < window + 1) {
220
+ return { trend: "flat", evidence: { series, window, delta: 0 } };
221
+ }
222
+ const sample = series.slice(-2 * window);
223
+ const usableLength = sample.length % 2 === 1 ? sample.length - 1 : sample.length;
224
+ const halfLength = usableLength / 2;
225
+ const leading = sample.slice(0, halfLength);
226
+ const trailing = sample.slice(sample.length - halfLength);
227
+ const delta = halfLength === 0 ? 0 : mean(trailing) - mean(leading);
228
+ const trend: Trend = delta >= riseDelta ? "rising" : delta <= fallDelta ? "regressing" : "flat";
229
+ return { trend, evidence: { series, window, delta } };
230
+ }