linersh 1.1.3 → 1.2.1

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.
Files changed (62) hide show
  1. package/README.md +15 -61
  2. package/bin/liner.js +60 -16
  3. package/package.json +10 -17
  4. package/cli-update-docs/README.md +0 -129
  5. package/cli-update-docs/SKILL.md +0 -294
  6. package/cli-update-docs/curator-notes.md +0 -85
  7. package/cli-update-docs/quality-check-tests.md +0 -435
  8. package/cli-update-docs/source-finding-tactics.md +0 -224
  9. package/cli-update-docs/source-quality-hierarchy.md +0 -209
  10. package/cli-update-docs/synthesis-guidance.md +0 -151
  11. package/dist/agents/artifact-validation.js +0 -567
  12. package/dist/agents/artifact-validation.js.map +0 -1
  13. package/dist/agents/candidate-longlist.js +0 -116
  14. package/dist/agents/candidate-longlist.js.map +0 -1
  15. package/dist/agents/detect.js +0 -169
  16. package/dist/agents/detect.js.map +0 -1
  17. package/dist/agents/evaluation-assembly.js +0 -270
  18. package/dist/agents/evaluation-assembly.js.map +0 -1
  19. package/dist/agents/events.js +0 -8
  20. package/dist/agents/events.js.map +0 -1
  21. package/dist/agents/headless-runner.js +0 -432
  22. package/dist/agents/headless-runner.js.map +0 -1
  23. package/dist/agents/jtbd-clarify.js +0 -169
  24. package/dist/agents/jtbd-clarify.js.map +0 -1
  25. package/dist/agents/models.js +0 -98
  26. package/dist/agents/models.js.map +0 -1
  27. package/dist/agents/parsers.js +0 -316
  28. package/dist/agents/parsers.js.map +0 -1
  29. package/dist/agents/prompts.js +0 -522
  30. package/dist/agents/prompts.js.map +0 -1
  31. package/dist/agents/run-log.js +0 -71
  32. package/dist/agents/run-log.js.map +0 -1
  33. package/dist/agents/runner.js +0 -906
  34. package/dist/agents/runner.js.map +0 -1
  35. package/dist/agents/types.js +0 -2
  36. package/dist/agents/types.js.map +0 -1
  37. package/dist/bin-resolver.js +0 -92
  38. package/dist/bin-resolver.js.map +0 -1
  39. package/dist/config.js +0 -282
  40. package/dist/config.js.map +0 -1
  41. package/dist/editor.js +0 -40
  42. package/dist/editor.js.map +0 -1
  43. package/dist/gates.js +0 -121
  44. package/dist/gates.js.map +0 -1
  45. package/dist/ipc.js +0 -145
  46. package/dist/ipc.js.map +0 -1
  47. package/dist/open-folder.js +0 -28
  48. package/dist/open-folder.js.map +0 -1
  49. package/dist/phases.js +0 -346
  50. package/dist/phases.js.map +0 -1
  51. package/dist/progress.js +0 -184
  52. package/dist/progress.js.map +0 -1
  53. package/dist/source-inbox.js +0 -297
  54. package/dist/source-inbox.js.map +0 -1
  55. package/dist/types.js +0 -3
  56. package/dist/types.js.map +0 -1
  57. package/dist/version.js +0 -18
  58. package/dist/version.js.map +0 -1
  59. package/dist/yaml-io.js +0 -376
  60. package/dist/yaml-io.js.map +0 -1
  61. package/maintenance-skill/SKILL.md +0 -18
  62. package/maintenance-skill/agents/openai.yaml +0 -4
@@ -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