agentxchain 2.154.7 → 2.154.9
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
|
@@ -68,18 +68,12 @@ import {
|
|
|
68
68
|
resolvePromptTransport,
|
|
69
69
|
resolveStartupWatchdogMs,
|
|
70
70
|
} from '../src/lib/adapters/local-cli-adapter.js';
|
|
71
|
+
import { CLAUDE_ENV_AUTH_KEYS } from '../src/lib/claude-local-auth.js';
|
|
71
72
|
|
|
72
73
|
const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
|
|
73
74
|
const REPO_ROOT_GUESS = resolve(SCRIPT_DIR, '..', '..');
|
|
74
75
|
|
|
75
76
|
const DIAGNOSTIC_ENV_KEYS = ['PATH', 'HOME', 'PWD', 'SHELL', 'TMPDIR'];
|
|
76
|
-
const AUTH_ENV_KEYS_TO_PROBE = [
|
|
77
|
-
'ANTHROPIC_API_KEY',
|
|
78
|
-
'CLAUDE_API_KEY',
|
|
79
|
-
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
80
|
-
'CLAUDE_CODE_USE_VERTEX',
|
|
81
|
-
'CLAUDE_CODE_USE_BEDROCK',
|
|
82
|
-
];
|
|
83
77
|
|
|
84
78
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
85
79
|
// Argv parsing — kept simple/explicit so tester can read what their flags do.
|
|
@@ -227,7 +221,7 @@ function snapshotEnv(env) {
|
|
|
227
221
|
if (typeof env[k] === 'string' && env[k].length > 0) visible[k] = env[k];
|
|
228
222
|
}
|
|
229
223
|
const authProbe = {};
|
|
230
|
-
for (const k of
|
|
224
|
+
for (const k of CLAUDE_ENV_AUTH_KEYS) {
|
|
231
225
|
authProbe[k] = typeof env[k] === 'string' && env[k].length > 0;
|
|
232
226
|
}
|
|
233
227
|
return { visible, auth_env_present: authProbe };
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
3
3
|
import { approvePhaseTransition } from '../lib/governed-state.js';
|
|
4
|
+
import { getNextPhase } from '../lib/gate-evaluator.js';
|
|
4
5
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
5
6
|
import { resolveGovernedRole } from '../lib/role-resolution.js';
|
|
6
7
|
import { checkCleanBaseline } from '../lib/repo-observer.js';
|
|
7
8
|
|
|
9
|
+
function getStandingPendingHumanExitGate(state, config) {
|
|
10
|
+
const phase = state?.phase || null;
|
|
11
|
+
const gateId = phase ? config?.routing?.[phase]?.exit_gate || null : null;
|
|
12
|
+
if (!phase || !gateId) return null;
|
|
13
|
+
const gate = config?.gates?.[gateId] || null;
|
|
14
|
+
if (!gate?.requires_human_approval) return null;
|
|
15
|
+
if ((state?.phase_gate_status || {})[gateId] !== 'pending') return null;
|
|
16
|
+
return {
|
|
17
|
+
gateId,
|
|
18
|
+
from: phase,
|
|
19
|
+
to: getNextPhase(phase, config?.routing || {}) || null,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
export async function approveTransitionCommand(opts) {
|
|
9
24
|
const context = loadProjectContext();
|
|
10
25
|
if (!context) {
|
|
@@ -25,6 +40,16 @@ export async function approveTransitionCommand(opts) {
|
|
|
25
40
|
if (state?.phase) {
|
|
26
41
|
console.log(chalk.dim(` Current phase: ${state.phase}`));
|
|
27
42
|
}
|
|
43
|
+
const standingGate = getStandingPendingHumanExitGate(state, config);
|
|
44
|
+
if (standingGate) {
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(chalk.yellow(` Gate "${standingGate.gateId}" is pending human approval, but no phase transition object is prepared.`));
|
|
47
|
+
if (standingGate.to) {
|
|
48
|
+
console.log(chalk.dim(` Expected transition: ${standingGate.from} → ${standingGate.to}`));
|
|
49
|
+
}
|
|
50
|
+
console.log(chalk.dim(' If this gate has an open human escalation, run: agentxchain unblock <hesc_id>'));
|
|
51
|
+
console.log(chalk.dim(` To inspect gate evidence, run: agentxchain gate show ${standingGate.gateId} --evaluate`));
|
|
52
|
+
}
|
|
28
53
|
process.exit(1);
|
|
29
54
|
}
|
|
30
55
|
|
package/src/commands/gate.js
CHANGED
|
@@ -312,4 +312,23 @@ function showGate(requestedGateId, { root, config, state, gateIds, opts }) {
|
|
|
312
312
|
}
|
|
313
313
|
console.log('');
|
|
314
314
|
}
|
|
315
|
+
|
|
316
|
+
const standingHint = getStandingRecoveryHint(gate, state);
|
|
317
|
+
if (standingHint) {
|
|
318
|
+
console.log(chalk.dim(' Recovery:'));
|
|
319
|
+
console.log(chalk.dim(` No phase transition is prepared for "${standingHint.gateId}".`));
|
|
320
|
+
console.log(chalk.dim(' If a human escalation is open, resolve with: agentxchain unblock <hesc_id>'));
|
|
321
|
+
console.log(chalk.dim(' After resolution, run: agentxchain approve-transition'));
|
|
322
|
+
console.log('');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function getStandingRecoveryHint(gate, state) {
|
|
327
|
+
if (!gate) return null;
|
|
328
|
+
if (!gate.requires_human_approval) return null;
|
|
329
|
+
if (gate.status !== 'pending') return null;
|
|
330
|
+
const currentPhase = state?.phase || null;
|
|
331
|
+
if (!currentPhase || gate.linked_phase !== currentPhase) return null;
|
|
332
|
+
if (state?.pending_phase_transition) return null;
|
|
333
|
+
return { gateId: gate.id };
|
|
315
334
|
}
|
package/src/commands/resume.js
CHANGED
|
@@ -65,35 +65,63 @@ function entrySatisfiesSyntheticGateVerification(gate, entry) {
|
|
|
65
65
|
return verificationStatus === 'pass' || verificationStatus === 'attested_pass';
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function
|
|
69
|
-
// Returns
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// escalation where the agent blocked BEFORE writing gate artifacts
|
|
74
|
-
// (schedule-daemon `needs_decision` fixture).
|
|
68
|
+
function evaluateHumanApprovalGateArtifacts(root, state, config, entry) {
|
|
69
|
+
// Returns the artifact readiness shape for a human-required phase exit gate.
|
|
70
|
+
// A turn can be a valid standing-gate source either by producing one of the
|
|
71
|
+
// required files, or by re-verifying already-complete gate artifacts as the
|
|
72
|
+
// phase entry role before escalating to the human reviewer.
|
|
75
73
|
const phase = state?.phase;
|
|
76
74
|
const gateId = phase ? config?.routing?.[phase]?.exit_gate : null;
|
|
77
|
-
if (!gateId) return false;
|
|
75
|
+
if (!gateId) return { ready: false, contributed: false, entryRole: false };
|
|
78
76
|
const gate = config?.gates?.[gateId];
|
|
79
77
|
if (!gate || !Array.isArray(gate.requires_files) || gate.requires_files.length === 0) {
|
|
80
|
-
return false;
|
|
78
|
+
return { ready: false, contributed: false, entryRole: false };
|
|
81
79
|
}
|
|
82
|
-
if (!gate.requires_human_approval) return false;
|
|
80
|
+
if (!gate.requires_human_approval) return { ready: false, contributed: false, entryRole: false };
|
|
83
81
|
const filesChanged = Array.isArray(entry?.files_changed) ? entry.files_changed : [];
|
|
84
82
|
const required = gate.requires_files.filter((p) => typeof p === 'string' && p.trim());
|
|
85
|
-
if (required.length === 0) return false;
|
|
83
|
+
if (required.length === 0) return { ready: false, contributed: false, entryRole: false };
|
|
86
84
|
const changedSet = new Set(filesChanged.filter((p) => typeof p === 'string'));
|
|
87
85
|
const contributed = required.some((relPath) => changedSet.has(relPath));
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
86
|
+
const ready = required.every((relPath) => existsSync(join(root, relPath)));
|
|
87
|
+
const entryRole = Boolean(config?.routing?.[phase]?.entry_role)
|
|
88
|
+
&& (entry?.role === config.routing[phase].entry_role || entry?.assigned_role === config.routing[phase].entry_role);
|
|
89
|
+
return { ready, contributed, entryRole };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function turnCanApproveHumanGateFromEscalation(root, state, config, entry, proposed) {
|
|
93
|
+
const artifacts = evaluateHumanApprovalGateArtifacts(root, state, config, entry);
|
|
94
|
+
if (!artifacts.ready) return false;
|
|
95
|
+
if (artifacts.contributed) return true;
|
|
96
|
+
const phase = state?.phase;
|
|
97
|
+
const gateId = phase ? config?.routing?.[phase]?.exit_gate : null;
|
|
98
|
+
const gateApprovalText = [
|
|
99
|
+
entry?.summary,
|
|
100
|
+
entry?.needs_human_reason,
|
|
101
|
+
entry?.verification?.evidence_summary,
|
|
102
|
+
state?.blocked_on,
|
|
103
|
+
state?.blocked_reason?.recovery?.detail,
|
|
104
|
+
...(Array.isArray(entry?.decisions)
|
|
105
|
+
? entry.decisions.flatMap((decision) => [decision?.statement, decision?.rationale])
|
|
106
|
+
: []),
|
|
107
|
+
]
|
|
108
|
+
.filter((value) => typeof value === 'string' && value.trim())
|
|
109
|
+
.join('\n')
|
|
110
|
+
.toLowerCase();
|
|
111
|
+
const referencesHumanGateApproval = Boolean(gateId)
|
|
112
|
+
&& (
|
|
113
|
+
gateApprovalText.includes(gateId.toLowerCase())
|
|
114
|
+
|| (gateApprovalText.includes('gate') && gateApprovalText.includes('human approval'))
|
|
115
|
+
|| (gateApprovalText.includes('gate') && gateApprovalText.includes('human-required'))
|
|
116
|
+
);
|
|
117
|
+
// tusq.dev BUG-52 real shape: PM re-verified already-complete planning
|
|
118
|
+
// artifacts, changed no files by design, and escalated to human because the
|
|
119
|
+
// gate requires human approval. The human unblock is the gate approval.
|
|
120
|
+
return proposed === 'human' && artifacts.entryRole && referencesHumanGateApproval;
|
|
93
121
|
}
|
|
94
122
|
|
|
95
|
-
function latestCompletedTurnWantsPhaseContinuation(root, state, config) {
|
|
96
|
-
const turnId = state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null;
|
|
123
|
+
function latestCompletedTurnWantsPhaseContinuation(root, state, config, opts = {}) {
|
|
124
|
+
const turnId = opts?.turnId || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null;
|
|
97
125
|
if (!turnId) return false;
|
|
98
126
|
const historyPath = join(root, config?.files?.history || '.agentxchain/history.jsonl');
|
|
99
127
|
if (!existsSync(historyPath)) return false;
|
|
@@ -130,7 +158,7 @@ function latestCompletedTurnWantsPhaseContinuation(root, state, config) {
|
|
|
130
158
|
if (
|
|
131
159
|
entry.status === 'needs_human'
|
|
132
160
|
&& entrySatisfiesSyntheticGateVerification(standingGate, entry)
|
|
133
|
-
&&
|
|
161
|
+
&& turnCanApproveHumanGateFromEscalation(root, state, config, entry, proposed)
|
|
134
162
|
) {
|
|
135
163
|
return true;
|
|
136
164
|
}
|
|
@@ -253,7 +281,7 @@ export async function resumeCommand(opts) {
|
|
|
253
281
|
state.status === 'blocked'
|
|
254
282
|
&& resumeVia === 'operator_unblock'
|
|
255
283
|
&& hasStandingPendingExitGate(state, config)
|
|
256
|
-
&& latestCompletedTurnWantsPhaseContinuation(root, state, config)
|
|
284
|
+
&& latestCompletedTurnWantsPhaseContinuation(root, state, config, { turnId: opts.turn || null })
|
|
257
285
|
) {
|
|
258
286
|
const reactivated = reactivateGovernedRun(root, state, { via: resumeVia, notificationConfig: config });
|
|
259
287
|
if (!reactivated.ok) {
|
|
@@ -272,6 +300,7 @@ export async function resumeCommand(opts) {
|
|
|
272
300
|
const phaseReconciliation = reconcilePhaseAdvanceBeforeDispatch(root, config, state, {
|
|
273
301
|
allow_active_turn_cleanup: true,
|
|
274
302
|
allow_standing_gate: true,
|
|
303
|
+
standing_gate_turn_id: opts.turn || null,
|
|
275
304
|
});
|
|
276
305
|
if (!phaseReconciliation.ok && !phaseReconciliation.state) {
|
|
277
306
|
console.log(chalk.red(`Failed to reconcile phase gate before dispatch: ${phaseReconciliation.error}`));
|
|
@@ -42,6 +42,7 @@ const DIAGNOSTIC_ENV_KEYS = [
|
|
|
42
42
|
];
|
|
43
43
|
const DIAGNOSTIC_STDERR_EXCERPT_LIMIT = 800;
|
|
44
44
|
const DEFAULT_STARTUP_WATCHDOG_MS = 180_000;
|
|
45
|
+
const DEFAULT_STARTUP_WATCHDOG_SIGKILL_GRACE_MS = 10_000;
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Launch a local CLI subprocess for a governed turn.
|
|
@@ -89,6 +90,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
89
90
|
return { ok: false, error: `Runtime "${runtimeId}" not found in config` };
|
|
90
91
|
}
|
|
91
92
|
const startupWatchdogMs = startupWatchdogOverrideMs ?? resolveStartupWatchdogMs(config, runtime);
|
|
93
|
+
const startupWatchdogKillGraceMs = resolveStartupWatchdogKillGraceMs(options.startupWatchdogKillGraceMs);
|
|
92
94
|
|
|
93
95
|
// Read the dispatch bundle prompt
|
|
94
96
|
const promptPath = join(root, getDispatchPromptPath(turn.turn_id));
|
|
@@ -188,6 +190,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
188
190
|
let spawnConfirmedAtMs = null;
|
|
189
191
|
let firstOutputLatencyMs = null;
|
|
190
192
|
let startupWatchdog = null;
|
|
193
|
+
let startupSigkillHandle = null;
|
|
191
194
|
let startupTimedOut = false;
|
|
192
195
|
let startupFailureType = null;
|
|
193
196
|
let stdoutBytes = 0;
|
|
@@ -205,6 +208,10 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
205
208
|
clearTimeout(startupWatchdog);
|
|
206
209
|
startupWatchdog = null;
|
|
207
210
|
}
|
|
211
|
+
if (startupSigkillHandle) {
|
|
212
|
+
clearTimeout(startupSigkillHandle);
|
|
213
|
+
startupSigkillHandle = null;
|
|
214
|
+
}
|
|
208
215
|
};
|
|
209
216
|
|
|
210
217
|
const armStartupWatchdog = () => {
|
|
@@ -218,15 +225,31 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
218
225
|
startupTimedOut = true;
|
|
219
226
|
startupFailureType = 'no_subprocess_output';
|
|
220
227
|
logs.push(`[adapter] Startup watchdog fired after ${Math.round(startupWatchdogMs / 1000)}s with no output.`);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
appendDiagnostic(logs, 'startup_watchdog_fired', {
|
|
229
|
+
startup_watchdog_ms: startupWatchdogMs,
|
|
230
|
+
startup_watchdog_sigkill_grace_ms: startupWatchdogKillGraceMs,
|
|
231
|
+
pid: child.pid ?? null,
|
|
232
|
+
spawn_confirmed_at: spawnConfirmedAt,
|
|
233
|
+
elapsed_since_spawn_ms: spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs),
|
|
234
|
+
});
|
|
235
|
+
try {
|
|
236
|
+
child.kill('SIGTERM');
|
|
229
237
|
} catch {}
|
|
238
|
+
if (startupWatchdogKillGraceMs > 0) {
|
|
239
|
+
startupSigkillHandle = setTimeout(() => {
|
|
240
|
+
logs.push('[adapter] Startup watchdog grace period expired. Sending SIGKILL.');
|
|
241
|
+
appendDiagnostic(logs, 'startup_watchdog_sigkill', {
|
|
242
|
+
startup_watchdog_ms: startupWatchdogMs,
|
|
243
|
+
startup_watchdog_sigkill_grace_ms: startupWatchdogKillGraceMs,
|
|
244
|
+
pid: child.pid ?? null,
|
|
245
|
+
spawn_confirmed_at: spawnConfirmedAt,
|
|
246
|
+
elapsed_since_spawn_ms: spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs),
|
|
247
|
+
});
|
|
248
|
+
try {
|
|
249
|
+
child.kill('SIGKILL');
|
|
250
|
+
} catch {}
|
|
251
|
+
}, startupWatchdogKillGraceMs);
|
|
252
|
+
}
|
|
230
253
|
}, startupWatchdogMs);
|
|
231
254
|
};
|
|
232
255
|
|
|
@@ -313,6 +336,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
313
336
|
// Timeout handling per §20.4
|
|
314
337
|
let timeoutHandle;
|
|
315
338
|
let sigkillHandle;
|
|
339
|
+
let abortSigkillHandle;
|
|
316
340
|
|
|
317
341
|
if (timeoutMs > 0 && timeoutMs < Infinity) {
|
|
318
342
|
timeoutHandle = setTimeout(() => {
|
|
@@ -337,11 +361,13 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
337
361
|
clearStartupWatchdog();
|
|
338
362
|
clearTimeout(timeoutHandle);
|
|
339
363
|
clearTimeout(sigkillHandle);
|
|
364
|
+
clearTimeout(abortSigkillHandle);
|
|
340
365
|
try {
|
|
341
366
|
child.kill('SIGTERM');
|
|
342
367
|
} catch {}
|
|
343
368
|
// Give it 5 seconds to exit gracefully
|
|
344
|
-
setTimeout(() => {
|
|
369
|
+
abortSigkillHandle = setTimeout(() => {
|
|
370
|
+
abortSigkillHandle = null;
|
|
345
371
|
try { child.kill('SIGKILL'); } catch {}
|
|
346
372
|
}, 5000);
|
|
347
373
|
};
|
|
@@ -355,6 +381,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
355
381
|
clearStartupWatchdog();
|
|
356
382
|
clearTimeout(timeoutHandle);
|
|
357
383
|
clearTimeout(sigkillHandle);
|
|
384
|
+
clearTimeout(abortSigkillHandle);
|
|
358
385
|
if (signal) signal.removeEventListener('abort', onAbort);
|
|
359
386
|
|
|
360
387
|
if (signal?.aborted) {
|
|
@@ -457,6 +484,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
457
484
|
clearStartupWatchdog();
|
|
458
485
|
clearTimeout(timeoutHandle);
|
|
459
486
|
clearTimeout(sigkillHandle);
|
|
487
|
+
clearTimeout(abortSigkillHandle);
|
|
460
488
|
if (signal) signal.removeEventListener('abort', onAbort);
|
|
461
489
|
// BUG-54 hypothesis #1 fix: explicitly release stdio streams on the
|
|
462
490
|
// error path so Node reclaims pipe handles immediately instead of
|
|
@@ -583,6 +611,13 @@ function resolveStartupWatchdogMs(config, runtime) {
|
|
|
583
611
|
return DEFAULT_STARTUP_WATCHDOG_MS;
|
|
584
612
|
}
|
|
585
613
|
|
|
614
|
+
function resolveStartupWatchdogKillGraceMs(value) {
|
|
615
|
+
if (Number.isInteger(value) && value >= 0) {
|
|
616
|
+
return value;
|
|
617
|
+
}
|
|
618
|
+
return DEFAULT_STARTUP_WATCHDOG_SIGKILL_GRACE_MS;
|
|
619
|
+
}
|
|
620
|
+
|
|
586
621
|
/**
|
|
587
622
|
* Check if the staged result file exists and has meaningful content.
|
|
588
623
|
* Delegates to the shared `hasMeaningfulStagedResult` helper so watchdog,
|
|
@@ -1605,7 +1605,7 @@ function resolvePhaseTransitionSource(historyEntries, gateFailure, fallbackTurnI
|
|
|
1605
1605
|
return requestedSource;
|
|
1606
1606
|
}
|
|
1607
1607
|
|
|
1608
|
-
function buildStandingPhaseTransitionSource(state, config) {
|
|
1608
|
+
function buildStandingPhaseTransitionSource(state, config, opts = {}) {
|
|
1609
1609
|
const phase = state?.phase;
|
|
1610
1610
|
const routing = phase ? config?.routing?.[phase] : null;
|
|
1611
1611
|
const gateId = routing?.exit_gate || null;
|
|
@@ -1617,7 +1617,7 @@ function buildStandingPhaseTransitionSource(state, config) {
|
|
|
1617
1617
|
return null;
|
|
1618
1618
|
}
|
|
1619
1619
|
return {
|
|
1620
|
-
turn_id: state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null,
|
|
1620
|
+
turn_id: opts?.turn_id || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null,
|
|
1621
1621
|
run_id: state?.run_id || null,
|
|
1622
1622
|
role: null,
|
|
1623
1623
|
assigned_role: null,
|
|
@@ -2746,7 +2746,9 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null,
|
|
|
2746
2746
|
currentState.queued_phase_transition || null,
|
|
2747
2747
|
);
|
|
2748
2748
|
if (!phaseSource?.phase_transition_request && opts?.allow_standing_gate === true) {
|
|
2749
|
-
phaseSource = buildStandingPhaseTransitionSource(currentState, config
|
|
2749
|
+
phaseSource = buildStandingPhaseTransitionSource(currentState, config, {
|
|
2750
|
+
turn_id: opts?.standing_gate_turn_id || null,
|
|
2751
|
+
});
|
|
2750
2752
|
}
|
|
2751
2753
|
if (!phaseSource?.phase_transition_request) {
|
|
2752
2754
|
return {
|
|
@@ -2801,6 +2803,7 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null,
|
|
|
2801
2803
|
const cleanup = cleanupPhaseAdvanceArtifacts(root, nextState, config, prevPhase);
|
|
2802
2804
|
nextState = cleanup.state;
|
|
2803
2805
|
writeState(root, nextState);
|
|
2806
|
+
writeSessionCheckpoint(root, nextState, 'phase_reconciled');
|
|
2804
2807
|
appendJsonl(root, LEDGER_PATH, {
|
|
2805
2808
|
type: 'approval_policy',
|
|
2806
2809
|
gate_type: 'phase_transition',
|
|
@@ -2919,6 +2922,7 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null,
|
|
|
2919
2922
|
nextState = cleanup.state;
|
|
2920
2923
|
|
|
2921
2924
|
writeState(root, nextState);
|
|
2925
|
+
writeSessionCheckpoint(root, nextState, 'phase_reconciled');
|
|
2922
2926
|
const retiredIntentIds = retireApprovedPhaseScopedIntents(root, nextState, config, prevPhase, now);
|
|
2923
2927
|
if (retiredIntentIds.length > 0) {
|
|
2924
2928
|
emitRunEvent(root, 'intent_retired_by_phase_advance', {
|
|
@@ -6152,17 +6156,26 @@ export function approvePhaseTransition(root, config, opts = {}) {
|
|
|
6152
6156
|
run_id: updatedState.run_id,
|
|
6153
6157
|
phase: updatedState.phase,
|
|
6154
6158
|
status: 'active',
|
|
6155
|
-
|
|
6159
|
+
turn: transition.requested_by_turn ? { turn_id: transition.requested_by_turn, role_id: null } : undefined,
|
|
6160
|
+
payload: {
|
|
6161
|
+
gate_id: transition.gate || null,
|
|
6162
|
+
gate_type: 'phase_transition',
|
|
6163
|
+
from: transition.from,
|
|
6164
|
+
to: transition.to,
|
|
6165
|
+
requested_by_turn: transition.requested_by_turn || null,
|
|
6166
|
+
},
|
|
6156
6167
|
});
|
|
6157
6168
|
emitRunEvent(root, 'phase_entered', {
|
|
6158
6169
|
run_id: updatedState.run_id,
|
|
6159
6170
|
phase: updatedState.phase,
|
|
6160
6171
|
status: 'active',
|
|
6172
|
+
turn: transition.requested_by_turn ? { turn_id: transition.requested_by_turn, role_id: null } : undefined,
|
|
6161
6173
|
payload: {
|
|
6162
6174
|
from: transition.from,
|
|
6163
6175
|
to: transition.to,
|
|
6164
6176
|
gate_id: transition.gate || 'no_gate',
|
|
6165
6177
|
trigger: 'human_approved',
|
|
6178
|
+
requested_by_turn: transition.requested_by_turn || null,
|
|
6166
6179
|
},
|
|
6167
6180
|
});
|
|
6168
6181
|
emitRunEvent(root, 'phase_cleanup', {
|