agentxchain 2.154.8 → 2.154.10
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 +1 -1
- package/src/commands/resume.js +134 -18
- package/src/lib/continuous-run.js +8 -2
- package/src/lib/governed-state.js +222 -0
package/package.json
CHANGED
package/src/commands/resume.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
getActiveTurnCount,
|
|
28
28
|
reactivateGovernedRun,
|
|
29
29
|
reconcilePhaseAdvanceBeforeDispatch,
|
|
30
|
+
reconcileRunCompletionBeforeDispatch,
|
|
30
31
|
transitionActiveTurnLifecycle,
|
|
31
32
|
STATE_PATH,
|
|
32
33
|
} from '../lib/governed-state.js';
|
|
@@ -59,42 +60,77 @@ function getStandingPendingExitGate(state, config) {
|
|
|
59
60
|
return config?.gates?.[gateId] || null;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function isFinalPhase(state, config) {
|
|
64
|
+
const phase = state?.phase;
|
|
65
|
+
if (!phase) return false;
|
|
66
|
+
const phases = Object.keys(config?.routing || {});
|
|
67
|
+
return phases.length > 0 && phases[phases.length - 1] === phase;
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
function entrySatisfiesSyntheticGateVerification(gate, entry) {
|
|
63
71
|
if (!gate?.requires_verification_pass) return true;
|
|
64
72
|
const verificationStatus = entry?.verification?.status;
|
|
65
73
|
return verificationStatus === 'pass' || verificationStatus === 'attested_pass';
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
function
|
|
69
|
-
// Returns
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// escalation where the agent blocked BEFORE writing gate artifacts
|
|
74
|
-
// (schedule-daemon `needs_decision` fixture).
|
|
76
|
+
function evaluateHumanApprovalGateArtifacts(root, state, config, entry) {
|
|
77
|
+
// Returns the artifact readiness shape for a human-required phase exit gate.
|
|
78
|
+
// A turn can be a valid standing-gate source either by producing one of the
|
|
79
|
+
// required files, or by re-verifying already-complete gate artifacts as the
|
|
80
|
+
// phase entry role before escalating to the human reviewer.
|
|
75
81
|
const phase = state?.phase;
|
|
76
82
|
const gateId = phase ? config?.routing?.[phase]?.exit_gate : null;
|
|
77
|
-
if (!gateId) return false;
|
|
83
|
+
if (!gateId) return { ready: false, contributed: false, entryRole: false };
|
|
78
84
|
const gate = config?.gates?.[gateId];
|
|
79
85
|
if (!gate || !Array.isArray(gate.requires_files) || gate.requires_files.length === 0) {
|
|
80
|
-
return false;
|
|
86
|
+
return { ready: false, contributed: false, entryRole: false };
|
|
81
87
|
}
|
|
82
|
-
if (!gate.requires_human_approval) return false;
|
|
88
|
+
if (!gate.requires_human_approval) return { ready: false, contributed: false, entryRole: false };
|
|
83
89
|
const filesChanged = Array.isArray(entry?.files_changed) ? entry.files_changed : [];
|
|
84
90
|
const required = gate.requires_files.filter((p) => typeof p === 'string' && p.trim());
|
|
85
|
-
if (required.length === 0) return false;
|
|
91
|
+
if (required.length === 0) return { ready: false, contributed: false, entryRole: false };
|
|
86
92
|
const changedSet = new Set(filesChanged.filter((p) => typeof p === 'string'));
|
|
87
93
|
const contributed = required.some((relPath) => changedSet.has(relPath));
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
94
|
+
const ready = required.every((relPath) => existsSync(join(root, relPath)));
|
|
95
|
+
const entryRole = Boolean(config?.routing?.[phase]?.entry_role)
|
|
96
|
+
&& (entry?.role === config.routing[phase].entry_role || entry?.assigned_role === config.routing[phase].entry_role);
|
|
97
|
+
return { ready, contributed, entryRole };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function turnCanApproveHumanGateFromEscalation(root, state, config, entry, proposed) {
|
|
101
|
+
const artifacts = evaluateHumanApprovalGateArtifacts(root, state, config, entry);
|
|
102
|
+
if (!artifacts.ready) return false;
|
|
103
|
+
if (artifacts.contributed) return true;
|
|
104
|
+
const phase = state?.phase;
|
|
105
|
+
const gateId = phase ? config?.routing?.[phase]?.exit_gate : null;
|
|
106
|
+
const gateApprovalText = [
|
|
107
|
+
entry?.summary,
|
|
108
|
+
entry?.needs_human_reason,
|
|
109
|
+
entry?.verification?.evidence_summary,
|
|
110
|
+
state?.blocked_on,
|
|
111
|
+
state?.blocked_reason?.recovery?.detail,
|
|
112
|
+
...(Array.isArray(entry?.decisions)
|
|
113
|
+
? entry.decisions.flatMap((decision) => [decision?.statement, decision?.rationale])
|
|
114
|
+
: []),
|
|
115
|
+
]
|
|
116
|
+
.filter((value) => typeof value === 'string' && value.trim())
|
|
117
|
+
.join('\n')
|
|
118
|
+
.toLowerCase();
|
|
119
|
+
const referencesHumanGateApproval = Boolean(gateId)
|
|
120
|
+
&& (
|
|
121
|
+
gateApprovalText.includes(gateId.toLowerCase())
|
|
122
|
+
|| (gateApprovalText.includes('gate') && gateApprovalText.includes('human approval'))
|
|
123
|
+
|| (gateApprovalText.includes('gate') && gateApprovalText.includes('human-required'))
|
|
124
|
+
);
|
|
125
|
+
// tusq.dev BUG-52 real shape: PM re-verified already-complete planning
|
|
126
|
+
// artifacts, changed no files by design, and escalated to human because the
|
|
127
|
+
// gate requires human approval. The human unblock is the gate approval.
|
|
128
|
+
return proposed === 'human' && artifacts.entryRole && referencesHumanGateApproval;
|
|
93
129
|
}
|
|
94
130
|
|
|
95
131
|
function latestCompletedTurnWantsPhaseContinuation(root, state, config, opts = {}) {
|
|
96
132
|
const turnId = opts?.turnId || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null;
|
|
97
|
-
if (!turnId) return false;
|
|
133
|
+
if (!turnId || isFinalPhase(state, config)) return false;
|
|
98
134
|
const historyPath = join(root, config?.files?.history || '.agentxchain/history.jsonl');
|
|
99
135
|
if (!existsSync(historyPath)) return false;
|
|
100
136
|
const lines = readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
@@ -130,7 +166,36 @@ function latestCompletedTurnWantsPhaseContinuation(root, state, config, opts = {
|
|
|
130
166
|
if (
|
|
131
167
|
entry.status === 'needs_human'
|
|
132
168
|
&& entrySatisfiesSyntheticGateVerification(standingGate, entry)
|
|
133
|
-
&&
|
|
169
|
+
&& turnCanApproveHumanGateFromEscalation(root, state, config, entry, proposed)
|
|
170
|
+
) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function latestCompletedTurnWantsRunCompletion(root, state, config, opts = {}) {
|
|
179
|
+
const turnId = opts?.turnId || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null;
|
|
180
|
+
if (!turnId || !isFinalPhase(state, config)) return false;
|
|
181
|
+
const historyPath = join(root, config?.files?.history || '.agentxchain/history.jsonl');
|
|
182
|
+
if (!existsSync(historyPath)) return false;
|
|
183
|
+
const lines = readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
184
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
185
|
+
let entry = null;
|
|
186
|
+
try {
|
|
187
|
+
entry = JSON.parse(lines[index]);
|
|
188
|
+
} catch {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (entry?.turn_id !== turnId) continue;
|
|
192
|
+
if (entry.run_completion_request === true) return true;
|
|
193
|
+
const standingGate = getStandingPendingExitGate(state, config);
|
|
194
|
+
const proposed = typeof entry.proposed_next_role === 'string' ? entry.proposed_next_role.trim() : '';
|
|
195
|
+
if (
|
|
196
|
+
entry.status === 'needs_human'
|
|
197
|
+
&& entrySatisfiesSyntheticGateVerification(standingGate, entry)
|
|
198
|
+
&& turnCanApproveHumanGateFromEscalation(root, state, config, entry, proposed)
|
|
134
199
|
) {
|
|
135
200
|
return true;
|
|
136
201
|
}
|
|
@@ -301,6 +366,57 @@ export async function resumeCommand(opts) {
|
|
|
301
366
|
}
|
|
302
367
|
}
|
|
303
368
|
|
|
369
|
+
if (
|
|
370
|
+
state.status === 'blocked'
|
|
371
|
+
&& resumeVia === 'operator_unblock'
|
|
372
|
+
&& hasStandingPendingExitGate(state, config)
|
|
373
|
+
&& latestCompletedTurnWantsRunCompletion(root, state, config, { turnId: opts.turn || null })
|
|
374
|
+
) {
|
|
375
|
+
const reactivated = reactivateGovernedRun(root, state, { via: resumeVia, notificationConfig: config });
|
|
376
|
+
if (!reactivated.ok) {
|
|
377
|
+
console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
state = reactivated.state;
|
|
381
|
+
console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
|
|
382
|
+
if (reactivated.migration_notice) {
|
|
383
|
+
console.log(chalk.yellow(reactivated.migration_notice));
|
|
384
|
+
}
|
|
385
|
+
if (reactivated.phantom_notice) {
|
|
386
|
+
console.log(chalk.yellow(reactivated.phantom_notice));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const completionReconciliation = reconcileRunCompletionBeforeDispatch(root, config, state, {
|
|
390
|
+
allow_active_turn_cleanup: true,
|
|
391
|
+
allow_standing_gate: true,
|
|
392
|
+
standing_gate_turn_id: opts.turn || null,
|
|
393
|
+
});
|
|
394
|
+
if (!completionReconciliation.ok && !completionReconciliation.state) {
|
|
395
|
+
console.log(chalk.red(`Failed to reconcile run completion before dispatch: ${completionReconciliation.error}`));
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
state = completionReconciliation.state || state;
|
|
399
|
+
if (completionReconciliation.completed) {
|
|
400
|
+
console.log(chalk.green(`Completed run before dispatch: ${state.run_id}`));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
markRunBlocked(root, {
|
|
404
|
+
category: 'needs_human',
|
|
405
|
+
blockedOn: state.blocked_on || 'human:unblock_completion_reconcile_failed',
|
|
406
|
+
recovery: {
|
|
407
|
+
typed_reason: 'needs_human',
|
|
408
|
+
owner: 'human',
|
|
409
|
+
recovery_action: 'agentxchain approve-completion',
|
|
410
|
+
turn_retained: true,
|
|
411
|
+
detail: 'Operator unblock resolved the escalation, but no run completion could be materialized from the current gate state.',
|
|
412
|
+
},
|
|
413
|
+
turnId: opts.turn || null,
|
|
414
|
+
notificationConfig: config,
|
|
415
|
+
});
|
|
416
|
+
console.log(chalk.red('Unblock did not materialize run completion; leaving the run blocked for manual recovery.'));
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
|
|
304
420
|
if (state.status === 'blocked' && activeCount > 0 && !skipRetainedRedispatch) {
|
|
305
421
|
let retainedTurn = null;
|
|
306
422
|
if (opts.turn) {
|
|
@@ -790,11 +790,15 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
790
790
|
}
|
|
791
791
|
|
|
792
792
|
const activeGovernedState = loadProjectState(root, context.config);
|
|
793
|
+
const queuedIntent = session.current_run_id
|
|
794
|
+
? findNextDispatchableIntent(root, { run_id: session.current_run_id })
|
|
795
|
+
: { ok: false };
|
|
793
796
|
if (
|
|
794
797
|
session.current_run_id
|
|
795
798
|
&& activeGovernedState?.status === 'active'
|
|
796
799
|
&& activeGovernedState.run_id === session.current_run_id
|
|
797
|
-
&&
|
|
800
|
+
&& !contOpts.continueFrom
|
|
801
|
+
&& !queuedIntent.ok
|
|
798
802
|
) {
|
|
799
803
|
log('Continuing active governed run.');
|
|
800
804
|
let execution;
|
|
@@ -855,7 +859,9 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
855
859
|
}
|
|
856
860
|
|
|
857
861
|
// Step 1: Check intake queue for pending work (BUG-34: scope to current run)
|
|
858
|
-
const queued =
|
|
862
|
+
const queued = queuedIntent.ok
|
|
863
|
+
? queuedIntent
|
|
864
|
+
: findNextDispatchableIntent(root, { run_id: session.current_run_id });
|
|
859
865
|
let targetIntentId = null;
|
|
860
866
|
let visionObjective = null;
|
|
861
867
|
|
|
@@ -1629,6 +1629,30 @@ function buildStandingPhaseTransitionSource(state, config, opts = {}) {
|
|
|
1629
1629
|
};
|
|
1630
1630
|
}
|
|
1631
1631
|
|
|
1632
|
+
function buildStandingRunCompletionSource(state, config, opts = {}) {
|
|
1633
|
+
const phase = state?.phase;
|
|
1634
|
+
const routing = phase ? config?.routing?.[phase] : null;
|
|
1635
|
+
const gateId = routing?.exit_gate || null;
|
|
1636
|
+
const nextPhase = getNextPhase(phase, config?.routing || {});
|
|
1637
|
+
if (!phase || !gateId || nextPhase) {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
if ((state?.phase_gate_status || {})[gateId] !== 'pending') {
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
return {
|
|
1644
|
+
turn_id: opts?.turn_id || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null,
|
|
1645
|
+
run_id: state?.run_id || null,
|
|
1646
|
+
role: null,
|
|
1647
|
+
assigned_role: null,
|
|
1648
|
+
phase,
|
|
1649
|
+
status: 'completed',
|
|
1650
|
+
run_completion_request: true,
|
|
1651
|
+
summary: `Synthetic ${gateId} completion source for operator-unblocked standing gate.`,
|
|
1652
|
+
verification: { status: 'pass' },
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1632
1656
|
function getPhaseRoles(config, phase) {
|
|
1633
1657
|
const routing = config?.routing?.[phase] || {};
|
|
1634
1658
|
const roles = new Set();
|
|
@@ -2974,6 +2998,204 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null,
|
|
|
2974
2998
|
};
|
|
2975
2999
|
}
|
|
2976
3000
|
|
|
3001
|
+
export function reconcileRunCompletionBeforeDispatch(root, config, state = null, opts = {}) {
|
|
3002
|
+
const currentState = state && typeof state === 'object' ? state : readState(root);
|
|
3003
|
+
if (!currentState) {
|
|
3004
|
+
return { ok: false, error: 'No governed state.json found' };
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
const activeTurnCount = getActiveTurnCount(currentState);
|
|
3008
|
+
const allowActiveTurnCleanup = opts?.allow_active_turn_cleanup === true;
|
|
3009
|
+
if (currentState.status !== 'active' || (activeTurnCount > 0 && !allowActiveTurnCleanup)) {
|
|
3010
|
+
return {
|
|
3011
|
+
ok: true,
|
|
3012
|
+
state: attachLegacyCurrentTurnAlias(currentState),
|
|
3013
|
+
completed: false,
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
const gateFailure = currentState.last_gate_failure;
|
|
3018
|
+
if (gateFailure && gateFailure.gate_type !== 'run_completion') {
|
|
3019
|
+
return {
|
|
3020
|
+
ok: true,
|
|
3021
|
+
state: attachLegacyCurrentTurnAlias(currentState),
|
|
3022
|
+
completed: false,
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
const historyEntries = readJsonlEntries(root, HISTORY_PATH);
|
|
3027
|
+
const requestedTurnId = gateFailure?.requested_by_turn
|
|
3028
|
+
|| currentState.queued_run_completion?.requested_by_turn
|
|
3029
|
+
|| currentState.last_completed_turn_id
|
|
3030
|
+
|| null;
|
|
3031
|
+
let completionSource = findHistoryTurnRequest(historyEntries, requestedTurnId, 'run_completion');
|
|
3032
|
+
if (!completionSource?.run_completion_request && opts?.allow_standing_gate === true) {
|
|
3033
|
+
completionSource = buildStandingRunCompletionSource(currentState, config, {
|
|
3034
|
+
turn_id: opts?.standing_gate_turn_id || null,
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
if (!completionSource?.run_completion_request) {
|
|
3038
|
+
return {
|
|
3039
|
+
ok: true,
|
|
3040
|
+
state: attachLegacyCurrentTurnAlias(currentState),
|
|
3041
|
+
completed: false,
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
const completionResult = evaluateRunCompletion({
|
|
3046
|
+
state: { ...currentState, history: historyEntries },
|
|
3047
|
+
config,
|
|
3048
|
+
acceptedTurn: completionSource,
|
|
3049
|
+
root,
|
|
3050
|
+
});
|
|
3051
|
+
|
|
3052
|
+
if (completionResult.action === 'awaiting_human_approval') {
|
|
3053
|
+
const approvalResult = evaluateApprovalPolicy({
|
|
3054
|
+
gateResult: completionResult,
|
|
3055
|
+
gateType: 'run_completion',
|
|
3056
|
+
state: { ...currentState, history: historyEntries },
|
|
3057
|
+
config,
|
|
3058
|
+
});
|
|
3059
|
+
|
|
3060
|
+
if (approvalResult.action === 'auto_approve') {
|
|
3061
|
+
const now = new Date().toISOString();
|
|
3062
|
+
const updatedState = {
|
|
3063
|
+
...currentState,
|
|
3064
|
+
status: 'completed',
|
|
3065
|
+
completed_at: now,
|
|
3066
|
+
blocked_on: null,
|
|
3067
|
+
blocked_reason: null,
|
|
3068
|
+
last_gate_failure: null,
|
|
3069
|
+
pending_run_completion: null,
|
|
3070
|
+
queued_run_completion: null,
|
|
3071
|
+
queued_phase_transition: null,
|
|
3072
|
+
phase_gate_status: {
|
|
3073
|
+
...(currentState.phase_gate_status || {}),
|
|
3074
|
+
[completionResult.gate_id || 'no_gate']: 'passed',
|
|
3075
|
+
},
|
|
3076
|
+
};
|
|
3077
|
+
writeState(root, updatedState);
|
|
3078
|
+
clearSlaReminders(root, 'pending_run_completion');
|
|
3079
|
+
appendJsonl(root, LEDGER_PATH, {
|
|
3080
|
+
type: 'approval_policy',
|
|
3081
|
+
gate_type: 'run_completion',
|
|
3082
|
+
action: 'auto_approve',
|
|
3083
|
+
matched_rule: approvalResult.matched_rule,
|
|
3084
|
+
reason: approvalResult.reason,
|
|
3085
|
+
gate_id: completionResult.gate_id || null,
|
|
3086
|
+
timestamp: now,
|
|
3087
|
+
});
|
|
3088
|
+
emitPendingLifecycleNotification(root, config, updatedState, 'run_completed', {
|
|
3089
|
+
completed_at: updatedState.completed_at,
|
|
3090
|
+
completed_via: 'approval_policy_reconciled_before_dispatch',
|
|
3091
|
+
gate: completionResult.gate_id || null,
|
|
3092
|
+
requested_by_turn: completionSource.turn_id || null,
|
|
3093
|
+
}, null);
|
|
3094
|
+
emitRunEvent(root, 'gate_approved', {
|
|
3095
|
+
run_id: updatedState.run_id,
|
|
3096
|
+
phase: updatedState.phase,
|
|
3097
|
+
status: 'completed',
|
|
3098
|
+
turn: completionSource.turn_id ? { turn_id: completionSource.turn_id, role_id: completionSource.role || completionSource.assigned_role || null } : undefined,
|
|
3099
|
+
payload: {
|
|
3100
|
+
gate_type: 'run_completion',
|
|
3101
|
+
gate_id: completionResult.gate_id || null,
|
|
3102
|
+
requested_by_turn: completionSource.turn_id || null,
|
|
3103
|
+
trigger: 'auto_approved',
|
|
3104
|
+
},
|
|
3105
|
+
});
|
|
3106
|
+
emitRunEvent(root, 'run_completed', {
|
|
3107
|
+
run_id: updatedState.run_id,
|
|
3108
|
+
phase: updatedState.phase,
|
|
3109
|
+
status: 'completed',
|
|
3110
|
+
payload: { completed_at: updatedState.completed_at },
|
|
3111
|
+
});
|
|
3112
|
+
writeSessionCheckpoint(root, updatedState, 'run_completed');
|
|
3113
|
+
recordRunHistory(root, updatedState, config, 'completed');
|
|
3114
|
+
return {
|
|
3115
|
+
ok: true,
|
|
3116
|
+
state: attachLegacyCurrentTurnAlias(updatedState),
|
|
3117
|
+
completed: true,
|
|
3118
|
+
gate_id: completionResult.gate_id || null,
|
|
3119
|
+
completionResult,
|
|
3120
|
+
approval_policy: approvalResult,
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
const pausedState = {
|
|
3125
|
+
...currentState,
|
|
3126
|
+
status: 'paused',
|
|
3127
|
+
blocked_on: `human_approval:${completionResult.gate_id}`,
|
|
3128
|
+
blocked_reason: null,
|
|
3129
|
+
pending_run_completion: {
|
|
3130
|
+
gate: completionResult.gate_id,
|
|
3131
|
+
requested_by_turn: completionSource.turn_id,
|
|
3132
|
+
requested_at: new Date().toISOString(),
|
|
3133
|
+
},
|
|
3134
|
+
queued_run_completion: null,
|
|
3135
|
+
queued_phase_transition: null,
|
|
3136
|
+
};
|
|
3137
|
+
writeState(root, pausedState);
|
|
3138
|
+
const approved = approveRunCompletion(root, config);
|
|
3139
|
+
return {
|
|
3140
|
+
ok: approved.ok,
|
|
3141
|
+
error: approved.error,
|
|
3142
|
+
state: approved.state || attachLegacyCurrentTurnAlias(readState(root)),
|
|
3143
|
+
completed: approved.ok,
|
|
3144
|
+
gate_id: completionResult.gate_id || null,
|
|
3145
|
+
completionResult,
|
|
3146
|
+
approval_policy: approvalResult,
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
if (completionResult.action !== 'complete') {
|
|
3151
|
+
return {
|
|
3152
|
+
ok: true,
|
|
3153
|
+
state: attachLegacyCurrentTurnAlias(currentState),
|
|
3154
|
+
completed: false,
|
|
3155
|
+
completionResult,
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
const now = new Date().toISOString();
|
|
3160
|
+
const updatedState = {
|
|
3161
|
+
...currentState,
|
|
3162
|
+
status: 'completed',
|
|
3163
|
+
completed_at: now,
|
|
3164
|
+
blocked_on: null,
|
|
3165
|
+
blocked_reason: null,
|
|
3166
|
+
last_gate_failure: null,
|
|
3167
|
+
pending_run_completion: null,
|
|
3168
|
+
queued_run_completion: null,
|
|
3169
|
+
queued_phase_transition: null,
|
|
3170
|
+
phase_gate_status: {
|
|
3171
|
+
...(currentState.phase_gate_status || {}),
|
|
3172
|
+
[completionResult.gate_id || 'no_gate']: 'passed',
|
|
3173
|
+
},
|
|
3174
|
+
};
|
|
3175
|
+
writeState(root, updatedState);
|
|
3176
|
+
emitPendingLifecycleNotification(root, config, updatedState, 'run_completed', {
|
|
3177
|
+
completed_at: updatedState.completed_at,
|
|
3178
|
+
completed_via: 'reconciled_before_dispatch',
|
|
3179
|
+
gate: completionResult.gate_id || null,
|
|
3180
|
+
requested_by_turn: completionSource.turn_id || null,
|
|
3181
|
+
}, null);
|
|
3182
|
+
emitRunEvent(root, 'run_completed', {
|
|
3183
|
+
run_id: updatedState.run_id,
|
|
3184
|
+
phase: updatedState.phase,
|
|
3185
|
+
status: 'completed',
|
|
3186
|
+
payload: { completed_at: updatedState.completed_at },
|
|
3187
|
+
});
|
|
3188
|
+
writeSessionCheckpoint(root, updatedState, 'run_completed');
|
|
3189
|
+
recordRunHistory(root, updatedState, config, 'completed');
|
|
3190
|
+
return {
|
|
3191
|
+
ok: true,
|
|
3192
|
+
state: attachLegacyCurrentTurnAlias(updatedState),
|
|
3193
|
+
completed: true,
|
|
3194
|
+
gate_id: completionResult.gate_id || null,
|
|
3195
|
+
completionResult,
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
|
|
2977
3199
|
// ── Core Operations ──────────────────────────────────────────────────────────
|
|
2978
3200
|
|
|
2979
3201
|
/**
|