klyro 1.0.20 → 1.0.21
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/dist/agent/anthropic-adapter.js +9 -1
- package/dist/agent/child-worker.js +21 -7
- package/dist/agent/orchestrator.js +27 -4
- package/dist/agent/provider-adapter.js +5 -0
- package/dist/agent/retry.js +6 -3
- package/dist/agent/runtime.js +13 -6
- package/dist/agent/worktree-manager.d.ts +2 -0
- package/dist/agent/worktree-manager.js +9 -2
- package/dist/checkpoints/store.js +26 -2
- package/dist/cli/dotenv.js +1 -1
- package/dist/cli/repl.js +6 -3
- package/dist/eval/harness.js +8 -3
- package/dist/mcp/remote.js +31 -7
- package/dist/mcp/sse.js +0 -1
- package/dist/persistence/audit.d.ts +0 -6
- package/dist/persistence/audit.js +11 -18
- package/dist/persistence/store.js +19 -13
- package/dist/policy/engine.js +43 -7
- package/dist/providers/model-info.d.ts +4 -1
- package/dist/providers/model-info.js +10 -2
- package/dist/repl.js +4 -2
- package/dist/tools/git/run-git.js +23 -4
- package/dist/tools/shell/background.js +52 -23
- package/dist/tools/verify/run-verify.js +11 -1
- package/dist/tui/app.d.ts +5 -0
- package/dist/tui/app.js +156 -51
- package/dist/tui/app.test.js +21 -0
- package/dist/tui/diff-parser.js +28 -5
- package/dist/tui/markdown.js +1 -1
- package/dist/tui/measure.js +4 -1
- package/dist/tui/scroll-model.d.ts +2 -0
- package/dist/tui/scroll-model.js +5 -0
- package/dist/tui/status.d.ts +3 -0
- package/dist/verification/engine.js +35 -2
- package/dist/verification/scoped.js +20 -5
- package/package.json +1 -1
package/dist/tui/diff-parser.js
CHANGED
|
@@ -17,22 +17,45 @@
|
|
|
17
17
|
export function parseUnifiedDiff(raw) {
|
|
18
18
|
const out = [];
|
|
19
19
|
let current = null;
|
|
20
|
+
// Candidate path from the `--- ` side: used when the `+++ ` side is
|
|
21
|
+
// /dev/null (deleted file) and therefore names no real path.
|
|
22
|
+
let fromPath = null;
|
|
20
23
|
for (const line of raw.split('\n')) {
|
|
21
24
|
if (line.startsWith('diff --git ')) {
|
|
22
25
|
if (current)
|
|
23
26
|
out.push(current);
|
|
24
27
|
current = null;
|
|
28
|
+
fromPath = null;
|
|
25
29
|
continue;
|
|
26
30
|
}
|
|
27
31
|
if (line.startsWith('+++ ')) {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
const toRaw = line.slice(4).trim();
|
|
33
|
+
// A new file section without a `diff --git` header must not swallow
|
|
34
|
+
// the previous file's lines — flush first.
|
|
35
|
+
if (current && current.lines.length > 0) {
|
|
36
|
+
out.push(current);
|
|
37
|
+
current = null;
|
|
38
|
+
}
|
|
39
|
+
const toPath = stripDiffPrefix(toRaw);
|
|
40
|
+
if (toPath && toPath !== '/dev/null') {
|
|
41
|
+
current = { path: toPath, lines: [] };
|
|
42
|
+
}
|
|
43
|
+
else if (fromPath && fromPath !== '/dev/null') {
|
|
44
|
+
// Deleted file: `--- a/f` / `+++ /dev/null` — keep the real path,
|
|
45
|
+
// not `/dev/null`.
|
|
46
|
+
current = { path: fromPath, lines: [] };
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
current = null;
|
|
50
|
+
}
|
|
51
|
+
fromPath = null;
|
|
31
52
|
continue;
|
|
32
53
|
}
|
|
33
54
|
if (line.startsWith('--- ')) {
|
|
34
|
-
//
|
|
35
|
-
//
|
|
55
|
+
// Stash the "from" path (see `+++ ` handling above) — we still
|
|
56
|
+
// prefer the "to" path so the heading matches the edited file.
|
|
57
|
+
const cand = stripDiffPrefix(line.slice(4).trim());
|
|
58
|
+
fromPath = cand || null;
|
|
36
59
|
continue;
|
|
37
60
|
}
|
|
38
61
|
if (line.startsWith('@@')) {
|
package/dist/tui/markdown.js
CHANGED
|
@@ -146,7 +146,7 @@ export function highlightCodeLine(line, lang) {
|
|
|
146
146
|
return parts;
|
|
147
147
|
}
|
|
148
148
|
/** Detect `file:line` / `file:line:col` references and attach OSC-8 hrefs. */
|
|
149
|
-
export const FILE_LINE_RE = /((?:\.{0,2}\/)?[\w./-]+\.[a-zA-Z]+\w*):(\d+)(?::(\d+))?/g;
|
|
149
|
+
export const FILE_LINE_RE = /(?<![:/\w])((?:\.{0,2}\/)?[\w./-]+\.[a-zA-Z]+\w*):(\d+)(?::(\d+))?/g;
|
|
150
150
|
export function annotateFileLinks(parts) {
|
|
151
151
|
const out = [];
|
|
152
152
|
for (const p of parts) {
|
package/dist/tui/measure.js
CHANGED
|
@@ -85,8 +85,11 @@ export function blockHeight(b, termWidth) {
|
|
|
85
85
|
return 1 + 1;
|
|
86
86
|
case 'diff': {
|
|
87
87
|
let n = 1; // summary
|
|
88
|
+
// Render prepends a 2-col `+ `/`- `/` ` prefix per line (app.tsx),
|
|
89
|
+
// so measure against cw-2 — a bare-text wrap at full cw under-counts
|
|
90
|
+
// borderline lines by a row each and drifts the scroll anchor.
|
|
88
91
|
for (const h of b.hunks)
|
|
89
|
-
n += 1 + h.lines.reduce((s, l) => s + wrapCount(l, cw), 0);
|
|
92
|
+
n += 1 + h.lines.reduce((s, l) => s + wrapCount(l, cw - 2), 0);
|
|
90
93
|
return n + 1;
|
|
91
94
|
}
|
|
92
95
|
case 'plan':
|
package/dist/tui/scroll-model.js
CHANGED
|
@@ -65,6 +65,11 @@ export function scrollReducer(s, a, ctx) {
|
|
|
65
65
|
}
|
|
66
66
|
void maxTop;
|
|
67
67
|
return { ...s, newSinceUnstick: s.newSinceUnstick + a.lines };
|
|
68
|
+
case 'PRUNE':
|
|
69
|
+
// Anchored item was compacted away: re-stick to bottom with a clean
|
|
70
|
+
// counter. Without the reset, a stale newSinceUnstick inflates the
|
|
71
|
+
// `↓ N new` badge after the re-stick.
|
|
72
|
+
return stickBottom();
|
|
68
73
|
case 'REFLOW':
|
|
69
74
|
return { ...s };
|
|
70
75
|
}
|
package/dist/tui/status.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export interface StatusSnapshot {
|
|
|
11
11
|
maxSteps: number;
|
|
12
12
|
usageInput: number;
|
|
13
13
|
usageOutput: number;
|
|
14
|
+
/** Prompt-cache counters (Anthropic-family): billed and context-consuming. */
|
|
15
|
+
usageCacheRead?: number;
|
|
16
|
+
usageCacheWrite?: number;
|
|
14
17
|
repairs: number;
|
|
15
18
|
status: 'idle' | 'running' | 'done' | 'error' | 'aborted';
|
|
16
19
|
errorMessage?: string;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* On failure: returns { ok: false, failure } with the most recent
|
|
8
8
|
* structured Failure (and the model has had up to N attempts to fix).
|
|
9
9
|
*/
|
|
10
|
-
import { spawn } from 'node:child_process';
|
|
10
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
11
11
|
import { detect, summarize } from './detect.js';
|
|
12
12
|
import { filteredVerifyEnv } from './registry.js';
|
|
13
13
|
import { redact } from '../policy/secret-redactor.js';
|
|
@@ -84,10 +84,26 @@ export async function verify(opts) {
|
|
|
84
84
|
const outCap = cappedOutput(MAX_VERIFY_BYTES);
|
|
85
85
|
const errCap = cappedOutput(MAX_VERIFY_BYTES);
|
|
86
86
|
let done = false;
|
|
87
|
+
const killTree = () => {
|
|
88
|
+
// Match run_verify/shell_exec: tree-kill so grandchildren die too.
|
|
89
|
+
// child.kill() alone only takes the /bin/sh (or cmd) wrapper.
|
|
90
|
+
if (process.platform === 'win32') {
|
|
91
|
+
try {
|
|
92
|
+
spawnSync('taskkill', ['/F', '/T', '/PID', String(child.pid)], { windowsHide: true });
|
|
93
|
+
}
|
|
94
|
+
catch { /* already gone */ }
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
try {
|
|
98
|
+
child.kill('SIGKILL');
|
|
99
|
+
}
|
|
100
|
+
catch { /* noop */ }
|
|
101
|
+
}
|
|
102
|
+
};
|
|
87
103
|
const timer = setTimeout(() => {
|
|
88
104
|
if (done)
|
|
89
105
|
return;
|
|
90
|
-
|
|
106
|
+
killTree();
|
|
91
107
|
done = true;
|
|
92
108
|
const so = outCap.text();
|
|
93
109
|
const se = errCap.text();
|
|
@@ -103,6 +119,23 @@ export async function verify(opts) {
|
|
|
103
119
|
}, timeout);
|
|
104
120
|
child.stdout.on('data', (b) => outCap.push(b));
|
|
105
121
|
child.stderr.on('data', (b) => errCap.push(b));
|
|
122
|
+
// A bad cwd (or missing shell) emits 'error' with no 'close' — without
|
|
123
|
+
// this the promise never settles and the run hangs to its own timeout.
|
|
124
|
+
child.on('error', (err) => {
|
|
125
|
+
if (done)
|
|
126
|
+
return;
|
|
127
|
+
done = true;
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
const raw = `verify spawn failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
130
|
+
emitFailed('runtime');
|
|
131
|
+
resolve({
|
|
132
|
+
ok: false,
|
|
133
|
+
exitCode: -1,
|
|
134
|
+
stdout: outCap.text(),
|
|
135
|
+
stderr: raw,
|
|
136
|
+
failure: { type: 'runtime', files: [], raw, exitCode: -1 },
|
|
137
|
+
});
|
|
138
|
+
});
|
|
106
139
|
child.on('close', (code) => {
|
|
107
140
|
if (done)
|
|
108
141
|
return;
|
|
@@ -39,13 +39,25 @@ export function findRelatedTests(cwd, editedFiles) {
|
|
|
39
39
|
const dir = path.dirname(edited);
|
|
40
40
|
for (const t of allTests) {
|
|
41
41
|
const tb = path.basename(t);
|
|
42
|
+
const stripped = path.basename(t, path.extname(t)).replace(/\.test|\.spec|test_/g, '');
|
|
42
43
|
// name match: foo.ts -> foo.test.ts, test_foo.py, foo_spec.ts, etc.
|
|
43
|
-
|
|
44
|
+
// Substring arms need a substantive core (>=4 chars): without the
|
|
45
|
+
// floor, `a.ts` matches nearly every test basename globally, and
|
|
46
|
+
// `catalog.ts` matches unrelated `cat.test.ts`. Exact stem equality
|
|
47
|
+
// always counts (covers single-char stems like `a.ts` -> `a.test.ts`).
|
|
48
|
+
if (stripped === base ||
|
|
49
|
+
(base.length >= 4 && tb.includes(base)) ||
|
|
50
|
+
(stripped.length >= 4 && base.includes(stripped))) {
|
|
44
51
|
related.add(path.relative(cwd, t));
|
|
45
52
|
}
|
|
46
|
-
// same
|
|
47
|
-
|
|
53
|
+
// same-directory similarity (not a raw string prefix: `/repo/foo2/…`
|
|
54
|
+
// must not match edited dir `/repo/foo`). The 4-char slice needs a
|
|
55
|
+
// real core name behind it, same floor as above.
|
|
56
|
+
const rel = path.relative(dir, t);
|
|
57
|
+
const inDir = rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
58
|
+
if (inDir && base.length >= 4 && tb.toLowerCase().includes(base.slice(0, 4).toLowerCase())) {
|
|
48
59
|
related.add(path.relative(cwd, t));
|
|
60
|
+
}
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
63
|
return [...related];
|
|
@@ -222,6 +234,7 @@ export async function syntaxCheck(cwd, file) {
|
|
|
222
234
|
}
|
|
223
235
|
}
|
|
224
236
|
if (ext === '.py') {
|
|
237
|
+
let spawnFailed = false;
|
|
225
238
|
const ok = await new Promise((resolve) => {
|
|
226
239
|
// S2: verify commands run with filtered env; servers needing keys must use explicit config.
|
|
227
240
|
const child = spawn('python', ['-m', 'py_compile', full], { cwd, shell: false, env: filteredVerifyEnv() });
|
|
@@ -236,11 +249,13 @@ export async function syntaxCheck(cwd, file) {
|
|
|
236
249
|
} }, 5000);
|
|
237
250
|
child.on('close', (code) => { if (done)
|
|
238
251
|
return; done = true; clearTimeout(t); resolve(code === 0); });
|
|
252
|
+
// A missing interpreter must not read as "syntax OK" — that false
|
|
253
|
+
// negative lets broken files pass scoped verification.
|
|
239
254
|
child.on('error', () => { if (done)
|
|
240
|
-
return; done = true; clearTimeout(t); resolve(
|
|
255
|
+
return; done = true; clearTimeout(t); spawnFailed = true; resolve(false); });
|
|
241
256
|
});
|
|
242
257
|
if (!ok)
|
|
243
|
-
return { ok: false, error: `syntax error in ${file} (py_compile)` };
|
|
258
|
+
return { ok: false, error: spawnFailed ? `python unavailable for py_compile (${file})` : `syntax error in ${file} (py_compile)` };
|
|
244
259
|
}
|
|
245
260
|
return { ok: true };
|
|
246
261
|
}
|