klyro 0.1.63 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/agent/anthropic-adapter.d.ts +36 -0
  2. package/dist/agent/anthropic-adapter.js +73 -16
  3. package/dist/agent/capabilities.d.ts +145 -0
  4. package/dist/agent/capabilities.js +191 -0
  5. package/dist/agent/child-worker.d.ts +104 -0
  6. package/dist/agent/child-worker.js +250 -0
  7. package/dist/agent/orchestrator.d.ts +232 -0
  8. package/dist/agent/orchestrator.js +589 -0
  9. package/dist/agent/provider-adapter.d.ts +17 -0
  10. package/dist/agent/provider-adapter.js +35 -3
  11. package/dist/agent/registry.d.ts +1 -0
  12. package/dist/agent/registry.js +1 -0
  13. package/dist/agent/retry.d.ts +13 -2
  14. package/dist/agent/retry.js +21 -3
  15. package/dist/agent/runtime.d.ts +70 -8
  16. package/dist/agent/runtime.js +205 -34
  17. package/dist/agent/scoped-registry.d.ts +22 -0
  18. package/dist/agent/scoped-registry.js +42 -0
  19. package/dist/agent/task-manager.d.ts +115 -0
  20. package/dist/agent/task-manager.js +250 -0
  21. package/dist/agent/worker-spawner.d.ts +17 -12
  22. package/dist/agent/worker-spawner.js +26 -20
  23. package/dist/agent/worktree-manager.d.ts +74 -0
  24. package/dist/agent/worktree-manager.js +189 -0
  25. package/dist/checkpoints/store.js +30 -5
  26. package/dist/cli/auth.js +16 -1
  27. package/dist/cli/config.d.ts +9 -3
  28. package/dist/cli/config.js +64 -3
  29. package/dist/cli/dotenv.d.ts +3 -0
  30. package/dist/cli/dotenv.js +57 -0
  31. package/dist/cli/eval.d.ts +6 -1
  32. package/dist/cli/eval.js +9 -0
  33. package/dist/cli/repl.js +183 -17
  34. package/dist/cli/run.d.ts +10 -11
  35. package/dist/cli/run.js +176 -14
  36. package/dist/cli/update.d.ts +5 -0
  37. package/dist/cli/update.js +62 -10
  38. package/dist/context/import-graph.d.ts +2 -0
  39. package/dist/context/import-graph.js +31 -3
  40. package/dist/context/klyro-md.d.ts +6 -0
  41. package/dist/context/klyro-md.js +25 -16
  42. package/dist/context/memory.d.ts +8 -0
  43. package/dist/context/memory.js +50 -2
  44. package/dist/context/project-map.d.ts +6 -0
  45. package/dist/context/project-map.js +50 -2
  46. package/dist/context/repo-map.d.ts +2 -0
  47. package/dist/context/repo-map.js +31 -1
  48. package/dist/context/trust.d.ts +42 -0
  49. package/dist/context/trust.js +111 -0
  50. package/dist/events/catalog.d.ts +99 -0
  51. package/dist/index.js +93 -4
  52. package/dist/mcp/client.d.ts +55 -0
  53. package/dist/mcp/client.js +294 -0
  54. package/dist/mcp/config.d.ts +40 -0
  55. package/dist/mcp/config.js +99 -0
  56. package/dist/mcp/policy.d.ts +13 -0
  57. package/dist/mcp/policy.js +12 -0
  58. package/dist/mcp/registry.d.ts +72 -0
  59. package/dist/mcp/registry.js +244 -0
  60. package/dist/mcp/schema.d.ts +18 -0
  61. package/dist/mcp/schema.js +57 -0
  62. package/dist/mcp/trust.d.ts +20 -0
  63. package/dist/mcp/trust.js +74 -0
  64. package/dist/persistence/audit.d.ts +28 -0
  65. package/dist/persistence/audit.js +101 -1
  66. package/dist/persistence/store.d.ts +26 -2
  67. package/dist/persistence/store.js +140 -13
  68. package/dist/policy/approval.d.ts +14 -0
  69. package/dist/policy/approval.js +44 -2
  70. package/dist/policy/engine.d.ts +1 -0
  71. package/dist/policy/engine.js +91 -10
  72. package/dist/policy/secret-redactor.js +4 -0
  73. package/dist/providers/model-info.d.ts +17 -0
  74. package/dist/providers/model-info.js +35 -2
  75. package/dist/repl.d.ts +6 -0
  76. package/dist/repl.js +12 -7
  77. package/dist/tools/agent/spawn-agent.d.ts +9 -0
  78. package/dist/tools/agent/spawn-agent.js +50 -0
  79. package/dist/tools/agent/task-apply.d.ts +4 -0
  80. package/dist/tools/agent/task-apply.js +44 -0
  81. package/dist/tools/agent/task-get.d.ts +8 -0
  82. package/dist/tools/agent/task-get.js +40 -0
  83. package/dist/tools/agent/task-list.d.ts +4 -0
  84. package/dist/tools/agent/task-list.js +41 -0
  85. package/dist/tools/agent/task-stop.d.ts +6 -0
  86. package/dist/tools/agent/task-stop.js +39 -0
  87. package/dist/tools/agent/task-wait.d.ts +17 -0
  88. package/dist/tools/agent/task-wait.js +79 -0
  89. package/dist/tools/fs/apply-patch.js +71 -0
  90. package/dist/tools/fs/edit-file.js +65 -0
  91. package/dist/tools/fs/multi-edit.d.ts +4 -0
  92. package/dist/tools/fs/multi-edit.js +66 -0
  93. package/dist/tools/fs/write-file.js +67 -0
  94. package/dist/tools/plan/todo-write.d.ts +1 -1
  95. package/dist/tools/registry.js +12 -0
  96. package/dist/tools/shell/background.js +6 -3
  97. package/dist/tools/shell/sandbox.d.ts +51 -0
  98. package/dist/tools/shell/sandbox.js +143 -0
  99. package/dist/tools/shell/shell-exec.d.ts +1 -0
  100. package/dist/tools/shell/shell-exec.js +83 -11
  101. package/dist/tools/shell/worker-entry.d.ts +12 -0
  102. package/dist/tools/shell/worker-entry.js +43 -0
  103. package/dist/tools/types.d.ts +18 -0
  104. package/dist/tools/verify/run-verify.js +3 -1
  105. package/dist/trace/writer.d.ts +13 -0
  106. package/dist/trace/writer.js +55 -4
  107. package/dist/tui/app.js +1 -1
  108. package/dist/tui/approval.js +20 -21
  109. package/dist/util.d.ts +1 -0
  110. package/dist/util.js +1 -0
  111. package/dist/verification/baseline.js +17 -3
  112. package/dist/verification/classify.js +23 -12
  113. package/dist/verification/engine.js +3 -1
  114. package/dist/verification/registry.d.ts +2 -0
  115. package/dist/verification/registry.js +33 -0
  116. package/dist/verification/scoped.js +28 -6
  117. package/package.json +1 -1
@@ -1,15 +1,49 @@
1
1
  /**
2
2
  * 8.4 — Working memory & reminders: memory_write → .klyro/memory/session-notes.md (≤1k tokens) + todos re-inject
3
+ *
4
+ * Durability: writes are atomic (tmp + rename in the same dir) so a crash
5
+ * never leaves a half-written note file. Oversized single writes are
6
+ * rejected with an Error (the memory_write tool's safe() wrapper converts
7
+ * that into a ToolResult error) instead of being silently truncated.
3
8
  */
4
9
  import * as fs from 'node:fs/promises';
10
+ import { readFileSync } from 'node:fs';
5
11
  import * as path from 'node:path';
12
+ import { redact } from '../policy/secret-redactor.js';
13
+ /** Single-write ceiling — anything larger is rejected, not sliced. */
14
+ export const MEMORY_WRITE_LIMIT_CHARS = 8000;
15
+ /** Steady-state cap (~1k tokens ≈ 4k chars) kept via tail slice. */
16
+ export const MEMORY_STEADY_STATE_CHARS = 4000;
6
17
  export async function memoryWrite(cwd, content) {
18
+ if (content.length > MEMORY_WRITE_LIMIT_CHARS) {
19
+ throw new Error(`memory budget exceeded: single write is ${content.length} chars (max ${MEMORY_WRITE_LIMIT_CHARS}) — split it into smaller notes`);
20
+ }
7
21
  const dir = path.join(cwd, '.klyro', 'memory');
8
22
  await fs.mkdir(dir, { recursive: true });
9
23
  const p = path.join(dir, 'session-notes.md');
10
24
  const prev = await fs.readFile(p, 'utf-8').catch(() => '');
11
- const next = (prev + '\n' + content).slice(-4000); // ≤1k tokens ~4k chars
12
- await fs.writeFile(p, next, 'utf-8');
25
+ // S4-at-rest: redact before appending — redact() only fires on secret
26
+ // shapes (key/token/password with [:=-]), so normal prose survives.
27
+ const next = (prev + '\n' + redact(content)).slice(-MEMORY_STEADY_STATE_CHARS); // ≤1k tokens ~4k chars
28
+ const tmp = path.join(dir, `.session-notes.md.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
29
+ await fs.writeFile(tmp, next, 'utf-8');
30
+ try {
31
+ const fh = await fs.open(tmp, 'r+');
32
+ try {
33
+ await fh.sync();
34
+ }
35
+ finally {
36
+ await fh.close();
37
+ }
38
+ }
39
+ catch { /* ignore on Windows */ }
40
+ try {
41
+ await fs.rename(tmp, p);
42
+ }
43
+ catch {
44
+ await fs.unlink(tmp).catch(() => undefined);
45
+ throw new Error('Failed to write memory file');
46
+ }
13
47
  return p;
14
48
  }
15
49
  export async function loadMemory(cwd) {
@@ -20,6 +54,20 @@ export async function loadMemory(cwd) {
20
54
  return '';
21
55
  }
22
56
  }
57
+ /** Synchronous read for the system-prompt build path (run on every turn). */
58
+ export function loadMemorySync(cwd) {
59
+ try {
60
+ return readFileSync(path.join(cwd, '.klyro', 'memory', 'session-notes.md'), 'utf-8');
61
+ }
62
+ catch {
63
+ return '';
64
+ }
65
+ }
66
+ /** Wrap persisted notes into the injected prompt block; '' when empty. */
67
+ export function memoryBlock(cwd) {
68
+ const notes = loadMemorySync(cwd).trim();
69
+ return notes ? `\n\n<memory>\n${notes.slice(0, 4000)}\n</memory>` : '';
70
+ }
23
71
  export function shouldRemind(turn, lastRemindTurn) {
24
72
  return turn - lastRemindTurn >= 20;
25
73
  }
@@ -45,6 +45,12 @@ export interface ProjectMap {
45
45
  hasDocker?: boolean;
46
46
  hasEnvExample?: boolean;
47
47
  }
48
+ /**
49
+ * Dirty-tree fingerprint: max mtimeMs over a bounded walk (≤200 files,
50
+ * depth ≤4, ignored dirs skipped). Cheap enough to run on every cache
51
+ * lookup; any edit under the tree changes the key and busts the cache.
52
+ */
53
+ export declare function dirtyMtime(root: string): Promise<number>;
48
54
  /** Build a project map for the repo rooted at `root`. */
49
55
  export declare function buildProjectMapCached(root: string): Promise<ProjectMap>;
50
56
  /** Build a project map for the repo rooted at `root`. */
@@ -383,7 +383,8 @@ function lockfileHash(root) {
383
383
  async function getCachedProjectMap(root) {
384
384
  const head = await gitHead(root);
385
385
  const hash = lockfileHash(root);
386
- const p = path.join(root, '.klyro', 'cache', `project-map-${head.slice(0, 8)}-${hash}.json`);
386
+ const dirty = await dirtyMtime(root);
387
+ const p = path.join(root, '.klyro', 'cache', `project-map-${head.slice(0, 8)}-${hash}-${dirty.toString(36)}.json`);
387
388
  try {
388
389
  const raw = await fs.readFile(p, 'utf-8');
389
390
  const j = JSON.parse(raw);
@@ -396,13 +397,60 @@ async function getCachedProjectMap(root) {
396
397
  async function setCachedProjectMap(root, m) {
397
398
  const head = await gitHead(root);
398
399
  const hash = lockfileHash(root);
399
- const p = path.join(root, '.klyro', 'cache', `project-map-${head.slice(0, 8)}-${hash}.json`);
400
+ const dirty = await dirtyMtime(root);
401
+ const p = path.join(root, '.klyro', 'cache', `project-map-${head.slice(0, 8)}-${hash}-${dirty.toString(36)}.json`);
400
402
  try {
401
403
  await fs.mkdir(path.dirname(p), { recursive: true });
402
404
  await fs.writeFile(p, JSON.stringify(m, null, 2), 'utf-8');
403
405
  }
404
406
  catch { /* ignore */ }
405
407
  }
408
+ /**
409
+ * Dirty-tree fingerprint: max mtimeMs over a bounded walk (≤200 files,
410
+ * depth ≤4, ignored dirs skipped). Cheap enough to run on every cache
411
+ * lookup; any edit under the tree changes the key and busts the cache.
412
+ */
413
+ export async function dirtyMtime(root) {
414
+ let max = 0;
415
+ let seen = 0;
416
+ let stop = false;
417
+ async function walk(dir, depth) {
418
+ if (stop || depth > 4)
419
+ return;
420
+ let entries;
421
+ try {
422
+ entries = await fs.readdir(dir, { withFileTypes: true });
423
+ }
424
+ catch {
425
+ return;
426
+ }
427
+ for (const e of entries) {
428
+ if (stop)
429
+ return;
430
+ if (IGNORED_DIRS.has(e.name))
431
+ continue;
432
+ const full = path.join(dir, e.name);
433
+ if (e.isDirectory()) {
434
+ await walk(full, depth + 1);
435
+ }
436
+ else if (e.isFile()) {
437
+ seen++;
438
+ try {
439
+ const s = await fs.stat(full);
440
+ if (s.mtimeMs > max)
441
+ max = s.mtimeMs;
442
+ }
443
+ catch { /* race */ }
444
+ if (seen >= 200) {
445
+ stop = true;
446
+ return;
447
+ }
448
+ }
449
+ }
450
+ }
451
+ await walk(root, 0);
452
+ return Math.floor(max);
453
+ }
406
454
  function detectMonorepo(root, rootFiles, rootDirs) {
407
455
  if (rootFiles.has('pnpm-workspace.yaml') || rootFiles.has('lerna.json') || rootFiles.has('nx.json'))
408
456
  return true;
@@ -18,6 +18,8 @@ export interface RepoFile {
18
18
  path: string;
19
19
  symbols: Symbol[];
20
20
  }
21
+ /** Invalidate the repo-map cache (tests + callers that mutate the tree). */
22
+ export declare function clearRepoMapCache(): void;
21
23
  /** Heuristic: walk cwd, return at most `maxFiles` files with symbols. */
22
24
  export declare function buildRepoMap(opts: {
23
25
  cwd: string;
@@ -12,11 +12,38 @@ import * as fs from 'node:fs/promises';
12
12
  import * as path from 'node:path';
13
13
  const SUPPORTED_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs']);
14
14
  const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.klyro', 'coverage', 'out']);
15
+ const REPO_MAP_TTL_MS = 60_000;
16
+ let repoMapCache = null;
17
+ /** Invalidate the repo-map cache (tests + callers that mutate the tree). */
18
+ export function clearRepoMapCache() {
19
+ repoMapCache = null;
20
+ }
21
+ /** Re-stat every file in the cached map; true when all stats still match. */
22
+ async function repoMapStatsMatch(cwd, stats) {
23
+ for (const [rel, prev] of stats) {
24
+ try {
25
+ const s = await fs.stat(path.join(cwd, rel));
26
+ if (s.mtimeMs !== prev.mtimeMs || s.size !== prev.size)
27
+ return false;
28
+ }
29
+ catch {
30
+ return false; // deleted (or unreadable) — rebuild
31
+ }
32
+ }
33
+ return true;
34
+ }
15
35
  /** Heuristic: walk cwd, return at most `maxFiles` files with symbols. */
16
36
  export async function buildRepoMap(opts) {
17
37
  const maxFiles = opts.maxFiles ?? 500;
18
38
  const maxFileBytes = opts.maxFileBytes ?? 256 * 1024;
39
+ const cached = repoMapCache;
40
+ if (cached && cached.cwd === opts.cwd && cached.maxFiles === maxFiles &&
41
+ cached.maxFileBytes === maxFileBytes && Date.now() - cached.at < REPO_MAP_TTL_MS &&
42
+ (await repoMapStatsMatch(opts.cwd, cached.stats))) {
43
+ return cached.files;
44
+ }
19
45
  const out = [];
46
+ const stats = new Map();
20
47
  await walk(opts.cwd, async (file) => {
21
48
  if (out.length >= maxFiles)
22
49
  return false;
@@ -39,9 +66,12 @@ export async function buildRepoMap(opts) {
39
66
  catch {
40
67
  return true;
41
68
  }
42
- out.push({ path: path.relative(opts.cwd, file), symbols: extractSymbols(content, ext) });
69
+ const rel = path.relative(opts.cwd, file);
70
+ stats.set(rel, { mtimeMs: stat.mtimeMs, size: stat.size });
71
+ out.push({ path: rel, symbols: extractSymbols(content, ext) });
43
72
  return true;
44
73
  });
74
+ repoMapCache = { cwd: opts.cwd, maxFiles, maxFileBytes, files: out, stats, at: Date.now() };
45
75
  return out;
46
76
  }
47
77
  async function walk(dir, visit) {
@@ -0,0 +1,42 @@
1
+ import type { KlyroEvent } from '../events/catalog.js';
2
+ import { type KlyroMdFile } from './klyro-md.js';
3
+ export interface TrustRecord {
4
+ sha256: string;
5
+ trustedAt: number;
6
+ }
7
+ export type TrustStore = Record<string, TrustRecord>;
8
+ export type TrustReason = 'unknown' | 'changed';
9
+ export declare function hashContent(content: string): string;
10
+ /** Pure evaluation: split files into trusted vs needs-approval. */
11
+ export declare function evaluateTrust(stored: TrustStore, files: KlyroMdFile[]): {
12
+ trusted: KlyroMdFile[];
13
+ untrusted: {
14
+ file: KlyroMdFile;
15
+ reason: TrustReason;
16
+ }[];
17
+ };
18
+ export declare function defaultTrustStorePath(): string;
19
+ export declare class ContextTrust {
20
+ private readonly storePath;
21
+ private store;
22
+ constructor(storePath?: string);
23
+ private load;
24
+ private save;
25
+ check(files: KlyroMdFile[]): ReturnType<typeof evaluateTrust>;
26
+ /** Record approval for the file's current content. */
27
+ approve(file: KlyroMdFile): void;
28
+ isTrusted(file: KlyroMdFile): boolean;
29
+ }
30
+ export interface TrustedLoadOpts {
31
+ trust: ContextTrust;
32
+ /** Return true to include an untrusted file (and persist the approval). */
33
+ approve: (file: KlyroMdFile, reason: TrustReason) => Promise<boolean>;
34
+ sessionId?: string;
35
+ emit?: (ev: KlyroEvent) => void;
36
+ }
37
+ /**
38
+ * Load KLYRO.md hierarchy with the trust gate applied. Untrusted files are
39
+ * offered to `approve`; declined files are excluded from the returned text.
40
+ * Every gate decision emits `context.trust_prompt` on the bus.
41
+ */
42
+ export declare function loadTrustedKlyroMd(cwd: string, opts: TrustedLoadOpts): Promise<string>;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * P0.3 — context-file trust gate (r-11-17.md §5.1).
3
+ *
4
+ * Closes the shared HIGH flaw: `KLYRO.md` / `AGENTS.md` are auto-loaded into
5
+ * the prompt, so a malicious commit can smuggle instructions that read as
6
+ * authenticated system guidance. Every auto-loaded file is hashed; the first
7
+ * sighting (or any change) requires explicit approval before the content may
8
+ * enter context. Decisions persist in `~/.klyro/context-trust.json`.
9
+ *
10
+ * Headless runs approve nothing (secure default): untrusted files are
11
+ * excluded and the exclusion is visible on the event bus.
12
+ */
13
+ import * as crypto from 'node:crypto';
14
+ import * as fs from 'node:fs';
15
+ import * as os from 'node:os';
16
+ import * as path from 'node:path';
17
+ import { loadKlyroMdFiles } from './klyro-md.js';
18
+ export function hashContent(content) {
19
+ return crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
20
+ }
21
+ /** Pure evaluation: split files into trusted vs needs-approval. */
22
+ export function evaluateTrust(stored, files) {
23
+ const trusted = [];
24
+ const untrusted = [];
25
+ for (const f of files) {
26
+ const rec = stored[f.path];
27
+ if (!rec) {
28
+ untrusted.push({ file: f, reason: 'unknown' });
29
+ }
30
+ else if (rec.sha256 !== hashContent(f.content)) {
31
+ untrusted.push({ file: f, reason: 'changed' });
32
+ }
33
+ else {
34
+ trusted.push(f);
35
+ }
36
+ }
37
+ return { trusted, untrusted };
38
+ }
39
+ export function defaultTrustStorePath() {
40
+ return path.join(os.homedir() || process.cwd(), '.klyro', 'context-trust.json');
41
+ }
42
+ export class ContextTrust {
43
+ storePath;
44
+ store = {};
45
+ constructor(storePath = defaultTrustStorePath()) {
46
+ this.storePath = storePath;
47
+ this.load();
48
+ }
49
+ load() {
50
+ try {
51
+ const raw = fs.readFileSync(this.storePath, 'utf-8');
52
+ const parsed = JSON.parse(raw);
53
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
54
+ this.store = parsed;
55
+ }
56
+ }
57
+ catch {
58
+ this.store = {};
59
+ }
60
+ }
61
+ save() {
62
+ try {
63
+ fs.mkdirSync(path.dirname(this.storePath), { recursive: true });
64
+ fs.writeFileSync(this.storePath, JSON.stringify(this.store, null, 2), 'utf-8');
65
+ }
66
+ catch {
67
+ /* best-effort — trust stays in memory for the session */
68
+ }
69
+ }
70
+ check(files) {
71
+ return evaluateTrust(this.store, files);
72
+ }
73
+ /** Record approval for the file's current content. */
74
+ approve(file) {
75
+ this.store[file.path] = { sha256: hashContent(file.content), trustedAt: Date.now() };
76
+ this.save();
77
+ }
78
+ isTrusted(file) {
79
+ const rec = this.store[file.path];
80
+ return !!rec && rec.sha256 === hashContent(file.content);
81
+ }
82
+ }
83
+ /**
84
+ * Load KLYRO.md hierarchy with the trust gate applied. Untrusted files are
85
+ * offered to `approve`; declined files are excluded from the returned text.
86
+ * Every gate decision emits `context.trust_prompt` on the bus.
87
+ */
88
+ export async function loadTrustedKlyroMd(cwd, opts) {
89
+ const sessionId = opts.sessionId ?? 'ephemeral';
90
+ const files = await loadKlyroMdFiles(cwd);
91
+ const { trusted, untrusted } = opts.trust.check(files);
92
+ const kept = [...trusted];
93
+ for (const { file, reason } of untrusted) {
94
+ let ok = false;
95
+ try {
96
+ ok = await opts.approve(file, reason);
97
+ }
98
+ catch {
99
+ ok = false;
100
+ }
101
+ opts.emit?.({ type: 'context.trust_prompt', ts: Date.now(), sessionId, path: file.path, reason, trusted: ok });
102
+ if (ok) {
103
+ opts.trust.approve(file);
104
+ kept.push(file);
105
+ }
106
+ }
107
+ // Preserve loader order.
108
+ const order = new Map(files.map((f, i) => [f.path, i]));
109
+ kept.sort((a, b) => (order.get(a.path) ?? 0) - (order.get(b.path) ?? 0));
110
+ return kept.map((f) => `# ${f.path}\n${f.content}`).join('\n\n---\n\n');
111
+ }
@@ -132,5 +132,104 @@ export type KlyroEvent = {
132
132
  ts: number;
133
133
  sessionId: string;
134
134
  reason: string;
135
+ } | {
136
+ type: 'subtask.started';
137
+ ts: number;
138
+ sessionId: string;
139
+ taskId: string;
140
+ parentTaskId?: string;
141
+ agentName: string;
142
+ depth: number;
143
+ model?: string;
144
+ } | {
145
+ type: 'subtask.progress';
146
+ ts: number;
147
+ sessionId: string;
148
+ taskId: string;
149
+ note: string;
150
+ } | {
151
+ type: 'subtask.completed';
152
+ ts: number;
153
+ sessionId: string;
154
+ taskId: string;
155
+ status: 'succeeded';
156
+ durationMs: number;
157
+ steps: number;
158
+ toolCalls: number;
159
+ } | {
160
+ type: 'subtask.failed';
161
+ ts: number;
162
+ sessionId: string;
163
+ taskId: string;
164
+ status: 'failed' | 'cancelled' | 'timed_out' | 'blocked';
165
+ durationMs: number;
166
+ error?: {
167
+ code: string;
168
+ message: string;
169
+ };
170
+ } | {
171
+ type: 'subtask.cancelled';
172
+ ts: number;
173
+ sessionId: string;
174
+ taskId: string;
175
+ status: 'cancelled';
176
+ durationMs: number;
177
+ error?: {
178
+ code: string;
179
+ message: string;
180
+ };
181
+ } | {
182
+ type: 'subtask.timed_out';
183
+ ts: number;
184
+ sessionId: string;
185
+ taskId: string;
186
+ status: 'timed_out';
187
+ durationMs: number;
188
+ error?: {
189
+ code: string;
190
+ message: string;
191
+ };
192
+ } | {
193
+ type: 'subtask.tool_call';
194
+ ts: number;
195
+ sessionId: string;
196
+ taskId: string;
197
+ callId: string;
198
+ name: string;
199
+ } | {
200
+ type: 'subtask.tool_result';
201
+ ts: number;
202
+ sessionId: string;
203
+ taskId: string;
204
+ callId: string;
205
+ name: string;
206
+ isError: boolean;
207
+ latencyMs: number;
208
+ } | {
209
+ type: 'subtask.merged';
210
+ ts: number;
211
+ sessionId: string;
212
+ taskId: string;
213
+ changedFiles: string[];
214
+ merged?: boolean;
215
+ } | {
216
+ type: 'provider.retry';
217
+ ts: number;
218
+ sessionId: string;
219
+ attempt: number;
220
+ status: string;
221
+ retryAfterMs?: number;
222
+ } | {
223
+ type: 'context.trust_prompt';
224
+ ts: number;
225
+ sessionId: string;
226
+ path: string;
227
+ reason: 'unknown' | 'changed';
228
+ trusted: boolean;
229
+ } | {
230
+ type: 'context.compacted';
231
+ ts: number;
232
+ sessionId: string;
233
+ dropped: number;
135
234
  };
136
235
  export type KlyroEventType = KlyroEvent['type'];
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ import { runCompletion } from './cli/completion.js';
23
23
  import { runUpdate } from './cli/update.js';
24
24
  import { runLogin, runLogout } from './cli/auth.js';
25
25
  import { readVersion } from './version.js';
26
+ import { verifyAuditChain } from './persistence/audit.js';
26
27
  const VERSION = readVersion();
27
28
  function parsePositiveInt(name, v) {
28
29
  const n = Number(v);
@@ -61,8 +62,21 @@ async function main() {
61
62
  .option('--output-format <fmt>', 'Headless output format: text|json|stream-json (default text)')
62
63
  .option('--no-stream', 'Disable streaming (buffer full response)')
63
64
  .option('--show-thinking', 'Show thinking blocks');
65
+ // Unknown commands / unknown options are usage errors (exit 2 per PRD).
66
+ // Help and version still exit 0; anything else rethrows to the last-resort
67
+ // handler (exit 1).
68
+ program.exitOverride((err) => {
69
+ if (err.code === 'commander.unknownCommand' || err.code === 'commander.unknownOption') {
70
+ process.stderr.write(`klyro: ${err.message}\n`);
71
+ process.exit(2);
72
+ }
73
+ if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version') {
74
+ process.exit(0);
75
+ }
76
+ throw err;
77
+ });
64
78
  // Handle global flags before any command runs
65
- program.hook('preAction', (thisCommand) => {
79
+ program.hook('preAction', async (thisCommand) => {
66
80
  const opts = thisCommand.optsWithGlobals();
67
81
  if (opts.cwd) {
68
82
  try {
@@ -73,6 +87,13 @@ async function main() {
73
87
  process.exit(2);
74
88
  }
75
89
  }
90
+ // Load <cwd>/.env globally (after chdir) so KLYRO_* vars resolve for
91
+ // every command. Never throws; explicit env wins (no-clobber).
92
+ try {
93
+ const { loadDotenv } = await import('./cli/dotenv.js');
94
+ loadDotenv(process.cwd());
95
+ }
96
+ catch { /* ignore */ }
76
97
  if (opts.config)
77
98
  process.env.KLYRO_CONFIG = opts.config;
78
99
  if (opts.debug)
@@ -271,9 +292,12 @@ async function main() {
271
292
  .option('--resume-session <id>', 'Resume from a persisted session (Level 9) by id prefix')
272
293
  .option('--verify', 'Enable verification after edits (Level 8, default: enabled)')
273
294
  .option('--verify-command <cmd>', 'Custom verification command (default: auto-detected)')
295
+ .option('--verify-mode <mode>', 'Verification mode: strict (default), advisory, off')
274
296
  .option('--max-repairs <n>', 'Max autonomous repair attempts (default 3)', (v) => parsePositiveInt('--max-repairs', v))
275
297
  .option('--persist', 'Enable session persistence (Level 9, default: enabled)')
276
298
  .option('--require-verify', 'Fail with exit 8 if no verification passed after edits (6.5)')
299
+ .option('--agent <name>', 'Run under an orchestrator context enabling spawn_agent/task_list/task_get (explorer|implementer|tester|reviewer)')
300
+ .option('--max-depth <n>', 'Max spawn depth for child agents (default 1)', (v) => parsePositiveInt('--max-depth', v))
277
301
  .action(async (prompt, opts) => {
278
302
  const model = opts.model ?? process.env.KLYRO_MODEL;
279
303
  if (!model) {
@@ -285,6 +309,11 @@ async function main() {
285
309
  process.stderr.write(`klyro: invalid --output: ${output} (expected human|json|silent)\n`);
286
310
  process.exit(2);
287
311
  }
312
+ const verifyMode = opts.verifyMode ?? 'strict';
313
+ if (verifyMode !== 'strict' && verifyMode !== 'advisory' && verifyMode !== 'off') {
314
+ process.stderr.write(`klyro: invalid --verify-mode: ${opts.verifyMode} (expected strict|advisory|off)\n`);
315
+ process.exit(2);
316
+ }
288
317
  // Provider validation is handled inside runOnce (single source of truth)
289
318
  try {
290
319
  const code = await runOnce({
@@ -304,9 +333,12 @@ async function main() {
304
333
  sessionId: opts.resumeSession,
305
334
  verify: opts.verify,
306
335
  verifyCommand: opts.verifyCommand,
336
+ verifyMode: verifyMode,
307
337
  maxRepairAttempts: opts.maxRepairs,
308
338
  persist: opts.persist,
309
339
  requireVerify: !!opts.requireVerify,
340
+ agent: opts.agent,
341
+ maxDepth: opts.maxDepth,
310
342
  });
311
343
  process.exit(code);
312
344
  }
@@ -566,15 +598,72 @@ async function main() {
566
598
  });
567
599
  // 10.1 — MCP
568
600
  const mcp = program.command('mcp').description('MCP client/server (10.1)');
569
- mcp.command('list').description('List MCP servers').action(async () => { process.stdout.write('mcp servers: (stub) github filesystem — use .mcp.json\n'); });
601
+ mcp.command('list').description('List MCP servers').action(async () => {
602
+ const { loadMcpServers } = await import('./mcp/config.js');
603
+ const cfg = loadMcpServers(process.cwd());
604
+ const names = Object.keys(cfg.servers);
605
+ if (names.length === 0) {
606
+ process.stdout.write('mcp servers: none configured (use .mcp.json)\n');
607
+ return;
608
+ }
609
+ for (const name of names) {
610
+ const spec = cfg.servers[name];
611
+ const source = cfg.sources[name] ?? 'unknown';
612
+ process.stdout.write(`${name} source=${source}${spec?.disabled ? ' disabled' : ''}\n`);
613
+ }
614
+ });
570
615
  mcp.command('add <name> <url>').description('Add MCP server').action(async (name) => { process.stdout.write(`added mcp ${name} (stub)\n`); });
571
616
  mcp.command('serve').description('Serve as MCP server').action(async () => { process.stdout.write('klyro mcp serve — exposing tools (stub)\n'); });
572
617
  // 10.2 — Hooks / agents
573
618
  program.command('hooks').description('List hooks (10.2)').action(async () => { process.stdout.write('hooks: SessionStart UserPromptSubmit PreToolUse PostToolUse (stub)\n'); });
574
- program.command('agents').description('List agents (10.2)').action(async () => { process.stdout.write('agents: explorer implementer tester reviewer (stub)\n'); });
619
+ program.command('agents [name]').description('List agents (10.2) or show one agent definition').action(async (name) => {
620
+ const { BUILTIN_AGENTS } = await import('./agent/orchestrator.js');
621
+ if (!name) {
622
+ for (const a of BUILTIN_AGENTS)
623
+ process.stdout.write(`${a.id} — ${a.description}\n`);
624
+ return;
625
+ }
626
+ const def = BUILTIN_AGENTS.find((a) => a.id === name);
627
+ if (!def) {
628
+ process.stderr.write(`klyro: unknown agent: ${name} (known: ${BUILTIN_AGENTS.map((a) => a.id).join(', ')})\n`);
629
+ process.exit(2);
630
+ }
631
+ const tools = def.allowedTools ?? ['<inherited: all parent tools>'];
632
+ const lines = [
633
+ `agent: ${def.id}`,
634
+ `description: ${def.description}`,
635
+ `tools (${tools.length}): ${tools.join(', ')}`,
636
+ `readonly: ${def.readonly ?? false}`,
637
+ `canSpawn: ${def.canSpawn ?? false}`,
638
+ `model: ${def.model ?? '<session default>'}`,
639
+ `maxSteps: ${def.maxSteps ?? '<default>'}`,
640
+ `maxTokens: ${def.maxTokens ?? '<default>'}`,
641
+ ];
642
+ process.stdout.write(lines.join('\n') + '\n');
643
+ });
575
644
  // 10.3 — Web / git workflows / SDK
576
645
  program.command('commit').description('Create commit (10.3)').action(async () => { process.stdout.write('commit — conventional message (stub, use /commit)\n'); });
577
- program.command('audit').description('Audit log (13.4)').action(async () => { process.stdout.write('audit — hash-chained JSONL (stub)\n'); });
646
+ program.command('audit [session]').description('Verify audit chain (13.4)').action(async (session) => {
647
+ if (!session) {
648
+ process.stderr.write('klyro: audit requires a session id (usage: klyro audit <session>)\n');
649
+ process.exit(2);
650
+ }
651
+ try {
652
+ const { getDefaultSessionsDir } = await import('./persistence/session.js');
653
+ const res = await verifyAuditChain(getDefaultSessionsDir(), session);
654
+ if (res.ok) {
655
+ process.stdout.write(`audit ok: ${res.events} events verified\n`);
656
+ process.exit(0);
657
+ }
658
+ process.stderr.write(`klyro: audit failed: ${res.error ?? 'chain broken'}\n`);
659
+ process.exit(1);
660
+ }
661
+ catch (err) {
662
+ const msg = err instanceof Error ? err.message : String(err);
663
+ process.stderr.write(`klyro: audit failed: ${msg}\n`);
664
+ process.exit(1);
665
+ }
666
+ });
578
667
  // 10.4 — Benchmark parity (10.5)
579
668
  program.command('benchmark').description('Run benchmark (10.5)').action(async () => {
580
669
  const { runHarness } = await import('./eval/harness.js');