@bastani/atomic 0.9.14-alpha.4 → 0.9.14-alpha.6

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 (141) hide show
  1. package/CHANGELOG.md +13 -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/package.json +1 -1
  12. package/dist/builtin/workflows/CHANGELOG.md +47 -1
  13. package/dist/builtin/workflows/README.md +6 -5
  14. package/dist/builtin/workflows/builtin/adversarial-verification-prompts.ts +13 -5
  15. package/dist/builtin/workflows/builtin/adversarial-verification-runner.ts +376 -89
  16. package/dist/builtin/workflows/builtin/adversarial-verification.d.ts +30 -6
  17. package/dist/builtin/workflows/builtin/adversarial-verification.ts +14 -9
  18. package/dist/builtin/workflows/builtin/generate-and-filter-prompts.ts +26 -3
  19. package/dist/builtin/workflows/builtin/generate-and-filter-runner.ts +18 -14
  20. package/dist/builtin/workflows/builtin/goal-artifacts.ts +9 -8
  21. package/dist/builtin/workflows/builtin/goal-convergence.ts +87 -0
  22. package/dist/builtin/workflows/builtin/goal-ledger.ts +4 -0
  23. package/dist/builtin/workflows/builtin/goal-prompts.ts +2 -0
  24. package/dist/builtin/workflows/builtin/goal-reducer.ts +6 -1
  25. package/dist/builtin/workflows/builtin/goal-reverify.ts +305 -0
  26. package/dist/builtin/workflows/builtin/goal-runner.ts +75 -10
  27. package/dist/builtin/workflows/builtin/goal-schemas.ts +7 -0
  28. package/dist/builtin/workflows/builtin/goal-types.ts +6 -0
  29. package/dist/builtin/workflows/builtin/loop-until-done-runner.ts +94 -6
  30. package/dist/builtin/workflows/builtin/loop-until-done.d.ts +8 -0
  31. package/dist/builtin/workflows/builtin/loop-until-done.ts +15 -0
  32. package/dist/builtin/workflows/builtin/progress-scoring.ts +230 -0
  33. package/dist/builtin/workflows/builtin/ralph-core.ts +11 -0
  34. package/dist/builtin/workflows/builtin/ralph-review-gate.ts +1 -0
  35. package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +2 -0
  36. package/dist/builtin/workflows/builtin/ralph-runner.ts +60 -10
  37. package/dist/builtin/workflows/builtin/selection-math.ts +156 -0
  38. package/dist/builtin/workflows/builtin/shared-prompts.ts +5 -0
  39. package/dist/builtin/workflows/builtin/tournament-prompts.ts +57 -75
  40. package/dist/builtin/workflows/builtin/tournament-runner.ts +384 -178
  41. package/dist/builtin/workflows/builtin/tournament.d.ts +46 -17
  42. package/dist/builtin/workflows/builtin/tournament.ts +66 -32
  43. package/dist/builtin/workflows/builtin/verification-criteria.ts +330 -0
  44. package/dist/builtin/workflows/builtin/verification-prompts.ts +206 -0
  45. package/dist/builtin/workflows/builtin/verification-usage.ts +44 -0
  46. package/dist/builtin/workflows/package.json +1 -1
  47. package/dist/builtin/workflows/skills/create-spec/SKILL.md +90 -30
  48. package/dist/builtin/workflows/skills/show-me/LICENSE.txt +21 -0
  49. package/dist/builtin/workflows/skills/show-me/SKILL.md +143 -0
  50. package/dist/builtin/workflows/src/authoring/workflow.ts +8 -0
  51. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  52. package/dist/builtin/workflows/src/durable/completed-catalog.ts +5 -2
  53. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +1 -1
  54. package/dist/builtin/workflows/src/durable/resume-eligibility.ts +5 -3
  55. package/dist/builtin/workflows/src/durable/run-timing.ts +41 -10
  56. package/dist/builtin/workflows/src/durable/tool-primitive.ts +24 -2
  57. package/dist/builtin/workflows/src/engine/options.ts +1 -0
  58. package/dist/builtin/workflows/src/engine/primitives/workflow.ts +12 -3
  59. package/dist/builtin/workflows/src/engine/run-budget.ts +308 -0
  60. package/dist/builtin/workflows/src/engine/run-returned-status.ts +8 -0
  61. package/dist/builtin/workflows/src/engine/run-tool-node-lifecycle.ts +6 -0
  62. package/dist/builtin/workflows/src/engine/run.ts +124 -2
  63. package/dist/builtin/workflows/src/engine/runtime.ts +9 -0
  64. package/dist/builtin/workflows/src/extension/config-file-loader.ts +6 -0
  65. package/dist/builtin/workflows/src/extension/config-loader.ts +24 -1
  66. package/dist/builtin/workflows/src/extension/dispatcher.ts +6 -5
  67. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +2 -0
  68. package/dist/builtin/workflows/src/extension/index.bundle.mjs +2975 -843
  69. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +51 -4
  70. package/dist/builtin/workflows/src/extension/public-types.ts +3 -1
  71. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +7 -1
  72. package/dist/builtin/workflows/src/extension/runtime.ts +22 -10
  73. package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +5 -0
  74. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +1 -0
  75. package/dist/builtin/workflows/src/extension/workflow-schema.ts +16 -0
  76. package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +44 -1
  77. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +10 -1
  78. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +21 -9
  79. package/dist/builtin/workflows/src/runs/foreground/executor-continuation.ts +14 -0
  80. package/dist/builtin/workflows/src/runs/foreground/executor-lifecycle.ts +15 -4
  81. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +62 -5
  82. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +4 -0
  83. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +2 -0
  84. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +3 -1
  85. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -1
  86. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +1 -0
  87. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +1 -0
  88. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +7 -0
  89. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +7 -0
  90. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +1 -0
  91. package/dist/builtin/workflows/src/shared/budget-meter.ts +34 -0
  92. package/dist/builtin/workflows/src/shared/budget.d.ts +67 -0
  93. package/dist/builtin/workflows/src/shared/budget.ts +127 -0
  94. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +92 -8
  95. package/dist/builtin/workflows/src/shared/persistence-restore.ts +11 -1
  96. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +15 -3
  97. package/dist/builtin/workflows/src/shared/returned-run-status.ts +35 -2
  98. package/dist/builtin/workflows/src/shared/store-public-types.ts +4 -1
  99. package/dist/builtin/workflows/src/shared/store-run-methods.ts +8 -1
  100. package/dist/builtin/workflows/src/shared/store-stage-methods.ts +1 -0
  101. package/dist/builtin/workflows/src/shared/store-types.ts +24 -0
  102. package/dist/builtin/workflows/src/shared/types.ts +3 -0
  103. package/dist/builtin/workflows/src/shared/workflow-artifacts.ts +1 -0
  104. package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +3 -0
  105. package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +3 -0
  106. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  107. package/dist/core/atomic-guide-command.js +1 -0
  108. package/dist/core/atomic-guide-command.js.map +1 -1
  109. package/dist/core/extensions/ui-types.d.ts +13 -3
  110. package/dist/core/extensions/ui-types.d.ts.map +1 -1
  111. package/dist/core/extensions/ui-types.js +15 -3
  112. package/dist/core/extensions/ui-types.js.map +1 -1
  113. package/dist/core/slash-commands.d.ts.map +1 -1
  114. package/dist/core/slash-commands.js +33 -3
  115. package/dist/core/slash-commands.js.map +1 -1
  116. package/dist/main-deferred-startup.d.ts.map +1 -1
  117. package/dist/main-deferred-startup.js +6 -2
  118. package/dist/main-deferred-startup.js.map +1 -1
  119. package/dist/modes/interactive/interactive-startup.js +4 -0
  120. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  121. package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
  122. package/dist/modes/interactive/interactive-tui.js +19 -1
  123. package/dist/modes/interactive/interactive-tui.js.map +1 -1
  124. package/dist/modes/interactive-engine/isolated-runtime.d.ts +7 -0
  125. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  126. package/dist/modes/interactive-engine/isolated-runtime.js +94 -37
  127. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  128. package/dist/modes/rpc/rpc-client.d.ts +1 -0
  129. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  130. package/dist/modes/rpc/rpc-client.js +15 -2
  131. package/dist/modes/rpc/rpc-client.js.map +1 -1
  132. package/dist/modes/rpc/rpc-input-scheduler.d.ts +3 -2
  133. package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
  134. package/dist/modes/rpc/rpc-input-scheduler.js +5 -2
  135. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  136. package/docs/extensions.md +1 -1
  137. package/docs/quickstart.md +1 -0
  138. package/docs/skills.md +4 -0
  139. package/docs/workflows.md +74 -10
  140. package/npm-shrinkwrap.json +29 -29
  141. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { writeFile } from "node:fs/promises";
1
+ import { readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { Type, type Static } from "typebox";
4
4
  import type { WorkflowRunContext, WorkflowSerializableValue } from "../src/shared/types.js";
@@ -61,19 +61,23 @@ export async function runGenerateAndFilter(ctx: WorkflowRunContext<Inputs>): Pro
61
61
  let shortlist = filteredShortlist.length > 0 ? filteredShortlist : fallbackShortlist;
62
62
  let judgePath: string | null = null;
63
63
  let decisionPath = filterPath;
64
- if (ctx.inputs.use_judge) {
65
- judgePath = join(root, "judge.json");
66
- const judged = await ctx.task("judge", {
67
- prompt: renderJudgePrompt(ctx.inputs.prompt, filterPath, shortlistLimit), context: "fresh",
68
- reads: [filterPath, ...shortlist], schema: judgeSchema,
69
- });
70
- const judgedDecision = judgeDecision(judged.structured);
71
- // Same inter-stage contract as the filter report above.
72
- await writeFile(judgePath, `${JSON.stringify(judgedDecision ?? { shortlist: [], rationale: "Judge stage produced no valid structured decision." }, null, 2)}\n`);
73
- const judgedShortlist = selectCandidates(judgedDecision?.shortlist ?? []);
74
- shortlist = judgedShortlist.length > 0 ? judgedShortlist : shortlist;
75
- decisionPath = judgePath;
76
- }
64
+ if (ctx.inputs.use_judge) {
65
+ judgePath = join(root, "judge.json");
66
+ const judgeCandidates = await Promise.all(
67
+ shortlist.map(async (path) => ({ path, body: await readFile(path, "utf8") })),
68
+ );
69
+ const judgePrompt = renderJudgePrompt(ctx.inputs.prompt, filterPath, shortlistLimit, judgeCandidates);
70
+ const judged = await ctx.task("judge", {
71
+ prompt: judgePrompt.prompt, context: "fresh",
72
+ reads: judgePrompt.reads, schema: judgeSchema,
73
+ });
74
+ const judgedDecision = judgeDecision(judged.structured);
75
+ // Same inter-stage contract as the filter report above.
76
+ await writeFile(judgePath, `${JSON.stringify(judgedDecision ?? { shortlist: [], rationale: "Judge stage produced no valid structured decision." }, null, 2)}\n`);
77
+ const judgedShortlist = selectCandidates(judgedDecision?.shortlist ?? []);
78
+ shortlist = judgedShortlist.length > 0 ? judgedShortlist : shortlist;
79
+ decisionPath = judgePath;
80
+ }
77
81
  const finalPath = join(root, "shortlist.md");
78
82
  const finalShortlist = await ctx.task("final-shortlist", {
79
83
  prompt: renderFinalShortlistPrompt(ctx.inputs.prompt, decisionPath), context: "fresh",
@@ -3,8 +3,10 @@ import { join } from "node:path";
3
3
  import type { ReviewDecision, ReviewRecord } from "./goal-types.js";
4
4
  import {
5
5
  consolidateFindingsBatch,
6
+ type ConsolidatedFinding,
6
7
  type ReviewConvergenceSummary,
7
8
  } from "./review-convergence.js";
9
+ import type { ReverifyAuditEntry, ReverifiableFinding } from "./goal-reverify.js";
8
10
 
9
11
  export function artifactSafeName(value: string): string {
10
12
  const safe = value
@@ -42,20 +44,19 @@ export async function writeReviewArtifact(
42
44
  export async function writeReviewRoundArtifact(
43
45
  artifactDir: string,
44
46
  reviews: readonly ReviewRecord[],
45
- ): Promise<string> {
46
- const artifactPath = join(artifactDir, "review-round-latest.json");
47
- const visibleReviews = reviews.map(withoutTurn);
48
- // Deduplicated cross-reviewer findings batch so the next orchestrator turn can
49
- // plan and repair the round's findings together instead of one at a time.
50
- const consolidatedFindings = consolidateFindingsBatch(
47
+ consolidatedFindings: readonly ConsolidatedFinding<ReverifiableFinding>[] = consolidateFindingsBatch(
51
48
  reviews.map((review) => ({
52
49
  reviewer: review.reviewer,
53
50
  findings: review.findings,
54
51
  })),
55
- );
52
+ ),
53
+ reverification: readonly ReverifyAuditEntry<ReverifiableFinding>[] = [],
54
+ ): Promise<string> {
55
+ const artifactPath = join(artifactDir, "review-round-latest.json");
56
+ const visibleReviews = reviews.map(withoutTurn);
56
57
  await writeFile(
57
58
  artifactPath,
58
- `${JSON.stringify({ reviews: visibleReviews, consolidated_findings: consolidatedFindings }, null, 2)}\n`,
59
+ `${JSON.stringify({ reviews: visibleReviews, consolidated_findings: consolidatedFindings, reverification }, null, 2)}\n`,
59
60
  { encoding: "utf8" },
60
61
  );
61
62
  return artifactPath;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Convergence evidence is observational only: it never approves, never
3
+ * terminates, never touches `stop_review_loop`, and feeds only
4
+ * `needs_human`-class escalation text.
5
+ */
6
+ import type { UsageTotals } from "./verification-usage.js";
7
+ import {
8
+ classify_trend,
9
+ DEFAULT_FALL_DELTA,
10
+ DEFAULT_RISE_DELTA,
11
+ type TrendResult,
12
+ } from "./progress-scoring.js";
13
+ import { VERIFICATION_SCALE } from "./verification-criteria.js";
14
+
15
+ /**
16
+ * `fractionProven` remains in its natural [0, 1] units. V7's hysteresis
17
+ * thresholds span a proportional part of `VERIFICATION_SCALE`, so applying
18
+ * that same proportion to the unit interval keeps the two series comparable
19
+ * without hard-coding a fraction-specific magic number.
20
+ */
21
+ export const FRACTION_TREND_RISE_DELTA =
22
+ DEFAULT_RISE_DELTA / (VERIFICATION_SCALE.max - VERIFICATION_SCALE.min);
23
+ export const FRACTION_TREND_FALL_DELTA =
24
+ DEFAULT_FALL_DELTA / (VERIFICATION_SCALE.max - VERIFICATION_SCALE.min);
25
+
26
+ export type ConvergenceEntry = {
27
+ readonly unresolvedBlockingCount: number;
28
+ readonly meanFindingConfidence: number | null;
29
+ readonly fractionProven: number;
30
+ readonly demotions: number;
31
+ readonly usage: UsageTotals;
32
+ };
33
+
34
+ /** Purely shape one already-folded review round into a ledger entry. */
35
+ export function record_convergence(round: {
36
+ readonly unresolvedBlockingCount: number;
37
+ readonly meanFindingConfidence: number | null;
38
+ readonly fractionProven: number;
39
+ readonly demotions: number;
40
+ readonly usage: UsageTotals;
41
+ }): ConvergenceEntry {
42
+ return {
43
+ unresolvedBlockingCount: round.unresolvedBlockingCount,
44
+ meanFindingConfidence: round.meanFindingConfidence,
45
+ fractionProven: round.fractionProven,
46
+ demotions: round.demotions,
47
+ usage: round.usage,
48
+ };
49
+ }
50
+
51
+ export function classify_convergence(entries: readonly ConvergenceEntry[]): {
52
+ blocking: TrendResult;
53
+ proven: TrendResult;
54
+ } {
55
+ return {
56
+ blocking: classify_trend(entries.map((entry) => entry.unresolvedBlockingCount)),
57
+ proven: classify_trend(entries.map((entry) => entry.fractionProven), {
58
+ riseDelta: FRACTION_TREND_RISE_DELTA,
59
+ fallDelta: FRACTION_TREND_FALL_DELTA,
60
+ }),
61
+ };
62
+ }
63
+
64
+ /**
65
+ * A FALLING `unresolvedBlockingCount` (classified "regressing" on the raw
66
+ * series) and a RISING `fractionProven` are the converging directions. A
67
+ * rising blocking count is never suppressed because it is the worsening
68
+ * direction on the axis the objective names first.
69
+ */
70
+ export function convergence_escalation_evidence(
71
+ entries: readonly ConvergenceEntry[],
72
+ ): readonly string[] {
73
+ if (entries.length === 0) return [];
74
+ const { blocking, proven } = classify_convergence(entries);
75
+ if (blocking.trend === "regressing" || (proven.trend === "rising" && blocking.trend !== "rising")) return [];
76
+
77
+ const latest = entries[entries.length - 1]?.meanFindingConfidence;
78
+ return [
79
+ `${entries.length} round${entries.length === 1 ? "" : "s"} recorded; no observed convergence: the blocking-count trend is ${blocking.trend}.`,
80
+ `Blocking-count trend: ${blocking.trend}; raw series: ${JSON.stringify(blocking.evidence.series)}.`,
81
+ `Fraction-proven trend: ${proven.trend}; raw series: ${JSON.stringify(proven.evidence.series)}.`,
82
+ latest === null
83
+ ? "Latest mean finding confidence: no findings were filed."
84
+ : `Latest mean finding confidence: ${latest}.`,
85
+ "This is escalation EVIDENCE only; it never approves or terminates anything.",
86
+ ];
87
+ }
@@ -34,6 +34,8 @@ function modelVisibleLedger(ledger: GoalLedger): ModelVisibleGoalLedger {
34
34
  blockers: ledger.blockers.map(withoutTurn),
35
35
  decisions: ledger.decisions.map(withoutTurn),
36
36
  lifecycle: ledger.lifecycle.map(withoutTurn),
37
+ reverification: ledger.reverification ?? [],
38
+ convergence: ledger.convergence ?? [],
37
39
  };
38
40
  }
39
41
 
@@ -73,6 +75,8 @@ export async function createGoalLedger(
73
75
  blockers: [],
74
76
  decisions: [],
75
77
  lifecycle: [],
78
+ reverification: [],
79
+ convergence: [],
76
80
  };
77
81
  appendLifecycleEvent(ledger, "created", "Goal created.", 0);
78
82
  const ledgerPath = join(artifactDir, LEDGER_FILENAME);
@@ -9,6 +9,7 @@ import {
9
9
  REVIEW_CODE_DELTA_CONTRACT,
10
10
  REVIEWER_INDEPENDENT_VERIFICATION_CONTRACT,
11
11
  REVIEWER_INTERCOM_COORDINATION_PROTOCOL,
12
+ REVIEWER_CALIBRATION_RULES,
12
13
  REVIEWER_OVERIMPLEMENTATION_GUARD,
13
14
  REVIEWER_SPEC_VS_OBJECTIVE_GUARD,
14
15
  SCOPE_DISCIPLINE_CONTRACT,
@@ -159,6 +160,7 @@ export function renderReviewerPrompt(args: {
159
160
  ["literal_contract", LITERAL_OBJECTIVE_CONTRACT],
160
161
  ["acceptance_criteria", ACCEPTANCE_MATRIX_CONTRACT],
161
162
  ["independent_verification", REVIEWER_INDEPENDENT_VERIFICATION_CONTRACT],
163
+ ["calibration", REVIEWER_CALIBRATION_RULES],
162
164
  ["code_delta_review", REVIEW_CODE_DELTA_CONTRACT],
163
165
  ["reviewer_coordination", REVIEWER_INTERCOM_COORDINATION_PROTOCOL],
164
166
  ["regression_evidence", REGRESSION_EVIDENCE_CONTRACT],
@@ -1,3 +1,5 @@
1
+ import type { ConvergenceEntry } from "./goal-convergence.js";
2
+ import { convergence_escalation_evidence } from "./goal-convergence.js";
1
3
  import type { BlockerObservation, GoalLedger, ReducerOutcome, ReviewRecord } from "./goal-types.js";
2
4
  import {
3
5
  summarizeReviewConvergence,
@@ -87,6 +89,7 @@ export function reduceGoalDecision(
87
89
  readonly reviewQuorum: number;
88
90
  readonly blockerThreshold: number;
89
91
  readonly nextActionOnComplete: ReviewNextAction;
92
+ readonly convergence?: readonly ConvergenceEntry[];
90
93
  },
91
94
  ): ReducerOutcome {
92
95
  const completeVotes = turnReviews.filter(
@@ -140,6 +143,8 @@ export function reduceGoalDecision(
140
143
  }
141
144
 
142
145
  if (options.turn >= options.maxTurns) {
146
+ const baseReason = `Orchestrator attempt budget reached without reviewer quorum. Remaining work: ${collectRemainingWork(turnReviews)}`;
147
+ const evidence = convergence_escalation_evidence(options.convergence ?? []);
143
148
  return {
144
149
  status: "needs_human",
145
150
  blockerObservation: observation,
@@ -147,7 +152,7 @@ export function reduceGoalDecision(
147
152
  ...reducerSummary(turnReviews, false, "needs_human"),
148
153
  turn: options.turn,
149
154
  decision: "needs_human",
150
- reason: `Orchestrator attempt budget reached without reviewer quorum. Remaining work: ${collectRemainingWork(turnReviews)}`,
155
+ reason: [baseReason, ...evidence].join("\n"),
151
156
  complete_votes: completeVotes,
152
157
  review_quorum: options.reviewQuorum,
153
158
  ...(observation ? { blocker: observation.blocker } : {}),
@@ -0,0 +1,305 @@
1
+ import { Type } from "typebox";
2
+ import type {
3
+ WorkflowRunContext,
4
+ WorkflowSerializableValue,
5
+ WorkflowTaskResult,
6
+ } from "../src/shared/types.js";
7
+ import {
8
+ build_scoring_prompt,
9
+ scoring_prompt_reads,
10
+ type ScoringCandidate,
11
+ type SharedHead,
12
+ } from "./verification-prompts.js";
13
+ import { VERIFICATION_SCALE } from "./verification-criteria.js";
14
+ import type {
15
+ ConsolidatableFinding,
16
+ ConsolidatedFinding,
17
+ } from "./review-convergence.js";
18
+
19
+ /** Confidence values strictly below this threshold can enter re-verification. */
20
+ export const DEFAULT_REVERIFY_THRESHOLD = 0.7;
21
+
22
+ const STANDARD_CONFIRM_THRESHOLD = 10;
23
+ const REQUIRED_CONFIRM_THRESHOLD = 6;
24
+ const DEFAULT_REPEATS = 3;
25
+ const REVERIFY_QUESTION =
26
+ "Assess this specific finding against the code it cites: is it a real, objective-relevant, currently-unresolved blocker?";
27
+
28
+ /** The finding fields needed by the re-verification door. */
29
+ export type ReverifiableFinding = ConsolidatableFinding & {
30
+ readonly body?: string;
31
+ readonly confidence_score?: number;
32
+ };
33
+
34
+ export type ReverifiableConsolidatedFinding = ConsolidatedFinding<ReverifiableFinding>;
35
+
36
+ export type ReverifyResult = {
37
+ readonly verdict: "confirmed" | "demoted";
38
+ readonly meanScore: number;
39
+ readonly perRepeat: readonly (number | null)[];
40
+ readonly evidence: readonly string[];
41
+ };
42
+
43
+ /** Audit evidence retained alongside the original consolidated finding. */
44
+ export type ReverifyAuditEntry<F extends ReverifiableFinding = ReverifiableFinding> = {
45
+ readonly finding: ConsolidatedFinding<F>;
46
+ readonly verdict: ReverifyResult["verdict"];
47
+ readonly meanScore: number;
48
+ readonly perRepeat: readonly (number | null)[];
49
+ readonly evidence: readonly string[];
50
+ };
51
+
52
+ export type ReverifyContext = Pick<WorkflowRunContext, "task">;
53
+
54
+ export type ReverifyBatchResult<F extends ReverifiableFinding = ReverifiableFinding> = {
55
+ readonly batch: readonly ConsolidatedFinding<F>[];
56
+ readonly audits: readonly ReverifyAuditEntry<F>[];
57
+ };
58
+
59
+ export type ReverifyBatchInput<F extends ReverifiableFinding = ReverifiableFinding> = {
60
+ readonly batch: readonly ConsolidatedFinding<F>[];
61
+ readonly context: {
62
+ readonly objective: string;
63
+ readonly candidateRefs: readonly string[];
64
+ };
65
+ readonly repeats?: number;
66
+ readonly threshold?: number;
67
+ };
68
+
69
+ const reverifySchema = Type.Object(
70
+ {
71
+ score: VERIFICATION_SCALE.schema,
72
+ evidence: Type.Array(Type.String()),
73
+ },
74
+ { additionalProperties: false },
75
+ );
76
+
77
+ type ReverifyReport = {
78
+ readonly score: number;
79
+ readonly evidence: readonly string[];
80
+ };
81
+
82
+ function isRecord(value: WorkflowSerializableValue | undefined): value is Record<string, WorkflowSerializableValue> {
83
+ return typeof value === "object" && value !== null && !Array.isArray(value);
84
+ }
85
+
86
+ function isStringArray(value: WorkflowSerializableValue | undefined): value is readonly string[] {
87
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
88
+ }
89
+
90
+ function parseReport(result: WorkflowTaskResult): ReverifyReport | undefined {
91
+ if (!isRecord(result.structured)) return undefined;
92
+ const score = result.structured.score;
93
+ if (
94
+ typeof score !== "number" ||
95
+ !Number.isInteger(score) ||
96
+ score < VERIFICATION_SCALE.min ||
97
+ score > VERIFICATION_SCALE.max
98
+ ) {
99
+ return undefined;
100
+ }
101
+ const evidence = result.structured.evidence;
102
+ return {
103
+ score,
104
+ evidence: isStringArray(evidence) ? evidence : [],
105
+ };
106
+ }
107
+
108
+ function requiredByObjective(finding: ReverifiableConsolidatedFinding): boolean {
109
+ return finding.finding.objective_alignment === "required_by_objective";
110
+ }
111
+
112
+ /**
113
+ * Pure eligibility predicate. Missing confidence is deliberately ineligible:
114
+ * an absent graded signal is doubt, and doubt remains blocking.
115
+ */
116
+ export function is_reverifiable(
117
+ finding: ReverifiableConsolidatedFinding,
118
+ threshold = DEFAULT_REVERIFY_THRESHOLD,
119
+ ): boolean {
120
+ const confidence = finding.finding.confidence_score;
121
+ return (
122
+ finding.blocking &&
123
+ finding.reviewers.length === 1 &&
124
+ typeof confidence === "number" &&
125
+ Number.isFinite(confidence) &&
126
+ confidence < threshold &&
127
+ finding.finding.objective_alignment !== "beyond_objective" &&
128
+ finding.finding.objective_alignment !== "contradicts_objective"
129
+ );
130
+ }
131
+
132
+ function readPaths(
133
+ finding: ReverifiableConsolidatedFinding,
134
+ candidateRefs: readonly string[],
135
+ ): readonly string[] {
136
+ const codePath = finding.finding.code_location?.absolute_file_path;
137
+ return [
138
+ ...(codePath === undefined ? [] : [codePath]),
139
+ ...candidateRefs,
140
+ ];
141
+ }
142
+
143
+ function promptFor(
144
+ finding: ReverifiableConsolidatedFinding,
145
+ context: ReverifyBatchInput["context"],
146
+ ): { readonly prompt: string; readonly reads: readonly string[] } {
147
+ const codePath = finding.finding.code_location?.absolute_file_path;
148
+ const candidate: ScoringCandidate = {
149
+ ...(codePath === undefined ? {} : { path: codePath }),
150
+ body: [
151
+ `Title: ${finding.finding.title}`,
152
+ `Body: ${finding.finding.body ?? ""}`,
153
+ `Code location: ${codePath ?? "not supplied"}`,
154
+ ].join("\n"),
155
+ };
156
+ const head: SharedHead = {
157
+ task: `${REVERIFY_QUESTION}\n\nObjective:\n${context.objective}`,
158
+ groundTruthNote: `The objective is the contract. Verify the finding against observed code, not reviewer narration.`,
159
+ candidates: [candidate],
160
+ outputFormat: "Call structured_output with score (an integer from 1 to 20) and evidence (an array of observed, concise reasons).",
161
+ };
162
+ const prompt = build_scoring_prompt(head, {
163
+ id: "finding_reverification",
164
+ name: "Finding re-verification",
165
+ description: REVERIFY_QUESTION,
166
+ });
167
+ return {
168
+ prompt,
169
+ reads: [
170
+ ...readPaths(finding, context.candidateRefs),
171
+ ...scoring_prompt_reads(head),
172
+ ],
173
+ };
174
+ }
175
+
176
+ async function runRepeat(
177
+ ctx: ReverifyContext,
178
+ finding: ReverifiableConsolidatedFinding,
179
+ context: ReverifyBatchInput["context"],
180
+ stageName: string,
181
+ ): Promise<ReverifyReport | undefined> {
182
+ try {
183
+ const rendered = promptFor(finding, context);
184
+ const options = {
185
+ prompt: rendered.prompt,
186
+ context: "fresh" as const,
187
+ reads: rendered.reads,
188
+ schema: reverifySchema,
189
+ };
190
+ return parseReport(await ctx.task(stageName, options));
191
+ } catch {
192
+ return undefined;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Re-score one eligible finding in K independent fresh contexts.
198
+ *
199
+ * A zero-valid-repeat mean is the deterministic sentinel `0`, matching the
200
+ * verification criteria module's empty-mean convention. It never authorizes a
201
+ * demotion because the quorum checks below require at least one valid score.
202
+ */
203
+ export async function reverify_finding(
204
+ ctx: ReverifyContext,
205
+ input: {
206
+ readonly finding: ReverifiableConsolidatedFinding;
207
+ readonly context: {
208
+ readonly objective: string;
209
+ readonly candidateRefs: readonly string[];
210
+ };
211
+ repeats?: number;
212
+ threshold?: number;
213
+ },
214
+ ): Promise<ReverifyResult> {
215
+ if (!is_reverifiable(input.finding, input.threshold)) {
216
+ throw new Error("Cannot re-verify an ineligible finding.");
217
+ }
218
+
219
+ const repeatCount = Number.isFinite(input.repeats) && input.repeats !== undefined
220
+ ? Math.max(0, Math.floor(input.repeats))
221
+ : DEFAULT_REPEATS;
222
+ const perRepeat: Array<number | null> = [];
223
+ const evidence: string[] = [];
224
+ const validScores: number[] = [];
225
+ for (let repeat = 1; repeat <= repeatCount; repeat += 1) {
226
+ let report = await runRepeat(ctx, input.finding, input.context, `reverify-${repeat}`);
227
+ if (report === undefined) {
228
+ report = await runRepeat(ctx, input.finding, input.context, `reverify-${repeat}-reask`);
229
+ }
230
+ if (report === undefined) {
231
+ perRepeat.push(null);
232
+ evidence.push(
233
+ `Re-verification repeat ${repeat} was invalid after one re-ask; doubt defaults to confirmed.`,
234
+ );
235
+ continue;
236
+ }
237
+ perRepeat.push(report.score);
238
+ validScores.push(report.score);
239
+ if (report.evidence.length === 0) {
240
+ evidence.push(`Re-verification repeat ${repeat} scored ${report.score}/20.`);
241
+ } else {
242
+ evidence.push(...report.evidence);
243
+ }
244
+ }
245
+
246
+ const meanScore = validScores.length === 0
247
+ ? 0
248
+ : validScores.reduce((total, score) => total + score, 0) / validScores.length;
249
+ const validCount = validScores.length;
250
+ const requiredQuorum = Math.ceil(repeatCount / 2);
251
+ const demoted = requiredByObjective(input.finding)
252
+ ? repeatCount > 0 && validCount === repeatCount && meanScore < REQUIRED_CONFIRM_THRESHOLD
253
+ : validCount > 0 && validCount >= requiredQuorum && meanScore < STANDARD_CONFIRM_THRESHOLD;
254
+ return {
255
+ verdict: demoted ? "demoted" : "confirmed",
256
+ meanScore,
257
+ perRepeat,
258
+ evidence,
259
+ };
260
+ }
261
+
262
+ function auditKey<F extends ReverifiableFinding>(finding: ConsolidatedFinding<F>): string {
263
+ return JSON.stringify([
264
+ finding.finding.code_location?.absolute_file_path ?? "",
265
+ finding.finding.title,
266
+ finding.reviewers,
267
+ ]);
268
+ }
269
+
270
+ /** Apply only evidence-backed demotions; the original finding remains present. */
271
+ export function apply_reverify_results<F extends ReverifiableFinding>(
272
+ batch: readonly ConsolidatedFinding<F>[],
273
+ audits: readonly ReverifyAuditEntry<F>[],
274
+ ): ReverifyBatchResult<F> {
275
+ const demoted = new Set(
276
+ audits
277
+ .filter((audit) => audit.verdict === "demoted")
278
+ .map((audit) => auditKey(audit.finding)),
279
+ );
280
+ return {
281
+ batch: batch.map((entry) =>
282
+ demoted.has(auditKey(entry)) ? { ...entry, blocking: false } : entry,
283
+ ),
284
+ audits,
285
+ };
286
+ }
287
+
288
+ /** Re-verify only eligible entries and return the batch plus durable audit data. */
289
+ export async function reverify_consolidated_batch<F extends ReverifiableFinding>(
290
+ ctx: ReverifyContext,
291
+ input: ReverifyBatchInput<F>,
292
+ ): Promise<ReverifyBatchResult<F>> {
293
+ const audits: ReverifyAuditEntry<F>[] = [];
294
+ for (const entry of input.batch) {
295
+ if (!is_reverifiable(entry, input.threshold)) continue;
296
+ const result = await reverify_finding(ctx, {
297
+ finding: entry,
298
+ context: input.context,
299
+ repeats: input.repeats,
300
+ threshold: input.threshold,
301
+ });
302
+ audits.push({ finding: entry, ...result });
303
+ }
304
+ return apply_reverify_results(input.batch, audits);
305
+ }