agentxchain 2.131.0 → 2.132.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.131.0",
3
+ "version": "2.132.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -82,6 +82,9 @@ ALLOWED_RELEASE_PATHS=(
82
82
  ".planning/MARKETING/REDDIT_POSTS.md"
83
83
  ".planning/MARKETING/HN_SUBMISSION.md"
84
84
  "website-v2/static/llms.txt"
85
+ "website-v2/docs/getting-started.mdx"
86
+ "website-v2/docs/quickstart.mdx"
87
+ "website-v2/docs/five-minute-tutorial.mdx"
85
88
  "cli/homebrew/agentxchain.rb"
86
89
  "cli/homebrew/README.md"
87
90
  )
@@ -65,6 +65,9 @@ function printEvent(evt) {
65
65
  const conflictDetail = evt.event_type === 'turn_conflicted'
66
66
  ? ` — ${formatConflictDetail(evt)}`
67
67
  : '';
68
+ const conflictResolvedDetail = evt.event_type === 'conflict_resolved'
69
+ ? ` — ${formatConflictResolvedDetail(evt)}`
70
+ : '';
68
71
  const rejectionDetail = evt.event_type === 'turn_rejected' && evt.payload?.reason
69
72
  ? ` — ${evt.payload.reason}${evt.payload.failed_stage ? ` (${evt.payload.failed_stage})` : ''}`
70
73
  : '';
@@ -82,7 +85,10 @@ function printEvent(evt) {
82
85
  : evt.event_type === 'human_escalation_resolved' && evt.payload?.escalation_id
83
86
  ? ` ${evt.payload.escalation_id} via ${evt.payload.resolved_via || '?'}`
84
87
  : '';
85
- console.log(`${chalk.dim(ts)} ${type} ${chalk.cyan(runId)} ${phase}${turnInfo}${intentInfo}${conflictDetail}${rejectionDetail}${acceptanceFailedDetail}${phaseTransitionDetail}${gateFailedDetail}${humanEscalationDetail}`);
88
+ const coordinatorRetryDetail = evt.event_type === 'coordinator_retry' && evt.payload
89
+ ? ` — ws ${evt.payload.workstream_id || '?'} repo ${evt.payload.repo_id || '?'} (retry of ${evt.payload.failed_turn_id || '?'})`
90
+ : '';
91
+ console.log(`${chalk.dim(ts)} ${type} ${chalk.cyan(runId)} ${phase}${turnInfo}${intentInfo}${conflictDetail}${conflictResolvedDetail}${rejectionDetail}${acceptanceFailedDetail}${phaseTransitionDetail}${gateFailedDetail}${humanEscalationDetail}${coordinatorRetryDetail}`);
86
92
  }
87
93
 
88
94
  function formatConflictDetail(evt) {
@@ -105,6 +111,16 @@ function formatConflictDetail(evt) {
105
111
  return parts.filter(Boolean).join(' | ');
106
112
  }
107
113
 
114
+ function formatConflictResolvedDetail(evt) {
115
+ const payload = evt.payload || {};
116
+ const fileSummary = summarizeList(payload.conflicting_files, 3) || 'resolved conflict';
117
+ const resolvedVia = payload.resolution ? `via ${payload.resolution}` : null;
118
+ const overlapRatio = typeof payload.overlap_ratio === 'number'
119
+ ? `${Math.round(payload.overlap_ratio * 100)}% overlap`
120
+ : null;
121
+ return [fileSummary, resolvedVia, overlapRatio].filter(Boolean).join(' | ');
122
+ }
123
+
108
124
  function summarizeList(items, limit) {
109
125
  if (!Array.isArray(items) || items.length === 0) return '';
110
126
  const shown = items.slice(0, limit).join(', ');
@@ -123,6 +139,7 @@ function colorEventType(type) {
123
139
  acceptance_failed: chalk.red.bold,
124
140
  turn_reissued: chalk.cyan,
125
141
  turn_conflicted: chalk.redBright,
142
+ conflict_resolved: chalk.greenBright,
126
143
  phase_entered: chalk.magenta,
127
144
  escalation_raised: chalk.red.bold,
128
145
  escalation_resolved: chalk.green,
@@ -132,6 +149,9 @@ function colorEventType(type) {
132
149
  gate_approved: chalk.green,
133
150
  gate_failed: chalk.red,
134
151
  budget_exceeded_warn: chalk.yellowBright,
152
+ coordinator_retry: chalk.cyan.bold,
153
+ turn_checkpointed: chalk.green,
154
+ dispatch_progress: chalk.blue.dim,
135
155
  };
136
156
  const colorFn = colors[type] || chalk.white;
137
157
  return colorFn(pad(type, 22));
@@ -1088,15 +1088,63 @@ function cleanupTurnArtifacts(root, turnId) {
1088
1088
  } catch { /* best-effort */ }
1089
1089
  }
1090
1090
 
1091
- function detectAcceptanceConflict(targetTurn, conflictFiles, historyEntries) {
1091
+ function getWorkflowArtifactOwners(config, filePath) {
1092
+ const owners = new Set();
1093
+ const phases = config?.workflow_kit?.phases;
1094
+ if (!phases || typeof phases !== 'object') {
1095
+ return owners;
1096
+ }
1097
+
1098
+ for (const phaseConfig of Object.values(phases)) {
1099
+ const artifacts = Array.isArray(phaseConfig?.artifacts) ? phaseConfig.artifacts : [];
1100
+ for (const artifact of artifacts) {
1101
+ if (artifact?.path !== filePath) {
1102
+ continue;
1103
+ }
1104
+ if (typeof artifact.owned_by === 'string' && artifact.owned_by.length > 0) {
1105
+ owners.add(artifact.owned_by);
1106
+ }
1107
+ }
1108
+ }
1109
+
1110
+ return owners;
1111
+ }
1112
+
1113
+ function isForwardRevisionFile(targetTurn, historyEntry, filePath, config) {
1114
+ if (!targetTurn || !historyEntry) {
1115
+ return false;
1116
+ }
1117
+ if (historyEntry.role !== targetTurn.assigned_role) {
1118
+ return false;
1119
+ }
1120
+
1121
+ const explicitOwners = getWorkflowArtifactOwners(config, filePath);
1122
+ if (explicitOwners.size > 0) {
1123
+ return explicitOwners.size === 1 && explicitOwners.has(targetTurn.assigned_role);
1124
+ }
1125
+
1126
+ if (!filePath.startsWith('.planning/')) {
1127
+ return false;
1128
+ }
1129
+
1130
+ return config?.routing?.planning?.entry_role === targetTurn.assigned_role;
1131
+ }
1132
+
1133
+ function classifyAcceptanceOverlap(targetTurn, conflictFiles, historyEntries, config) {
1092
1134
  const observedFiles = [...new Set(Array.isArray(conflictFiles) ? conflictFiles : [])];
1093
1135
  if (observedFiles.length === 0) {
1094
- return null;
1136
+ return {
1137
+ conflict: null,
1138
+ forward_revision_files: [],
1139
+ forward_revision_turns: [],
1140
+ };
1095
1141
  }
1096
1142
 
1097
1143
  const observedFileSet = new Set(observedFiles);
1098
1144
  const acceptedSince = [];
1099
1145
  const conflictingFiles = new Set();
1146
+ const forwardRevisionFiles = new Set();
1147
+ const forwardRevisionTurns = new Map();
1100
1148
 
1101
1149
  for (const entry of historyEntries) {
1102
1150
  if ((entry.accepted_sequence || 0) <= (targetTurn.assigned_sequence || 0)) {
@@ -1108,35 +1156,77 @@ function detectAcceptanceConflict(targetTurn, conflictFiles, historyEntries) {
1108
1156
  continue;
1109
1157
  }
1110
1158
 
1111
- overlap.forEach(file => conflictingFiles.add(file));
1112
- acceptedSince.push({
1113
- turn_id: entry.turn_id,
1114
- role: entry.role,
1115
- accepted_sequence: entry.accepted_sequence,
1116
- files_changed: overlap,
1117
- });
1159
+ const destructiveFiles = [];
1160
+ const forwardFiles = [];
1161
+ for (const file of overlap) {
1162
+ if (isForwardRevisionFile(targetTurn, entry, file, config)) {
1163
+ forwardFiles.push(file);
1164
+ } else {
1165
+ destructiveFiles.push(file);
1166
+ }
1167
+ }
1168
+
1169
+ if (destructiveFiles.length > 0) {
1170
+ destructiveFiles.forEach(file => conflictingFiles.add(file));
1171
+ acceptedSince.push({
1172
+ turn_id: entry.turn_id,
1173
+ role: entry.role,
1174
+ accepted_sequence: entry.accepted_sequence,
1175
+ files_changed: destructiveFiles,
1176
+ });
1177
+ }
1178
+
1179
+ if (forwardFiles.length > 0) {
1180
+ forwardFiles.forEach(file => forwardRevisionFiles.add(file));
1181
+ const existing = forwardRevisionTurns.get(entry.turn_id) || {
1182
+ turn_id: entry.turn_id,
1183
+ role: entry.role,
1184
+ accepted_sequence: entry.accepted_sequence,
1185
+ files_changed: [],
1186
+ };
1187
+ existing.files_changed = [...new Set([...existing.files_changed, ...forwardFiles])];
1188
+ forwardRevisionTurns.set(entry.turn_id, existing);
1189
+ }
1118
1190
  }
1119
1191
 
1192
+ const forwardRevisionContext = {
1193
+ files: [...forwardRevisionFiles],
1194
+ accepted_since_turn_ids: [...forwardRevisionTurns.values()].map((entry) => entry.turn_id),
1195
+ accepted_since: [...forwardRevisionTurns.values()],
1196
+ };
1197
+
1120
1198
  if (acceptedSince.length === 0) {
1121
- return null;
1199
+ return {
1200
+ conflict: null,
1201
+ forward_revision_files: forwardRevisionContext.files,
1202
+ forward_revision_turns: forwardRevisionContext.accepted_since,
1203
+ };
1122
1204
  }
1123
1205
 
1124
1206
  const conflicting = [...conflictingFiles];
1125
1207
  const overlapRatio = observedFiles.length > 0 ? conflicting.length / observedFiles.length : 0;
1126
1208
 
1127
1209
  return {
1128
- type: 'file_conflict',
1129
- conflicting_turn: {
1130
- turn_id: targetTurn.turn_id,
1131
- role: targetTurn.assigned_role,
1132
- attempt: targetTurn.attempt,
1133
- files_changed: observedFiles,
1210
+ conflict: {
1211
+ type: 'file_conflict',
1212
+ conflicting_turn: {
1213
+ turn_id: targetTurn.turn_id,
1214
+ role: targetTurn.assigned_role,
1215
+ attempt: targetTurn.attempt,
1216
+ files_changed: observedFiles,
1217
+ },
1218
+ accepted_since: acceptedSince,
1219
+ conflicting_files: conflicting,
1220
+ non_conflicting_files: observedFiles.filter(
1221
+ (file) => !conflictingFiles.has(file) && !forwardRevisionFiles.has(file),
1222
+ ),
1223
+ forward_revision_files: forwardRevisionContext.files,
1224
+ forward_revision_turns: forwardRevisionContext.accepted_since,
1225
+ overlap_ratio: overlapRatio,
1226
+ suggested_resolution: overlapRatio < 0.5 ? 'reject_and_reassign' : 'human_merge',
1134
1227
  },
1135
- accepted_since: acceptedSince,
1136
- conflicting_files: conflicting,
1137
- non_conflicting_files: observedFiles.filter(file => !conflictingFiles.has(file)),
1138
- overlap_ratio: overlapRatio,
1139
- suggested_resolution: overlapRatio < 0.5 ? 'reject_and_reassign' : 'human_merge',
1228
+ forward_revision_files: forwardRevisionContext.files,
1229
+ forward_revision_turns: forwardRevisionContext.accepted_since,
1140
1230
  };
1141
1231
  }
1142
1232
 
@@ -2749,40 +2839,6 @@ function _acceptGovernedTurnLocked(root, config, opts) {
2749
2839
  error_code: 'protocol_error',
2750
2840
  };
2751
2841
  }
2752
-
2753
- if (currentTurn.conflict_state.status !== 'human_merging') {
2754
- appendJsonl(root, LEDGER_PATH, {
2755
- timestamp: new Date().toISOString(),
2756
- decision: 'conflict_resolution_selected',
2757
- turn_id: currentTurn.turn_id,
2758
- attempt: currentTurn.attempt,
2759
- role: currentTurn.assigned_role,
2760
- phase: state.phase,
2761
- conflict: {
2762
- conflicting_files: currentTurn.conflict_state.conflict_error?.conflicting_files || [],
2763
- accepted_since_turn_ids: (currentTurn.conflict_state.conflict_error?.accepted_since || []).map((entry) => entry.turn_id),
2764
- overlap_ratio: currentTurn.conflict_state.conflict_error?.overlap_ratio ?? 0,
2765
- },
2766
- resolution_chosen: 'human_merge',
2767
- });
2768
-
2769
- state = {
2770
- ...state,
2771
- active_turns: {
2772
- ...getActiveTurns(state),
2773
- [currentTurn.turn_id]: {
2774
- ...currentTurn,
2775
- status: 'conflicted',
2776
- conflict_state: {
2777
- ...currentTurn.conflict_state,
2778
- status: 'human_merging',
2779
- },
2780
- },
2781
- },
2782
- };
2783
- writeState(root, state);
2784
- currentTurn = state.active_turns[currentTurn.turn_id];
2785
- }
2786
2842
  }
2787
2843
 
2788
2844
  const turnStagingPath = getTurnStagingResultPath(currentTurn.turn_id);
@@ -3142,11 +3198,29 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3142
3198
  };
3143
3199
  }
3144
3200
 
3145
- const conflict = detectAcceptanceConflict(
3201
+ const overlapClassification = classifyAcceptanceOverlap(
3146
3202
  currentTurn,
3147
3203
  buildConflictCandidateFiles(rawObservation, observation, turnResult.files_changed || []),
3148
3204
  historyEntries,
3205
+ config,
3149
3206
  );
3207
+ const forwardRevision = overlapClassification.forward_revision_files.length > 0
3208
+ ? {
3209
+ files: overlapClassification.forward_revision_files,
3210
+ accepted_since_turn_ids: overlapClassification.forward_revision_turns.map((entry) => entry.turn_id),
3211
+ accepted_since: overlapClassification.forward_revision_turns,
3212
+ }
3213
+ : null;
3214
+ const conflict = resolutionMode === 'human_merge' ? null : overlapClassification.conflict;
3215
+ const conflictResolution = resolutionMode === 'human_merge'
3216
+ ? {
3217
+ mode: 'human_merge',
3218
+ merge_strategy: 'operator_authoritative_staged_result',
3219
+ conflicting_files: currentTurn.conflict_state?.conflict_error?.conflicting_files || [],
3220
+ accepted_since_turn_ids: (currentTurn.conflict_state?.conflict_error?.accepted_since || []).map((entry) => entry.turn_id),
3221
+ overlap_ratio: currentTurn.conflict_state?.conflict_error?.overlap_ratio ?? 0,
3222
+ }
3223
+ : null;
3150
3224
 
3151
3225
  if (conflict) {
3152
3226
  const detectionCount = (currentTurn.conflict_state?.detection_count || 0) + 1;
@@ -3301,6 +3375,8 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3301
3375
  assigned_sequence: Number.isInteger(currentTurn.assigned_sequence) ? currentTurn.assigned_sequence : acceptedSequence,
3302
3376
  accepted_sequence: acceptedSequence,
3303
3377
  concurrent_with: Array.isArray(currentTurn.concurrent_with) ? currentTurn.concurrent_with : [],
3378
+ ...(forwardRevision ? { forward_revision: forwardRevision } : {}),
3379
+ ...(conflictResolution ? { conflict_resolution: conflictResolution } : {}),
3304
3380
  cost: turnResult.cost || {},
3305
3381
  ...(currentTurn.started_at ? { started_at: currentTurn.started_at } : {}),
3306
3382
  accepted_at: now,
@@ -3357,6 +3433,49 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3357
3433
  });
3358
3434
  }
3359
3435
  }
3436
+ if (forwardRevision) {
3437
+ ledgerEntries.push({
3438
+ timestamp: now,
3439
+ decision: 'forward_revision_accepted',
3440
+ turn_id: currentTurn.turn_id,
3441
+ attempt: currentTurn.attempt,
3442
+ role: currentTurn.assigned_role,
3443
+ phase: state.phase,
3444
+ forward_revision: {
3445
+ files: forwardRevision.files,
3446
+ accepted_since_turn_ids: forwardRevision.accepted_since_turn_ids,
3447
+ },
3448
+ });
3449
+ }
3450
+ if (conflictResolution) {
3451
+ const conflictSummary = {
3452
+ conflicting_files: conflictResolution.conflicting_files,
3453
+ accepted_since_turn_ids: conflictResolution.accepted_since_turn_ids,
3454
+ overlap_ratio: conflictResolution.overlap_ratio,
3455
+ };
3456
+ ledgerEntries.push({
3457
+ timestamp: now,
3458
+ decision: 'conflict_resolution_selected',
3459
+ turn_id: currentTurn.turn_id,
3460
+ attempt: currentTurn.attempt,
3461
+ role: currentTurn.assigned_role,
3462
+ phase: state.phase,
3463
+ conflict: conflictSummary,
3464
+ resolution_chosen: conflictResolution.mode,
3465
+ merge_strategy: conflictResolution.merge_strategy,
3466
+ });
3467
+ ledgerEntries.push({
3468
+ timestamp: now,
3469
+ decision: 'conflict_resolved',
3470
+ turn_id: currentTurn.turn_id,
3471
+ attempt: currentTurn.attempt,
3472
+ role: currentTurn.assigned_role,
3473
+ phase: state.phase,
3474
+ conflict: conflictSummary,
3475
+ resolution_chosen: conflictResolution.mode,
3476
+ merge_strategy: conflictResolution.merge_strategy,
3477
+ });
3478
+ }
3360
3479
 
3361
3480
  const turnNumber = turnResult.turn_id.replace(/^turn_/, '').slice(0, 8);
3362
3481
  const talkSection = `## Turn ${turnNumber} — ${turnResult.role} (${state.phase})\n\n- **Status:** ${turnResult.status}\n- **Summary:** ${turnResult.summary}\n${turnResult.decisions?.length ? turnResult.decisions.map(d => `- **Decision ${d.id}:** ${d.statement}`).join('\n') + '\n' : ''}${turnResult.objections?.length ? turnResult.objections.map(o => `- **Objection ${o.id} (${o.severity}):** ${o.statement}`).join('\n') + '\n' : ''}- **Proposed next:** ${turnResult.proposed_next_role || 'human'}\n\n---\n`;
@@ -4107,6 +4226,22 @@ function _acceptGovernedTurnLocked(root, config, opts) {
4107
4226
  intent_id: currentTurn.intake_context?.intent_id || null,
4108
4227
  payload: turnAcceptedPayload,
4109
4228
  });
4229
+ if (conflictResolution) {
4230
+ emitRunEvent(root, 'conflict_resolved', {
4231
+ run_id: updatedState.run_id,
4232
+ phase: updatedState.phase,
4233
+ status: updatedState.status,
4234
+ turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
4235
+ intent_id: currentTurn.intake_context?.intent_id || null,
4236
+ payload: {
4237
+ resolution: conflictResolution.mode,
4238
+ merge_strategy: conflictResolution.merge_strategy,
4239
+ conflicting_files: conflictResolution.conflicting_files,
4240
+ accepted_since_turn_ids: conflictResolution.accepted_since_turn_ids,
4241
+ overlap_ratio: conflictResolution.overlap_ratio,
4242
+ },
4243
+ });
4244
+ }
4110
4245
 
4111
4246
  if (updatedState.status === 'blocked') {
4112
4247
  // DEC-RHTR-SPEC: Record blocked outcome in cross-run history (non-fatal)
@@ -36,6 +36,21 @@ function describeEvent(eventType, entry) {
36
36
  case 'gate_approved':
37
37
  case 'gate_failed':
38
38
  return `${prefix}${eventType}${gateId ? ` (${gateId})` : ''}`;
39
+ case 'turn_conflicted':
40
+ return `${prefix}${eventType}${roleId ? ` [${roleId}]` : ''}`;
41
+ case 'conflict_resolved': {
42
+ const resolution = trimToNull(entry.payload?.resolution);
43
+ return `${prefix}${eventType}${roleId ? ` [${roleId}]` : ''}${resolution ? ` via ${resolution}` : ''}`;
44
+ }
45
+ case 'coordinator_retry': {
46
+ const wsId = trimToNull(entry.payload?.workstream_id);
47
+ const retryRepo = trimToNull(entry.payload?.repo_id);
48
+ return `${prefix}${eventType}${wsId ? ` ${wsId}` : ''}${retryRepo ? ` (${retryRepo})` : ''}`;
49
+ }
50
+ case 'turn_checkpointed':
51
+ return `${prefix}${eventType}${roleId ? ` [${roleId}]` : ''}`;
52
+ case 'dispatch_progress':
53
+ return `${prefix}${eventType}${roleId ? ` [${roleId}]` : ''}`;
39
54
  case 'run_blocked':
40
55
  case 'run_completed':
41
56
  case 'run_started':
@@ -22,6 +22,12 @@ export function formatCount(value) {
22
22
  return new Intl.NumberFormat('en-US').format(value);
23
23
  }
24
24
 
25
+ const ONBOARDING_PREREQ_DOCS = [
26
+ 'website-v2/docs/getting-started.mdx',
27
+ 'website-v2/docs/quickstart.mdx',
28
+ 'website-v2/docs/five-minute-tutorial.mdx',
29
+ ];
30
+
25
31
  function normalizeEvidenceText(value) {
26
32
  return value
27
33
  .replace(/^\s*-\s*/, '')
@@ -137,6 +143,28 @@ function validateTextIncludesVersionAndEvidence(relativePath, label) {
137
143
  };
138
144
  }
139
145
 
146
+ function validateOnboardingPrereqs(ctx, repoRoot) {
147
+ const errors = [];
148
+ const requiredTokens = [
149
+ `Minimum CLI version: \`agentxchain ${ctx.targetVersion}\` or newer`,
150
+ 'agentxchain --version',
151
+ 'npm install -g agentxchain@latest',
152
+ 'brew upgrade agentxchain',
153
+ 'npx --yes -p agentxchain@latest -c "agentxchain <command>"',
154
+ ];
155
+
156
+ for (const relativePath of ONBOARDING_PREREQ_DOCS) {
157
+ const content = read(repoRoot, relativePath);
158
+ for (const token of requiredTokens) {
159
+ if (!content.includes(token)) {
160
+ errors.push(`${relativePath} must include: ${token}`);
161
+ }
162
+ }
163
+ }
164
+
165
+ return errors;
166
+ }
167
+
140
168
  export const RELEASE_ALIGNMENT_SURFACES = [
141
169
  {
142
170
  id: 'changelog',
@@ -274,6 +302,12 @@ export const RELEASE_ALIGNMENT_SURFACES = [
274
302
  : [`website-v2/static/llms.txt must list ${ctx.releaseRoute}`];
275
303
  },
276
304
  },
305
+ {
306
+ id: 'onboarding_prereqs',
307
+ label: 'onboarding docs CLI-version prerequisites',
308
+ scopes: [RELEASE_ALIGNMENT_SCOPES.PREBUMP, RELEASE_ALIGNMENT_SCOPES.CURRENT],
309
+ check: validateOnboardingPrereqs,
310
+ },
277
311
  {
278
312
  id: 'homebrew_formula_url',
279
313
  label: 'homebrew mirror formula url',
@@ -18,6 +18,7 @@ export const VALID_RUN_EVENTS = [
18
18
  'turn_accepted',
19
19
  'turn_rejected',
20
20
  'turn_conflicted',
21
+ 'conflict_resolved',
21
22
  'acceptance_failed',
22
23
  'turn_reissued',
23
24
  'turn_checkpointed',