premanmcp 1.1.6 → 1.1.7
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/agent.js +129 -11
- package/bin/connect/pairing.js +26 -0
- package/bin/connect.js +8 -2
- package/bin/eval.js +94 -5
- package/bin/runner.js +13 -0
- package/package.json +2 -1
package/bin/agent.js
CHANGED
|
@@ -829,6 +829,11 @@ export function createDock({
|
|
|
829
829
|
// old rows before painting new ones, and after the event they are no longer
|
|
830
830
|
// derivable from the terminal -- it has already reflowed.
|
|
831
831
|
let painted = null;
|
|
832
|
+
// Where the transcript has got to, in screen coordinates. The terminal keeps
|
|
833
|
+
// this too, in its saved cursor, and that copy is the one the writes use --
|
|
834
|
+
// but a resize destroys it, so it is mirrored here to be put back. Null means
|
|
835
|
+
// nobody knows: before `enable`, and after any stretch the dock did not paint.
|
|
836
|
+
let at = null;
|
|
832
837
|
|
|
833
838
|
const size = () => ({
|
|
834
839
|
columns: stream.columns || 80,
|
|
@@ -838,6 +843,82 @@ export function createDock({
|
|
|
838
843
|
/** Last row the transcript may use; everything below belongs to the dock. */
|
|
839
844
|
const floor = () => Math.max(1, size().rows - DOCK_ROWS);
|
|
840
845
|
|
|
846
|
+
/**
|
|
847
|
+
* Follow the transcript's cursor through a chunk.
|
|
848
|
+
*
|
|
849
|
+
* The terminal is tracking the same thing in its saved cursor and does it
|
|
850
|
+
* perfectly, so this exists for one moment only: a resize, after which that
|
|
851
|
+
* saved position no longer means anything and the dock has to name a row to
|
|
852
|
+
* carry on from. It used to name the floor, which is why a session that had
|
|
853
|
+
* printed ten lines onto a fifty-row window jumped to the bottom of it and
|
|
854
|
+
* scrolled the banner away to say the eleventh.
|
|
855
|
+
*
|
|
856
|
+
* Only the finals that move the cursor are read. The erases and the colour
|
|
857
|
+
* runs that make up most of a transcript leave it exactly where it was, and
|
|
858
|
+
* a sequence this does not recognise is likelier to be one of those than a
|
|
859
|
+
* jump -- so the unknown case is "no movement" rather than a guess.
|
|
860
|
+
*/
|
|
861
|
+
function advance(chunk) {
|
|
862
|
+
if (!at) return;
|
|
863
|
+
const { columns } = size();
|
|
864
|
+
const limit = floor();
|
|
865
|
+
let { row, column } = at;
|
|
866
|
+
// At the floor the region scrolls under the cursor rather than moving it:
|
|
867
|
+
// everything already printed goes up a row and the cursor stays put.
|
|
868
|
+
const down = () => {
|
|
869
|
+
if (row < limit) row += 1;
|
|
870
|
+
};
|
|
871
|
+
const text = String(chunk);
|
|
872
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
873
|
+
const ch = text[i];
|
|
874
|
+
if (ch === "\u001b") {
|
|
875
|
+
const next = text[i + 1];
|
|
876
|
+
if (next === "[") {
|
|
877
|
+
let j = i + 2;
|
|
878
|
+
while (j < text.length && !/[@-~]/.test(text[j])) j += 1;
|
|
879
|
+
const params = text.slice(i + 2, j);
|
|
880
|
+
const final = text[j];
|
|
881
|
+
if (final === "H" || final === "f") {
|
|
882
|
+
const [r, c] = params.split(";");
|
|
883
|
+
row = Math.min(Math.max(Number(r) || 1, 1), limit);
|
|
884
|
+
column = Math.min(Math.max(Number(c) || 1, 1), columns);
|
|
885
|
+
} else if (final === "G") {
|
|
886
|
+
column = Math.min(Math.max(Number(params) || 1, 1), columns);
|
|
887
|
+
}
|
|
888
|
+
i = j;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
if (next === "]") {
|
|
892
|
+
while (i < text.length && text[i] !== "\u0007") i += 1;
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
i += 1;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (ch === "\r") {
|
|
899
|
+
column = 1;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (ch === "\n") {
|
|
903
|
+
// ONLCR: stdout to a terminal turns a bare newline into CR+LF, so the
|
|
904
|
+
// column goes back to one. Modelling it as a pure index down would
|
|
905
|
+
// stair-step every line of the transcript to the right.
|
|
906
|
+
column = 1;
|
|
907
|
+
down();
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (ch === "\u0007") continue;
|
|
911
|
+
// Deferred wrap, the way a terminal does it: the glyph in the last column
|
|
912
|
+
// leaves the cursor on that column, and the *next* one moves the line on.
|
|
913
|
+
if (column > columns) {
|
|
914
|
+
column = 1;
|
|
915
|
+
down();
|
|
916
|
+
}
|
|
917
|
+
column += 1;
|
|
918
|
+
}
|
|
919
|
+
at = { row, column };
|
|
920
|
+
}
|
|
921
|
+
|
|
841
922
|
/**
|
|
842
923
|
* Is there a window here to dock in at all?
|
|
843
924
|
*
|
|
@@ -983,16 +1064,32 @@ export function createDock({
|
|
|
983
1064
|
return {
|
|
984
1065
|
live,
|
|
985
1066
|
|
|
986
|
-
/**
|
|
987
|
-
|
|
1067
|
+
/**
|
|
1068
|
+
* Reserve the bottom rows and remember where the transcript is.
|
|
1069
|
+
*
|
|
1070
|
+
* `row` is where the transcript has got to on a screen the caller knows the
|
|
1071
|
+
* state of -- a session that has just cleared the screen and printed a
|
|
1072
|
+
* banner knows exactly that. Without it the screen is assumed to be full,
|
|
1073
|
+
* which is the honest reading when the dock is coming back up after
|
|
1074
|
+
* something else owned the terminal: scroll to make room for the furniture
|
|
1075
|
+
* and carry on at the floor.
|
|
1076
|
+
*/
|
|
1077
|
+
enable({ row } = {}) {
|
|
988
1078
|
if (!live || open) return;
|
|
989
1079
|
open = true;
|
|
990
1080
|
listen();
|
|
991
1081
|
stream.write(SET_TITLE);
|
|
1082
|
+
// Whatever a previous life left here says nothing about this screen.
|
|
1083
|
+
at = null;
|
|
992
1084
|
if (!roomy()) return;
|
|
993
|
-
|
|
1085
|
+
if (row == null) {
|
|
1086
|
+
stream.write("\n".repeat(DOCK_ROWS));
|
|
1087
|
+
at = { row: floor(), column: 1 };
|
|
1088
|
+
} else {
|
|
1089
|
+
at = { row: Math.min(Math.max(1, row), floor()), column: 1 };
|
|
1090
|
+
}
|
|
994
1091
|
stream.write(`\u001b[1;${floor()}r`);
|
|
995
|
-
stream.write(`\u001b[${
|
|
1092
|
+
stream.write(`\u001b[${at.row};${at.column}H`);
|
|
996
1093
|
stream.write("\u001b7");
|
|
997
1094
|
frame();
|
|
998
1095
|
},
|
|
@@ -1023,12 +1120,18 @@ export function createDock({
|
|
|
1023
1120
|
*/
|
|
1024
1121
|
write(chunk) {
|
|
1025
1122
|
if (!open || !roomy()) {
|
|
1123
|
+
// Nothing is following the cursor down a screen the dock does not own,
|
|
1124
|
+
// so whatever was tracked is now a guess. Said rather than kept: a
|
|
1125
|
+
// window that grows back is better off assuming the transcript filled
|
|
1126
|
+
// it than resuming at a row from before it went blind.
|
|
1127
|
+
at = null;
|
|
1026
1128
|
stream.write(chunk);
|
|
1027
1129
|
return;
|
|
1028
1130
|
}
|
|
1029
1131
|
// One write rather than three: the restore, the chunk and the save are a
|
|
1030
1132
|
// single sequence to the terminal, so nothing can land between them.
|
|
1031
1133
|
stream.write(`\u001b8${chunk}\u001b7`);
|
|
1134
|
+
advance(chunk);
|
|
1032
1135
|
repaint();
|
|
1033
1136
|
},
|
|
1034
1137
|
|
|
@@ -1096,10 +1199,18 @@ export function createDock({
|
|
|
1096
1199
|
painted = null;
|
|
1097
1200
|
if (!roomy()) return;
|
|
1098
1201
|
stream.write(`\u001b[1;${floor()}r`);
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
//
|
|
1102
|
-
|
|
1202
|
+
// Where the transcript actually is, clamped into the new window -- not
|
|
1203
|
+
// the floor, which is what this used to say. Naming the floor meant every
|
|
1204
|
+
// drag of a window edge moved the transcript to the bottom of it, so the
|
|
1205
|
+
// next line printed scrolled the region and the banner climbed a row
|
|
1206
|
+
// towards the top and off. On a session that had barely started, that is
|
|
1207
|
+
// the whole screen going blank above a reply pinned to the bottom.
|
|
1208
|
+
const { columns } = size();
|
|
1209
|
+
at = {
|
|
1210
|
+
row: Math.min(Math.max(1, at?.row ?? floor()), floor()),
|
|
1211
|
+
column: Math.min(Math.max(1, at?.column ?? 1), columns),
|
|
1212
|
+
};
|
|
1213
|
+
stream.write(`\u001b[${at.row};${at.column}H`);
|
|
1103
1214
|
stream.write("\u001b7");
|
|
1104
1215
|
frame();
|
|
1105
1216
|
},
|
|
@@ -1122,6 +1233,7 @@ export function createDock({
|
|
|
1122
1233
|
}
|
|
1123
1234
|
if (!open) return;
|
|
1124
1235
|
open = false;
|
|
1236
|
+
at = null;
|
|
1125
1237
|
const { rows } = size();
|
|
1126
1238
|
stream.write("\u001b[r");
|
|
1127
1239
|
stream.write(`\u001b[${rows};1H\u001b[2K`);
|
|
@@ -1650,8 +1762,6 @@ export async function agentCommand(commandArgs = [], { authenticate = authentica
|
|
|
1650
1762
|
|
|
1651
1763
|
if (fullScreen) process.stdout.write(`${ENTER_ALT}${CLEAR_SCREEN}`);
|
|
1652
1764
|
|
|
1653
|
-
process.stdout.write(banner({ paint, workspace, backend: backendUrl(args), conversation }));
|
|
1654
|
-
|
|
1655
1765
|
const dock = createDock({
|
|
1656
1766
|
stream: process.stdout,
|
|
1657
1767
|
paint,
|
|
@@ -1660,7 +1770,15 @@ export async function agentCommand(commandArgs = [], { authenticate = authentica
|
|
|
1660
1770
|
const prompt = `${paint.caret("\u276f")} `;
|
|
1661
1771
|
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt });
|
|
1662
1772
|
dock.attach(rl);
|
|
1663
|
-
|
|
1773
|
+
// The screen was just cleared, so the transcript starts at the top of it and
|
|
1774
|
+
// the dock is told so. The banner then goes through the dock rather than
|
|
1775
|
+
// around it, which is what keeps the two in step: printing it to stdout first
|
|
1776
|
+
// and docking afterwards left the transcript anchored at the bottom of the
|
|
1777
|
+
// window, eleven blank rows below a banner that the first line of the first
|
|
1778
|
+
// reply then scrolled a row closer to the top.
|
|
1779
|
+
dock.enable(fullScreen ? { row: 1 } : undefined);
|
|
1780
|
+
|
|
1781
|
+
dock.write(banner({ paint, workspace, backend: backendUrl(args), conversation }));
|
|
1664
1782
|
const onResize = () => dock.resize();
|
|
1665
1783
|
process.stdout.on("resize", onResize);
|
|
1666
1784
|
|
package/bin/connect/pairing.js
CHANGED
|
@@ -51,6 +51,32 @@ async function verifyKey(args, apiKey) {
|
|
|
51
51
|
return { pairCode: "", pairingId: "", stale: result.status_code === 401 };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Redeem the pair code from this terminal.
|
|
56
|
+
*
|
|
57
|
+
* The link used to be closed by the agent calling `preman_status`, but the MCP
|
|
58
|
+
* server is a pure bridge now and defines no such tool, so nothing has made this
|
|
59
|
+
* call since. This terminal holds both halves anyway — the key and the code it
|
|
60
|
+
* just minted — so it closes the link itself rather than waiting for a call that
|
|
61
|
+
* no longer happens. Failure is not fatal: the config is already written.
|
|
62
|
+
*/
|
|
63
|
+
export async function redeemPairCode(args, agent, apiKey, pairCode) {
|
|
64
|
+
if (!pairCode) return false;
|
|
65
|
+
const result = await callBackendJson(args, "POST", "/workbench/coding-agent/heartbeat", {
|
|
66
|
+
token: apiKey,
|
|
67
|
+
json: {
|
|
68
|
+
pair_code: pairCode,
|
|
69
|
+
agent: agent.id,
|
|
70
|
+
project_path: process.cwd(),
|
|
71
|
+
source: "premanmcp",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
if (!result.ok) {
|
|
75
|
+
process.stdout.write(`Note: could not close the link (${result.status_code}).\n`);
|
|
76
|
+
}
|
|
77
|
+
return Boolean(result.ok);
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
/** Whichever of the two ways to learn the backend still accepts this key. */
|
|
55
81
|
export async function checkKeyAndPair(args, agent, apiKey) {
|
|
56
82
|
return args.has("--no-pair")
|
package/bin/connect.js
CHANGED
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
renderCodexToml,
|
|
50
50
|
verifyWrittenConfig,
|
|
51
51
|
} from "./connect/configs.js";
|
|
52
|
-
import { checkKeyAndPair, refreshStaleCredentials } from "./connect/pairing.js";
|
|
52
|
+
import { checkKeyAndPair, redeemPairCode, refreshStaleCredentials } from "./connect/pairing.js";
|
|
53
53
|
import {
|
|
54
54
|
autoCheckIn,
|
|
55
55
|
lastLine,
|
|
@@ -258,6 +258,8 @@ export async function connectCommand(commandArgs) {
|
|
|
258
258
|
);
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
const redeemed = await redeemPairCode(args, agent, apiKey, pairCode);
|
|
262
|
+
|
|
261
263
|
// A non-interactive run can still be handed the credential up front, so this
|
|
262
264
|
// stays reachable; the prompt inside only fires when there is a TTY, and by
|
|
263
265
|
// then onboarding is done.
|
|
@@ -273,6 +275,7 @@ export async function connectCommand(commandArgs) {
|
|
|
273
275
|
serverConfig,
|
|
274
276
|
projectInstall,
|
|
275
277
|
pairingId,
|
|
278
|
+
preLinked: redeemed,
|
|
276
279
|
});
|
|
277
280
|
// The agent goes back to the caller because `onboard` runs steps after this one
|
|
278
281
|
// that need to know which agent to drive, and asking twice is a question we
|
|
@@ -394,7 +397,7 @@ async function establishCheckIn(
|
|
|
394
397
|
args,
|
|
395
398
|
agent,
|
|
396
399
|
apiKey,
|
|
397
|
-
{ serverName, written, serverConfig, projectInstall = false, pairingId = "" }
|
|
400
|
+
{ serverName, written, serverConfig, projectInstall = false, pairingId = "", preLinked = false }
|
|
398
401
|
) {
|
|
399
402
|
const notes = [];
|
|
400
403
|
// Both set only when this directory redirects the agent: where it can be
|
|
@@ -402,6 +405,9 @@ async function establishCheckIn(
|
|
|
402
405
|
let elsewhere = null;
|
|
403
406
|
let blockedHere = "";
|
|
404
407
|
const done = (linked) => ({ linked, blockedHere });
|
|
408
|
+
// Already linked by the terminal that wrote the config: nothing to self-test
|
|
409
|
+
// into, no window to open, and no check-in to wait for.
|
|
410
|
+
if (preLinked) return done(true);
|
|
405
411
|
const ticker = pollTicker();
|
|
406
412
|
const say = (text) => {
|
|
407
413
|
ticker.end();
|
package/bin/eval.js
CHANGED
|
@@ -470,6 +470,18 @@ export function runDir(cwd, job) {
|
|
|
470
470
|
return path.join(cwd, "artifacts", "results", String(job.suite), String(job.run));
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Where the suite's own output lands — the parent of every run directory.
|
|
475
|
+
*
|
|
476
|
+
* Only ever joined with a name from `SUITE_ARTIFACTS`. The same directory also
|
|
477
|
+
* holds assert-ai's versioned `artifacts/<stage>/v0001/` tree, which nothing
|
|
478
|
+
* reads back and which a device walking the directory would start uploading;
|
|
479
|
+
* naming the files explicitly is what keeps that out.
|
|
480
|
+
*/
|
|
481
|
+
export function suiteDir(cwd, job) {
|
|
482
|
+
return path.join(cwd, "artifacts", "results", String(job.suite));
|
|
483
|
+
}
|
|
484
|
+
|
|
473
485
|
/**
|
|
474
486
|
* The four stages as one activity list, current stage active, earlier ones done.
|
|
475
487
|
*
|
|
@@ -522,6 +534,21 @@ export const RUN_ARTIFACTS = [
|
|
|
522
534
|
"artifacts.json",
|
|
523
535
|
];
|
|
524
536
|
|
|
537
|
+
/**
|
|
538
|
+
* The files that describe the bank rather than one run.
|
|
539
|
+
*
|
|
540
|
+
* These are the cases themselves and the rubric they are scored against, and
|
|
541
|
+
* they live one level up from the run directory because every run of the suite
|
|
542
|
+
* shares them. Without them the store holds verdicts with no record of what was
|
|
543
|
+
* asked: the dashboard can say "case 3 failed" and cannot say what case 3 was.
|
|
544
|
+
*
|
|
545
|
+
* No scope is sent with an upload. The server derives it from the filename
|
|
546
|
+
* (`eval_runner_artifacts.scope_of`), files these under the suite rather than
|
|
547
|
+
* the run, and applies its own write-once rule — so a second run of the same
|
|
548
|
+
* suite offers them again and the server declines to rewrite history.
|
|
549
|
+
*/
|
|
550
|
+
export const SUITE_ARTIFACTS = ["taxonomy.json", "test_set.jsonl"];
|
|
551
|
+
|
|
525
552
|
/**
|
|
526
553
|
* Upload the artifacts that changed since last time.
|
|
527
554
|
*
|
|
@@ -536,10 +563,15 @@ export const RUN_ARTIFACTS = [
|
|
|
536
563
|
* success and the next tick retries.
|
|
537
564
|
*/
|
|
538
565
|
export async function syncArtifacts(job, cwd, sent, { call, log = () => {}, lease } = {}) {
|
|
539
|
-
|
|
566
|
+
// Both sets on the same tick, not the suite files at the end: `systematize`
|
|
567
|
+
// and `test_set` finish before the first case does, so a run that dies
|
|
568
|
+
// halfway would otherwise publish scores for cases it never published.
|
|
569
|
+
const sources = [
|
|
570
|
+
...RUN_ARTIFACTS.map((name) => [name, path.join(runDir(cwd, job), name)]),
|
|
571
|
+
...SUITE_ARTIFACTS.map((name) => [name, path.join(suiteDir(cwd, job), name)]),
|
|
572
|
+
];
|
|
540
573
|
let uploaded = 0;
|
|
541
|
-
for (const name of
|
|
542
|
-
const file = path.join(dir, name);
|
|
574
|
+
for (const [name, file] of sources) {
|
|
543
575
|
let mark;
|
|
544
576
|
try {
|
|
545
577
|
const stat = statSync(file);
|
|
@@ -561,7 +593,18 @@ export async function syncArtifacts(job, cwd, sent, { call, log = () => {}, leas
|
|
|
561
593
|
form.set("lease_token", lease);
|
|
562
594
|
form.set("name", name);
|
|
563
595
|
form.set("file", new Blob([body]), name);
|
|
564
|
-
|
|
596
|
+
// A refused upload and an unsendable one are the same situation to this
|
|
597
|
+
// loop -- the file is still on disk, unrecorded in `sent`, and the next
|
|
598
|
+
// tick will offer it again. They are not the same to `callBackendJson`,
|
|
599
|
+
// which answers the first and *throws* the second, so without this a
|
|
600
|
+
// backend that blinks mid-run takes the run's process down with it.
|
|
601
|
+
let result;
|
|
602
|
+
try {
|
|
603
|
+
result = await call(`/workbench/coding-agent/local-runner/evals/${job.id}/artifacts`, form);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
log(`upload of ${name} could not be sent: ${error.message}; will retry`);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
565
608
|
if (result.status_code === 409) return { uploaded, lost: true };
|
|
566
609
|
if (!result.ok) {
|
|
567
610
|
log(`upload of ${name} answered ${result.status_code}; will retry`);
|
|
@@ -629,6 +672,39 @@ export function readProgress(cwd, job) {
|
|
|
629
672
|
* Returns "" when there is nothing wrong, so the caller can tell "fine" from
|
|
630
673
|
* "unreadable".
|
|
631
674
|
*/
|
|
675
|
+
/**
|
|
676
|
+
* What this run alone did, out of counters that belong to the whole invocation.
|
|
677
|
+
*
|
|
678
|
+
* The adapter is opened once and every run in the batch shares it, so its
|
|
679
|
+
* counters are lifetime totals -- which is the honest thing for them to be, and
|
|
680
|
+
* the wrong thing to hand a guard asking about one run. Read as totals they
|
|
681
|
+
* fail a run for the run before it: a batch where the first eval made one tool
|
|
682
|
+
* call and the second legitimately made none reported "the agent made 1 tool
|
|
683
|
+
* call(s) and none of them reached the transcript" against the second, quoting
|
|
684
|
+
* the first one's number. The same arithmetic hides the opposite fault --
|
|
685
|
+
* `reached` never returns to zero after any run reaches the agent, so a later
|
|
686
|
+
* run where every single turn failed cannot be caught at all.
|
|
687
|
+
*
|
|
688
|
+
* Subtracting rather than resetting: `adapterStats()` is the adapter's own
|
|
689
|
+
* lifetime record and other readers rely on it being exactly that. Zeroing
|
|
690
|
+
* shared state from inside one run would also be wrong the day two overlap.
|
|
691
|
+
*
|
|
692
|
+
* `lastFailure` is passed through rather than differenced. It is a string, and
|
|
693
|
+
* the most recent one is the useful one whenever `failures` moved at all.
|
|
694
|
+
*/
|
|
695
|
+
export function statsSince(baseline, stats) {
|
|
696
|
+
if (!stats) return stats;
|
|
697
|
+
if (!baseline) return stats;
|
|
698
|
+
const since = (key) => Math.max(0, Number(stats[key] || 0) - Number(baseline[key] || 0));
|
|
699
|
+
return {
|
|
700
|
+
...stats,
|
|
701
|
+
turns: since("turns"),
|
|
702
|
+
reached: since("reached"),
|
|
703
|
+
failures: since("failures"),
|
|
704
|
+
toolCalls: since("toolCalls"),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
632
708
|
export function missingToolEvidence(dir, stats) {
|
|
633
709
|
const forwarded = Number(stats?.toolCalls);
|
|
634
710
|
if (!Number.isFinite(forwarded) || forwarded <= 0) return "";
|
|
@@ -903,6 +979,10 @@ export async function executeEvalRun(
|
|
|
903
979
|
// Before the harness starts, not after it ends: a run that dies mid-way would
|
|
904
980
|
// otherwise leave its last steps to appear under whichever run came next.
|
|
905
981
|
stepFeed.reset();
|
|
982
|
+
// Same reason, for the counters that cannot be reset because the adapter they
|
|
983
|
+
// belong to is shared with every other run in this batch. See `statsSince`.
|
|
984
|
+
const adapterBaseline = adapterStats();
|
|
985
|
+
const baselineAt = adapterBaseline ? { ...adapterBaseline } : null;
|
|
906
986
|
|
|
907
987
|
const timeout = Math.min(RUN_TIMEOUT_MS, RUN_TIMEOUT_CAP_MS);
|
|
908
988
|
const child = spawn(python.command, [...python.argv, HARNESS_PATH, "run", "--config", laid.configPath], {
|
|
@@ -965,6 +1045,15 @@ export async function executeEvalRun(
|
|
|
965
1045
|
// before it spends an uplink on artifacts nobody will accept.
|
|
966
1046
|
const synced = await syncArtifacts(job, laid.cwd, sent, { call, log, lease });
|
|
967
1047
|
if (synced.lost) return lose("lease rejected an upload");
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
// Nothing this tick reports is worth the run for. It is liveness: the
|
|
1050
|
+
// stages and steps somebody watching sees, and a copy of artifacts the
|
|
1051
|
+
// final sync sends again anyway. Nobody awaits this callback, so an
|
|
1052
|
+
// escaping rejection is an *unhandled* one, and Node's answer to that
|
|
1053
|
+
// is to end the process -- killing a harness mid-stage, skipping the
|
|
1054
|
+
// completion callback, and leaving the run to sit until its lease
|
|
1055
|
+
// lapses. A logged tick and a live run is the better trade.
|
|
1056
|
+
log(`eval ${job.id}: progress tick failed (${error.message}); the run continues`);
|
|
968
1057
|
} finally {
|
|
969
1058
|
ticking = false;
|
|
970
1059
|
}
|
|
@@ -1022,7 +1111,7 @@ export async function executeEvalRun(
|
|
|
1022
1111
|
// Turns are counted rather than failures, because a run where the agent
|
|
1023
1112
|
// answered some of the time is a real measurement of a flaky agent and
|
|
1024
1113
|
// deciding otherwise here would throw away the finding.
|
|
1025
|
-
const stats = adapterStats();
|
|
1114
|
+
const stats = statsSince(baselineAt, adapterStats());
|
|
1026
1115
|
// `reached` rather than `turns`: the adapter counts a turn when it starts
|
|
1027
1116
|
// one, so a run where every turn failed has as many turns as cases.
|
|
1028
1117
|
if (stats && (stats.reached ?? stats.turns) === 0 && stats.failures > 0) {
|
package/bin/runner.js
CHANGED
|
@@ -869,6 +869,19 @@ export async function runnerLoop(
|
|
|
869
869
|
try {
|
|
870
870
|
if (kind === "eval") await runLeasedEval(args, state, job, { log, headless });
|
|
871
871
|
else await executeJob(args, state, job, { log, fullAccess });
|
|
872
|
+
} catch (error) {
|
|
873
|
+
// Attribution, not recovery. Both executors report their own
|
|
874
|
+
// failures and return; a throw escaping one is a bug in it, and
|
|
875
|
+
// letting it reach the stream's catch below labels that bug
|
|
876
|
+
// `stream dropped` -- blaming the network for something that
|
|
877
|
+
// happened on this machine, which is the wrong place to look and
|
|
878
|
+
// the reason the last one took all evening to find.
|
|
879
|
+
//
|
|
880
|
+
// Counted as an attempt below rather than halting the batch. A
|
|
881
|
+
// crash that stopped the loop silently is what the caller then
|
|
882
|
+
// reports as a finished run, which is the failure this is here
|
|
883
|
+
// to stop being invisible.
|
|
884
|
+
log(`${kind} ${job.id} failed unexpectedly: ${error?.stack || error.message}`);
|
|
872
885
|
} finally {
|
|
873
886
|
busy = false;
|
|
874
887
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "premanmcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "PreMan CLI and stdio proxy for PreMan's hosted MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"test": "npm run build && npm run test:proxy",
|
|
16
16
|
"test:proxy": "node --test scripts/smoke-proxy.mjs",
|
|
17
17
|
"test:connect": "node scripts/smoke-connect.mjs",
|
|
18
|
+
"test:connect-heartbeat": "node scripts/smoke-connect-heartbeat.mjs",
|
|
18
19
|
"test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-agent-session.mjs scripts/smoke-agent-dock.mjs scripts/smoke-terminal-lifecycle.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-cli-update.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-eval.mjs scripts/smoke-eval-behaviors.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.mjs scripts/smoke-local-detect.mjs scripts/smoke-agent-target.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs scripts/smoke-shared-errors.mjs scripts/smoke-process-title.mjs scripts/smoke-predict.mjs",
|
|
19
20
|
"test:dmg": "node --test --test-timeout=300000 scripts/smoke-install-desktop-volume.mjs"
|
|
20
21
|
},
|