@sagentlab/navarch-runtime 0.1.22 → 0.1.23

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.
@@ -5,6 +5,7 @@ exports.extractUsageFromClaudeJson = extractUsageFromClaudeJson;
5
5
  exports.parseCodexJsonEvents = parseCodexJsonEvents;
6
6
  exports.extractUsageFromCodexEvents = extractUsageFromCodexEvents;
7
7
  exports.extractFinalMessageFromCodexEvents = extractFinalMessageFromCodexEvents;
8
+ exports.leadingVerificationVerdict = leadingVerificationVerdict;
8
9
  exports.mapExitCondition = mapExitCondition;
9
10
  /**
10
11
  * Best-effort parse of `claude -p --output-format json` stdout into the
@@ -158,6 +159,21 @@ function extractFinalMessageFromCodexEvents(events) {
158
159
  }
159
160
  return null;
160
161
  }
162
+ /**
163
+ * Parses the leading `Verification verdict:` line convention shared with the
164
+ * control plane (lib/navarch/verification-verdict.ts). Verify completions
165
+ * must open with `Verification verdict: PASS|FAIL|BLOCKED`, and the
166
+ * completion API rejects a completion whose verdict contradicts its lease
167
+ * outcome, keeping the lease active.
168
+ */
169
+ function leadingVerificationVerdict(report) {
170
+ const firstLine = report
171
+ .split(/\r?\n/)
172
+ .map((line) => line.trim())
173
+ .find(Boolean);
174
+ const match = /^Verification verdict:\s*(PASS|FAIL|BLOCKED)\b/i.exec(firstLine ?? "");
175
+ return match?.[1] ? match[1].toLowerCase() : null;
176
+ }
161
177
  function summarize(text, maxLen = 500) {
162
178
  const trimmed = text.trim();
163
179
  if (!trimmed)
@@ -234,6 +250,19 @@ function mapExitCondition(result) {
234
250
  // parseClaudeJsonResult's doc comment).
235
251
  const reportText = result.reportText ?? parsedJson?.result ?? result.stdout;
236
252
  const reportSummary = summarize(reportText) || "Adapter completed with no report text.";
253
+ // A verify agent's FAIL/BLOCKED verdict is a successful agent run whose
254
+ // conclusion is that the delivery did not pass. The completion API only
255
+ // accepts those verdicts with a failed outcome, so map them to a failed
256
+ // lease instead of posting a "completed" the control plane must reject.
257
+ const verdict = leadingVerificationVerdict(reportText);
258
+ if (verdict === "fail" || verdict === "blocked") {
259
+ return {
260
+ leaseOutcome: "failed",
261
+ exitStatus: "failed",
262
+ reportSummary,
263
+ evidenceUrls,
264
+ };
265
+ }
237
266
  // Headless agent CLIs normally exit 0 after producing a final response,
238
267
  // including when that response says the task could not start. Treat an
239
268
  // explicit leading blocked verdict as a failed lease so the dispatcher can
package/dist/session.cjs CHANGED
@@ -107,7 +107,7 @@ async function runSession(deps, claimed, sessionId) {
107
107
  const failureSummary = `Project ${task.project_id} has no GitHub repository URL. Set it in Project settings before dispatching work.`;
108
108
  await api.completeLease(leaseId, {
109
109
  status: "failed",
110
- report: failureSummary,
110
+ report: verificationFailureReport(task.task_type, failureSummary),
111
111
  failure_summary: failureSummary,
112
112
  evidence_urls: [],
113
113
  cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
@@ -181,7 +181,7 @@ async function runSession(deps, claimed, sessionId) {
181
181
  await api
182
182
  .completeLease(leaseId, {
183
183
  status: "failed",
184
- report: "Docker sandbox unavailable on this machine.",
184
+ report: verificationFailureReport(task.task_type, "Docker sandbox unavailable on this machine."),
185
185
  failure_summary: "Docker sandbox unavailable on this machine.",
186
186
  evidence_urls: [],
187
187
  cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
@@ -395,9 +395,12 @@ async function runSession(deps, claimed, sessionId) {
395
395
  // redact it like every other warn in this block.
396
396
  log.warn(`transcript upload failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(String(err), knownSecrets)}`);
397
397
  }
398
+ const redactedReport = (0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets);
398
399
  const completion = {
399
400
  status: mapping.leaseOutcome,
400
- report: (0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets),
401
+ report: mapping.leaseOutcome === "failed"
402
+ ? verificationFailureReport(task.task_type, redactedReport)
403
+ : redactedReport,
401
404
  evidence_urls: mapping.evidenceUrls,
402
405
  cost: {
403
406
  ...(result.tokensIn !== undefined ? { tokens_in: result.tokensIn } : {}),
@@ -448,9 +451,9 @@ async function runSession(deps, claimed, sessionId) {
448
451
  await api.completeLease(leaseId, {
449
452
  ...completion,
450
453
  status: "failed",
451
- report: remediable
454
+ report: verificationFailureReport(task.task_type, remediable
452
455
  ? redactedRejection
453
- : `${redactedRejection}\n\n---\n\n${completion.report}`,
456
+ : `${redactedRejection}\n\n---\n\n${completion.report}`),
454
457
  failure_summary: redactedRejection,
455
458
  exit_status: "failed",
456
459
  });
@@ -482,9 +485,9 @@ async function runSession(deps, claimed, sessionId) {
482
485
  await api
483
486
  .completeLease(leaseId, {
484
487
  status: "failed",
485
- report: lastCompletion
488
+ report: verificationFailureReport(task.task_type, lastCompletion
486
489
  ? `${lastCompletion.report}\n\n---\n\n${failureSummary}`
487
- : failureSummary,
490
+ : failureSummary),
488
491
  failure_summary: failureSummary,
489
492
  evidence_urls: lastCompletion?.evidence_urls ?? [],
490
493
  cost: lastCompletion?.cost ?? { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
@@ -525,6 +528,24 @@ function sumReportedUsage(attempts, key) {
525
528
  });
526
529
  return reported.length > 0 ? reported.reduce((sum, value) => sum + value, 0) : undefined;
527
530
  }
531
+ /**
532
+ * The control plane refuses to release a verify lease whose failed completion
533
+ * does not open with `Verification verdict: FAIL` or `BLOCKED`
534
+ * (verification_verdict_invalid), and that gate applies to runtime-authored
535
+ * failure reports too. When the report carries no usable verdict — the agent
536
+ * crashed, timed out, exited non-zero, or opened with PASS despite the failed
537
+ * outcome — label it BLOCKED: the failure prevented an acceptance decision.
538
+ * Without this, the failure completion is itself rejected, the session
539
+ * crashes, and the lease dangles until expiry.
540
+ */
541
+ function verificationFailureReport(taskType, report) {
542
+ if (taskType !== "verify")
543
+ return report;
544
+ const verdict = (0, exit_conditions_cjs_1.leadingVerificationVerdict)(report);
545
+ if (verdict === "fail" || verdict === "blocked")
546
+ return report;
547
+ return `Verification verdict: BLOCKED — the session ended before verification reached an acceptance decision.\n\n${report}`;
548
+ }
528
549
  /** Rejection codes another agent turn in the same worktree can plausibly fix. */
529
550
  const REMEDIABLE_REJECTION_CODES = new Set([
530
551
  "pr_required",
@@ -536,6 +557,12 @@ const REMEDIABLE_REJECTION_CODES = new Set([
536
557
  // The marker is already on the right head; the reviewer only has to relabel
537
558
  // the body, which is exactly what a remediation turn can do.
538
559
  "review_evidence_mislabeled",
560
+ // A verify agent that exited zero without the mandated leading
561
+ // `Verification verdict:` line only has to restate its report; the
562
+ // rejection prose spells out the exact format. Failed outcomes never reach
563
+ // remediation (see the leaseOutcome === "completed" gate) — their reports
564
+ // are verdict-labelled by verificationFailureReport before posting.
565
+ "verification_verdict_invalid",
539
566
  ]);
540
567
  /**
541
568
  * A 409 the control plane raised to reject *this* completion's contents (as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through Claude Code, Codex, or Gemini CLI.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",