agentxchain 2.107.0 → 2.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib/report.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  } from './coordinator-next-actions.js';
12
12
  import { buildCoordinatorRepoStatusEntries } from './coordinator-repo-status-presentation.js';
13
13
  import { summarizeCoordinatorEvent } from './coordinator-event-narrative.js';
14
+ import { extractGateActionDigest } from './gate-actions.js';
14
15
 
15
16
  export const GOVERNANCE_REPORT_VERSION = '0.1';
16
17
 
@@ -119,6 +120,34 @@ function yesNo(value) {
119
120
  return value ? 'yes' : 'no';
120
121
  }
121
122
 
123
+ function normalizeConflictingFiles(conflict) {
124
+ if (!conflict || typeof conflict !== 'object' || Array.isArray(conflict)) return [];
125
+ if (Array.isArray(conflict.conflicting_files)) {
126
+ return conflict.conflicting_files.filter((entry) => typeof entry === 'string' && entry.length > 0);
127
+ }
128
+ if (Array.isArray(conflict.files)) {
129
+ return conflict.files.filter((entry) => typeof entry === 'string' && entry.length > 0);
130
+ }
131
+ return [];
132
+ }
133
+
134
+ function normalizeAcceptedSinceTurnIds(conflict) {
135
+ if (!conflict || typeof conflict !== 'object' || Array.isArray(conflict)) return [];
136
+ if (Array.isArray(conflict.accepted_since_turn_ids)) {
137
+ return conflict.accepted_since_turn_ids.filter((entry) => typeof entry === 'string' && entry.length > 0);
138
+ }
139
+ if (Array.isArray(conflict.accepted_since)) {
140
+ return conflict.accepted_since
141
+ .map((entry) => {
142
+ if (typeof entry === 'string') return entry;
143
+ if (entry && typeof entry === 'object' && typeof entry.turn_id === 'string') return entry.turn_id;
144
+ return null;
145
+ })
146
+ .filter(Boolean);
147
+ }
148
+ return [];
149
+ }
150
+
122
151
  function summarizeBlockedOn(blockedOn) {
123
152
  if (!blockedOn) return 'none';
124
153
  if (typeof blockedOn === 'string') return blockedOn;
@@ -143,6 +172,9 @@ function renderGovernanceEventDetailText(lines, evt, indent) {
143
172
  if (evt.conflicting_files?.length > 0) {
144
173
  lines.push(`${indent}files: ${evt.conflicting_files.join(', ')}`);
145
174
  }
175
+ if (evt.accepted_since_turn_ids?.length > 0) {
176
+ lines.push(`${indent}accepted since: ${evt.accepted_since_turn_ids.join(', ')}`);
177
+ }
146
178
  if (evt.overlap_ratio != null) {
147
179
  lines.push(`${indent}overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%`);
148
180
  }
@@ -151,8 +183,23 @@ function renderGovernanceEventDetailText(lines, evt, indent) {
151
183
  if (evt.conflicting_files?.length > 0) {
152
184
  lines.push(`${indent}files: ${evt.conflicting_files.join(', ')}`);
153
185
  }
186
+ if (evt.accepted_since_turn_ids?.length > 0) {
187
+ lines.push(`${indent}accepted since: ${evt.accepted_since_turn_ids.join(', ')}`);
188
+ }
189
+ if (evt.operator_reason) {
190
+ lines.push(`${indent}operator reason: ${evt.operator_reason}`);
191
+ }
154
192
  break;
155
193
  case 'conflict_resolution_selected':
194
+ if (evt.conflicting_files?.length > 0) {
195
+ lines.push(`${indent}files: ${evt.conflicting_files.join(', ')}`);
196
+ }
197
+ if (evt.accepted_since_turn_ids?.length > 0) {
198
+ lines.push(`${indent}accepted since: ${evt.accepted_since_turn_ids.join(', ')}`);
199
+ }
200
+ if (evt.overlap_ratio != null) {
201
+ lines.push(`${indent}overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%`);
202
+ }
156
203
  if (evt.resolution_method) {
157
204
  lines.push(`${indent}resolution: ${evt.resolution_method}`);
158
205
  }
@@ -179,6 +226,9 @@ function renderGovernanceEventDetailMarkdown(lines, evt) {
179
226
  if (evt.conflicting_files?.length > 0) {
180
227
  lines.push(` - Files: ${evt.conflicting_files.map((f) => `\`${f}\``).join(', ')}`);
181
228
  }
229
+ if (evt.accepted_since_turn_ids?.length > 0) {
230
+ lines.push(` - Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `\`${turnId}\``).join(', ')}`);
231
+ }
182
232
  if (evt.overlap_ratio != null) {
183
233
  lines.push(` - Overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%`);
184
234
  }
@@ -187,8 +237,21 @@ function renderGovernanceEventDetailMarkdown(lines, evt) {
187
237
  if (evt.conflicting_files?.length > 0) {
188
238
  lines.push(` - Files: ${evt.conflicting_files.map((f) => `\`${f}\``).join(', ')}`);
189
239
  }
240
+ if (evt.accepted_since_turn_ids?.length > 0) {
241
+ lines.push(` - Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `\`${turnId}\``).join(', ')}`);
242
+ }
243
+ if (evt.operator_reason) lines.push(` - Operator reason: ${evt.operator_reason}`);
190
244
  break;
191
245
  case 'conflict_resolution_selected':
246
+ if (evt.conflicting_files?.length > 0) {
247
+ lines.push(` - Files: ${evt.conflicting_files.map((f) => `\`${f}\``).join(', ')}`);
248
+ }
249
+ if (evt.accepted_since_turn_ids?.length > 0) {
250
+ lines.push(` - Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `\`${turnId}\``).join(', ')}`);
251
+ }
252
+ if (evt.overlap_ratio != null) {
253
+ lines.push(` - Overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%`);
254
+ }
192
255
  if (evt.resolution_method) lines.push(` - Resolution: \`${evt.resolution_method}\``);
193
256
  break;
194
257
  case 'operator_escalated':
@@ -449,6 +512,11 @@ function extractGateFailureDigest(artifact) {
449
512
  }));
450
513
  }
451
514
 
515
+ function extractGateActionEventDigest(artifact) {
516
+ const data = extractFileData(artifact, '.agentxchain/decision-ledger.jsonl');
517
+ return extractGateActionDigest(data);
518
+ }
519
+
452
520
  const GOVERNANCE_EVENT_TYPES = new Set([
453
521
  'policy_escalation',
454
522
  'conflict_detected',
@@ -480,14 +548,21 @@ function extractGovernanceEventDigest(artifact, relPath = '.agentxchain/decision
480
548
  })) : [];
481
549
  break;
482
550
  case 'conflict_detected':
483
- base.conflicting_files = Array.isArray(d.conflict?.files) ? d.conflict.files : [];
551
+ base.conflicting_files = normalizeConflictingFiles(d.conflict);
552
+ base.accepted_since_turn_ids = normalizeAcceptedSinceTurnIds(d.conflict);
484
553
  base.overlap_ratio = typeof d.conflict?.overlap_ratio === 'number' ? d.conflict.overlap_ratio : null;
485
554
  break;
486
555
  case 'conflict_rejected':
487
- base.conflicting_files = Array.isArray(d.conflict?.files) ? d.conflict.files : [];
556
+ base.conflicting_files = normalizeConflictingFiles(d.conflict);
557
+ base.accepted_since_turn_ids = normalizeAcceptedSinceTurnIds(d.conflict);
558
+ base.overlap_ratio = typeof d.conflict?.overlap_ratio === 'number' ? d.conflict.overlap_ratio : null;
559
+ base.operator_reason = d.operator_reason || null;
488
560
  break;
489
561
  case 'conflict_resolution_selected':
490
- base.resolution_method = d.conflict?.resolution || null;
562
+ base.conflicting_files = normalizeConflictingFiles(d.conflict);
563
+ base.accepted_since_turn_ids = normalizeAcceptedSinceTurnIds(d.conflict);
564
+ base.overlap_ratio = typeof d.conflict?.overlap_ratio === 'number' ? d.conflict.overlap_ratio : null;
565
+ base.resolution_method = d.resolution_chosen || d.conflict?.resolution || null;
491
566
  break;
492
567
  case 'operator_escalated':
493
568
  base.blocked_on = d.blocked_on || null;
@@ -917,6 +992,7 @@ function buildRunSubject(artifact) {
917
992
  const decisions = extractDecisionDigest(artifact);
918
993
  const approvalPolicyEvents = extractApprovalPolicyDigest(artifact);
919
994
  const gateFailures = extractGateFailureDigest(artifact);
995
+ const gateActions = extractGateActionEventDigest(artifact);
920
996
  const timeoutEvents = extractTimeoutEventDigest(artifact);
921
997
  const hookSummary = extractHookSummary(artifact);
922
998
  const timing = computeTiming(artifact, turns);
@@ -965,6 +1041,7 @@ function buildRunSubject(artifact) {
965
1041
  approval_policy_events: approvalPolicyEvents,
966
1042
  governance_events: governanceEvents,
967
1043
  gate_failures: gateFailures,
1044
+ gate_actions: gateActions,
968
1045
  timeout_events: timeoutEvents,
969
1046
  delegation_summary: delegationSummary,
970
1047
  hook_summary: hookSummary,
@@ -1362,6 +1439,18 @@ export function formatGovernanceReportText(report) {
1362
1439
  }
1363
1440
  }
1364
1441
 
1442
+ if (run.gate_actions && run.gate_actions.length > 0) {
1443
+ lines.push('', 'Gate Actions:');
1444
+ for (const action of run.gate_actions) {
1445
+ const label = action.action_label || action.command || `action ${action.action_index || '?'}`;
1446
+ const exit = action.exit_code == null ? 'n/a' : String(action.exit_code);
1447
+ lines.push(` - ${action.gate_id || 'unknown'} | ${action.gate_type || 'unknown'} | action ${action.action_index || '?'} | ${action.status} | ${label} | exit: ${exit} | at: ${action.timestamp || 'n/a'}`);
1448
+ if (action.stderr_tail) {
1449
+ lines.push(` stderr: ${action.stderr_tail}`);
1450
+ }
1451
+ }
1452
+ }
1453
+
1365
1454
  if (run.approval_policy_events && run.approval_policy_events.length > 0) {
1366
1455
  lines.push('', 'Approval Policy:');
1367
1456
  for (const evt of run.approval_policy_events) {
@@ -1920,6 +2009,18 @@ export function formatGovernanceReportMarkdown(report) {
1920
2009
  }
1921
2010
  }
1922
2011
 
2012
+ if (run.gate_actions && run.gate_actions.length > 0) {
2013
+ lines.push('', '## Gate Actions', '');
2014
+ for (const action of run.gate_actions) {
2015
+ const label = action.action_label || action.command || `action ${action.action_index || '?'}`;
2016
+ const exit = action.exit_code == null ? 'n/a' : String(action.exit_code);
2017
+ lines.push(`- \`${action.gate_id || 'unknown'}\` (${action.gate_type || 'unknown'}) action ${action.action_index || '?'} — **${action.status}** at \`${action.timestamp || 'n/a'}\`: ${label} (exit \`${exit}\`)`);
2018
+ if (action.stderr_tail) {
2019
+ lines.push(` - stderr: ${action.stderr_tail}`);
2020
+ }
2021
+ }
2022
+ }
2023
+
1923
2024
  if (run.approval_policy_events && run.approval_policy_events.length > 0) {
1924
2025
  lines.push('', '## Approval Policy', '');
1925
2026
  for (const evt of run.approval_policy_events) {
@@ -2420,7 +2521,19 @@ function renderHtmlGovEventDetail(evt) {
2420
2521
  break;
2421
2522
  case 'conflict_detected':
2422
2523
  if (evt.conflicting_files?.length > 0) parts.push(`<li>Files: ${evt.conflicting_files.map((f) => `<code>${esc(f)}</code>`).join(', ')}</li>`);
2524
+ if (evt.accepted_since_turn_ids?.length > 0) parts.push(`<li>Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `<code>${esc(turnId)}</code>`).join(', ')}</li>`);
2525
+ if (evt.overlap_ratio != null) parts.push(`<li>Overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%</li>`);
2526
+ break;
2527
+ case 'conflict_rejected':
2528
+ if (evt.conflicting_files?.length > 0) parts.push(`<li>Files: ${evt.conflicting_files.map((f) => `<code>${esc(f)}</code>`).join(', ')}</li>`);
2529
+ if (evt.accepted_since_turn_ids?.length > 0) parts.push(`<li>Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `<code>${esc(turnId)}</code>`).join(', ')}</li>`);
2530
+ if (evt.operator_reason) parts.push(`<li>Operator reason: ${esc(evt.operator_reason)}</li>`);
2531
+ break;
2532
+ case 'conflict_resolution_selected':
2533
+ if (evt.conflicting_files?.length > 0) parts.push(`<li>Files: ${evt.conflicting_files.map((f) => `<code>${esc(f)}</code>`).join(', ')}</li>`);
2534
+ if (evt.accepted_since_turn_ids?.length > 0) parts.push(`<li>Accepted since: ${evt.accepted_since_turn_ids.map((turnId) => `<code>${esc(turnId)}</code>`).join(', ')}</li>`);
2423
2535
  if (evt.overlap_ratio != null) parts.push(`<li>Overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%</li>`);
2536
+ if (evt.resolution_method) parts.push(`<li>Resolution: <code>${esc(evt.resolution_method)}</code></li>`);
2424
2537
  break;
2425
2538
  case 'operator_escalated':
2426
2539
  if (evt.reason) parts.push(`<li>Reason: ${esc(evt.reason)}</li>`);
@@ -2605,6 +2718,21 @@ function renderRunHtml(report) {
2605
2718
  sections.push(`<div class="section">${htmlSection('Gate Failures', gfHtml)}</div>`);
2606
2719
  }
2607
2720
 
2721
+ if (run.gate_actions && run.gate_actions.length > 0) {
2722
+ let gaHtml = '<ul>';
2723
+ for (const action of run.gate_actions) {
2724
+ const label = action.action_label || action.command || `action ${action.action_index || '?'}`;
2725
+ const exit = action.exit_code == null ? 'n/a' : String(action.exit_code);
2726
+ gaHtml += `<li><code>${esc(action.gate_id || 'unknown')}</code> (${esc(action.gate_type || 'unknown')}) action ${esc(String(action.action_index || '?'))} — <strong>${esc(action.status)}</strong> at <code>${esc(action.timestamp || 'n/a')}</code>: ${esc(label)} (exit <code>${esc(exit)}</code>)`;
2727
+ if (action.stderr_tail) {
2728
+ gaHtml += `<br><code>${esc(action.stderr_tail)}</code>`;
2729
+ }
2730
+ gaHtml += '</li>';
2731
+ }
2732
+ gaHtml += '</ul>';
2733
+ sections.push(`<div class="section">${htmlSection('Gate Actions', gaHtml)}</div>`);
2734
+ }
2735
+
2608
2736
  // Approval Policy
2609
2737
  if (run.approval_policy_events && run.approval_policy_events.length > 0) {
2610
2738
  let apHtml = '<ul>';
@@ -17,6 +17,7 @@ export const VALID_RUN_EVENTS = [
17
17
  'turn_dispatched',
18
18
  'turn_accepted',
19
19
  'turn_rejected',
20
+ 'turn_conflicted',
20
21
  'run_blocked',
21
22
  'run_completed',
22
23
  'escalation_raised',
@@ -35,6 +35,7 @@ import {
35
35
  import { runAdmissionControl } from './admission-control.js';
36
36
  import { mkdirSync, writeFileSync } from 'fs';
37
37
  import { join, dirname } from 'path';
38
+ import { evaluateApprovalSlaReminders } from './notification-runner.js';
38
39
 
39
40
  const DEFAULT_MAX_TURNS = 50;
40
41
 
@@ -340,6 +341,26 @@ async function executeParallelTurns(root, config, state, maxConcurrent, callback
340
341
  const acceptResult = acceptTurn(root, config, { turnId: turn.turn_id });
341
342
  if (!acceptResult.ok) {
342
343
  errors.push(`acceptTurn(${roleId}): ${acceptResult.error}`);
344
+
345
+ // Conflict-aware handling (DEC-RUN-LOOP-CONFLICT-001)
346
+ if (acceptResult.error_code === 'conflict') {
347
+ history.push({
348
+ role: roleId, turn_id: turn.turn_id, accepted: false,
349
+ error_code: 'conflict', accept_error: acceptResult.error,
350
+ conflict: acceptResult.conflict,
351
+ });
352
+ emit({
353
+ type: 'turn_conflicted', turn, role: roleId,
354
+ error_code: 'conflict', conflict: acceptResult.conflict,
355
+ state: acceptResult.state,
356
+ });
357
+ if (acceptResult.state?.status === 'blocked') {
358
+ emit({ type: 'blocked', state: acceptResult.state });
359
+ return { terminal: true, ok: false, stop_reason: 'conflict_loop', history, acceptedCount };
360
+ }
361
+ continue;
362
+ }
363
+
343
364
  // Record failure but try other turns
344
365
  history.push({ role: roleId, turn_id: turn.turn_id, accepted: false, accept_error: acceptResult.error });
345
366
  continue;
@@ -372,8 +393,10 @@ async function executeParallelTurns(root, config, state, maxConcurrent, callback
372
393
  if (acceptedCount === 0 && history.length > 0) {
373
394
  const allFailed = history.every(h => !h.accepted);
374
395
  if (allFailed) {
375
- errors.push('All parallel turns failed acceptance — stalled');
376
- return { terminal: true, ok: false, stop_reason: 'blocked', history, acceptedCount };
396
+ const allConflicts = history.every(h => h.error_code === 'conflict');
397
+ const stopReason = allConflicts ? 'conflict_stall' : 'blocked';
398
+ errors.push(`All parallel turns failed acceptance — ${stopReason}`);
399
+ return { terminal: true, ok: false, stop_reason: stopReason, history, acceptedCount };
377
400
  }
378
401
  }
379
402
 
@@ -419,6 +442,29 @@ async function dispatchAndProcess(root, config, turn, assignState, callbacks, em
419
442
  const acceptResult = acceptTurn(root, config);
420
443
  if (!acceptResult.ok) {
421
444
  errors.push(`acceptTurn(${roleId}): ${acceptResult.error}`);
445
+
446
+ // Conflict-aware handling (DEC-RUN-LOOP-CONFLICT-001)
447
+ if (acceptResult.error_code === 'conflict') {
448
+ history.push({
449
+ role: roleId, turn_id: turn.turn_id, accepted: false,
450
+ error_code: 'conflict', accept_error: acceptResult.error,
451
+ conflict: acceptResult.conflict,
452
+ });
453
+ emit({
454
+ type: 'turn_conflicted', turn, role: roleId,
455
+ error_code: 'conflict', conflict: acceptResult.conflict,
456
+ state: acceptResult.state,
457
+ });
458
+ // If the resulting state is blocked (conflict_loop), terminate
459
+ if (acceptResult.state?.status === 'blocked') {
460
+ emit({ type: 'blocked', state: acceptResult.state });
461
+ return { terminal: true, ok: false, stop_reason: 'conflict_loop', history };
462
+ }
463
+ // Otherwise the turn is conflicted but the run is still active — let the
464
+ // main loop re-enter and try another role or handle the paused state
465
+ return { terminal: false, accepted: false, history };
466
+ }
467
+
422
468
  return { terminal: true, ok: false, stop_reason: 'blocked', history };
423
469
  }
424
470
 
@@ -450,6 +496,8 @@ async function dispatchAndProcess(root, config, turn, assignState, callbacks, em
450
496
  * Handle a paused state by checking for pending gates and calling approveGate.
451
497
  */
452
498
  async function handleGatePause(root, config, state, callbacks, emit) {
499
+ evaluateApprovalSlaReminders(root, config, state);
500
+
453
501
  if (state.pending_phase_transition) {
454
502
  emit({ type: 'gate_paused', gateType: 'phase_transition', state });
455
503
  let approved;