@obversa/runtime 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -1,14 +1,16 @@
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, 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, normalizeFeedbackSeverity, nodeJobContext, outcomeFromAgentText, requireNonEmptyFiles, requireNoFiles, panelReviewers, delegateNodeJob, branchCommits, RECORDED_ENGINE_USAGE, compareGitWorkspaceSnapshots } from './chunk-ZXVFWRW5.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-ZXVFWRW5.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';
@@ -17,9 +19,9 @@ import { join, resolve, basename, extname, dirname, isAbsolute, relative, sep }
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,111 @@ 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: "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
+ }
160
+ }
161
+ };
162
+ }
163
+ function judge(seat, opts) {
164
+ if (!Number.isSafeInteger(opts.cap) || opts.cap < 1) {
165
+ throw new TypeError("judge cap must be a positive integer");
166
+ }
167
+ return { kind: "judge", seat, cap: opts.cap, questions: opts.questions ?? stopQuestions() };
168
+ }
169
+ function isJudge(value) {
170
+ return typeof value === "object" && value !== null && value.kind === "judge";
171
+ }
172
+ function hasBlockFinding(findings) {
173
+ return (findings ?? []).some((finding) => normalizeFeedbackSeverity(finding.severity) === "block");
174
+ }
175
+ var SEVERITIES = ["block", "should-fix", "nice-to-have", "approve"];
176
+ function countBySeverity(findings) {
177
+ const counts = Object.fromEntries(SEVERITIES.map((severity) => [severity, 0]));
178
+ for (const finding of findings) counts[normalizeFeedbackSeverity(finding.severity)] += 1;
179
+ return counts;
180
+ }
181
+ function judgeDecision(answers) {
182
+ const worth = answers.worth_another_round?.noul ?? answers.worth_another_round?.probability;
183
+ const doing = answers.worth_doing?.noul ?? answers.worth_doing?.probability;
184
+ const holds = answers.holds?.noul ?? answers.holds?.probability;
185
+ const reason = answers.stop_reason?.choice ?? "unknown";
186
+ if (reason !== "unknown" && reason !== "continue") {
187
+ return { again: false, reason: `the judge chose ${reason}` };
188
+ }
189
+ if (typeof holds === "number" && holds >= 0.5) {
190
+ return { again: false, reason: `the judge says it holds (${holds.toFixed(2)})` };
191
+ }
192
+ if (typeof doing === "number" && doing < 0.5) {
193
+ return { again: false, reason: `the judge says the findings are not worth doing (${doing.toFixed(2)})` };
194
+ }
195
+ if (typeof worth === "number" && worth < 0.5) {
196
+ return { again: false, reason: `the judge says another round is not worth it (${worth.toFixed(2)})` };
197
+ }
198
+ return {
199
+ again: true,
200
+ reason: `the judge says another round is worth it${typeof worth === "number" ? ` (${worth.toFixed(2)})` : ""}`
201
+ };
202
+ }
203
+ async function askJudge(cfg, state, ctx, path) {
204
+ const judgeJob = agentJob({
205
+ label: "refine:judge",
206
+ engine: cfg.seat.engine,
207
+ model: cfg.seat.identity.model,
208
+ workspaceMode: "none",
209
+ tools: [],
210
+ leaf: true,
211
+ prompt: JSON.stringify({ state, questions: cfg.questions })
212
+ });
213
+ const outcome = await judgeJob({ ...ctx, depth: ctx.depth + 1, path: [...path, "refine-judge"] });
214
+ let answers = {};
215
+ if (outcome.status === "pass") {
216
+ try {
217
+ const parsed = JSON.parse(String(outcome.data ?? ""));
218
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
219
+ answers = parsed;
220
+ }
221
+ } catch {
222
+ }
223
+ }
224
+ const decision = judgeDecision(answers);
225
+ ctx.emit({ kind: "refine:judge", ts: Date.now(), path: [...path], answers, reason: decision.reason });
226
+ return { answers, decision };
227
+ }
228
+
122
229
  // src/core/dag.ts
123
230
  function slug(s) {
124
231
  return s.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-+|-+$)/g, "") || "node";
@@ -158,10 +265,11 @@ function validateKickbackBudget(budget, nodes) {
158
265
  message: `dag maxKickbacks names unknown target "${target}"`
159
266
  });
160
267
  }
268
+ if (isJudge(limit2)) continue;
161
269
  if (!Number.isInteger(limit2) || limit2 < 0) {
162
270
  throw new LoopError({
163
271
  code: "CONFIG",
164
- message: `dag maxKickbacks for "${target}" must be a non-negative integer`
272
+ message: `dag maxKickbacks for "${target}" must be a non-negative integer or a judge`
165
273
  });
166
274
  }
167
275
  }
@@ -220,8 +328,18 @@ function dag(config) {
220
328
  const stopOnError = config.stopOnError ?? true;
221
329
  const maxKickbacks = config.maxKickbacks ?? 0;
222
330
  const perTargetBudget = typeof maxKickbacks !== "number";
223
- const targetLimit = (target) => typeof maxKickbacks === "number" ? maxKickbacks : maxKickbacks[target] ?? 0;
331
+ const targetJudge = (target) => {
332
+ if (typeof maxKickbacks === "number") return void 0;
333
+ const budget = maxKickbacks[target];
334
+ return isJudge(budget) ? budget : void 0;
335
+ };
336
+ const targetLimit = (target) => {
337
+ if (typeof maxKickbacks === "number") return maxKickbacks;
338
+ const budget = maxKickbacks[target] ?? 0;
339
+ return isJudge(budget) ? budget.cap : budget;
340
+ };
224
341
  const routeKickbacks = config.maxKickbacks !== void 0 && (perTargetBudget || maxKickbacks > 0);
342
+ const judgeHistory = /* @__PURE__ */ new Map();
225
343
  const dependents = new Map(names.map((n) => [n, []]));
226
344
  for (const [dep, name] of edges) dependents.get(dep).push(name);
227
345
  const ancestorsOf = (name) => {
@@ -533,6 +651,30 @@ function dag(config) {
533
651
  emitKickback(from, to, reason, false, count, limit3, note);
534
652
  continue;
535
653
  }
654
+ const requestFindings = request.findings ?? [];
655
+ const cfgJudge = targetJudge(to);
656
+ const withinBudget = perTargetBudget ? count <= limit3 : used < limit3;
657
+ let effectiveReason = reason;
658
+ if (cfgJudge !== void 0 && withinBudget && !hasBlockFinding(requestFindings)) {
659
+ const history = judgeHistory.get(to) ?? [];
660
+ const state = {
661
+ latestFindings: requestFindings,
662
+ rounds: history,
663
+ round: count,
664
+ cap: cfgJudge.cap
665
+ };
666
+ const { decision } = await askJudge(cfgJudge, state, parent, path);
667
+ judgeHistory.set(to, [
668
+ ...history,
669
+ { round: count, findings: requestFindings, counts: countBySeverity(requestFindings) }
670
+ ]);
671
+ if (!decision.again) {
672
+ rejected.add(from);
673
+ emitKickback(from, to, `${reason} (${decision.reason})`, false, count, limit3, decision.reason);
674
+ continue;
675
+ }
676
+ effectiveReason = `${reason} (${decision.reason})`;
677
+ }
536
678
  if (perTargetBudget ? count > limit3 : used >= limit3) {
537
679
  emitKickback(
538
680
  from,
@@ -550,7 +692,7 @@ function dag(config) {
550
692
  break;
551
693
  }
552
694
  used += 1;
553
- emitKickback(from, to, reason, true, count, limit3);
695
+ emitKickback(from, to, effectiveReason, true, count, limit3);
554
696
  const dirty = dirtyFrom(to);
555
697
  for (const d of dirty) {
556
698
  memo.delete(d);
@@ -559,8 +701,8 @@ function dag(config) {
559
701
  }
560
702
  pendingKickback.set(to, {
561
703
  status: "fail",
562
- summary: `Kicked back from "${from}": ${reason}`,
563
- revision: { ...request, source: request.source ?? from }
704
+ summary: `Kicked back from "${from}": ${effectiveReason}`,
705
+ revision: { ...request, reason: effectiveReason, source: request.source ?? from }
564
706
  });
565
707
  stopped = false;
566
708
  await Promise.all(names.map(run2));
@@ -1613,259 +1755,6 @@ function assertSafeDirectory(root, dir, label) {
1613
1755
  throw new Error(`unsafe ${label}: resolves outside the workspace`);
1614
1756
  }
1615
1757
  }
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
1758
  function applyEvent(states, event) {
1870
1759
  switch (event.kind) {
1871
1760
  case "callback-requested": {
@@ -2162,14 +2051,11 @@ var MonitorFold = class {
2162
2051
  ...event.note !== void 0 ? { note: event.note } : {}
2163
2052
  });
2164
2053
  }
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;
2054
+ const line = formatEvent(event, this.usage);
2055
+ if (line !== "") {
2056
+ this.events.push({ ts: event.ts, line });
2057
+ if (this.events.length > EVENT_TAIL) this.events.splice(0, this.events.length - EVENT_TAIL);
2170
2058
  }
2171
- this.events.push(line);
2172
- if (this.events.length > EVENT_TAIL) this.events.splice(0, this.events.length - EVENT_TAIL);
2173
2059
  }
2174
2060
  finish(outcome) {
2175
2061
  this.status = "done";
@@ -2359,6 +2245,15 @@ function page(name) {
2359
2245
  (() => {
2360
2246
  const el = (id) => document.getElementById(id);
2361
2247
  const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
2248
+ // What the question is about: the raw input, and a link when it names one,
2249
+ // so a reviewer can open the thing being approved instead of taking the
2250
+ // question's word for it.
2251
+ function aboutHtml(input) {
2252
+ if (input === null || input === undefined) return '';
2253
+ const url = input && typeof input === 'object' && !Array.isArray(input) && typeof input.url === 'string' ? input.url : null;
2254
+ const link = url ? '<p><a href="' + esc(url) + '" target="_blank" rel="noopener">' + esc(url) + '</a></p>' : '';
2255
+ return '<pre>' + esc(JSON.stringify(input, null, 2)) + '</pre>' + link;
2256
+ }
2362
2257
  async function answer(requestId, approved) {
2363
2258
  const box = document.querySelector('[data-note="' + CSS.escape(requestId) + '"]');
2364
2259
  const note = box && box.value ? box.value : '';
@@ -2384,8 +2279,8 @@ function page(name) {
2384
2279
  + '<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
2280
  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
2281
  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');
2282
+ 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('');
2283
+ el('record').textContent = s.events.slice(-40).map((e) => new Date(e.ts).toISOString().slice(11, 19) + ' ' + e.line).join('\\n');
2389
2284
  if (s.status !== 'done') setTimeout(render, 1000);
2390
2285
  }
2391
2286
  render();
@@ -2733,24 +2628,29 @@ function writesOf(config) {
2733
2628
  if (!writes.length || new Set(writes).size !== writes.length) throw new TypeError("writes must contain unique paths");
2734
2629
  return writes;
2735
2630
  }
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;
2631
+ function refineCount(refine, label) {
2632
+ if (refine === void 0) return 0;
2633
+ if (!Number.isSafeInteger(refine) || refine < 0) throw new TypeError(`${label} must be a non-negative integer`);
2634
+ return refine;
2635
+ }
2636
+ function refineCap(refine) {
2637
+ return isJudge(refine) ? refine.cap : refine;
2740
2638
  }
2741
2639
  function optionalFlag(optional) {
2742
2640
  if (optional === void 0) return void 0;
2743
2641
  if (typeof optional !== "boolean") throw new TypeError("optional must be a boolean");
2744
2642
  return optional;
2745
2643
  }
2746
- function retryOf(config) {
2747
- return config.retry === void 0 ? 1 : retryCount(config.retry, "retry");
2644
+ function refineOf(config) {
2645
+ if (config.refine === void 0) return 1;
2646
+ if (isJudge(config.refine)) return config.refine;
2647
+ return refineCount(config.refine, "refine");
2748
2648
  }
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");
2649
+ function refineForStage(config, receivesKickback) {
2650
+ if ("reviewedBy" in config && config.reviewedBy) return refineOf(config);
2651
+ if (receivesKickback) return refineOf(config);
2652
+ if (config.refine !== void 0) {
2653
+ throw new TypeError("refine must be on a reviewed stage or a kickback target");
2754
2654
  }
2755
2655
  return 0;
2756
2656
  }
@@ -3035,6 +2935,62 @@ function unchangedNoteGuard(label, job, writes) {
3035
2935
  return outcome;
3036
2936
  };
3037
2937
  }
2938
+ function lineDiffCount(before, after) {
2939
+ if (before === void 0) return after.split("\n").length;
2940
+ const a = new Set(before.split("\n"));
2941
+ const b = new Set(after.split("\n"));
2942
+ let changed = 0;
2943
+ for (const line of a) if (!b.has(line)) changed += 1;
2944
+ for (const line of b) if (!a.has(line)) changed += 1;
2945
+ return changed;
2946
+ }
2947
+ function judgedReview(brief, config, cfgJudge, panel) {
2948
+ const useCase = /^Use case:\s*([\s\S]*?)\n\s*\n/m.exec(brief.brief)?.[1]?.replace(/\s+/g, " ").trim();
2949
+ const file = writesOf(config)[0];
2950
+ const history = [];
2951
+ let previousDraft;
2952
+ return async (ctx) => {
2953
+ let draft;
2954
+ let changedLines;
2955
+ if (file !== void 0) {
2956
+ try {
2957
+ draft = await readFile(join(ctx.workspace.dir, file), "utf8");
2958
+ changedLines = lineDiffCount(previousDraft, draft);
2959
+ } catch {
2960
+ }
2961
+ previousDraft = draft;
2962
+ }
2963
+ const panelOutcome = await panel({
2964
+ ...ctx,
2965
+ depth: ctx.depth + 1,
2966
+ path: [...ctx.path, "review-panel"]
2967
+ });
2968
+ if (panelOutcome.status === "pass") return panelOutcome;
2969
+ const findings = revisionFromOutcome(panelOutcome)?.findings ?? [];
2970
+ if (hasBlockFinding(findings)) return panelOutcome;
2971
+ const round2 = history.length + 1;
2972
+ const state = {
2973
+ ...useCase !== void 0 ? { useCase } : {},
2974
+ ...file !== void 0 ? { file } : {},
2975
+ ...draft !== void 0 ? { draft } : {},
2976
+ latestFindings: findings,
2977
+ rounds: history,
2978
+ round: round2,
2979
+ cap: cfgJudge.cap
2980
+ };
2981
+ const { decision } = await askJudge(cfgJudge, state, ctx, ctx.path);
2982
+ history.push({
2983
+ round: round2,
2984
+ findings,
2985
+ counts: countBySeverity(findings),
2986
+ ...changedLines !== void 0 ? { changedLines } : {}
2987
+ });
2988
+ if (!decision.again) {
2989
+ return { status: "pass", confidence: panelOutcome.confidence, summary: decision.reason, data: panelOutcome.data };
2990
+ }
2991
+ return { ...panelOutcome, summary: `${panelOutcome.summary} (${decision.reason})` };
2992
+ };
2993
+ }
3038
2994
  function stageJob(brief, named, roles, files, declaredFiles, targetFiles, familyTargetStageNames, familyTargetFamilies) {
3039
2995
  const config = named.config;
3040
2996
  if ("agent" in config && config.agent !== void 0) {
@@ -3044,7 +3000,13 @@ function stageJob(brief, named, roles, files, declaredFiles, targetFiles, family
3044
3000
  if (config.reviewedBy === void 0) return job;
3045
3001
  const reviewers = panelRole(roles, config.reviewedBy);
3046
3002
  const panel = reviewerPanel(brief, named, reviewers, files, declaredFiles, void 0);
3047
- const retry = retryOf(config);
3003
+ const refine = refineOf(config);
3004
+ const cap = refineCap(refine);
3005
+ const review = isJudge(refine) ? judgedReview(brief, config, refine, panel) : async (ctx) => panel({
3006
+ ...ctx,
3007
+ depth: ctx.depth + 1,
3008
+ path: [...ctx.path, "review-panel"]
3009
+ });
3048
3010
  const reviewLoop = loop({
3049
3011
  name: `${named.name}-review`,
3050
3012
  body: job,
@@ -3060,15 +3022,9 @@ function stageJob(brief, named, roles, files, declaredFiles, targetFiles, family
3060
3022
  }
3061
3023
  return true;
3062
3024
  }, `${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,
3025
+ review,
3026
+ max: cap + 1,
3027
+ maxReviewRestarts: cap,
3072
3028
  noProgress: { window: 2, gate: true }
3073
3029
  });
3074
3030
  return copyJobMeta(
@@ -3267,7 +3223,7 @@ function workflow(name, config) {
3267
3223
  if (reviewedBy !== void 0 && stageConfig.sendsBackTo !== void 0) {
3268
3224
  throw new TypeError(`stage ${stageName} cannot use both reviewedBy and sendsBackTo`);
3269
3225
  }
3270
- retryForStage(stageConfig, incomingTargets.has(stageName));
3226
+ refineForStage(stageConfig, incomingTargets.has(stageName));
3271
3227
  optionalFlag(stageConfig.optional);
3272
3228
  if (stageConfig.retrySafe !== void 0 && typeof stageConfig.retrySafe !== "boolean") {
3273
3229
  throw new TypeError("retrySafe must be a boolean");
@@ -3307,10 +3263,9 @@ function workflow(name, config) {
3307
3263
  for (const named of config.stages) {
3308
3264
  if (named.config.sendsBackTo !== void 0) {
3309
3265
  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
- );
3266
+ const refine = refineOf(target.config);
3267
+ const existing = maxKickbacks[named.config.sendsBackTo];
3268
+ maxKickbacks[named.config.sendsBackTo] = isJudge(refine) ? refine : Math.max(isJudge(existing) ? 0 : existing ?? 0, refine);
3314
3269
  }
3315
3270
  }
3316
3271
  const stageJobIdentity = resumeIdentity(workflowName, config);
@@ -9160,7 +9115,7 @@ function pipeline(name, stages, opts) {
9160
9115
  });
9161
9116
  return dag({ name, nodes, ...opts });
9162
9117
  }
9163
- function slug3(s) {
9118
+ function slug2(s) {
9164
9119
  return s.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-+|-+$)/g, "") || "x";
9165
9120
  }
9166
9121
  function tournament(config) {
@@ -9193,7 +9148,7 @@ function tournament(config) {
9193
9148
  Array.from(
9194
9149
  { length: config.n },
9195
9150
  (_, i) => limit2(async () => {
9196
- const branch = `lines/${slug3(config.name)}-cand-${i}`;
9151
+ const branch = `lines/${slug2(config.name)}-cand-${i}`;
9197
9152
  const wt = await addWorktree(base.dir, {
9198
9153
  branch,
9199
9154
  base: "HEAD",
@@ -9271,7 +9226,7 @@ function tournament(config) {
9271
9226
  }
9272
9227
 
9273
9228
  // src/core/isolated.ts
9274
- var slug4 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "job";
9229
+ var slug3 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "job";
9275
9230
  var forkSeq = 0;
9276
9231
  function isolated(job, opts = {}) {
9277
9232
  const label = opts.label ?? "isolated";
@@ -9292,7 +9247,7 @@ function isolated(job, opts = {}) {
9292
9247
  );
9293
9248
  return job(parent);
9294
9249
  }
9295
- const branch = `lines/${slug4(label)}-${forkSeq += 1}`;
9250
+ const branch = `lines/${slug3(label)}-${forkSeq += 1}`;
9296
9251
  const wt = await addWorktree(base.dir, {
9297
9252
  branch,
9298
9253
  base: "HEAD",
@@ -9326,7 +9281,7 @@ function isolated(job, opts = {}) {
9326
9281
  ...outcome.summary === void 0 ? {} : { summary: outcome.summary }
9327
9282
  }) : void 0;
9328
9283
  const sha = await commit(
9329
- message ?? { subject: `chore(${slug4(label)}): worktree changes` },
9284
+ message ?? { subject: `chore(${slug3(label)}): worktree changes` },
9330
9285
  { cwd: wt.dir, signal: parent.signal }
9331
9286
  );
9332
9287
  const merged = await mergeLock(
@@ -9770,6 +9725,6 @@ function defineJob(job) {
9770
9725
  return job;
9771
9726
  }
9772
9727
 
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 };
9728
+ 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
9729
  //# sourceMappingURL=api.js.map
9775
9730
  //# sourceMappingURL=api.js.map