codetrap 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -1
- package/docs/installation.md +21 -3
- package/package.json +3 -1
- package/scripts/dogfood-eval.ts +53 -0
- package/src/commands/workflow.ts +260 -1
- package/src/db/connection.ts +1 -1
- package/src/domain/session.ts +119 -0
- package/src/index.ts +8 -0
- package/src/lib/command-requests.ts +156 -0
- package/src/lib/search-eval.ts +412 -0
- package/src/lib/session-capture.ts +96 -0
- package/src/lib/session-codec.ts +261 -0
- package/src/lib/session-conflicts.ts +104 -0
- package/src/lib/session-operations.ts +214 -0
- package/src/lib/session-store.ts +503 -0
- package/src/lib/trap-quality.ts +111 -0
- package/src/lib/trap-scope-match.ts +1 -1
- package/src/web/project-registry.ts +106 -0
- package/src/web/server.ts +441 -0
- package/src/web/static.ts +776 -0
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ codetrap show 1
|
|
|
56
56
|
## Features
|
|
57
57
|
|
|
58
58
|
- **Structured trap recording** — title, category, context, mistake, fix, severity, tags, lifecycle, evidence, before/after code
|
|
59
|
+
- **Session mode capture** — record implementation notes, promote explicit structured trap notes into candidates, and save only user-accepted lessons
|
|
59
60
|
- **Dual scope** — project-scoped (`.codetrap/traps.db`) and global (`~/.codetrap/traps.db`)
|
|
60
61
|
- **CLI-first agent API** — `search/show/list/stats/doctor --json` and stdin query support for shell-friendly automation
|
|
61
62
|
- **Three search modes** — FTS (SQLite FTS5), semantic (Jina embeddings), hybrid (RRF fusion)
|
|
@@ -81,9 +82,17 @@ codetrap/
|
|
|
81
82
|
│ │ ├── tools.ts 10 MCP tool definitions
|
|
82
83
|
│ │ └── resources.ts 4 MCP resource URIs
|
|
83
84
|
│ ├── domain/trap.ts Trap types, builders, schemas
|
|
85
|
+
│ ├── domain/session.ts Session, note, and candidate trap types
|
|
84
86
|
│ ├── lib/
|
|
85
87
|
│ │ ├── store.ts Project/global scope orchestration
|
|
86
88
|
│ │ ├── trap-operations.ts Shared CLI/MCP operation semantics
|
|
89
|
+
│ │ ├── session-operations.ts Session command semantics + accept/reject flow
|
|
90
|
+
│ │ ├── session-store.ts Session files, active state, index, recaps
|
|
91
|
+
│ │ ├── session-codec.ts Session JSON/Markdown/candidate file conversion
|
|
92
|
+
│ │ ├── session-capture.ts Candidate trap extraction from explicit structured notes
|
|
93
|
+
│ │ ├── session-conflicts.ts Candidate vs active-trap conflict checks
|
|
94
|
+
│ │ ├── trap-quality.ts Deterministic candidate quality scoring
|
|
95
|
+
│ │ ├── command-requests.ts CLI/MCP request normalization helpers
|
|
87
96
|
│ │ ├── output-json.ts Shared CLI/MCP JSON presenters
|
|
88
97
|
│ │ ├── scope-context.ts cwd/project/global DB context + repo selection
|
|
89
98
|
│ │ ├── scope-migration.ts Safe project trap scope repair/migration
|
|
@@ -115,6 +124,7 @@ codetrap/
|
|
|
115
124
|
│ └── tests/
|
|
116
125
|
│ ├── search-*.test.ts
|
|
117
126
|
│ ├── trap-*.test.ts
|
|
127
|
+
│ ├── session-cli.test.ts
|
|
118
128
|
│ ├── mcp-tools.test.ts
|
|
119
129
|
│ ├── scope.test.ts
|
|
120
130
|
│ ├── scope-migration-cli.test.ts
|
|
@@ -150,8 +160,25 @@ codetrap/
|
|
|
150
160
|
| `repair-scope` | Move legacy mis-scoped project traps into the current project (dry-run by default, `--apply` to mutate, `--json`) |
|
|
151
161
|
| `migrate-project` | Move project traps between initialized projects (`--from-project-path`, `--to-project-path`, dry-run by default, `--apply`, `--json`) |
|
|
152
162
|
| `embed` | Generate embeddings (requires JINA_API_KEY) |
|
|
163
|
+
| `session` | Start a development session, append notes, promote explicit structured trap notes into candidates, and accept/reject candidates |
|
|
153
164
|
| `serve` | Start MCP server |
|
|
154
165
|
|
|
166
|
+
### Session Mode
|
|
167
|
+
|
|
168
|
+
Session mode stores temporary working memory in `.codetrap/sessions/`. It does not add anything to `traps.db` until a candidate is explicitly accepted.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
codetrap session start "implement agent harness" --spec docs/agent-harness-spec.md --module agent-runtime
|
|
172
|
+
codetrap session note --kind decision --text "Defaulted tool calls to 30s because the spec does not define timeout behavior."
|
|
173
|
+
codetrap session note --kind review --text $'Title: Do not parse nested tool calls with regex\nContext: When implementing parser logic for nested tool-call arguments.\nMistake: Using regex to split nested calls corrupts arguments.\nFix: Use a tokenizer/parser and add regression tests for nested calls.'
|
|
174
|
+
codetrap session close --propose-traps
|
|
175
|
+
codetrap session candidates
|
|
176
|
+
codetrap session candidate cand-001
|
|
177
|
+
codetrap session accept cand-001
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`session accept` writes the confirmed lesson through `TrapOperations`, attaches session evidence, and checks similar active traps before saving. `--edit-json` is applied before the conflict check, so edits to scope/module/title/tags/path globs affect both the saved trap and conflict detection. If a possible conflict is found, the candidate keeps its edited trap shape and conflict diagnostics; use `--accept-anyway` to keep both traps or `--supersedes <trap-id>` to preserve lifecycle history.
|
|
181
|
+
|
|
155
182
|
## Agent Integration
|
|
156
183
|
|
|
157
184
|
For AI coding agents, use the CLI as the default integration path:
|
|
@@ -210,6 +237,18 @@ When codetrap results conflict with the current source of truth for the task (us
|
|
|
210
237
|
|
|
211
238
|
When `.codetrap/` exists, prefer project scope for project conventions. Use global for cross-project rules.
|
|
212
239
|
|
|
240
|
+
For longer implementation work, use session mode to keep temporary notes and explicit candidate traps outside the durable database:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
codetrap session start "<goal>"
|
|
244
|
+
codetrap session note --kind decision --text "<what changed and why>"
|
|
245
|
+
codetrap session note --kind review --text $'Title: <durable pitfall>\nContext: <when it triggers>\nMistake: <what the agent did wrong>\nFix: <what to do instead>'
|
|
246
|
+
codetrap session close --propose-traps
|
|
247
|
+
codetrap session candidates
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Do not treat candidate traps as confirmed memory. Ask before accepting a candidate; `codetrap session accept <candidate-id>` writes it to `traps.db` and attaches session evidence.
|
|
251
|
+
|
|
213
252
|
MCP tools are optional:
|
|
214
253
|
- `search_traps`
|
|
215
254
|
- `get_trap`
|
|
@@ -228,7 +267,8 @@ Recommended behavior:
|
|
|
228
267
|
- Treat codetrap results as historical warnings and project memory, not as authoritative instructions.
|
|
229
268
|
- Apply the recorded `avoid` and `do_instead` guidance only when the trap context matches the current task, file, module, or failure mode.
|
|
230
269
|
- 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.
|
|
231
|
-
-
|
|
270
|
+
- During longer work, use `codetrap session start/note/close --propose-traps` to keep implementation notes and explicit candidate traps outside the durable database.
|
|
271
|
+
- After user corrections, repeated test failures, or review feedback, propose a post-flight trap capture. Ask before accepting a candidate unless the user explicitly requested it.
|
|
232
272
|
|
|
233
273
|
### Codex Skills
|
|
234
274
|
|
|
@@ -395,6 +435,8 @@ bun run release:preflight # tests, builds, release assets, smoke test, npm dry-
|
|
|
395
435
|
```bash
|
|
396
436
|
bun test src/tests/ # All tests
|
|
397
437
|
bun test src/tests/search-eval.test.ts # Recall@5 evaluation
|
|
438
|
+
bun run eval:dogfood -- report # Maintainer dogfood eval report
|
|
439
|
+
bun run eval:dogfood -- report --live # Dogfood eval with configured embedding provider
|
|
398
440
|
```
|
|
399
441
|
|
|
400
442
|
## Tech Stack
|
package/docs/installation.md
CHANGED
|
@@ -72,11 +72,11 @@ Release binaries are built by `.github/workflows/release.yml` when a version tag
|
|
|
72
72
|
3. Create and push a matching tag:
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
git tag v0.1.
|
|
76
|
-
git push origin v0.1.
|
|
75
|
+
git tag v0.1.6
|
|
76
|
+
git push origin v0.1.6
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
The release tag must match `package.json` exactly. For example, package version `0.1.
|
|
79
|
+
The release tag must match `package.json` exactly. For example, package version `0.1.6` must use tag `v0.1.6`.
|
|
80
80
|
|
|
81
81
|
The workflow runs:
|
|
82
82
|
|
|
@@ -312,6 +312,24 @@ To add a lesson:
|
|
|
312
312
|
|
|
313
313
|
codetrap add --json '{...}' --output-json
|
|
314
314
|
|
|
315
|
+
For longer implementation work, keep temporary notes and explicit candidate traps in session files first:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
codetrap session start "<goal>"
|
|
319
|
+
codetrap session note --kind decision --text "<what changed and why>"
|
|
320
|
+
codetrap session note --kind review --text $'Title: <durable pitfall>\nContext: <when it triggers>\nMistake: <what the agent did wrong>\nFix: <what to do instead>'
|
|
321
|
+
codetrap session close --propose-traps
|
|
322
|
+
codetrap session candidates
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Only accepted candidates are written to `traps.db`:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
codetrap session accept <candidate-id>
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`codetrap session accept --edit-json ...` applies the edit before conflict detection. If a possible active-trap conflict is found, the candidate remains proposed and records conflict diagnostics until you choose `--accept-anyway`, `--supersedes <trap-id>`, or reject it.
|
|
332
|
+
|
|
315
333
|
To save a lesson from an external article or reference, let the agent read the source and attach the URL as evidence after the user confirms the trap:
|
|
316
334
|
|
|
317
335
|
codetrap add_trap_evidence <id> --scope global --source_type article --source_ref "https://example.com/post" --output-json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codetrap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Capture and retrieve coding pitfalls so AI doesn't repeat mistakes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"src/domain",
|
|
31
31
|
"src/lib",
|
|
32
32
|
"src/mcp",
|
|
33
|
+
"src/web",
|
|
33
34
|
"src/index.ts",
|
|
34
35
|
"src/mcp-server.ts",
|
|
35
36
|
"skills",
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"build:release": "bun run scripts/build-release.ts",
|
|
51
52
|
"release:preflight": "bun run scripts/release-preflight.ts",
|
|
52
53
|
"check:release-version": "bun run scripts/check-release-version.ts",
|
|
54
|
+
"eval:dogfood": "bun run scripts/dogfood-eval.ts",
|
|
53
55
|
"build": "bun build ./src/index.ts --compile --outfile dist/codetrap && bun build ./src/mcp-server.ts --compile --outfile dist/codetrap-serve",
|
|
54
56
|
"build:cli": "bun build ./src/index.ts --compile --outfile dist/codetrap",
|
|
55
57
|
"build:serve": "bun build ./src/mcp-server.ts --compile --outfile dist/codetrap-serve"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_SEARCH_EVAL_FIXTURE,
|
|
5
|
+
formatSearchEvalReport,
|
|
6
|
+
recordDogfoodCase,
|
|
7
|
+
reportDogfood,
|
|
8
|
+
} from "../src/lib/search-eval";
|
|
9
|
+
|
|
10
|
+
async function main(): Promise<void> {
|
|
11
|
+
const args = parseArgs(process.argv.slice(2));
|
|
12
|
+
const command = args.positionals[0];
|
|
13
|
+
const fixturePath = args.opts.fixture ?? DEFAULT_SEARCH_EVAL_FIXTURE;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (command === "record") {
|
|
17
|
+
console.log(JSON.stringify(recordDogfoodCase(fixturePath, args.opts.json), null, 2));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (command === "report") {
|
|
22
|
+
const result = await reportDogfood(fixturePath, args.opts.live === "true");
|
|
23
|
+
console.log(args.opts.json === "true" ? JSON.stringify(result, null, 2) : formatSearchEvalReport(result));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw new Error([
|
|
28
|
+
"Usage:",
|
|
29
|
+
" bun run eval:dogfood -- report [--live] [--json] [--fixture path]",
|
|
30
|
+
" bun run eval:dogfood -- record --json '<record>' [--fixture path]",
|
|
31
|
+
].join("\n"));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseArgs(args: string[]): { opts: Record<string, string>; positionals: string[] } {
|
|
39
|
+
const opts: Record<string, string> = {};
|
|
40
|
+
const positionals: string[] = [];
|
|
41
|
+
for (let i = 0; i < args.length; i++) {
|
|
42
|
+
const arg = args[i];
|
|
43
|
+
if (arg.startsWith("--")) {
|
|
44
|
+
const key = arg.slice(2);
|
|
45
|
+
opts[key] = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true";
|
|
46
|
+
} else {
|
|
47
|
+
positionals.push(arg);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { opts, positionals };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await main();
|
package/src/commands/workflow.ts
CHANGED
|
@@ -2,6 +2,7 @@ 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";
|
|
5
6
|
import {
|
|
6
7
|
formatScopeMigrationText,
|
|
7
8
|
runScopeMigration,
|
|
@@ -10,6 +11,15 @@ import {
|
|
|
10
11
|
import { TrapOperations } from "../lib/trap-operations";
|
|
11
12
|
import { buildDoctorReport, formatDoctorText } from "../lib/doctor";
|
|
12
13
|
import { searchDefaultsFromConfig } from "../lib/config";
|
|
14
|
+
import { SessionStore } from "../lib/session-store";
|
|
15
|
+
import { SessionOperations, type SessionConflictResult } from "../lib/session-operations";
|
|
16
|
+
import {
|
|
17
|
+
CANDIDATES_FILE,
|
|
18
|
+
NOTES_FILE,
|
|
19
|
+
RECAP_FILE,
|
|
20
|
+
sessionRelativeDir,
|
|
21
|
+
sessionRelativeFile,
|
|
22
|
+
} from "../lib/session-codec";
|
|
13
23
|
import {
|
|
14
24
|
toCliSearchJson,
|
|
15
25
|
toListJson,
|
|
@@ -28,6 +38,15 @@ import {
|
|
|
28
38
|
evidenceRequestFromArgs,
|
|
29
39
|
listRequestFromArgs,
|
|
30
40
|
searchRequestFromArgs,
|
|
41
|
+
sessionAcceptRequestFromArgs,
|
|
42
|
+
sessionCandidateRequestFromArgs,
|
|
43
|
+
sessionCloseRequestFromArgs,
|
|
44
|
+
sessionIdRequestFromArgs,
|
|
45
|
+
sessionListRequestFromArgs,
|
|
46
|
+
sessionNoteRequestFromArgs,
|
|
47
|
+
sessionRejectRequestFromArgs,
|
|
48
|
+
sessionShowRequestFromArgs,
|
|
49
|
+
sessionStartRequestFromArgs,
|
|
31
50
|
statsRequestFromArgs,
|
|
32
51
|
} from "../lib/command-requests";
|
|
33
52
|
|
|
@@ -80,10 +99,12 @@ export async function executeCommand(strip: string[], store: TrapStore): Promise
|
|
|
80
99
|
return cmdScopeMigration("migrate-project", args, operations);
|
|
81
100
|
case "embed":
|
|
82
101
|
return cmdEmbed(args, store);
|
|
102
|
+
case "session":
|
|
103
|
+
return cmdSession(args, store, operations);
|
|
83
104
|
default:
|
|
84
105
|
return errorResult([
|
|
85
106
|
`Unknown command: ${sub}`,
|
|
86
|
-
"Commands: init, add, search, list, show, edit, delete, add_trap_evidence, archive_trap, supersede_trap, export, import, stats, doctor, repair-scope, migrate-project, embed",
|
|
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",
|
|
87
108
|
].join("\n"));
|
|
88
109
|
}
|
|
89
110
|
}
|
|
@@ -370,6 +391,204 @@ async function cmdEmbed(args: string[], store: TrapStore): Promise<CommandResult
|
|
|
370
391
|
}
|
|
371
392
|
}
|
|
372
393
|
|
|
394
|
+
async function cmdSession(args: string[], store: TrapStore, trapOperations: TrapOperations): Promise<CommandResult> {
|
|
395
|
+
const sub = args[0];
|
|
396
|
+
const rest = args.slice(1);
|
|
397
|
+
const projectRoot = store.getProjectRoot();
|
|
398
|
+
if (!projectRoot) {
|
|
399
|
+
return errorResult("Not in a project. Run 'codetrap init' first.");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const sessions = new SessionOperations(new SessionStore(projectRoot), trapOperations);
|
|
403
|
+
try {
|
|
404
|
+
switch (sub) {
|
|
405
|
+
case "start":
|
|
406
|
+
return cmdSessionStart(rest, sessions);
|
|
407
|
+
case "note":
|
|
408
|
+
return cmdSessionNote(rest, sessions);
|
|
409
|
+
case "status":
|
|
410
|
+
return cmdSessionStatus(rest, sessions);
|
|
411
|
+
case "list":
|
|
412
|
+
return cmdSessionList(rest, sessions);
|
|
413
|
+
case "show":
|
|
414
|
+
return cmdSessionShow(rest, sessions);
|
|
415
|
+
case "notes":
|
|
416
|
+
return cmdSessionNotes(rest, sessions);
|
|
417
|
+
case "close":
|
|
418
|
+
return cmdSessionClose(rest, sessions);
|
|
419
|
+
case "candidates":
|
|
420
|
+
return cmdSessionCandidates(rest, sessions);
|
|
421
|
+
case "candidate":
|
|
422
|
+
return cmdSessionCandidate(rest, sessions);
|
|
423
|
+
case "accept":
|
|
424
|
+
return cmdSessionAccept(rest, sessions);
|
|
425
|
+
case "reject":
|
|
426
|
+
return cmdSessionReject(rest, sessions);
|
|
427
|
+
default:
|
|
428
|
+
return errorResult("Usage: codetrap session <start|note|status|list|show|notes|close|candidates|candidate|accept|reject>");
|
|
429
|
+
}
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return errorFrom(error);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function cmdSessionStart(args: string[], sessions: SessionOperations): CommandResult {
|
|
436
|
+
const { opts, positionals } = parseArgs(args);
|
|
437
|
+
const session = sessions.startSession(sessionStartRequestFromArgs(positionals, opts));
|
|
438
|
+
const payload = sessionPayload(session);
|
|
439
|
+
if (opts.json !== undefined) return jsonResult(payload);
|
|
440
|
+
return textResult([
|
|
441
|
+
`Started session ${session.id}`,
|
|
442
|
+
`Notes: ${payload.notes_path}`,
|
|
443
|
+
].join("\n"));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function cmdSessionNote(args: string[], sessions: SessionOperations): CommandResult {
|
|
447
|
+
const { opts, positionals } = parseArgs(args);
|
|
448
|
+
const result = sessions.addNote(sessionNoteRequestFromArgs(positionals, opts, {
|
|
449
|
+
isTTY: process.stdin.isTTY === true,
|
|
450
|
+
read: () => readFileSync(0, "utf-8"),
|
|
451
|
+
}));
|
|
452
|
+
const payload = {
|
|
453
|
+
session_id: result.session.id,
|
|
454
|
+
kind: result.note.kind,
|
|
455
|
+
note: result.note,
|
|
456
|
+
notes_path: result.notes_path,
|
|
457
|
+
};
|
|
458
|
+
if (opts.json !== undefined) return jsonResult(payload);
|
|
459
|
+
return textResult([
|
|
460
|
+
`Added ${result.note.kind} note to session ${result.session.id}.`,
|
|
461
|
+
`Notes: ${result.notes_path}`,
|
|
462
|
+
].join("\n"));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function cmdSessionStatus(args: string[], sessions: SessionOperations): CommandResult {
|
|
466
|
+
const { opts } = parseArgs(args);
|
|
467
|
+
const status = sessions.status();
|
|
468
|
+
if (opts.json !== undefined) {
|
|
469
|
+
return jsonResult({
|
|
470
|
+
active_session_id: status.active_session_id,
|
|
471
|
+
session: status.session ? sessionPayload(status.session) : null,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
if (!status.session) return textResult("No active session.");
|
|
475
|
+
return textResult([
|
|
476
|
+
`Active session ${status.session.id}`,
|
|
477
|
+
`Goal: ${status.session.goal}`,
|
|
478
|
+
`Notes: ${sessionRelativeFile(status.session.id, NOTES_FILE)}`,
|
|
479
|
+
].join("\n"));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function cmdSessionList(args: string[], sessions: SessionOperations): CommandResult {
|
|
483
|
+
const { opts } = parseArgs(args);
|
|
484
|
+
const entries = sessions.listSessions(sessionListRequestFromArgs(opts));
|
|
485
|
+
if (opts.json !== undefined) return jsonResult(entries);
|
|
486
|
+
if (entries.length === 0) return textResult("No sessions found.");
|
|
487
|
+
return textResult(entries.map((entry) => `${entry.id} [${entry.status}] ${entry.goal}`).join("\n"));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function cmdSessionShow(args: string[], sessions: SessionOperations): CommandResult {
|
|
491
|
+
const { opts, positionals } = parseArgs(args);
|
|
492
|
+
const request = sessionShowRequestFromArgs(positionals);
|
|
493
|
+
const shown = sessions.showSession(request.sessionId);
|
|
494
|
+
if (opts.json !== undefined) {
|
|
495
|
+
return jsonResult({
|
|
496
|
+
...sessionPayload(shown.session),
|
|
497
|
+
session_dir: shown.session_dir,
|
|
498
|
+
recap: shown.recap,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
if (shown.recap) return textResult(shown.recap.trimEnd());
|
|
502
|
+
return textResult([
|
|
503
|
+
`Session ${shown.session.id} [${shown.session.status}]`,
|
|
504
|
+
`Goal: ${shown.session.goal}`,
|
|
505
|
+
`Notes: ${shown.notes_path}`,
|
|
506
|
+
].join("\n"));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function cmdSessionNotes(args: string[], sessions: SessionOperations): CommandResult {
|
|
510
|
+
const { opts, positionals } = parseArgs(args);
|
|
511
|
+
const request = sessionIdRequestFromArgs(positionals);
|
|
512
|
+
const summary = sessions.summarizeNotes(request.sessionId);
|
|
513
|
+
if (opts.json !== undefined) return jsonResult(summary);
|
|
514
|
+
return textResult(summary.content.trimEnd());
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function cmdSessionClose(args: string[], sessions: SessionOperations): CommandResult {
|
|
518
|
+
const { opts, positionals } = parseArgs(args);
|
|
519
|
+
const request = sessionCloseRequestFromArgs(positionals, opts);
|
|
520
|
+
const result = sessions.closeSession(request.sessionId, request.proposeTraps);
|
|
521
|
+
const payload = {
|
|
522
|
+
...sessionPayload(result.session),
|
|
523
|
+
recap_path: result.recap_path,
|
|
524
|
+
candidate_count: result.candidate_count,
|
|
525
|
+
traps_written: result.traps_written,
|
|
526
|
+
};
|
|
527
|
+
if (opts.json !== undefined) return jsonResult(payload);
|
|
528
|
+
const lines = [
|
|
529
|
+
`Closed session ${result.session.id}`,
|
|
530
|
+
`Generated ${RECAP_FILE}`,
|
|
531
|
+
];
|
|
532
|
+
if (opts["propose-traps"] !== undefined) {
|
|
533
|
+
lines.push(`Proposed ${result.candidate_count} candidate traps`);
|
|
534
|
+
lines.push(`0 traps were written. Use \`codetrap session accept <candidate-id> --session ${result.session.id}\` to save one.`);
|
|
535
|
+
}
|
|
536
|
+
return textResult(lines.join("\n"));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function cmdSessionCandidates(args: string[], sessions: SessionOperations): CommandResult {
|
|
540
|
+
const { opts, positionals } = parseArgs(args);
|
|
541
|
+
const request = sessionIdRequestFromArgs(positionals);
|
|
542
|
+
const document = sessions.candidateDocument(request.sessionId);
|
|
543
|
+
if (opts.json !== undefined) return jsonResult(document);
|
|
544
|
+
if (document.candidates.length === 0) return textResult("No candidate traps found.");
|
|
545
|
+
return textResult(document.candidates.map((candidate) =>
|
|
546
|
+
`${candidate.id} [${candidate.status}] ${candidate.trap.title} (${candidate.quality_score.toFixed(2)})`
|
|
547
|
+
).join("\n"));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function cmdSessionCandidate(args: string[], sessions: SessionOperations): CommandResult {
|
|
551
|
+
const { opts, positionals } = parseArgs(args);
|
|
552
|
+
const request = sessionCandidateRequestFromArgs(positionals, opts);
|
|
553
|
+
const result = sessions.getCandidate(request.candidateId, request.sessionId);
|
|
554
|
+
if (opts.json !== undefined) return jsonResult({ session_id: result.session.id, candidate: result.candidate });
|
|
555
|
+
return textResult(JSON.stringify(result.candidate, null, 2));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function cmdSessionAccept(args: string[], sessions: SessionOperations): Promise<CommandResult> {
|
|
559
|
+
const { opts, positionals } = parseArgs(args);
|
|
560
|
+
const accepted = await sessions.acceptCandidate(sessionAcceptRequestFromArgs(positionals, opts));
|
|
561
|
+
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
|
+
};
|
|
572
|
+
if (opts.json !== undefined) return jsonResult(payload);
|
|
573
|
+
const lines = [`Accepted ${accepted.candidate.id}; wrote trap #${accepted.trap_id} to ${accepted.scope} scope.`];
|
|
574
|
+
if (accepted.superseded_id !== null) lines.push(`Superseded trap #${accepted.superseded_id}.`);
|
|
575
|
+
return textResult(lines.join("\n"));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function cmdSessionReject(args: string[], sessions: SessionOperations): CommandResult {
|
|
579
|
+
const { opts, positionals } = parseArgs(args);
|
|
580
|
+
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
|
+
};
|
|
588
|
+
if (opts.json !== undefined) return jsonResult(payload);
|
|
589
|
+
return textResult(`Rejected ${rejected.candidate.id}.`);
|
|
590
|
+
}
|
|
591
|
+
|
|
373
592
|
function formatStatsText(stats: ReturnType<TrapOperations["getStats"]>): string {
|
|
374
593
|
const sections: string[] = [];
|
|
375
594
|
if (stats.project) {
|
|
@@ -417,3 +636,43 @@ function errorFrom(error: unknown): CommandResult {
|
|
|
417
636
|
function errorMessage(error: unknown): string {
|
|
418
637
|
return error instanceof Error ? error.message : String(error);
|
|
419
638
|
}
|
|
639
|
+
|
|
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
|
+
function possibleConflictResult(
|
|
651
|
+
result: SessionConflictResult,
|
|
652
|
+
asJson: boolean
|
|
653
|
+
): 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"));
|
|
678
|
+
}
|
package/src/db/connection.ts
CHANGED
|
@@ -29,8 +29,8 @@ export function openProject(root: string): Database {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function configureDatabase(db: Database): void {
|
|
32
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
32
33
|
db.exec("PRAGMA journal_mode=WAL");
|
|
33
34
|
db.exec("PRAGMA foreign_keys=ON");
|
|
34
|
-
db.exec("PRAGMA busy_timeout=5000");
|
|
35
35
|
initSchema(db);
|
|
36
36
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { TrapEvidenceInput, TrapInput } from "./trap";
|
|
2
|
+
import type { Scope } from "../lib/constants";
|
|
3
|
+
|
|
4
|
+
export const SESSION_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
export const SESSION_STATUSES = ["active", "closed"] as const;
|
|
7
|
+
export type SessionStatus = (typeof SESSION_STATUSES)[number];
|
|
8
|
+
|
|
9
|
+
export const SESSION_NOTE_KINDS = [
|
|
10
|
+
"decision",
|
|
11
|
+
"deviation",
|
|
12
|
+
"tradeoff",
|
|
13
|
+
"open_question",
|
|
14
|
+
"failure",
|
|
15
|
+
"test_failure",
|
|
16
|
+
"correction",
|
|
17
|
+
"review",
|
|
18
|
+
"observation",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export type SessionNoteKind = (typeof SESSION_NOTE_KINDS)[number];
|
|
22
|
+
|
|
23
|
+
export const CANDIDATE_STATUSES = ["proposed", "accepted", "rejected"] as const;
|
|
24
|
+
export type CandidateStatus = (typeof CANDIDATE_STATUSES)[number];
|
|
25
|
+
|
|
26
|
+
export interface SessionMetadata {
|
|
27
|
+
version: typeof SESSION_VERSION;
|
|
28
|
+
id: string;
|
|
29
|
+
goal: string;
|
|
30
|
+
status: SessionStatus;
|
|
31
|
+
created_at: string;
|
|
32
|
+
updated_at: string;
|
|
33
|
+
closed_at: string | null;
|
|
34
|
+
scope: Scope;
|
|
35
|
+
project_path: string;
|
|
36
|
+
module: string | null;
|
|
37
|
+
owner: string | null;
|
|
38
|
+
spec_ref: string | null;
|
|
39
|
+
notes_path: string;
|
|
40
|
+
recap_path: string;
|
|
41
|
+
candidate_traps_path: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SessionNote {
|
|
45
|
+
created_at: string;
|
|
46
|
+
kind: SessionNoteKind;
|
|
47
|
+
text: string;
|
|
48
|
+
related_files: string[];
|
|
49
|
+
source_ref: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type SessionNoteCounts = Partial<Record<SessionNoteKind, number>>;
|
|
53
|
+
|
|
54
|
+
export interface SessionIndexEntry {
|
|
55
|
+
id: string;
|
|
56
|
+
goal: string;
|
|
57
|
+
status: SessionStatus;
|
|
58
|
+
created_at: string;
|
|
59
|
+
closed_at: string | null;
|
|
60
|
+
module: string | null;
|
|
61
|
+
owner: string | null;
|
|
62
|
+
note_counts: SessionNoteCounts;
|
|
63
|
+
candidate_count: number;
|
|
64
|
+
accepted_count: number;
|
|
65
|
+
summary: string | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SessionIndexDocument {
|
|
69
|
+
version: typeof SESSION_VERSION;
|
|
70
|
+
sessions: SessionIndexEntry[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ActiveSessionDocument {
|
|
74
|
+
active_session_id: string | null;
|
|
75
|
+
updated_at: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CandidateQuality {
|
|
79
|
+
has_clear_trigger: boolean;
|
|
80
|
+
has_clear_mistake: boolean;
|
|
81
|
+
has_actionable_fix: boolean;
|
|
82
|
+
not_too_broad: boolean;
|
|
83
|
+
future_reuse_likely: boolean;
|
|
84
|
+
proper_scope: boolean;
|
|
85
|
+
evidence_count: number;
|
|
86
|
+
conflict_checked: boolean;
|
|
87
|
+
conflict_status: "none" | "possible" | "confirmed";
|
|
88
|
+
staleness_risk: "low" | "medium" | "high";
|
|
89
|
+
suggested_action: "accept" | "edit" | "supersede" | "archive_old" | "reject";
|
|
90
|
+
warnings: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface CandidateTrap {
|
|
94
|
+
id: string;
|
|
95
|
+
status: CandidateStatus;
|
|
96
|
+
quality_score: number;
|
|
97
|
+
quality: CandidateQuality;
|
|
98
|
+
trap: TrapInput;
|
|
99
|
+
evidence: TrapEvidenceInput[];
|
|
100
|
+
accepted_trap_id?: number;
|
|
101
|
+
accepted_scope?: Scope;
|
|
102
|
+
accepted_at?: string;
|
|
103
|
+
rejected_at?: string;
|
|
104
|
+
rejection_reason?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface CandidateTrapDocument {
|
|
108
|
+
version: typeof SESSION_VERSION;
|
|
109
|
+
session_id: string;
|
|
110
|
+
candidates: CandidateTrap[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function parseSessionNoteKind(value: string | undefined): SessionNoteKind {
|
|
114
|
+
const normalized = value ?? "observation";
|
|
115
|
+
if ((SESSION_NOTE_KINDS as readonly string[]).includes(normalized)) {
|
|
116
|
+
return normalized as SessionNoteKind;
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`Invalid session note kind: ${normalized}. Expected one of: ${SESSION_NOTE_KINDS.join(", ")}`);
|
|
119
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ if (args.length === 0) {
|
|
|
14
14
|
showHelp();
|
|
15
15
|
} else if (args[0] === "serve") {
|
|
16
16
|
import("./mcp/server").then((m) => m.start());
|
|
17
|
+
} else if (args[0] === "web") {
|
|
18
|
+
const { startWebServerFromArgs } = await import("./web/server");
|
|
19
|
+
await startWebServerFromArgs(args.slice(1));
|
|
17
20
|
} else if (args[0] === "init") {
|
|
18
21
|
const cwd = process.cwd();
|
|
19
22
|
if (findProjectRoot(cwd)) {
|
|
@@ -43,6 +46,8 @@ function showHelp(): void {
|
|
|
43
46
|
console.log(" archive_trap Archive a trap");
|
|
44
47
|
console.log(" supersede_trap Mark one trap as superseded by another");
|
|
45
48
|
console.log(" embed Generate embeddings for semantic search");
|
|
49
|
+
console.log(" session Record implementation notes and capture candidate traps");
|
|
50
|
+
console.log(" web Start local candidate review console");
|
|
46
51
|
console.log(" export Export traps as JSON");
|
|
47
52
|
console.log(" import <file.json> Import traps from JSON");
|
|
48
53
|
console.log(" stats Show statistics");
|
|
@@ -63,6 +68,9 @@ function showHelp(): void {
|
|
|
63
68
|
console.log(" --no-rerank Disable query-aware search reranking");
|
|
64
69
|
console.log(" --ranking-signals Include search ranking diagnostics in JSON cards");
|
|
65
70
|
console.log(" --batch-size <n> Embedding generation batch size");
|
|
71
|
+
console.log(" --project <path> Project path for web review console");
|
|
72
|
+
console.log(" --host <host> Host for web review console (default 127.0.0.1)");
|
|
73
|
+
console.log(" --port <n> Port for web review console (default 4737)");
|
|
66
74
|
console.log(" --json JSON output for search/show/list/stats/doctor; JSON input for add/edit");
|
|
67
75
|
console.log(" --output-json JSON output for add/edit when --json is used as input");
|
|
68
76
|
console.log(" --from-project-path <path> Source project path for scope repair/migration");
|