aidimag 1.0.0
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/LICENSE +102 -0
- package/README.md +113 -0
- package/dist/capture/bootstrap.d.ts +25 -0
- package/dist/capture/bootstrap.js +188 -0
- package/dist/capture/commit-miner.d.ts +59 -0
- package/dist/capture/commit-miner.js +381 -0
- package/dist/capture/harvest.d.ts +50 -0
- package/dist/capture/harvest.js +207 -0
- package/dist/capture/pr-miner.d.ts +57 -0
- package/dist/capture/pr-miner.js +185 -0
- package/dist/capture/session-briefing.d.ts +36 -0
- package/dist/capture/session-briefing.js +150 -0
- package/dist/capture/session-extraction.d.ts +23 -0
- package/dist/capture/session-extraction.js +54 -0
- package/dist/capture/triage.d.ts +30 -0
- package/dist/capture/triage.js +108 -0
- package/dist/cli/commands/capture.d.ts +5 -0
- package/dist/cli/commands/capture.js +357 -0
- package/dist/cli/commands/hosts.d.ts +5 -0
- package/dist/cli/commands/hosts.js +98 -0
- package/dist/cli/commands/knowledge.d.ts +5 -0
- package/dist/cli/commands/knowledge.js +121 -0
- package/dist/cli/commands/memory.d.ts +6 -0
- package/dist/cli/commands/memory.js +392 -0
- package/dist/cli/commands/sync.d.ts +5 -0
- package/dist/cli/commands/sync.js +307 -0
- package/dist/cli/commands/tickets.d.ts +6 -0
- package/dist/cli/commands/tickets.js +328 -0
- package/dist/cli/commands/verify.d.ts +5 -0
- package/dist/cli/commands/verify.js +136 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.js +46 -0
- package/dist/cli/shared.d.ts +37 -0
- package/dist/cli/shared.js +175 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.js +51 -0
- package/dist/context/generate.d.ts +24 -0
- package/dist/context/generate.js +115 -0
- package/dist/critique/critique.d.ts +40 -0
- package/dist/critique/critique.js +110 -0
- package/dist/db/schema.d.ts +28 -0
- package/dist/db/schema.js +269 -0
- package/dist/db/store.d.ts +182 -0
- package/dist/db/store.js +906 -0
- package/dist/debug.d.ts +14 -0
- package/dist/debug.js +25 -0
- package/dist/embeddings/provider.d.ts +16 -0
- package/dist/embeddings/provider.js +100 -0
- package/dist/embeddings/search.d.ts +22 -0
- package/dist/embeddings/search.js +95 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/knowledge/chunk.d.ts +9 -0
- package/dist/knowledge/chunk.js +78 -0
- package/dist/knowledge/extract.d.ts +34 -0
- package/dist/knowledge/extract.js +113 -0
- package/dist/knowledge/ingest.d.ts +79 -0
- package/dist/knowledge/ingest.js +305 -0
- package/dist/knowledge/llm.d.ts +21 -0
- package/dist/knowledge/llm.js +110 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +532 -0
- package/dist/security/evidence.d.ts +11 -0
- package/dist/security/evidence.js +15 -0
- package/dist/security/seal.d.ts +3 -0
- package/dist/security/seal.js +28 -0
- package/dist/security/sync-push.d.ts +2 -0
- package/dist/security/sync-push.js +37 -0
- package/dist/security/url.d.ts +6 -0
- package/dist/security/url.js +67 -0
- package/dist/sync/client.d.ts +84 -0
- package/dist/sync/client.js +391 -0
- package/dist/sync/server.d.ts +75 -0
- package/dist/sync/server.js +659 -0
- package/dist/tickets/provider.d.ts +80 -0
- package/dist/tickets/provider.js +375 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.js +5 -0
- package/dist/ui/page.d.ts +5 -0
- package/dist/ui/page.js +841 -0
- package/dist/ui/server.d.ts +10 -0
- package/dist/ui/server.js +437 -0
- package/dist/verify/check.d.ts +38 -0
- package/dist/verify/check.js +121 -0
- package/dist/verify/engine.d.ts +39 -0
- package/dist/verify/engine.js +178 -0
- package/dist/verify/hooks.d.ts +24 -0
- package/dist/verify/hooks.js +125 -0
- package/dist/verify/runners.d.ts +26 -0
- package/dist/verify/runners.js +193 -0
- package/package.json +78 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR miner (Part 2 idea #4 — PR/review-comment capture).
|
|
3
|
+
*
|
|
4
|
+
* Mines merged GitHub pull requests — title, description, and crucially the
|
|
5
|
+
* REVIEW COMMENTS (where humans say "don't do X here", "we always Y", "this
|
|
6
|
+
* broke prod last time") — into memory proposals via the same LLM extraction
|
|
7
|
+
* contract as `dim mine --llm` and `dim harvest`.
|
|
8
|
+
*
|
|
9
|
+
* Uses the `gh` CLI (already authenticated on the developer's machine), so no
|
|
10
|
+
* token handling here. Cursor-tracked by merge time; every proposal is
|
|
11
|
+
* anchored to the PR's merge commit (COMMIT_REF) so verification can re-check
|
|
12
|
+
* it, and carries the reviewer's verbatim words in the rationale.
|
|
13
|
+
*
|
|
14
|
+
* Proposals land in the review queue (source `pr-miner`) — never directly in
|
|
15
|
+
* memory.
|
|
16
|
+
*/
|
|
17
|
+
import type { Proposal } from "../types.js";
|
|
18
|
+
import type { MemoryStore } from "../db/store.js";
|
|
19
|
+
export interface MinedPr {
|
|
20
|
+
number: number;
|
|
21
|
+
title: string;
|
|
22
|
+
body: string;
|
|
23
|
+
mergedAt: string;
|
|
24
|
+
mergeCommitSha: string | null;
|
|
25
|
+
headRefName: string;
|
|
26
|
+
files: string[];
|
|
27
|
+
/** review comments + top-level reviews, oldest first */
|
|
28
|
+
comments: Array<{
|
|
29
|
+
author: string;
|
|
30
|
+
path: string | null;
|
|
31
|
+
body: string;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
export interface PrMineResult {
|
|
35
|
+
scanned: number;
|
|
36
|
+
proposed: Proposal[];
|
|
37
|
+
skippedDuplicates: number;
|
|
38
|
+
provider: string | null;
|
|
39
|
+
}
|
|
40
|
+
/** True if the `gh` CLI is installed and authenticated for this repo's host. */
|
|
41
|
+
export declare function ghAvailable(repoRoot: string): boolean;
|
|
42
|
+
/** List merged PRs (newest first), stopping at the cursor. */
|
|
43
|
+
export declare function listMergedPrs(repoRoot: string, sinceMergedAt: string | null, limit?: number): MinedPr[];
|
|
44
|
+
/** Fetch review comments (inline, with file paths) + review bodies for one PR. */
|
|
45
|
+
export declare function fetchPrComments(repoRoot: string, prNumber: number): MinedPr["comments"];
|
|
46
|
+
export declare const PR_EXTRACT_INSTRUCTIONS = "You are mining a merged GitHub pull request \u2014 its description and especially its REVIEW COMMENTS \u2014 for durable, project-specific knowledge worth remembering across AI coding sessions.\n\nReview comments are where senior engineers state the unwritten rules: \"we never call the DB from controllers\", \"this exact retry pattern caused the March outage\", \"always use the factory here\". Those are the claims to extract.\n\nRules:\n1. Most PRs contain NOTHING durable \u2014 routine reviews (\"nit\", \"LGTM\", style back-and-forth). Return zero claims for those. Do NOT invent.\n2. SYNTHESIZE falsifiable claims about the codebase \u2014 do not parrot the comment. Include the WHY when a reviewer gives one.\n3. Prefer claims grounded in review comments over the PR description; keep the reviewer's key phrase in the rationale.\n4. kinds: DECISION, CONVENTION, GOTCHA, FAILED_APPROACH, ARCHITECTURE, INVARIANT, GUARDRAIL (guardrail_level never|ask-first|always), SKILL, TODO_CONTEXT.\n5. Scope with the commented file paths; add \"static_check\" (cheap shell command, exit 0 iff true) when an honest one exists.\n6. 0\u20133 claims per PR. Zero is the common case.\n\nRespond with ONLY: {\"claims\":[{\"kind\":\"CONVENTION\",\"claim\":\"...\",\"paths\":[\"src/x\"],\"symbols\":[],\"guardrail_level\":null,\"rationale\":\"...\",\"static_check\":null}]}";
|
|
47
|
+
export declare function buildPrPrompt(pr: MinedPr): string;
|
|
48
|
+
/**
|
|
49
|
+
* Mine merged PRs since the cursor. Requires the `gh` CLI and an LLM
|
|
50
|
+
* provider; returns provider:null when no provider is available (the CLI
|
|
51
|
+
* explains how to get one — there is no regex fallback here because review
|
|
52
|
+
* threads need synthesis, and `dim mine` already covers merge-commit bodies).
|
|
53
|
+
*/
|
|
54
|
+
export declare function minePrs(store: MemoryStore, repoRoot: string, opts?: {
|
|
55
|
+
max?: number;
|
|
56
|
+
all?: boolean;
|
|
57
|
+
}): Promise<PrMineResult>;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR miner (Part 2 idea #4 — PR/review-comment capture).
|
|
3
|
+
*
|
|
4
|
+
* Mines merged GitHub pull requests — title, description, and crucially the
|
|
5
|
+
* REVIEW COMMENTS (where humans say "don't do X here", "we always Y", "this
|
|
6
|
+
* broke prod last time") — into memory proposals via the same LLM extraction
|
|
7
|
+
* contract as `dim mine --llm` and `dim harvest`.
|
|
8
|
+
*
|
|
9
|
+
* Uses the `gh` CLI (already authenticated on the developer's machine), so no
|
|
10
|
+
* token handling here. Cursor-tracked by merge time; every proposal is
|
|
11
|
+
* anchored to the PR's merge commit (COMMIT_REF) so verification can re-check
|
|
12
|
+
* it, and carries the reviewer's verbatim words in the rationale.
|
|
13
|
+
*
|
|
14
|
+
* Proposals land in the review queue (source `pr-miner`) — never directly in
|
|
15
|
+
* memory.
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import { extractTicketId, readTicketsConfig, DEFAULT_TICKET_PATTERN } from "../tickets/provider.js";
|
|
19
|
+
import { scopeFromFiles } from "./commit-miner.js";
|
|
20
|
+
import { debugLog } from "../debug.js";
|
|
21
|
+
const PR_CURSOR_KEY = "pr_miner_last_merged_at";
|
|
22
|
+
const MAX_PRS_PER_RUN = 20; // LLM deep tier — keep runs bounded
|
|
23
|
+
const MAX_COMMENT_CHARS = 800; // per comment, keeps the prompt honest
|
|
24
|
+
const MAX_PROMPT_CHARS = 12_000;
|
|
25
|
+
function gh(repoRoot, args) {
|
|
26
|
+
return execFileSync("gh", args, { cwd: repoRoot, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
|
|
27
|
+
}
|
|
28
|
+
/** True if the `gh` CLI is installed and authenticated for this repo's host. */
|
|
29
|
+
export function ghAvailable(repoRoot) {
|
|
30
|
+
try {
|
|
31
|
+
gh(repoRoot, ["auth", "status"]);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** List merged PRs (newest first), stopping at the cursor. */
|
|
39
|
+
export function listMergedPrs(repoRoot, sinceMergedAt, limit = MAX_PRS_PER_RUN) {
|
|
40
|
+
const raw = gh(repoRoot, [
|
|
41
|
+
"pr", "list",
|
|
42
|
+
"--state", "merged",
|
|
43
|
+
"--limit", String(limit),
|
|
44
|
+
"--json", "number,title,body,mergedAt,mergeCommit,headRefName,files",
|
|
45
|
+
]);
|
|
46
|
+
const rows = JSON.parse(raw);
|
|
47
|
+
return rows
|
|
48
|
+
.filter((r) => !sinceMergedAt || r.mergedAt > sinceMergedAt)
|
|
49
|
+
.map((r) => ({
|
|
50
|
+
number: r.number,
|
|
51
|
+
title: r.title ?? "",
|
|
52
|
+
body: r.body ?? "",
|
|
53
|
+
mergedAt: r.mergedAt,
|
|
54
|
+
mergeCommitSha: r.mergeCommit?.oid ?? null,
|
|
55
|
+
headRefName: r.headRefName ?? "",
|
|
56
|
+
files: (r.files ?? []).map((f) => f.path),
|
|
57
|
+
comments: [],
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
/** Fetch review comments (inline, with file paths) + review bodies for one PR. */
|
|
61
|
+
export function fetchPrComments(repoRoot, prNumber) {
|
|
62
|
+
const comments = [];
|
|
63
|
+
// inline review comments carry the file path — the highest-signal source
|
|
64
|
+
try {
|
|
65
|
+
const raw = gh(repoRoot, ["api", `repos/{owner}/{repo}/pulls/${prNumber}/comments`, "--paginate"]);
|
|
66
|
+
for (const c of JSON.parse(raw)) {
|
|
67
|
+
if (c.body?.trim()) {
|
|
68
|
+
comments.push({ author: c.user?.login ?? "reviewer", path: c.path ?? null, body: c.body.trim() });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
// comments endpoint unavailable — reviews below still apply
|
|
74
|
+
debugLog(`pr #${prNumber} review comments fetch`, err);
|
|
75
|
+
}
|
|
76
|
+
// top-level review bodies ("Approving, but note that …")
|
|
77
|
+
try {
|
|
78
|
+
const raw = gh(repoRoot, ["pr", "view", String(prNumber), "--json", "reviews"]);
|
|
79
|
+
const { reviews } = JSON.parse(raw);
|
|
80
|
+
for (const r of reviews ?? []) {
|
|
81
|
+
if (r.body?.trim())
|
|
82
|
+
comments.push({ author: r.author?.login ?? "reviewer", path: null, body: r.body.trim() });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
// review list unavailable — PR body alone still has signal
|
|
87
|
+
debugLog(`pr #${prNumber} reviews fetch`, err);
|
|
88
|
+
}
|
|
89
|
+
return comments;
|
|
90
|
+
}
|
|
91
|
+
export const PR_EXTRACT_INSTRUCTIONS = `You are mining a merged GitHub pull request — its description and especially its REVIEW COMMENTS — for durable, project-specific knowledge worth remembering across AI coding sessions.
|
|
92
|
+
|
|
93
|
+
Review comments are where senior engineers state the unwritten rules: "we never call the DB from controllers", "this exact retry pattern caused the March outage", "always use the factory here". Those are the claims to extract.
|
|
94
|
+
|
|
95
|
+
Rules:
|
|
96
|
+
1. Most PRs contain NOTHING durable — routine reviews ("nit", "LGTM", style back-and-forth). Return zero claims for those. Do NOT invent.
|
|
97
|
+
2. SYNTHESIZE falsifiable claims about the codebase — do not parrot the comment. Include the WHY when a reviewer gives one.
|
|
98
|
+
3. Prefer claims grounded in review comments over the PR description; keep the reviewer's key phrase in the rationale.
|
|
99
|
+
4. kinds: DECISION, CONVENTION, GOTCHA, FAILED_APPROACH, ARCHITECTURE, INVARIANT, GUARDRAIL (guardrail_level never|ask-first|always), SKILL, TODO_CONTEXT.
|
|
100
|
+
5. Scope with the commented file paths; add "static_check" (cheap shell command, exit 0 iff true) when an honest one exists.
|
|
101
|
+
6. 0–3 claims per PR. Zero is the common case.
|
|
102
|
+
|
|
103
|
+
Respond with ONLY: {"claims":[{"kind":"CONVENTION","claim":"...","paths":["src/x"],"symbols":[],"guardrail_level":null,"rationale":"...","static_check":null}]}`;
|
|
104
|
+
export function buildPrPrompt(pr) {
|
|
105
|
+
const lines = [
|
|
106
|
+
`PR #${pr.number}: ${pr.title}`,
|
|
107
|
+
`Branch: ${pr.headRefName}`,
|
|
108
|
+
`Description:\n${pr.body?.trim() || "(none)"}`,
|
|
109
|
+
`Files touched: ${pr.files.slice(0, 30).join(", ") || "(unknown)"}`,
|
|
110
|
+
"",
|
|
111
|
+
`Review comments (${pr.comments.length}):`,
|
|
112
|
+
];
|
|
113
|
+
for (const c of pr.comments) {
|
|
114
|
+
lines.push(`- @${c.author}${c.path ? ` on ${c.path}` : ""}: ${c.body.slice(0, MAX_COMMENT_CHARS)}`);
|
|
115
|
+
}
|
|
116
|
+
return lines.join("\n").slice(0, MAX_PROMPT_CHARS);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Mine merged PRs since the cursor. Requires the `gh` CLI and an LLM
|
|
120
|
+
* provider; returns provider:null when no provider is available (the CLI
|
|
121
|
+
* explains how to get one — there is no regex fallback here because review
|
|
122
|
+
* threads need synthesis, and `dim mine` already covers merge-commit bodies).
|
|
123
|
+
*/
|
|
124
|
+
export async function minePrs(store, repoRoot, opts = {}) {
|
|
125
|
+
const { getTextProvider } = await import("../knowledge/llm.js");
|
|
126
|
+
const { parseClaims } = await import("../knowledge/extract.js");
|
|
127
|
+
const provider = await getTextProvider();
|
|
128
|
+
if (!provider)
|
|
129
|
+
return { scanned: 0, proposed: [], skippedDuplicates: 0, provider: null };
|
|
130
|
+
const cursor = opts.all ? null : store.getMeta(PR_CURSOR_KEY);
|
|
131
|
+
const prs = listMergedPrs(repoRoot, cursor, Math.min(opts.max ?? MAX_PRS_PER_RUN, MAX_PRS_PER_RUN));
|
|
132
|
+
const ticketPattern = readTicketsConfig(repoRoot).pattern ?? DEFAULT_TICKET_PATTERN;
|
|
133
|
+
const proposed = [];
|
|
134
|
+
let skippedDuplicates = 0;
|
|
135
|
+
for (const pr of prs) {
|
|
136
|
+
pr.comments = fetchPrComments(repoRoot, pr.number);
|
|
137
|
+
// description-less PRs with no review discussion carry no reviewable signal
|
|
138
|
+
if (!pr.body.trim() && pr.comments.length === 0)
|
|
139
|
+
continue;
|
|
140
|
+
let claims;
|
|
141
|
+
try {
|
|
142
|
+
claims = parseClaims(await provider.generate(PR_EXTRACT_INSTRUCTIONS, buildPrPrompt(pr)));
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
debugLog(`llm mining pr #${pr.number} (skipped)`, err);
|
|
146
|
+
continue; // provider hiccup on one PR shouldn't kill the run
|
|
147
|
+
}
|
|
148
|
+
const ticketRef = extractTicketId(`${pr.title}\n${pr.body}\n${pr.headRefName}`, ticketPattern) ?? undefined;
|
|
149
|
+
for (const cl of claims.slice(0, 3)) {
|
|
150
|
+
const evidence = [];
|
|
151
|
+
if (pr.mergeCommitSha)
|
|
152
|
+
evidence.push({ type: "COMMIT_REF", payload: pr.mergeCommitSha });
|
|
153
|
+
if (cl.staticCheck)
|
|
154
|
+
evidence.push({ type: "STATIC_CHECK", payload: cl.staticCheck });
|
|
155
|
+
if (ticketRef)
|
|
156
|
+
evidence.push({ type: "TICKET_REF", payload: ticketRef });
|
|
157
|
+
const p = store.propose({
|
|
158
|
+
kind: cl.kind,
|
|
159
|
+
claim: cl.claim,
|
|
160
|
+
paths: cl.paths ?? scopeFromFiles(pr.files),
|
|
161
|
+
symbols: cl.symbols,
|
|
162
|
+
guardrailLevel: cl.guardrailLevel,
|
|
163
|
+
evidence: evidence.length ? evidence : undefined,
|
|
164
|
+
source: "pr-miner",
|
|
165
|
+
sourceRef: `pr:${pr.number}`,
|
|
166
|
+
rationale: cl.rationale ?? `Mined from PR #${pr.number}: ${pr.title}`,
|
|
167
|
+
ticketRef,
|
|
168
|
+
});
|
|
169
|
+
if (p)
|
|
170
|
+
proposed.push(p);
|
|
171
|
+
else
|
|
172
|
+
skippedDuplicates++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// advance cursor to the newest merge time seen (list is newest-first)
|
|
176
|
+
if (prs.length > 0)
|
|
177
|
+
store.setMeta(PR_CURSOR_KEY, prs[0].mergedAt);
|
|
178
|
+
return {
|
|
179
|
+
scanned: prs.length,
|
|
180
|
+
proposed,
|
|
181
|
+
skippedDuplicates,
|
|
182
|
+
provider: `${provider.name}/${provider.model}`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=pr-miner.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-start briefing (KARPATHY_LAYERS Feature 6) — Spec layer.
|
|
3
|
+
*
|
|
4
|
+
* "Agile speccing" applied to each coding session. Before an agent starts work,
|
|
5
|
+
* this gathers what aidimag already knows about the area it's about to touch:
|
|
6
|
+
* relevant memories, stale warnings, in-scope guardrails, coverage gaps, and a
|
|
7
|
+
* few clarifying questions the agent should ask the human instead of guessing.
|
|
8
|
+
*
|
|
9
|
+
* Exposed as the MCP resource aidimag://session-briefing and the session_start
|
|
10
|
+
* prompt.
|
|
11
|
+
*/
|
|
12
|
+
import type { MemoryStore } from "../db/store.js";
|
|
13
|
+
import type { GuardrailLevel, MemoryEntry } from "../types.js";
|
|
14
|
+
export interface SessionBriefing {
|
|
15
|
+
branch: string | null;
|
|
16
|
+
ticket: string | null;
|
|
17
|
+
changedFiles: string[];
|
|
18
|
+
relevantMemories: MemoryEntry[];
|
|
19
|
+
staleWarnings: MemoryEntry[];
|
|
20
|
+
guardrailsInScope: Array<{
|
|
21
|
+
memory: MemoryEntry;
|
|
22
|
+
level: GuardrailLevel;
|
|
23
|
+
}>;
|
|
24
|
+
coverageGaps: string[];
|
|
25
|
+
/** recent searches (agent or human) that found NOTHING — questions memory couldn't answer */
|
|
26
|
+
unansweredSearches: Array<{
|
|
27
|
+
query: string;
|
|
28
|
+
misses: number;
|
|
29
|
+
}>;
|
|
30
|
+
suggestedQuestions: string[];
|
|
31
|
+
}
|
|
32
|
+
export declare function buildSessionBriefing(store: MemoryStore, root: string): SessionBriefing;
|
|
33
|
+
/** Human/agent-readable rendering of the briefing for the MCP resource + prompt. */
|
|
34
|
+
export declare function renderBriefing(b: SessionBriefing): string;
|
|
35
|
+
/** Prompt that tells the agent to interview the user using the briefing (Karpathy: extract the true goal). */
|
|
36
|
+
export declare function sessionStartPrompt(b: SessionBriefing): string;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-start briefing (KARPATHY_LAYERS Feature 6) — Spec layer.
|
|
3
|
+
*
|
|
4
|
+
* "Agile speccing" applied to each coding session. Before an agent starts work,
|
|
5
|
+
* this gathers what aidimag already knows about the area it's about to touch:
|
|
6
|
+
* relevant memories, stale warnings, in-scope guardrails, coverage gaps, and a
|
|
7
|
+
* few clarifying questions the agent should ask the human instead of guessing.
|
|
8
|
+
*
|
|
9
|
+
* Exposed as the MCP resource aidimag://session-briefing and the session_start
|
|
10
|
+
* prompt.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { detectBranchTicket } from "../tickets/provider.js";
|
|
14
|
+
function currentBranch(root) {
|
|
15
|
+
try {
|
|
16
|
+
return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Files changed on this branch vs its merge-base with the default branch, plus uncommitted work. */
|
|
23
|
+
function branchChangedFiles(root) {
|
|
24
|
+
const tryGit = (args) => {
|
|
25
|
+
try {
|
|
26
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const files = new Set();
|
|
33
|
+
for (const base of ["origin/main", "origin/master", "main", "master"]) {
|
|
34
|
+
const mb = tryGit(["merge-base", base, "HEAD"]).trim();
|
|
35
|
+
if (mb) {
|
|
36
|
+
tryGit(["diff", "--name-only", `${mb}..HEAD`]).split("\n").filter(Boolean).forEach((f) => files.add(f));
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// include staged + unstaged working changes
|
|
41
|
+
tryGit(["diff", "--name-only", "HEAD"]).split("\n").filter(Boolean).forEach((f) => files.add(f));
|
|
42
|
+
return [...files];
|
|
43
|
+
}
|
|
44
|
+
export function buildSessionBriefing(store, root) {
|
|
45
|
+
const branch = currentBranch(root);
|
|
46
|
+
const ticket = detectBranchTicket(root);
|
|
47
|
+
const changedFiles = branchChangedFiles(root);
|
|
48
|
+
const scoped = changedFiles.length ? store.getForFiles(changedFiles, 100) : store.list(20).filter((m) => m.status !== "REFUTED");
|
|
49
|
+
const relevantMemories = scoped
|
|
50
|
+
.filter((m) => m.kind !== "GUARDRAIL" && m.status !== "REFUTED")
|
|
51
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
52
|
+
.slice(0, 8);
|
|
53
|
+
const staleWarnings = scoped.filter((m) => m.status === "STALE");
|
|
54
|
+
const guardrailsInScope = scoped
|
|
55
|
+
.filter((m) => m.kind === "GUARDRAIL" && m.status !== "REFUTED")
|
|
56
|
+
.map((m) => ({ memory: m, level: m.guardrailLevel ?? "ask-first" }));
|
|
57
|
+
// coverage gaps: changed files no memory is scoped to
|
|
58
|
+
const covered = new Set();
|
|
59
|
+
for (const m of scoped)
|
|
60
|
+
for (const sp of m.scope.paths)
|
|
61
|
+
for (const f of changedFiles)
|
|
62
|
+
if (f.startsWith(sp) || sp.startsWith(f))
|
|
63
|
+
covered.add(f);
|
|
64
|
+
const coverageGaps = changedFiles.filter((f) => !covered.has(f));
|
|
65
|
+
// knowledge gaps: recent zero-hit searches (see store.searchGaps / `dim gaps`)
|
|
66
|
+
let unansweredSearches = [];
|
|
67
|
+
try {
|
|
68
|
+
unansweredSearches = store.searchGaps({ sinceDays: 14, limit: 5 }).map((g) => ({ query: g.query, misses: g.misses }));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* pre-migration DB — gaps are advisory */
|
|
72
|
+
}
|
|
73
|
+
const suggestedQuestions = [];
|
|
74
|
+
for (const s of staleWarnings.slice(0, 2)) {
|
|
75
|
+
suggestedQuestions.push(`A ${s.kind} for ${s.scope.paths.join(", ") || "the repo"} is STALE — has it changed? ("${s.claim.slice(0, 80)}")`);
|
|
76
|
+
}
|
|
77
|
+
for (const f of coverageGaps.slice(0, 2)) {
|
|
78
|
+
suggestedQuestions.push(`No memory covers ${f} — want me to explore it before changing it?`);
|
|
79
|
+
}
|
|
80
|
+
if (unansweredSearches.length) {
|
|
81
|
+
suggestedQuestions.push(`Past sessions searched for "${unansweredSearches[0].query}" and found nothing — do you know the answer? (I'll save it with context_note.)`);
|
|
82
|
+
}
|
|
83
|
+
if (guardrailsInScope.some((g) => g.level === "ask-first")) {
|
|
84
|
+
suggestedQuestions.push("An ASK-FIRST guardrail applies to these files — confirm the intended scope before I proceed.");
|
|
85
|
+
}
|
|
86
|
+
return { branch, ticket, changedFiles, relevantMemories, staleWarnings, guardrailsInScope, coverageGaps, unansweredSearches, suggestedQuestions };
|
|
87
|
+
}
|
|
88
|
+
const GUARDRAIL_ICON = { never: "🚫", always: "✅", "ask-first": "🤚" };
|
|
89
|
+
/** Human/agent-readable rendering of the briefing for the MCP resource + prompt. */
|
|
90
|
+
export function renderBriefing(b) {
|
|
91
|
+
const lines = ["# Session briefing"];
|
|
92
|
+
lines.push(`branch: ${b.branch ?? "(unknown)"}${b.ticket ? ` · ticket: ${b.ticket}` : ""}`);
|
|
93
|
+
lines.push(b.changedFiles.length ? `in scope: ${b.changedFiles.length} changed file(s)` : "no branch diff detected — showing recent memory");
|
|
94
|
+
lines.push("");
|
|
95
|
+
if (b.guardrailsInScope.length) {
|
|
96
|
+
lines.push("## Guardrails in scope ⚠️");
|
|
97
|
+
for (const g of b.guardrailsInScope)
|
|
98
|
+
lines.push(`- ${GUARDRAIL_ICON[g.level]} ${g.level.toUpperCase()}: ${g.memory.claim}`);
|
|
99
|
+
lines.push("");
|
|
100
|
+
}
|
|
101
|
+
if (b.relevantMemories.length) {
|
|
102
|
+
lines.push("## Relevant memory");
|
|
103
|
+
for (const m of b.relevantMemories)
|
|
104
|
+
lines.push(`- (${m.status}, ${m.kind}) ${m.claim}`);
|
|
105
|
+
lines.push("");
|
|
106
|
+
}
|
|
107
|
+
if (b.staleWarnings.length) {
|
|
108
|
+
lines.push("## Stale — do NOT trust without re-verifying");
|
|
109
|
+
for (const m of b.staleWarnings)
|
|
110
|
+
lines.push(`- (${m.kind}) ${m.claim}`);
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
113
|
+
if (b.coverageGaps.length) {
|
|
114
|
+
lines.push("## No memory coverage");
|
|
115
|
+
for (const f of b.coverageGaps.slice(0, 10))
|
|
116
|
+
lines.push(`- ${f}`);
|
|
117
|
+
lines.push("");
|
|
118
|
+
}
|
|
119
|
+
if (b.unansweredSearches.length) {
|
|
120
|
+
lines.push("## Unanswered questions (searches that found nothing)");
|
|
121
|
+
for (const g of b.unansweredSearches)
|
|
122
|
+
lines.push(`- "${g.query}"${g.misses > 1 ? ` (asked ${g.misses}×)` : ""}`);
|
|
123
|
+
lines.push("If this session answers one of these, persist it (context_note / memory_propose).");
|
|
124
|
+
lines.push("");
|
|
125
|
+
}
|
|
126
|
+
if (b.suggestedQuestions.length) {
|
|
127
|
+
lines.push("## Ask the user before guessing");
|
|
128
|
+
for (const q of b.suggestedQuestions)
|
|
129
|
+
lines.push(`- ${q}`);
|
|
130
|
+
lines.push("");
|
|
131
|
+
}
|
|
132
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
133
|
+
}
|
|
134
|
+
/** Prompt that tells the agent to interview the user using the briefing (Karpathy: extract the true goal). */
|
|
135
|
+
export function sessionStartPrompt(b) {
|
|
136
|
+
return (`You're starting a coding session. Before writing any code, ground yourself in what this project already knows.\n\n` +
|
|
137
|
+
renderBriefing(b) +
|
|
138
|
+
`\nNow:\n` +
|
|
139
|
+
`1. Treat the guardrails above as binding (never = refuse, ask-first = confirm, always = do).\n` +
|
|
140
|
+
`2. Trust VERIFIED memory; re-verify anything STALE before relying on it (\`memory_verify\`).\n` +
|
|
141
|
+
`3. Ask the user the clarifying questions above instead of guessing — extract the true goal first.\n` +
|
|
142
|
+
`4. Use \`memory_search\` / \`memory_get_for_files\` as you go, and \`memory_critique\` before you finish.\n` +
|
|
143
|
+
`5. IMPORTANT: When the user states durable facts about the codebase (decisions, conventions, guardrails, architecture), IMMEDIATELY capture them with \`context_note\`. Examples:\n` +
|
|
144
|
+
` • "We use X because Y" → capture as DECISION\n` +
|
|
145
|
+
` • "Never do X" → capture as GUARDRAIL\n` +
|
|
146
|
+
` • "We always do X" → capture as CONVENTION\n` +
|
|
147
|
+
` • "We tried X and it failed" → capture as FAILED_APPROACH\n` +
|
|
148
|
+
` Don't wait for session end. Capture facts as they're stated, then continue the conversation naturally.`);
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=session-briefing.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-end extraction (Phase 2 capture pipeline).
|
|
3
|
+
*
|
|
4
|
+
* The agent does the summarizing — we provide a tight, structured prompt
|
|
5
|
+
* (exposed as an MCP prompt) and a `memory_propose` tool that routes the
|
|
6
|
+
* agent's structured proposals into the human review queue.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SESSION_END_PROMPT = "You are finishing a coding session. Before you stop, extract durable knowledge about this codebase so future sessions don't have to re-discover it.\n\nReview what you learned this session and propose memories using the `memory_propose` tool. Rules:\n\n1. Only propose things that are DURABLE (true beyond this session) and NON-OBVIOUS (not derivable from a quick file read).\n2. Write each claim as a FALSIFIABLE statement \u2014 something a checker could verify against the code. Bad: \"the auth code is tricky\". Good: \"JWT refresh in src/auth/refresh.ts must run before middleware chain; reordering breaks session renewal (see commit abc123)\".\n3. Pick the right kind:\n - DECISION: a choice made and why (alternatives rejected)\n - CONVENTION: a rule consistently followed in this repo\n - GOTCHA: surprising behavior that cost you time\n - FAILED_APPROACH: something you tried that did NOT work, and why\n - ARCHITECTURE: how components fit together\n - INVARIANT: something that must always/never hold\n - GUARDRAIL: a behavioral rule for future agents \u2014 pass guardrail_level: 'never' (refuse + explain), 'always' (do without asking), or 'ask-first' (confirm with the user first)\n - SKILL: a reusable step-by-step procedure (e.g. \"Deploy: 1) \u2026 2) \u2026 3) \u2026\") that the team runs repeatedly\n - TODO_CONTEXT: unfinished work + the context needed to resume it\n4. Scope each memory to the paths/symbols it applies to. Repo-wide only if truly global.\n5. Attach evidence whenever possible: COMMIT_REF (a sha), STATIC_CHECK (a grep/assertion command that passes iff the claim holds), TEST_RESULT (a test command), or HUMAN_ATTESTED as last resort.\n6. FAILED_APPROACH memories are especially valuable \u2014 they prevent future sessions from repeating dead ends.\n7. Propose 0\u20137 memories. Zero is fine if nothing durable was learned. Do NOT pad.\n8. Before proposing, call `memory_critique` with a short summary of what you did and the files you touched. It checks your work against the project's existing memory and guardrails \u2014 resolve any contradictions or guardrail concerns it raises first.\n\nRespect all GUARDRAIL memories you've seen this session: 'never' = refuse and explain why, 'always' = do without asking, 'ask-first' = ask the user before proceeding.\n\nYour proposals enter a human review queue (`dim review`); they do not become active memory until approved.";
|
|
9
|
+
/**
|
|
10
|
+
* Ticket-aware variant (TICKETS_DESIGN T5): when the current branch carries a
|
|
11
|
+
* ticket id, teach the agent to pull the ticket's WHY into its proposals —
|
|
12
|
+
* "you fixed the race — should I remember that token refresh must never run
|
|
13
|
+
* concurrently?" The MCP agent is often the better prompting surface.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sessionEndPromptFor(ticketId: string | null): string;
|
|
16
|
+
/** One-line summary of a proposal for human review UIs. */
|
|
17
|
+
export declare function proposalSummaryLine(p: {
|
|
18
|
+
id: string;
|
|
19
|
+
kind: string;
|
|
20
|
+
claim: string;
|
|
21
|
+
source: string;
|
|
22
|
+
rationale?: string;
|
|
23
|
+
}): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-end extraction (Phase 2 capture pipeline).
|
|
3
|
+
*
|
|
4
|
+
* The agent does the summarizing — we provide a tight, structured prompt
|
|
5
|
+
* (exposed as an MCP prompt) and a `memory_propose` tool that routes the
|
|
6
|
+
* agent's structured proposals into the human review queue.
|
|
7
|
+
*/
|
|
8
|
+
export const SESSION_END_PROMPT = `You are finishing a coding session. Before you stop, extract durable knowledge about this codebase so future sessions don't have to re-discover it.
|
|
9
|
+
|
|
10
|
+
Review what you learned this session and propose memories using the \`memory_propose\` tool. Rules:
|
|
11
|
+
|
|
12
|
+
1. Only propose things that are DURABLE (true beyond this session) and NON-OBVIOUS (not derivable from a quick file read).
|
|
13
|
+
2. Write each claim as a FALSIFIABLE statement — something a checker could verify against the code. Bad: "the auth code is tricky". Good: "JWT refresh in src/auth/refresh.ts must run before middleware chain; reordering breaks session renewal (see commit abc123)".
|
|
14
|
+
3. Pick the right kind:
|
|
15
|
+
- DECISION: a choice made and why (alternatives rejected)
|
|
16
|
+
- CONVENTION: a rule consistently followed in this repo
|
|
17
|
+
- GOTCHA: surprising behavior that cost you time
|
|
18
|
+
- FAILED_APPROACH: something you tried that did NOT work, and why
|
|
19
|
+
- ARCHITECTURE: how components fit together
|
|
20
|
+
- INVARIANT: something that must always/never hold
|
|
21
|
+
- GUARDRAIL: a behavioral rule for future agents — pass guardrail_level: 'never' (refuse + explain), 'always' (do without asking), or 'ask-first' (confirm with the user first)
|
|
22
|
+
- SKILL: a reusable step-by-step procedure (e.g. "Deploy: 1) … 2) … 3) …") that the team runs repeatedly
|
|
23
|
+
- TODO_CONTEXT: unfinished work + the context needed to resume it
|
|
24
|
+
4. Scope each memory to the paths/symbols it applies to. Repo-wide only if truly global.
|
|
25
|
+
5. Attach evidence whenever possible: COMMIT_REF (a sha), STATIC_CHECK (a grep/assertion command that passes iff the claim holds), TEST_RESULT (a test command), or HUMAN_ATTESTED as last resort.
|
|
26
|
+
6. FAILED_APPROACH memories are especially valuable — they prevent future sessions from repeating dead ends.
|
|
27
|
+
7. Propose 0–7 memories. Zero is fine if nothing durable was learned. Do NOT pad.
|
|
28
|
+
8. Before proposing, call \`memory_critique\` with a short summary of what you did and the files you touched. It checks your work against the project's existing memory and guardrails — resolve any contradictions or guardrail concerns it raises first.
|
|
29
|
+
|
|
30
|
+
Respect all GUARDRAIL memories you've seen this session: 'never' = refuse and explain why, 'always' = do without asking, 'ask-first' = ask the user before proceeding.
|
|
31
|
+
|
|
32
|
+
Your proposals enter a human review queue (\`dim review\`); they do not become active memory until approved.`;
|
|
33
|
+
/**
|
|
34
|
+
* Ticket-aware variant (TICKETS_DESIGN T5): when the current branch carries a
|
|
35
|
+
* ticket id, teach the agent to pull the ticket's WHY into its proposals —
|
|
36
|
+
* "you fixed the race — should I remember that token refresh must never run
|
|
37
|
+
* concurrently?" The MCP agent is often the better prompting surface.
|
|
38
|
+
*/
|
|
39
|
+
export function sessionEndPromptFor(ticketId) {
|
|
40
|
+
if (!ticketId)
|
|
41
|
+
return SESSION_END_PROMPT;
|
|
42
|
+
return (SESSION_END_PROMPT +
|
|
43
|
+
`
|
|
44
|
+
|
|
45
|
+
This session's branch is tied to ticket ${ticketId}. Additionally:
|
|
46
|
+
8. Call \`ticket_get\` first — the ticket usually carries the WHY (root cause, rejected alternatives, acceptance criteria) that the code alone doesn't.
|
|
47
|
+
9. Combine sources: claim from what you did + ticket title/description for the rationale. A bug ticket's root cause is usually a GOTCHA; what didn't fix it is a FAILED_APPROACH; acceptance criteria are INVARIANT candidates.
|
|
48
|
+
10. Proposals are tagged with ${ticketId} automatically (ticket_ref); you don't need to mention the id in claims.`);
|
|
49
|
+
}
|
|
50
|
+
/** One-line summary of a proposal for human review UIs. */
|
|
51
|
+
export function proposalSummaryLine(p) {
|
|
52
|
+
return `[${p.id.slice(0, 8)}] (${p.kind}, via ${p.source}) ${p.claim}`;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=session-extraction.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proposal triage — keeps the review queue from becoming homework.
|
|
3
|
+
*
|
|
4
|
+
* Scores every pending proposal 0–1 from cheap, local signals:
|
|
5
|
+
* + machine-checkable evidence attached (falsifiable on arrival)
|
|
6
|
+
* + trusted source (human-stated context > knowledge docs > miners)
|
|
7
|
+
* + concrete scope (paths/symbols)
|
|
8
|
+
* − similarity to previously REJECTED claims (the correction loop:
|
|
9
|
+
* what you drop teaches the queue what not to surface first)
|
|
10
|
+
* − similarity to existing ACTIVE memory (likely duplicate knowledge)
|
|
11
|
+
*
|
|
12
|
+
* `dim review` walks the queue best-first and shows the score;
|
|
13
|
+
* `dim review approve all --min-score <s>` batch-approves above a bar.
|
|
14
|
+
*/
|
|
15
|
+
import type { MemoryStore } from "../db/store.js";
|
|
16
|
+
import type { Proposal } from "../types.js";
|
|
17
|
+
export interface TriagedProposal {
|
|
18
|
+
proposal: Proposal;
|
|
19
|
+
score: number;
|
|
20
|
+
reasons: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function tokenize(text: string): Set<string>;
|
|
23
|
+
/** Jaccard similarity over content tokens — cheap, offline, good enough for triage. */
|
|
24
|
+
export declare function claimSimilarity(a: string, b: string): number;
|
|
25
|
+
export declare function scoreProposal(p: Proposal, rejectedClaims: string[], activeClaims: string[]): {
|
|
26
|
+
score: number;
|
|
27
|
+
reasons: string[];
|
|
28
|
+
};
|
|
29
|
+
/** Pending proposals, best-first, each with its score and the reasons behind it. */
|
|
30
|
+
export declare function triagePending(store: MemoryStore, limit?: number): TriagedProposal[];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proposal triage — keeps the review queue from becoming homework.
|
|
3
|
+
*
|
|
4
|
+
* Scores every pending proposal 0–1 from cheap, local signals:
|
|
5
|
+
* + machine-checkable evidence attached (falsifiable on arrival)
|
|
6
|
+
* + trusted source (human-stated context > knowledge docs > miners)
|
|
7
|
+
* + concrete scope (paths/symbols)
|
|
8
|
+
* − similarity to previously REJECTED claims (the correction loop:
|
|
9
|
+
* what you drop teaches the queue what not to surface first)
|
|
10
|
+
* − similarity to existing ACTIVE memory (likely duplicate knowledge)
|
|
11
|
+
*
|
|
12
|
+
* `dim review` walks the queue best-first and shows the score;
|
|
13
|
+
* `dim review approve all --min-score <s>` batch-approves above a bar.
|
|
14
|
+
*/
|
|
15
|
+
const STOPWORDS = new Set([
|
|
16
|
+
"the", "a", "an", "is", "are", "was", "were", "in", "on", "of", "to", "and",
|
|
17
|
+
"or", "for", "with", "that", "this", "it", "be", "as", "at", "by", "from",
|
|
18
|
+
"not", "no", "all", "any", "into", "through", "must", "should", "never",
|
|
19
|
+
"always", "made", "make", "makes", "was", "were",
|
|
20
|
+
]);
|
|
21
|
+
export function tokenize(text) {
|
|
22
|
+
return new Set(text
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^a-z0-9\s/._-]/g, " ")
|
|
25
|
+
.split(/\s+/)
|
|
26
|
+
.filter((t) => t.length > 2 && !STOPWORDS.has(t)));
|
|
27
|
+
}
|
|
28
|
+
/** Jaccard similarity over content tokens — cheap, offline, good enough for triage. */
|
|
29
|
+
export function claimSimilarity(a, b) {
|
|
30
|
+
const ta = tokenize(a);
|
|
31
|
+
const tb = tokenize(b);
|
|
32
|
+
if (ta.size === 0 || tb.size === 0)
|
|
33
|
+
return 0;
|
|
34
|
+
let inter = 0;
|
|
35
|
+
for (const t of ta)
|
|
36
|
+
if (tb.has(t))
|
|
37
|
+
inter++;
|
|
38
|
+
return inter / (ta.size + tb.size - inter);
|
|
39
|
+
}
|
|
40
|
+
const SOURCE_TRUST = [
|
|
41
|
+
{ prefix: "context:", boost: 0.25, label: "user-stated in chat" },
|
|
42
|
+
{ prefix: "harvest:", boost: 0.2, label: "user-typed (harvested)" },
|
|
43
|
+
{ prefix: "knowledge:", boost: 0.15, label: "from curated doc" },
|
|
44
|
+
{ prefix: "bootstrap", boost: 0.1, label: "repo survey" },
|
|
45
|
+
{ prefix: "session:", boost: 0.1, label: "agent session" },
|
|
46
|
+
{ prefix: "verify:stale", boost: 0.15, label: "stale-memory follow-up" },
|
|
47
|
+
{ prefix: "pr-miner", boost: 0.15, label: "from PR review threads" },
|
|
48
|
+
{ prefix: "commit-miner", boost: 0.05, label: "mined from commits" },
|
|
49
|
+
];
|
|
50
|
+
const SIMILARITY_THRESHOLD = 0.55;
|
|
51
|
+
export function scoreProposal(p, rejectedClaims, activeClaims) {
|
|
52
|
+
let score = 0.4; // base
|
|
53
|
+
const reasons = [];
|
|
54
|
+
const machineEvidence = p.evidence.filter((e) => e.type === "STATIC_CHECK" || e.type === "TEST_RESULT" || e.type === "EXEC_TRACE");
|
|
55
|
+
if (machineEvidence.length) {
|
|
56
|
+
score += 0.2;
|
|
57
|
+
reasons.push("machine-checkable evidence");
|
|
58
|
+
}
|
|
59
|
+
else if (p.evidence.some((e) => e.type === "COMMIT_REF")) {
|
|
60
|
+
score += 0.08;
|
|
61
|
+
reasons.push("commit-anchored");
|
|
62
|
+
}
|
|
63
|
+
if (p.evidence.some((e) => e.type === "HUMAN_ATTESTED")) {
|
|
64
|
+
score += 0.08;
|
|
65
|
+
reasons.push("human-attested");
|
|
66
|
+
}
|
|
67
|
+
const trust = SOURCE_TRUST.find((s) => p.source.startsWith(s.prefix));
|
|
68
|
+
if (trust) {
|
|
69
|
+
score += trust.boost;
|
|
70
|
+
reasons.push(trust.label);
|
|
71
|
+
}
|
|
72
|
+
if (p.paths.length || p.symbols.length) {
|
|
73
|
+
score += 0.07;
|
|
74
|
+
reasons.push("concretely scoped");
|
|
75
|
+
}
|
|
76
|
+
// correction loop: proposals resembling past rejections start at the back
|
|
77
|
+
let maxRejected = 0;
|
|
78
|
+
for (const r of rejectedClaims)
|
|
79
|
+
maxRejected = Math.max(maxRejected, claimSimilarity(p.claim, r));
|
|
80
|
+
if (maxRejected >= SIMILARITY_THRESHOLD) {
|
|
81
|
+
score -= 0.35;
|
|
82
|
+
reasons.push(`similar to a claim you rejected (${(maxRejected * 100).toFixed(0)}%)`);
|
|
83
|
+
}
|
|
84
|
+
// near-duplicate of active memory → low urgency
|
|
85
|
+
let maxActive = 0;
|
|
86
|
+
for (const a of activeClaims)
|
|
87
|
+
maxActive = Math.max(maxActive, claimSimilarity(p.claim, a));
|
|
88
|
+
if (maxActive >= SIMILARITY_THRESHOLD) {
|
|
89
|
+
score -= 0.25;
|
|
90
|
+
reasons.push(`similar to existing memory (${(maxActive * 100).toFixed(0)}%)`);
|
|
91
|
+
}
|
|
92
|
+
return { score: Math.max(0, Math.min(1, score)), reasons };
|
|
93
|
+
}
|
|
94
|
+
/** Pending proposals, best-first, each with its score and the reasons behind it. */
|
|
95
|
+
export function triagePending(store, limit = 1000) {
|
|
96
|
+
const pending = store.listProposals("PENDING", limit);
|
|
97
|
+
if (!pending.length)
|
|
98
|
+
return [];
|
|
99
|
+
const rejectedClaims = store.listRejectedClaims(500);
|
|
100
|
+
const activeClaims = store
|
|
101
|
+
.list(1000)
|
|
102
|
+
.filter((m) => m.status !== "REFUTED")
|
|
103
|
+
.map((m) => m.claim);
|
|
104
|
+
return pending
|
|
105
|
+
.map((proposal) => ({ proposal, ...scoreProposal(proposal, rejectedClaims, activeClaims) }))
|
|
106
|
+
.sort((a, b) => b.score - a.score);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=triage.js.map
|