agentxchain 2.153.0 → 2.154.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.
@@ -73,6 +73,7 @@ import { injectCommand } from '../src/commands/inject.js';
73
73
  import { escalateCommand } from '../src/commands/escalate.js';
74
74
  import { acceptTurnCommand } from '../src/commands/accept-turn.js';
75
75
  import { checkpointTurnCommand } from '../src/commands/checkpoint-turn.js';
76
+ import { reconcileStateCommand } from '../src/commands/reconcile-state.js';
76
77
  import { rejectTurnCommand } from '../src/commands/reject-turn.js';
77
78
  import { reissueTurnCommand } from '../src/commands/reissue-turn.js';
78
79
  import { proposalListCommand, proposalDiffCommand, proposalApplyCommand, proposalRejectCommand } from '../src/commands/proposal.js';
@@ -701,6 +702,12 @@ program
701
702
  .option('--turn <id>', 'Checkpoint a specific accepted turn from history')
702
703
  .action(checkpointTurnCommand);
703
704
 
705
+ program
706
+ .command('reconcile-state')
707
+ .description('Reconcile safe operator commits into governed run state')
708
+ .option('--accept-operator-head', 'Accept safe fast-forward operator commits as the new governed baseline')
709
+ .action(reconcileStateCommand);
710
+
704
711
  program
705
712
  .command('reject-turn')
706
713
  .description('Reject the current governed turn result and retry or escalate')
@@ -757,6 +764,7 @@ program
757
764
  .option('--no-auto-retry-on-ghost', 'Disable bounded automatic retry for continuous-mode startup ghost turns')
758
765
  .option('--auto-retry-on-ghost-max-retries <n>', 'Maximum startup ghost retries per continuous run (default: config or 3)', parseInt)
759
766
  .option('--auto-retry-on-ghost-cooldown-seconds <n>', 'Seconds to wait between startup ghost retries (default: config or 5)', parseInt)
767
+ .option('--reconcile-operator-commits <mode>', 'Continuous reconcile posture for operator commits: manual, auto_safe_only, or disabled (default: config or manual; auto_safe_only under full-auto approval policy)')
760
768
  .option('--auto-checkpoint', 'Auto-commit accepted writable turns after acceptance')
761
769
  .option('--no-auto-checkpoint', 'Disable automatic checkpointing after accepted writable turns')
762
770
  .action(runCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.153.0",
3
+ "version": "2.154.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,49 @@
1
+ import chalk from 'chalk';
2
+ import { loadProjectContext } from '../lib/config.js';
3
+ import { reconcileOperatorHead } from '../lib/operator-commit-reconcile.js';
4
+
5
+ export async function reconcileStateCommand(opts = {}) {
6
+ const context = loadProjectContext();
7
+ if (!context) {
8
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
9
+ process.exit(1);
10
+ }
11
+
12
+ const { root, config } = context;
13
+ if (config.protocol_mode !== 'governed') {
14
+ console.log(chalk.red('The reconcile-state command is only available for governed projects.'));
15
+ process.exit(1);
16
+ }
17
+
18
+ if (!opts.acceptOperatorHead) {
19
+ console.log(chalk.red('No reconciliation action selected.'));
20
+ console.log(chalk.dim('Run `agentxchain reconcile-state --accept-operator-head` to accept safe operator commits on top of the last checkpoint.'));
21
+ process.exit(1);
22
+ }
23
+
24
+ const result = reconcileOperatorHead(root);
25
+ if (!result.ok) {
26
+ console.log(chalk.red(`Reconcile refused (${result.error_class || 'unknown'}).`));
27
+ console.log(chalk.red(result.error || 'Unable to reconcile operator commits.'));
28
+ if (result.offending_path) {
29
+ console.log(chalk.dim(`Offending path: ${result.offending_path}`));
30
+ }
31
+ if (result.offending_commit) {
32
+ console.log(chalk.dim(`Offending commit: ${result.offending_commit}`));
33
+ }
34
+ console.log(chalk.dim('Manual recovery: inspect the commit range, restore governed state artifacts if needed, then restart from an explicit checkpoint.'));
35
+ process.exit(1);
36
+ }
37
+
38
+ if (result.no_op) {
39
+ console.log(chalk.green(`State already reconciled at ${result.accepted_head.slice(0, 8)}.`));
40
+ return;
41
+ }
42
+
43
+ console.log(chalk.green(`Reconciled ${result.accepted_commits.length} operator commit(s).`));
44
+ console.log(chalk.dim(`Previous baseline: ${result.previous_baseline}`));
45
+ console.log(chalk.dim(`Accepted HEAD: ${result.accepted_head}`));
46
+ if (result.paths_touched.length > 0) {
47
+ console.log(chalk.dim(`Paths touched: ${result.paths_touched.join(', ')}`));
48
+ }
49
+ }
@@ -129,8 +129,15 @@ export function getContinuityStatus(root, state) {
129
129
  && checkpoint.run_id !== state.run_id
130
130
  );
131
131
 
132
- const action = deriveRecommendedContinuityAction(state);
133
132
  const drift = deriveCheckpointDrift(root, checkpoint, staleCheckpoint);
133
+ const action = drift.drift_detected === true
134
+ ? {
135
+ recommended_command: 'agentxchain reconcile-state --accept-operator-head',
136
+ recommended_reason: 'operator_commit_drift',
137
+ recommended_detail: 'accept safe fast-forward operator commits as the new baseline',
138
+ restart_recommended: false,
139
+ }
140
+ : deriveRecommendedContinuityAction(state);
134
141
 
135
142
  return {
136
143
  checkpoint,
@@ -33,6 +33,8 @@ import {
33
33
  buildGhostRetryExhaustionMirror,
34
34
  classifyGhostRetryDecision,
35
35
  } from './ghost-retry.js';
36
+ import { reconcileOperatorHead } from './operator-commit-reconcile.js';
37
+ import { getContinuityStatus } from './continuity-status.js';
36
38
  import {
37
39
  archiveStaleIntentsForRun,
38
40
  formatLegacyIntentMigrationNotice,
@@ -335,6 +337,99 @@ export function findNextQueuedIntent(root, options = {}) {
335
337
  return findNextDispatchableIntent(root, { run_id: options.run_id || null });
336
338
  }
337
339
 
340
+ /**
341
+ * BUG-62 slice 2: when `run_loop.continuous.reconcile_operator_commits` is
342
+ * `auto_safe_only`, the continuous loop consults the session-checkpoint /
343
+ * governed-state baseline vs current git HEAD before dispatch. If operator
344
+ * commits landed on top of the baseline and the Turn 184 safety primitive
345
+ * accepts them, the baseline is auto-rolled forward so the next dispatch
346
+ * proceeds without manual `agentxchain reconcile-state` intervention. If the
347
+ * safety primitive refuses the commits (governed-state edits or history
348
+ * rewrite), the continuous loop pauses with the refusal class mirrored into
349
+ * `blocked_reason.recovery.detail`, preserving the manual primitive as the
350
+ * operator's single audited safety function per the BUG-62 spec.
351
+ */
352
+ export function maybeAutoReconcileOperatorCommits(context, session, contOpts, log = console.log) {
353
+ const mode = contOpts.reconcileOperatorCommits || 'manual';
354
+ if (mode !== 'auto_safe_only') {
355
+ return null;
356
+ }
357
+ const { root } = context;
358
+ const state = loadProjectState(root, context.config);
359
+ const continuity = getContinuityStatus(root, state);
360
+ if (!continuity || continuity.drift_detected !== true) {
361
+ return null;
362
+ }
363
+
364
+ const result = reconcileOperatorHead(root, { safetyMode: 'auto_safe_only' });
365
+ if (result.ok) {
366
+ if (result.no_op) {
367
+ return null;
368
+ }
369
+ const acceptedCount = result.accepted_commits?.length || 0;
370
+ log(
371
+ `Operator-commit auto-reconcile accepted ${acceptedCount} commit${acceptedCount === 1 ? '' : 's'} `
372
+ + `(${result.previous_baseline.slice(0, 8)} -> ${result.accepted_head.slice(0, 8)}).`
373
+ );
374
+ return null;
375
+ }
376
+
377
+ const errorClass = result.error_class || 'reconcile_refused';
378
+ const detailLines = [
379
+ `Operator-commit auto-reconcile refused (${errorClass}).`,
380
+ result.error || 'Unsafe operator commits detected; manual recovery required.',
381
+ 'Run: agentxchain reconcile-state --accept-operator-head once the unsafe changes are resolved, or revert them.',
382
+ ];
383
+ const detail = detailLines.join(' ');
384
+
385
+ if (state) {
386
+ const nextState = {
387
+ ...state,
388
+ status: 'blocked',
389
+ blocked_on: state.blocked_on || 'operator_commit_reconcile_refused',
390
+ blocked_reason: {
391
+ ...(state.blocked_reason || {}),
392
+ category: 'operator_commit_reconcile_refused',
393
+ error_class: errorClass,
394
+ recovery: {
395
+ ...((state.blocked_reason || {}).recovery || {}),
396
+ recovery_action: 'agentxchain reconcile-state --accept-operator-head',
397
+ detail,
398
+ },
399
+ },
400
+ };
401
+ safeWriteJson(join(root, '.agentxchain', 'state.json'), nextState);
402
+ }
403
+
404
+ emitRunEvent(root, 'operator_commit_reconcile_refused', {
405
+ run_id: state?.run_id || session.current_run_id || null,
406
+ phase: state?.phase || state?.current_phase || null,
407
+ status: 'blocked',
408
+ payload: {
409
+ error_class: errorClass,
410
+ message: result.error || null,
411
+ previous_baseline: result.previous_baseline || null,
412
+ current_head: result.current_head || null,
413
+ offending_commit: result.offending_commit || null,
414
+ offending_path: result.offending_path || null,
415
+ safety_mode: 'auto_safe_only',
416
+ },
417
+ });
418
+
419
+ session.status = 'paused';
420
+ writeContinuousSession(root, session);
421
+ log(detail);
422
+ return {
423
+ ok: true,
424
+ status: 'blocked',
425
+ action: 'operator_commit_reconcile_refused',
426
+ run_id: session.current_run_id,
427
+ recovery_action: 'agentxchain reconcile-state --accept-operator-head',
428
+ blocked_category: 'operator_commit_reconcile_refused',
429
+ error_class: errorClass,
430
+ };
431
+ }
432
+
338
433
  function reconcileContinuousStartupState(context, session, contOpts, log) {
339
434
  const { root, config } = context;
340
435
  const governedState = loadProjectState(root, config);
@@ -483,10 +578,24 @@ export function resolveContinuousOptions(opts, config) {
483
578
  const configCont = config?.run_loop?.continuous || {};
484
579
  const configGhostRetry = configCont.auto_retry_on_ghost || {};
485
580
  const explicitConfigGhostEnabled = Object.prototype.hasOwnProperty.call(configGhostRetry, 'enabled');
486
- const fullAutoGhostDefault = Boolean((opts.continuous ?? configCont.enabled ?? false) && isFullAutoApprovalPolicy(config));
581
+ const fullAuto = Boolean((opts.continuous ?? configCont.enabled ?? false) && isFullAutoApprovalPolicy(config));
582
+ const fullAutoGhostDefault = fullAuto;
487
583
  const resolvedGhostEnabled = opts.autoRetryOnGhost
488
584
  ?? (explicitConfigGhostEnabled ? configGhostRetry.enabled : fullAutoGhostDefault);
489
585
 
586
+ const validReconcileModes = new Set(['manual', 'auto_safe_only', 'disabled']);
587
+ const configuredReconcile = typeof configCont.reconcile_operator_commits === 'string'
588
+ && validReconcileModes.has(configCont.reconcile_operator_commits)
589
+ ? configCont.reconcile_operator_commits
590
+ : null;
591
+ const cliReconcile = typeof opts.reconcileOperatorCommits === 'string'
592
+ && validReconcileModes.has(opts.reconcileOperatorCommits)
593
+ ? opts.reconcileOperatorCommits
594
+ : null;
595
+ const reconcileOperatorCommits = cliReconcile
596
+ ?? configuredReconcile
597
+ ?? (fullAuto ? 'auto_safe_only' : 'manual');
598
+
490
599
  return {
491
600
  enabled: opts.continuous ?? configCont.enabled ?? false,
492
601
  continueFrom: opts.continueFrom ?? null,
@@ -507,6 +616,7 @@ export function resolveContinuousOptions(opts, config) {
507
616
  ?? configGhostRetry.cooldown_seconds
508
617
  ?? 5,
509
618
  },
619
+ reconcileOperatorCommits,
510
620
  };
511
621
  }
512
622
 
@@ -564,6 +674,9 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
564
674
 
565
675
  reconcileContinuousStartupState(context, session, contOpts, log);
566
676
 
677
+ const reconcileBlock = maybeAutoReconcileOperatorCommits(context, session, contOpts, log);
678
+ if (reconcileBlock) return reconcileBlock;
679
+
567
680
  // Paused-session guard: if session is paused (blocked run awaiting unblock),
568
681
  // check governed state before attempting to advance. Without this guard, the
569
682
  // loop would try to startIntent() on a blocked project, hit the blocked-state
@@ -646,6 +646,8 @@ export function validateRunLoopConfig(runLoop) {
646
646
  return errors;
647
647
  }
648
648
 
649
+ export const VALID_RECONCILE_OPERATOR_COMMITS = ['manual', 'auto_safe_only', 'disabled'];
650
+
649
651
  function validateRunLoopContinuousConfig(path, continuous, errors) {
650
652
  if (typeof continuous !== 'object' || Array.isArray(continuous)) {
651
653
  errors.push(`${path} must be an object`);
@@ -654,6 +656,19 @@ function validateRunLoopContinuousConfig(path, continuous, errors) {
654
656
  if (continuous.auto_retry_on_ghost !== undefined && continuous.auto_retry_on_ghost !== null) {
655
657
  validateAutoRetryOnGhostConfig(`${path}.auto_retry_on_ghost`, continuous.auto_retry_on_ghost, errors);
656
658
  }
659
+ if (
660
+ continuous.reconcile_operator_commits !== undefined
661
+ && continuous.reconcile_operator_commits !== null
662
+ ) {
663
+ if (
664
+ typeof continuous.reconcile_operator_commits !== 'string'
665
+ || !VALID_RECONCILE_OPERATOR_COMMITS.includes(continuous.reconcile_operator_commits)
666
+ ) {
667
+ errors.push(
668
+ `${path}.reconcile_operator_commits must be one of: ${VALID_RECONCILE_OPERATOR_COMMITS.join(', ')}`
669
+ );
670
+ }
671
+ }
657
672
  }
658
673
 
659
674
  function validateAutoRetryOnGhostConfig(path, value, errors) {
@@ -0,0 +1,260 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { emitRunEvent } from './run-events.js';
5
+ import { captureBaselineRef, readSessionCheckpoint, SESSION_PATH } from './session-checkpoint.js';
6
+ import { safeWriteJson } from './safe-write.js';
7
+
8
+ const STATE_PATH = '.agentxchain/state.json';
9
+ const CRITICAL_DELETION_PATHS = new Set([
10
+ '.planning/acceptance-matrix.md',
11
+ ]);
12
+
13
+ function git(root, args) {
14
+ return execFileSync('git', args, {
15
+ cwd: root,
16
+ encoding: 'utf8',
17
+ stdio: ['ignore', 'pipe', 'pipe'],
18
+ }).trim();
19
+ }
20
+
21
+ function gitOk(root, args) {
22
+ try {
23
+ execFileSync('git', args, {
24
+ cwd: root,
25
+ encoding: 'utf8',
26
+ stdio: ['ignore', 'pipe', 'pipe'],
27
+ });
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function extractGitError(err) {
35
+ const stderr = typeof err?.stderr === 'string' ? err.stderr.trim() : '';
36
+ const stdout = typeof err?.stdout === 'string' ? err.stdout.trim() : '';
37
+ return stderr || stdout || err?.message || 'git command failed';
38
+ }
39
+
40
+ function readState(root) {
41
+ const statePath = join(root, STATE_PATH);
42
+ if (!existsSync(statePath)) return null;
43
+ try {
44
+ return JSON.parse(readFileSync(statePath, 'utf8'));
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function normalizeGitRef(ref) {
51
+ if (typeof ref !== 'string') return null;
52
+ const trimmed = ref.trim();
53
+ if (!trimmed) return null;
54
+ return trimmed.startsWith('git:') ? trimmed.slice(4).trim() || null : trimmed;
55
+ }
56
+
57
+ function resolvePreviousBaseline(state, session) {
58
+ return (
59
+ normalizeGitRef(session?.baseline_ref?.git_head)
60
+ || normalizeGitRef(state?.accepted_integration_ref)
61
+ || normalizeGitRef(state?.last_completed_turn?.checkpoint_sha)
62
+ || null
63
+ );
64
+ }
65
+
66
+ function parseNameStatus(raw) {
67
+ if (!raw.trim()) return [];
68
+ return raw.split('\n').filter(Boolean).map((line) => {
69
+ const parts = line.split('\t');
70
+ const status = parts[0] || '';
71
+ const paths = parts.slice(1).filter(Boolean);
72
+ return { status, paths };
73
+ });
74
+ }
75
+
76
+ function listCommitPaths(root, sha) {
77
+ const raw = git(root, ['diff-tree', '--no-commit-id', '--name-status', '-r', sha]);
78
+ return parseNameStatus(raw);
79
+ }
80
+
81
+ function summarizeCommit(root, sha) {
82
+ const subject = git(root, ['log', '-1', '--format=%s', sha]);
83
+ const entries = listCommitPaths(root, sha);
84
+ const paths = [...new Set(entries.flatMap((entry) => entry.paths))].sort();
85
+ return {
86
+ sha,
87
+ subject,
88
+ paths_touched: paths,
89
+ name_status: entries,
90
+ };
91
+ }
92
+
93
+ function classifyUnsafeCommit(commit) {
94
+ for (const entry of commit.name_status) {
95
+ for (const pathName of entry.paths) {
96
+ if (pathName === '.agentxchain' || pathName.startsWith('.agentxchain/')) {
97
+ return {
98
+ error_class: 'governance_state_modified',
99
+ message: `Commit ${commit.sha.slice(0, 8)} modifies governed state path ${pathName}; reconcile cannot auto-accept .agentxchain edits.`,
100
+ commit: commit.sha,
101
+ path: pathName,
102
+ };
103
+ }
104
+ if (entry.status.startsWith('D') && CRITICAL_DELETION_PATHS.has(pathName)) {
105
+ return {
106
+ error_class: 'critical_artifact_deleted',
107
+ message: `Commit ${commit.sha.slice(0, 8)} deletes critical governed evidence ${pathName}; restore the artifact or restart from an explicit recovery point.`,
108
+ commit: commit.sha,
109
+ path: pathName,
110
+ };
111
+ }
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
117
+ function writeSessionBaseline(root, state, previousBaseline, acceptedHead, acceptedCommits) {
118
+ const existing = readSessionCheckpoint(root) || {};
119
+ const checkpoint = {
120
+ ...existing,
121
+ run_id: state?.run_id || existing.run_id || null,
122
+ last_checkpoint_at: new Date().toISOString(),
123
+ checkpoint_reason: 'operator_commit_reconciled',
124
+ run_status: state?.status || existing.run_status || null,
125
+ phase: state?.phase || state?.current_phase || existing.phase || null,
126
+ last_phase: state?.phase || state?.current_phase || existing.last_phase || null,
127
+ last_completed_turn_id: state?.last_completed_turn_id || existing.last_completed_turn_id || null,
128
+ baseline_ref: captureBaselineRef(root),
129
+ operator_commit_reconciliation: {
130
+ previous_baseline: previousBaseline,
131
+ accepted_head: acceptedHead,
132
+ commit_count: acceptedCommits.length,
133
+ },
134
+ };
135
+ writeFileSync(join(root, SESSION_PATH), JSON.stringify(checkpoint, null, 2) + '\n');
136
+ }
137
+
138
+ export function reconcileOperatorHead(root, opts = {}) {
139
+ let currentHead;
140
+ try {
141
+ if (git(root, ['rev-parse', '--is-inside-work-tree']) !== 'true') {
142
+ return { ok: false, error_class: 'not_git_repo', error: 'reconcile-state requires a git repository.' };
143
+ }
144
+ currentHead = git(root, ['rev-parse', 'HEAD']);
145
+ } catch (err) {
146
+ return { ok: false, error_class: 'git_unavailable', error: `Unable to inspect git HEAD: ${extractGitError(err)}` };
147
+ }
148
+
149
+ const state = readState(root);
150
+ const session = readSessionCheckpoint(root);
151
+ const previousBaseline = resolvePreviousBaseline(state, session);
152
+ if (!previousBaseline) {
153
+ return {
154
+ ok: false,
155
+ error_class: 'missing_baseline',
156
+ error: 'No prior checkpoint baseline found in session.json, state.accepted_integration_ref, or last_completed_turn.checkpoint_sha.',
157
+ };
158
+ }
159
+
160
+ if (previousBaseline === currentHead) {
161
+ return {
162
+ ok: true,
163
+ no_op: true,
164
+ previous_baseline: previousBaseline,
165
+ accepted_head: currentHead,
166
+ accepted_commits: [],
167
+ };
168
+ }
169
+
170
+ if (!gitOk(root, ['merge-base', '--is-ancestor', previousBaseline, currentHead])) {
171
+ return {
172
+ ok: false,
173
+ error_class: 'history_rewrite',
174
+ error: `Cannot reconcile operator HEAD: baseline ${previousBaseline.slice(0, 8)} is not an ancestor of current HEAD ${currentHead.slice(0, 8)}.`,
175
+ previous_baseline: previousBaseline,
176
+ current_head: currentHead,
177
+ };
178
+ }
179
+
180
+ let shas;
181
+ try {
182
+ shas = git(root, ['rev-list', '--reverse', `${previousBaseline}..${currentHead}`])
183
+ .split('\n')
184
+ .map((value) => value.trim())
185
+ .filter(Boolean);
186
+ } catch (err) {
187
+ return { ok: false, error_class: 'commit_walk_failed', error: `Failed to inspect operator commits: ${extractGitError(err)}` };
188
+ }
189
+
190
+ const commits = shas.map((sha) => summarizeCommit(root, sha));
191
+ for (const commit of commits) {
192
+ const unsafe = classifyUnsafeCommit(commit);
193
+ if (unsafe) {
194
+ return {
195
+ ok: false,
196
+ error_class: unsafe.error_class,
197
+ error: unsafe.message,
198
+ offending_commit: unsafe.commit,
199
+ offending_path: unsafe.path,
200
+ previous_baseline: previousBaseline,
201
+ current_head: currentHead,
202
+ };
203
+ }
204
+ }
205
+
206
+ const acceptedAt = new Date().toISOString();
207
+ const pathsTouched = [...new Set(commits.flatMap((commit) => commit.paths_touched))].sort();
208
+ const nextState = state
209
+ ? {
210
+ ...state,
211
+ accepted_integration_ref: `git:${currentHead}`,
212
+ operator_commit_reconciliation: {
213
+ reconciled_at: acceptedAt,
214
+ previous_baseline: previousBaseline,
215
+ accepted_head: currentHead,
216
+ commit_count: commits.length,
217
+ safety_mode: opts.safetyMode || 'manual_safe_only',
218
+ },
219
+ }
220
+ : null;
221
+
222
+ if (nextState) {
223
+ safeWriteJson(join(root, STATE_PATH), nextState);
224
+ }
225
+
226
+ emitRunEvent(root, 'state_reconciled_operator_commits', {
227
+ run_id: state?.run_id || null,
228
+ phase: state?.phase || state?.current_phase || null,
229
+ status: state?.status || null,
230
+ payload: {
231
+ previous_baseline: previousBaseline,
232
+ accepted_head: currentHead,
233
+ accepted_commits: commits.map((commit) => ({
234
+ sha: commit.sha,
235
+ subject: commit.subject,
236
+ paths_touched: commit.paths_touched,
237
+ })),
238
+ paths_touched: pathsTouched,
239
+ safety_checks: {
240
+ baseline_is_ancestor: true,
241
+ rejected_state_paths: ['.agentxchain/'],
242
+ rejected_deletions: [...CRITICAL_DELETION_PATHS],
243
+ },
244
+ },
245
+ });
246
+
247
+ writeSessionBaseline(root, nextState || state, previousBaseline, currentHead, commits);
248
+
249
+ return {
250
+ ok: true,
251
+ previous_baseline: previousBaseline,
252
+ accepted_head: currentHead,
253
+ accepted_commits: commits.map((commit) => ({
254
+ sha: commit.sha,
255
+ subject: commit.subject,
256
+ paths_touched: commit.paths_touched,
257
+ })),
258
+ paths_touched: pathsTouched,
259
+ };
260
+ }
@@ -46,6 +46,8 @@ export const VALID_RUN_EVENTS = [
46
46
  'session_continuation',
47
47
  'auto_retried_ghost',
48
48
  'ghost_retry_exhausted',
49
+ 'state_reconciled_operator_commits',
50
+ 'operator_commit_reconcile_refused',
49
51
  ];
50
52
 
51
53
  /**