codetrap 0.1.7 → 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 (58) hide show
  1. package/README.md +151 -52
  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 +144 -68
  15. package/src/db/embedding-queries.ts +230 -48
  16. package/src/db/queries.ts +0 -25
  17. package/src/db/repository.ts +32 -21
  18. package/src/db/schema.ts +80 -0
  19. package/src/index.ts +28 -3
  20. package/src/lib/command-requests.ts +112 -1
  21. package/src/lib/config.ts +57 -7
  22. package/src/lib/constants.ts +1 -1
  23. package/src/lib/doctor.ts +42 -12
  24. package/src/lib/embedder.ts +118 -3
  25. package/src/lib/embedding-health.ts +3 -1
  26. package/src/lib/embedding-job.ts +3 -0
  27. package/src/lib/embedding-management.ts +65 -0
  28. package/src/lib/embedding-runtime.ts +177 -0
  29. package/src/lib/output-json.ts +0 -2
  30. package/src/lib/scope-context.ts +12 -6
  31. package/src/lib/scope-migration.ts +2 -1
  32. package/src/lib/scope.ts +0 -2
  33. package/src/lib/search-eval.ts +38 -18
  34. package/src/lib/search-policy-sweep.ts +563 -0
  35. package/src/lib/search-policy.ts +0 -4
  36. package/src/lib/search-service.ts +14 -15
  37. package/src/lib/session-candidate-document.ts +175 -0
  38. package/src/lib/session-candidate-scope.ts +6 -0
  39. package/src/lib/session-capture.ts +298 -32
  40. package/src/lib/session-codec.ts +1 -8
  41. package/src/lib/session-operations.ts +83 -60
  42. package/src/lib/session-review.ts +327 -0
  43. package/src/lib/session-store.ts +87 -73
  44. package/src/lib/store.ts +74 -10
  45. package/src/lib/string-list.ts +3 -0
  46. package/src/lib/text-lines.ts +7 -0
  47. package/src/lib/trap-search-document.ts +2 -1
  48. package/src/lib/value-types.ts +3 -0
  49. package/src/web/client-review.ts +171 -0
  50. package/src/web/client-script.ts +426 -51
  51. package/src/web/client-shell.ts +414 -0
  52. package/src/web/client-text.ts +112 -0
  53. package/src/web/project-registry.ts +3 -5
  54. package/src/web/server.ts +117 -103
  55. package/src/web/static.ts +364 -19
  56. package/skills/codetrap-capture-external/SKILL.md +0 -62
  57. package/skills/codetrap-check/SKILL.md +0 -69
  58. package/src/lib/embedding-index.ts +0 -53
@@ -1,8 +1,20 @@
1
1
  import type { CandidateTrap } from "../domain/session";
2
- import { buildTrapInput } from "../domain/trap";
3
2
  import type { Scope } from "./constants";
4
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";
5
11
  import { findCandidateConflicts, type CandidateConflict } from "./session-conflicts";
12
+ import {
13
+ projectCandidateReviewSummary,
14
+ sessionCandidateReviewSummary,
15
+ sessionIndexEntryWithReview,
16
+ type ProjectCandidateReviewSummary,
17
+ } from "./session-review";
6
18
  import type {
7
19
  AcceptCandidateResult,
8
20
  AddSessionNoteArgs,
@@ -13,6 +25,7 @@ import type {
13
25
  SessionStore,
14
26
  StartSessionArgs,
15
27
  } from "./session-store";
28
+ import { uniqueStrings } from "./string-list";
16
29
 
17
30
  export type SessionAcceptRequest = {
18
31
  candidateId: string;
@@ -40,6 +53,15 @@ export type SessionPruneRequest = {
40
53
  now?: Date;
41
54
  };
42
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
+
43
65
  export type SessionConflictResult = {
44
66
  success: false;
45
67
  session_id: string;
@@ -53,6 +75,18 @@ export type SessionAcceptSuccess = AcceptCandidateResult & {
53
75
 
54
76
  export type SessionAcceptResult = SessionAcceptSuccess | SessionConflictResult;
55
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
+
56
90
  export class SessionOperations {
57
91
  constructor(
58
92
  private readonly sessions: SessionStore,
@@ -68,11 +102,23 @@ export class SessionOperations {
68
102
  }
69
103
 
70
104
  status() {
71
- return this.sessions.status();
105
+ return {
106
+ ...this.sessions.status(),
107
+ candidate_review: this.candidateReviewSummary(),
108
+ };
72
109
  }
73
110
 
74
111
  listSessions(args: { status?: string; limit?: number } = {}) {
75
- 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);
76
122
  }
77
123
 
78
124
  showSession(id: string) {
@@ -87,6 +133,39 @@ export class SessionOperations {
87
133
  return this.sessions.closeSession(id, proposeTraps);
88
134
  }
89
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
+
90
169
  candidateDocument(id?: string) {
91
170
  return this.sessions.candidateDocument(id);
92
171
  }
@@ -183,69 +262,13 @@ export class SessionOperations {
183
262
  .filter((candidate) => {
184
263
  const trapId = candidate.accepted_trap_id;
185
264
  if (trapId === undefined) return true;
186
- return !this.traps.getTrapDetails(trapId, acceptedScope(candidate));
265
+ return !this.traps.getTrapDetails(trapId, candidateAcceptedScope(candidate));
187
266
  })
188
267
  .map((candidate) => candidate.id);
189
268
  return this.sessions.removeCandidates(sessionId, missingCandidateIds);
190
269
  }
191
270
  }
192
271
 
193
- function candidateWithTrapEdits(candidate: CandidateTrap, edit: Record<string, unknown> | undefined): CandidateTrap {
194
- return {
195
- ...candidate,
196
- trap: normalizeCandidateTrap({
197
- ...candidate.trap,
198
- ...trapEdits(edit),
199
- }) as CandidateTrap["trap"],
200
- };
201
- }
202
-
203
- function normalizeCandidateTrap(args: Record<string, unknown>) {
204
- return buildTrapInput({
205
- ...args,
206
- tags: stringArray(args.tags),
207
- path_globs: stringArray(args.path_globs),
208
- module: optionalText(args.module),
209
- owner: optionalText(args.owner),
210
- before_code: optionalText(args.before_code),
211
- after_code: optionalText(args.after_code),
212
- });
213
- }
214
-
215
- function stringArray(value: unknown): string[] | undefined {
216
- if (Array.isArray(value)) return value.map(String).map((item) => item.trim()).filter(Boolean);
217
- if (typeof value === "string") {
218
- const values = value.split(",").map((item) => item.trim()).filter(Boolean);
219
- return values.length > 0 ? values : undefined;
220
- }
221
- return undefined;
222
- }
223
-
224
- function optionalText(value: unknown): string | null | undefined {
225
- if (value === undefined) return undefined;
226
- if (value === null) return null;
227
- const text = String(value).trim();
228
- return text ? text : null;
229
- }
230
-
231
- function trapEdits(edit: Record<string, unknown> | undefined): Record<string, unknown> {
232
- if (!edit) return {};
233
- const nested = edit.trap;
234
- return isRecord(nested) ? nested : edit;
235
- }
236
-
237
- function isRecord(value: unknown): value is Record<string, unknown> {
238
- return typeof value === "object" && value !== null && !Array.isArray(value);
239
- }
240
-
241
272
  function candidateRelatedFiles(candidate: CandidateTrap): string[] {
242
273
  return uniqueStrings(candidate.evidence.flatMap((evidence) => evidence.related_files ?? []));
243
274
  }
244
-
245
- function acceptedScope(candidate: CandidateTrap): Scope {
246
- return candidate.accepted_scope ?? (candidate.trap.scope === "global" ? "global" : "project");
247
- }
248
-
249
- function uniqueStrings(values: string[]): string[] {
250
- return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
251
- }
@@ -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
+ }