Skip to content

OpenCode Repository Memory: A Dumb PR Review Workflow

The general memory model is collect, label, distill, and approve. This page turns that model into a small repository-level OpenCode workflow. It is a deliberately dumb proof: files, fixed labels, two constrained LLM calls, and an explicit human gate. There is no vector database and no automatic rule promotion.

Research scope: This is a general workflow pattern documented by myblog. The .opencode/, scripts/, and .agent-memory/ paths below are illustrative files for a separate target repository or workspace. They are not part of myblog's own agent harness.

The responsibility split

The workflow has separate processes with separate responsibilities:

  1. Collect raw OpenCode events and PR review notes without interpreting them.
  2. Label each item with a fixed category using a constrained classifier.
  3. Distill labeled items into evidence-backed candidate rules.
  4. Approve each candidate interactively; a person may reject or rewrite it.
  5. Integrate only approved candidates into AGENTS.md, a skill, or another instruction file through a normal Git change.

The person does not manually author every rule. The LLM produces the candidate rule; the person decides whether the candidate deserves to become durable project knowledge.

Repository layout

.opencode/
  commands/review-memory.md
  plugins/review-capture.ts
  skills/pr-review-memory/SKILL.md
scripts/
  review-memory/
    collect
    label
    distill
    approve
.agent-memory/
  reviews/
    inbox/
    labeled/
    proposals/

Keep raw inbox files and generated session material ignored. Keep approved changes in normal repository files so they can be reviewed in the same PR as any other change.

Step 1: collect raw evidence

Collection does not use an LLM. It receives a PR review export, a pasted review note, or an OpenCode event and stores the original content with provenance:

function collect(source):
    item = {
        id: generate_id(),
        source_type: source.type,
        source_reference: source.reference,
        repository: source.repository,
        captured_at: now(),
        raw_text: source.text
    }

    write(".agent-memory/reviews/inbox/" + item.id + ".json", item)

An OpenCode project plugin can call the collector on a lifecycle event:

plugin ReviewCapture:
    on event "session.idle":
        collect({
            type: "opencode-session",
            reference: current_session_id(),
            repository: current_worktree(),
            text: event_metadata_only()
        })

The hook records an event; it does not summarize the conversation or edit instructions. PR review text can enter through a GitHub adapter, a workspace file, or a human paste. All of those sources use the same inbox format.

Step 2: label items with a fixed taxonomy

The category is not chosen ad hoc by the person collecting the review. A separate labeler invokes an LLM with a closed taxonomy:

Label Meaning
rule A stable project convention that should apply repeatedly
workflow A repeatable process or command sequence
testing A validation, test, build, or release expectation
security A security boundary or handling requirement
architecture A durable design decision or constraint
documentation A missing or misleading explanation
noise A one-off comment, preference, or non-durable observation

One item may receive multiple labels. The labeler must also return evidence and confidence, so downstream steps can reject vague classifications:

function label(item):
    result = LLM.classify(
        input = item.raw_text,
        instructions = """
            Use only the listed labels.
            Return zero or more labels.
            Quote the exact evidence span for every label.
            Return noise when the observation is one-off or not durable.
            Return JSON with labels, evidence, confidence, and explanation.
        """,
        taxonomy = FIXED_TAXONOMY
    )

    write("labeled/" + item.id + ".json", merge(item, result))

This is the first LLM judgment. A low-confidence item, an item labeled noise, or an item without an evidence span does not proceed automatically. A maintainer may correct a label later, but manual labeling is an exception, not the normal path.

Step 3: distill labeled evidence into candidate rules

Distillation is different from summarization. A summary describes a review; distillation proposes a durable rule only when the evidence supports one.

The distiller groups labeled items by repository, scope, and topic, then asks an LLM to produce a candidate record:

function distill(labeled_items):
    groups = group_by_repository_scope_and_topic(labeled_items)

    for group in groups:
        candidate = LLM.distill(
            input = group,
            instructions = """
                Propose a rule only when it is actionable and broadly reusable.
                Prefer repeated independent evidence.
                Preserve links to every supporting item.
                Do not treat a personal preference as a project rule.
                Do not edit any harness file.
                Return JSON with rule_text, evidence_refs, scope,
                confidence, rationale, and suggested_target.
            """
        )

        if candidate.is_actionable and candidate.evidence_refs is not empty:
            write("proposals/" + candidate.id + ".json", candidate)

A proposal exists to solve the problem between noisy evidence and a durable instruction file. It gives the human a compact, traceable unit to review instead of requiring the human to rediscover and rewrite the rule from every review comment.

Example proposal:

candidate_id: rule-042
rule_text: Run the documentation build before deploying site changes.
labels: [workflow, testing]
scope: repository
evidence_refs: [pr-123-comment-4, pr-127-comment-2]
confidence: 0.91
rationale: The same missing validation was identified in two independent reviews.
suggested_target: AGENTS.md
status: pending

The LLM is allowed to suggest wording and a target. It is not allowed to promote the candidate. A single explicit maintainer decision can be enough for some rules, but the proposal must say why that evidence is sufficient.

Step 4: review candidates interactively

The approval process displays the generated rule, its evidence, and its proposed target. The human chooses whether to accept, rewrite, reject, defer, or change the target:

function review(proposals):
    for proposal in proposals where proposal.status == "pending":
        display(proposal.rule_text)
        display(proposal.evidence_refs)
        display(proposal.rationale)
        display(proposal.suggested_target)

        decision = ask_user(
            "accept, edit, reject, defer, or retarget?"
        )

        if decision == "accept":
            proposal.status = "approved"
        if decision == "edit":
            proposal.rule_text = ask_user("Provide corrected wording")
            proposal.status = "approved"
        if decision == "retarget":
            proposal.suggested_target = ask_user("Choose a harness target")
            proposal.status = "approved"
        if decision == "reject":
            proposal.status = "rejected"
        if decision == "defer":
            proposal.status = "deferred"

        save(proposal)

This is human-in-the-loop review, not manual rule generation. The person is judging the LLM's evidence, scope, wording, and durability.

Step 5: integrate approved candidates

Integration consumes only approved proposal records and creates a normal Git patch or pull request:

function integrate(approved_proposal):
    target = approved_proposal.suggested_target

    require target exists
    require approved_proposal.status == "approved"
    require approved_proposal.evidence_refs is not empty

    patch = append_rule(
        file = target,
        section = choose_section(target, approved_proposal.labels),
        text = approved_proposal.rule_text
    )

    create_branch("memory-rule/" + approved_proposal.id)
    apply(patch)
    add_commit_message("Add approved repository memory rule")
    open_pull_request(
        body = include_evidence_and_rationale(approved_proposal)
    )

The integration process must never silently append to AGENTS.md or a skill. The PR remains the final shared review boundary.

A minimal skill target

If the target is an OpenCode skill, it must already have valid frontmatter:

---
name: pr-review-memory
description: Apply approved project rules learned from PR review
---

## Approved rules

Project skills live under .opencode/skills/<skill-name>/SKILL.md. Keep a skill focused on a repeatable procedure; keep project facts and stable commands in AGENTS.md when they should be loaded for every task.

What is automatic, and what is not

Stage Automatic behavior LLM judgment
Collection Capture raw text and provenance None
Labeling Run the fixed taxonomy classifier Category, evidence, confidence
Distillation Group related labeled items Candidate rule, rationale, scope, target
Approval Present candidates and evidence Human accepts, edits, rejects, or defers
Integration Create a patch or PR None; only approved records are eligible

The “dumb” part is the infrastructure, not the absence of intelligence: it uses two narrow LLM tasks with structured output and keeps the irreversible decision outside the model.

Guardrails

  • Raw inbox files are evidence, not instructions.
  • The labeler cannot invent categories.
  • The distiller must preserve evidence references.
  • noise and low-confidence items stop before distillation.
  • The proposal process never edits harness files.
  • The approval process can rewrite or reject generated rules.
  • Approved rules are integrated through normal Git review.
  • Redact credentials, customer data, and prompt-injected text before labeling.

Only after this proof is useful should a team consider automatic PR retrieval, semantic search, or a hosted memory service.

References