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
45 changes: 45 additions & 0 deletions docs/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,60 @@ Retention and redaction defaults:
severity, add a clearer `description`, or remove the `productPaths` until the
check is stable.

## Verified Learning Lifecycle

Flat memory sections are useful, but durable engineering knowledge should come
from verified outcomes: confirmed regressions, repairs, refuted hypotheses,
accepted risks, incidents, ADRs, conventions, ownership changes, and proof
recipes.

Use versioned `learningEvents` in `.codedecay/memory.json`:

```bash
# Preview a proposal (does not mutate memory)
npx codedecay memory learning --action propose --input learning-event.json

# Persist the proposal
npx codedecay memory learning --action propose --input learning-event.json --apply

# Explicit human review
npx codedecay memory learning --action approve --event-id <id> --actor kunal --reason "Verified against payout retry CI" --apply
npx codedecay memory learning --action reject --event-id <id> --apply
npx codedecay memory learning --action supersede --event-id <id> --apply
npx codedecay memory learning --action expire --event-id <id> --apply
npx codedecay memory learning --action revoke --event-id <id> --apply
```

Rules:

- Agent output, PR text, comments, and external memory stay `proposed` until an
explicit approve/reject/supersede/expire/revoke operation.
- Trusted runtime/tool evidence can raise proposal confidence, but never silently
writes durable approved memory.
- Every event keeps source evidence IDs, scope (repo/revision/files/symbols),
trust class, creator, timestamps, review status, and an audit trail.
- Retrieval only surfaces approved, in-scope, non-expired events and explains
inclusion and suppression.
- Refuted hypotheses affect ranking only inside a narrowly matched scope; they
cannot globally disable a rule.
- Redteam/analyze reports show when a prior approved learning influenced
investigation or proof planning (`memory-learning-influenced`).

Conflict detection flags duplicates, contradictions (for example confirmed
regression vs refuted hypothesis), and ownership/architecture overlaps that
should supersede stale routing.

## Report Behavior

When memory matches a PR, CodeDecay may add:

- findings for impacted invariants
- findings for past regression areas
- findings for matching architecture notes
- findings for approved learning events that match the change
- recommended checks for flows
- recommended commands from the memory file
- recommended proof recipes from approved learnings

CodeDecay does not run memory commands automatically. They are reported as
project-specific checks for the user or future execution adapters.
Expand Down
67 changes: 65 additions & 2 deletions packages/cli/src/commands/memory.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { readFileSync } from "node:fs";
import { dirname, extname, resolve } from "node:path";
import {
appendLearningEventProposal,
applyLearningEventOperation,
detectLearningConflicts,
importCodeDecayMemory,
learnCodeDecayMemory,
loadCodeDecayMemory,
writeCodeDecayMemory
writeCodeDecayMemory,
type MemoryLearningEventInput,
type MemoryLearningConflict
} from "@submuxhq/codedecay-memory";
import { write } from "../io";
import {
parseMemoryArgs,
parseMemoryImportArgs,
parseMemoryLearnArgs,
parseMemoryLearningArgs,
parseMemorySetupArgs
} from "../parsers/args";
import {
renderMemory,
renderMemoryImportResult,
renderMemoryLearnResult
renderMemoryLearnResult,
renderMemoryLearningResult
} from "../renderers/memory";
import {
createMemorySetupResult,
Expand All @@ -37,6 +44,14 @@ export function runMemoryCommand(context: CliCommandContext, dependencies: Memor
return;
}

if (context.args[0] === "learning") {
runMemoryLearningCommand({
...context,
args: context.args.slice(1)
}, dependencies);
return;
}

const options = parseMemoryArgs(context.args);
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
const rootDir = dependencies.resolveRepoRoot(cwd, { format: "markdown" });
Expand Down Expand Up @@ -94,6 +109,54 @@ export function runMemoryLearnCommand(context: CliCommandContext, dependencies:
);
}

export function runMemoryLearningCommand(context: CliCommandContext, dependencies: MemoryCommandDependencies): void {
const options = parseMemoryLearningArgs(context.args);
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
const rootDir = dependencies.resolveRepoRoot(cwd, { format: "markdown" });
const loadedMemory = loadCodeDecayMemory(rootDir);
const timestamp = new Date().toISOString();
let memory = loadedMemory.memory;
let eventId = options.eventId;
let conflicts: MemoryLearningConflict[] = detectLearningConflicts(memory);

if (options.action === "propose") {
const inputPath = resolve(context.runtimeCwd, options.input!);
const proposal = JSON.parse(readFileSync(inputPath, "utf8")) as MemoryLearningEventInput;
// Proposals always land as reviewStatus=proposed; approve/reject/etc. are explicit ops.
const appended = appendLearningEventProposal(memory, {
...proposal,
timestamp: proposal.timestamp ?? timestamp,
creator: proposal.creator ?? options.actor
});
memory = appended.memory;
eventId = appended.event.id;
conflicts = appended.conflicts;
} else {
memory = applyLearningEventOperation(memory, {
eventId: options.eventId!,
action: options.action,
actor: options.actor,
timestamp,
reason: options.reason,
evidenceIds: options.evidenceIds
});
conflicts = detectLearningConflicts(memory);
}

const writtenPath = options.apply ? writeCodeDecayMemory(rootDir, memory) : undefined;
write(
context.runtime.stdout,
renderMemoryLearningResult({
format: options.format,
action: options.action,
eventId: eventId!,
writtenPath,
conflicts,
applied: options.apply
})
);
}

function parseMemoryLearningInput(inputPath: string): unknown {
const raw = readFileSync(inputPath, "utf8");
if (isMarkdownPath(inputPath)) {
Expand Down
25 changes: 20 additions & 5 deletions packages/cli/src/docs/command-docs/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,40 @@ export const STATE_COMMAND_DOCS: Record<string, CommandDoc> = {
memory: {
name: "memory",
summary: "Show local repo memory.",
usage: ["codedecay memory [options]", "codedecay memory setup [options]"],
usage: [
"codedecay memory [options]",
"codedecay memory setup [options]",
"codedecay memory learning --action <action> [options]"
],
description: [
"Load `.codedecay/memory.json` and render the normalized memory sections used by redteam and agent workflows.",
"`codedecay memory setup` prints safe setup guidance for local, Mem0, and Supermemory providers without installing packages or touching tracked config."
"`codedecay memory setup` prints safe setup guidance for local, Mem0, and Supermemory providers without installing packages or touching tracked config.",
"`codedecay memory learning` proposes or reviews versioned learning events (approve/reject/supersede/expire/revoke) without auto-approving untrusted sources."
],
options: [
{ flag: "--cwd <path>", description: "Repository working directory (default: current directory)" },
{ flag: "--format <format>", description: "json or markdown (default: json for memory, markdown for setup)" },
{ flag: "setup --provider <provider>", description: "local, mem0, supermemory, or all (default: all)" },
{ flag: "setup --apply", description: "Write .codedecay/local/memory-providers.yml review snippet" }
{ flag: "setup --apply", description: "Write .codedecay/local/memory-providers.yml review snippet" },
{ flag: "learning --action <action>", description: "propose|approve|reject|supersede|expire|revoke" },
{ flag: "learning --event-id <id>", description: "Existing learning event id (required except propose)" },
{ flag: "learning --input <path>", description: "JSON learning event proposal (required for propose)" },
{ flag: "learning --actor <name>", description: "Reviewer/proposer identity (default: maintainer)" },
{ flag: "learning --reason <text>", description: "Audit reason for the operation" },
{ flag: "learning --evidence-id <id>", description: "Optional evidence id (repeatable)" },
{ flag: "learning --apply", description: "Write `.codedecay/memory.json` instead of preview only" }
],
examples: [
"codedecay memory --format markdown",
"codedecay memory --cwd ../my-repo --format json",
"codedecay memory setup --provider all",
"codedecay memory setup --provider supermemory --apply"
"codedecay memory setup --provider supermemory --apply",
"codedecay memory learning --action propose --input learning.json",
"codedecay memory learning --action approve --event-id learn_abc --apply"
],
notes: [
"Memory setup is preview-only by default. It does not install packages, call providers, or edit `.codedecay/config.yml`."
"Memory setup is preview-only by default. It does not install packages, call providers, or edit `.codedecay/config.yml`.",
"Learning events stay proposed until an explicit approve/reject/supersede/expire/revoke operation."
]
},
"memory-import": {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/parsers/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export { parseExecuteArgs } from "./execute";
export { parseLlmReviewArgs } from "./llm-review";
export { parseLoopArgs } from "./loop";
export { parseMcpArgs } from "./mcp";
export { parseMemoryArgs, parseMemoryImportArgs, parseMemoryLearnArgs, parseMemorySetupArgs } from "./memory";
export { parseMemoryArgs, parseMemoryImportArgs, parseMemoryLearnArgs, parseMemoryLearningArgs, parseMemorySetupArgs } from "./memory";
export { parseMigrationArgs } from "./migration";
export { parseRevalidateArgs } from "./revalidate";
export { parseRuntimeArgs } from "./runtime";
Expand Down
141 changes: 140 additions & 1 deletion packages/cli/src/parsers/memory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { MemoryImportOptions, MemoryLearnOptions, MemoryOptions, MemorySetupOptions, MemorySetupProvider } from "../types";
import type {
MemoryImportOptions,
MemoryLearnOptions,
MemoryLearningOptions,
MemoryOptions,
MemorySetupOptions,
MemorySetupProvider
} from "../types";
import { parseConfigFormat, requireValue } from "./primitives";
import { HelpRequested, throwUnknownOption } from "./shared";

Expand Down Expand Up @@ -247,3 +254,135 @@ export function parseMemoryLearnArgs(args: string[]): MemoryLearnOptions {

return options;
}

export function parseMemoryLearningArgs(args: string[]): MemoryLearningOptions {
const options: MemoryLearningOptions = {
format: "json",
apply: false,
action: "approve",
actor: "maintainer",
reason: "Explicit human review of learning event."
};

for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (!arg) {
continue;
}

if (arg === "--help" || arg === "-h") {
throw new HelpRequested();
}

if (arg.startsWith("--cwd=")) {
options.cwd = arg.slice("--cwd=".length);
continue;
}
if (arg === "--cwd") {
options.cwd = requireValue(args, index, arg);
index += 1;
continue;
}

if (arg.startsWith("--format=")) {
options.format = parseConfigFormat(arg.slice("--format=".length));
continue;
}
if (arg === "--format") {
options.format = parseConfigFormat(requireValue(args, index, arg));
index += 1;
continue;
}

if (arg === "--apply") {
options.apply = true;
continue;
}

if (arg.startsWith("--action=")) {
options.action = parseLearningAction(arg.slice("--action=".length));
continue;
}
if (arg === "--action") {
options.action = parseLearningAction(requireValue(args, index, arg));
index += 1;
continue;
}

if (arg.startsWith("--event-id=")) {
options.eventId = arg.slice("--event-id=".length);
continue;
}
if (arg === "--event-id") {
options.eventId = requireValue(args, index, arg);
index += 1;
continue;
}

if (arg.startsWith("--actor=")) {
options.actor = arg.slice("--actor=".length);
continue;
}
if (arg === "--actor") {
options.actor = requireValue(args, index, arg);
index += 1;
continue;
}

if (arg.startsWith("--reason=")) {
options.reason = arg.slice("--reason=".length);
continue;
}
if (arg === "--reason") {
options.reason = requireValue(args, index, arg);
index += 1;
continue;
}

if (arg.startsWith("--input=")) {
options.input = arg.slice("--input=".length);
continue;
}
if (arg === "--input") {
options.input = requireValue(args, index, arg);
index += 1;
continue;
}

if (arg.startsWith("--evidence-id=")) {
options.evidenceIds = [...(options.evidenceIds ?? []), arg.slice("--evidence-id=".length)];
continue;
}
if (arg === "--evidence-id") {
options.evidenceIds = [...(options.evidenceIds ?? []), requireValue(args, index, arg)];
index += 1;
continue;
}

throwUnknownOption(arg, "memory learning");
}

if (options.action === "propose" && !options.input) {
throw new Error('Missing value for --input. Propose requires a JSON learning event file.');
}

if (options.action !== "propose" && !options.eventId) {
throw new Error('Missing value for --event-id. Use "codedecay memory learning --help" for usage.');
}

return options;
}

function parseLearningAction(value: string): MemoryLearningOptions["action"] {
if (
value === "approve" ||
value === "reject" ||
value === "supersede" ||
value === "expire" ||
value === "revoke" ||
value === "propose"
) {
return value;
}
throw new Error(`Invalid --action ${value}. Expected approve|reject|supersede|expire|revoke|propose.`);
}
Loading
Loading