Process work sessions into the claude-code-plugins knowledge graph — preserves the session transcript, writes or links its chronicle entry as the session's synthesis, turns the transcript into observations, updates the patterns and decisions they support, folds in named plugins and skills, and proposes promotions to the personal commons. Also resolves-and-preserves an on-demand Claude Code docs URL when one comes up. Use when the user says "process this session", "run /process", names a session transcript or UUID, names a chronicle entry whose session should be processed, asks what's in the processing queue, or points at a Claude Code release-notes / docs page to capture.
This is the orchestrator for the claude-code-plugins knowledge graph. It turns raw material — session
transcripts, and the occasional Claude Code docs page — into graph notes, via one human-approved plan per
run. It does not enforce anything beyond that approval: the graph's health rests on the
conventions in the sibling knowledge-graph skill and on the plan you read before it runs.
Four things happen to a session, and the shape matters:
session:{uuid}.Stages 3 and 4 are siblings, both made from the transcript; neither is upstream of the other. The chronicle is not a filter the observations pass through, and it is not an input tier. It was one, and everything that arrangement required — an ordering between two tiers, a guard against double-counting a session through both — existed only to contain the inversion.
Values written in like this were filled from .commons.yml when this skill was generated by
graph-init. They are concrete names now, not placeholders — read them as this graph's actual type
names, directories, and sinks.
/process <input> takes a session (a transcript path, a UUID, or "this session"), a URL (a Claude Code
docs page), or pasted text naming one piece of raw material. A chronicle entry is not an input: it is
this pipeline's own output, and naming one means processing the session it describes.
/process with no argument enumerates the queue instead of processing anything. One tier has a queue:
*.jsonl directly inside a directory matching ~/.claude/projects/*claude-code-plugins*/
with no source note carrying its session:{uuid}. Preview each by date and size; a bare UUID tells the
reader nothing.The ledger test is the recorded source: value, not a filename mention. An identifier can appear in
the prose of some other source note without having been processed; grepping for it rather than for
source: <value> reports it as done when it isn't.
List entries human-readably, never as bare identifiers. Show the queue and stop — running it is a separate, explicit invocation per item (or "process all of these"), not implied by listing. The claude-code-docs tier arrives by hand (a pasted URL — nothing to glob), so it has no queue: ask the user to paste or name the docs page instead.
Match the input to one of this graph's source tiers and normalize it to a canonical source: identity —
the value the ledger will key on.
Session. Input is a transcript path, a session UUID, or a request to
process a session by date — including "this session". Transcripts live directly inside
~/.claude/projects/-Users-derek-personal-Developer-claude-code-plugins/ as <uuid>.jsonl, with
worktree sessions in sibling directories whose slug carries the worktree suffix. Check all of them.
Subagent transcripts nest one level down, in a directory named for the parent session's UUID — they are
not sessions and are never queued.
Canonical source: is session:{uuid}, not the file path: a transcript moves between those
directories when a worktree is created or removed, and a path key would resolve to a second, duplicate
source note for the same session.
Resolve by invoking meta-claude:session-export to render the JSONL into readable text — raw JSONL is
unreadable at this size and burns context for nothing. Name the rendered file
YYYY-MM-DDThhmm-<short-description>.txt and send it to a scratch directory outside the repo, not
to knowledge/sources/raw/ — the gate below runs on it there, and only a passing export is moved into
the archive. State that destination when invoking the skill so it doesn't have to ask. Transcripts are always large, so this tier always takes the subagent-fanout path in step 4.
The chronicle is not a source tier. A chronicle entry is the session-level synthesis — this
pipeline's own output, written for a human reader — so it lives in types.synthesis, not in sources:.
It used to be registered as a second, lower-ranked source tier, with an ordering between the two and a
guard against double-counting the same session through both. All of that existed to contain one
inversion: a distillation sitting on the input side. There is one raw source for a session, and it is
the transcript.
A session is a conversation, not a project's property, and the glob above assumes otherwise. A single
session can span several repositories — the founding one ran 590 turns in commons and 190 in
claude-code-plugins, across four working directories and three branches. Its transcript gets exactly one
home regardless, and which one is close to arbitrary. That one landed under a commons worktree slug,
where the *claude-code-plugins* pattern could never see it, and the graph concluded from the empty result
that the transcript had been pruned — a claim that reached this skill, a decision note, and a spec before
anyone checked. It was intact the whole time; it has since been moved here and archived at
~/Developer/session-archive/.
So: before concluding any transcript is gone, search ~/.claude/projects/*/ by UUID, not by project
glob. An empty result from the narrow pattern is evidence about the pattern. When a cross-project session
turns out to live elsewhere, moving it under the slug of the project that was its primary subject makes it
reachable — but that is a repair, not a rule, and the next such session will land wherever it lands.
When the transcript is genuinely gone. Transcripts do get pruned, and this is the real case the
fallback tier was invented for. Where a session's transcript no longer exists but its chronicle entry
does, the run continues from the chronicle: extraction reads it instead of the transcript, and the source
note records raw: unavailable with the reason and the date the absence was observed — so the graph shows
a degraded note rather than leaving a reader to wonder later why it is thin. This is a permanent path, and
it is the only route by which a pre-existing chronicle entry whose session is gone can be processed at all.
It fires only when the transcript is verifiably absent, and "verifiably" carries the whole weight —
a UUID search across ~/.claude/projects/*/, per the paragraph above, not a project-scoped glob that came
back empty. That distinction is not hypothetical: this graph marked its founding source note
raw: unavailable on the strength of a scoped miss, and the transcript was 2.8 MB and intact. Never fire
this path because a transcript is merely large, or because the chronicle is an easier read. Extracting
from the chronicle while the transcript is alive is exactly the failure step 4 exists to prevent, and this
path must not become the loophole for it. Where neither the transcript nor a chronicle entry exists, there
is nothing to process: say so and stop.
Claude Code docs (on-demand). Input is a URL to a Claude Code release-notes or feature-docs page,
handed in when a session references one. Canonical source: is the URL itself, normalized (strip
tracking params, resolve redirects). Fetch it with WebFetch — fetched material can vanish, which is why
the preserve step below keeps what mattered rather than a link to it. No queue for this tier; it is only
ever processed when explicitly handed in.
Before anything is inspected, write the source note and put the raw material somewhere it will still be there in a year. This pipeline used to have no such step — step 3 searched for a source note and step 9 stamped one, and nothing created one. For the docs tier the gap is invisible, because a fetched page is small enough to inline and an implementer fills in the obvious. For a session it is not.
Sessions archive; docs pages inline.
knowledge/sources/raw/, committed with the repo — by way of a scratch path and the gate below,
never written there directly — and the source note carries identity, date, a one-line description,
the processed: stamp, and an archive: pointer to that file.sources/, with no separate archive.The archive is committed, and that is the whole fix. Its predecessor was .claude/session-exports/,
gitignored as a derived artifact — and a directory outside version control is where a source note's
pointer comes to dangle. This graph's founding transcript,
session:284b79f5-c34f-4ad3-b97d-9c78cdc9c46f, spent months in no repository, backed up by nothing, its
originating worktree deleted and its slug outside the resolver's glob: intact, but defended by nothing and
reachable only by someone searching outside the pattern. An archive under knowledge/ is versioned and
backed up wherever this repo is, which is the difference between material that survived and material that
is kept.
This stage writes before the step-5 plan is approved, deliberately. Every other write in the run
waits for that gate. Preservation does not, because it is the one stage whose omission destroys
something: a run abandoned at plan review should still have kept the transcript. It writes the source
note and the archived file — never observations, never attractors, never the stamp. The processed:
stamp goes on at step 9, so an abandoned run leaves an unstamped source note, which is the recoverable
state.
It writes; it does not stage or commit. The archived file lands in the working tree and this repo's normal flow commits it, so the raw material rides in the same commit as the notes the run produced.
Preserving twice must not overwrite. A re-run resolves to the same session:{uuid} and arrives back
here. If a source note already carries this source:, leave it alone but for anything genuinely missing
(an archive: pointer it lacks); never replace its body, and never touch its processed: history. If
the archived file already exists, leave it. Step 3 reads that history and decides what this run has left
to do.
This repository is public. That is the assumption this gate was written under, stated here rather
than left to be inferred — a repo that is private today can be opened later and no run would notice. Two
checks run over the rendered export, and both must pass before the file is written into
knowledge/sources/raw/.
Export to a scratch path first, never straight into knowledge/sources/raw/. Tell
meta-claude:session-export to write outside the repo — a temp directory — and scan it there. A
transcript exported directly into the archive is already in the committed tree before the gate has run,
and "withhold" then means deleting a file rather than never writing one. It moves into
knowledge/sources/raw/ only once both checks pass.
plugins/knowledge-commons/scripts/scan-secrets.sh over the rendered export. (The
repo-relative path is deliberate: this project is the plugin's source, so the working copy is always
the current scan, where a path into the installed plugin cache would name whatever version happened to
be installed.) It is a fixed scan for credential shapes — private-key blocks, known token prefixes,
credential assignments carrying a real value, connection strings with a password, Authorization:
headers with a bearer value. It exits non-zero on a hit and non-zero on its own failure, and here
those mean the same thing: stop. No archive file, no archive: pointer, nothing written. A scan
that could not run is not a scan that passed.Resolve each hit on its own, with its surrounding context shown, as redact (replace the value with a marker and archive the rest), withhold (this transcript is not archived at all), or accept (a false positive). Record every redaction and every withholding on the source note, so a degraded archive says so in the graph instead of looking complete. Blanket-withholding throws away a whole session over one false positive; blanket-accepting is not a gate.
If the hits cannot be put to a human — a non-interactive invocation, an unattended run — withhold. Do not archive on the assumption that someone will review it later. This repo is public and the failure is irreversible: git history, GitHub's caches, and forks all outlive a later deletion.
Search the graph for the source note carrying this source:. Source notes are the only note type that
carries a source: field, so a targeted grep across sources/ is enough — do not build an index for
this.
processed: stamp and the notes it already produced (the
stamp's ran: list names them). Work out what's new by reading the current content against what's
already captured — never by hashing or diffing the raw file. Tell the user plainly what already exists
rather than re-proposing it.source:): stop and ask which is
correct. Never guess at ledger identity — a wrong guess either duplicates history or silently merges
two distinct inputs.Findings come from the transcript, never from the chronicle. Where a session has a chronicle entry,
read it for orientation if it helps — it is a serviceable map of a long transcript and will tell you
where to look — but every finding is sourced from, and quoted from, the transcript itself. The reason is
the whole point of the constraint, so it survives a rewrite: the chronicle is written for a human reader
by the agent whose blind spots this graph exists to catch, and it has already compressed away the
corrections, the dead ends, and the exact wording that an observation is made of. Extraction that runs
on it inherits every one of those omissions and has no way to notice. The first real run of this pipeline
found three divergences between a session's own account of itself and what its transcript showed; a
reader of the account alone would have found none of them.
The one exception is the path named in step 2, where the transcript is verifiably gone. There the
chronicle is what remains, extraction reads it, and the source note carries raw: unavailable so the
graph shows it.
Read the resolved input and decide what it contains, sized to the input:
A reader that returns nothing has not reported an empty class. Expect to have to ask: readers routinely finish their read and go idle without volunteering findings, so silence, an empty message, and a reader that simply stopped are all the same thing — no report yet. Follow up and get the findings. Only an explicit "nothing in this class" from that reader about that class lets you record it as empty. If a reader still won't report after a follow-up, the class failed — route it to step 8 rather than carrying it into the plan as zero findings. A run that reads silence as "nothing found" builds its plan from an empty set, stamps the source processed in step 9, and reports a clean sweep; since the stamp is what makes a re-run resume instead of redo, that session is marked handled for good. The failure looks exactly like success.
The signal classes this graph looks for in a session:
decision attractor
or evidence for an existing one. ("Reverted the validator; graph health rests on approval +
conventions.")pattern evidence (often an anti-pattern crystallizing).observation
supporting a pattern attractor, or a new pattern once it has ≥1 piece of evidence.Lay out, per note, what will be created and what will be updated — and just as importantly, what's being
skipped and why (already captured, too operational, not durable — see the sibling knowledge-graph
skill's conventions). Present it as [y / edit / explain]. One approval gates the entire run; there is
no per-note confirmation after this point except the re-pause condition in step 6. The Todoist sink
(step 7) and the promotion candidates (step 11) ride in this same plan, clearly marked.
The plan names the synthesis work. Say which it will be: linking the chronicle entry that already
exists for this session, or invoking meta-claude:session-chronicle to write one. Both are ordinary — a
session chronicled at the time and processed later arrives with its synthesis written; a session
processed fresh does not. It belongs here because what the chronicle covers is one of the things being
approved, and because a write that never appears in the plan is a write escaping the one gate this
pipeline rests on.
The plan also names what step 2 already wrote — the source note, and the archived transcript or the inlined page. Preservation runs before this gate by design, so a plan that doesn't mention it leaves the reader unable to tell what has already landed and what this approval is actually deciding.
Execute the approved plan in dependency order — notes that other notes will link to get written first (an attractor before the evidence that cites it, unless the attractor already exists). Re-pause only for genuine ambiguity or a conflict with the existing record (the plan said one thing and the graph, on write, turns out to already say something incompatible). Creating a new entity, attractor, or note that simply doesn't exist yet is not ambiguity — proceed without pausing.
The chronicle is written here, as a sibling of extraction. Whatever step 5 proposed — linking the
existing entry, or invoking meta-claude:session-chronicle to write one — happens now, alongside the
observations rather than before or after them. Neither feeds the other: both are made from the same
transcript. Link it from the source note when it lands, and do not extract from it once it exists.
Invoke meta-claude:session-chronicle; never reimplement it. It has its own rules about how it writes,
what it asks, and in what order — including a reflection-ordering constraint that is its to enforce, not
this skill's to second-guess. Call it and link what comes back.
For each finding from step 4: if it supports an existing pattern or decision, follow the sibling
knowledge-graph skill's Capture workflow and its Extraction Workflow: session — write the
observation under observations/, add it to the attractor's ## evidence section, and update that
attractor's map-entry annotation in maps/patterns.md or maps/decisions.md if the "so what" shifted.
If a finding names a plugin or skill not yet in entities/, add a lookup-only entity note and list it
under the right heading in maps/entities.md. If a finding is a genuinely new generalization with no
attractor yet, follow "Promoting a cluster to an attractor" rather than forcing a link — but per this
graph's judgment bar (capture when unclear), it is fine to record an observation and flag it as
unattached for a future run to cluster. A design choice with reasoning becomes (or feeds) a decision;
a recurring truth becomes (or feeds) a pattern.
Route any non-graph output (tasks) to the sink named for it, never as a note in the graph.
Tasks (Todoist). A finding that names a concrete, actionable follow-up — not a knowledge claim —
routes here: a fix to make, an issue to file, a skill to sharpen. These are created with the td CLI in
the claude code marketplace project. Approval mode is batch: the tasks are listed inside the
step-5 plan and approved once, together with the notes — this orchestrator does not add a second
confirmation. When creating them, always pass --labels next (or --labels waiting if the task is
genuinely blocked on something external); a label-less task falls out of the GTD filters. Run
td task add --help first if unsure of the flags. This sink runs after any note whose link it
references exists — never against a stale or missing one.
If a step in the approved plan fails, skip it and continue with the remaining independent steps rather than aborting the run. Collect every failure and report them together at the end, with enough detail to retry (what was attempted, what error came back), and offer to retry the failed steps now.
A signal class whose reader never reported belongs here too, even though it failed before the plan existed — that is the one failure with no error message attached, so a step that only collects throws will miss it. Name the class, say plainly that it was never inspected, and offer its re-read alongside the other retries. A class that drops out quietly between step 4 and this report is indistinguishable from a class that genuinely had nothing in it.
Write or update the processed: stamp on the source note — a YAML list entry with date:, ran:
(what was actually written), skipped: (what was deliberately not written, and why), and errored:
(what failed, per step 8). It's a list so history accumulates across augment-mode runs rather than being
overwritten.
Don't write the stamp while a fanned-out class is unaccounted for. If step 4 handed classes to subagents, every one must have either returned findings or explicitly reported none before this stamp goes on. While any is outstanding, withhold it, name the classes you never heard from, and offer the re-read. The stamp is what makes a re-run resume instead of redo, so stamping over an uninspected class doesn't lose it temporarily — it closes the source for good. This gate is about the fanout only: an inline read has no readers to hear from and inspected every class in this conversation, so it stamps normally.
Summarize created / updated / skipped / errored, then append a changelog entry to knowledge/changelog.md
following this graph's own conventions — see the "Session Protocol" section of the sibling
knowledge-graph skill for the format. If the run surfaced something a skill outside the plan could
act on, suggest it in the report rather than auto-running it.
Pending template patches belong in this report. This skill was generated from a plugin template and
has diverged since — you own it. When the plugin fixes a defect in that template, nothing about the fix
reaches this file on its own; the plugin's /graph-patch skill is what propagates it, and it only runs
when someone invokes it.
You cannot tell whether anything is actually pending, and must not imply you can. What has already been
applied here is recorded in .commons.yml under generated:, but what exists to apply lives in the
plugin's delta log — and this skill has no path to the plugin. Any plugin path written into this file was
resolved when the file was generated, so it names the version that generated it, not the one installed
now. Suggest /graph-patch and let it answer; never read the log yourself, and never auto-run it, since
it edits this prose under per-delta approval.
Withhold the suggestion on a cadence rather than raising it every run — a line that always appears carries no information:
generated: block in .commons.yml — nothing has ever been propagated here. Raise it every run;
the condition ends the first time /graph-patch runs.[patch-check-suggested] into this run's changelog entry, and before raising it again scan both
knowledge/changelog.md and the most recent knowledge/changelog/YYYY-MM.md for that marker. Both
halves matter: free-form prose isn't reliably recognizable to the run that has to find it, and this
graph rotates its changelog monthly, so scanning only the live file would re-raise at every month
boundary.A category with no type is a schema gap — report it. Separately from the notes this run wrote: did
the session keep naming a kind of thing this graph has no entity type for? This graph declares
plugin and skill. If a run keeps naming agents, hooks, or MCP servers as things a reader would look
up, that isn't a missing note — it's a missing type, and no amount of note-writing fixes it. Ask this
whether or not the graph already has an entity type: one that declared none is exactly the graph most
likely to need its first.
The bar is recurrence, not one sighting. Several distinct instances of one category, each named as
something you'd look up, none covered by plugin or skill — and existing notes in the graph can supply
corroborating instances when a single run is thin. Name the category and list the instances evidencing
it, so the reader judges from evidence rather than from your assertion.
Recommend; don't do. Where an entity: list already exists, the work is a new entry in
.commons.yml, a directory, a map, and backfilling notes for the instances you found. Where there is
none, the work is establishing the tier — the larger ask, and worth saying rather than folding in.
Either way the sanctioned route is re-interviewing graph-init's block 2; .commons.yml belongs to
graph-init, never to this skill.
Gate it the same way as pending patches. When you raise a category, write [entity-type-gap: <category>]
into this run's changelog entry. Before raising one, scan both knowledge/changelog.md and the most
recent knowledge/changelog/YYYY-MM.md for the literal prefix [entity-type-gap:, read the category
names already recorded under it, and judge whether yours is the same gap by meaning — not by string
match. Nothing holds the wording stable between runs: "MCP servers" one month and "MCP server
integrations" the next are one gap already raised, and matching the whole marker literally would re-raise
it every run while each run looked locally correct. Suppression is per-category, so a genuinely different
gap still surfaces; it expires when the marker rotates out of both files, letting a still-recurring
category earn a second mention.
After the plan in step 5 is assembled — before presenting it — ask one more question of the run's
findings: does any of this generalize beyond claude-code-plugins? Candidates go into the same plan,
clearly marked as promotions, not into a separate pass someone has to remember to run.
Refresh the target before the screen runs. ~/Developer/commons is a shared graph other machines push
to, and the screen below reads whatever this clone last saw. If it's a git repo with a remote, fetch and
fast-forward first, then say what came in — new claims and graduated questions change the screen's answer,
and a stale copy makes "not already steering" decorative. The check is that the fast-forward actually
happened, not that the fetch returned: a fetch can exit cleanly while the fast-forward is refused because
the local copy diverged or the tree is dirty, and that reads exactly like "already current." With no
remote, no network, no git, a fetch error, or a refused fast-forward, run the screen anyway — but say
plainly it ran against a possibly-stale copy, rather than letting silence imply the target is current.
The screen, applied to each candidate:
~/Developer/commons, or ~/.claude/rules/ if promoting
from the commons) for something that already says this before proposing it again.When uncertain, propose anyway — a declined candidate costs one skip at plan review; a missed observation is invisible damage that never gets a second chance.
This domain, concretely. Craft principles tend to generalize out: human-in-the-loop refinement, spec-first, agent orchestration, prose-over-machinery — anything true of building good AI tooling in general, stated as method. What must never leave: this repo's file layout, the marketplace's structure, specific plugin versions or quirks, exact skill and agent names. If a lesson only holds because of these plugins' particular shape, it stays; if it would help a future self building something unrelated, it goes.
Approved promotion candidates are executed by invoking this plugin's promote skill once per candidate,
after the rest of the plan has run — a promotion derives from a note that needs to exist first.
24e103e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.