| name | accelint-archive-synthesis |
| description | Periodically lint the full OpenSpec archive for cross-change decision drift, index/spec reconciliation, and structural over-coupling, the gap nothing else in the QRSPI/OpenSpec stack covers, since every other drift check (accelint-onboard-openspec, accelint-architecture-doc, accelint-qrspi-apply Phase 4) only looks forward from a single change's own artifacts. Use this skill when the user wants to "run archive synthesis," "lint the openspec archive," "check for decision drift," "audit the spec archive for contradictions," "find stale specs," "reconcile the specs index," "check capability coupling," or when accelint-qrspi-archive has surfaced its own suggestion that 15+ changes have archived since the last synthesis run. Also use when the user asks whether an old design decision still holds given everything decided since, whether specs/INDEX.md still matches the actual spec.md files on disk, or whether some capability has become an over-coupled refactor candidate. This skill never runs automatically and never blocks another skill — it is always either a human-invoked audit or an offered suggestion the human accepts explicitly. |
| license | Apache-2.0 |
| compatibility | Requires the OpenSpec CLI, sub-agent support, and a project already onboarded with accelint-qrspi-archive so that openspec/changes/archive/INDEX.md and openspec/specs/INDEX.md exist and are populated. Routing confirmed findings requires the shared findings - interface (Mode 3 Refresh support) in whichever writer skill(s) a given finding targets; without it, this skill still produces its report but degrades to manual guidance for that step. |
| metadata | {"author":"accelint","version":"1.1.1"} |
Accelint Archive Synthesis
Read backward across the entire archived-change history to check whether it still agrees with itself, check the running index itself against the live files it claims to summarize, and surface any capability whose accumulated relationships suggest it has outgrown its own boundaries. This is the "lint" operation in Karpathy's LLM Wiki pattern: ingest already exists as accelint-qrspi-archive, query already exists as artifact loading at propose/apply time, but nothing periodically re-reads the whole corpus to check it is still internally consistent. Every existing drift check in this stack, including accelint-qrspi-apply Step 5's own hub-doc refresh, is forward-looking and scoped to one change's own proposal and design. This skill is the only one that looks the other direction — and, since accelint-qrspi-archive moved to row-level index patching for its own efficiency, the only one that ever re-checks specs/INDEX.md against the spec.md files it summarizes at all.
That backward-looking scope is also what keeps this skill's footprint small and deliberate. It reads two indexes and, for genuine candidates only, a handful of design.md files and, for the reconciliation check, a lightweight top-of-file read of every spec.md. It never rewrites a hub doc directly, and every write it does make — on either index — is a single targeted line, gated behind an explicit human confirmation of that specific finding, and never runs on its own initiative. A human always decides which findings get acted on.
What This Skill Does
Automates: a periodic, corpus-wide consistency check across every archived OpenSpec change, surfacing contradictions between past decisions, flagging capabilities that have become structurally over-coupled, and flagging specs/INDEX.md rows that have drifted from the spec.md files they summarize.
Scope: openspec/changes/archive/INDEX.md, openspec/specs/INDEX.md, and — for the reconciliation check only — a lightweight read of each spec.md's ## Purpose heading and related: frontmatter. This skill never originates a change, never implements a fix, and never edits a hub doc itself.
Output: a CRITICAL / WARNING / SUGGESTION report in the same register as /opsx:verify, plus, only after a human confirms a specific finding, a Status column update on the relevant archive row, a single-row patch or removal on specs/INDEX.md, and/or an independent invocation of the affected writer skill(s) via the shared findings: interface — see Step 8 for which finding types get which.
Does NOT: run automatically, run as a blocking step inside any other skill's workflow, rewrite any document directly, write any archive/INDEX.md column other than Status, write to specs/INDEX.md beyond a single confirmed row's patch or removal, introduce any severity or status state beyond CRITICAL/WARNING/SUGGESTION and current/superseded, or reconcile a contradiction on its own judgment without a human confirming which side of it stands.
Prerequisites
- OpenSpec CLI installed and initialized, with
accelint-qrspi-archive already in regular use — this skill is a consumer of the indexes that skill produces, not a replacement for it.
openspec/changes/archive/INDEX.md and openspec/specs/INDEX.md both exist and contain at least one row. See Verification Task B.
- Sub-agent support, for the same reason
accelint-qrspi-archive's Steps 2 and 5 require it: opening a candidate change's design.md, or a capability's spec.md for reconciliation, should return a structured extract to the parent, not the raw file contents. This skill's Step 3 and Step 4 both always delegate those targeted reads to subagents.
- Read access to every capability directory under
openspec/specs/ — plain file reads are sufficient, no OpenSpec CLI shellout needed, the same reasoning accelint-qrspi-archive already applies to its own local spec reads. Step 4 is the only step that needs this beyond the two index files.
- For routing to actually land anywhere beyond the report itself, the four writer skills' Mode 3 Refresh path needs to accept a
findings: list (the same shared interface accelint-qrspi-apply Step 5 uses). If a targeted writer skill doesn't yet support this, this skill still produces the finding — it just can't hand it off automatically (see Error Handling).
Build Order Context
This skill is deliberately the last of four pieces to exist, and each of the other three is a hard dependency, not a nice-to-have:
accelint-qrspi-archive has to already be in regular use. It's what produces both indexes this skill reads and the related:/frontmatter fields Steps 3, 4, and 5 depend on — without it, there is no corpus here at all.
accelint-qrspi-propose's Phase 2 extension (reading specs/INDEX.md at propose time) needs to exist so that specs/INDEX.md is actually a load-bearing artifact in the workflow, not just a side effect of archiving — otherwise nothing downstream would notice or care if this skill's structural-coupling flags ever pointed at stale data.
- The shared
findings: interface — the Mode 3 Refresh extension across the four writer skills, paired with accelint-qrspi-apply Step 5's matching prompt change — has to land before this skill has anywhere to route a confirmed decision-drift or structural-coupling finding at all. Without it, Step 8 has a report and a human decision but no destination for those two finding types. Index reconciliation findings (Step 4) don't depend on this at all — they never route anywhere, confirmed or not, so that check works the same regardless of whether this interface exists yet.
The practical consequence: if any of these three aren't in place yet, this skill can still run Steps 1 through 7 (scan, detect, report) and produce a valid report, but Step 8 degrades to "tell the human what to do manually" for whichever half of the interface is missing, on decision-drift and structural-coupling findings only. It is never a reason to refuse the run outright.
The Two Indexes This Skill Reads (Owned Elsewhere)
Neither index below is written by this skill in the general case. Both are written and maintained by accelint-qrspi-archive; this skill only ever reads them, with two narrow exceptions, both spelled out under Step 8: a Status column update on archive/INDEX.md, and a single-row patch or removal on specs/INDEX.md.
openspec/changes/archive/INDEX.md, one row per archived change:
| Change | Date | Decision | Specs touched | Status |
|---------------|------------|---------------------------------------------------------|-------------------------------------|---------|
| add-live-sync | 2026-03-02 | polling with 5s interval, no infra budget this quarter | sync/protocol, ui/status-indicator | current |
openspec/specs/INDEX.md, one row per capability, rebuilt on every archive:
| Capability | Purpose | Related | Last touched by |
|---------------|----------------------------------------------------------|------------------------|------------------------------|
| sync/protocol | Defines how client and server exchange live state | ui/status-indicator | add-live-sync (2026-03-02) |
The Decision column is a condensed one-line summary of that change's design.md frontmatter (choice + rationale). This skill's Step 3 works off that summary first, cheaply, and only opens the full design.md — for its alternatives: field and any nuance the one-liner compresses away — for the specific candidates flagged as plausibly contradictory. This mirrors the same cost-control discipline accelint-qrspi-archive uses when it defers opening a change's folder until its own index flags it as relevant.
The Log This Skill Owns
Neither index above records a synthesis checkpoint — that's expected, since both are owned entirely by accelint-qrspi-archive and neither has a reason to know this skill exists. So this skill maintains one small file of its own, openspec/changes/archive/SYNTHESIS-LOG.md, purely to answer two questions neither index can: "how many changes have archived since I last ran," and "has a human already looked at this specific decision-drift pair and judged it not real." It's a plain append-only list of one line per completed run, date plus the archive/INDEX.md row count checked through, plus an optional dismissed: sub-list of decision-drift pairs a human explicitly dismissed that run.
This file is not a column of either index and is never touched by accelint-qrspi-archive — it exists solely so Step 2 and Step 1, Task A below can read it, and Step 3 can filter against its accumulated dismissed: history. Step 9 is the only step that ever appends to it, and it only ever appends — a dismissal recorded on one run is never later removed by this skill itself; if a human wants to reconsider a dismissed pair, that's a manual edit to the log file, outside this skill's own write path. The exact line format, including the dismissed: sub-list, is shown where Step 9 writes it.
Only decision-drift dismissals get this treatment — structural coupling dismissals don't, on purpose. A decision-drift pair is anchored to two specific, immutable archived changes; a human's judgment that a specific pair isn't a real contradiction stays true forever, since the underlying design.md files never change. A structural coupling finding is a live snapshot of a related: count that moves every time accelint-qrspi-archive runs — permanently silencing "sync/protocol is over-coupled" would blind this skill to sync/protocol's count climbing further, which is exactly the trend this signal exists to catch. So structural coupling findings always resurface every run regardless of a prior dismissal; only decision-drift dismissals are ever written to the log.
Findings Interface (Shared Contract, Not New)
Routing a confirmed finding to a writer skill uses the exact same findings: shape accelint-qrspi-apply Step 5 already uses — this skill is the interface's second caller, not a new one:
/accelint-architecture-doc
We found the following during periodic archive synthesis. Treat this as known
context and refresh the affected section(s).
findings:
- [Confirmed finding, rephrased as a plain factual statement, e.g., "add-live-sync
(2026-03-02) chose polling for sync/protocol citing no infra budget for a
message broker, but adopt-websocket-gateway (2026-09-14) later adopted a
message broker for an unrelated capability — worth confirming the original
budget constraint still holds before sync/protocol's spec is next touched"]
The writer skill merges this with its own codebase scan before presenting anything to the human — same Mode 3 Refresh path a manual run would take. Nothing about invoking it here is special-cased for this skill. This interface is used for decision-drift and structural-coupling findings only — index reconciliation findings (Step 4) never route through it, since a stale specs/INDEX.md row isn't a hub-doc content gap; see Step 8 for what happens to those instead.
Rephrasing discipline. Every line inside findings: is a plain factual statement about what the archive shows, never an instruction and never a conclusion the writer skill hasn't reached itself yet. "sync/protocol's stated budget constraint may no longer hold, given a later change's message-broker adoption" is correctly phrased; "update sync/protocol's spec to remove the budget constraint" is not — that second phrasing pre-empts the writer skill's own Mode 3 interview, which is exactly the judgment call this skill's Steps 7/8 split exists to keep with a human, not hand to a downstream skill as a foregone conclusion.
Relationship to /opsx:verify
This skill borrows /opsx:verify's CRITICAL/WARNING/SUGGESTION register deliberately, so a report from either reads as a familiar artifact — but the two check fundamentally different things. /opsx:verify runs once, per change, before that change archives: forward-looking, scoped to whether one implementation matches its own design.md and tasks.md. This skill runs periodically, across the whole archive, after changes have already landed: backward-looking, scoped to whether the archive as a whole still agrees with itself. A change can pass /opsx:verify cleanly the day it archives and still surface in a decision-drift finding two years later, once enough later changes touch related capabilities. The two are complementary, not redundant, and neither substitutes for the other.
Workflow Overview
┌───────────────────────────────────────────────────────────────────────────────┐
│ Step Action Output │
├───────────────────────────────────────────────────────────────────────────────┤
│ 1 Preflight Read SYNTHESIS-LOG.md, verify dependencies Go / no-go│
│ exist, sanity-check the two reasoned-default + notes │
│ thresholds (Tasks A, B, C) │
│ 2 Scan indexes Read archive/INDEX.md + specs/INDEX.md, load In-memory │
│ prior dismissed: pairs from the log model │
│ 3 Decision drift Coarse-scan Decision column for same/related- Candidate │
│ capability collisions, skipping already-dismissed findings │
│ pairs; SUBAGENT opens design.md for the rest │
│ 4 Reconciliation Confirm every spec.md still exists; read Purpose Candidate │
│ + related: from each and diff against its findings │
│ specs/INDEX.md row │
│ 5 Structural Median related-count across specs/INDEX.md, Candidate │
│ coupling flag outliers ≥5 and ≥2× median findings │
│ 6 Compile report Assemble CRITICAL / WARNING / SUGGESTION Draft │
│ findings, /opsx:verify register report │
│ 7 Human review Present report; human confirms, dismisses, or Confirmed │
│ defers each finding findings │
│ 8 Route Update Status, or patch/remove a specs/INDEX.md Docs + │
│ row (confirmed findings only) + invoke affected Status + │
│ writer skill(s) via findings: specs row │
│ 9 Log + report Append run checkpoint + any new dismissed: Summary │
│ pairs to SYNTHESIS-LOG.md, summarize │
└───────────────────────────────────────────────────────────────────────────────┘
Step 3's design.md reads and Step 4's spec.md reads both always delegate to
a subagent, one per candidate, regardless of how many candidates surface. This
keeps raw design.md and spec.md contents out of the parent's context on every
run, the same discipline accelint-qrspi-archive applies to its own Step 2
and Step 5.
Implementation Steps
Execute these steps in order without stopping between them unless an error occurs:
-
Preflight — confirm this run has something to check and that its two reasoned-default thresholds still look reasonable, before reading anything else.
Verification Task A — trigger cadence sanity-check. Read openspec/changes/archive/SYNTHESIS-LOG.md (introduced above, under "The Log This Skill Owns") to find the last completed run's date and the archive row count it checked through. If the file doesn't exist yet, this is the first-ever run — skip the cadence math entirely rather than dividing by a history that doesn't exist yet, and note in the report that no prior checkpoint exists. Otherwise, take the Date column across every archive/INDEX.md row appended since that checkpoint and derive an average time-between-archives. The 15-archived-changes-since-last-run trigger is a reasoned starting point, not a measured one, so if the actual cadence implies 15 changes would mean checking twice a year or checking every few days, surface this as an informational note in the final report — never adjust the threshold automatically. Changing it is a decision for the human reading the report, not this skill. This task never blocks a run: if a human manually invokes this skill the day after the last run, with zero new rows since the checkpoint, proceed anyway — say so plainly ("0 changes since the last run, proceeding at your request"), since freshness is informational, not a gate.
Verification Task B — dependency check. Confirm both openspec/changes/archive/INDEX.md and openspec/specs/INDEX.md exist and contain at least one row each. If either is missing or empty, stop and report that accelint-qrspi-archive needs to run first — this skill has nothing to read yet, and guessing at index content would be worse than declining. If the archive has fewer than roughly 10 rows total, proceed only if the human explicitly wants a low-signal run anyway; note plainly in the report that a corpus this small has little for cross-archive synthesis to find, per the same reasoning that put this skill last in the build order.
Verification Task C — structural coupling threshold sanity-check. "At least 5 and at least double the index median" is the same kind of reasoned-not-measured default as Task A's trigger count. Compute the actual median related: count across every row in specs/INDEX.md and note, informationally only, whether the fixed floor of 5 or the 2× multiplier look like they'd flag either far too many capabilities or none at all against this project's real distribution. Same rule as Task A: report the observation, never silently change the threshold.
If Tasks A and C surface nothing unusual, say so briefly and move on — these are sanity checks, not a mandatory finding every run.
- Scan Indexes — build an in-memory model of the archive without opening a single change folder yet.
Read archive/INDEX.md in full: every row's Change, Date, Decision summary, Specs touched list, and current Status. Read specs/INDEX.md in full: every row's Capability, Purpose, Related list, and Last touched by/date. Both files stay cheap regardless of how large the archive has grown — that is the entire point of maintaining them — so this phase never needs to look past them.
Also read every dismissed: sub-list across SYNTHESIS-LOG.md's full history (not just the most recent run — a pair dismissed three runs ago still needs to stay suppressed) and build one flat set of dismissed-pair identities, each in the form <change-A slug>|<change-B slug>|<capability> with the two slugs alphabetically sorted so the same pair always produces the same identity regardless of which run or which order they were first compared in. If the log doesn't exist yet, this set starts empty — same first-run handling as Task A.
- Decision Drift Detection — find archived decisions that no longer cohere with each other, without opening every
design.md in the corpus to do it.
Step 1 — group by shared or related capability. Using Specs touched from archive/INDEX.md and related: from specs/INDEX.md, cluster archived changes that touched the same capability directly, or touched capabilities each other's related: lists connect.
Step 2 — coarse scan. Before comparing anything, drop any pair already present in Step 2's dismissed-pair set (loaded during the Scan Indexes step) — a human already looked at that exact pair and judged it not real, and that judgment doesn't expire. For everything else, within each cluster, compare Decision one-liners for signals of tension. A starter list of opposing-choice pairs worth pattern-matching on (extend it as a project's own vocabulary reveals its own oppositions — this isn't exhaustive):
- polling vs. push/websocket
- synchronous vs. asynchronous
- in-process vs. external service/broker
- monolith vs. microservice/split
- eager vs. lazy (loading, evaluation, initialization)
- centralized vs. distributed
- client-side vs. server-side (validation, rendering, state)
Beyond direct opposing pairs, also flag a stated rationale later contradicted by a rationale in a more recent change touching a related capability, and a capability with several changes clustered nearby in time while its own spec's Last touched by date sits conspicuously earlier. This step works entirely off the index text already in memory — no file I/O. A coarse-scan hit is cheap to be wrong about: it costs one subagent call in Step 3, not a bad finding in the final report, so this list is intentionally biased toward over-flagging rather than under-flagging.
Step 3 — targeted verification. For each candidate the coarse scan flags, spawn one subagent per candidate pair to open the full design.md for each change involved and confirm or dismiss the contradiction using the complete choice/rationale/alternatives fields, not just the index's compressed summary. For example, given these two frontmatter blocks:
---
change: add-live-sync
specs_touched: [sync/protocol, ui/status-indicator]
decisions:
- id: D1
choice: polling with 5s interval
rationale: no infra budget for a message broker this quarter
alternatives: [websocket push, long polling]
---
---
change: adopt-notification-gateway
specs_touched: [notifications/dispatch]
decisions:
- id: D1
choice: message broker (RabbitMQ)
rationale: notification fan-out needed guaranteed delivery and retry
alternatives: [polling, webhook callbacks]
---
the subagent reads both choice/rationale pairs directly and confirms a genuine contradiction candidate: add-live-sync's stated constraint ("no infra budget for a message broker") is now in tension with a later change clearing exactly that obstacle for a different capability. Whether this actually rises to a finding, and at what severity, is settled by the rule below — not by this step alone. Return only a structured verdict (confirmed / dismissed, plus a supporting quote-free summary) to the parent — never the raw design.md contents.
Step 4 — staleness flag. Separately, flag any capability where specs/INDEX.md's Last touched by date is old relative to a cluster of recent, related activity nearby (per Step 1's grouping) — this doesn't require opening any design.md, since it's a pure date comparison already available from the index data already loaded in Step 2.
Classification rule. A confirmed contradiction is CRITICAL if the affected capability's specs/INDEX.md Last touched by date falls after the earlier of the two contradicting changes' dates — meaning something was built or touched on top of a decision that may no longer hold. Otherwise it's WARNING: the contradiction is real, but nothing has actively depended on the resolved version since. Both halves of this comparison come straight from the index model Step 2 already built, so the rule needs no new lookup and produces the same classification on a re-run given the same data. Staleness flags (Step 4) are always SUGGESTION-level, since they're a signal to look, not a confirmed contradiction.
- Index Reconciliation — check whether
specs/INDEX.md still matches the actual spec.md files it claims to summarize — the one thing nothing else in this pipeline verifies. accelint-qrspi-archive's Step 6 patches only the rows for capabilities the current batch's changes declared in specs_touched, and only performs a full corpus-wide scan once, at bootstrap, when the index doesn't exist yet. Every archive after that leaves every untouched row exactly as it was, indefinitely — a spec.md hand-edited outside the archive workflow, or a capability directory renamed or deleted directly, has no mechanism anywhere in this stack that would ever notice unless that specific capability happens to be touched by some future change. accelint-qrspi-archive's own SKILL.md names this gap explicitly and names this skill as the natural place to close it, since periodic, corpus-wide checking is exactly this skill's charter.
Step 1 — existence check (every row, no content read). For every row in specs/INDEX.md, confirm openspec/specs/<capability>/spec.md still exists on disk. This is a file-existence check, not a content read, so it costs nothing meaningful even against a large corpus. A missing file or directory is an immediate CRITICAL finding — every other step in this workflow that touches this row (Step 3's clustering, Step 5's median) is working from a reference that no longer resolves to anything.
Step 2 — content check (every row whose file exists). Read just the top of each spec.md — its ## Purpose heading body and related: frontmatter list, not the full spec body — and compare against that row's Purpose and Related columns. A mismatch on either is a WARNING finding: "specs/INDEX.md row for <capability> disagrees with its own spec.md — index says <X>, file currently says <Y>." This is a small, bounded read (frontmatter plus one heading, the same shape of read Step 3's detailed verification already does against design.md), not a full-corpus content scan — it stays proportionate to corpus size the same way Step 5's median calculation does, just with a per-row cost instead of a zero cost.
What this step does not do. It never writes anything itself — Step 4 is detection only. It never routes a confirmed finding to a writer skill either, since this isn't a hub-doc content gap — it's the index itself being wrong. What happens after a human confirms a reconciliation finding is Step 8's job, not this one; see Step 8 for the narrow, confirmation-gated write permission this skill has on specs/INDEX.md for exactly this case.
- Structural Coupling Signal — surface capabilities whose
related: count suggests they may have outgrown their own boundary — at zero extra cost, since the counts already exist in specs/INDEX.md for other reasons.
Compute the median related: list length across every row loaded in Step 2. Flag any capability whose count is both at least 5 and at least double that median as a SUGGESTION-level finding, phrased as a plain fact: for example, "sync/protocol relates to 14 other capabilities, more than double the index median of 6." This is a second reading of data the pipeline was already maintaining, not new bookkeeping, and it never requires opening any design.md.