@sagentlab/navarch-runtime 0.1.96 → 0.1.97

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.
@@ -9,6 +9,7 @@ exports.extractUsageFromCodexEvents = extractUsageFromCodexEvents;
9
9
  exports.extractFinalMessageFromCodexEvents = extractFinalMessageFromCodexEvents;
10
10
  exports.leadingVerificationVerdict = leadingVerificationVerdict;
11
11
  exports.leadWithReviewFixMarker = leadWithReviewFixMarker;
12
+ exports.leadWithReviewAttestation = leadWithReviewAttestation;
12
13
  exports.mapExitCondition = mapExitCondition;
13
14
  /**
14
15
  * Leading text of every report for a turn the coding agent CLI refused because
@@ -242,6 +243,61 @@ function leadWithReviewFixMarker(report) {
242
243
  .trim();
243
244
  return rest ? `${line}\n\n${rest}` : line;
244
245
  }
246
+ const REVIEW_ATTESTATION = /^(?:\[(Approve|Code Review: Blocking|Code Review: Comment)\]\s+)?NAVARCH_REVIEW_ATTESTATION:\s*(approve|request_changes|comment)\s+([0-9a-f]{40}|[0-9a-f]{64})(?:\s|$)/i;
247
+ const REVIEW_PREFIXES = {
248
+ approve: "[approve]",
249
+ request_changes: "[code review: blocking]",
250
+ comment: "[code review: comment]",
251
+ };
252
+ function parseReviewAttestation(line) {
253
+ const match = REVIEW_ATTESTATION.exec(line.trim());
254
+ if (!match)
255
+ return null;
256
+ const verdict = (match[2] ?? "").toLowerCase();
257
+ if (match[1] && `[${match[1]}]`.toLowerCase() !== REVIEW_PREFIXES[verdict])
258
+ return null;
259
+ return { verdict, headSha: (match[3] ?? "").toLowerCase() };
260
+ }
261
+ /**
262
+ * Moves a review's `NAVARCH_REVIEW_ATTESTATION` line to the top of the report.
263
+ * The control plane accepts it only as the first line
264
+ * (lib/navarch/review-attestation.ts). Claude often writes a sentence of
265
+ * preamble before it, and every such review was rejected with
266
+ * `review_attestation_required` and needed a correction turn. Kept in step
267
+ * with leadWithPlatformReviewAttestation there; this package cannot import
268
+ * it. Two lines that disagree are left for the control plane to reject.
269
+ */
270
+ function leadWithReviewAttestation(report) {
271
+ if (parseReviewAttestation(report))
272
+ return report;
273
+ const lines = report.split("\n");
274
+ const claims = lines
275
+ .map((line, index) => ({ index, line: line.trim(), parsed: parseReviewAttestation(line) }))
276
+ .filter((entry) => entry.parsed !== null);
277
+ const first = claims[0];
278
+ if (!first)
279
+ return report;
280
+ const agree = claims.every((entry) => entry.parsed.verdict === first.parsed.verdict && entry.parsed.headSha === first.parsed.headSha);
281
+ if (!agree)
282
+ return report;
283
+ const claimLines = new Set(claims.map((entry) => entry.index));
284
+ const rest = lines
285
+ .filter((_, index) => !claimLines.has(index))
286
+ .join("\n")
287
+ .replace(/\n{3,}/g, "\n\n")
288
+ .trim();
289
+ return rest ? `${first.line}\n\n${rest}` : first.line;
290
+ }
291
+ /**
292
+ * True when the report opens with `Build outcome: FAILED|BLOCKED`. Also
293
+ * accepts `Task outcome:` and Markdown emphasis, which is how agents write
294
+ * "report a failed completion". Kept in step with declaresBuildFailure in
295
+ * lib/navarch/build-outcome.ts.
296
+ */
297
+ function declaresBuildFailure(report) {
298
+ const firstLine = report.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
299
+ return /^(?:Build|Task) outcome:[*_`~]*\s*[*_`~]*(?:FAILED|BLOCKED)(?![a-z0-9_])/i.test(firstLine.replace(/^(?:#{1,6}\s+|>\s*|[*_`~]+\s*)+/, ""));
300
+ }
245
301
  function summarize(text, maxLen = 500) {
246
302
  const trimmed = text.trim();
247
303
  if (!trimmed)
@@ -341,7 +397,8 @@ function mapExitCondition(result) {
341
397
  // falls back to raw stdout when neither is available (see
342
398
  // parseClaudeJsonResult's doc comment).
343
399
  const reportText = result.reportText ?? parsedJson?.result ?? result.stdout;
344
- const reportSummary = summarize(leadWithReviewFixMarker(reportText)) || "Adapter completed with no report text.";
400
+ const reportSummary = summarize(leadWithReviewAttestation(leadWithReviewFixMarker(reportText))) ||
401
+ "Adapter completed with no report text.";
345
402
  // A verify agent's FAIL/BLOCKED verdict is a successful agent run whose
346
403
  // conclusion is that the delivery did not pass. The completion API only
347
404
  // accepts those verdicts with a failed outcome, so map them to a failed
@@ -362,7 +419,7 @@ function mapExitCondition(result) {
362
419
  // Build agents also use an explicit outcome marker. A zero process exit
363
420
  // means the conversation finished, not that the requested build succeeded.
364
421
  if (/^blocked(?:\s|:|$)/i.test(reportText.trim()) ||
365
- /^Build outcome:\s*(?:FAILED|BLOCKED)\b/i.test(reportText.trim())) {
422
+ declaresBuildFailure(reportText)) {
366
423
  return {
367
424
  leaseOutcome: "failed",
368
425
  exitStatus: "failed",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.96",
3
+ "version": "0.1.97",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",