klyro 1.0.5 → 1.0.6
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/custom-agents.d.ts +3 -0
- package/dist/agent/custom-agents.js +96 -0
- package/dist/agent/orchestrator.d.ts +22 -2
- package/dist/agent/orchestrator.js +30 -4
- package/dist/agent/runtime.d.ts +5 -0
- package/dist/agent/runtime.js +164 -50
- package/dist/checkpoints/store.d.ts +9 -0
- package/dist/checkpoints/store.js +20 -0
- package/dist/cli/completion.js +2 -2
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/eval.d.ts +15 -1
- package/dist/cli/eval.js +34 -2
- package/dist/cli/hooks.d.ts +54 -5
- package/dist/cli/hooks.js +85 -6
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +60 -0
- package/dist/cli/repl.js +146 -25
- package/dist/cli/run.d.ts +7 -1
- package/dist/cli/run.js +92 -50
- package/dist/cli/slash/custom.d.ts +25 -0
- package/dist/cli/slash/custom.js +166 -0
- package/dist/cli/slash/parser.d.ts +9 -1
- package/dist/cli/slash/parser.js +31 -9
- package/dist/context/memory.js +18 -1
- package/dist/eval/harness.d.ts +21 -3
- package/dist/eval/harness.js +31 -3
- package/dist/eval/judge.d.ts +32 -0
- package/dist/eval/judge.js +63 -0
- package/dist/eval/tasks.js +134 -0
- package/dist/index.js +150 -127
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +42 -2
- package/dist/mcp/config.d.ts +3 -1
- package/dist/mcp/config.js +19 -1
- package/dist/mcp/registry.d.ts +13 -0
- package/dist/mcp/registry.js +41 -1
- package/dist/mcp/remote.d.ts +29 -0
- package/dist/mcp/remote.js +153 -0
- package/dist/policy/approval.d.ts +15 -1
- package/dist/policy/approval.js +8 -0
- package/dist/providers/endpoints.d.ts +43 -0
- package/dist/providers/endpoints.js +104 -0
- package/dist/providers.js +13 -10
- package/dist/tui/app.js +140 -13
- package/dist/tui/app.test.js +24 -0
- package/dist/tui/approval.js +53 -1
- package/package.json +1 -1
package/dist/cli/repl.js
CHANGED
|
@@ -293,6 +293,8 @@ export async function startRepl(opts = {}) {
|
|
|
293
293
|
let isMounted = false;
|
|
294
294
|
// Live vim input mode for the TUI (toggled by /vim, persisted to config).
|
|
295
295
|
let vimLive = 'insert';
|
|
296
|
+
// Custom-command recursion guard (a command body may invoke /commands).
|
|
297
|
+
const customDepthRef = { current: 0 };
|
|
296
298
|
let directHooks;
|
|
297
299
|
// Plain-text mirror for exit replay (scroll.md §1.2: session survives in
|
|
298
300
|
// native scrollback after the alt screen is torn down). Cap 300 lines.
|
|
@@ -844,6 +846,7 @@ export async function startRepl(opts = {}) {
|
|
|
844
846
|
let activeCallId = null;
|
|
845
847
|
let activeCallName = null;
|
|
846
848
|
let activeCallArgs = '';
|
|
849
|
+
let runEndStatus = 'error';
|
|
847
850
|
try {
|
|
848
851
|
const result = await run({
|
|
849
852
|
task: taskText,
|
|
@@ -976,6 +979,7 @@ export async function startRepl(opts = {}) {
|
|
|
976
979
|
});
|
|
977
980
|
if (result.finalText)
|
|
978
981
|
lastAssistantText = result.finalText;
|
|
982
|
+
runEndStatus = result.status;
|
|
979
983
|
if (result.verification) {
|
|
980
984
|
const v = result.verification;
|
|
981
985
|
queuedAppend({
|
|
@@ -1015,6 +1019,12 @@ export async function startRepl(opts = {}) {
|
|
|
1015
1019
|
queuedAppend({ id: `err-${Date.now()}`, kind: 'error', message });
|
|
1016
1020
|
queuedStatus({ status: 'error', errorMessage: message });
|
|
1017
1021
|
}
|
|
1022
|
+
// sessionEnd hooks: best-effort end-of-run side effects. Never throws.
|
|
1023
|
+
try {
|
|
1024
|
+
const { runSessionEndHooks } = await import('./hooks.js');
|
|
1025
|
+
await runSessionEndHooks(cwd, sessionId, runEndStatus);
|
|
1026
|
+
}
|
|
1027
|
+
catch { /* ignore */ }
|
|
1018
1028
|
}
|
|
1019
1029
|
async function handleSlash(cmd) {
|
|
1020
1030
|
switch (cmd.kind) {
|
|
@@ -1095,6 +1105,22 @@ export async function startRepl(opts = {}) {
|
|
|
1095
1105
|
});
|
|
1096
1106
|
return;
|
|
1097
1107
|
}
|
|
1108
|
+
case 'memory-append': {
|
|
1109
|
+
// Human write path: same redaction + atomicity as memory_write.
|
|
1110
|
+
if (!cmd.text) {
|
|
1111
|
+
queuedAppend({ id: `mem-${Date.now()}`, kind: 'text', text: 'usage: /memory append <text>', role: 'assistant' });
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
const { memoryWrite } = await import('../context/memory.js');
|
|
1116
|
+
const p = await memoryWrite(cwd, cmd.text);
|
|
1117
|
+
queuedAppend({ id: `mem-${Date.now()}`, kind: 'text', text: `Noted → ${p}`, role: 'assistant' });
|
|
1118
|
+
}
|
|
1119
|
+
catch (err) {
|
|
1120
|
+
queuedAppend({ id: `mem-err-${Date.now()}`, kind: 'error', message: `memory append failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1121
|
+
}
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1098
1124
|
case 'jobs': {
|
|
1099
1125
|
const { listJobs } = await import('../tools/shell/background.js');
|
|
1100
1126
|
const jobs = listJobs();
|
|
@@ -1151,8 +1177,8 @@ export async function startRepl(opts = {}) {
|
|
|
1151
1177
|
case 'undo': {
|
|
1152
1178
|
try {
|
|
1153
1179
|
const { undo } = await import('../checkpoints/store.js');
|
|
1154
|
-
await undo(cwd);
|
|
1155
|
-
queuedAppend({ id: `undo-${Date.now()}`, kind: 'text', text:
|
|
1180
|
+
await undo(cwd, cmd.n ?? 1);
|
|
1181
|
+
queuedAppend({ id: `undo-${Date.now()}`, kind: 'text', text: `Undone ${cmd.n ?? 1} checkpoint(s)`, role: 'assistant' });
|
|
1156
1182
|
}
|
|
1157
1183
|
catch (err) {
|
|
1158
1184
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1161,10 +1187,47 @@ export async function startRepl(opts = {}) {
|
|
|
1161
1187
|
return;
|
|
1162
1188
|
}
|
|
1163
1189
|
case 'rewind': {
|
|
1190
|
+
// Numbered rewind menu: bare /rewind lists snapshots (1 = latest),
|
|
1191
|
+
// /rewind <n> restores, /rewind <n> summary also reports reverted files.
|
|
1192
|
+
const { listCheckpointInfo, undo } = await import('../checkpoints/store.js');
|
|
1193
|
+
const infos = await listCheckpointInfo(cwd);
|
|
1194
|
+
if (infos.length === 0) {
|
|
1195
|
+
queuedAppend({ id: `rewind-${Date.now()}`, kind: 'text', text: 'No checkpoints yet — snapshots are taken after each file mutation.', role: 'assistant' });
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const n = cmd.n ?? 1;
|
|
1199
|
+
if (cmd.n === undefined && !cmd.summary) {
|
|
1200
|
+
const rows = infos.slice(0, 10).map((c) => {
|
|
1201
|
+
const age = c.ts > 0 ? ` (${Math.max(1, Math.round((Date.now() - c.ts) / 60000))}m ago)` : '';
|
|
1202
|
+
return ` ${c.index}. ${c.id.slice(0, 12)} · ${c.files} file(s)${age}`;
|
|
1203
|
+
});
|
|
1204
|
+
queuedAppend({ id: `rewind-${Date.now()}`, kind: 'text', text: `Checkpoints (1 = latest):\n${rows.join('\n')}\nrun /rewind <n> to restore, /rewind <n> summary for a revert report`, role: 'assistant' });
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (n < 1 || n > infos.length) {
|
|
1208
|
+
queuedAppend({ id: `rewind-err-${Date.now()}`, kind: 'error', message: `rewind failed: only ${infos.length} checkpoint(s), got n=${n}` });
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1164
1211
|
try {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1212
|
+
// Capture the pre-restore dirty state so `summary` can report it.
|
|
1213
|
+
let before = '';
|
|
1214
|
+
if (cmd.summary) {
|
|
1215
|
+
try {
|
|
1216
|
+
const { execFileSync } = await import('node:child_process');
|
|
1217
|
+
before = execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf-8', timeout: 5000 });
|
|
1218
|
+
}
|
|
1219
|
+
catch {
|
|
1220
|
+
before = '';
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
await undo(cwd, n);
|
|
1224
|
+
const target = infos[n - 1];
|
|
1225
|
+
let text = `Rewound to checkpoint ${n} (${target.id.slice(0, 12)}, ${target.files} file(s))`;
|
|
1226
|
+
if (cmd.summary) {
|
|
1227
|
+
const files = before.split('\n').map((l) => l.trim()).filter(Boolean).slice(0, 20);
|
|
1228
|
+
text += files.length > 0 ? `\nReverted working-tree changes:\n${files.map((f) => ` ${f}`).join('\n')}` : '\nWorking tree was clean before restore.';
|
|
1229
|
+
}
|
|
1230
|
+
queuedAppend({ id: `rewind-${Date.now()}`, kind: 'text', text, role: 'assistant' });
|
|
1168
1231
|
}
|
|
1169
1232
|
catch (err) {
|
|
1170
1233
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1172,6 +1235,27 @@ export async function startRepl(opts = {}) {
|
|
|
1172
1235
|
}
|
|
1173
1236
|
return;
|
|
1174
1237
|
}
|
|
1238
|
+
case 'checkpoints': {
|
|
1239
|
+
try {
|
|
1240
|
+
const { listCheckpointInfo } = await import('../checkpoints/store.js');
|
|
1241
|
+
const infos = await listCheckpointInfo(cwd);
|
|
1242
|
+
if (infos.length === 0) {
|
|
1243
|
+
queuedAppend({ id: `ckpt-${Date.now()}`, kind: 'text', text: 'No checkpoints yet.', role: 'assistant' });
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
const rows = infos.slice(0, 15).map((c) => {
|
|
1247
|
+
const age = c.ts > 0 ? ` (${Math.max(1, Math.round((Date.now() - c.ts) / 60000))}m ago)` : '';
|
|
1248
|
+
return ` ${c.index}. ${c.id.slice(0, 12)} · ${c.files} file(s)${age}`;
|
|
1249
|
+
});
|
|
1250
|
+
queuedAppend({ id: `ckpt-${Date.now()}`, kind: 'text', text: `Checkpoints (1 = latest):\n${rows.join('\n')}`, role: 'assistant' });
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
catch (err) {
|
|
1254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1255
|
+
queuedAppend({ id: `ckpt-err-${Date.now()}`, kind: 'error', message: `checkpoints failed: ${msg}` });
|
|
1256
|
+
}
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1175
1259
|
case 'verify': {
|
|
1176
1260
|
const { detectVerifiers, primaryVerifyCommand } = await import('../verification/registry.js');
|
|
1177
1261
|
const { verify } = await import('../verification/engine.js');
|
|
@@ -1339,26 +1423,18 @@ export async function startRepl(opts = {}) {
|
|
|
1339
1423
|
return;
|
|
1340
1424
|
}
|
|
1341
1425
|
case 'init': {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1426
|
+
try {
|
|
1427
|
+
const { initProject, nextStepsText } = await import('./init.js');
|
|
1428
|
+
const { created, skipped } = await initProject(cwd);
|
|
1429
|
+
const lines = [
|
|
1430
|
+
...created.map((f) => `created ${f}`),
|
|
1431
|
+
...skipped.map((f) => `kept ${f}`),
|
|
1432
|
+
nextStepsText(),
|
|
1433
|
+
];
|
|
1434
|
+
queuedAppend({ id: `init-${Date.now()}`, kind: 'text', text: lines.join('\n'), role: 'assistant' });
|
|
1347
1435
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
const { runScan } = await import('./scan.js');
|
|
1351
|
-
let out = '';
|
|
1352
|
-
const orig = process.stdout.write.bind(process.stdout);
|
|
1353
|
-
process.stdout.write = ((c) => { out += String(c); return true; });
|
|
1354
|
-
await runScan({ cwd, json: false });
|
|
1355
|
-
process.stdout.write = orig;
|
|
1356
|
-
writeFileSync(target, `# KLYRO.md\n\nProject: ${cwd}\n\n## Stack\n\n${out.slice(0, 2000)}\n\n## Conventions\n\n- Prefer smallest change that solves the task.\n- Run verification after edits.\n`);
|
|
1357
|
-
queuedAppend({ id: `init2-${Date.now()}`, kind: 'text', text: `created ${target}`, role: 'assistant' });
|
|
1358
|
-
}
|
|
1359
|
-
catch (err) {
|
|
1360
|
-
queuedAppend({ id: `init-err-${Date.now()}`, kind: 'error', message: `init failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1361
|
-
}
|
|
1436
|
+
catch (err) {
|
|
1437
|
+
queuedAppend({ id: `init-err-${Date.now()}`, kind: 'error', message: `init failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1362
1438
|
}
|
|
1363
1439
|
return;
|
|
1364
1440
|
}
|
|
@@ -2573,7 +2649,7 @@ export async function startRepl(opts = {}) {
|
|
|
2573
2649
|
catch { /* persist best-effort */ }
|
|
2574
2650
|
if (isMounted && directHooks)
|
|
2575
2651
|
directHooks.setVimMode(next);
|
|
2576
|
-
queuedAppend({ id: `vim-${Date.now()}`, kind: 'text', text: `vim mode: ${next}${next === 'normal' ? ' (h/l move ·
|
|
2652
|
+
queuedAppend({ id: `vim-${Date.now()}`, kind: 'text', text: `vim mode: ${next}${next === 'normal' ? ' (h/l/0/$ move · 3h counts · w/b words · x/D/dd delete · i/a insert · j/k scroll)' : ''}`, role: 'assistant' });
|
|
2577
2653
|
return;
|
|
2578
2654
|
}
|
|
2579
2655
|
case 'theme':
|
|
@@ -2817,6 +2893,51 @@ export async function startRepl(opts = {}) {
|
|
|
2817
2893
|
await handleSlash(parse(target + extra));
|
|
2818
2894
|
return;
|
|
2819
2895
|
}
|
|
2896
|
+
// Custom command files: .klyro/commands/*.md (project wins).
|
|
2897
|
+
// Body expands $1..$9/$@ and runs as a prompt (recursion-guarded).
|
|
2898
|
+
if (m) {
|
|
2899
|
+
const { loadCustomCommands, expandArgs } = await import('./slash/custom.js');
|
|
2900
|
+
const custom = loadCustomCommands(cwd).find((c) => c.name === m[1].toLowerCase());
|
|
2901
|
+
if (custom) {
|
|
2902
|
+
const depth = (customDepthRef.current ?? 0) + 1;
|
|
2903
|
+
if (depth > 3) {
|
|
2904
|
+
queuedAppend({ id: `cust-${Date.now()}`, kind: 'error', message: `custom command recursion limit: /${custom.name}` });
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
const argList = m[2] ? m[2].split(/\s+/) : [];
|
|
2908
|
+
const expanded = expandArgs(custom.body, argList);
|
|
2909
|
+
customDepthRef.current = depth;
|
|
2910
|
+
try {
|
|
2911
|
+
if (expanded.trimStart().startsWith('/'))
|
|
2912
|
+
await handleSlash(parse(expanded));
|
|
2913
|
+
else
|
|
2914
|
+
await runWithBridge(expanded);
|
|
2915
|
+
}
|
|
2916
|
+
finally {
|
|
2917
|
+
customDepthRef.current = depth - 1;
|
|
2918
|
+
}
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
// MCP prompt-commands: /mcp__<server>__<prompt> [args...]
|
|
2923
|
+
if (m) {
|
|
2924
|
+
const pm = /^mcp__([A-Za-z0-9_-]{1,20})__([A-Za-z0-9_-]+)$/.exec(m[1].toLowerCase());
|
|
2925
|
+
if (pm) {
|
|
2926
|
+
const { runMcpPrompt } = await import('../mcp/registry.js');
|
|
2927
|
+
const argList = m[2] ? m[2].split(/\s+/) : [];
|
|
2928
|
+
const args = {};
|
|
2929
|
+
argList.forEach((a, i) => { args[`arg${i + 1}`] = a; });
|
|
2930
|
+
args['input'] = m[2] ?? '';
|
|
2931
|
+
try {
|
|
2932
|
+
const text = await runMcpPrompt(cwd, pm[1], pm[2], args);
|
|
2933
|
+
await runWithBridge(text);
|
|
2934
|
+
}
|
|
2935
|
+
catch (err) {
|
|
2936
|
+
queuedAppend({ id: `mcp-p-${Date.now()}`, kind: 'error', message: `mcp prompt failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
2937
|
+
}
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2820
2941
|
queuedAppend({
|
|
2821
2942
|
id: `unk-${Date.now()}`,
|
|
2822
2943
|
kind: 'error',
|
package/dist/cli/run.d.ts
CHANGED
|
@@ -66,6 +66,12 @@ export interface RunCliOptions {
|
|
|
66
66
|
persist?: boolean;
|
|
67
67
|
sessionId?: string;
|
|
68
68
|
sessionsDir?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Bare mode (`--bare`): skip MCP servers, all hooks, memory/KLYRO.md/L6
|
|
71
|
+
* context, and session persistence for fast deterministic runs. The model
|
|
72
|
+
* gets the base system prompt + tools only.
|
|
73
|
+
*/
|
|
74
|
+
bare?: boolean;
|
|
69
75
|
/** P1.4 — run the task under a named child-capable orchestrator context. */
|
|
70
76
|
agent?: string;
|
|
71
77
|
maxDepth?: number;
|
|
@@ -78,5 +84,5 @@ export interface RunCliOptions {
|
|
|
78
84
|
export declare function shouldForceExit(lastSigintAt: number | undefined, now: number): boolean;
|
|
79
85
|
export declare function runOnce(opts: RunCliOptions): Promise<number>;
|
|
80
86
|
/** Wrap a system-prompt fn to inject Level-6 context (project map etc.) + KLYRO.md (4.4). */
|
|
81
|
-
export declare function makeRunSystemPrompt(cwd: string, base: SystemPromptFn): Promise<SystemPromptFn>;
|
|
87
|
+
export declare function makeRunSystemPrompt(cwd: string, base: SystemPromptFn, bare?: boolean): Promise<SystemPromptFn>;
|
|
82
88
|
export declare function loadTranscript(path: string): Message[];
|
package/dist/cli/run.js
CHANGED
|
@@ -19,6 +19,7 @@ import { DenyAllApprovalPrompt } from '../policy/approval.js';
|
|
|
19
19
|
import { redact } from '../policy/secret-redactor.js';
|
|
20
20
|
import { buildLevel6Context } from '../context/level6.js';
|
|
21
21
|
import { memoryBlock } from '../context/memory.js';
|
|
22
|
+
import { estimateCost } from '../providers/model-info.js';
|
|
22
23
|
import { resolveSessionId } from '../persistence/session.js';
|
|
23
24
|
import * as fs from 'node:fs';
|
|
24
25
|
function readEnv(name, fallback) {
|
|
@@ -53,7 +54,8 @@ export async function runOnce(opts) {
|
|
|
53
54
|
catch { /* ignore */ }
|
|
54
55
|
// P1.4 — validate --agent early so typos fail fast (exit 2) even without API keys.
|
|
55
56
|
if (opts.agent) {
|
|
56
|
-
const {
|
|
57
|
+
const { listAllAgents } = await import('../agent/orchestrator.js');
|
|
58
|
+
const _known = listAllAgents(opts.cwd);
|
|
57
59
|
if (!_known.some((a) => a.id === opts.agent)) {
|
|
58
60
|
stderr.write(`klyro: unknown agent: ${opts.agent} (known: ${_known.map((a) => a.id).join(', ')})\n`);
|
|
59
61
|
return 2;
|
|
@@ -141,36 +143,39 @@ export async function runOnce(opts) {
|
|
|
141
143
|
// approve). Project-sourced servers auto-connect ONLY when their exact
|
|
142
144
|
// spec hash is already in the McpTrust store (e.g. approved in a prior
|
|
143
145
|
// REPL session); unknown specs are skipped with a warning.
|
|
146
|
+
// --bare skips MCP entirely (deterministic, no subprocesses).
|
|
144
147
|
let closeMcp;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
148
|
+
if (!opts.bare) {
|
|
149
|
+
try {
|
|
150
|
+
const { loadAndRegisterMcp } = await import('../mcp/registry.js');
|
|
151
|
+
const { McpTrust, hashSpec } = await import('../mcp/trust.js');
|
|
152
|
+
const { loadMcpServers } = await import('../mcp/config.js');
|
|
153
|
+
const mcpTrust = new McpTrust();
|
|
154
|
+
const mcpSpecs = loadMcpServers(opts.cwd).servers;
|
|
155
|
+
const mcp = await loadAndRegisterMcp({
|
|
156
|
+
cwd: opts.cwd,
|
|
157
|
+
registry,
|
|
158
|
+
policy,
|
|
159
|
+
approveProjectServer: async ({ name }) => {
|
|
160
|
+
const spec = mcpSpecs[name];
|
|
161
|
+
if (spec && mcpTrust.isTrusted(name, hashSpec(spec)))
|
|
162
|
+
return true;
|
|
163
|
+
stderr.write(`klyro: mcp project server "${name}" not in trust store (skipped — headless cannot prompt)\n`);
|
|
164
|
+
return false;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
for (const e of mcp.errors)
|
|
168
|
+
stderr.write(`klyro: mcp ${e.server}: ${e.message}\n`);
|
|
169
|
+
if (mcp.registered.length > 0 && output === 'human') {
|
|
170
|
+
stderr.write(`klyro: mcp tools: ${mcp.registered.join(', ')}\n`);
|
|
171
|
+
}
|
|
172
|
+
closeMcp = mcp.closeAll;
|
|
167
173
|
}
|
|
168
|
-
|
|
174
|
+
catch { /* ignore — MCP is optional */ }
|
|
169
175
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const persistEnabled = opts.persist !== false;
|
|
176
|
+
const systemPrompt = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt, opts.bare);
|
|
177
|
+
// Level 9 — session setup (create or resume). --bare skips persistence.
|
|
178
|
+
const persistEnabled = opts.persist !== false && !opts.bare;
|
|
174
179
|
let store;
|
|
175
180
|
let sessionId;
|
|
176
181
|
let initialTranscript;
|
|
@@ -256,17 +261,18 @@ export async function runOnce(opts) {
|
|
|
256
261
|
...(opts.verifyMode ? { mode: opts.verifyMode } : {}),
|
|
257
262
|
};
|
|
258
263
|
let result;
|
|
264
|
+
let endStatus = 'error';
|
|
259
265
|
// P1.4 — if --agent is requested, stand up a parent orchestrator so the
|
|
260
266
|
// model can call spawn_agent / task_list / task_get. The root run keeps
|
|
261
267
|
// depth 0; children are capped at maxDepth (default 1 per r-6-10.fix.md).
|
|
262
268
|
let agentBridge;
|
|
263
269
|
let parentContext;
|
|
264
270
|
if (opts.agent) {
|
|
265
|
-
const { AgentOrchestrator,
|
|
266
|
-
const def =
|
|
271
|
+
const { AgentOrchestrator, findAgent } = await import('../agent/orchestrator.js');
|
|
272
|
+
const def = findAgent(opts.agent, opts.cwd); // validated above
|
|
267
273
|
const maxDepth = opts.maxDepth ?? 1;
|
|
268
274
|
const rootDeps = { adapter, registry, policy, approval: new DenyAllApprovalPrompt(), systemPrompt };
|
|
269
|
-
const orchestrator = new AgentOrchestrator({ sessionId: sessionId ?? 'ephemeral', deps: rootDeps });
|
|
275
|
+
const orchestrator = new AgentOrchestrator({ sessionId: sessionId ?? 'ephemeral', deps: rootDeps, cwd: opts.cwd });
|
|
270
276
|
const allowedTools = new Set(registry.list().map((t) => t.name));
|
|
271
277
|
parentContext = {
|
|
272
278
|
sessionId: sessionId ?? 'ephemeral',
|
|
@@ -304,6 +310,7 @@ export async function runOnce(opts) {
|
|
|
304
310
|
task: opts.task,
|
|
305
311
|
cwd: opts.cwd,
|
|
306
312
|
model: opts.model,
|
|
313
|
+
...(opts.bare ? { bare: true } : {}),
|
|
307
314
|
maxSteps: opts.maxSteps,
|
|
308
315
|
maxTokens: opts.maxTokens,
|
|
309
316
|
temperature: opts.temperature,
|
|
@@ -371,6 +378,7 @@ export async function runOnce(opts) {
|
|
|
371
378
|
adapter, registry, policy, approval: new DenyAllApprovalPrompt(), systemPrompt,
|
|
372
379
|
...(failoverAdapters ? { failoverAdapters } : {}),
|
|
373
380
|
});
|
|
381
|
+
endStatus = result.status;
|
|
374
382
|
}
|
|
375
383
|
finally {
|
|
376
384
|
doneSigint();
|
|
@@ -378,6 +386,19 @@ export async function runOnce(opts) {
|
|
|
378
386
|
await closeMcp?.();
|
|
379
387
|
}
|
|
380
388
|
catch { /* ignore */ }
|
|
389
|
+
// sessionEnd hooks: best-effort end-of-run side effects (logging,
|
|
390
|
+
// notifications, cleanup). Skipped in --bare. Never affects exit code.
|
|
391
|
+
if (!opts.bare) {
|
|
392
|
+
try {
|
|
393
|
+
const { runSessionEndHooks } = await import('./hooks.js');
|
|
394
|
+
const outs = await runSessionEndHooks(opts.cwd, sessionId, endStatus);
|
|
395
|
+
for (const o of outs) {
|
|
396
|
+
if (output !== 'silent' && o.output)
|
|
397
|
+
stderr.write(`[hook ${o.name}] ${o.output.slice(0, 300)}\n`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch { /* ignore */ }
|
|
401
|
+
}
|
|
381
402
|
}
|
|
382
403
|
if (store && sessionId) {
|
|
383
404
|
// Finalize session status
|
|
@@ -406,52 +427,70 @@ export async function runOnce(opts) {
|
|
|
406
427
|
// Final cost line: headless runs previously surfaced cost only through
|
|
407
428
|
// [budget] warnings. Human/silent → stderr one-liner; json → additive
|
|
408
429
|
// cost_usd/usage fields on the final object (purely additive, no shape break).
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
430
|
+
const finalCost = estimateCost(opts.model, result.usage.input, result.usage.output);
|
|
431
|
+
const costLine = `$${finalCost.toFixed(4)} · ${result.usage.input} in / ${result.usage.output} out${result.usage.estimated ? ' (estimated)' : ''}`;
|
|
432
|
+
if (output === 'json') {
|
|
433
|
+
stdout.write(JSON.stringify({ kind: 'cost', cost_usd: finalCost, usage: result.usage }) + '\n');
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
stderr.write(`klyro: cost ${costLine}\n`);
|
|
437
|
+
}
|
|
438
|
+
// Stable result envelope (machine contract): exactly one `kind:result`
|
|
439
|
+
// line per run in json mode, after all legacy per-status lines. Parsers
|
|
440
|
+
// should read the LAST line; legacy `kind:final` lines are kept for compat.
|
|
441
|
+
const emitEnvelope = (exitCode, extra = {}) => {
|
|
413
442
|
if (output === 'json') {
|
|
414
|
-
stdout.write(JSON.stringify({
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
443
|
+
stdout.write(JSON.stringify({
|
|
444
|
+
kind: 'result',
|
|
445
|
+
status: result.status,
|
|
446
|
+
exit_code: exitCode,
|
|
447
|
+
text: result.finalText,
|
|
448
|
+
steps: result.steps,
|
|
449
|
+
toolCalls: result.toolCalls,
|
|
450
|
+
cost_usd: finalCost,
|
|
451
|
+
usage: result.usage,
|
|
452
|
+
...(result.verification ? { verification: result.verification } : {}),
|
|
453
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
454
|
+
...extra,
|
|
455
|
+
}) + '\n');
|
|
418
456
|
}
|
|
419
|
-
|
|
457
|
+
return exitCode;
|
|
458
|
+
};
|
|
420
459
|
if (result.status === 'max_steps') {
|
|
421
460
|
if (output === 'json')
|
|
422
461
|
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps }) + '\n');
|
|
423
462
|
else
|
|
424
463
|
stderr.write(`klyro: hit max steps (${result.steps}); consider raising --max-steps\n`);
|
|
425
|
-
return 7;
|
|
464
|
+
return emitEnvelope(7);
|
|
426
465
|
}
|
|
427
466
|
if (result.status === 'limit') {
|
|
428
467
|
if (output === 'json')
|
|
429
468
|
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps, text: result.finalText }) + '\n');
|
|
430
469
|
else
|
|
431
470
|
stderr.write(`klyro: stopped early: ${result.finalText || result.status} (after ${result.steps} steps)\n`);
|
|
432
|
-
return 7;
|
|
471
|
+
return emitEnvelope(7);
|
|
433
472
|
}
|
|
434
473
|
if (result.status === 'stuck') {
|
|
435
474
|
if (output === 'json')
|
|
436
475
|
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps }) + '\n');
|
|
437
476
|
else
|
|
438
477
|
stderr.write(`klyro: stuck — repeated the same action with no progress; aborting after ${result.steps} steps\n`);
|
|
439
|
-
return 7;
|
|
478
|
+
return emitEnvelope(7);
|
|
440
479
|
}
|
|
441
480
|
if (result.status === 'aborted') {
|
|
442
|
-
return 130;
|
|
481
|
+
return emitEnvelope(130);
|
|
443
482
|
}
|
|
444
483
|
if (result.status === 'no_final') {
|
|
445
484
|
if (output !== 'json')
|
|
446
485
|
stderr.write('klyro: provider error — no final answer\n');
|
|
447
|
-
return 5;
|
|
486
|
+
return emitEnvelope(5);
|
|
448
487
|
}
|
|
449
488
|
if (result.status === 'verify_failed') {
|
|
450
489
|
if (output === 'json')
|
|
451
490
|
stdout.write(JSON.stringify({ kind: 'final', status: 'verify_failed', failureType: result.verification?.failureType }) + '\n');
|
|
452
491
|
else
|
|
453
492
|
stderr.write(`klyro: verification failed after ${result.verification?.attempts ?? 3} repairs — see output above\n`);
|
|
454
|
-
return 8;
|
|
493
|
+
return emitEnvelope(8);
|
|
455
494
|
}
|
|
456
495
|
// 6.5 — --require-verify: if edits were made but verification never passed, exit 8
|
|
457
496
|
if (opts.requireVerify && result.verification && !result.verification.ok) {
|
|
@@ -459,7 +498,7 @@ export async function runOnce(opts) {
|
|
|
459
498
|
stdout.write(JSON.stringify({ kind: 'final', status: 'require_verify_failed' }) + '\n');
|
|
460
499
|
else
|
|
461
500
|
stderr.write('klyro: --require-verify: verification required but not passed\n');
|
|
462
|
-
return 8;
|
|
501
|
+
return emitEnvelope(8);
|
|
463
502
|
}
|
|
464
503
|
if (opts.requireVerify && !result.verification && result.hasEdits) {
|
|
465
504
|
// Edits were made but no verification command found
|
|
@@ -467,11 +506,11 @@ export async function runOnce(opts) {
|
|
|
467
506
|
stdout.write(JSON.stringify({ kind: 'final', status: 'require_verify_missing' }) + '\n');
|
|
468
507
|
else
|
|
469
508
|
stderr.write('klyro: --require-verify: no verification command found and edits were made\n');
|
|
470
|
-
return 8;
|
|
509
|
+
return emitEnvelope(8);
|
|
471
510
|
}
|
|
472
511
|
if (output === 'json')
|
|
473
512
|
stdout.write(JSON.stringify({ kind: 'final', status: 'ok', text: result.finalText }) + '\n');
|
|
474
|
-
return 0;
|
|
513
|
+
return emitEnvelope(0);
|
|
475
514
|
}
|
|
476
515
|
async function dryRunReport(opts) {
|
|
477
516
|
// Assemble the REAL prompt (Level-6 context + KLYRO.md), not the bare base —
|
|
@@ -509,7 +548,10 @@ function defaultRunSystemPrompt(_ctx) {
|
|
|
509
548
|
return _ctx.telemetry ? { system: base, suffix: _ctx.telemetry } : { system: base };
|
|
510
549
|
}
|
|
511
550
|
/** Wrap a system-prompt fn to inject Level-6 context (project map etc.) + KLYRO.md (4.4). */
|
|
512
|
-
export async function makeRunSystemPrompt(cwd, base) {
|
|
551
|
+
export async function makeRunSystemPrompt(cwd, base, bare = false) {
|
|
552
|
+
// --bare: base prompt only — no L6 scan, memory, or KLYRO.md (fast + deterministic).
|
|
553
|
+
if (bare)
|
|
554
|
+
return base;
|
|
513
555
|
const ctxBlock = await buildLevel6Context({ cwd });
|
|
514
556
|
const prefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
|
|
515
557
|
// Session memory: .klyro/memory/session-notes.md injected so memory_write
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface FrontmatterResult {
|
|
2
|
+
data: Record<string, string>;
|
|
3
|
+
body: string;
|
|
4
|
+
}
|
|
5
|
+
/** Minimal frontmatter parser: `---` fences + `key: value` lines only. */
|
|
6
|
+
export declare function parseFrontmatter(text: string): FrontmatterResult;
|
|
7
|
+
/** Parse a scalar list: `[a, b]`, `a, b`, or newline/`- ` items. */
|
|
8
|
+
export declare function parseList(value: string | undefined): string[];
|
|
9
|
+
export declare function parseBool(value: string | undefined, fallback: boolean): boolean;
|
|
10
|
+
export declare function parseInt_(value: string | undefined): number | undefined;
|
|
11
|
+
export interface CustomCommand {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
body: string;
|
|
15
|
+
source: 'project' | 'global';
|
|
16
|
+
}
|
|
17
|
+
/** Expand `$1..$9` and `$@` (all args joined by space). Missing args → ''. */
|
|
18
|
+
export declare function expandArgs(body: string, args: string[]): string;
|
|
19
|
+
/** Load custom commands: global first, project wins on name clash. Never throws. */
|
|
20
|
+
export declare function loadCustomCommands(cwd: string): CustomCommand[];
|
|
21
|
+
/**
|
|
22
|
+
* Relative file paths for `@`-completion: recursive walk, ignored dirs
|
|
23
|
+
* skipped, capped (directories sort first via trailing `/`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function listCompletableFiles(cwd: string): string[];
|