codetrap 0.1.4 → 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 +64 -2
- package/docs/installation.md +25 -3
- package/package.json +3 -1
- package/plugins/codetrap-agent/.codex-plugin/plugin.json +3 -2
- package/plugins/codetrap-agent/skills/codetrap-capture-external/SKILL.md +19 -0
- package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +2 -0
- package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +4 -0
- package/scripts/dogfood-eval.ts +53 -0
- package/skills/codetrap-add/SKILL.md +4 -0
- package/skills/codetrap-capture-external/SKILL.md +62 -0
- package/skills/codetrap-check/SKILL.md +3 -1
- package/skills/codetrap-search/SKILL.md +3 -1
- package/src/commands/workflow.ts +261 -2
- package/src/db/connection.ts +1 -1
- package/src/domain/session.ts +119 -0
- package/src/domain/trap.ts +1 -1
- package/src/index.ts +9 -0
- package/src/lib/command-requests.ts +156 -0
- package/src/lib/constants.ts +1 -1
- 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/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
|
}
|
|
@@ -226,7 +247,7 @@ function cmdAddTrapEvidence(args: string[], operations: TrapOperations): Command
|
|
|
226
247
|
const { opts, positionals } = parseArgs(args);
|
|
227
248
|
const id = parseId(
|
|
228
249
|
positionals[0],
|
|
229
|
-
"Usage: codetrap add_trap_evidence <id> --source_type manual|conversation|commit|issue|test_failure [--scope project|global] [--source_ref X] [--related_files a,b] [--note X]"
|
|
250
|
+
"Usage: codetrap add_trap_evidence <id> --source_type manual|conversation|commit|issue|test_failure|article [--scope project|global] [--source_ref X] [--related_files a,b] [--note X]"
|
|
230
251
|
);
|
|
231
252
|
if (typeof id !== "number") return id;
|
|
232
253
|
|
|
@@ -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/domain/trap.ts
CHANGED
|
@@ -246,7 +246,7 @@ export function trapEvidenceInputSchema(): JsonSchema {
|
|
|
246
246
|
enum: [...EVIDENCE_SOURCE_TYPES] as string[],
|
|
247
247
|
description: "Where this evidence came from",
|
|
248
248
|
},
|
|
249
|
-
source_ref: { type: "string", description: "Optional file path, commit SHA, issue URL, or transcript ID" },
|
|
249
|
+
source_ref: { type: "string", description: "Optional file path, commit SHA, issue/article URL, or transcript ID" },
|
|
250
250
|
observed_at: { type: "string", description: "When this was observed (ISO-like timestamp, optional)" },
|
|
251
251
|
related_files: {
|
|
252
252
|
type: "array",
|
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");
|
|
@@ -59,9 +64,13 @@ function showHelp(): void {
|
|
|
59
64
|
console.log(" --path <file> Filter/boost traps scoped to a file path");
|
|
60
65
|
console.log(" --module <name> Filter/boost traps scoped to a module");
|
|
61
66
|
console.log(" --owner <name> Filter/boost traps scoped to an owner/team");
|
|
67
|
+
console.log(" --source_type <type> Evidence source: manual, conversation, commit, issue, test_failure, article");
|
|
62
68
|
console.log(" --no-rerank Disable query-aware search reranking");
|
|
63
69
|
console.log(" --ranking-signals Include search ranking diagnostics in JSON cards");
|
|
64
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)");
|
|
65
74
|
console.log(" --json JSON output for search/show/list/stats/doctor; JSON input for add/edit");
|
|
66
75
|
console.log(" --output-json JSON output for add/edit when --json is used as input");
|
|
67
76
|
console.log(" --from-project-path <path> Source project path for scope repair/migration");
|
|
@@ -16,6 +16,58 @@ export type StatsRequest = {
|
|
|
16
16
|
scope?: string;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
export type SessionStartRequest = {
|
|
20
|
+
goal: string;
|
|
21
|
+
specRef?: string;
|
|
22
|
+
module?: string;
|
|
23
|
+
owner?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type SessionNoteRequest = {
|
|
27
|
+
kind?: string;
|
|
28
|
+
text: string;
|
|
29
|
+
relatedFiles?: string[];
|
|
30
|
+
sourceRef?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type SessionListRequest = {
|
|
34
|
+
status?: string;
|
|
35
|
+
limit?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type SessionCloseRequest = {
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
proposeTraps: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type SessionIdRequest = {
|
|
44
|
+
sessionId?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type SessionShowRequest = {
|
|
48
|
+
sessionId: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type SessionCandidateRequest = {
|
|
52
|
+
candidateId: string;
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type SessionAcceptRequest = SessionCandidateRequest & {
|
|
57
|
+
edit?: Record<string, unknown>;
|
|
58
|
+
supersedesId?: number;
|
|
59
|
+
acceptAnyway: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type SessionRejectRequest = SessionCandidateRequest & {
|
|
63
|
+
reason?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type SessionNoteStdin = {
|
|
67
|
+
isTTY: boolean;
|
|
68
|
+
read: () => string;
|
|
69
|
+
};
|
|
70
|
+
|
|
19
71
|
export function searchRequestFromArgs(query: string, args: RawArgs, defaults: SearchDefaults): SearchTrapsArgs {
|
|
20
72
|
return {
|
|
21
73
|
query,
|
|
@@ -70,6 +122,89 @@ export function evidenceRequestFromArgs(args: RawArgs): RawArgs {
|
|
|
70
122
|
};
|
|
71
123
|
}
|
|
72
124
|
|
|
125
|
+
export function sessionStartRequestFromArgs(positionals: string[], args: RawArgs): SessionStartRequest {
|
|
126
|
+
const goal = positionals.join(" ").trim();
|
|
127
|
+
if (!goal) throw new Error("Session goal is required.");
|
|
128
|
+
return {
|
|
129
|
+
goal,
|
|
130
|
+
specRef: stringOption(args, "spec"),
|
|
131
|
+
module: stringOption(args, "module"),
|
|
132
|
+
owner: stringOption(args, "owner"),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function sessionNoteRequestFromArgs(
|
|
137
|
+
positionals: string[],
|
|
138
|
+
args: RawArgs,
|
|
139
|
+
stdin: SessionNoteStdin
|
|
140
|
+
): SessionNoteRequest {
|
|
141
|
+
const hasText = args.text !== undefined;
|
|
142
|
+
const hasStdin = args.stdin !== undefined;
|
|
143
|
+
if (hasText && hasStdin) throw new Error("Choose either --text or --stdin, not both.");
|
|
144
|
+
if (hasStdin && stdin.isTTY) throw new Error("--stdin requires piped input.");
|
|
145
|
+
|
|
146
|
+
const text = hasStdin
|
|
147
|
+
? stdin.read().trim()
|
|
148
|
+
: stringOption(args, "text") ?? positionals.join(" ").trim();
|
|
149
|
+
if (!text) throw new Error("Session note text is required.");
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
kind: stringOption(args, "kind"),
|
|
153
|
+
text,
|
|
154
|
+
relatedFiles: csvOrArrayOption(args, "related_files", "related-files"),
|
|
155
|
+
sourceRef: stringOption(args, "source_ref", "source-ref"),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function sessionListRequestFromArgs(args: RawArgs): SessionListRequest {
|
|
160
|
+
return {
|
|
161
|
+
status: stringOption(args, "status") ?? "all",
|
|
162
|
+
limit: optionalIntOption(args, "limit"),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function sessionCloseRequestFromArgs(positionals: string[], args: RawArgs): SessionCloseRequest {
|
|
167
|
+
return {
|
|
168
|
+
sessionId: positionals[0],
|
|
169
|
+
proposeTraps: flagPresent(args, "propose-traps"),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function sessionIdRequestFromArgs(positionals: string[]): SessionIdRequest {
|
|
174
|
+
return {
|
|
175
|
+
sessionId: positionals[0],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function sessionShowRequestFromArgs(positionals: string[]): SessionShowRequest {
|
|
180
|
+
return {
|
|
181
|
+
sessionId: requiredPositional(positionals, 0, "session-id"),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function sessionCandidateRequestFromArgs(positionals: string[], args: RawArgs): SessionCandidateRequest {
|
|
186
|
+
return {
|
|
187
|
+
candidateId: requiredPositional(positionals, 0, "candidate-id"),
|
|
188
|
+
sessionId: stringOption(args, "session"),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function sessionAcceptRequestFromArgs(positionals: string[], args: RawArgs): SessionAcceptRequest {
|
|
193
|
+
return {
|
|
194
|
+
...sessionCandidateRequestFromArgs(positionals, args),
|
|
195
|
+
edit: jsonObjectOption(args, "edit-json"),
|
|
196
|
+
supersedesId: optionalIntOption(args, "supersedes"),
|
|
197
|
+
acceptAnyway: flagPresent(args, "accept-anyway"),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function sessionRejectRequestFromArgs(positionals: string[], args: RawArgs): SessionRejectRequest {
|
|
202
|
+
return {
|
|
203
|
+
...sessionCandidateRequestFromArgs(positionals, args),
|
|
204
|
+
reason: stringOption(args, "reason"),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
73
208
|
function stringOption(args: RawArgs, ...keys: string[]): string | undefined {
|
|
74
209
|
for (const key of keys) {
|
|
75
210
|
const value = args[key];
|
|
@@ -131,3 +266,24 @@ function csvOrArrayOption(args: RawArgs, ...keys: string[]): string[] | undefine
|
|
|
131
266
|
}
|
|
132
267
|
return undefined;
|
|
133
268
|
}
|
|
269
|
+
|
|
270
|
+
function jsonObjectOption(args: RawArgs, key: string): Record<string, unknown> | undefined {
|
|
271
|
+
const value = stringOption(args, key);
|
|
272
|
+
if (value === undefined) return undefined;
|
|
273
|
+
try {
|
|
274
|
+
const parsed = JSON.parse(value) as unknown;
|
|
275
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
276
|
+
throw new Error(`${key} must be a JSON object.`);
|
|
277
|
+
}
|
|
278
|
+
return parsed as Record<string, unknown>;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
281
|
+
throw new Error(`Invalid --${key}: ${message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function requiredPositional(positionals: string[], index: number, name: string): string {
|
|
286
|
+
const value = positionals[index];
|
|
287
|
+
if (!value) throw new Error(`${name} is required.`);
|
|
288
|
+
return value;
|
|
289
|
+
}
|
package/src/lib/constants.ts
CHANGED
|
@@ -37,7 +37,7 @@ export type Scope = (typeof SCOPES)[number];
|
|
|
37
37
|
export const TRAP_STATUSES = ["active", "superseded", "archived"] as const;
|
|
38
38
|
export type TrapStatus = (typeof TRAP_STATUSES)[number];
|
|
39
39
|
|
|
40
|
-
export const EVIDENCE_SOURCE_TYPES = ["manual", "conversation", "commit", "issue", "test_failure"] as const;
|
|
40
|
+
export const EVIDENCE_SOURCE_TYPES = ["manual", "conversation", "commit", "issue", "test_failure", "article"] as const;
|
|
41
41
|
export type EvidenceSourceType = (typeof EVIDENCE_SOURCE_TYPES)[number];
|
|
42
42
|
|
|
43
43
|
export const SEARCH_MODES = ["fts", "semantic", "hybrid"] as const;
|