@obversa/runtime 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -1,25 +1,27 @@
1
1
  import { compileGraph } from './chunk-DEW5R23M.js';
2
2
  export { compileGraph } from './chunk-DEW5R23M.js';
3
- import { LoopError, needDecisionsOf, DEFAULT_FANOUT_CONCURRENCY, setMeta, jobMeta, describeConditions, Budget, currentBranch, assertDistinctSeats, seatIdentity, copyJobMeta, predicate, setLabel, resolveEnv, scrubCapture, withConditionPreparation, toCondition, prepareCondition, validateCallbackRequest, reportedUsage, validateIncompleteResultEvidence, engineSelection, validateAgentResult, requireFinalResultText, isRepo, addWorktree, childContext, stageAll, commit, mergeBranch, removeWorktree, deleteBranch, headSha, hasStagedChanges, reviewPanel, createCallbackGate, defineAgent, isInfrastructureError, validateCallbackResponse, gitRoot, captureGitWorkspaceSnapshot, workspaceFingerprint, revisionFromOutcome, agentJob, commandJob, approval, mergeNoCommit, mergeAbort, nodeJobContext, outcomeFromAgentText, requireNonEmptyFiles, requireNoFiles, panelReviewers, delegateNodeJob, branchCommits, RECORDED_ENGINE_USAGE, compareGitWorkspaceSnapshots } from './chunk-DV5P4QLI.js';
4
- export { LoopError, RECORDED_ENGINE_USAGE, agentCheck, agentJob, all, always, any, approval, bodyPassed, callbackRequestDigest, commandJob, commandSucceeds, copyJobMeta, createCallbackGate, defineAgent, defineSkill, describeConditions, failed, finalResultPart, finalResultText, fnJob, fromFile, gateJob, jobMeta, kickback, minConfidence, never, not, passed, predicate, prove, quorum, renderPlan, reviewContext, reviewPanel, revisionRequest, toCondition, validateAgentResult, validateCallbackRequest, validateCallbackResponse, withEnv } from './chunk-DV5P4QLI.js';
3
+ import { LoopError, needDecisionsOf, interactionIdentity, DEFAULT_FANOUT_CONCURRENCY, interactionDeclaration, setMeta, jobMeta, describeConditions, Budget, currentBranch, assertDistinctSeats, seatIdentity, copyJobMeta, predicate, setLabel, resolveEnv, scrubCapture, withConditionPreparation, toCondition, prepareCondition, validateCallbackRequest, reportedUsage, validateIncompleteResultEvidence, engineSelection, validateAgentResult, requireFinalResultText, isRepo, addWorktree, childContext, stageAll, commit, mergeBranch, removeWorktree, deleteBranch, headSha, hasStagedChanges, reviewPanel, createCallbackGate, defineAgent, isInfrastructureError, validateCallbackResponse, gitRoot, captureGitWorkspaceSnapshot, workspaceFingerprint, savedInteraction, revisionFromOutcome, checkpointInteraction, jsonSnapshot, outcomeSnapshot, agentJob, humanReview, commandJob, approval, requestInteraction, mergeNoCommit, mergeAbort, normalizeFeedbackSeverity, DEFAULT_INTERACTION, nodeJobContext, outcomeFromAgentText, requireNonEmptyFiles, requireNoFiles, panelReviewers, hasSavedInteraction, fnJob, kickback, delegateNodeJob, branchCommits, RECORDED_ENGINE_USAGE, compareGitWorkspaceSnapshots } from './chunk-G2OIKOUI.js';
4
+ export { LoopError, RECORDED_ENGINE_USAGE, agentCheck, agentJob, all, always, any, approval, bodyPassed, callbackRequestDigest, commandJob, commandSucceeds, copyJobMeta, createCallbackGate, defineAgent, defineSkill, describeConditions, failed, finalResultPart, finalResultText, fnJob, fromFile, gateJob, humanReview, jobMeta, kickback, minConfidence, never, not, passed, predicate, prove, quorum, renderPlan, reviewContext, reviewPanel, revisionRequest, toCondition, validateAgentResult, validateCallbackRequest, validateCallbackResponse, withEnv } from './chunk-G2OIKOUI.js';
5
5
  import { runRuntimeProcess, processText } from './chunk-NIBHM5I5.js';
6
6
  import { loadRunDefinition, StorageError, validateDomainEventBatch, validateNewDomainEvent, validateDomainEventEnvelope, validateDomainEventId, validateArtifactReference, validateStorageId } from './chunk-RZLMX3IA.js';
7
7
  export { StorageError, loadRunDefinition, persistRunDefinition, validateArtifactReference, validateArtifactScope, validateDomainEventBatch, validateDomainEventEnvelope, validateDomainEventId, validateEventStreamRef, validateNewArtifact, validateNewDomainEvent, validateRunDefinition, validateRunStartRecord, validateRunStoragePolicy, validateRunStorageRecord } from './chunk-RZLMX3IA.js';
8
8
  export { resolveGraphPlan, validateGraphDescription } from './chunk-3L6YNPN6.js';
9
9
  import { cloneFrozenJson, JsonValueError, digestJson, canonicalJson, GraphValidationError } from './chunk-5GLEABOU.js';
10
10
  export { GraphValidationError, JsonValueError } from './chunk-5GLEABOU.js';
11
- import { randomUUID, createHash, randomBytes } from 'crypto';
11
+ import { newRunId, startSupervisor, formatEvent, runningTotal } from './chunk-XDMDT67Q.js';
12
+ export { formatEvent, renderRecord, summarizeRecord } from './chunk-XDMDT67Q.js';
13
+ import { randomUUID, createHash } from 'crypto';
12
14
  import { SUBAGENT_TOOLS, snapshotApprovalSubject, assertApprovalPermissionsAdmitted, validateAcceptedResultRecord, EngineIncompleteResultError, EngineError, classifyEngineFailure, LANE_DEAD_FAILURES, validateCallbackEvent, ApprovalSubjectError, isEngine, cloneFrozenJson as cloneFrozenJson$1, validateApprovalRecord, digestJson as digestJson$1, GraphExecutionError, validateApprovalSubject, modelIdentity, validateActionDecision } from '@obversa/api';
13
15
  export { ApprovalSubjectError, EngineError, EngineIncompleteResultError, GraphExecutionError, LANE_DEAD_FAILURES, acceptedResultMatches, approvalSubjectDigest, classifyEngineFailure, createApprovalCallbackGate, validateAcceptedResultRecord, validateApprovalRecord, validateCallbackEvent } from '@obversa/api';
14
16
  import { readFileSync, mkdirSync, existsSync, writeFileSync, appendFileSync, lstatSync, realpathSync } from 'fs';
15
- import { mkdir, stat, open, rename, rm, realpath, readFile, lstat } from 'fs/promises';
17
+ import { mkdir, readFile, stat, open, rename, rm, realpath, lstat } from 'fs/promises';
16
18
  import { join, resolve, basename, extname, dirname, isAbsolute, relative, sep } from 'path';
17
19
  import pLimit from 'p-limit';
18
20
  import toposort from 'toposort';
19
21
  import '@obversa/core/command';
20
- import { homedir } from 'os';
21
22
  import { createServer } from 'http';
22
23
  import { isDeepStrictEqual } from 'util';
24
+ import { homedir } from 'os';
23
25
  import { hasExactFields, isObject, bindingFromSubject } from '@obversa/api/approval-support';
24
26
  import { acceptedResultSupport } from '@obversa/api/accepted-result-support';
25
27
  import { fileURLToPath } from 'url';
@@ -119,6 +121,139 @@ Write a concise MERGE SYNTHESIS for the commit body: what this line of work acco
119
121
  return requireFinalResultText(out).trim();
120
122
  }
121
123
 
124
+ // src/core/judge.ts
125
+ function stopQuestions(what = "the draft") {
126
+ return {
127
+ holds: {
128
+ type: "noul",
129
+ instructions: `For this use case, does ${what} hold as it stands?`,
130
+ criteria: {
131
+ true: "The latest findings are nits, taste or nothing, and a reader of this kind of work would not notice what remains.",
132
+ false: "The latest findings include a block, or shoulds a reader of this kind of work would notice."
133
+ }
134
+ },
135
+ worth_doing: {
136
+ type: "noul",
137
+ instructions: "Read the latest findings themselves. For this use case, are they worth acting on?",
138
+ criteria: {
139
+ true: "They name things a reader of this kind of work would stumble on, misread or distrust.",
140
+ false: "They are taste, edge cases, or polish past the bar the use case sets; acting on them would not change what a reader gets."
141
+ }
142
+ },
143
+ worth_another_round: {
144
+ type: "noul",
145
+ instructions: "Given the whole history and whether the latest findings are worth doing, is another round worth it?",
146
+ criteria: {
147
+ true: "The findings are worth doing, and the rounds so far have fixed what was found, so one more pass would land them.",
148
+ false: "The findings are not worth doing for this use case; or the same class of finding keeps returning; or the change between rounds is small; or the work is being polished past the bar."
149
+ }
150
+ },
151
+ stop_reason: {
152
+ type: "choice",
153
+ instructions: "If a person must settle a product decision, choose product_decision. Otherwise, are we at the point of diminishing returns? If so, which kind; if not, continue.",
154
+ criteria: {
155
+ holds: `${what} holds for this use case.`,
156
+ over_polishing: "The remaining findings are taste, nits or edge cases past the bar.",
157
+ not_converging: "The same class of finding keeps returning, so another round will not fix it.",
158
+ continue: "Another round is worth it.",
159
+ product_decision: "A person must settle a product decision before this review can continue."
160
+ }
161
+ }
162
+ };
163
+ }
164
+ function judge(seat, opts) {
165
+ if (!Number.isSafeInteger(opts.cap) || opts.cap < 1) {
166
+ throw new TypeError("judge cap must be a positive integer");
167
+ }
168
+ return { kind: "judge", seat, cap: opts.cap, questions: opts.questions ?? stopQuestions(), ...opts.interaction ? { interaction: opts.interaction } : {} };
169
+ }
170
+ function isJudge(value) {
171
+ return typeof value === "object" && value !== null && value.kind === "judge";
172
+ }
173
+ function hasBlockFinding(findings) {
174
+ return (findings ?? []).some((finding) => normalizeFeedbackSeverity(finding.severity) === "block");
175
+ }
176
+ var SEVERITIES = ["block", "should-fix", "nice-to-have", "approve"];
177
+ function countBySeverity(findings) {
178
+ const counts = Object.fromEntries(SEVERITIES.map((severity) => [severity, 0]));
179
+ for (const finding of findings) counts[normalizeFeedbackSeverity(finding.severity)] += 1;
180
+ return counts;
181
+ }
182
+ function judgeDecision(answers) {
183
+ const worth = answers.worth_another_round?.noul ?? answers.worth_another_round?.probability;
184
+ const doing = answers.worth_doing?.noul ?? answers.worth_doing?.probability;
185
+ const holds = answers.holds?.noul ?? answers.holds?.probability;
186
+ const reason = answers.stop_reason?.choice ?? "unknown";
187
+ if (reason === "product_decision") return { again: false, stop: "product_decision", reason: "the judge requested a product decision" };
188
+ if (reason === "holds" || reason === "over_polishing") {
189
+ return { again: false, stop: "ship", reason: `the judge chose ${reason}` };
190
+ }
191
+ if (reason !== "unknown" && reason !== "continue") {
192
+ return { again: false, stop: "fail", reason: `the judge chose ${reason}` };
193
+ }
194
+ if (typeof holds === "number" && holds >= 0.5) {
195
+ return { again: false, stop: "ship", reason: `the judge says it holds (${holds.toFixed(2)})` };
196
+ }
197
+ if (typeof doing === "number" && doing < 0.5) {
198
+ return { again: false, stop: "ship", reason: `the judge says the findings are not worth doing (${doing.toFixed(2)})` };
199
+ }
200
+ if (typeof worth === "number" && worth < 0.5) {
201
+ return { again: false, stop: "fail", reason: `the judge says another round is not worth it (${worth.toFixed(2)})` };
202
+ }
203
+ return {
204
+ again: true,
205
+ reason: `the judge says another round is worth it${typeof worth === "number" ? ` (${worth.toFixed(2)})` : ""}`
206
+ };
207
+ }
208
+ async function askJudge(cfg, state, ctx, path) {
209
+ const judgeJob = agentJob({
210
+ label: "refine:judge",
211
+ engine: cfg.seat.engine,
212
+ model: cfg.seat.identity.model,
213
+ workspaceMode: "none",
214
+ tools: [],
215
+ leaf: true,
216
+ prompt: JSON.stringify({ state, questions: cfg.questions })
217
+ });
218
+ const outcome = await judgeJob({ ...ctx, depth: ctx.depth + 1, path: [...path, "refine-judge"] });
219
+ let answers = {};
220
+ if (outcome.status === "pass") {
221
+ try {
222
+ const parsed = JSON.parse(String(outcome.data ?? ""));
223
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
224
+ answers = parsed;
225
+ }
226
+ } catch {
227
+ }
228
+ }
229
+ const decision = judgeDecision(answers);
230
+ ctx.emit({ kind: "refine:judge", ts: Date.now(), path: [...path], answers, reason: decision.reason });
231
+ return { answers, decision };
232
+ }
233
+ async function consultJudge(cfg, initialState, ctx, path, options) {
234
+ let state = initialState;
235
+ let pending = options.pending;
236
+ for (; ; ) {
237
+ if (pending) {
238
+ options.save(state);
239
+ const answer2 = await requestInteraction(
240
+ cfg.interaction ?? DEFAULT_INTERACTION,
241
+ "What product decision should guide this review?",
242
+ jsonSnapshot({
243
+ requester: { path, identity: options.identity, engine: cfg.seat.identity, round: state.round },
244
+ material: state
245
+ }),
246
+ ctx
247
+ );
248
+ if ("paused" in answer2) return { state, paused: answer2.paused };
249
+ state = { ...state, productFeedback: [...state.productFeedback ?? [], answer2.response] };
250
+ }
251
+ const { decision } = await askJudge(cfg, state, ctx, path);
252
+ if (decision.stop !== "product_decision") return { state, decision };
253
+ pending = true;
254
+ }
255
+ }
256
+
122
257
  // src/core/dag.ts
123
258
  function slug(s) {
124
259
  return s.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-+|-+$)/g, "") || "node";
@@ -158,10 +293,11 @@ function validateKickbackBudget(budget, nodes) {
158
293
  message: `dag maxKickbacks names unknown target "${target}"`
159
294
  });
160
295
  }
296
+ if (isJudge(limit2)) continue;
161
297
  if (!Number.isInteger(limit2) || limit2 < 0) {
162
298
  throw new LoopError({
163
299
  code: "CONFIG",
164
- message: `dag maxKickbacks for "${target}" must be a non-negative integer`
300
+ message: `dag maxKickbacks for "${target}" must be a non-negative integer or a judge`
165
301
  });
166
302
  }
167
303
  }
@@ -220,8 +356,18 @@ function dag(config) {
220
356
  const stopOnError = config.stopOnError ?? true;
221
357
  const maxKickbacks = config.maxKickbacks ?? 0;
222
358
  const perTargetBudget = typeof maxKickbacks !== "number";
223
- const targetLimit = (target) => typeof maxKickbacks === "number" ? maxKickbacks : maxKickbacks[target] ?? 0;
359
+ const targetJudge = (target) => {
360
+ if (typeof maxKickbacks === "number") return void 0;
361
+ const budget = maxKickbacks[target];
362
+ return isJudge(budget) ? budget : void 0;
363
+ };
364
+ const targetLimit = (target) => {
365
+ if (typeof maxKickbacks === "number") return maxKickbacks;
366
+ const budget = maxKickbacks[target] ?? 0;
367
+ return isJudge(budget) ? budget.cap : budget;
368
+ };
224
369
  const routeKickbacks = config.maxKickbacks !== void 0 && (perTargetBudget || maxKickbacks > 0);
370
+ const identity = interactionIdentity(config);
225
371
  const dependents = new Map(names.map((n) => [n, []]));
226
372
  for (const [dep, name] of edges) dependents.get(dep).push(name);
227
373
  const ancestorsOf = (name) => {
@@ -253,12 +399,17 @@ function dag(config) {
253
399
  const path = [...parent.path, config.name];
254
400
  const depth = parent.depth + 1;
255
401
  const ts = () => Date.now();
256
- const targetCounts = /* @__PURE__ */ new Map();
402
+ const checkpointPath = [...path, "@judge-kickback"];
403
+ const saved = savedInteraction(parent, checkpointPath, identity);
404
+ let pending = saved?.pending;
405
+ const targetCounts = new Map(Object.entries(saved?.targetCounts ?? {}));
406
+ const judgeHistory = new Map(Object.entries(saved?.history ?? {}));
407
+ const productFeedback = new Map(Object.entries(saved?.productFeedback ?? {}));
257
408
  parent.emit({ kind: "dag:start", ts: ts(), path, depth, nodes: names });
258
409
  const limit2 = pLimit(limitN);
259
- const results = /* @__PURE__ */ new Map();
260
- const memo = /* @__PURE__ */ new Map();
261
- const attempts = /* @__PURE__ */ new Map();
410
+ const results = new Map(Object.entries(saved?.results ?? {}));
411
+ const memo = new Map([...results].map(([name, outcome2]) => [name, Promise.resolve(outcome2)]));
412
+ const attempts = new Map(Object.entries(saved?.attempts ?? {}));
262
413
  let stopped = false;
263
414
  const pendingKickback = /* @__PURE__ */ new Map();
264
415
  const nodeContext = (name) => {
@@ -497,8 +648,8 @@ function dag(config) {
497
648
  };
498
649
  await Promise.all(names.map(run2));
499
650
  if (routeKickbacks) {
500
- let used = 0;
501
- const rejected = /* @__PURE__ */ new Set();
651
+ let used = Number(saved?.used ?? 0);
652
+ const rejected = new Set(saved?.rejected ?? []);
502
653
  const emitKickback = (from, to, reason, accepted, count, limit3, note) => parent.emit({
503
654
  kind: "dag:kickback",
504
655
  ts: ts(),
@@ -523,7 +674,7 @@ function dag(config) {
523
674
  const request = revisionFromOutcome(results.get(from));
524
675
  const to = request.target;
525
676
  const { reason } = request;
526
- const count = (targetCounts.get(to) ?? 0) + 1;
677
+ const count = pending?.from === from ? pending.count : (targetCounts.get(to) ?? 0) + 1;
527
678
  targetCounts.set(to, count);
528
679
  const limit3 = targetLimit(to);
529
680
  const allow = nodes.get(from).acceptsKickbackTo;
@@ -533,6 +684,71 @@ function dag(config) {
533
684
  emitKickback(from, to, reason, false, count, limit3, note);
534
685
  continue;
535
686
  }
687
+ const requestFindings = request.findings ?? [];
688
+ const cfgJudge = targetJudge(to);
689
+ const withinBudget = perTargetBudget ? count <= limit3 : used < limit3;
690
+ let effectiveReason = reason;
691
+ if (cfgJudge !== void 0 && withinBudget && !hasBlockFinding(requestFindings)) {
692
+ const history = judgeHistory.get(to) ?? [];
693
+ const state = pending?.from === from ? pending.state : {
694
+ ...productFeedback.has(to) ? { productFeedback: productFeedback.get(to) } : {},
695
+ latestFindings: requestFindings,
696
+ rounds: history,
697
+ round: count,
698
+ cap: cfgJudge.cap
699
+ };
700
+ const result = await consultJudge(cfgJudge, state, parent, path, {
701
+ identity: interactionIdentity({ identity, from, to }),
702
+ pending: pending?.from === from,
703
+ save: (questionState) => checkpointInteraction(parent, checkpointPath, identity, jsonSnapshot({
704
+ pending: { from, count, state: questionState },
705
+ results: Object.fromEntries([...results].map(([name, outcome2]) => [name, outcomeSnapshot(outcome2)])),
706
+ attempts: Object.fromEntries(attempts),
707
+ targetCounts: Object.fromEntries(targetCounts),
708
+ history: Object.fromEntries(judgeHistory),
709
+ productFeedback: Object.fromEntries(productFeedback),
710
+ used,
711
+ rejected: [...rejected]
712
+ }))
713
+ });
714
+ if ("paused" in result) {
715
+ record4(from, result.paused, "done");
716
+ break;
717
+ }
718
+ checkpointInteraction(parent, checkpointPath, identity, null);
719
+ pending = void 0;
720
+ productFeedback.set(to, result.state.productFeedback ?? []);
721
+ const { decision } = result;
722
+ judgeHistory.set(to, [
723
+ ...history,
724
+ { round: count, findings: requestFindings, counts: countBySeverity(requestFindings) }
725
+ ]);
726
+ if (!decision.again) {
727
+ emitKickback(from, to, `${reason} (${decision.reason})`, false, count, limit3, decision.reason);
728
+ if (decision.stop === "ship") {
729
+ const shipped = record4(from, {
730
+ status: "pass",
731
+ confidence: results.get(from).confidence,
732
+ summary: decision.reason,
733
+ data: results.get(from).data
734
+ }, "done");
735
+ memo.set(from, Promise.resolve(shipped));
736
+ const unblocked = dirtyFrom(from);
737
+ unblocked.delete(from);
738
+ for (const d of unblocked) {
739
+ memo.delete(d);
740
+ results.delete(d);
741
+ rejected.delete(d);
742
+ }
743
+ stopped = false;
744
+ await Promise.all(names.map(run2));
745
+ continue;
746
+ }
747
+ rejected.add(from);
748
+ continue;
749
+ }
750
+ effectiveReason = `${reason} (${decision.reason})`;
751
+ }
536
752
  if (perTargetBudget ? count > limit3 : used >= limit3) {
537
753
  emitKickback(
538
754
  from,
@@ -550,7 +766,7 @@ function dag(config) {
550
766
  break;
551
767
  }
552
768
  used += 1;
553
- emitKickback(from, to, reason, true, count, limit3);
769
+ emitKickback(from, to, effectiveReason, true, count, limit3);
554
770
  const dirty = dirtyFrom(to);
555
771
  for (const d of dirty) {
556
772
  memo.delete(d);
@@ -559,8 +775,8 @@ function dag(config) {
559
775
  }
560
776
  pendingKickback.set(to, {
561
777
  status: "fail",
562
- summary: `Kicked back from "${from}": ${reason}`,
563
- revision: { ...request, source: request.source ?? from }
778
+ summary: `Kicked back from "${from}": ${effectiveReason}`,
779
+ revision: { ...request, reason: effectiveReason, source: request.source ?? from }
564
780
  });
565
781
  stopped = false;
566
782
  await Promise.all(names.map(run2));
@@ -608,7 +824,7 @@ function dag(config) {
608
824
  parent.emit({ kind: "dag:end", ts: ts(), path, outcome });
609
825
  return outcome;
610
826
  };
611
- return setMeta(job, {
827
+ return interactionDeclaration(setMeta(job, {
612
828
  kind: "dag",
613
829
  name: config.name,
614
830
  ...config.maxKickbacks !== void 0 ? { maxKickbacks: config.maxKickbacks } : {},
@@ -629,7 +845,7 @@ function dag(config) {
629
845
  job: jobMeta(nodeJob)
630
846
  };
631
847
  })
632
- });
848
+ }), config);
633
849
  }
634
850
  function sequence(name, ...jobs) {
635
851
  const nodes = {};
@@ -802,9 +1018,15 @@ function loop(config) {
802
1018
  const path = [...parent.path, config.name];
803
1019
  const depth = parent.depth + 1;
804
1020
  const ts = () => Date.now();
805
- let lastReview;
806
- let lastGate;
807
- let iteration = 0;
1021
+ const checkpointPath = [...path, "@interaction-loop"];
1022
+ const identity = interactionIdentity({ config, params: parent.params });
1023
+ const saved = savedInteraction(parent, checkpointPath, identity);
1024
+ let checkpointed = saved !== void 0;
1025
+ let resumePhase = saved?.phase;
1026
+ let lastReview = saved?.lastReview ?? parent.lastReview;
1027
+ let lastGate = saved?.lastGate;
1028
+ let iteration = typeof saved?.iteration === "number" ? saved.iteration : 0;
1029
+ if (resumePhase === "body") iteration -= 1;
808
1030
  const ctxAt = (iter, lastOutcome) => childContext(parent, {
809
1031
  depth,
810
1032
  path,
@@ -815,6 +1037,7 @@ function loop(config) {
815
1037
  });
816
1038
  parent.emit({ kind: "loop:start", ts: ts(), path, depth, max: config.max });
817
1039
  const finish = async (outcome, iterations) => {
1040
+ if (checkpointed && outcome.status !== "paused" && outcome.status !== "aborted") checkpointInteraction(parent, checkpointPath, identity, null);
818
1041
  parent.emit({ kind: "loop:end", ts: ts(), path, outcome, iterations });
819
1042
  if (config.onComplete) {
820
1043
  try {
@@ -861,7 +1084,7 @@ function loop(config) {
861
1084
  config.until ? prepareCondition(config.until, entryContext) : void 0,
862
1085
  config.stopOn ? prepareCondition(config.stopOn, entryContext) : void 0
863
1086
  ]);
864
- if (start) {
1087
+ if (start && !saved) {
865
1088
  const r = await gate(start, "start", ctxAt(0), void 0);
866
1089
  parent.emit({
867
1090
  kind: "loop:condition",
@@ -877,11 +1100,30 @@ function loop(config) {
877
1100
  0
878
1101
  );
879
1102
  }
880
- let last;
881
- let consecutiveErrors = 0;
882
- let consecutiveReviewFails = 0;
1103
+ let last = saved?.last;
1104
+ let consecutiveErrors = Number(saved?.consecutiveErrors ?? 0);
1105
+ let consecutiveReviewFails = Number(saved?.consecutiveReviewFails ?? 0);
883
1106
  const tracker = noProgress ? new ProgressTracker(noProgress) : void 0;
1107
+ const samples = [...saved?.samples ?? []];
1108
+ for (const sample of samples) tracker?.record(sample);
884
1109
  let warnedInert = false;
1110
+ const checkpointContext = (ctx, phase) => ({
1111
+ ...ctx,
1112
+ interactionCheckpoint() {
1113
+ checkpointed = true;
1114
+ parent.interactionCheckpoint?.();
1115
+ checkpointInteraction(parent, checkpointPath, identity, jsonSnapshot({
1116
+ phase,
1117
+ iteration,
1118
+ consecutiveErrors,
1119
+ consecutiveReviewFails,
1120
+ samples,
1121
+ ...last ? { last: outcomeSnapshot(last) } : {},
1122
+ ...lastReview ? { lastReview: outcomeSnapshot(lastReview) } : {},
1123
+ ...lastGate ? { lastGate: Object.fromEntries(Object.entries(lastGate).filter(([, value]) => value !== void 0)) } : {}
1124
+ }));
1125
+ }
1126
+ });
885
1127
  const evaluateConvergence = async (at, gateContext, bodyOutcome) => {
886
1128
  const conv = until ? await gate(until, "until", gateContext, bodyOutcome) : {
887
1129
  met: bodyOutcome.status === "pass",
@@ -926,7 +1168,7 @@ function loop(config) {
926
1168
  }
927
1169
  let reviewOutcome;
928
1170
  try {
929
- reviewOutcome = await config.review(ctxAt(at, bodyOutcome));
1171
+ reviewOutcome = await config.review(checkpointContext(ctxAt(at, bodyOutcome), "review"));
930
1172
  } catch (e) {
931
1173
  throw LoopError.from(e, {
932
1174
  code: "VALIDATION",
@@ -993,7 +1235,7 @@ function loop(config) {
993
1235
  }
994
1236
  return { conv, turnReview: reviewOutcome };
995
1237
  };
996
- if (config.checkFirst) {
1238
+ if (config.checkFirst && !saved) {
997
1239
  await yieldToLoop();
998
1240
  if (parent.signal.aborted)
999
1241
  return finish(
@@ -1010,7 +1252,7 @@ function loop(config) {
1010
1252
  { status: "aborted", summary: "aborted by signal" },
1011
1253
  iteration
1012
1254
  );
1013
- if (config.max != null && iteration >= config.max) {
1255
+ if (resumePhase !== "review" && config.max != null && iteration >= config.max) {
1014
1256
  return finish(
1015
1257
  {
1016
1258
  status: "exhausted",
@@ -1022,13 +1264,15 @@ function loop(config) {
1022
1264
  iteration
1023
1265
  );
1024
1266
  }
1025
- iteration += 1;
1267
+ const resumingReview = resumePhase === "review";
1268
+ resumePhase = void 0;
1269
+ if (!resumingReview) iteration += 1;
1026
1270
  const ctx = ctxAt(iteration, last);
1027
1271
  let turnReview;
1028
- parent.emit({ kind: "loop:iteration", ts: ts(), path, iteration });
1272
+ if (!resumingReview) parent.emit({ kind: "loop:iteration", ts: ts(), path, iteration });
1029
1273
  let bodyThrew = false;
1030
1274
  try {
1031
- last = await config.body(ctx);
1275
+ if (!resumingReview) last = await config.body(checkpointContext(ctx, "body"));
1032
1276
  consecutiveErrors = 0;
1033
1277
  } catch (e) {
1034
1278
  bodyThrew = true;
@@ -1119,7 +1363,7 @@ function loop(config) {
1119
1363
  iteration
1120
1364
  );
1121
1365
  }
1122
- if (config.onIteration) {
1366
+ if (config.onIteration && !resumingReview) {
1123
1367
  try {
1124
1368
  await config.onIteration(last, ctx);
1125
1369
  } catch (e) {
@@ -1131,7 +1375,7 @@ function loop(config) {
1131
1375
  });
1132
1376
  }
1133
1377
  }
1134
- if (stopOn) {
1378
+ if (stopOn && !resumingReview) {
1135
1379
  const r = await gate(stopOn, "stopOn", ctx, last);
1136
1380
  parent.emit({
1137
1381
  kind: "loop:condition",
@@ -1179,7 +1423,7 @@ function loop(config) {
1179
1423
  });
1180
1424
  }
1181
1425
  }
1182
- const report = tracker.record({
1426
+ const sample = {
1183
1427
  iteration,
1184
1428
  fingerprint,
1185
1429
  signal: signalValue,
@@ -1188,7 +1432,9 @@ function loop(config) {
1188
1432
  gate: noProgress.gate && until && !conv.met ? conv.output : void 0,
1189
1433
  confidence: turnReview?.confidence ?? conv.confidence ?? last.confidence,
1190
1434
  reason: turnReview ? turnReview.summary ?? "review rejected" : conv.reason
1191
- });
1435
+ };
1436
+ samples.push(Object.fromEntries(Object.entries(sample).filter(([, value]) => value !== void 0)));
1437
+ const report = tracker.record(sample);
1192
1438
  if (!warnedInert && tracker.isInert()) {
1193
1439
  warnedInert = true;
1194
1440
  parent.log(
@@ -1244,7 +1490,7 @@ function loop(config) {
1244
1490
  );
1245
1491
  }
1246
1492
  };
1247
- return setMeta(job, {
1493
+ return interactionDeclaration(setMeta(job, {
1248
1494
  kind: "loop",
1249
1495
  name: config.name,
1250
1496
  max: config.max,
@@ -1256,7 +1502,7 @@ function loop(config) {
1256
1502
  review: !!config.review,
1257
1503
  maxReviewRestarts: config.maxReviewRestarts,
1258
1504
  body: jobMeta(config.body)
1259
- });
1505
+ }), config);
1260
1506
  }
1261
1507
  function delay(ms, signal) {
1262
1508
  if (signal.aborted) return Promise.resolve();
@@ -1475,11 +1721,13 @@ var NOISE = /* @__PURE__ */ new Set([
1475
1721
  "engine:thinking"
1476
1722
  ]);
1477
1723
  function readResumeRecord(path) {
1724
+ const receipts = [];
1725
+ const interactions = /* @__PURE__ */ new Map();
1478
1726
  const anchors = /* @__PURE__ */ new Map();
1479
1727
  const stages = /* @__PURE__ */ new Map();
1480
1728
  const usage2 = /* @__PURE__ */ new Map();
1481
1729
  const started = /* @__PURE__ */ new Set();
1482
- if (!existsSync(path)) return { outcomes: { anchors, stages }, usage: usage2 };
1730
+ if (!existsSync(path)) return { outcomes: { anchors, stages, interactions }, usage: usage2, receipts };
1483
1731
  const lines = readFileSync(path, "utf8").split(/\r?\n/);
1484
1732
  for (const [lineNumber, line] of lines.entries()) {
1485
1733
  if (!line) continue;
@@ -1489,11 +1737,19 @@ function readResumeRecord(path) {
1489
1737
  } catch {
1490
1738
  continue;
1491
1739
  }
1492
- if (event.kind === "workflow:start") {
1740
+ if (event.kind === "engine:usage") receipts.push(event.usage);
1741
+ if (event.kind === "interaction:checkpoint") {
1742
+ const key = event.path.join("/");
1743
+ if (event.data === null) interactions.delete(key);
1744
+ else interactions.set(key, { identity: event.identity, workspace: event.workspace, data: cloneFrozenJson(event.data) });
1745
+ } else if (event.kind === "workflow:start") {
1493
1746
  const key = event.path.join("/");
1494
1747
  const prior = anchors.get(key);
1495
1748
  if (prior?.identity !== event.identity || prior.workspace !== event.workspace) {
1496
1749
  const prefix = key ? `${key}/` : "";
1750
+ for (const stage2 of interactions.keys()) {
1751
+ if (stage2.startsWith(prefix)) interactions.delete(stage2);
1752
+ }
1497
1753
  for (const stage2 of stages.keys()) {
1498
1754
  if (stage2.startsWith(prefix)) stages.delete(stage2);
1499
1755
  }
@@ -1529,7 +1785,7 @@ function readResumeRecord(path) {
1529
1785
  }
1530
1786
  }
1531
1787
  }
1532
- return { outcomes: { anchors, stages }, usage: usage2 };
1788
+ return { outcomes: { anchors, stages, interactions }, usage: usage2, receipts };
1533
1789
  }
1534
1790
  function ensureDir(path) {
1535
1791
  const dir = dirname(path);
@@ -1613,259 +1869,6 @@ function assertSafeDirectory(root, dir, label) {
1613
1869
  throw new Error(`unsafe ${label}: resolves outside the workspace`);
1614
1870
  }
1615
1871
  }
1616
- var NOISE2 = /* @__PURE__ */ new Set([
1617
- "engine:text",
1618
- "engine:thinking"
1619
- ]);
1620
- function runsHome() {
1621
- return join(process.env.OBVERSA_HOME ?? join(homedir(), ".obversa"), "runs");
1622
- }
1623
- function slug2(value) {
1624
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "run";
1625
- }
1626
- function newRunId(title) {
1627
- return `${slug2(title)}-${randomBytes(3).toString("hex")}`;
1628
- }
1629
- function startSupervisor(input) {
1630
- const dir = join(runsHome(), input.runId);
1631
- const eventsPath = join(dir, "events.jsonl");
1632
- const proofsPath = join(dir, "proofs.jsonl");
1633
- const statusPath = join(dir, "status.json");
1634
- mkdirSync(dir, { recursive: true });
1635
- writeBestEffort(eventsPath, "");
1636
- writeBestEffort(proofsPath, "");
1637
- const startedAt = Date.now();
1638
- const status = {
1639
- runId: input.runId,
1640
- pid: process.pid,
1641
- cwd: input.cwd,
1642
- title: input.title,
1643
- startedAt,
1644
- updatedAt: startedAt,
1645
- status: "running",
1646
- shape: input.shape,
1647
- live: {
1648
- path: [],
1649
- iteration: 0,
1650
- usage: {
1651
- inputTokens: 0,
1652
- outputTokens: 0,
1653
- cacheReadInputTokens: 0,
1654
- cacheCreationInputTokens: 0,
1655
- calls: 0,
1656
- unmeasuredCalls: 0
1657
- }
1658
- }
1659
- };
1660
- const active = /* @__PURE__ */ new Map();
1661
- let proofCount = 0;
1662
- const writeStatus = () => {
1663
- status.updatedAt = Date.now();
1664
- writeBestEffort(statusPath, JSON.stringify(status, null, 2));
1665
- };
1666
- const refreshCurrent = () => {
1667
- status.live.active = active.size ? Object.fromEntries(active.entries()) : void 0;
1668
- status.live.current = [...active.values()].sort((a, b) => {
1669
- const deadline = (a.deadlineAt ?? Number.POSITIVE_INFINITY) - (b.deadlineAt ?? Number.POSITIVE_INFINITY);
1670
- return deadline || b.startedAt - a.startedAt;
1671
- })[0];
1672
- };
1673
- const activeKey = (kind, path) => `${kind}:${path.join("\0")}`;
1674
- writeStatus();
1675
- const sink = (event) => {
1676
- if (!NOISE2.has(event.kind)) appendBestEffort(eventsPath, event);
1677
- switch (event.kind) {
1678
- case "run:end":
1679
- status.live.lastOutcome = outcomeSummary(event.outcome);
1680
- break;
1681
- case "loop:iteration":
1682
- status.live.path = event.path;
1683
- status.live.iteration = event.iteration;
1684
- break;
1685
- case "loop:condition":
1686
- status.live.lastGate = {
1687
- which: event.which,
1688
- met: event.result.met,
1689
- confidence: event.result.confidence,
1690
- reason: event.result.reason
1691
- };
1692
- break;
1693
- case "condition:result":
1694
- status.live.lastGate = {
1695
- which: event.label,
1696
- met: event.result.met,
1697
- confidence: event.result.confidence,
1698
- reason: event.result.reason
1699
- };
1700
- break;
1701
- case "dag:node": {
1702
- const path = [...event.path, event.node];
1703
- status.live.path = path;
1704
- if (event.phase === "start") {
1705
- active.set(activeKey("dag-node", path), {
1706
- kind: "dag-node",
1707
- path,
1708
- node: event.node,
1709
- startedAt: event.ts,
1710
- timeoutMs: event.timeoutMs,
1711
- deadlineAt: event.timeoutMs ? event.ts + event.timeoutMs : void 0
1712
- });
1713
- } else {
1714
- active.delete(activeKey("dag-node", path));
1715
- }
1716
- refreshCurrent();
1717
- break;
1718
- }
1719
- case "job:start": {
1720
- const path = [...event.path, event.label];
1721
- active.set(activeKey("job", path), {
1722
- kind: "job",
1723
- path: event.path,
1724
- label: event.label,
1725
- startedAt: event.ts,
1726
- timeoutMs: event.timeoutMs,
1727
- deadlineAt: event.timeoutMs ? event.ts + event.timeoutMs : void 0
1728
- });
1729
- refreshCurrent();
1730
- break;
1731
- }
1732
- case "loop:end":
1733
- case "dag:end":
1734
- case "job:end":
1735
- status.live.path = event.path;
1736
- status.live.lastOutcome = outcomeSummary(event.outcome);
1737
- if (event.kind === "job:end") {
1738
- active.delete(activeKey("job", [...event.path, event.label]));
1739
- refreshCurrent();
1740
- }
1741
- break;
1742
- case "engine:usage":
1743
- status.live.usage.calls += 1;
1744
- if (event.usage.kind === "unknown") {
1745
- status.live.usage.unmeasuredCalls += 1;
1746
- } else {
1747
- status.live.usage.inputTokens += event.usage.inputTokens;
1748
- status.live.usage.outputTokens += event.usage.outputTokens;
1749
- status.live.usage.cacheReadInputTokens += event.usage.cacheReadInputTokens ?? 0;
1750
- status.live.usage.cacheCreationInputTokens += event.usage.cacheCreationInputTokens ?? 0;
1751
- }
1752
- break;
1753
- case "proof": {
1754
- const proof = {
1755
- name: event.name,
1756
- path: event.path,
1757
- artifact: event.artifact
1758
- };
1759
- proofCount += 1;
1760
- appendBestEffort(proofsPath, proof);
1761
- status.evidence = {
1762
- count: proofCount,
1763
- indexPath: proofsPath,
1764
- latest: proof
1765
- };
1766
- break;
1767
- }
1768
- }
1769
- if (!NOISE2.has(event.kind)) writeStatus();
1770
- };
1771
- const finish = (outcome) => {
1772
- status.status = outcome.status;
1773
- status.endedAt = Date.now();
1774
- status.live.current = void 0;
1775
- status.live.active = void 0;
1776
- status.live.lastOutcome = outcomeSummary(outcome);
1777
- writeStatus();
1778
- };
1779
- return { runId: input.runId, dir, sink, finish };
1780
- }
1781
- function outcomeSummary(outcome) {
1782
- return {
1783
- status: outcome.status,
1784
- summary: outcome.summary,
1785
- late: outcome.late
1786
- };
1787
- }
1788
- function writeBestEffort(path, text6) {
1789
- try {
1790
- writeFileSync(path, text6);
1791
- } catch {
1792
- }
1793
- }
1794
- function appendBestEffort(path, value) {
1795
- try {
1796
- appendFileSync(path, `${JSON.stringify(value)}
1797
- `);
1798
- } catch {
1799
- }
1800
- }
1801
- function runningTotal(totals) {
1802
- const read = totals.cacheReadInputTokens ?? 0;
1803
- const unmeasured = totals.unmeasuredCalls ?? 0;
1804
- const missing = unmeasured > 0 ? `, usage unknown on ${unmeasured} call${unmeasured === 1 ? "" : "s"}` : "";
1805
- const base = `${totals.inputTokens}/${totals.outputTokens} tok`;
1806
- return `${base}${read > 0 ? `, ${read} tok from cache` : ""}${missing}`;
1807
- }
1808
- function toLine(value) {
1809
- return value.replace(/[\u0000-\u001f\u007f]+/g, " ");
1810
- }
1811
- function formatEvent(event, totals) {
1812
- return toLine(renderEvent(event, totals));
1813
- }
1814
- function renderEvent(event, totals) {
1815
- const at = event.path.length ? `${event.path.join(" \u203A ")} ` : "";
1816
- switch (event.kind) {
1817
- case "run:start":
1818
- return "\u25B8 run";
1819
- case "run:end":
1820
- return `\u25C2 run ${event.outcome.status}${event.outcome.late ? " late" : ""} (${runningTotal(event.usage)})`;
1821
- case "loop:start":
1822
- return `${at}\u25B8 loop${event.max ? ` (max ${event.max})` : ""}`;
1823
- case "dag:start":
1824
- return `${at}\u25B8 dag (${event.nodes.length} nodes)`;
1825
- case "loop:iteration":
1826
- return `${at}\xB7 iteration ${event.iteration}`;
1827
- case "loop:condition":
1828
- return `${at}\xB7 ${event.which} ${event.result.met ? "met" : "not met"}: ${event.result.reason}`;
1829
- case "condition:result":
1830
- return `${at}\xB7 ${event.label} ${event.result.met ? "met" : "not met"}: ${event.result.reason}`;
1831
- case "loop:review":
1832
- return `${at}\xB7 review: ${event.outcome.status}${event.outcome.late ? " late" : ""}`;
1833
- case "loop:end":
1834
- return `${at}\u25C2 ${event.outcome.status}${event.outcome.late ? " late" : ""} (${event.iterations} iter)`;
1835
- case "dag:node":
1836
- return `${at}\xB7 node ${event.node}: ${event.phase}${event.outcome ? ` (${event.outcome.status}${event.outcome.late ? " late" : ""})` : ""}`;
1837
- case "dag:kickback":
1838
- return `${at}\u21A9 kickback ${event.accepted ? "accepted" : "rejected"} ${event.from} -> ${event.to} [${event.count}/${event.limit}]: ${event.reason}${event.note ? ` (${event.note})` : ""}`;
1839
- case "dag:end":
1840
- return `${at}\u25C2 dag ${event.outcome.status}${event.outcome.late ? " late" : ""}`;
1841
- case "job:start":
1842
- return `${at}\u2022 ${event.label}`;
1843
- case "advisor:consult":
1844
- return `${at}\u25C7 advisor ${event.label} #${event.call}: ${event.question}`;
1845
- case "proof":
1846
- return `${at}\u25C8 proof ${event.name}: ${event.artifact.title ?? event.artifact.path ?? event.artifact.kind}`;
1847
- case "job:end":
1848
- return `${at}\u2022 ${event.label}: ${event.outcome.status}${event.outcome.late ? " late" : ""}`;
1849
- case "engine:tool":
1850
- return `${at} tool ${event.name} ${event.phase}`;
1851
- case "engine:usage": {
1852
- const call = event.usage.kind === "unknown" ? "usage unknown" : `${event.usage.inputTokens}/${event.usage.outputTokens} tok`;
1853
- return `${at} ${event.model}: ${call}${totals ? ` (run ${runningTotal(totals)})` : ""}`;
1854
- }
1855
- case "loop:stall":
1856
- return `${at}\u23F9 stalled after ${event.report.iterations.length} no-progress iterations: ${event.report.reason}`;
1857
- case "limit:wait":
1858
- return `${at}\u23F8 limit ${event.code}: waiting ${Math.round(event.waitMs / 1e3)}s`;
1859
- case "limit:pause":
1860
- return `${at}\u23F8 paused (${event.code}): ${event.reason}`;
1861
- case "log":
1862
- return `${at}${event.message}`;
1863
- case "error":
1864
- return `${at}\u2717 ${event.code}: ${event.message}`;
1865
- default:
1866
- return `${at}${event.kind}`;
1867
- }
1868
- }
1869
1872
  function applyEvent(states, event) {
1870
1873
  switch (event.kind) {
1871
1874
  case "callback-requested": {
@@ -2162,14 +2165,11 @@ var MonitorFold = class {
2162
2165
  ...event.note !== void 0 ? { note: event.note } : {}
2163
2166
  });
2164
2167
  }
2165
- const line = { kind: event.kind, ts: event.ts, path: [...event.path] };
2166
- if ("node" in event && typeof event.node === "string") line.node = event.node;
2167
- if ("label" in event && typeof event.label === "string") line.label = event.label;
2168
- if ("outcome" in event && event.outcome && typeof event.outcome === "object" && "summary" in event.outcome && typeof event.outcome.summary === "string") {
2169
- line.summary = event.outcome.summary;
2168
+ const line = formatEvent(event, this.usage);
2169
+ if (line !== "") {
2170
+ this.events.push({ ts: event.ts, line });
2171
+ if (this.events.length > EVENT_TAIL) this.events.splice(0, this.events.length - EVENT_TAIL);
2170
2172
  }
2171
- this.events.push(line);
2172
- if (this.events.length > EVENT_TAIL) this.events.splice(0, this.events.length - EVENT_TAIL);
2173
2173
  }
2174
2174
  finish(outcome) {
2175
2175
  this.status = "done";
@@ -2359,6 +2359,15 @@ function page(name) {
2359
2359
  (() => {
2360
2360
  const el = (id) => document.getElementById(id);
2361
2361
  const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
2362
+ // What the question is about: the raw input, and a link when it names one,
2363
+ // so a reviewer can open the thing being approved instead of taking the
2364
+ // question's word for it.
2365
+ function aboutHtml(input) {
2366
+ if (input === null || input === undefined) return '';
2367
+ const url = input && typeof input === 'object' && !Array.isArray(input) && typeof input.url === 'string' ? input.url : null;
2368
+ const link = url ? '<p><a href="' + esc(url) + '" target="_blank" rel="noopener">' + esc(url) + '</a></p>' : '';
2369
+ return '<pre>' + esc(JSON.stringify(input, null, 2)) + '</pre>' + link;
2370
+ }
2362
2371
  async function answer(requestId, approved) {
2363
2372
  const box = document.querySelector('[data-note="' + CSS.escape(requestId) + '"]');
2364
2373
  const note = box && box.value ? box.value : '';
@@ -2384,8 +2393,8 @@ function page(name) {
2384
2393
  + '<span class="phase" data-phase="' + esc(v.phase) + '" data-status="' + esc(v.outcome ? v.outcome.status : '') + '">' + esc(v.phase === 'done' && v.outcome ? v.outcome.status : v.phase) + '</span></li>'; }).join('');
2385
2394
  el('kickbacks').innerHTML = s.kickbacks.map((k) => '<p class="kick">' + esc(k.from) + ' sent work back to ' + esc(k.to) + (k.accepted ? '' : ' (not accepted' + (k.note ? ': ' + esc(k.note) : '') + ')') + ': ' + esc(k.reason) + ' (' + k.count + ' of ' + k.limit + ')</p>').join('');
2386
2395
  el('pending-title').hidden = s.pending.length === 0;
2387
- el('pending').innerHTML = s.pending.map((p) => '<div><p>' + esc(p.decisionText) + '</p><form onsubmit="return false"><input data-note="' + esc(p.requestId) + '" placeholder="a note, if any"><button class="yes" data-answer="yes" data-request="' + esc(p.requestId) + '">Yes</button><button data-answer="no" data-request="' + esc(p.requestId) + '">No</button></form></div>').join('');
2388
- el('record').textContent = s.events.slice(-40).map((e) => new Date(e.ts).toISOString().slice(11, 19) + ' ' + e.kind.padEnd(14) + (e.node || e.label || '') + (e.summary ? ' ' + e.summary : '')).join('\\n');
2396
+ el('pending').innerHTML = s.pending.map((p) => '<div><p>' + esc(p.decisionText) + '</p>' + aboutHtml(p.input) + '<form onsubmit="return false"><input data-note="' + esc(p.requestId) + '" placeholder="a note, if any"><button class="yes" data-answer="yes" data-request="' + esc(p.requestId) + '">Yes</button><button data-answer="no" data-request="' + esc(p.requestId) + '">No</button></form></div>').join('');
2397
+ el('record').textContent = s.events.slice(-40).map((e) => new Date(e.ts).toISOString().slice(11, 19) + ' ' + e.line).join('\\n');
2389
2398
  if (s.status !== 'done') setTimeout(render, 1000);
2390
2399
  }
2391
2400
  render();
@@ -2475,7 +2484,10 @@ async function run(job, options = {}) {
2475
2484
  thin: options.recordTo === "auto",
2476
2485
  ...resumed === void 0 ? {} : { resume: true }
2477
2486
  }));
2478
- if (resumed !== void 0 && resumed.outcomes.anchors.size > 0) {
2487
+ if (resumed !== void 0) {
2488
+ if (resumed.outcomes.interactions.size > 0) {
2489
+ for (const receipt of resumed.receipts) budget?.addUsage(receipt);
2490
+ }
2479
2491
  initialState[RESUME_STAGE_OUTCOMES] = resumed.outcomes;
2480
2492
  initialState[RESUME_RECORDED_USAGE] = resumed.usage;
2481
2493
  }
@@ -2706,8 +2718,8 @@ function parseFrontMatter(source) {
2706
2718
  function briefFromFile(path) {
2707
2719
  return parseFrontMatter(readFileSync(path, "utf8"));
2708
2720
  }
2709
- function person(question) {
2710
- return { kind: "person", question: text(question, "person question") };
2721
+ function person(question, options = {}) {
2722
+ return { kind: "person", question: text(question, "person question"), ...options };
2711
2723
  }
2712
2724
  function stage(name, config) {
2713
2725
  return { name: text(name, "stage name"), config: { ...config } };
@@ -2733,24 +2745,29 @@ function writesOf(config) {
2733
2745
  if (!writes.length || new Set(writes).size !== writes.length) throw new TypeError("writes must contain unique paths");
2734
2746
  return writes;
2735
2747
  }
2736
- function retryCount(retry, label) {
2737
- if (retry === void 0) return 0;
2738
- if (!Number.isSafeInteger(retry) || retry < 0) throw new TypeError(`${label} must be a non-negative integer`);
2739
- return retry;
2748
+ function refineCount(refine, label) {
2749
+ if (refine === void 0) return 0;
2750
+ if (!Number.isSafeInteger(refine) || refine < 0) throw new TypeError(`${label} must be a non-negative integer`);
2751
+ return refine;
2752
+ }
2753
+ function refineCap(refine) {
2754
+ return isJudge(refine) ? refine.cap : refine;
2740
2755
  }
2741
2756
  function optionalFlag(optional) {
2742
2757
  if (optional === void 0) return void 0;
2743
2758
  if (typeof optional !== "boolean") throw new TypeError("optional must be a boolean");
2744
2759
  return optional;
2745
2760
  }
2746
- function retryOf(config) {
2747
- return config.retry === void 0 ? 1 : retryCount(config.retry, "retry");
2761
+ function refineOf(config) {
2762
+ if (config.refine === void 0) return 1;
2763
+ if (isJudge(config.refine)) return config.refine;
2764
+ return refineCount(config.refine, "refine");
2748
2765
  }
2749
- function retryForStage(config, receivesKickback) {
2750
- if ("reviewedBy" in config && config.reviewedBy) return retryOf(config);
2751
- if (receivesKickback) return retryOf(config);
2752
- if (config.retry !== void 0) {
2753
- throw new TypeError("retry must be on a reviewed stage or a kickback target");
2766
+ function refineForStage(config, receivesKickback) {
2767
+ if ("reviewedBy" in config && config.reviewedBy) return refineOf(config);
2768
+ if (receivesKickback) return refineOf(config);
2769
+ if (config.refine !== void 0) {
2770
+ throw new TypeError("refine must be on a reviewed stage or a kickback target");
2754
2771
  }
2755
2772
  return 0;
2756
2773
  }
@@ -2882,6 +2899,7 @@ function recordedFamilyGate(panel, reviewers, writerStageNames, writerFamilies,
2882
2899
  try {
2883
2900
  const all2 = recordedUsage(ctx);
2884
2901
  const beforeLength = all2.length;
2902
+ const resumingInteraction = hasSavedInteraction(ctx, ctx.path);
2885
2903
  const writerSide = (records) => records.filter(
2886
2904
  (record4) => record4.role === "writer" && record4.stage !== void 0 && writerStageNames.includes(record4.stage)
2887
2905
  );
@@ -2893,7 +2911,7 @@ function recordedFamilyGate(panel, reviewers, writerStageNames, writerFamilies,
2893
2911
  const current = recordedUsage(ctx);
2894
2912
  const after = current.slice(beforeLength);
2895
2913
  const afterWriters = writerSide(after);
2896
- if (writerRunsInsidePanel && afterWriters.length === 0) {
2914
+ if (writerRunsInsidePanel && afterWriters.length === 0 && !resumingInteraction) {
2897
2915
  throw new LoopError({
2898
2916
  code: "BODY",
2899
2917
  phase: "review",
@@ -2901,7 +2919,7 @@ function recordedFamilyGate(panel, reviewers, writerStageNames, writerFamilies,
2901
2919
  });
2902
2920
  }
2903
2921
  assertRecordedFamilies(afterWriters, reviewerDeclarations);
2904
- const reviewerSide = after.filter((record4) => record4.role === "reviewer");
2922
+ const reviewerSide = (resumingInteraction ? current : after).filter((record4) => record4.role === "reviewer" && record4.stage === panelStage);
2905
2923
  if (reviewerSide.length === 0) {
2906
2924
  throw new LoopError({
2907
2925
  code: "BODY",
@@ -3006,6 +3024,26 @@ function guardedAgent(brief, named, seat, files, declaredFiles) {
3006
3024
  return requireNoFiles(named.name, required, ctx.workspace.dir, forbidden, "body")(ctx);
3007
3025
  };
3008
3026
  }
3027
+ function guardedFn(fn, named, declaredFiles) {
3028
+ const writes = writesOf(named.config);
3029
+ const target = named.config.sendsBackTo;
3030
+ const body = fnJob(named.name, async (ctx) => {
3031
+ const outcome = await fn(ctx);
3032
+ if (outcome.status === "fail" && target !== void 0 && revisionFromOutcome(outcome)?.target === void 0) {
3033
+ return kickback(target, outcome.summary ?? `${named.name} failed`, {
3034
+ confidence: outcome.confidence,
3035
+ data: outcome.data,
3036
+ error: outcome.error
3037
+ });
3038
+ }
3039
+ return outcome;
3040
+ });
3041
+ const forbidden = declaredFiles.filter((file) => !writes.includes(file));
3042
+ return async (ctx) => {
3043
+ const required = writes.length ? requireNonEmptyFiles(named.name, body, ctx.workspace.dir, writes) : body;
3044
+ return requireNoFiles(named.name, required, ctx.workspace.dir, forbidden, "body")(ctx);
3045
+ };
3046
+ }
3009
3047
  function unchangedNoteGuard(label, job, writes) {
3010
3048
  const previousHashKey = `declarativePreviousHash:${label}`;
3011
3049
  return async (ctx) => {
@@ -3035,6 +3073,84 @@ function unchangedNoteGuard(label, job, writes) {
3035
3073
  return outcome;
3036
3074
  };
3037
3075
  }
3076
+ function lineDiffCount(before, after) {
3077
+ if (before === void 0) return after.split("\n").length;
3078
+ const a = new Set(before.split("\n"));
3079
+ const b = new Set(after.split("\n"));
3080
+ let changed = 0;
3081
+ for (const line of a) if (!b.has(line)) changed += 1;
3082
+ for (const line of b) if (!a.has(line)) changed += 1;
3083
+ return changed;
3084
+ }
3085
+ function judgedReview(brief, config, cfgJudge, panel) {
3086
+ const useCase = /^Use case:\s*([\s\S]*?)\n\s*\n/m.exec(brief.brief)?.[1]?.replace(/\s+/g, " ").trim();
3087
+ const file = writesOf(config)[0];
3088
+ const history = [];
3089
+ let previousDraft;
3090
+ let productFeedback = [];
3091
+ const identity = interactionIdentity({ brief, config, cfgJudge });
3092
+ return async (ctx) => {
3093
+ const checkpointPath = [...ctx.path, "@judge-review"];
3094
+ let saved = savedInteraction(ctx, checkpointPath, identity);
3095
+ let draft;
3096
+ let changedLines;
3097
+ if (file !== void 0) {
3098
+ try {
3099
+ draft = await readFile(join(ctx.workspace.dir, file), "utf8");
3100
+ changedLines = lineDiffCount(previousDraft, draft);
3101
+ } catch {
3102
+ }
3103
+ previousDraft = draft;
3104
+ }
3105
+ if (saved && saved.state.draft !== draft) saved = void 0;
3106
+ if (saved) {
3107
+ history.splice(0, history.length, ...saved.state.rounds);
3108
+ productFeedback = saved.state.productFeedback ?? [];
3109
+ previousDraft = draft;
3110
+ }
3111
+ const panelOutcome = saved ? saved.panel : await panel({
3112
+ ...ctx,
3113
+ depth: ctx.depth + 1,
3114
+ path: [...ctx.path, "review-panel"]
3115
+ });
3116
+ if (panelOutcome.status === "pass") return panelOutcome;
3117
+ const findings = revisionFromOutcome(panelOutcome)?.findings ?? [];
3118
+ if (hasBlockFinding(findings)) return panelOutcome;
3119
+ const round2 = history.length + 1;
3120
+ const state = saved ? saved.state : {
3121
+ ...productFeedback.length ? { productFeedback } : {},
3122
+ ...useCase !== void 0 ? { useCase } : {},
3123
+ ...file !== void 0 ? { file } : {},
3124
+ ...draft !== void 0 ? { draft } : {},
3125
+ latestFindings: findings,
3126
+ rounds: history,
3127
+ round: round2,
3128
+ cap: cfgJudge.cap
3129
+ };
3130
+ const result = await consultJudge(cfgJudge, state, ctx, ctx.path, {
3131
+ identity,
3132
+ pending: saved !== void 0,
3133
+ save: (questionState) => checkpointInteraction(ctx, checkpointPath, identity, jsonSnapshot({ state: questionState, panel: outcomeSnapshot(panelOutcome) }))
3134
+ });
3135
+ if ("paused" in result) return result.paused;
3136
+ checkpointInteraction(ctx, checkpointPath, identity, null);
3137
+ productFeedback = result.state.productFeedback ?? [];
3138
+ const { decision } = result;
3139
+ history.push({
3140
+ round: round2,
3141
+ findings,
3142
+ counts: countBySeverity(findings),
3143
+ ...changedLines !== void 0 ? { changedLines } : {}
3144
+ });
3145
+ if (!decision.again) {
3146
+ if (decision.stop === "fail") {
3147
+ throw new LoopError({ code: "VALIDATION", phase: "review", path: ctx.path, message: decision.reason });
3148
+ }
3149
+ return { status: "pass", confidence: panelOutcome.confidence, summary: decision.reason, data: panelOutcome.data };
3150
+ }
3151
+ return { ...panelOutcome, summary: `${panelOutcome.summary} (${decision.reason})` };
3152
+ };
3153
+ }
3038
3154
  function stageJob(brief, named, roles, files, declaredFiles, targetFiles, familyTargetStageNames, familyTargetFamilies) {
3039
3155
  const config = named.config;
3040
3156
  if ("agent" in config && config.agent !== void 0) {
@@ -3042,9 +3158,33 @@ function stageJob(brief, named, roles, files, declaredFiles, targetFiles, family
3042
3158
  const guarded = guardedAgent(brief, named, seatRole(roles, config.agent), files, declaredFiles);
3043
3159
  const job = config.reviewedBy === void 0 ? guarded : unchangedNoteGuard(named.name, guarded, writes);
3044
3160
  if (config.reviewedBy === void 0) return job;
3161
+ const reviewRole = role(roles, config.reviewedBy);
3162
+ if (!Array.isArray(reviewRole) && "kind" in reviewRole && reviewRole.kind === "person") {
3163
+ if (!reviewRole.interaction) throw new TypeError("a human reviewer needs an interaction binding");
3164
+ const humanLoop = loop({
3165
+ name: `${named.name}-review`,
3166
+ body: job,
3167
+ max: refineCap(refineOf(config)) + 1,
3168
+ review: humanReview(named.name, {
3169
+ question: reviewRole.question,
3170
+ interaction: reviewRole.interaction,
3171
+ input: async (ctx) => Object.fromEntries(await Promise.all(writes.map(async (file) => [file, await readFile(join(ctx.workspace.dir, file), "utf8")])))
3172
+ })
3173
+ });
3174
+ return interactionDeclaration(copyJobMeta(async (ctx) => {
3175
+ const outcome = await humanLoop(ctx);
3176
+ return outcome.status === "exhausted" ? { ...outcome, status: "fail" } : outcome;
3177
+ }, humanLoop), { humanLoop });
3178
+ }
3045
3179
  const reviewers = panelRole(roles, config.reviewedBy);
3046
3180
  const panel = reviewerPanel(brief, named, reviewers, files, declaredFiles, void 0);
3047
- const retry = retryOf(config);
3181
+ const refine = refineOf(config);
3182
+ const cap = refineCap(refine);
3183
+ const review = isJudge(refine) ? judgedReview(brief, config, refine, panel) : async (ctx) => panel({
3184
+ ...ctx,
3185
+ depth: ctx.depth + 1,
3186
+ path: [...ctx.path, "review-panel"]
3187
+ });
3048
3188
  const reviewLoop = loop({
3049
3189
  name: `${named.name}-review`,
3050
3190
  body: job,
@@ -3060,15 +3200,9 @@ function stageJob(brief, named, roles, files, declaredFiles, targetFiles, family
3060
3200
  }
3061
3201
  return true;
3062
3202
  }, `${named.name} writes`),
3063
- // Review events use a child path. The jobs' role tags, not their
3064
- // paths, separate the recorded sides. The outcome passes through.
3065
- review: async (ctx) => panel({
3066
- ...ctx,
3067
- depth: ctx.depth + 1,
3068
- path: [...ctx.path, "review-panel"]
3069
- }),
3070
- max: retry + 1,
3071
- maxReviewRestarts: retry,
3203
+ review,
3204
+ max: cap + 1,
3205
+ maxReviewRestarts: cap,
3072
3206
  noProgress: { window: 2, gate: true }
3073
3207
  });
3074
3208
  return copyJobMeta(
@@ -3102,9 +3236,27 @@ function stageJob(brief, named, roles, files, declaredFiles, targetFiles, family
3102
3236
  }
3103
3237
  if ("input" in config && config.input !== void 0) {
3104
3238
  const personRole = inputRole(roles, config.input);
3105
- return approval(named.name, { question: personRole.question, target: config.sendsBackTo });
3239
+ if (!personRole.interaction) return approval(named.name, { question: personRole.question, target: config.sendsBackTo });
3240
+ return async (ctx) => {
3241
+ const identity = interactionIdentity({ brief, named, personRole });
3242
+ const checkpointPath = [...ctx.path, "@person-interaction"];
3243
+ const input = jsonSnapshot({ requester: { path: ctx.path, identity }, material: brief });
3244
+ const result = await requestInteraction(personRole.interaction, personRole.question, input, {
3245
+ ...ctx,
3246
+ interactionCheckpoint() {
3247
+ ctx.interactionCheckpoint?.();
3248
+ checkpointInteraction(ctx, checkpointPath, identity, input);
3249
+ }
3250
+ });
3251
+ if ("paused" in result) return result.paused;
3252
+ checkpointInteraction(ctx, checkpointPath, identity, null);
3253
+ return { status: "pass", summary: result.response.prompt, data: result.response };
3254
+ };
3255
+ }
3256
+ if ("fn" in config && config.fn !== void 0) {
3257
+ return guardedFn(config.fn, named, declaredFiles);
3106
3258
  }
3107
- throw new TypeError(`stage ${named.name} must declare agent, run, panel or input`);
3259
+ throw new TypeError(`stage ${named.name} must declare agent, run, panel, input or fn`);
3108
3260
  }
3109
3261
  function workflowRecord(outcome) {
3110
3262
  return {
@@ -3129,26 +3281,13 @@ function panelReviewFiles(stages, index) {
3129
3281
  const targetWrites = target === void 0 ? [] : writesOf(target.config);
3130
3282
  return targetWrites.length ? targetWrites : preceding === void 0 ? [] : writesOf(preceding.config);
3131
3283
  }
3132
- function canonicalForDigest(value) {
3133
- if (typeof value === "function") return `fn:${value.toString()}`;
3134
- if (Array.isArray(value)) return value.map(canonicalForDigest);
3135
- if (value !== null && typeof value === "object") {
3136
- return Object.fromEntries(
3137
- Object.keys(value).sort().map((key) => [
3138
- key,
3139
- canonicalForDigest(value[key])
3140
- ])
3141
- );
3142
- }
3143
- return value;
3144
- }
3145
3284
  function resumeIdentity(name, config) {
3146
3285
  const declared = {
3147
3286
  name,
3148
3287
  brief: config.brief,
3149
3288
  stages: config.stages.map((named) => ({
3150
3289
  name: named.name,
3151
- config: canonicalForDigest(named.config)
3290
+ config: interactionIdentity(named.config)
3152
3291
  })),
3153
3292
  roles: Object.fromEntries(Object.entries(config.roles).map(([key, value]) => [
3154
3293
  key,
@@ -3183,6 +3322,10 @@ function resumeGuard(job, identity, label, retrySafe) {
3183
3322
  const parentPath = ctx.path.slice(0, -1).join("/");
3184
3323
  const anchor = resumed?.anchors.get(parentPath);
3185
3324
  const recorded = anchor?.identity === identity && anchor.workspace === ctx.workspace.dir ? resumed?.stages.get(ctx.path.join("/")) : void 0;
3325
+ if (anchor?.identity === identity && anchor.workspace === ctx.workspace.dir && hasSavedInteraction(ctx, ctx.path)) {
3326
+ restoreRecordedUsage(ctx);
3327
+ return delegateNodeJob(ctx, guarded, job, ctx);
3328
+ }
3186
3329
  if (ctx.graph?.attempt === 1 && recorded !== void 0) {
3187
3330
  if (recorded.kind === "interrupted") {
3188
3331
  if (!retrySafe) {
@@ -3206,7 +3349,7 @@ function resumeGuard(job, identity, label, retrySafe) {
3206
3349
  }
3207
3350
  return delegateNodeJob(ctx, guarded, job, ctx);
3208
3351
  };
3209
- return guarded;
3352
+ return interactionDeclaration(guarded, { identity, label, job });
3210
3353
  }
3211
3354
  async function reconcileInterrupted(ctx, owner, job, label, identity, recordId, startLine) {
3212
3355
  const question = approval(`reconcile ${label}`, {
@@ -3267,7 +3410,7 @@ function workflow(name, config) {
3267
3410
  if (reviewedBy !== void 0 && stageConfig.sendsBackTo !== void 0) {
3268
3411
  throw new TypeError(`stage ${stageName} cannot use both reviewedBy and sendsBackTo`);
3269
3412
  }
3270
- retryForStage(stageConfig, incomingTargets.has(stageName));
3413
+ refineForStage(stageConfig, incomingTargets.has(stageName));
3271
3414
  optionalFlag(stageConfig.optional);
3272
3415
  if (stageConfig.retrySafe !== void 0 && typeof stageConfig.retrySafe !== "boolean") {
3273
3416
  throw new TypeError("retrySafe must be a boolean");
@@ -3277,10 +3420,12 @@ function workflow(name, config) {
3277
3420
  for (const [index, named] of config.stages.entries()) {
3278
3421
  const stageConfig = named.config;
3279
3422
  if ("agent" in stageConfig && stageConfig.agent !== void 0 && stageConfig.reviewedBy !== void 0) {
3280
- assertDistinctSeats([
3423
+ const reviewer = role(config.roles, stageConfig.reviewedBy);
3424
+ if (Array.isArray(reviewer)) assertDistinctSeats([
3281
3425
  seatRole(config.roles, stageConfig.agent),
3282
3426
  ...panelRole(config.roles, stageConfig.reviewedBy)
3283
3427
  ]);
3428
+ else if (!("kind" in reviewer) || reviewer.kind !== "person") throw new TypeError("reviewedBy must name a reviewer panel or person");
3284
3429
  }
3285
3430
  if ("panel" in stageConfig && stageConfig.panel !== void 0) {
3286
3431
  const preceding = precedingAgent(config.stages, index);
@@ -3307,10 +3452,9 @@ function workflow(name, config) {
3307
3452
  for (const named of config.stages) {
3308
3453
  if (named.config.sendsBackTo !== void 0) {
3309
3454
  const target = config.stages.find((candidate) => candidate.name === named.config.sendsBackTo);
3310
- maxKickbacks[named.config.sendsBackTo] = Math.max(
3311
- maxKickbacks[named.config.sendsBackTo] ?? 0,
3312
- retryOf(target.config)
3313
- );
3455
+ const refine = refineOf(target.config);
3456
+ const existing = maxKickbacks[named.config.sendsBackTo];
3457
+ maxKickbacks[named.config.sendsBackTo] = isJudge(refine) ? refine : Math.max(isJudge(existing) ? 0 : existing ?? 0, refine);
3314
3458
  }
3315
3459
  }
3316
3460
  const stageJobIdentity = resumeIdentity(workflowName, config);
@@ -9160,7 +9304,7 @@ function pipeline(name, stages, opts) {
9160
9304
  });
9161
9305
  return dag({ name, nodes, ...opts });
9162
9306
  }
9163
- function slug3(s) {
9307
+ function slug2(s) {
9164
9308
  return s.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-+|-+$)/g, "") || "x";
9165
9309
  }
9166
9310
  function tournament(config) {
@@ -9193,7 +9337,7 @@ function tournament(config) {
9193
9337
  Array.from(
9194
9338
  { length: config.n },
9195
9339
  (_, i) => limit2(async () => {
9196
- const branch = `lines/${slug3(config.name)}-cand-${i}`;
9340
+ const branch = `lines/${slug2(config.name)}-cand-${i}`;
9197
9341
  const wt = await addWorktree(base.dir, {
9198
9342
  branch,
9199
9343
  base: "HEAD",
@@ -9271,7 +9415,7 @@ function tournament(config) {
9271
9415
  }
9272
9416
 
9273
9417
  // src/core/isolated.ts
9274
- var slug4 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "job";
9418
+ var slug3 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "job";
9275
9419
  var forkSeq = 0;
9276
9420
  function isolated(job, opts = {}) {
9277
9421
  const label = opts.label ?? "isolated";
@@ -9292,7 +9436,7 @@ function isolated(job, opts = {}) {
9292
9436
  );
9293
9437
  return job(parent);
9294
9438
  }
9295
- const branch = `lines/${slug4(label)}-${forkSeq += 1}`;
9439
+ const branch = `lines/${slug3(label)}-${forkSeq += 1}`;
9296
9440
  const wt = await addWorktree(base.dir, {
9297
9441
  branch,
9298
9442
  base: "HEAD",
@@ -9326,7 +9470,7 @@ function isolated(job, opts = {}) {
9326
9470
  ...outcome.summary === void 0 ? {} : { summary: outcome.summary }
9327
9471
  }) : void 0;
9328
9472
  const sha = await commit(
9329
- message ?? { subject: `chore(${slug4(label)}): worktree changes` },
9473
+ message ?? { subject: `chore(${slug3(label)}): worktree changes` },
9330
9474
  { cwd: wt.dir, signal: parent.signal }
9331
9475
  );
9332
9476
  const merged = await mergeLock(
@@ -9770,6 +9914,6 @@ function defineJob(job) {
9770
9914
  return job;
9771
9915
  }
9772
9916
 
9773
- export { EXIT_PAUSED, RESUME_STAGE_OUTCOMES, assertGraph, briefFromFile, confidenceCondition, confidenceFromText, convergence, costReport, createAcceptedResultRecord, createCallbackClient, createGitWorktreeProvider, createGraphExecutor, createProofCache, createStoredCallbackClient, dag, dag2 as dagGraphType, defineAgentFromMarkdown, defineJob, directRouter, exitCodeFor, fallbackEngine, formatCostReport, formatEvent, formatPreflight, interruptRunPreflight, isolated, lastDecisionLine, lastGateBrief, loop, parallel, person, pipeline, preflight, preflightEngine, projectTeamRooms, ratchet, readRunPreflight, replayCallbackClient, resolveAcceptedResult, resolveApproval, run, sampled, sequence, stage, team, teamGraphType, tournament, workflow, writeProofArtifact, writeScope };
9917
+ export { EXIT_PAUSED, RESUME_STAGE_OUTCOMES, assertGraph, briefFromFile, confidenceCondition, confidenceFromText, convergence, costReport, createAcceptedResultRecord, createCallbackClient, createGitWorktreeProvider, createGraphExecutor, createProofCache, createStoredCallbackClient, dag, dag2 as dagGraphType, defineAgentFromMarkdown, defineJob, directRouter, exitCodeFor, fallbackEngine, formatCostReport, formatPreflight, interruptRunPreflight, isJudge, isolated, judge, lastDecisionLine, lastGateBrief, loop, parallel, person, pipeline, preflight, preflightEngine, projectTeamRooms, ratchet, readRunPreflight, replayCallbackClient, resolveAcceptedResult, resolveApproval, run, sampled, sequence, stage, stopQuestions, team, teamGraphType, tournament, workflow, writeProofArtifact, writeScope };
9774
9918
  //# sourceMappingURL=api.js.map
9775
9919
  //# sourceMappingURL=api.js.map