linersh 1.1.2 → 1.2.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/README.md +12 -58
- package/bin/liner.js +62 -14
- package/package.json +10 -17
- package/cli-update-docs/README.md +0 -129
- package/cli-update-docs/SKILL.md +0 -294
- package/cli-update-docs/curator-notes.md +0 -85
- package/cli-update-docs/quality-check-tests.md +0 -435
- package/cli-update-docs/source-finding-tactics.md +0 -224
- package/cli-update-docs/source-quality-hierarchy.md +0 -209
- package/cli-update-docs/synthesis-guidance.md +0 -151
- package/dist/agents/artifact-validation.js +0 -567
- package/dist/agents/artifact-validation.js.map +0 -1
- package/dist/agents/candidate-longlist.js +0 -116
- package/dist/agents/candidate-longlist.js.map +0 -1
- package/dist/agents/detect.js +0 -169
- package/dist/agents/detect.js.map +0 -1
- package/dist/agents/evaluation-assembly.js +0 -270
- package/dist/agents/evaluation-assembly.js.map +0 -1
- package/dist/agents/events.js +0 -8
- package/dist/agents/events.js.map +0 -1
- package/dist/agents/headless-runner.js +0 -432
- package/dist/agents/headless-runner.js.map +0 -1
- package/dist/agents/jtbd-clarify.js +0 -169
- package/dist/agents/jtbd-clarify.js.map +0 -1
- package/dist/agents/models.js +0 -98
- package/dist/agents/models.js.map +0 -1
- package/dist/agents/parsers.js +0 -316
- package/dist/agents/parsers.js.map +0 -1
- package/dist/agents/prompts.js +0 -522
- package/dist/agents/prompts.js.map +0 -1
- package/dist/agents/run-log.js +0 -71
- package/dist/agents/run-log.js.map +0 -1
- package/dist/agents/runner.js +0 -906
- package/dist/agents/runner.js.map +0 -1
- package/dist/agents/types.js +0 -2
- package/dist/agents/types.js.map +0 -1
- package/dist/bin-resolver.js +0 -92
- package/dist/bin-resolver.js.map +0 -1
- package/dist/config.js +0 -282
- package/dist/config.js.map +0 -1
- package/dist/editor.js +0 -40
- package/dist/editor.js.map +0 -1
- package/dist/gates.js +0 -121
- package/dist/gates.js.map +0 -1
- package/dist/ipc.js +0 -145
- package/dist/ipc.js.map +0 -1
- package/dist/open-folder.js +0 -28
- package/dist/open-folder.js.map +0 -1
- package/dist/phases.js +0 -346
- package/dist/phases.js.map +0 -1
- package/dist/progress.js +0 -184
- package/dist/progress.js.map +0 -1
- package/dist/source-inbox.js +0 -297
- package/dist/source-inbox.js.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/version.js +0 -18
- package/dist/version.js.map +0 -1
- package/dist/yaml-io.js +0 -376
- package/dist/yaml-io.js.map +0 -1
- package/maintenance-skill/SKILL.md +0 -18
- package/maintenance-skill/agents/openai.yaml +0 -4
package/dist/agents/runner.js
DELETED
|
@@ -1,906 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { validatePhaseArtifact } from "./artifact-validation.js";
|
|
5
|
-
import { groupCandidateLonglist, readCandidateLonglist } from "./candidate-longlist.js";
|
|
6
|
-
import { ensureEvaluationArtifact, validateEvaluationFragment } from "./evaluation-assembly.js";
|
|
7
|
-
import { buildEvaluationSectionPrompt, buildPhasePrompt, WORKSPACE_DISCIPLINE } from "./prompts.js";
|
|
8
|
-
import { LineBuffer, parseLine } from "./parsers.js";
|
|
9
|
-
import { closeRunLog, openRunLog } from "./run-log.js";
|
|
10
|
-
import { resolveRunProfile } from "./models.js";
|
|
11
|
-
import { readConfig } from "../config.js";
|
|
12
|
-
// How long to wait for the agent's first byte of output before declaring it
|
|
13
|
-
// unreachable. Generous enough to cover a cold CLI start + auth refresh, short
|
|
14
|
-
// enough that a dead subscription doesn't hang the TUI indefinitely.
|
|
15
|
-
const WATCHDOG_MS = 15_000;
|
|
16
|
-
const DEFAULT_EVALUATION_CLOSURE_IDLE_MS = 120_000;
|
|
17
|
-
/**
|
|
18
|
-
* Spawn the configured agent CLI with a phase-specific prompt. The agent's
|
|
19
|
-
* newline-delimited JSON stdout is parsed into AgentEvents so the PhaseRunner
|
|
20
|
-
* can render tool calls / streaming text / summary as structured panes.
|
|
21
|
-
*
|
|
22
|
-
* Thin wrapper over `runAgentTask` — builds the per-phase prompt via
|
|
23
|
-
* `buildPhasePrompt` and labels the audit log with the phase id.
|
|
24
|
-
*/
|
|
25
|
-
export function runPhaseWithAgent(args) {
|
|
26
|
-
const recoverableArtifacts = new Set([
|
|
27
|
-
"candidates",
|
|
28
|
-
"evaluation",
|
|
29
|
-
"quality",
|
|
30
|
-
"synthesis",
|
|
31
|
-
"assembly",
|
|
32
|
-
]);
|
|
33
|
-
if (args.resume && recoverableArtifacts.has(args.phaseId)) {
|
|
34
|
-
const existing = ensurePhaseArtifact(args.project, args.phaseId);
|
|
35
|
-
if (existing.ok) {
|
|
36
|
-
const label = args.phaseId.charAt(0).toUpperCase() + args.phaseId.slice(1);
|
|
37
|
-
args.onEvent({
|
|
38
|
-
kind: "raw",
|
|
39
|
-
text: `[liner] ${label} artifact already valid; continuing without another AI run.`,
|
|
40
|
-
});
|
|
41
|
-
return { cancel: () => { }, done: Promise.resolve({ code: 0, stderr: "" }) };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (args.phaseId === "evaluation" && (!args.resume || hasSectionedEvaluationState(args.project))) {
|
|
45
|
-
return runEvaluationSectionsWithAgent(args);
|
|
46
|
-
}
|
|
47
|
-
const prompt = buildPhasePrompt({
|
|
48
|
-
phaseId: args.phaseId,
|
|
49
|
-
project: args.project,
|
|
50
|
-
skillPath: args.skillPath,
|
|
51
|
-
tape: args.tape,
|
|
52
|
-
});
|
|
53
|
-
// Per-phase model: the built-in map downgrades the heavy phases, with the
|
|
54
|
-
// user's config.yaml overrides winning. The compile phase isn't an agent
|
|
55
|
-
// run; gates don't reach here either.
|
|
56
|
-
const config = readConfig();
|
|
57
|
-
const overrides = config.models?.[args.agent.id];
|
|
58
|
-
const profile = resolveRunProfile(args.agent.id, args.phaseId, overrides, config.providerPreferences?.[args.agent.id]);
|
|
59
|
-
const handle = runAgentTask({
|
|
60
|
-
agent: args.agent,
|
|
61
|
-
project: args.project,
|
|
62
|
-
skillPath: args.skillPath,
|
|
63
|
-
prompt,
|
|
64
|
-
model: profile.model,
|
|
65
|
-
reasoningEffort: profile.reasoningEffort,
|
|
66
|
-
modelFallbackSource: profile.modelSource === "builtin" || profile.modelSource === "auto" ? profile.modelSource : undefined,
|
|
67
|
-
allowEffortFallback: profile.effortSource === "auto",
|
|
68
|
-
taskLabel: args.phaseId,
|
|
69
|
-
resume: args.resume,
|
|
70
|
-
onEvent: args.onEvent,
|
|
71
|
-
});
|
|
72
|
-
return {
|
|
73
|
-
cancel: handle.cancel,
|
|
74
|
-
done: handle.done.then((result) => {
|
|
75
|
-
if (result.code !== 0)
|
|
76
|
-
return result;
|
|
77
|
-
const validation = ensurePhaseArtifact(args.project, args.phaseId);
|
|
78
|
-
if (validation.ok) {
|
|
79
|
-
if ("message" in validation && validation.message) {
|
|
80
|
-
args.onEvent({ kind: "raw", text: `[liner] ${validation.message}` });
|
|
81
|
-
}
|
|
82
|
-
return result;
|
|
83
|
-
}
|
|
84
|
-
const message = `[liner] Artifact validation failed after ${args.phaseId}: ${validation.message}`;
|
|
85
|
-
args.onEvent({ kind: "text", text: message });
|
|
86
|
-
return {
|
|
87
|
-
...result,
|
|
88
|
-
code: 1,
|
|
89
|
-
stderr: result.stderr ? `${result.stderr}\n${message}` : message,
|
|
90
|
-
};
|
|
91
|
-
}),
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function runEvaluationSectionsWithAgent(args) {
|
|
95
|
-
if (args.resume) {
|
|
96
|
-
const existingEvaluation = ensureEvaluationArtifact(args.project);
|
|
97
|
-
if (existingEvaluation.ok) {
|
|
98
|
-
const message = existingEvaluation.message || "Evaluation artifact already complete.";
|
|
99
|
-
args.onEvent({ kind: "raw", text: `[liner] ${message}` });
|
|
100
|
-
return { cancel: () => { }, done: Promise.resolve({ code: 0, stderr: "" }) };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
const groups = groupCandidateLonglist(readCandidateLonglist(join(args.project, "working/02-candidate-longlist.md")));
|
|
104
|
-
if (groups.length === 0) {
|
|
105
|
-
const message = "[liner] Cannot run Evaluation: working/02-candidate-longlist.md has no URL candidates.";
|
|
106
|
-
args.onEvent({ kind: "raw", text: message });
|
|
107
|
-
return { cancel: () => { }, done: Promise.resolve({ code: 1, stderr: message }) };
|
|
108
|
-
}
|
|
109
|
-
const config = readConfig();
|
|
110
|
-
const overrides = config.models?.[args.agent.id];
|
|
111
|
-
const profile = resolveRunProfile(args.agent.id, "evaluation", overrides, config.providerPreferences?.[args.agent.id]);
|
|
112
|
-
let cancelCurrent = () => { };
|
|
113
|
-
let cancelled = false;
|
|
114
|
-
let lastLogPath;
|
|
115
|
-
const done = (async () => {
|
|
116
|
-
markSectionedEvaluationStarted(args.project, groups.length);
|
|
117
|
-
args.onEvent({
|
|
118
|
-
kind: "raw",
|
|
119
|
-
text: `[liner] Running Evaluation in ${groups.length} section chunk${groups.length === 1 ? "" : "s"}.`,
|
|
120
|
-
});
|
|
121
|
-
for (const group of groups) {
|
|
122
|
-
if (cancelled)
|
|
123
|
-
return { code: 1, stderr: "[liner] Evaluation cancelled." };
|
|
124
|
-
const fragment = validateEvaluationFragment(join(args.project, group.fragmentPath), group.candidates);
|
|
125
|
-
if (fragment.ok) {
|
|
126
|
-
args.onEvent({
|
|
127
|
-
kind: "raw",
|
|
128
|
-
text: `[liner] Evaluation chunk ${group.index}/${group.total} already complete: ` +
|
|
129
|
-
`${group.section} (${fragment.count} candidates).`,
|
|
130
|
-
});
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
args.onEvent({
|
|
134
|
-
kind: "raw",
|
|
135
|
-
text: `[liner] Evaluation chunk ${group.index}/${group.total}: ` +
|
|
136
|
-
`${group.section} (${group.candidates.length} candidates).`,
|
|
137
|
-
});
|
|
138
|
-
const handle = runAgentTask({
|
|
139
|
-
agent: args.agent,
|
|
140
|
-
project: args.project,
|
|
141
|
-
skillPath: args.skillPath,
|
|
142
|
-
prompt: buildEvaluationSectionPrompt({
|
|
143
|
-
project: args.project,
|
|
144
|
-
skillPath: args.skillPath,
|
|
145
|
-
tape: args.tape,
|
|
146
|
-
group,
|
|
147
|
-
}),
|
|
148
|
-
model: profile.model,
|
|
149
|
-
reasoningEffort: profile.reasoningEffort,
|
|
150
|
-
modelFallbackSource: profile.modelSource === "builtin" || profile.modelSource === "auto" ? profile.modelSource : undefined,
|
|
151
|
-
allowEffortFallback: profile.effortSource === "auto",
|
|
152
|
-
taskLabel: "evaluation",
|
|
153
|
-
onEvent: args.onEvent,
|
|
154
|
-
});
|
|
155
|
-
cancelCurrent = handle.cancel;
|
|
156
|
-
const result = await handle.done;
|
|
157
|
-
lastLogPath = result.logPath;
|
|
158
|
-
if (result.code !== 0)
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
const validation = ensureEvaluationArtifact(args.project);
|
|
162
|
-
if (!validation.ok) {
|
|
163
|
-
const message = `[liner] Artifact validation failed after evaluation: ${validation.message}`;
|
|
164
|
-
args.onEvent({ kind: "text", text: message });
|
|
165
|
-
return { code: 1, stderr: message, logPath: lastLogPath };
|
|
166
|
-
}
|
|
167
|
-
if (validation.message) {
|
|
168
|
-
args.onEvent({ kind: "raw", text: `[liner] ${validation.message}` });
|
|
169
|
-
}
|
|
170
|
-
return { code: 0, stderr: "", logPath: lastLogPath };
|
|
171
|
-
})();
|
|
172
|
-
return {
|
|
173
|
-
cancel: () => {
|
|
174
|
-
cancelled = true;
|
|
175
|
-
cancelCurrent();
|
|
176
|
-
},
|
|
177
|
-
done,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
function hasSectionedEvaluationState(project) {
|
|
181
|
-
if (existsSync(join(project, "working/evaluation-decisions/.liner-sectioned-evaluation.json"))) {
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
const dir = join(project, "working/evaluation-decisions");
|
|
185
|
-
if (!existsSync(dir))
|
|
186
|
-
return false;
|
|
187
|
-
return readdirSync(dir).some((file) => /\.ya?ml$/i.test(file));
|
|
188
|
-
}
|
|
189
|
-
function markSectionedEvaluationStarted(project, chunkCount) {
|
|
190
|
-
const dir = join(project, "working/evaluation-decisions");
|
|
191
|
-
mkdirSync(dir, { recursive: true });
|
|
192
|
-
writeFileSync(join(dir, ".liner-sectioned-evaluation.json"), JSON.stringify({
|
|
193
|
-
version: 1,
|
|
194
|
-
chunkCount,
|
|
195
|
-
updatedAt: new Date().toISOString(),
|
|
196
|
-
}, null, 2) + "\n", "utf8");
|
|
197
|
-
}
|
|
198
|
-
function ensurePhaseArtifact(project, phaseId) {
|
|
199
|
-
if (phaseId !== "evaluation")
|
|
200
|
-
return validatePhaseArtifact(project, phaseId);
|
|
201
|
-
return ensureEvaluationArtifact(project);
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Run a single agent task with a caller-supplied prompt. Used by methodology
|
|
205
|
-
* phases (via `runPhaseWithAgent`) and by one-off side tasks like the JTBD
|
|
206
|
-
* elicitation step in the new-mixtape wizard. The agent's stdout is parsed
|
|
207
|
-
* the same way (stream JSON → AgentEvent) so callers get tool calls + final
|
|
208
|
-
* text via the same callback interface.
|
|
209
|
-
*/
|
|
210
|
-
export function runAgentTask(args) {
|
|
211
|
-
// Delegating cancel: a model-rejection fallback re-spawns under the hood, so
|
|
212
|
-
// the handle's cancel must target whichever child is currently live.
|
|
213
|
-
let cancelCurrent = () => { };
|
|
214
|
-
const done = runAttempt(args, {}, (c) => {
|
|
215
|
-
cancelCurrent = c;
|
|
216
|
-
});
|
|
217
|
-
return { cancel: () => cancelCurrent(), done };
|
|
218
|
-
}
|
|
219
|
-
const CLAUDE_METHOD_TOOLS = "Read Write Edit Glob Grep WebFetch";
|
|
220
|
-
const CLAUDE_METHOD_BUILTIN_TOOLS = "Read,Write,Edit,Glob,Grep,WebFetch";
|
|
221
|
-
const CLAUDE_HARD_DENIED_TOOLS = "Bash Task WebSearch ToolSearch";
|
|
222
|
-
function isolatedImprovementDeniedTools(workspace) {
|
|
223
|
-
const corpus = portableDirname(portableDirname(portableDirname(workspace)));
|
|
224
|
-
return [
|
|
225
|
-
CLAUDE_HARD_DENIED_TOOLS,
|
|
226
|
-
`Edit(${claudeAbsolutePermissionPath(portableJoin(corpus, "tape.yaml"))})`,
|
|
227
|
-
`Edit(${claudeAbsolutePermissionPath(portableJoin(corpus, "synthesis.md"))})`,
|
|
228
|
-
`Edit(${claudeAbsolutePermissionPath(portableJoin(corpus, "working"))}/**)`,
|
|
229
|
-
].join(" ");
|
|
230
|
-
}
|
|
231
|
-
function portableDirname(path) {
|
|
232
|
-
const normalized = path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
233
|
-
const index = normalized.lastIndexOf("/");
|
|
234
|
-
if (index < 0)
|
|
235
|
-
return ".";
|
|
236
|
-
if (index === 0)
|
|
237
|
-
return "/";
|
|
238
|
-
return normalized.slice(0, index);
|
|
239
|
-
}
|
|
240
|
-
function portableJoin(parent, child) {
|
|
241
|
-
return `${parent.replace(/\\/g, "/").replace(/\/+$/, "")}/${child}`;
|
|
242
|
-
}
|
|
243
|
-
function claudeAbsolutePermissionPath(path) {
|
|
244
|
-
const normalized = path.replace(/\\/g, "/");
|
|
245
|
-
if (/^[A-Za-z]:\//.test(normalized))
|
|
246
|
-
return `//${normalized}`;
|
|
247
|
-
if (normalized.startsWith("/"))
|
|
248
|
-
return `/${normalized}`;
|
|
249
|
-
return `//${normalized}`;
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* One spawn attempt. Built-in choices and the Auto model policy may retry once on the provider
|
|
253
|
-
* default after an unsupported model or effort. User-selected options fail
|
|
254
|
-
* clearly without substitution. `registerCancel` is re-invoked per attempt so
|
|
255
|
-
* the caller's cancel always hits the live child.
|
|
256
|
-
*/
|
|
257
|
-
function runAttempt(args, attempt, registerCancel) {
|
|
258
|
-
const command = args.agent.bin;
|
|
259
|
-
const resume = args.resume === true;
|
|
260
|
-
const resumeSessionId = args.agent.id === "codex" && resume
|
|
261
|
-
? findLatestCodexSessionId(args.project, args.taskLabel)
|
|
262
|
-
: undefined;
|
|
263
|
-
if (args.agent.id === "codex" && resume && !resumeSessionId) {
|
|
264
|
-
const message = `[liner] Cannot resume OpenAI ${args.taskLabel}: no prior Codex CLI session id was found ` +
|
|
265
|
-
`in .liner-runs/${args.taskLabel}. Retry this phase from a fresh run instead.`;
|
|
266
|
-
args.onEvent({ kind: "raw", text: message });
|
|
267
|
-
const log = openRunLog(args.project, args.taskLabel, {
|
|
268
|
-
agent: args.agent.id,
|
|
269
|
-
resume,
|
|
270
|
-
});
|
|
271
|
-
log.write(JSON.stringify({ type: "_liner_error", message }));
|
|
272
|
-
closeRunLog(log, 1, message.length);
|
|
273
|
-
return Promise.resolve({ code: 1, stderr: message, logPath: log.path });
|
|
274
|
-
}
|
|
275
|
-
const cliArgs = buildArgs(args.agent.id, args.project, args.skillPath, resume, args.model, resumeSessionId, args.taskLabel, args.reasoningEffort);
|
|
276
|
-
const child = spawn(command, cliArgs, {
|
|
277
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
278
|
-
cwd: args.project,
|
|
279
|
-
env: {
|
|
280
|
-
...process.env,
|
|
281
|
-
...(args.agent.configHome
|
|
282
|
-
? {
|
|
283
|
-
[args.agent.id === "claude" ? "CLAUDE_CONFIG_DIR" : "CODEX_HOME"]: args.agent.configHome,
|
|
284
|
-
}
|
|
285
|
-
: {}),
|
|
286
|
-
// Disable extended/interleaved thinking for the spawned Claude Code.
|
|
287
|
-
// A long multi-turn tool loop with thinking on hits a hard API 400 once
|
|
288
|
-
// Claude Code auto-compacts the conversation: a prior `thinking` block
|
|
289
|
-
// gets modified on replay and the API rejects the next turn
|
|
290
|
-
// ("`thinking` or `redacted_thinking` blocks ... cannot be modified").
|
|
291
|
-
// It surfaces both on fresh runs (~25+ turns) and instantly on resume
|
|
292
|
-
// (the resume replays the already-corrupted message). Turning thinking
|
|
293
|
-
// off removes the blocks entirely, so the failure class can't occur.
|
|
294
|
-
// Only Claude reads this; Codex ignores it.
|
|
295
|
-
...(args.agent.id === "claude" ? { MAX_THINKING_TOKENS: "0" } : {}),
|
|
296
|
-
},
|
|
297
|
-
});
|
|
298
|
-
let cancelled = false;
|
|
299
|
-
registerCancel(() => {
|
|
300
|
-
cancelled = true;
|
|
301
|
-
if (!child.killed)
|
|
302
|
-
child.kill("SIGTERM");
|
|
303
|
-
});
|
|
304
|
-
// Accumulated tail of the agent's own output (stdout + stderr). Used only to
|
|
305
|
-
// recognize a model-rejection error for the fallback; capped so a long run
|
|
306
|
-
// doesn't retain the whole transcript.
|
|
307
|
-
let outputTail = "";
|
|
308
|
-
const noteForDetection = (chunk) => {
|
|
309
|
-
outputTail = (outputTail + chunk).slice(-4000);
|
|
310
|
-
};
|
|
311
|
-
// On resume, the previous session already contains the prompt + all prior
|
|
312
|
-
// tool-call results in context. Send a tiny continuation nudge instead of
|
|
313
|
-
// re-shipping the full prompt — that's the whole point of resume (cheaper,
|
|
314
|
-
// no re-fetching).
|
|
315
|
-
const stdinPayload = resume
|
|
316
|
-
? args.resumePrompt ?? resumePromptForTask(args.taskLabel)
|
|
317
|
-
: args.prompt;
|
|
318
|
-
child.stdin?.write(stdinPayload, "utf8");
|
|
319
|
-
child.stdin?.end();
|
|
320
|
-
// Tee-style audit log of the raw JSONL stream. Not load-bearing (Claude's
|
|
321
|
-
// session.json already covers resume) — this gives us a per-run transcript
|
|
322
|
-
// the curator can scroll back through and a forensic record when things
|
|
323
|
-
// misbehave.
|
|
324
|
-
const log = openRunLog(args.project, args.taskLabel, {
|
|
325
|
-
agent: args.agent.id,
|
|
326
|
-
resume,
|
|
327
|
-
});
|
|
328
|
-
// Agent-unavailability watchdog. A dead subscription / revoked auth / model
|
|
329
|
-
// deprecation can leave the agent process launched but mute — no stream
|
|
330
|
-
// events, no stderr, no exit — and the TUI would otherwise spin forever. If
|
|
331
|
-
// nothing comes back within the window, surface a clear error and kill the
|
|
332
|
-
// process so the run lands in the failed state (where retry / switch-agent
|
|
333
|
-
// live). Any output at all — stdout or stderr — clears the watchdog.
|
|
334
|
-
let sawOutput = false;
|
|
335
|
-
const watchdog = setTimeout(() => {
|
|
336
|
-
if (sawOutput || child.killed)
|
|
337
|
-
return;
|
|
338
|
-
const name = args.agent.id === "claude" ? "Claude" : "OpenAI";
|
|
339
|
-
args.onEvent({
|
|
340
|
-
kind: "raw",
|
|
341
|
-
text: `[liner] No response from ${name} after ${WATCHDOG_MS / 1000}s. ` +
|
|
342
|
-
`It may not be signed in, or its subscription/quota may be exhausted. ` +
|
|
343
|
-
`Try running \`${args.agent.id}\` on its own to check, then retry or switch agents.`,
|
|
344
|
-
});
|
|
345
|
-
log.write(JSON.stringify({ type: "_liner_watchdog_timeout", ms: WATCHDOG_MS }));
|
|
346
|
-
child.kill("SIGTERM");
|
|
347
|
-
}, WATCHDOG_MS);
|
|
348
|
-
watchdog.unref?.();
|
|
349
|
-
const noteOutput = () => {
|
|
350
|
-
if (sawOutput)
|
|
351
|
-
return;
|
|
352
|
-
sawOutput = true;
|
|
353
|
-
clearTimeout(watchdog);
|
|
354
|
-
};
|
|
355
|
-
const closureIdleMs = evaluationClosureIdleMs();
|
|
356
|
-
const canCloseEvaluationArtifact = args.taskLabel === "evaluation" &&
|
|
357
|
-
!attempt.artifactClosure &&
|
|
358
|
-
closureIdleMs > 0;
|
|
359
|
-
let evaluationClosureTimer;
|
|
360
|
-
let evaluationClosureTimedOut = false;
|
|
361
|
-
let childClosed = false;
|
|
362
|
-
let forcedEvaluationResult;
|
|
363
|
-
const activeTools = new Set();
|
|
364
|
-
const clearEvaluationClosureTimer = () => {
|
|
365
|
-
if (!evaluationClosureTimer)
|
|
366
|
-
return;
|
|
367
|
-
clearTimeout(evaluationClosureTimer);
|
|
368
|
-
evaluationClosureTimer = undefined;
|
|
369
|
-
};
|
|
370
|
-
const scheduleEvaluationClosureCheck = () => {
|
|
371
|
-
if (!canCloseEvaluationArtifact || cancelled || child.killed || childClosed)
|
|
372
|
-
return;
|
|
373
|
-
clearEvaluationClosureTimer();
|
|
374
|
-
evaluationClosureTimer = setTimeout(() => {
|
|
375
|
-
if (cancelled || child.killed)
|
|
376
|
-
return;
|
|
377
|
-
if (activeTools.size > 0) {
|
|
378
|
-
scheduleEvaluationClosureCheck();
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
const validation = ensureEvaluationArtifact(args.project);
|
|
382
|
-
if (validation.ok) {
|
|
383
|
-
forcedEvaluationResult = {
|
|
384
|
-
code: 0,
|
|
385
|
-
message: "[liner] Evaluation artifact is valid; stopping the quiet agent turn.",
|
|
386
|
-
};
|
|
387
|
-
args.onEvent({ kind: "raw", text: forcedEvaluationResult.message });
|
|
388
|
-
log.write(JSON.stringify({
|
|
389
|
-
type: "_liner_artifact_closure_complete",
|
|
390
|
-
phase: "evaluation",
|
|
391
|
-
ms: closureIdleMs,
|
|
392
|
-
}));
|
|
393
|
-
child.kill("SIGTERM");
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
evaluationClosureTimedOut = true;
|
|
397
|
-
const idleLabel = closureIdleMs >= 1000 ? `${Math.round(closureIdleMs / 1000)}s` : `${closureIdleMs}ms`;
|
|
398
|
-
const message = `[liner] Evaluation has been quiet for ${idleLabel} ` +
|
|
399
|
-
`with no valid working/03-evaluation.yaml. Stopping this turn and resuming once ` +
|
|
400
|
-
`with a write-only artifact closure prompt.`;
|
|
401
|
-
args.onEvent({ kind: "raw", text: message });
|
|
402
|
-
log.write(JSON.stringify({
|
|
403
|
-
type: "_liner_artifact_closure_timeout",
|
|
404
|
-
phase: "evaluation",
|
|
405
|
-
ms: closureIdleMs,
|
|
406
|
-
validation: validation.message,
|
|
407
|
-
}));
|
|
408
|
-
child.kill("SIGTERM");
|
|
409
|
-
}, closureIdleMs);
|
|
410
|
-
evaluationClosureTimer.unref?.();
|
|
411
|
-
};
|
|
412
|
-
const noteAgentEvent = (event) => {
|
|
413
|
-
if (!canCloseEvaluationArtifact)
|
|
414
|
-
return;
|
|
415
|
-
if (event.kind === "tool_start" && event.id) {
|
|
416
|
-
activeTools.add(event.id);
|
|
417
|
-
scheduleEvaluationClosureCheck();
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
if (event.kind === "tool_done" && event.id) {
|
|
421
|
-
activeTools.delete(event.id);
|
|
422
|
-
scheduleEvaluationClosureCheck();
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
if (event.kind === "summary") {
|
|
426
|
-
clearEvaluationClosureTimer();
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
scheduleEvaluationClosureCheck();
|
|
430
|
-
};
|
|
431
|
-
if (args.taskLabel === "evaluation" && attempt.artifactClosure && closureIdleMs > 0) {
|
|
432
|
-
clearEvaluationClosureTimer();
|
|
433
|
-
evaluationClosureTimer = setTimeout(() => {
|
|
434
|
-
if (cancelled || child.killed || childClosed)
|
|
435
|
-
return;
|
|
436
|
-
const validation = ensureEvaluationArtifact(args.project);
|
|
437
|
-
const idleLabel = closureIdleMs >= 1000 ? `${Math.round(closureIdleMs / 1000)}s` : `${closureIdleMs}ms`;
|
|
438
|
-
if (validation.ok) {
|
|
439
|
-
forcedEvaluationResult = {
|
|
440
|
-
code: 0,
|
|
441
|
-
message: `[liner] Evaluation artifact closure produced a valid YAML file within ${idleLabel}; stopping the idle closure turn.`,
|
|
442
|
-
};
|
|
443
|
-
args.onEvent({ kind: "raw", text: forcedEvaluationResult.message });
|
|
444
|
-
log.write(JSON.stringify({
|
|
445
|
-
type: "_liner_artifact_closure_complete",
|
|
446
|
-
phase: "evaluation",
|
|
447
|
-
ms: closureIdleMs,
|
|
448
|
-
}));
|
|
449
|
-
child.kill("SIGTERM");
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
forcedEvaluationResult = {
|
|
453
|
-
code: 1,
|
|
454
|
-
message: `[liner] Evaluation artifact closure timed out after ${idleLabel}: ${validation.message}. ` +
|
|
455
|
-
"Retry Evaluation fresh or reduce the candidate set before continuing.",
|
|
456
|
-
};
|
|
457
|
-
args.onEvent({ kind: "raw", text: forcedEvaluationResult.message });
|
|
458
|
-
log.write(JSON.stringify({
|
|
459
|
-
type: "_liner_artifact_closure_failed",
|
|
460
|
-
phase: "evaluation",
|
|
461
|
-
ms: closureIdleMs,
|
|
462
|
-
validation: validation.message,
|
|
463
|
-
}));
|
|
464
|
-
child.kill("SIGTERM");
|
|
465
|
-
}, closureIdleMs);
|
|
466
|
-
evaluationClosureTimer.unref?.();
|
|
467
|
-
}
|
|
468
|
-
const buffer = new LineBuffer();
|
|
469
|
-
if (child.stdout) {
|
|
470
|
-
child.stdout.setEncoding("utf8");
|
|
471
|
-
child.stdout.on("data", (chunk) => {
|
|
472
|
-
noteOutput();
|
|
473
|
-
noteForDetection(chunk);
|
|
474
|
-
const lines = buffer.push(chunk);
|
|
475
|
-
for (const line of lines) {
|
|
476
|
-
log.write(line);
|
|
477
|
-
for (const event of parseLine(args.agent.id, line)) {
|
|
478
|
-
noteAgentEvent(event);
|
|
479
|
-
args.onEvent(event);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
let stderr = "";
|
|
485
|
-
if (child.stderr) {
|
|
486
|
-
child.stderr.setEncoding("utf8");
|
|
487
|
-
child.stderr.on("data", (chunk) => {
|
|
488
|
-
noteOutput();
|
|
489
|
-
noteForDetection(chunk);
|
|
490
|
-
stderr += chunk;
|
|
491
|
-
// Surface meaningful stderr as raw events so users see install/auth
|
|
492
|
-
// hints inline, while keeping repetitive cosmetic Codex startup warnings
|
|
493
|
-
// out of the live TUI. The full stream is still recorded in the run log.
|
|
494
|
-
for (const line of chunk.split("\n")) {
|
|
495
|
-
const trimmed = line.trim();
|
|
496
|
-
if (trimmed) {
|
|
497
|
-
// Record stderr in the log too — wrapped so it doesn't masquerade
|
|
498
|
-
// as a real JSON event when the log is replayed.
|
|
499
|
-
log.write(JSON.stringify({ type: "_liner_stderr", text: trimmed }));
|
|
500
|
-
if (shouldSurfaceAgentStderr(args.agent.id, trimmed)) {
|
|
501
|
-
args.onEvent({ kind: "raw", text: trimmed });
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
return new Promise((resolve) => {
|
|
508
|
-
child.on("close", (code) => {
|
|
509
|
-
clearTimeout(watchdog);
|
|
510
|
-
clearEvaluationClosureTimer();
|
|
511
|
-
childClosed = true;
|
|
512
|
-
// Flush any trailing partial line.
|
|
513
|
-
for (const line of buffer.flush()) {
|
|
514
|
-
log.write(line);
|
|
515
|
-
for (const event of parseLine(args.agent.id, line)) {
|
|
516
|
-
noteAgentEvent(event);
|
|
517
|
-
args.onEvent(event);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
closeRunLog(log, code, stderr.length);
|
|
521
|
-
if (forcedEvaluationResult) {
|
|
522
|
-
resolve({
|
|
523
|
-
code: forcedEvaluationResult.code,
|
|
524
|
-
stderr: forcedEvaluationResult.code === 0
|
|
525
|
-
? stderr
|
|
526
|
-
: stderr
|
|
527
|
-
? `${stderr}\n${forcedEvaluationResult.message}`
|
|
528
|
-
: forcedEvaluationResult.message,
|
|
529
|
-
logPath: log.path,
|
|
530
|
-
});
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
if (evaluationClosureTimedOut && !cancelled) {
|
|
534
|
-
args.onEvent({
|
|
535
|
-
kind: "raw",
|
|
536
|
-
text: "[liner] Resuming Evaluation once to close working/03-evaluation.yaml. No more source fetching in this pass.",
|
|
537
|
-
});
|
|
538
|
-
resolve(runAttempt({
|
|
539
|
-
...args,
|
|
540
|
-
resume: true,
|
|
541
|
-
resumePrompt: evaluationArtifactClosurePrompt(),
|
|
542
|
-
}, { artifactClosure: true }, registerCancel));
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
// Configured-option rejection. Auto-owned options may retry once on the
|
|
546
|
-
// provider default; user-owned options fail closed. Resumes and
|
|
547
|
-
// cancellations are exempt, and both classifiers require plausible
|
|
548
|
-
// provider rejection output so auth or quota failures surface normally.
|
|
549
|
-
if (code !== 0 &&
|
|
550
|
-
!cancelled &&
|
|
551
|
-
!resume &&
|
|
552
|
-
args.reasoningEffort &&
|
|
553
|
-
looksLikeEffortRejection(outputTail, args.reasoningEffort)) {
|
|
554
|
-
if (!attempt.modelFallback && args.allowEffortFallback === true) {
|
|
555
|
-
const policy = args.modelFallbackSource === "auto" ? "the Auto model policy" : "Liner's default Thinking effort";
|
|
556
|
-
args.onEvent({
|
|
557
|
-
kind: "raw",
|
|
558
|
-
text: `[liner] ${args.agent.name} rejected ${policy} for ${args.taskLabel} — ` +
|
|
559
|
-
"retrying once on the provider default model and Thinking effort.",
|
|
560
|
-
});
|
|
561
|
-
log.write(JSON.stringify({
|
|
562
|
-
type: "_liner_auto_model_policy_fallback",
|
|
563
|
-
model: args.model,
|
|
564
|
-
reasoningEffort: args.reasoningEffort,
|
|
565
|
-
}));
|
|
566
|
-
resolve(runAttempt({
|
|
567
|
-
...args,
|
|
568
|
-
// Drop what Liner chose (Auto or built-in); keep a model from
|
|
569
|
-
// Settings or a phase override.
|
|
570
|
-
model: args.modelFallbackSource === undefined ? args.model : undefined,
|
|
571
|
-
reasoningEffort: undefined,
|
|
572
|
-
}, { modelFallback: true }, registerCancel));
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
const message = `[liner] ${args.agent.name} rejected configured Thinking effort "${args.reasoningEffort}". ` +
|
|
576
|
-
"Choose another effort in Settings; Liner did not substitute another effort or model.";
|
|
577
|
-
args.onEvent({ kind: "raw", text: message });
|
|
578
|
-
resolve({ code, stderr: stderr ? `${message}\n${stderr}` : message, logPath: log.path });
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
if (code !== 0 &&
|
|
582
|
-
!cancelled &&
|
|
583
|
-
!resume &&
|
|
584
|
-
!attempt.modelFallback &&
|
|
585
|
-
args.model &&
|
|
586
|
-
args.modelFallbackSource !== undefined &&
|
|
587
|
-
looksLikeModelRejection(outputTail, args.model)) {
|
|
588
|
-
const fallbackMessage = args.modelFallbackSource === "auto"
|
|
589
|
-
? `[liner] ${args.agent.name} rejected the Auto model policy model "${args.model}" for ${args.taskLabel} — ` +
|
|
590
|
-
"retrying once on the provider default. To change this choice, open Settings and select another Model."
|
|
591
|
-
: `[liner] ${args.agent.name} rejected the built-in model "${args.model}" for ${args.taskLabel} — ` +
|
|
592
|
-
`retrying once on the provider default. To change this phase override, set ` +
|
|
593
|
-
`models.${args.agent.id}.${args.taskLabel} in ~/.liner/config.yaml.`;
|
|
594
|
-
args.onEvent({
|
|
595
|
-
kind: "raw",
|
|
596
|
-
text: fallbackMessage,
|
|
597
|
-
});
|
|
598
|
-
log.write(JSON.stringify({ type: "_liner_model_fallback", model: args.model }));
|
|
599
|
-
resolve(runAttempt({
|
|
600
|
-
...args,
|
|
601
|
-
model: undefined,
|
|
602
|
-
reasoningEffort: args.allowEffortFallback === true ? undefined : args.reasoningEffort,
|
|
603
|
-
}, { modelFallback: true }, registerCancel));
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
if (code !== 0 &&
|
|
607
|
-
!cancelled &&
|
|
608
|
-
!resume &&
|
|
609
|
-
args.model &&
|
|
610
|
-
args.modelFallbackSource === undefined &&
|
|
611
|
-
looksLikeModelRejection(outputTail, args.model)) {
|
|
612
|
-
const message = `[liner] ${args.agent.name} rejected configured model "${args.model}". ` +
|
|
613
|
-
"Choose another model in Settings; Liner did not substitute another model.";
|
|
614
|
-
args.onEvent({ kind: "raw", text: message });
|
|
615
|
-
resolve({ code, stderr: stderr ? `${message}\n${stderr}` : message, logPath: log.path });
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
resolve({ code, stderr, logPath: log.path });
|
|
619
|
-
});
|
|
620
|
-
child.on("error", (e) => {
|
|
621
|
-
clearTimeout(watchdog);
|
|
622
|
-
clearEvaluationClosureTimer();
|
|
623
|
-
args.onEvent({ kind: "raw", text: `[runner error] ${e.message}` });
|
|
624
|
-
log.write(JSON.stringify({ type: "_liner_error", message: e.message }));
|
|
625
|
-
closeRunLog(log, 1, stderr.length);
|
|
626
|
-
resolve({ code: 1, stderr: e.message, logPath: log.path });
|
|
627
|
-
});
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
export function resumePromptForTask(taskLabel) {
|
|
631
|
-
const base = "Continue from where you left off. Pick up the same task; do not re-do tool calls you already completed." +
|
|
632
|
-
`\n\n${WORKSPACE_DISCIPLINE}`;
|
|
633
|
-
if (taskLabel === "candidates") {
|
|
634
|
-
return `${base}\n\nPhase 2 reminder: stop any open-ended web/search loop. If working/05-operating-fit-audit.md exists with status: improvement_recommended, treat this as a focused improvement pass and search for the missing source roles named there. Otherwise use the verified URLs, curator-provided tape sources, titles, and reasons already gathered in this conversation. Write working/02-candidate-longlist.md now, grouped by the knowledge-map sections, replacing the placeholder completely. Do not run more than one final targeted search for a genuinely empty section; if a section is still thin, document the best verified candidates you have and stop after writing the file.`;
|
|
635
|
-
}
|
|
636
|
-
if (taskLabel === "evaluation") {
|
|
637
|
-
return `${base}\n\nPhase 4 reminder: do not get stuck chasing unavailable content. For any candidate whose content is still unavailable, make at most one more recovery attempt, then evaluate from verified metadata, the Phase 2 reason, and any partial content. Every candidate still needs a keep/trim/drop decision. Kept/trim entries need rating, jtbd_fit (direct/bridge/background), section, rationale, fetch_status (readable/partial only), content_quality (high/medium only), at least two content-specific evidence bullets, and the three-part curator note. Do not keep sources from URL/title/search snippets/model memory alone. Prefer section-sized fragments under working/evaluation-decisions/ so Liner can assemble working/03-evaluation.yaml; if you write the final file directly, do not say you are ready to write the artifact before writing it.`;
|
|
638
|
-
}
|
|
639
|
-
if (taskLabel === "quality") {
|
|
640
|
-
return `${base}\n\nPhase 5 reminder: stop any open-ended web/search loop. Quality is a bounded audit, not fresh discovery. Use the existing kept/trim decisions in working/03-evaluation.yaml, assign missing jtbd_fit and source kinds from the title/note/section/source type, repair weak curator notes from existing evidence, spend no more external search attempts, and write working/04-quality-checks.md now with Test 0 core-action fit, Tests 1-6, Perspectives audit, the kind distribution, and the note-quality check. If a material source-role gap remains, write working/05-operating-fit-audit.md with status: improvement_recommended and concrete search lanes for a focused second pass; do not call the corpus ready with a limitation.`;
|
|
641
|
-
}
|
|
642
|
-
return base;
|
|
643
|
-
}
|
|
644
|
-
export function evaluationArtifactClosurePrompt() {
|
|
645
|
-
return [
|
|
646
|
-
"Continue Phase 4, but switch to artifact closure only.",
|
|
647
|
-
"",
|
|
648
|
-
WORKSPACE_DISCIPLINE,
|
|
649
|
-
"",
|
|
650
|
-
"Do not fetch, search, or read external sources in this pass. Use the evidence, metadata, Phase 2 rationale, and partial content already gathered in this conversation.",
|
|
651
|
-
"Write complete section-sized YAML fragments under working/evaluation-decisions/ now, or write working/03-evaluation.yaml directly if it is small enough.",
|
|
652
|
-
"Every URL candidate in working/02-candidate-longlist.md must appear exactly once across those fragments or in the final file.",
|
|
653
|
-
"Use decision: dropped for uncertain, duplicate, unavailable, or metadata-only candidates unless the prior evidence clearly supports kept or trim.",
|
|
654
|
-
"For kept/trim entries include rating, jtbd_fit (direct/bridge/background), fetch_status (readable/partial only), content_quality (high/medium only), at least two content-specific evidence bullets, section, rationale, and the three-part curator note. For dropped entries include URL, title, decision, section if known, and one rationale sentence.",
|
|
655
|
-
"Do not keep sources from URL/title/search snippets/model memory alone; if evidence is not already in the conversation, mark the candidate dropped in this closure pass.",
|
|
656
|
-
"After writing fragments, Liner will assemble working/03-evaluation.yaml; if you write the final file yourself, parse/check it before the required Phase 4 final message.",
|
|
657
|
-
].join("\n");
|
|
658
|
-
}
|
|
659
|
-
export function evaluationClosureIdleMs(env = process.env) {
|
|
660
|
-
const raw = env["LINER_EVALUATION_CLOSURE_IDLE_MS"];
|
|
661
|
-
if (raw == null || raw.trim() === "")
|
|
662
|
-
return DEFAULT_EVALUATION_CLOSURE_IDLE_MS;
|
|
663
|
-
const parsed = Number(raw);
|
|
664
|
-
if (!Number.isFinite(parsed) || parsed < 0)
|
|
665
|
-
return DEFAULT_EVALUATION_CLOSURE_IDLE_MS;
|
|
666
|
-
return parsed;
|
|
667
|
-
}
|
|
668
|
-
export function buildArgs(id, project, skillPath, resume, model, resumeSessionId, taskLabel, reasoningEffort) {
|
|
669
|
-
const isolatedImprovement = taskLabel === "improvement";
|
|
670
|
-
if (id === "claude") {
|
|
671
|
-
// Stream-json requires --verbose. --include-partial-messages would give
|
|
672
|
-
// us mid-message chunking but the cost is ~10× more events; we let the
|
|
673
|
-
// assistant message arrive as one block per turn instead.
|
|
674
|
-
//
|
|
675
|
-
// Tool scoping keeps the agent's reach bounded even with permissions
|
|
676
|
-
// skipped. --tools constrains Claude's built-in tool surface shown in the
|
|
677
|
-
// init stream, while --allowedTools/--disallowedTools guard permission
|
|
678
|
-
// checks and any globally-provided names that may appear despite the
|
|
679
|
-
// built-in list. The methodology needs local file reads/writes plus
|
|
680
|
-
// targeted fetches; it should not shell out, launch subagents, or web-search.
|
|
681
|
-
//
|
|
682
|
-
// `--continue` (resume mode) picks up the most recent session in the
|
|
683
|
-
// current working directory. Since we spawn with cwd=project, "most
|
|
684
|
-
// recent session" is the one we started for this phase. The previous
|
|
685
|
-
// conversation — prompt, tool calls, partial work — stays in context.
|
|
686
|
-
//
|
|
687
|
-
// `--strict-mcp-config` with no `--mcp-config` loads zero MCP servers, so
|
|
688
|
-
// the methodology agent doesn't inherit the user's global connectors
|
|
689
|
-
// (Gmail, Drive, etc.). Those are irrelevant to curation, bloat the
|
|
690
|
-
// context window, and widen the agent's reach for no benefit.
|
|
691
|
-
const base = [
|
|
692
|
-
"-p",
|
|
693
|
-
"--output-format",
|
|
694
|
-
"stream-json",
|
|
695
|
-
"--verbose",
|
|
696
|
-
...(!isolatedImprovement ? ["--add-dir", skillPath] : []),
|
|
697
|
-
...(isolatedImprovement ? ["--setting-sources", "", "--permission-mode", "default"] : []),
|
|
698
|
-
"--strict-mcp-config",
|
|
699
|
-
...(!isolatedImprovement ? ["--dangerously-skip-permissions"] : []),
|
|
700
|
-
"--tools",
|
|
701
|
-
CLAUDE_METHOD_BUILTIN_TOOLS,
|
|
702
|
-
"--allowedTools",
|
|
703
|
-
isolatedImprovement
|
|
704
|
-
? `Read Glob Grep WebFetch Edit(${claudeAbsolutePermissionPath(project)}/**)`
|
|
705
|
-
: CLAUDE_METHOD_TOOLS,
|
|
706
|
-
"--disallowedTools",
|
|
707
|
-
isolatedImprovement ? isolatedImprovementDeniedTools(project) : CLAUDE_HARD_DENIED_TOOLS,
|
|
708
|
-
"--disable-slash-commands",
|
|
709
|
-
];
|
|
710
|
-
// Apply the per-phase model on fresh runs only — a resumed session keeps
|
|
711
|
-
// the model it was started with, and `--model` alongside `--continue`
|
|
712
|
-
// wouldn't change the already-established conversation's model.
|
|
713
|
-
if (model && !resume)
|
|
714
|
-
base.push("--model", model);
|
|
715
|
-
if (reasoningEffort && !resume)
|
|
716
|
-
base.push("--effort", reasoningEffort);
|
|
717
|
-
if (resume)
|
|
718
|
-
base.push("--continue");
|
|
719
|
-
return base;
|
|
720
|
-
}
|
|
721
|
-
if (id === "codex") {
|
|
722
|
-
if (resume) {
|
|
723
|
-
// Codex's `exec resume` subcommand has a narrower option set than fresh
|
|
724
|
-
// exec: there is no --cd or --sandbox flag here. Use Liner's recorded
|
|
725
|
-
// session id instead of --last so a resume cannot pick up unrelated
|
|
726
|
-
// Codex work from the user's machine.
|
|
727
|
-
const sessionArg = resumeSessionId ? [resumeSessionId] : ["--last"];
|
|
728
|
-
return [
|
|
729
|
-
"exec",
|
|
730
|
-
"resume",
|
|
731
|
-
"--skip-git-repo-check",
|
|
732
|
-
"--json",
|
|
733
|
-
...sessionArg,
|
|
734
|
-
"-",
|
|
735
|
-
];
|
|
736
|
-
}
|
|
737
|
-
return [
|
|
738
|
-
"exec",
|
|
739
|
-
"--cd",
|
|
740
|
-
project,
|
|
741
|
-
...(model ? ["--model", model] : []),
|
|
742
|
-
...(reasoningEffort ? ["-c", `model_reasoning_effort=${JSON.stringify(reasoningEffort)}`] : []),
|
|
743
|
-
...(!isolatedImprovement ? ["--add-dir", skillPath] : []),
|
|
744
|
-
"--skip-git-repo-check",
|
|
745
|
-
"-s",
|
|
746
|
-
"workspace-write",
|
|
747
|
-
"--json",
|
|
748
|
-
"-",
|
|
749
|
-
];
|
|
750
|
-
}
|
|
751
|
-
return [];
|
|
752
|
-
}
|
|
753
|
-
export function findLatestCodexSessionId(project, taskLabel) {
|
|
754
|
-
const dir = join(project, ".liner-runs", taskLabel.replace(/[^a-zA-Z0-9_-]/g, "-") || "task");
|
|
755
|
-
if (!existsSync(dir))
|
|
756
|
-
return undefined;
|
|
757
|
-
const files = readdirSync(dir)
|
|
758
|
-
.filter((name) => name.endsWith(".jsonl"))
|
|
759
|
-
.map((name) => join(dir, name))
|
|
760
|
-
.filter((path) => {
|
|
761
|
-
try {
|
|
762
|
-
return statSync(path).isFile();
|
|
763
|
-
}
|
|
764
|
-
catch {
|
|
765
|
-
return false;
|
|
766
|
-
}
|
|
767
|
-
})
|
|
768
|
-
.sort((a, b) => {
|
|
769
|
-
try {
|
|
770
|
-
return statSync(b).mtimeMs - statSync(a).mtimeMs;
|
|
771
|
-
}
|
|
772
|
-
catch {
|
|
773
|
-
return 0;
|
|
774
|
-
}
|
|
775
|
-
});
|
|
776
|
-
const fallback = [];
|
|
777
|
-
for (const file of files) {
|
|
778
|
-
const candidate = codexSessionIdFromRunLog(file, false);
|
|
779
|
-
if (candidate)
|
|
780
|
-
return candidate;
|
|
781
|
-
const resumedCandidate = codexSessionIdFromRunLog(file, true);
|
|
782
|
-
if (resumedCandidate)
|
|
783
|
-
fallback.push(resumedCandidate);
|
|
784
|
-
}
|
|
785
|
-
return fallback[0];
|
|
786
|
-
}
|
|
787
|
-
function codexSessionIdFromRunLog(path, allowResumeLog) {
|
|
788
|
-
let metaAgent = "";
|
|
789
|
-
let metaResume = false;
|
|
790
|
-
try {
|
|
791
|
-
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
792
|
-
if (!line.trim())
|
|
793
|
-
continue;
|
|
794
|
-
let value;
|
|
795
|
-
try {
|
|
796
|
-
value = JSON.parse(line);
|
|
797
|
-
}
|
|
798
|
-
catch {
|
|
799
|
-
continue;
|
|
800
|
-
}
|
|
801
|
-
if (!value || typeof value !== "object")
|
|
802
|
-
continue;
|
|
803
|
-
const record = value;
|
|
804
|
-
if (record.type === "_liner_meta") {
|
|
805
|
-
metaAgent = typeof record.agent === "string" ? record.agent : "";
|
|
806
|
-
metaResume = record.resume === true;
|
|
807
|
-
continue;
|
|
808
|
-
}
|
|
809
|
-
if (record.type !== "thread.started")
|
|
810
|
-
continue;
|
|
811
|
-
if (metaAgent !== "codex")
|
|
812
|
-
return undefined;
|
|
813
|
-
if (metaResume && !allowResumeLog)
|
|
814
|
-
return undefined;
|
|
815
|
-
return typeof record.thread_id === "string" && record.thread_id.trim() !== ""
|
|
816
|
-
? record.thread_id
|
|
817
|
-
: undefined;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
catch {
|
|
821
|
-
return undefined;
|
|
822
|
-
}
|
|
823
|
-
return undefined;
|
|
824
|
-
}
|
|
825
|
-
/**
|
|
826
|
-
* Heuristic: did this failure come from the agent not recognizing the model?
|
|
827
|
-
* Matches the common "unknown/invalid model" phrasings both CLIs emit, plus
|
|
828
|
-
* the offending model id echoed next to an error token. Deliberately
|
|
829
|
-
* conservative — a false negative just means no auto-fallback (the normal
|
|
830
|
-
* error still surfaces); we only want to claim "model rejected" when it
|
|
831
|
-
* plausibly was, so genuine auth/quota failures aren't mislabeled.
|
|
832
|
-
*/
|
|
833
|
-
export function looksLikeModelRejection(output, model) {
|
|
834
|
-
const t = output.toLowerCase();
|
|
835
|
-
if (!t)
|
|
836
|
-
return false;
|
|
837
|
-
const phrases = [
|
|
838
|
-
"unknown model",
|
|
839
|
-
"invalid model",
|
|
840
|
-
"model not found",
|
|
841
|
-
"model_not_found",
|
|
842
|
-
"no such model",
|
|
843
|
-
"unrecognized model",
|
|
844
|
-
"unsupported model",
|
|
845
|
-
"not a valid model",
|
|
846
|
-
"model does not exist",
|
|
847
|
-
"is not a supported model",
|
|
848
|
-
];
|
|
849
|
-
if (phrases.some((p) => t.includes(p)))
|
|
850
|
-
return true;
|
|
851
|
-
// The model id echoed back alongside a generic error/invalid token.
|
|
852
|
-
const m = model.toLowerCase();
|
|
853
|
-
return (m.length > 0 &&
|
|
854
|
-
t.includes(m) &&
|
|
855
|
-
(t.includes("error") || t.includes("invalid") || t.includes("unknown") || t.includes("not found")));
|
|
856
|
-
}
|
|
857
|
-
export function looksLikeEffortRejection(output, _effort) {
|
|
858
|
-
const value = output.toLowerCase();
|
|
859
|
-
if (!value.trim())
|
|
860
|
-
return false;
|
|
861
|
-
const mentionsEffort = value.includes("reasoning effort") ||
|
|
862
|
-
value.includes("reasoning_effort") ||
|
|
863
|
-
value.includes("model_reasoning_effort") ||
|
|
864
|
-
value.includes("--effort") ||
|
|
865
|
-
value.includes("effort level");
|
|
866
|
-
if (!mentionsEffort)
|
|
867
|
-
return false;
|
|
868
|
-
return [
|
|
869
|
-
"invalid",
|
|
870
|
-
"unsupported",
|
|
871
|
-
"unknown",
|
|
872
|
-
"not supported",
|
|
873
|
-
"does not support",
|
|
874
|
-
"not available",
|
|
875
|
-
"unrecognized",
|
|
876
|
-
].some((token) => value.includes(token));
|
|
877
|
-
}
|
|
878
|
-
export function shouldSurfaceAgentStderr(agent, line) {
|
|
879
|
-
const value = line.trim();
|
|
880
|
-
if (!value)
|
|
881
|
-
return false;
|
|
882
|
-
if (agent !== "codex")
|
|
883
|
-
return true;
|
|
884
|
-
return !isNoisyCodexStderr(value);
|
|
885
|
-
}
|
|
886
|
-
function isNoisyCodexStderr(line) {
|
|
887
|
-
const value = line.toLowerCase();
|
|
888
|
-
return (isCosmeticCodexStartupWarning(line) ||
|
|
889
|
-
isCodexGlobalSkillLoadWarning(value) ||
|
|
890
|
-
isNoisyCodexMcpTransport(value) ||
|
|
891
|
-
value.includes("rmcp::transport::auth") ||
|
|
892
|
-
(value.includes("oauth") && (value.includes("mcp") || value.includes("connector"))) ||
|
|
893
|
-
value.includes("write_stdin failed: stdin is closed"));
|
|
894
|
-
}
|
|
895
|
-
function isCodexGlobalSkillLoadWarning(value) {
|
|
896
|
-
return value.includes("failed to load skill");
|
|
897
|
-
}
|
|
898
|
-
function isNoisyCodexMcpTransport(value) {
|
|
899
|
-
return value.includes("rmcp::transport::worker") && value.includes("transport channel closed");
|
|
900
|
-
}
|
|
901
|
-
function isCosmeticCodexStartupWarning(line) {
|
|
902
|
-
return (line.includes("codex_core_plugins::manifest: ignoring interface.defaultPrompt") ||
|
|
903
|
-
line.includes("codex_core_skills::loader: ignoring interface.icon_small") ||
|
|
904
|
-
line.includes("codex_core_skills::loader: ignoring interface.icon_large"));
|
|
905
|
-
}
|
|
906
|
-
//# sourceMappingURL=runner.js.map
|