PRD: Team Knowledge Capability
Status: Draft — awaiting founder approval
Version: 1.4
Author: Product Manager Agent
Date: 2026-09-19
Issue (canonical): #2995 — per docs/PRD_REGISTRY.md, the issue body is authoritative over this mirror
Parent: Master PRD #1
Scope model parent: PRD #1206 (Team as Capability Container) — not amended by this PRD; see §10
Delivery vehicle: tracker #2991 (media.net SRE Day-0 onboarding), track B2
Repos: upsquad-core (F2–F6, and F8/F9 services, tools and detectors) · upsquad-client (F7, and the F8 / F9 surfaces the UX acceptance binds)
Verified against: origin/main db20a314 (v1.0); the v1.1 Wiki.js additions against d02f4ad3, 2026-09-05
0. Locked Decisions (founder, 2026-09-05 — do not re-open)
These five were settled by the founder when the media.net plan was approved. Every requirement below is written to them; a design that contradicts one is wrong, not a trade-off.
- Reach. Agents must be able to reference any document the team's engineers have access to. The bound on an agent's corpus is the team's own access, not what somebody remembered to upload.
- Grain. Capabilities are held at the team level. A team is the unit that owns a connector credential, a corpus, a budget and a withhold.
- Knowledge access is team-scoped with NO clearance dimension. Clearance does not filter which documents a caller may retrieve. Clearance continues to govern MCP server floors, governance/approval paths, and the knowledge management surface (create/ingest/delete a source) — those are unchanged. See §9 non-goal NG-3, which is enforced by a guard, not by a promise.
- No compromise on fidelity. Knowledge is of utmost importance. Where correctness and convenience conflict, correctness wins; where a shortcut would make an answer look complete, we do not take it.
- New capabilities are named as new. This is the Team Knowledge Capability — a named capability with its own PRD, not a patch on an existing one.
Numbering
Feature ids F2–F7 are inherited verbatim from the approved plan so cross-references survive. Requirement ids are TK-<n>.<m> where <n> is the plan's feature number. There is deliberately no TK-1 — F1 belongs to the plan's LLM-connector track (PRD #2644, Model Gateway) and is not part of this capability. F8 and F9 are new in v1.3; they continue the sequence past the plan's F2–F7, carry no plan lineage, and follow the same TK-<n>.<m> rule.
1. Problem Statement
Who. The media.net SRE team — the first external team to run on UpsQuad — plus every team after them.
The scenario. An SRE engineer asks their agent: "what's our runbook for a Kafka partition rebalance, and what changed in it last week?" Their documentation lives in Wiki.js (founder, 2026-09-05), their code in GitLab, other shared material in Google Drive, their incidents in YouTrack. Which home holds the primary runbook corpus is precisely what the topology session settles — the media.net asks doc now requests per-home document counts, and the answer re-sequences F6 (§6).
Today that request fails in three independent ways, and each failure is invisible to the person asking.
1.1 The agent has chunks, not documents
Ingestion reads the uploaded bytes, extracts text, chunks it, embeds the chunks, and discards both the raw bytes and the extracted text (internal/knowledge/ingest.go:98 reads raw, :168 extracts, :207 persists chunks — nothing persists the document). rag_chunks.source_doc_id is documented in migration 004_rag_chunks_embeddings.up.sql:15 as "opaque reference to the originating document" — and there is no documents table anywhere in the tree for it to reference. grep -r knowledge_docs returns nothing.
Consequences that a user experiences directly:
- There is no "open the runbook" — only "here are three fragments that scored above a threshold".
- The
KnowledgeServicehas fourteen RPCs (proto/upsquad/knowledge/v1/knowledge.proto) and noGetDocument. - Chunks carry a materialised 64-token overlap (
internal/context/embedding/chunker.go:30-31, applied at:350), so even naive re-concatenation by a caller produces duplicated text. - A byte-level question ("what exactly does line 40 of the playbook say?") is structurally unanswerable.
1.2 What the agent can see is not what the engineer can see
The Google Drive connector authenticates as one platform-wide read-only service account loaded from GDRIVE_CREDENTIALS_FILE via ADC (internal/knowledge/gdrive_client.go:7-8, :34; documented at internal/knowledge/gdrive.go:12-16). The corpus is therefore bounded by "what somebody remembered to share with UpsQuad's service account" — a set that has no defined relationship to what the SRE team can read.
Nor do we capture what the upstream system thinks. The Drive file listing requests exactly id, name, mimeType, size, modifiedTime (gdrive_client.go:54) — no permissions, no owners. grep -r acl_hash and grep -r external_principals both return nothing. We have never recorded an external ACL, so we cannot answer "should this team be able to read this?" at all.
The founder's mandate is reach (locked decision 1). Today reach is a manual, lossy, un-auditable set intersection performed by whoever did the sharing.
1.3 The answer is as old as the last sync
A runbook edited five minutes ago is answered from the last interval sync. Our own research document designed the fix — docs/research/rag-connectors.md §4 specifies a three-tier criticality model whose top tier performs a query-time freshness probe and re-fetch. The interval half shipped (internal/knowledge/refresh_worker.go, RefreshSource); the query-time half did not. internal/context/retrieval/service.go:364 calls triggerStaleRefresh after the results are already chosen — it corrects the corpus for the next question, never this one. For an incident runbook, "the version from before the incident" is the wrong answer delivered confidently.
1.4 And there is no surface a team can use to fix any of it
Everything above is configured, if at all, by an operator with an API client. There is no team-level console: no team-grain connector credential, no coverage view, no restriction primitive for knowledge (mcp_server_units and llm_endpoint_units both carry binding_kind IN ('grant','withhold') — migrations 164:50 and 212:145 — while knowledge_source_units carries only visibility IN ('inherit_down','private','shared_siblings'), 107:44), and no tenant-facing budget RPC at the team grain at all (the only budget RPCs in the tree are agent-grain SetBudgetCap / GetBudgetStatus, proto/upsquad/agent/v1/agent.proto:83,86; team_budgets has never had a tenant-facing writer).
1.5 Why now
Tracker #2991 puts the media.net SRE team on their own VM inside two weeks. A pilot in which the agent answers "I don't have that document" — or worse, answers confidently from a fragment of a stale one — is a failed pilot, and it fails on the single dimension the founder has called non-negotiable.
2. Goals & Success Metrics
Goals
- G1 — Reach. For a declared pilot document set, the documents an agent can retrieve equal the documents the team's engineers can read.
- G2 — Fidelity. Any document behind any retrieved chunk can be fetched whole, byte-faithfully.
- G3 — Freshness. A high-criticality document edited at T is answerable from its edited state, not from the last sync.
- G4 — Containment. Team scoping is a proven property of the retrieval and document-fetch paths, not an intention.
- G5 — Self-service. A team Manager configures the whole capability from the Team page.
- G6 — No clearance dimension. Locked decision 3 holds under test, permanently.
Success Metrics (binary or numeric; each has a named instrument)
| # | Metric | Target | Instrument |
|---|---|---|---|
| M1 | Access-parity report over the pilot corpus: documents readable by the team but not retrievable by its agents (false negatives) | 0, modulo declared exclusions | TK-3.7 parity report |
| M2 | Same report: documents retrievable by agents but not readable by the team (false positives — the leak direction) | 0, no exclusions permitted | TK-3.7 parity report |
| M3 | GetDocument byte-fidelity: returned bytes hash to the stored content_hash | 100% | TK-2.8 integrity assertion |
| M4 | Documents in the registry with no retrievable raw/extracted artefact, post-backfill | 0 | TK-2.6 backfill report |
| M5 | Freshness lag for a high-criticality source, edit → answerable | ≤ 60 s | TK-4.6 measurement |
| M6 | Cross-team retrieval or document-fetch leakage in the scope test battery | 0 | TK-5.8 battery |
| M7 | Retrieval result set changes when only the caller's clearance changes, team scope held fixed | 0 differences | TK-5.9 guard (enforces NG-3) |
| M8 | Manager-persona task set (add member, connect source, withhold, read coverage, set budget) completed from the Team page alone | 5 / 5 | TK-7 persona walk |
| M9 | Gmail registered as a knowledge source, by any path | refused at the API | TK-6.7 guard (enforces NG-2) |
| M10 | Silent-empty retrievals attributable to the unset-min_score default | 0 | #2618 fix + regression test |
| M11 (v1.3) | Conflict-queue precision — confirmed ÷ surfaced, over human-adjudicated findings — with the funnel pairs considered → judged → surfaced displayed beside it | published from the first sweep, wherever it lands; no threshold is invented here, the requirement direction is precision over recall, and a threshold review is committed for after the pilot's first month of adjudicated verdicts (TK-9.7) | TK-9.7 funnel |
| M12 (v1.3) | Repo-map results returned to an agent without a staleness marker while map@commit ≠ HEAD | 0 | TK-8.7 guard |
| M13 (v1.3) | Sweep runs that did work and produced zero llm_usage_events rows attributed to the corpus-integrity agent | 0 | TK-9.21 reconciliation |
M2, M6, M7, M9, M12 and M13 are containment metrics: a regression in any of them is a P0, not a quality dip. M13 belongs here for the governance sense of containment: a sweep that consumed models without leaving usage rows is an ungoverned path inside our own product. M12 joins them because a map presented without its staleness is a confident answer about code that may have moved — the same failure shape as serving a stale document as current.
Source homes are F6-conditional (v1.1; predicate tightened v1.2). M1 and M2 are measured over the source homes whose connector has landed and whose ACL source is captured (TK-3.3) — both conditions, not either. A connector that ingests without an ACL source leaves "readable by the team" uncomputable for that home while its documents are already retrievable by agents, which is M2 reporting green over an unstated subset. "Outside M2's scope" is not "excluded from M2": M2 admits no exclusions (Phase-2 acceptance 3). A home with no captured ACL source is not measured by M2 at all and is named as such in the report; the declared-exclusions clause governs M1 only. Wiki.js joins the "readable by the team" population — and therefore both M1's denominator and M2's scope — when TK-6.8 ships, not before. A parity report published while a home has no connector must name the homes it did not cover; a report silent about an uncovered home overstates parity, which is exactly the M2 direction this PRD does not permit. This inflates no Phase-1 claim: claim bounds C1 and C4 (§14) stand unaltered.
3. Personas
| Persona | Clearance | What they do here |
|---|---|---|
| SRE engineer (media.net; the primary user) | L2–L3 | Asks the agent operational questions through Claude Code / Codex via the Model Gateway and the team MCP gateway. Never opens a UpsQuad admin screen. |
| Team Manager | L4 | Owns the team's corpus, connectors, restrictions and budget. Configures from the Team page. |
| Org admin | L5 | Approves the connector's existence and its egress; audits access parity across teams; answers a compliance question about what an agent could read on a given date. |
| The agent | (its own) | A first-class consumer of the contract. It needs whole documents, stable provenance, and errors it can act on — a silent empty result is the failure mode that hurts it most. |
4. User Stories
US-1 — SRE engineer, reach. As an SRE engineer, I want my agent to answer from any runbook, wiki page or ticket my team can already read, so that I don't have to re-upload documents that already exist. AC: for the declared pilot corpus, every document a team member can open in any covered source home — Wiki.js, GitLab, Drive, YouTrack — is retrievable by the team's agents; the parity report (TK-3.7) shows zero false negatives outside declared exclusions. "Covered" is the §2 predicate (connector landed and ACL source captured); a home that is not yet covered is named in the report, never omitted from it.
US-2 — SRE engineer, fidelity. As an SRE engineer, I want the agent to be able to open the whole runbook it just quoted, so that a partial match does not become a partial answer. AC: every retrieved chunk carries a document reference; GetDocument on that reference returns the full document; the bytes hash to the ingested content_hash; where the document exceeds the context budget the response is explicitly and visibly truncated, never silently trimmed.
US-3 — SRE engineer, freshness. As an SRE engineer mid-incident, I want the answer to reflect the runbook as it is now, so that I don't follow a procedure that was corrected an hour ago. AC: on a high-criticality source, a document edited at T is answerable from its edited state within 60 s; when live fetch cannot confirm currency, the answer says so rather than presenting stale content as current.
US-4 — Team Manager, self-service. As a team Manager, I want to connect our Wiki.js, GitLab and Drive with our own credential and see what the agents can now reach, so that I own my team's corpus without filing a ticket. AC: I connect a credential at the team grain; a coverage meter shows documents discovered / ingested / failed with reasons; no platform operator is involved.
US-5 — Team Manager, restriction. As a team Manager, I want to withhold a specific source from my subtree, so that a sensitive corpus stops reaching agents below me without me having to revoke anyone's access to the underlying system. AC: a withhold binding removes the source for the subtree and leaves sibling subtrees untouched; clearing it restores the previous set byte-identically; the UI states plainly that this restricts agent retrieval, not the human's access upstream.
US-6 — Team Manager, cost. As a team Manager, I want to see and set my team's budget from the same page I configure everything else, so that capability and cost live together. AC: a team-grain budget can be read and set from the Team page; the window semantics match the org grain (recurring, on the org's billing anchor, per PRD #2644 §9 / #2928); a cap of zero is not silently read as "no cap".
US-7 — Org admin, audit. As an org admin, I want to answer "what could this team's agents read on 12 August?", so that I can respond to a security or compliance question. AC: document-level access decisions are auditable — the registry records what was ingested, from where, with which captured ACL, and every GetDocument is an audited read.
US-8 — Org admin, containment. As an org admin, I want proof that one team's corpus cannot reach another team's agents, so that I can onboard a second team without re-auditing the first. AC: the scope battery (TK-5.8) exercises cross-team retrieval and document fetch and shows zero leakage; a withhold and a grant on the same source and subtree resolve to withhold.
US-9 — The agent, actionable failure. As an agent, I want a named, actionable error instead of an empty result, so that I can tell the user why I have nothing rather than inventing something. AC: no_results, below_threshold, access_revoked_upstream, source_unavailable, document_no_longer_exists and document_ref_unresolved (v1.2) are distinguishable at the contract; an unset min_score never silently applies a 0.7 floor (#2618).
US-10 — SRE engineer, repo awareness (v1.3). As an SRE engineer, I want the agent to know which repository and which directory a change belongs in, so that it stops proposing edits in the wrong place. AC: list_repos returns the team's repositories with a one-line responsibility each; get_repo_map returns purpose, responsibilities, a directory-ownership table, conventions and do-not-touch zones — always accompanied by how stale the map is; and the map never presents itself as a substitute for reading the code (TK-8.9).
US-11 — Team Manager, corpus integrity (v1.3). As a team Manager, I want to know where our documentation contradicts itself or the code, so that I can fix the source rather than watch agents answer confidently from the wrong half. AC: when retrieved passages disagree the answer states the disagreement and cites both, never blends them; confirmed findings arrive as a badge and a weekly digest rather than waiting to be discovered; every finding is permalinked so a fix task can point at it; and an empty queue tells me when it last ran and how much it checked.
5. Entity Model
Team (org unit)
├── knowledge_source_units ── binding_kind: grant | withhold [NEW: withhold]
│ └── knowledge_source
│ ├── connector credential @ TEAM grain (vault slot) [NEW: team grain in use]
│ ├── external access scope (Wiki.js wiki+locale /
│ │ GitLab group / Drive shared drive or folder) [NEW]
│ └── knowledge_doc (registry) [NEW ENTITY]
│ ├── source_id, source_doc_id, external_id
│ ├── filename, content_hash, size, modified_at
│ ├── acl_hash ← captured at sync [NEW]
│ └── raw_ref → blob store (raw + extracted) [NEW]
│ └── rag_chunks (existing; chunk_index ordered,
│ 64-token overlap materialised)
├── team_budgets ── read/write from the Team page [NEW: tenant-facing RPC]
├── repo (GitLab project) ── registry entity [NEW v1.3: F8]
│ └── repo_map ── purpose / responsibilities / directory-
│ ownership table / conventions / do-not-touch,
│ per-section LOCKS + provenance (run, commit,
│ curators) + staleness (map@commit vs HEAD) [NEW v1.3: F8]
├── conflict ── one of five classes, permalinked, with a
│ lifecycle (open/confirmed/fixed/dismissed/superseded) [NEW v1.3: F9]
└── members ── external_principals mapping (email join) [NEW]
The one structural change: today a chunk is the only knowledge entity that exists. This PRD introduces the document as a first-class, addressable, byte-faithful entity, and the captured external ACL as the thing that decides which documents a team's agents may address. (v1.3 adds two more first-class entities on the same principle — the repository and its curated map, so that an agent can reason about where code lives, and the conflict, so that incoherence in the corpus is a tracked object rather than a surprise in an answer.)
Deltas from the MCP capability model, deliberate:
- No clearance floor on content.
mcp_serverscarrymin_clearance(migration 162). Knowledge sources do not, and must not (locked decision 3). withholdis the only restriction primitive. No allow-list, no per-document grant table, no risk tiers. One primitive, mirroringmcp_server_units.binding_kindsemantics (migration164).- The external system stays authoritative. We register, mirror and serve; we never become the system of record. A document deleted upstream is a document that stops being current here.
6. Functional Requirements
Owner column: A = Ashik (core backend, per the current team split), V = Vaisakh (client portal). Correct at review if the split has moved.
F2 — Document registry + byte-faithful full-document retrieval [BOOTSTRAP]
Ends discard-at-ingestion. This is the foundation every other feature stands on, and it is what the knowledge MCP server (#2993) consumes.
| # | Requirement | Owner |
|---|---|---|
| TK-2.1 | A knowledge_docs registry exists, one row per ingested document, carrying at minimum: source_id, source_doc_id, external_id, filename, content_hash, size, modified_at, acl_hash, raw_ref. rag_chunks.source_doc_id resolves to exactly one registry row. | A |
| TK-2.2 | Both the raw bytes and the extracted text are persisted to a blob store at ingestion and are retrievable by raw_ref. Extraction is not repeated on read. | A |
| TK-2.3 | A GetDocument RPC returns a document by reference, with provenance (source, external id, filename, modified_at, content_hash, ingestion time). | A |
| TK-2.4 | GetDocument supports whole-document and bounded-range retrieval. When the request cannot be served whole, the response is explicitly marked truncated and states what was omitted. Silent trimming is a defect. | A |
| TK-2.5 | Document assembly from chunks de-overlaps the materialised 64-token overlap (chunker.go:30) and orders by chunk_index. Assembled text must not contain duplicated overlap regions. | A |
| TK-2.6 | A backfill maps existing rag_chunks.source_doc_id values to registry rows. Documents whose raw bytes are unrecoverable are recorded as raw_unavailable — never silently omitted from the registry. A backfill report states the counts. | A |
| TK-2.7 | Every GetDocument is an audited read (who, which document, when, granted or refused with reason), on the existing knowledge audit chain. | A |
| TK-2.8 | An integrity check asserts that bytes returned by GetDocument hash to the stored content_hash; a mismatch is surfaced as an integrity error and never resolved by silently preferring one artefact over the other. | A |
| TK-2.9 | Registry rows inherit the source's delete semantics: DeleteSource removes registry rows and their blobs in the same transaction boundary as the chunk deletion (founder Design Call D2, migration 098 header). | A |
F3 — Access Parity, Tier-1 (team grain)
Makes "what the agent can see" derive from "what the team can see". Tier-2 (per-user) is explicitly Phase 2 — see NG-1.
| # | Requirement | Owner |
|---|---|---|
| TK-3.1 | A knowledge source may hold a per-team connector credential, stored in the existing 3-grain vault slot model — provider_keys.team_id already carries '_default' (org), '<unit_id>' (team) and '_agent_<id>' (agent) grains (migration 208:18-19). No new credential store. | A |
| TK-3.2 | A source declares its external access scope: a Wiki.js wiki and locale (TK-6.8), a GitLab group, a YouTrack project (TK-6.4), a Drive shared drive or folder, a web source's URL (migration 109:41 already requires one) — or, for any home added later, the scope shape declared by that home's own F6 requirement. The declared scope is the outer bound of what sync will discover, and every home in F6 must have one — a source whose scope cannot be declared cannot be bounded. (v1.3: YouTrack named and the list closed generally; v1.2 asserted exhaustiveness over a three-item list while F6 had four homes.) | A |
| TK-3.3 | Sync captures the upstream ACL for every document — Drive permissions and owners; GitLab group membership; Wiki.js group membership and page rules once TK-6.8 lands (v1.1) — and records a stable acl_hash on the registry row. (Today the Drive listing requests neither field, gdrive_client.go:54.) A connector may not be counted as covered by F3 without an ACL source (sharpened in v1.3): a home whose permissions we cannot read is a home parity cannot cover, and it must be declared uncovered rather than quietly counted. This bars it from the parity claim, not from ingesting — TK-3.9 governs that case, and the earlier phrasing "may not enter F3" read as though it barred both. | A |
| TK-3.4 | external_principals maps upstream identities to UpsQuad members, joined on email. Unmapped principals are recorded, not dropped. | A |
| TK-3.5 | When a source has a team-grain credential, sync must not fall back to the platform service account. Absence of the credential is a loud, actionable sync_status='error' naming the missing credential — never a silent degradation to today's behaviour. | A |
| TK-3.6 | Connector egress passes the existing SSRF/egress guard (internal/knowledge/webfetch.go); a blocked host produces an error naming the host and the allowlist entry required. | A |
| TK-3.7 | An access-parity report, per source: documents the team can read but that are not ingested (false negatives), documents ingested that the team cannot read (false positives), and unmapped external principals. This report is the instrument for M1/M2. | A |
| TK-3.8 | ACL capture is auditable: an ACL change between syncs is recorded as a document-level event with the before/after acl_hash. | A |
| TK-3.9 (v1.3) | Ingestion is bounded by identity even where no ACL is captured. A home may ingest into the corpus while sitting outside M2's scope — TK-3.3 names no ACL source for YouTrack, and none exists for web. That is permitted only because the team-grain credential (TK-3.1) together with the declared external access scope (TK-3.2) bounds ingestion to what the team's own identity can read, per home. The bound is a property of the grain, not of the connector: §1.2's whole critique of the platform-wide Drive service account is that a platform-grain credential leaves the corpus with no defined relationship to what the team can read. Therefore: a home that authenticates must authenticate at team grain — a home whose credential is org-grain, platform-wide or env-held may not ingest at all, ACL source or none. The quantifier ranges over homes that carry a credential. web is a live connector src_type (internal/knowledge/service.go:66, live at :75) that carries none: it fetches a public URL through the egress guard, and its bound is its declared scope plus the public nature of its target (migration 109:41 requires a url), not an identity. A credential-less home is not an exception to the rule — it is outside the rule's domain, and saying so is the difference between a bound and a contradiction. Every home with no captured ACL, credentialled or not, is listed in the parity report as outside M2's scope. | A |
F4 — Query-time live fetch
Completes the platform's own freshness design (docs/research/rag-connectors.md §4), whose interval half already shipped.
| # | Requirement | Owner |
|---|---|---|
| TK-4.1 | A fetch_document(ref) path retrieves the current document from the upstream system at query time, through the team's connector credential and the egress guard. | A |
| TK-4.2 | The fetch is team-scope-checked before it is issued. A reference the caller's team cannot reach is refused without any upstream call being made. | A |
| TK-4.3 | Live fetch is available on high-criticality sources; low/med retain today's interval semantics. Which tier applies is a per-source setting, visible on the Team page. | A |
| TK-4.4 | Ingest-on-read: a live-fetched document that differs from the stored copy updates the registry, blobs and chunks, so the correction persists for the next question. | A |
| TK-4.5 | Live fetch is bounded: a per-request timeout, a per-source rate limit, and a defined behaviour on exhaustion (serve the stored copy marked as unverified, never as current). | A |
| TK-4.6 | Freshness lag (edit → answerable) is measured and reported for the pilot corpus. This is the instrument for M5. | A |
| TK-4.7 (v1.1) | fetch_document(ref) accepts a declared, enumerated set of ref types: Drive file id, GitLab repository blob path, YouTrack issue id, and Wiki.js page id or page path/URL. Where the upstream offers a stable id, the id is the identity and a path-shaped ref is resolved to it — paths move, ids do not. A ref type with no live-fetch adapter is refused by name, never silently degraded to the stored copy. | A |
F5 — Team-scoping enforcement seams [BOOTSTRAP]
Turns "team-scoped" from an intention into a proven property, and fixes the retrieval defects that make the corpus look emptier than it is.
| # | Requirement | Owner |
|---|---|---|
| TK-5.1 | Retrieval resolves the effective source set through the Org Model v2.4 resolver. Today internal/context/retrieval/service.go:387 calls SourceVisibilityResolver.EffectiveSourceIDsForScope directly — the v2.3-shaped walk — bypassing the v2.4 resolver that internal/knowledge/org_axis.go exists to feed, and with it the unit_accepted_offers cross-team gate. One resolver, one answer. | A |
| TK-5.2 | The knowledge axis is frozen in the session snapshot, like the tools/MCP axis. internal/runtime/session/snapshot.go:1-4 freezes configuration at session creation (AR-F09); knowledge is presently resolved per query and so changes mid-session while every other axis does not. | A |
| TK-5.3 | knowledge_source_units gains binding_kind IN ('grant','withhold'), matching mcp_server_units (migration 164:50). withhold is the only restriction primitive for knowledge — no clearance floor, no per-document grants, no risk tiers. A withhold at a unit removes the source for that subtree and does not touch siblings; clearing it restores the prior set byte-identically. | A |
| TK-5.4 | Agent scoping topology is unified. Knowledge uses a per-agent disable list (agent_knowledge_overrides, migration 100: absence = enabled), while MCP uses a narrow-only allowlist intersected at snapshot freeze (snapshot.go:49-58). Two shapes for one concept — converge them, preserving narrow-only semantics in both cases (an agent may never widen beyond its team's set). | A |
| TK-5.5 | Fix #2618: an unset min_score must not silently apply a 0.7 floor (retrieval/service.go:283-284 and :607-608). The default, whatever it becomes, must be explicit in the contract and a below-threshold outcome must be distinguishable from no-results (US-9). | A |
| TK-5.6 | Fix #2619 (single-hop recall returns a relevant chunk but misses the most complete one in the same document — the defect F2's document grain most directly relieves), #1401 and #1411. | A |
| TK-5.7 | Chunk hygiene: chunk boundaries, metadata and provenance are consistent enough that a document reassembled from chunks is byte-comparable to its stored extraction (the assertion behind TK-2.8). | A |
| TK-5.8 | A scope battery exercises cross-team retrieval and cross-team GetDocument, multi-parent (v2.4) reachability by two paths yielding one attributed result, and withhold beating grant on the same source and subtree. Instrument for M6. | A |
| TK-5.9 | A guard proves NG-3: for a fixed team scope, varying only the caller's clearance produces an identical retrieved document set. Instrument for M7. | A |
F6 — Connectors
| # | Requirement | Owner |
|---|---|---|
| TK-6.1 | GitLab repositories as a knowledge source, scoped to a declared group, using the team's credential. | A |
| TK-6.2 | GitLab wikis as a knowledge source, same scoping and credential. | A |
| TK-6.3 | Code-aware chunking (tree-sitter) for source files, so a chunk boundary falls on a syntactic boundary rather than mid-function. Prose chunking is unchanged. | A |
| TK-6.4 | YouTrack tickets as knowledge: issues and their comment threads ingested as documents, scoped to the projects the team's service user can see. | A |
| TK-6.5 | Grafana is Phase 2. Not built in this PRD. | — |
| TK-6.6 | Gmail is NEVER a corpus. Mail is reachable only as a live, per-user MCP tool. No mailbox is ingested, indexed, embedded or persisted. This is a privacy boundary, not a sequencing decision. | A |
| TK-6.7 | TK-6.6 is enforced as a guard: any attempt to register a Gmail/mail source is refused at the API, with a test asserting the refusal. Absence from the UI is not enforcement. Instrument for M9. | A |
| TK-6.8 (v1.1) | Wiki.js pages as knowledge: pages ingested as documents through the Wiki.js 2.x GraphQL API (page list + single-page fetch), scoped to the declared wiki and locale and bounded by what the team's credential can read, carrying title, path, tags and updatedAt as provenance. Content is markdown-native, so TK-6.3's code-aware chunking does not apply — the existing prose chunker is the correct instrument. The credential is a team-grain vault credential per TK-3.1; it does not inherit the env-held API key the Day-0 live arm (#3014) uses. | A |
F6 sequencing (v1.1) — conditional, because the deciding fact is not in yet.
- If the topology session confirms Wiki.js is the primary runbook corpus — the media.net asks doc now requests per-home document counts — then TK-6.8 is built first in F6, ahead of TK-6.1 (GitLab repositories). Both halves of the effort-against-value test point the same way: Wiki.js is markdown-native and needs no tree-sitter work, making it the smaller build (S/M against GitLab repositories' M/L), while being the larger corpus.
- If it does not, F6 keeps its v1.0 order and TK-6.8 lands beside TK-6.4.
- The git-sync lever, which can change the answer. Wiki.js 2.x can sync page storage to a git repository. If the media.net instance has that enabled against a GitLab repo — the topology ask now covers this — then TK-6.1 would double-cover the wiki: the same pages ingested twice, once as Wiki.js pages and once as markdown files in a repository. Double coverage is not merely wasteful; it corrupts the coverage meter and the parity report by counting one document as two. Where git sync is enabled, exactly one connector owns the wiki corpus and the other excludes it by path. Which one is an HLD decision; that the exclusion is explicit rather than emergent is a requirement.
F7 — Team page as the capability console (upsquad-client)
Delivered in upsquad-client. The two-frontend parity rule applies (SPA and Next portal, CI-checked per PR): a surface shipped to one frontend is not done.
| # | Requirement | Owner |
|---|---|---|
| TK-7.1 | Real add-member on the Team page — the current surface does not perform the action. | V |
| TK-7.2 | Knowledge management from the team surface: connect a source, see its status, refresh it, remove it — without leaving the Team page. | V |
| TK-7.3 | Withhold control (TK-5.3), with copy that states plainly it restricts agent retrieval and does not revoke anyone's access to the upstream system. | V |
| TK-7.4 | Coverage meter: documents discovered / ingested / failed, with failure reasons, driven by GetSourceStatus (which already exists, knowledge.proto:105) extended for document-grain counts. Counts are per source home, and a home with no landed connector is shown as not covered rather than omitted — an omitted home reads as full coverage. Wiki.js becomes a counted home when TK-6.8 ships (v1.1). | V |
| TK-7.5 | GetTeamBudget / SetTeamBudget RPCs and their Team-page surface. team_budgets currently has no tenant-facing RPC — the only budget RPCs are agent-grain (agent.proto:83,86). Window semantics follow the org grain (recurring, on the org's billing anchor); the request carries no period fields; a cap of zero must not be readable as "no cap". | A (RPC) / V (UI) |
| TK-7.6 | Harvest the unbacked mocks from the old portal — budget dashboard, scopes/risk, approver/SLA — and either back them with real data on this surface or delete them. A mock that looks like a control is worse than an absent control. | V |
| TK-7.7 | Access-parity report (TK-3.7) is visible to the Manager on the Team page, not only to an operator. | V |
| TK-7.8 (v1.3) | The console's information architecture follows the founder-approved mockup (2026-09-05): two tabs — Knowledge / Conflicts and Repositories. That approval is a nod to the shape; it is not approval of this PRD, which the founder approves separately. | V |
F8 — Repo Awareness (new in v1.3)
An agent that can read every document a team owns still cannot answer "where does this change go?" — that is a property of the code, not of the corpus.
| # | Requirement | Owner |
|---|---|---|
| TK-8.1 | Team Repo Registry. The team's GitLab repositories are first-class entities: project id, path, default branch, primary language, links, and a one-line statement of what the repository is responsible for. Bound to the team at the same grain as every other capability (F3). | A |
| TK-8.2 | Repo Maps. Per repository, an agent-facing map carrying: purpose; responsibilities; a directory-ownership table (path → owns → touch-when); conventions; and do-not-touch zones. | A |
| TK-8.3 (amended v1.4) | Maps are generated by a tool-less generation run under the identity of the team's visible repo-map service agent, which meters and caps it. It is not an agent session: it has no tools and never executes source. The run reads an immutable, read-only snapshot pinned to a commit: the repository's complete tree listing plus a bounded sample of file excerpts read at that commit. It never reads a live working tree or the retrieval index. The commit is recorded (TK-8.5). Source bytes are not retained after the run. Because the input is a sample, every agent read of a map states its coverage in the tool output itself: files read whole, files excerpted, and the complete tree-entry count. This is the same rule TK-8.7 applies to staleness. A sampled map never presents itself as a full read of the code (C6). | A |
| TK-8.4 | Maps are curated by the team, with per-section locks. A locked section survives regeneration verbatim; an unlocked section regenerates when the repository drifts. A regeneration that would have altered a locked section reports the divergence rather than silently keeping the old text — a lock must not become a way to not be told. | A |
| TK-8.5 | Provenance is first-class: every map carries its generation run id, the commit it was generated from, and the curator of each curated section. A map without provenance is not a map. | A |
| TK-8.6 | Freshness. GitLab webhooks drive incremental re-index and a map-staleness marker: map@commit against branch HEAD, plus how many commits behind. | A |
| TK-8.7 | Staleness is always shown to agents, in the tool output itself — never only in a UI a human might look at. An agent must never receive a map without being told how current it is. Instrument for M12. | A |
| TK-8.8 | Tools: list_repos, get_repo_map, get_repo_activity (a live commits/MRs digest), and search_code (rides F6's index). | A |
| TK-8.9 | The honesty requirement, carried from measurement. A governed checkout remains the primary instrument for deep code work. Maps and search_code narrow the search; they do not replace reading the code. The evidence is our own, and its size is stated with it: gwpoc FINDINGS #28 (rep-1) and #30 (rep-2), measured on our own corpus in the gwpoc rehearsal, n = 2 reps — the grep lane read whole files and caught what semantic retrieval missed, including two documentation sections contradicting each other. The two reps failed by different mechanisms (rep-1 truncation, since fixed; rep-2 retrieval depth), and #30 is the same defect this PRD already carries as TK-5.6 (#2619) — which is why rep-2 is the observation that still binds. Binding on artefacts as claim bound C6. | A |
F9 — Corpus Integrity (new in v1.3)
F2–F6 make the corpus complete and current. F9 is about it being coherent — and about never letting the platform paper over the fact that it is not.
| # | Requirement | Owner |
|---|---|---|
| TK-9.1 | Five conflict classes are first-class, named entities: doc↔doc contradiction, intra-doc inconsistency, doc↔code drift, duplication, ambiguity. | A |
| TK-9.2 | Supersession candidates — time-scoped or versioned facts — are suggested newer-wins and never auto-applied. The platform proposes; a human decides. | A |
| TK-9.3 | Detector (i) — answer-time surfacing (window-eligible). When retrieved passages disagree, the composition layer states the disagreement and cites both. It never silently blends them into one confident answer. This is the cheapest detector and the one that protects the user first, which is why it is first. | A |
| TK-9.4 | Detector (ii) — reporting. A report_knowledge_issue agent tool, and human in-context reporting whose primary entry is the doc-viewer / answer surface; the console button is the fallback (founder-ratified). Reports dedupe into existing conflicts as additional sightings, carrying the task context they were reported from — a second sighting is evidence, not a duplicate row. | A / V |
| TK-9.5 | Detector (iii) — reference checks at sync. Paths, scripts and flags a document mentions are existence-verified against the repo index. Mechanical, no LLM. | A |
| TK-9.6 | Detector (iv) — nightly claims sweep. Claim extraction (entity–attribute–value + source + timestamp), candidate pairs by similarity, an LLM judge adjudicates, and only judged-confident findings surface. | A |
| TK-9.7 | The precision funnel is displayed — pairs considered → judged → surfaced — and precision over recall is a requirement, not a preference. A queue that cries wolf is worthless; this is the same rule the platform already applies to its prod-enablement register, where precision beats recall because an entry nobody trusts is an entry nobody reads. No precision threshold is set here: a floor invented without data is the fresh-but-false failure, and this queue has no distribution to set one against yet. The commitment is instead to a review — after the pilot's first month of adjudicated verdicts, the threshold is reviewed against the real distribution, with TK-9.13's calibration loop as the mechanism expected to move the number. The commitment is to the review, not to a value. Instrument for M11. | A / V |
| TK-9.8 | Detector (v) — duplication clustering, with a single-owner recommendation (which copy should survive, and who owns it). | A |
| TK-9.9 | Detector (vi) — term-cluster ambiguity. Last, after every other detector has earned trust. Ambiguity is the class most likely to generate noise, and it ships into a queue that has already demonstrated its precision or not at all. | A |
| TK-9.10 | Queue lifecycle: open → confirmed (an assignee, and a fix task carrying the citations) → fixed; or dismissed; or superseded. | A / V |
| TK-9.11 | dismissed is sticky. It persists across re-syncs until either passage changes. A re-sync must never resurrect a dismissed conflict — a queue that forgets its own verdicts trains people to stop giving them. | A |
| TK-9.12 | Human adjudication is required for confirmed and dismissed. The platform never self-confirms a conflict, and never edits a tenant's document to resolve one. | A |
| TK-9.13 | Confirmed and dismissed verdicts feed judge calibration, so the funnel of TK-9.7 improves against real verdicts rather than against itself. | A |
Execution identity and cost attribution. The detectors above consume models, so who runs them and who pays are requirements, not implementation details.
| # | Requirement | Owner |
|---|---|---|
| TK-9.14 | The sweep runs as a registered service agent (corpus-integrity), owned by the team — never a platform side-channel job. Every LLM call it makes routes through the Model Gateway under its own agent identity: metered per request into llm_usage_events (cache-aware), guardrail-governed, audit-logged, and visible in the team's agent list like any other agent. The reason is not tidiness: an unmetered internal sweep would be exactly the ungoverned blind spot this platform exists to eliminate, and it would be ours. | A |
| TK-9.15 | Candidate pairing is embeddings only — no LLM call. Embedding spend lands on the tenant's own cloud bill through the configured embedding provider (PRD #996). | A |
| TK-9.16 | Claim extraction uses a small-tier model and is delta-driven at sync — it re-extracts what changed, never the corpus. | A |
| TK-9.17 | Conflict judging uses a stronger-tier model, over judged candidates only. The funnel of TK-9.7 is what bounds this spend; a funnel that does not narrow is a cost defect as well as a precision defect. | A |
| TK-9.18 | Answer-time surfacing (TK-9.3) makes ZERO model calls on the hot path. It is a pure lookup against the open-conflicts / claims index. This is precisely why the approved mockup's C-108 flag reads "already open": a new disagreement reaches the index through an agent report (TK-9.4) or the next sweep — never through an inline model call bolted onto a user's query. | A |
| TK-9.19 | Attribution split. Background hygiene bills to the corpus-integrity agent (team-grain rollup, shown as its own line in Models & Budget on the Team page, F7). Work done while serving a specific agent's query bills to that agent's turn. One rule, no third case. | A / V |
| TK-9.20 | Hard cap, and honest exhaustion. The agent-grain budget on corpus-integrity is a hard cap enforced through the standard gate chain — not an advisory alert. On cap-hit mid-run the sweep must report incomplete: budget-exhausted, naming what was and was not covered. A silently-partial sweep presented as complete is the silent-partial defect class this PRD already carries (TK-5.6 / #2619) reappearing one layer up, and it is worse here because the surface it feeds is a governance queue. Model tiers per role are team configuration under BYOK. | A |
| TK-9.21 | Reconciliation is a check, not a report. Every sweep run reconciles against the corpus-integrity agent's usage rows. A run that did work and produced zero usage rows reddens — that is the side-channel smell, and the check exists to make it impossible to ship one quietly. Instrument for M13. | A |
7. Observability — what users actually get
| Surface | Who reads it | What it answers |
|---|---|---|
| Coverage meter (TK-7.4) | Manager | "How much of our wiki actually made it in, which homes are not covered yet, and why did 12 documents fail?" |
| Access-parity report (TK-3.7, TK-7.7) | Manager, org admin | "Is there anything my team can read that our agents can't — or worse, the reverse?" |
| Unmapped-principals list (TK-3.4) | Org admin | "Which of our people did not map to a UpsQuad member, so their access is invisible to parity?" |
| Freshness lag (TK-4.6) | Manager | "How stale can an answer be?" |
| Document read audit (TK-2.7) | Org admin, auditor | "What could this team's agents read on 12 August, and what did they open?" |
| ACL-change events (TK-3.8) | Org admin | "When did this document's upstream permissions change?" |
| Conflicts queue + precision funnel (TK-9.7, TK-9.10) (v1.3) | Manager | "Where does our documentation contradict itself or the code — and is this queue worth reading?" |
| Repo map staleness (TK-8.6, TK-8.7) (v1.3) | Manager, and the agent | "How far behind HEAD is what the agent was told about this repository?" |
corpus-integrity agent spend (TK-9.14, TK-9.19) (v1.3) | Manager, FinOps | "What is keeping our documentation honest costing us — and is it inside its cap?" |
| Team budget (TK-7.5) | Manager, FinOps | "What is this team spending against what cap?" |
8. Non-Functional Requirements
- NFR-1 — Containment is fail-closed. Every scoping decision (team scope,
withhold, ACL) fails closed. A resolver error fails the search; it never falls back to a wider set. This preserves the existing stated behaviour atretrieval/service.go:373. - NFR-2 — Live fetch must not dominate the query path. Live fetch is bounded by timeout and rate limit (TK-4.5); exceeding either degrades to the stored copy marked unverified, and the p95 retrieval budget is not regressed for
low/medsources. - NFR-3 — Raw persistence is bounded and tenant-local. Blob storage reuses the existing ingestion size caps. On a single-VM install the blob store is on the tenant's own volume; no document bytes egress the tenant boundary by virtue of this feature.
- NFR-4 — Fidelity beats convenience. No path may return content that looks whole and is not. Truncation, staleness and integrity failures are all explicit in the response.
- NFR-5 — Multi-tenancy unchanged. Every new table carries
org_idand RLS consistent with the existing knowledge tables; the migration ceiling onmainis225. - NFR-6 — Cost visibility. Document fetch, live fetch and re-ingestion are attributable to a team for metering, consistent with PRD #2644's attribution model.
9. Scope
In scope
- F2 document registry, raw + extracted persistence,
GetDocument, backfill. - F3 Tier-1 (team-grain) access parity: team connector credentials, external access scope, ACL capture,
external_principals, parity report. - F4 query-time live fetch with ingest-on-read.
- F5 team-scoping enforcement seams and the named retrieval defects.
- F6 GitLab repos + wikis with code-aware chunking; YouTrack tickets; Wiki.js pages (v1.1), sequenced per §6.
- F7 the Team page capability console, including team-grain budget RPCs.
- F8 repo awareness (v1.3): the team repo registry, curated repo maps with locks and provenance, webhook-driven freshness, and the four repo tools.
- F9 corpus integrity (v1.3): the five conflict classes, six detectors delivered in the stated order, and the human-adjudicated conflicts queue.
Out of scope / Non-Goals (explicit)
- NG-1 — Tier-2 per-user access parity is Phase 2. v1 achieves parity at the team grain: the corpus equals what the team can read, not what each individual member can read. A document readable by only some team members is, in v1, readable by the team's agents. This is a deliberate simplification of the founder's team-grain mandate (locked decision 2) and it must be stated to the tenant, not assumed.
- NG-2 — Gmail is never a corpus. Live per-user MCP tool only. Enforced by TK-6.7, not by omission. (v1.1 — contrast Wiki.js, which is reachable live from Day-0 as
wikijs-mcp(#3014) and is corpus-eligible via TK-6.8. Having a live MCP arm is not what makes a source corpus-ineligible; the privacy judgement about a person's mailbox is. NG-2 is unchanged.) - NG-3 — Clearance is not a dimension of knowledge access. Explicit founder non-goal. Clearance keeps its role for MCP server floors, governance and approval paths, and the knowledge management surface (
internal/knowledge/service.go:33-53— L1 read, L2 ingest, L3 per-agent override, L5 manage). It gains no role in deciding which documents a caller may retrieve. Enforced by TK-5.9. - NG-4 — Grafana connector is Phase 2.
- NG-5 — No new retrieval algorithm. No re-ranker, no new embedding model, no hybrid-search redesign. The named defects (#2618, #2619, #1401, #1411) and chunk hygiene only. Embedding configuration is PRD #996's territory.
- NG-6 — Not a document management system. The external system remains the system of record. We register, mirror, serve and audit; we do not offer editing, versioning or as-the-source-of-truth storage.
- NG-7 — No cross-org sharing. Tenant isolation is unchanged and is not a feature surface here.
- NG-8 — No per-document grant table.
withholdis the only restriction primitive (TK-5.3). Anything finer is a future PRD with its own justification. - NG-9 — Repo maps are not ACL objects (v1.3). A map describes a repository; it never grants or withholds access to one. Repository access parity rides F3's GitLab group grain exactly like every other home. A map must not become a second, softer permission model that drifts from the first.
- NG-10 — The platform does not edit a tenant's documents (v1.3). No auto-fix, no auto-merge of duplicates, no auto-applied supersession. Every resolution is a human-adjudicated task (TK-9.2, TK-9.12). A system that silently rewrites the corpus to remove a contradiction has destroyed the evidence that there was one.
10. Dependencies
| Dependency | Relationship |
|---|---|
| Tracker #2991 (media.net Day-0) | Delivery vehicle. This PRD is track B2. |
| #2993 upsquad-knowledge-mcp | Consumes TK-2.3/2.5 — its get_document tool is this PRD's contract surfaced as MCP. Filed separately; not delivered here. |
| #2992 tenant install profile | Hosts the blob store (NFR-3) on the single-VM install. |
| #2994 mcp-credential-helper | Carries the agent's identity to the team gateway; unrelated to scoping but on the same critical path. |
#3014 wikijs-mcp | (v1.1) The live-access arm for Wiki.js, shipping Day-0 as the sixth external MCP server, independent of this PRD. It answers "read this page now"; TK-6.8 answers "search the wiki as part of the team's corpus". Neither blocks the other, and #3014's README points back at TK-6.8 for the ingestion half. This is the same live-vs-corpus split this PRD already draws for Gmail — with the opposite corpus verdict (NG-2). |
| PRD #1206 (Team as Capability Container) | Parent scope model; not amended. #1206 resolved that membership grants availability of the team's RAG, and wrote its clearance ≥ floor predicate about tool invocation, not knowledge content. Locked decision 3 is therefore consistent with #1206, not a change to it. #1206 also lists "real RAG connector implementations" as out of its scope — this PRD is where they live. |
| Org Model v2.4 (matrix org: uniform units, multi-parent, union inheritance) | TK-5.1 depends on the v2.4 resolver; internal/knowledge/org_axis.go is already written against it. |
| PRD #2644 (Model Gateway) | A build dependency for F9 (corrected in v1.3): TK-9.14 routes every sweep LLM call through the gateway under the corpus-integrity agent's own identity (TK-9.15's candidate pairing is embeddings-only and does not touch it), so the metering, gate chain and audit hop are the gateway's. It remains merely a sibling capability for F2–F8. TK-7.5's budget-window semantics follow the ruling recorded there (#2928), and TK-9.20's hard cap rides the same gate chain. |
| PRD #996 (embedding) | Owns the embedding model and its configuration. NG-5 keeps this PRD out of it. |
Vault 3-grain slots (migrations 144, 208) | TK-3.1 reuses them; no new credential store. |
Egress guard internal/knowledge/webfetch.go | TK-3.6 / TK-4.1 reuse it; no second egress implementation. |
| Bugs #2618, #2619, #1401, #1411 | Absorbed as TK-5.5 / TK-5.6. |
| F6 code index | search_code (TK-8.8) rides F6's index; F8's registry and maps do not, which is why they can land earlier. |
| GitLab webhooks | TK-8.6's freshness signal. Absent them, staleness is still computed on read (§11) — the webhook makes it cheap, not correct. |
gwpoc FINDINGS #28 / #30 (upsquad-ai/upsquad-client : tools/gateway-poc/FINDINGS.md on main — the reachable canonical. The devbox operating copy at /opt/upsquad/ops/gateway-poc is a local-only twin: byte-identical as of 2026-09-05, proven by identical git blob b5c56279, in a repo with no remote. Cite the client-repo path; a reader cannot follow the devbox one.) | The measurement behind TK-8.9 and part of the case for TK-9.3: the grep lane caught the two documentation sections that disagreed; semantic retrieval did not. |
| media.net inputs | Wiki.js topology — the base URL and whether it is publicly resolvable, per-home document counts, and whether git storage sync is enabled (v1.1; reachability added v1.2), Drive topology, GitLab group structure, YouTrack project visibility, and the service accounts — the plan's Day-1 asks. TK-3 cannot start without them, and the F6 order (§6) stays undecided until the Wiki.js answers land. A self-hosted wiki on a private address needs an egress-allowlist entry before TK-6.8 can sync at all (TK-3.6). |
11. Edge Cases & Failure Modes
| Case | Required behaviour |
|---|---|
| Document deleted upstream between sync and query | Serve the persisted copy explicitly marked document_no_longer_exists. Never present it as current. |
| Upstream access revoked between sync and query (live fetch returns 403) | Fail closed. Drop from results, mark the registry row for ACL re-capture, and do not serve the cached copy. (Note the deliberate asymmetry with the row above: gone ⇒ serve with a marker; forbidden ⇒ do not serve.) |
| Document exceeds the context budget | Bounded, ordered, de-overlapped slice with an explicit truncation marker naming what was omitted (TK-2.4). |
| Naive chunk concatenation | Must de-overlap the materialised 64-token overlap (TK-2.5); duplicated overlap text in an assembled document is a defect. |
| Stored raw and reassembled chunks disagree | Integrity error surfaced (TK-2.8). Never silently prefer one artefact. |
| Source reachable by two paths under v2.4 multi-parent | One result, attributed to the binding that granted it. |
withhold and grant on the same source + subtree | withhold wins, mirroring mcp_server_units (migration 164). |
| Team has no connector credential | Loud sync_status='error' naming the missing credential (TK-3.5). Never fall back to the platform service account. |
| Egress guard blocks the connector host | Error names the host and the allowlist entry required (TK-3.6). |
| Wiki.js page moved or renamed between sync and query (v1.1; identity corrected v1.2) | Resolve by page id where one exists (TK-4.7). A path-shaped ref that no longer resolves is document_ref_unresolved — never an empty result, and never document_no_longer_exists, which asserts an upstream deletion we have not observed. Different upstream facts, different things the agent should say. The persisted copy is served alongside the marker (v1.3): the bytes are still valid, so serving them marked is both the most useful answer and the pattern of both sibling rows — "never an empty result" would otherwise be satisfied by a bare error. |
min_score unset | Must not silently apply 0.7 (TK-5.5). Below-threshold is distinguishable from no-results. |
| Gmail source registration attempted | Refused at the API with a test asserting it (TK-6.7). |
External principal does not map to a member (alias, + suffix, delegated account) | Recorded in the unmapped list and surfaced in the parity report. Never silently dropped — a silent drop is a silent parity failure. |
| ACL captured at sync is stale at query time | See OQ-2. Until resolved, the staleness window is bounded by the refresh interval and must be stated to the tenant. |
| Live fetch rate limit exhausted | Serve the stored copy marked unverified (TK-4.5). |
| Backfill finds a chunk whose raw bytes are unrecoverable | Registry row created with raw_unavailable; counted in the backfill report (TK-2.6). Never omitted. |
| Webhook missed or never delivered (v1.3) | Staleness is computed on read from map@commit against HEAD, so a lost webhook delays the re-index and never suppresses the marker (TK-8.7). The webhook makes freshness cheap; it is not what makes it correct. |
| A locked map section has drifted from the code (v1.3) | Regeneration reports the divergence and leaves the locked text intact (TK-8.4). A lock suppresses the edit, never the notification — otherwise a lock becomes a way to stop being told. |
| Repository removed from the team, or archived upstream (v1.3) | Its registry entry and map become unavailable to the tools, not silently stale. Permalinks to it resolve to an explicit no longer in this team's registry, so a fix task pointing at it does not dead-end. |
| A dismissed conflict whose underlying passage then changes (v1.3) | The dismissal lapses and the conflict re-opens (TK-9.11) — stickiness is scoped to the passages that were judged, not to the finding id forever. |
| The nightly sweep does not run (v1.3) | The queue's empty state must show the last successful sweep time and the pairs checked; "0 conflicts" from a dead detector and "0 conflicts" from a clean corpus are different facts and must not render identically. |
12. Risks
| # | Risk | Mitigation |
|---|---|---|
| R1 | A security property derived from a cache. acl_hash is captured at sync; a revocation upstream is not reflected until the next sync. A stale ACL grants an agent a document the team has lost. | OQ-2; live-fetch re-check on high criticality; bounded, stated staleness window; the 403 path fails closed (§11). |
| R2 | New tenant-data surface. Persisting raw + extracted bytes creates a document store that did not previously exist. | NFR-3 (tenant-local, existing size caps); TK-2.9 (delete cascades); OQ-1 (retention contract). |
| R3 | Phase 1 ships without access parity. In weeks 1–2 the pilot corpus is still bounded by the platform service account. | Claim bound C1 (§14). No access-parity claim — internal or external — until TK-3 is delivered and its parity report is green. |
| R4 | Email is a fragile join key for external_principals. Aliases and delegated accounts break it, and the failure is silent by default. | TK-3.4 records unmapped principals; TK-3.7 surfaces them; OQ-3 decides whether an explicit mapping surface is required. |
| R5 | F7 is in upsquad-client and the FE lags. Two prior PRDs (#2644 slices 1 and 2) were accepted backend-complete because the FE had not started, making persona-level goals unmeasurable. G5/M8 are FE-measured and would repeat that pattern. | Named here so it is a decision, not a discovery. F7 is first-class scope, subject to the two-frontend parity rule; if it slips, G5 is reported unmeasured rather than assumed met. |
| R6 | withhold will be read as an access revocation by Managers, and it is not — the human can still open the document upstream. | TK-7.3 copy requirement; OQ-4. |
| R7 | Scope creep into a DMS. A registry with raw bytes invites versioning, editing, and "UpsQuad as the docs home". | NG-6, stated as a non-goal rather than left to judgement. |
| R8 (v1.3) | The conflicts queue cries wolf and becomes shelfware. A detector that surfaces plausible-but-wrong findings costs a Manager's trust once and never regains it — and an ignored governance surface is worse than an absent one, because it reads as coverage. | TK-9.7 makes precision a requirement and publishes the funnel; TK-9.9 puts the noisiest class last; TK-9.12 requires human adjudication; TK-9.13 feeds verdicts back. M11 publishes the number from the first sweep whatever it is. |
| R10 (v1.3) | The sweep is an always-on model consumer a Manager did not consciously buy. Nightly claim extraction plus judging is recurring spend attached to a background job, and background spend is the kind people discover on an invoice. | TK-9.15–9.17 keep the expensive tier behind the funnel; TK-9.19 gives it its own visible line rather than burying it in a team total; TK-9.20 makes the cap hard and exhaustion loud; M13 proves the spend is attributed at all. |
| R9 (v1.3) | Repo maps get read as authoritative and displace the checkout. A confident, well-formatted map is exactly the artefact an agent will trust over reading the code — and gwpoc #30 is our own measurement of that failure. | TK-8.9 states the bound as a requirement; C6 binds every artefact; TK-8.7 forces staleness into the tool output so the map can never present itself as timeless. |
13. Open Questions
Four, each with a recommended default so nothing stalls. Unless the founder overrides, the recommendation is what gets built.
- OQ-1 — Raw-document retention. Chunks are hard-deleted on
DeleteSource(founder Design Call D2, migration098header). Do raw/extracted blobs follow the same hard delete, and is there any independent retention window? Regulated tenants may want both "keep the evidence" and "erase on request". Recommendation: same hard delete, in the same transaction boundary (TK-2.9); no independent retention window in v1; state plainly that the pilot has no automated retention sweeping. - OQ-2 — ACL staleness window. Between syncs, an upstream revocation is not reflected. (a) accept the interval staleness, (b) re-check ACL at query time for
high-criticality sources, or (c) require a live ACL check for any document whoseacl_hashis older than N. Recommendation: (b). It reuses F4's machinery, costs nothing onlow/med, and puts the strictest check exactly where the most sensitive documents are. Whichever is chosen, the residual window must be stated to the tenant. - OQ-3 — External-identity mapping. Join on verified primary email with an unmapped-principals report, or require an explicit member ↔ external-identity mapping surface? Recommendation: auto-map on verified primary email plus the report (TK-3.4/3.7). The report is what converts a silent parity failure into a visible one; an explicit mapping surface can be added later without changing the data model.
- OQ-4 — What a
withholdmeans to a human. For MCP,withholdsubtracts availability for a subtree. For knowledge, the withheld document usually remains readable by the engineer directly in Wiki.js, GitLab or Drive — we cannot revoke that. Iswithhold(a) an agent-retrieval restriction only, or (b) additionally a signal that the Team page hides the source from humans? Recommendation: (a), labelled as such (TK-7.3). Hiding a source a person can still open teaches Managers that the control is stronger than it is.
14. Phasing & Acceptance Criteria
Window-eligible arms (v1.3). Two arms of the new features are small enough to land inside the Day-0 two-week window, because each rides something that already exists rather than needing new infrastructure:
- F8 — the repo registry (TK-8.1) and maps delivered as markdown documents through the existing knowledge MCP server, i.e. no new service and no new index.
- F9 — answer-time surfacing (TK-9.3), which rides the same composition path.
These are not Phase-1 acceptance criteria and gate nothing. Phase 1's list below is unchanged from v1.0. If either arm lands in the window it is a bonus; if neither does, no Phase-1 criterion is affected. Everything else in F8 and F9 — the code index, search_code, get_repo_activity, and detectors (ii)–(vi) — is Phase 2 or later, riding F6 in weeks 3–5.
Phase 1 — Weeks 1–2 (runs alongside the media.net Day-0 install)
Scope: F2 in full; F5 started — TK-5.1, TK-5.3, TK-5.5, and the TK-5.8 battery stood up.
Rationale: F2 is what makes the corpus a corpus and is the contract #2993's get_document consumes. TK-5.1/5.3/5.5 are the seams that make "team-scoped" provable and stop the corpus looking emptier than it is. Neither can be deferred behind connector work without the connectors landing on an unproven base.
Phase 1 acceptance (all required):
knowledge_docsexists; everyrag_chunks.source_doc_idon the pilot corpus resolves to exactly one row (M4 = 0).GetDocumentreturns whole documents; returned bytes hash to the storedcontent_hashon 100% of a sampled set (M3).- Assembled documents contain no duplicated overlap region (TK-2.5), asserted by test.
- Backfill report published with counts, including
raw_unavailable. - Retrieval resolves through the v2.4 resolver (TK-5.1); the old direct call site is deleted, not merely bypassed.
binding_kindexists onknowledge_source_units; the withhold battery passes — subtree removed, sibling untouched, clear restores byte-identically,withholdbeatsgrant.- #2618 fixed; below-threshold distinguishable from no-results (M10 = 0).
- TK-5.9 guard green: varying clearance alone changes nothing (M7 = 0).
- Scope battery green for cross-team retrieval and cross-team
GetDocument(M6 = 0).
Phase 2 — Weeks 3–5
Scope: F3, F4, F6, F7, and the remainder of F5 (TK-5.2, TK-5.4, TK-5.6, TK-5.7).
Phase 2 acceptance (all required):
- A team connects each covered source home — Wiki.js, GitLab, Drive — with a team-grain credential; sync uses it; the absent-credential case produces the loud error of TK-3.5, verified as a failure case and not only as a success case. The grain is checked per home, and explicitly for Wiki.js: TK-6.8's connector reads a team-grain vault credential and not the env-held API key the Day-0 live arm (#3014) uses. A home syncing from an env-held or platform-wide credential fails this criterion however green its coverage meter. Discharged only by the discriminating run (v1.3): with the env key present, a successful sync is consistent with either credential source, so inspection cannot settle it — the sign-off must show vault slot emptied ⇒ sync fails with TK-3.5's loud error, and env-held key absent or altered ⇒ sync still succeeds.
- ACL captured for every document;
acl_hashpopulated; an upstream ACL change produces a recorded event (TK-3.8). - Parity report published for the pilot corpus: M2 = 0 with no exclusions, M1 = 0 modulo declared exclusions, unmapped principals listed. (v1.3 — "no exclusions" is about M1's exclusion clause not applying to M2. A home that ingests without a captured ACL source is outside M2's scope (TK-3.9) rather than excluded from it, and the report names it as such. This criterion is P0 and should not require composing two sections to read.)
- Live fetch demonstrated on a
high-criticality source: edit → answerable within 60 s (M5); the 403 path fails closed; the exhaustion path serves a copy marked unverified. - Ingest-on-read persists the correction (TK-4.4).
- GitLab repos + wikis and YouTrack tickets ingest under the team credential; code chunks fall on syntactic boundaries. Wiki.js pages ingest under a team-grain credential (TK-6.8) carrying title/path/tags/
updatedAtprovenance; where Wiki.js git sync is enabled, exactly one connector owns the wiki corpus and the coverage meter shows no double-counted document. (v1.1; F6 order per §6.) - Gmail registration refused at the API, asserted by test (M9).
- Knowledge axis frozen in the session snapshot (TK-5.2); agent scoping topology unified (TK-5.4); #2619 / #1401 / #1411 closed.
- Team page persona walk: 5/5 (M8), passing the two-frontend parity check.
GetTeamBudget/SetTeamBudgetlive; request carries no period fields; a cap of zero is not readable as "no cap".
F8 and F9 acceptance (Phase 2+; the window-eligible arms above gate nothing)
F8:
list_reposreturns the team's registry with a one-line responsibility per repository; a repository outside the team's F3 GitLab group grain does not appear.- A map is generated from a pinned commit through the TK-8.3 snapshot, and carries run id, commit and per-section curator (TK-8.5). A map lacking any of the three fails. (v1.4) No map reaches an agent, on any read path, without its TK-8.3 coverage statement.
- Lock behaviour proven in both directions: an unlocked section regenerates on drift; a locked section survives verbatim and the would-be change is reported (TK-8.4). Proving only the survival half leaves a lock that silences.
- M12 = 0 — no map reaches an agent without its staleness marker, exercised with
map@commit ≠ HEAD(TK-8.7). search_codeandget_repo_activityreturn against the pilot repositories; neither is described anywhere as a substitute for the checkout (C6).
F9:
- Two documents that contradict each other produce an answer that states the disagreement and cites both — verified with a seeded contradiction, asserting the blended answer does not occur (TK-9.3).
- A supersession candidate is suggested and not applied; the corpus is byte-unchanged until a human acts (TK-9.2, NG-10).
report_knowledge_issueand the in-context human report both dedupe into one conflict with two sightings, each carrying its task context (TK-9.4).- The nightly sweep publishes its funnel — pairs considered, judged, surfaced — and M11 is published whatever it is (TK-9.7).
- A dismissed conflict survives a re-sync, and re-opens when either passage changes (TK-9.11). Both arms required; the first alone is a queue that forgets, the second alone is a queue that nags.
- No conflict reaches
confirmedordismissedwithout a human verdict (TK-9.12). - The
corpus-integrityagent appears in the team's agent list, and a sweep's LLM calls appear inllm_usage_eventsattributed to it — cache-aware, guardrail-governed, audit-logged (TK-9.14). - M13 = 0, verified as a failure case and not only a success case: a sweep run forced to produce no usage rows must redden the reconciliation (TK-9.21). A check that only ever passes is not the check.
- Answer-time surfacing issues zero model calls, asserted on the hot path — not inferred from latency (TK-9.18).
- Cap-hit is exercised, not assumed: a run driven into its cap reports
incomplete: budget-exhaustedand names what was not covered; the partial result is never presented as complete (TK-9.20). - Attribution split verified in both directions: a background sweep bills
corpus-integrity; conflict work done while serving an agent's query bills that agent's turn (TK-9.19).
UX acceptance (v1.3) — binding on F7's console and the F8 / F9 surfaces
From the founder-requested design sweep against the approved mockup. These are acceptance criteria, not guidance:
- Permalinks. Every conflict and every repository has a stable, addressable id (the
C-108shape from the mockup). A fix task, an issue, or a Slack message must be able to point at exactly one. - Empty states prove liveness. "0 conflicts — last sweep 03:14, checked 1,204 pairs." An empty queue must be distinguishable from a dead detector — the same defect class as this platform's own silent-skip greens, where a check that never ran and a check that passed rendered identically.
- Discoverability is push, not pull. A tab badge and a weekly digest/notification for new confirmed-severity findings. A queue behind an unbadged tab is shelfware — R8 by another route.
- Dismissed items are hidden behind a filter by default, and reachable through it.
- Taxonomy chips are neutral; only severity carries semantic colour. Colouring the class as well trains the eye to read severity where none was stated.
- The detail drawer overlays with an explicit close affordance on narrow screens.
- Row selection is keyboard-accessible —
tablist/tabpanelsemantics with managed focus. Accessibility is acceptance here, not aspiration.
Claim bounds (binding on all internal and external artefacts)
- C1 — Until TK-3 is delivered and its parity report is green, no artefact may claim access parity, "agents see what your team sees", or equivalent. Permitted form during Phase 1: "agents retrieve from the corpus connected to the team."
- C2 — No artefact may describe knowledge access as clearance-governed. It is team-scoped. Clearance's remaining roles (MCP floors, governance, knowledge management) may be described as such and must not be generalised.
- C3 — Until F4 ships, freshness is "as of the last sync", never "live" or "always current".
- C4 — v1 parity is at the team grain (NG-1). No artefact may imply per-user parity. The tenant must be told which grain they are getting.
- C5 — Nothing may describe UpsQuad as the system of record for a tenant's documents (NG-6).
- C6 (v1.3) — No artefact may present repo maps or
search_codeas a substitute for a governed checkout. Permitted form: "maps and code search narrow where to look; the checkout is what reads the code." Grounded in gwpoc FINDINGS #28 (rep-1) and #30 (rep-2) — measured on our own corpus (gwpoc rehearsal, n = 2 reps), the platform's only controlled two-lane comparison. Both reps produced a confidently incomplete answer from the retrieval lane where the grep lane was complete, but the mechanisms differed: rep-1 was our own display-cap truncation (since fixed), rep-2 was retrieval depth (#2619 / TK-5.6, still open). That makes rep-2 the load-bearing observation, because its cause is still live. The small n is part of the bound, not a reason to drop it: any artefact invoking this evidence states the basis, the n, and that the two mechanisms differed, and none may generalise it into a universal claim about retrieval systems (TK-8.9, R9). - C7 (v1.3) — Until the nightly sweep has published a funnel (TK-9.7), no artefact may claim the platform detects contradictions. Permitted form during the window: "when retrieved passages disagree, the answer says so and cites both" — which is TK-9.3 and is true the day it ships.
15. Pricing Tier Mapping
Fidelity is never a tier lever. Gating byte-faithful retrieval, team scoping or access parity by tier directly contradicts locked decision 4. Those ship at every paid tier, cloud and on-prem.
| Capability | Starter | Business | Enterprise | On-prem |
|---|---|---|---|---|
Document registry + GetDocument (F2) | ✅ | ✅ | ✅ | ✅ |
Team-scoping seams + withhold (F5) | ✅ | ✅ | ✅ | ✅ |
| Access parity, team grain (F3) | ✅ | ✅ | ✅ | ✅ |
| Team page console (F7) | ✅ | ✅ | ✅ | ✅ |
| Query-time live fetch (F4) | quota-limited | ✅ | ✅ | ✅ |
| Connector breadth (F6) | Drive + web + Wiki.js | + GitLab | + YouTrack, + Grafana (Ph2) | all |
| Repo registry + curated maps (F8) (v1.3) | ✅ | ✅ | ✅ | ✅ |
Code index, search_code, repo activity (F8) (v1.3) | — | ✅ | ✅ | ✅ |
| Answer-time conflict surfacing + reporting (F9) (v1.3) | ✅ | ✅ | ✅ | ✅ |
| Nightly claims sweep + conflicts queue (F9) (v1.3) | — | ✅ | ✅ | ✅ |
| Sources per team / corpus size | tiered | tiered | tiered | unmetered |
Tier levers are quantitative (source count, corpus size, live-fetch rate, connector breadth), never qualitative. Wiki.js sits at Starter (v1.1) for that same reason: for a team whose runbooks live in a wiki, gating the wiki connector above the entry tier is a qualitative gate on whether knowledge works at all. Answer-time conflict surfacing (TK-9.3) is at every tier for the same reason fidelity is (v1.3): telling a user that their sources disagree is an honesty property, not a feature, and a tier in which the platform silently blends contradictory passages is a tier that lies. The same applies to a repo map never arriving without its staleness. What is tiered is the machinery — the nightly sweep, the code index — never the honesty. On-prem is priced per agent-hour and includes the capability in full.
16. Changelog
- v1.4 (2026-09-19) — TK-8.3 amended to match the approved map-generation design. Two edits: one requirement and one acceptance line.
- Trigger: core#3455 gate 3. Founder decision D4 settled map generation as a tool-less server pipeline, not an agent session. The pipeline reads a complete tree listing plus a bounded sample of file excerpts at a pinned commit, through a read-only adapter, and deletes the source bytes when the run ends. D4 comes from the founder decision record, #3455
5743862793. That record was posted on the founder's behalf and, as of 2026-09-19, the founder had not confirmed it; approving this version confirms the D4 reading it relies on. v1.3's wording, "agent pass over a governed checkout", describes a different mechanism. Any implementation of D4 would have failed TK-8.3 and F8 acceptance 2 as written. - Ruling: the PRD wording changes, not the design. v1.3's wording protected three things, and all three are kept: a commit pin that is recorded (TK-8.5), never the live working tree, and never the retrieval index. The word "checkout" described a mechanism, not an intent. A full clone would bring a tool-holding session that executes source over untrusted repository content, and it would move every source byte. The output is an orientation artefact, and C6 already says it is not the instrument for reading code.
- Added obligation, and why it is not scope creep: under v1.3 a full checkout made the map's input complete by construction. A sample removes that guarantee. Locked decision 4 forbids a shortcut that makes an answer look complete, so coverage is now part of every map read, in the tool output, as TK-8.7 already requires for staleness. This is the design's own coverage marker (core#3455 rev 3.1 §C, product finding P2), recorded here so that it binds.
- Unchanged: TK-8.9, C6 and R9. Their "governed checkout" is the instrument for deep code work, which the map generator never was. That instrument is designed in core#3478. v1.3 used one phrase for two different things; v1.4 separates them. The requirement carries no numbers because the sample's default and ceiling are design parameters (core#3455 §F).
- Known drift, owed as a separate amendment and deliberately not folded here, because the brief covered TK-8.3 only:
- TK-8.1 names GitLab only, but the delivery covers GitHub and GitLab (D7).
- TK-8.6 says webhooks drive freshness, but D4 defers webhooks and makes polling the complete path.
- TK-8.8 lists
search_code, which D7 moves to core#3478.
- Canonical copy: issue #2995's body is authoritative (
docs/PRD_REGISTRY.md). It will be edited to match only after the founder approves this version.
- Trigger: core#3455 gate 3. Founder decision D4 settled map generation as a tool-less server pipeline, not an agent session. The pipeline reads a complete tree listing plus a bounded sample of file excerpts at a pinned commit, through a read-only adapter, and deletes the source bytes when the run ends. D4 comes from the founder decision record, #3455
- v1.3 (2026-09-05) — Repo Awareness (F8), Corpus Integrity (F9), UX acceptance, and two ledgered fixes. Trigger: the founder approved the Team-page mockup (2026-09-05 — two tabs, Knowledge / Conflicts and Repositories) and requested a design sweep; the mockup nod is approval of the shape, not of this PRD. Changes: F8 — Repo Awareness (TK-8.1–8.9): a team repo registry; per-repo agent-facing maps with a directory-ownership table, generated over a governed checkout pinned to a commit and curated with per-section locks whose divergence is reported rather than swallowed; provenance first-class; webhook-driven freshness with a staleness marker that is always in the tool output, never only in a UI (M12); the four repo tools; and the honesty requirement that a governed checkout stays the primary instrument for deep code work, grounded in our own gwpoc FINDINGS #28/#30 and bound as C6. F9 — Corpus Integrity (TK-9.1–9.13): the five conflict classes plus supersession candidates that are suggested, never auto-applied; six detectors in a deliberate order, beginning with answer-time surfacing (state the disagreement, cite both, never blend) and ending with term-cluster ambiguity; a displayed precision funnel with precision-over-recall as a requirement (M11); reports that dedupe into sightings; and a queue lifecycle whose
dismissedis sticky until either passage changes and whose verdicts require a human and feed judge calibration. UX acceptance (7 criteria, binding not aspirational): permalinks, empty states that prove liveness, push-not-pull discoverability, dismissed-behind-a-filter, neutral taxonomy chips, an overlay drawer with a close affordance, and keyboard-accessible row selection. Two ledgered fixes from the architect's #3016 review: TK-3.2's exhaustiveness claim now names YouTrack and closes the list generally (v1.2 asserted completeness over three shapes while F6 had four homes); and new TK-3.9 states the bound that makes an ACL-less home defensible — team-grain credential plus declared scope bounds ingestion to what the team's own identity can read, a property of the grain, so a home whose credential is not team-grain may not ingest at all. Also folded, both architect-identified and flagged as beyond the amendment brief: §11's page-move row now serves the persisted copy alongside the marker ("never an empty result" was satisfiable by a bare error), and Phase-2 acceptance 1 is discharged only by the discriminating run (with the env key present, a successful sync is consistent with either credential source, so the criterion was settleable by inspection). Supporting edits: US-10/US-11; §5 gains the repository, map and conflict entities; §2 gains M11/M12 and disambiguates outside M2's scope from excluded from M2; §7, §9 (NG-9 maps-are-not-ACL-objects, NG-10 the-platform-does-not-edit-tenant-documents), §10, §11, §12 (R8 cries-wolf, R9 maps-displace-the-checkout), §15. Unchanged: claim bounds C1–C5 (C6 and C7 are additions, not edits), NG-1 and NG-2, the phasing, and every Phase-1 acceptance criterion — F8 and F9 are Phase-2+ scope, and their two window-eligible arms (F8's registry + maps-as-markdown through the existing knowledge MCP; F9's answer-time surfacing) are named as gating nothing. Folded into this version rather than a v1.4 — the founder Q&A that ratified it landed the same day, before v1.3 had merged anywhere: F9 execution identity and cost attribution (TK-9.14–9.21). The sweep runs as a registered service agent (corpus-integrity) owned by the team, never a platform side-channel job, with every LLM call routed through the Model Gateway under its own agent identity — metered cache-aware intollm_usage_events, guardrail-governed, audit-logged, visible in the team's agent list — because an unmetered internal sweep would be exactly the ungoverned blind spot this platform exists to eliminate. Cost shape is stated per stage: pairing is embeddings only, no LLM (spend on the tenant's own cloud bill via PRD #996's provider); extraction is small-tier and delta-driven at sync; judging is stronger-tier over judged candidates only, bounded by the funnel; and answer-time surfacing makes zero model calls on the hot path, a pure index lookup — which is why the mockup'sC-108flag reads "already open". Attribution splits one way with no third case: background hygiene billscorpus-integrityat team-grain rollup with its own line in Models & Budget, and work serving a specific agent's query bills that agent's turn. The agent-grain budget is a hard cap through the standard gate chain, with honest exhaustion —incomplete: budget-exhaustednaming what was and was not covered, because a silently-partial sweep is TK-5.6's silent-partial defect class one layer up. M13 reddens when a sweep run produced no usage rows at all — the side-channel smell — and R10 names the recurring-spend risk the whole shape exists to contain. This also corrects §10: PRD #2644 was recorded as "not a build dependency", which TK-9.14 makes false for F9. Three judgement calls raised at routing were answered and are recorded here rather than left in a comment thread: C6 keeps its gwpoc basis, with the honest framing that it is measured on our own corpus (gwpoc rehearsal, n = 2 reps) and that the small n is part of the bound rather than a reason to drop it; M11 invents no threshold and instead commits to a review after the pilot's first month of adjudicated verdicts (TK-9.7); and M12 stays a containment metric, because a map served without its staleness marker is the repo-shaped twin of serving a stale document as current, which locked decision 4 covers. Three further content notes from the architect's approving review of #3041 were then folded, still at v1.3: the header's repo mapping now covers F8 and F9 (it stopped at F7 while the UX acceptance binds the F8/F9 surfaces); TK-3.9's closing quantifier was scoped to homes that carry a credential, because as written it outlawedweb— a live connectorsrc_typethat carries none and is bounded instead by its declared URL and the public nature of its target; and C6's citation was corrected from #29/#30 to #28 (rep-1) / #30 (rep-2), because #29 is a containment finding about ungoverned Bash and never evidenced the completeness class at all. Correcting it surfaced something the bound now carries: the two reps failed by different mechanisms — rep-1 our own display-cap truncation, since fixed; rep-2 retrieval depth, still open — so rep-2 is named as the load-bearing observation. The irony is recorded rather than smoothed over: TK-3.9 is the second quantifier-over-an-unchecked-enumeration in two rounds, and both appeared in fixes written to answer review — v1.2's TK-3.2 asserted completeness over three scope shapes while F6 had four homes, and v1.3's TK-3.9 asserted a rule over all homes while the platform ships a credential-less one. Applying the architect's resulting rule — check a quantifier against the live enumeration, including an enumeration introduced in the same edit — to the whole document then found three more sites, all fixed here: TK-3.2 omittedweb's URL from the same scope list; TK-3.3's "may not enter F3" read as barring ingestion when it means barring the parity claim, so it now says "may not be counted as covered by F3" and composes with TK-3.9 instead of appearing to contradict it; and §10's Model Gateway row said "every sweep call" where TK-9.14 says every sweep LLM call, over-claiming against TK-9.15's embeddings-only pairing stage. The citation's location then took a third pass and is recorded here because it had been wrong in both directions: §10 now citesupsquad-ai/upsquad-client:tools/gateway-poc/FINDINGS.mdonmain, the copy a reader can actually reach, and notes the devbox copy at/opt/upsquad/ops/gateway-pocas a local-only twin — a real git repo, but one with no remote, so the earlier phrase "outside the fourupsquad-airepos" was confident and unreachable at once. Byte-identity was established by identical git blob hashes (b5c56279, 37,744 B) rather than by comparing text, which settles content and line numbering together. This is the discipline the whole C6 episode earned: a claim bound's basis has to survive being followed, so the pointer is verified the same way the finding was. - v1.2 (2026-09-05) — Premise sweep + three corrections. Trigger: the principal architect's review of the v1.1 mirror (PR #3015, APPROVED; content finding recorded on this issue as
5551961129) established that v1.0's document-home enumeration had been corrected where it was noticed, not everywhere it occurs — and that on approval the residual sites become binding as written. Changes: seven residual sites swept to the corrected topology. The architect named four (§14 Phase-2 acceptance 1; US-1's AC; TK-3.2; US-4); three more of the same two classes were found in the sweep — §5's entity-model diagram (TK-3.2's structural sibling), §7's coverage-meter illustration (which after v1.1 contradicted its own per-source-home requirement), and OQ-4's illustration (whose "we cannot revoke that" argument is equally true of Wiki.js). §14 item 1 additionally now checks the credential grain per home and names the #3014 env-key exclusion explicitly — the check TK-6.8's load-bearing sentence needed. §2's predicate tightened from "connector has landed" to "connector landed and ACL source captured (TK-3.3)", so the sentence an HLD author lifts verbatim matches the requirement. §11's page-move answer renameddocument_no_longer_exists→document_ref_unresolved: the old name asserts an upstream deletion we have not observed and so contradicted the very clause it discharged; US-9's contract enumeration gains the new identity, without which §11 would require an identity the contract does not list. §10's media.net asks gain the Wiki.js base URL and whether it is publicly resolvable — a self-hosted wiki on a private address needs an egress-allowlist entry before TK-6.8 can sync at all (TK-3.6). Unchanged: every claim bound including C1 and C4, the phasing, all Phase-1 acceptance criteria, NG-2, and every requirement's substance — v1.2 corrects where the PRD said what it already meant and adds no scope. The architect's HLD inputs (a third TK-7.4 state for a deliberately-excluded home; union-vs-intersection for "readable by the team"; ingest page source, not rendered output) are recorded on the issue and are deliberately not PRD changes. - v1.1 (2026-09-05) — Wiki.js. Trigger: the founder established that the media.net SRE team documents in Wiki.js, a home v1.0's problem statement had placed in Drive and GitLab. Changes: §1 corrects the document homes and points at the topology session; TK-6.8 adds Wiki.js pages-as-knowledge (Wiki.js 2.x GraphQL page list + single fetch; markdown-native, so TK-6.3's tree-sitter work does not apply; team-grain vault credential per TK-3.1) together with an explicit topology-conditional F6 sequencing rule and the git-sync double-coverage lever; TK-4.7 enumerates the
fetch_documentref types including Wiki.js page id/path and makes id the identity; TK-3.3 adds Wiki.js as an ACL source and rules that no connector enters F3 without one; TK-7.4 and a new §2 note make source-home coverage explicit and F6-conditional; §10 records #3014wikijs-mcpas the independent Day-0 live-access arm and extends the media.net input asks; §11 adds the page-move failure mode; §14 extends Phase-2 acceptance; §15 places Wiki.js at Starter. Unchanged: NG-2 (Gmail stays never-corpus — the contrast with Wiki.js is now stated explicitly), all five claim bounds including C1 and C4, the phasing, and every Phase-1 acceptance criterion. No Phase-1 claim is inflated: Wiki.js counts toward parity and coverage only once TK-6.8 lands. - v1.0 (2026-09-05) — Initial draft. Productizes the founder-approved B2 design from the media.net onboarding plan (tracker #2991) into six features (F2–F7 / TK-2..TK-7), five locked decisions, ten success metrics, nine user stories, four open questions and five claim bounds. Every gap claim verified against
origin/maindb20a314with file:line evidence. Records that PRD #1206 is the parent scope model and is not amended: itsclearance ≥ floorpredicate was written about tool invocation, so locked decision 3 is consistent with it, and #1206 already places connector implementations out of its own scope.