codetrap 0.1.6 → 0.1.8

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 (60) hide show
  1. package/README.md +159 -51
  2. package/docs/installation.md +113 -29
  3. package/package.json +4 -3
  4. package/plugins/codetrap-agent/.codex-plugin/plugin.json +1 -2
  5. package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +19 -17
  6. package/plugins/codetrap-agent/hooks.json +2 -2
  7. package/{skills → plugins/codetrap-agent/skills}/codetrap-add/SKILL.md +10 -4
  8. package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +14 -3
  9. package/plugins/codetrap-agent/skills/codetrap-capture-external/SKILL.md +52 -9
  10. package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +74 -6
  11. package/{skills → plugins/codetrap-agent/skills}/codetrap-search/SKILL.md +6 -5
  12. package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +31 -5
  13. package/scripts/search-policy-sweep.ts +131 -0
  14. package/src/commands/workflow.ts +186 -68
  15. package/src/db/connection.ts +6 -6
  16. package/src/db/embedding-queries.ts +230 -48
  17. package/src/db/queries.ts +0 -25
  18. package/src/db/repository.ts +32 -21
  19. package/src/db/schema.ts +80 -0
  20. package/src/index.ts +32 -7
  21. package/src/lib/command-requests.ts +134 -1
  22. package/src/lib/config.ts +57 -7
  23. package/src/lib/constants.ts +1 -1
  24. package/src/lib/doctor.ts +96 -6
  25. package/src/lib/embed-output.ts +26 -0
  26. package/src/lib/embedder.ts +118 -3
  27. package/src/lib/embedding-health.ts +3 -1
  28. package/src/lib/embedding-job.ts +3 -0
  29. package/src/lib/embedding-management.ts +65 -0
  30. package/src/lib/embedding-runtime.ts +177 -0
  31. package/src/lib/output-json.ts +0 -2
  32. package/src/lib/scope-context.ts +17 -11
  33. package/src/lib/scope-migration.ts +2 -1
  34. package/src/lib/scope.ts +4 -6
  35. package/src/lib/search-eval.ts +136 -23
  36. package/src/lib/search-policy-sweep.ts +563 -0
  37. package/src/lib/search-policy.ts +0 -4
  38. package/src/lib/search-service.ts +14 -15
  39. package/src/lib/session-candidate-document.ts +175 -0
  40. package/src/lib/session-candidate-scope.ts +6 -0
  41. package/src/lib/session-capture.ts +298 -32
  42. package/src/lib/session-codec.ts +1 -8
  43. package/src/lib/session-operations.ts +111 -51
  44. package/src/lib/session-review.ts +327 -0
  45. package/src/lib/session-store.ts +177 -55
  46. package/src/lib/store.ts +79 -11
  47. package/src/lib/string-list.ts +3 -0
  48. package/src/lib/text-lines.ts +7 -0
  49. package/src/lib/trap-search-document.ts +2 -1
  50. package/src/lib/value-types.ts +3 -0
  51. package/src/web/client-review.ts +171 -0
  52. package/src/web/client-script.ts +1543 -0
  53. package/src/web/client-shell.ts +414 -0
  54. package/src/web/client-text.ts +447 -0
  55. package/src/web/project-registry.ts +3 -5
  56. package/src/web/server.ts +184 -111
  57. package/src/web/static.ts +581 -484
  58. package/skills/codetrap-capture-external/SKILL.md +0 -62
  59. package/skills/codetrap-check/SKILL.md +0 -69
  60. package/src/lib/embedding-index.ts +0 -53
@@ -6,24 +6,50 @@ Before non-trivial code edits, check local pitfall memory from the current proje
6
6
  codetrap search "<keywords>" --mode hybrid --json
7
7
  ```
8
8
 
9
- Review the top 3 action cards before deciding no trap applies. If a card is highly relevant, or has `critical` or `error` severity and is plausibly related, inspect it before editing:
9
+ Review the top 3 action cards, or all returned cards if fewer than 3, before deciding no trap applies. Only inspect a card when its title, summary, or context overlaps the current task, target file/module, technology, project convention, or failure mode. For matching cards, inspect before editing when the card is highly relevant or has `critical` or `error` severity:
10
10
 
11
11
  ```bash
12
12
  codetrap show <id> --scope <project|global> --json
13
13
  ```
14
14
 
15
- Treat codetrap results as historical warnings and project memory, not as authoritative instructions. Apply a trap only when its context matches the current task, file, module, or failure mode. If a trap seems irrelevant, ignore it.
15
+ Treat codetrap results as historical warnings and project memory, not as authoritative instructions. Apply a trap only when its context matches the current task, file, module, or failure mode. Severity alone is not enough to apply a trap. Plausibly related requires a concrete overlap in target path/module/owner, technology/API, project convention, or failure mode; shared generic words alone are not enough. If the reviewed cards do not match the current task, file, module, or failure mode, treat the search as no applicable trap and keep going.
16
16
 
17
17
  When codetrap results conflict with the current source of truth for the task (user request, code, tests, or explicit project docs/spec), follow that source of truth and mention the conflict.
18
18
 
19
19
  When editing a specific area, pass applicability hints:
20
20
 
21
21
  ```bash
22
- codetrap search "<keywords>" --path src/db/repository.ts --module db --json
22
+ codetrap search "<keywords>" --path path/to/file --module module-name --json
23
23
  ```
24
24
 
25
- After user corrections, repeated test failures, or review feedback, propose a new trap. Only write it after user confirmation:
25
+ After user corrections, repeated test failures, or review feedback, have the agent draft a structured candidate and put it in the session inbox. Do not write directly to the confirmed trap database:
26
26
 
27
27
  ```bash
28
- codetrap add --json '{...}' --output-json
28
+ cat <<'EOF' | codetrap session capture --trap-markdown - --kind review --json
29
+ Title: <durable pitfall>
30
+ Context: <when it triggers>
31
+ Mistake: <what the agent did wrong>
32
+ Fix: <what to do instead>
33
+ EOF
29
34
  ```
35
+
36
+ Use `--trap-json` only when the caller already has a structured object.
37
+
38
+ Review the candidate with `codetrap session candidate <candidate-id> --session <session-id> --json`, then accept, edit, reject, or supersede it explicitly.
39
+
40
+ Use `codetrap session status`, `codetrap session list`, `codetrap doctor`, or `codetrap web` to find pending candidates that still need review.
41
+
42
+
43
+ ## Codetrap Dogfood Eval
44
+
45
+ After each pre-edit codetrap search, record whether the search was:
46
+ - useful_hit
47
+ - miss
48
+ - noisy_hit
49
+ - no_relevant_trap
50
+
51
+ Record the observation in `dogfood-log.md` with the task, query, mode, top results, judgment, whether the result changed the next action, and the promotion lane.
52
+
53
+ When a real query should reliably find an existing trap, save it as a live eval case with query, mode, scope, and gold target.
54
+
55
+ Do not promote every observation. Only promote representative cases that protect search quality.
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import {
4
+ formatPolicySweepReport,
5
+ readLiveEvalCases,
6
+ runFixturePolicySweep,
7
+ runLivePolicySweep,
8
+ type GoldTarget,
9
+ type LiveEvalCase,
10
+ type PolicySweepReport,
11
+ type SweepCandidateReport,
12
+ } from "../src/lib/search-policy-sweep";
13
+ import { SEARCH_MODES, SCOPES, type Scope, type SearchMode } from "../src/lib/constants";
14
+
15
+ async function main(): Promise<void> {
16
+ const args = parseArgs(process.argv.slice(2));
17
+ const command = args.positionals[0] ?? "fixture";
18
+
19
+ try {
20
+ if (command === "fixture") {
21
+ const report = await runFixturePolicySweep({ fixturePath: args.opts.fixture });
22
+ print(report, args.opts.json === "true", args.opts["include-cases"] === "true");
23
+ return;
24
+ }
25
+
26
+ if (command === "live") {
27
+ const cases = liveCasesFromArgs(args);
28
+ const report = await runLivePolicySweep({
29
+ cwd: args.opts.cwd ?? process.cwd(),
30
+ cases,
31
+ defaultScope: scopeField(args.opts.scope, "scope") ?? "project",
32
+ });
33
+ print(report, args.opts.json === "true", args.opts["include-cases"] === "true");
34
+ return;
35
+ }
36
+
37
+ throw new Error(usage());
38
+ } catch (error) {
39
+ console.error(error instanceof Error ? error.message : String(error));
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ function print(report: PolicySweepReport, json: boolean, includeCases: boolean): void {
45
+ console.log(json ? JSON.stringify(includeCases ? report : compactReport(report), null, 2) : formatPolicySweepReport(report));
46
+ }
47
+
48
+ function compactReport(report: PolicySweepReport): Omit<PolicySweepReport, "baseline" | "best" | "candidates"> & {
49
+ baseline: Omit<SweepCandidateReport, "cases">;
50
+ best: Omit<SweepCandidateReport, "cases">;
51
+ candidates: Omit<SweepCandidateReport, "cases">[];
52
+ } {
53
+ return {
54
+ ...report,
55
+ baseline: compactCandidate(report.baseline),
56
+ best: compactCandidate(report.best),
57
+ candidates: report.candidates.map(compactCandidate),
58
+ };
59
+ }
60
+
61
+ function compactCandidate(candidate: SweepCandidateReport): Omit<SweepCandidateReport, "cases"> {
62
+ const { cases: _cases, ...rest } = candidate;
63
+ return rest;
64
+ }
65
+
66
+ function liveCasesFromArgs(args: { opts: Record<string, string>; positionals: string[] }): LiveEvalCase[] {
67
+ if (args.opts.queries) return readLiveEvalCases(args.opts.queries);
68
+ if (!args.opts.query) throw new Error(usage());
69
+ const gold = goldFromArgs(args.opts);
70
+ return [{
71
+ query: args.opts.query,
72
+ mode: modeField(args.opts.mode, "mode") ?? "hybrid",
73
+ scope: scopeField(args.opts.scope, "scope") ?? "project",
74
+ gold: gold.length > 0 ? gold : undefined,
75
+ }];
76
+ }
77
+
78
+ function goldFromArgs(opts: Record<string, string>): GoldTarget[] {
79
+ if (!opts["gold-id"] && !opts["gold-title"]) return [];
80
+ const id = opts["gold-id"] ? Number(opts["gold-id"]) : undefined;
81
+ if (id !== undefined && (!Number.isInteger(id) || id <= 0)) throw new Error("--gold-id must be a positive integer.");
82
+ const title = opts["gold-title"]?.trim() || undefined;
83
+ return [{
84
+ id,
85
+ title,
86
+ scope: scopeField(opts.scope, "scope"),
87
+ }];
88
+ }
89
+
90
+ function modeField(value: string | undefined, key: string): SearchMode | undefined {
91
+ if (value === undefined) return undefined;
92
+ if (!(SEARCH_MODES as readonly string[]).includes(value)) {
93
+ throw new Error(`--${key} must be one of: ${SEARCH_MODES.join(", ")}`);
94
+ }
95
+ return value as SearchMode;
96
+ }
97
+
98
+ function scopeField(value: string | undefined, key: string): Scope | undefined {
99
+ if (value === undefined) return undefined;
100
+ if (!(SCOPES as readonly string[]).includes(value)) {
101
+ throw new Error(`--${key} must be one of: ${SCOPES.join(", ")}`);
102
+ }
103
+ return value as Scope;
104
+ }
105
+
106
+ function parseArgs(args: string[]): { opts: Record<string, string>; positionals: string[] } {
107
+ const opts: Record<string, string> = {};
108
+ const positionals: string[] = [];
109
+ for (let i = 0; i < args.length; i++) {
110
+ const arg = args[i];
111
+ if (arg.startsWith("--")) {
112
+ const key = arg.slice(2);
113
+ opts[key] = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true";
114
+ } else {
115
+ positionals.push(arg);
116
+ }
117
+ }
118
+ return { opts, positionals };
119
+ }
120
+
121
+ function usage(): string {
122
+ return [
123
+ "Usage:",
124
+ " bun run eval:search-policy -- fixture [--fixture path] [--json]",
125
+ " bun run eval:search-policy -- live --cwd /path/to/project --queries live-queries.json [--scope project|global] [--json]",
126
+ " bun run eval:search-policy -- live --cwd /path/to/project --query '<query>' [--gold-id n] [--gold-title '<title>'] [--scope project|global] [--mode fts|semantic|hybrid] [--json]",
127
+ " Add --include-cases with --json to include full per-case output.",
128
+ ].join("\n");
129
+ }
130
+
131
+ await main();
@@ -2,7 +2,6 @@ import { readFileSync } from "node:fs";
2
2
  import { TrapStore } from "../lib/store";
3
3
  import { formatTrapShort, formatTrapDetails, formatTrapActionCard } from "../lib/format";
4
4
  import type { Trap } from "../domain/trap";
5
- import type { SessionMetadata } from "../domain/session";
6
5
  import {
7
6
  formatScopeMigrationText,
8
7
  runScopeMigration,
@@ -10,6 +9,13 @@ import {
10
9
  } from "../lib/scope-migration";
11
10
  import { TrapOperations } from "../lib/trap-operations";
12
11
  import { buildDoctorReport, formatDoctorText } from "../lib/doctor";
12
+ import { formatEmbedText } from "../lib/embed-output";
13
+ import {
14
+ formatEmbeddingProfilesText,
15
+ formatEmbeddingStatusText,
16
+ formatEmbeddingsUseText,
17
+ type EmbeddingsUseResult,
18
+ } from "../lib/embedding-management";
13
19
  import { searchDefaultsFromConfig } from "../lib/config";
14
20
  import { SessionStore } from "../lib/session-store";
15
21
  import { SessionOperations, type SessionConflictResult } from "../lib/session-operations";
@@ -17,9 +23,17 @@ import {
17
23
  CANDIDATES_FILE,
18
24
  NOTES_FILE,
19
25
  RECAP_FILE,
20
- sessionRelativeDir,
21
26
  sessionRelativeFile,
22
27
  } from "../lib/session-codec";
28
+ import {
29
+ sessionAcceptPayload,
30
+ sessionCliConflictPayload,
31
+ sessionCleanupPayload,
32
+ sessionConflictPayload,
33
+ sessionConflictText,
34
+ sessionPayload,
35
+ sessionRejectPayload,
36
+ } from "../lib/session-review";
23
37
  import {
24
38
  toCliSearchJson,
25
39
  toListJson,
@@ -35,15 +49,18 @@ import {
35
49
  import { mutationJsonPayload } from "../lib/trap-mutation-result";
36
50
  import {
37
51
  embedRequestFromArgs,
52
+ embeddingsUseRequestFromArgs,
38
53
  evidenceRequestFromArgs,
39
54
  listRequestFromArgs,
40
55
  searchRequestFromArgs,
41
56
  sessionAcceptRequestFromArgs,
57
+ sessionCaptureRequestFromArgs,
42
58
  sessionCandidateRequestFromArgs,
43
59
  sessionCloseRequestFromArgs,
44
60
  sessionIdRequestFromArgs,
45
61
  sessionListRequestFromArgs,
46
62
  sessionNoteRequestFromArgs,
63
+ sessionPruneRequestFromArgs,
47
64
  sessionRejectRequestFromArgs,
48
65
  sessionShowRequestFromArgs,
49
66
  sessionStartRequestFromArgs,
@@ -99,12 +116,14 @@ export async function executeCommand(strip: string[], store: TrapStore): Promise
99
116
  return cmdScopeMigration("migrate-project", args, operations);
100
117
  case "embed":
101
118
  return cmdEmbed(args, store);
119
+ case "embeddings":
120
+ return cmdEmbeddings(args, store);
102
121
  case "session":
103
122
  return cmdSession(args, store, operations);
104
123
  default:
105
124
  return errorResult([
106
125
  `Unknown command: ${sub}`,
107
- "Commands: init, add, search, list, show, edit, delete, add_trap_evidence, archive_trap, supersede_trap, export, import, stats, doctor, repair-scope, migrate-project, embed, session",
126
+ "Commands: init, add, search, list, show, edit, delete, add_trap_evidence, archive_trap, supersede_trap, export, import, stats, doctor, repair-scope, migrate-project, embed, embeddings, session",
108
127
  ].join("\n"));
109
128
  }
110
129
  }
@@ -339,9 +358,13 @@ function cmdStats(args: string[], operations: TrapOperations): CommandResult {
339
358
  : textResult(formatStatsText(stats));
340
359
  }
341
360
 
342
- function cmdDoctor(args: string[], store: TrapStore, operations: TrapOperations): CommandResult {
361
+ async function cmdDoctor(args: string[], store: TrapStore, operations: TrapOperations): Promise<CommandResult> {
343
362
  const { opts } = parseArgs(args);
344
- const report = buildDoctorReport(store, operations);
363
+ const projectRoot = store.getProjectRoot();
364
+ const candidateReview = projectRoot
365
+ ? new SessionOperations(new SessionStore(projectRoot), operations).candidateReviewSummary()
366
+ : null;
367
+ const report = await buildDoctorReport(store, operations, process.cwd(), candidateReview);
345
368
  return opts.json !== undefined
346
369
  ? jsonResult(report)
347
370
  : textResult(formatDoctorText(report));
@@ -380,12 +403,60 @@ async function cmdEmbed(args: string[], store: TrapStore): Promise<CommandResult
380
403
  const { opts } = parseArgs(args);
381
404
  try {
382
405
  const result = await store.ensureEmbeddings(embedRequestFromArgs(opts));
383
- return textResult([
384
- ...result.scopes.map((scoped) =>
385
- `[${scoped.scope}] embeddings generated: ${scoped.generated}, skipped: ${scoped.skipped}, batches: ${scoped.batches}`
386
- ),
387
- `Total generated: ${result.generated}, skipped: ${result.skipped}, batches: ${result.batches}`,
388
- ].join("\n"));
406
+ return textResult(formatEmbedText(result));
407
+ } catch (error) {
408
+ return errorFrom(error);
409
+ }
410
+ }
411
+
412
+ async function cmdEmbeddings(args: string[], store: TrapStore): Promise<CommandResult> {
413
+ const sub = args[0] ?? "status";
414
+ const rest = args.length === 0 ? [] : args.slice(1);
415
+
416
+ try {
417
+ switch (sub) {
418
+ case "status": {
419
+ const { opts } = parseArgs(rest);
420
+ const status = await store.embeddingStatus({ scope: opts.scope });
421
+ return opts.json !== undefined
422
+ ? jsonResult(status)
423
+ : textResult(formatEmbeddingStatusText(status));
424
+ }
425
+ case "list":
426
+ case "profiles": {
427
+ const { opts } = parseArgs(rest);
428
+ const profiles = store.embeddingProfiles({ scope: opts.scope });
429
+ const payload = {
430
+ active_profile_id: store.embeddingRuntimeStatus().profile_id,
431
+ ...profiles,
432
+ };
433
+ return opts.json !== undefined
434
+ ? jsonResult(payload)
435
+ : textResult(formatEmbeddingProfilesText(profiles));
436
+ }
437
+ case "use": {
438
+ const { opts, positionals } = parseArgs(rest);
439
+ const request = embeddingsUseRequestFromArgs(positionals, opts);
440
+ const written = store.configureEmbeddings(request.embeddings);
441
+ const scope = store.hasProject() ? "project" : "global";
442
+ const result: EmbeddingsUseResult = {
443
+ ...written,
444
+ embeddings: written.config.embeddings ?? request.embeddings,
445
+ next_action: {
446
+ command: `codetrap embeddings reindex --scope ${scope}`,
447
+ reason: "Generate embeddings for the selected profile.",
448
+ },
449
+ };
450
+ return opts.json !== undefined
451
+ ? jsonResult(result)
452
+ : textResult(formatEmbeddingsUseText(result));
453
+ }
454
+ case "reindex":
455
+ case "embed":
456
+ return cmdEmbed(rest, store);
457
+ default:
458
+ return errorResult("Usage: codetrap embeddings <status|list|profiles|use|reindex> [--json]");
459
+ }
389
460
  } catch (error) {
390
461
  return errorFrom(error);
391
462
  }
@@ -416,6 +487,8 @@ async function cmdSession(args: string[], store: TrapStore, trapOperations: Trap
416
487
  return cmdSessionNotes(rest, sessions);
417
488
  case "close":
418
489
  return cmdSessionClose(rest, sessions);
490
+ case "capture":
491
+ return cmdSessionCapture(rest, sessions);
419
492
  case "candidates":
420
493
  return cmdSessionCandidates(rest, sessions);
421
494
  case "candidate":
@@ -424,8 +497,14 @@ async function cmdSession(args: string[], store: TrapStore, trapOperations: Trap
424
497
  return cmdSessionAccept(rest, sessions);
425
498
  case "reject":
426
499
  return cmdSessionReject(rest, sessions);
500
+ case "delete":
501
+ return cmdSessionDelete(rest, sessions);
502
+ case "prune":
503
+ return cmdSessionPrune(rest, sessions);
504
+ case "cleanup":
505
+ return cmdSessionCleanup(rest, sessions);
427
506
  default:
428
- return errorResult("Usage: codetrap session <start|note|status|list|show|notes|close|candidates|candidate|accept|reject>");
507
+ return errorResult("Usage: codetrap session <start|note|status|list|show|notes|close|capture|candidates|candidate|accept|reject|delete|prune|cleanup>");
429
508
  }
430
509
  } catch (error) {
431
510
  return errorFrom(error);
@@ -469,14 +548,31 @@ function cmdSessionStatus(args: string[], sessions: SessionOperations): CommandR
469
548
  return jsonResult({
470
549
  active_session_id: status.active_session_id,
471
550
  session: status.session ? sessionPayload(status.session) : null,
551
+ candidate_review: status.candidate_review,
472
552
  });
473
553
  }
474
- if (!status.session) return textResult("No active session.");
475
- return textResult([
554
+ if (!status.session) {
555
+ const lines = ["No active session."];
556
+ if (status.candidate_review.pending_count > 0) {
557
+ lines.push(
558
+ `Pending candidate review: ${status.candidate_review.pending_count} candidate(s) across ${status.candidate_review.pending_session_count} session(s).`
559
+ );
560
+ if (status.candidate_review.next_session_id) {
561
+ lines.push(`Review: codetrap session candidates ${status.candidate_review.next_session_id}`);
562
+ }
563
+ lines.push("Open web review: codetrap web");
564
+ }
565
+ return textResult(lines.join("\n"));
566
+ }
567
+ const lines = [
476
568
  `Active session ${status.session.id}`,
477
569
  `Goal: ${status.session.goal}`,
478
570
  `Notes: ${sessionRelativeFile(status.session.id, NOTES_FILE)}`,
479
- ].join("\n"));
571
+ ];
572
+ if (status.candidate_review.pending_count > 0) {
573
+ lines.push(`Pending candidate review: ${status.candidate_review.pending_count} candidate(s).`);
574
+ }
575
+ return textResult(lines.join("\n"));
480
576
  }
481
577
 
482
578
  function cmdSessionList(args: string[], sessions: SessionOperations): CommandResult {
@@ -484,7 +580,9 @@ function cmdSessionList(args: string[], sessions: SessionOperations): CommandRes
484
580
  const entries = sessions.listSessions(sessionListRequestFromArgs(opts));
485
581
  if (opts.json !== undefined) return jsonResult(entries);
486
582
  if (entries.length === 0) return textResult("No sessions found.");
487
- return textResult(entries.map((entry) => `${entry.id} [${entry.status}] ${entry.goal}`).join("\n"));
583
+ return textResult(entries.map((entry) =>
584
+ `${entry.id} [${entry.status}] ${entry.goal} (${entry.pending_count} pending, ${entry.reviewed_count} reviewed)`
585
+ ).join("\n"));
488
586
  }
489
587
 
490
588
  function cmdSessionShow(args: string[], sessions: SessionOperations): CommandResult {
@@ -522,7 +620,6 @@ function cmdSessionClose(args: string[], sessions: SessionOperations): CommandRe
522
620
  ...sessionPayload(result.session),
523
621
  recap_path: result.recap_path,
524
622
  candidate_count: result.candidate_count,
525
- traps_written: result.traps_written,
526
623
  };
527
624
  if (opts.json !== undefined) return jsonResult(payload);
528
625
  const lines = [
@@ -536,6 +633,39 @@ function cmdSessionClose(args: string[], sessions: SessionOperations): CommandRe
536
633
  return textResult(lines.join("\n"));
537
634
  }
538
635
 
636
+ function cmdSessionCapture(args: string[], sessions: SessionOperations): CommandResult {
637
+ const { opts } = parseArgs(args);
638
+ const result = sessions.captureCandidate(sessionCaptureRequestFromArgs(opts, {
639
+ isTTY: process.stdin.isTTY === true,
640
+ readStdin: () => readFileSync(0, "utf-8"),
641
+ readFile: (path) => readFileSync(path, "utf-8"),
642
+ }));
643
+ const nextAction = `codetrap session candidate ${result.candidate.id} --session ${result.session.id} --json`;
644
+ const payload = {
645
+ success: true,
646
+ session_id: result.session.id,
647
+ candidate_id: result.candidate.id,
648
+ status: result.candidate.status,
649
+ quality_score: result.candidate.quality_score,
650
+ candidate_count: result.candidate_count,
651
+ created_session: result.created_session,
652
+ closed_session: result.closed_session,
653
+ duplicate: result.duplicate,
654
+ candidate_traps_path: sessionRelativeFile(result.session.id, CANDIDATES_FILE),
655
+ recap_path: result.recap_path,
656
+ next_action: {
657
+ command: nextAction,
658
+ },
659
+ };
660
+ if (opts.json !== undefined) return jsonResult(payload);
661
+ return textResult([
662
+ `${result.duplicate ? "Reused" : "Captured"} candidate ${result.candidate.id} in session ${result.session.id}.`,
663
+ result.created_session ? "Created and closed a post-flight session." : "Session remains active.",
664
+ `Candidate inbox: ${payload.candidate_traps_path}`,
665
+ `Review: ${nextAction}`,
666
+ ].join("\n"));
667
+ }
668
+
539
669
  function cmdSessionCandidates(args: string[], sessions: SessionOperations): CommandResult {
540
670
  const { opts, positionals } = parseArgs(args);
541
671
  const request = sessionIdRequestFromArgs(positionals);
@@ -559,16 +689,7 @@ async function cmdSessionAccept(args: string[], sessions: SessionOperations): Pr
559
689
  const { opts, positionals } = parseArgs(args);
560
690
  const accepted = await sessions.acceptCandidate(sessionAcceptRequestFromArgs(positionals, opts));
561
691
  if (!accepted.success) return possibleConflictResult(accepted, opts.json !== undefined);
562
- const payload = {
563
- success: true,
564
- session_id: accepted.session.id,
565
- candidate_id: accepted.candidate.id,
566
- status: accepted.candidate.status,
567
- trap_id: accepted.trap_id,
568
- scope: accepted.scope,
569
- evidence_id: accepted.evidence_id,
570
- superseded_id: accepted.superseded_id,
571
- };
692
+ const payload = sessionAcceptPayload(accepted);
572
693
  if (opts.json !== undefined) return jsonResult(payload);
573
694
  const lines = [`Accepted ${accepted.candidate.id}; wrote trap #${accepted.trap_id} to ${accepted.scope} scope.`];
574
695
  if (accepted.superseded_id !== null) lines.push(`Superseded trap #${accepted.superseded_id}.`);
@@ -578,17 +699,45 @@ async function cmdSessionAccept(args: string[], sessions: SessionOperations): Pr
578
699
  function cmdSessionReject(args: string[], sessions: SessionOperations): CommandResult {
579
700
  const { opts, positionals } = parseArgs(args);
580
701
  const rejected = sessions.rejectCandidate(sessionRejectRequestFromArgs(positionals, opts));
581
- const payload = {
582
- success: true,
583
- session_id: rejected.session.id,
584
- candidate_id: rejected.candidate.id,
585
- status: rejected.candidate.status,
586
- reason: rejected.candidate.rejection_reason ?? null,
587
- };
702
+ const payload = sessionRejectPayload(rejected);
588
703
  if (opts.json !== undefined) return jsonResult(payload);
589
704
  return textResult(`Rejected ${rejected.candidate.id}.`);
590
705
  }
591
706
 
707
+ function cmdSessionDelete(args: string[], sessions: SessionOperations): CommandResult {
708
+ const { opts, positionals } = parseArgs(args);
709
+ const request = sessionShowRequestFromArgs(positionals);
710
+ const result = sessions.deleteSession(request.sessionId);
711
+ const payload = { success: result.deleted, ...result };
712
+ if (opts.json !== undefined) return jsonResult(payload);
713
+ return textResult(`Deleted session ${result.session_id}.`);
714
+ }
715
+
716
+ function cmdSessionPrune(args: string[], sessions: SessionOperations): CommandResult {
717
+ const { opts } = parseArgs(args);
718
+ const result = sessions.pruneSessions(sessionPruneRequestFromArgs(opts));
719
+ if (opts.json !== undefined) return jsonResult(result);
720
+ const verb = result.dry_run ? "Would delete" : "Deleted";
721
+ const lines = [`${verb} ${result.dry_run ? result.sessions.length : result.deleted_count} session(s) older than ${result.cutoff}.`];
722
+ if (result.dry_run && result.sessions.length > 0) {
723
+ lines.push("Run with --apply to delete them.");
724
+ }
725
+ lines.push(...result.sessions.map((session) => `- ${session.id} [${session.status}] ${session.goal}`));
726
+ return textResult(lines.join("\n"));
727
+ }
728
+
729
+ function cmdSessionCleanup(args: string[], sessions: SessionOperations): CommandResult {
730
+ const { opts, positionals } = parseArgs(args);
731
+ if (opts["deleted-trap-candidates"] === undefined && opts.deleted_trap_candidates === undefined) {
732
+ return errorResult("Usage: codetrap session cleanup [session-id] --deleted-trap-candidates [--json]");
733
+ }
734
+ const request = sessionIdRequestFromArgs(positionals);
735
+ const result = sessions.cleanupDeletedTrapCandidates(request.sessionId);
736
+ const payload = sessionCleanupPayload(result);
737
+ if (opts.json !== undefined) return jsonResult(payload);
738
+ return textResult(`Removed ${result.removed_count} deleted-trap candidate(s) from session ${result.session.id}.`);
739
+ }
740
+
592
741
  function formatStatsText(stats: ReturnType<TrapOperations["getStats"]>): string {
593
742
  const sections: string[] = [];
594
743
  if (stats.project) {
@@ -637,42 +786,11 @@ function errorMessage(error: unknown): string {
637
786
  return error instanceof Error ? error.message : String(error);
638
787
  }
639
788
 
640
- function sessionPayload(session: SessionMetadata) {
641
- return {
642
- ...session,
643
- session_dir: sessionRelativeDir(session.id),
644
- notes_path: sessionRelativeFile(session.id, NOTES_FILE),
645
- recap_path: sessionRelativeFile(session.id, RECAP_FILE),
646
- candidate_traps_path: sessionRelativeFile(session.id, CANDIDATES_FILE),
647
- };
648
- }
649
-
650
789
  function possibleConflictResult(
651
790
  result: SessionConflictResult,
652
791
  asJson: boolean
653
792
  ): CommandResult {
654
- const payload = {
655
- success: false,
656
- error: "Possible active trap conflict found.",
657
- session_id: result.session_id,
658
- candidate_id: result.candidate_id,
659
- possible_conflicts: result.possible_conflicts,
660
- next_actions: [
661
- `codetrap session accept ${result.candidate_id} --session ${result.session_id} --accept-anyway`,
662
- `codetrap session accept ${result.candidate_id} --session ${result.session_id} --supersedes <trap-id>`,
663
- `codetrap session reject ${result.candidate_id} --session ${result.session_id} --reason <reason>`,
664
- ],
665
- };
666
- if (asJson) return jsonResult(payload, 1);
667
-
668
- return errorResult([
669
- "Possible active trap conflict found:",
670
- ...result.possible_conflicts.map((conflict) => [
671
- `#${conflict.trap_id} ${conflict.title}`,
672
- ` reason: ${conflict.reason}`,
673
- ` fix: ${conflict.fix}`,
674
- ].join("\n")),
675
- "",
676
- `Use --accept-anyway to save as a new trap, or --supersedes <trap-id> to preserve lifecycle history.`,
677
- ].join("\n"));
793
+ const payload = sessionConflictPayload(result);
794
+ if (asJson) return jsonResult(sessionCliConflictPayload(payload), 1);
795
+ return errorResult(sessionConflictText(payload));
678
796
  }
@@ -2,7 +2,7 @@ import { Database } from "bun:sqlite";
2
2
  import { getGlobalDB, getProjectDB } from "../lib/scope";
3
3
  import { initSchema } from "./schema";
4
4
 
5
- let globalDB: Database | null = null;
5
+ const globalDBs = new Map<string, Database>();
6
6
  const projectDBs = new Map<string, Database>();
7
7
 
8
8
  export function openDatabase(path = ":memory:"): Database {
@@ -11,12 +11,12 @@ export function openDatabase(path = ":memory:"): Database {
11
11
  return db;
12
12
  }
13
13
 
14
- export function openGlobal(): Database {
15
- if (!globalDB) {
16
- const path = getGlobalDB();
17
- globalDB = openDatabase(path);
14
+ export function openGlobal(home?: string): Database {
15
+ const path = getGlobalDB(home);
16
+ if (!globalDBs.has(path)) {
17
+ globalDBs.set(path, openDatabase(path));
18
18
  }
19
- return globalDB;
19
+ return globalDBs.get(path)!;
20
20
  }
21
21
 
22
22
  export function openProject(root: string): Database {