klyro 1.0.16 → 1.0.18
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 +26 -1
- package/dist/agent/provider-adapter.d.ts +7 -0
- package/dist/agent/provider-adapter.js +26 -1
- package/dist/agent/runtime.d.ts +14 -0
- package/dist/agent/runtime.js +59 -22
- package/dist/chat.js +16 -56
- package/dist/checkpoints/store.d.ts +15 -1
- package/dist/checkpoints/store.js +7 -2
- package/dist/cli/args.d.ts +7 -0
- package/dist/cli/args.js +10 -0
- package/dist/cli/completion.js +0 -3
- package/dist/cli/doctor.d.ts +13 -0
- package/dist/cli/doctor.js +133 -1
- package/dist/cli/errors.js +1 -1
- package/dist/cli/eval.d.ts +32 -0
- package/dist/cli/eval.js +83 -6
- package/dist/cli/markdown.js +1 -1
- package/dist/cli/repl.js +13 -24
- package/dist/cli/run.d.ts +37 -0
- package/dist/cli/run.js +81 -9
- package/dist/cli/update.js +7 -1
- package/dist/context/accounting.js +1 -1
- package/dist/context/klyro-md.d.ts +11 -0
- package/dist/context/klyro-md.js +59 -4
- package/dist/context/level7.d.ts +4 -0
- package/dist/context/level7.js +20 -1
- package/dist/context/project-map.js +14 -13
- package/dist/eval/baseline.d.ts +47 -0
- package/dist/eval/baseline.js +55 -0
- package/dist/eval/harness.d.ts +14 -2
- package/dist/eval/harness.js +42 -4
- package/dist/index.js +57 -3
- package/dist/persistence/store.js +2 -2
- package/dist/policy/approval.js +3 -0
- package/dist/policy/path-guard.d.ts +9 -4
- package/dist/policy/path-guard.js +47 -23
- package/dist/providers/endpoints.js +8 -0
- package/dist/providers/model-info.d.ts +1 -0
- package/dist/providers/model-info.js +19 -0
- package/dist/providers.js +6 -2
- package/dist/renderers/terminal.d.ts +7 -0
- package/dist/renderers/terminal.js +52 -0
- package/dist/repl.d.ts +9 -0
- package/dist/repl.js +24 -5
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/proxy.d.ts +37 -0
- package/dist/shared/proxy.js +360 -0
- package/dist/tools/agent/task-list.d.ts +1 -1
- package/dist/tools/fs/apply-patch.js +4 -4
- package/dist/tools/fs/edit-file.js +2 -4
- package/dist/tools/fs/list-dir.js +1 -1
- package/dist/tools/fs/multi-edit.js +2 -2
- package/dist/tools/fs/read-file.js +1 -1
- package/dist/tools/fs/write-file.js +2 -2
- package/dist/tools/git/git-blame.d.ts +5 -0
- package/dist/tools/git/git-blame.js +22 -0
- package/dist/tools/git/git-diff.js +3 -20
- package/dist/tools/git/git-log.js +5 -19
- package/dist/tools/git/git-show.d.ts +5 -0
- package/dist/tools/git/git-show.js +22 -0
- package/dist/tools/git/git-status.js +4 -21
- package/dist/tools/git/run-git.d.ts +5 -0
- package/dist/tools/git/run-git.js +18 -0
- package/dist/tools/lsp/diagnostics.js +2 -3
- package/dist/tools/plan/todo-write.d.ts +1 -1
- package/dist/tools/registry.js +4 -0
- package/dist/tools/search/dependencies.d.ts +4 -4
- package/dist/tools/search/dependencies.js +1 -1
- package/dist/tools/search/glob.js +9 -4
- package/dist/tools/search/grep.js +11 -4
- package/dist/tools/search/ignore.d.ts +8 -0
- package/dist/tools/search/ignore.js +49 -0
- package/dist/tools/search/recent-files.js +1 -1
- package/dist/tools/search/search-files.js +11 -4
- package/dist/tools/web/web-fetch.js +2 -1
- package/dist/tools/web/web-search.js +3 -1
- package/dist/tui/app.js +117 -10
- package/dist/tui/app.test.js +65 -4
- package/dist/tui/measure.js +1 -0
- package/dist/tui/mouse.d.ts +7 -5
- package/dist/tui/mouse.js +9 -6
- package/dist/tui/scroll-flow.test.js +0 -1
- package/dist/tui/transcript-commands.d.ts +14 -0
- package/dist/tui/transcript-commands.js +31 -0
- package/dist/util/log.d.ts +19 -10
- package/dist/util/log.js +87 -52
- package/dist/verification/classify.d.ts +1 -1
- package/dist/verification/classify.js +2 -2
- package/dist/verification/engine.js +0 -6
- package/dist/verification/scoped.js +6 -4
- package/package.json +6 -1
package/dist/cli/run.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* even on a long task. Tool calls and policy decisions go to stderr so
|
|
8
8
|
* they don't pollute the captured `finalText` if the user pipes.
|
|
9
9
|
*/
|
|
10
|
+
import * as path from 'node:path';
|
|
10
11
|
import { stdout, stderr } from 'node:process';
|
|
11
12
|
import { httpChatAdapter } from '../agent/provider-adapter.js';
|
|
12
13
|
import { anthropicAdapter } from '../agent/anthropic-adapter.js';
|
|
@@ -20,8 +21,48 @@ import { redact } from '../policy/secret-redactor.js';
|
|
|
20
21
|
import { buildLevel6Context } from '../context/level6.js';
|
|
21
22
|
import { memoryBlock } from '../context/memory.js';
|
|
22
23
|
import { estimateCost } from '../providers/model-info.js';
|
|
24
|
+
import { resolveModelAlias } from '../providers/model-info.js';
|
|
25
|
+
import { logDebug, logInfo } from '../util/log.js';
|
|
26
|
+
import { applyAutoAnswer } from './args.js';
|
|
23
27
|
import { resolveSessionId } from '../persistence/session.js';
|
|
24
28
|
import * as fs from 'node:fs';
|
|
29
|
+
/**
|
|
30
|
+
* Split a comma-separated tool/dir list (string or string[]) into names.
|
|
31
|
+
* Exported for tests.
|
|
32
|
+
*/
|
|
33
|
+
export function splitToolList(v) {
|
|
34
|
+
const arr = v === undefined ? [] : Array.isArray(v) ? v : [v];
|
|
35
|
+
return arr.flatMap((s) => String(s).split(',')).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
36
|
+
}
|
|
37
|
+
/** Smaller of two optional caps (--max-turns aliases --max-steps). Exported for tests. */
|
|
38
|
+
export function minDefined(a, b) {
|
|
39
|
+
if (a === undefined)
|
|
40
|
+
return b;
|
|
41
|
+
if (b === undefined)
|
|
42
|
+
return a;
|
|
43
|
+
return Math.min(a, b);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Layer --system-prompt/--append-system-prompt over the assembled prompt
|
|
47
|
+
* (2.3): replace wins wholesale, append rides after the assembled system
|
|
48
|
+
* (preserving any suffix the base carries). Exported for tests.
|
|
49
|
+
*/
|
|
50
|
+
export function withSystemPromptOverrides(base, opts) {
|
|
51
|
+
if (opts.replace === undefined && opts.append === undefined)
|
|
52
|
+
return base;
|
|
53
|
+
return (ctx) => {
|
|
54
|
+
if (opts.replace !== undefined) {
|
|
55
|
+
return opts.append ? `${opts.replace}\n\n${opts.append}` : opts.replace;
|
|
56
|
+
}
|
|
57
|
+
const r = base(ctx);
|
|
58
|
+
const system = typeof r === 'string' ? r : r.system;
|
|
59
|
+
const suffix = typeof r === 'string' ? undefined : r.suffix;
|
|
60
|
+
const out = { system: `${system}\n\n${opts.append}` };
|
|
61
|
+
if (suffix !== undefined)
|
|
62
|
+
out.suffix = suffix;
|
|
63
|
+
return out;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
25
66
|
/**
|
|
26
67
|
* Double-Ctrl+C detector (pure, exported for tests): the second SIGINT
|
|
27
68
|
* within 1500ms of the first forces `process.exit(130)`. The live handler
|
|
@@ -31,6 +72,9 @@ export function shouldForceExit(lastSigintAt, now) {
|
|
|
31
72
|
return lastSigintAt !== undefined && now - lastSigintAt < 1500;
|
|
32
73
|
}
|
|
33
74
|
export async function runOnce(opts) {
|
|
75
|
+
// 5.3 — --auto-answer sets KLYRO_AUTO_ANSWER before anything runs so
|
|
76
|
+
// headless ask_user calls resolve without prompting.
|
|
77
|
+
applyAutoAnswer(opts.autoAnswer);
|
|
34
78
|
// P0.5 — load <cwd>/.env first so KLYRO_* vars resolve without `export`.
|
|
35
79
|
// Never throws (missing file is a no-op); explicit env wins (no-clobber).
|
|
36
80
|
try {
|
|
@@ -38,6 +82,9 @@ export async function runOnce(opts) {
|
|
|
38
82
|
loadDotenv(opts.cwd);
|
|
39
83
|
}
|
|
40
84
|
catch { /* ignore */ }
|
|
85
|
+
// Model aliases (2.2: sonnet/opus/haiku/gpt/local) resolve once here so
|
|
86
|
+
// the session store, cost math, and every adapter agree on the id.
|
|
87
|
+
opts = { ...opts, model: resolveModelAlias(opts.model) };
|
|
41
88
|
const output = opts.output ?? 'human';
|
|
42
89
|
if (opts.dryRun) {
|
|
43
90
|
return await dryRunReport(opts);
|
|
@@ -138,7 +185,18 @@ export async function runOnce(opts) {
|
|
|
138
185
|
catch { /* best-effort — single-provider run proceeds */ }
|
|
139
186
|
}
|
|
140
187
|
const registry = builtinRegistry();
|
|
141
|
-
const
|
|
188
|
+
const policyConfig = clonePolicyConfig();
|
|
189
|
+
// Explicit CLI overrides (3.4). Persisted rules load next via applyRules;
|
|
190
|
+
// the engine's deny-first precedence keeps any denial terminal.
|
|
191
|
+
if (opts.permissionMode !== undefined)
|
|
192
|
+
policyConfig.mode = opts.permissionMode;
|
|
193
|
+
for (const t of splitToolList(opts.allowedTools))
|
|
194
|
+
policyConfig.allow.push(t);
|
|
195
|
+
for (const t of splitToolList(opts.disallowedTools))
|
|
196
|
+
policyConfig.deny.push(t);
|
|
197
|
+
for (const d of splitToolList(opts.addDir))
|
|
198
|
+
policyConfig.additionalDirs.push(path.resolve(opts.cwd, d));
|
|
199
|
+
const policy = new PolicyEngine(builtinRules(), policyConfig);
|
|
142
200
|
// Persisted "always allow" patterns apply to one-shot runs too.
|
|
143
201
|
try {
|
|
144
202
|
const { loadPermissionRules } = await import('./config.js');
|
|
@@ -181,7 +239,7 @@ export async function runOnce(opts) {
|
|
|
181
239
|
}
|
|
182
240
|
catch { /* ignore — MCP is optional */ }
|
|
183
241
|
}
|
|
184
|
-
const systemPrompt = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt, opts.bare);
|
|
242
|
+
const systemPrompt = withSystemPromptOverrides(await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt, opts.bare), { replace: opts.systemPromptText, append: opts.appendSystemPrompt });
|
|
185
243
|
// Level 9 — session setup (create or resume). --bare skips persistence.
|
|
186
244
|
const persistEnabled = opts.persist !== false && !opts.bare;
|
|
187
245
|
let store;
|
|
@@ -219,9 +277,6 @@ export async function runOnce(opts) {
|
|
|
219
277
|
stderr.write(`klyro: resuming session ${sessionId.slice(0, 8)} — task: "${rec.task}"\n`);
|
|
220
278
|
}
|
|
221
279
|
}
|
|
222
|
-
else if (opts.resumePath) {
|
|
223
|
-
// already handled
|
|
224
|
-
}
|
|
225
280
|
else {
|
|
226
281
|
// Create new session
|
|
227
282
|
const rec = await store.create({ cwd: opts.cwd, task: opts.task, config: { model: opts.model, maxSteps: opts.maxSteps ?? 30 } });
|
|
@@ -313,23 +368,34 @@ export async function runOnce(opts) {
|
|
|
313
368
|
model: opts.model,
|
|
314
369
|
});
|
|
315
370
|
}
|
|
371
|
+
// 2.5 latency tracking: TTFT = first model event after run start.
|
|
372
|
+
const runStartMs = Date.now();
|
|
373
|
+
let ttftMs;
|
|
374
|
+
logDebug('run start', { model: opts.model, maxSteps: opts.maxSteps, bare: !!opts.bare });
|
|
316
375
|
try {
|
|
317
376
|
result = await run({
|
|
318
377
|
task: opts.task,
|
|
319
378
|
cwd: opts.cwd,
|
|
320
379
|
model: opts.model,
|
|
321
380
|
...(opts.bare ? { bare: true } : {}),
|
|
322
|
-
maxSteps: opts.maxSteps,
|
|
381
|
+
maxSteps: minDefined(opts.maxSteps, opts.maxTurns),
|
|
323
382
|
maxTokens: opts.maxTokens,
|
|
324
383
|
temperature: opts.temperature,
|
|
384
|
+
reasoningEffort: opts.reasoningEffort,
|
|
325
385
|
signal: ac.signal,
|
|
326
386
|
nonInteractive: true,
|
|
327
387
|
initialTranscript,
|
|
388
|
+
// Sandbox roots for file tools: cwd plus --add-dir extras (resolved
|
|
389
|
+
// absolute). checkAllowedPaths narrows to exactly this set.
|
|
390
|
+
allowedPaths: [path.resolve(opts.cwd), ...(policyConfig.additionalDirs ?? [])],
|
|
328
391
|
verify: verifyOpts,
|
|
329
392
|
persist: store && sessionId ? { store, sessionId } : undefined,
|
|
330
393
|
...(agentBridge ? { agentBridge } : {}),
|
|
331
394
|
...(parentContext ? { parentContext } : {}),
|
|
332
395
|
onEvent: (ev) => {
|
|
396
|
+
if (ttftMs === undefined && (ev.kind === 'text_delta' || ev.kind === 'tool_call_start')) {
|
|
397
|
+
ttftMs = Date.now() - runStartMs;
|
|
398
|
+
}
|
|
333
399
|
if (output === 'json') {
|
|
334
400
|
stdout.write(JSON.stringify(ev) + '\n');
|
|
335
401
|
return;
|
|
@@ -436,9 +502,12 @@ export async function runOnce(opts) {
|
|
|
436
502
|
// [budget] warnings. Human/silent → stderr one-liner; json → additive
|
|
437
503
|
// cost_usd/usage fields on the final object (purely additive, no shape break).
|
|
438
504
|
const finalCost = estimateCost(opts.model, result.usage.input, result.usage.output);
|
|
439
|
-
const
|
|
505
|
+
const totalMs = Date.now() - runStartMs;
|
|
506
|
+
logInfo('run finish', { status: result.status, steps: result.steps, toolCalls: result.toolCalls, totalMs });
|
|
507
|
+
const ttftPart = ttftMs !== undefined ? ` · ttft ${ttftMs}ms` : '';
|
|
508
|
+
const costLine = `$${finalCost.toFixed(4)} · ${result.usage.input} in / ${result.usage.output} out${result.usage.estimated ? ' (estimated)' : ''}${ttftPart} · total ${totalMs}ms`;
|
|
440
509
|
if (output === 'json') {
|
|
441
|
-
stdout.write(JSON.stringify({ kind: 'cost', cost_usd: finalCost, usage: result.usage }) + '\n');
|
|
510
|
+
stdout.write(JSON.stringify({ kind: 'cost', cost_usd: finalCost, usage: result.usage, ttft_ms: ttftMs ?? null, total_ms: totalMs }) + '\n');
|
|
442
511
|
}
|
|
443
512
|
else {
|
|
444
513
|
stderr.write(`klyro: cost ${costLine}\n`);
|
|
@@ -457,6 +526,8 @@ export async function runOnce(opts) {
|
|
|
457
526
|
toolCalls: result.toolCalls,
|
|
458
527
|
cost_usd: finalCost,
|
|
459
528
|
usage: result.usage,
|
|
529
|
+
ttft_ms: ttftMs ?? null,
|
|
530
|
+
total_ms: totalMs,
|
|
460
531
|
...(result.verification ? { verification: result.verification } : {}),
|
|
461
532
|
...(sessionId ? { session_id: sessionId } : {}),
|
|
462
533
|
...extra,
|
|
@@ -523,7 +594,7 @@ export async function runOnce(opts) {
|
|
|
523
594
|
async function dryRunReport(opts) {
|
|
524
595
|
// Assemble the REAL prompt (Level-6 context + KLYRO.md), not the bare base —
|
|
525
596
|
// otherwise dry-run shows a different prompt than production runs use.
|
|
526
|
-
const systemPromptFn = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt);
|
|
597
|
+
const systemPromptFn = withSystemPromptOverrides(await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt), { replace: opts.systemPromptText, append: opts.appendSystemPrompt });
|
|
527
598
|
const { resolveSystemPrompt } = await import('../agent/runtime.js');
|
|
528
599
|
const { system, suffix } = resolveSystemPrompt(systemPromptFn, { cwd: opts.cwd });
|
|
529
600
|
const systemPrompt = suffix ? `${system}\n\n${suffix}` : system;
|
|
@@ -536,6 +607,7 @@ async function dryRunReport(opts) {
|
|
|
536
607
|
maxSteps: opts.maxSteps,
|
|
537
608
|
maxTokens: opts.maxTokens,
|
|
538
609
|
temperature: opts.temperature,
|
|
610
|
+
reasoningEffort: opts.reasoningEffort,
|
|
539
611
|
// Secrets must never leak into a printable report: redact both the
|
|
540
612
|
// assembled system prompt and the task before printing.
|
|
541
613
|
systemPrompt: redact(systemPrompt),
|
package/dist/cli/update.js
CHANGED
|
@@ -13,6 +13,7 @@ import * as fs from 'node:fs/promises';
|
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import * as os from 'node:os';
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
16
|
+
import { proxiedFetch } from '../shared/proxy.js';
|
|
16
17
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
17
18
|
const REGISTRY_BASE = 'https://registry.npmjs.org/klyro';
|
|
18
19
|
const FETCH_TIMEOUT_MS = 3000;
|
|
@@ -64,7 +65,7 @@ async function fetchWithTimeout(url, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
|
64
65
|
const ctrl = new AbortController();
|
|
65
66
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
66
67
|
try {
|
|
67
|
-
const res = await
|
|
68
|
+
const res = await proxiedFetch(url, { signal: ctrl.signal, timeoutMs });
|
|
68
69
|
if (!res.ok)
|
|
69
70
|
throw new Error(`HTTP ${res.status}`);
|
|
70
71
|
return res;
|
|
@@ -95,6 +96,11 @@ async function verifyTarballIntegrity(dist) {
|
|
|
95
96
|
export async function checkForUpdate(current) {
|
|
96
97
|
if (process.env.KLYRO_NO_UPDATE_CHECK === '1')
|
|
97
98
|
return null;
|
|
99
|
+
// Disabled in CI unless explicitly enabled (1.5): automated pipelines
|
|
100
|
+
// must never phone home or suggest interactive updates.
|
|
101
|
+
if ((process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true') && process.env.KLYRO_UPDATE_CHECK !== '1') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
98
104
|
const cache = cachePath();
|
|
99
105
|
try {
|
|
100
106
|
const raw = await fs.readFile(cache, 'utf-8');
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* 8.1 — Accounting & stable assembly
|
|
3
3
|
* Live token estimate, ctx%, compactAt, reserveOutput, toolResultMax
|
|
4
4
|
*/
|
|
5
|
-
import { totalTokens } from './tokenizer.js';
|
|
6
5
|
import { getModelInfo } from '../providers/model-info.js';
|
|
6
|
+
import { totalTokens } from './tokenizer.js';
|
|
7
7
|
/**
|
|
8
8
|
* Single output-reserve used by every budget in the harness (displayed
|
|
9
9
|
* accounting, runtime enforcement, compaction elide). One constant so the
|
|
@@ -12,5 +12,16 @@ export interface KlyroMdFile {
|
|
|
12
12
|
}
|
|
13
13
|
/** Per-file loader (used by the trust gate to approve/hash individual files). */
|
|
14
14
|
export declare function loadKlyroMdFiles(cwd: string): Promise<KlyroMdFile[]>;
|
|
15
|
+
/**
|
|
16
|
+
* 4.4a — Subdirectory instructions: root files PLUS instruction files found
|
|
17
|
+
* in ancestor directories of targetPath from cwd down to targetPath's dir
|
|
18
|
+
* (nearest-wins order root→leaf, each capped like cap()).
|
|
19
|
+
*
|
|
20
|
+
* Laziness preserved: plain loadKlyroMdFiles(cwd) never reads subdirs —
|
|
21
|
+
* callers pass the relevant active path explicitly.
|
|
22
|
+
*
|
|
23
|
+
* Containment: a targetPath escaping cwd is rejected (root files only).
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadKlyroMdForPath(cwd: string, targetPath: string): Promise<KlyroMdFile[]>;
|
|
15
26
|
export declare function loadKlyroMd(cwd: string): Promise<string>;
|
|
16
27
|
export declare function handleInit(cwd: string): Promise<string>;
|
package/dist/context/klyro-md.js
CHANGED
|
@@ -30,16 +30,65 @@ export async function loadKlyroMdFiles(cwd) {
|
|
|
30
30
|
// Klyro's own instruction file is KLYRO.md. The rest are read-only
|
|
31
31
|
// fallbacks for imported repos that follow other conventions — Klyro
|
|
32
32
|
// never writes them. KLYRO.md wins by load order.
|
|
33
|
-
for (const name of
|
|
33
|
+
for (const name of INSTRUCTION_NAMES) {
|
|
34
34
|
const p = path.join(cwd, name);
|
|
35
35
|
try {
|
|
36
36
|
const t = await fs.readFile(p, 'utf-8');
|
|
37
|
-
files.push({ path: p, content: cap(await resolveImports(t, path.dirname(p), cwd), MAX_FILE_CHARS) });
|
|
37
|
+
files.push({ path: p, content: cap(await resolveImports(t, path.dirname(p), cwd, 0, [p]), MAX_FILE_CHARS) });
|
|
38
38
|
}
|
|
39
39
|
catch { /* ignore */ }
|
|
40
40
|
}
|
|
41
41
|
return files;
|
|
42
42
|
}
|
|
43
|
+
/** Instruction file names read at every level (root + subdirectories). */
|
|
44
|
+
const INSTRUCTION_NAMES = ['KLYRO.md', 'KLYRO.local.md', 'CLAUDE.md', 'CLAUDE.local.md', 'AGENTS.md', '.cursorrules'];
|
|
45
|
+
/**
|
|
46
|
+
* 4.4a — Subdirectory instructions: root files PLUS instruction files found
|
|
47
|
+
* in ancestor directories of targetPath from cwd down to targetPath's dir
|
|
48
|
+
* (nearest-wins order root→leaf, each capped like cap()).
|
|
49
|
+
*
|
|
50
|
+
* Laziness preserved: plain loadKlyroMdFiles(cwd) never reads subdirs —
|
|
51
|
+
* callers pass the relevant active path explicitly.
|
|
52
|
+
*
|
|
53
|
+
* Containment: a targetPath escaping cwd is rejected (root files only).
|
|
54
|
+
*/
|
|
55
|
+
export async function loadKlyroMdForPath(cwd, targetPath) {
|
|
56
|
+
const files = await loadKlyroMdFiles(cwd);
|
|
57
|
+
const root = path.resolve(cwd);
|
|
58
|
+
const absTarget = path.resolve(root, targetPath);
|
|
59
|
+
const relTarget = path.relative(root, absTarget);
|
|
60
|
+
if (relTarget.startsWith('..') || path.isAbsolute(relTarget))
|
|
61
|
+
return files;
|
|
62
|
+
// Target dir: the target itself when it is an existing directory,
|
|
63
|
+
// otherwise its parent dir (i.e. the target is a file being edited).
|
|
64
|
+
let targetDir = absTarget;
|
|
65
|
+
try {
|
|
66
|
+
const st = await fs.stat(absTarget);
|
|
67
|
+
if (!st.isDirectory())
|
|
68
|
+
targetDir = path.dirname(absTarget);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
targetDir = path.dirname(absTarget);
|
|
72
|
+
}
|
|
73
|
+
const relDir = path.relative(root, targetDir);
|
|
74
|
+
if (relDir.startsWith('..') || path.isAbsolute(relDir))
|
|
75
|
+
return files;
|
|
76
|
+
const segs = relDir.split(path.sep).filter((s) => s && s !== '.');
|
|
77
|
+
let prefix = root;
|
|
78
|
+
for (const seg of segs) {
|
|
79
|
+
prefix = path.join(prefix, seg);
|
|
80
|
+
const dir = prefix;
|
|
81
|
+
for (const name of INSTRUCTION_NAMES) {
|
|
82
|
+
const p = path.join(dir, name);
|
|
83
|
+
try {
|
|
84
|
+
const t = await fs.readFile(p, 'utf-8');
|
|
85
|
+
files.push({ path: p, content: cap(await resolveImports(t, path.dirname(p), root, 0, [p]), MAX_FILE_CHARS) });
|
|
86
|
+
}
|
|
87
|
+
catch { /* ignore */ }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return files;
|
|
91
|
+
}
|
|
43
92
|
export async function loadKlyroMd(cwd) {
|
|
44
93
|
const parts = [];
|
|
45
94
|
let total = 0;
|
|
@@ -56,7 +105,7 @@ export async function loadKlyroMd(cwd) {
|
|
|
56
105
|
}
|
|
57
106
|
return parts.join('\n\n---\n\n');
|
|
58
107
|
}
|
|
59
|
-
async function resolveImports(text, base, root, depth = 0) {
|
|
108
|
+
async function resolveImports(text, base, root, depth = 0, chain = []) {
|
|
60
109
|
if (depth > 5)
|
|
61
110
|
return text;
|
|
62
111
|
const importRe = /^@import\s+(.+)$/gm;
|
|
@@ -69,9 +118,15 @@ async function resolveImports(text, base, root, depth = 0) {
|
|
|
69
118
|
const relToRoot = path.relative(root, p);
|
|
70
119
|
if (relToRoot.startsWith('..') || path.isAbsolute(relToRoot))
|
|
71
120
|
continue;
|
|
121
|
+
// Cycle guard: an @import target already on the current chain resolves
|
|
122
|
+
// to a marker instead of recursing (depth cap stays as backstop).
|
|
123
|
+
if (chain.includes(p)) {
|
|
124
|
+
out = out.replace(m[0], `<!-- klyro: import cycle skipped: ${rel} -->`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
72
127
|
try {
|
|
73
128
|
const t = await fs.readFile(p, 'utf-8');
|
|
74
|
-
const resolved = await resolveImports(cap(t, MAX_FILE_CHARS), path.dirname(p), root, depth + 1);
|
|
129
|
+
const resolved = await resolveImports(cap(t, MAX_FILE_CHARS), path.dirname(p), root, depth + 1, [...chain, p]);
|
|
75
130
|
out = out.replace(m[0], resolved);
|
|
76
131
|
}
|
|
77
132
|
catch { /* ignore missing */ }
|
package/dist/context/level7.d.ts
CHANGED
|
@@ -61,6 +61,10 @@ export declare class RuntimeTelemetry {
|
|
|
61
61
|
private errorCount;
|
|
62
62
|
private recent;
|
|
63
63
|
constructor(opts?: Level7Options);
|
|
64
|
+
private muted;
|
|
65
|
+
/** Disable telemetry accumulation + reporting (also via KLYRO_TELEMETRY=0). */
|
|
66
|
+
mute(): void;
|
|
67
|
+
isMuted(): boolean;
|
|
64
68
|
setMaxSteps(n: number): void;
|
|
65
69
|
recordStepStart(step: number): void;
|
|
66
70
|
recordUsage(input: number, output: number): void;
|
package/dist/context/level7.js
CHANGED
|
@@ -40,14 +40,29 @@ export class RuntimeTelemetry {
|
|
|
40
40
|
maxRecentCalls: opts.maxRecentCalls ?? DEFAULT_MAX_RECENT,
|
|
41
41
|
maxChars: opts.maxChars ?? DEFAULT_MAX_CHARS,
|
|
42
42
|
};
|
|
43
|
+
// KLYRO_TELEMETRY=0 disables telemetry accumulation + reporting.
|
|
44
|
+
this.muted = process.env.KLYRO_TELEMETRY === '0';
|
|
45
|
+
}
|
|
46
|
+
muted = false;
|
|
47
|
+
/** Disable telemetry accumulation + reporting (also via KLYRO_TELEMETRY=0). */
|
|
48
|
+
mute() {
|
|
49
|
+
this.muted = true;
|
|
50
|
+
}
|
|
51
|
+
isMuted() {
|
|
52
|
+
return this.muted;
|
|
43
53
|
}
|
|
44
54
|
setMaxSteps(n) { this.maxSteps = n; }
|
|
45
|
-
recordStepStart(step) { this.
|
|
55
|
+
recordStepStart(step) { if (!this.muted)
|
|
56
|
+
this.step = step; }
|
|
46
57
|
recordUsage(input, output) {
|
|
58
|
+
if (this.muted)
|
|
59
|
+
return;
|
|
47
60
|
this.inputTokens += input;
|
|
48
61
|
this.outputTokens += output;
|
|
49
62
|
}
|
|
50
63
|
recordToolCall(call, latencyMs, isError) {
|
|
64
|
+
if (this.muted)
|
|
65
|
+
return;
|
|
51
66
|
this.toolCallCount++;
|
|
52
67
|
if (isError)
|
|
53
68
|
this.errorCount++;
|
|
@@ -63,6 +78,8 @@ export class RuntimeTelemetry {
|
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
recordError(msg) {
|
|
81
|
+
if (this.muted)
|
|
82
|
+
return;
|
|
66
83
|
this.lastError = msg;
|
|
67
84
|
}
|
|
68
85
|
/**
|
|
@@ -89,6 +106,8 @@ export class RuntimeTelemetry {
|
|
|
89
106
|
};
|
|
90
107
|
}
|
|
91
108
|
format() {
|
|
109
|
+
if (this.muted)
|
|
110
|
+
return emptyTelemetryBlock();
|
|
92
111
|
const s = this.snapshot();
|
|
93
112
|
const lines = [];
|
|
94
113
|
lines.push('# Runtime telemetry');
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* what to walk past.
|
|
22
22
|
*/
|
|
23
23
|
import * as fs from 'node:fs/promises';
|
|
24
|
+
import { statSync, readFileSync } from 'node:fs';
|
|
24
25
|
import * as path from 'node:path';
|
|
25
26
|
export const IGNORED_DIRS = new Set([
|
|
26
27
|
'node_modules', '.git', 'dist', 'build', '.next', '.turbo',
|
|
@@ -246,7 +247,7 @@ function detectPackageManager(root, pkg) {
|
|
|
246
247
|
// Avoid the async cost by re-using the root listing we already do.
|
|
247
248
|
// (Caller threads the listing into detectFrameworks; here we re-stat directly.)
|
|
248
249
|
try {
|
|
249
|
-
|
|
250
|
+
statSync(path.join(root, lock));
|
|
250
251
|
return pm;
|
|
251
252
|
}
|
|
252
253
|
catch { /* not present */ }
|
|
@@ -295,9 +296,9 @@ function extractBuildCommands(pkg, root) {
|
|
|
295
296
|
// Makefile: pick the first `target:` block as a hint.
|
|
296
297
|
// (Don't try to parse make fully — just surface the targets as a single hint.)
|
|
297
298
|
try {
|
|
298
|
-
const stat =
|
|
299
|
+
const stat = statSync(path.join(root, 'Makefile'));
|
|
299
300
|
if (stat.isFile() && stat.size < MAX_BYTES) {
|
|
300
|
-
const raw =
|
|
301
|
+
const raw = readFileSync(path.join(root, 'Makefile'), 'utf-8');
|
|
301
302
|
const targets = [...raw.matchAll(/^([a-zA-Z_][\w-]*)\s*:/gm)].map((m) => m[1]).slice(0, 6);
|
|
302
303
|
if (targets.length)
|
|
303
304
|
out.push(`make <target> # targets: ${targets.join(', ')}`);
|
|
@@ -368,11 +369,11 @@ async function gitHead(root) {
|
|
|
368
369
|
}
|
|
369
370
|
function lockfileHash(root) {
|
|
370
371
|
const candidates = ['pnpm-lock.yaml', 'yarn.lock', 'package-lock.json', 'bun.lockb', 'Cargo.lock', 'go.sum', 'poetry.lock', 'uv.lock'];
|
|
371
|
-
|
|
372
|
+
const h = crypto.createHash('sha1');
|
|
372
373
|
let found = false;
|
|
373
374
|
for (const f of candidates) {
|
|
374
375
|
try {
|
|
375
|
-
const d =
|
|
376
|
+
const d = readFileSync(path.join(root, f));
|
|
376
377
|
h.update(d);
|
|
377
378
|
found = true;
|
|
378
379
|
}
|
|
@@ -456,17 +457,17 @@ function detectMonorepo(root, rootFiles, rootDirs) {
|
|
|
456
457
|
return true;
|
|
457
458
|
if (rootDirs.includes('packages') || rootDirs.includes('apps')) {
|
|
458
459
|
try {
|
|
459
|
-
const s =
|
|
460
|
+
const s = statSync(path.join(root, 'packages'));
|
|
460
461
|
if (s.isDirectory())
|
|
461
462
|
return true;
|
|
462
463
|
}
|
|
463
|
-
catch { }
|
|
464
|
+
catch { /* ignore — best-effort probe */ }
|
|
464
465
|
try {
|
|
465
|
-
const s2 =
|
|
466
|
+
const s2 = statSync(path.join(root, 'apps'));
|
|
466
467
|
if (s2.isDirectory())
|
|
467
468
|
return true;
|
|
468
469
|
}
|
|
469
|
-
catch { }
|
|
470
|
+
catch { /* ignore — best-effort probe */ }
|
|
470
471
|
}
|
|
471
472
|
return false;
|
|
472
473
|
}
|
|
@@ -477,20 +478,20 @@ async function detectRuntimeVersions(root) {
|
|
|
477
478
|
if (v)
|
|
478
479
|
out['node'] = v.trim();
|
|
479
480
|
}
|
|
480
|
-
catch { }
|
|
481
|
+
catch { /* ignore — best-effort probe */ }
|
|
481
482
|
try {
|
|
482
483
|
const pkg = await readJsonSafe(path.join(root, 'package.json'));
|
|
483
484
|
const eng = pkg?.engines;
|
|
484
485
|
if (eng?.node)
|
|
485
486
|
out['node-eng'] = eng.node;
|
|
486
487
|
}
|
|
487
|
-
catch { }
|
|
488
|
+
catch { /* ignore — best-effort probe */ }
|
|
488
489
|
try {
|
|
489
490
|
const py = await readTextSafe(path.join(root, '.python-version'));
|
|
490
491
|
if (py)
|
|
491
492
|
out['python'] = py.trim();
|
|
492
493
|
}
|
|
493
|
-
catch { }
|
|
494
|
+
catch { /* ignore — best-effort probe */ }
|
|
494
495
|
try {
|
|
495
496
|
const go = await readTextSafe(path.join(root, 'go.mod'));
|
|
496
497
|
if (go) {
|
|
@@ -499,7 +500,7 @@ async function detectRuntimeVersions(root) {
|
|
|
499
500
|
out['go'] = m[1];
|
|
500
501
|
}
|
|
501
502
|
}
|
|
502
|
-
catch { }
|
|
503
|
+
catch { /* ignore — best-effort probe */ }
|
|
503
504
|
return out;
|
|
504
505
|
}
|
|
505
506
|
async function detectEntryPoints(root) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 5.5b — eval baseline writer. Records the environment alongside suite
|
|
3
|
+
* results so regressions can be attributed (model/node/platform/commit).
|
|
4
|
+
*
|
|
5
|
+
* The tracked `evals/results/baseline.json` is refreshed by `klyro eval
|
|
6
|
+
* --suite <name>` (best-effort; suite failures never fail the write path).
|
|
7
|
+
*/
|
|
8
|
+
export interface EvalBaselineMeta {
|
|
9
|
+
/** Model id the suite ran under (KLYRO_MODEL fallback, then 'mock'). */
|
|
10
|
+
model: string;
|
|
11
|
+
/** Node version (process.version). */
|
|
12
|
+
node: string;
|
|
13
|
+
/** OS platform (process.platform). */
|
|
14
|
+
platform: string;
|
|
15
|
+
/** Git HEAD, best-effort ('unknown' outside a repo). */
|
|
16
|
+
commit: string;
|
|
17
|
+
/** ISO timestamp of the run. */
|
|
18
|
+
timestamp: string;
|
|
19
|
+
}
|
|
20
|
+
export interface EvalBaseline extends EvalBaselineMeta {
|
|
21
|
+
suite: string;
|
|
22
|
+
total: number;
|
|
23
|
+
passed: number;
|
|
24
|
+
failed: number;
|
|
25
|
+
passRate: number;
|
|
26
|
+
durationMs: number;
|
|
27
|
+
}
|
|
28
|
+
/** Best-effort `git rev-parse HEAD` (never throws). Exported for tests. */
|
|
29
|
+
export declare function gitCommit(cwd?: string): string;
|
|
30
|
+
/** Collect the environment half of a baseline record. Exported for tests. */
|
|
31
|
+
export declare function collectBaselineMetadata(opts?: {
|
|
32
|
+
model?: string;
|
|
33
|
+
cwd?: string;
|
|
34
|
+
}): EvalBaselineMeta;
|
|
35
|
+
/** Combine metadata + suite counts into a full baseline record. */
|
|
36
|
+
export declare function buildEvalBaseline(meta: EvalBaselineMeta, suite: {
|
|
37
|
+
suite: string;
|
|
38
|
+
total: number;
|
|
39
|
+
passed: number;
|
|
40
|
+
failed: number;
|
|
41
|
+
durationMs: number;
|
|
42
|
+
}): EvalBaseline;
|
|
43
|
+
/**
|
|
44
|
+
* Write `baseline.json` into `dir` (created if needed). Returns the path.
|
|
45
|
+
* Never throws for missing dirs — the caller wraps in try/catch anyway.
|
|
46
|
+
*/
|
|
47
|
+
export declare function writeEvalBaseline(dir: string, baseline: EvalBaseline): Promise<string>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 5.5b — eval baseline writer. Records the environment alongside suite
|
|
3
|
+
* results so regressions can be attributed (model/node/platform/commit).
|
|
4
|
+
*
|
|
5
|
+
* The tracked `evals/results/baseline.json` is refreshed by `klyro eval
|
|
6
|
+
* --suite <name>` (best-effort; suite failures never fail the write path).
|
|
7
|
+
*/
|
|
8
|
+
import * as fsp from 'node:fs/promises';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
|
+
/** Best-effort `git rev-parse HEAD` (never throws). Exported for tests. */
|
|
12
|
+
export function gitCommit(cwd = process.cwd()) {
|
|
13
|
+
try {
|
|
14
|
+
const out = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
15
|
+
cwd,
|
|
16
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
17
|
+
}).toString().trim();
|
|
18
|
+
return out || 'unknown';
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return 'unknown';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Collect the environment half of a baseline record. Exported for tests. */
|
|
25
|
+
export function collectBaselineMetadata(opts = {}) {
|
|
26
|
+
return {
|
|
27
|
+
model: opts.model ?? process.env.KLYRO_MODEL ?? 'mock',
|
|
28
|
+
node: process.version,
|
|
29
|
+
platform: process.platform,
|
|
30
|
+
commit: gitCommit(opts.cwd),
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Combine metadata + suite counts into a full baseline record. */
|
|
35
|
+
export function buildEvalBaseline(meta, suite) {
|
|
36
|
+
return {
|
|
37
|
+
...meta,
|
|
38
|
+
suite: suite.suite,
|
|
39
|
+
total: suite.total,
|
|
40
|
+
passed: suite.passed,
|
|
41
|
+
failed: suite.failed,
|
|
42
|
+
passRate: suite.total === 0 ? 0 : suite.passed / suite.total,
|
|
43
|
+
durationMs: suite.durationMs,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Write `baseline.json` into `dir` (created if needed). Returns the path.
|
|
48
|
+
* Never throws for missing dirs — the caller wraps in try/catch anyway.
|
|
49
|
+
*/
|
|
50
|
+
export async function writeEvalBaseline(dir, baseline) {
|
|
51
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
52
|
+
const out = path.join(dir, 'baseline.json');
|
|
53
|
+
await fsp.writeFile(out, JSON.stringify(baseline, null, 2) + '\n');
|
|
54
|
+
return out;
|
|
55
|
+
}
|
package/dist/eval/harness.d.ts
CHANGED
|
@@ -77,10 +77,21 @@ export interface FileFixture {
|
|
|
77
77
|
script?: StreamEvent[][];
|
|
78
78
|
}
|
|
79
79
|
export declare function loadFileFixture(dir: string): Promise<FileFixture>;
|
|
80
|
-
export declare function
|
|
80
|
+
export declare function sanitizedFixtureEnv(base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
81
|
+
/**
|
|
82
|
+
* 5.4b — guard: fixture workdirs must stay inside `os.tmpdir()` (the
|
|
83
|
+
* harness relies on that isolation). Throws otherwise unless the caller
|
|
84
|
+
* explicitly passes `allowWorkdirOutsideTmp`. Exported for tests.
|
|
85
|
+
*/
|
|
86
|
+
export declare function assertTmpWorkdir(dir: string, opts?: {
|
|
87
|
+
allowWorkdirOutsideTmp?: boolean;
|
|
88
|
+
}): void;
|
|
89
|
+
export interface FileFixtureOptions {
|
|
81
90
|
runs?: number;
|
|
82
91
|
parallel?: number;
|
|
83
|
-
|
|
92
|
+
allowWorkdirOutsideTmp?: boolean;
|
|
93
|
+
}
|
|
94
|
+
export declare function runFileFixture(fixture: FileFixture, _opts?: FileFixtureOptions): Promise<TaskResult>;
|
|
84
95
|
/**
|
|
85
96
|
* Agent-driven fixture: seed a tmp repo, run the fixture's canned script
|
|
86
97
|
* through the REAL runtime + tools, then assert outcomes with check.sh
|
|
@@ -90,6 +101,7 @@ export declare function runFileFixture(fixture: FileFixture, opts?: {
|
|
|
90
101
|
export declare function runAgentFixture(fixture: FileFixture, opts?: {
|
|
91
102
|
judgeAdapter?: ProviderAdapter;
|
|
92
103
|
judgeModel?: string;
|
|
104
|
+
allowWorkdirOutsideTmp?: boolean;
|
|
93
105
|
}): Promise<TaskResult>;
|
|
94
106
|
/** Hook the harness up to a session + audit log so task runs are durable. */ export declare function runHarnessWithPersistence(tasks: ScriptedTask[], opts: {
|
|
95
107
|
storeDir: string;
|