agentxchain 2.155.43 → 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
|
@@ -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('- `
|
|
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
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`:
|
|
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`:
|
|
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) {
|
package/src/lib/repo-observer.js
CHANGED
|
@@ -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
|