@loops-adk/core 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { redactSecrets } from './chunk-JFTXJ7I2.js';
2
- import { isEngine } from './chunk-XC46B4FD.js';
2
+ import { isEngine } from './chunk-MA6NDQMO.js';
3
3
  import { isLimitError, waitMsFor } from './chunk-Y2SD7GBL.js';
4
4
  import { LoopError } from './chunk-I3STY7U6.js';
5
5
  import { readFileSync, mkdtempSync, existsSync, writeFileSync, appendFileSync, readdirSync, mkdirSync, rmSync } from 'fs';
@@ -35,6 +35,19 @@ function describeConditions(input) {
35
35
  return [condLabel(input)];
36
36
  }
37
37
  var count = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
38
+ function renderContract(value) {
39
+ const c = value;
40
+ if (!c || typeof c !== "object") return void 0;
41
+ const bits = [];
42
+ if (c.tier) bits.push(`tier ${c.tier}`);
43
+ if (c.outputs?.length) bits.push(`outputs ${c.outputs.join(", ")}`);
44
+ if (c.capabilities?.length) bits.push(`capabilities ${c.capabilities.join(", ")}`);
45
+ if (c.requiresSkills?.length) bits.push(`requires ${c.requiresSkills.join(", ")}`);
46
+ if (c.usesSkills?.length) bits.push(`uses ${c.usesSkills.join(", ")}`);
47
+ if (c.humanGates?.length) bits.push(`gates ${c.humanGates.join(", ")}`);
48
+ if (c.failureModes?.length) bits.push(`failure modes ${c.failureModes.join(", ")}`);
49
+ return bits.join("; ");
50
+ }
38
51
  function renderPlan(meta, indent = "") {
39
52
  if (!meta) return [`${indent}(a runnable job, shape not introspectable)`];
40
53
  const nm = meta.name ? ` "${meta.name}"` : "";
@@ -69,6 +82,10 @@ function renderPlan(meta, indent = "") {
69
82
  }
70
83
  case "agent":
71
84
  out.push(`${indent}agent${nm}${meta.ground ? " (grounded)" : ""}`);
85
+ {
86
+ const contract = renderContract(meta.contract);
87
+ if (contract) out.push(`${indent} contract: ${contract}`);
88
+ }
72
89
  break;
73
90
  case "fn":
74
91
  out.push(`${indent}fn${nm}`);
@@ -129,12 +146,59 @@ function defineSkill(skill) {
129
146
  if (!skill.instructions?.trim()) throw new Error(`defineSkill "${skill.name}": empty instructions`);
130
147
  return skill;
131
148
  }
149
+ function skillRefName(ref) {
150
+ return typeof ref === "string" ? ref : ref.name;
151
+ }
152
+ function validateName(value, label) {
153
+ if (!value?.trim()) throw new Error(`${label}: \`name\` is required`);
154
+ }
155
+ function validateSkillRef(ref, label) {
156
+ if (typeof ref === "string") {
157
+ if (!ref.trim()) throw new Error(`${label}: empty skill name`);
158
+ return;
159
+ }
160
+ defineSkill(ref);
161
+ }
132
162
  function defineAgent(def) {
133
163
  if (!def.name) throw new Error("defineAgent: `name` is required");
134
164
  if (!def.system?.trim()) throw new Error(`defineAgent "${def.name}": empty system prompt`);
135
165
  def.skills?.forEach((s) => defineSkill(s));
166
+ def.requiresSkills?.forEach(
167
+ (s) => validateSkillRef(s, `defineAgent "${def.name}" requiresSkills`)
168
+ );
169
+ def.usesSkills?.forEach(
170
+ (s) => validateSkillRef(s, `defineAgent "${def.name}" usesSkills`)
171
+ );
172
+ def.outputs?.forEach(
173
+ (o) => validateName(o.name, `defineAgent "${def.name}" outputs`)
174
+ );
175
+ def.humanGates?.forEach(
176
+ (g) => validateName(g.name, `defineAgent "${def.name}" humanGates`)
177
+ );
178
+ def.failureModes?.forEach((f) => {
179
+ if (!f.mode?.trim())
180
+ throw new Error(`defineAgent "${def.name}" failureModes: \`mode\` is required`);
181
+ if (!f.recovery?.trim())
182
+ throw new Error(`defineAgent "${def.name}" failureModes "${f.mode}": \`recovery\` is required`);
183
+ });
136
184
  return def;
137
185
  }
186
+ function agentContract(agent) {
187
+ if (!agent) return void 0;
188
+ const summary = {};
189
+ if (agent.tier) summary.tier = agent.tier;
190
+ if (agent.capabilities?.length) summary.capabilities = [...agent.capabilities];
191
+ if (agent.outputs?.length) summary.outputs = agent.outputs.map((o) => o.name);
192
+ if (agent.requiresSkills?.length)
193
+ summary.requiresSkills = agent.requiresSkills.map(skillRefName);
194
+ if (agent.usesSkills?.length)
195
+ summary.usesSkills = agent.usesSkills.map(skillRefName);
196
+ if (agent.humanGates?.length)
197
+ summary.humanGates = agent.humanGates.map((g) => g.name);
198
+ if (agent.failureModes?.length)
199
+ summary.failureModes = agent.failureModes.map((f) => f.mode);
200
+ return Object.keys(summary).length ? summary : void 0;
201
+ }
138
202
  function resolveSystem(agent) {
139
203
  if (!agent.skills?.length) return agent.system;
140
204
  const methods = agent.skills.map((s) => `### ${s.name}
@@ -928,6 +992,15 @@ function resetLedger(workspace) {
928
992
  reset(ledgerPath(workspace));
929
993
  }
930
994
 
995
+ // src/core/text.ts
996
+ function oneLine(text) {
997
+ return text.replace(/\s+/g, " ").trim();
998
+ }
999
+ function truncate(s, max) {
1000
+ return s.length > max ? `${s.slice(0, max).trimEnd()}
1001
+ \u2026` : s;
1002
+ }
1003
+
931
1004
  // src/core/consolidate.ts
932
1005
  var CONSOLIDATE_SYSTEM = "You maintain a project's CONSOLIDATED LEDGER from its commit history \u2014 the bounded coarse memory a fresh context reads to continue safely. Capture the current state and the open threads, and PRESERVE every binding decision, convention and constraint with its exact values verbatim (downstream work must honour them, so dropping or generalising even one is a failure). Tight markdown; MERGE new commits into the prior ledger, deduplicate, omit only narrative \u2014 never omit a decision.";
933
1006
  function digest(body, n = 280) {
@@ -966,11 +1039,6 @@ Output the updated consolidated ledger.`,
966
1039
  return result.text.trim();
967
1040
  }
968
1041
  var COMPACT_SYSTEM = "You write the HANDOFF a future agent reads if it lost ALL memory of this work. Include EVERYTHING it needs to continue safely, as structured markdown: ## Why (the problem and the root cause), ## What (exactly what changed, and where \u2014 names, paths, signatures), ## Alternatives (what was ruled out and why), ## Constraints (the invariants and limits that shaped it), ## Next (what is left or to watch). Preserve every decision and specific value verbatim. Completeness matters more than brevity \u2014 drop only literal repetition and play-by-play narration, never a decision or a detail. Omit a section only if it truly has nothing. No preamble.";
969
- function truncate(s, n) {
970
- const t = s.trim();
971
- return t.length > n ? `${t.slice(0, n).trimEnd()}
972
- \u2026` : t;
973
- }
974
1042
  async function compactLedger(ctx, text, opts = {}) {
975
1043
  const trimmed = text.trim();
976
1044
  if (!trimmed) return "";
@@ -1039,10 +1107,6 @@ function consolidateJob(config = {}) {
1039
1107
  }
1040
1108
 
1041
1109
  // src/core/ground.ts
1042
- function truncate2(s, n) {
1043
- return s.length > n ? `${s.slice(0, n).trimEnd()}
1044
- \u2026` : s;
1045
- }
1046
1110
  async function groundingText(workspace, opts = {}) {
1047
1111
  const records = await log({
1048
1112
  cwd: workspace.dir,
@@ -1059,7 +1123,7 @@ What prior iterations already did and why \u2014 read it before working so you d
1059
1123
  const head = `### ${r.sha.slice(0, 7)} ${r.subject}`;
1060
1124
  return r.body ? `${head}
1061
1125
 
1062
- ${truncate2(r.body, bodyChars)}` : head;
1126
+ ${truncate(r.body, bodyChars)}` : head;
1063
1127
  });
1064
1128
  return `${header}
1065
1129
 
@@ -1115,12 +1179,305 @@ Commits a search judged relevant \u2014 read them before working.`;
1115
1179
  const head = `### ${r.sha.slice(0, 7)} ${r.subject}`;
1116
1180
  return r.body ? `${head}
1117
1181
 
1118
- ${truncate2(r.body, bodyChars)}` : head;
1182
+ ${truncate(r.body, bodyChars)}` : head;
1119
1183
  });
1120
1184
  return `${header}
1121
1185
 
1122
1186
  ${entries.join("\n\n")}`;
1123
1187
  }
1188
+ function normalizeFeedbackSeverity(severity) {
1189
+ switch (severity) {
1190
+ case "advisory":
1191
+ return "nice-to-have";
1192
+ case "blocking":
1193
+ case void 0:
1194
+ return "block";
1195
+ default:
1196
+ return severity;
1197
+ }
1198
+ }
1199
+ function isRequiredFeedbackSeverity(severity) {
1200
+ const normalized = normalizeFeedbackSeverity(severity);
1201
+ return normalized === "block" || normalized === "should-fix";
1202
+ }
1203
+ function findingLine(finding) {
1204
+ const reviewer = finding.reviewer ? `${finding.reviewer} ` : "";
1205
+ const severity = normalizeFeedbackSeverity(finding.severity);
1206
+ const decision = finding.decision ? ` Decision: ${finding.decision}.` : "";
1207
+ const recommendation = finding.recommendation ? ` Recommendation: ${oneLine(finding.recommendation)}` : "";
1208
+ return `- ${reviewer}[${severity}]: ${oneLine(finding.evidence)}${decision}${recommendation}`;
1209
+ }
1210
+ function defaultReason(findings) {
1211
+ if (!findings?.length) return "Revision requested";
1212
+ if (findings.length === 1) return oneLine(findings[0].evidence);
1213
+ return `${findings.length} findings require another pass`;
1214
+ }
1215
+ function normalizeRevision(input) {
1216
+ const reason = input.reason?.trim() || defaultReason(input.findings);
1217
+ return {
1218
+ reason,
1219
+ target: input.target,
1220
+ findings: input.findings,
1221
+ rerun: input.rerun ?? (input.target ? "target-and-dependents" : void 0),
1222
+ source: input.source,
1223
+ decision: input.decision
1224
+ };
1225
+ }
1226
+ function revisionRequest(input, over = {}) {
1227
+ const revision = normalizeRevision(input);
1228
+ return {
1229
+ status: over.status ?? "fail",
1230
+ confidence: over.confidence,
1231
+ summary: over.summary ?? revision.reason,
1232
+ data: over.data,
1233
+ error: over.error,
1234
+ revision
1235
+ };
1236
+ }
1237
+ function kickback(to, reason, over = {}) {
1238
+ return revisionRequest({ target: to, reason }, over);
1239
+ }
1240
+ function revisionFromOutcome(outcome) {
1241
+ return outcome.revision;
1242
+ }
1243
+ function feedbackBlock(outcome) {
1244
+ const revision = revisionFromOutcome(outcome);
1245
+ const parts = [
1246
+ "## Feedback to address",
1247
+ "A review or downstream stage requested another pass. Address this before unrelated work."
1248
+ ];
1249
+ if (revision?.target) parts.push(`Target: ${revision.target}`);
1250
+ if (revision?.source) parts.push(`Source: ${revision.source}`);
1251
+ if (revision?.decision) parts.push(`Caller decision: ${revision.decision}`);
1252
+ const reason = revision?.reason ?? outcome.summary;
1253
+ if (reason) parts.push(`Reason: ${reason}`);
1254
+ const findings = revision?.findings ?? [];
1255
+ if (findings.length) {
1256
+ parts.push("Findings:");
1257
+ parts.push(findings.map(findingLine).join("\n"));
1258
+ }
1259
+ return parts.join("\n\n");
1260
+ }
1261
+ function graphPositionBlock(graph) {
1262
+ return [
1263
+ "## Graph position",
1264
+ `DAG: ${graph.dag}`,
1265
+ `Current node: ${graph.node}`,
1266
+ `Path: ${graph.path.join(" > ")}`,
1267
+ `Depends on: ${graph.needs.length ? graph.needs.join(", ") : "none"}`,
1268
+ `Direct dependents: ${graph.dependents.length ? graph.dependents.join(", ") : "none"}`
1269
+ ].join("\n");
1270
+ }
1271
+ async function runReviewer(reviewer, index, ctx) {
1272
+ const name = reviewer.name ?? `reviewer-${index + 1}`;
1273
+ try {
1274
+ if ("job" in reviewer) {
1275
+ const outcome = await reviewer.job(ctx);
1276
+ return {
1277
+ name,
1278
+ met: outcome.status === "pass",
1279
+ confidence: outcome.confidence,
1280
+ reason: outcome.summary ?? outcome.status
1281
+ };
1282
+ }
1283
+ const result = await toCondition(reviewer.review)(
1284
+ ctx,
1285
+ ctx.lastOutcome
1286
+ );
1287
+ return {
1288
+ name,
1289
+ met: result.met,
1290
+ confidence: result.confidence,
1291
+ reason: result.reason
1292
+ };
1293
+ } catch (e) {
1294
+ if (ctx.signal.aborted) throw e;
1295
+ return {
1296
+ name,
1297
+ met: false,
1298
+ reason: `reviewer errored: ${e instanceof Error ? e.message : String(e)}`
1299
+ };
1300
+ }
1301
+ }
1302
+ function reviewFinding(result) {
1303
+ return {
1304
+ reviewer: result.name,
1305
+ severity: "block",
1306
+ evidence: result.reason
1307
+ };
1308
+ }
1309
+ function findingSeverityCounts(findings) {
1310
+ const counts = {};
1311
+ for (const finding of findings) {
1312
+ const severity = normalizeFeedbackSeverity(finding.severity);
1313
+ counts[severity] = (counts[severity] ?? 0) + 1;
1314
+ }
1315
+ return counts;
1316
+ }
1317
+ function reviewPanel(config) {
1318
+ const label = config.label ?? "review-panel";
1319
+ if (!config.reviewers.length)
1320
+ throw new LoopError({
1321
+ code: "CONFIG",
1322
+ message: `reviewPanel "${label}": at least one reviewer is required`
1323
+ });
1324
+ const job = async (ctx) => {
1325
+ ctx.emit({ kind: "job:start", ts: Date.now(), path: [...ctx.path], label });
1326
+ const results = await Promise.all(
1327
+ config.reviewers.map((reviewer, i) => runReviewer(reviewer, i, ctx))
1328
+ );
1329
+ const passedCount = results.filter((r) => r.met).length;
1330
+ const required = config.pass === void 0 || config.pass === "all" ? results.length : config.pass;
1331
+ const findings = results.filter((r) => !r.met).map(reviewFinding);
1332
+ const passed = passedCount >= required;
1333
+ const summaryHead = `Review panel: ${passedCount}/${results.length} reviewer(s) cleared`;
1334
+ const summary = findings.length ? `${summaryHead}.
1335
+ ${findings.map(findingLine).join("\n")}` : `${summaryHead}.`;
1336
+ const scored = results.map((r) => r.confidence).filter((c) => c != null);
1337
+ const confidence = scored.length ? scored.reduce((sum, c) => sum + c, 0) / scored.length : void 0;
1338
+ const data = {
1339
+ findings,
1340
+ results,
1341
+ passed: passedCount,
1342
+ required,
1343
+ severityCounts: findingSeverityCounts(findings)
1344
+ };
1345
+ const outcome = passed ? { status: "pass", summary, confidence, data } : revisionRequest(
1346
+ {
1347
+ target: config.target,
1348
+ // A clean one-line reason. The findings ride the `findings` array, so
1349
+ // feedbackBlock renders them once (not embedded in the reason too) and
1350
+ // the records/tail `reason` stays a single tidy line. The full
1351
+ // multi-line `summary` is kept on the outcome below for logs/TUI.
1352
+ reason: `${summaryHead}.`,
1353
+ findings,
1354
+ rerun: config.rerun
1355
+ },
1356
+ { summary, confidence, data }
1357
+ );
1358
+ ctx.emit({ kind: "job:end", ts: Date.now(), path: [...ctx.path], label, outcome });
1359
+ return outcome;
1360
+ };
1361
+ return setMeta(job, { kind: "reviewPanel", name: label });
1362
+ }
1363
+ async function gitOutput(cwd, args, signal) {
1364
+ const out = await execa("git", args, {
1365
+ cwd,
1366
+ reject: false,
1367
+ stripFinalNewline: false,
1368
+ cancelSignal: signal
1369
+ });
1370
+ return out.stdout.trim();
1371
+ }
1372
+ async function resolveFiles(ctx, patterns) {
1373
+ const fromGit = await gitOutput(
1374
+ ctx.workspace.dir,
1375
+ ["ls-files", "--", ...patterns],
1376
+ ctx.signal
1377
+ ).catch(() => "");
1378
+ const files = fromGit ? fromGit.split("\n").filter(Boolean) : [];
1379
+ if (files.length) return files;
1380
+ return patterns.filter((p) => existsSync(join(ctx.workspace.dir, p)));
1381
+ }
1382
+ function reviewContext(config) {
1383
+ return async (ctx, last) => {
1384
+ const max = config.maxChars ?? 6e3;
1385
+ const buildTests = async () => {
1386
+ if (!config.tests) return [];
1387
+ if (config.tests === true) {
1388
+ const lines = [];
1389
+ if (last?.status) lines.push(`Last outcome status: ${last.status}`);
1390
+ if (last?.summary) lines.push(`Last outcome summary: ${last.summary}`);
1391
+ if (last?.data !== void 0) {
1392
+ let rendered;
1393
+ try {
1394
+ rendered = JSON.stringify(last.data, null, 2);
1395
+ } catch {
1396
+ rendered = String(last.data);
1397
+ }
1398
+ lines.push(`Last outcome data: ${rendered}`);
1399
+ }
1400
+ return lines.length ? [`## Test and outcome context
1401
+
1402
+ ${lines.join("\n")}`] : [];
1403
+ }
1404
+ const cwd = config.tests.cwd ?? ctx.workspace.dir;
1405
+ const result = await execa(
1406
+ config.tests.command,
1407
+ config.tests.args ?? [],
1408
+ { cwd, reject: false, stripFinalNewline: false, cancelSignal: ctx.signal }
1409
+ ).catch((e) => {
1410
+ if (ctx.signal.aborted) throw e;
1411
+ return {
1412
+ exitCode: void 0,
1413
+ stdout: "",
1414
+ stderr: e instanceof Error ? e.message : String(e)
1415
+ };
1416
+ });
1417
+ const exit = result.exitCode ?? "(command did not run)";
1418
+ return [
1419
+ `## Test command
1420
+
1421
+ ${config.tests.command} ${(config.tests.args ?? []).join(" ")}
1422
+
1423
+ exit: ${exit}
1424
+
1425
+ stdout:
1426
+ ${truncate(result.stdout ?? "", max)}
1427
+
1428
+ stderr:
1429
+ ${truncate(result.stderr ?? "", max)}`
1430
+ ];
1431
+ };
1432
+ const buildDiff = async () => {
1433
+ if (!config.diff) return [];
1434
+ const diff2 = await gitOutput(
1435
+ ctx.workspace.dir,
1436
+ ["diff", "HEAD", "--"],
1437
+ ctx.signal
1438
+ ).catch(() => "");
1439
+ return diff2 ? [`## Git diff
1440
+
1441
+ ${truncate(diff2, max)}`] : [];
1442
+ };
1443
+ const buildFiles = async () => {
1444
+ if (!config.files?.length) return [];
1445
+ const files2 = await resolveFiles(ctx, config.files);
1446
+ const out = [];
1447
+ for (const file of files2) {
1448
+ const path = join(ctx.workspace.dir, file);
1449
+ if (!existsSync(path)) continue;
1450
+ out.push(`## File: ${file}
1451
+
1452
+ ${truncate(readFileSync(path, "utf8"), max)}`);
1453
+ }
1454
+ return out;
1455
+ };
1456
+ const buildLedger = async () => {
1457
+ if (!config.ledger) return [];
1458
+ const out = [];
1459
+ const live = [readPrompt(ctx.workspace), readLedger(ctx.workspace)].filter(Boolean).join("\n\n");
1460
+ if (live) out.push(`## Live ledger
1461
+
1462
+ ${truncate(live, max)}`);
1463
+ const committed = await groundingText(ctx.workspace, {
1464
+ max: 5,
1465
+ bodyChars: 1200,
1466
+ signal: ctx.signal
1467
+ }).catch(() => "");
1468
+ if (committed) out.push(truncate(committed, max));
1469
+ return out;
1470
+ };
1471
+ const [tests, diff, files, ledger] = await Promise.all([
1472
+ buildTests(),
1473
+ buildDiff(),
1474
+ buildFiles(),
1475
+ buildLedger()
1476
+ ]);
1477
+ const sections = [...tests, ...diff, ...files, ...ledger];
1478
+ return sections.join("\n\n---\n\n") || "(no review context)";
1479
+ };
1480
+ }
1124
1481
 
1125
1482
  // src/core/job.ts
1126
1483
  var HANDOFF_MARK = "===HANDOFF===";
@@ -1198,6 +1555,16 @@ var TERMINAL = (text) => ({
1198
1555
  summary: text.trim().slice(0, 280),
1199
1556
  data: text
1200
1557
  });
1558
+ function withOperationalContext(ctx, userPrompt, config) {
1559
+ const parts = [userPrompt];
1560
+ if (config.consumeFeedback && ctx.lastReview) {
1561
+ parts.push(feedbackBlock(ctx.lastReview));
1562
+ }
1563
+ if (config.graphContext && ctx.graph) {
1564
+ parts.push(graphPositionBlock(ctx.graph));
1565
+ }
1566
+ return parts.join("\n\n---\n\n");
1567
+ }
1201
1568
  function agentJob(config) {
1202
1569
  const job = async (ctx) => {
1203
1570
  const path = [...ctx.path];
@@ -1205,7 +1572,8 @@ function agentJob(config) {
1205
1572
  ctx.emit({ kind: "job:start", ts: Date.now(), path, label });
1206
1573
  const engine = ctx.resolveEngine(config.engine);
1207
1574
  const userPrompt = typeof config.prompt === "function" ? await config.prompt(ctx) : config.prompt;
1208
- const prompt = config.ground ? await withGrounding(ctx, userPrompt, config.ground) : userPrompt;
1575
+ const contextualPrompt = withOperationalContext(ctx, userPrompt, config);
1576
+ const prompt = config.ground ? await withGrounding(ctx, contextualPrompt, config.ground) : contextualPrompt;
1209
1577
  const system = config.system !== void 0 ? typeof config.system === "function" ? config.system(ctx) : config.system : config.agent ? resolveSystem(config.agent) : void 0;
1210
1578
  let result;
1211
1579
  const toolUses = /* @__PURE__ */ new Map();
@@ -1306,7 +1674,8 @@ function agentJob(config) {
1306
1674
  return setMeta(job, {
1307
1675
  kind: "agent",
1308
1676
  name: config.label ?? config.agent?.name ?? "agent",
1309
- ground: !!config.ground
1677
+ ground: !!config.ground,
1678
+ contract: agentContract(config.agent)
1310
1679
  });
1311
1680
  }
1312
1681
  function composeWay(ctx, last) {
@@ -1382,14 +1751,6 @@ function commitJob(config) {
1382
1751
  }
1383
1752
  };
1384
1753
  }
1385
- function kickback(to, reason, over) {
1386
- return {
1387
- status: "fail",
1388
- summary: reason,
1389
- ...over,
1390
- kickback: { to, reason }
1391
- };
1392
- }
1393
1754
  function fnJob(label, fn) {
1394
1755
  const job = async (ctx) => {
1395
1756
  const path = [...ctx.path];
@@ -1439,6 +1800,7 @@ function childContext(parent, over) {
1439
1800
  log: parent.log,
1440
1801
  depth: over.depth,
1441
1802
  path: over.path,
1803
+ graph: over.graph ?? parent.graph,
1442
1804
  // Inherit the enclosing iteration by default. A `loop` always passes one
1443
1805
  // explicitly; a `dag`/`sequence` does not, so without this a node nested in a
1444
1806
  // loop would reset to 0 — the "Attempt 0" confound where a retry body could not
@@ -1746,13 +2108,18 @@ function loop(config) {
1746
2108
  iteration
1747
2109
  });
1748
2110
  }
2111
+ const reviewPassed = reviewOutcome.status === "pass";
2112
+ const restartsExhausted = config.maxReviewRestarts != null && consecutiveReviewFails + 1 >= config.maxReviewRestarts;
2113
+ const iterationsRemain = config.max == null || iteration < config.max;
2114
+ const willReenter = !reviewPassed && !restartsExhausted && iterationsRemain;
1749
2115
  parent.emit({
1750
2116
  kind: "loop:review",
1751
2117
  ts: ts(),
1752
2118
  path,
1753
- outcome: reviewOutcome
2119
+ outcome: reviewOutcome,
2120
+ accepted: willReenter
1754
2121
  });
1755
- if (reviewOutcome.status === "pass") {
2122
+ if (reviewPassed) {
1756
2123
  await recordMilestone(ctxAt(iteration, last));
1757
2124
  return finish(
1758
2125
  {
@@ -1770,7 +2137,7 @@ function loop(config) {
1770
2137
  `review did not pass (${reviewOutcome.summary ?? reviewOutcome.status}); re-entering ${config.name}`,
1771
2138
  "warn"
1772
2139
  );
1773
- if (config.maxReviewRestarts != null && consecutiveReviewFails >= config.maxReviewRestarts) {
2140
+ if (restartsExhausted) {
1774
2141
  return finish(
1775
2142
  {
1776
2143
  status: "exhausted",
@@ -1866,14 +2233,14 @@ var EngineRegistry = class {
1866
2233
  this.register(
1867
2234
  "agent-sdk",
1868
2235
  (o) => lazy(
1869
- () => import('./agent-sdk-RF5VJZAT.js').then((m) => new m.AgentSdkEngine(o)),
2236
+ () => import('./agent-sdk-4QJDWM7N.js').then((m) => new m.AgentSdkEngine(o)),
1870
2237
  "agent-sdk"
1871
2238
  )
1872
2239
  );
1873
2240
  this.register(
1874
2241
  "claude-cli",
1875
2242
  (o) => lazy(
1876
- () => import('./claude-cli-U7WEVAOL.js').then((m) => new m.ClaudeCliEngine(o)),
2243
+ () => import('./claude-cli-75AOQUKG.js').then((m) => new m.ClaudeCliEngine(o)),
1877
2244
  "claude-cli"
1878
2245
  )
1879
2246
  );
@@ -1886,7 +2253,7 @@ var EngineRegistry = class {
1886
2253
  );
1887
2254
  this.register(
1888
2255
  "codex",
1889
- (o) => lazy(() => import('./codex-6I5UZ2HM.js').then((m) => new m.CodexEngine(o)), "codex")
2256
+ (o) => lazy(() => import('./codex-LYZF52WL.js').then((m) => new m.CodexEngine(o)), "codex")
1890
2257
  );
1891
2258
  }
1892
2259
  };
@@ -1972,6 +2339,188 @@ var Stats = class {
1972
2339
  return m;
1973
2340
  }
1974
2341
  };
2342
+ function ensureDir2(path) {
2343
+ const dir = dirname(path);
2344
+ if (dir && dir !== ".") mkdirSync(dir, { recursive: true });
2345
+ }
2346
+ function outcomeSummary(outcome) {
2347
+ return {
2348
+ status: outcome.status,
2349
+ summary: outcome.summary,
2350
+ confidence: outcome.confidence
2351
+ };
2352
+ }
2353
+ function strongestFinding(findings) {
2354
+ if (!findings?.length) return void 0;
2355
+ const severities = findings.map((f) => normalizeFeedbackSeverity(f.severity));
2356
+ if (severities.includes("block")) return "block";
2357
+ if (severities.includes("should-fix")) return "should-fix";
2358
+ if (severities.includes("nice-to-have")) return "nice-to-have";
2359
+ if (severities.includes("approve")) return "approve";
2360
+ return void 0;
2361
+ }
2362
+ function emittedRevisionRecord(event, outcome) {
2363
+ const revision = revisionFromOutcome(outcome);
2364
+ return revision ? [
2365
+ {
2366
+ kind: "revision-emitted",
2367
+ ts: event.ts,
2368
+ path: event.path,
2369
+ sourceEvent: "job:end",
2370
+ revision
2371
+ }
2372
+ ] : [];
2373
+ }
2374
+ function semanticRecordsFromEvent(event) {
2375
+ switch (event.kind) {
2376
+ case "job:start":
2377
+ return [
2378
+ {
2379
+ kind: "dispatch",
2380
+ ts: event.ts,
2381
+ path: event.path,
2382
+ unit: "job",
2383
+ label: event.label
2384
+ }
2385
+ ];
2386
+ case "dag:node":
2387
+ if (event.phase === "start")
2388
+ return [
2389
+ {
2390
+ kind: "dispatch",
2391
+ ts: event.ts,
2392
+ path: [...event.path, event.node],
2393
+ unit: "dag-node",
2394
+ node: event.node,
2395
+ attempt: event.attempt
2396
+ }
2397
+ ];
2398
+ return event.outcome ? [
2399
+ {
2400
+ kind: "completion",
2401
+ ts: event.ts,
2402
+ path: [...event.path, event.node],
2403
+ unit: "dag-node",
2404
+ label: event.node,
2405
+ outcome: outcomeSummary(event.outcome),
2406
+ attempt: event.attempt
2407
+ }
2408
+ ] : [];
2409
+ case "job:end":
2410
+ return [
2411
+ {
2412
+ kind: "completion",
2413
+ ts: event.ts,
2414
+ path: event.path,
2415
+ unit: "job",
2416
+ label: event.label,
2417
+ outcome: outcomeSummary(event.outcome)
2418
+ },
2419
+ ...emittedRevisionRecord(event, event.outcome)
2420
+ ];
2421
+ case "loop:review": {
2422
+ if (event.outcome.status === "pass") return [];
2423
+ const revision = revisionFromOutcome(event.outcome);
2424
+ const decision = event.accepted === false ? "rejected" : "accepted";
2425
+ const records = [
2426
+ {
2427
+ kind: "surfacing",
2428
+ ts: event.ts,
2429
+ path: event.path,
2430
+ source: "loop-review",
2431
+ decision,
2432
+ severity: strongestFinding(revision?.findings),
2433
+ reason: revision?.reason ?? event.outcome.summary ?? event.outcome.status
2434
+ }
2435
+ ];
2436
+ if (revision) {
2437
+ records.push({
2438
+ kind: "revision-routed",
2439
+ ts: event.ts,
2440
+ path: event.path,
2441
+ sourceEvent: "loop:review",
2442
+ decision,
2443
+ revision
2444
+ });
2445
+ }
2446
+ return records;
2447
+ }
2448
+ case "loop:end":
2449
+ return [
2450
+ {
2451
+ kind: "completion",
2452
+ ts: event.ts,
2453
+ path: event.path,
2454
+ unit: "loop",
2455
+ outcome: outcomeSummary(event.outcome),
2456
+ iterations: event.iterations
2457
+ }
2458
+ ];
2459
+ case "dag:kickback": {
2460
+ const at = [...event.path, event.to];
2461
+ const decision = event.accepted ? "accepted" : "rejected";
2462
+ return [
2463
+ {
2464
+ kind: "surfacing",
2465
+ ts: event.ts,
2466
+ path: at,
2467
+ source: "dag-kickback",
2468
+ decision,
2469
+ severity: "block",
2470
+ from: event.from,
2471
+ to: event.to,
2472
+ reason: event.reason,
2473
+ note: event.note
2474
+ },
2475
+ {
2476
+ kind: "revision-routed",
2477
+ ts: event.ts,
2478
+ path: at,
2479
+ sourceEvent: "dag:kickback",
2480
+ decision,
2481
+ revision: {
2482
+ target: event.to,
2483
+ reason: event.reason,
2484
+ source: event.from,
2485
+ rerun: event.accepted ? "target-and-dependents" : void 0
2486
+ }
2487
+ }
2488
+ ];
2489
+ }
2490
+ case "dag:end":
2491
+ return [
2492
+ {
2493
+ kind: "completion",
2494
+ ts: event.ts,
2495
+ path: event.path,
2496
+ unit: "dag",
2497
+ outcome: outcomeSummary(event.outcome)
2498
+ }
2499
+ ];
2500
+ default:
2501
+ return [];
2502
+ }
2503
+ }
2504
+ function makeSemanticRecorder(path) {
2505
+ try {
2506
+ ensureDir2(path);
2507
+ writeFileSync(path, "");
2508
+ } catch {
2509
+ return () => {
2510
+ };
2511
+ }
2512
+ return (event) => {
2513
+ const records = semanticRecordsFromEvent(event);
2514
+ if (!records.length) return;
2515
+ try {
2516
+ for (const record of records) {
2517
+ appendFileSync(path, `${JSON.stringify(record)}
2518
+ `);
2519
+ }
2520
+ } catch {
2521
+ }
2522
+ };
2523
+ }
1975
2524
  var NOISE = /* @__PURE__ */ new Set([
1976
2525
  "engine:text",
1977
2526
  "engine:thinking"
@@ -1990,11 +2539,13 @@ function startSupervisor(input) {
1990
2539
  const dir = join(runsHome(), input.runId);
1991
2540
  mkdirSync(dir, { recursive: true });
1992
2541
  const eventsPath = join(dir, "events.jsonl");
2542
+ const semanticPath = join(dir, "semantic.jsonl");
1993
2543
  const statusPath = join(dir, "status.json");
1994
2544
  try {
1995
2545
  writeFileSync(eventsPath, "");
1996
2546
  } catch {
1997
2547
  }
2548
+ const semanticSink = makeSemanticRecorder(semanticPath);
1998
2549
  const status = {
1999
2550
  runId: input.runId,
2000
2551
  pid: process.pid,
@@ -2025,6 +2576,7 @@ function startSupervisor(input) {
2025
2576
  `);
2026
2577
  } catch {
2027
2578
  }
2579
+ semanticSink(event);
2028
2580
  }
2029
2581
  switch (event.kind) {
2030
2582
  case "loop:iteration":
@@ -2101,6 +2653,9 @@ function listRuns() {
2101
2653
  function runEventsPath(runId) {
2102
2654
  return join(runsHome(), runId, "events.jsonl");
2103
2655
  }
2656
+ function runSemanticRecordsPath(runId) {
2657
+ return join(runsHome(), runId, "semantic.jsonl");
2658
+ }
2104
2659
  function formatEvent(event) {
2105
2660
  const at = event.path.length ? `${event.path.join(" \u203A ")} ` : "";
2106
2661
  switch (event.kind) {
@@ -2118,6 +2673,8 @@ function formatEvent(event) {
2118
2673
  return `${at}\u25C2 ${event.outcome.status} (${event.iterations} iter)`;
2119
2674
  case "dag:node":
2120
2675
  return `${at}\xB7 node ${event.node}: ${event.phase}${event.outcome ? ` (${event.outcome.status})` : ""}`;
2676
+ case "dag:kickback":
2677
+ return `${at}\u21A9 kickback ${event.accepted ? "accepted" : "rejected"} ${event.from} -> ${event.to}: ${event.reason}${event.note ? ` (${event.note})` : ""}`;
2121
2678
  case "dag:end":
2122
2679
  return `${at}\u25C2 dag ${event.outcome.status}`;
2123
2680
  case "job:start":
@@ -2150,12 +2707,12 @@ var CHECKPOINT_AT = /* @__PURE__ */ new Set([
2150
2707
  "dag:end",
2151
2708
  "job:end"
2152
2709
  ]);
2153
- function ensureDir2(path) {
2710
+ function ensureDir3(path) {
2154
2711
  const dir = dirname(path);
2155
2712
  if (dir && dir !== ".") mkdirSync(dir, { recursive: true });
2156
2713
  }
2157
2714
  function makeRecorder(path) {
2158
- ensureDir2(path);
2715
+ ensureDir3(path);
2159
2716
  writeFileSync(path, "");
2160
2717
  return (event) => {
2161
2718
  if (NOISE2.has(event.kind)) return;
@@ -2167,14 +2724,14 @@ function makeRecorder(path) {
2167
2724
  };
2168
2725
  }
2169
2726
  function makeCheckpointer(path, state) {
2170
- ensureDir2(path);
2727
+ ensureDir3(path);
2171
2728
  return (event) => {
2172
2729
  if (!CHECKPOINT_AT.has(event.kind)) return;
2173
2730
  flushCheckpoint(path, state);
2174
2731
  };
2175
2732
  }
2176
2733
  function flushCheckpoint(path, state) {
2177
- ensureDir2(path);
2734
+ ensureDir3(path);
2178
2735
  try {
2179
2736
  writeFileSync(path, JSON.stringify({ ts: Date.now(), state }, null, 2));
2180
2737
  } catch {
@@ -2344,6 +2901,6 @@ function exitCodeFor(outcome) {
2344
2901
  }
2345
2902
  }
2346
2903
 
2347
- export { Budget, EXIT_PAUSED, EngineRegistry, GhForge, MockForge, Stats, addWorktree, agentCheck, agentJob, all, always, any, appendLedger, appendPrompt, bodyPassed, buildChecksArgs, buildCreateArgs, buildEditArgs, buildMergeArgs, buildViewArgs, childContext, commandSucceeds, commit, commitJob, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, defineAgent, defineSkill, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, fnJob, forgeChecks, formatEvent, fromFile, gateJob, groundingText, hasStagedChanges, headSha, isDirty, isForge, isRepo, jobMeta, kickback, ledgerPath, listRuns, log, loop, mergeAbort, mergeBranch, mergeNoCommit, minConfidence, never, not, predicate, promptPath, push, quorum, readLedger, readPrompt, readRunStatus, removeWorktree, renderPlan, resetLedger, resetPrompt, resolveSystem, retrieveLedger, run, runEventsPath, runsHome, setMeta, stageAll, toCondition };
2348
- //# sourceMappingURL=chunk-6BDWTFOS.js.map
2349
- //# sourceMappingURL=chunk-6BDWTFOS.js.map
2904
+ export { Budget, EXIT_PAUSED, EngineRegistry, GhForge, MockForge, Stats, addWorktree, agentCheck, agentContract, agentJob, all, always, any, appendLedger, appendPrompt, bodyPassed, buildChecksArgs, buildCreateArgs, buildEditArgs, buildMergeArgs, buildViewArgs, childContext, commandSucceeds, commit, commitJob, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, defineAgent, defineSkill, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, feedbackBlock, fnJob, forgeChecks, formatEvent, fromFile, gateJob, graphPositionBlock, groundingText, hasStagedChanges, headSha, isDirty, isForge, isRepo, isRequiredFeedbackSeverity, jobMeta, kickback, ledgerPath, listRuns, log, loop, mergeAbort, mergeBranch, mergeNoCommit, minConfidence, never, normalizeFeedbackSeverity, not, predicate, promptPath, push, quorum, readLedger, readPrompt, readRunStatus, removeWorktree, renderPlan, resetLedger, resetPrompt, resolveSystem, retrieveLedger, reviewContext, reviewPanel, revisionFromOutcome, revisionRequest, run, runEventsPath, runSemanticRecordsPath, runsHome, semanticRecordsFromEvent, setMeta, stageAll, toCondition };
2905
+ //# sourceMappingURL=chunk-WM5QVHM2.js.map
2906
+ //# sourceMappingURL=chunk-WM5QVHM2.js.map