agentxchain 2.21.0 → 2.23.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/README.md +1 -1
- package/bin/agentxchain.js +32 -0
- package/package.json +1 -1
- package/scripts/release-bump.sh +55 -8
- package/src/commands/escalate.js +1 -1
- package/src/commands/proposal.js +144 -0
- package/src/commands/resume.js +9 -3
- package/src/commands/status.js +2 -2
- package/src/commands/step.js +10 -5
- package/src/lib/adapters/api-proxy-adapter.js +47 -15
- package/src/lib/blocked-state.js +78 -8
- package/src/lib/config.js +11 -2
- package/src/lib/dispatch-bundle.js +176 -1
- package/src/lib/governed-state.js +481 -17
- package/src/lib/normalized-config.js +3 -3
- package/src/lib/proposal-ops.js +451 -0
- package/src/lib/repo-observer.js +1 -0
- package/src/lib/schemas/turn-result.schema.json +28 -0
- package/src/lib/turn-result-validator.js +35 -0
package/src/lib/blocked-state.js
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
deriveConflictLoopRecoveryAction,
|
|
3
|
+
deriveDispatchRecoveryAction,
|
|
4
|
+
deriveEscalationRecoveryAction,
|
|
5
|
+
deriveHookTamperRecoveryAction,
|
|
6
|
+
deriveNeedsHumanRecoveryAction,
|
|
7
|
+
getActiveTurnCount,
|
|
8
|
+
} from './governed-state.js';
|
|
2
9
|
|
|
3
|
-
|
|
10
|
+
function isLegacyEscalationRecoveryAction(action) {
|
|
11
|
+
return action === 'Resolve the escalation, then run agentxchain step --resume'
|
|
12
|
+
|| action === 'Resolve the escalation, then run agentxchain step';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isLegacyNeedsHumanRecoveryAction(action) {
|
|
16
|
+
return action === 'Resolve the stated issue, then run agentxchain step --resume';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isLegacyHookTamperRecoveryAction(action) {
|
|
20
|
+
return action === 'Disable or fix the hook, verify protected files, then run agentxchain step --resume';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isLegacyConflictLoopRecoveryAction(action) {
|
|
24
|
+
return typeof action === 'string' && action.startsWith('Serialize the conflicting work, then run agentxchain step --resume');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function maybeRefreshRecoveryAction(state, config, persistedRecovery, turnRetained) {
|
|
28
|
+
if (!config || !persistedRecovery || typeof persistedRecovery !== 'object') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const typedReason = persistedRecovery.typed_reason;
|
|
33
|
+
const currentAction = persistedRecovery.recovery_action || null;
|
|
34
|
+
const turnId = state?.blocked_reason?.turn_id ?? state?.escalation?.from_turn_id ?? null;
|
|
35
|
+
if (typedReason === 'retries_exhausted' || ((typedReason === 'operator_escalation') && isLegacyEscalationRecoveryAction(currentAction))) {
|
|
36
|
+
return deriveEscalationRecoveryAction(state, config, {
|
|
37
|
+
turnRetained,
|
|
38
|
+
turnId,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typedReason === 'needs_human' && isLegacyNeedsHumanRecoveryAction(currentAction)) {
|
|
43
|
+
return deriveNeedsHumanRecoveryAction(state, config, {
|
|
44
|
+
turnRetained,
|
|
45
|
+
turnId,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typedReason === 'hook_tamper' && isLegacyHookTamperRecoveryAction(currentAction)) {
|
|
50
|
+
return deriveHookTamperRecoveryAction(state, config, {
|
|
51
|
+
turnRetained,
|
|
52
|
+
turnId,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typedReason === 'conflict_loop' && isLegacyConflictLoopRecoveryAction(currentAction)) {
|
|
57
|
+
return deriveConflictLoopRecoveryAction(turnId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function deriveRecoveryDescriptor(state, config = null) {
|
|
4
64
|
if (!state || typeof state !== 'object') {
|
|
5
65
|
return null;
|
|
6
66
|
}
|
|
@@ -29,10 +89,13 @@ export function deriveRecoveryDescriptor(state) {
|
|
|
29
89
|
|
|
30
90
|
const persistedRecovery = state.blocked_reason?.recovery;
|
|
31
91
|
if (persistedRecovery && typeof persistedRecovery === 'object') {
|
|
92
|
+
const refreshedRecoveryAction = maybeRefreshRecoveryAction(state, config, persistedRecovery, turnRetained);
|
|
32
93
|
return {
|
|
33
94
|
typed_reason: persistedRecovery.typed_reason || 'unknown_block',
|
|
34
95
|
owner: persistedRecovery.owner || 'human',
|
|
35
|
-
recovery_action:
|
|
96
|
+
recovery_action: refreshedRecoveryAction
|
|
97
|
+
|| persistedRecovery.recovery_action
|
|
98
|
+
|| 'Inspect state.json and resolve manually before rerunning agentxchain step',
|
|
36
99
|
turn_retained: typeof persistedRecovery.turn_retained === 'boolean'
|
|
37
100
|
? persistedRecovery.turn_retained
|
|
38
101
|
: turnRetained,
|
|
@@ -48,7 +111,10 @@ export function deriveRecoveryDescriptor(state) {
|
|
|
48
111
|
return {
|
|
49
112
|
typed_reason: 'needs_human',
|
|
50
113
|
owner: 'human',
|
|
51
|
-
recovery_action:
|
|
114
|
+
recovery_action: deriveNeedsHumanRecoveryAction(state, config, {
|
|
115
|
+
turnRetained,
|
|
116
|
+
turnId: state.blocked_reason?.turn_id ?? null,
|
|
117
|
+
}),
|
|
52
118
|
turn_retained: turnRetained,
|
|
53
119
|
detail: state.blocked_on.slice('human:'.length) || null,
|
|
54
120
|
};
|
|
@@ -66,9 +132,10 @@ export function deriveRecoveryDescriptor(state) {
|
|
|
66
132
|
|
|
67
133
|
if (state.blocked_on.startsWith('escalation:')) {
|
|
68
134
|
const isOperatorEscalation = state.blocked_on.startsWith('escalation:operator:') || state.escalation?.source === 'operator';
|
|
69
|
-
const recoveryAction =
|
|
70
|
-
|
|
71
|
-
:
|
|
135
|
+
const recoveryAction = deriveEscalationRecoveryAction(state, config, {
|
|
136
|
+
turnRetained,
|
|
137
|
+
turnId: state.blocked_reason?.turn_id ?? state.escalation?.from_turn_id ?? null,
|
|
138
|
+
});
|
|
72
139
|
return {
|
|
73
140
|
typed_reason: isOperatorEscalation ? 'operator_escalation' : 'retries_exhausted',
|
|
74
141
|
owner: 'human',
|
|
@@ -82,7 +149,10 @@ export function deriveRecoveryDescriptor(state) {
|
|
|
82
149
|
return {
|
|
83
150
|
typed_reason: 'dispatch_error',
|
|
84
151
|
owner: 'human',
|
|
85
|
-
recovery_action:
|
|
152
|
+
recovery_action: deriveDispatchRecoveryAction(state, config, {
|
|
153
|
+
turnRetained,
|
|
154
|
+
turnId: state.blocked_reason?.turn_id ?? null,
|
|
155
|
+
}),
|
|
86
156
|
turn_retained: turnRetained,
|
|
87
157
|
detail: state.blocked_on.slice('dispatch:'.length) || state.blocked_on,
|
|
88
158
|
};
|
package/src/lib/config.js
CHANGED
|
@@ -3,7 +3,12 @@ import { join, parse as pathParse, resolve } from 'path';
|
|
|
3
3
|
import { safeParseJson, validateConfigSchema, validateLockSchema, validateProjectStateSchema, validateStateSchema } from './schema.js';
|
|
4
4
|
import { loadNormalizedConfig } from './normalized-config.js';
|
|
5
5
|
import { safeWriteJson } from './safe-write.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
normalizeGovernedStateShape,
|
|
8
|
+
getActiveTurn,
|
|
9
|
+
reconcileBudgetStatusWithConfig,
|
|
10
|
+
reconcileRecoveryActionsWithConfig,
|
|
11
|
+
} from './governed-state.js';
|
|
7
12
|
|
|
8
13
|
function attachLegacyCurrentTurnAlias(state) {
|
|
9
14
|
if (!state || typeof state !== 'object') {
|
|
@@ -148,7 +153,11 @@ export function loadProjectState(root, config) {
|
|
|
148
153
|
if (config?.protocol_mode === 'governed') {
|
|
149
154
|
const normalized = normalizeGovernedStateShape(stateData);
|
|
150
155
|
stateData = normalized.state;
|
|
151
|
-
|
|
156
|
+
const reconciledBudget = reconcileBudgetStatusWithConfig(stateData, config);
|
|
157
|
+
stateData = reconciledBudget.state;
|
|
158
|
+
const reconciledRecovery = reconcileRecoveryActionsWithConfig(stateData, config);
|
|
159
|
+
stateData = reconciledRecovery.state;
|
|
160
|
+
if (normalized.changed || reconciledBudget.changed || reconciledRecovery.changed) {
|
|
152
161
|
safeWriteJson(filePath, stateData);
|
|
153
162
|
}
|
|
154
163
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* orchestrator turn loop call it after assignGovernedTurn().
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'fs';
|
|
17
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync } from 'fs';
|
|
18
18
|
import { join } from 'path';
|
|
19
19
|
import { getActiveTurn, getActiveTurns } from './governed-state.js';
|
|
20
20
|
import {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
const HISTORY_PATH = '.agentxchain/history.jsonl';
|
|
32
32
|
const FILE_PREVIEW_MAX_FILES = 5;
|
|
33
33
|
const FILE_PREVIEW_MAX_LINES = 120;
|
|
34
|
+
const PROPOSAL_SUMMARY_MAX_LINES = 80;
|
|
34
35
|
const GATE_FILE_PREVIEW_MAX_LINES = 60;
|
|
35
36
|
const DISPATCH_LOG_MAX_LINES = 50;
|
|
36
37
|
const DISPATCH_LOG_MAX_LINE_BYTES = 8192;
|
|
@@ -229,6 +230,21 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
229
230
|
lines.push('');
|
|
230
231
|
lines.push('- You may propose changes as patches but cannot directly commit.');
|
|
231
232
|
lines.push('- Your artifact type should be `patch`.');
|
|
233
|
+
if (runtimeType === 'api_proxy') {
|
|
234
|
+
lines.push('- **This runtime cannot write repo files directly.** When doing work, you MUST return proposed changes as structured JSON.');
|
|
235
|
+
lines.push('- Include a `proposed_changes` array in your turn result with each file change (omit or set to `[]` on completion-only turns):');
|
|
236
|
+
lines.push(' ```json');
|
|
237
|
+
lines.push(' "proposed_changes": [');
|
|
238
|
+
lines.push(' { "path": "src/lib/foo.js", "action": "create", "content": "// full file..." },');
|
|
239
|
+
lines.push(' { "path": "src/lib/bar.js", "action": "modify", "content": "// full new content..." },');
|
|
240
|
+
lines.push(' { "path": "src/old.js", "action": "delete" }');
|
|
241
|
+
lines.push(' ]');
|
|
242
|
+
lines.push(' ```');
|
|
243
|
+
lines.push('- Valid actions: `create` (new file), `modify` (replace content), `delete` (remove file).');
|
|
244
|
+
lines.push('- `content` is required for `create` and `modify` actions.');
|
|
245
|
+
lines.push('- The orchestrator will materialize your proposal to `.agentxchain/proposed/<turn_id>/` for review.');
|
|
246
|
+
lines.push('- List all proposed file paths in `files_changed`.');
|
|
247
|
+
}
|
|
232
248
|
lines.push('');
|
|
233
249
|
}
|
|
234
250
|
|
|
@@ -366,6 +382,22 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
366
382
|
lines.push(`- **You are in the \`${currentPhase}\` phase (final phase).** When ready to ship, set \`run_completion_request: true\` and \`phase_transition_request: null\`.`);
|
|
367
383
|
}
|
|
368
384
|
}
|
|
385
|
+
// Phase-specific guidance for proposed roles
|
|
386
|
+
if (role.write_authority === 'proposed' && phaseNames.length > 0) {
|
|
387
|
+
const currentPhase = state?.phase;
|
|
388
|
+
const phaseIdx = currentPhase ? phaseNames.indexOf(currentPhase) : -1;
|
|
389
|
+
if (phaseIdx >= 0 && phaseIdx < phaseNames.length - 1) {
|
|
390
|
+
const nextPhase = phaseNames[phaseIdx + 1];
|
|
391
|
+
const currentGate = config.routing?.[currentPhase]?.exit_gate;
|
|
392
|
+
const gateClause = currentGate ? ` and the exit gate (\`${currentGate}\`) is satisfied` : '';
|
|
393
|
+
lines.push(`- **You are in the \`${currentPhase}\` phase (not final phase).** When your work is complete${gateClause}, set \`phase_transition_request: "${nextPhase}"\`.`);
|
|
394
|
+
} else if (phaseIdx >= 0 && phaseIdx === phaseNames.length - 1) {
|
|
395
|
+
lines.push(`- **You are in the \`${currentPhase}\` phase (final phase).** When ready to ship, set \`run_completion_request: true\` and \`phase_transition_request: null\`.`);
|
|
396
|
+
if (runtimeType === 'api_proxy') {
|
|
397
|
+
lines.push('- **Completion turns must be no-op:** set `proposed_changes` to `[]` or omit it, set `files_changed` to `[]`, and set `artifact.type` to `"review"`. Do NOT propose file changes on a completion turn.');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
369
401
|
// Phase-specific guidance for review_only roles (terminal phase ship readiness)
|
|
370
402
|
if (role.write_authority === 'review_only' && phaseNames.length > 0) {
|
|
371
403
|
const currentPhase = state?.phase;
|
|
@@ -467,6 +499,42 @@ function renderContext(state, config, root, turn, role) {
|
|
|
467
499
|
}
|
|
468
500
|
}
|
|
469
501
|
|
|
502
|
+
const proposalPreview = role?.write_authority === 'review_only'
|
|
503
|
+
? buildProposalArtifactPreview(root, lastTurn)
|
|
504
|
+
: null;
|
|
505
|
+
if (proposalPreview) {
|
|
506
|
+
lines.push('### Proposed Artifact');
|
|
507
|
+
lines.push('');
|
|
508
|
+
lines.push(`- **Artifact ref:** \`${proposalPreview.artifactRef}\``);
|
|
509
|
+
lines.push(`- **Proposed files:** ${proposalPreview.changeCount}`);
|
|
510
|
+
lines.push('');
|
|
511
|
+
lines.push('```');
|
|
512
|
+
lines.push(proposalPreview.summary);
|
|
513
|
+
lines.push('```');
|
|
514
|
+
if (proposalPreview.summaryTruncated) {
|
|
515
|
+
lines.push('');
|
|
516
|
+
lines.push(`_Preview truncated after ${PROPOSAL_SUMMARY_MAX_LINES} lines._`);
|
|
517
|
+
}
|
|
518
|
+
lines.push('');
|
|
519
|
+
|
|
520
|
+
if (proposalPreview.filePreviews.length > 0) {
|
|
521
|
+
lines.push('### Proposed File Previews');
|
|
522
|
+
lines.push('');
|
|
523
|
+
for (const preview of proposalPreview.filePreviews) {
|
|
524
|
+
lines.push(`#### \`${preview.path}\` (${preview.action})`);
|
|
525
|
+
lines.push('');
|
|
526
|
+
lines.push('```');
|
|
527
|
+
lines.push(preview.content);
|
|
528
|
+
lines.push('```');
|
|
529
|
+
if (preview.truncated) {
|
|
530
|
+
lines.push('');
|
|
531
|
+
lines.push(`_Preview truncated after ${FILE_PREVIEW_MAX_LINES} lines._`);
|
|
532
|
+
}
|
|
533
|
+
lines.push('');
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
470
538
|
// Verification evidence from the previous turn
|
|
471
539
|
// Use raw verification (has commands, machine_evidence, evidence_summary)
|
|
472
540
|
// and supplement with normalized_verification status when available
|
|
@@ -683,6 +751,113 @@ function buildChangedFilePreviews(root, filesChanged) {
|
|
|
683
751
|
return previews;
|
|
684
752
|
}
|
|
685
753
|
|
|
754
|
+
function buildProposalArtifactPreview(root, lastTurn) {
|
|
755
|
+
const artifactRef = lastTurn?.artifact?.ref;
|
|
756
|
+
if (typeof artifactRef !== 'string' || !artifactRef.startsWith('.agentxchain/proposed/')) {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const absProposalDir = join(root, artifactRef);
|
|
761
|
+
const summaryPath = join(absProposalDir, 'PROPOSAL.md');
|
|
762
|
+
|
|
763
|
+
let summary = '';
|
|
764
|
+
let summaryTruncated = false;
|
|
765
|
+
|
|
766
|
+
if (existsSync(summaryPath)) {
|
|
767
|
+
try {
|
|
768
|
+
const raw = readFileSync(summaryPath, 'utf8');
|
|
769
|
+
const lines = raw.replace(/\r\n/g, '\n').split('\n');
|
|
770
|
+
summaryTruncated = lines.length > PROPOSAL_SUMMARY_MAX_LINES;
|
|
771
|
+
summary = (summaryTruncated ? lines.slice(0, PROPOSAL_SUMMARY_MAX_LINES) : lines)
|
|
772
|
+
.join('\n')
|
|
773
|
+
.trimEnd();
|
|
774
|
+
} catch {
|
|
775
|
+
summary = '';
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const changeActions = extractProposalActions(summary);
|
|
780
|
+
const materializedFiles = collectProposalFiles(absProposalDir);
|
|
781
|
+
|
|
782
|
+
if (!summary) {
|
|
783
|
+
const fallbackLines = [
|
|
784
|
+
`# Proposed Changes — ${lastTurn.turn_id || '(unknown)'}`,
|
|
785
|
+
'',
|
|
786
|
+
lastTurn.summary || '(no summary)',
|
|
787
|
+
'',
|
|
788
|
+
];
|
|
789
|
+
for (const filePath of materializedFiles) {
|
|
790
|
+
fallbackLines.push(`- \`${filePath}\` — ${changeActions.get(filePath) || 'create'}`);
|
|
791
|
+
}
|
|
792
|
+
summary = fallbackLines.join('\n');
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const filePreviews = [];
|
|
796
|
+
for (const relPath of materializedFiles.slice(0, FILE_PREVIEW_MAX_FILES)) {
|
|
797
|
+
const absPath = join(absProposalDir, relPath);
|
|
798
|
+
|
|
799
|
+
let raw;
|
|
800
|
+
try {
|
|
801
|
+
raw = readFileSync(absPath, 'utf8');
|
|
802
|
+
} catch {
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const lines = raw.replace(/\r\n/g, '\n').split('\n');
|
|
807
|
+
const truncated = lines.length > FILE_PREVIEW_MAX_LINES;
|
|
808
|
+
const previewLines = truncated ? lines.slice(0, FILE_PREVIEW_MAX_LINES) : lines;
|
|
809
|
+
filePreviews.push({
|
|
810
|
+
path: relPath,
|
|
811
|
+
action: changeActions.get(relPath) || 'create',
|
|
812
|
+
content: previewLines.join('\n').trimEnd(),
|
|
813
|
+
truncated,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return {
|
|
818
|
+
artifactRef,
|
|
819
|
+
summary,
|
|
820
|
+
summaryTruncated,
|
|
821
|
+
filePreviews,
|
|
822
|
+
changeCount: changeActions.size || materializedFiles.length,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function extractProposalActions(summary) {
|
|
827
|
+
const actions = new Map();
|
|
828
|
+
if (!summary) {
|
|
829
|
+
return actions;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const matches = summary.matchAll(/^- `([^`]+)` — ([a-z]+)/gm);
|
|
833
|
+
for (const match of matches) {
|
|
834
|
+
actions.set(match[1], match[2]);
|
|
835
|
+
}
|
|
836
|
+
return actions;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function collectProposalFiles(absProposalDir, relPrefix = '') {
|
|
840
|
+
if (!existsSync(absProposalDir)) {
|
|
841
|
+
return [];
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const files = [];
|
|
845
|
+
for (const entry of readdirSync(absProposalDir, { withFileTypes: true })) {
|
|
846
|
+
if (entry.name === 'PROPOSAL.md') {
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const relPath = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
851
|
+
const absPath = join(absProposalDir, entry.name);
|
|
852
|
+
if (entry.isDirectory()) {
|
|
853
|
+
files.push(...collectProposalFiles(absPath, relPath));
|
|
854
|
+
} else if (entry.isFile()) {
|
|
855
|
+
files.push(relPath);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return files.sort();
|
|
859
|
+
}
|
|
860
|
+
|
|
686
861
|
function buildDispatchLogExcerpt(root, turnId) {
|
|
687
862
|
const logPath = join(root, getDispatchLogPath(turnId));
|
|
688
863
|
if (!existsSync(logPath)) {
|