agentxchain 2.147.0 → 2.148.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/package.json
CHANGED
|
@@ -31,6 +31,15 @@ import {
|
|
|
31
31
|
import { verifyDispatchManifestForAdapter } from '../dispatch-manifest.js';
|
|
32
32
|
import { hasMeaningfulStagedResult } from '../staged-result-proof.js';
|
|
33
33
|
|
|
34
|
+
const DIAGNOSTIC_ENV_KEYS = [
|
|
35
|
+
'PATH',
|
|
36
|
+
'HOME',
|
|
37
|
+
'PWD',
|
|
38
|
+
'SHELL',
|
|
39
|
+
'TMPDIR',
|
|
40
|
+
'AGENTXCHAIN_TURN_ID',
|
|
41
|
+
];
|
|
42
|
+
|
|
34
43
|
/**
|
|
35
44
|
* Launch a local CLI subprocess for a governed turn.
|
|
36
45
|
*
|
|
@@ -112,6 +121,10 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
112
121
|
|
|
113
122
|
// Capture logs for dispatch record
|
|
114
123
|
const logs = [];
|
|
124
|
+
const runtimeCwd = runtime.cwd ? join(root, runtime.cwd) : root;
|
|
125
|
+
const spawnEnv = { ...process.env, AGENTXCHAIN_TURN_ID: turn.turn_id };
|
|
126
|
+
const stdinBytes = transport === 'stdin' ? Buffer.byteLength(fullPrompt, 'utf8') : 0;
|
|
127
|
+
const diagnosticArgs = redactPromptArgs(args, fullPrompt, transport);
|
|
115
128
|
|
|
116
129
|
return new Promise((resolve) => {
|
|
117
130
|
if (signal?.aborted) {
|
|
@@ -121,12 +134,23 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
121
134
|
|
|
122
135
|
let child;
|
|
123
136
|
try {
|
|
137
|
+
appendDiagnostic(logs, 'spawn_prepare', {
|
|
138
|
+
runtime_id: runtimeId,
|
|
139
|
+
turn_id: turn.turn_id,
|
|
140
|
+
command,
|
|
141
|
+
args: diagnosticArgs,
|
|
142
|
+
cwd: runtimeCwd,
|
|
143
|
+
prompt_transport: transport,
|
|
144
|
+
stdin_bytes: stdinBytes,
|
|
145
|
+
env: pickDiagnosticEnv(spawnEnv),
|
|
146
|
+
});
|
|
124
147
|
child = spawn(command, args, {
|
|
125
|
-
cwd:
|
|
148
|
+
cwd: runtimeCwd,
|
|
126
149
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
127
|
-
env:
|
|
150
|
+
env: spawnEnv,
|
|
128
151
|
});
|
|
129
152
|
} catch (err) {
|
|
153
|
+
appendDiagnostic(logs, 'spawn_error', normalizeDiagnosticError(err));
|
|
130
154
|
resolve({
|
|
131
155
|
ok: false,
|
|
132
156
|
startupFailure: true,
|
|
@@ -140,9 +164,13 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
140
164
|
let settled = false;
|
|
141
165
|
let firstOutputAt = null;
|
|
142
166
|
let spawnConfirmedAt = null;
|
|
167
|
+
let spawnConfirmedAtMs = null;
|
|
168
|
+
let firstOutputLatencyMs = null;
|
|
143
169
|
let startupWatchdog = null;
|
|
144
170
|
let startupTimedOut = false;
|
|
145
171
|
let startupFailureType = null;
|
|
172
|
+
let stdoutBytes = 0;
|
|
173
|
+
let stderrBytes = 0;
|
|
146
174
|
|
|
147
175
|
const settle = (result) => {
|
|
148
176
|
if (settled) return;
|
|
@@ -168,8 +196,14 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
168
196
|
startupTimedOut = true;
|
|
169
197
|
startupFailureType = 'no_subprocess_output';
|
|
170
198
|
logs.push(`[adapter] Startup watchdog fired after ${Math.round(startupWatchdogMs / 1000)}s with no output.`);
|
|
171
|
-
|
|
172
|
-
|
|
199
|
+
appendDiagnostic(logs, 'startup_watchdog_fired', {
|
|
200
|
+
startup_watchdog_ms: startupWatchdogMs,
|
|
201
|
+
pid: child.pid ?? null,
|
|
202
|
+
spawn_confirmed_at: spawnConfirmedAt,
|
|
203
|
+
elapsed_since_spawn_ms: spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs),
|
|
204
|
+
});
|
|
205
|
+
try {
|
|
206
|
+
child.kill('SIGTERM');
|
|
173
207
|
} catch {}
|
|
174
208
|
}, startupWatchdogMs);
|
|
175
209
|
};
|
|
@@ -177,7 +211,14 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
177
211
|
const recordFirstOutput = (stream) => {
|
|
178
212
|
if (firstOutputAt) return;
|
|
179
213
|
firstOutputAt = new Date().toISOString();
|
|
214
|
+
firstOutputLatencyMs = spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs);
|
|
180
215
|
clearStartupWatchdog();
|
|
216
|
+
appendDiagnostic(logs, 'first_output', {
|
|
217
|
+
at: firstOutputAt,
|
|
218
|
+
stream,
|
|
219
|
+
pid: child.pid ?? null,
|
|
220
|
+
startup_latency_ms: firstOutputLatencyMs,
|
|
221
|
+
});
|
|
181
222
|
if (onFirstOutput) {
|
|
182
223
|
try {
|
|
183
224
|
onFirstOutput({ pid: child.pid ?? null, at: firstOutputAt, stream });
|
|
@@ -186,7 +227,13 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
186
227
|
};
|
|
187
228
|
|
|
188
229
|
child.once('spawn', () => {
|
|
230
|
+
spawnConfirmedAtMs = Date.now();
|
|
189
231
|
spawnConfirmedAt = new Date().toISOString();
|
|
232
|
+
appendDiagnostic(logs, 'spawn_attached', {
|
|
233
|
+
pid: child.pid ?? null,
|
|
234
|
+
at: spawnConfirmedAt,
|
|
235
|
+
startup_watchdog_ms: startupWatchdogMs,
|
|
236
|
+
});
|
|
190
237
|
if (onSpawnAttached) {
|
|
191
238
|
try {
|
|
192
239
|
onSpawnAttached({ pid: child.pid ?? null, at: spawnConfirmedAt });
|
|
@@ -197,18 +244,32 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
197
244
|
|
|
198
245
|
// Deliver prompt via stdin if transport is "stdin"; otherwise close immediately
|
|
199
246
|
if (child.stdin) {
|
|
247
|
+
child.stdin.on('error', (err) => {
|
|
248
|
+
appendDiagnostic(logs, 'stdin_error', {
|
|
249
|
+
at: new Date().toISOString(),
|
|
250
|
+
stdin_bytes: stdinBytes,
|
|
251
|
+
...normalizeDiagnosticError(err),
|
|
252
|
+
});
|
|
253
|
+
});
|
|
200
254
|
try {
|
|
201
255
|
if (transport === 'stdin') {
|
|
202
256
|
child.stdin.write(fullPrompt);
|
|
203
257
|
}
|
|
204
258
|
child.stdin.end();
|
|
205
|
-
} catch {
|
|
259
|
+
} catch (err) {
|
|
260
|
+
appendDiagnostic(logs, 'stdin_error', {
|
|
261
|
+
at: new Date().toISOString(),
|
|
262
|
+
stdin_bytes: stdinBytes,
|
|
263
|
+
...normalizeDiagnosticError(err),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
206
266
|
}
|
|
207
267
|
|
|
208
268
|
// Collect stdout/stderr
|
|
209
269
|
if (child.stdout) {
|
|
210
270
|
child.stdout.on('data', (chunk) => {
|
|
211
271
|
const text = chunk.toString();
|
|
272
|
+
stdoutBytes += Buffer.byteLength(text);
|
|
212
273
|
recordFirstOutput('stdout');
|
|
213
274
|
logs.push(text);
|
|
214
275
|
if (onStdout) onStdout(text);
|
|
@@ -218,6 +279,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
218
279
|
if (child.stderr) {
|
|
219
280
|
child.stderr.on('data', (chunk) => {
|
|
220
281
|
const text = chunk.toString();
|
|
282
|
+
stderrBytes += Buffer.byteLength(text);
|
|
221
283
|
recordFirstOutput('stderr');
|
|
222
284
|
logs.push('[stderr] ' + text);
|
|
223
285
|
if (onStderr) onStderr(text);
|
|
@@ -283,6 +345,28 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
283
345
|
if (hasResult && !firstOutputAt) {
|
|
284
346
|
recordFirstOutput('staged_result');
|
|
285
347
|
}
|
|
348
|
+
const exitDiagnostic = {
|
|
349
|
+
pid: child.pid ?? null,
|
|
350
|
+
exit_code: exitCode,
|
|
351
|
+
signal: killSignal,
|
|
352
|
+
spawn_confirmed_at: spawnConfirmedAt,
|
|
353
|
+
elapsed_since_spawn_ms: spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs),
|
|
354
|
+
first_output_at: firstOutputAt,
|
|
355
|
+
startup_latency_ms: firstOutputLatencyMs,
|
|
356
|
+
stdout_bytes: stdoutBytes,
|
|
357
|
+
stderr_bytes: stderrBytes,
|
|
358
|
+
staged_result_ready: hasResult,
|
|
359
|
+
};
|
|
360
|
+
if (startupTimedOut) {
|
|
361
|
+
exitDiagnostic.startup_failure_type = startupFailureType || 'no_subprocess_output';
|
|
362
|
+
} else if (!spawnConfirmedAt) {
|
|
363
|
+
exitDiagnostic.startup_failure_type = 'runtime_spawn_failed';
|
|
364
|
+
} else if (timedOut) {
|
|
365
|
+
exitDiagnostic.timed_out = true;
|
|
366
|
+
} else if (!firstOutputAt) {
|
|
367
|
+
exitDiagnostic.startup_failure_type = 'no_subprocess_output';
|
|
368
|
+
}
|
|
369
|
+
appendDiagnostic(logs, 'process_exit', exitDiagnostic);
|
|
286
370
|
|
|
287
371
|
if (hasResult) {
|
|
288
372
|
settle({ ok: true, exitCode, timedOut: false, aborted: false, logs, firstOutputAt });
|
|
@@ -344,6 +428,25 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
344
428
|
clearTimeout(timeoutHandle);
|
|
345
429
|
clearTimeout(sigkillHandle);
|
|
346
430
|
if (signal) signal.removeEventListener('abort', onAbort);
|
|
431
|
+
// BUG-54 hypothesis #1 fix: explicitly release stdio streams on the
|
|
432
|
+
// error path so Node reclaims pipe handles immediately instead of
|
|
433
|
+
// waiting for GC. Without this, repeated `runtime_spawn_failed` turns
|
|
434
|
+
// leak ~4 handles per failure until the next GC sweep, which in a
|
|
435
|
+
// long-running `run --continuous` session can push the parent process
|
|
436
|
+
// toward its fd limit and cascade additional spawn failures.
|
|
437
|
+
try { child.stdin?.destroy(); } catch {}
|
|
438
|
+
try { child.stdout?.destroy(); } catch {}
|
|
439
|
+
try { child.stderr?.destroy(); } catch {}
|
|
440
|
+
appendDiagnostic(logs, 'spawn_error', {
|
|
441
|
+
pid: child.pid ?? null,
|
|
442
|
+
spawn_confirmed_at: spawnConfirmedAt,
|
|
443
|
+
elapsed_since_spawn_ms: spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs),
|
|
444
|
+
first_output_at: firstOutputAt,
|
|
445
|
+
startup_latency_ms: firstOutputLatencyMs,
|
|
446
|
+
stdout_bytes: stdoutBytes,
|
|
447
|
+
stderr_bytes: stderrBytes,
|
|
448
|
+
...normalizeDiagnosticError(err),
|
|
449
|
+
});
|
|
347
450
|
settle({
|
|
348
451
|
ok: false,
|
|
349
452
|
startupFailure: !firstOutputAt,
|
|
@@ -458,4 +561,38 @@ function resolveTargetTurn(state, turnId) {
|
|
|
458
561
|
return state?.current_turn || Object.values(state?.active_turns || {})[0];
|
|
459
562
|
}
|
|
460
563
|
|
|
564
|
+
function appendDiagnostic(logs, label, payload) {
|
|
565
|
+
logs.push(`[adapter:diag] ${label} ${JSON.stringify(payload)}\n`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function pickDiagnosticEnv(env) {
|
|
569
|
+
return Object.fromEntries(
|
|
570
|
+
DIAGNOSTIC_ENV_KEYS
|
|
571
|
+
.filter((key) => typeof env?.[key] === 'string' && env[key].length > 0)
|
|
572
|
+
.map((key) => [key, env[key]]),
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function redactPromptArgs(args, fullPrompt, transport) {
|
|
577
|
+
const promptPlaceholder = `<prompt:${Buffer.byteLength(fullPrompt, 'utf8')} bytes>`;
|
|
578
|
+
return args.map((arg) => {
|
|
579
|
+
if (typeof arg !== 'string') {
|
|
580
|
+
return arg;
|
|
581
|
+
}
|
|
582
|
+
if (transport === 'argv' && arg === fullPrompt) {
|
|
583
|
+
return promptPlaceholder;
|
|
584
|
+
}
|
|
585
|
+
return arg;
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function normalizeDiagnosticError(err) {
|
|
590
|
+
return {
|
|
591
|
+
code: err?.code || null,
|
|
592
|
+
errno: err?.errno || null,
|
|
593
|
+
syscall: err?.syscall || null,
|
|
594
|
+
message: err?.message || String(err),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
461
598
|
export { resolvePromptTransport };
|
|
@@ -3729,8 +3729,37 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3729
3729
|
],
|
|
3730
3730
|
);
|
|
3731
3731
|
if (!dirtyParity.clean) {
|
|
3732
|
-
|
|
3733
|
-
|
|
3732
|
+
// BUG-55 sub-defect B: when the turn declared verification commands or
|
|
3733
|
+
// machine evidence, undeclared dirty files are most likely verification
|
|
3734
|
+
// outputs that need classification under verification.produced_files
|
|
3735
|
+
// (disposition 'ignore' to clean up, or 'artifact' to include in the
|
|
3736
|
+
// checkpoint). Surface a dedicated error class + message so the agent
|
|
3737
|
+
// knows the correct remediation surface, instead of the generic
|
|
3738
|
+
// files_changed-or-produced_files-or-clean advice that the non-
|
|
3739
|
+
// verification path emits.
|
|
3740
|
+
const verification = turnResult.verification && typeof turnResult.verification === 'object'
|
|
3741
|
+
? turnResult.verification
|
|
3742
|
+
: {};
|
|
3743
|
+
const declaredVerificationCommands = Array.isArray(verification.commands)
|
|
3744
|
+
&& verification.commands.some((c) => typeof c === 'string' && c.trim().length > 0);
|
|
3745
|
+
const declaredMachineEvidence = Array.isArray(verification.machine_evidence)
|
|
3746
|
+
&& verification.machine_evidence.some((e) => e && typeof e === 'object' && typeof e.command === 'string' && e.command.trim().length > 0);
|
|
3747
|
+
const verificationWasDeclared = declaredVerificationCommands || declaredMachineEvidence;
|
|
3748
|
+
|
|
3749
|
+
let failureReason = dirtyParity.reason;
|
|
3750
|
+
let failureErrorCode = 'artifact_dirty_tree_mismatch';
|
|
3751
|
+
if (verificationWasDeclared) {
|
|
3752
|
+
failureErrorCode = 'undeclared_verification_outputs';
|
|
3753
|
+
const undeclared = Array.isArray(dirtyParity.unexpected_dirty_files)
|
|
3754
|
+
? dirtyParity.unexpected_dirty_files
|
|
3755
|
+
: [];
|
|
3756
|
+
const listForMessage = undeclared.slice(0, 5).join(', ')
|
|
3757
|
+
+ (undeclared.length > 5 ? '...' : '');
|
|
3758
|
+
failureReason = `Verification was declared (commands or machine_evidence), but these files are dirty and not classified: ${listForMessage}. Classify each under verification.produced_files with disposition "ignore" (the file should be cleaned up after replay) or "artifact" (the file should be checkpointed as part of the turn), OR add it to files_changed if it is a core turn mutation. Acceptance cannot proceed until the declared contract matches the working tree.`;
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
transitionToFailedAcceptance(root, state, currentTurn, failureReason, {
|
|
3762
|
+
error_code: failureErrorCode,
|
|
3734
3763
|
stage: 'artifact_observation',
|
|
3735
3764
|
extra: {
|
|
3736
3765
|
unexpected_dirty_files: dirtyParity.unexpected_dirty_files,
|
|
@@ -3739,13 +3768,14 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3739
3768
|
});
|
|
3740
3769
|
return {
|
|
3741
3770
|
ok: false,
|
|
3742
|
-
error:
|
|
3771
|
+
error: failureReason,
|
|
3772
|
+
error_code: failureErrorCode,
|
|
3743
3773
|
validation: {
|
|
3744
3774
|
...validation,
|
|
3745
3775
|
ok: false,
|
|
3746
3776
|
stage: 'artifact_observation',
|
|
3747
3777
|
error_class: 'artifact_error',
|
|
3748
|
-
errors: [
|
|
3778
|
+
errors: [failureReason],
|
|
3749
3779
|
warnings: validation.warnings,
|
|
3750
3780
|
},
|
|
3751
3781
|
};
|
|
@@ -148,7 +148,10 @@
|
|
|
148
148
|
},
|
|
149
149
|
"commands": {
|
|
150
150
|
"type": "array",
|
|
151
|
-
"items": {
|
|
151
|
+
"items": {
|
|
152
|
+
"type": "string",
|
|
153
|
+
"pattern": "\\S"
|
|
154
|
+
},
|
|
152
155
|
"description": "Verification commands that were run."
|
|
153
156
|
},
|
|
154
157
|
"evidence_summary": {
|
|
@@ -161,7 +164,10 @@
|
|
|
161
164
|
"type": "object",
|
|
162
165
|
"required": ["command", "exit_code"],
|
|
163
166
|
"properties": {
|
|
164
|
-
"command": {
|
|
167
|
+
"command": {
|
|
168
|
+
"type": "string",
|
|
169
|
+
"pattern": "\\S"
|
|
170
|
+
},
|
|
165
171
|
"exit_code": { "type": "integer" }
|
|
166
172
|
}
|
|
167
173
|
}
|
|
@@ -139,6 +139,53 @@ function buildCheckpointCommit(entry) {
|
|
|
139
139
|
return { subject, body: bodyLines.join('\n') };
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
function diffMissingDeclaredPaths(declaredFiles, stagedFiles) {
|
|
143
|
+
const stagedSet = new Set((Array.isArray(stagedFiles) ? stagedFiles : []).map((value) => value.trim()).filter(Boolean));
|
|
144
|
+
return (Array.isArray(declaredFiles) ? declaredFiles : []).filter((filePath) => !stagedSet.has(filePath));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Partition paths that were missing from the staged-diff into
|
|
149
|
+
* (a) paths genuinely absent from git (untracked or dirty without staging)
|
|
150
|
+
* (b) paths already committed upstream (tracked in HEAD, no pending diff)
|
|
151
|
+
*
|
|
152
|
+
* BUG-55A completeness must only fail on (a). An actor that committed a
|
|
153
|
+
* declared file before `checkpoint-turn` ran (see BUG-23 scenario) is
|
|
154
|
+
* already-checkpointed-upstream; treating that as "missing from checkpoint"
|
|
155
|
+
* is a false positive from the completeness gate.
|
|
156
|
+
*/
|
|
157
|
+
function partitionDeclaredPathsByUpstreamPresence(root, missingPaths) {
|
|
158
|
+
const genuinelyMissing = [];
|
|
159
|
+
const alreadyCommittedUpstream = [];
|
|
160
|
+
for (const filePath of missingPaths) {
|
|
161
|
+
let tracked = false;
|
|
162
|
+
try {
|
|
163
|
+
git(root, ['ls-files', '--error-unmatch', '--', filePath]);
|
|
164
|
+
tracked = true;
|
|
165
|
+
} catch {
|
|
166
|
+
tracked = false;
|
|
167
|
+
}
|
|
168
|
+
if (!tracked) {
|
|
169
|
+
genuinelyMissing.push(filePath);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
let hasDivergence = false;
|
|
173
|
+
try {
|
|
174
|
+
const headDiff = git(root, ['diff', 'HEAD', '--', filePath]);
|
|
175
|
+
const cachedDiff = git(root, ['diff', '--cached', '--', filePath]);
|
|
176
|
+
hasDivergence = Boolean(headDiff) || Boolean(cachedDiff);
|
|
177
|
+
} catch {
|
|
178
|
+
hasDivergence = true;
|
|
179
|
+
}
|
|
180
|
+
if (hasDivergence) {
|
|
181
|
+
genuinelyMissing.push(filePath);
|
|
182
|
+
} else {
|
|
183
|
+
alreadyCommittedUpstream.push(filePath);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { genuinelyMissing, alreadyCommittedUpstream };
|
|
187
|
+
}
|
|
188
|
+
|
|
142
189
|
export function detectPendingCheckpoint(root, dirtyFiles = []) {
|
|
143
190
|
const actorDirtyFiles = normalizeFilesChanged(dirtyFiles);
|
|
144
191
|
if (actorDirtyFiles.length === 0) return { required: false };
|
|
@@ -233,12 +280,29 @@ export function checkpointAcceptedTurn(root, opts = {}) {
|
|
|
233
280
|
};
|
|
234
281
|
}
|
|
235
282
|
|
|
283
|
+
const rawMissingFromStage = diffMissingDeclaredPaths(filesChanged, staged);
|
|
284
|
+
const { genuinelyMissing, alreadyCommittedUpstream } =
|
|
285
|
+
partitionDeclaredPathsByUpstreamPresence(root, rawMissingFromStage);
|
|
286
|
+
if (genuinelyMissing.length > 0) {
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
turn: entry,
|
|
290
|
+
error: `Checkpoint completeness failure: accepted turn ${entry.turn_id} declared ${filesChanged.length} checkpointable file(s), but Git staged only ${staged.length} and ${genuinelyMissing.length} declared path(s) are absent from git. Missing from checkpoint: ${genuinelyMissing.join(', ')}.`,
|
|
291
|
+
missing_declared_paths: genuinelyMissing,
|
|
292
|
+
already_committed_upstream: alreadyCommittedUpstream,
|
|
293
|
+
staged_paths: staged,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
236
297
|
if (staged.length === 0) {
|
|
237
298
|
return {
|
|
238
299
|
ok: true,
|
|
239
300
|
skipped: true,
|
|
240
301
|
turn: entry,
|
|
241
|
-
reason:
|
|
302
|
+
reason: alreadyCommittedUpstream.length > 0
|
|
303
|
+
? `Accepted turn ${entry.turn_id} has no staged repo changes to checkpoint; all ${alreadyCommittedUpstream.length} declared file(s) already present in HEAD.`
|
|
304
|
+
: `Accepted turn ${entry.turn_id} has no staged repo changes to checkpoint.`,
|
|
305
|
+
already_committed_upstream: alreadyCommittedUpstream,
|
|
242
306
|
};
|
|
243
307
|
}
|
|
244
308
|
|
|
@@ -600,6 +600,15 @@ function validateVerification(tr) {
|
|
|
600
600
|
}
|
|
601
601
|
}
|
|
602
602
|
|
|
603
|
+
if (Array.isArray(v.commands)) {
|
|
604
|
+
for (let i = 0; i < v.commands.length; i++) {
|
|
605
|
+
const command = v.commands[i];
|
|
606
|
+
if (typeof command !== 'string' || command.trim().length === 0) {
|
|
607
|
+
errors.push(`verification.commands[${i}] must be a non-empty string.`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
603
612
|
// machine_evidence exit codes should be consistent with status
|
|
604
613
|
if (Array.isArray(v.machine_evidence)) {
|
|
605
614
|
for (let i = 0; i < v.machine_evidence.length; i++) {
|
|
@@ -608,8 +617,8 @@ function validateVerification(tr) {
|
|
|
608
617
|
errors.push(`verification.machine_evidence[${i}] must be an object.`);
|
|
609
618
|
continue;
|
|
610
619
|
}
|
|
611
|
-
if (typeof entry.command !== 'string') {
|
|
612
|
-
errors.push(`verification.machine_evidence[${i}].command must be a string.`);
|
|
620
|
+
if (typeof entry.command !== 'string' || entry.command.trim().length === 0) {
|
|
621
|
+
errors.push(`verification.machine_evidence[${i}].command must be a non-empty string.`);
|
|
613
622
|
}
|
|
614
623
|
if (typeof entry.exit_code !== 'number' || !Number.isInteger(entry.exit_code)) {
|
|
615
624
|
errors.push(`verification.machine_evidence[${i}].exit_code must be an integer.`);
|