linersh 0.5.7 → 1.0.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 +65 -165
- package/bin/liner.js +82 -44
- package/cli-update-docs/ABOUT.md +16 -16
- package/cli-update-docs/AI-HANDOFF.md +62 -204
- package/cli-update-docs/CURATION.md +48 -19
- package/cli-update-docs/JTBD-MASTER-PROMPT-ABOUT.md +9 -12
- package/cli-update-docs/JTBD-MASTER-PROMPT.md +1 -1
- package/cli-update-docs/LINER-MASTER.md +77 -63
- package/cli-update-docs/LINER-MD-PRD.md +531 -0
- package/cli-update-docs/MIXTAPE-FORMAT.md +76 -44
- package/cli-update-docs/PLATFORM.md +47 -46
- package/cli-update-docs/README.md +6 -6
- package/cli-update-docs/SKILL.md +72 -37
- package/cli-update-docs/TUI-BACKLOG.md +205 -26
- package/cli-update-docs/quality-check-tests.md +256 -8
- package/cli-update-docs/source-finding-tactics.md +140 -3
- package/cli-update-docs/source-quality-hierarchy.md +83 -0
- package/cli-update-docs/synthesis-guidance.md +19 -5
- package/dist/agents/artifact-validation.js +440 -0
- package/dist/agents/artifact-validation.js.map +1 -0
- package/dist/agents/candidate-longlist.js +100 -0
- package/dist/agents/candidate-longlist.js.map +1 -0
- package/dist/agents/detect.js +3 -2
- package/dist/agents/detect.js.map +1 -1
- package/dist/agents/evaluation-assembly.js +270 -0
- package/dist/agents/evaluation-assembly.js.map +1 -0
- package/dist/agents/headless-runner.js +227 -0
- package/dist/agents/headless-runner.js.map +1 -0
- package/dist/agents/jtbd-clarify.js +42 -41
- package/dist/agents/jtbd-clarify.js.map +1 -1
- package/dist/agents/models.js +6 -7
- package/dist/agents/models.js.map +1 -1
- package/dist/agents/prompts.js +220 -20
- package/dist/agents/prompts.js.map +1 -1
- package/dist/agents/runner.js +462 -21
- package/dist/agents/runner.js.map +1 -1
- package/dist/phases.js +13 -13
- package/dist/phases.js.map +1 -1
- package/dist/source-inbox.js +297 -0
- package/dist/source-inbox.js.map +1 -0
- package/dist/version.js +3 -9
- package/dist/version.js.map +1 -1
- package/dist/yaml-io.js +42 -11
- package/dist/yaml-io.js.map +1 -1
- package/package.json +26 -23
- package/cli-update-docs/JOB-STORY-MIGRATION.md +0 -99
- package/dist/App.js +0 -220
- package/dist/App.js.map +0 -1
- package/dist/colors.js +0 -132
- package/dist/colors.js.map +0 -1
- package/dist/components/Header.js +0 -15
- package/dist/components/Header.js.map +0 -1
- package/dist/components/KeyHints.js +0 -17
- package/dist/components/KeyHints.js.map +0 -1
- package/dist/components/LabeledBox.js +0 -53
- package/dist/components/LabeledBox.js.map +0 -1
- package/dist/components/NextSteps.js +0 -18
- package/dist/components/NextSteps.js.map +0 -1
- package/dist/index.js +0 -17
- package/dist/index.js.map +0 -1
- package/dist/screens/AgentSetup.js +0 -224
- package/dist/screens/AgentSetup.js.map +0 -1
- package/dist/screens/BootSplash.js +0 -163
- package/dist/screens/BootSplash.js.map +0 -1
- package/dist/screens/CompileView.js +0 -567
- package/dist/screens/CompileView.js.map +0 -1
- package/dist/screens/FramingConfirmReview.js +0 -197
- package/dist/screens/FramingConfirmReview.js.map +0 -1
- package/dist/screens/Gate1Review.js +0 -147
- package/dist/screens/Gate1Review.js.map +0 -1
- package/dist/screens/Gate2Review.js +0 -162
- package/dist/screens/Gate2Review.js.map +0 -1
- package/dist/screens/ImportPrompt.js +0 -173
- package/dist/screens/ImportPrompt.js.map +0 -1
- package/dist/screens/JsSetup.js +0 -77
- package/dist/screens/JsSetup.js.map +0 -1
- package/dist/screens/LocalFilePicker.js +0 -139
- package/dist/screens/LocalFilePicker.js.map +0 -1
- package/dist/screens/NewMixtapeWizard.js +0 -662
- package/dist/screens/NewMixtapeWizard.js.map +0 -1
- package/dist/screens/PhaseRunner.js +0 -1203
- package/dist/screens/PhaseRunner.js.map +0 -1
- package/dist/screens/ProcessManifest.js +0 -160
- package/dist/screens/ProcessManifest.js.map +0 -1
- package/dist/screens/ProjectHub.js +0 -557
- package/dist/screens/ProjectHub.js.map +0 -1
- package/dist/screens/SourceModal.js +0 -269
- package/dist/screens/SourceModal.js.map +0 -1
- package/dist/screens/TapeBrowser.js +0 -312
- package/dist/screens/TapeBrowser.js.map +0 -1
- package/dist/screens/TapeDraftReview.js +0 -444
- package/dist/screens/TapeDraftReview.js.map +0 -1
- package/dist/screens/TapeEditor.js +0 -310
- package/dist/screens/TapeEditor.js.map +0 -1
- package/dist/screens/cassette-art.js +0 -72
- package/dist/screens/cassette-art.js.map +0 -1
- package/dist/store.js +0 -10
- package/dist/store.js.map +0 -1
package/dist/agents/runner.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
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";
|
|
3
8
|
import { LineBuffer, parseLine } from "./parsers.js";
|
|
4
9
|
import { closeRunLog, openRunLog } from "./run-log.js";
|
|
5
10
|
import { modelForPhase } from "./models.js";
|
|
@@ -8,6 +13,7 @@ import { readConfig } from "../config.js";
|
|
|
8
13
|
// unreachable. Generous enough to cover a cold CLI start + auth refresh, short
|
|
9
14
|
// enough that a dead subscription doesn't hang the TUI indefinitely.
|
|
10
15
|
const WATCHDOG_MS = 15_000;
|
|
16
|
+
const DEFAULT_EVALUATION_CLOSURE_IDLE_MS = 120_000;
|
|
11
17
|
/**
|
|
12
18
|
* Spawn the configured agent CLI with a phase-specific prompt. The agent's
|
|
13
19
|
* newline-delimited JSON stdout is parsed into AgentEvents so the PhaseRunner
|
|
@@ -17,6 +23,9 @@ const WATCHDOG_MS = 15_000;
|
|
|
17
23
|
* `buildPhasePrompt` and labels the audit log with the phase id.
|
|
18
24
|
*/
|
|
19
25
|
export function runPhaseWithAgent(args) {
|
|
26
|
+
if (args.phaseId === "evaluation" && (!args.resume || hasSectionedEvaluationState(args.project))) {
|
|
27
|
+
return runEvaluationSectionsWithAgent(args);
|
|
28
|
+
}
|
|
20
29
|
const prompt = buildPhasePrompt({
|
|
21
30
|
phaseId: args.phaseId,
|
|
22
31
|
project: args.project,
|
|
@@ -28,7 +37,7 @@ export function runPhaseWithAgent(args) {
|
|
|
28
37
|
// run; gates don't reach here either.
|
|
29
38
|
const overrides = readConfig().models?.[args.agent.id];
|
|
30
39
|
const model = modelForPhase(args.agent.id, args.phaseId, overrides);
|
|
31
|
-
|
|
40
|
+
const handle = runAgentTask({
|
|
32
41
|
agent: args.agent,
|
|
33
42
|
project: args.project,
|
|
34
43
|
skillPath: args.skillPath,
|
|
@@ -38,6 +47,129 @@ export function runPhaseWithAgent(args) {
|
|
|
38
47
|
resume: args.resume,
|
|
39
48
|
onEvent: args.onEvent,
|
|
40
49
|
});
|
|
50
|
+
return {
|
|
51
|
+
cancel: handle.cancel,
|
|
52
|
+
done: handle.done.then((result) => {
|
|
53
|
+
if (result.code !== 0)
|
|
54
|
+
return result;
|
|
55
|
+
const validation = ensurePhaseArtifact(args.project, args.phaseId);
|
|
56
|
+
if (validation.ok) {
|
|
57
|
+
if ("message" in validation && validation.message) {
|
|
58
|
+
args.onEvent({ kind: "raw", text: `[liner] ${validation.message}` });
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
const message = `[liner] Artifact validation failed after ${args.phaseId}: ${validation.message}`;
|
|
63
|
+
args.onEvent({ kind: "text", text: message });
|
|
64
|
+
return {
|
|
65
|
+
code: 1,
|
|
66
|
+
stderr: result.stderr ? `${result.stderr}\n${message}` : message,
|
|
67
|
+
};
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function runEvaluationSectionsWithAgent(args) {
|
|
72
|
+
if (args.resume) {
|
|
73
|
+
const existingEvaluation = ensureEvaluationArtifact(args.project);
|
|
74
|
+
if (existingEvaluation.ok) {
|
|
75
|
+
const message = existingEvaluation.message || "Evaluation artifact already complete.";
|
|
76
|
+
args.onEvent({ kind: "raw", text: `[liner] ${message}` });
|
|
77
|
+
return { cancel: () => { }, done: Promise.resolve({ code: 0, stderr: "" }) };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const groups = groupCandidateLonglist(readCandidateLonglist(join(args.project, "working/02-candidate-longlist.md")));
|
|
81
|
+
if (groups.length === 0) {
|
|
82
|
+
const message = "[liner] Cannot run Evaluation: working/02-candidate-longlist.md has no URL candidates.";
|
|
83
|
+
args.onEvent({ kind: "raw", text: message });
|
|
84
|
+
return { cancel: () => { }, done: Promise.resolve({ code: 1, stderr: message }) };
|
|
85
|
+
}
|
|
86
|
+
const overrides = readConfig().models?.[args.agent.id];
|
|
87
|
+
const model = modelForPhase(args.agent.id, "evaluation", overrides);
|
|
88
|
+
let cancelCurrent = () => { };
|
|
89
|
+
let cancelled = false;
|
|
90
|
+
const done = (async () => {
|
|
91
|
+
markSectionedEvaluationStarted(args.project, groups.length);
|
|
92
|
+
args.onEvent({
|
|
93
|
+
kind: "raw",
|
|
94
|
+
text: `[liner] Running Evaluation in ${groups.length} section chunk${groups.length === 1 ? "" : "s"}.`,
|
|
95
|
+
});
|
|
96
|
+
for (const group of groups) {
|
|
97
|
+
if (cancelled)
|
|
98
|
+
return { code: 1, stderr: "[liner] Evaluation cancelled." };
|
|
99
|
+
const fragment = validateEvaluationFragment(join(args.project, group.fragmentPath), group.candidates);
|
|
100
|
+
if (fragment.ok) {
|
|
101
|
+
args.onEvent({
|
|
102
|
+
kind: "raw",
|
|
103
|
+
text: `[liner] Evaluation chunk ${group.index}/${group.total} already complete: ` +
|
|
104
|
+
`${group.section} (${fragment.count} candidates).`,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
args.onEvent({
|
|
109
|
+
kind: "raw",
|
|
110
|
+
text: `[liner] Evaluation chunk ${group.index}/${group.total}: ` +
|
|
111
|
+
`${group.section} (${group.candidates.length} candidates).`,
|
|
112
|
+
});
|
|
113
|
+
const handle = runAgentTask({
|
|
114
|
+
agent: args.agent,
|
|
115
|
+
project: args.project,
|
|
116
|
+
skillPath: args.skillPath,
|
|
117
|
+
prompt: buildEvaluationSectionPrompt({
|
|
118
|
+
project: args.project,
|
|
119
|
+
skillPath: args.skillPath,
|
|
120
|
+
tape: args.tape,
|
|
121
|
+
group,
|
|
122
|
+
}),
|
|
123
|
+
model,
|
|
124
|
+
taskLabel: "evaluation",
|
|
125
|
+
onEvent: args.onEvent,
|
|
126
|
+
});
|
|
127
|
+
cancelCurrent = handle.cancel;
|
|
128
|
+
const result = await handle.done;
|
|
129
|
+
if (result.code !== 0)
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
const validation = ensureEvaluationArtifact(args.project);
|
|
133
|
+
if (!validation.ok) {
|
|
134
|
+
const message = `[liner] Artifact validation failed after evaluation: ${validation.message}`;
|
|
135
|
+
args.onEvent({ kind: "text", text: message });
|
|
136
|
+
return { code: 1, stderr: message };
|
|
137
|
+
}
|
|
138
|
+
if (validation.message) {
|
|
139
|
+
args.onEvent({ kind: "raw", text: `[liner] ${validation.message}` });
|
|
140
|
+
}
|
|
141
|
+
return { code: 0, stderr: "" };
|
|
142
|
+
})();
|
|
143
|
+
return {
|
|
144
|
+
cancel: () => {
|
|
145
|
+
cancelled = true;
|
|
146
|
+
cancelCurrent();
|
|
147
|
+
},
|
|
148
|
+
done,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function hasSectionedEvaluationState(project) {
|
|
152
|
+
if (existsSync(join(project, "working/evaluation-decisions/.liner-sectioned-evaluation.json"))) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
const dir = join(project, "working/evaluation-decisions");
|
|
156
|
+
if (!existsSync(dir))
|
|
157
|
+
return false;
|
|
158
|
+
return readdirSync(dir).some((file) => /\.ya?ml$/i.test(file));
|
|
159
|
+
}
|
|
160
|
+
function markSectionedEvaluationStarted(project, chunkCount) {
|
|
161
|
+
const dir = join(project, "working/evaluation-decisions");
|
|
162
|
+
mkdirSync(dir, { recursive: true });
|
|
163
|
+
writeFileSync(join(dir, ".liner-sectioned-evaluation.json"), JSON.stringify({
|
|
164
|
+
version: 1,
|
|
165
|
+
chunkCount,
|
|
166
|
+
updatedAt: new Date().toISOString(),
|
|
167
|
+
}, null, 2) + "\n", "utf8");
|
|
168
|
+
}
|
|
169
|
+
function ensurePhaseArtifact(project, phaseId) {
|
|
170
|
+
if (phaseId !== "evaluation")
|
|
171
|
+
return validatePhaseArtifact(project, phaseId);
|
|
172
|
+
return ensureEvaluationArtifact(project);
|
|
41
173
|
}
|
|
42
174
|
/**
|
|
43
175
|
* Run a single agent task with a caller-supplied prompt. Used by methodology
|
|
@@ -50,11 +182,14 @@ export function runAgentTask(args) {
|
|
|
50
182
|
// Delegating cancel: a model-rejection fallback re-spawns under the hood, so
|
|
51
183
|
// the handle's cancel must target whichever child is currently live.
|
|
52
184
|
let cancelCurrent = () => { };
|
|
53
|
-
const done = runAttempt(args,
|
|
185
|
+
const done = runAttempt(args, {}, (c) => {
|
|
54
186
|
cancelCurrent = c;
|
|
55
187
|
});
|
|
56
188
|
return { cancel: () => cancelCurrent(), done };
|
|
57
189
|
}
|
|
190
|
+
const CLAUDE_METHOD_TOOLS = "Read Write Edit Glob Grep WebFetch";
|
|
191
|
+
const CLAUDE_METHOD_BUILTIN_TOOLS = "Read,Write,Edit,Glob,Grep,WebFetch";
|
|
192
|
+
const CLAUDE_HARD_DENIED_TOOLS = "Bash Task WebSearch ToolSearch";
|
|
58
193
|
/**
|
|
59
194
|
* One spawn attempt. On a model-rejection failure — and only on a fresh,
|
|
60
195
|
* model-pinned run — it warns the curator with the exact fix and retries once
|
|
@@ -62,10 +197,25 @@ export function runAgentTask(args) {
|
|
|
62
197
|
* degrades to "ran on the default" instead of a hard failure. `registerCancel`
|
|
63
198
|
* is re-invoked per attempt so the caller's cancel always hits the live child.
|
|
64
199
|
*/
|
|
65
|
-
function runAttempt(args,
|
|
200
|
+
function runAttempt(args, attempt, registerCancel) {
|
|
66
201
|
const command = args.agent.bin;
|
|
67
202
|
const resume = args.resume === true;
|
|
68
|
-
const
|
|
203
|
+
const resumeSessionId = args.agent.id === "codex" && resume
|
|
204
|
+
? findLatestCodexSessionId(args.project, args.taskLabel)
|
|
205
|
+
: undefined;
|
|
206
|
+
if (args.agent.id === "codex" && resume && !resumeSessionId) {
|
|
207
|
+
const message = `[liner] Cannot resume Codex ${args.taskLabel}: no prior Codex session id was found ` +
|
|
208
|
+
`in .liner-runs/${args.taskLabel}. Retry this phase from a fresh run instead.`;
|
|
209
|
+
args.onEvent({ kind: "raw", text: message });
|
|
210
|
+
const log = openRunLog(args.project, args.taskLabel, {
|
|
211
|
+
agent: args.agent.id,
|
|
212
|
+
resume,
|
|
213
|
+
});
|
|
214
|
+
log.write(JSON.stringify({ type: "_liner_error", message }));
|
|
215
|
+
closeRunLog(log, 1, message.length);
|
|
216
|
+
return Promise.resolve({ code: 1, stderr: message });
|
|
217
|
+
}
|
|
218
|
+
const cliArgs = buildArgs(args.agent.id, args.project, args.skillPath, resume, args.model, resumeSessionId);
|
|
69
219
|
const child = spawn(command, cliArgs, {
|
|
70
220
|
stdio: ["pipe", "pipe", "pipe"],
|
|
71
221
|
cwd: args.project,
|
|
@@ -101,7 +251,7 @@ function runAttempt(args, isModelFallback, registerCancel) {
|
|
|
101
251
|
// re-shipping the full prompt — that's the whole point of resume (cheaper,
|
|
102
252
|
// no re-fetching).
|
|
103
253
|
const stdinPayload = resume
|
|
104
|
-
?
|
|
254
|
+
? args.resumePrompt ?? resumePromptForTask(args.taskLabel)
|
|
105
255
|
: args.prompt;
|
|
106
256
|
child.stdin?.write(stdinPayload, "utf8");
|
|
107
257
|
child.stdin?.end();
|
|
@@ -140,6 +290,119 @@ function runAttempt(args, isModelFallback, registerCancel) {
|
|
|
140
290
|
sawOutput = true;
|
|
141
291
|
clearTimeout(watchdog);
|
|
142
292
|
};
|
|
293
|
+
const closureIdleMs = evaluationClosureIdleMs();
|
|
294
|
+
const canCloseEvaluationArtifact = args.taskLabel === "evaluation" &&
|
|
295
|
+
!attempt.artifactClosure &&
|
|
296
|
+
closureIdleMs > 0;
|
|
297
|
+
let evaluationClosureTimer;
|
|
298
|
+
let evaluationClosureTimedOut = false;
|
|
299
|
+
let childClosed = false;
|
|
300
|
+
let forcedEvaluationResult;
|
|
301
|
+
const activeTools = new Set();
|
|
302
|
+
const clearEvaluationClosureTimer = () => {
|
|
303
|
+
if (!evaluationClosureTimer)
|
|
304
|
+
return;
|
|
305
|
+
clearTimeout(evaluationClosureTimer);
|
|
306
|
+
evaluationClosureTimer = undefined;
|
|
307
|
+
};
|
|
308
|
+
const scheduleEvaluationClosureCheck = () => {
|
|
309
|
+
if (!canCloseEvaluationArtifact || cancelled || child.killed || childClosed)
|
|
310
|
+
return;
|
|
311
|
+
clearEvaluationClosureTimer();
|
|
312
|
+
evaluationClosureTimer = setTimeout(() => {
|
|
313
|
+
if (cancelled || child.killed)
|
|
314
|
+
return;
|
|
315
|
+
if (activeTools.size > 0) {
|
|
316
|
+
scheduleEvaluationClosureCheck();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const validation = ensureEvaluationArtifact(args.project);
|
|
320
|
+
if (validation.ok) {
|
|
321
|
+
forcedEvaluationResult = {
|
|
322
|
+
code: 0,
|
|
323
|
+
message: "[liner] Evaluation artifact is valid; stopping the quiet agent turn.",
|
|
324
|
+
};
|
|
325
|
+
args.onEvent({ kind: "raw", text: forcedEvaluationResult.message });
|
|
326
|
+
log.write(JSON.stringify({
|
|
327
|
+
type: "_liner_artifact_closure_complete",
|
|
328
|
+
phase: "evaluation",
|
|
329
|
+
ms: closureIdleMs,
|
|
330
|
+
}));
|
|
331
|
+
child.kill("SIGTERM");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
evaluationClosureTimedOut = true;
|
|
335
|
+
const idleLabel = closureIdleMs >= 1000 ? `${Math.round(closureIdleMs / 1000)}s` : `${closureIdleMs}ms`;
|
|
336
|
+
const message = `[liner] Evaluation has been quiet for ${idleLabel} ` +
|
|
337
|
+
`with no valid working/03-evaluation.yaml. Stopping this turn and resuming once ` +
|
|
338
|
+
`with a write-only artifact closure prompt.`;
|
|
339
|
+
args.onEvent({ kind: "raw", text: message });
|
|
340
|
+
log.write(JSON.stringify({
|
|
341
|
+
type: "_liner_artifact_closure_timeout",
|
|
342
|
+
phase: "evaluation",
|
|
343
|
+
ms: closureIdleMs,
|
|
344
|
+
validation: validation.message,
|
|
345
|
+
}));
|
|
346
|
+
child.kill("SIGTERM");
|
|
347
|
+
}, closureIdleMs);
|
|
348
|
+
evaluationClosureTimer.unref?.();
|
|
349
|
+
};
|
|
350
|
+
const noteAgentEvent = (event) => {
|
|
351
|
+
if (!canCloseEvaluationArtifact)
|
|
352
|
+
return;
|
|
353
|
+
if (event.kind === "tool_start" && event.id) {
|
|
354
|
+
activeTools.add(event.id);
|
|
355
|
+
scheduleEvaluationClosureCheck();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (event.kind === "tool_done" && event.id) {
|
|
359
|
+
activeTools.delete(event.id);
|
|
360
|
+
scheduleEvaluationClosureCheck();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (event.kind === "summary") {
|
|
364
|
+
clearEvaluationClosureTimer();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
scheduleEvaluationClosureCheck();
|
|
368
|
+
};
|
|
369
|
+
if (args.taskLabel === "evaluation" && attempt.artifactClosure && closureIdleMs > 0) {
|
|
370
|
+
clearEvaluationClosureTimer();
|
|
371
|
+
evaluationClosureTimer = setTimeout(() => {
|
|
372
|
+
if (cancelled || child.killed || childClosed)
|
|
373
|
+
return;
|
|
374
|
+
const validation = ensureEvaluationArtifact(args.project);
|
|
375
|
+
const idleLabel = closureIdleMs >= 1000 ? `${Math.round(closureIdleMs / 1000)}s` : `${closureIdleMs}ms`;
|
|
376
|
+
if (validation.ok) {
|
|
377
|
+
forcedEvaluationResult = {
|
|
378
|
+
code: 0,
|
|
379
|
+
message: `[liner] Evaluation artifact closure produced a valid YAML file within ${idleLabel}; stopping the idle closure turn.`,
|
|
380
|
+
};
|
|
381
|
+
args.onEvent({ kind: "raw", text: forcedEvaluationResult.message });
|
|
382
|
+
log.write(JSON.stringify({
|
|
383
|
+
type: "_liner_artifact_closure_complete",
|
|
384
|
+
phase: "evaluation",
|
|
385
|
+
ms: closureIdleMs,
|
|
386
|
+
}));
|
|
387
|
+
child.kill("SIGTERM");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
forcedEvaluationResult = {
|
|
391
|
+
code: 1,
|
|
392
|
+
message: `[liner] Evaluation artifact closure timed out after ${idleLabel}: ${validation.message}. ` +
|
|
393
|
+
"Retry Evaluation fresh or reduce the candidate set before continuing.",
|
|
394
|
+
};
|
|
395
|
+
args.onEvent({ kind: "raw", text: forcedEvaluationResult.message });
|
|
396
|
+
log.write(JSON.stringify({
|
|
397
|
+
type: "_liner_artifact_closure_failed",
|
|
398
|
+
phase: "evaluation",
|
|
399
|
+
ms: closureIdleMs,
|
|
400
|
+
validation: validation.message,
|
|
401
|
+
}));
|
|
402
|
+
child.kill("SIGTERM");
|
|
403
|
+
}, closureIdleMs);
|
|
404
|
+
evaluationClosureTimer.unref?.();
|
|
405
|
+
}
|
|
143
406
|
const buffer = new LineBuffer();
|
|
144
407
|
if (child.stdout) {
|
|
145
408
|
child.stdout.setEncoding("utf8");
|
|
@@ -150,6 +413,7 @@ function runAttempt(args, isModelFallback, registerCancel) {
|
|
|
150
413
|
for (const line of lines) {
|
|
151
414
|
log.write(line);
|
|
152
415
|
for (const event of parseLine(args.agent.id, line)) {
|
|
416
|
+
noteAgentEvent(event);
|
|
153
417
|
args.onEvent(event);
|
|
154
418
|
}
|
|
155
419
|
}
|
|
@@ -162,14 +426,18 @@ function runAttempt(args, isModelFallback, registerCancel) {
|
|
|
162
426
|
noteOutput();
|
|
163
427
|
noteForDetection(chunk);
|
|
164
428
|
stderr += chunk;
|
|
165
|
-
// Surface stderr as raw events so users see install/auth
|
|
429
|
+
// Surface meaningful stderr as raw events so users see install/auth
|
|
430
|
+
// hints inline, while keeping repetitive cosmetic Codex startup warnings
|
|
431
|
+
// out of the live TUI. The full stream is still recorded in the run log.
|
|
166
432
|
for (const line of chunk.split("\n")) {
|
|
167
433
|
const trimmed = line.trim();
|
|
168
434
|
if (trimmed) {
|
|
169
|
-
args.onEvent({ kind: "raw", text: trimmed });
|
|
170
435
|
// Record stderr in the log too — wrapped so it doesn't masquerade
|
|
171
436
|
// as a real JSON event when the log is replayed.
|
|
172
437
|
log.write(JSON.stringify({ type: "_liner_stderr", text: trimmed }));
|
|
438
|
+
if (shouldSurfaceAgentStderr(args.agent.id, trimmed)) {
|
|
439
|
+
args.onEvent({ kind: "raw", text: trimmed });
|
|
440
|
+
}
|
|
173
441
|
}
|
|
174
442
|
}
|
|
175
443
|
});
|
|
@@ -177,14 +445,40 @@ function runAttempt(args, isModelFallback, registerCancel) {
|
|
|
177
445
|
return new Promise((resolve) => {
|
|
178
446
|
child.on("close", (code) => {
|
|
179
447
|
clearTimeout(watchdog);
|
|
448
|
+
clearEvaluationClosureTimer();
|
|
449
|
+
childClosed = true;
|
|
180
450
|
// Flush any trailing partial line.
|
|
181
451
|
for (const line of buffer.flush()) {
|
|
182
452
|
log.write(line);
|
|
183
453
|
for (const event of parseLine(args.agent.id, line)) {
|
|
454
|
+
noteAgentEvent(event);
|
|
184
455
|
args.onEvent(event);
|
|
185
456
|
}
|
|
186
457
|
}
|
|
187
458
|
closeRunLog(log, code, stderr.length);
|
|
459
|
+
if (forcedEvaluationResult) {
|
|
460
|
+
resolve({
|
|
461
|
+
code: forcedEvaluationResult.code,
|
|
462
|
+
stderr: forcedEvaluationResult.code === 0
|
|
463
|
+
? stderr
|
|
464
|
+
: stderr
|
|
465
|
+
? `${stderr}\n${forcedEvaluationResult.message}`
|
|
466
|
+
: forcedEvaluationResult.message,
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (evaluationClosureTimedOut && !cancelled) {
|
|
471
|
+
args.onEvent({
|
|
472
|
+
kind: "raw",
|
|
473
|
+
text: "[liner] Resuming Evaluation once to close working/03-evaluation.yaml. No more source fetching in this pass.",
|
|
474
|
+
});
|
|
475
|
+
resolve(runAttempt({
|
|
476
|
+
...args,
|
|
477
|
+
resume: true,
|
|
478
|
+
resumePrompt: evaluationArtifactClosurePrompt(),
|
|
479
|
+
}, { artifactClosure: true }, registerCancel));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
188
482
|
// Model-rejection fallback. If a fresh, model-pinned run failed because
|
|
189
483
|
// the agent didn't recognize the model (renamed / retired / mistyped in
|
|
190
484
|
// config), warn the curator with the exact fix, then retry once on the
|
|
@@ -194,7 +488,7 @@ function runAttempt(args, isModelFallback, registerCancel) {
|
|
|
194
488
|
if (code !== 0 &&
|
|
195
489
|
!cancelled &&
|
|
196
490
|
!resume &&
|
|
197
|
-
!
|
|
491
|
+
!attempt.modelFallback &&
|
|
198
492
|
args.model &&
|
|
199
493
|
looksLikeModelRejection(outputTail, args.model)) {
|
|
200
494
|
args.onEvent({
|
|
@@ -205,13 +499,14 @@ function runAttempt(args, isModelFallback, registerCancel) {
|
|
|
205
499
|
`~/.liner/config.yaml, or remove it to keep the default.`,
|
|
206
500
|
});
|
|
207
501
|
log.write(JSON.stringify({ type: "_liner_model_fallback", model: args.model }));
|
|
208
|
-
resolve(runAttempt({ ...args, model: undefined }, true, registerCancel));
|
|
502
|
+
resolve(runAttempt({ ...args, model: undefined }, { modelFallback: true }, registerCancel));
|
|
209
503
|
return;
|
|
210
504
|
}
|
|
211
505
|
resolve({ code, stderr });
|
|
212
506
|
});
|
|
213
507
|
child.on("error", (e) => {
|
|
214
508
|
clearTimeout(watchdog);
|
|
509
|
+
clearEvaluationClosureTimer();
|
|
215
510
|
args.onEvent({ kind: "raw", text: `[runner error] ${e.message}` });
|
|
216
511
|
log.write(JSON.stringify({ type: "_liner_error", message: e.message }));
|
|
217
512
|
closeRunLog(log, 1, stderr.length);
|
|
@@ -219,15 +514,56 @@ function runAttempt(args, isModelFallback, registerCancel) {
|
|
|
219
514
|
});
|
|
220
515
|
});
|
|
221
516
|
}
|
|
222
|
-
function
|
|
517
|
+
export function resumePromptForTask(taskLabel) {
|
|
518
|
+
const base = "Continue from where you left off. Pick up the same task; do not re-do tool calls you already completed." +
|
|
519
|
+
`\n\n${WORKSPACE_DISCIPLINE}`;
|
|
520
|
+
if (taskLabel === "candidates") {
|
|
521
|
+
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.`;
|
|
522
|
+
}
|
|
523
|
+
if (taskLabel === "evaluation") {
|
|
524
|
+
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.`;
|
|
525
|
+
}
|
|
526
|
+
if (taskLabel === "quality") {
|
|
527
|
+
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.`;
|
|
528
|
+
}
|
|
529
|
+
return base;
|
|
530
|
+
}
|
|
531
|
+
export function evaluationArtifactClosurePrompt() {
|
|
532
|
+
return [
|
|
533
|
+
"Continue Phase 4, but switch to artifact closure only.",
|
|
534
|
+
"",
|
|
535
|
+
WORKSPACE_DISCIPLINE,
|
|
536
|
+
"",
|
|
537
|
+
"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.",
|
|
538
|
+
"Write complete section-sized YAML fragments under working/evaluation-decisions/ now, or write working/03-evaluation.yaml directly if it is small enough.",
|
|
539
|
+
"Every URL candidate in working/02-candidate-longlist.md must appear exactly once across those fragments or in the final file.",
|
|
540
|
+
"Use decision: dropped for uncertain, duplicate, unavailable, or metadata-only candidates unless the prior evidence clearly supports kept or trim.",
|
|
541
|
+
"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.",
|
|
542
|
+
"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.",
|
|
543
|
+
"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.",
|
|
544
|
+
].join("\n");
|
|
545
|
+
}
|
|
546
|
+
export function evaluationClosureIdleMs(env = process.env) {
|
|
547
|
+
const raw = env["LINER_EVALUATION_CLOSURE_IDLE_MS"];
|
|
548
|
+
if (raw == null || raw.trim() === "")
|
|
549
|
+
return DEFAULT_EVALUATION_CLOSURE_IDLE_MS;
|
|
550
|
+
const parsed = Number(raw);
|
|
551
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
552
|
+
return DEFAULT_EVALUATION_CLOSURE_IDLE_MS;
|
|
553
|
+
return parsed;
|
|
554
|
+
}
|
|
555
|
+
export function buildArgs(id, project, skillPath, resume, model, resumeSessionId) {
|
|
223
556
|
if (id === "claude") {
|
|
224
557
|
// Stream-json requires --verbose. --include-partial-messages would give
|
|
225
558
|
// us mid-message chunking but the cost is ~10× more events; we let the
|
|
226
559
|
// assistant message arrive as one block per turn instead.
|
|
227
560
|
//
|
|
228
|
-
// Tool
|
|
229
|
-
// skipped
|
|
230
|
-
//
|
|
561
|
+
// Tool scoping keeps the agent's reach bounded even with permissions
|
|
562
|
+
// skipped. --tools constrains Claude's built-in tool surface shown in the
|
|
563
|
+
// init stream, while --allowedTools/--disallowedTools guard permission
|
|
564
|
+
// checks and any globally-provided names that may appear despite the
|
|
565
|
+
// built-in list. The methodology needs local file reads/writes plus
|
|
566
|
+
// targeted fetches; it should not shell out, launch subagents, or web-search.
|
|
231
567
|
//
|
|
232
568
|
// `--continue` (resume mode) picks up the most recent session in the
|
|
233
569
|
// current working directory. Since we spawn with cwd=project, "most
|
|
@@ -247,8 +583,13 @@ function buildArgs(id, project, skillPath, resume, model) {
|
|
|
247
583
|
skillPath,
|
|
248
584
|
"--strict-mcp-config",
|
|
249
585
|
"--dangerously-skip-permissions",
|
|
586
|
+
"--tools",
|
|
587
|
+
CLAUDE_METHOD_BUILTIN_TOOLS,
|
|
250
588
|
"--allowedTools",
|
|
251
|
-
|
|
589
|
+
CLAUDE_METHOD_TOOLS,
|
|
590
|
+
"--disallowedTools",
|
|
591
|
+
CLAUDE_HARD_DENIED_TOOLS,
|
|
592
|
+
"--disable-slash-commands",
|
|
252
593
|
];
|
|
253
594
|
// Apply the per-phase model on fresh runs only — a resumed session keeps
|
|
254
595
|
// the model it was started with, and `--model` alongside `--continue`
|
|
@@ -261,17 +602,17 @@ function buildArgs(id, project, skillPath, resume, model) {
|
|
|
261
602
|
}
|
|
262
603
|
if (id === "codex") {
|
|
263
604
|
if (resume) {
|
|
264
|
-
// Codex's `exec resume` subcommand
|
|
605
|
+
// Codex's `exec resume` subcommand has a narrower option set than fresh
|
|
606
|
+
// exec: there is no --cd or --sandbox flag here. Use Liner's recorded
|
|
607
|
+
// session id instead of --last so a resume cannot pick up unrelated
|
|
608
|
+
// Codex work from the user's machine.
|
|
609
|
+
const sessionArg = resumeSessionId ? [resumeSessionId] : ["--last"];
|
|
265
610
|
return [
|
|
266
611
|
"exec",
|
|
267
612
|
"resume",
|
|
268
|
-
"--last",
|
|
269
|
-
"--cd",
|
|
270
|
-
project,
|
|
271
613
|
"--skip-git-repo-check",
|
|
272
|
-
"-s",
|
|
273
|
-
"workspace-write",
|
|
274
614
|
"--json",
|
|
615
|
+
...sessionArg,
|
|
275
616
|
"-",
|
|
276
617
|
];
|
|
277
618
|
}
|
|
@@ -291,6 +632,78 @@ function buildArgs(id, project, skillPath, resume, model) {
|
|
|
291
632
|
}
|
|
292
633
|
return [];
|
|
293
634
|
}
|
|
635
|
+
export function findLatestCodexSessionId(project, taskLabel) {
|
|
636
|
+
const dir = join(project, ".liner-runs", taskLabel.replace(/[^a-zA-Z0-9_-]/g, "-") || "task");
|
|
637
|
+
if (!existsSync(dir))
|
|
638
|
+
return undefined;
|
|
639
|
+
const files = readdirSync(dir)
|
|
640
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
641
|
+
.map((name) => join(dir, name))
|
|
642
|
+
.filter((path) => {
|
|
643
|
+
try {
|
|
644
|
+
return statSync(path).isFile();
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
})
|
|
650
|
+
.sort((a, b) => {
|
|
651
|
+
try {
|
|
652
|
+
return statSync(b).mtimeMs - statSync(a).mtimeMs;
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
return 0;
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
const fallback = [];
|
|
659
|
+
for (const file of files) {
|
|
660
|
+
const candidate = codexSessionIdFromRunLog(file, false);
|
|
661
|
+
if (candidate)
|
|
662
|
+
return candidate;
|
|
663
|
+
const resumedCandidate = codexSessionIdFromRunLog(file, true);
|
|
664
|
+
if (resumedCandidate)
|
|
665
|
+
fallback.push(resumedCandidate);
|
|
666
|
+
}
|
|
667
|
+
return fallback[0];
|
|
668
|
+
}
|
|
669
|
+
function codexSessionIdFromRunLog(path, allowResumeLog) {
|
|
670
|
+
let metaAgent = "";
|
|
671
|
+
let metaResume = false;
|
|
672
|
+
try {
|
|
673
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
674
|
+
if (!line.trim())
|
|
675
|
+
continue;
|
|
676
|
+
let value;
|
|
677
|
+
try {
|
|
678
|
+
value = JSON.parse(line);
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (!value || typeof value !== "object")
|
|
684
|
+
continue;
|
|
685
|
+
const record = value;
|
|
686
|
+
if (record.type === "_liner_meta") {
|
|
687
|
+
metaAgent = typeof record.agent === "string" ? record.agent : "";
|
|
688
|
+
metaResume = record.resume === true;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (record.type !== "thread.started")
|
|
692
|
+
continue;
|
|
693
|
+
if (metaAgent !== "codex")
|
|
694
|
+
return undefined;
|
|
695
|
+
if (metaResume && !allowResumeLog)
|
|
696
|
+
return undefined;
|
|
697
|
+
return typeof record.thread_id === "string" && record.thread_id.trim() !== ""
|
|
698
|
+
? record.thread_id
|
|
699
|
+
: undefined;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
294
707
|
/**
|
|
295
708
|
* Heuristic: did this failure come from the agent not recognizing the model?
|
|
296
709
|
* Matches the common "unknown/invalid model" phrasings both CLIs emit, plus
|
|
@@ -323,4 +736,32 @@ export function looksLikeModelRejection(output, model) {
|
|
|
323
736
|
t.includes(m) &&
|
|
324
737
|
(t.includes("error") || t.includes("invalid") || t.includes("unknown") || t.includes("not found")));
|
|
325
738
|
}
|
|
739
|
+
export function shouldSurfaceAgentStderr(agent, line) {
|
|
740
|
+
const value = line.trim();
|
|
741
|
+
if (!value)
|
|
742
|
+
return false;
|
|
743
|
+
if (agent !== "codex")
|
|
744
|
+
return true;
|
|
745
|
+
return !isNoisyCodexStderr(value);
|
|
746
|
+
}
|
|
747
|
+
function isNoisyCodexStderr(line) {
|
|
748
|
+
const value = line.toLowerCase();
|
|
749
|
+
return (isCosmeticCodexStartupWarning(line) ||
|
|
750
|
+
isCodexGlobalSkillLoadWarning(value) ||
|
|
751
|
+
isNoisyCodexMcpTransport(value) ||
|
|
752
|
+
value.includes("rmcp::transport::auth") ||
|
|
753
|
+
(value.includes("oauth") && (value.includes("mcp") || value.includes("connector"))) ||
|
|
754
|
+
value.includes("write_stdin failed: stdin is closed"));
|
|
755
|
+
}
|
|
756
|
+
function isCodexGlobalSkillLoadWarning(value) {
|
|
757
|
+
return value.includes("failed to load skill");
|
|
758
|
+
}
|
|
759
|
+
function isNoisyCodexMcpTransport(value) {
|
|
760
|
+
return value.includes("rmcp::transport::worker") && value.includes("transport channel closed");
|
|
761
|
+
}
|
|
762
|
+
function isCosmeticCodexStartupWarning(line) {
|
|
763
|
+
return (line.includes("codex_core_plugins::manifest: ignoring interface.defaultPrompt") ||
|
|
764
|
+
line.includes("codex_core_skills::loader: ignoring interface.icon_small") ||
|
|
765
|
+
line.includes("codex_core_skills::loader: ignoring interface.icon_large"));
|
|
766
|
+
}
|
|
326
767
|
//# sourceMappingURL=runner.js.map
|