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.
- package/README.md +159 -51
- package/docs/installation.md +113 -29
- package/package.json +4 -3
- package/plugins/codetrap-agent/.codex-plugin/plugin.json +1 -2
- package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +19 -17
- package/plugins/codetrap-agent/hooks.json +2 -2
- package/{skills → plugins/codetrap-agent/skills}/codetrap-add/SKILL.md +10 -4
- package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +14 -3
- package/plugins/codetrap-agent/skills/codetrap-capture-external/SKILL.md +52 -9
- package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +74 -6
- package/{skills → plugins/codetrap-agent/skills}/codetrap-search/SKILL.md +6 -5
- package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +31 -5
- package/scripts/search-policy-sweep.ts +131 -0
- package/src/commands/workflow.ts +186 -68
- package/src/db/connection.ts +6 -6
- package/src/db/embedding-queries.ts +230 -48
- package/src/db/queries.ts +0 -25
- package/src/db/repository.ts +32 -21
- package/src/db/schema.ts +80 -0
- package/src/index.ts +32 -7
- package/src/lib/command-requests.ts +134 -1
- package/src/lib/config.ts +57 -7
- package/src/lib/constants.ts +1 -1
- package/src/lib/doctor.ts +96 -6
- package/src/lib/embed-output.ts +26 -0
- package/src/lib/embedder.ts +118 -3
- package/src/lib/embedding-health.ts +3 -1
- package/src/lib/embedding-job.ts +3 -0
- package/src/lib/embedding-management.ts +65 -0
- package/src/lib/embedding-runtime.ts +177 -0
- package/src/lib/output-json.ts +0 -2
- package/src/lib/scope-context.ts +17 -11
- package/src/lib/scope-migration.ts +2 -1
- package/src/lib/scope.ts +4 -6
- package/src/lib/search-eval.ts +136 -23
- package/src/lib/search-policy-sweep.ts +563 -0
- package/src/lib/search-policy.ts +0 -4
- package/src/lib/search-service.ts +14 -15
- package/src/lib/session-candidate-document.ts +175 -0
- package/src/lib/session-candidate-scope.ts +6 -0
- package/src/lib/session-capture.ts +298 -32
- package/src/lib/session-codec.ts +1 -8
- package/src/lib/session-operations.ts +111 -51
- package/src/lib/session-review.ts +327 -0
- package/src/lib/session-store.ts +177 -55
- package/src/lib/store.ts +79 -11
- package/src/lib/string-list.ts +3 -0
- package/src/lib/text-lines.ts +7 -0
- package/src/lib/trap-search-document.ts +2 -1
- package/src/lib/value-types.ts +3 -0
- package/src/web/client-review.ts +171 -0
- package/src/web/client-script.ts +1543 -0
- package/src/web/client-shell.ts +414 -0
- package/src/web/client-text.ts +447 -0
- package/src/web/project-registry.ts +3 -5
- package/src/web/server.ts +184 -111
- package/src/web/static.ts +581 -484
- package/skills/codetrap-capture-external/SKILL.md +0 -62
- package/skills/codetrap-check/SKILL.md +0 -69
- 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.
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
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();
|
package/src/commands/workflow.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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)
|
|
475
|
-
|
|
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
|
-
]
|
|
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) =>
|
|
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
|
-
|
|
656
|
-
|
|
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
|
}
|
package/src/db/connection.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
19
|
+
return globalDBs.get(path)!;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function openProject(root: string): Database {
|