agentxchain 2.46.0 → 2.47.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/bin/agentxchain.js +4 -1
- package/dashboard/app.js +6 -0
- package/dashboard/components/coordinator-timeouts.js +220 -0
- package/dashboard/components/timeouts.js +201 -0
- package/dashboard/index.html +2 -0
- package/package.json +1 -1
- package/scripts/publish-from-tag.sh +33 -4
- package/src/commands/history.js +41 -1
- package/src/commands/init.js +1 -0
- package/src/commands/migrate.js +1 -0
- package/src/commands/run.js +32 -1
- package/src/commands/status.js +55 -0
- package/src/lib/approval-policy.js +139 -0
- package/src/lib/blocked-state.js +11 -0
- package/src/lib/dashboard/bridge-server.js +14 -0
- package/src/lib/dashboard/coordinator-timeout-status.js +139 -0
- package/src/lib/dashboard/timeout-status.js +201 -0
- package/src/lib/export.js +2 -0
- package/src/lib/governed-state.js +428 -30
- package/src/lib/normalized-config.js +123 -0
- package/src/lib/reference-conformance-adapter.js +1 -0
- package/src/lib/repo-observer.js +132 -1
- package/src/lib/report.js +331 -6
- package/src/lib/run-history.js +111 -0
- package/src/lib/run-loop.js +9 -3
- package/src/lib/run-provenance.js +90 -0
- package/src/lib/schema.js +47 -0
- package/src/lib/timeout-evaluator.js +234 -0
package/src/lib/export.js
CHANGED
|
@@ -6,6 +6,7 @@ import { join, relative, resolve } from 'node:path';
|
|
|
6
6
|
import { loadProjectContext, loadProjectState } from './config.js';
|
|
7
7
|
import { loadCoordinatorConfig, COORDINATOR_CONFIG_FILE } from './coordinator-config.js';
|
|
8
8
|
import { loadCoordinatorState } from './coordinator-state.js';
|
|
9
|
+
import { normalizeRunProvenance } from './run-provenance.js';
|
|
9
10
|
|
|
10
11
|
const EXPORT_SCHEMA_VERSION = '0.3';
|
|
11
12
|
|
|
@@ -296,6 +297,7 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
296
297
|
run_id: state?.run_id || null,
|
|
297
298
|
status: state?.status || null,
|
|
298
299
|
phase: state?.phase || null,
|
|
300
|
+
provenance: normalizeRunProvenance(state?.provenance),
|
|
299
301
|
active_turn_ids: activeTurns,
|
|
300
302
|
retained_turn_ids: retainedTurns,
|
|
301
303
|
history_entries: countJsonl(files, '.agentxchain/history.jsonl'),
|
|
@@ -20,11 +20,15 @@ import { join, dirname } from 'path';
|
|
|
20
20
|
import { randomBytes, createHash } from 'crypto';
|
|
21
21
|
import { safeWriteJson } from './safe-write.js';
|
|
22
22
|
import { validateStagedTurnResult } from './turn-result-validator.js';
|
|
23
|
-
import { evaluatePhaseExit, evaluateRunCompletion } from './gate-evaluator.js';
|
|
23
|
+
import { evaluatePhaseExit, evaluateRunCompletion, getNextPhase } from './gate-evaluator.js';
|
|
24
|
+
import { evaluateApprovalPolicy } from './approval-policy.js';
|
|
24
25
|
import { evaluatePolicies } from './policy-evaluator.js';
|
|
26
|
+
import { buildTimeoutBlockedReason, evaluateTimeouts } from './timeout-evaluator.js';
|
|
25
27
|
import {
|
|
26
28
|
captureBaseline,
|
|
27
29
|
observeChanges,
|
|
30
|
+
attributeObservedChangesToTurn,
|
|
31
|
+
buildConflictCandidateFiles,
|
|
28
32
|
classifyObservedChanges,
|
|
29
33
|
buildObservedArtifact,
|
|
30
34
|
normalizeVerification,
|
|
@@ -38,6 +42,7 @@ import { runHooks } from './hook-runner.js';
|
|
|
38
42
|
import { emitNotifications } from './notification-runner.js';
|
|
39
43
|
import { writeSessionCheckpoint } from './session-checkpoint.js';
|
|
40
44
|
import { recordRunHistory } from './run-history.js';
|
|
45
|
+
import { buildDefaultRunProvenance } from './run-provenance.js';
|
|
41
46
|
|
|
42
47
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
43
48
|
|
|
@@ -57,6 +62,92 @@ function generateId(prefix) {
|
|
|
57
62
|
return `${prefix}_${randomBytes(8).toString('hex')}`;
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
function getInitialPhase(config) {
|
|
66
|
+
return Object.keys(config?.routing || {})[0] || 'planning';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildInitialPhaseGateStatus(config) {
|
|
70
|
+
return Object.fromEntries(
|
|
71
|
+
[...new Set(
|
|
72
|
+
Object.values(config?.routing || {})
|
|
73
|
+
.map((route) => route?.exit_gate)
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
)].map((gateId) => [gateId, 'pending'])
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildFreshIdleStateForNewRun(state, config) {
|
|
80
|
+
return {
|
|
81
|
+
schema_version: state?.schema_version || GOVERNED_SCHEMA_VERSION,
|
|
82
|
+
run_id: null,
|
|
83
|
+
project_id: state?.project_id || config?.project?.id || null,
|
|
84
|
+
status: 'idle',
|
|
85
|
+
phase: getInitialPhase(config),
|
|
86
|
+
accepted_integration_ref: null,
|
|
87
|
+
active_turns: {},
|
|
88
|
+
turn_sequence: 0,
|
|
89
|
+
last_completed_turn_id: null,
|
|
90
|
+
blocked_on: null,
|
|
91
|
+
blocked_reason: null,
|
|
92
|
+
escalation: null,
|
|
93
|
+
pending_phase_transition: null,
|
|
94
|
+
pending_run_completion: null,
|
|
95
|
+
queued_phase_transition: null,
|
|
96
|
+
queued_run_completion: null,
|
|
97
|
+
last_gate_failure: null,
|
|
98
|
+
phase_gate_status: buildInitialPhaseGateStatus(config),
|
|
99
|
+
budget_reservations: {},
|
|
100
|
+
budget_status: {
|
|
101
|
+
spent_usd: 0,
|
|
102
|
+
remaining_usd: config?.budget?.per_run_max_usd ?? null,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeGateFailure(value) {
|
|
108
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
gate_type: value.gate_type === 'run_completion' ? 'run_completion' : 'phase_transition',
|
|
113
|
+
gate_id: typeof value.gate_id === 'string' && value.gate_id.length > 0 ? value.gate_id : null,
|
|
114
|
+
phase: typeof value.phase === 'string' && value.phase.length > 0 ? value.phase : 'unknown',
|
|
115
|
+
from_phase: typeof value.from_phase === 'string' && value.from_phase.length > 0 ? value.from_phase : null,
|
|
116
|
+
to_phase: typeof value.to_phase === 'string' && value.to_phase.length > 0 ? value.to_phase : null,
|
|
117
|
+
requested_by_turn: typeof value.requested_by_turn === 'string' && value.requested_by_turn.length > 0 ? value.requested_by_turn : null,
|
|
118
|
+
failed_at: typeof value.failed_at === 'string' && value.failed_at.length > 0 ? value.failed_at : null,
|
|
119
|
+
queued_request: value.queued_request === true,
|
|
120
|
+
reasons: Array.isArray(value.reasons) ? value.reasons.filter((reason) => typeof reason === 'string' && reason.length > 0) : [],
|
|
121
|
+
missing_files: Array.isArray(value.missing_files) ? value.missing_files.filter((path) => typeof path === 'string' && path.length > 0) : [],
|
|
122
|
+
missing_verification: value.missing_verification === true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildGateFailureRecord({
|
|
127
|
+
gateType,
|
|
128
|
+
gateResult,
|
|
129
|
+
phase,
|
|
130
|
+
fromPhase = null,
|
|
131
|
+
toPhase = null,
|
|
132
|
+
requestedByTurn = null,
|
|
133
|
+
failedAt,
|
|
134
|
+
queuedRequest,
|
|
135
|
+
}) {
|
|
136
|
+
return normalizeGateFailure({
|
|
137
|
+
gate_type: gateType,
|
|
138
|
+
gate_id: gateResult?.gate_id || null,
|
|
139
|
+
phase,
|
|
140
|
+
from_phase: fromPhase,
|
|
141
|
+
to_phase: toPhase,
|
|
142
|
+
requested_by_turn: requestedByTurn,
|
|
143
|
+
failed_at: failedAt,
|
|
144
|
+
queued_request: queuedRequest,
|
|
145
|
+
reasons: Array.isArray(gateResult?.reasons) ? gateResult.reasons : [],
|
|
146
|
+
missing_files: Array.isArray(gateResult?.missing_files) ? gateResult.missing_files : [],
|
|
147
|
+
missing_verification: gateResult?.missing_verification === true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
60
151
|
function emitBlockedNotification(root, config, state, details = {}, turn = null) {
|
|
61
152
|
if (!config?.notifications?.webhooks?.length) {
|
|
62
153
|
return;
|
|
@@ -608,6 +699,7 @@ function normalizeV1toV1_1(state) {
|
|
|
608
699
|
: {},
|
|
609
700
|
queued_phase_transition: state.queued_phase_transition ?? null,
|
|
610
701
|
queued_run_completion: state.queued_run_completion ?? null,
|
|
702
|
+
last_gate_failure: normalizeGateFailure(state.last_gate_failure),
|
|
611
703
|
};
|
|
612
704
|
}
|
|
613
705
|
|
|
@@ -880,8 +972,8 @@ function cleanupTurnArtifacts(root, turnId) {
|
|
|
880
972
|
} catch { /* best-effort */ }
|
|
881
973
|
}
|
|
882
974
|
|
|
883
|
-
function detectAcceptanceConflict(targetTurn,
|
|
884
|
-
const observedFiles = [...new Set(
|
|
975
|
+
function detectAcceptanceConflict(targetTurn, conflictFiles, historyEntries) {
|
|
976
|
+
const observedFiles = [...new Set(Array.isArray(conflictFiles) ? conflictFiles : [])];
|
|
885
977
|
if (observedFiles.length === 0) {
|
|
886
978
|
return null;
|
|
887
979
|
}
|
|
@@ -1000,6 +1092,101 @@ function buildBlockedReason({ category, recovery, turnId, blockedAt = new Date()
|
|
|
1000
1092
|
};
|
|
1001
1093
|
}
|
|
1002
1094
|
|
|
1095
|
+
function buildTimeoutLedgerEntry(timeoutResult, timestamp, turnId, phase, type = 'timeout') {
|
|
1096
|
+
return {
|
|
1097
|
+
type,
|
|
1098
|
+
scope: timeoutResult.scope,
|
|
1099
|
+
phase: timeoutResult.phase || phase || null,
|
|
1100
|
+
turn_id: turnId || null,
|
|
1101
|
+
limit_minutes: timeoutResult.limit_minutes,
|
|
1102
|
+
elapsed_minutes: timeoutResult.elapsed_minutes,
|
|
1103
|
+
exceeded_by_minutes: timeoutResult.exceeded_by_minutes,
|
|
1104
|
+
action: timeoutResult.action,
|
|
1105
|
+
timestamp,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function attemptTimeoutPhaseSkip({ root, config, updatedState, nextHistoryEntries, historyEntry, timeoutResult, now }) {
|
|
1110
|
+
const currentPhase = updatedState.phase;
|
|
1111
|
+
const nextPhase = getNextPhase(currentPhase, config.routing || {});
|
|
1112
|
+
if (!nextPhase) {
|
|
1113
|
+
return {
|
|
1114
|
+
ok: false,
|
|
1115
|
+
error: `Phase "${currentPhase}" has no next phase to skip to`,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const syntheticTurn = {
|
|
1120
|
+
...historyEntry,
|
|
1121
|
+
verification: historyEntry.verification || {},
|
|
1122
|
+
phase_transition_request: nextPhase,
|
|
1123
|
+
};
|
|
1124
|
+
const postAcceptanceState = {
|
|
1125
|
+
...updatedState,
|
|
1126
|
+
history: nextHistoryEntries,
|
|
1127
|
+
};
|
|
1128
|
+
const gateResult = evaluatePhaseExit({
|
|
1129
|
+
state: postAcceptanceState,
|
|
1130
|
+
config,
|
|
1131
|
+
acceptedTurn: syntheticTurn,
|
|
1132
|
+
root,
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
if (gateResult.action === 'advance') {
|
|
1136
|
+
return {
|
|
1137
|
+
ok: true,
|
|
1138
|
+
gateResult,
|
|
1139
|
+
updatedState: {
|
|
1140
|
+
...updatedState,
|
|
1141
|
+
phase: gateResult.next_phase,
|
|
1142
|
+
phase_entered_at: now,
|
|
1143
|
+
last_gate_failure: null,
|
|
1144
|
+
phase_gate_status: {
|
|
1145
|
+
...(updatedState.phase_gate_status || {}),
|
|
1146
|
+
[gateResult.gate_id || 'no_gate']: 'passed',
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1149
|
+
ledgerEntry: {
|
|
1150
|
+
...buildTimeoutLedgerEntry(timeoutResult, now, historyEntry.turn_id, currentPhase, 'timeout_skip'),
|
|
1151
|
+
from_phase: currentPhase,
|
|
1152
|
+
to_phase: gateResult.next_phase,
|
|
1153
|
+
gate_id: gateResult.gate_id || null,
|
|
1154
|
+
},
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const reasons = gateResult.action === 'awaiting_human_approval'
|
|
1159
|
+
? [`Gate "${gateResult.gate_id || 'unknown'}" still requires human approval; timeout skip cannot auto-advance`]
|
|
1160
|
+
: Array.isArray(gateResult.reasons) && gateResult.reasons.length > 0
|
|
1161
|
+
? gateResult.reasons
|
|
1162
|
+
: ['Timeout skip failed for an unknown reason'];
|
|
1163
|
+
const gateFailure = buildGateFailureRecord({
|
|
1164
|
+
gateType: 'phase_transition',
|
|
1165
|
+
gateResult: {
|
|
1166
|
+
...gateResult,
|
|
1167
|
+
reasons,
|
|
1168
|
+
},
|
|
1169
|
+
phase: currentPhase,
|
|
1170
|
+
fromPhase: currentPhase,
|
|
1171
|
+
toPhase: nextPhase,
|
|
1172
|
+
requestedByTurn: historyEntry.turn_id,
|
|
1173
|
+
failedAt: now,
|
|
1174
|
+
queuedRequest: false,
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
return {
|
|
1178
|
+
ok: false,
|
|
1179
|
+
gateFailure,
|
|
1180
|
+
ledgerEntry: {
|
|
1181
|
+
...buildTimeoutLedgerEntry(timeoutResult, now, historyEntry.turn_id, currentPhase, 'timeout_skip_failed'),
|
|
1182
|
+
from_phase: currentPhase,
|
|
1183
|
+
to_phase: nextPhase,
|
|
1184
|
+
gate_id: gateResult.gate_id || null,
|
|
1185
|
+
reasons,
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1003
1190
|
function slugifyEscalationReason(reason) {
|
|
1004
1191
|
if (typeof reason !== 'string') {
|
|
1005
1192
|
return 'operator';
|
|
@@ -1410,6 +1597,15 @@ export function normalizeGovernedStateShape(state) {
|
|
|
1410
1597
|
changed = true;
|
|
1411
1598
|
}
|
|
1412
1599
|
|
|
1600
|
+
const normalizedLastGateFailure = normalizeGateFailure(nextState.last_gate_failure);
|
|
1601
|
+
if (JSON.stringify(nextState.last_gate_failure ?? null) !== JSON.stringify(normalizedLastGateFailure)) {
|
|
1602
|
+
nextState = {
|
|
1603
|
+
...nextState,
|
|
1604
|
+
last_gate_failure: normalizedLastGateFailure,
|
|
1605
|
+
};
|
|
1606
|
+
changed = true;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1413
1609
|
return { state: stripLegacyCurrentTurn(nextState), changed };
|
|
1414
1610
|
}
|
|
1415
1611
|
|
|
@@ -1613,30 +1809,40 @@ export function reactivateGovernedRun(root, state, details = {}) {
|
|
|
1613
1809
|
* @param {object} config - normalized config
|
|
1614
1810
|
* @returns {{ ok: boolean, error?: string, state?: object }}
|
|
1615
1811
|
*/
|
|
1616
|
-
export function initializeGovernedRun(root, config) {
|
|
1617
|
-
|
|
1812
|
+
export function initializeGovernedRun(root, config, options = {}) {
|
|
1813
|
+
let state = readState(root);
|
|
1618
1814
|
if (!state) {
|
|
1619
1815
|
return { ok: false, error: 'No governed state.json found' };
|
|
1620
1816
|
}
|
|
1621
|
-
|
|
1817
|
+
const allowTerminalRestart = options.allow_terminal_restart === true
|
|
1818
|
+
&& (state.status === 'completed' || state.status === 'blocked');
|
|
1819
|
+
if (state.status === 'completed' && !allowTerminalRestart) {
|
|
1622
1820
|
return { ok: false, error: 'Cannot initialize run: this run is already completed. Start a new project or reset state.' };
|
|
1623
1821
|
}
|
|
1624
1822
|
const allowBlockedBootstrap = state.status === 'blocked' && state.run_id === null && getActiveTurnCount(state) === 0;
|
|
1625
|
-
if (state.status !== 'idle' && state.status !== 'paused' && !allowBlockedBootstrap) {
|
|
1823
|
+
if (state.status !== 'idle' && state.status !== 'paused' && !allowBlockedBootstrap && !allowTerminalRestart) {
|
|
1626
1824
|
return { ok: false, error: `Cannot initialize run: status is "${state.status}", expected "idle", "paused", or pre-run "blocked"` };
|
|
1627
1825
|
}
|
|
1826
|
+
if (allowTerminalRestart) {
|
|
1827
|
+
state = buildFreshIdleStateForNewRun(state, config);
|
|
1828
|
+
}
|
|
1628
1829
|
|
|
1629
1830
|
const runId = generateId('run');
|
|
1831
|
+
const now = new Date().toISOString();
|
|
1832
|
+
const provenance = buildDefaultRunProvenance(options.provenance);
|
|
1630
1833
|
const updatedState = {
|
|
1631
1834
|
...state,
|
|
1632
1835
|
run_id: runId,
|
|
1836
|
+
created_at: now,
|
|
1837
|
+
phase_entered_at: now,
|
|
1633
1838
|
status: 'active',
|
|
1634
1839
|
blocked_on: null,
|
|
1635
1840
|
blocked_reason: null,
|
|
1636
1841
|
budget_status: {
|
|
1637
1842
|
spent_usd: 0,
|
|
1638
1843
|
remaining_usd: config.budget?.per_run_max_usd ?? null
|
|
1639
|
-
}
|
|
1844
|
+
},
|
|
1845
|
+
provenance,
|
|
1640
1846
|
};
|
|
1641
1847
|
|
|
1642
1848
|
writeState(root, updatedState);
|
|
@@ -2092,7 +2298,9 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2092
2298
|
const stagingFile = join(root, resolvedStagingPath);
|
|
2093
2299
|
const now = new Date().toISOString();
|
|
2094
2300
|
const baseline = currentTurn.baseline || null;
|
|
2095
|
-
const
|
|
2301
|
+
const rawObservation = observeChanges(root, baseline);
|
|
2302
|
+
const historyEntries = readJsonlEntries(root, HISTORY_PATH);
|
|
2303
|
+
const observation = attributeObservedChangesToTurn(rawObservation, currentTurn, historyEntries);
|
|
2096
2304
|
const role = config.roles?.[turnResult.role];
|
|
2097
2305
|
const runtimeId = turnResult.runtime_id;
|
|
2098
2306
|
const runtime = config.runtimes?.[runtimeId];
|
|
@@ -2125,7 +2333,6 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2125
2333
|
const normalizedVerification = normalizeVerification(turnResult.verification, runtimeType);
|
|
2126
2334
|
const artifactType = turnResult.artifact?.type || 'review';
|
|
2127
2335
|
const derivedRef = deriveAcceptedRef(observation, artifactType, state.accepted_integration_ref);
|
|
2128
|
-
const historyEntries = readJsonlEntries(root, HISTORY_PATH);
|
|
2129
2336
|
|
|
2130
2337
|
// Policy evaluation — declarative governance rules (spec: POLICY_ENGINE_SPEC.md)
|
|
2131
2338
|
const policyResult = evaluatePolicies(config.policies || [], {
|
|
@@ -2203,7 +2410,11 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2203
2410
|
};
|
|
2204
2411
|
}
|
|
2205
2412
|
|
|
2206
|
-
const conflict = detectAcceptanceConflict(
|
|
2413
|
+
const conflict = detectAcceptanceConflict(
|
|
2414
|
+
currentTurn,
|
|
2415
|
+
buildConflictCandidateFiles(rawObservation, observation, turnResult.files_changed || []),
|
|
2416
|
+
historyEntries,
|
|
2417
|
+
);
|
|
2207
2418
|
|
|
2208
2419
|
if (conflict) {
|
|
2209
2420
|
const detectionCount = (currentTurn.conflict_state?.detection_count || 0) + 1;
|
|
@@ -2338,6 +2549,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2338
2549
|
cost: turnResult.cost || {},
|
|
2339
2550
|
accepted_at: now,
|
|
2340
2551
|
};
|
|
2552
|
+
const nextHistoryEntries = [...historyEntries, historyEntry];
|
|
2341
2553
|
// Build ledger entries for the journal
|
|
2342
2554
|
const ledgerEntries = [];
|
|
2343
2555
|
if (turnResult.decisions && turnResult.decisions.length > 0) {
|
|
@@ -2451,6 +2663,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2451
2663
|
|
|
2452
2664
|
let gateResult = null;
|
|
2453
2665
|
let completionResult = null;
|
|
2666
|
+
let timeoutResult = null;
|
|
2454
2667
|
const hasRemainingTurns = Object.keys(remainingTurns).length > 0;
|
|
2455
2668
|
if (turnResult.status !== 'needs_human') {
|
|
2456
2669
|
if (hasRemainingTurns) {
|
|
@@ -2469,7 +2682,6 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2469
2682
|
};
|
|
2470
2683
|
}
|
|
2471
2684
|
} else {
|
|
2472
|
-
const nextHistoryEntries = [...historyEntries, historyEntry];
|
|
2473
2685
|
const postAcceptanceState = {
|
|
2474
2686
|
...state,
|
|
2475
2687
|
active_turns: remainingTurns,
|
|
@@ -2491,6 +2703,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2491
2703
|
if (completionResult.action === 'complete') {
|
|
2492
2704
|
updatedState.status = 'completed';
|
|
2493
2705
|
updatedState.completed_at = now;
|
|
2706
|
+
updatedState.last_gate_failure = null;
|
|
2494
2707
|
if (completionResult.gate_id) {
|
|
2495
2708
|
updatedState.phase_gate_status = {
|
|
2496
2709
|
...(updatedState.phase_gate_status || {}),
|
|
@@ -2500,16 +2713,71 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2500
2713
|
updatedState.queued_run_completion = null;
|
|
2501
2714
|
updatedState.queued_phase_transition = null;
|
|
2502
2715
|
} else if (completionResult.action === 'awaiting_human_approval') {
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2716
|
+
// Evaluate approval policy — may auto-approve
|
|
2717
|
+
const approvalResult = evaluateApprovalPolicy({
|
|
2718
|
+
gateResult: completionResult,
|
|
2719
|
+
gateType: 'run_completion',
|
|
2720
|
+
state: { ...updatedState, history: nextHistoryEntries },
|
|
2721
|
+
config,
|
|
2722
|
+
});
|
|
2723
|
+
|
|
2724
|
+
if (approvalResult.action === 'auto_approve') {
|
|
2725
|
+
updatedState.status = 'completed';
|
|
2726
|
+
updatedState.completed_at = now;
|
|
2727
|
+
updatedState.last_gate_failure = null;
|
|
2728
|
+
if (completionResult.gate_id) {
|
|
2729
|
+
updatedState.phase_gate_status = {
|
|
2730
|
+
...(updatedState.phase_gate_status || {}),
|
|
2731
|
+
[completionResult.gate_id]: 'passed',
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
updatedState.queued_run_completion = null;
|
|
2735
|
+
updatedState.queued_phase_transition = null;
|
|
2736
|
+
ledgerEntries.push({
|
|
2737
|
+
type: 'approval_policy',
|
|
2738
|
+
gate_type: 'run_completion',
|
|
2739
|
+
action: 'auto_approve',
|
|
2740
|
+
matched_rule: approvalResult.matched_rule,
|
|
2741
|
+
reason: approvalResult.reason,
|
|
2742
|
+
gate_id: completionResult.gate_id,
|
|
2743
|
+
timestamp: now,
|
|
2744
|
+
});
|
|
2745
|
+
} else {
|
|
2746
|
+
updatedState.status = 'paused';
|
|
2747
|
+
updatedState.blocked_on = `human_approval:${completionResult.gate_id}`;
|
|
2748
|
+
updatedState.blocked_reason = null;
|
|
2749
|
+
updatedState.last_gate_failure = null;
|
|
2750
|
+
updatedState.pending_run_completion = {
|
|
2751
|
+
gate: completionResult.gate_id,
|
|
2752
|
+
requested_by_turn: completionSource.turn_id,
|
|
2753
|
+
requested_at: now,
|
|
2754
|
+
};
|
|
2755
|
+
updatedState.queued_run_completion = null;
|
|
2756
|
+
updatedState.queued_phase_transition = null;
|
|
2757
|
+
}
|
|
2758
|
+
} else if (completionResult.action === 'gate_failed') {
|
|
2759
|
+
const gateFailure = buildGateFailureRecord({
|
|
2760
|
+
gateType: 'run_completion',
|
|
2761
|
+
gateResult: completionResult,
|
|
2762
|
+
phase: state.phase,
|
|
2763
|
+
fromPhase: state.phase,
|
|
2764
|
+
toPhase: null,
|
|
2765
|
+
requestedByTurn: completionSource?.turn_id || state.queued_run_completion?.requested_by_turn || null,
|
|
2766
|
+
failedAt: now,
|
|
2767
|
+
queuedRequest: Boolean(state.queued_run_completion && !turnResult.run_completion_request),
|
|
2768
|
+
});
|
|
2769
|
+
updatedState.last_gate_failure = gateFailure;
|
|
2770
|
+
if (completionResult.gate_id) {
|
|
2771
|
+
updatedState.phase_gate_status = {
|
|
2772
|
+
...(updatedState.phase_gate_status || {}),
|
|
2773
|
+
[completionResult.gate_id]: 'failed',
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2511
2776
|
updatedState.queued_run_completion = null;
|
|
2512
|
-
|
|
2777
|
+
ledgerEntries.push({
|
|
2778
|
+
type: 'gate_failure',
|
|
2779
|
+
...gateFailure,
|
|
2780
|
+
});
|
|
2513
2781
|
} else if (state.queued_run_completion) {
|
|
2514
2782
|
updatedState.queued_run_completion = null;
|
|
2515
2783
|
}
|
|
@@ -2531,22 +2799,78 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2531
2799
|
|
|
2532
2800
|
if (gateResult.action === 'advance') {
|
|
2533
2801
|
updatedState.phase = gateResult.next_phase;
|
|
2802
|
+
updatedState.phase_entered_at = now;
|
|
2803
|
+
updatedState.last_gate_failure = null;
|
|
2534
2804
|
updatedState.phase_gate_status = {
|
|
2535
2805
|
...(updatedState.phase_gate_status || {}),
|
|
2536
2806
|
[gateResult.gate_id || 'no_gate']: 'passed',
|
|
2537
2807
|
};
|
|
2538
2808
|
updatedState.queued_phase_transition = null;
|
|
2539
2809
|
} else if (gateResult.action === 'awaiting_human_approval') {
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2810
|
+
// Evaluate approval policy — may auto-approve
|
|
2811
|
+
const approvalResult = evaluateApprovalPolicy({
|
|
2812
|
+
gateResult,
|
|
2813
|
+
gateType: 'phase_transition',
|
|
2814
|
+
state: { ...updatedState, history: nextHistoryEntries },
|
|
2815
|
+
config,
|
|
2816
|
+
});
|
|
2817
|
+
|
|
2818
|
+
if (approvalResult.action === 'auto_approve') {
|
|
2819
|
+
updatedState.phase = gateResult.next_phase;
|
|
2820
|
+
updatedState.phase_entered_at = now;
|
|
2821
|
+
updatedState.last_gate_failure = null;
|
|
2822
|
+
updatedState.phase_gate_status = {
|
|
2823
|
+
...(updatedState.phase_gate_status || {}),
|
|
2824
|
+
[gateResult.gate_id || 'no_gate']: 'passed',
|
|
2825
|
+
};
|
|
2826
|
+
updatedState.queued_phase_transition = null;
|
|
2827
|
+
ledgerEntries.push({
|
|
2828
|
+
type: 'approval_policy',
|
|
2829
|
+
gate_type: 'phase_transition',
|
|
2830
|
+
action: 'auto_approve',
|
|
2831
|
+
matched_rule: approvalResult.matched_rule,
|
|
2832
|
+
from_phase: state.phase,
|
|
2833
|
+
to_phase: gateResult.next_phase,
|
|
2834
|
+
reason: approvalResult.reason,
|
|
2835
|
+
gate_id: gateResult.gate_id,
|
|
2836
|
+
timestamp: now,
|
|
2837
|
+
});
|
|
2838
|
+
} else {
|
|
2839
|
+
updatedState.status = 'paused';
|
|
2840
|
+
updatedState.blocked_on = `human_approval:${gateResult.gate_id}`;
|
|
2841
|
+
updatedState.blocked_reason = null;
|
|
2842
|
+
updatedState.last_gate_failure = null;
|
|
2843
|
+
updatedState.pending_phase_transition = {
|
|
2844
|
+
from: state.phase,
|
|
2845
|
+
to: gateResult.next_phase,
|
|
2846
|
+
gate: gateResult.gate_id,
|
|
2847
|
+
requested_by_turn: phaseSource.turn_id,
|
|
2848
|
+
};
|
|
2849
|
+
updatedState.queued_phase_transition = null;
|
|
2850
|
+
}
|
|
2851
|
+
} else if (gateResult.action === 'gate_failed') {
|
|
2852
|
+
const gateFailure = buildGateFailureRecord({
|
|
2853
|
+
gateType: 'phase_transition',
|
|
2854
|
+
gateResult,
|
|
2855
|
+
phase: state.phase,
|
|
2856
|
+
fromPhase: state.phase,
|
|
2857
|
+
toPhase: gateResult.transition_request || state.queued_phase_transition?.to || null,
|
|
2858
|
+
requestedByTurn: phaseSource?.turn_id || state.queued_phase_transition?.requested_by_turn || null,
|
|
2859
|
+
failedAt: now,
|
|
2860
|
+
queuedRequest: Boolean(state.queued_phase_transition && !turnResult.phase_transition_request),
|
|
2861
|
+
});
|
|
2862
|
+
updatedState.last_gate_failure = gateFailure;
|
|
2863
|
+
if (gateResult.gate_id) {
|
|
2864
|
+
updatedState.phase_gate_status = {
|
|
2865
|
+
...(updatedState.phase_gate_status || {}),
|
|
2866
|
+
[gateResult.gate_id]: 'failed',
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2549
2869
|
updatedState.queued_phase_transition = null;
|
|
2870
|
+
ledgerEntries.push({
|
|
2871
|
+
type: 'gate_failure',
|
|
2872
|
+
...gateFailure,
|
|
2873
|
+
});
|
|
2550
2874
|
} else if (state.queued_phase_transition) {
|
|
2551
2875
|
updatedState.queued_phase_transition = null;
|
|
2552
2876
|
}
|
|
@@ -2554,6 +2878,77 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2554
2878
|
}
|
|
2555
2879
|
}
|
|
2556
2880
|
|
|
2881
|
+
const timeoutEvaluation = evaluateTimeouts({
|
|
2882
|
+
config,
|
|
2883
|
+
state: updatedState,
|
|
2884
|
+
turn: currentTurn,
|
|
2885
|
+
turnResult,
|
|
2886
|
+
now,
|
|
2887
|
+
});
|
|
2888
|
+
for (const warning of timeoutEvaluation.warnings) {
|
|
2889
|
+
ledgerEntries.push(buildTimeoutLedgerEntry(warning, now, currentTurn.turn_id, updatedState.phase, 'timeout_warning'));
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
if (updatedState.status === 'active' && timeoutEvaluation.exceeded.length > 0) {
|
|
2893
|
+
timeoutResult = timeoutEvaluation.exceeded[0];
|
|
2894
|
+
|
|
2895
|
+
if (timeoutResult.action === 'skip_phase' && timeoutResult.scope === 'phase') {
|
|
2896
|
+
const skipAttempt = attemptTimeoutPhaseSkip({
|
|
2897
|
+
root,
|
|
2898
|
+
config,
|
|
2899
|
+
updatedState,
|
|
2900
|
+
nextHistoryEntries,
|
|
2901
|
+
historyEntry,
|
|
2902
|
+
timeoutResult,
|
|
2903
|
+
now,
|
|
2904
|
+
});
|
|
2905
|
+
|
|
2906
|
+
if (skipAttempt.ok) {
|
|
2907
|
+
updatedState.phase = skipAttempt.updatedState.phase;
|
|
2908
|
+
updatedState.phase_entered_at = skipAttempt.updatedState.phase_entered_at;
|
|
2909
|
+
updatedState.last_gate_failure = skipAttempt.updatedState.last_gate_failure;
|
|
2910
|
+
updatedState.phase_gate_status = skipAttempt.updatedState.phase_gate_status;
|
|
2911
|
+
ledgerEntries.push(skipAttempt.ledgerEntry);
|
|
2912
|
+
} else {
|
|
2913
|
+
const escalatedTimeout = { ...timeoutResult, action: 'escalate' };
|
|
2914
|
+
timeoutResult = escalatedTimeout;
|
|
2915
|
+
updatedState.status = 'blocked';
|
|
2916
|
+
updatedState.blocked_on = `timeout:${escalatedTimeout.scope}`;
|
|
2917
|
+
updatedState.blocked_reason = buildBlockedReason({
|
|
2918
|
+
...buildTimeoutBlockedReason(escalatedTimeout, {
|
|
2919
|
+
turnRetained: Object.keys(getActiveTurns(updatedState)).length > 0,
|
|
2920
|
+
}),
|
|
2921
|
+
turnId: currentTurn.turn_id,
|
|
2922
|
+
blockedAt: now,
|
|
2923
|
+
});
|
|
2924
|
+
updatedState.last_gate_failure = skipAttempt.gateFailure || null;
|
|
2925
|
+
if (skipAttempt.gateFailure?.gate_id) {
|
|
2926
|
+
updatedState.phase_gate_status = {
|
|
2927
|
+
...(updatedState.phase_gate_status || {}),
|
|
2928
|
+
[skipAttempt.gateFailure.gate_id]: 'failed',
|
|
2929
|
+
};
|
|
2930
|
+
ledgerEntries.push({
|
|
2931
|
+
type: 'gate_failure',
|
|
2932
|
+
...skipAttempt.gateFailure,
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
ledgerEntries.push(skipAttempt.ledgerEntry);
|
|
2936
|
+
ledgerEntries.push(buildTimeoutLedgerEntry(escalatedTimeout, now, currentTurn.turn_id, updatedState.phase));
|
|
2937
|
+
}
|
|
2938
|
+
} else if (timeoutResult.action === 'escalate') {
|
|
2939
|
+
updatedState.status = 'blocked';
|
|
2940
|
+
updatedState.blocked_on = `timeout:${timeoutResult.scope}`;
|
|
2941
|
+
updatedState.blocked_reason = buildBlockedReason({
|
|
2942
|
+
...buildTimeoutBlockedReason(timeoutResult, {
|
|
2943
|
+
turnRetained: Object.keys(getActiveTurns(updatedState)).length > 0,
|
|
2944
|
+
}),
|
|
2945
|
+
turnId: currentTurn.turn_id,
|
|
2946
|
+
blockedAt: now,
|
|
2947
|
+
});
|
|
2948
|
+
ledgerEntries.push(buildTimeoutLedgerEntry(timeoutResult, now, currentTurn.turn_id, updatedState.phase));
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2557
2952
|
// ── Transaction journal: prepare before committing writes ──────────────
|
|
2558
2953
|
const transactionId = generateId('txn');
|
|
2559
2954
|
const journal = {
|
|
@@ -2974,9 +3369,11 @@ export function approvePhaseTransition(root, config) {
|
|
|
2974
3369
|
const updatedState = {
|
|
2975
3370
|
...state,
|
|
2976
3371
|
phase: transition.to,
|
|
3372
|
+
phase_entered_at: new Date().toISOString(),
|
|
2977
3373
|
status: 'active',
|
|
2978
3374
|
blocked_on: null,
|
|
2979
3375
|
blocked_reason: null,
|
|
3376
|
+
last_gate_failure: null,
|
|
2980
3377
|
pending_phase_transition: null,
|
|
2981
3378
|
phase_gate_status: {
|
|
2982
3379
|
...(state.phase_gate_status || {}),
|
|
@@ -3071,6 +3468,7 @@ export function approveRunCompletion(root, config) {
|
|
|
3071
3468
|
completed_at: new Date().toISOString(),
|
|
3072
3469
|
blocked_on: null,
|
|
3073
3470
|
blocked_reason: null,
|
|
3471
|
+
last_gate_failure: null,
|
|
3074
3472
|
pending_run_completion: null,
|
|
3075
3473
|
phase_gate_status: {
|
|
3076
3474
|
...(state.phase_gate_status || {}),
|