agentxchain 2.155.29 → 2.155.31

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.
@@ -708,6 +708,7 @@ program
708
708
  .option('--checkpoint', 'Checkpoint the accepted turn to git immediately after acceptance')
709
709
  .option('--resolution <mode>', 'Conflict resolution mode for conflicted turns (standard, human_merge)', 'standard')
710
710
  .option('--normalize-artifact-type <type>', 'Normalize an empty workspace artifact to a safe artifact type before acceptance (currently: review)')
711
+ .option('--normalize-staged-result', 'Run known-safe staged-result normalization before acceptance')
711
712
  .action(acceptTurnCommand);
712
713
 
713
714
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.155.29",
3
+ "version": "2.155.31",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,7 @@ export async function acceptTurnCommand(opts = {}) {
23
23
  turnId: opts.turn,
24
24
  resolutionMode: opts.resolution || 'standard',
25
25
  normalizeArtifactType: opts.normalizeArtifactType || null,
26
+ normalizeStagedResult: Boolean(opts.normalizeStagedResult),
26
27
  });
27
28
  if (!result.ok) {
28
29
  if (result.error_code === 'policy_escalation' || result.error_code === 'policy_violation') {
@@ -137,6 +138,10 @@ export async function acceptTurnCommand(opts = {}) {
137
138
  console.log(` ${chalk.dim('Reason:')} ${errorClass}`);
138
139
  console.log(` ${chalk.dim('Owner:')} human`);
139
140
  console.log(` ${chalk.dim('Action:')} Fix staged result and rerun agentxchain accept-turn, or reject with agentxchain reject-turn --reason "..."`);
141
+ if (result.validation?.errors?.some((err) => /objections\[\d+\]\.statement/.test(err))) {
142
+ const retainedTurnId = opts.turn || result.state?.current_turn?.turn_id || '(turn_id)';
143
+ console.log(` ${chalk.dim('Recovery:')} agentxchain accept-turn --turn ${retainedTurnId} --normalize-staged-result`);
144
+ }
140
145
  console.log(` ${chalk.dim('Turn:')} retained`);
141
146
  if (result.validation?.errors?.length) {
142
147
  for (const err of result.validation.errors) {
@@ -546,6 +546,7 @@ You are the **${role.title}** on this project.
546
546
  3. Write your structured turn result to the turn-scoped staging path printed by the orchestrator (\`.agentxchain/staging/<turn_id>/turn-result.json\`).
547
547
  4. If you make zero repo file edits, set \`artifact.type: "review"\` and \`files_changed: []\`.
548
548
  5. Only set \`artifact.type: "workspace"\` when you actually modified repo files and listed every changed path in \`files_changed\`.
549
+ 6. Every objection must include a non-empty \`statement\`; do not use \`summary\` or \`detail\` as a substitute.
549
550
 
550
551
  ## File Access
551
552
 
@@ -502,6 +502,7 @@ function renderPrompt(role, roleId, turn, state, config, root) {
502
502
  lines.push('- `artifact.type`: one of `workspace`, `patch`, `commit`, `review`');
503
503
  lines.push('- If you make zero repo file edits, set `artifact.type` to `"review"` and `files_changed` to `[]`.');
504
504
  lines.push('- Only set `artifact.type` to `"workspace"` when you actually modified repo files and listed every changed path in `files_changed`.');
505
+ lines.push('- Every `objections[]` item must include a non-empty `statement`; do not use `summary` or `detail` as a substitute.');
505
506
  lines.push('- `proposed_next_role`: must be in allowed_next_roles for current phase, or `human`');
506
507
  if (role.write_authority === 'review_only') {
507
508
  lines.push('- `objections`: **must be non-empty** (challenge requirement for review_only roles)');
@@ -4315,7 +4315,10 @@ function _acceptGovernedTurnLocked(root, config, opts) {
4315
4315
  }
4316
4316
 
4317
4317
  if (!validation.ok) {
4318
- const failError = `Validation failed at stage ${validation.stage}: ${validation.errors.join('; ')}`;
4318
+ const recoveryHint = validation.errors.some((error) => /objections\[\d+\]\.statement/.test(error))
4319
+ ? ` Recovery: agentxchain accept-turn --turn ${currentTurn.turn_id} --normalize-staged-result`
4320
+ : '';
4321
+ const failError = `Validation failed at stage ${validation.stage}: ${validation.errors.join('; ')}${recoveryHint}`;
4319
4322
  transitionToFailedAcceptance(root, state, currentTurn, failError, {
4320
4323
  error_code: 'validation_failed',
4321
4324
  stage: validation.stage,
@@ -4345,6 +4348,22 @@ function _acceptGovernedTurnLocked(root, config, opts) {
4345
4348
  },
4346
4349
  });
4347
4350
  }
4351
+ for (const event of Array.isArray(validation.normalization_events) ? validation.normalization_events : []) {
4352
+ emitRunEvent(root, 'staged_result_auto_normalized', {
4353
+ run_id: state.run_id,
4354
+ phase: state.phase,
4355
+ status: state.status,
4356
+ turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
4357
+ intent_id: currentTurn.intake_context?.intent_id || null,
4358
+ payload: {
4359
+ field: event.field,
4360
+ original_value: event.original_value,
4361
+ normalized_value: event.normalized_value,
4362
+ rationale: event.rationale,
4363
+ staging_path: resolvedStagingPath,
4364
+ },
4365
+ });
4366
+ }
4348
4367
 
4349
4368
  const rawTurnResult = validation.turnResult;
4350
4369
  const verificationProducedFiles = normalizeVerificationProducedFiles(rawTurnResult.verification);
@@ -6948,6 +6967,12 @@ function evaluateIntentCoverage(turnResult, intakeContext, { state = null, confi
6948
6967
  continue;
6949
6968
  }
6950
6969
 
6970
+ const roadmapCoverage = evaluateRoadmapDerivedConditionalCoverage(item, turnResult, intakeContext, state);
6971
+ if (roadmapCoverage === true) {
6972
+ addressed.push(item);
6973
+ continue;
6974
+ }
6975
+
6951
6976
  // Check 1: Structural — intent_response field with explicit status
6952
6977
  const structuralEntry = responseMap.get(normalizedItem);
6953
6978
  if (structuralEntry && ['addressed', 'deferred', 'rejected'].includes(structuralEntry.status)) {
@@ -7031,6 +7056,68 @@ function evaluateIdleExpansionConditionalCoverage(item, turnResult, intakeContex
7031
7056
  return null;
7032
7057
  }
7033
7058
 
7059
+ // ── Roadmap-derived intent conditional coverage (BUG-80) ────────────────────
7060
+ //
7061
+ // Roadmap-derived intents (charter starts with "[roadmap]") include literal
7062
+ // implementation text as acceptance items. PM turns in planning phase produce
7063
+ // charter/scoping artifacts, not implementation code, so the generic 50%
7064
+ // keyword-overlap check fails. This function provides phase-aware evaluation:
7065
+ // - "Evidence source:" items are metadata provenance → always addressed
7066
+ // - "Unchecked roadmap item completed:" in planning phase → milestone-mention check
7067
+ // - In implementation+ phases → falls through to normal semantic matching
7068
+
7069
+ function evaluateRoadmapDerivedConditionalCoverage(item, turnResult, intakeContext, state) {
7070
+ const charter = intakeContext?.charter || '';
7071
+ if (!charter.startsWith('[roadmap]')) {
7072
+ return null;
7073
+ }
7074
+
7075
+ const normalizedItem = typeof item === 'string' ? item.toLowerCase().trim() : '';
7076
+
7077
+ // "Evidence source:" items are provenance metadata, not deliverables.
7078
+ // No turn of any role can "address" a file path reference.
7079
+ if (normalizedItem.startsWith('evidence source:')) {
7080
+ return true;
7081
+ }
7082
+
7083
+ // "Unchecked roadmap item completed:" contains literal implementation text.
7084
+ // In planning phase, PM scopes the milestone — check for milestone mention
7085
+ // rather than requiring implementation-keyword overlap.
7086
+ // In implementation/qa/launch phases, fall through to normal semantic matching
7087
+ // so dev turns are still evaluated against actual implementation keywords.
7088
+ if (normalizedItem.startsWith('unchecked roadmap item completed:')) {
7089
+ const currentPhase = state?.phase || '';
7090
+ if (currentPhase !== 'planning') {
7091
+ return null;
7092
+ }
7093
+
7094
+ // Extract milestone section identifier from charter: "[roadmap] M28: ..."
7095
+ const sectionMatch = charter.match(/\[roadmap\]\s*(M\d+)/i);
7096
+ if (!sectionMatch) {
7097
+ return null;
7098
+ }
7099
+
7100
+ const milestoneId = sectionMatch[1].toLowerCase();
7101
+
7102
+ // Build a searchable corpus from the turn result
7103
+ const corpus = [
7104
+ turnResult.summary || '',
7105
+ ...(turnResult.decisions || []).map(d => `${d.statement || ''} ${d.rationale || ''}`),
7106
+ ...(turnResult.objections || []).map(o => o.statement || ''),
7107
+ ...(turnResult.files_changed || []),
7108
+ ...(turnResult.artifacts_created || []),
7109
+ ...(Array.isArray(turnResult.intent_response)
7110
+ ? turnResult.intent_response.map(r => `${r.item || ''} ${r.detail || ''}`)
7111
+ : []),
7112
+ ].join('\n').toLowerCase();
7113
+
7114
+ // If the turn mentions the milestone section ID, the item is addressed
7115
+ return corpus.includes(milestoneId);
7116
+ }
7117
+
7118
+ return null;
7119
+ }
7120
+
7034
7121
  export {
7035
7122
  STATE_PATH,
7036
7123
  HISTORY_PATH,
@@ -50,6 +50,7 @@ export const VALID_RUN_EVENTS = [
50
50
  'operator_commit_reconcile_refused',
51
51
  'charter_materialization_required',
52
52
  'artifact_type_auto_normalized',
53
+ 'staged_result_auto_normalized',
53
54
  ];
54
55
 
55
56
  /**
@@ -94,7 +94,7 @@ export function validateStagedTurnResult(root, state, config, opts = {}) {
94
94
  if (opts.normalizeArtifactType === 'review') {
95
95
  normContext.forceReviewArtifact = true;
96
96
  }
97
- const { normalized, corrections } = normalizeTurnResult(turnResult, config, normContext);
97
+ const { normalized, corrections, normalizationEvents } = normalizeTurnResult(turnResult, config, normContext);
98
98
  turnResult = normalized;
99
99
  const sidecarResult = maybeAttachIdleExpansionSidecar(
100
100
  root,
@@ -159,6 +159,7 @@ export function validateStagedTurnResult(root, state, config, opts = {}) {
159
159
  warnings: allWarnings,
160
160
  turnResult,
161
161
  normalizations: corrections,
162
+ normalization_events: normalizationEvents,
162
163
  };
163
164
  }
164
165
 
@@ -995,8 +996,9 @@ function validateProtocol(tr, state, config) {
995
996
  */
996
997
  export function normalizeTurnResult(tr, config, context = {}) {
997
998
  const corrections = [];
999
+ const normalizationEvents = [];
998
1000
  if (tr === null || typeof tr !== 'object' || Array.isArray(tr)) {
999
- return { normalized: tr, corrections };
1001
+ return { normalized: tr, corrections, normalizationEvents };
1000
1002
  }
1001
1003
 
1002
1004
  const normalized = { ...tr };
@@ -1023,7 +1025,7 @@ export function normalizeTurnResult(tr, config, context = {}) {
1023
1025
  && !Array.isArray(normalized.artifact)
1024
1026
  && normalized.artifact.type === 'workspace'
1025
1027
  && filesChangedIsEmpty
1026
- && (context.forceReviewArtifact || isReviewOnly || hasExplicitNoEditLifecycleSignal)
1028
+ && (context.forceReviewArtifact || hasExplicitNoEditLifecycleSignal)
1027
1029
  ) {
1028
1030
  normalized.artifact = {
1029
1031
  ...normalized.artifact,
@@ -1033,6 +1035,42 @@ export function normalizeTurnResult(tr, config, context = {}) {
1033
1035
  : normalized.artifact.ref ?? null,
1034
1036
  };
1035
1037
  corrections.push('artifact.type: auto-normalized empty workspace artifact to review because files_changed is empty and no repo mutation was declared');
1038
+ normalizationEvents.push({
1039
+ field: 'artifact.type',
1040
+ original_value: 'workspace',
1041
+ normalized_value: 'review',
1042
+ rationale: 'empty_files_changed_no_repo_mutation_declared',
1043
+ });
1044
+ }
1045
+
1046
+ if (Array.isArray(normalized.objections)) {
1047
+ normalized.objections = normalized.objections.map((objection, index) => {
1048
+ if (objection === null || typeof objection !== 'object' || Array.isArray(objection)) {
1049
+ return objection;
1050
+ }
1051
+ const statement = typeof objection.statement === 'string' ? objection.statement.trim() : '';
1052
+ if (statement) {
1053
+ return objection;
1054
+ }
1055
+ const summary = typeof objection.summary === 'string' ? objection.summary.trim() : '';
1056
+ const detail = typeof objection.detail === 'string' ? objection.detail.trim() : '';
1057
+ const sourceField = summary ? 'summary' : detail ? 'detail' : null;
1058
+ const sourceValue = summary || detail;
1059
+ if (!sourceField) {
1060
+ return objection;
1061
+ }
1062
+ corrections.push(`objections[${index}].statement: copied from ${sourceField}`);
1063
+ normalizationEvents.push({
1064
+ field: `objections[${index}].statement`,
1065
+ original_value: objection.statement ?? null,
1066
+ normalized_value: sourceValue,
1067
+ rationale: `copied_from_${sourceField}`,
1068
+ });
1069
+ return {
1070
+ ...objection,
1071
+ statement: sourceValue,
1072
+ };
1073
+ });
1036
1074
  }
1037
1075
 
1038
1076
  const pickAllowedRoleFallback = () => {
@@ -1238,7 +1276,7 @@ export function normalizeTurnResult(tr, config, context = {}) {
1238
1276
  }
1239
1277
  }
1240
1278
 
1241
- return { normalized, corrections };
1279
+ return { normalized, corrections, normalizationEvents };
1242
1280
  }
1243
1281
 
1244
1282
  function normalizeIdleExpansionMutualExclusionSentinel(result) {