kodi-dev 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/assets/agents/build/build-orchestrator.md +17 -6
  2. package/assets/agents/build/refactor-engineer.md +85 -0
  3. package/assets/agents/build/security.md +31 -0
  4. package/assets/rules/memory.md +48 -0
  5. package/assets/skills/remember/SKILL.md +56 -0
  6. package/assets/skills/retickets/SKILL.md +85 -0
  7. package/assets/skills/ticket-start/SKILL.md +3 -2
  8. package/board-dist/.next/BUILD_ID +1 -1
  9. package/board-dist/.next/build-manifest.json +3 -3
  10. package/board-dist/.next/prerender-manifest.json +3 -3
  11. package/board-dist/.next/server/app/_global-error.html +1 -1
  12. package/board-dist/.next/server/app/_global-error.rsc +1 -1
  13. package/board-dist/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  14. package/board-dist/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/board-dist/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  16. package/board-dist/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  17. package/board-dist/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/board-dist/.next/server/app/_not-found.html +1 -1
  19. package/board-dist/.next/server/app/_not-found.rsc +1 -1
  20. package/board-dist/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  21. package/board-dist/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  22. package/board-dist/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  23. package/board-dist/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  24. package/board-dist/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  25. package/board-dist/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/board-dist/.next/server/app/page/server-reference-manifest.json +1 -1
  27. package/board-dist/.next/server/app/page_client-reference-manifest.js +1 -1
  28. package/board-dist/.next/server/chunks/ssr/[root-of-the-server]__0_2b22l._.js +1 -1
  29. package/board-dist/.next/server/chunks/ssr/[root-of-the-server]__0eauvyk._.js +1 -1
  30. package/board-dist/.next/server/chunks/ssr/_1wslo41._.js +1 -1
  31. package/board-dist/.next/server/middleware-build-manifest.js +3 -3
  32. package/board-dist/.next/server/pages/404.html +1 -1
  33. package/board-dist/.next/server/pages/500.html +1 -1
  34. package/board-dist/.next/server/server-reference-manifest.js +1 -1
  35. package/board-dist/.next/server/server-reference-manifest.json +2 -2
  36. package/board-dist/.next/static/chunks/{2b8wyk_yjz96q.js → 3pq88fk6tzyrb.js} +1 -1
  37. package/dist/index.js +1393 -272
  38. package/package.json +2 -2
  39. /package/board-dist/.next/static/{jxv0GcmWplyzie2rdOiIo → I_lHe5DQGPJBZViq6L3bS}/_buildManifest.js +0 -0
  40. /package/board-dist/.next/static/{jxv0GcmWplyzie2rdOiIo → I_lHe5DQGPJBZViq6L3bS}/_clientMiddlewareManifest.js +0 -0
  41. /package/board-dist/.next/static/{jxv0GcmWplyzie2rdOiIo → I_lHe5DQGPJBZViq6L3bS}/_ssgManifest.js +0 -0
package/dist/index.js CHANGED
@@ -98,6 +98,10 @@ function registerAddCommand(program2) {
98
98
  });
99
99
  }
100
100
 
101
+ // src/commands/hook.ts
102
+ import { existsSync as existsSync4 } from "fs";
103
+ import { isAbsolute as isAbsolute3, relative as relative2 } from "path";
104
+
101
105
  // src/bootstrap.ts
102
106
  var ORCHESTRATOR_BOOTSTRAP = `# You are the kodi orchestrator
103
107
 
@@ -138,43 +142,19 @@ board \u2014 there is no message bus.
138
142
  - Manage tickets and PRs ONLY through the \`kodi\` CLI (\`kodi tickets \u2026\`,
139
143
  \`kodi pr \u2026\`) \u2014 it proxies \`gh\`/\`az\` and enforces the templates. Remote
140
144
  mutations are dry-run unless \`--yes\`.
145
+ - Persist and recall project knowledge with \`kodi memory\`: \`kodi memory query
146
+ <text>\` before working a subsystem, and \`kodi memory store\` a durable finding
147
+ (decision / gotcha / convention / architecture / reference / task-note) as you
148
+ learn it. Cross-session and project-scoped \u2014 see the memory rule.
141
149
  - The thin \`CLAUDE.md\` is the single source of truth for the stack, gate
142
150
  commands, provider, and installed skill-packs.
143
151
  `;
144
152
 
145
- // src/commands/hook.ts
146
- function registerHookCommand(program2) {
147
- const hook = program2.command("hook").description("Emit Claude Code hook output (internal)");
148
- hook.command("session-start").description("Emit the orchestrator bootstrap as SessionStart additionalContext").action(() => {
149
- const payload = {
150
- hookSpecificOutput: {
151
- hookEventName: "SessionStart",
152
- additionalContext: ORCHESTRATOR_BOOTSTRAP
153
- }
154
- };
155
- process.stdout.write(JSON.stringify(payload));
156
- });
157
- return hook;
158
- }
159
-
160
- // src/commands/init.ts
161
- import {
162
- copyFileSync as copyFileSync2,
163
- existsSync as existsSync4,
164
- mkdirSync as mkdirSync3,
165
- readdirSync as readdirSync2,
166
- readFileSync as readFileSync4,
167
- statSync as statSync2,
168
- writeFileSync as writeFileSync3
169
- } from "fs";
170
- import { dirname as dirname4, join as join3, relative as relative2 } from "path";
171
- import { fileURLToPath as fileURLToPath2 } from "url";
172
- import { stringify as stringifyYaml2 } from "yaml";
173
-
174
153
  // src/config.ts
175
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
154
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
155
+ import { homedir } from "os";
176
156
  import { dirname as dirname3, join as join2 } from "path";
177
- import { parse as parseYaml3 } from "yaml";
157
+ import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
178
158
 
179
159
  // src/providers/status-index.ts
180
160
  import {
@@ -260,21 +240,21 @@ var KEY_RE = /^[A-Z][A-Z0-9]*-\d+$/;
260
240
  var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
261
241
  var FILE_RE = /^([A-Z][A-Z0-9]*-\d+)-([a-z0-9]+(?:-[a-z0-9]+)*)\.md$/;
262
242
  function slugForStatus(status) {
263
- const slug = STATUS_TO_SLUG[status];
264
- if (slug === void 0) {
243
+ const slug2 = STATUS_TO_SLUG[status];
244
+ if (slug2 === void 0) {
265
245
  throw new Error(`unknown ticket status: ${String(status)}`);
266
246
  }
267
- return slug;
247
+ return slug2;
268
248
  }
269
249
  function assertKey(key) {
270
250
  if (!KEY_RE.test(key)) {
271
251
  throw new Error(`invalid ticket key ${JSON.stringify(key)}: must match ${KEY_RE.source}`);
272
252
  }
273
253
  }
274
- function assertSlug(slug) {
275
- if (!SLUG_RE.test(slug)) {
254
+ function assertSlug(slug2) {
255
+ if (!SLUG_RE.test(slug2)) {
276
256
  throw new Error(
277
- `invalid slug ${JSON.stringify(slug)}: must be strict kebab-case (${SLUG_RE.source})`
257
+ `invalid slug ${JSON.stringify(slug2)}: must be strict kebab-case (${SLUG_RE.source})`
278
258
  );
279
259
  }
280
260
  }
@@ -284,10 +264,10 @@ function isTicketStatus(value) {
284
264
  function freshTickets() {
285
265
  return /* @__PURE__ */ Object.create(null);
286
266
  }
287
- function composeFile(status, key, slug) {
267
+ function composeFile(status, key, slug2) {
288
268
  assertKey(key);
289
- assertSlug(slug);
290
- return `${slugForStatus(status)}/${key}-${slug}.md`;
269
+ assertSlug(slug2);
270
+ return `${slugForStatus(status)}/${key}-${slug2}.md`;
291
271
  }
292
272
  function emptyDocument() {
293
273
  return {
@@ -302,12 +282,12 @@ function serialize(doc) {
302
282
  const entry = doc.tickets[key];
303
283
  tickets[key] = { column: entry.column, file: entry.file };
304
284
  }
305
- const out2 = {
285
+ const out3 = {
306
286
  version: doc.version,
307
287
  columns: [...TICKET_STATUSES],
308
288
  tickets
309
289
  };
310
- const text = stringifyYaml(out2, { lineWidth: 0 });
290
+ const text = stringifyYaml(out3, { lineWidth: 0 });
311
291
  return text.endsWith("\n") ? text : `${text}
312
292
  `;
313
293
  }
@@ -406,6 +386,13 @@ function resolveFile(statusYamlPath, key, entry) {
406
386
  }
407
387
 
408
388
  // src/config.ts
389
+ function kodiHome() {
390
+ const override = process.env.KODI_HOME?.trim();
391
+ return override && override.length > 0 ? override : join2(homedir(), ".kodi");
392
+ }
393
+ function ragDbPath() {
394
+ return join2(kodiHome(), "rag.db");
395
+ }
409
396
  var DEFAULTS = { provider: "local", prefix: "KODI" };
410
397
  var STATE_FILE = "kodi-dev.yaml";
411
398
  function stateFilePath(root) {
@@ -430,6 +417,18 @@ function loadBoardConfig(cwd = process.cwd()) {
430
417
  return { ...DEFAULTS };
431
418
  }
432
419
  }
420
+ function writeMemoryBinding(root, memory) {
421
+ const path = stateFilePath(root);
422
+ if (!existsSync3(path)) return;
423
+ let raw = {};
424
+ try {
425
+ raw = parseYaml3(readFileSync3(path, "utf-8")) ?? {};
426
+ } catch {
427
+ raw = {};
428
+ }
429
+ raw.memory = memory;
430
+ writeFileSync3(path, stringifyYaml2(raw), "utf-8");
431
+ }
433
432
  function localPaths(cwd = process.cwd()) {
434
433
  const root = join2(findProjectRoot(cwd), "docs", "tickets");
435
434
  return {
@@ -443,6 +442,774 @@ function localPaths(cwd = process.cwd()) {
443
442
  };
444
443
  }
445
444
 
445
+ // src/memory/db.ts
446
+ import { mkdirSync as mkdirSync3 } from "fs";
447
+ import { createRequire } from "module";
448
+ import { dirname as dirname4 } from "path";
449
+ var nodeRequire = createRequire(import.meta.url);
450
+ var nodeSqlite = nodeRequire(["node", "sqlite"].join(":"));
451
+ var SCHEMA = `
452
+ CREATE TABLE IF NOT EXISTS collections (
453
+ id TEXT PRIMARY KEY,
454
+ name TEXT NOT NULL,
455
+ root_path TEXT UNIQUE,
456
+ created_at TEXT NOT NULL
457
+ );
458
+
459
+ CREATE TABLE IF NOT EXISTS memories (
460
+ id TEXT PRIMARY KEY,
461
+ collection_id TEXT NOT NULL,
462
+ content TEXT NOT NULL,
463
+ title TEXT NOT NULL,
464
+ type TEXT NOT NULL,
465
+ ticket TEXT,
466
+ files_json TEXT NOT NULL DEFAULT '[]',
467
+ created_at TEXT NOT NULL,
468
+ content_hash TEXT NOT NULL,
469
+ -- veracity score (see docs/memory-veracity-score.md): 0-5, fresh = 3.
470
+ score INTEGER NOT NULL DEFAULT 3,
471
+ status TEXT NOT NULL DEFAULT 'active', -- active | tombstoned
472
+ needs_reverify INTEGER NOT NULL DEFAULT 0,
473
+ file_hashes TEXT, -- JSON { path: sha256 }
474
+ verified_at TEXT,
475
+ tombstone_reason TEXT,
476
+ UNIQUE(collection_id, content_hash)
477
+ );
478
+ CREATE INDEX IF NOT EXISTS idx_memories_collection ON memories(collection_id);
479
+
480
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
481
+ memory_id UNINDEXED,
482
+ content,
483
+ title
484
+ );
485
+ `;
486
+ var VERACITY_COLUMNS = [
487
+ ["score", "INTEGER NOT NULL DEFAULT 3"],
488
+ ["status", "TEXT NOT NULL DEFAULT 'active'"],
489
+ ["needs_reverify", "INTEGER NOT NULL DEFAULT 0"],
490
+ ["file_hashes", "TEXT"],
491
+ ["verified_at", "TEXT"],
492
+ ["tombstone_reason", "TEXT"]
493
+ ];
494
+ function migrate(db) {
495
+ const rows = db.prepare("PRAGMA table_info(memories)").all();
496
+ const cols = new Set(rows.map((c) => c.name));
497
+ for (const [name2, ddl] of VERACITY_COLUMNS) {
498
+ if (!cols.has(name2)) db.exec(`ALTER TABLE memories ADD COLUMN ${name2} ${ddl}`);
499
+ }
500
+ }
501
+ function openDb(path = ragDbPath()) {
502
+ mkdirSync3(dirname4(path), { recursive: true });
503
+ const db = new nodeSqlite.DatabaseSync(path);
504
+ db.exec("PRAGMA busy_timeout = 5000;");
505
+ db.exec("PRAGMA journal_mode = WAL;");
506
+ db.exec("PRAGMA synchronous = NORMAL;");
507
+ db.exec("PRAGMA foreign_keys = ON;");
508
+ db.exec(SCHEMA);
509
+ migrate(db);
510
+ return db;
511
+ }
512
+
513
+ // src/memory/store.ts
514
+ import { createHash as createHash3, randomUUID } from "crypto";
515
+ import { basename as basename3 } from "path";
516
+
517
+ // src/memory/template.ts
518
+ import { createHash } from "crypto";
519
+ import { basename as basename2 } from "path";
520
+ import { z as z3 } from "zod";
521
+ var MEMORY_TYPES = [
522
+ "decision",
523
+ // a choice made + why (mini-ADR)
524
+ "gotcha",
525
+ // a trap / bug root-cause to not repeat
526
+ "convention",
527
+ // a project rule/pattern to follow
528
+ "architecture",
529
+ // how a subsystem/flow works
530
+ "reference",
531
+ // pointer to a doc/URL/resource
532
+ "task-note"
533
+ // context tied to a ticket's implementation
534
+ ];
535
+ var MemoryDraftSchema = z3.object({
536
+ content: z3.string().trim().min(1, "content is required"),
537
+ type: z3.enum(MEMORY_TYPES),
538
+ /** Ticket/task in flight when this was learned — optional (null when absent). */
539
+ ticket: z3.string().trim().min(1).nullish().transform((v) => v ?? null),
540
+ /**
541
+ * Repo-relative paths the finding touches — at least one is REQUIRED: the veracity
542
+ * loop verifies a memory against its files, so a memory with no file can't be scored.
543
+ */
544
+ files: z3.array(z3.string().trim().min(1)).min(1, "at least one --file is required"),
545
+ /** Optional display title; a preview of `content` is derived when omitted. */
546
+ title: z3.string().trim().min(1).optional()
547
+ });
548
+ var MemoryImportRecordSchema = z3.object({
549
+ content: z3.string().trim().min(1),
550
+ type: z3.enum(MEMORY_TYPES),
551
+ ticket: z3.string().trim().min(1).nullish().transform((v) => v ?? null),
552
+ files: z3.array(z3.string().trim().min(1)).min(1, "at least one file is required"),
553
+ title: z3.string().trim().min(1).optional(),
554
+ /** Preserved when present so an export→import keeps the original timestamp. */
555
+ createdAt: z3.string().trim().min(1).optional()
556
+ });
557
+ var MemoryExportDocSchema = z3.object({
558
+ collection: z3.string().optional(),
559
+ exportedAt: z3.string().optional(),
560
+ memories: z3.array(MemoryImportRecordSchema).default([])
561
+ });
562
+ function contentHash(content) {
563
+ return createHash("sha256").update(content.trim()).digest("hex");
564
+ }
565
+ function derivePreview(content, max = 80) {
566
+ const firstLine = content.trim().split("\n", 1)[0].trim();
567
+ return firstLine.length > max ? firstLine.slice(0, max - 1).trimEnd() + "\u2026" : firstLine;
568
+ }
569
+ function fileNames(files) {
570
+ return files.map((f) => basename2(f));
571
+ }
572
+
573
+ // src/memory/veracity.ts
574
+ import { createHash as createHash2 } from "crypto";
575
+ import { readFileSync as readFileSync4 } from "fs";
576
+ import { isAbsolute as isAbsolute2, join as join3 } from "path";
577
+ var SCORE_FRESH = 3;
578
+ var SCORE_MAX = 5;
579
+ var SCORE_STALE_CAP = 2;
580
+ var AUTO_INJECT_MIN = 4;
581
+ var RELEVANCE_MIN = 3;
582
+ function hashFile(root, relPath) {
583
+ const abs = isAbsolute2(relPath) ? relPath : join3(root, relPath);
584
+ try {
585
+ return createHash2("sha256").update(readFileSync4(abs)).digest("hex");
586
+ } catch {
587
+ return "missing";
588
+ }
589
+ }
590
+ function hashFiles(root, files) {
591
+ const out3 = {};
592
+ for (const f of files) out3[f] = hashFile(root, f);
593
+ return out3;
594
+ }
595
+ function anyFileChanged(root, fileHashes) {
596
+ if (!fileHashes) return false;
597
+ for (const [path, stored] of Object.entries(fileHashes)) {
598
+ if (hashFile(root, path) !== stored) return true;
599
+ }
600
+ return false;
601
+ }
602
+ function parseFileHashes(json) {
603
+ if (!json) return null;
604
+ try {
605
+ return JSON.parse(json);
606
+ } catch {
607
+ return null;
608
+ }
609
+ }
610
+
611
+ // src/memory/store.ts
612
+ function tx(db, fn) {
613
+ db.exec("BEGIN IMMEDIATE");
614
+ try {
615
+ const result = fn();
616
+ db.exec("COMMIT");
617
+ return result;
618
+ } catch (e) {
619
+ try {
620
+ db.exec("ROLLBACK");
621
+ } catch {
622
+ }
623
+ throw e;
624
+ }
625
+ }
626
+ function queryOne(db, sql, ...params) {
627
+ return db.prepare(sql).get(...params);
628
+ }
629
+ function queryAll(db, sql, ...params) {
630
+ return db.prepare(sql).all(...params);
631
+ }
632
+ function slug(s) {
633
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
634
+ }
635
+ function shortHash(s) {
636
+ return createHash3("sha256").update(s).digest("hex").slice(0, 6);
637
+ }
638
+ function ensureCollectionRow(db, id, name2, root) {
639
+ db.prepare(
640
+ "INSERT INTO collections (id, name, root_path, created_at) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING"
641
+ ).run(id, name2, root, (/* @__PURE__ */ new Date()).toISOString());
642
+ }
643
+ function provisionCollection(db, root, preferredName) {
644
+ const name2 = slug(preferredName ?? basename3(root));
645
+ const id = `${name2}-${shortHash(root)}`;
646
+ ensureCollectionRow(db, id, name2, root);
647
+ const row = queryOne(
648
+ db,
649
+ "SELECT id, name FROM collections WHERE root_path = ?",
650
+ root
651
+ );
652
+ return row ? { collection: row.id, name: row.name } : { collection: id, name: name2 };
653
+ }
654
+ function resolveCollection(db, cwd = process.cwd()) {
655
+ const root = findProjectRoot(cwd);
656
+ const cfg = loadBoardConfig(cwd);
657
+ if (cfg.memory?.collection) {
658
+ const name2 = cfg.memory.name ?? cfg.memory.collection;
659
+ ensureCollectionRow(db, cfg.memory.collection, name2, root);
660
+ return { collection: cfg.memory.collection, name: name2 };
661
+ }
662
+ const provisioned = provisionCollection(db, root);
663
+ writeMemoryBinding(root, provisioned);
664
+ return provisioned;
665
+ }
666
+ function lookupCollection(db, cwd = process.cwd()) {
667
+ const cfg = loadBoardConfig(cwd);
668
+ if (cfg.memory?.collection) {
669
+ return { collection: cfg.memory.collection, name: cfg.memory.name ?? cfg.memory.collection };
670
+ }
671
+ const row = queryOne(
672
+ db,
673
+ "SELECT id, name FROM collections WHERE root_path = ?",
674
+ findProjectRoot(cwd)
675
+ );
676
+ return row ? { collection: row.id, name: row.name } : null;
677
+ }
678
+ function rowToRecord(r) {
679
+ return {
680
+ id: r.id,
681
+ collection: r.collection_id,
682
+ content: r.content,
683
+ title: r.title,
684
+ type: r.type,
685
+ ticket: r.ticket ?? null,
686
+ files: JSON.parse(r.files_json || "[]"),
687
+ createdAt: r.created_at,
688
+ contentHash: r.content_hash,
689
+ score: r.score,
690
+ status: r.status,
691
+ needsReverify: !!r.needs_reverify,
692
+ fileHashes: parseFileHashes(r.file_hashes),
693
+ verifiedAt: r.verified_at ?? null,
694
+ tombstoneReason: r.tombstone_reason ?? null
695
+ };
696
+ }
697
+ function rawInsert(db, collectionId, v) {
698
+ const id = `mem_${randomUUID().slice(0, 8)}`;
699
+ const fileHashesJson = JSON.stringify(v.fileHashes);
700
+ tx(db, () => {
701
+ db.prepare(
702
+ `INSERT INTO memories
703
+ (id, collection_id, content, title, type, ticket, files_json, created_at, content_hash,
704
+ score, status, needs_reverify, file_hashes, verified_at)
705
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, NULL)`
706
+ ).run(
707
+ id,
708
+ collectionId,
709
+ v.content,
710
+ v.title,
711
+ v.type,
712
+ v.ticket,
713
+ JSON.stringify(v.files),
714
+ v.createdAt,
715
+ v.hash,
716
+ SCORE_FRESH,
717
+ fileHashesJson
718
+ );
719
+ db.prepare("INSERT INTO memories_fts (memory_id, content, title) VALUES (?, ?, ?)").run(
720
+ id,
721
+ v.content,
722
+ v.title
723
+ );
724
+ });
725
+ return {
726
+ id,
727
+ collection: collectionId,
728
+ content: v.content,
729
+ title: v.title,
730
+ type: v.type,
731
+ ticket: v.ticket,
732
+ files: v.files,
733
+ createdAt: v.createdAt,
734
+ contentHash: v.hash,
735
+ score: SCORE_FRESH,
736
+ status: "active",
737
+ needsReverify: false,
738
+ fileHashes: v.fileHashes,
739
+ verifiedAt: null,
740
+ tombstoneReason: null
741
+ };
742
+ }
743
+ function insertMemory(db, collectionId, draft, root) {
744
+ const hash = contentHash(draft.content);
745
+ const existing = queryOne(
746
+ db,
747
+ "SELECT * FROM memories WHERE collection_id = ? AND content_hash = ?",
748
+ collectionId,
749
+ hash
750
+ );
751
+ if (existing) {
752
+ if (existing.status === "tombstoned") {
753
+ return { record: rowToRecord(existing), deduped: false, blocked: true };
754
+ }
755
+ return { record: rowToRecord(existing), deduped: true };
756
+ }
757
+ const record = rawInsert(db, collectionId, {
758
+ content: draft.content,
759
+ title: draft.title ?? derivePreview(draft.content),
760
+ type: draft.type,
761
+ ticket: draft.ticket ?? null,
762
+ files: draft.files,
763
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
764
+ hash,
765
+ fileHashes: hashFiles(root, draft.files)
766
+ });
767
+ return { record, deduped: false };
768
+ }
769
+ function amendMemory(db, id, patch, root) {
770
+ const cur = queryOne(db, "SELECT * FROM memories WHERE id = ?", id);
771
+ if (!cur) return null;
772
+ const prev = rowToRecord(cur);
773
+ const content = patch.content ?? prev.content;
774
+ const title = patch.title ?? (patch.content ? derivePreview(patch.content) : prev.title);
775
+ const type = patch.type ?? prev.type;
776
+ const ticket = patch.ticket !== void 0 ? patch.ticket : prev.ticket;
777
+ const files = patch.files ?? prev.files;
778
+ const hash = contentHash(content);
779
+ if (hash !== prev.contentHash) {
780
+ const clash = queryOne(
781
+ db,
782
+ "SELECT id FROM memories WHERE collection_id = ? AND content_hash = ? AND id <> ?",
783
+ prev.collection,
784
+ hash,
785
+ id
786
+ );
787
+ if (clash) {
788
+ throw new Error(
789
+ `another memory (${clash.id}) already has identical content in this project.`
790
+ );
791
+ }
792
+ }
793
+ const fileHashes = hashFiles(root, files);
794
+ tx(db, () => {
795
+ db.prepare(
796
+ `UPDATE memories SET content=?, title=?, type=?, ticket=?, files_json=?, content_hash=?,
797
+ score=?, needs_reverify=0, file_hashes=?, verified_at=NULL WHERE id=?`
798
+ ).run(
799
+ content,
800
+ title,
801
+ type,
802
+ ticket,
803
+ JSON.stringify(files),
804
+ hash,
805
+ SCORE_FRESH,
806
+ JSON.stringify(fileHashes),
807
+ id
808
+ );
809
+ db.prepare("UPDATE memories_fts SET content=?, title=? WHERE memory_id=?").run(
810
+ content,
811
+ title,
812
+ id
813
+ );
814
+ });
815
+ return {
816
+ ...prev,
817
+ content,
818
+ title,
819
+ type,
820
+ ticket,
821
+ files,
822
+ contentHash: hash,
823
+ score: SCORE_FRESH,
824
+ needsReverify: false,
825
+ fileHashes,
826
+ verifiedAt: null
827
+ };
828
+ }
829
+ function verifyMemory(db, id, pass, root, reason) {
830
+ const cur = queryOne(db, "SELECT * FROM memories WHERE id = ?", id);
831
+ if (!cur) return null;
832
+ const prev = rowToRecord(cur);
833
+ if (prev.status === "tombstoned") return prev;
834
+ if (pass) {
835
+ const fileHashes = hashFiles(root, prev.files);
836
+ const score = Math.min(SCORE_MAX, prev.score + 1);
837
+ const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
838
+ tx(db, () => {
839
+ db.prepare(
840
+ "UPDATE memories SET score=?, needs_reverify=0, file_hashes=?, verified_at=? WHERE id=?"
841
+ ).run(score, JSON.stringify(fileHashes), verifiedAt, id);
842
+ });
843
+ return { ...prev, score, needsReverify: false, fileHashes, verifiedAt };
844
+ }
845
+ const tombstoneReason = reason ?? "refuted on verification";
846
+ tx(db, () => {
847
+ db.prepare(
848
+ "UPDATE memories SET status='tombstoned', score=0, needs_reverify=0, tombstone_reason=? WHERE id=?"
849
+ ).run(tombstoneReason, id);
850
+ db.prepare("DELETE FROM memories_fts WHERE memory_id=?").run(id);
851
+ });
852
+ return { ...prev, status: "tombstoned", score: 0, needsReverify: false, tombstoneReason };
853
+ }
854
+ function flagStaleForFile(db, root, relPath) {
855
+ const rows = queryAll(
856
+ db,
857
+ "SELECT * FROM memories WHERE status='active' AND EXISTS (SELECT 1 FROM json_each(files_json) WHERE value = ?)",
858
+ relPath
859
+ );
860
+ const current = hashFile(root, relPath);
861
+ let flagged = 0;
862
+ for (const r of rows) {
863
+ const stored = parseFileHashes(r.file_hashes) ?? {};
864
+ if (stored[relPath] === current) continue;
865
+ const merged = { ...stored, [relPath]: current };
866
+ db.prepare("UPDATE memories SET needs_reverify=1, score=?, file_hashes=? WHERE id=?").run(
867
+ Math.min(r.score, SCORE_STALE_CAP),
868
+ JSON.stringify(merged),
869
+ r.id
870
+ );
871
+ flagged++;
872
+ }
873
+ return flagged;
874
+ }
875
+ function reconcileStale(db, root, collectionId) {
876
+ const rows = queryAll(
877
+ db,
878
+ "SELECT * FROM memories WHERE collection_id=? AND status='active' AND needs_reverify=0",
879
+ collectionId
880
+ );
881
+ let flagged = 0;
882
+ for (const r of rows) {
883
+ if (!anyFileChanged(root, parseFileHashes(r.file_hashes))) continue;
884
+ const files = JSON.parse(r.files_json || "[]");
885
+ db.prepare("UPDATE memories SET needs_reverify=1, score=?, file_hashes=? WHERE id=?").run(
886
+ Math.min(r.score, SCORE_STALE_CAP),
887
+ JSON.stringify(hashFiles(root, files)),
888
+ r.id
889
+ );
890
+ flagged++;
891
+ }
892
+ return flagged;
893
+ }
894
+ function removeMemory(db, id) {
895
+ return tx(db, () => {
896
+ const info = db.prepare("DELETE FROM memories WHERE id = ?").run(id);
897
+ db.prepare("DELETE FROM memories_fts WHERE memory_id = ?").run(id);
898
+ return Number(info.changes) > 0;
899
+ });
900
+ }
901
+ var STOPWORDS = /* @__PURE__ */ new Set([
902
+ "the",
903
+ "a",
904
+ "an",
905
+ "is",
906
+ "are",
907
+ "was",
908
+ "were",
909
+ "be",
910
+ "been",
911
+ "to",
912
+ "of",
913
+ "in",
914
+ "on",
915
+ "and",
916
+ "or",
917
+ "for",
918
+ "with",
919
+ "this",
920
+ "that",
921
+ "it",
922
+ "as",
923
+ "at",
924
+ "by",
925
+ "we",
926
+ "you",
927
+ "do",
928
+ "does",
929
+ "how",
930
+ "why",
931
+ "what",
932
+ "when",
933
+ "from",
934
+ "our",
935
+ "its"
936
+ ]);
937
+ function splitIdentifier(word) {
938
+ return word.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(/\s+/);
939
+ }
940
+ function toFtsQuery(text) {
941
+ const tokens = /* @__PURE__ */ new Set();
942
+ for (const word of text.match(/[A-Za-z0-9]+/g) ?? []) {
943
+ for (const part of splitIdentifier(word)) {
944
+ const t = part.toLowerCase();
945
+ if (t.length < 2 || STOPWORDS.has(t)) continue;
946
+ tokens.add(t);
947
+ }
948
+ }
949
+ if (tokens.size === 0) return null;
950
+ return [...tokens].map((t) => t.length >= 3 ? `${t}*` : t).join(" OR ");
951
+ }
952
+ function queryMemories(db, collectionId, opts) {
953
+ const limit = opts.limit && opts.limit > 0 ? opts.limit : 10;
954
+ const filters = ["m.collection_id = ?"];
955
+ const params = [collectionId];
956
+ const addFilter = (clause, param) => {
957
+ filters.push(clause);
958
+ params.push(param);
959
+ };
960
+ if (!opts.includeTombstoned) filters.push("m.status = 'active'");
961
+ if (opts.type) addFilter("m.type = ?", opts.type);
962
+ if (opts.ticket) addFilter("m.ticket = ?", opts.ticket);
963
+ if (opts.minScore != null) addFilter("m.score >= ?", opts.minScore);
964
+ if (opts.file)
965
+ addFilter(
966
+ "EXISTS (SELECT 1 FROM json_each(m.files_json) WHERE value LIKE ?)",
967
+ `%${opts.file}%`
968
+ );
969
+ if (opts.since) addFilter("m.created_at >= ?", opts.since);
970
+ const fts = opts.text ? toFtsQuery(opts.text) : null;
971
+ if (fts) {
972
+ const pool = Math.max(limit * 3, 30);
973
+ const sql2 = `SELECT m.*, bm25(memories_fts, 0.0, 1.0, 2.0) AS bm25
974
+ FROM memories_fts f JOIN memories m ON m.id = f.memory_id
975
+ WHERE memories_fts MATCH ? AND ${filters.join(" AND ")}
976
+ ORDER BY bm25 ASC LIMIT ?`;
977
+ const rows = queryAll(db, sql2, fts, ...params, pool);
978
+ const now = Date.now();
979
+ return rows.map((r) => {
980
+ const ageDays = Math.max(0, (now - Date.parse(r.created_at)) / 864e5);
981
+ return { r, blended: (r.bm25 ?? 0) + 0.03 * ageDays };
982
+ }).sort((a, b) => a.blended - b.blended).slice(0, limit).map(({ r }) => ({ ...rowToRecord(r), bm25: r.bm25 }));
983
+ }
984
+ const sql = `SELECT m.* FROM memories m WHERE ${filters.join(" AND ")} ORDER BY m.created_at DESC, m.rowid DESC LIMIT ?`;
985
+ return queryAll(db, sql, ...params, limit).map(rowToRecord);
986
+ }
987
+ function exportMemories(db, collectionId, type) {
988
+ const rows = type ? queryAll(
989
+ db,
990
+ "SELECT * FROM memories WHERE collection_id = ? AND status='active' AND type = ? ORDER BY created_at ASC",
991
+ collectionId,
992
+ type
993
+ ) : queryAll(
994
+ db,
995
+ "SELECT * FROM memories WHERE collection_id = ? AND status='active' ORDER BY created_at ASC",
996
+ collectionId
997
+ );
998
+ return rows.map(rowToRecord);
999
+ }
1000
+ function importMemories(db, collectionId, records, root, type) {
1001
+ let added = 0;
1002
+ let skipped = 0;
1003
+ for (const r of records) {
1004
+ if (type && r.type !== type) continue;
1005
+ const hash = contentHash(r.content);
1006
+ const dupe = db.prepare("SELECT id FROM memories WHERE collection_id = ? AND content_hash = ?").get(collectionId, hash);
1007
+ if (dupe) {
1008
+ skipped++;
1009
+ continue;
1010
+ }
1011
+ rawInsert(db, collectionId, {
1012
+ content: r.content,
1013
+ title: r.title ?? derivePreview(r.content),
1014
+ type: r.type,
1015
+ ticket: r.ticket ?? null,
1016
+ files: r.files,
1017
+ createdAt: r.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1018
+ hash,
1019
+ fileHashes: hashFiles(root, r.files)
1020
+ });
1021
+ added++;
1022
+ }
1023
+ return { added, skipped };
1024
+ }
1025
+
1026
+ // src/commands/hook.ts
1027
+ function readHookInput() {
1028
+ return new Promise((resolve4) => {
1029
+ if (process.stdin.isTTY) return resolve4({});
1030
+ let data = "";
1031
+ process.stdin.setEncoding("utf-8");
1032
+ process.stdin.on("data", (c) => data += c);
1033
+ process.stdin.on("end", () => {
1034
+ try {
1035
+ resolve4(data.trim() ? JSON.parse(data) : {});
1036
+ } catch {
1037
+ resolve4({});
1038
+ }
1039
+ });
1040
+ process.stdin.on("error", () => resolve4({}));
1041
+ });
1042
+ }
1043
+ function withReadDb(fallback, fn) {
1044
+ try {
1045
+ if (!existsSync4(ragDbPath())) return fallback;
1046
+ const db = openDb();
1047
+ try {
1048
+ return fn(db);
1049
+ } finally {
1050
+ db.close();
1051
+ }
1052
+ } catch {
1053
+ return fallback;
1054
+ }
1055
+ }
1056
+ function bullet(m) {
1057
+ return `- ${m.score}\u2605 [${m.type}] ${m.ticket ? m.ticket + " " : ""}${m.title}`;
1058
+ }
1059
+ function memoryDigest() {
1060
+ return withReadDb("", (db) => {
1061
+ const col = lookupCollection(db);
1062
+ if (!col) return "";
1063
+ try {
1064
+ reconcileStale(db, findProjectRoot(), col.collection);
1065
+ } catch {
1066
+ }
1067
+ const recent = queryMemories(db, col.collection, { minScore: AUTO_INJECT_MIN, limit: 5 });
1068
+ if (recent.length === 0) return "";
1069
+ return `
1070
+
1071
+ ## Project memory (kodi)
1072
+
1073
+ Trusted findings (score \u2265 ${AUTO_INJECT_MIN}) for this project \u2014 query more with \`kodi memory query <text>\`, and \`kodi memory verify <id>\` when a finding is confirmed/refuted:
1074
+
1075
+ ${recent.map(bullet).join("\n")}
1076
+ `;
1077
+ });
1078
+ }
1079
+ function promptInjection(prompt, cwd) {
1080
+ if (!prompt.trim()) return "";
1081
+ return withReadDb("", (db) => {
1082
+ const col = lookupCollection(db, cwd);
1083
+ if (!col) return "";
1084
+ const hits = queryMemories(db, col.collection, {
1085
+ text: prompt,
1086
+ minScore: RELEVANCE_MIN,
1087
+ limit: 3
1088
+ });
1089
+ const lines = [];
1090
+ let used = 0;
1091
+ for (const m of hits) {
1092
+ const line = bullet(m);
1093
+ if (used + line.length > 1200) break;
1094
+ lines.push(line);
1095
+ used += line.length + 1;
1096
+ }
1097
+ if (lines.length === 0) return "";
1098
+ return `## Possibly relevant project memory (kodi)
1099
+
1100
+ Retrieved for your request \u2014 expand with \`kodi memory query "<topic>" --json\`:
1101
+
1102
+ ${lines.join("\n")}
1103
+ `;
1104
+ });
1105
+ }
1106
+ function parseVulnerabilities(command) {
1107
+ if (!/\bkodi\s+pr\s+create\b/.test(command)) return [];
1108
+ const out3 = [];
1109
+ const re = /--vulnerability(?:=|\s+)(?:"([^"]*)"|'([^']*)'|(\S+))/g;
1110
+ let m;
1111
+ while (m = re.exec(command)) {
1112
+ const v = (m[1] ?? m[2] ?? m[3] ?? "").trim();
1113
+ if (v) out3.push(v);
1114
+ }
1115
+ return out3;
1116
+ }
1117
+ function parseVulnFile(vuln) {
1118
+ const m = vuln.match(/([A-Za-z0-9_.\-/]+\/[A-Za-z0-9_.\-]+\.[A-Za-z0-9]+)/);
1119
+ return m ? m[1] : null;
1120
+ }
1121
+ function captureFromCommand(command, cwd, root) {
1122
+ try {
1123
+ const vulns = parseVulnerabilities(command);
1124
+ if (vulns.length === 0) return;
1125
+ const db = openDb();
1126
+ try {
1127
+ const col = resolveCollection(db, cwd);
1128
+ for (const v of vulns) {
1129
+ const file = parseVulnFile(v);
1130
+ if (!file) continue;
1131
+ insertMemory(
1132
+ db,
1133
+ col.collection,
1134
+ MemoryDraftSchema.parse({
1135
+ content: `Security finding: ${v}`,
1136
+ type: "gotcha",
1137
+ files: [file]
1138
+ }),
1139
+ root
1140
+ );
1141
+ }
1142
+ } finally {
1143
+ db.close();
1144
+ }
1145
+ } catch {
1146
+ }
1147
+ }
1148
+ function flagStaleForEdit(filePath, root) {
1149
+ try {
1150
+ if (!existsSync4(ragDbPath())) return;
1151
+ const rel = isAbsolute3(filePath) ? relative2(root, filePath) : filePath;
1152
+ const db = openDb();
1153
+ try {
1154
+ flagStaleForFile(db, root, rel);
1155
+ } finally {
1156
+ db.close();
1157
+ }
1158
+ } catch {
1159
+ }
1160
+ }
1161
+ function registerHookCommand(program2) {
1162
+ const hook = program2.command("hook").description("Emit Claude Code hook output (internal)");
1163
+ hook.command("session-start").description("Emit the orchestrator bootstrap as SessionStart additionalContext").action(() => {
1164
+ const payload = {
1165
+ hookSpecificOutput: {
1166
+ hookEventName: "SessionStart",
1167
+ additionalContext: ORCHESTRATOR_BOOTSTRAP + memoryDigest()
1168
+ }
1169
+ };
1170
+ process.stdout.write(JSON.stringify(payload));
1171
+ });
1172
+ hook.command("user-prompt-submit").description("Inject memories relevant to the submitted prompt (UserPromptSubmit)").action(async () => {
1173
+ const input = await readHookInput();
1174
+ const prompt = typeof input.prompt === "string" ? input.prompt : "";
1175
+ const cwd = typeof input.cwd === "string" ? input.cwd : process.cwd();
1176
+ const context = promptInjection(prompt, cwd);
1177
+ if (!context) return;
1178
+ const payload = {
1179
+ hookSpecificOutput: {
1180
+ hookEventName: "UserPromptSubmit",
1181
+ additionalContext: context
1182
+ }
1183
+ };
1184
+ process.stdout.write(JSON.stringify(payload));
1185
+ });
1186
+ hook.command("post-tool-use").description("Capture security findings + flag file-edited memories stale (PostToolUse)").action(async () => {
1187
+ const input = await readHookInput();
1188
+ const toolInput = input.tool_input ?? {};
1189
+ const cwd = typeof input.cwd === "string" ? input.cwd : process.cwd();
1190
+ const root = findProjectRoot(cwd);
1191
+ const command = typeof toolInput.command === "string" ? toolInput.command : "";
1192
+ if (command) captureFromCommand(command, cwd, root);
1193
+ const filePath = typeof toolInput.file_path === "string" ? toolInput.file_path : "";
1194
+ if (filePath) flagStaleForEdit(filePath, root);
1195
+ });
1196
+ return hook;
1197
+ }
1198
+
1199
+ // src/commands/init.ts
1200
+ import {
1201
+ copyFileSync as copyFileSync2,
1202
+ existsSync as existsSync5,
1203
+ mkdirSync as mkdirSync4,
1204
+ readdirSync as readdirSync2,
1205
+ readFileSync as readFileSync5,
1206
+ statSync as statSync2,
1207
+ writeFileSync as writeFileSync4
1208
+ } from "fs";
1209
+ import { basename as basename4, dirname as dirname5, join as join4, relative as relative3, resolve as resolve2 } from "path";
1210
+ import { fileURLToPath as fileURLToPath2 } from "url";
1211
+ import { stringify as stringifyYaml3 } from "yaml";
1212
+
446
1213
  // src/exec.ts
447
1214
  import { spawnSync } from "child_process";
448
1215
  function quote(args) {
@@ -486,11 +1253,11 @@ function inline(s) {
486
1253
  return escapeHtml(s).replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/`(.+?)`/g, "<code>$1</code>");
487
1254
  }
488
1255
  function mdToHtml(md) {
489
- const out2 = [];
1256
+ const out3 = [];
490
1257
  let list = null;
491
1258
  const closeList = () => {
492
1259
  if (list) {
493
- out2.push("</ul>");
1260
+ out3.push("</ul>");
494
1261
  list = null;
495
1262
  }
496
1263
  };
@@ -498,7 +1265,7 @@ function mdToHtml(md) {
498
1265
  const line = raw.trimEnd();
499
1266
  if (/^<!--/.test(line.trim())) {
500
1267
  closeList();
501
- out2.push(line);
1268
+ out3.push(line);
502
1269
  continue;
503
1270
  }
504
1271
  if (line.trim() === "") {
@@ -509,23 +1276,23 @@ function mdToHtml(md) {
509
1276
  if (heading) {
510
1277
  closeList();
511
1278
  const level = heading[1].length;
512
- out2.push(`<h${level}>${inline(heading[2])}</h${level}>`);
1279
+ out3.push(`<h${level}>${inline(heading[2])}</h${level}>`);
513
1280
  continue;
514
1281
  }
515
1282
  const item = /^[-*]\s+(.*)$/.exec(line);
516
1283
  if (item) {
517
1284
  if (!list) {
518
- out2.push("<ul>");
1285
+ out3.push("<ul>");
519
1286
  list = "ul";
520
1287
  }
521
- out2.push(`<li>${inline(item[1])}</li>`);
1288
+ out3.push(`<li>${inline(item[1])}</li>`);
522
1289
  continue;
523
1290
  }
524
1291
  closeList();
525
- out2.push(`<p>${inline(line)}</p>`);
1292
+ out3.push(`<p>${inline(line)}</p>`);
526
1293
  }
527
1294
  closeList();
528
- return out2.join("\n");
1295
+ return out3.join("\n");
529
1296
  }
530
1297
 
531
1298
  // src/providers/azure.ts
@@ -548,6 +1315,9 @@ function columnForStatus(status, cols) {
548
1315
  return cols.todo;
549
1316
  }
550
1317
  }
1318
+ function stateForColumn(column, columnStates) {
1319
+ return columnStates?.[column] ?? column;
1320
+ }
551
1321
  function statusFromColumn(column, cols) {
552
1322
  if (column === cols.todo) return "Pending";
553
1323
  if (column === (cols.inProgress ?? DEFAULT_COLUMNS.inProgress)) return "In progress";
@@ -572,7 +1342,7 @@ function parseWorkItem(fields, id, cols = DEFAULT_COLUMNS) {
572
1342
  }
573
1343
  const parsed = TicketSchema.safeParse(JSON.parse(json));
574
1344
  if (!parsed.success) return null;
575
- const column = fields["System.State"] ?? fields["System.BoardColumn"] ?? "";
1345
+ const column = fields["System.BoardColumn"] ?? fields["System.State"] ?? "";
576
1346
  const status = statusFromColumn(column, cols) ?? parsed.data.status;
577
1347
  return {
578
1348
  ...parsed.data,
@@ -581,7 +1351,7 @@ function parseWorkItem(fields, id, cols = DEFAULT_COLUMNS) {
581
1351
  status
582
1352
  };
583
1353
  }
584
- function createArgs(coords, title, html, column) {
1354
+ function createArgs(coords, title, html, state) {
585
1355
  const args = [
586
1356
  "az",
587
1357
  "boards",
@@ -592,7 +1362,7 @@ function createArgs(coords, title, html, column) {
592
1362
  "--type",
593
1363
  "Issue",
594
1364
  "--fields",
595
- `System.State=${column}`,
1365
+ `System.State=${state}`,
596
1366
  "--description",
597
1367
  html,
598
1368
  "--output",
@@ -606,10 +1376,37 @@ var AzureTicketProvider = class {
606
1376
  constructor(opts) {
607
1377
  this.opts = opts;
608
1378
  this.columns = opts.columns ?? DEFAULT_COLUMNS;
1379
+ this.columnStates = opts.columnStates;
609
1380
  }
610
1381
  opts;
611
1382
  name = "azure";
612
1383
  columns;
1384
+ columnStates;
1385
+ /**
1386
+ * The writable state for a logical status. The user maps each status to a BOARD
1387
+ * COLUMN in `kodi init`, but `System.BoardColumn` is a read-only computed field
1388
+ * (Azure rejects writes with TF401326), so the only thing kodi can set is
1389
+ * `System.State`. We therefore translate the chosen column → the state it maps
1390
+ * to (via `columnStates`) and set that. When several columns share a state, Azure
1391
+ * places the card in the board's default column for that state.
1392
+ */
1393
+ stateFor(status) {
1394
+ return stateForColumn(columnForStatus(status, this.columns), this.columnStates);
1395
+ }
1396
+ /** Set work-item `id`'s state (the only board-driving field Azure lets us write). */
1397
+ setStateArgs(id, state) {
1398
+ return [
1399
+ "az",
1400
+ "boards",
1401
+ "work-item",
1402
+ "update",
1403
+ "--id",
1404
+ id,
1405
+ "--fields",
1406
+ `System.State=${state}`,
1407
+ ...this.orgArgs()
1408
+ ];
1409
+ }
613
1410
  coords() {
614
1411
  return { organization: this.opts.organization, project: this.opts.project };
615
1412
  }
@@ -627,19 +1424,19 @@ var AzureTicketProvider = class {
627
1424
  return "(assigned by azure on create)";
628
1425
  }
629
1426
  async create(input) {
630
- const slug = input.slug ?? slugify(input.title);
631
- const draft = { ...input, key: "(pending)", slug };
1427
+ const slug2 = input.slug ?? slugify(input.title);
1428
+ const draft = { ...input, key: "(pending)", slug: slug2 };
632
1429
  const html = descriptionHtml(draft);
633
1430
  const res = execMutate(
634
- createArgs(this.coords(), input.title, html, columnForStatus(input.status, this.columns)),
1431
+ createArgs(this.coords(), input.title, html, this.stateFor(input.status)),
635
1432
  this.opts.dryRun
636
1433
  );
637
1434
  if (!res.ran) return { ...draft, key: "(dry-run)" };
638
- const id = JSON.parse(res.stdout).id;
639
- return { ...draft, key: String(id) };
1435
+ const id = String(JSON.parse(res.stdout).id);
1436
+ return { ...draft, key: id };
640
1437
  }
641
1438
  async get(key) {
642
- const out2 = execRead([
1439
+ const out3 = execRead([
643
1440
  "az",
644
1441
  "boards",
645
1442
  "work-item",
@@ -650,12 +1447,12 @@ var AzureTicketProvider = class {
650
1447
  "json",
651
1448
  ...this.orgArgs()
652
1449
  ]);
653
- const wi = JSON.parse(out2);
1450
+ const wi = JSON.parse(out3);
654
1451
  return parseWorkItem(wi.fields ?? {}, wi.id, this.columns);
655
1452
  }
656
1453
  async list() {
657
1454
  const wiql = "SELECT [System.Id] FROM WorkItems WHERE [System.WorkItemType] = 'Issue' ORDER BY [System.Id]";
658
- const out2 = execRead([
1455
+ const out3 = execRead([
659
1456
  "az",
660
1457
  "boards",
661
1458
  "query",
@@ -665,7 +1462,7 @@ var AzureTicketProvider = class {
665
1462
  "json",
666
1463
  ...this.scopeArgs()
667
1464
  ]);
668
- const rows = JSON.parse(out2);
1465
+ const rows = JSON.parse(out3);
669
1466
  const refs = [];
670
1467
  for (const row of rows) {
671
1468
  const id = row.id ?? row.fields?.["System.Id"];
@@ -691,20 +1488,7 @@ var AzureTicketProvider = class {
691
1488
  async setStatus(key, status) {
692
1489
  const current = await this.get(key);
693
1490
  if (!current) throw new Error(`work-item ${key} not found`);
694
- execMutate(
695
- [
696
- "az",
697
- "boards",
698
- "work-item",
699
- "update",
700
- "--id",
701
- key,
702
- "--fields",
703
- `System.State=${columnForStatus(status, this.columns)}`,
704
- ...this.orgArgs()
705
- ],
706
- this.opts.dryRun
707
- );
1491
+ execMutate(this.setStateArgs(key, this.stateFor(status)), this.opts.dryRun);
708
1492
  return { ...current, status };
709
1493
  }
710
1494
  async start(key, _p) {
@@ -753,8 +1537,8 @@ function parseProjects(json) {
753
1537
  return items.map((p) => p?.name).filter((n) => typeof n === "string");
754
1538
  }
755
1539
  function listProjects(org, run = defaultRunner) {
756
- const out2 = run(["az", "devops", "project", "list", "--org", org, "--output", "json"]);
757
- return parseProjects(out2);
1540
+ const out3 = run(["az", "devops", "project", "list", "--org", org, "--output", "json"]);
1541
+ return parseProjects(out3);
758
1542
  }
759
1543
  function parseProjectInfo(json) {
760
1544
  const d = JSON.parse(json);
@@ -783,34 +1567,74 @@ function getProjectInfo(org, project, run = defaultRunner) {
783
1567
  function processSupportsIssues(template) {
784
1568
  return template !== "Agile" && template !== "Scrum";
785
1569
  }
786
- function parseStates(json) {
1570
+ function parseTeams(json) {
787
1571
  const d = JSON.parse(json);
788
1572
  const items = Array.isArray(d) ? d : d.value ?? [];
789
- return items.map((s) => ({ name: s?.name, category: s?.category })).filter((s) => typeof s.name === "string");
1573
+ return items.map((t) => t?.name).filter((n) => typeof n === "string");
790
1574
  }
791
- function listIssueStates(org, project, run = defaultRunner) {
792
- const out2 = run([
793
- "az",
794
- "devops",
795
- "invoke",
796
- "--area",
797
- "wit",
798
- "--resource",
799
- "workItemTypeStates",
800
- "--route-parameters",
801
- `project=${project}`,
802
- "type=Issue",
803
- "--org",
804
- org,
805
- "--detect",
806
- "false",
807
- "--output",
808
- "json"
809
- ]);
810
- return parseStates(out2);
1575
+ function listTeams(org, project, run = defaultRunner) {
1576
+ return parseTeams(
1577
+ run(["az", "devops", "team", "list", "--org", org, "--project", project, "--output", "json"])
1578
+ );
811
1579
  }
812
- function statesInCategory(states, category) {
813
- return states.filter((s) => s.category === category).map((s) => s.name);
1580
+ function parseBoards(json) {
1581
+ const d = JSON.parse(json);
1582
+ const items = Array.isArray(d) ? d : d.value ?? [];
1583
+ return items.map((b) => b?.name).filter((n) => typeof n === "string");
1584
+ }
1585
+ function listBoards(org, project, team, run = defaultRunner) {
1586
+ return parseBoards(
1587
+ run([
1588
+ "az",
1589
+ "devops",
1590
+ "invoke",
1591
+ "--area",
1592
+ "work",
1593
+ "--resource",
1594
+ "boards",
1595
+ "--route-parameters",
1596
+ `project=${project}`,
1597
+ `team=${team}`,
1598
+ "--org",
1599
+ org,
1600
+ "--detect",
1601
+ "false",
1602
+ "--output",
1603
+ "json"
1604
+ ])
1605
+ );
1606
+ }
1607
+ function parseBoardColumns(json) {
1608
+ const d = JSON.parse(json);
1609
+ const items = Array.isArray(d) ? d : d.value ?? [];
1610
+ return items.map((c) => ({
1611
+ name: c?.name,
1612
+ columnType: c?.columnType ?? "",
1613
+ state: c?.stateMappings?.Issue ?? ""
1614
+ })).filter((c) => typeof c.name === "string");
1615
+ }
1616
+ function listBoardColumns(org, project, team, board, run = defaultRunner) {
1617
+ return parseBoardColumns(
1618
+ run([
1619
+ "az",
1620
+ "devops",
1621
+ "invoke",
1622
+ "--area",
1623
+ "work",
1624
+ "--resource",
1625
+ "columns",
1626
+ "--route-parameters",
1627
+ `project=${project}`,
1628
+ `team=${team}`,
1629
+ `board=${board}`,
1630
+ "--org",
1631
+ org,
1632
+ "--detect",
1633
+ "false",
1634
+ "--output",
1635
+ "json"
1636
+ ])
1637
+ );
814
1638
  }
815
1639
 
816
1640
  // src/providers/github-discovery.ts
@@ -821,7 +1645,7 @@ function parseProjects2(json) {
821
1645
  return items.map((p) => ({ number: p?.number, title: p?.title ?? "", id: p?.id ?? "" })).filter((p) => typeof p.number === "number");
822
1646
  }
823
1647
  function listProjects2(owner, run = defaultRunner2) {
824
- const out2 = run([
1648
+ const out3 = run([
825
1649
  "gh",
826
1650
  "project",
827
1651
  "list",
@@ -832,19 +1656,19 @@ function listProjects2(owner, run = defaultRunner2) {
832
1656
  "--limit",
833
1657
  "100"
834
1658
  ]);
835
- return parseProjects2(out2);
1659
+ return parseProjects2(out3);
836
1660
  }
837
1661
  function resolveViewerLogin(run = defaultRunner2) {
838
1662
  return run(["gh", "api", "user", "-q", ".login"]).trim();
839
1663
  }
840
1664
  function tokenScopes(run = defaultRunner2) {
841
- let out2;
1665
+ let out3;
842
1666
  try {
843
- out2 = run(["gh", "api", "-i", "user"]);
1667
+ out3 = run(["gh", "api", "-i", "user"]);
844
1668
  } catch {
845
1669
  return null;
846
1670
  }
847
- const m = /^x-oauth-scopes:\s*(.*)$/im.exec(out2);
1671
+ const m = /^x-oauth-scopes:\s*(.*)$/im.exec(out3);
848
1672
  if (!m) return null;
849
1673
  return m[1].split(",").map((s) => s.trim()).filter(Boolean);
850
1674
  }
@@ -862,8 +1686,8 @@ function parseRepos(json) {
862
1686
  return items.map((r) => r?.nameWithOwner).filter((n) => typeof n === "string");
863
1687
  }
864
1688
  function listRepos(owner, run = defaultRunner2) {
865
- const out2 = run(["gh", "repo", "list", owner, "--json", "nameWithOwner", "--limit", "200"]);
866
- return parseRepos(out2);
1689
+ const out3 = run(["gh", "repo", "list", owner, "--json", "nameWithOwner", "--limit", "200"]);
1690
+ return parseRepos(out3);
867
1691
  }
868
1692
  function parseStatusField(json) {
869
1693
  const data = JSON.parse(json);
@@ -878,7 +1702,7 @@ function parseStatusField(json) {
878
1702
  return { id: status.id, options };
879
1703
  }
880
1704
  function listStatusField(owner, number, run = defaultRunner2) {
881
- const out2 = run([
1705
+ const out3 = run([
882
1706
  "gh",
883
1707
  "project",
884
1708
  "field-list",
@@ -890,7 +1714,7 @@ function listStatusField(owner, number, run = defaultRunner2) {
890
1714
  "--limit",
891
1715
  "100"
892
1716
  ]);
893
- return parseStatusField(out2);
1717
+ return parseStatusField(out3);
894
1718
  }
895
1719
  function parseProjectId(json) {
896
1720
  return JSON.parse(json)?.id ?? "";
@@ -942,30 +1766,90 @@ function readlinePrompter() {
942
1766
  // src/commands/init.ts
943
1767
  var HOOK_COMMAND = "kodi hook session-start";
944
1768
  var SESSION_MATCHER = "startup|resume|clear|compact";
945
- function mergeSessionStartHook(settings) {
946
- settings.hooks ??= {};
947
- const arr = settings.hooks.SessionStart ??= [];
948
- const already = arr.some((e) => e.hooks?.some((h) => h.command === HOOK_COMMAND));
949
- if (already) return false;
950
- arr.push({ matcher: SESSION_MATCHER, hooks: [{ type: "command", command: HOOK_COMMAND }] });
1769
+ var UPS_COMMAND = "kodi hook user-prompt-submit";
1770
+ var PTU_COMMAND = "kodi hook post-tool-use";
1771
+ function addHookOnce(settings, event, command, matcher) {
1772
+ const hooks = settings.hooks ??= {};
1773
+ const arr = hooks[event] ??= [];
1774
+ if (arr.some((e) => e.hooks?.some((h) => h.command === command))) return false;
1775
+ const entry = { hooks: [{ type: "command", command }] };
1776
+ if (matcher) entry.matcher = matcher;
1777
+ arr.push(entry);
951
1778
  return true;
952
1779
  }
1780
+ function mergeSessionStartHook(settings) {
1781
+ return addHookOnce(settings, "SessionStart", HOOK_COMMAND, SESSION_MATCHER);
1782
+ }
1783
+ function mergeUserPromptSubmitHook(settings) {
1784
+ return addHookOnce(settings, "UserPromptSubmit", UPS_COMMAND);
1785
+ }
1786
+ function mergePostToolUseHook(settings) {
1787
+ return addHookOnce(settings, "PostToolUse", PTU_COMMAND, "Bash|Write|Edit");
1788
+ }
1789
+ var BOARD_CLI_COMMANDS = [
1790
+ "gh pr",
1791
+ // GitHub pull requests
1792
+ "gh issue",
1793
+ // GitHub issues (board tasks)
1794
+ "gh project",
1795
+ // GitHub Projects (boards)
1796
+ "az repos pr",
1797
+ // Azure DevOps pull requests
1798
+ "az boards"
1799
+ // Azure DevOps boards (work items / tasks)
1800
+ ];
1801
+ function denyDirectAndRtk(cmd) {
1802
+ return [`Bash(${cmd}:*)`, `Bash(rtk ${cmd}:*)`];
1803
+ }
1804
+ var PERMISSION_DENY = [
1805
+ ...BOARD_CLI_COMMANDS.flatMap(denyDirectAndRtk),
1806
+ ...denyDirectAndRtk("kodi init")
1807
+ // the one-time human setup command, direct and via rtk
1808
+ ];
1809
+ var PERMISSION_ALLOW = [
1810
+ "Read(.claude/agents/**)",
1811
+ // read the installed agents
1812
+ "Read(.claude/skills/**)",
1813
+ // read the installed skills
1814
+ // kodi is the sanctioned board proxy — allow all of it. `kodi init` stays in the
1815
+ // deny list, and deny rules override allow, so every kodi command runs without a
1816
+ // prompt EXCEPT init. The rtk-proxied form is allowed for the same reason.
1817
+ "Bash(kodi:*)",
1818
+ "Bash(rtk kodi:*)"
1819
+ ];
1820
+ function mergePermissions(settings) {
1821
+ const perms = settings.permissions ??= {};
1822
+ const deny = perms.deny ??= [];
1823
+ const allow = perms.allow ??= [];
1824
+ let changed = false;
1825
+ for (const rule of PERMISSION_DENY)
1826
+ if (!deny.includes(rule)) {
1827
+ deny.push(rule);
1828
+ changed = true;
1829
+ }
1830
+ for (const rule of PERMISSION_ALLOW)
1831
+ if (!allow.includes(rule)) {
1832
+ allow.push(rule);
1833
+ changed = true;
1834
+ }
1835
+ return changed;
1836
+ }
953
1837
  function defaultAssetsDir() {
954
1838
  return fileURLToPath2(new URL("../assets/", import.meta.url));
955
1839
  }
956
1840
  function copyTree2(srcRoot, destRoot, force, reportBase) {
957
1841
  const written = [];
958
- if (!existsSync4(srcRoot)) return written;
1842
+ if (!existsSync5(srcRoot)) return written;
959
1843
  const walk = (src, dest) => {
960
1844
  for (const entry of readdirSync2(src)) {
961
- const s = join3(src, entry);
962
- const d = join3(dest, entry);
1845
+ const s = join4(src, entry);
1846
+ const d = join4(dest, entry);
963
1847
  if (statSync2(s).isDirectory()) walk(s, d);
964
1848
  else {
965
- if (existsSync4(d) && !force) continue;
966
- mkdirSync3(dirname4(d), { recursive: true });
1849
+ if (existsSync5(d) && !force) continue;
1850
+ mkdirSync4(dirname5(d), { recursive: true });
967
1851
  copyFileSync2(s, d);
968
- written.push(join3(reportBase, relative2(destRoot, d)));
1852
+ written.push(join4(reportBase, relative3(destRoot, d)));
969
1853
  }
970
1854
  }
971
1855
  };
@@ -974,17 +1858,17 @@ function copyTree2(srcRoot, destRoot, force, reportBase) {
974
1858
  }
975
1859
  function copyMarkdownFlat(srcRoot, destDir, force, reportBase) {
976
1860
  const written = [];
977
- if (!existsSync4(srcRoot)) return written;
1861
+ if (!existsSync5(srcRoot)) return written;
978
1862
  const walk = (dir) => {
979
1863
  for (const entry of readdirSync2(dir)) {
980
- const s = join3(dir, entry);
1864
+ const s = join4(dir, entry);
981
1865
  if (statSync2(s).isDirectory()) walk(s);
982
1866
  else if (entry.endsWith(".md") && entry !== "README.md") {
983
- const d = join3(destDir, entry);
984
- if (existsSync4(d) && !force) continue;
985
- mkdirSync3(destDir, { recursive: true });
1867
+ const d = join4(destDir, entry);
1868
+ if (existsSync5(d) && !force) continue;
1869
+ mkdirSync4(destDir, { recursive: true });
986
1870
  copyFileSync2(s, d);
987
- written.push(join3(reportBase, entry));
1871
+ written.push(join4(reportBase, entry));
988
1872
  }
989
1873
  }
990
1874
  };
@@ -994,25 +1878,35 @@ function copyMarkdownFlat(srcRoot, destDir, force, reportBase) {
994
1878
  function installHarness(root, opts = {}) {
995
1879
  const force = opts.force ?? false;
996
1880
  const assetsDir = opts.assetsDir ?? defaultAssetsDir();
997
- const claude = join3(root, ".claude");
1881
+ const claude = join4(root, ".claude");
998
1882
  const changed = [];
999
- mkdirSync3(claude, { recursive: true });
1000
- const settingsPath = join3(claude, "settings.json");
1001
- const settings = existsSync4(settingsPath) ? JSON.parse(readFileSync4(settingsPath, "utf-8")) : {};
1002
- if (mergeSessionStartHook(settings)) {
1003
- writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1004
- changed.push(".claude/settings.json (SessionStart hook)");
1883
+ mkdirSync4(claude, { recursive: true });
1884
+ const settingsPath = join4(claude, "settings.json");
1885
+ const settings = existsSync5(settingsPath) ? JSON.parse(readFileSync5(settingsPath, "utf-8")) : {};
1886
+ const hookChanged = mergeSessionStartHook(settings);
1887
+ const upsChanged = mergeUserPromptSubmitHook(settings);
1888
+ const ptuChanged = mergePostToolUseHook(settings);
1889
+ const permsChanged = mergePermissions(settings);
1890
+ if (hookChanged || upsChanged || ptuChanged || permsChanged) {
1891
+ writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1892
+ const parts = [
1893
+ hookChanged ? "SessionStart hook" : null,
1894
+ upsChanged ? "UserPromptSubmit hook" : null,
1895
+ ptuChanged ? "PostToolUse hook" : null,
1896
+ permsChanged ? "permissions" : null
1897
+ ].filter(Boolean).join(" + ");
1898
+ changed.push(`.claude/settings.json (${parts})`);
1005
1899
  }
1006
1900
  changed.push(
1007
- ...copyTree2(join3(assetsDir, "skills"), join3(claude, "skills"), force, ".claude/skills"),
1008
- ...copyMarkdownFlat(join3(assetsDir, "agents"), join3(claude, "agents"), force, ".claude/agents"),
1009
- ...copyTree2(join3(assetsDir, "rules"), join3(claude, "rules"), force, ".claude/rules")
1901
+ ...copyTree2(join4(assetsDir, "skills"), join4(claude, "skills"), force, ".claude/skills"),
1902
+ ...copyMarkdownFlat(join4(assetsDir, "agents"), join4(claude, "agents"), force, ".claude/agents"),
1903
+ ...copyTree2(join4(assetsDir, "rules"), join4(claude, "rules"), force, ".claude/rules")
1010
1904
  );
1011
1905
  const provider = opts.provider ?? "local";
1012
1906
  const docsSubs = ["prd", "adr", "diagrams", "plan", "security"];
1013
1907
  if (provider === "local") docsSubs.push("tickets");
1014
1908
  for (const sub of docsSubs) {
1015
- mkdirSync3(join3(root, "docs", sub), { recursive: true });
1909
+ mkdirSync4(join4(root, "docs", sub), { recursive: true });
1016
1910
  }
1017
1911
  return changed;
1018
1912
  }
@@ -1056,54 +1950,65 @@ async function configureBoard(prompter, opts = {}) {
1056
1950
  `project "${project}" uses the ${info.processTemplate} process, which has no "Issue" work-item type. kodi creates tickets as Issues \u2014 use a Basic-process project (or add the Issue type), then re-run \`kodi init\`.`
1057
1951
  );
1058
1952
  }
1059
- let states = [];
1953
+ let team = opts.team;
1954
+ if (!team) {
1955
+ let teams = [];
1956
+ try {
1957
+ teams = listTeams(org, project, opts.runner);
1958
+ } catch {
1959
+ }
1960
+ team = teams.length === 1 ? teams[0] : teams.length > 1 ? await prompter.select("Select the team that owns the board", teams) : await prompter.input("Team that owns the board", `${project} Team`);
1961
+ }
1962
+ if (!team) throw new InitAbort("missing: team.");
1963
+ let board = opts.board;
1964
+ if (!board) {
1965
+ let boards = [];
1966
+ try {
1967
+ boards = listBoards(org, project, team, opts.runner);
1968
+ } catch {
1969
+ }
1970
+ const issuesBoard = boards.find((b) => b.toLowerCase() === "issues");
1971
+ board = boards.length === 1 ? boards[0] : issuesBoard ? issuesBoard : boards.length > 1 ? await prompter.select("Select the board", boards) : await prompter.input("Board name", "Issues");
1972
+ }
1973
+ if (!board) throw new InitAbort("missing: board.");
1974
+ let boardColumns = [];
1060
1975
  try {
1061
- states = listIssueStates(org, project, opts.runner);
1976
+ boardColumns = listBoardColumns(org, project, team, board, opts.runner);
1062
1977
  } catch {
1063
1978
  }
1064
- const proposed = statesInCategory(states, "Proposed");
1065
- const inProg = statesInCategory(states, "InProgress");
1066
- const completed = statesInCategory(states, "Completed");
1067
- const pick = async (flag, message, candidates, def) => {
1979
+ const columnNames = boardColumns.map((c) => c.name);
1980
+ const pick = async (flag, message, def) => {
1068
1981
  if (flag) return flag;
1069
- if (candidates.length === 1) return candidates[0];
1070
- if (candidates.length > 1) return prompter.select(message, candidates);
1982
+ if (columnNames.length > 0) return prompter.select(message, columnNames);
1071
1983
  return await prompter.input(message, def) || def;
1072
1984
  };
1073
- let todo;
1074
- if (opts.todoColumn) {
1075
- todo = opts.todoColumn;
1076
- } else if (states.length > 0 && proposed.length === 0) {
1077
- throw new InitAbort(
1078
- 'no To Do-type column: the Issue type on this board has no "Proposed" state where new issues can land. Add a To Do column/state to the board, then re-run `kodi init`.'
1079
- );
1080
- } else if (proposed.length > 0) {
1081
- todo = proposed.length === 1 ? proposed[0] : await prompter.select("To Do column (where new issues are created)", proposed);
1082
- } else {
1083
- todo = await prompter.input(
1985
+ const columns = {
1986
+ todo: await pick(
1987
+ opts.todoColumn,
1084
1988
  "To Do column (where new issues are created)",
1085
1989
  DEFAULT_COLUMNS.todo
1086
- );
1087
- if (!todo) throw new InitAbort("missing: the To Do column.");
1088
- }
1089
- const columns = {
1090
- todo,
1091
- inProgress: await pick(
1092
- opts.inProgressColumn,
1093
- "In Progress column",
1094
- inProg,
1095
- DEFAULT_COLUMNS.inProgress
1096
- ),
1097
- toReview: await pick(
1098
- opts.toReviewColumn,
1099
- "To Review column",
1100
- inProg,
1101
- DEFAULT_COLUMNS.toReview
1102
1990
  ),
1103
- done: await pick(opts.doneColumn, "Done column", completed, DEFAULT_COLUMNS.done)
1991
+ inProgress: await pick(opts.inProgressColumn, "In Progress column", DEFAULT_COLUMNS.inProgress),
1992
+ toReview: await pick(opts.toReviewColumn, "To Review column", DEFAULT_COLUMNS.toReview),
1993
+ done: await pick(opts.doneColumn, "Done column", DEFAULT_COLUMNS.done)
1104
1994
  };
1995
+ const stateByColumn = new Map(boardColumns.map((c) => [c.name, c.state]));
1996
+ const columnStates = {};
1997
+ for (const col of [columns.todo, columns.inProgress, columns.toReview, columns.done]) {
1998
+ columnStates[col] = stateByColumn.get(col) ?? col;
1999
+ }
1105
2000
  const repository = opts.repository ?? (await prompter.input("Repository name for pull requests", project) || project);
1106
- return { provider: "azure", prefix: "KODI", organization: org, project, repository, columns };
2001
+ return {
2002
+ provider: "azure",
2003
+ prefix: "KODI",
2004
+ organization: org,
2005
+ project,
2006
+ team,
2007
+ board,
2008
+ repository,
2009
+ columns,
2010
+ columnStates
2011
+ };
1107
2012
  }
1108
2013
  async function configureGithub(prompter, opts) {
1109
2014
  if (hasProjectWriteScope(opts.runner) === false) {
@@ -1203,12 +2108,12 @@ async function configureGithub(prompter, opts) {
1203
2108
  }
1204
2109
  function writeState(root, config) {
1205
2110
  const path = stateFilePath(root);
1206
- mkdirSync3(dirname4(path), { recursive: true });
1207
- writeFileSync3(path, stringifyYaml2(config), "utf-8");
2111
+ mkdirSync4(dirname5(path), { recursive: true });
2112
+ writeFileSync4(path, stringifyYaml3(config), "utf-8");
1208
2113
  return path;
1209
2114
  }
1210
2115
  function registerInitCommand(program2) {
1211
- program2.command("init").description("Install the kodi harness and configure the board (interactive)").option("-d, --dir <path>", "target project directory", process.cwd()).option("--force", "overwrite existing agents/skills", false).option("--provider <local|github|azure>", "skip the provider prompt").option("--prefix <prefix>", "local ticket key prefix (default KODI)").option("--org <url>", "azure org URL (non-interactive)").option("--project <name>", "azure project (non-interactive)").option("--owner-type <org|user>", "github board owner type (non-interactive)").option("--project-owner <login>", "github Projects owner login (org or user)").option("--project-number <n>", "github Projects board number", (v) => Number(v)).option("--todo-column <name>", "To Do column (non-interactive)").option("--in-progress-column <name>", "In Progress column").option("--to-review-column <name>", "To Review column").option("--done-column <name>", "Done column").option("--repository <name>", "repository for PRs (azure: name; github: owner/repo)").action(async (o) => {
2116
+ program2.command("init").description("Install the kodi harness and configure the board (interactive)").option("-d, --dir <path>", "target project directory", process.cwd()).option("--force", "overwrite existing agents/skills", false).option("--provider <local|github|azure>", "skip the provider prompt").option("--prefix <prefix>", "local ticket key prefix (default KODI)").option("--org <url>", "azure org URL (non-interactive)").option("--project <name>", "azure project (non-interactive)").option("--team <name>", "azure team that owns the board (non-interactive)").option("--board <name>", "azure board name (non-interactive, default Issues)").option("--owner-type <org|user>", "github board owner type (non-interactive)").option("--project-owner <login>", "github Projects owner login (org or user)").option("--project-number <n>", "github Projects board number", (v) => Number(v)).option("--todo-column <name>", "To Do column (non-interactive)").option("--in-progress-column <name>", "In Progress column").option("--to-review-column <name>", "To Review column").option("--done-column <name>", "Done column").option("--repository <name>", "repository for PRs (azure: name; github: owner/repo)").action(async (o) => {
1212
2117
  const root = String(o.dir);
1213
2118
  let provider = o.provider;
1214
2119
  if (!provider && !process.stdin.isTTY) provider = "local";
@@ -1220,6 +2125,8 @@ function registerInitCommand(program2) {
1220
2125
  prefix: o.prefix,
1221
2126
  org: o.org,
1222
2127
  project: o.project,
2128
+ team: o.team,
2129
+ board: o.board,
1223
2130
  ownerType: o.ownerType,
1224
2131
  projectOwner: o.projectOwner,
1225
2132
  projectNumber: o.projectNumber,
@@ -1242,12 +2149,19 @@ kodi init aborted \u2014 ${e.message}
1242
2149
  prompter.close();
1243
2150
  }
1244
2151
  const changed = installHarness(root, { force: o.force, provider: config.provider });
2152
+ try {
2153
+ const db = openDb();
2154
+ const displayName = config.project ?? (config.repository ? basename4(config.repository) : basename4(resolve2(root)));
2155
+ config.memory = provisionCollection(db, resolve2(root), displayName);
2156
+ db.close();
2157
+ } catch {
2158
+ }
1245
2159
  const statePath = writeState(root, config);
1246
2160
  process.stdout.write(
1247
2161
  `
1248
2162
  kodi init: installed
1249
2163
  ${changed.map((c) => ` + ${c}`).join("\n")}
1250
- + ${relative2(root, statePath)} (provider: ${config.provider})
2164
+ + ${relative3(root, statePath)} (provider: ${config.provider})
1251
2165
 
1252
2166
  SessionStart wired to \`${HOOK_COMMAND}\` (matchers: ${SESSION_MATCHER}).
1253
2167
  `
@@ -1255,36 +2169,233 @@ SessionStart wired to \`${HOOK_COMMAND}\` (matchers: ${SESSION_MATCHER}).
1255
2169
  });
1256
2170
  }
1257
2171
 
2172
+ // src/commands/memory.ts
2173
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
2174
+ import { parse as parseYaml4, stringify as stringifyYaml4 } from "yaml";
2175
+ function out(data, json, human) {
2176
+ if (json) process.stdout.write(JSON.stringify(data) + "\n");
2177
+ else process.stdout.write(human() + "\n");
2178
+ }
2179
+ function collect(value, previous) {
2180
+ return previous.concat([value]);
2181
+ }
2182
+ function asType(v) {
2183
+ if (v == null) return void 0;
2184
+ if (!MEMORY_TYPES.includes(v)) {
2185
+ throw new Error(`invalid --type "${v}". One of: ${MEMORY_TYPES.join(", ")}.`);
2186
+ }
2187
+ return v;
2188
+ }
2189
+ function renderLine(m) {
2190
+ const files = m.files.length ? ` {${fileNames(m.files).join(", ")}}` : "";
2191
+ const ticket = m.ticket ? ` ${m.ticket}` : "";
2192
+ const flag = m.needsReverify ? " \u26A0reverify" : "";
2193
+ return `${m.id} ${m.score}\u2605 [${m.type}]${ticket}${flag} ${m.title}${files}`;
2194
+ }
2195
+ function renderList(hits) {
2196
+ return hits.length ? hits.map(renderLine).join("\n") : "no memories";
2197
+ }
2198
+ function withDb(fn) {
2199
+ const db = openDb();
2200
+ try {
2201
+ return fn(db);
2202
+ } finally {
2203
+ db.close();
2204
+ }
2205
+ }
2206
+ function withCollection(fn) {
2207
+ return withDb((db) => fn(db, resolveCollection(db), findProjectRoot()));
2208
+ }
2209
+ function registerMemoryCommand(program2) {
2210
+ const memory = program2.command("memory").description("Cross-session knowledge store (lexical FTS) \u2014 store/query project findings");
2211
+ memory.command("store").description("Store a finding (deduped by content \u2014 idempotent)").option("-d, --draft <path>", "YAML draft file (validated against the memory template)").option("-c, --content <text>", "the finding to remember").option("-t, --type <type>", `one of: ${MEMORY_TYPES.join(", ")}`).option("--ticket <key>", "ticket/task in flight (optional)").option(
2212
+ "--file <path>",
2213
+ "repo-relative file the finding touches (repeatable, \u22651 required)",
2214
+ collect,
2215
+ []
2216
+ ).option("--title <text>", "optional display title (else derived from content)").option("--json", "machine-readable output", false).action((o) => {
2217
+ const draft = o.draft ? MemoryDraftSchema.parse(parseYaml4(readFileSync6(String(o.draft), "utf-8"))) : MemoryDraftSchema.parse({
2218
+ content: o.content,
2219
+ type: o.type,
2220
+ ticket: o.ticket,
2221
+ files: o.file ?? [],
2222
+ title: o.title
2223
+ });
2224
+ withCollection((db, col, root) => {
2225
+ const { record, deduped, blocked } = insertMemory(db, col.collection, draft, root);
2226
+ if (blocked) {
2227
+ out(
2228
+ { ...record, blocked: true },
2229
+ o.json,
2230
+ () => `Blocked (re-learn guard): ${record.id} was disproven \u2014 ${record.tombstoneReason ?? "refuted"}`
2231
+ );
2232
+ process.exitCode = 1;
2233
+ return;
2234
+ }
2235
+ out(
2236
+ { ...record, deduped },
2237
+ o.json,
2238
+ () => deduped ? `Already stored (dedup): ${record.id} \u2014 ${record.title}` : `Stored ${record.id} [${record.type}] (score ${record.score}) \u2014 ${record.title}`
2239
+ );
2240
+ });
2241
+ });
2242
+ memory.command("query [text...]").description("Search memories (BM25 text + filters), scoped to this project").option("-t, --type <type>", "filter by type").option("--ticket <key>", "filter by ticket").option("--file <path>", "filter by a referenced file path (substring)").option("--since <iso>", "only memories created on/after this ISO timestamp").option("-n, --limit <n>", "max results (default 10)", (v) => Number(v)).option("--json", "machine-readable output", false).action((text, o) => {
2243
+ withCollection((db, col) => {
2244
+ const hits = queryMemories(db, col.collection, {
2245
+ text: text?.length ? text.join(" ") : void 0,
2246
+ type: asType(o.type),
2247
+ ticket: o.ticket,
2248
+ file: o.file,
2249
+ since: o.since,
2250
+ limit: o.limit
2251
+ });
2252
+ out(hits, o.json, () => renderList(hits));
2253
+ });
2254
+ });
2255
+ memory.command("list").description("Browse recent memories in this project (newest first)").option("-t, --type <type>", "filter by type").option("--ticket <key>", "filter by ticket").option("-n, --limit <n>", "max results (default 10)", (v) => Number(v)).option("--json", "machine-readable output", false).action((o) => {
2256
+ withCollection((db, col) => {
2257
+ const hits = queryMemories(db, col.collection, {
2258
+ type: asType(o.type),
2259
+ ticket: o.ticket,
2260
+ limit: o.limit
2261
+ });
2262
+ out(hits, o.json, () => renderList(hits));
2263
+ });
2264
+ });
2265
+ memory.command("amend <id>").description("Edit a memory in place").option("-c, --content <text>").option("-t, --type <type>").option("--ticket <key>").option("--file <path>", "replace referenced files (repeatable)", collect).option("--title <text>").option("--json", "machine-readable output", false).action((id, o) => {
2266
+ withDb((db) => {
2267
+ const updated = amendMemory(
2268
+ db,
2269
+ id,
2270
+ {
2271
+ content: o.content,
2272
+ type: asType(o.type),
2273
+ ticket: o.ticket,
2274
+ files: o.file,
2275
+ title: o.title
2276
+ },
2277
+ findProjectRoot()
2278
+ );
2279
+ if (!updated) {
2280
+ out({ error: "not found", id }, o.json, () => `no memory ${id}`);
2281
+ process.exitCode = 1;
2282
+ return;
2283
+ }
2284
+ out(
2285
+ updated,
2286
+ o.json,
2287
+ () => `Amended ${updated.id} (score reset to ${updated.score}) \u2014 ${updated.title}`
2288
+ );
2289
+ });
2290
+ });
2291
+ memory.command("verify <id>").description("Record a veracity judgment: --pass raises the score, --fail tombstones").option("--pass", "the memory still holds against its files (score +1)", false).option("--fail", "the memory is disproven (tombstone + re-learn guard)", false).option("--reason <text>", "why it failed (stored on the tombstone)").option("--json", "machine-readable output", false).action((id, o) => {
2292
+ if (o.pass === o.fail) throw new Error("pass exactly one of --pass or --fail.");
2293
+ withDb((db) => {
2294
+ const updated = verifyMemory(db, id, !!o.pass, findProjectRoot(), o.reason);
2295
+ if (!updated) {
2296
+ out({ error: "not found", id }, o.json, () => `no memory ${id}`);
2297
+ process.exitCode = 1;
2298
+ return;
2299
+ }
2300
+ out(
2301
+ updated,
2302
+ o.json,
2303
+ () => updated.status === "tombstoned" ? `Tombstoned ${updated.id} \u2014 ${updated.tombstoneReason}` : `Verified ${updated.id} \u2014 score now ${updated.score}`
2304
+ );
2305
+ });
2306
+ });
2307
+ memory.command("rm <id>").description("Delete a memory").option("--json", "machine-readable output", false).action((id, o) => {
2308
+ withDb((db) => {
2309
+ const removed = removeMemory(db, id);
2310
+ out({ removed, id }, o.json, () => removed ? `Removed ${id}` : `no memory ${id}`);
2311
+ if (!removed) process.exitCode = 1;
2312
+ });
2313
+ });
2314
+ memory.command("export").description("Export this project\u2019s memories as YAML (all, or one --type)").option("-t, --type <type>", "export only this type").option("-f, --file <path>", "write to a file (default: stdout)").action((o) => {
2315
+ withCollection((db, col) => {
2316
+ const records = exportMemories(db, col.collection, asType(o.type));
2317
+ const doc = {
2318
+ collection: col.name,
2319
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
2320
+ memories: records.map((m) => ({
2321
+ content: m.content,
2322
+ type: m.type,
2323
+ ticket: m.ticket,
2324
+ files: m.files,
2325
+ title: m.title,
2326
+ createdAt: m.createdAt
2327
+ }))
2328
+ };
2329
+ const yaml = stringifyYaml4(doc);
2330
+ if (o.file) {
2331
+ writeFileSync5(String(o.file), yaml, "utf-8");
2332
+ process.stderr.write(`Exported ${records.length} memories -> ${o.file}
2333
+ `);
2334
+ } else {
2335
+ process.stdout.write(yaml);
2336
+ }
2337
+ });
2338
+ });
2339
+ memory.command("import <path>").description("Import memories from a YAML file into this project (deduped)").option("-t, --type <type>", "import only this type").option("--json", "machine-readable output", false).action((path, o) => {
2340
+ const doc = MemoryExportDocSchema.parse(parseYaml4(readFileSync6(path, "utf-8")));
2341
+ withCollection((db, col, root) => {
2342
+ const res = importMemories(db, col.collection, doc.memories, root, asType(o.type));
2343
+ out(res, o.json, () => `Imported: +${res.added} added, ${res.skipped} skipped`);
2344
+ });
2345
+ });
2346
+ return memory;
2347
+ }
2348
+
1258
2349
  // src/commands/pr.ts
1259
- import { mkdtempSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2350
+ import { mkdtempSync, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
1260
2351
  import { tmpdir } from "os";
1261
- import { join as join4 } from "path";
2352
+ import { join as join5 } from "path";
1262
2353
 
1263
2354
  // src/templates/pr.ts
1264
- import { z as z3 } from "zod";
1265
- var PrSchema = z3.object({
1266
- title: z3.string().min(3, "title must be at least 3 characters"),
1267
- summary: z3.string().min(1, "summary is required"),
1268
- includedChanges: z3.array(z3.string().min(1)).default([]),
1269
- features: z3.array(z3.string().min(1)).default([]),
1270
- fixes: z3.array(z3.string().min(1)).default([]),
1271
- improvements: z3.array(z3.string().min(1)).default([]),
1272
- relatedIssues: z3.array(z3.string().min(1)).default([]),
1273
- notes: z3.string().optional(),
1274
- reviewers: z3.array(z3.string().min(1)).default([])
2355
+ import { z as z4 } from "zod";
2356
+ var PrSchema = z4.object({
2357
+ title: z4.string().min(3, "title must be at least 3 characters"),
2358
+ summary: z4.string().min(1, "summary is required"),
2359
+ includedChanges: z4.array(z4.string().min(1)).default([]),
2360
+ features: z4.array(z4.string().min(1)).default([]),
2361
+ fixes: z4.array(z4.string().min(1)).default([]),
2362
+ improvements: z4.array(z4.string().min(1)).default([]),
2363
+ /**
2364
+ * Security vulnerabilities the slice surfaced, each referencing its report
2365
+ * (e.g. "CRITICAL — SQLi in login (docs/security/KODI-014-sqli-login.md)").
2366
+ * Rendered as a prominent section so reviewers see findings that still need
2367
+ * follow-up tickets. Empty when the slice produced no security reports.
2368
+ */
2369
+ vulnerabilities: z4.array(z4.string().min(1)).default([]),
2370
+ relatedIssues: z4.array(z4.string().min(1)).default([]),
2371
+ notes: z4.string().optional(),
2372
+ reviewers: z4.array(z4.string().min(1)).default([])
1275
2373
  });
2374
+ function neutralizeAutolinks(text) {
2375
+ return text.replace(/#(?=\d)/g, "#\u200B");
2376
+ }
2377
+ var MAX_PR_BODY = 4e3;
2378
+ function assertWithinBodyLimit(body) {
2379
+ if (body.length > MAX_PR_BODY) {
2380
+ throw new Error(
2381
+ `PR body is ${body.length} chars; the limit is ${MAX_PR_BODY}. Shorten the summary/notes or trim bullets, then retry.`
2382
+ );
2383
+ }
2384
+ }
1276
2385
  function bulletSection(title, items) {
1277
2386
  if (!items.length) return [];
1278
2387
  return ["", `## ${title}`, "", ...items.map((i) => `- ${i}`)];
1279
2388
  }
1280
2389
  function renderPrMarkdown(pr) {
1281
- const lines = ["## Summary", "", pr.summary];
1282
- lines.push(...bulletSection("Included changes", pr.includedChanges));
1283
- lines.push(...bulletSection("Features", pr.features));
1284
- lines.push(...bulletSection("Fixes", pr.fixes));
1285
- lines.push(...bulletSection("Improvements", pr.improvements));
2390
+ const s = neutralizeAutolinks;
2391
+ const lines = ["## Summary", "", s(pr.summary)];
2392
+ lines.push(...bulletSection("Security vulnerabilities", pr.vulnerabilities.map(s)));
2393
+ lines.push(...bulletSection("Included changes", pr.includedChanges.map(s)));
2394
+ lines.push(...bulletSection("Features", pr.features.map(s)));
2395
+ lines.push(...bulletSection("Fixes", pr.fixes.map(s)));
2396
+ lines.push(...bulletSection("Improvements", pr.improvements.map(s)));
1286
2397
  lines.push(...bulletSection("Related issues", pr.relatedIssues));
1287
- if (pr.notes) lines.push("", "## Notes", "", pr.notes);
2398
+ if (pr.notes) lines.push("", "## Notes", "", s(pr.notes));
1288
2399
  return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
1289
2400
  }
1290
2401
  function renderPrHtml(pr) {
@@ -1301,7 +2412,7 @@ function resolveTarget(explicit) {
1301
2412
  );
1302
2413
  }
1303
2414
  function draftFromOptions(o) {
1304
- if (o.file) return PrSchema.parse(JSON.parse(readFileSync5(String(o.file), "utf-8")));
2415
+ if (o.file) return PrSchema.parse(JSON.parse(readFileSync7(String(o.file), "utf-8")));
1305
2416
  return PrSchema.parse({
1306
2417
  title: o.title,
1307
2418
  summary: o.summary,
@@ -1309,6 +2420,7 @@ function draftFromOptions(o) {
1309
2420
  fixes: o.fix ?? [],
1310
2421
  improvements: o.improvement ?? [],
1311
2422
  includedChanges: o.change ?? [],
2423
+ vulnerabilities: o.vulnerability ?? [],
1312
2424
  relatedIssues: o.issue ?? [],
1313
2425
  notes: o.notes,
1314
2426
  reviewers: o.reviewer ?? []
@@ -1352,14 +2464,21 @@ function azureCreateArgs(pr, html, source, target, repo) {
1352
2464
  }
1353
2465
  function registerPrCommand(program2) {
1354
2466
  const pr = program2.command("pr").description("Manage pull requests (proxy gh/az) with a validated template");
1355
- pr.command("create").description("Create a PR from a validated template draft").option("-f, --file <path>", "JSON PR draft (validated against the template)").option("-t, --title <title>").option("-s, --summary <summary>").option("--feature <text>", "feature (repeatable)", collect, []).option("--fix <text>", "fix (repeatable)", collect, []).option("--improvement <text>", "improvement (repeatable)", collect, []).option("--change <text>", "included change (repeatable)", collect, []).option("--issue <ref>", "related issue (repeatable)", collect, []).option("--reviewer <name>", "reviewer (repeatable)", collect, []).option("--notes <text>").requiredOption("--source <branch>", "branch the PR is opened from").requiredOption("--target <branch>", "branch the PR merges into").option("--provider <github|azure>", "override the PR provider").option("--repository <repo>", "repository (gh: OWNER/REPO; az: name)").option("--yes", "execute (default: dry-run)", false).action((o) => {
2467
+ pr.command("create").description("Create a PR from a validated template draft").option("-f, --file <path>", "JSON PR draft (validated against the template)").option("-t, --title <title>").option("-s, --summary <summary>").option("--feature <text>", "feature (repeatable)", collect2, []).option("--fix <text>", "fix (repeatable)", collect2, []).option("--improvement <text>", "improvement (repeatable)", collect2, []).option("--change <text>", "included change (repeatable)", collect2, []).option(
2468
+ "--vulnerability <ref>",
2469
+ "security vulnerability the slice surfaced, referencing its report (repeatable)",
2470
+ collect2,
2471
+ []
2472
+ ).option("--issue <ref>", "related issue (repeatable)", collect2, []).option("--reviewer <name>", "reviewer (repeatable)", collect2, []).option("--notes <text>").requiredOption("--source <branch>", "branch the PR is opened from").requiredOption("--target <branch>", "branch the PR merges into").option("--provider <github|azure>", "override the PR provider").option("--repository <repo>", "repository (gh: OWNER/REPO; az: name)").option("--yes", "execute (default: dry-run)", false).action((o) => {
1356
2473
  const draft = draftFromOptions(o);
1357
2474
  const target = resolveTarget(o.provider);
1358
2475
  const repo = o.repository ?? loadBoardConfig().repository;
2476
+ const body = renderPrMarkdown(draft);
2477
+ assertWithinBodyLimit(body);
1359
2478
  let args;
1360
2479
  if (target === "github") {
1361
- const bodyFile = join4(mkdtempSync(join4(tmpdir(), "kodi-pr-")), "body.md");
1362
- writeFileSync4(bodyFile, renderPrMarkdown(draft), "utf-8");
2480
+ const bodyFile = join5(mkdtempSync(join5(tmpdir(), "kodi-pr-")), "body.md");
2481
+ writeFileSync6(bodyFile, body, "utf-8");
1363
2482
  args = githubCreateArgs(draft, bodyFile, o.source, o.target, repo);
1364
2483
  } else {
1365
2484
  args = azureCreateArgs(draft, renderPrHtml(draft), o.source, o.target, repo);
@@ -1381,27 +2500,27 @@ function registerPrCommand(program2) {
1381
2500
  });
1382
2501
  return pr;
1383
2502
  }
1384
- function collect(value, previous) {
2503
+ function collect2(value, previous) {
1385
2504
  return previous.concat([value]);
1386
2505
  }
1387
2506
 
1388
2507
  // src/commands/serve.ts
1389
2508
  import { spawn } from "child_process";
1390
- import { existsSync as existsSync5 } from "fs";
2509
+ import { existsSync as existsSync6 } from "fs";
1391
2510
  import net from "net";
1392
- import { dirname as dirname5, resolve as resolve2 } from "path";
2511
+ import { dirname as dirname6, resolve as resolve3 } from "path";
1393
2512
  import { fileURLToPath as fileURLToPath3 } from "url";
1394
2513
  var LISTEN_TIMEOUT_MS = 15e3;
1395
2514
  var PROBE_INTERVAL_MS = 200;
1396
2515
  var KILL_GRACE_MS = 2500;
1397
2516
  function resolveBoardServer() {
1398
- const moduleDir = dirname5(fileURLToPath3(import.meta.url));
2517
+ const moduleDir = dirname6(fileURLToPath3(import.meta.url));
1399
2518
  const candidates = [
1400
- resolve2(moduleDir, "..", "board-dist", "server.js"),
1401
- resolve2(moduleDir, "..", "..", "board-dist", "server.js")
2519
+ resolve3(moduleDir, "..", "board-dist", "server.js"),
2520
+ resolve3(moduleDir, "..", "..", "board-dist", "server.js")
1402
2521
  ];
1403
2522
  for (const candidate of candidates) {
1404
- if (existsSync5(candidate)) return candidate;
2523
+ if (existsSync6(candidate)) return candidate;
1405
2524
  }
1406
2525
  throw new Error("board not built \u2014 run `pnpm build` before `kodi tickets serve`");
1407
2526
  }
@@ -1496,7 +2615,7 @@ function registerServeCommand(program2) {
1496
2615
  throw new Error("internal: `tickets` command must be registered before `serve`");
1497
2616
  }
1498
2617
  tickets.command("serve").alias("open").description("Launch the local board UI in the browser (read-only view of docs/tickets)").option("--port <n>", "bind the board to a specific port (default: an OS-picked free port)").action(async (o) => {
1499
- const ticketsDir = resolve2(localPaths(process.cwd()).root);
2618
+ const ticketsDir = resolve3(localPaths(process.cwd()).root);
1500
2619
  let serverJs;
1501
2620
  try {
1502
2621
  serverJs = resolveBoardServer();
@@ -1580,22 +2699,22 @@ function registerServeCommand(program2) {
1580
2699
  }
1581
2700
 
1582
2701
  // src/commands/tickets.ts
1583
- import { readFileSync as readFileSync7 } from "fs";
2702
+ import { readFileSync as readFileSync9 } from "fs";
1584
2703
 
1585
2704
  // src/providers/local.ts
1586
2705
  import { randomBytes as randomBytes2 } from "crypto";
1587
2706
  import {
1588
- existsSync as existsSync6,
2707
+ existsSync as existsSync7,
1589
2708
  lstatSync,
1590
- mkdirSync as mkdirSync4,
2709
+ mkdirSync as mkdirSync5,
1591
2710
  readdirSync as readdirSync3,
1592
- readFileSync as readFileSync6,
2711
+ readFileSync as readFileSync8,
1593
2712
  renameSync as renameSync2,
1594
2713
  unlinkSync as unlinkSync2,
1595
- writeFileSync as writeFileSync5
2714
+ writeFileSync as writeFileSync7
1596
2715
  } from "fs";
1597
- import { basename as basename2, join as join5 } from "path";
1598
- import { parse as parseYaml4, stringify as stringifyYaml3 } from "yaml";
2716
+ import { basename as basename5, join as join6 } from "path";
2717
+ import { parse as parseYaml5, stringify as stringifyYaml5 } from "yaml";
1599
2718
  var FRONTMATTER = /^---\n([\s\S]*?)\n---\n?/;
1600
2719
  var LEGACY_FOLDERS = ["backlog", "done"];
1601
2720
  var LEGACY_FILENAME_RE = /^[A-Z][A-Z0-9]*-\d+-.*\.md$/;
@@ -1626,11 +2745,11 @@ var LocalTicketProvider = class {
1626
2745
  */
1627
2746
  scaffold() {
1628
2747
  this.detectLegacyData();
1629
- if (!existsSync6(this.paths.statusYaml)) {
2748
+ if (!existsSync7(this.paths.statusYaml)) {
1630
2749
  save(this.paths.statusYaml, emptyDocument());
1631
2750
  }
1632
2751
  for (const status of TICKET_STATUSES) {
1633
- mkdirSync4(this.paths.folderFor(status), { recursive: true });
2752
+ mkdirSync5(this.paths.folderFor(status), { recursive: true });
1634
2753
  }
1635
2754
  }
1636
2755
  /**
@@ -1648,7 +2767,7 @@ var LocalTicketProvider = class {
1648
2767
  * the first confirmed ticket-shaped file (SR-H).
1649
2768
  */
1650
2769
  detectLegacyData() {
1651
- if (existsSync6(this.paths.statusYaml)) return;
2770
+ if (existsSync7(this.paths.statusYaml)) return;
1652
2771
  const states = LEGACY_FOLDERS.map((name2) => ({ name: name2, state: this.legacyDirState(name2) }));
1653
2772
  const present = states.filter((s) => s.state !== "absent");
1654
2773
  if (present.length === 0) return;
@@ -1658,7 +2777,7 @@ var LocalTicketProvider = class {
1658
2777
  }
1659
2778
  for (const { name: name2, state } of states) {
1660
2779
  if (state !== "dir") continue;
1661
- const dir = join5(this.paths.root, name2);
2780
+ const dir = join6(this.paths.root, name2);
1662
2781
  let entries;
1663
2782
  try {
1664
2783
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -1668,7 +2787,7 @@ var LocalTicketProvider = class {
1668
2787
  for (const dirent of entries) {
1669
2788
  if (!dirent.isFile()) continue;
1670
2789
  if (!LEGACY_FILENAME_RE.test(dirent.name)) continue;
1671
- if (legacyFileHasKey(join5(dir, dirent.name))) {
2790
+ if (legacyFileHasKey(join6(dir, dirent.name))) {
1672
2791
  throw new LegacyDataError(folders, 1);
1673
2792
  }
1674
2793
  }
@@ -1681,7 +2800,7 @@ var LocalTicketProvider = class {
1681
2800
  * `unsafe` (SR-B fail-safe); a real directory is `dir`.
1682
2801
  */
1683
2802
  legacyDirState(name2) {
1684
- const dir = join5(this.paths.root, name2);
2803
+ const dir = join6(this.paths.root, name2);
1685
2804
  let st;
1686
2805
  try {
1687
2806
  st = lstatSync(dir);
@@ -1696,13 +2815,13 @@ var LocalTicketProvider = class {
1696
2815
  parseFile(path) {
1697
2816
  let raw;
1698
2817
  try {
1699
- raw = readFileSync6(path, "utf-8");
2818
+ raw = readFileSync8(path, "utf-8");
1700
2819
  } catch {
1701
2820
  return null;
1702
2821
  }
1703
2822
  const m = FRONTMATTER.exec(raw);
1704
2823
  if (!m) return null;
1705
- const meta = parseYaml4(m[1]) ?? {};
2824
+ const meta = parseYaml5(m[1]) ?? {};
1706
2825
  const parsed = TicketSchema.safeParse(meta);
1707
2826
  if (!parsed.success || !parsed.data.key || !parsed.data.slug) return null;
1708
2827
  return parsed.data;
@@ -1736,21 +2855,21 @@ var LocalTicketProvider = class {
1736
2855
  */
1737
2856
  writeTicketFile(ticket) {
1738
2857
  const dir = this.paths.folderFor(ticket.status);
1739
- mkdirSync4(dir, { recursive: true });
1740
- const finalName = basename2(composeFile(ticket.status, ticket.key, ticket.slug));
1741
- const finalPath = join5(dir, finalName);
2858
+ mkdirSync5(dir, { recursive: true });
2859
+ const finalName = basename5(composeFile(ticket.status, ticket.key, ticket.slug));
2860
+ const finalPath = join6(dir, finalName);
1742
2861
  const body = `---
1743
- ${stringifyYaml3(sortTicket(ticket)).trimEnd()}
2862
+ ${stringifyYaml5(sortTicket(ticket)).trimEnd()}
1744
2863
  ---
1745
2864
 
1746
2865
  ` + renderTicketMarkdown(ticket);
1747
- const tmp = join5(dir, `.${finalName}.${randomBytes2(6).toString("hex")}.tmp`);
2866
+ const tmp = join6(dir, `.${finalName}.${randomBytes2(6).toString("hex")}.tmp`);
1748
2867
  try {
1749
- writeFileSync5(tmp, body, { encoding: "utf-8", flag: "wx", mode: 384 });
2868
+ writeFileSync7(tmp, body, { encoding: "utf-8", flag: "wx", mode: 384 });
1750
2869
  renameSync2(tmp, finalPath);
1751
2870
  } catch (err) {
1752
2871
  try {
1753
- if (existsSync6(tmp)) unlinkSync2(tmp);
2872
+ if (existsSync7(tmp)) unlinkSync2(tmp);
1754
2873
  } catch {
1755
2874
  }
1756
2875
  throw err;
@@ -1788,11 +2907,11 @@ ${stringifyYaml3(sortTicket(ticket)).trimEnd()}
1788
2907
  if (load(this.paths.statusYaml).tickets[key]) {
1789
2908
  throw new Error(`ticket ${key} already exists`);
1790
2909
  }
1791
- const slug = input.slug ?? slugify(input.title);
1792
- const ticket = { ...input, key, slug };
2910
+ const slug2 = input.slug ?? slugify(input.title);
2911
+ const ticket = { ...input, key, slug: slug2 };
1793
2912
  this.writeTicketFile(ticket);
1794
2913
  const doc = load(this.paths.statusYaml);
1795
- upsert(doc, { key, column: ticket.status, slug });
2914
+ upsert(doc, { key, column: ticket.status, slug: slug2 });
1796
2915
  save(this.paths.statusYaml, doc);
1797
2916
  return ticket;
1798
2917
  }
@@ -1861,7 +2980,7 @@ ${stringifyYaml3(sortTicket(ticket)).trimEnd()}
1861
2980
  function legacyFileHasKey(path) {
1862
2981
  let raw;
1863
2982
  try {
1864
- raw = readFileSync6(path, "utf-8");
2983
+ raw = readFileSync8(path, "utf-8");
1865
2984
  } catch {
1866
2985
  return false;
1867
2986
  }
@@ -1869,7 +2988,7 @@ function legacyFileHasKey(path) {
1869
2988
  if (!m) return false;
1870
2989
  let meta;
1871
2990
  try {
1872
- meta = parseYaml4(m[1]);
2991
+ meta = parseYaml5(m[1]);
1873
2992
  } catch {
1874
2993
  return false;
1875
2994
  }
@@ -1903,9 +3022,9 @@ function sortTicket(t) {
1903
3022
  }
1904
3023
 
1905
3024
  // src/providers/github.ts
1906
- import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync6 } from "fs";
3025
+ import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync8 } from "fs";
1907
3026
  import { tmpdir as tmpdir2 } from "os";
1908
- import { join as join6 } from "path";
3027
+ import { join as join7 } from "path";
1909
3028
  var MARKER_RE2 = /<!--\s*kodi:ticket\s+(\{[\s\S]*?\})\s*-->/;
1910
3029
  var DEFAULT_COLUMNS2 = {
1911
3030
  todo: "Todo",
@@ -1947,19 +3066,19 @@ function parseMarker(body) {
1947
3066
  function parseItems(json) {
1948
3067
  const data = JSON.parse(json);
1949
3068
  const items = Array.isArray(data) ? data : data.items ?? [];
1950
- const out2 = [];
3069
+ const out3 = [];
1951
3070
  for (const it of items) {
1952
3071
  const content = it?.content ?? {};
1953
3072
  if (content.type !== "Issue" || typeof content.number !== "number" || typeof it?.id !== "string")
1954
3073
  continue;
1955
- out2.push({
3074
+ out3.push({
1956
3075
  itemId: it.id,
1957
3076
  issueNumber: content.number,
1958
3077
  statusName: typeof it?.status === "string" ? it.status : void 0,
1959
3078
  body: typeof content.body === "string" ? content.body : void 0
1960
3079
  });
1961
3080
  }
1962
- return out2;
3081
+ return out3;
1963
3082
  }
1964
3083
  function createIssueArgs(repo, title, bodyFile) {
1965
3084
  const args = ["gh", "issue", "create", "--title", title, "--body-file", bodyFile];
@@ -2015,7 +3134,7 @@ var GithubTicketProvider = class {
2015
3134
  /** Read (and cache) all board items in one call. */
2016
3135
  items() {
2017
3136
  if (this.itemsCache) return this.itemsCache;
2018
- const out2 = execRead([
3137
+ const out3 = execRead([
2019
3138
  "gh",
2020
3139
  "project",
2021
3140
  "item-list",
@@ -2027,7 +3146,7 @@ var GithubTicketProvider = class {
2027
3146
  "--limit",
2028
3147
  "500"
2029
3148
  ]);
2030
- return this.itemsCache = parseItems(out2);
3149
+ return this.itemsCache = parseItems(out3);
2031
3150
  }
2032
3151
  /** Body for an item — from item-list when present, else a per-issue fallback fetch. */
2033
3152
  bodyFor(item) {
@@ -2055,8 +3174,8 @@ var GithubTicketProvider = class {
2055
3174
  return "(assigned by github on create)";
2056
3175
  }
2057
3176
  async create(input) {
2058
- const slug = input.slug ?? slugify(input.title);
2059
- const draft = { ...input, key: "(pending)", slug };
3177
+ const slug2 = input.slug ?? slugify(input.title);
3178
+ const draft = { ...input, key: "(pending)", slug: slug2 };
2060
3179
  const bodyFile = writeTempBody(serializeBody(draft));
2061
3180
  const r1 = execMutate(createIssueArgs(this.opts.repo, input.title, bodyFile), this.opts.dryRun);
2062
3181
  if (!r1.ran) {
@@ -2147,8 +3266,8 @@ function toRef3(t) {
2147
3266
  };
2148
3267
  }
2149
3268
  function writeTempBody(body) {
2150
- const file = join6(mkdtempSync2(join6(tmpdir2(), "kodi-gh-")), "body.md");
2151
- writeFileSync6(file, body, "utf-8");
3269
+ const file = join7(mkdtempSync2(join7(tmpdir2(), "kodi-gh-")), "body.md");
3270
+ writeFileSync8(file, body, "utf-8");
2152
3271
  return file;
2153
3272
  }
2154
3273
 
@@ -2172,6 +3291,7 @@ function resolveProvider(cwd = process.cwd(), opts = {}) {
2172
3291
  organization: cfg.organization,
2173
3292
  project: cfg.project,
2174
3293
  columns: cfg.columns,
3294
+ columnStates: cfg.columnStates,
2175
3295
  dryRun: !opts.yes,
2176
3296
  cwd
2177
3297
  });
@@ -2181,13 +3301,13 @@ function resolveProvider(cwd = process.cwd(), opts = {}) {
2181
3301
  }
2182
3302
 
2183
3303
  // src/commands/tickets.ts
2184
- function out(data, json, human) {
3304
+ function out2(data, json, human) {
2185
3305
  if (json) process.stdout.write(JSON.stringify(data) + "\n");
2186
3306
  else process.stdout.write(human() + "\n");
2187
3307
  }
2188
3308
  function draftFromOptions2(o) {
2189
3309
  if (o.file) {
2190
- const raw = JSON.parse(readFileSync7(String(o.file), "utf-8"));
3310
+ const raw = JSON.parse(readFileSync9(String(o.file), "utf-8"));
2191
3311
  return TicketSchema.parse(raw);
2192
3312
  }
2193
3313
  return TicketSchema.parse({
@@ -2206,14 +3326,14 @@ function draftFromOptions2(o) {
2206
3326
  }
2207
3327
  function registerTicketsCommand(program2) {
2208
3328
  const tickets = program2.command("tickets").description("Manage tickets across the active provider (local / github / azure)");
2209
- tickets.command("create").description("Create a ticket (validates the template)").option("-f, --file <path>", "JSON draft file (validated against the ticket template)").option("-t, --title <title>").option("-s, --summary <summary>").option("--ac <criterion>", "acceptance criterion (repeatable)", collect2, []).option("--non-goal <text>", "non-goal (repeatable)", collect2, []).option("--dep <key>", "dependency ticket key (repeatable)", collect2, []).option("--prd <ref>", "PRD driver").option("--adr <ref>", "ADR driver (repeatable)", collect2, []).option("--security <ref>", "security driver").option("--notes <text>").option("--yes", "execute remote mutations (default: dry-run)", false).option("--json", "machine-readable output", false).action(async (o) => {
3329
+ tickets.command("create").description("Create a ticket (validates the template)").option("-f, --file <path>", "JSON draft file (validated against the ticket template)").option("-t, --title <title>").option("-s, --summary <summary>").option("--ac <criterion>", "acceptance criterion (repeatable)", collect3, []).option("--non-goal <text>", "non-goal (repeatable)", collect3, []).option("--dep <key>", "dependency ticket key (repeatable)", collect3, []).option("--prd <ref>", "PRD driver").option("--adr <ref>", "ADR driver (repeatable)", collect3, []).option("--security <ref>", "security driver").option("--notes <text>").option("--yes", "execute remote mutations (default: dry-run)", false).option("--json", "machine-readable output", false).action(async (o) => {
2210
3330
  const draft = draftFromOptions2(o);
2211
3331
  try {
2212
3332
  const created = await resolveProvider(process.cwd(), { yes: o.yes }).create(draft);
2213
- out(created, o.json, () => `Created ${created.key} (${created.status}) \u2014 ${created.title}`);
3333
+ out2(created, o.json, () => `Created ${created.key} (${created.status}) \u2014 ${created.title}`);
2214
3334
  } catch (err) {
2215
3335
  if (err instanceof LegacyDataError) {
2216
- out(err.report, o.json, () => renderLegacyRefusal(err.report));
3336
+ out2(err.report, o.json, () => renderLegacyRefusal(err.report));
2217
3337
  process.exitCode = 1;
2218
3338
  return;
2219
3339
  }
@@ -2222,7 +3342,7 @@ function registerTicketsCommand(program2) {
2222
3342
  });
2223
3343
  tickets.command("list").description("List all tickets").option("--json", "machine-readable output", false).action(async (o) => {
2224
3344
  const refs = await resolveProvider().list();
2225
- out(
3345
+ out2(
2226
3346
  refs,
2227
3347
  o.json,
2228
3348
  () => refs.length ? refs.map((t) => `${t.key} ${t.status.padEnd(12)} ${t.title}`).join("\n") : "No tickets."
@@ -2230,7 +3350,7 @@ function registerTicketsCommand(program2) {
2230
3350
  });
2231
3351
  tickets.command("list-ready").description("List tickets ready to start (no unmet dependency) + the blocked set").option("--json", "machine-readable output", false).action(async (o) => {
2232
3352
  const res = await resolveProvider().listReady();
2233
- out(res, o.json, () => {
3353
+ out2(res, o.json, () => {
2234
3354
  const ready = res.ready.map((t) => ` ${t.key} ${t.title}`).join("\n") || " (none)";
2235
3355
  const blocked = res.blocked.map((b) => ` ${b.ticket.key} ${b.ticket.title} \u2190 ${b.blockedBy.join(", ")}`).join("\n") || " (none)";
2236
3356
  return `Ready:
@@ -2248,7 +3368,7 @@ ${blocked}`;
2248
3368
  process.exitCode = 1;
2249
3369
  return;
2250
3370
  }
2251
- out(t, o.json, () => JSON.stringify(t, null, 2));
3371
+ out2(t, o.json, () => JSON.stringify(t, null, 2));
2252
3372
  });
2253
3373
  tickets.command("set-status <key> <status>").description(`Transition a ticket (${TICKET_STATUSES.join(" | ")})`).option("--yes", "execute remote mutations (default: dry-run)", false).option("--json", "machine-readable output", false).action(async (key, status, o) => {
2254
3374
  if (!TICKET_STATUSES.includes(status)) {
@@ -2261,32 +3381,32 @@ ${blocked}`;
2261
3381
  key,
2262
3382
  status
2263
3383
  );
2264
- out(t, o.json, () => `${t.key} \u2192 ${t.status}`);
3384
+ out2(t, o.json, () => `${t.key} \u2192 ${t.status}`);
2265
3385
  });
2266
3386
  tickets.command("start <key>").description("Mark a ticket started (In progress)").option("--branch <name>").option("--yes", "execute remote mutations (default: dry-run)", false).option("--json", "machine-readable output", false).action(async (key, o) => {
2267
3387
  const t = await resolveProvider(process.cwd(), { yes: o.yes }).start(key, {
2268
3388
  branch: o.branch
2269
3389
  });
2270
- out(t, o.json, () => `${t.key} \u2192 ${t.status}`);
3390
+ out2(t, o.json, () => `${t.key} \u2192 ${t.status}`);
2271
3391
  });
2272
3392
  tickets.command("delete <key>").description("Delete a ticket").option("--yes", "execute remote mutations (default: dry-run)", false).option("--json", "machine-readable output", false).action(async (key, o) => {
2273
3393
  await resolveProvider(process.cwd(), { yes: o.yes }).delete(key);
2274
- out({ deleted: key }, o.json, () => `Deleted ${key}`);
3394
+ out2({ deleted: key }, o.json, () => `Deleted ${key}`);
2275
3395
  });
2276
3396
  tickets.command("next-id").description("Compute the next ticket key").option("--json", "machine-readable output", false).action(async (o) => {
2277
3397
  const id = await resolveProvider().nextId();
2278
- out({ nextId: id }, o.json, () => id);
3398
+ out2({ nextId: id }, o.json, () => id);
2279
3399
  });
2280
- tickets.command("amend <key>").description("Edit a ticket (from --file patch or flags)").option("-f, --file <path>", "JSON patch (validated against the template)").option("-s, --summary <text>").option("--notes <text>").option("--ac <criterion>", "replace acceptance criteria (repeatable)", collect2, []).option("--dep <key>", "replace dependencies (repeatable)", collect2, []).option("--pr <ref>", "link a PR").option("--yes", "execute remote mutations (default: dry-run)", false).option("--json", "machine-readable output", false).action(async (key, o) => {
2281
- const patch = o.file ? TicketSchema.partial().parse(JSON.parse(readFileSync7(String(o.file), "utf-8"))) : buildPatch(o);
3400
+ tickets.command("amend <key>").description("Edit a ticket (from --file patch or flags)").option("-f, --file <path>", "JSON patch (validated against the template)").option("-s, --summary <text>").option("--notes <text>").option("--ac <criterion>", "replace acceptance criteria (repeatable)", collect3, []).option("--dep <key>", "replace dependencies (repeatable)", collect3, []).option("--pr <ref>", "link a PR").option("--yes", "execute remote mutations (default: dry-run)", false).option("--json", "machine-readable output", false).action(async (key, o) => {
3401
+ const patch = o.file ? TicketSchema.partial().parse(JSON.parse(readFileSync9(String(o.file), "utf-8"))) : buildPatch(o);
2282
3402
  const t = await resolveProvider(process.cwd(), { yes: o.yes }).amend(key, patch);
2283
- out(t, o.json, () => `Amended ${t.key}`);
3403
+ out2(t, o.json, () => `Amended ${t.key}`);
2284
3404
  });
2285
3405
  tickets.command("link-pr <key> <pr>").description("Bind a pull request to a ticket").option("--yes", "execute remote mutations (default: dry-run)", false).option("--json", "machine-readable output", false).action(async (key, pr, o) => {
2286
3406
  const t = await resolveProvider(process.cwd(), { yes: o.yes }).amend(key, { prUrl: pr });
2287
- out(t, o.json, () => `${t.key} \u2194 ${pr}`);
3407
+ out2(t, o.json, () => `${t.key} \u2194 ${pr}`);
2288
3408
  });
2289
- tickets.command("deps <key>").description("Read a ticket's dependencies, or declare new ones with --add").option("--add <key>", "declare a dependency (repeatable)", collect2, []).option("--yes", "execute remote mutations (default: dry-run)", false).option("--json", "machine-readable output", false).action(async (key, o) => {
3409
+ tickets.command("deps <key>").description("Read a ticket's dependencies, or declare new ones with --add").option("--add <key>", "declare a dependency (repeatable)", collect3, []).option("--yes", "execute remote mutations (default: dry-run)", false).option("--json", "machine-readable output", false).action(async (key, o) => {
2290
3410
  const provider = resolveProvider(process.cwd(), { yes: o.yes });
2291
3411
  const current = await provider.get(key);
2292
3412
  if (!current) {
@@ -2298,9 +3418,9 @@ ${blocked}`;
2298
3418
  if (o.add.length) {
2299
3419
  const deps = Array.from(/* @__PURE__ */ new Set([...current.dependencies, ...o.add]));
2300
3420
  const t = await provider.amend(key, { dependencies: deps });
2301
- out(t.dependencies, o.json, () => `${key} deps: ${t.dependencies.join(", ") || "(none)"}`);
3421
+ out2(t.dependencies, o.json, () => `${key} deps: ${t.dependencies.join(", ") || "(none)"}`);
2302
3422
  } else {
2303
- out(
3423
+ out2(
2304
3424
  current.dependencies,
2305
3425
  o.json,
2306
3426
  () => `${key} deps: ${current.dependencies.join(", ") || "(none)"}`
@@ -2311,11 +3431,11 @@ ${blocked}`;
2311
3431
  const provider = resolveProvider(process.cwd(), { yes: o.yes });
2312
3432
  if (o.pr) await provider.amend(key, { prUrl: o.pr });
2313
3433
  const t = await provider.setStatus(key, "To review");
2314
- out(t, o.json, () => `${t.key} \u2192 ${t.status}${o.pr ? ` (PR ${o.pr})` : ""}`);
3434
+ out2(t, o.json, () => `${t.key} \u2192 ${t.status}${o.pr ? ` (PR ${o.pr})` : ""}`);
2315
3435
  });
2316
3436
  return tickets;
2317
3437
  }
2318
- function collect2(value, previous) {
3438
+ function collect3(value, previous) {
2319
3439
  return previous.concat([value]);
2320
3440
  }
2321
3441
  function renderLegacyRefusal(report) {
@@ -2337,7 +3457,7 @@ function buildPatch(o) {
2337
3457
 
2338
3458
  // package.json
2339
3459
  var name = "kodi-dev";
2340
- var version = "1.0.1";
3460
+ var version = "1.2.0";
2341
3461
  var description = "kodi.dev \u2014 Claude Code-native agent orchestrator CLI";
2342
3462
 
2343
3463
  // src/index.ts
@@ -2349,6 +3469,7 @@ registerPrCommand(program);
2349
3469
  registerHookCommand(program);
2350
3470
  registerInitCommand(program);
2351
3471
  registerAddCommand(program);
3472
+ registerMemoryCommand(program);
2352
3473
  program.parseAsync(process.argv).catch((err) => {
2353
3474
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}
2354
3475
  `);