agentxchain 2.155.42 → 2.155.45

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.155.42",
3
+ "version": "2.155.45",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -490,17 +490,19 @@ function renderPrompt(role, roleId, turn, state, config, root) {
490
490
  lines.push('');
491
491
  lines.push('- `schema_version`: always `"1.0"`');
492
492
  lines.push('- `run_id`, `turn_id`, `role`, `runtime_id`: must match the values above exactly');
493
- lines.push('- `status`: one of `completed`, `blocked`, `needs_human`, `failed`');
493
+ lines.push('- `status`: one of `completed`, `blocked`, `needs_human`, `failed`. Do NOT use `complete`, `success`, `done`, or any other synonym — use the exact enum value `completed`.');
494
494
  lines.push('- `summary`: concise description of what you did this turn');
495
- lines.push('- `decisions[].id`: pattern `DEC-NNN` (increment from previous turn)');
495
+ lines.push('- `files_changed`: array of **strings** (file paths only). Do NOT use objects like `{path, change_type}` — just the path string (e.g. `["src/cli.js", "tests/smoke.mjs"]`).');
496
+ lines.push('- `decisions[].id`: pattern `DEC-NNN` where NNN is digits only (e.g. `DEC-001`, `DEC-002`). Do NOT use `D1`, `D2`, or freeform IDs.');
497
+ lines.push('- `decisions[].statement`: non-empty string describing the decision. Do NOT use `decision` or `description` as the field name — the field is `statement`.');
496
498
  lines.push('- `decisions[].category`: one of `implementation`, `architecture`, `scope`, `process`, `quality`, `release`');
497
- lines.push('- `objections[].id`: pattern `OBJ-NNN`');
499
+ lines.push('- `objections[].id`: pattern `OBJ-NNN` where NNN is digits only (e.g. `OBJ-001`, `OBJ-002`). Do NOT append extra suffixes like `-M31` or use non-numeric characters after `OBJ-`.');
498
500
  lines.push('- `objections[].severity`: one of `low`, `medium`, `high`, `blocking`');
499
- lines.push('- `verification.status`: one of `pass`, `fail`, `skipped`');
501
+ lines.push('- `verification.status`: **REQUIRED**. One of `pass`, `fail`, `skipped`. Always include this field in the `verification` object.');
500
502
  lines.push('- `verification.status: "pass"` is valid only when every `verification.machine_evidence[].exit_code` is `0`');
501
503
  lines.push('- Expected-failure checks must be wrapped in a verifier that exits `0` when the failure occurs as expected; do not list raw non-zero negative-case commands on a passing turn');
502
504
  lines.push('- If verification commands produce side-effect files (e.g., `.tusq/plan.json`, `coverage/`, `.cache/`), declare each in `verification.produced_files` with `disposition: "ignore"` (temporary output to clean up) or `disposition: "artifact"` (output to checkpoint as a turn deliverable). Undeclared dirty files with declared verification will be auto-cleaned but declaring them is preferred.');
503
- lines.push('- `artifact.type`: one of `workspace`, `patch`, `commit`, `review`');
505
+ lines.push('- `artifact.type`: **REQUIRED**. One of `workspace`, `patch`, `commit`, `review`.');
504
506
  lines.push('- If you make zero repo file edits, set `artifact.type` to `"review"` and `files_changed` to `[]`.');
505
507
  lines.push('- Only set `artifact.type` to `"workspace"` when you actually modified repo files and listed every changed path in `files_changed`.');
506
508
  lines.push('- Every `objections[]` item must include a non-empty `statement`; do not use `summary` or `detail` as a substitute.');
@@ -38,6 +38,7 @@ import {
38
38
  deriveAcceptedRef,
39
39
  checkCleanBaseline,
40
40
  detectDirtyFilesOutsideAllowed,
41
+ getBaselineUnchangedFiles,
41
42
  isOperationalPath,
42
43
  isBaselineExemptPath,
43
44
  normalizeCheckpointableFiles,
@@ -4504,6 +4505,22 @@ function _acceptGovernedTurnLocked(root, config, opts) {
4504
4505
  }
4505
4506
  }
4506
4507
 
4508
+ // BUG-91: Files that were dirty at dispatch baseline and have NOT been
4509
+ // modified during the turn (same SHA marker) should not block acceptance.
4510
+ // These are inherited workspace state — the turn did not create them.
4511
+ // Example: dogfood evidence files (.planning/dogfood-100-turn-evidence/)
4512
+ // that remain dirty across turns but are not turn-attributed mutations.
4513
+ const baselineUnchangedDirtyFiles = getBaselineUnchangedFiles(root, baseline);
4514
+ if (baselineUnchangedDirtyFiles.length > 0) {
4515
+ emitRunEvent(root, 'baseline_dirty_unchanged_excluded', {
4516
+ run_id: state.run_id,
4517
+ turn_id: currentTurn.turn_id,
4518
+ phase: state.phase,
4519
+ excluded_files: baselineUnchangedDirtyFiles,
4520
+ rationale: 'Files were dirty at dispatch baseline with matching SHA markers at acceptance time — inherited workspace state, not turn-attributed mutations.',
4521
+ });
4522
+ }
4523
+
4507
4524
  const dirtyParity = detectDirtyFilesOutsideAllowed(
4508
4525
  root,
4509
4526
  writeAuthority,
@@ -4511,6 +4528,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
4511
4528
  ...(turnResult.files_changed || []),
4512
4529
  ...concurrentAllowedDirtyFiles,
4513
4530
  ...uncheckpointedPriorFiles,
4531
+ ...baselineUnchangedDirtyFiles,
4514
4532
  ],
4515
4533
  );
4516
4534
  if (!dirtyParity.clean) {
@@ -835,6 +835,26 @@ export function captureDirtyWorkspaceSnapshot(root) {
835
835
  return snapshot;
836
836
  }
837
837
 
838
+ /**
839
+ * Return file paths that were dirty at dispatch baseline and have NOT changed
840
+ * since (same SHA marker). These files are inherited workspace state — the
841
+ * turn did not create or modify them — and should not block acceptance.
842
+ * BUG-91: prevents false-positive dirty-parity failures on pre-existing dirt.
843
+ */
844
+ export function getBaselineUnchangedFiles(root, baseline) {
845
+ const snapshot = baseline?.dirty_snapshot;
846
+ if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
847
+ return [];
848
+ }
849
+ const unchanged = [];
850
+ for (const [filePath, baselineSha] of Object.entries(snapshot)) {
851
+ if (baselineSha === getWorkspaceFileMarker(root, filePath)) {
852
+ unchanged.push(filePath);
853
+ }
854
+ }
855
+ return unchanged;
856
+ }
857
+
838
858
  function filterBaselineDirtyFiles(root, changedFiles, baseline) {
839
859
  const snapshot = baseline?.dirty_snapshot;
840
860
  if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
@@ -1018,6 +1018,138 @@ export function normalizeTurnResult(tr, config, context = {}) {
1018
1018
  const hasExplicitNoEditLifecycleSignal = normalized.run_completion_request === true
1019
1019
  || (typeof normalized.phase_transition_request === 'string' && normalized.phase_transition_request.trim().length > 0);
1020
1020
 
1021
+ // ── BUG-90: normalize status synonyms ────────────────────────────────
1022
+ const STATUS_SYNONYMS = { complete: 'completed', success: 'completed', done: 'completed', error: 'failed', failure: 'failed' };
1023
+ if (typeof normalized.status === 'string' && !VALID_STATUSES.includes(normalized.status)) {
1024
+ const mapped = STATUS_SYNONYMS[normalized.status.toLowerCase()];
1025
+ if (mapped) {
1026
+ corrections.push(`status: rewritten "${normalized.status}" → "${mapped}"`);
1027
+ normalizationEvents.push({
1028
+ field: 'status',
1029
+ original_value: normalized.status,
1030
+ normalized_value: mapped,
1031
+ rationale: 'status_synonym_rewritten',
1032
+ });
1033
+ normalized.status = mapped;
1034
+ }
1035
+ }
1036
+
1037
+ // ── BUG-90: normalize files_changed objects to path strings ──────────
1038
+ if (Array.isArray(normalized.files_changed)) {
1039
+ let filesCoerced = false;
1040
+ normalized.files_changed = normalized.files_changed.map((item, i) => {
1041
+ if (typeof item === 'string') return item;
1042
+ if (item !== null && typeof item === 'object' && typeof item.path === 'string') {
1043
+ filesCoerced = true;
1044
+ return item.path;
1045
+ }
1046
+ return item; // let validator catch
1047
+ });
1048
+ if (filesCoerced) {
1049
+ corrections.push('files_changed: coerced object entries to path strings');
1050
+ normalizationEvents.push({
1051
+ field: 'files_changed',
1052
+ original_value: '(object entries)',
1053
+ normalized_value: '(path strings)',
1054
+ rationale: 'files_changed_object_to_string',
1055
+ });
1056
+ }
1057
+ }
1058
+
1059
+ // ── BUG-90: normalize decisions ─────────────────────────────────────
1060
+ if (Array.isArray(normalized.decisions)) {
1061
+ normalized.decisions = normalized.decisions.map((dec, index) => {
1062
+ if (dec === null || typeof dec !== 'object' || Array.isArray(dec)) return dec;
1063
+ let patched = dec;
1064
+
1065
+ // Normalize id: "D1" / "D2" / arbitrary → "DEC-001" / "DEC-002"
1066
+ const validDecIdPattern = /^DEC-\d+$/;
1067
+ const currentDecId = typeof patched.id === 'string' ? patched.id : '';
1068
+ if (!validDecIdPattern.test(currentDecId)) {
1069
+ const normalizedDecId = `DEC-${String(index + 1).padStart(3, '0')}`;
1070
+ corrections.push(`decisions[${index}].id: rewritten "${currentDecId || '(missing)'}" → ${normalizedDecId}`);
1071
+ normalizationEvents.push({
1072
+ field: `decisions[${index}].id`,
1073
+ original_value: patched.id ?? null,
1074
+ normalized_value: normalizedDecId,
1075
+ rationale: 'invalid_decision_id_rewritten',
1076
+ });
1077
+ patched = { ...patched, id: normalizedDecId };
1078
+ }
1079
+
1080
+ // Normalize missing statement from decision/description field
1081
+ const stmt = typeof patched.statement === 'string' ? patched.statement.trim() : '';
1082
+ if (!stmt) {
1083
+ const alt = typeof patched.decision === 'string' ? patched.decision.trim()
1084
+ : typeof patched.description === 'string' ? patched.description.trim() : '';
1085
+ if (alt) {
1086
+ const srcField = typeof patched.decision === 'string' && patched.decision.trim() ? 'decision' : 'description';
1087
+ corrections.push(`decisions[${index}].statement: copied from ${srcField}`);
1088
+ normalizationEvents.push({
1089
+ field: `decisions[${index}].statement`,
1090
+ original_value: patched.statement ?? null,
1091
+ normalized_value: alt,
1092
+ rationale: `copied_from_${srcField}`,
1093
+ });
1094
+ patched = { ...patched, statement: alt };
1095
+ }
1096
+ }
1097
+
1098
+ // Default missing category to 'implementation'
1099
+ if (!patched.category || !VALID_CATEGORIES.includes(patched.category)) {
1100
+ const defaultCat = 'implementation';
1101
+ corrections.push(`decisions[${index}].category: defaulted to "${defaultCat}"`);
1102
+ normalizationEvents.push({
1103
+ field: `decisions[${index}].category`,
1104
+ original_value: patched.category ?? null,
1105
+ normalized_value: defaultCat,
1106
+ rationale: 'missing_category_defaulted',
1107
+ });
1108
+ patched = { ...patched, category: defaultCat };
1109
+ }
1110
+
1111
+ return patched;
1112
+ });
1113
+ }
1114
+
1115
+ // ── BUG-90: normalize missing verification.status ───────────────────
1116
+ if (
1117
+ normalized.verification
1118
+ && typeof normalized.verification === 'object'
1119
+ && !Array.isArray(normalized.verification)
1120
+ && !('status' in normalized.verification)
1121
+ ) {
1122
+ const exitCode = normalized.verification.exit_code;
1123
+ const inferredStatus = exitCode === 0 ? 'pass' : typeof exitCode === 'number' ? 'fail' : 'skipped';
1124
+ corrections.push(`verification.status: inferred "${inferredStatus}" from exit_code=${exitCode}`);
1125
+ normalizationEvents.push({
1126
+ field: 'verification.status',
1127
+ original_value: null,
1128
+ normalized_value: inferredStatus,
1129
+ rationale: 'verification_status_inferred_from_exit_code',
1130
+ });
1131
+ normalized.verification = { ...normalized.verification, status: inferredStatus };
1132
+ }
1133
+
1134
+ // ── BUG-90: normalize missing artifact.type ─────────────────────────
1135
+ if (
1136
+ normalized.artifact
1137
+ && typeof normalized.artifact === 'object'
1138
+ && !Array.isArray(normalized.artifact)
1139
+ && !('type' in normalized.artifact)
1140
+ ) {
1141
+ const hasFiles = Array.isArray(normalized.files_changed) && normalized.files_changed.length > 0;
1142
+ const inferredType = hasFiles ? 'workspace' : 'review';
1143
+ corrections.push(`artifact.type: inferred "${inferredType}" from files_changed`);
1144
+ normalizationEvents.push({
1145
+ field: 'artifact.type',
1146
+ original_value: null,
1147
+ normalized_value: inferredType,
1148
+ rationale: 'artifact_type_inferred_from_files_changed',
1149
+ });
1150
+ normalized.artifact = { ...normalized.artifact, type: inferredType };
1151
+ }
1152
+
1021
1153
  // ── Rule 0a: empty workspace artifact → no-edit review normalization ──
1022
1154
  if (
1023
1155
  normalized.artifact
@@ -1048,26 +1180,45 @@ export function normalizeTurnResult(tr, config, context = {}) {
1048
1180
  if (objection === null || typeof objection !== 'object' || Array.isArray(objection)) {
1049
1181
  return objection;
1050
1182
  }
1051
- const statement = typeof objection.statement === 'string' ? objection.statement.trim() : '';
1183
+
1184
+ let patched = objection;
1185
+
1186
+ // ── BUG-89: normalize invalid objection IDs to OBJ-NNN ──────────
1187
+ const validIdPattern = /^OBJ-\d+$/;
1188
+ const currentId = typeof patched.id === 'string' ? patched.id : '';
1189
+ if (!validIdPattern.test(currentId)) {
1190
+ const normalizedId = `OBJ-${String(index + 1).padStart(3, '0')}`;
1191
+ corrections.push(`objections[${index}].id: rewritten "${currentId || '(missing)'}" → ${normalizedId}`);
1192
+ normalizationEvents.push({
1193
+ field: `objections[${index}].id`,
1194
+ original_value: patched.id ?? null,
1195
+ normalized_value: normalizedId,
1196
+ rationale: 'invalid_objection_id_rewritten',
1197
+ });
1198
+ patched = { ...patched, id: normalizedId };
1199
+ }
1200
+
1201
+ // ── BUG-79: normalize missing statement from summary/detail ─────
1202
+ const statement = typeof patched.statement === 'string' ? patched.statement.trim() : '';
1052
1203
  if (statement) {
1053
- return objection;
1204
+ return patched;
1054
1205
  }
1055
- const summary = typeof objection.summary === 'string' ? objection.summary.trim() : '';
1056
- const detail = typeof objection.detail === 'string' ? objection.detail.trim() : '';
1206
+ const summary = typeof patched.summary === 'string' ? patched.summary.trim() : '';
1207
+ const detail = typeof patched.detail === 'string' ? patched.detail.trim() : '';
1057
1208
  const sourceField = summary ? 'summary' : detail ? 'detail' : null;
1058
1209
  const sourceValue = summary || detail;
1059
1210
  if (!sourceField) {
1060
- return objection;
1211
+ return patched;
1061
1212
  }
1062
1213
  corrections.push(`objections[${index}].statement: copied from ${sourceField}`);
1063
1214
  normalizationEvents.push({
1064
1215
  field: `objections[${index}].statement`,
1065
- original_value: objection.statement ?? null,
1216
+ original_value: patched.statement ?? null,
1066
1217
  normalized_value: sourceValue,
1067
1218
  rationale: `copied_from_${sourceField}`,
1068
1219
  });
1069
1220
  return {
1070
- ...objection,
1221
+ ...patched,
1071
1222
  statement: sourceValue,
1072
1223
  };
1073
1224
  });