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
@@ -1,14 +1,31 @@
1
1
  import type { CandidateTrap } from "../domain/session";
2
- import { buildTrapInput } from "../domain/trap";
2
+ import type { Scope } from "./constants";
3
3
  import type { TrapOperations } from "./trap-operations";
4
+ import {
5
+ capturedCandidateDraft,
6
+ capturedTrapInput,
7
+ candidateWithTrapEdits,
8
+ captureGoal,
9
+ } from "./session-capture";
10
+ import { candidateAcceptedScope } from "./session-candidate-scope";
4
11
  import { findCandidateConflicts, type CandidateConflict } from "./session-conflicts";
12
+ import {
13
+ projectCandidateReviewSummary,
14
+ sessionCandidateReviewSummary,
15
+ sessionIndexEntryWithReview,
16
+ type ProjectCandidateReviewSummary,
17
+ } from "./session-review";
5
18
  import type {
6
19
  AcceptCandidateResult,
7
20
  AddSessionNoteArgs,
8
21
  CloseSessionResult,
22
+ DeleteSessionResult,
23
+ PruneSessionsResult,
24
+ RemoveSessionCandidatesResult,
9
25
  SessionStore,
10
26
  StartSessionArgs,
11
27
  } from "./session-store";
28
+ import { uniqueStrings } from "./string-list";
12
29
 
13
30
  export type SessionAcceptRequest = {
14
31
  candidateId: string;
@@ -30,6 +47,21 @@ export type SessionRejectRequest = {
30
47
  reason?: string | null;
31
48
  };
32
49
 
50
+ export type SessionPruneRequest = {
51
+ olderThanDays: number;
52
+ apply: boolean;
53
+ now?: Date;
54
+ };
55
+
56
+ export type SessionCaptureRequest = {
57
+ trap: Record<string, unknown>;
58
+ goal?: string;
59
+ kind?: string;
60
+ relatedFiles?: string[];
61
+ sourceRef?: string;
62
+ evidenceNote?: string;
63
+ };
64
+
33
65
  export type SessionConflictResult = {
34
66
  success: false;
35
67
  session_id: string;
@@ -43,6 +75,18 @@ export type SessionAcceptSuccess = AcceptCandidateResult & {
43
75
 
44
76
  export type SessionAcceptResult = SessionAcceptSuccess | SessionConflictResult;
45
77
 
78
+ export type SessionCaptureResult = {
79
+ success: true;
80
+ session: ReturnType<SessionStore["getSession"]>;
81
+ candidate: CandidateTrap;
82
+ candidate_count: number;
83
+ candidates_path: string;
84
+ created_session: boolean;
85
+ closed_session: boolean;
86
+ duplicate: boolean;
87
+ recap_path: string | null;
88
+ };
89
+
46
90
  export class SessionOperations {
47
91
  constructor(
48
92
  private readonly sessions: SessionStore,
@@ -58,11 +102,23 @@ export class SessionOperations {
58
102
  }
59
103
 
60
104
  status() {
61
- return this.sessions.status();
105
+ return {
106
+ ...this.sessions.status(),
107
+ candidate_review: this.candidateReviewSummary(),
108
+ };
62
109
  }
63
110
 
64
111
  listSessions(args: { status?: string; limit?: number } = {}) {
65
- return this.sessions.listSessions(args);
112
+ return this.sessions.listSessions(args).map((session) =>
113
+ sessionIndexEntryWithReview(session, this.sessions.candidateDocument(session.id).candidates)
114
+ );
115
+ }
116
+
117
+ candidateReviewSummary(): ProjectCandidateReviewSummary {
118
+ const sessions = this.sessions.listSessions({ status: "all" }).map((session) =>
119
+ sessionCandidateReviewSummary(session, this.sessions.candidateDocument(session.id).candidates)
120
+ );
121
+ return projectCandidateReviewSummary(sessions);
66
122
  }
67
123
 
68
124
  showSession(id: string) {
@@ -77,6 +133,39 @@ export class SessionOperations {
77
133
  return this.sessions.closeSession(id, proposeTraps);
78
134
  }
79
135
 
136
+ captureCandidate(request: SessionCaptureRequest): SessionCaptureResult {
137
+ const trap = capturedTrapInput(request.trap);
138
+ const active = this.sessions.status().session;
139
+ const createdSession = active === null;
140
+ const session = active ?? this.sessions.startSession({
141
+ goal: captureGoal(request.goal, trap.title),
142
+ module: trap.module,
143
+ owner: trap.owner,
144
+ });
145
+ const captured = this.sessions.addCandidate({
146
+ sessionId: session.id,
147
+ draft: capturedCandidateDraft(session, {
148
+ trap,
149
+ kind: request.kind,
150
+ relatedFiles: request.relatedFiles,
151
+ sourceRef: request.sourceRef,
152
+ evidenceNote: request.evidenceNote,
153
+ }),
154
+ });
155
+ const closed = createdSession ? this.sessions.closeSession(session.id, false) : null;
156
+ return {
157
+ success: true,
158
+ session: closed?.session ?? captured.session,
159
+ candidate: captured.candidate,
160
+ candidate_count: this.sessions.candidateDocument(session.id).candidates.length,
161
+ candidates_path: captured.candidates_path,
162
+ created_session: createdSession,
163
+ closed_session: closed !== null,
164
+ duplicate: captured.duplicate,
165
+ recap_path: closed?.recap_path ?? null,
166
+ };
167
+ }
168
+
80
169
  candidateDocument(id?: string) {
81
170
  return this.sessions.candidateDocument(id);
82
171
  }
@@ -155,60 +244,31 @@ export class SessionOperations {
155
244
  reason: request.reason,
156
245
  });
157
246
  }
158
- }
159
-
160
- function candidateWithTrapEdits(candidate: CandidateTrap, edit: Record<string, unknown> | undefined): CandidateTrap {
161
- return {
162
- ...candidate,
163
- trap: normalizeCandidateTrap({
164
- ...candidate.trap,
165
- ...trapEdits(edit),
166
- }) as CandidateTrap["trap"],
167
- };
168
- }
169
-
170
- function normalizeCandidateTrap(args: Record<string, unknown>) {
171
- return buildTrapInput({
172
- ...args,
173
- tags: stringArray(args.tags),
174
- path_globs: stringArray(args.path_globs),
175
- module: optionalText(args.module),
176
- owner: optionalText(args.owner),
177
- before_code: optionalText(args.before_code),
178
- after_code: optionalText(args.after_code),
179
- });
180
- }
181
247
 
182
- function stringArray(value: unknown): string[] | undefined {
183
- if (Array.isArray(value)) return value.map(String).map((item) => item.trim()).filter(Boolean);
184
- if (typeof value === "string") {
185
- const values = value.split(",").map((item) => item.trim()).filter(Boolean);
186
- return values.length > 0 ? values : undefined;
248
+ deleteSession(sessionId: string): DeleteSessionResult {
249
+ return this.sessions.deleteSession(sessionId);
187
250
  }
188
- return undefined;
189
- }
190
-
191
- function optionalText(value: unknown): string | null | undefined {
192
- if (value === undefined) return undefined;
193
- if (value === null) return null;
194
- const text = String(value).trim();
195
- return text ? text : null;
196
- }
197
251
 
198
- function trapEdits(edit: Record<string, unknown> | undefined): Record<string, unknown> {
199
- if (!edit) return {};
200
- const nested = edit.trap;
201
- return isRecord(nested) ? nested : edit;
202
- }
252
+ pruneSessions(request: SessionPruneRequest): PruneSessionsResult {
253
+ const now = request.now ?? new Date();
254
+ const cutoff = new Date(now.getTime() - request.olderThanDays * 24 * 60 * 60 * 1000);
255
+ return this.sessions.pruneSessions({ cutoff, dryRun: !request.apply });
256
+ }
203
257
 
204
- function isRecord(value: unknown): value is Record<string, unknown> {
205
- return typeof value === "object" && value !== null && !Array.isArray(value);
258
+ cleanupDeletedTrapCandidates(sessionId?: string): RemoveSessionCandidatesResult {
259
+ const document = this.sessions.candidateDocument(sessionId);
260
+ const missingCandidateIds = document.candidates
261
+ .filter((candidate) => candidate.status === "accepted")
262
+ .filter((candidate) => {
263
+ const trapId = candidate.accepted_trap_id;
264
+ if (trapId === undefined) return true;
265
+ return !this.traps.getTrapDetails(trapId, candidateAcceptedScope(candidate));
266
+ })
267
+ .map((candidate) => candidate.id);
268
+ return this.sessions.removeCandidates(sessionId, missingCandidateIds);
269
+ }
206
270
  }
207
271
 
208
272
  function candidateRelatedFiles(candidate: CandidateTrap): string[] {
209
273
  return uniqueStrings(candidate.evidence.flatMap((evidence) => evidence.related_files ?? []));
210
274
  }
211
-
212
- function uniqueStrings(values: string[]): string[] {
213
- return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
214
- }
@@ -0,0 +1,327 @@
1
+ import type { CandidateTrap, SessionIndexEntry, SessionMetadata } from "../domain/session";
2
+ import type { Scope } from "./constants";
3
+ import { candidateAcceptedScope } from "./session-candidate-scope";
4
+ import {
5
+ CANDIDATES_FILE,
6
+ NOTES_FILE,
7
+ RECAP_FILE,
8
+ sessionRelativeDir,
9
+ sessionRelativeFile,
10
+ } from "./session-codec";
11
+ import type { CandidateConflict } from "./session-conflicts";
12
+ import type { TrapOperations } from "./trap-operations";
13
+
14
+ export type SessionPayload = SessionMetadata & {
15
+ session_dir: string;
16
+ notes_path: string;
17
+ recap_path: string;
18
+ candidate_traps_path: string;
19
+ };
20
+
21
+ export type SessionConflictPayload = {
22
+ success: false;
23
+ error: string;
24
+ session_id: string;
25
+ candidate_id: string;
26
+ possible_conflicts: CandidateConflict[];
27
+ };
28
+
29
+ export type SessionCliConflictPayload = SessionConflictPayload & {
30
+ next_actions: string[];
31
+ };
32
+
33
+ export type SessionAcceptPayload = {
34
+ success: true;
35
+ session_id: string;
36
+ candidate_id: string;
37
+ status: CandidateTrap["status"];
38
+ trap_id: number;
39
+ scope: string;
40
+ evidence_id: number | null;
41
+ superseded_id: number | null;
42
+ };
43
+
44
+ export type SessionRejectPayload = {
45
+ success: true;
46
+ session_id: string;
47
+ candidate_id: string;
48
+ status: CandidateTrap["status"];
49
+ reason: string | null;
50
+ };
51
+
52
+ export type SessionCleanupPayload = {
53
+ success: true;
54
+ session_id: string;
55
+ removed_count: number;
56
+ removed_candidate_ids: string[];
57
+ };
58
+
59
+ export type SessionCandidateReview =
60
+ | { status: "pending"; label: string }
61
+ | {
62
+ status: "accepted";
63
+ label: string;
64
+ trap_id: number;
65
+ scope: string;
66
+ trap_present: true;
67
+ trap_status: string;
68
+ trap_title: string;
69
+ }
70
+ | {
71
+ status: "accepted_missing";
72
+ label: string;
73
+ trap_id?: number;
74
+ scope?: string;
75
+ trap_present: false;
76
+ }
77
+ | {
78
+ status: "rejected";
79
+ label: string;
80
+ rejected_at?: string;
81
+ rejection_reason?: string;
82
+ };
83
+
84
+ export type ReviewedSessionCandidate = CandidateTrap & { review: SessionCandidateReview };
85
+
86
+ export type CandidateReviewCounts = {
87
+ candidate_count: number;
88
+ pending_count: number;
89
+ reviewed_count: number;
90
+ accepted_count: number;
91
+ rejected_count: number;
92
+ high_quality_pending_count: number;
93
+ needs_edit_count: number;
94
+ };
95
+
96
+ export type SessionCandidateReviewSummary = CandidateReviewCounts & {
97
+ session_id: string;
98
+ goal: string;
99
+ status: SessionIndexEntry["status"];
100
+ };
101
+
102
+ export type ProjectCandidateReviewSummary = CandidateReviewCounts & {
103
+ session_count: number;
104
+ pending_session_count: number;
105
+ next_session_id: string | null;
106
+ };
107
+
108
+ export type SessionIndexEntryWithReview = SessionIndexEntry & CandidateReviewCounts & {
109
+ candidate_review: SessionCandidateReviewSummary;
110
+ };
111
+
112
+ export function sessionPayload(session: SessionMetadata): SessionPayload {
113
+ return {
114
+ ...session,
115
+ session_dir: sessionRelativeDir(session.id),
116
+ notes_path: sessionRelativeFile(session.id, NOTES_FILE),
117
+ recap_path: sessionRelativeFile(session.id, RECAP_FILE),
118
+ candidate_traps_path: sessionRelativeFile(session.id, CANDIDATES_FILE),
119
+ };
120
+ }
121
+
122
+ export function sessionConflictPayload(result: {
123
+ session_id: string;
124
+ candidate_id: string;
125
+ possible_conflicts: CandidateConflict[];
126
+ }): SessionConflictPayload {
127
+ return {
128
+ success: false,
129
+ error: "Possible active trap conflict found.",
130
+ session_id: result.session_id,
131
+ candidate_id: result.candidate_id,
132
+ possible_conflicts: result.possible_conflicts,
133
+ };
134
+ }
135
+
136
+ export function sessionCliConflictPayload(payload: SessionConflictPayload): SessionCliConflictPayload {
137
+ return {
138
+ ...payload,
139
+ next_actions: [
140
+ `codetrap session accept ${payload.candidate_id} --session ${payload.session_id} --accept-anyway`,
141
+ `codetrap session accept ${payload.candidate_id} --session ${payload.session_id} --supersedes <trap-id>`,
142
+ `codetrap session reject ${payload.candidate_id} --session ${payload.session_id} --reason <reason>`,
143
+ ],
144
+ };
145
+ }
146
+
147
+ export function sessionConflictText(payload: SessionConflictPayload): string {
148
+ return [
149
+ "Possible active trap conflict found:",
150
+ ...payload.possible_conflicts.map((conflict) => [
151
+ `#${conflict.trap_id} ${conflict.title}`,
152
+ ` reason: ${conflict.reason}`,
153
+ ` fix: ${conflict.fix}`,
154
+ ].join("\n")),
155
+ "",
156
+ "Use --accept-anyway to save as a new trap, or --supersedes <trap-id> to preserve lifecycle history.",
157
+ ].join("\n");
158
+ }
159
+
160
+ export function sessionAcceptPayload(accepted: {
161
+ session: SessionMetadata;
162
+ candidate: CandidateTrap;
163
+ trap_id: number;
164
+ scope: string;
165
+ evidence_id: number | null;
166
+ superseded_id: number | null;
167
+ }): SessionAcceptPayload {
168
+ return {
169
+ success: true,
170
+ session_id: accepted.session.id,
171
+ candidate_id: accepted.candidate.id,
172
+ status: accepted.candidate.status,
173
+ trap_id: accepted.trap_id,
174
+ scope: accepted.scope,
175
+ evidence_id: accepted.evidence_id,
176
+ superseded_id: accepted.superseded_id,
177
+ };
178
+ }
179
+
180
+ export function sessionRejectPayload(rejected: {
181
+ session: SessionMetadata;
182
+ candidate: CandidateTrap;
183
+ }): SessionRejectPayload {
184
+ return {
185
+ success: true,
186
+ session_id: rejected.session.id,
187
+ candidate_id: rejected.candidate.id,
188
+ status: rejected.candidate.status,
189
+ reason: rejected.candidate.rejection_reason ?? null,
190
+ };
191
+ }
192
+
193
+ export function sessionCleanupPayload(result: {
194
+ session: SessionMetadata;
195
+ removed_count: number;
196
+ removed_candidate_ids: string[];
197
+ }): SessionCleanupPayload {
198
+ return {
199
+ success: true,
200
+ session_id: result.session.id,
201
+ removed_count: result.removed_count,
202
+ removed_candidate_ids: result.removed_candidate_ids,
203
+ };
204
+ }
205
+
206
+ export function reviewedSessionCandidates(
207
+ candidates: CandidateTrap[],
208
+ traps: TrapOperations
209
+ ): ReviewedSessionCandidate[] {
210
+ return candidates.map((candidate) => ({
211
+ ...candidate,
212
+ review: sessionCandidateReview(candidate, traps),
213
+ }));
214
+ }
215
+
216
+ export function candidateReviewCounts(candidates: CandidateTrap[]): CandidateReviewCounts {
217
+ const pending = candidates.filter((candidate) => candidate.status === "proposed");
218
+ const accepted = candidates.filter((candidate) => candidate.status === "accepted");
219
+ const rejected = candidates.filter((candidate) => candidate.status === "rejected");
220
+ return {
221
+ candidate_count: candidates.length,
222
+ pending_count: pending.length,
223
+ reviewed_count: accepted.length + rejected.length,
224
+ accepted_count: accepted.length,
225
+ rejected_count: rejected.length,
226
+ high_quality_pending_count: pending.filter((candidate) =>
227
+ candidate.quality_score >= 0.8 && candidate.quality.suggested_action === "accept"
228
+ ).length,
229
+ needs_edit_count: pending.filter((candidate) => candidate.quality.suggested_action === "edit").length,
230
+ };
231
+ }
232
+
233
+ export function sessionCandidateReviewSummary(
234
+ session: Pick<SessionIndexEntry, "id" | "goal" | "status">,
235
+ candidates: CandidateTrap[]
236
+ ): SessionCandidateReviewSummary {
237
+ return {
238
+ session_id: session.id,
239
+ goal: session.goal,
240
+ status: session.status,
241
+ ...candidateReviewCounts(candidates),
242
+ };
243
+ }
244
+
245
+ export function sessionIndexEntryWithReview(
246
+ session: SessionIndexEntry,
247
+ candidates: CandidateTrap[]
248
+ ): SessionIndexEntryWithReview {
249
+ const review = sessionCandidateReviewSummary(session, candidates);
250
+ return {
251
+ ...session,
252
+ ...review,
253
+ candidate_review: review,
254
+ };
255
+ }
256
+
257
+ export function projectCandidateReviewSummary(
258
+ sessions: SessionCandidateReviewSummary[]
259
+ ): ProjectCandidateReviewSummary {
260
+ const empty = candidateReviewCounts([]);
261
+ const totals = sessions.reduce<CandidateReviewCounts>((acc, session) => ({
262
+ candidate_count: acc.candidate_count + session.candidate_count,
263
+ pending_count: acc.pending_count + session.pending_count,
264
+ reviewed_count: acc.reviewed_count + session.reviewed_count,
265
+ accepted_count: acc.accepted_count + session.accepted_count,
266
+ rejected_count: acc.rejected_count + session.rejected_count,
267
+ high_quality_pending_count: acc.high_quality_pending_count + session.high_quality_pending_count,
268
+ needs_edit_count: acc.needs_edit_count + session.needs_edit_count,
269
+ }), empty);
270
+ const nextPending = sessions.find((session) => session.pending_count > 0);
271
+ return {
272
+ ...totals,
273
+ session_count: sessions.length,
274
+ pending_session_count: sessions.filter((session) => session.pending_count > 0).length,
275
+ next_session_id: nextPending?.session_id ?? null,
276
+ };
277
+ }
278
+
279
+ export function sessionCandidateReview(
280
+ candidate: CandidateTrap,
281
+ traps: TrapOperations
282
+ ): SessionCandidateReview {
283
+ if (candidate.status === "proposed") {
284
+ return { status: "pending", label: "pending review" };
285
+ }
286
+
287
+ if (candidate.status === "rejected") {
288
+ return {
289
+ status: "rejected",
290
+ label: "rejected",
291
+ rejected_at: candidate.rejected_at,
292
+ rejection_reason: candidate.rejection_reason,
293
+ };
294
+ }
295
+
296
+ const trapId = candidate.accepted_trap_id;
297
+ const scope = candidateAcceptedScope(candidate);
298
+ if (trapId === undefined) {
299
+ return {
300
+ status: "accepted_missing",
301
+ label: "accepted -> trap link missing",
302
+ scope,
303
+ trap_present: false,
304
+ };
305
+ }
306
+
307
+ const details = traps.getTrapDetails(trapId, scope);
308
+ if (!details) {
309
+ return {
310
+ status: "accepted_missing",
311
+ label: `accepted -> trap #${trapId} deleted`,
312
+ trap_id: trapId,
313
+ scope,
314
+ trap_present: false,
315
+ };
316
+ }
317
+
318
+ return {
319
+ status: "accepted",
320
+ label: `accepted -> trap #${trapId}`,
321
+ trap_id: trapId,
322
+ scope: details.scope,
323
+ trap_present: true,
324
+ trap_status: details.trap.status,
325
+ trap_title: details.trap.title,
326
+ };
327
+ }