tines 0.0.315 → 0.0.317
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/README.md +2 -1
- package/dist/index.js +651 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -135,6 +135,7 @@ TINES_API_KEY=tines_… tines runner install --name laptop --harness claude-code
|
|
|
135
135
|
|
|
136
136
|
registers this machine as a local runner and installs the daemon as a launchd/systemd
|
|
137
137
|
service that polls for work and keeps itself updated (`tines runner daemon` with the same
|
|
138
|
-
flags runs it in the foreground instead).
|
|
138
|
+
flags runs it in the foreground instead). `--harness` is `claude-code`, `codex`, `pi` or
|
|
139
|
+
`custom`. See
|
|
139
140
|
[docs/runner-daemon.md](https://github.com/tbuckley/tines/blob/main/docs/runner-daemon.md)
|
|
140
141
|
for the flags, token rotation, and what the service does.
|
package/dist/index.js
CHANGED
|
@@ -5848,6 +5848,7 @@ function isSupportedCodexRolloutVersion(version) {
|
|
|
5848
5848
|
// ../shared/src/effort.ts
|
|
5849
5849
|
var EFFORT_TOKEN_PATTERN = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
5850
5850
|
var EFFORT_CAPABILITIES_MAX_BYTES = 64 * 1024;
|
|
5851
|
+
var EFFORT_CAPABILITIES_MAX_MODELS = 256;
|
|
5851
5852
|
var RECOGNIZED_EFFORT_VALUES = ["low", "medium", "high", "xhigh", "max", "ultra"];
|
|
5852
5853
|
function admitEffort(allowed, value, assertable) {
|
|
5853
5854
|
if (allowed !== null)
|
|
@@ -7917,7 +7918,7 @@ function register3(program3) {
|
|
|
7917
7918
|
const blockedBy = await Promise.all(
|
|
7918
7919
|
(opts.blockedBy ?? []).map(async (ref) => (await resolveIssue(api, ref)).id)
|
|
7919
7920
|
);
|
|
7920
|
-
const
|
|
7921
|
+
const blocks2 = await Promise.all(
|
|
7921
7922
|
(opts.blocks ?? []).map(async (ref) => (await resolveIssue(api, ref)).id)
|
|
7922
7923
|
);
|
|
7923
7924
|
const duplicateOf = opts.duplicateOf ? (await resolveIssue(api, opts.duplicateOf)).id : void 0;
|
|
@@ -7929,7 +7930,7 @@ function register3(program3) {
|
|
|
7929
7930
|
schedule: schedule2,
|
|
7930
7931
|
labels: opts.label,
|
|
7931
7932
|
...blockedBy.length > 0 ? { blocked_by: blockedBy } : {},
|
|
7932
|
-
...
|
|
7933
|
+
...blocks2.length > 0 ? { blocks: blocks2 } : {},
|
|
7933
7934
|
...duplicateOf ? { duplicate_of: duplicateOf } : {}
|
|
7934
7935
|
});
|
|
7935
7936
|
if (opts.json) return printJson(issue);
|
|
@@ -9474,6 +9475,7 @@ import {
|
|
|
9474
9475
|
createWriteStream,
|
|
9475
9476
|
existsSync as existsSync6,
|
|
9476
9477
|
mkdirSync as mkdirSync5,
|
|
9478
|
+
readdirSync as readdirSync3,
|
|
9477
9479
|
readFileSync as readFileSync10,
|
|
9478
9480
|
realpathSync,
|
|
9479
9481
|
rmSync as rmSync4,
|
|
@@ -9482,7 +9484,7 @@ import {
|
|
|
9482
9484
|
writeFileSync as writeFileSync4
|
|
9483
9485
|
} from "node:fs";
|
|
9484
9486
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
9485
|
-
import { dirname as dirname4, join as
|
|
9487
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
9486
9488
|
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
9487
9489
|
|
|
9488
9490
|
// src/version.ts
|
|
@@ -9507,6 +9509,7 @@ function copySummary(value) {
|
|
|
9507
9509
|
return {
|
|
9508
9510
|
...value,
|
|
9509
9511
|
...value.usage ? { usage: { ...value.usage } } : {},
|
|
9512
|
+
...value.harnessOutcome ? { harnessOutcome: { ...value.harnessOutcome } } : {},
|
|
9510
9513
|
...value.pricingEvidence ? {
|
|
9511
9514
|
pricingEvidence: {
|
|
9512
9515
|
...value.pricingEvidence,
|
|
@@ -9851,6 +9854,284 @@ var CodexStreamRenderer = class {
|
|
|
9851
9854
|
}
|
|
9852
9855
|
};
|
|
9853
9856
|
|
|
9857
|
+
// src/daemon/pi-stream.ts
|
|
9858
|
+
function blocks(content) {
|
|
9859
|
+
return Array.isArray(content) ? content : [];
|
|
9860
|
+
}
|
|
9861
|
+
function blockText(content) {
|
|
9862
|
+
return (content ?? []).map((block) => typeof block?.text === "string" ? block.text : "").join("").trim();
|
|
9863
|
+
}
|
|
9864
|
+
function failedStop(message3) {
|
|
9865
|
+
return message3.stopReason === "error" || message3.stopReason === "aborted";
|
|
9866
|
+
}
|
|
9867
|
+
function toolLine(event) {
|
|
9868
|
+
const name2 = event.toolName ?? "tool";
|
|
9869
|
+
const args = event.args && typeof event.args === "object" ? event.args : {};
|
|
9870
|
+
if (name2 === "bash" && typeof args.command === "string") {
|
|
9871
|
+
return `[tool] bash: ${clip(args.command, 300)}`;
|
|
9872
|
+
}
|
|
9873
|
+
if (typeof args.path === "string") return `[tool] ${name2} ${clip(args.path, 300)}`;
|
|
9874
|
+
const rest = event.args === void 0 ? "" : JSON.stringify(event.args);
|
|
9875
|
+
return `[tool] ${name2} ${clip(rest, 300)}`.trimEnd();
|
|
9876
|
+
}
|
|
9877
|
+
function renderPiEvent(event) {
|
|
9878
|
+
switch (event.type) {
|
|
9879
|
+
case "session":
|
|
9880
|
+
return ["[session] started"];
|
|
9881
|
+
case "message_end": {
|
|
9882
|
+
if (event.message?.role !== "assistant") return [];
|
|
9883
|
+
const lines2 = [];
|
|
9884
|
+
for (const block of blocks(event.message.content)) {
|
|
9885
|
+
if (block.type === "text" && block.text?.trim()) {
|
|
9886
|
+
lines2.push(`[agent] ${clip(block.text.trim(), 2e3)}`);
|
|
9887
|
+
}
|
|
9888
|
+
}
|
|
9889
|
+
return lines2;
|
|
9890
|
+
}
|
|
9891
|
+
case "tool_execution_start":
|
|
9892
|
+
return [toolLine(event)];
|
|
9893
|
+
case "tool_execution_end": {
|
|
9894
|
+
const exitCode = event.result?.structuredContent?.exit_code;
|
|
9895
|
+
if (!event.isError && !(typeof exitCode === "number" && exitCode !== 0)) return [];
|
|
9896
|
+
const output = blockText(event.result?.content) || (event.result?.structuredContent?.output ?? "").trim();
|
|
9897
|
+
return [`[tool] ${event.toolName ?? "tool"} failed: ${clip(output, 300)}`];
|
|
9898
|
+
}
|
|
9899
|
+
case "auto_retry_start":
|
|
9900
|
+
return [
|
|
9901
|
+
`[retry] attempt ${event.attempt ?? "?"}/${event.maxAttempts ?? "?"}: ${clip(event.errorMessage ?? "", 300)}`
|
|
9902
|
+
];
|
|
9903
|
+
case "auto_retry_end":
|
|
9904
|
+
if (event.success !== false) return [];
|
|
9905
|
+
return [`[error] ${clip(event.finalError ?? "retries exhausted", 2e3)}`];
|
|
9906
|
+
default:
|
|
9907
|
+
return [];
|
|
9908
|
+
}
|
|
9909
|
+
}
|
|
9910
|
+
function messageChars(message3) {
|
|
9911
|
+
let chars = typeof message3.content === "string" ? message3.content.length : 0;
|
|
9912
|
+
for (const block of blocks(message3.content)) {
|
|
9913
|
+
if (typeof block.text === "string") chars += block.text.length;
|
|
9914
|
+
if (typeof block.thinking === "string") chars += block.thinking.length;
|
|
9915
|
+
if (block.type === "toolCall") {
|
|
9916
|
+
chars += (block.name ?? "").length + JSON.stringify(block.arguments ?? {}).length;
|
|
9917
|
+
}
|
|
9918
|
+
}
|
|
9919
|
+
for (const section of Object.values(message3.sections ?? {})) {
|
|
9920
|
+
if (typeof section === "string") chars += section.length;
|
|
9921
|
+
}
|
|
9922
|
+
if (message3.toolsAdded !== void 0) chars += JSON.stringify(message3.toolsAdded).length;
|
|
9923
|
+
return chars;
|
|
9924
|
+
}
|
|
9925
|
+
function classifyFailure(detail) {
|
|
9926
|
+
if (/^429\b/.test(detail)) return { kind: "rate_limited", detail };
|
|
9927
|
+
if (/^5\d\d\b/.test(detail) || /Connection error|fetch failed|ECONNREFUSED/.test(detail)) {
|
|
9928
|
+
return { kind: "provider_error", detail };
|
|
9929
|
+
}
|
|
9930
|
+
return { kind: "error", detail };
|
|
9931
|
+
}
|
|
9932
|
+
var UNSPOOLED_PI_EVENTS = /* @__PURE__ */ new Set(["message_update", "message_start", "tool_execution_update"]);
|
|
9933
|
+
function spooledPiLine(line) {
|
|
9934
|
+
let type = /^\s*\{"type":"([A-Za-z_]+)"/.exec(line)?.[1];
|
|
9935
|
+
if (type === void 0 && line.trimStart().startsWith("{")) {
|
|
9936
|
+
try {
|
|
9937
|
+
const event = JSON.parse(line);
|
|
9938
|
+
if (typeof event?.type === "string") type = event.type;
|
|
9939
|
+
} catch {
|
|
9940
|
+
return line;
|
|
9941
|
+
}
|
|
9942
|
+
}
|
|
9943
|
+
if (type === void 0) return line;
|
|
9944
|
+
if (UNSPOOLED_PI_EVENTS.has(type)) return null;
|
|
9945
|
+
return type === "agent_end" ? '{"type":"agent_end"}' : line;
|
|
9946
|
+
}
|
|
9947
|
+
var PiRawSpoolFilter = class {
|
|
9948
|
+
pending = "";
|
|
9949
|
+
/** Feeds a chunk in; returns the complete lines to spool, newline-terminated. */
|
|
9950
|
+
write(chunk) {
|
|
9951
|
+
this.pending += chunk;
|
|
9952
|
+
let out = "";
|
|
9953
|
+
let nl = this.pending.indexOf("\n");
|
|
9954
|
+
while (nl !== -1) {
|
|
9955
|
+
const kept = spooledPiLine(this.pending.slice(0, nl));
|
|
9956
|
+
if (kept !== null) out += `${kept}
|
|
9957
|
+
`;
|
|
9958
|
+
this.pending = this.pending.slice(nl + 1);
|
|
9959
|
+
nl = this.pending.indexOf("\n");
|
|
9960
|
+
}
|
|
9961
|
+
return out;
|
|
9962
|
+
}
|
|
9963
|
+
/** The trailing partial line, if the stream ended without a newline. */
|
|
9964
|
+
end() {
|
|
9965
|
+
const last = this.pending;
|
|
9966
|
+
this.pending = "";
|
|
9967
|
+
return last ? spooledPiLine(last) ?? "" : "";
|
|
9968
|
+
}
|
|
9969
|
+
};
|
|
9970
|
+
var PiStreamRenderer = class {
|
|
9971
|
+
constructor(emit, opts = {}) {
|
|
9972
|
+
this.emit = emit;
|
|
9973
|
+
this.opts = opts;
|
|
9974
|
+
}
|
|
9975
|
+
emit;
|
|
9976
|
+
opts;
|
|
9977
|
+
pending = "";
|
|
9978
|
+
finished = false;
|
|
9979
|
+
providerSessionId;
|
|
9980
|
+
numTurns = 0;
|
|
9981
|
+
tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
9982
|
+
cost = 0;
|
|
9983
|
+
/** Assistant messages that ended, and those of them that were real answers. */
|
|
9984
|
+
assistantMessages = 0;
|
|
9985
|
+
answered = 0;
|
|
9986
|
+
lastAssistant = null;
|
|
9987
|
+
lastRetryFailure = null;
|
|
9988
|
+
appliedEffort;
|
|
9989
|
+
/** The estimate's running totals, in characters. */
|
|
9990
|
+
contextChars = 0;
|
|
9991
|
+
estimatedInputChars = 0;
|
|
9992
|
+
estimatedOutputChars = 0;
|
|
9993
|
+
write(chunk) {
|
|
9994
|
+
this.pending += chunk;
|
|
9995
|
+
let nl = this.pending.indexOf("\n");
|
|
9996
|
+
while (nl !== -1) {
|
|
9997
|
+
this.line(this.pending.slice(0, nl));
|
|
9998
|
+
this.pending = this.pending.slice(nl + 1);
|
|
9999
|
+
nl = this.pending.indexOf("\n");
|
|
10000
|
+
}
|
|
10001
|
+
}
|
|
10002
|
+
/**
|
|
10003
|
+
* Renders any trailing partial line, then — when the model server reported
|
|
10004
|
+
* no usage at all — the labelled estimate. Call once the harness has exited.
|
|
10005
|
+
*/
|
|
10006
|
+
finish() {
|
|
10007
|
+
if (this.finished) return;
|
|
10008
|
+
this.finished = true;
|
|
10009
|
+
if (this.pending) {
|
|
10010
|
+
this.line(this.pending);
|
|
10011
|
+
this.pending = "";
|
|
10012
|
+
}
|
|
10013
|
+
if (this.answered > 0 && !this.measured()) {
|
|
10014
|
+
this.emit(
|
|
10015
|
+
`[usage] the model server reported no token usage; rough estimate ~${Math.round(this.estimatedInputChars / 4)} input / ~${Math.round(this.estimatedOutputChars / 4)} output tokens (characters \xF7 4, not recorded)${this.opts.resumed ? "; this run's messages only" : ""}
|
|
10016
|
+
`
|
|
10017
|
+
);
|
|
10018
|
+
}
|
|
10019
|
+
}
|
|
10020
|
+
summary() {
|
|
10021
|
+
const usage2 = this.usage();
|
|
10022
|
+
return copySummary({
|
|
10023
|
+
...this.providerSessionId ? { providerSessionId: this.providerSessionId } : {},
|
|
10024
|
+
...usage2 ? { usage: usage2 } : {},
|
|
10025
|
+
...this.numTurns > 0 ? { numTurns: this.numTurns } : {},
|
|
10026
|
+
harnessOutcome: this.outcome(),
|
|
10027
|
+
...this.appliedEffort ? { appliedEffort: this.appliedEffort } : {}
|
|
10028
|
+
});
|
|
10029
|
+
}
|
|
10030
|
+
measured() {
|
|
10031
|
+
return this.cost > 0 || Object.values(this.tokens).some((value) => value > 0);
|
|
10032
|
+
}
|
|
10033
|
+
/**
|
|
10034
|
+
* Nothing when every sum is zero: a local model server that reports no
|
|
10035
|
+
* usage must not be recorded as a run that used zero tokens. Tokens alone
|
|
10036
|
+
* carry no `cost_source` — `none` beside tokens reads as inconsistent to
|
|
10037
|
+
* usage accounting — and Pi's own cost is passed on only when it has one.
|
|
10038
|
+
*/
|
|
10039
|
+
usage() {
|
|
10040
|
+
if (!this.measured()) return void 0;
|
|
10041
|
+
return {
|
|
10042
|
+
input_tokens: this.tokens.input,
|
|
10043
|
+
output_tokens: this.tokens.output,
|
|
10044
|
+
cache_read_tokens: this.tokens.cacheRead,
|
|
10045
|
+
cache_write_tokens: this.tokens.cacheWrite,
|
|
10046
|
+
...this.cost > 0 ? { cost_usd: this.cost, cost_source: "provider" } : {}
|
|
10047
|
+
};
|
|
10048
|
+
}
|
|
10049
|
+
outcome() {
|
|
10050
|
+
const last = this.lastAssistant;
|
|
10051
|
+
if (!last) return { kind: "error", detail: "pi ended without an assistant message" };
|
|
10052
|
+
if (!failedStop(last)) return { kind: "ok" };
|
|
10053
|
+
return classifyFailure(last.errorMessage || this.lastRetryFailure || last.stopReason);
|
|
10054
|
+
}
|
|
10055
|
+
line(raw) {
|
|
10056
|
+
const trimmed = raw.trim();
|
|
10057
|
+
if (!trimmed) return;
|
|
10058
|
+
if (!trimmed.startsWith("{")) {
|
|
10059
|
+
this.emit(`${raw}
|
|
10060
|
+
`);
|
|
10061
|
+
return;
|
|
10062
|
+
}
|
|
10063
|
+
let event;
|
|
10064
|
+
try {
|
|
10065
|
+
event = JSON.parse(trimmed);
|
|
10066
|
+
} catch {
|
|
10067
|
+
this.emit(`${raw}
|
|
10068
|
+
`);
|
|
10069
|
+
return;
|
|
10070
|
+
}
|
|
10071
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return;
|
|
10072
|
+
for (const line of this.collect(event)) this.emit(`${line}
|
|
10073
|
+
`);
|
|
10074
|
+
for (const line of renderPiEvent(event)) this.emit(`${line}
|
|
10075
|
+
`);
|
|
10076
|
+
}
|
|
10077
|
+
/** Updates the summary; returns the lines only the renderer's state can write. */
|
|
10078
|
+
collect(event) {
|
|
10079
|
+
switch (event.type) {
|
|
10080
|
+
case "session":
|
|
10081
|
+
if (!this.providerSessionId && validProviderSessionId(event.id)) {
|
|
10082
|
+
this.providerSessionId = event.id;
|
|
10083
|
+
}
|
|
10084
|
+
return [];
|
|
10085
|
+
case "turn_end":
|
|
10086
|
+
this.numTurns++;
|
|
10087
|
+
return [];
|
|
10088
|
+
case "auto_retry_end":
|
|
10089
|
+
if (event.success === false) this.lastRetryFailure = event.finalError ?? null;
|
|
10090
|
+
return [];
|
|
10091
|
+
case "message_end":
|
|
10092
|
+
return event.message && typeof event.message === "object" ? this.collectMessage(event.message) : [];
|
|
10093
|
+
default:
|
|
10094
|
+
return [];
|
|
10095
|
+
}
|
|
10096
|
+
}
|
|
10097
|
+
collectMessage(message3) {
|
|
10098
|
+
const chars = messageChars(message3);
|
|
10099
|
+
if (message3.role !== "assistant") {
|
|
10100
|
+
if (message3.role === "toolResult") this.addUsage(message3.usage);
|
|
10101
|
+
this.contextChars += chars;
|
|
10102
|
+
return [];
|
|
10103
|
+
}
|
|
10104
|
+
const lines2 = [];
|
|
10105
|
+
if (this.assistantMessages++ === 0 && message3.model) {
|
|
10106
|
+
const model = message3.provider ? `${message3.provider}/${message3.model}` : message3.model;
|
|
10107
|
+
lines2.push(
|
|
10108
|
+
`[session] model ${model}${message3.thinkingLevel ? ` thinking=${message3.thinkingLevel}` : ""}`
|
|
10109
|
+
);
|
|
10110
|
+
}
|
|
10111
|
+
this.lastAssistant = message3;
|
|
10112
|
+
this.addUsage(message3.usage);
|
|
10113
|
+
if (failedStop(message3)) return lines2;
|
|
10114
|
+
this.answered++;
|
|
10115
|
+
this.estimatedInputChars += this.contextChars;
|
|
10116
|
+
this.estimatedOutputChars += chars;
|
|
10117
|
+
this.contextChars += chars;
|
|
10118
|
+
if (this.appliedEffort === void 0 && typeof message3.thinkingLevel === "string") {
|
|
10119
|
+
this.appliedEffort = message3.thinkingLevel;
|
|
10120
|
+
this.opts.onAppliedEffort?.(message3.thinkingLevel);
|
|
10121
|
+
}
|
|
10122
|
+
return lines2;
|
|
10123
|
+
}
|
|
10124
|
+
addUsage(usage2) {
|
|
10125
|
+
if (!usage2 || typeof usage2 !== "object") return;
|
|
10126
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
|
|
10127
|
+
const value = usage2[key];
|
|
10128
|
+
if (validMetric(value)) this.tokens[key] += value;
|
|
10129
|
+
}
|
|
10130
|
+
const cost = usage2.cost?.total;
|
|
10131
|
+
if (validMetric(cost)) this.cost += cost;
|
|
10132
|
+
}
|
|
10133
|
+
};
|
|
10134
|
+
|
|
9854
10135
|
// src/daemon/codex-rollout.ts
|
|
9855
10136
|
import { open, opendir, realpath, stat } from "node:fs/promises";
|
|
9856
10137
|
import { isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
@@ -10299,15 +10580,24 @@ var MAX_STDOUT = 1024 * 1024;
|
|
|
10299
10580
|
var DEADLINE_MS = 5e3;
|
|
10300
10581
|
var EFFORT_CAPABILITIES_TTL_MS = 10 * 6e4;
|
|
10301
10582
|
var MIN_CLAUDE_EFFORT_VERSION = [2, 1, 258];
|
|
10302
|
-
|
|
10583
|
+
var MIN_PI_VERSION = [0, 99, 2];
|
|
10584
|
+
var PI_VERSION_FLOOR = MIN_PI_VERSION.join(".");
|
|
10585
|
+
var PI_DEADLINE_MS = 2e4;
|
|
10586
|
+
function versionAtLeast(version, floor) {
|
|
10303
10587
|
const parsed = version.match(/(\d+)\.(\d+)\.(\d+)/)?.slice(1).map(Number);
|
|
10304
10588
|
if (!parsed) return false;
|
|
10305
|
-
for (let i = 0; i <
|
|
10306
|
-
if (parsed[i] >
|
|
10307
|
-
if (parsed[i] <
|
|
10589
|
+
for (let i = 0; i < floor.length; i++) {
|
|
10590
|
+
if (parsed[i] > floor[i]) return true;
|
|
10591
|
+
if (parsed[i] < floor[i]) return false;
|
|
10308
10592
|
}
|
|
10309
10593
|
return true;
|
|
10310
10594
|
}
|
|
10595
|
+
function claudeEffortVersionSupported(version) {
|
|
10596
|
+
return versionAtLeast(version, MIN_CLAUDE_EFFORT_VERSION);
|
|
10597
|
+
}
|
|
10598
|
+
function piVersionSupported(version) {
|
|
10599
|
+
return versionAtLeast(version, MIN_PI_VERSION);
|
|
10600
|
+
}
|
|
10311
10601
|
function assignmentEffortRejection(assignment, capabilities, harness) {
|
|
10312
10602
|
if (!assignment.effort) return null;
|
|
10313
10603
|
if (!capabilities || capabilities.version !== 1 || !("models" in capabilities))
|
|
@@ -10473,8 +10763,106 @@ async function discoverCodex(daemonVersion) {
|
|
|
10473
10763
|
);
|
|
10474
10764
|
});
|
|
10475
10765
|
}
|
|
10766
|
+
async function discoverPi(daemonVersion, deadlineMs = PI_DEADLINE_MS) {
|
|
10767
|
+
let harnessVersion;
|
|
10768
|
+
try {
|
|
10769
|
+
harnessVersion = (await run("pi", ["--version"])).trim().slice(0, 100);
|
|
10770
|
+
} catch (error) {
|
|
10771
|
+
return failure("pi", daemonVersion, error instanceof Error ? error.message : String(error));
|
|
10772
|
+
}
|
|
10773
|
+
if (!piVersionSupported(harnessVersion))
|
|
10774
|
+
return failure(
|
|
10775
|
+
"pi",
|
|
10776
|
+
daemonVersion,
|
|
10777
|
+
`installed pi ${harnessVersion || "unknown"} predates the supported ${MIN_PI_VERSION.join(".")}`
|
|
10778
|
+
);
|
|
10779
|
+
return new Promise((resolve3) => {
|
|
10780
|
+
const child = spawn("pi", ["--mode", "rpc", "--no-session"], {
|
|
10781
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
10782
|
+
});
|
|
10783
|
+
let buffer = "";
|
|
10784
|
+
let bytes2 = 0;
|
|
10785
|
+
let settled = false;
|
|
10786
|
+
let requestId = 0;
|
|
10787
|
+
let awaiting = null;
|
|
10788
|
+
const finish = (report) => {
|
|
10789
|
+
if (settled) return;
|
|
10790
|
+
settled = true;
|
|
10791
|
+
clearTimeout(timer);
|
|
10792
|
+
child.kill();
|
|
10793
|
+
resolve3(report);
|
|
10794
|
+
};
|
|
10795
|
+
const fail = (reason) => finish(failure("pi", daemonVersion, reason));
|
|
10796
|
+
const request = (command) => new Promise((deliver) => {
|
|
10797
|
+
const id2 = `tines-${++requestId}`;
|
|
10798
|
+
awaiting = { id: id2, deliver };
|
|
10799
|
+
child.stdin.write(`${JSON.stringify({ id: id2, ...command })}
|
|
10800
|
+
`);
|
|
10801
|
+
});
|
|
10802
|
+
const timer = setTimeout(() => fail("pi model discovery timed out"), deadlineMs);
|
|
10803
|
+
child.on("error", (error) => fail(error.message));
|
|
10804
|
+
child.stdin.on("error", (error) => fail(`pi rpc stdin: ${error.message}`));
|
|
10805
|
+
child.on(
|
|
10806
|
+
"close",
|
|
10807
|
+
(code, signal) => fail(`pi rpc exited (${signal ?? `code ${code}`}) before listing models`)
|
|
10808
|
+
);
|
|
10809
|
+
child.stdout.on("data", (chunk) => {
|
|
10810
|
+
bytes2 += chunk.length;
|
|
10811
|
+
if (bytes2 > MAX_STDOUT) return fail("pi model discovery exceeded 1 MiB");
|
|
10812
|
+
buffer += chunk.toString("utf8");
|
|
10813
|
+
for (; ; ) {
|
|
10814
|
+
const newline = buffer.indexOf("\n");
|
|
10815
|
+
if (newline < 0) break;
|
|
10816
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
10817
|
+
buffer = buffer.slice(newline + 1);
|
|
10818
|
+
let record;
|
|
10819
|
+
try {
|
|
10820
|
+
record = JSON.parse(line);
|
|
10821
|
+
} catch {
|
|
10822
|
+
continue;
|
|
10823
|
+
}
|
|
10824
|
+
if (record?.type !== "response" || !awaiting || record.id !== awaiting.id) continue;
|
|
10825
|
+
const { deliver } = awaiting;
|
|
10826
|
+
awaiting = null;
|
|
10827
|
+
deliver(record);
|
|
10828
|
+
}
|
|
10829
|
+
});
|
|
10830
|
+
void (async () => {
|
|
10831
|
+
const listed = await request({ type: "get_available_models" });
|
|
10832
|
+
if (!listed.success)
|
|
10833
|
+
return fail(`pi get_available_models failed: ${String(listed.error ?? "no reason given")}`);
|
|
10834
|
+
const models = [];
|
|
10835
|
+
const names = /* @__PURE__ */ new Set();
|
|
10836
|
+
for (const entry of Array.isArray(listed.data?.models) ? listed.data.models : []) {
|
|
10837
|
+
if (settled || models.length >= EFFORT_CAPABILITIES_MAX_MODELS) break;
|
|
10838
|
+
if (typeof entry?.provider !== "string" || typeof entry?.id !== "string") continue;
|
|
10839
|
+
const model = `${entry.provider}/${entry.id}`;
|
|
10840
|
+
if (model.length > 200 || names.has(model)) continue;
|
|
10841
|
+
const selected = await request({
|
|
10842
|
+
type: "set_model",
|
|
10843
|
+
provider: entry.provider,
|
|
10844
|
+
modelId: entry.id
|
|
10845
|
+
});
|
|
10846
|
+
if (!selected.success) continue;
|
|
10847
|
+
const thinking = await request({ type: "get_available_thinking_levels" });
|
|
10848
|
+
const levels = thinking.success && Array.isArray(thinking.data?.levels) ? thinking.data.levels : [];
|
|
10849
|
+
names.add(model);
|
|
10850
|
+
models.push({ model, efforts: [...new Set(levels.filter(isRecognizedEffort))] });
|
|
10851
|
+
}
|
|
10852
|
+
finish({
|
|
10853
|
+
version: 1,
|
|
10854
|
+
daemon_version: daemonVersion,
|
|
10855
|
+
harness: "pi",
|
|
10856
|
+
harness_version: harnessVersion,
|
|
10857
|
+
catalog_digest: digest(models),
|
|
10858
|
+
models
|
|
10859
|
+
});
|
|
10860
|
+
})();
|
|
10861
|
+
});
|
|
10862
|
+
}
|
|
10476
10863
|
async function discoverEffortCapabilities(harness, daemonVersion) {
|
|
10477
10864
|
if (harness === "custom") return void 0;
|
|
10865
|
+
if (harness === "pi") return discoverPi(daemonVersion);
|
|
10478
10866
|
return harness === "codex" ? discoverCodex(daemonVersion) : discoverClaude(daemonVersion);
|
|
10479
10867
|
}
|
|
10480
10868
|
var EffortCapabilityRefresher = class {
|
|
@@ -10511,11 +10899,12 @@ var EffortCapabilityRefresher = class {
|
|
|
10511
10899
|
// src/daemon/cli-refresh.ts
|
|
10512
10900
|
import { spawn as spawn2 } from "node:child_process";
|
|
10513
10901
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync8 } from "node:fs";
|
|
10514
|
-
import { dirname as dirname3, join as
|
|
10902
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
10515
10903
|
|
|
10516
10904
|
// src/daemon/support.ts
|
|
10517
|
-
import { delimiter, sep as sep3 } from "node:path";
|
|
10518
|
-
var HARNESS_KINDS = ["claude_code", "codex", "custom"];
|
|
10905
|
+
import { delimiter, join as join5, sep as sep3 } from "node:path";
|
|
10906
|
+
var HARNESS_KINDS = ["claude_code", "codex", "pi", "custom"];
|
|
10907
|
+
var PI_SESSION_DIR = ".pi-sessions";
|
|
10519
10908
|
var KEEP_WORKSPACES_MODES = ["never", "failed", "always"];
|
|
10520
10909
|
function keepWorkspace(mode, outcome) {
|
|
10521
10910
|
return mode === "always" || mode === "failed" && outcome === "failed";
|
|
@@ -10548,6 +10937,14 @@ function buildHarnessInvocation(spec, input2) {
|
|
|
10548
10937
|
input2.prompt
|
|
10549
10938
|
]
|
|
10550
10939
|
};
|
|
10940
|
+
case "pi":
|
|
10941
|
+
return {
|
|
10942
|
+
file: "sh",
|
|
10943
|
+
args: [
|
|
10944
|
+
"-c",
|
|
10945
|
+
`pi --mode json --no-approve --session-dir ${shellQuote(join5(input2.workspace, PI_SESSION_DIR))}${input2.resumeSessionId ? ` --session ${shellQuote(input2.resumeSessionId)}` : ""}${input2.model ? ` --model ${shellQuote(input2.model)}` : ""}${input2.effort ? ` --thinking ${shellQuote(input2.effort)}` : ""}${(input2.skillDirs ?? []).map((dir) => ` --skill ${shellQuote(dir)}`).join("")} < ${shellQuote(input2.promptFile)}`
|
|
10946
|
+
]
|
|
10947
|
+
};
|
|
10551
10948
|
case "custom": {
|
|
10552
10949
|
if (!spec.command) throw new Error("the custom harness needs a --command template");
|
|
10553
10950
|
return { file: "sh", args: ["-c", expandCommandTemplate(spec.command, input2)] };
|
|
@@ -10569,7 +10966,9 @@ function formatLaunchCommand(invocation) {
|
|
|
10569
10966
|
function formatLaunchBanner(invocation, input2, meta) {
|
|
10570
10967
|
const fields = [
|
|
10571
10968
|
`harness=${meta.harness}`,
|
|
10572
|
-
|
|
10969
|
+
// Pi with no model named is not fixed: it runs whatever its own
|
|
10970
|
+
// settings default to, and the stream's `[session] model` line says which.
|
|
10971
|
+
`model=${input2.model ?? (meta.harness === "pi" ? "(pi default)" : "(fixed)")}`,
|
|
10573
10972
|
`effort=${input2.effort ?? "(provider-default)"}`,
|
|
10574
10973
|
`timeout=${meta.timeoutMinutes}m`,
|
|
10575
10974
|
`cli=${meta.cliVersion}`,
|
|
@@ -10599,6 +10998,68 @@ function exitLineForRun(run3, exit) {
|
|
|
10599
10998
|
if (run3.settled) return null;
|
|
10600
10999
|
return formatExitLine({ ...exit, timedOut: run3.timedOut });
|
|
10601
11000
|
}
|
|
11001
|
+
function classifyExit(facts) {
|
|
11002
|
+
const mask = (text4) => redactSecrets(text4, facts.secrets);
|
|
11003
|
+
if (facts.timedOut) {
|
|
11004
|
+
return {
|
|
11005
|
+
status: "failed",
|
|
11006
|
+
error: `run exceeded the ${facts.timeoutMinutes}m timeout; harness killed`
|
|
11007
|
+
};
|
|
11008
|
+
}
|
|
11009
|
+
if (facts.effortMismatch) return { status: "failed", error: mask(facts.effortMismatch) };
|
|
11010
|
+
if (facts.signal) return { status: "failed", error: `harness killed by ${facts.signal}` };
|
|
11011
|
+
const outcome = facts.harness === "pi" ? facts.harnessOutcome : void 0;
|
|
11012
|
+
if (facts.code !== 0) {
|
|
11013
|
+
if (facts.limited) {
|
|
11014
|
+
const detail = mask(facts.limited.detail);
|
|
11015
|
+
return {
|
|
11016
|
+
status: "failed",
|
|
11017
|
+
error: `rate limited: ${detail}`,
|
|
11018
|
+
judgment: "rate_limited",
|
|
11019
|
+
...facts.limited.resumeAt !== null ? { resume_at: facts.limited.resumeAt } : {},
|
|
11020
|
+
note: `harness rate limited (${detail})`
|
|
11021
|
+
};
|
|
11022
|
+
}
|
|
11023
|
+
if (facts.providerError) {
|
|
11024
|
+
const detail = mask(facts.providerError.detail);
|
|
11025
|
+
return {
|
|
11026
|
+
status: "failed",
|
|
11027
|
+
error: `provider error: ${detail}`,
|
|
11028
|
+
judgment: "interrupted",
|
|
11029
|
+
note: `transient provider error (${detail})`
|
|
11030
|
+
};
|
|
11031
|
+
}
|
|
11032
|
+
return (outcome && providerVerdict(outcome, mask)) ?? {
|
|
11033
|
+
status: "failed",
|
|
11034
|
+
error: `harness exited with code ${facts.code}`
|
|
11035
|
+
};
|
|
11036
|
+
}
|
|
11037
|
+
if (outcome && outcome.kind !== "ok") {
|
|
11038
|
+
return providerVerdict(outcome, mask) ?? { status: "failed", error: `pi: ${mask(outcome.detail)}` };
|
|
11039
|
+
}
|
|
11040
|
+
return { status: "completed" };
|
|
11041
|
+
}
|
|
11042
|
+
function providerVerdict(outcome, mask) {
|
|
11043
|
+
if (outcome.kind === "rate_limited") {
|
|
11044
|
+
const detail = mask(outcome.detail);
|
|
11045
|
+
return {
|
|
11046
|
+
status: "failed",
|
|
11047
|
+
error: `rate limited: ${detail}`,
|
|
11048
|
+
judgment: "rate_limited",
|
|
11049
|
+
note: `harness rate limited (${detail})`
|
|
11050
|
+
};
|
|
11051
|
+
}
|
|
11052
|
+
if (outcome.kind === "provider_error") {
|
|
11053
|
+
const detail = mask(outcome.detail);
|
|
11054
|
+
return {
|
|
11055
|
+
status: "failed",
|
|
11056
|
+
error: `provider error: ${detail}`,
|
|
11057
|
+
judgment: "interrupted",
|
|
11058
|
+
note: `transient provider error (${detail})`
|
|
11059
|
+
};
|
|
11060
|
+
}
|
|
11061
|
+
return null;
|
|
11062
|
+
}
|
|
10602
11063
|
var RunTable = class {
|
|
10603
11064
|
constructor(effects, opts = {}) {
|
|
10604
11065
|
this.effects = effects;
|
|
@@ -10913,14 +11374,14 @@ var SecretRedactor = class {
|
|
|
10913
11374
|
var PACKAGE = "tines";
|
|
10914
11375
|
var NPM_ARGS = ["--min-release-age=0", "--no-audit", "--no-fund", "--loglevel=error"];
|
|
10915
11376
|
function agentCliPrefix(configDir) {
|
|
10916
|
-
return
|
|
11377
|
+
return join6(configDir, "cli");
|
|
10917
11378
|
}
|
|
10918
11379
|
function binDirOf(prefix) {
|
|
10919
|
-
return
|
|
11380
|
+
return join6(prefix, "node_modules", ".bin");
|
|
10920
11381
|
}
|
|
10921
11382
|
function installedVersion(prefix) {
|
|
10922
11383
|
try {
|
|
10923
|
-
const pkg = readFileSync8(
|
|
11384
|
+
const pkg = readFileSync8(join6(prefix, "node_modules", PACKAGE, "package.json"), "utf8");
|
|
10924
11385
|
const version = JSON.parse(pkg).version;
|
|
10925
11386
|
return typeof version === "string" ? version : null;
|
|
10926
11387
|
} catch {
|
|
@@ -10929,7 +11390,7 @@ function installedVersion(prefix) {
|
|
|
10929
11390
|
}
|
|
10930
11391
|
function lastGood(prefix) {
|
|
10931
11392
|
const binDir = binDirOf(prefix);
|
|
10932
|
-
if (!existsSync5(
|
|
11393
|
+
if (!existsSync5(join6(binDir, PACKAGE))) return AMBIENT_CLI;
|
|
10933
11394
|
return { binDir, version: installedVersion(prefix), source: "stale" };
|
|
10934
11395
|
}
|
|
10935
11396
|
function runNpm(file, args, cwd, timeoutMs) {
|
|
@@ -10974,7 +11435,7 @@ async function installAgentCli(opts) {
|
|
|
10974
11435
|
const args = ["install", "--prefix", prefix, `${PACKAGE}@latest`, ...NPM_ARGS];
|
|
10975
11436
|
let result = await runNpm("npm", args, prefix, timeoutMs);
|
|
10976
11437
|
if (result.spawnError?.code === "ENOENT") {
|
|
10977
|
-
const sibling =
|
|
11438
|
+
const sibling = join6(dirname3(process.execPath), "npm");
|
|
10978
11439
|
if (existsSync5(sibling)) result = await runNpm(sibling, args, prefix, timeoutMs);
|
|
10979
11440
|
}
|
|
10980
11441
|
if (result.code !== 0) {
|
|
@@ -11001,12 +11462,12 @@ import { arch, hostname, platform } from "node:os";
|
|
|
11001
11462
|
|
|
11002
11463
|
// src/daemon/store.ts
|
|
11003
11464
|
import { readdirSync as readdirSync2, readFileSync as readFileSync9, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
|
|
11004
|
-
import { join as
|
|
11465
|
+
import { join as join7 } from "node:path";
|
|
11005
11466
|
function credentialsKey(url, name2) {
|
|
11006
11467
|
return `${url.replace(/\/+$/, "")}#${name2}`;
|
|
11007
11468
|
}
|
|
11008
11469
|
function credentialsPath(dir) {
|
|
11009
|
-
return
|
|
11470
|
+
return join7(dir, "runners.json");
|
|
11010
11471
|
}
|
|
11011
11472
|
function loadRunnerCredentials(dir, url, name2) {
|
|
11012
11473
|
const all = readJsonFile(credentialsPath(dir));
|
|
@@ -11029,7 +11490,7 @@ function clearRunnerCredentials(dir, url, name2) {
|
|
|
11029
11490
|
writeJsonFile(path2, all, { secret: true });
|
|
11030
11491
|
}
|
|
11031
11492
|
function daemonStatePath(dir, runnerId) {
|
|
11032
|
-
return
|
|
11493
|
+
return join7(dir, `daemon-state-${runnerId}.json`);
|
|
11033
11494
|
}
|
|
11034
11495
|
function loadDaemonState(path2) {
|
|
11035
11496
|
const state = readJsonFile(path2);
|
|
@@ -11042,7 +11503,7 @@ function saveDaemonState(path2, runs) {
|
|
|
11042
11503
|
writeJsonFile(path2, { runs });
|
|
11043
11504
|
}
|
|
11044
11505
|
function daemonDeclinesPath(dir, runnerId) {
|
|
11045
|
-
return
|
|
11506
|
+
return join7(dir, `daemon-declines-${runnerId}.json`);
|
|
11046
11507
|
}
|
|
11047
11508
|
function loadDaemonDeclines(path2) {
|
|
11048
11509
|
const state = readJsonFile(path2);
|
|
@@ -11072,10 +11533,10 @@ function processStartTimeMs(pid) {
|
|
|
11072
11533
|
}
|
|
11073
11534
|
}
|
|
11074
11535
|
function workspacesDir(configDir) {
|
|
11075
|
-
return
|
|
11536
|
+
return join7(configDir, "workspaces");
|
|
11076
11537
|
}
|
|
11077
11538
|
function keptMarkerPath(workspace) {
|
|
11078
|
-
return
|
|
11539
|
+
return join7(workspace, "kept.json");
|
|
11079
11540
|
}
|
|
11080
11541
|
function writeKeptMarker(workspace, marker) {
|
|
11081
11542
|
writeJsonFile(keptMarkerPath(workspace), marker);
|
|
@@ -11097,7 +11558,7 @@ function listKeptWorkspaces(configDir) {
|
|
|
11097
11558
|
}
|
|
11098
11559
|
const kept = [];
|
|
11099
11560
|
for (const name2 of names) {
|
|
11100
|
-
const path2 =
|
|
11561
|
+
const path2 = join7(root, name2);
|
|
11101
11562
|
const marker = readKeptMarker(path2);
|
|
11102
11563
|
if (marker) kept.push({ ...marker, path: path2 });
|
|
11103
11564
|
}
|
|
@@ -11134,7 +11595,7 @@ function directorySizeBytes(path2) {
|
|
|
11134
11595
|
return 0;
|
|
11135
11596
|
}
|
|
11136
11597
|
for (const entry of entries) {
|
|
11137
|
-
const child =
|
|
11598
|
+
const child = join7(path2, entry.name);
|
|
11138
11599
|
if (entry.isDirectory()) total += directorySizeBytes(child);
|
|
11139
11600
|
else if (entry.isFile()) {
|
|
11140
11601
|
try {
|
|
@@ -11213,6 +11674,15 @@ async function uploadRawLog(run3) {
|
|
|
11213
11674
|
}
|
|
11214
11675
|
var log = (message3) => console.log(`[${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)}] ${message3}`);
|
|
11215
11676
|
var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
11677
|
+
function piSessionExists(workspace, sessionId) {
|
|
11678
|
+
try {
|
|
11679
|
+
return readdirSync3(join8(workspace, PI_SESSION_DIR)).some(
|
|
11680
|
+
(name2) => name2.endsWith(`_${sessionId}.jsonl`)
|
|
11681
|
+
);
|
|
11682
|
+
} catch {
|
|
11683
|
+
return false;
|
|
11684
|
+
}
|
|
11685
|
+
}
|
|
11216
11686
|
function killTree(pid, signal) {
|
|
11217
11687
|
try {
|
|
11218
11688
|
process.kill(-pid, signal);
|
|
@@ -11298,7 +11768,7 @@ async function runDaemon(opts) {
|
|
|
11298
11768
|
const selfUpdate = opts.selfUpdate && opts.cliRefresh && managedInstall;
|
|
11299
11769
|
if (opts.selfUpdate && opts.cliRefresh) {
|
|
11300
11770
|
log(
|
|
11301
|
-
managedInstall ? `self-update: on \u2014 this daemon (tines ${DAEMON_VERSION}) runs from ${prefix} and will restart once idle after a newer release is installed there` : `self-update: off \u2014 this daemon (tines ${DAEMON_VERSION}) runs from ${selfPath ?? process.argv[1] ?? "?"}, not ${prefix}; launch it from ${
|
|
11771
|
+
managedInstall ? `self-update: on \u2014 this daemon (tines ${DAEMON_VERSION}) runs from ${prefix} and will restart once idle after a newer release is installed there` : `self-update: off \u2014 this daemon (tines ${DAEMON_VERSION}) runs from ${selfPath ?? process.argv[1] ?? "?"}, not ${prefix}; launch it from ${join8(prefix, "node_modules", ".bin", "tines")} to have refreshes apply on restart \u2014 \`tines runner install --name ${opts.name} --harness ${opts.harness.replaceAll("_", "-")}\` sets that up as a service (docs/runner-daemon.md)`
|
|
11302
11772
|
);
|
|
11303
11773
|
}
|
|
11304
11774
|
let pendingUpdate = null;
|
|
@@ -11471,7 +11941,7 @@ async function runDaemon(opts) {
|
|
|
11471
11941
|
`run ${runId}: resume workspace ${assignment.resume.workspace_path} is gone; launching fresh`
|
|
11472
11942
|
);
|
|
11473
11943
|
}
|
|
11474
|
-
const workspace = resume ? resume.workspace_path :
|
|
11944
|
+
const workspace = resume ? resume.workspace_path : join8(workspacesDir(opts.configDir), runId);
|
|
11475
11945
|
const issueLabel = assignment.run.issue_ref ? `${assignment.run.issue_ref.project_name}/${assignment.run.issue_ref.number}` : void 0;
|
|
11476
11946
|
const run3 = {
|
|
11477
11947
|
runId,
|
|
@@ -11516,7 +11986,7 @@ async function runDaemon(opts) {
|
|
|
11516
11986
|
const currentRepoDirs = assignment.bundle.repos.map((repo) => repo.dir);
|
|
11517
11987
|
let retainedRepoDirs = [];
|
|
11518
11988
|
if (resume) {
|
|
11519
|
-
const retainedPath =
|
|
11989
|
+
const retainedPath = join8(workspace, "repos.json");
|
|
11520
11990
|
let retained;
|
|
11521
11991
|
try {
|
|
11522
11992
|
retained = JSON.parse(readFileSync10(retainedPath, "utf8"));
|
|
@@ -11539,14 +12009,14 @@ async function runDaemon(opts) {
|
|
|
11539
12009
|
rmSync4(workspace, { recursive: true, force: true });
|
|
11540
12010
|
mkdirSync5(workspace, { recursive: true });
|
|
11541
12011
|
} else {
|
|
11542
|
-
rmSync4(
|
|
12012
|
+
rmSync4(join8(workspace, "kept.json"), { force: true });
|
|
11543
12013
|
}
|
|
11544
12014
|
materializeSkills(workspace, assignment.bundle.skills);
|
|
11545
|
-
writeFileSync4(
|
|
12015
|
+
writeFileSync4(join8(workspace, "prompt.md"), `${assignment.prompt}
|
|
11546
12016
|
`);
|
|
11547
12017
|
if (!resume)
|
|
11548
12018
|
writeFileSync4(
|
|
11549
|
-
|
|
12019
|
+
join8(workspace, "repos.json"),
|
|
11550
12020
|
`${JSON.stringify(assignment.bundle.repos, null, 2)}
|
|
11551
12021
|
`
|
|
11552
12022
|
);
|
|
@@ -11578,13 +12048,24 @@ async function runDaemon(opts) {
|
|
|
11578
12048
|
` : `warning: no daemon-managed tines CLI; using whatever \`tines\` is on this machine's PATH
|
|
11579
12049
|
`
|
|
11580
12050
|
);
|
|
12051
|
+
let resumeSessionId = resume?.provider_session_id;
|
|
12052
|
+
if (opts.harness === "pi" && resumeSessionId && !piSessionExists(workspace, resumeSessionId)) {
|
|
12053
|
+
log(`run ${runId}: pi session ${resumeSessionId} is gone; launching fresh`);
|
|
12054
|
+
run3.batcher.append(`pi session ${resumeSessionId} is gone; launching fresh
|
|
12055
|
+
`);
|
|
12056
|
+
resumeSessionId = void 0;
|
|
12057
|
+
run3.priorTurns = 0;
|
|
12058
|
+
}
|
|
11581
12059
|
const harnessInput = {
|
|
11582
12060
|
workspace,
|
|
11583
|
-
promptFile:
|
|
12061
|
+
promptFile: join8(workspace, "prompt.md"),
|
|
11584
12062
|
prompt: assignment.prompt,
|
|
11585
12063
|
model: assignment.run.model,
|
|
11586
12064
|
effort: assignment.effort?.value ?? null,
|
|
11587
|
-
...
|
|
12065
|
+
...resumeSessionId ? { resumeSessionId } : {},
|
|
12066
|
+
// Pi does not load `.agents/skills` in a folder it has not been told
|
|
12067
|
+
// to trust, so each materialized skill is named on the command line.
|
|
12068
|
+
...opts.harness === "pi" ? { skillDirs: assignment.bundle.skills.map((skill) => `.agents/skills/${skill.name}`) } : {}
|
|
11588
12069
|
};
|
|
11589
12070
|
const invocation = buildHarnessInvocation(
|
|
11590
12071
|
{ harness: opts.harness, command: opts.command },
|
|
@@ -11646,7 +12127,7 @@ async function runDaemon(opts) {
|
|
|
11646
12127
|
(event) => limiter.noteStreamEvent(event)
|
|
11647
12128
|
);
|
|
11648
12129
|
run3.renderer = renderer;
|
|
11649
|
-
const spoolPath =
|
|
12130
|
+
const spoolPath = join8(opts.configDir, "rawlogs", `${runId}.ndjson`);
|
|
11650
12131
|
mkdirSync5(dirname4(spoolPath), { recursive: true });
|
|
11651
12132
|
run3.rawSpoolPath = spoolPath;
|
|
11652
12133
|
run3.rawSpool = createWriteStream(spoolPath);
|
|
@@ -11667,6 +12148,63 @@ async function runDaemon(opts) {
|
|
|
11667
12148
|
run3.rawSpool?.write(rawRedactor.write(text4));
|
|
11668
12149
|
renderer.write(text4);
|
|
11669
12150
|
});
|
|
12151
|
+
} else if (opts.harness === "pi") {
|
|
12152
|
+
const renderer = new PiStreamRenderer((line) => run3.batcher.append(line), {
|
|
12153
|
+
resumed: Boolean(resumeSessionId),
|
|
12154
|
+
// Pi clamps `--thinking` to what the model supports without
|
|
12155
|
+
// saying so; the level its first answer records is the proof.
|
|
12156
|
+
// A match confirms an enforced effort. Anything else is a run
|
|
12157
|
+
// at an effort nobody assigned, so it stops here.
|
|
12158
|
+
onAppliedEffort: (level) => {
|
|
12159
|
+
const assigned = assignment.effort?.value;
|
|
12160
|
+
if (!assigned || run3.settled) return;
|
|
12161
|
+
if (level === assigned) {
|
|
12162
|
+
run3.effortEvidence = {
|
|
12163
|
+
status: "confirmed",
|
|
12164
|
+
attempted_effort: assigned,
|
|
12165
|
+
transport: "argv"
|
|
12166
|
+
};
|
|
12167
|
+
} else {
|
|
12168
|
+
const reason = `pi applied thinking level ${level.slice(0, 100)}, not the assigned ${assigned}`;
|
|
12169
|
+
run3.effortEvidence = {
|
|
12170
|
+
status: "rejected",
|
|
12171
|
+
attempted_effort: assigned,
|
|
12172
|
+
transport: "argv",
|
|
12173
|
+
reason,
|
|
12174
|
+
observed_effort: level.slice(0, 100)
|
|
12175
|
+
};
|
|
12176
|
+
run3.effortMismatch = reason;
|
|
12177
|
+
log(`run ${runId}: ${reason}; killing`);
|
|
12178
|
+
run3.batcher.append(`[effort] ${reason}; stopping the run
|
|
12179
|
+
`);
|
|
12180
|
+
if (child.pid) killTree(child.pid, "SIGTERM");
|
|
12181
|
+
if (child.pid) setTimeout(() => killTree(child.pid, "SIGKILL"), 5e3).unref?.();
|
|
12182
|
+
}
|
|
12183
|
+
void client2.appendRunLog(runId, { chunk: "", effort_application: run3.effortEvidence }).catch((err) => log(`run ${runId}: effort evidence rejected: ${message2(err)}`));
|
|
12184
|
+
}
|
|
12185
|
+
});
|
|
12186
|
+
run3.renderer = renderer;
|
|
12187
|
+
const spoolPath = join8(opts.configDir, "rawlogs", `${runId}.ndjson`);
|
|
12188
|
+
mkdirSync5(dirname4(spoolPath), { recursive: true });
|
|
12189
|
+
run3.rawSpoolPath = spoolPath;
|
|
12190
|
+
run3.rawSpool = createWriteStream(spoolPath);
|
|
12191
|
+
run3.rawUpload = (body) => client2.putRunLogRaw(runId, body);
|
|
12192
|
+
const decoder = new StringDecoder2("utf8");
|
|
12193
|
+
const spoolFilter = new PiRawSpoolFilter();
|
|
12194
|
+
const rawRedactor = new SecretRedactor(secretEnvValues);
|
|
12195
|
+
run3.drain = () => {
|
|
12196
|
+
const trailing = decoder.end();
|
|
12197
|
+
run3.rawSpool?.write(
|
|
12198
|
+
rawRedactor.write(spoolFilter.write(trailing) + spoolFilter.end()) + rawRedactor.end()
|
|
12199
|
+
);
|
|
12200
|
+
if (trailing) renderer.write(trailing);
|
|
12201
|
+
renderer.finish();
|
|
12202
|
+
};
|
|
12203
|
+
child.stdout?.on("data", (data) => {
|
|
12204
|
+
const text4 = decoder.write(data);
|
|
12205
|
+
run3.rawSpool?.write(rawRedactor.write(spoolFilter.write(text4)));
|
|
12206
|
+
renderer.write(text4);
|
|
12207
|
+
});
|
|
11670
12208
|
} else if (opts.harness === "codex") {
|
|
11671
12209
|
const renderer = new CodexStreamRenderer((line) => run3.batcher.append(line));
|
|
11672
12210
|
const decoder = new StringDecoder2("utf8");
|
|
@@ -11731,38 +12269,21 @@ async function runDaemon(opts) {
|
|
|
11731
12269
|
durationMs: Date.now() - (run3.spawnedAt ?? Date.now())
|
|
11732
12270
|
});
|
|
11733
12271
|
if (closing) run3.batcher.append(closing);
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
judgment: "rate_limited",
|
|
11750
|
-
...limited.resumeAt !== null ? { resume_at: limited.resumeAt } : {}
|
|
11751
|
-
});
|
|
11752
|
-
} else if (providerError) {
|
|
11753
|
-
const detail = redactSecrets(providerError.detail, secretEnvValues);
|
|
11754
|
-
log(`run ${runId}: transient provider error (${detail}); reporting without a strike`);
|
|
11755
|
-
void table2.finishAndCleanup(run3, "failed", `provider error: ${detail}`, {
|
|
11756
|
-
judgment: "interrupted"
|
|
11757
|
-
});
|
|
11758
|
-
} else {
|
|
11759
|
-
void table2.finishAndCleanup(
|
|
11760
|
-
run3,
|
|
11761
|
-
"failed",
|
|
11762
|
-
signal ? `harness killed by ${signal}` : `harness exited with code ${code}`
|
|
11763
|
-
);
|
|
11764
|
-
}
|
|
11765
|
-
}
|
|
12272
|
+
const { status, error, note, ...judgment } = classifyExit({
|
|
12273
|
+
harness: opts.harness,
|
|
12274
|
+
code,
|
|
12275
|
+
signal,
|
|
12276
|
+
timedOut: run3.timedOut,
|
|
12277
|
+
timeoutMinutes: assignment.timeout_minutes,
|
|
12278
|
+
limited: run3.limiter?.signal() ?? null,
|
|
12279
|
+
providerError: run3.limiter?.providerError() ?? null,
|
|
12280
|
+
// pi: its stream, not its exit code, says how the run ended.
|
|
12281
|
+
...run3.renderer ? { harnessOutcome: run3.renderer.summary().harnessOutcome } : {},
|
|
12282
|
+
effortMismatch: run3.effortMismatch ?? null,
|
|
12283
|
+
secrets: secretEnvValues
|
|
12284
|
+
});
|
|
12285
|
+
if (note) log(`run ${runId}: ${note}; reporting without a strike`);
|
|
12286
|
+
void table2.finishAndCleanup(run3, status, error, judgment.judgment ? judgment : void 0);
|
|
11766
12287
|
});
|
|
11767
12288
|
} catch (err) {
|
|
11768
12289
|
void table2.finishAndCleanup(
|
|
@@ -11929,7 +12450,7 @@ function withDaemonFlags(cmd) {
|
|
|
11929
12450
|
return cmd.option(
|
|
11930
12451
|
"--name <name>",
|
|
11931
12452
|
"runner name, unique per user; name it machine-plus-harness, e.g. macbook-claude (default: this hostname)"
|
|
11932
|
-
).option("--harness <harness>", "claude-code | codex | custom", "claude-code").option(
|
|
12453
|
+
).option("--harness <harness>", "claude-code | codex | pi | custom", "claude-code").option(
|
|
11933
12454
|
"--command <template>",
|
|
11934
12455
|
"custom harness command template ({prompt_file}, {workspace}, {model}, {effort})"
|
|
11935
12456
|
).option(
|
|
@@ -11959,7 +12480,7 @@ function withDaemonFlags(cmd) {
|
|
|
11959
12480
|
function parseDaemonFlags(opts) {
|
|
11960
12481
|
const harness = opts.harness.replaceAll("-", "_");
|
|
11961
12482
|
if (!HARNESS_KINDS.includes(harness)) {
|
|
11962
|
-
die(`--harness must be claude-code, codex, or custom, got "${opts.harness}"`);
|
|
12483
|
+
die(`--harness must be claude-code, codex, pi, or custom, got "${opts.harness}"`);
|
|
11963
12484
|
}
|
|
11964
12485
|
if (harness === "custom" && !opts.command) {
|
|
11965
12486
|
die(
|
|
@@ -12010,7 +12531,7 @@ import { promisify } from "node:util";
|
|
|
12010
12531
|
|
|
12011
12532
|
// src/daemon/service.ts
|
|
12012
12533
|
import { existsSync as existsSync7, statSync as statSync5 } from "node:fs";
|
|
12013
|
-
import { delimiter as delimiter2, dirname as dirname5, join as
|
|
12534
|
+
import { delimiter as delimiter2, dirname as dirname5, join as join9, sep as sep4 } from "node:path";
|
|
12014
12535
|
function defaultServiceManager(platform2 = process.platform) {
|
|
12015
12536
|
if (platform2 === "darwin") return "launchd";
|
|
12016
12537
|
if (platform2 === "linux") return "systemd";
|
|
@@ -12021,13 +12542,13 @@ function serviceLabel(name2, kind) {
|
|
|
12021
12542
|
}
|
|
12022
12543
|
function unitPath(name2, kind, home) {
|
|
12023
12544
|
const label = serviceLabel(name2, kind);
|
|
12024
|
-
return kind === "launchd" ?
|
|
12545
|
+
return kind === "launchd" ? join9(home, "Library", "LaunchAgents", `${label}.plist`) : join9(home, ".config", "systemd", "user", `${label}.service`);
|
|
12025
12546
|
}
|
|
12026
12547
|
function serviceLogPath(configDir, name2) {
|
|
12027
|
-
return
|
|
12548
|
+
return join9(configDir, "logs", `runner-${name2}.log`);
|
|
12028
12549
|
}
|
|
12029
12550
|
function managedBinary(prefix) {
|
|
12030
|
-
return
|
|
12551
|
+
return join9(prefix, "node_modules", ".bin", "tines");
|
|
12031
12552
|
}
|
|
12032
12553
|
function daemonArgs(input2) {
|
|
12033
12554
|
const args = [
|
|
@@ -12056,7 +12577,7 @@ function daemonArgs(input2) {
|
|
|
12056
12577
|
function findOnPath(name2, envPath) {
|
|
12057
12578
|
for (const dir of (envPath ?? "").split(delimiter2)) {
|
|
12058
12579
|
if (!dir) continue;
|
|
12059
|
-
const candidate =
|
|
12580
|
+
const candidate = join9(dir, name2);
|
|
12060
12581
|
try {
|
|
12061
12582
|
if (statSync5(candidate).isFile()) return candidate;
|
|
12062
12583
|
} catch {
|
|
@@ -12077,6 +12598,11 @@ function servicePath(input2) {
|
|
|
12077
12598
|
);
|
|
12078
12599
|
return { path: [...new Set(dirs)].join(delimiter2), missing };
|
|
12079
12600
|
}
|
|
12601
|
+
function listedPiModels(stdout) {
|
|
12602
|
+
const lines2 = stdout.split("\n").filter((line) => line.trim() !== "");
|
|
12603
|
+
if (lines2.length === 0 || !/^provider\s+model\b/.test(lines2[0])) return 0;
|
|
12604
|
+
return lines2.length - 1;
|
|
12605
|
+
}
|
|
12080
12606
|
function xml(value) {
|
|
12081
12607
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
12082
12608
|
}
|
|
@@ -12205,6 +12731,12 @@ function managedBinaryPresent(prefix) {
|
|
|
12205
12731
|
var run2 = promisify(execFile2);
|
|
12206
12732
|
var STARTUP_TIMEOUT_MS = 3e4;
|
|
12207
12733
|
var log2 = (message3) => console.log(message3);
|
|
12734
|
+
var HARNESS_BINARIES = {
|
|
12735
|
+
claude_code: "claude",
|
|
12736
|
+
codex: "codex",
|
|
12737
|
+
pi: "pi",
|
|
12738
|
+
custom: null
|
|
12739
|
+
};
|
|
12208
12740
|
function resolveServiceManager(flag) {
|
|
12209
12741
|
if (flag !== void 0) {
|
|
12210
12742
|
if (flag !== "launchd" && flag !== "systemd")
|
|
@@ -12257,6 +12789,44 @@ async function waitForStart(logPath, offset, name2) {
|
|
|
12257
12789
|
}
|
|
12258
12790
|
return false;
|
|
12259
12791
|
}
|
|
12792
|
+
async function requirePiVersion(path2) {
|
|
12793
|
+
const pi = findOnPath("pi", path2);
|
|
12794
|
+
if (!pi) return false;
|
|
12795
|
+
let version;
|
|
12796
|
+
try {
|
|
12797
|
+
version = (await run2(pi, ["--version"], { timeout: 1e4 })).stdout.trim();
|
|
12798
|
+
} catch (err) {
|
|
12799
|
+
die(`\`${pi} --version\` failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
12800
|
+
}
|
|
12801
|
+
if (!piVersionSupported(version)) {
|
|
12802
|
+
die(
|
|
12803
|
+
`${pi} is pi ${version || "(unknown version)"}, older than ${PI_VERSION_FLOOR}, the oldest the pi harness supports \u2014 update it (\`npm install -g @earendil-works/pi-coding-agent\`), then install again`
|
|
12804
|
+
);
|
|
12805
|
+
}
|
|
12806
|
+
return true;
|
|
12807
|
+
}
|
|
12808
|
+
async function warnIfPiListsNoModels(path2) {
|
|
12809
|
+
let count;
|
|
12810
|
+
try {
|
|
12811
|
+
const { stdout } = await run2("pi", ["--list-models"], {
|
|
12812
|
+
env: { PATH: path2, HOME: homedir3() },
|
|
12813
|
+
timeout: 3e4
|
|
12814
|
+
});
|
|
12815
|
+
count = listedPiModels(stdout);
|
|
12816
|
+
} catch (err) {
|
|
12817
|
+
log2(
|
|
12818
|
+
`warning: could not run \`pi --list-models\` with the service's PATH (${err instanceof Error ? err.message.split("\n")[0] : String(err)}); check the model list yourself before routing work here`
|
|
12819
|
+
);
|
|
12820
|
+
return;
|
|
12821
|
+
}
|
|
12822
|
+
if (count > 0) {
|
|
12823
|
+
log2(`pi lists ${count} model${count === 1 ? "" : "s"} in the service's environment`);
|
|
12824
|
+
return;
|
|
12825
|
+
}
|
|
12826
|
+
log2(
|
|
12827
|
+
'warning: `pi --list-models` lists no models in the service\'s environment. The service gets only PATH and HOME, not this shell\'s variables, so model credentials that depend on one (an API key exported in your shell profile) are invisible to it and every run would fail. Put the credential where pi reads it without the shell (`~/.pi/agent/auth.json`, or a literal key in `~/.pi/agent/models.json`), then `tines runner restart`. See docs/runner-daemon.md, "Pi" \u2192 "Model credentials".'
|
|
12828
|
+
);
|
|
12829
|
+
}
|
|
12260
12830
|
function serviceTarget(name2, kind) {
|
|
12261
12831
|
return { kind, name: name2, unitPath: unitPath(name2, kind, homedir3()), uid: userInfo().uid };
|
|
12262
12832
|
}
|
|
@@ -12292,6 +12862,14 @@ ${lines2.join("\n")}
|
|
|
12292
12862
|
stop it first \u2014 \`tines runners pause ${settings.name}\`, wait for 0 active runs, kill that process \u2014 then run install again; it reconnects with the stored token, so no key is needed`
|
|
12293
12863
|
);
|
|
12294
12864
|
}
|
|
12865
|
+
const harnessBinary = HARNESS_BINARIES[settings.harness];
|
|
12866
|
+
const { path: path2, missing } = servicePath({
|
|
12867
|
+
execPath: process.execPath,
|
|
12868
|
+
required: [...harnessBinary ? [harnessBinary] : [], "git"],
|
|
12869
|
+
envPath: process.env.PATH,
|
|
12870
|
+
platform: process.platform
|
|
12871
|
+
});
|
|
12872
|
+
const piPresent = settings.harness === "pi" && await requirePiVersion(path2);
|
|
12295
12873
|
if (!managedBinaryPresent(prefix)) {
|
|
12296
12874
|
log2(`installing the daemon-managed tines into ${prefix} \u2026`);
|
|
12297
12875
|
await installAgentCli({ configDir, log: log2 });
|
|
@@ -12317,18 +12895,12 @@ stop it first \u2014 \`tines runners pause ${settings.name}\`, wait for 0 active
|
|
|
12317
12895
|
} catch (err) {
|
|
12318
12896
|
die(err instanceof Error ? err.message : String(err));
|
|
12319
12897
|
}
|
|
12320
|
-
const harnessBinary = settings.harness === "claude_code" ? "claude" : settings.harness === "codex" ? "codex" : null;
|
|
12321
|
-
const { path: path2, missing } = servicePath({
|
|
12322
|
-
execPath: process.execPath,
|
|
12323
|
-
required: [...harnessBinary ? [harnessBinary] : [], "git"],
|
|
12324
|
-
envPath: process.env.PATH,
|
|
12325
|
-
platform: process.platform
|
|
12326
|
-
});
|
|
12327
12898
|
for (const name2 of missing) {
|
|
12328
12899
|
log2(
|
|
12329
12900
|
`warning: \`${name2}\` is not on this shell's PATH, so it is not on the service's either \u2014 install it, then \`tines runner install\` again`
|
|
12330
12901
|
);
|
|
12331
12902
|
}
|
|
12903
|
+
if (piPresent) await warnIfPiListsNoModels(path2);
|
|
12332
12904
|
const target = serviceTarget(settings.name, kind);
|
|
12333
12905
|
const logPath = serviceLogPath(configDir, settings.name);
|
|
12334
12906
|
const spec = {
|