@obversa/runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/dist/api.d.ts +72 -0
  4. package/dist/api.js +9775 -0
  5. package/dist/api.js.map +1 -0
  6. package/dist/artifacts/conformance.d.ts +1 -0
  7. package/dist/artifacts/file-store.d.ts +8 -0
  8. package/dist/artifacts/store.d.ts +1 -0
  9. package/dist/callback/approval.d.ts +8 -0
  10. package/dist/callback/client.d.ts +38 -0
  11. package/dist/callback/gate.d.ts +1 -0
  12. package/dist/callback/stored-client.d.ts +5 -0
  13. package/dist/chunk-3L6YNPN6.js +3 -0
  14. package/dist/chunk-3L6YNPN6.js.map +1 -0
  15. package/dist/chunk-5GLEABOU.js +3 -0
  16. package/dist/chunk-5GLEABOU.js.map +1 -0
  17. package/dist/chunk-DEW5R23M.js +338 -0
  18. package/dist/chunk-DEW5R23M.js.map +1 -0
  19. package/dist/chunk-DV5P4QLI.js +3056 -0
  20. package/dist/chunk-DV5P4QLI.js.map +1 -0
  21. package/dist/chunk-NIBHM5I5.js +34 -0
  22. package/dist/chunk-NIBHM5I5.js.map +1 -0
  23. package/dist/chunk-RZLMX3IA.js +368 -0
  24. package/dist/chunk-RZLMX3IA.js.map +1 -0
  25. package/dist/core/agent-md.d.ts +36 -0
  26. package/dist/core/agent.d.ts +86 -0
  27. package/dist/core/approval-job.d.ts +43 -0
  28. package/dist/core/assert-graph.d.ts +34 -0
  29. package/dist/core/budget.d.ts +49 -0
  30. package/dist/core/concurrency.d.ts +2 -0
  31. package/dist/core/condition.d.ts +209 -0
  32. package/dist/core/context.d.ts +36 -0
  33. package/dist/core/cost.d.ts +59 -0
  34. package/dist/core/dag.d.ts +20 -0
  35. package/dist/core/decision.d.ts +29 -0
  36. package/dist/core/describe.d.ts +56 -0
  37. package/dist/core/engine-meta.d.ts +5 -0
  38. package/dist/core/env-overlay.d.ts +35 -0
  39. package/dist/core/errors.d.ts +46 -0
  40. package/dist/core/feedback.d.ts +68 -0
  41. package/dist/core/git.d.ts +152 -0
  42. package/dist/core/guards.d.ts +70 -0
  43. package/dist/core/isolated.d.ts +40 -0
  44. package/dist/core/job.d.ts +134 -0
  45. package/dist/core/limits.d.ts +22 -0
  46. package/dist/core/loop.d.ts +24 -0
  47. package/dist/core/merge.d.ts +30 -0
  48. package/dist/core/pipeline.d.ts +35 -0
  49. package/dist/core/process.d.ts +11 -0
  50. package/dist/core/progress.d.ts +82 -0
  51. package/dist/core/redact.d.ts +1 -0
  52. package/dist/core/stats.d.ts +63 -0
  53. package/dist/core/team.d.ts +34 -0
  54. package/dist/core/text.d.ts +8 -0
  55. package/dist/core/tournament.d.ts +25 -0
  56. package/dist/core/types.d.ts +650 -0
  57. package/dist/engines/command-runner.d.ts +1 -0
  58. package/dist/engines/conformance.d.ts +1 -0
  59. package/dist/engines/engine.d.ts +4 -0
  60. package/dist/engines/failure.d.ts +1 -0
  61. package/dist/engines/fallback.d.ts +35 -0
  62. package/dist/engines/message-map.d.ts +1 -0
  63. package/dist/engines/mock.d.ts +1 -0
  64. package/dist/engines/preflight.d.ts +36 -0
  65. package/dist/env/command.d.ts +50 -0
  66. package/dist/env/command.js +65 -0
  67. package/dist/env/command.js.map +1 -0
  68. package/dist/env/environment.d.ts +4 -0
  69. package/dist/env/mock.d.ts +24 -0
  70. package/dist/events/conformance.d.ts +1 -0
  71. package/dist/events/envelope.d.ts +1 -0
  72. package/dist/events/jsonl-store.d.ts +8 -0
  73. package/dist/events/store.d.ts +1 -0
  74. package/dist/graph/commands.d.ts +5 -0
  75. package/dist/graph/conformance.d.ts +32 -0
  76. package/dist/graph/kernel.d.ts +5 -0
  77. package/dist/graph/plan.d.ts +1 -0
  78. package/dist/graph/type.d.ts +7 -0
  79. package/dist/graph/value.d.ts +1 -0
  80. package/dist/graph-types/dag.d.ts +137 -0
  81. package/dist/graph-types/loop.d.ts +194 -0
  82. package/dist/graph-types/team.d.ts +68 -0
  83. package/dist/memory.d.ts +82 -0
  84. package/dist/memory.js +397 -0
  85. package/dist/memory.js.map +1 -0
  86. package/dist/proof/acceptance.d.ts +5 -0
  87. package/dist/proof/artifact.d.ts +6 -0
  88. package/dist/proof/cache.d.ts +4 -0
  89. package/dist/runtime/attempt.d.ts +19 -0
  90. package/dist/runtime/budget.d.ts +4 -0
  91. package/dist/runtime/engine-availability.d.ts +18 -0
  92. package/dist/runtime/graph-executor.d.ts +7 -0
  93. package/dist/runtime/monitor.d.ts +80 -0
  94. package/dist/runtime/node-lifecycle.d.ts +84 -0
  95. package/dist/runtime/paths.d.ts +2 -0
  96. package/dist/runtime/persist.d.ts +31 -0
  97. package/dist/runtime/preflight-record.d.ts +149 -0
  98. package/dist/runtime/process-tree.d.ts +1 -0
  99. package/dist/runtime/result-contract.d.ts +4 -0
  100. package/dist/runtime/result-parts.d.ts +1 -0
  101. package/dist/runtime/run-definition.d.ts +24 -0
  102. package/dist/runtime/run-event.d.ts +9 -0
  103. package/dist/runtime/runner.d.ts +139 -0
  104. package/dist/runtime/supervisor.d.ts +111 -0
  105. package/dist/runtime/team-rooms.d.ts +13 -0
  106. package/dist/runtime/workspace-policy.d.ts +25 -0
  107. package/dist/storage/error.d.ts +1 -0
  108. package/dist/storage/id.d.ts +1 -0
  109. package/dist/storage/local.d.ts +12 -0
  110. package/dist/storage/local.js +1492 -0
  111. package/dist/storage/local.js.map +1 -0
  112. package/dist/testing.d.ts +15 -0
  113. package/dist/testing.js +274 -0
  114. package/dist/testing.js.map +1 -0
  115. package/dist/workflow-agent-response.d.ts +3 -0
  116. package/dist/workflow-support.d.ts +36 -0
  117. package/dist/workflow-support.js +5 -0
  118. package/dist/workflow-support.js.map +1 -0
  119. package/dist/workflow.d.ts +73 -0
  120. package/dist/workspace/conformance.d.ts +1 -0
  121. package/dist/workspace/git-provider.d.ts +26 -0
  122. package/dist/workspace/provider.d.ts +2 -0
  123. package/package.json +91 -0
@@ -0,0 +1,3056 @@
1
+ import { runRuntimeProcess, processText } from './chunk-NIBHM5I5.js';
2
+ import { cloneFrozenJson } from './chunk-5GLEABOU.js';
3
+ import { createHash } from 'crypto';
4
+ import { stat, readFile, lstat, readlink, realpath } from 'fs/promises';
5
+ import { relative, resolve, join, isAbsolute } from 'path';
6
+ import { readFileSync, existsSync, mkdtempSync } from 'fs';
7
+ import { setTimeout } from 'timers/promises';
8
+ import { EngineError, requireFinalResultText, createCallbackGate } from '@obversa/api';
9
+ export { callbackRequestDigest, createCallbackGate, engineSelection, finalResultPart, finalResultText, reportedUsage, requireFinalResultText, validateAgentResult, validateCallbackRequest, validateCallbackResponse, validateIncompleteResultEvidence } from '@obversa/api';
10
+ import { scrubCapture, redactEnvValues, redactSecrets } from '@obversa/core/command';
11
+ export { scrubCapture } from '@obversa/core/command';
12
+ import pLimit from 'p-limit';
13
+ import { tmpdir } from 'os';
14
+
15
+ // src/core/describe.ts
16
+ var META = /* @__PURE__ */ new WeakMap();
17
+ var LABEL = /* @__PURE__ */ new WeakMap();
18
+ function setMeta(target, meta) {
19
+ META.set(target, meta);
20
+ return target;
21
+ }
22
+ function jobMeta(job) {
23
+ return typeof job === "function" ? META.get(job) : void 0;
24
+ }
25
+ function copyJobMeta(target, source) {
26
+ const meta = jobMeta(source);
27
+ return meta ? setMeta(target, meta) : target;
28
+ }
29
+ function setLabel(cond, label) {
30
+ LABEL.set(cond, label);
31
+ return cond;
32
+ }
33
+ function condLabel(input) {
34
+ if (typeof input === "function") {
35
+ const l = LABEL.get(input);
36
+ if (l) return l;
37
+ }
38
+ return "check";
39
+ }
40
+ function describeConditions(input) {
41
+ if (input == null) return [];
42
+ if (Array.isArray(input)) return input.flatMap(describeConditions);
43
+ return [condLabel(input)];
44
+ }
45
+ var count = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
46
+ function renderContract(value) {
47
+ const c = value;
48
+ if (!c || typeof c !== "object") return void 0;
49
+ const bits = [];
50
+ if (c.tier) bits.push(`tier ${c.tier}`);
51
+ if (c.outputs?.length) bits.push(`outputs ${c.outputs.join(", ")}`);
52
+ if (c.capabilities?.length) bits.push(`capabilities ${c.capabilities.join(", ")}`);
53
+ if (c.requiresSkills?.length) bits.push(`requires ${c.requiresSkills.join(", ")}`);
54
+ if (c.usesSkills?.length) bits.push(`uses ${c.usesSkills.join(", ")}`);
55
+ if (c.failureModes?.length) bits.push(`failure modes ${c.failureModes.join(", ")}`);
56
+ return bits.join("; ");
57
+ }
58
+ function renderPlan(meta, indent = "") {
59
+ if (!meta) return [`${indent}(a runnable job, shape not introspectable)`];
60
+ const nm = meta.name ? ` "${meta.name}"` : "";
61
+ const out = [];
62
+ switch (meta.kind) {
63
+ case "loop": {
64
+ const caps = [];
65
+ if (typeof meta.max === "number") caps.push(`max ${meta.max}`);
66
+ if (typeof meta.noProgress === "number")
67
+ caps.push(`stall after ${meta.noProgress} flat`);
68
+ if (meta.checkFirst) caps.push("check first");
69
+ const max = caps.length ? ` (${caps.join("; ")})` : "";
70
+ out.push(`${indent}loop${nm}${max}`);
71
+ const start = meta.start;
72
+ const gate = meta.gate;
73
+ const stopOn = meta.stopOn;
74
+ if (start?.length) out.push(`${indent} start: ${start.join(", ")}`);
75
+ if (gate?.length) out.push(`${indent} gate: ${gate.join(", ")}`);
76
+ if (stopOn?.length) out.push(`${indent} stopOn: ${stopOn.join(", ")}`);
77
+ const tail = [meta.review ? "review" : null].filter(Boolean);
78
+ if (tail.length) out.push(`${indent} on convergence: ${tail.join(" + ")}`);
79
+ out.push(`${indent} body:`);
80
+ out.push(...renderPlan(meta.body, `${indent} `));
81
+ break;
82
+ }
83
+ case "dag": {
84
+ const nodes = meta.nodes ?? [];
85
+ out.push(`${indent}dag${nm} (${count(nodes.length, "node")})`);
86
+ const kickbacks = meta.maxKickbacks;
87
+ if (typeof kickbacks === "number") {
88
+ out.push(`${indent} kickbacks: ${kickbacks}`);
89
+ } else if (kickbacks && typeof kickbacks === "object" && !Array.isArray(kickbacks)) {
90
+ const limits = Object.entries(kickbacks).map(([target, limit]) => `${target} ${String(limit)}`).join(", ");
91
+ out.push(`${indent} kickbacks: ${limits}`);
92
+ }
93
+ for (const node of nodes) {
94
+ const bits = [];
95
+ if (node.needs?.length) bits.push(`needs ${node.needs.join(", ")}`);
96
+ if (node.optional) bits.push("optional");
97
+ if (node.when?.length) bits.push(`when: ${node.when.join(", ")}`);
98
+ if (node.isolate) bits.push("isolated");
99
+ out.push(`${indent} - ${node.name}${bits.length ? ` (${bits.join("; ")})` : ""}`);
100
+ if (node.desc) out.push(`${indent} desc: ${node.desc}`);
101
+ if (node.gate) out.push(`${indent} gate: ${node.gate}`);
102
+ out.push(...renderPlan(node.job, `${indent} `));
103
+ }
104
+ break;
105
+ }
106
+ case "agent":
107
+ out.push(`${indent}agent${nm}`);
108
+ {
109
+ const contract = renderContract(meta.contract);
110
+ if (contract) out.push(`${indent} contract: ${contract}`);
111
+ }
112
+ break;
113
+ case "fn":
114
+ out.push(`${indent}fn${nm}`);
115
+ break;
116
+ default:
117
+ out.push(`${indent}${meta.kind}${nm}`);
118
+ }
119
+ return out;
120
+ }
121
+
122
+ // src/core/context.ts
123
+ function criterionFor(ctx) {
124
+ if (ctx.stageGate !== void 0) return ctx.stageGate ?? void 0;
125
+ if (ctx.reviewerGate !== void 0) return ctx.reviewerGate ?? void 0;
126
+ return ctx.graph?.gate;
127
+ }
128
+ function childContext(parent, over) {
129
+ return {
130
+ engine: parent.engine,
131
+ resolveEngine: parent.resolveEngine,
132
+ signal: parent.signal,
133
+ runId: parent.runId,
134
+ fingerprintExcludePaths: parent.fingerprintExcludePaths,
135
+ emit: parent.emit,
136
+ params: parent.params,
137
+ state: parent.state,
138
+ memory: parent.memory,
139
+ callbacks: parent.callbacks,
140
+ onCallback: parent.onCallback,
141
+ // A child inherits the parent's workspace by default; a concurrency
142
+ // boundary forks it into an isolated worktree by passing `workspace`.
143
+ workspace: over.workspace ?? parent.workspace,
144
+ environment: over.environment ?? parent.environment,
145
+ // Inherited, not override-only: pinning deliberately survives the dag
146
+ // worktree boundary, where a node ctx REPLACES `environment` with a
147
+ // per-team handle. An explicit `withEnv` wins over a per-team stack's vars.
148
+ envOverlay: over.envOverlay ?? parent.envOverlay,
149
+ budget: parent.budget,
150
+ onLimit: parent.onLimit,
151
+ maxWaitMs: parent.maxWaitMs,
152
+ log: parent.log,
153
+ depth: over.depth,
154
+ path: over.path,
155
+ graph: over.graph ?? parent.graph,
156
+ reviewerGate: over.reviewerGate !== void 0 ? over.reviewerGate : parent.reviewerGate,
157
+ stageGate: over.stageGate !== void 0 ? over.stageGate : parent.stageGate,
158
+ timeoutMs: over.timeoutMs ?? parent.timeoutMs,
159
+ timeoutGraceMs: over.timeoutGraceMs ?? parent.timeoutGraceMs,
160
+ // Inherit the enclosing iteration by default. A `loop` always passes one
161
+ // explicitly; a `dag`/`sequence` does not, so without this a node nested in a
162
+ // loop would reset to 0, the "Attempt 0" confound where a retry body could not
163
+ // see which attempt it was on. A top-level dag still gets 0 (the root's value).
164
+ iteration: over.iteration ?? parent.iteration,
165
+ lastOutcome: over.lastOutcome,
166
+ lastReview: over.lastReview,
167
+ lastGate: over.lastGate,
168
+ // Not inherited: a nested job sees its own dag node's needs, never an ancestor's.
169
+ needs: over.needs
170
+ };
171
+ }
172
+ var LoopError = class _LoopError extends Error {
173
+ code;
174
+ phase;
175
+ path;
176
+ iteration;
177
+ retryable;
178
+ /** Suggested wait before retry, in ms (e.g. a `retry-after` header). */
179
+ retryAfterMs;
180
+ /** When the limit resets, as epoch ms. The wait policy prefers this. */
181
+ resetAt;
182
+ constructor(init) {
183
+ super(
184
+ init.message,
185
+ init.cause !== void 0 ? { cause: init.cause } : void 0
186
+ );
187
+ this.name = "LoopError";
188
+ this.code = init.code;
189
+ this.phase = init.phase;
190
+ this.path = init.path;
191
+ this.iteration = init.iteration;
192
+ this.retryAfterMs = init.retryAfterMs;
193
+ this.resetAt = init.resetAt;
194
+ this.retryable = init.retryable ?? defaultRetryable(init);
195
+ }
196
+ /** Wrap an arbitrary thrown value, preserving a `LoopError` as-is. */
197
+ static from(value, fallback) {
198
+ if (value instanceof _LoopError) return value;
199
+ if (value instanceof EngineError) {
200
+ return new _LoopError({
201
+ ...fallback,
202
+ code: loopCodeForEngineFailure(value.kind, fallback.code),
203
+ message: value.message,
204
+ cause: value,
205
+ retryAfterMs: value.retryAfterMs,
206
+ resetAt: value.resetAt
207
+ });
208
+ }
209
+ const message = value instanceof Error ? value.message : String(value);
210
+ return new _LoopError({ ...fallback, message, cause: value });
211
+ }
212
+ toJSON() {
213
+ return {
214
+ name: this.name,
215
+ code: this.code,
216
+ message: this.message,
217
+ phase: this.phase,
218
+ path: this.path,
219
+ iteration: this.iteration,
220
+ retryable: this.retryable,
221
+ retryAfterMs: this.retryAfterMs,
222
+ resetAt: this.resetAt
223
+ };
224
+ }
225
+ };
226
+ function loopCodeForEngineFailure(kind, fallback) {
227
+ switch (kind) {
228
+ case "rate-limit":
229
+ return "RATE_LIMIT";
230
+ case "quota":
231
+ return "QUOTA";
232
+ case "timeout":
233
+ return "TIMEOUT";
234
+ case "aborted":
235
+ return "ABORTED";
236
+ case "invalid-config":
237
+ return "CONFIG";
238
+ default:
239
+ return fallback;
240
+ }
241
+ }
242
+ var INFRASTRUCTURE_ERROR_CODES = /* @__PURE__ */ new Set([
243
+ "ENGINE",
244
+ "TIMEOUT",
245
+ "RATE_LIMIT",
246
+ "QUOTA",
247
+ "BUDGET"
248
+ ]);
249
+ function isInfrastructureError(error) {
250
+ return error instanceof LoopError && INFRASTRUCTURE_ERROR_CODES.has(error.code);
251
+ }
252
+ function defaultRetryable(init) {
253
+ switch (init.code) {
254
+ case "ENGINE":
255
+ case "TIMEOUT":
256
+ case "RATE_LIMIT":
257
+ return true;
258
+ case "QUOTA":
259
+ return init.resetAt != null || init.retryAfterMs != null;
260
+ default:
261
+ return false;
262
+ }
263
+ }
264
+
265
+ // src/core/approval-job.ts
266
+ var NODE_JOBS = /* @__PURE__ */ new WeakMap();
267
+ function nodeJobContext(ctx, job) {
268
+ const scoped = { ...ctx };
269
+ if (job !== void 0) NODE_JOBS.set(scoped, job);
270
+ return scoped;
271
+ }
272
+ function delegateNodeJob(ctx, owner, job, child) {
273
+ return job(nodeJobContext(child, NODE_JOBS.get(ctx) === owner ? job : void 0));
274
+ }
275
+ var RESPONSE_SCHEMA = {
276
+ type: "object",
277
+ properties: { approved: { type: "boolean" }, note: { type: "string" } },
278
+ required: ["approved"]
279
+ };
280
+ function aboutOf(ctx) {
281
+ if (ctx.needs !== void 0) {
282
+ return Object.fromEntries(
283
+ Object.entries(ctx.needs).map(([name, outcome]) => [name, outcome.summary ?? outcome.status])
284
+ );
285
+ }
286
+ return ctx.lastOutcome?.summary ?? null;
287
+ }
288
+ function toAnswer(response) {
289
+ const record = response !== null && typeof response === "object" && !Array.isArray(response) ? response : void 0;
290
+ if (record === void 0 || typeof record.approved !== "boolean") {
291
+ throw new LoopError({ code: "VALIDATION", message: 'an approval answer needs a boolean "approved"' });
292
+ }
293
+ return {
294
+ approved: record.approved,
295
+ ...typeof record.note === "string" ? { note: record.note } : {}
296
+ };
297
+ }
298
+ function stateOf(events) {
299
+ let state = "absent";
300
+ let answer;
301
+ for (const event of events) {
302
+ switch (event.kind) {
303
+ case "callback-requested":
304
+ state = "pending";
305
+ break;
306
+ case "callback-claimed":
307
+ state = "claimed";
308
+ break;
309
+ case "callback-released":
310
+ state = "pending";
311
+ break;
312
+ case "callback-submitted":
313
+ state = "answered";
314
+ answer = toAnswer(event.response);
315
+ break;
316
+ case "callback-superseded":
317
+ state = "superseded";
318
+ break;
319
+ }
320
+ }
321
+ return answer === void 0 ? { state } : { state, answer };
322
+ }
323
+ function approval(label, opts) {
324
+ const job = async (ctx) => {
325
+ const path = [...ctx.path];
326
+ ctx.emit({ kind: "job:start", ts: Date.now(), path, label, timeoutMs: ctx.timeoutMs });
327
+ let outcome;
328
+ try {
329
+ outcome = await decide(ctx, label, opts, NODE_JOBS.get(ctx) === job);
330
+ } catch (e) {
331
+ const error = LoopError.from(e, { code: "BODY", phase: "body", path: ctx.path, iteration: ctx.iteration });
332
+ outcome = { status: "fail", summary: error.message, error };
333
+ ctx.emit({ kind: "error", ts: Date.now(), path, message: error.message, code: error.code });
334
+ }
335
+ ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
336
+ return outcome;
337
+ };
338
+ return setMeta(job, {
339
+ kind: "approval",
340
+ name: label,
341
+ question: opts.question,
342
+ ...opts.target !== void 0 ? { target: opts.target } : {}
343
+ });
344
+ }
345
+ async function decide(ctx, label, opts, ownsNode) {
346
+ const client = ctx.callbacks;
347
+ if (client === void 0) {
348
+ throw new LoopError({
349
+ code: "VALIDATION",
350
+ message: `approval "${label}" needs the run's callbacks client; run it through run()`
351
+ });
352
+ }
353
+ const request = createCallbackGate({
354
+ gateId: label,
355
+ gateVersion: 1,
356
+ decisionText: opts.question,
357
+ responseSchema: RESPONSE_SCHEMA,
358
+ input: opts.input ?? aboutOf(ctx)
359
+ });
360
+ let { state, answer } = stateOf(await client.history(request.requestId));
361
+ if (answer === void 0) {
362
+ await client.post(request);
363
+ ({ state, answer } = stateOf(await client.history(request.requestId)));
364
+ if (answer === void 0 && state !== "pending" && state !== "claimed") {
365
+ return {
366
+ status: "fail",
367
+ summary: `the question "${opts.question}" is not pending and not answered after it was asked (${state}); nobody can answer it`,
368
+ data: request
369
+ };
370
+ }
371
+ }
372
+ if (answer === void 0 && opts.answer !== void 0) {
373
+ answer = await answerInProcess(client, request, `${label}:answer`, opts.answer);
374
+ }
375
+ const paused = { status: "paused", summary: `waiting for a person: ${opts.question}`, data: request };
376
+ if (ownsNode && answer === void 0 && ctx.onCallback === "wait" && !ctx.signal.aborted && ctx.graph !== void 0) {
377
+ ctx.emit({
378
+ kind: "dag:node",
379
+ ts: Date.now(),
380
+ path: ctx.graph.path.slice(0, -1),
381
+ node: ctx.graph.node,
382
+ phase: "done",
383
+ attempt: ctx.graph.attempt,
384
+ needs: [...ctx.graph.needs],
385
+ ...ctx.graph.desc === void 0 ? {} : { desc: ctx.graph.desc },
386
+ ...ctx.graph.gate === void 0 ? {} : { gate: ctx.graph.gate },
387
+ timeoutMs: ctx.timeoutMs,
388
+ outcome: paused
389
+ });
390
+ }
391
+ while (answer === void 0 && ctx.onCallback === "wait" && !ctx.signal.aborted && (state === "pending" || state === "claimed")) {
392
+ try {
393
+ await setTimeout(1e3, void 0, { signal: ctx.signal });
394
+ } catch (error) {
395
+ if (!ctx.signal.aborted) throw error;
396
+ break;
397
+ }
398
+ ({ state, answer } = stateOf(await client.history(request.requestId)));
399
+ }
400
+ if (answer === void 0) {
401
+ return paused;
402
+ }
403
+ if (answer.approved) {
404
+ const accepted = {
405
+ approved: true,
406
+ ...typeof answer.note === "string" && answer.note !== "" ? { note: redactSecrets(answer.note) } : {}
407
+ };
408
+ return { status: "pass", summary: `approved: ${opts.question}`, data: accepted };
409
+ }
410
+ const note = typeof answer.note === "string" && answer.note !== "" ? redactSecrets(answer.note) : `refused: ${opts.question}`;
411
+ const scrubbed = { approved: false, note };
412
+ if (opts.target === void 0) {
413
+ return { status: "fail", summary: note, data: scrubbed };
414
+ }
415
+ return {
416
+ status: "fail",
417
+ summary: note,
418
+ data: scrubbed,
419
+ revision: {
420
+ target: opts.target,
421
+ reason: note,
422
+ findings: [{ evidence: note, severity: "block" }],
423
+ rerun: "target-and-dependents"
424
+ }
425
+ };
426
+ }
427
+ async function answerInProcess(client, request, routerId, respond) {
428
+ const claim = await client.claim(request.requestId, routerId);
429
+ if (!claim.ok) {
430
+ throw new LoopError({
431
+ code: "VALIDATION",
432
+ message: `the question "${request.decisionText}" could not be claimed to answer it (${claim.kind})`
433
+ });
434
+ }
435
+ let response;
436
+ try {
437
+ response = await respond(request);
438
+ } catch (error) {
439
+ await client.release(request.requestId, claim.claimToken);
440
+ throw error;
441
+ }
442
+ const submission = {
443
+ approved: response.approved,
444
+ ...typeof response.note === "string" ? { note: response.note } : {}
445
+ };
446
+ let submitted;
447
+ try {
448
+ submitted = await client.submit(
449
+ request.requestId,
450
+ claim.claimToken,
451
+ routerId,
452
+ request.digest,
453
+ submission
454
+ );
455
+ } catch (error) {
456
+ await client.release(request.requestId, claim.claimToken);
457
+ throw error;
458
+ }
459
+ if (!submitted.ok) {
460
+ await client.release(request.requestId, claim.claimToken);
461
+ throw new LoopError({
462
+ code: "VALIDATION",
463
+ message: `the answer to "${request.decisionText}" was refused: ${submitted.reason}`
464
+ });
465
+ }
466
+ return toAnswer(submitted.response);
467
+ }
468
+
469
+ // src/core/env-overlay.ts
470
+ function mergeEnv(...layers) {
471
+ let merged;
472
+ for (const layer of layers) {
473
+ if (!layer) continue;
474
+ for (const [k, v] of Object.entries(layer)) {
475
+ (merged ??= /* @__PURE__ */ Object.create(null))[k] = v;
476
+ }
477
+ }
478
+ return merged;
479
+ }
480
+ function resolveEnv(ctx, perCall) {
481
+ return mergeEnv(ctx.environment?.env, ctx.envOverlay, perCall);
482
+ }
483
+ function withEnv(overlay, job) {
484
+ if (!overlay || typeof overlay !== "object" || Array.isArray(overlay)) {
485
+ throw new LoopError({
486
+ code: "CONFIG",
487
+ message: `withEnv requires a plain object of string values (got ${Array.isArray(overlay) ? "an array" : typeof overlay})`
488
+ });
489
+ }
490
+ for (const [k, v] of Object.entries(overlay)) {
491
+ if (!k || k.includes("=")) {
492
+ throw new LoopError({
493
+ code: "CONFIG",
494
+ message: `withEnv key ${JSON.stringify(k)} is not a valid env var name (empty, or contains "=")`
495
+ });
496
+ }
497
+ if (typeof v !== "string") {
498
+ throw new LoopError({
499
+ code: "CONFIG",
500
+ message: `withEnv value for "${k}" must be a string (got ${typeof v})`
501
+ });
502
+ }
503
+ }
504
+ const wrapper = (ctx) => delegateNodeJob(
505
+ ctx,
506
+ wrapper,
507
+ job,
508
+ // Transparent: same depth and path (no new tree segment, no events of
509
+ // its own), and the loop-feedback fields are carried through so wrapping
510
+ // a loop body does not hide the previous iteration from it.
511
+ childContext(ctx, {
512
+ depth: ctx.depth,
513
+ path: ctx.path,
514
+ lastOutcome: ctx.lastOutcome,
515
+ lastReview: ctx.lastReview,
516
+ lastGate: ctx.lastGate,
517
+ envOverlay: { ...ctx.envOverlay, ...overlay }
518
+ })
519
+ );
520
+ const meta = jobMeta(job);
521
+ return meta ? setMeta(wrapper, meta) : wrapper;
522
+ }
523
+
524
+ // src/core/budget.ts
525
+ var Budget = class {
526
+ limit;
527
+ headroom;
528
+ soft;
529
+ tokens = 0;
530
+ unknownCalls = 0;
531
+ constructor(config) {
532
+ this.limit = config.limit;
533
+ this.headroom = config.headroom ?? 0;
534
+ this.soft = config.soft ?? false;
535
+ }
536
+ /** Record consumed tokens. Non-finite or non-positive values are ignored. */
537
+ add(tokens) {
538
+ if (Number.isFinite(tokens) && tokens > 0) this.tokens += tokens;
539
+ }
540
+ addUsage(usage) {
541
+ if (usage.kind === "unknown") {
542
+ this.unknownCalls += 1;
543
+ return;
544
+ }
545
+ this.add(usage.inputTokens + usage.outputTokens);
546
+ }
547
+ unknownUsageCalls() {
548
+ return this.unknownCalls;
549
+ }
550
+ spent() {
551
+ return this.tokens;
552
+ }
553
+ remaining() {
554
+ return Math.max(0, this.limit - this.tokens);
555
+ }
556
+ /** True once the next call would breach the cap (accounting for headroom). */
557
+ exceeded() {
558
+ return this.tokens + this.headroom >= this.limit;
559
+ }
560
+ };
561
+ function assertBudget(ctx) {
562
+ const budget = ctx.budget;
563
+ if (!budget) return;
564
+ if (budget.unknownUsageCalls() > 0) {
565
+ if (budget.soft) {
566
+ ctx.log(
567
+ `token usage is unknown for ${budget.unknownUsageCalls()} call(s) \u2014 continuing (soft)`,
568
+ "warn"
569
+ );
570
+ return;
571
+ }
572
+ throw new LoopError({
573
+ code: "BUDGET",
574
+ phase: "engine",
575
+ message: `token budget cannot continue: usage is unknown for ${budget.unknownUsageCalls()} call(s)`
576
+ });
577
+ }
578
+ if (!budget.exceeded()) return;
579
+ if (budget.soft) {
580
+ ctx.log(
581
+ `token budget reached (${budget.spent()}/${budget.limit}) \u2014 continuing (soft)`,
582
+ "warn"
583
+ );
584
+ return;
585
+ }
586
+ throw new LoopError({
587
+ code: "BUDGET",
588
+ phase: "engine",
589
+ message: `token budget exhausted: ${budget.spent()}/${budget.limit} tokens spent`
590
+ });
591
+ }
592
+ function fromFile(path) {
593
+ return readFileSync(path, "utf8").trim();
594
+ }
595
+ function defineSkill(skill) {
596
+ if (!skill.name) throw new Error("defineSkill: `name` is required");
597
+ if (!skill.instructions?.trim()) throw new Error(`defineSkill "${skill.name}": empty instructions`);
598
+ return skill;
599
+ }
600
+ function skillRefName(ref) {
601
+ return typeof ref === "string" ? ref : ref.name;
602
+ }
603
+ function validateName(value, label) {
604
+ if (!value?.trim()) throw new Error(`${label}: \`name\` is required`);
605
+ }
606
+ function validateSkillRef(ref, label) {
607
+ if (typeof ref === "string") {
608
+ if (!ref.trim()) throw new Error(`${label}: empty skill name`);
609
+ return;
610
+ }
611
+ defineSkill(ref);
612
+ }
613
+ function defineAgent(def) {
614
+ if (!def.name) throw new Error("defineAgent: `name` is required");
615
+ if (!def.system?.trim()) throw new Error(`defineAgent "${def.name}": empty system prompt`);
616
+ def.skills?.forEach((s) => defineSkill(s));
617
+ def.requiresSkills?.forEach(
618
+ (s) => validateSkillRef(s, `defineAgent "${def.name}" requiresSkills`)
619
+ );
620
+ def.usesSkills?.forEach(
621
+ (s) => validateSkillRef(s, `defineAgent "${def.name}" usesSkills`)
622
+ );
623
+ def.outputs?.forEach(
624
+ (o) => validateName(o.name, `defineAgent "${def.name}" outputs`)
625
+ );
626
+ def.failureModes?.forEach((f) => {
627
+ if (!f.mode?.trim())
628
+ throw new Error(`defineAgent "${def.name}" failureModes: \`mode\` is required`);
629
+ if (!f.recovery?.trim())
630
+ throw new Error(`defineAgent "${def.name}" failureModes "${f.mode}": \`recovery\` is required`);
631
+ });
632
+ return def;
633
+ }
634
+ function agentContract(agent) {
635
+ if (!agent) return void 0;
636
+ const summary = {};
637
+ if (agent.tier) summary.tier = agent.tier;
638
+ if (agent.capabilities?.length) summary.capabilities = [...agent.capabilities];
639
+ if (agent.outputs?.length) summary.outputs = agent.outputs.map((o) => o.name);
640
+ if (agent.requiresSkills?.length)
641
+ summary.requiresSkills = agent.requiresSkills.map(skillRefName);
642
+ if (agent.usesSkills?.length)
643
+ summary.usesSkills = agent.usesSkills.map(skillRefName);
644
+ if (agent.failureModes?.length)
645
+ summary.failureModes = agent.failureModes.map((f) => f.mode);
646
+ return Object.keys(summary).length ? summary : void 0;
647
+ }
648
+ function resolveSystem(agent) {
649
+ if (!agent.skills?.length) return agent.system;
650
+ const methods = agent.skills.map((s) => `### ${s.name}
651
+
652
+ ${s.instructions.trim()}`).join("\n\n");
653
+ return `${agent.system.trim()}
654
+
655
+ ## Methodologies you apply
656
+
657
+ ${methods}`;
658
+ }
659
+
660
+ // src/core/text.ts
661
+ function oneLine(text) {
662
+ return text.replace(/\s+/g, " ").trim();
663
+ }
664
+ function truncate(s, max) {
665
+ if (s.length <= max) return s;
666
+ const marker = max > 1 ? "\n\u2026" : "\u2026".slice(0, Math.max(0, max));
667
+ return `${s.slice(0, Math.max(0, max - marker.length)).trimEnd()}${marker}`;
668
+ }
669
+
670
+ // src/core/engine-meta.ts
671
+ function leafId(ctx, label) {
672
+ const raw = [...ctx.path, label, String(ctx.iteration)].join("/");
673
+ return raw.replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/(^-+|-+$)/g, "") || "leaf";
674
+ }
675
+ function attemptRequestMeta(ctx, label) {
676
+ return {
677
+ leaf: true,
678
+ runId: ctx.runId,
679
+ leafId: leafId(ctx, label),
680
+ path: [...ctx.path],
681
+ label,
682
+ iteration: ctx.iteration
683
+ };
684
+ }
685
+ function logEngineTransportFailure(ctx, result, env) {
686
+ if (!result.transportFailure) return;
687
+ ctx.log(scrubCapture(result.transportFailure.message, env, 1e3), "warn");
688
+ }
689
+
690
+ // src/core/condition.ts
691
+ var COMMAND_FAILURE_TAIL_MAX = 3e3;
692
+ var COMMAND_FAILURE_TAIL_MARKER = "\u2026 [output truncated]\n";
693
+ function commandFailureTail(text, env) {
694
+ const scrubbed = scrubCapture(text, env);
695
+ if (scrubbed.length <= COMMAND_FAILURE_TAIL_MAX) return scrubbed;
696
+ return COMMAND_FAILURE_TAIL_MARKER + scrubbed.slice(
697
+ -(COMMAND_FAILURE_TAIL_MAX - COMMAND_FAILURE_TAIL_MARKER.length)
698
+ );
699
+ }
700
+ function toCondition(input, combine = "all") {
701
+ if (Array.isArray(input)) {
702
+ const conds = input.map((i) => toCondition(i, combine));
703
+ return combine === "any" ? any(...conds) : all(...conds);
704
+ }
705
+ return coerceOne(input);
706
+ }
707
+ var conditionPreparations = /* @__PURE__ */ new WeakMap();
708
+ function withConditionPreparation(condition, prepare) {
709
+ conditionPreparations.set(condition, prepare);
710
+ return condition;
711
+ }
712
+ async function prepareCondition(input, ctx, combine = "all") {
713
+ if (Array.isArray(input)) {
714
+ const prepared = await Promise.all(
715
+ input.map((item) => prepareCondition(item, ctx, combine))
716
+ );
717
+ return combine === "any" ? any(...prepared) : all(...prepared);
718
+ }
719
+ const prepare = conditionPreparations.get(input);
720
+ return prepare ? prepare(ctx) : toCondition(input, combine);
721
+ }
722
+ function coerceOne(fn) {
723
+ return async (ctx, last) => {
724
+ const r = await fn(
725
+ ctx,
726
+ last
727
+ );
728
+ if (typeof r === "boolean") {
729
+ return { met: r, reason: `predicate: ${r}` };
730
+ }
731
+ if (r && typeof r === "object" && "met" in r) {
732
+ const res = r;
733
+ if (typeof res.met !== "boolean") {
734
+ throw new LoopError({
735
+ code: "VALIDATION",
736
+ message: `condition returned a non-boolean "met": ${String(res.met)}`
737
+ });
738
+ }
739
+ return r;
740
+ }
741
+ return { met: Boolean(r), reason: `coerced: ${String(r)}` };
742
+ };
743
+ }
744
+ function predicate(fn, reason = "predicate") {
745
+ return async (ctx, last) => {
746
+ const met = await fn(ctx, last);
747
+ return { met, reason: met ? `${reason}: true` : `${reason}: false` };
748
+ };
749
+ }
750
+ function bodyPassed() {
751
+ return async (_ctx, last) => ({
752
+ met: last?.status === "pass",
753
+ confidence: last?.confidence,
754
+ reason: `last body status = ${last?.status ?? "none"}`
755
+ });
756
+ }
757
+ function minConfidence(threshold) {
758
+ return async (_ctx, last) => {
759
+ const c = last?.confidence ?? 0;
760
+ return {
761
+ met: c >= threshold,
762
+ confidence: c,
763
+ reason: `confidence ${c.toFixed(2)} ${c >= threshold ? ">=" : "<"} ${threshold}`
764
+ };
765
+ };
766
+ }
767
+ function commandSucceeds(command, args = [], opts = {}) {
768
+ return setLabel(async (ctx) => {
769
+ try {
770
+ const env = resolveEnv(ctx, opts.env);
771
+ const r = await runRuntimeProcess({
772
+ executable: command,
773
+ args,
774
+ cwd: opts.cwd ?? ctx.workspace.dir,
775
+ timeoutMs: opts.timeoutMs,
776
+ signal: ctx.signal,
777
+ env
778
+ });
779
+ const stdout = processText(r.stdout);
780
+ const stderr = processText(r.stderr);
781
+ const all2 = `${stdout}${stderr}`;
782
+ if (r.exitCode === 0 && !r.timedOut) {
783
+ return { met: true, reason: `\`${command}\` exited 0` };
784
+ }
785
+ const baseReason = r.timedOut ? `\`${command}\` timed out after ${opts.timeoutMs} ms` : `\`${command}\` exited ${r.exitCode ?? "?"}`;
786
+ const tail = opts.captureOutput ? commandFailureTail(all2, env) : "";
787
+ return {
788
+ met: false,
789
+ reason: tail ? `${baseReason}
790
+
791
+ command output (tail):
792
+ ${tail}` : baseReason,
793
+ // The evidence behind the failure, in the same shape `reviewContext`
794
+ // gives a judge. Scrubbed (`scrubCapture`) because it flows into JSONL
795
+ // event records — a failing command often echoes its config, and a
796
+ // pinned credential's shape is unknowable to pattern scrubbing.
797
+ output: `exit: ${r.exitCode ?? "(command did not run)"}
798
+
799
+ stdout:
800
+ ${scrubCapture(stdout, env, 4e3)}
801
+
802
+ stderr:
803
+ ${scrubCapture(stderr, env, 4e3)}`
804
+ };
805
+ } catch (e) {
806
+ return {
807
+ met: false,
808
+ reason: `\`${command}\` failed to run: ${e instanceof Error ? e.message : String(e)}`
809
+ };
810
+ }
811
+ }, `${command}${args.length ? ` ${args.join(" ")}` : ""}`);
812
+ }
813
+ var always = async () => ({ met: true, reason: "always" });
814
+ var never = async () => ({ met: false, reason: "never" });
815
+ function not(c) {
816
+ const cond = toCondition(c);
817
+ const condition = async (ctx, last) => {
818
+ const r = await cond(ctx, last);
819
+ return {
820
+ met: !r.met,
821
+ confidence: r.confidence,
822
+ reason: `not(${r.reason})`,
823
+ output: r.output
824
+ };
825
+ };
826
+ setMeta(condition, { kind: "condition", name: "not", inputs: [c] });
827
+ return withConditionPreparation(
828
+ condition,
829
+ async (ctx) => not(await prepareCondition(c, ctx))
830
+ );
831
+ }
832
+ function all(...inputs) {
833
+ const conds = inputs.map((i) => toCondition(i));
834
+ const condition = async (ctx, last) => {
835
+ const results = [];
836
+ for (const c of conds) {
837
+ const r = await c(ctx, last);
838
+ results.push(r);
839
+ if (!r.met)
840
+ return {
841
+ met: false,
842
+ reason: `all -> failed: ${r.reason}`,
843
+ output: r.output
844
+ };
845
+ }
846
+ return {
847
+ met: true,
848
+ reason: `all(${results.map((r) => r.reason).join(" & ")})`
849
+ };
850
+ };
851
+ setMeta(condition, { kind: "condition", name: "all", inputs });
852
+ return withConditionPreparation(
853
+ condition,
854
+ async (ctx) => all(...await Promise.all(inputs.map((input) => prepareCondition(input, ctx))))
855
+ );
856
+ }
857
+ function any(...inputs) {
858
+ const conds = inputs.map((i) => toCondition(i));
859
+ const condition = async (ctx, last) => {
860
+ const reasons = [];
861
+ let output;
862
+ for (const c of conds) {
863
+ const r = await c(ctx, last);
864
+ reasons.push(r.reason);
865
+ if (r.met)
866
+ return {
867
+ met: true,
868
+ confidence: r.confidence,
869
+ reason: `any -> ${r.reason}`,
870
+ output: r.output
871
+ };
872
+ output ??= r.output;
873
+ }
874
+ return { met: false, reason: `any(${reasons.join(" | ")})`, output };
875
+ };
876
+ setMeta(condition, { kind: "condition", name: "any", inputs });
877
+ return withConditionPreparation(
878
+ condition,
879
+ async (ctx) => any(...await Promise.all(inputs.map((input) => prepareCondition(input, ctx))))
880
+ );
881
+ }
882
+ function quorum(k, ...inputs) {
883
+ if (k < 1 || k > inputs.length)
884
+ throw new LoopError({
885
+ code: "CONFIG",
886
+ message: `quorum requires 1 <= k <= inputs (got k=${k}, n=${inputs.length})`
887
+ });
888
+ const conds = inputs.map((i) => toCondition(i));
889
+ const condition = setLabel(async (ctx, last) => {
890
+ const settled = await Promise.allSettled(conds.map((c) => c(ctx, last)));
891
+ const engineErrors = settled.filter(
892
+ (s) => s.status === "rejected" && isInfrastructureError(s.reason)
893
+ ).map((s) => s.reason);
894
+ const results = settled.map((s) => {
895
+ if (s.status === "fulfilled") return s.value;
896
+ return {
897
+ met: false,
898
+ reason: `judge errored: ${s.reason instanceof Error ? s.reason.message : String(s.reason)}`
899
+ };
900
+ });
901
+ const held = results.filter((r) => r.met);
902
+ const output = results.find((r) => !r.met && r.output !== void 0)?.output;
903
+ if (held.length < k && held.length + engineErrors.length >= k) {
904
+ const first = engineErrors[0];
905
+ if (!output) throw first;
906
+ throw new LoopError({
907
+ code: first.code,
908
+ message: first.message,
909
+ phase: first.phase,
910
+ path: first.path,
911
+ iteration: first.iteration,
912
+ retryable: first.retryable,
913
+ retryAfterMs: first.retryAfterMs,
914
+ resetAt: first.resetAt,
915
+ cause: { error: first, output }
916
+ });
917
+ }
918
+ const confs = held.map((r) => r.confidence).filter((c) => typeof c === "number");
919
+ const confidence = confs.length ? confs.reduce((a, b) => a + b, 0) / confs.length : void 0;
920
+ const met = held.length >= k;
921
+ return {
922
+ met,
923
+ confidence,
924
+ reason: `quorum ${held.length}/${inputs.length} held (need ${k})`,
925
+ output: met ? void 0 : output
926
+ };
927
+ }, `quorum ${k}/${inputs.length}`);
928
+ setMeta(condition, { kind: "condition", name: "quorum", inputs });
929
+ return withConditionPreparation(
930
+ condition,
931
+ async (ctx) => quorum(
932
+ k,
933
+ ...await Promise.all(inputs.map((input) => prepareCondition(input, ctx)))
934
+ )
935
+ );
936
+ }
937
+ function defaultContext(ctx, last) {
938
+ const parts = [];
939
+ if (last?.summary) parts.push(`Last outcome summary: ${last.summary}`);
940
+ if (last?.status) parts.push(`Last outcome status: ${last.status}`);
941
+ if (last?.data !== void 0)
942
+ parts.push(`Last outcome data: ${safeJson(last.data)}`);
943
+ const stateKeys = Object.keys(ctx.state);
944
+ if (stateKeys.length) parts.push(`Shared state: ${safeJson(ctx.state)}`);
945
+ return parts.join("\n") || "(no prior context)";
946
+ }
947
+ function safeJson(value, limit = 4e3) {
948
+ try {
949
+ const s = JSON.stringify(value, null, 2) ?? String(value);
950
+ return s.length > limit ? `${s.slice(0, limit)}\u2026 (truncated)` : s;
951
+ } catch {
952
+ return String(value);
953
+ }
954
+ }
955
+ function* balancedObjects(text) {
956
+ let cursor = 0;
957
+ while (cursor < text.length) {
958
+ const start = text.indexOf("{", cursor);
959
+ if (start === -1) return;
960
+ let depth = 0;
961
+ let inString = false;
962
+ let escaped = false;
963
+ let end = -1;
964
+ for (let i = start; i < text.length; i += 1) {
965
+ const ch = text[i];
966
+ if (inString) {
967
+ if (escaped) escaped = false;
968
+ else if (ch === "\\") escaped = true;
969
+ else if (ch === '"') inString = false;
970
+ continue;
971
+ }
972
+ if (ch === '"') inString = true;
973
+ else if (ch === "{") depth += 1;
974
+ else if (ch === "}" && --depth === 0) {
975
+ end = i;
976
+ break;
977
+ }
978
+ }
979
+ if (end === -1) return;
980
+ yield text.slice(start, end + 1);
981
+ cursor = end + 1;
982
+ }
983
+ }
984
+ function toVerdict(obj) {
985
+ const verdict = obj.verdict === "yes" ? "yes" : "no";
986
+ const confidence = typeof obj.confidence === "number" ? clamp01(obj.confidence) : 0;
987
+ const reason = typeof obj.reason === "string" ? obj.reason : "(no reason given)";
988
+ return { verdict, confidence, reason };
989
+ }
990
+ function parseVerdict(text) {
991
+ let fallback;
992
+ for (const candidate of balancedObjects(text)) {
993
+ let parsed;
994
+ try {
995
+ parsed = JSON.parse(candidate);
996
+ } catch {
997
+ continue;
998
+ }
999
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1000
+ continue;
1001
+ const obj = parsed;
1002
+ if ("verdict" in obj) return toVerdict(obj);
1003
+ fallback ??= obj;
1004
+ }
1005
+ if (fallback) return toVerdict(fallback);
1006
+ throw new LoopError({
1007
+ code: "VALIDATION",
1008
+ message: `validator returned no JSON verdict: ${text.slice(0, 200)}`
1009
+ });
1010
+ }
1011
+ function clamp01(n) {
1012
+ return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0;
1013
+ }
1014
+ var VALIDATOR_SYSTEM = 'You are a strict, sceptical evaluator. You judge whether a stated condition is truly met given the evidence. Do not be generous. The `confidence` field is MANDATORY: always include a number in 0..1 for how sure you are, and when in doubt give a LOW number \u2014 never omit it (an omitted confidence is treated as zero). Respond with ONLY a single JSON object and no other text:\n{"verdict":"yes"|"no","confidence":<number 0..1>,"reason":"<one sentence>"}';
1015
+ var CONFIDENCE_TAG_SYSTEM = "You are a rigorous, report-only reviewer. Do not edit anything and do not imply you will. Assess the evidence against the stated condition, listing each concern tied to a concrete location and a concrete failure scenario (not a vibe). Judge against the stated contract, not an ideal: do not penalise the absence of hardening the contract does not require, and when the evidence meets the contract and you cannot name a concrete fault, say so plainly. Close with a single final line and nothing after it: `<confidence>N%</confidence>` \u2014 N is an integer 0-100, where 100 means you found no genuine contract violation or real bug, and below 100 means at least one concrete, addressable concern is open.";
1016
+ function parseConfidenceTag(text) {
1017
+ const re = /<confidence>\s*([0-9]+(?:\.[0-9]+)?)\s*%?\s*<\/confidence>/gi;
1018
+ let m;
1019
+ let last = null;
1020
+ while ((m = re.exec(text)) !== null) last = m;
1021
+ if (!last) return null;
1022
+ let n = parseFloat(last[1]);
1023
+ if (n > 1) n = n / 100;
1024
+ return { confidence: clamp01(n), findings: text.slice(0, last.index).trim() };
1025
+ }
1026
+ function validatorScoreSystem(dimensions) {
1027
+ return `You are a strict, sceptical evaluator. Score how well the condition is met on EACH named dimension, from 0 (not at all) to 1 (fully). Do not be generous; when in doubt score low. Respond with ONLY a single JSON object and no other text:
1028
+ {"scores":{${dimensions.map((d) => `"${d}":<0..1>`).join(",")}},"reason":"<one sentence>"}`;
1029
+ }
1030
+ function geometricMean(values) {
1031
+ if (!values.length) return 0;
1032
+ if (values.some((v) => v <= 0)) return 0;
1033
+ return Math.exp(values.reduce((a, b) => a + Math.log(b), 0) / values.length);
1034
+ }
1035
+ var JUDGE_OUTPUT_CAP = 2e4;
1036
+ function judgeOutput(text) {
1037
+ return scrubCapture(text, void 0, JUDGE_OUTPUT_CAP);
1038
+ }
1039
+ function parseScores(text, dimensions) {
1040
+ for (const candidate of balancedObjects(text)) {
1041
+ let parsed;
1042
+ try {
1043
+ parsed = JSON.parse(candidate);
1044
+ } catch {
1045
+ continue;
1046
+ }
1047
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1048
+ continue;
1049
+ const raw = parsed.scores;
1050
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
1051
+ const scoreObj = raw;
1052
+ const scores = {};
1053
+ for (const d of dimensions) {
1054
+ const val = scoreObj[d];
1055
+ scores[d] = typeof val === "number" ? clamp01(val) : 0;
1056
+ }
1057
+ const reasonField = parsed.reason;
1058
+ return {
1059
+ score: geometricMean(dimensions.map((d) => scores[d])),
1060
+ scores,
1061
+ reason: typeof reasonField === "string" ? reasonField : "(no reason given)"
1062
+ };
1063
+ }
1064
+ throw new LoopError({
1065
+ code: "VALIDATION",
1066
+ message: `validator returned no JSON scores: ${text.slice(0, 200)}`
1067
+ });
1068
+ }
1069
+ function agentCheck(config) {
1070
+ const threshold = config.threshold ?? 0.8;
1071
+ const confidenceTag = config.confidenceTag === true;
1072
+ const maxReasonChars = config.maxReasonChars ?? 280;
1073
+ const dimensions = !confidenceTag && config.dimensions?.length ? config.dimensions : void 0;
1074
+ return setLabel(async (ctx, last) => {
1075
+ const engine = ctx.resolveEngine(config.engine);
1076
+ const contextText = await (config.context ?? defaultContext)(ctx, last);
1077
+ const acceptanceCriterion = ctx.reviewerGate ? `
1078
+
1079
+ ACCEPTANCE CRITERION:
1080
+ ${ctx.reviewerGate}` : "";
1081
+ const closing = confidenceTag ? "Write your review now, then close with `<confidence>N%</confidence>`." : `Return the JSON ${dimensions ? "scores" : "verdict"} now.`;
1082
+ const prompt = `CONDITION TO EVALUATE:
1083
+ ${config.question}
1084
+
1085
+ EVIDENCE:
1086
+ ${contextText}${acceptanceCriterion}
1087
+
1088
+ ` + closing;
1089
+ const baseSystem = confidenceTag ? CONFIDENCE_TAG_SYSTEM : dimensions ? validatorScoreSystem(dimensions) : VALIDATOR_SYSTEM;
1090
+ const system = config.agent ? `${resolveSystem(config.agent)}
1091
+
1092
+ ${baseSystem}` : baseSystem;
1093
+ const env = resolveEnv(ctx);
1094
+ let result;
1095
+ try {
1096
+ assertBudget(ctx);
1097
+ result = await engine.run(
1098
+ {
1099
+ prompt,
1100
+ system,
1101
+ model: config.model ?? config.agent?.model,
1102
+ // A report-then-rate reviewer needs room for findings before the tag.
1103
+ maxTokens: config.maxTokens ?? (confidenceTag ? 2048 : 512),
1104
+ cwd: config.cwd,
1105
+ timeoutMs: config.timeoutMs ?? ctx.timeoutMs,
1106
+ timeoutGraceMs: ctx.timeoutGraceMs,
1107
+ env,
1108
+ leaf: true,
1109
+ attempt: attemptRequestMeta(ctx, config.agent?.name ?? "agent-check")
1110
+ },
1111
+ (e) => {
1112
+ if (e.type === "usage") {
1113
+ ctx.emit({
1114
+ kind: "engine:usage",
1115
+ ts: Date.now(),
1116
+ path: [...ctx.path],
1117
+ model: e.model,
1118
+ usage: e.usage
1119
+ });
1120
+ }
1121
+ },
1122
+ ctx.signal
1123
+ );
1124
+ } catch (e) {
1125
+ throw LoopError.from(e, { code: "ENGINE", path: ctx.path });
1126
+ }
1127
+ logEngineTransportFailure(ctx, result, env);
1128
+ const text = redactEnvValues(requireFinalResultText(result), env);
1129
+ if (confidenceTag) {
1130
+ const parsed = parseConfidenceTag(text);
1131
+ if (!parsed)
1132
+ return {
1133
+ met: false,
1134
+ confidence: 0,
1135
+ reason: `no <confidence> tag: ${redactSecrets(text).slice(0, 140)}`,
1136
+ output: judgeOutput(text)
1137
+ };
1138
+ const pct = Math.round(parsed.confidence * 100);
1139
+ const need = Math.round(threshold * 100);
1140
+ const findings = redactSecrets(parsed.findings);
1141
+ return {
1142
+ met: parsed.confidence >= threshold,
1143
+ confidence: parsed.confidence,
1144
+ reason: `confidence ${pct}% (need ${need}%)${findings ? ` \u2014 ${findings.slice(0, maxReasonChars)}` : ""}`,
1145
+ // The FULL findings — what lets a failing check brief a fix-up body
1146
+ // without re-running the judge. A tag-only reply has no evidence to carry.
1147
+ output: findings ? truncate(findings, JUDGE_OUTPUT_CAP) : void 0
1148
+ };
1149
+ }
1150
+ if (dimensions) {
1151
+ let sv;
1152
+ try {
1153
+ sv = parseScores(text, dimensions);
1154
+ } catch {
1155
+ return {
1156
+ met: false,
1157
+ confidence: 0,
1158
+ reason: `unparseable scores: ${redactSecrets(text).slice(0, 120)}`,
1159
+ output: judgeOutput(text)
1160
+ };
1161
+ }
1162
+ const detail = dimensions.map((d) => `${d}=${sv.scores[d].toFixed(2)}`).join(", ");
1163
+ return {
1164
+ met: sv.score >= threshold,
1165
+ confidence: sv.score,
1166
+ // The judge's free-prose reason gets the same scrub as `output` — it
1167
+ // flows into the same persisted records.
1168
+ reason: `geo ${sv.score.toFixed(2)} (need ${threshold}) [${detail}] \u2014 ${redactSecrets(sv.reason)}`,
1169
+ output: judgeOutput(text)
1170
+ };
1171
+ }
1172
+ let v;
1173
+ try {
1174
+ v = parseVerdict(text);
1175
+ } catch {
1176
+ return {
1177
+ met: false,
1178
+ confidence: 0,
1179
+ reason: `unparseable verdict: ${redactSecrets(text).slice(0, 120)}`,
1180
+ output: judgeOutput(text)
1181
+ };
1182
+ }
1183
+ const met = v.verdict === "yes" && v.confidence >= threshold;
1184
+ return {
1185
+ met,
1186
+ confidence: v.confidence,
1187
+ reason: `${v.verdict} @ ${v.confidence.toFixed(2)} (need ${threshold}) \u2014 ${redactSecrets(v.reason)}`,
1188
+ output: judgeOutput(text)
1189
+ };
1190
+ }, `judge "${config.question}" >=${threshold}`);
1191
+ }
1192
+ function gateJob(label, condition, opts = {}) {
1193
+ const cond = toCondition(condition);
1194
+ return setMeta(async (ctx) => {
1195
+ ctx.emit({
1196
+ kind: "job:start",
1197
+ ts: Date.now(),
1198
+ path: [...ctx.path],
1199
+ label,
1200
+ timeoutMs: ctx.timeoutMs
1201
+ });
1202
+ const r = await cond(ctx, ctx.lastOutcome);
1203
+ ctx.emit({
1204
+ kind: "condition:result",
1205
+ ts: Date.now(),
1206
+ path: [...ctx.path],
1207
+ label,
1208
+ iteration: ctx.iteration,
1209
+ result: r
1210
+ });
1211
+ const outcome = !r.met && opts.target !== void 0 ? {
1212
+ status: "fail",
1213
+ confidence: r.confidence,
1214
+ summary: r.reason,
1215
+ data: r.output,
1216
+ revision: {
1217
+ target: opts.target,
1218
+ reason: r.reason,
1219
+ findings: [{ evidence: r.output ?? r.reason, severity: "block" }],
1220
+ rerun: "target-and-dependents"
1221
+ }
1222
+ } : {
1223
+ status: r.met ? "pass" : "fail",
1224
+ confidence: r.confidence,
1225
+ summary: r.reason
1226
+ };
1227
+ if (r.output !== void 0 && outcome.data === void 0) outcome.data = r.output;
1228
+ ctx.emit({
1229
+ kind: "job:end",
1230
+ ts: Date.now(),
1231
+ path: [...ctx.path],
1232
+ label,
1233
+ outcome
1234
+ });
1235
+ return outcome;
1236
+ }, { kind: "gate", name: label });
1237
+ }
1238
+ function commandJob(label, command, opts = {}) {
1239
+ const [executable, ...args] = splitCommand(command);
1240
+ return gateJob(
1241
+ label,
1242
+ commandSucceeds(executable, args, {
1243
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
1244
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
1245
+ ...opts.env !== void 0 ? { env: opts.env } : {},
1246
+ captureOutput: opts.capture ?? true
1247
+ }),
1248
+ opts.target !== void 0 ? { target: opts.target } : {}
1249
+ );
1250
+ }
1251
+ function splitCommand(command) {
1252
+ if (typeof command === "string" && /["']/.test(command)) {
1253
+ throw new LoopError({
1254
+ code: "VALIDATION",
1255
+ message: `commandJob: "${command}" has a quote in it; pass the command as an array, one argument per entry`
1256
+ });
1257
+ }
1258
+ if (typeof command === "string" && /[|&;<>$`()]/.test(command)) {
1259
+ throw new LoopError({
1260
+ code: "VALIDATION",
1261
+ message: `commandJob: "${command}" has shell syntax in it, and nothing here runs through a shell; give one command as an array, one argument per entry, or run a script`
1262
+ });
1263
+ }
1264
+ const parts = typeof command === "string" ? command.trim().split(/\s+/) : [...command];
1265
+ const [executable, ...args] = parts;
1266
+ if (executable === void 0 || executable === "") {
1267
+ throw new LoopError({ code: "VALIDATION", message: "commandJob needs a command to run" });
1268
+ }
1269
+ return [executable, ...args];
1270
+ }
1271
+ function needOutcome(ctx, name, caller) {
1272
+ const outcome = ctx.needs?.[name];
1273
+ if (outcome === void 0) {
1274
+ throw new LoopError({
1275
+ code: "VALIDATION",
1276
+ message: `${caller}("${name}"): "${name}" is not a dependency of this node; add it to the node's needs`
1277
+ });
1278
+ }
1279
+ return outcome;
1280
+ }
1281
+ function passed(name) {
1282
+ const cond = async (ctx) => {
1283
+ const outcome = needOutcome(ctx, name, "passed");
1284
+ const met = outcome.status === "pass";
1285
+ return { met, reason: `${name} ${outcome.status}` };
1286
+ };
1287
+ setMeta(cond, { kind: "condition", name: "passed", need: name });
1288
+ return setLabel(cond, `passed ${name}`);
1289
+ }
1290
+ function failed(name) {
1291
+ const cond = async (ctx) => {
1292
+ const outcome = needOutcome(ctx, name, "failed");
1293
+ const met = outcome.status === "fail";
1294
+ return { met, reason: `${name} ${outcome.status}` };
1295
+ };
1296
+ setMeta(cond, { kind: "condition", name: "failed", need: name });
1297
+ return setLabel(cond, `failed ${name}`);
1298
+ }
1299
+ function needDecisionsOf(input) {
1300
+ if (Array.isArray(input)) return input.flatMap((item) => needDecisionsOf(item));
1301
+ if (typeof input !== "function") return [];
1302
+ const meta = jobMeta(input);
1303
+ if (meta?.kind !== "condition") return [];
1304
+ if ((meta.name === "passed" || meta.name === "failed") && typeof meta.need === "string") {
1305
+ return [{ on: meta.name, need: meta.need }];
1306
+ }
1307
+ if (meta.name === "all" && Array.isArray(meta.inputs)) {
1308
+ return meta.inputs.flatMap((item) => needDecisionsOf(item));
1309
+ }
1310
+ return [];
1311
+ }
1312
+ var DEFAULT_FANOUT_CONCURRENCY = 4;
1313
+ async function mapWithConcurrency(items, concurrency, fn) {
1314
+ const limit = pLimit(concurrency);
1315
+ return Promise.all(items.map((item, index) => limit(() => fn(item, index))));
1316
+ }
1317
+ async function git(args, { cwd, signal }, input) {
1318
+ const r = await runRuntimeProcess({
1319
+ executable: "git",
1320
+ args,
1321
+ cwd,
1322
+ signal,
1323
+ ...input === void 0 ? {} : { stdin: input }
1324
+ });
1325
+ return {
1326
+ stdout: processText(r.stdout).replace(/\r?\n$/u, ""),
1327
+ exitCode: r.exitCode ?? 1
1328
+ };
1329
+ }
1330
+ async function isRepo(opts) {
1331
+ try {
1332
+ const r = await git(["rev-parse", "--is-inside-work-tree"], opts);
1333
+ return r.exitCode === 0 && r.stdout.trim() === "true";
1334
+ } catch {
1335
+ return false;
1336
+ }
1337
+ }
1338
+ async function currentBranch(opts) {
1339
+ const r = await git(["rev-parse", "--abbrev-ref", "HEAD"], opts);
1340
+ if (r.exitCode !== 0) return void 0;
1341
+ const name = r.stdout.trim();
1342
+ return name && name !== "HEAD" ? name : void 0;
1343
+ }
1344
+ async function gitRoot(opts) {
1345
+ const r = await git(["rev-parse", "--show-toplevel"], opts);
1346
+ return r.exitCode === 0 ? r.stdout.trim() || void 0 : void 0;
1347
+ }
1348
+ async function headSha(opts) {
1349
+ const r = await git(["rev-parse", "HEAD"], opts);
1350
+ return r.exitCode === 0 ? r.stdout.trim() || void 0 : void 0;
1351
+ }
1352
+ async function stageAll(opts) {
1353
+ await git(["add", "-A"], opts);
1354
+ }
1355
+ async function hasStagedChanges(opts) {
1356
+ const r = await git(["diff", "--cached", "--quiet"], opts);
1357
+ return r.exitCode === 1;
1358
+ }
1359
+ async function workspaceFingerprint(opts) {
1360
+ try {
1361
+ if (!await isRepo(opts)) return void 0;
1362
+ const excluded = (opts.excludePaths ?? []).map((p) => relative(opts.cwd, resolve(opts.cwd, p)).replace(/\\/g, "/")).filter((p) => p && !p.startsWith("..") && p !== ".");
1363
+ const included = [...new Set(opts.includePaths ?? [])].map((p) => p.trim()).filter(Boolean).sort();
1364
+ const scoped = included.length > 0;
1365
+ const pathspec = scoped || excluded.length ? [
1366
+ "--",
1367
+ ...scoped ? included : ["."],
1368
+ ...excluded.map((p) => `:(exclude)${p}`)
1369
+ ] : [];
1370
+ const output = async (args) => {
1371
+ const result = await git(args, opts);
1372
+ if (result.exitCode !== 0)
1373
+ throw new Error(`git ${args[0] ?? "command"} failed`);
1374
+ return result.stdout;
1375
+ };
1376
+ const hash = createHash("sha256");
1377
+ const feed = (label, value) => {
1378
+ hash.update(label);
1379
+ hash.update("");
1380
+ hash.update(value);
1381
+ hash.update("");
1382
+ };
1383
+ if (scoped) {
1384
+ feed("scope", included.join("\n"));
1385
+ feed(
1386
+ "tracked",
1387
+ await output(["ls-files", "--stage", ...pathspec])
1388
+ );
1389
+ } else {
1390
+ feed("head", await headSha(opts) ?? "");
1391
+ }
1392
+ feed(
1393
+ "status",
1394
+ await output(["status", "--porcelain", ...pathspec])
1395
+ );
1396
+ feed("unstaged", await output(["diff", ...pathspec]));
1397
+ feed("staged", await output(["diff", "--cached", ...pathspec]));
1398
+ const untracked = (await output([
1399
+ "ls-files",
1400
+ "--others",
1401
+ ...scoped ? [] : ["--exclude-standard"],
1402
+ ...pathspec
1403
+ ])).split("\n").filter(Boolean);
1404
+ for (let i = 0; i < untracked.length; i += 500) {
1405
+ const chunk = untracked.slice(i, i + 500);
1406
+ feed(`untracked-paths:${i}`, chunk.join("\n"));
1407
+ feed(
1408
+ `untracked-content:${i}`,
1409
+ await output(["hash-object", "--", ...chunk])
1410
+ );
1411
+ }
1412
+ return hash.digest("hex");
1413
+ } catch {
1414
+ return void 0;
1415
+ }
1416
+ }
1417
+ function parseDirtyStatus(output) {
1418
+ const records = output.split("\0");
1419
+ const entries = [];
1420
+ for (let index = 0; index < records.length; index += 1) {
1421
+ const record = records[index];
1422
+ if (!record) continue;
1423
+ if (record.length < 4 || record[2] !== " ") {
1424
+ throw new Error("git status returned an invalid porcelain record");
1425
+ }
1426
+ const status = record.slice(0, 2);
1427
+ entries.push({ status, path: record.slice(3) });
1428
+ if (status.includes("R") || status.includes("C")) {
1429
+ const source = records[index + 1];
1430
+ if (!source) throw new Error("git status omitted a rename source");
1431
+ entries.push({ status: `${status}:source`, path: source });
1432
+ index += 1;
1433
+ }
1434
+ }
1435
+ return entries;
1436
+ }
1437
+ function parseIndexState(output) {
1438
+ const entries = /* @__PURE__ */ new Map();
1439
+ for (const record of output.split("\0")) {
1440
+ if (!record) continue;
1441
+ const match = /^(\d{6}) ([0-9a-f]+) (\d)\t(.+)$/u.exec(record);
1442
+ if (!match) throw new Error("git index returned an invalid entry");
1443
+ const [, mode, oid, stage, path] = match;
1444
+ if (stage !== "0") {
1445
+ throw new Error(`git index has an unresolved entry at ${path}`);
1446
+ }
1447
+ if (entries.has(path)) {
1448
+ throw new Error(`git index returned a duplicate entry at ${path}`);
1449
+ }
1450
+ entries.set(path, { mode, oid });
1451
+ }
1452
+ return entries;
1453
+ }
1454
+ function lineDigests(bytes) {
1455
+ if (bytes.byteLength === 0) return Object.freeze([]);
1456
+ const lines = [];
1457
+ let start = 0;
1458
+ for (let index = 0; index < bytes.byteLength; index += 1) {
1459
+ if (bytes[index] !== 10) continue;
1460
+ lines.push(
1461
+ createHash("sha256").update(bytes.subarray(start, index + 1)).digest("hex")
1462
+ );
1463
+ start = index + 1;
1464
+ }
1465
+ if (start < bytes.byteLength) {
1466
+ lines.push(createHash("sha256").update(bytes.subarray(start)).digest("hex"));
1467
+ }
1468
+ return Object.freeze(lines);
1469
+ }
1470
+ async function dirtyState(root, entry) {
1471
+ const path = join(root, entry.path);
1472
+ try {
1473
+ const stat2 = await lstat(path);
1474
+ let bytes;
1475
+ let kind;
1476
+ if (stat2.isFile()) {
1477
+ kind = "file";
1478
+ bytes = await readFile(path);
1479
+ } else if (stat2.isSymbolicLink()) {
1480
+ kind = "symlink";
1481
+ bytes = await readlink(path, { encoding: "buffer" });
1482
+ } else {
1483
+ throw new Error(
1484
+ `git workspace contains an unsupported dirty entry at ${entry.path}`
1485
+ );
1486
+ }
1487
+ return {
1488
+ status: entry.status,
1489
+ kind,
1490
+ mode: stat2.mode,
1491
+ digest: createHash("sha256").update(bytes).digest("hex"),
1492
+ lines: lineDigests(bytes)
1493
+ };
1494
+ } catch (error) {
1495
+ const code = error.code;
1496
+ if (code === "ENOENT") {
1497
+ return {
1498
+ status: entry.status,
1499
+ kind: "missing",
1500
+ mode: 0,
1501
+ digest: null,
1502
+ lines: Object.freeze([])
1503
+ };
1504
+ }
1505
+ throw error;
1506
+ }
1507
+ }
1508
+ async function captureGitWorkspaceSnapshot(opts) {
1509
+ const root = await gitRoot(opts);
1510
+ if (!root) throw new Error("not a git repository");
1511
+ const commandOpts = { ...opts, cwd: root };
1512
+ const included = [...new Set(opts.includePaths ?? [])].map((path) => path.trim()).filter(Boolean).sort();
1513
+ const pathspec = included.length > 0 ? ["--", ...included] : [];
1514
+ const [head, indexResult, statusResult] = await Promise.all([
1515
+ headSha(commandOpts),
1516
+ git([
1517
+ ...pathspec.length === 0 ? ["--literal-pathspecs"] : [],
1518
+ "ls-files",
1519
+ "--stage",
1520
+ "-z",
1521
+ ...pathspec
1522
+ ], commandOpts),
1523
+ git(
1524
+ ["status", "--porcelain=v1", "-z", "--untracked-files=all", ...pathspec],
1525
+ commandOpts
1526
+ )
1527
+ ]);
1528
+ if (indexResult.exitCode !== 0) throw new Error("git index inspection failed");
1529
+ if (statusResult.exitCode !== 0) throw new Error("git status inspection failed");
1530
+ const index = parseIndexState(indexResult.stdout);
1531
+ const dirtyEntries = parseDirtyStatus(statusResult.stdout);
1532
+ const dirty = /* @__PURE__ */ new Map();
1533
+ for (const entry of dirtyEntries) {
1534
+ dirty.set(entry.path, await dirtyState(root, entry));
1535
+ }
1536
+ const paths = [.../* @__PURE__ */ new Set([...index.keys(), ...dirty.keys()])].sort();
1537
+ const files = paths.map((path) => Object.freeze({
1538
+ path,
1539
+ index: index.get(path) ?? null,
1540
+ dirty: dirty.get(path) ?? null
1541
+ }));
1542
+ return Object.freeze({
1543
+ root,
1544
+ head: head ?? null,
1545
+ files: Object.freeze(files)
1546
+ });
1547
+ }
1548
+ function fileStateKey(value) {
1549
+ if (!value) return "[absent]";
1550
+ return JSON.stringify({ index: value.index, dirty: value.dirty });
1551
+ }
1552
+ function lineEditCount(before, after) {
1553
+ if (before.length === after.length && before.every((line, index) => line === after[index])) {
1554
+ return 0;
1555
+ }
1556
+ const maximum = before.length + after.length;
1557
+ let frontier = /* @__PURE__ */ new Map([[1, 0]]);
1558
+ for (let distance = 0; distance <= maximum; distance += 1) {
1559
+ const next = /* @__PURE__ */ new Map();
1560
+ for (let diagonal = -distance; diagonal <= distance; diagonal += 2) {
1561
+ const down = frontier.get(diagonal + 1) ?? -1;
1562
+ const right = (frontier.get(diagonal - 1) ?? -1) + 1;
1563
+ let x = diagonal === -distance || diagonal !== distance && right < down ? down : right;
1564
+ let y = x - diagonal;
1565
+ while (x < before.length && y < after.length && before[x] === after[y]) {
1566
+ x += 1;
1567
+ y += 1;
1568
+ }
1569
+ if (x >= before.length && y >= after.length) return distance;
1570
+ next.set(diagonal, x);
1571
+ }
1572
+ frontier = next;
1573
+ }
1574
+ return maximum;
1575
+ }
1576
+ async function blobLines(root, oid, cache, signal) {
1577
+ if (!oid) return Object.freeze([]);
1578
+ const cached = cache.get(oid);
1579
+ if (cached) return cached;
1580
+ const result = await runRuntimeProcess({
1581
+ executable: "git",
1582
+ args: ["cat-file", "blob", oid],
1583
+ cwd: root,
1584
+ signal
1585
+ });
1586
+ if (result.exitCode !== 0) throw new Error(`git cannot read workspace blob ${oid}`);
1587
+ const lines = lineDigests(result.stdout);
1588
+ cache.set(oid, lines);
1589
+ return lines;
1590
+ }
1591
+ async function worktreeLines(root, state, cache, signal) {
1592
+ if (!state) return Object.freeze([]);
1593
+ if (state.dirty) return state.dirty.lines;
1594
+ return await blobLines(root, state.index?.oid, cache, signal);
1595
+ }
1596
+ async function compareGitWorkspaceSnapshots(before, after, signal) {
1597
+ if (before.root !== after.root) {
1598
+ throw new Error("git workspace root changed during the attempt");
1599
+ }
1600
+ const beforeFiles = new Map(before.files.map((file) => [file.path, file]));
1601
+ const afterFiles = new Map(after.files.map((file) => [file.path, file]));
1602
+ const changedPaths = [.../* @__PURE__ */ new Set([
1603
+ ...beforeFiles.keys(),
1604
+ ...afterFiles.keys()
1605
+ ])].filter(
1606
+ (path) => fileStateKey(beforeFiles.get(path)) !== fileStateKey(afterFiles.get(path))
1607
+ ).sort();
1608
+ const cache = /* @__PURE__ */ new Map();
1609
+ let linesChanged = 0;
1610
+ for (const path of changedPaths) {
1611
+ const beforeState = beforeFiles.get(path);
1612
+ const afterState = afterFiles.get(path);
1613
+ const [beforeWorktree, afterWorktree, beforeIndex, afterIndex] = await Promise.all([
1614
+ worktreeLines(before.root, beforeState, cache, signal),
1615
+ worktreeLines(after.root, afterState, cache, signal),
1616
+ blobLines(before.root, beforeState?.index?.oid, cache, signal),
1617
+ blobLines(after.root, afterState?.index?.oid, cache, signal)
1618
+ ]);
1619
+ linesChanged += Math.max(
1620
+ lineEditCount(beforeWorktree, afterWorktree),
1621
+ lineEditCount(beforeIndex, afterIndex)
1622
+ );
1623
+ if (!Number.isSafeInteger(linesChanged)) {
1624
+ throw new Error("workspace line count exceeded the safe integer range");
1625
+ }
1626
+ }
1627
+ return Object.freeze({
1628
+ headChanged: before.head !== after.head,
1629
+ changedPaths: Object.freeze(changedPaths),
1630
+ filesChanged: changedPaths.length,
1631
+ linesChanged
1632
+ });
1633
+ }
1634
+ async function commit(input, opts) {
1635
+ if (!input.allowEmpty && !await hasStagedChanges(opts)) return void 0;
1636
+ const message = input.body ? `${input.subject}
1637
+
1638
+ ${input.body}
1639
+ ` : `${input.subject}
1640
+ `;
1641
+ const args = ["commit", "-F", "-"];
1642
+ if (input.allowEmpty) args.push("--allow-empty");
1643
+ const r = await git(args, opts, message);
1644
+ if (r.exitCode !== 0) {
1645
+ throw new Error(
1646
+ `git commit failed (exit ${r.exitCode}): ${r.stdout}`.trim()
1647
+ );
1648
+ }
1649
+ return headSha(opts);
1650
+ }
1651
+ async function branchCommits(opts) {
1652
+ const field = "";
1653
+ const record = "";
1654
+ const result = await git(
1655
+ ["log", `-n${opts.max}`, `--format=%s${field}%b${record}`, opts.ref],
1656
+ opts
1657
+ );
1658
+ if (result.exitCode !== 0) return [];
1659
+ return result.stdout.split(record).map((value) => value.replace(/^\n+/, "").split(field)).filter((fields) => fields[0]?.trim()).map(([subject, body]) => ({
1660
+ subject: subject.trim(),
1661
+ body: (body ?? "").trim()
1662
+ }));
1663
+ }
1664
+ var worktreeQueues = /* @__PURE__ */ new Map();
1665
+ async function withWorktreeQueue(repoDir, signal, operation) {
1666
+ const common = await git(["rev-parse", "--git-common-dir"], { cwd: repoDir, signal });
1667
+ if (common.exitCode !== 0) throw new Error("git common directory unavailable");
1668
+ const key = await realpath(resolve(repoDir, common.stdout.trim()));
1669
+ const result = (worktreeQueues.get(key) ?? Promise.resolve()).then(operation);
1670
+ const tail = result.then(() => void 0, () => void 0);
1671
+ worktreeQueues.set(key, tail);
1672
+ try {
1673
+ return await result;
1674
+ } finally {
1675
+ if (worktreeQueues.get(key) === tail) worktreeQueues.delete(key);
1676
+ }
1677
+ }
1678
+ async function addWorktree(repoDir, opts) {
1679
+ return withWorktreeQueue(repoDir, opts.signal, async () => {
1680
+ const dir = mkdtempSync(join(tmpdir(), "lines-wt-"));
1681
+ const r = await git(
1682
+ ["worktree", "add", "-b", opts.branch, dir, opts.base ?? "HEAD"],
1683
+ { cwd: repoDir, signal: opts.signal }
1684
+ );
1685
+ if (r.exitCode !== 0)
1686
+ throw new Error(
1687
+ `git worktree add failed (exit ${r.exitCode}): ${r.stdout}`.trim()
1688
+ );
1689
+ return { dir, branch: opts.branch };
1690
+ });
1691
+ }
1692
+ async function removeWorktree(repoDir, dir, opts = {}) {
1693
+ await withWorktreeQueue(repoDir, opts.signal, () => git(["worktree", "remove", "--force", dir], {
1694
+ cwd: repoDir,
1695
+ signal: opts.signal
1696
+ }));
1697
+ }
1698
+ async function deleteBranch(repoDir, branch, opts = {}) {
1699
+ await git(["branch", "-D", branch], { cwd: repoDir, signal: opts.signal });
1700
+ }
1701
+ async function mergeBranch(repoDir, branch, opts = {}) {
1702
+ const r = await git(
1703
+ ["merge", "--no-ff", "-m", opts.message ?? `merge ${branch}`, branch],
1704
+ { cwd: repoDir, signal: opts.signal }
1705
+ );
1706
+ if (r.exitCode === 0) return { ok: true, conflict: false };
1707
+ await git(["merge", "--abort"], { cwd: repoDir, signal: opts.signal });
1708
+ return { ok: false, conflict: true };
1709
+ }
1710
+ async function mergeNoCommit(repoDir, branch, opts = {}) {
1711
+ const r = await git(["merge", "--no-ff", "--no-commit", branch], {
1712
+ cwd: repoDir,
1713
+ signal: opts.signal
1714
+ });
1715
+ if (r.exitCode === 0) return { clean: true, conflicted: [] };
1716
+ return { clean: false, conflicted: await conflictedFiles(repoDir, opts) };
1717
+ }
1718
+ async function conflictedFiles(repoDir, opts = {}) {
1719
+ const r = await git(["diff", "--name-only", "--diff-filter=U"], {
1720
+ cwd: repoDir,
1721
+ signal: opts.signal
1722
+ });
1723
+ return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
1724
+ }
1725
+ async function mergeAbort(repoDir, opts = {}) {
1726
+ await git(["merge", "--abort"], { cwd: repoDir, signal: opts.signal });
1727
+ }
1728
+
1729
+ // src/core/feedback.ts
1730
+ function normalizeFeedbackSeverity(severity) {
1731
+ return severity ?? "block";
1732
+ }
1733
+ function findingLine(finding) {
1734
+ const reviewer = finding.reviewer ? `${finding.reviewer} ` : "";
1735
+ const severity = normalizeFeedbackSeverity(finding.severity);
1736
+ const decision = finding.decision ? ` Decision: ${finding.decision}.` : "";
1737
+ const recommendation = finding.recommendation ? ` Recommendation: ${oneLine(finding.recommendation)}` : "";
1738
+ return `- ${reviewer}[${severity}]: ${oneLine(finding.evidence)}${decision}${recommendation}`;
1739
+ }
1740
+ function defaultReason(findings) {
1741
+ if (!findings?.length) return "Revision requested";
1742
+ if (findings.length === 1) return oneLine(findings[0].evidence);
1743
+ return `${findings.length} findings require another pass`;
1744
+ }
1745
+ function normalizeRevision(input) {
1746
+ const reason = input.reason?.trim() || defaultReason(input.findings);
1747
+ return {
1748
+ reason,
1749
+ target: input.target,
1750
+ findings: input.findings,
1751
+ rerun: input.rerun ?? (input.target ? "target-and-dependents" : void 0),
1752
+ source: input.source,
1753
+ decision: input.decision
1754
+ };
1755
+ }
1756
+ function revisionRequest(input, over = {}) {
1757
+ const revision = normalizeRevision(input);
1758
+ return {
1759
+ status: over.status ?? "fail",
1760
+ confidence: over.confidence,
1761
+ summary: over.summary ?? revision.reason,
1762
+ data: over.data,
1763
+ error: over.error,
1764
+ revision
1765
+ };
1766
+ }
1767
+ function kickback(to, reason, over = {}) {
1768
+ return revisionRequest({ target: to, reason }, over);
1769
+ }
1770
+ function revisionFromOutcome(outcome) {
1771
+ return outcome.revision;
1772
+ }
1773
+ function feedbackBlock(outcome) {
1774
+ const revision = revisionFromOutcome(outcome);
1775
+ const parts = [
1776
+ "## Feedback to address",
1777
+ "A review or downstream stage requested another pass. Address this before unrelated work."
1778
+ ];
1779
+ if (revision?.target) parts.push(`Target: ${revision.target}`);
1780
+ if (revision?.source) parts.push(`Source: ${revision.source}`);
1781
+ if (revision?.decision) parts.push(`Caller decision: ${revision.decision}`);
1782
+ const reason = revision?.reason ?? outcome.summary;
1783
+ if (reason) parts.push(`Reason: ${reason}`);
1784
+ const findings = revision?.findings ?? [];
1785
+ if (findings.length) {
1786
+ parts.push("Findings:");
1787
+ parts.push(findings.map(findingLine).join("\n"));
1788
+ }
1789
+ return parts.join("\n\n");
1790
+ }
1791
+ function graphPositionBlock(graph, reviewerGate) {
1792
+ return [
1793
+ "## Graph position",
1794
+ `DAG: ${graph.dag}`,
1795
+ `Current node: ${graph.node}`,
1796
+ ...graph.desc ? [`Description: ${graph.desc}`] : [],
1797
+ ...reviewerGate ? [`Gate: ${reviewerGate}`] : [],
1798
+ `Path: ${graph.path.join(" > ")}`,
1799
+ `Depends on: ${graph.needs.length ? graph.needs.join(", ") : "none"}`,
1800
+ `Direct dependents: ${graph.dependents.length ? graph.dependents.join(", ") : "none"}`
1801
+ ].join("\n");
1802
+ }
1803
+ var SHA256_FINGERPRINT = /^[0-9a-f]{64}$/;
1804
+ function reviewerCacheIdentity(reviewer, reviewerGate) {
1805
+ const invalidateOn = [...new Set(reviewer.invalidateOn ?? [])].map((path) => path.trim()).filter(Boolean).sort();
1806
+ return createHash("sha256").update(
1807
+ JSON.stringify({
1808
+ name: reviewer.name.trim(),
1809
+ cacheVersion: reviewer.cacheVersion.trim(),
1810
+ kind: "job" in reviewer ? "job" : "review",
1811
+ scope: reviewer.scope ?? null,
1812
+ invalidateOn,
1813
+ reviewerGate: reviewerGate ?? null
1814
+ })
1815
+ ).digest("hex");
1816
+ }
1817
+ function reviewPassCacheKey(ctx, label) {
1818
+ return `lines:review-panel:${JSON.stringify([...ctx.path, label])}`;
1819
+ }
1820
+ function reviewPassCache(value) {
1821
+ const cache = {};
1822
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1823
+ return cache;
1824
+ for (const [name, entry] of Object.entries(value))
1825
+ Object.defineProperty(cache, name, {
1826
+ value: entry,
1827
+ enumerable: true,
1828
+ configurable: true,
1829
+ writable: true
1830
+ });
1831
+ return cache;
1832
+ }
1833
+ function setReviewPass(cache, name, pass) {
1834
+ Object.defineProperty(cache, name, {
1835
+ value: pass,
1836
+ enumerable: true,
1837
+ configurable: true,
1838
+ writable: true
1839
+ });
1840
+ }
1841
+ function reusableReviewPass(value, minConfidence2, identity) {
1842
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1843
+ return false;
1844
+ const pass = value;
1845
+ return pass.identity === identity && typeof pass.fingerprint === "string" && SHA256_FINGERPRINT.test(pass.fingerprint) && typeof pass.confidence === "number" && Number.isFinite(pass.confidence) && pass.confidence >= minConfidence2 && pass.confidence <= 1;
1846
+ }
1847
+ var REVIEW_PANEL_DEFAULT_CONCURRENCY = DEFAULT_FANOUT_CONCURRENCY;
1848
+ function isReviewInfrastructureError(error) {
1849
+ return isInfrastructureError(error);
1850
+ }
1851
+ function isReviewBoundaryError(error) {
1852
+ return error?.code === "WRITE_BOUNDARY";
1853
+ }
1854
+ function findingFromOutput(reviewer, scope, output) {
1855
+ const evidence = output?.trim();
1856
+ if (!evidence) return void 0;
1857
+ return [{ reviewer, severity: "block", scope, evidence }];
1858
+ }
1859
+ function outputFromInfrastructureError(error) {
1860
+ const cause = error.cause;
1861
+ if (typeof cause !== "object" || cause === null) return void 0;
1862
+ const output = cause.output;
1863
+ return typeof output === "string" ? output : void 0;
1864
+ }
1865
+ async function runReviewer(reviewer, index, ctx) {
1866
+ const name = reviewer.name ?? `reviewer-${index + 1}`;
1867
+ const reviewerCtx = {
1868
+ ...ctx,
1869
+ reviewerGate: criterionFor(ctx)
1870
+ };
1871
+ try {
1872
+ if ("job" in reviewer) {
1873
+ const outcome = await reviewer.job(reviewerCtx);
1874
+ const outcomeError = outcome.error;
1875
+ if (isReviewInfrastructureError(outcomeError) || isReviewBoundaryError(outcomeError)) {
1876
+ return {
1877
+ kind: "engine-error",
1878
+ name,
1879
+ scope: reviewer.scope,
1880
+ reason: outcome.summary ?? outcomeError.message,
1881
+ error: outcomeError,
1882
+ blocking: isReviewBoundaryError(outcomeError)
1883
+ };
1884
+ }
1885
+ return {
1886
+ kind: "verdict",
1887
+ name,
1888
+ met: outcome.status === "pass",
1889
+ confidence: outcome.confidence,
1890
+ reason: outcome.summary ?? outcome.status,
1891
+ scope: reviewer.scope,
1892
+ findings: revisionFromOutcome(outcome)?.findings
1893
+ };
1894
+ }
1895
+ const result = await toCondition(reviewer.review)(
1896
+ reviewerCtx,
1897
+ ctx.lastOutcome
1898
+ );
1899
+ return {
1900
+ kind: "verdict",
1901
+ name,
1902
+ met: result.met,
1903
+ confidence: result.confidence,
1904
+ reason: result.reason,
1905
+ scope: reviewer.scope,
1906
+ findings: result.met ? void 0 : findingFromOutput(name, reviewer.scope, result.output)
1907
+ };
1908
+ } catch (e) {
1909
+ if (ctx.signal.aborted) throw e;
1910
+ const error = e instanceof LoopError ? e : void 0;
1911
+ if (error && (isReviewInfrastructureError(error) || isReviewBoundaryError(error))) {
1912
+ return {
1913
+ kind: "engine-error",
1914
+ name,
1915
+ scope: reviewer.scope,
1916
+ reason: error.message,
1917
+ error,
1918
+ blocking: isReviewBoundaryError(error),
1919
+ findings: findingFromOutput(
1920
+ name,
1921
+ reviewer.scope,
1922
+ outputFromInfrastructureError(error)
1923
+ )
1924
+ };
1925
+ }
1926
+ return {
1927
+ kind: "verdict",
1928
+ name,
1929
+ met: false,
1930
+ scope: reviewer.scope,
1931
+ reason: `reviewer errored: ${e instanceof Error ? e.message : String(e)}`
1932
+ };
1933
+ }
1934
+ }
1935
+ async function runPersistedReviewer(reviewer, index, ctx, cache, minConfidence2) {
1936
+ const name = reviewer.name;
1937
+ const identity = reviewerCacheIdentity(
1938
+ reviewer,
1939
+ criterionFor(ctx)
1940
+ );
1941
+ const before = await workspaceFingerprint({
1942
+ cwd: ctx.workspace.dir,
1943
+ signal: ctx.signal,
1944
+ excludePaths: ctx.fingerprintExcludePaths,
1945
+ includePaths: reviewer.invalidateOn
1946
+ });
1947
+ const cached = cache[name];
1948
+ if (before !== void 0 && reusableReviewPass(cached, minConfidence2, identity) && cached.fingerprint === before) {
1949
+ return {
1950
+ result: {
1951
+ kind: "verdict",
1952
+ name,
1953
+ met: true,
1954
+ confidence: cached.confidence,
1955
+ reason: "reused persisted pass",
1956
+ scope: reviewer.scope
1957
+ },
1958
+ reusedFingerprint: before
1959
+ };
1960
+ }
1961
+ delete cache[name];
1962
+ const result = await runReviewer(reviewer, index, ctx);
1963
+ if (before === void 0 || result.kind !== "verdict" || !result.met || result.confidence === void 0 || !Number.isFinite(result.confidence) || result.confidence < minConfidence2 || result.confidence > 1)
1964
+ return { result };
1965
+ const after = await workspaceFingerprint({
1966
+ cwd: ctx.workspace.dir,
1967
+ signal: ctx.signal,
1968
+ excludePaths: ctx.fingerprintExcludePaths,
1969
+ includePaths: reviewer.invalidateOn
1970
+ });
1971
+ if (after !== void 0 && after === before)
1972
+ setReviewPass(cache, name, {
1973
+ identity,
1974
+ fingerprint: after,
1975
+ confidence: result.confidence
1976
+ });
1977
+ return { result };
1978
+ }
1979
+ async function settlePersistedReviewers(reviewers, initial, ctx, cache, minConfidence2, concurrency) {
1980
+ const staleIndices = (await mapWithConcurrency(initial, concurrency, async (run, index) => {
1981
+ if (run.reusedFingerprint === void 0) return void 0;
1982
+ const current = await workspaceFingerprint({
1983
+ cwd: ctx.workspace.dir,
1984
+ signal: ctx.signal,
1985
+ excludePaths: ctx.fingerprintExcludePaths,
1986
+ includePaths: reviewers[index].invalidateOn
1987
+ });
1988
+ return current === run.reusedFingerprint ? void 0 : index;
1989
+ })).filter((index) => index !== void 0);
1990
+ for (const index of staleIndices) delete cache[reviewers[index].name];
1991
+ const reruns = await mapWithConcurrency(
1992
+ staleIndices,
1993
+ concurrency,
1994
+ async (index) => ({
1995
+ index,
1996
+ run: await runPersistedReviewer(
1997
+ reviewers[index],
1998
+ index,
1999
+ ctx,
2000
+ cache,
2001
+ minConfidence2
2002
+ )
2003
+ })
2004
+ );
2005
+ const results = initial.map((run) => run.result);
2006
+ for (const { index, run } of reruns) results[index] = run.result;
2007
+ await mapWithConcurrency(results, concurrency, async (result, index) => {
2008
+ if (result.kind !== "verdict" || !result.met) return;
2009
+ const reviewer = reviewers[index];
2010
+ const name = reviewer.name;
2011
+ const cached = cache[name];
2012
+ if (!reusableReviewPass(
2013
+ cached,
2014
+ minConfidence2,
2015
+ reviewerCacheIdentity(
2016
+ reviewer,
2017
+ criterionFor(ctx)
2018
+ )
2019
+ ))
2020
+ return;
2021
+ const current = await workspaceFingerprint({
2022
+ cwd: ctx.workspace.dir,
2023
+ signal: ctx.signal,
2024
+ excludePaths: ctx.fingerprintExcludePaths,
2025
+ includePaths: reviewer.invalidateOn
2026
+ });
2027
+ if (current !== cached.fingerprint) {
2028
+ delete cache[name];
2029
+ results[index] = {
2030
+ ...result,
2031
+ met: false,
2032
+ reason: "review evidence changed during panel"
2033
+ };
2034
+ }
2035
+ });
2036
+ return results;
2037
+ }
2038
+ function reviewFindings(result) {
2039
+ const findings = result.findings?.length ? result.findings : [
2040
+ {
2041
+ reviewer: result.name,
2042
+ severity: "block",
2043
+ scope: result.scope,
2044
+ evidence: result.reason
2045
+ }
2046
+ ];
2047
+ return findings.map((finding) => ({
2048
+ ...finding,
2049
+ reviewer: finding.reviewer ?? result.name,
2050
+ severity: finding.severity ?? "block",
2051
+ scope: finding.scope ?? result.scope
2052
+ }));
2053
+ }
2054
+ function isActionableFinding(finding, actionableScopes) {
2055
+ if (!actionableScopes?.length) return true;
2056
+ return !finding.scope || actionableScopes.includes(finding.scope);
2057
+ }
2058
+ function escalatedFinding(finding) {
2059
+ return { ...finding, decision: "escalated" };
2060
+ }
2061
+ function findingSeverityCounts(findings) {
2062
+ const counts = {};
2063
+ for (const finding of findings) {
2064
+ const severity = normalizeFeedbackSeverity(finding.severity);
2065
+ counts[severity] = (counts[severity] ?? 0) + 1;
2066
+ }
2067
+ return counts;
2068
+ }
2069
+ function reviewPanel(config) {
2070
+ const label = config.label ?? "review-panel";
2071
+ if (!config.reviewers.length)
2072
+ throw new LoopError({
2073
+ code: "CONFIG",
2074
+ message: `reviewPanel "${label}": at least one reviewer is required`
2075
+ });
2076
+ if (typeof config.pass === "number" && (!Number.isInteger(config.pass) || config.pass < 1 || config.pass > config.reviewers.length))
2077
+ throw new LoopError({
2078
+ code: "CONFIG",
2079
+ message: `reviewPanel "${label}": pass must be an integer from 1 to ${config.reviewers.length} (got ${config.pass})`
2080
+ });
2081
+ const concurrency = config.concurrency ?? REVIEW_PANEL_DEFAULT_CONCURRENCY;
2082
+ if (!Number.isInteger(concurrency) || concurrency <= 0)
2083
+ throw new LoopError({
2084
+ code: "CONFIG",
2085
+ message: `reviewPanel "${label}": concurrency must be a positive integer`
2086
+ });
2087
+ if (config.persistPasses) {
2088
+ const minConfidence2 = config.persistPasses.minConfidence;
2089
+ if (!Number.isFinite(minConfidence2) || minConfidence2 < 0 || minConfidence2 > 1)
2090
+ throw new LoopError({
2091
+ code: "CONFIG",
2092
+ message: `reviewPanel "${label}": persistPasses.minConfidence must be from 0 to 1`
2093
+ });
2094
+ if (!config.label?.trim())
2095
+ throw new LoopError({
2096
+ code: "CONFIG",
2097
+ message: "reviewPanel persistPasses requires an explicit label"
2098
+ });
2099
+ const names = config.reviewers.map((reviewer) => reviewer.name?.trim());
2100
+ if (names.some((name) => !name))
2101
+ throw new LoopError({
2102
+ code: "CONFIG",
2103
+ message: `reviewPanel "${label}" persistPasses requires every reviewer to have an explicit name`
2104
+ });
2105
+ if (new Set(names).size !== names.length)
2106
+ throw new LoopError({
2107
+ code: "CONFIG",
2108
+ message: `reviewPanel "${label}" persistPasses requires unique reviewer names`
2109
+ });
2110
+ if (config.reviewers.some((reviewer) => !reviewer.cacheVersion?.trim()))
2111
+ throw new LoopError({
2112
+ code: "CONFIG",
2113
+ message: `reviewPanel "${label}" persistPasses requires every reviewer to have an explicit cacheVersion`
2114
+ });
2115
+ }
2116
+ const job = async (ctx) => {
2117
+ ctx.emit({
2118
+ kind: "job:start",
2119
+ ts: Date.now(),
2120
+ path: [...ctx.path],
2121
+ label,
2122
+ timeoutMs: ctx.timeoutMs
2123
+ });
2124
+ const cacheKey = config.persistPasses ? reviewPassCacheKey(ctx, label) : void 0;
2125
+ const cache = cacheKey ? reviewPassCache(ctx.state[cacheKey]) : void 0;
2126
+ if (cache) {
2127
+ const names = new Set(config.reviewers.map((reviewer) => reviewer.name));
2128
+ for (const name of Object.keys(cache))
2129
+ if (!names.has(name)) delete cache[name];
2130
+ }
2131
+ if (cacheKey && cache) ctx.state[cacheKey] = cache;
2132
+ let results;
2133
+ if (cache && config.persistPasses) {
2134
+ const initial = await mapWithConcurrency(
2135
+ config.reviewers,
2136
+ concurrency,
2137
+ (reviewer, i) => runPersistedReviewer(
2138
+ reviewer,
2139
+ i,
2140
+ ctx,
2141
+ cache,
2142
+ config.persistPasses.minConfidence
2143
+ )
2144
+ );
2145
+ results = await settlePersistedReviewers(
2146
+ config.reviewers,
2147
+ initial,
2148
+ ctx,
2149
+ cache,
2150
+ config.persistPasses.minConfidence,
2151
+ concurrency
2152
+ );
2153
+ } else {
2154
+ results = await mapWithConcurrency(
2155
+ config.reviewers,
2156
+ concurrency,
2157
+ (reviewer, i) => runReviewer(reviewer, i, ctx)
2158
+ );
2159
+ }
2160
+ if (cacheKey && cache && !Object.keys(cache).length)
2161
+ delete ctx.state[cacheKey];
2162
+ const verdicts = results.filter(
2163
+ (r) => r.kind === "verdict"
2164
+ );
2165
+ const errors = results.filter(
2166
+ (r) => r.kind === "engine-error"
2167
+ );
2168
+ const blockingErrors = errors.filter((error) => error.blocking);
2169
+ const passedCount = verdicts.filter((r) => r.met).length;
2170
+ const required = config.pass === void 0 || config.pass === "all" ? results.length : config.pass;
2171
+ const rawFindings = [
2172
+ ...verdicts.filter((r) => !r.met).flatMap(reviewFindings),
2173
+ ...errors.flatMap((r) => r.findings ?? [])
2174
+ ];
2175
+ const findings = rawFindings.filter(
2176
+ (f) => isActionableFinding(f, config.actionableScopes)
2177
+ );
2178
+ const escalatedFindings = rawFindings.filter((f) => !isActionableFinding(f, config.actionableScopes)).map(escalatedFinding);
2179
+ const passed2 = blockingErrors.length === 0 && verdicts.length > 0 && passedCount >= required;
2180
+ const summaryHead = `Review panel: ${passedCount}/${results.length} reviewer(s) cleared`;
2181
+ const summaryParts = [`${summaryHead}.`];
2182
+ if (findings.length) summaryParts.push(findings.map(findingLine).join("\n"));
2183
+ if (escalatedFindings.length)
2184
+ summaryParts.push(
2185
+ `Escalated:
2186
+ ${escalatedFindings.map(findingLine).join("\n")}`
2187
+ );
2188
+ if (errors.length)
2189
+ summaryParts.push(
2190
+ `Engine errors:
2191
+ ${errors.map((e) => `- ${e.name}: ${oneLine(e.reason)}`).join("\n")}`
2192
+ );
2193
+ const summary = summaryParts.join("\n");
2194
+ const scored = verdicts.map((r) => r.confidence).filter((c) => c != null);
2195
+ const confidence = scored.length ? scored.reduce((sum, c) => sum + c, 0) / scored.length : void 0;
2196
+ const data = {
2197
+ findings,
2198
+ escalatedFindings,
2199
+ errors,
2200
+ results,
2201
+ passed: passedCount,
2202
+ required,
2203
+ severityCounts: findingSeverityCounts(findings)
2204
+ };
2205
+ const blockedByInfrastructure = blockingErrors.length > 0 || errors.length > 0 && (config.pass === void 0 || config.pass === "all" ? true : passedCount < required && passedCount + errors.length >= required);
2206
+ let outcome;
2207
+ if (passed2) {
2208
+ outcome = { status: "pass", summary, confidence, data };
2209
+ } else if (blockedByInfrastructure || !findings.length && errors.length) {
2210
+ const first = errors[0];
2211
+ outcome = {
2212
+ status: "paused",
2213
+ summary,
2214
+ confidence,
2215
+ data,
2216
+ error: first.error ?? new LoopError({
2217
+ code: "ENGINE",
2218
+ phase: "review",
2219
+ message: first.reason
2220
+ })
2221
+ };
2222
+ } else if (!findings.length && escalatedFindings.length) {
2223
+ outcome = { status: "pass", summary, confidence, data };
2224
+ } else {
2225
+ outcome = revisionRequest(
2226
+ {
2227
+ target: config.target,
2228
+ // A clean one-line reason. The findings ride the `findings` array, so
2229
+ // feedbackBlock renders them once (not embedded in the reason too) and
2230
+ // the records/tail `reason` stays a single tidy line. The full
2231
+ // multi-line `summary` is kept on the outcome below for host logs.
2232
+ reason: `${summaryHead}.`,
2233
+ findings,
2234
+ rerun: config.rerun
2235
+ },
2236
+ { summary, confidence, data }
2237
+ );
2238
+ }
2239
+ ctx.emit({ kind: "job:end", ts: Date.now(), path: [...ctx.path], label, outcome });
2240
+ return outcome;
2241
+ };
2242
+ return setMeta(job, {
2243
+ kind: "reviewPanel",
2244
+ name: label,
2245
+ concurrency,
2246
+ actionableScopes: config.actionableScopes
2247
+ });
2248
+ }
2249
+ async function gitOutput(cwd, args, signal) {
2250
+ const out = await runRuntimeProcess({
2251
+ executable: "git",
2252
+ args,
2253
+ cwd,
2254
+ signal
2255
+ });
2256
+ return processText(out.stdout).trim();
2257
+ }
2258
+ async function resolveFiles(ctx, patterns) {
2259
+ const fromGit = await gitOutput(
2260
+ ctx.workspace.dir,
2261
+ ["ls-files", "--", ...patterns],
2262
+ ctx.signal
2263
+ ).catch(() => "");
2264
+ const files = fromGit ? fromGit.split("\n").filter(Boolean) : [];
2265
+ if (files.length) return files;
2266
+ return patterns.filter((p) => existsSync(join(ctx.workspace.dir, p)));
2267
+ }
2268
+ function reviewContext(config) {
2269
+ return async (ctx, last) => {
2270
+ const max = config.maxChars ?? 6e3;
2271
+ const buildTests = async () => {
2272
+ if (!config.tests) return [];
2273
+ if (config.tests === true) {
2274
+ const lines = [];
2275
+ if (last?.status) lines.push(`Last outcome status: ${last.status}`);
2276
+ if (last?.summary) lines.push(`Last outcome summary: ${last.summary}`);
2277
+ if (last?.data !== void 0) {
2278
+ let rendered;
2279
+ try {
2280
+ rendered = JSON.stringify(last.data, null, 2);
2281
+ } catch {
2282
+ rendered = String(last.data);
2283
+ }
2284
+ lines.push(`Last outcome data: ${rendered}`);
2285
+ }
2286
+ return lines.length ? [`## Test and outcome context
2287
+
2288
+ ${lines.join("\n")}`] : [];
2289
+ }
2290
+ const cwd = config.tests.cwd ?? ctx.workspace.dir;
2291
+ const result = await runRuntimeProcess({
2292
+ executable: config.tests.command,
2293
+ args: config.tests.args ?? [],
2294
+ cwd,
2295
+ signal: ctx.signal
2296
+ }).catch((e) => {
2297
+ if (ctx.signal.aborted) throw e;
2298
+ return void 0;
2299
+ });
2300
+ const exit = result?.exitCode ?? "(command did not run)";
2301
+ const stdout = result === void 0 ? "" : processText(result.stdout);
2302
+ const stderr = result === void 0 ? "" : processText(result.stderr);
2303
+ return [
2304
+ `## Test command
2305
+
2306
+ ${config.tests.command} ${(config.tests.args ?? []).join(" ")}
2307
+
2308
+ exit: ${exit}
2309
+
2310
+ stdout:
2311
+ ${truncate(stdout, max)}
2312
+
2313
+ stderr:
2314
+ ${truncate(stderr, max)}`
2315
+ ];
2316
+ };
2317
+ const buildDiff = async () => {
2318
+ if (!config.diff) return [];
2319
+ const diff2 = await gitOutput(
2320
+ ctx.workspace.dir,
2321
+ ["diff", "HEAD", "--"],
2322
+ ctx.signal
2323
+ ).catch(() => "");
2324
+ return diff2 ? [`## Git diff
2325
+
2326
+ ${truncate(diff2, max)}`] : [];
2327
+ };
2328
+ const buildFiles = async () => {
2329
+ if (!config.files?.length) return [];
2330
+ const files2 = await resolveFiles(ctx, config.files);
2331
+ const out = [];
2332
+ for (const file of files2) {
2333
+ const path = join(ctx.workspace.dir, file);
2334
+ if (!existsSync(path)) continue;
2335
+ out.push(`## File: ${file}
2336
+
2337
+ ${truncate(readFileSync(path, "utf8"), max)}`);
2338
+ }
2339
+ return out;
2340
+ };
2341
+ const [tests, diff, files] = await Promise.all([
2342
+ buildTests(),
2343
+ buildDiff(),
2344
+ buildFiles()
2345
+ ]);
2346
+ const sections = [...tests, ...diff, ...files];
2347
+ return truncate(
2348
+ sections.join("\n\n---\n\n") || "(no review context)",
2349
+ max
2350
+ );
2351
+ };
2352
+ }
2353
+
2354
+ // src/core/job.ts
2355
+ var RECORDED_ENGINE_USAGE = "obversa:recorded-engine-usage";
2356
+ function recordEngineUsage(ctx, model, path, recordAs) {
2357
+ const records = ctx.state[RECORDED_ENGINE_USAGE];
2358
+ const entry = {
2359
+ model,
2360
+ path: [...path],
2361
+ ...recordAs === void 0 ? {} : { role: recordAs.role, stage: recordAs.stage }
2362
+ };
2363
+ if (records === void 0) {
2364
+ ctx.state[RECORDED_ENGINE_USAGE] = [entry];
2365
+ } else {
2366
+ records.push(entry);
2367
+ }
2368
+ }
2369
+ var TERMINAL = (text) => ({
2370
+ status: "pass",
2371
+ summary: text.trim().slice(0, 280),
2372
+ data: text
2373
+ });
2374
+ function advisorInstruction(maxCalls) {
2375
+ return `## Advisor consults
2376
+ When you hit a hard design fork, you may request at most ${maxCalls} advisor consult${maxCalls === 1 ? "" : "s"}. Do not shell out to another model. Instead, make your whole reply exactly this block:
2377
+
2378
+ <consult_advisor>
2379
+ <question>the precise question</question>
2380
+ <context>the minimum context the advisor needs</context>
2381
+ </consult_advisor>
2382
+
2383
+ The runtime will record the consult and return the advisor reply to you.`;
2384
+ }
2385
+ function withOperationalContext(ctx, userPrompt, config) {
2386
+ const parts = [userPrompt];
2387
+ if (config.consumeFeedback && ctx.lastReview) {
2388
+ parts.push(feedbackBlock(ctx.lastReview));
2389
+ }
2390
+ if (config.graphContext && ctx.graph) {
2391
+ parts.push(graphPositionBlock(ctx.graph, ctx.reviewerGate));
2392
+ }
2393
+ if (config.advisor) parts.push(advisorInstruction(config.advisor.maxCalls ?? 1));
2394
+ return parts.join("\n\n---\n\n");
2395
+ }
2396
+ function tagValue(text, tag) {
2397
+ const match = new RegExp(`<${tag}>\\s*([\\s\\S]*?)\\s*</${tag}>`, "i").exec(text);
2398
+ return match?.[1]?.trim() || void 0;
2399
+ }
2400
+ function parseAdvisorRequest(text) {
2401
+ const block = /^\s*<consult_advisor>\s*([\s\S]*?)\s*<\/consult_advisor>\s*$/i.exec(text)?.[1];
2402
+ if (!block) return void 0;
2403
+ const question = tagValue(block, "question") ?? block.trim();
2404
+ if (!question) return void 0;
2405
+ return { question, context: tagValue(block, "context") };
2406
+ }
2407
+ function advisorFollowup(call, request, reply) {
2408
+ return `Advisor consult #${call}
2409
+
2410
+ Question:
2411
+ ${request.question}
2412
+
2413
+ ${request.context ? `Context supplied:
2414
+ ${request.context}
2415
+
2416
+ ` : ""}Advisor reply:
2417
+ ${reply}
2418
+
2419
+ Continue the original task. Do not ask the same question again.`;
2420
+ }
2421
+ async function runAdvisorConsult(ctx, label, config, call, request, inherited, redactionEnv) {
2422
+ assertBudget(ctx);
2423
+ const engine = ctx.resolveEngine(config.engine);
2424
+ const result = await engine.run(
2425
+ {
2426
+ prompt: `Question:
2427
+ ${request.question}` + (request.context ? `
2428
+
2429
+ Context:
2430
+ ${request.context}` : ""),
2431
+ system: config.systemPrompt,
2432
+ model: config.model,
2433
+ maxTokens: config.maxTokens ?? inherited.maxTokens,
2434
+ timeoutMs: config.timeoutMs ?? inherited.timeoutMs,
2435
+ timeoutGraceMs: config.timeoutGraceMs ?? inherited.timeoutGraceMs,
2436
+ cwd: ctx.workspace.dir,
2437
+ leaf: true,
2438
+ attempt: attemptRequestMeta(ctx, `${label}:advisor`)
2439
+ },
2440
+ (event) => {
2441
+ const ts = Date.now();
2442
+ if (event.type === "usage") {
2443
+ ctx.emit({
2444
+ kind: "engine:usage",
2445
+ ts,
2446
+ path: [...ctx.path],
2447
+ model: event.model,
2448
+ usage: event.usage
2449
+ });
2450
+ recordEngineUsage(ctx, event.model, ctx.path);
2451
+ } else if (event.type === "tool") {
2452
+ ctx.emit({
2453
+ kind: "engine:tool",
2454
+ ts,
2455
+ path: [...ctx.path],
2456
+ name: event.name,
2457
+ phase: event.phase
2458
+ });
2459
+ }
2460
+ },
2461
+ ctx.signal
2462
+ );
2463
+ logEngineTransportFailure(ctx, result, redactionEnv);
2464
+ const reply = scrubCapture(
2465
+ requireFinalResultText(result),
2466
+ redactionEnv
2467
+ ).trim();
2468
+ ctx.emit({
2469
+ kind: "advisor:consult",
2470
+ ts: Date.now(),
2471
+ path: [...ctx.path],
2472
+ label,
2473
+ call,
2474
+ question: request.question,
2475
+ reply,
2476
+ model: result.effective.model ?? void 0
2477
+ });
2478
+ return { reply, model: result.effective.model ?? void 0 };
2479
+ }
2480
+ function agentJob(config) {
2481
+ const job = async (ctx) => {
2482
+ const recordAs = "recordAs" in config ? config.recordAs : void 0;
2483
+ const path = [...ctx.path];
2484
+ const label = config.label ?? config.agent?.name ?? "agent";
2485
+ const defaultTimeoutMs = config.timeoutMs ?? ctx.timeoutMs;
2486
+ const defaultTimeoutGraceMs = config.timeoutGraceMs ?? ctx.timeoutGraceMs;
2487
+ ctx.emit({
2488
+ kind: "job:start",
2489
+ ts: Date.now(),
2490
+ path,
2491
+ label,
2492
+ timeoutMs: defaultTimeoutMs
2493
+ });
2494
+ const userPrompt = typeof config.prompt === "function" ? await config.prompt(ctx) : config.prompt;
2495
+ const contextualPrompt = withOperationalContext(ctx, userPrompt, config);
2496
+ const system = config.system !== void 0 ? typeof config.system === "function" ? config.system(ctx) : config.system : config.agent ? resolveSystem(config.agent) : void 0;
2497
+ const env = resolveEnv(ctx, config.env);
2498
+ const fallbacks = config.fallback ? Array.isArray(config.fallback) ? config.fallback : [config.fallback] : [];
2499
+ const routes = [
2500
+ {
2501
+ engine: config.engine,
2502
+ model: config.model ?? config.agent?.model,
2503
+ timeoutMs: defaultTimeoutMs,
2504
+ timeoutGraceMs: defaultTimeoutGraceMs
2505
+ },
2506
+ ...fallbacks
2507
+ ];
2508
+ const fallbackOn = new Set(
2509
+ config.fallbackOn ?? ["RATE_LIMIT", "QUOTA"]
2510
+ );
2511
+ let result;
2512
+ for (let i = 0; i < routes.length; i += 1) {
2513
+ const route = routes[i];
2514
+ const routeModel = route.model;
2515
+ const timeoutMs = route.timeoutMs ?? defaultTimeoutMs;
2516
+ const timeoutGraceMs = route.timeoutGraceMs ?? defaultTimeoutGraceMs;
2517
+ try {
2518
+ const basePrompt = contextualPrompt;
2519
+ const engine = ctx.resolveEngine(route.engine ?? config.engine);
2520
+ const maxAdvisorCalls = config.advisor?.maxCalls ?? 1;
2521
+ const advisorReplies = [];
2522
+ for (; ; ) {
2523
+ const prompt = advisorReplies.length ? `${basePrompt}
2524
+
2525
+ ---
2526
+
2527
+ ${advisorReplies.join("\n\n---\n\n")}` : basePrompt;
2528
+ assertBudget(ctx);
2529
+ result = await engine.run(
2530
+ {
2531
+ prompt,
2532
+ system,
2533
+ model: routeModel,
2534
+ maxTokens: config.maxTokens,
2535
+ tools: config.tools ?? config.agent?.tools,
2536
+ allowedTools: config.allowedTools,
2537
+ workspaceMode: config.workspaceMode,
2538
+ leaf: config.leaf ?? config.agent?.leaf,
2539
+ cwd: config.cwd ?? ctx.workspace.dir,
2540
+ timeoutMs,
2541
+ timeoutGraceMs,
2542
+ env,
2543
+ attempt: attemptRequestMeta(ctx, label)
2544
+ },
2545
+ (e) => {
2546
+ const ts = Date.now();
2547
+ switch (e.type) {
2548
+ case "text":
2549
+ ctx.emit({ kind: "engine:text", ts, path, delta: e.delta });
2550
+ break;
2551
+ case "thinking":
2552
+ ctx.emit({ kind: "engine:thinking", ts, path, delta: e.delta });
2553
+ break;
2554
+ case "tool":
2555
+ ctx.emit({
2556
+ kind: "engine:tool",
2557
+ ts,
2558
+ path,
2559
+ name: e.name,
2560
+ phase: e.phase
2561
+ });
2562
+ break;
2563
+ case "usage":
2564
+ ctx.emit({
2565
+ kind: "engine:usage",
2566
+ ts,
2567
+ path,
2568
+ model: e.model,
2569
+ usage: e.usage,
2570
+ ...recordAs
2571
+ });
2572
+ recordEngineUsage(ctx, e.model, path, recordAs);
2573
+ break;
2574
+ }
2575
+ },
2576
+ ctx.signal
2577
+ );
2578
+ logEngineTransportFailure(ctx, result, env);
2579
+ const advisor = config.advisor;
2580
+ const capturedText = scrubCapture(
2581
+ requireFinalResultText(result),
2582
+ env
2583
+ );
2584
+ const consult = advisor ? parseAdvisorRequest(capturedText) : void 0;
2585
+ if (!advisor || !consult) break;
2586
+ if (advisorReplies.length >= maxAdvisorCalls) {
2587
+ throw new LoopError({
2588
+ code: "BUDGET",
2589
+ phase: "body",
2590
+ message: `${label} exceeded advisor consult cap (${maxAdvisorCalls})`
2591
+ });
2592
+ }
2593
+ const call = advisorReplies.length + 1;
2594
+ const { reply } = await runAdvisorConsult(
2595
+ ctx,
2596
+ label,
2597
+ advisor,
2598
+ call,
2599
+ consult,
2600
+ { maxTokens: config.maxTokens, timeoutMs, timeoutGraceMs },
2601
+ env
2602
+ );
2603
+ advisorReplies.push(advisorFollowup(call, consult, reply));
2604
+ }
2605
+ break;
2606
+ } catch (e) {
2607
+ const error = LoopError.from(e, {
2608
+ code: ctx.signal.aborted ? "ABORTED" : "ENGINE",
2609
+ phase: "body",
2610
+ path: ctx.path,
2611
+ iteration: ctx.iteration
2612
+ });
2613
+ if (i < routes.length - 1 && !ctx.signal.aborted && fallbackOn.has(error.code)) {
2614
+ ctx.log(
2615
+ `${label} primary route hit ${error.code}; trying fallback route ${i + 2}`,
2616
+ "warn"
2617
+ );
2618
+ continue;
2619
+ }
2620
+ ctx.emit({
2621
+ kind: "error",
2622
+ ts: Date.now(),
2623
+ path,
2624
+ message: error.message,
2625
+ code: error.code
2626
+ });
2627
+ const outcome2 = {
2628
+ status: ctx.signal.aborted ? "aborted" : "fail",
2629
+ summary: error.message,
2630
+ error
2631
+ };
2632
+ ctx.emit({
2633
+ kind: "job:end",
2634
+ ts: Date.now(),
2635
+ path,
2636
+ label,
2637
+ outcome: outcome2
2638
+ });
2639
+ return outcome2;
2640
+ }
2641
+ }
2642
+ if (!result)
2643
+ throw new LoopError({
2644
+ code: "ENGINE",
2645
+ phase: "body",
2646
+ message: `${label} produced no engine result`
2647
+ });
2648
+ const text = scrubCapture(requireFinalResultText(result), env);
2649
+ const outcome = config.outcome ? await config.outcome(text, ctx) : TERMINAL(text);
2650
+ const finalOutcome = result.transportFailure?.kind === "timeout" && outcome.late !== true ? { ...outcome, late: true } : outcome;
2651
+ ctx.emit({
2652
+ kind: "job:end",
2653
+ ts: Date.now(),
2654
+ path,
2655
+ label,
2656
+ outcome: finalOutcome
2657
+ });
2658
+ return finalOutcome;
2659
+ };
2660
+ return setMeta(job, {
2661
+ kind: "agent",
2662
+ name: config.label ?? config.agent?.name ?? "agent",
2663
+ contract: agentContract(config.agent)
2664
+ });
2665
+ }
2666
+ var OUTCOME_STATUSES = /* @__PURE__ */ new Set(["pass", "fail", "aborted", "exhausted", "paused"]);
2667
+ function isOutcome(value) {
2668
+ return typeof value === "object" && value !== null && OUTCOME_STATUSES.has(value.status);
2669
+ }
2670
+ function describeReturn(value) {
2671
+ if (value === null) return "null";
2672
+ if (typeof value === "object") {
2673
+ return `an object with status ${JSON.stringify(value.status)}`;
2674
+ }
2675
+ return `a ${typeof value}`;
2676
+ }
2677
+ function fnJob(label, fn) {
2678
+ const job = async (ctx) => {
2679
+ const path = [...ctx.path];
2680
+ ctx.emit({
2681
+ kind: "job:start",
2682
+ ts: Date.now(),
2683
+ path,
2684
+ label,
2685
+ timeoutMs: ctx.timeoutMs
2686
+ });
2687
+ let outcome;
2688
+ try {
2689
+ const returned = await fn(ctx);
2690
+ if (returned === void 0) {
2691
+ outcome = { status: "pass", summary: label };
2692
+ } else if (typeof returned === "string") {
2693
+ outcome = { status: "pass", summary: returned };
2694
+ } else if (isOutcome(returned)) {
2695
+ outcome = returned;
2696
+ } else {
2697
+ throw new LoopError({
2698
+ code: "VALIDATION",
2699
+ message: `fnJob "${label}" returned ${describeReturn(returned)}, which is not an outcome, a string or nothing`
2700
+ });
2701
+ }
2702
+ } catch (e) {
2703
+ const error = LoopError.from(e, {
2704
+ code: "BODY",
2705
+ phase: "body",
2706
+ path: ctx.path,
2707
+ iteration: ctx.iteration
2708
+ });
2709
+ outcome = { status: "fail", summary: error.message, error };
2710
+ ctx.emit({
2711
+ kind: "error",
2712
+ ts: Date.now(),
2713
+ path,
2714
+ message: error.message,
2715
+ code: error.code
2716
+ });
2717
+ }
2718
+ ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
2719
+ return outcome;
2720
+ };
2721
+ return setMeta(job, { kind: "fn", name: label });
2722
+ }
2723
+ function validateProofArtifact(name, artifact) {
2724
+ if (!artifact || typeof artifact !== "object") {
2725
+ throw new LoopError({
2726
+ code: "VALIDATION",
2727
+ message: `prove "${name}" returned no artifact descriptor`
2728
+ });
2729
+ }
2730
+ if (!["html", "image", "markdown", "table", "json"].includes(artifact.kind)) {
2731
+ throw new LoopError({
2732
+ code: "VALIDATION",
2733
+ message: `prove "${name}" returned unsupported kind "${String(artifact.kind)}"`
2734
+ });
2735
+ }
2736
+ const hasPath = typeof artifact.path === "string" && artifact.path.trim() !== "";
2737
+ const hasData = artifact.data !== void 0;
2738
+ if (hasPath === hasData) {
2739
+ throw new LoopError({
2740
+ code: "VALIDATION",
2741
+ message: `prove "${name}" must return exactly one of path or data`
2742
+ });
2743
+ }
2744
+ if (artifact.data !== void 0 && !isJsonValue(artifact.data)) {
2745
+ throw new LoopError({
2746
+ code: "VALIDATION",
2747
+ message: `prove "${name}" data must be JSON-serializable`
2748
+ });
2749
+ }
2750
+ if (artifact.meta !== void 0 && !isJsonValue(artifact.meta)) {
2751
+ throw new LoopError({
2752
+ code: "VALIDATION",
2753
+ message: `prove "${name}" meta must be JSON-serializable`
2754
+ });
2755
+ }
2756
+ }
2757
+ function isJsonValue(value) {
2758
+ try {
2759
+ cloneFrozenJson(value);
2760
+ return true;
2761
+ } catch {
2762
+ return false;
2763
+ }
2764
+ }
2765
+ function prove(name, producer) {
2766
+ const job = async (ctx) => {
2767
+ const path = [...ctx.path];
2768
+ ctx.emit({
2769
+ kind: "job:start",
2770
+ ts: Date.now(),
2771
+ path,
2772
+ label: name,
2773
+ timeoutMs: ctx.timeoutMs
2774
+ });
2775
+ let outcome;
2776
+ try {
2777
+ const artifact = await producer(ctx);
2778
+ validateProofArtifact(name, artifact);
2779
+ if (artifact.path) {
2780
+ const artifactPath = isAbsolute(artifact.path) ? artifact.path : resolve(ctx.workspace.dir, artifact.path);
2781
+ if (!existsSync(artifactPath)) {
2782
+ throw new LoopError({
2783
+ code: "VALIDATION",
2784
+ message: `prove "${name}" path does not exist: ${artifact.path}`
2785
+ });
2786
+ }
2787
+ }
2788
+ ctx.emit({ kind: "proof", ts: Date.now(), path, name, artifact });
2789
+ outcome = {
2790
+ status: "pass",
2791
+ summary: `proof registered: ${artifact.title ?? name}`,
2792
+ data: { proof: artifact }
2793
+ };
2794
+ } catch (e) {
2795
+ const error = LoopError.from(e, {
2796
+ code: "VALIDATION",
2797
+ phase: "body",
2798
+ path,
2799
+ iteration: ctx.iteration
2800
+ });
2801
+ outcome = { status: "fail", summary: error.message, error };
2802
+ ctx.emit({
2803
+ kind: "error",
2804
+ ts: Date.now(),
2805
+ path,
2806
+ message: error.message,
2807
+ code: error.code
2808
+ });
2809
+ }
2810
+ ctx.emit({ kind: "job:end", ts: Date.now(), path, label: name, outcome });
2811
+ return outcome;
2812
+ };
2813
+ return setMeta(job, { kind: "prove", name });
2814
+ }
2815
+
2816
+ // src/workflow-agent-response.ts
2817
+ var INVALID_TEAM_DECISION = "The engine response was not a valid team decision JSON object.";
2818
+ function firstObject(text) {
2819
+ const start = text.indexOf("{");
2820
+ if (start === -1) return void 0;
2821
+ let depth = 0;
2822
+ let inString = false;
2823
+ let escaped = false;
2824
+ for (let index = start; index < text.length; index += 1) {
2825
+ const character = text[index];
2826
+ if (inString) {
2827
+ if (escaped) escaped = false;
2828
+ else if (character === "\\") escaped = true;
2829
+ else if (character === '"') inString = false;
2830
+ continue;
2831
+ }
2832
+ if (character === '"') inString = true;
2833
+ else if (character === "{") depth += 1;
2834
+ else if (character === "}" && --depth === 0) return text.slice(start, index + 1);
2835
+ }
2836
+ return void 0;
2837
+ }
2838
+ function parseDecision(text) {
2839
+ const trimmed = text.trim();
2840
+ const object = firstObject(trimmed);
2841
+ if (!object) return void 0;
2842
+ try {
2843
+ const value = JSON.parse(object);
2844
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
2845
+ const candidate = value;
2846
+ if (candidate.status !== "pass" && candidate.status !== "revise") return void 0;
2847
+ if (typeof candidate.summary !== "string" || !candidate.summary.trim()) return void 0;
2848
+ if (candidate.findings !== void 0 && !Array.isArray(candidate.findings)) return void 0;
2849
+ return {
2850
+ status: candidate.status,
2851
+ summary: candidate.summary,
2852
+ findings: candidate.findings ? [...candidate.findings] : void 0
2853
+ };
2854
+ } catch {
2855
+ return void 0;
2856
+ }
2857
+ }
2858
+ function outcomeFromAgentText(text, target) {
2859
+ const decision = parseDecision(text);
2860
+ if (!decision) {
2861
+ return {
2862
+ status: "fail",
2863
+ summary: INVALID_TEAM_DECISION,
2864
+ data: { response: text }
2865
+ };
2866
+ }
2867
+ if (decision.status === "pass") {
2868
+ return { status: "pass", summary: decision.summary };
2869
+ }
2870
+ return revisionRequest({
2871
+ target,
2872
+ reason: decision.summary,
2873
+ findings: decision.findings ? [...decision.findings] : void 0
2874
+ });
2875
+ }
2876
+
2877
+ // src/workflow-support.ts
2878
+ function seatIdentity(seat) {
2879
+ const identity = seat.identity;
2880
+ if (!identity || typeof identity.adapter !== "string" || typeof identity.provider !== "string" || typeof identity.modelFamily !== "string" || typeof identity.model !== "string" || !Array.isArray(identity.tools) || !identity.adapter.trim() || !identity.provider.trim() || !identity.model.trim() || !identity.modelFamily.trim()) {
2881
+ throw new TypeError("engine identity must contain adapter, provider, model family, and model");
2882
+ }
2883
+ return identity;
2884
+ }
2885
+ function assertDistinctSeats(seats) {
2886
+ const identities = seats.map((seat, index) => {
2887
+ try {
2888
+ return seatIdentity(seat);
2889
+ } catch (cause) {
2890
+ const message = cause instanceof Error ? cause.message : "invalid engine identity";
2891
+ throw new TypeError(`seat ${index + 1} has an invalid engine identity: ${message}`, { cause });
2892
+ }
2893
+ });
2894
+ for (let leftIndex = 0; leftIndex < identities.length; leftIndex += 1) {
2895
+ for (let rightIndex = leftIndex + 1; rightIndex < identities.length; rightIndex += 1) {
2896
+ const left = identities[leftIndex];
2897
+ const right = identities[rightIndex];
2898
+ const sameFamily = left.modelFamily === right.modelFamily;
2899
+ if (!sameFamily) continue;
2900
+ throw new TypeError(
2901
+ `seats ${leftIndex + 1} and ${rightIndex + 1} report the same model family ${left.modelFamily}; model family must be distinct per seat`
2902
+ );
2903
+ }
2904
+ }
2905
+ }
2906
+ function requireNonEmptyFiles(label, job, workspace, files) {
2907
+ return async (ctx) => {
2908
+ const outcome = await job(ctx);
2909
+ if (outcome.status !== "pass") return outcome;
2910
+ for (const file of files) {
2911
+ const path = join(workspace, file);
2912
+ try {
2913
+ const details = await stat(path);
2914
+ if (!details.isFile() || details.size === 0) {
2915
+ return {
2916
+ status: "fail",
2917
+ summary: `${label} did not produce a non-empty file: ${file}`
2918
+ };
2919
+ }
2920
+ } catch {
2921
+ return {
2922
+ status: "fail",
2923
+ summary: `${label} did not produce a non-empty file: ${file}`
2924
+ };
2925
+ }
2926
+ }
2927
+ return outcome;
2928
+ };
2929
+ }
2930
+ function requireNoFiles(label, job, workspace, files, phase = "review") {
2931
+ const wrapper = async (ctx) => {
2932
+ const before = /* @__PURE__ */ new Map();
2933
+ for (const file of files) {
2934
+ before.set(file, await snapshotFile(join(workspace, file)));
2935
+ }
2936
+ const outcome = await job(ctx);
2937
+ const changed = [];
2938
+ for (const file of files) {
2939
+ const after = await snapshotFile(join(workspace, file));
2940
+ const previous = before.get(file);
2941
+ if (previous.exists !== after.exists || previous.hash !== after.hash) {
2942
+ changed.push(file);
2943
+ }
2944
+ }
2945
+ if (changed.length) {
2946
+ const summary = `${label} wrote, changed or removed the implementation before its step completed: ${changed.join(", ")}`;
2947
+ return {
2948
+ status: "fail",
2949
+ summary,
2950
+ error: new LoopError({ code: "WRITE_BOUNDARY", phase, message: summary })
2951
+ };
2952
+ }
2953
+ return outcome;
2954
+ };
2955
+ return copyJobMeta(wrapper, job);
2956
+ }
2957
+ async function snapshotFile(path) {
2958
+ try {
2959
+ const details = await stat(path);
2960
+ if (!details.isFile()) return { exists: true, hash: null };
2961
+ const contents = await readFile(path);
2962
+ return { exists: true, hash: createHash("sha256").update(contents).digest("hex") };
2963
+ } catch {
2964
+ return { exists: false, hash: null };
2965
+ }
2966
+ }
2967
+ function rolePrompt(role, brief) {
2968
+ return [
2969
+ `Obversa team role: ${role}`,
2970
+ `Work brief: ${brief}`,
2971
+ "Work in the supplied cwd."
2972
+ ].join("\n");
2973
+ }
2974
+ function teamAgent(label, seat, input, instructions, target, decisionFile, workspaceMode = "write", recordAs) {
2975
+ const identity = seatIdentity(seat);
2976
+ if (workspaceMode === "read" && identity.tools.length === 0) {
2977
+ throw new TypeError(`${label} cannot read the workspace: reviewer seat declares no tools`);
2978
+ }
2979
+ const agent = agentJob({
2980
+ label,
2981
+ engine: seat.engine,
2982
+ model: identity.model,
2983
+ tools: [...identity.tools],
2984
+ allowedTools: [...identity.tools],
2985
+ workspaceMode,
2986
+ cwd: input.workspace,
2987
+ consumeFeedback: target !== void 0,
2988
+ ...recordAs === void 0 ? {} : { recordAs },
2989
+ prompt: (ctx) => `${rolePrompt(label, input.brief)}
2990
+ ${typeof instructions === "function" ? instructions(ctx) : instructions}
2991
+ Return one JSON object: {"status":"pass"|"revise","summary":"...","findings":[{"evidence":"..."}]}`,
2992
+ outcome: (text) => outcomeFromAgentText(text, target)
2993
+ });
2994
+ if (!decisionFile) return agent;
2995
+ return async (ctx) => {
2996
+ const path = join(input.workspace, decisionFile);
2997
+ const before = await snapshotFile(path);
2998
+ const replyOutcome = await agent(ctx);
2999
+ const after = await snapshotFile(path);
3000
+ const changed = before.exists !== after.exists || before.hash !== after.hash;
3001
+ if (!changed || !after.exists || after.hash === null) return replyOutcome;
3002
+ try {
3003
+ const fileOutcome = outcomeFromAgentText(await readFile(path, "utf8"), target);
3004
+ return fileOutcome.status === "fail" && fileOutcome.summary === INVALID_TEAM_DECISION ? replyOutcome : fileOutcome;
3005
+ } catch {
3006
+ return replyOutcome;
3007
+ }
3008
+ };
3009
+ }
3010
+ function panelReviewers(reviewers, input, reviewTarget, recordAs) {
3011
+ return reviewers.map((reviewer) => {
3012
+ const instructions = [
3013
+ `Review target: ${reviewTarget ?? "the supplied files and test evidence"}.`,
3014
+ "Judge only that target against this stage gate; do not require files from another stage.",
3015
+ reviewer.scope ? `Scope: ${reviewer.scope}` : void 0
3016
+ ].filter(Boolean).join("\n");
3017
+ const review = teamAgent(
3018
+ reviewer.name,
3019
+ reviewer.seat,
3020
+ input,
3021
+ instructions,
3022
+ void 0,
3023
+ void 0,
3024
+ "read",
3025
+ recordAs
3026
+ );
3027
+ const retry = teamAgent(
3028
+ reviewer.name,
3029
+ reviewer.seat,
3030
+ input,
3031
+ `${instructions}
3032
+ Your previous response was not a valid decision. Return only the required JSON object.`,
3033
+ void 0,
3034
+ void 0,
3035
+ "read",
3036
+ recordAs
3037
+ );
3038
+ const job = async (ctx) => {
3039
+ const first = await review(ctx);
3040
+ if (first.status !== "fail" || first.summary !== INVALID_TEAM_DECISION) return first;
3041
+ const second = await retry(ctx);
3042
+ if (second.status !== "fail" || second.summary !== INVALID_TEAM_DECISION) return second;
3043
+ const summary = `reviewer ${reviewer.name} returned no decision`;
3044
+ return {
3045
+ status: "fail",
3046
+ summary,
3047
+ error: new LoopError({ code: "ENGINE", phase: "review", message: summary })
3048
+ };
3049
+ };
3050
+ return { name: reviewer.name, scope: reviewer.scope, job };
3051
+ });
3052
+ }
3053
+
3054
+ export { Budget, DEFAULT_FANOUT_CONCURRENCY, INVALID_TEAM_DECISION, LoopError, RECORDED_ENGINE_USAGE, addWorktree, agentCheck, agentJob, all, always, any, approval, assertDistinctSeats, bodyPassed, branchCommits, captureGitWorkspaceSnapshot, childContext, commandJob, commandSucceeds, commit, compareGitWorkspaceSnapshots, copyJobMeta, currentBranch, defineAgent, defineSkill, delegateNodeJob, deleteBranch, describeConditions, failed, fnJob, fromFile, gateJob, gitRoot, hasStagedChanges, headSha, isInfrastructureError, isRepo, jobMeta, kickback, mergeAbort, mergeBranch, mergeNoCommit, minConfidence, needDecisionsOf, never, nodeJobContext, not, outcomeFromAgentText, panelReviewers, passed, predicate, prepareCondition, prove, quorum, removeWorktree, renderPlan, requireNoFiles, requireNonEmptyFiles, resolveEnv, reviewContext, reviewPanel, revisionFromOutcome, revisionRequest, seatIdentity, setLabel, setMeta, stageAll, teamAgent, toCondition, withConditionPreparation, withEnv, workspaceFingerprint };
3055
+ //# sourceMappingURL=chunk-DV5P4QLI.js.map
3056
+ //# sourceMappingURL=chunk-DV5P4QLI.js.map