agentxchain 2.145.0 → 2.147.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 (43) hide show
  1. package/dashboard/app.js +3 -0
  2. package/dashboard/components/notifications.js +127 -0
  3. package/dashboard/index.html +1 -0
  4. package/package.json +1 -1
  5. package/scripts/publish-npm.sh +16 -0
  6. package/scripts/release-downstream-truth.sh +16 -8
  7. package/scripts/sync-homebrew.sh +14 -1
  8. package/scripts/verify-post-publish.sh +55 -4
  9. package/src/commands/init.js +66 -31
  10. package/src/commands/reissue-turn.js +16 -0
  11. package/src/commands/reject-turn.js +14 -1
  12. package/src/commands/restart.js +33 -3
  13. package/src/commands/resume.js +78 -66
  14. package/src/commands/run.js +67 -10
  15. package/src/commands/schedule.js +34 -7
  16. package/src/commands/status.js +38 -5
  17. package/src/commands/step.js +117 -34
  18. package/src/lib/adapters/api-proxy-adapter.js +8 -0
  19. package/src/lib/adapters/local-cli-adapter.js +131 -13
  20. package/src/lib/adapters/manual-adapter.js +9 -10
  21. package/src/lib/adapters/mcp-adapter.js +3 -5
  22. package/src/lib/adapters/remote-agent-adapter.js +3 -5
  23. package/src/lib/config.js +4 -1
  24. package/src/lib/continuous-run.js +71 -6
  25. package/src/lib/dashboard/actions.js +9 -3
  26. package/src/lib/dashboard/bridge-server.js +11 -0
  27. package/src/lib/dashboard/notifications-reader.js +91 -0
  28. package/src/lib/dashboard/state-reader.js +16 -4
  29. package/src/lib/dispatch-bundle.js +1 -1
  30. package/src/lib/dispatch-progress.js +5 -3
  31. package/src/lib/governed-state.js +355 -13
  32. package/src/lib/intake.js +10 -1
  33. package/src/lib/normalized-config.js +51 -1
  34. package/src/lib/recent-event-summary.js +12 -0
  35. package/src/lib/run-events.js +4 -0
  36. package/src/lib/run-loop.js +67 -2
  37. package/src/lib/runner-interface.js +1 -0
  38. package/src/lib/schema.js +7 -0
  39. package/src/lib/schemas/agentxchain-config.schema.json +15 -1
  40. package/src/lib/staged-result-proof.js +43 -0
  41. package/src/lib/stale-turn-watchdog.js +308 -34
  42. package/src/lib/turn-result-shape.js +38 -0
  43. package/src/lib/turn-result-validator.js +4 -1
@@ -99,6 +99,9 @@ function describeContinuousTerminalStep(step, contOpts) {
99
99
  return `Continuous loop failed: ${reason}. Check "agentxchain status" for details.`;
100
100
  }
101
101
  if (step.status === 'blocked') {
102
+ if (step.recovery_action) {
103
+ return `Continuous loop paused on blocker. Recovery: ${step.recovery_action}`;
104
+ }
102
105
  return 'Continuous loop paused on blocker. Use "agentxchain unblock <id>" to resume.';
103
106
  }
104
107
  return null;
@@ -116,6 +119,14 @@ function isBlockedContinuousExecution(execution) {
116
119
  || stopReason === 'reject_exhausted';
117
120
  }
118
121
 
122
+ function getBlockedRecoveryAction(state) {
123
+ return state?.blocked_reason?.recovery?.recovery_action || null;
124
+ }
125
+
126
+ function getBlockedCategory(state) {
127
+ return state?.blocked_reason?.category || null;
128
+ }
129
+
119
130
  // ---------------------------------------------------------------------------
120
131
  // Intake queue check
121
132
  // ---------------------------------------------------------------------------
@@ -361,7 +372,14 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
361
372
  if (governedState?.status === 'blocked') {
362
373
  // Still blocked — stay paused, do not attempt new work
363
374
  writeContinuousSession(root, session);
364
- return { ok: true, status: 'blocked', action: 'still_blocked', run_id: session.current_run_id };
375
+ return {
376
+ ok: true,
377
+ status: 'blocked',
378
+ action: 'still_blocked',
379
+ run_id: session.current_run_id,
380
+ recovery_action: getBlockedRecoveryAction(governedState),
381
+ blocked_category: getBlockedCategory(governedState),
382
+ };
365
383
  }
366
384
  // Unblocked — resume by continuing the existing governed run directly.
367
385
  // Skip the intake pipeline: the run is already in progress, and startIntent
@@ -388,10 +406,20 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
388
406
  const resumeStopReason = execution.result?.stop_reason;
389
407
 
390
408
  if (isBlockedContinuousExecution(execution)) {
409
+ const blockedRecoveryAction = getBlockedRecoveryAction(execution?.result?.state || loadProjectState(root, context.config));
391
410
  session.status = 'paused';
392
- log('Resumed run blocked again — continuous loop re-paused.');
411
+ log(blockedRecoveryAction
412
+ ? `Resumed run blocked again — continuous loop re-paused. Recovery: ${blockedRecoveryAction}`
413
+ : 'Resumed run blocked again — continuous loop re-paused.');
393
414
  writeContinuousSession(root, session);
394
- return { ok: true, status: 'blocked', action: 'run_blocked', run_id: session.current_run_id };
415
+ return {
416
+ ok: true,
417
+ status: 'blocked',
418
+ action: 'run_blocked',
419
+ run_id: session.current_run_id,
420
+ recovery_action: blockedRecoveryAction,
421
+ blocked_category: getBlockedCategory(execution?.result?.state || loadProjectState(root, context.config)),
422
+ };
395
423
  }
396
424
 
397
425
  if (execution.exitCode !== 0 || !execution.result) {
@@ -473,9 +501,35 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
473
501
  return { ok: false, status: 'failed', action: 'prepare_failed', stop_reason: preparedIntent.error, intent_id: targetIntentId };
474
502
  }
475
503
 
504
+ // BUG-53: Auto-chain audit trail. When this advance step seeds a NEXT run
505
+ // (i.e., at least one prior run already completed in this session), emit a
506
+ // `session_continuation` event so operators have a visible record that the
507
+ // loop auto-derived the next vision objective without intervention. Event
508
+ // is emitted BEFORE we overwrite session.current_run_id so previous_run_id
509
+ // reflects the just-completed run and next_run_id reflects the newly
510
+ // prepared one. See HUMAN-ROADMAP BUG-53 fix #4.
511
+ const previousRunId = session.current_run_id;
512
+ const nextObjective = visionObjective || preparedIntent.intent?.charter || null;
513
+ if ((session.runs_completed || 0) >= 1 && previousRunId && previousRunId !== preparedIntent.run_id) {
514
+ emitRunEvent(root, 'session_continuation', {
515
+ run_id: preparedIntent.run_id,
516
+ phase: null,
517
+ status: 'active',
518
+ payload: {
519
+ session_id: session.session_id,
520
+ previous_run_id: previousRunId,
521
+ next_run_id: preparedIntent.run_id,
522
+ next_objective: nextObjective,
523
+ next_intent_id: targetIntentId,
524
+ runs_completed: session.runs_completed || 0,
525
+ trigger: visionObjective ? 'vision_scan' : 'intake',
526
+ },
527
+ });
528
+ }
529
+
476
530
  // Execute the governed run
477
531
  session.current_run_id = preparedIntent.run_id;
478
- session.current_vision_objective = visionObjective || preparedIntent.intent?.charter || null;
532
+ session.current_vision_objective = nextObjective;
479
533
  session.status = 'running';
480
534
  writeContinuousSession(root, session);
481
535
 
@@ -519,6 +573,7 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
519
573
  }
520
574
 
521
575
  if (isBlockedContinuousExecution(execution)) {
576
+ const blockedRecoveryAction = getBlockedRecoveryAction(execution?.result?.state || loadProjectState(root, context.config));
522
577
  const resolved = resolveIntent(root, targetIntentId);
523
578
  if (!resolved.ok) {
524
579
  log(`Continuous resolve error: ${resolved.error}`);
@@ -527,9 +582,19 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
527
582
  return { ok: false, status: 'failed', action: 'resolve_failed', stop_reason: resolved.error, intent_id: targetIntentId };
528
583
  }
529
584
  session.status = 'paused';
530
- log('Run blocked — continuous loop paused. Use `agentxchain unblock <id>` to resume.');
585
+ log(blockedRecoveryAction
586
+ ? `Run blocked — continuous loop paused. Recovery: ${blockedRecoveryAction}`
587
+ : 'Run blocked — continuous loop paused. Use `agentxchain unblock <id>` to resume.');
531
588
  writeContinuousSession(root, session);
532
- return { ok: true, status: 'blocked', action: 'run_blocked', run_id: session.current_run_id, intent_id: targetIntentId };
589
+ return {
590
+ ok: true,
591
+ status: 'blocked',
592
+ action: 'run_blocked',
593
+ run_id: session.current_run_id,
594
+ intent_id: targetIntentId,
595
+ recovery_action: blockedRecoveryAction,
596
+ blocked_category: getBlockedCategory(execution?.result?.state || loadProjectState(root, context.config)),
597
+ };
533
598
  }
534
599
 
535
600
  if (stopReason === 'caller_stopped') {
@@ -1,5 +1,5 @@
1
1
  import { dirname } from 'path';
2
- import { loadProjectContext } from '../config.js';
2
+ import { loadProjectContext, loadProjectState } from '../config.js';
3
3
  import { approvePhaseTransition, approveRunCompletion } from '../governed-state.js';
4
4
  import { deriveGovernedRunNextActions, deriveRecoveryDescriptor } from '../blocked-state.js';
5
5
  import {
@@ -205,10 +205,16 @@ function approveCoordinatorGate(workspacePath, state, config) {
205
205
 
206
206
  export function approvePendingDashboardGate(agentxchainDir) {
207
207
  const workspacePath = dirname(agentxchainDir);
208
- const repoState = readJsonFile(agentxchainDir, 'state.json');
208
+ const context = loadProjectContext(workspacePath);
209
+
210
+ // Use loadProjectState to get reconciled state — approval-pause repair
211
+ // may surface a pending_run_completion from an orphaned blocked_on marker,
212
+ // and we must route on the reconciled truth, not the raw state.json.
213
+ const repoState = (context?.config?.protocol_mode === 'governed'
214
+ ? loadProjectState(workspacePath, context.config)
215
+ : null) || readJsonFile(agentxchainDir, 'state.json');
209
216
 
210
217
  if (repoState?.pending_phase_transition || repoState?.pending_run_completion) {
211
- const context = loadProjectContext(workspacePath);
212
218
  return approveRepoGate(workspacePath, context?.config, repoState);
213
219
  }
214
220
 
@@ -23,6 +23,7 @@ import { readCoordinatorRepoStatusRows } from './coordinator-repo-status.js';
23
23
  import { readCoordinatorTimeoutStatus } from './coordinator-timeout-status.js';
24
24
  import { readAggregatedCoordinatorEvents, watchChildRepoEvents } from './coordinator-event-aggregation.js';
25
25
  import { readWorkflowKitArtifacts } from './workflow-kit-artifacts.js';
26
+ import { readNotificationSnapshot } from './notifications-reader.js';
26
27
  import { readConnectorHealthSnapshot } from './connectors.js';
27
28
  import { readTimeoutStatus } from './timeout-status.js';
28
29
  import { queryRunHistory } from '../run-history.js';
@@ -431,6 +432,16 @@ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847,
431
432
  return;
432
433
  }
433
434
 
435
+ if (pathname === '/api/notifications') {
436
+ if (replayMode) {
437
+ writeJson(res, 200, { ok: true, replay_mode: true, message: 'Notification audit is live-only and not available in replay mode.' });
438
+ return;
439
+ }
440
+ const result = readNotificationSnapshot(workspacePath);
441
+ writeJson(res, result.status, result.body);
442
+ return;
443
+ }
444
+
434
445
  if (pathname === '/api/connectors') {
435
446
  const result = readConnectorHealthSnapshot(workspacePath);
436
447
  writeJson(res, result.status, result.body);
@@ -0,0 +1,91 @@
1
+ import { loadConfig, loadProjectContext } from '../config.js';
2
+ import { readJsonlFile } from './state-reader.js';
3
+
4
+ function summarizeAuditEntries(entries) {
5
+ const summary = {
6
+ total_attempts: entries.length,
7
+ delivered: 0,
8
+ failed: 0,
9
+ timed_out: 0,
10
+ last_emitted_at: null,
11
+ last_failure_at: null,
12
+ };
13
+
14
+ for (const entry of entries) {
15
+ if (entry?.delivered === true) {
16
+ summary.delivered += 1;
17
+ } else {
18
+ summary.failed += 1;
19
+ if (!summary.last_failure_at || String(entry?.emitted_at || '') > summary.last_failure_at) {
20
+ summary.last_failure_at = entry?.emitted_at || null;
21
+ }
22
+ }
23
+ if (entry?.timed_out === true) {
24
+ summary.timed_out += 1;
25
+ }
26
+ if (!summary.last_emitted_at || String(entry?.emitted_at || '') > summary.last_emitted_at) {
27
+ summary.last_emitted_at = entry?.emitted_at || null;
28
+ }
29
+ }
30
+
31
+ return summary;
32
+ }
33
+
34
+ function normalizeWebhook(webhook) {
35
+ return {
36
+ name: webhook.name,
37
+ timeout_ms: webhook.timeout_ms,
38
+ event_count: Array.isArray(webhook.events) ? webhook.events.length : 0,
39
+ events: Array.isArray(webhook.events) ? webhook.events : [],
40
+ };
41
+ }
42
+
43
+ export function readNotificationSnapshot(workspacePath) {
44
+ const context = loadProjectContext(workspacePath);
45
+ const governedContext = context?.config ? context : null;
46
+ const legacyConfigResult = governedContext ? null : loadConfig(workspacePath);
47
+ if (!governedContext && !legacyConfigResult) {
48
+ return {
49
+ ok: false,
50
+ status: 404,
51
+ body: {
52
+ ok: false,
53
+ code: 'config_missing',
54
+ error: 'Project config not found. Run `agentxchain init --governed` first.',
55
+ },
56
+ };
57
+ }
58
+
59
+ const root = governedContext?.root || legacyConfigResult.root;
60
+ const config = governedContext?.config || legacyConfigResult.config;
61
+ const notifications = config?.notifications || {};
62
+ const webhooks = Array.isArray(notifications.webhooks)
63
+ ? notifications.webhooks.map(normalizeWebhook)
64
+ : [];
65
+ const configured = webhooks.length > 0;
66
+ const approvalSla = notifications.approval_sla
67
+ ? {
68
+ enabled: notifications.approval_sla.enabled !== false,
69
+ reminder_after_seconds: Array.isArray(notifications.approval_sla.reminder_after_seconds)
70
+ ? notifications.approval_sla.reminder_after_seconds
71
+ : [],
72
+ }
73
+ : null;
74
+
75
+ const auditEntries = (readJsonlFile(`${root}/.agentxchain`, 'notification-audit.jsonl') || [])
76
+ .slice()
77
+ .sort((a, b) => String(b?.emitted_at || '').localeCompare(String(a?.emitted_at || '')));
78
+
79
+ return {
80
+ ok: true,
81
+ status: 200,
82
+ body: {
83
+ ok: true,
84
+ configured,
85
+ webhooks,
86
+ approval_sla: approvalSla,
87
+ summary: summarizeAuditEntries(auditEntries),
88
+ recent: auditEntries.slice(0, 10),
89
+ },
90
+ };
91
+ }
@@ -12,8 +12,9 @@ import {
12
12
  deriveGovernedRunNextActions,
13
13
  deriveRuntimeBlockedGuidance,
14
14
  } from '../blocked-state.js';
15
- import { loadProjectContext } from '../config.js';
15
+ import { loadProjectContext, loadProjectState } from '../config.js';
16
16
  import { getContinuityStatus } from '../continuity-status.js';
17
+ import { reconcileStaleTurns } from '../stale-turn-watchdog.js';
17
18
  import { readRepoDecisions, summarizeRepoDecisions } from '../repo-decisions.js';
18
19
  import { readAllDispatchProgress } from '../dispatch-progress.js';
19
20
 
@@ -136,10 +137,21 @@ function enrichGovernedState(agentxchainDir, state) {
136
137
  return state;
137
138
  }
138
139
 
140
+ // Use loadProjectState to get reconciled state (approval-pause repair,
141
+ // budget reconciliation, recovery-action reconciliation applied and
142
+ // persisted to disk). Then apply stale-turn reconciliation so recovery
143
+ // and next-action surfaces reflect the post-watchdog truth — matching
144
+ // the same ordering used by the CLI `status` command.
145
+ let reconciledState = loadProjectState(workspacePath, context.config) || state;
146
+ const staleResult = reconcileStaleTurns(workspacePath, reconciledState, context.config);
147
+ if (staleResult.changed) {
148
+ reconciledState = staleResult.state;
149
+ }
150
+
139
151
  return {
140
- ...state,
141
- runtime_guidance: deriveRuntimeBlockedGuidance(state, context.config),
142
- next_actions: deriveGovernedRunNextActions(state, context.config),
152
+ ...reconciledState,
153
+ runtime_guidance: deriveRuntimeBlockedGuidance(reconciledState, context.config),
154
+ next_actions: deriveGovernedRunNextActions(reconciledState, context.config),
143
155
  dispatch_progress: readAllDispatchProgress(workspacePath),
144
156
  };
145
157
  }
@@ -55,7 +55,7 @@ const RESERVED_PATHS = [
55
55
  * Write a dispatch bundle for the currently assigned turn.
56
56
  *
57
57
  * @param {string} root - project root directory
58
- * @param {object} state - current governed state (must have current_turn)
58
+ * @param {object} state - current governed state (must expose an active turn via active_turns; current_turn is a non-enumerable compatibility alias re-attached on load, not a persisted schema field)
59
59
  * @param {object} config - normalized config
60
60
  * @param {object} [opts]
61
61
  * @param {string} [opts.turnId]
@@ -71,9 +71,10 @@ export function createDispatchProgressTracker(root, turn, options = {}) {
71
71
  runtime_id: turn.runtime_id || null,
72
72
  adapter_type,
73
73
  started_at: null,
74
+ first_output_at: null,
74
75
  last_activity_at: null,
75
- activity_type: 'output',
76
- activity_summary: 'Dispatch starting',
76
+ activity_type: 'starting',
77
+ activity_summary: 'Waiting for first output',
77
78
  output_lines: 0,
78
79
  stderr_lines: 0,
79
80
  silent_since: null,
@@ -123,7 +124,7 @@ export function createDispatchProgressTracker(root, turn, options = {}) {
123
124
  const now = new Date().toISOString();
124
125
  state.started_at = now;
125
126
  state.last_activity_at = now;
126
- state.activity_type = 'output';
127
+ state.activity_type = 'starting';
127
128
  state.activity_summary = 'Subprocess started';
128
129
  dirty = true;
129
130
  writeProgress();
@@ -137,6 +138,7 @@ export function createDispatchProgressTracker(root, turn, options = {}) {
137
138
  const now = new Date().toISOString();
138
139
  const wasSilent = state.activity_type === 'silent';
139
140
  state.last_activity_at = now;
141
+ state.first_output_at = state.first_output_at || now;
140
142
  state.activity_type = 'output';
141
143
  state.silent_since = null;
142
144
  if (stream === 'stderr') {