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 ofmyblog's own agent harness.
The responsibility split
The workflow has separate processes with separate responsibilities:
- Collect raw OpenCode events and PR review notes without interpreting them.
- Label each item with a fixed category using a constrained classifier.
- Distill labeled items into evidence-backed candidate rules.
- Approve each candidate interactively; a person may reject or rewrite it.
- 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.
noiseand 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.