Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -863,23 +863,27 @@ async function main(): Promise<void> {
}
case "preflight": {
const commandValue = requireOption(parsed, "command");
const result = await preflightCommand({
cwd,
command: commandValue,
sessionId: typeof parsed.options.session === "string" ? parsed.options.session : undefined
});
const enforce = Boolean(parsed.options.enforce);
const loaded = await import("../core/context.js").then((m) => m.loadProject(cwd));
try {
const result = await preflightCommand({
cwd,
command: commandValue,
sessionId: typeof parsed.options.session === "string" ? parsed.options.session : undefined
});
const enforce = Boolean(parsed.options.enforce);

render(asJson ? result : formatTextPreflight(result), asJson);
render(asJson ? result : formatTextPreflight(result), asJson);

if (enforce) {
if (result.decision === "block") {
process.exitCode = 2;
} else if (result.decision === "warn") {
process.exitCode = 1;
if (enforce) {
if (result.decision === "block") {
process.exitCode = loaded.context.config.preflight.enforce_block_exit_code;
} else if (result.decision === "warn") {
process.exitCode = loaded.context.config.preflight.enforce_warn_exit_code;
}
}
} finally {
loaded.close();
}

return;
}
case "run": {
Expand Down
66 changes: 66 additions & 0 deletions src/evals/projectmind/projectmind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,72 @@ export async function runProjectMindEval(): Promise<ProjectMindResult> {
});
}

// Scenario 5: architecture-decision
{
const noMemoryCwd = await tempWorkspace("ad-no-mem-");
workspaces.push(noMemoryCwd);
await initProject({ cwd: noMemoryCwd });
const withMemoryCwd = await tempWorkspace("ad-with-mem-");
workspaces.push(withMemoryCwd);
await initProject({ cwd: withMemoryCwd });
await createMemory({
cwd: withMemoryCwd,
content: "Use repository layer; no direct DB access from route handlers.",
type: "architecture_note",
source: "user_explicit",
severity: "high"
});

const noMem = await retrieveMemories({ cwd: noMemoryCwd, task: "update package.json", dryRun: true });
const withMem = await retrieveMemories({ cwd: withMemoryCwd, task: "add direct DB query to the user route handler", dryRun: true });
const hasDecision = withMem.some((m) => m.type === "architecture_note");

scenarios.push({
name: "architecture-decision",
passed: !noMem.some((m) => m.type === "architecture_note") && hasDecision,
noMemory: { memoryCount: noMem.length },
withMemory: { memoryCount: withMem.length, includesArchitectureNote: hasDecision },
delta: hasDecision ? "architecture decision surfaced for DB-related task" : "no delta"
});
}

// Scenario 6: supersession-visibility
{
const cwd = await tempWorkspace("ss-mem-");
workspaces.push(cwd);
await initProject({ cwd });
const old = await createMemory({
cwd,
content: "Use npm for package management.",
type: "workflow_rule",
source: "user_explicit"
});
const replacement = await createMemory({
cwd,
content: "Use pnpm for all package operations.",
type: "workflow_rule",
source: "user_explicit"
});
const { supersedeMemory } = await import("../../lifecycle/lifecycle.js");
await supersedeMemory({ cwd, oldMemoryId: old.id, newMemoryId: replacement.id, reason: "Updated to pnpm" });

let results = await retrieveMemories({ cwd, task: "package manager", dryRun: true });
const oldHidden = !results.some((m) => m.id === old.id);
const replacementVisible = results.some((m) => m.id === replacement.id);

await quarantineMemory({ cwd, memoryId: replacement.id, reason: "Test quarantine" });
results = await retrieveMemories({ cwd, task: "package manager", dryRun: true });
const replacementHidden = !results.some((m) => m.id === replacement.id);

scenarios.push({
name: "supersession-visibility",
passed: oldHidden && replacementVisible && replacementHidden,
noMemory: {},
withMemory: { oldSupersededAndHidden: oldHidden, replacementVisible, replacementHiddenAfterQuarantine: replacementHidden },
delta: "superseded memory hidden; superseding memory hidden after quarantine"
});
}

const passed = scenarios.filter((s) => s.passed).length;
const failed = scenarios.filter((s) => !s.passed).length;

Expand Down
27 changes: 22 additions & 5 deletions src/formatters/pack-markdown.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { MemoryRecord } from "../domain/types.js";
import { planPackMemories } from "./pack-planner.js";

export interface PackSectionItem {
id: string;
Expand Down Expand Up @@ -136,17 +137,33 @@ export function formatPackMarkdown(
memories: MemoryRecord[],
options: { generatedAt?: string; budgetCharacters?: number } = {}
): string {
const sections = buildPackSections(memories);
let selected = memories;
let omittedSummary = "";

if (options.budgetCharacters !== undefined && options.budgetCharacters > 0) {
const headerChars = renderHeader(projectName, options.generatedAt).join("\n").length + 50;
const plan = planPackMemories({
memories,
budgetCharacters: options.budgetCharacters,
reservedCharacters: headerChars
});
selected = plan.selected;

if (plan.omitted.length > 0) {
omittedSummary = `\n## Omitted Memories\n- ${plan.omitted.length} lower-priority memories omitted due to configured memory budget.\n`;
}
}

const sections = buildPackSections(selected);
const hasConstraints = sections.some((section) => section.title === "Critical Constraints");
const markdown = [
...renderHeader(projectName, options.generatedAt),
...sections.map(renderSection),
hasConstraints ? "## Suggested Verification\n- Prefer safe local checks before risky commands.\n" : ""
hasConstraints ? "## Suggested Verification\n- Prefer safe local checks before risky commands.\n" : "",
omittedSummary
]
.filter(Boolean)
.join("\n");

return options.budgetCharacters === undefined
? markdown
: truncateMarkdown(markdown, options.budgetCharacters);
return markdown;
}
80 changes: 80 additions & 0 deletions src/formatters/pack-planner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { MemoryRecord } from "../domain/types.js";

export interface PackPlan {
selected: MemoryRecord[];
omitted: Array<{
memoryId: string;
type: MemoryRecord["type"];
reason: string;
}>;
}

function isMandatory(memory: MemoryRecord): boolean {
if (memory.pinned) return true;
if (memory.priority > 0) return true;
if (memory.type === "command_policy" && memory.metadata.decision === "block") return true;
if (
memory.severity === "high" &&
(memory.type === "fragile_file" ||
memory.type === "failed_attempt" ||
memory.type === "known_fix" ||
memory.type === "agent_mistake")
)
return true;
return false;
}

function estimatedCharSize(memory: MemoryRecord): number {
return memory.content.length + 80;
}

export function planPackMemories({
memories,
budgetCharacters,
reservedCharacters = 0
}: {
memories: MemoryRecord[];
budgetCharacters: number;
reservedCharacters?: number;
}): PackPlan {
const available = budgetCharacters - reservedCharacters;
if (available <= 0) {
return { selected: [], omitted: memories.map((m) => ({ memoryId: m.id, type: m.type, reason: "No budget remaining" })) };
}

const mandatory = memories.filter(isMandatory);
const optional = memories
.filter((m) => !isMandatory(m))
.sort((a, b) => {
const scoreA = typeof a.metadata.retrieval === "object" && a.metadata.retrieval && "score" in a.metadata.retrieval
? (a.metadata.retrieval.score as number) : 0;
const scoreB = typeof b.metadata.retrieval === "object" && b.metadata.retrieval && "score" in b.metadata.retrieval
? (b.metadata.retrieval.score as number) : 0;
return scoreB - scoreA || b.createdAt.localeCompare(a.createdAt);
});

const selected: MemoryRecord[] = [];
let used = 0;

for (const memory of mandatory) {
selected.push(memory);
used += estimatedCharSize(memory);
}

for (const memory of optional) {
if (used >= available) break;
selected.push(memory);
used += estimatedCharSize(memory);
}

const selectedIds = new Set(selected.map((m) => m.id));
const omitted = memories
.filter((m) => !selectedIds.has(m.id))
.map((m) => ({
memoryId: m.id,
type: m.type,
reason: "Lower priority — omitted due to configured memory budget"
}));

return { selected, omitted };
}
10 changes: 10 additions & 0 deletions src/vector/vector-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export function searchVectorIndexReadOnly({
query,
limit,
visibleMemoryIds,
projectId,
providerId = "local"
}: {
memoryDir: string;
Expand All @@ -212,6 +213,15 @@ export function searchVectorIndexReadOnly({
if (!index || index.entries.length === 0) {
return [];
}
if (index.projectId !== projectId) {
return [];
}
if (index.provider !== provider.name) {
return [];
}
if (index.dimensions !== provider.dimensions) {
return [];
}
const queryEmbedding = provider.embed(query);
return index.entries
.filter((entry) => visibleMemoryIds.has(entry.memoryId))
Expand Down
Loading