Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ pnpm exec rgr team compare .ragmir/team/alice.json --local-label christophe

The comparison names configuration differences plus local-only, peer-only, and changed files, then
gives ordered repair steps. It never guesses which copy is authoritative: the team keeps that
decision in Git, Drive, or its existing source workflow. Detailed safeguards live in the
decision in Git, Drive, or its existing source workflow. `ready` means the index can serve and be
compared; privacy warnings are reported as non-blocking security advisories with a separate
`rgr security-audit` action. Existing v2.19 snapshots remain comparable without reindexing.
Detailed safeguards live in the
[team guide](./docs/agent-integration.md#team-knowledge-bases) and
[configuration reference](./docs/configuration.md#stable-team-source-configuration).

Expand Down
9 changes: 7 additions & 2 deletions docs/agent-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ The result distinguishes configuration drift, local-only files, peer-only files,
It provides ordered commands for readiness, upgrade, ingestion, or rebuild work. Ragmir never
chooses which copy is correct; use the declared Git commit, Drive revision, or team folder as the
authority, synchronize it, ingest again, then compare fresh snapshots until
`status=synchronized`.
`status=synchronized`. Operational readiness and privacy review are independent: a matching index
with local extractor or permission warnings remains synchronized, while the comparison exposes
per-side security advisory counts and recommends `rgr security-audit`. Do not rebuild a healthy
index only to clear an advisory. Existing v2.19 snapshots are interpreted from their stored health
metadata, so teammates can upgrade at different times without regenerating them first.

Version stable directory or glob contracts instead of rewriting a tracked config with the files
found on the current machine. The lower-level `corpusFingerprint` returned by `rgr status --json`,
Expand All @@ -104,7 +108,8 @@ responsible for distributing the source files.
An agent using the bundled Ragmir skill should check readiness before relying on retrieval. When a
peer snapshot is available, it should run the comparison and warn the user in the user's language
when `status` is not `synchronized`. It should summarize the exact drift and suggested commands,
never silently pick a winner or overwrite source files.
never silently pick a winner or overwrite source files. It should describe security advisories as
separate, non-blocking follow-ups when the operational indexes already match.

## MCP tools

Expand Down
5 changes: 5 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,11 @@ types that callers commonly compose explicitly.
| Upgrades | `UpgradeInspection`, `UpgradeOptions`, `UpgradeResult`, `UpgradeStatus` |
| Setup and commands | `SetupOptions`, `SetupResult`, `SetupSemanticResult`, `PackageManager`, `RagmirCommand`, `PromptRouteDecision`, `PromptRouteTool` |

`TeamSnapshot.ready` describes operational index readiness. `TeamComparison.securityAdvisories`
reports local and peer privacy-warning counts separately; advisory-only differences do not prevent
`status: "synchronized"`. `compareTeamSnapshots` accepts existing v2.19 snapshots and derives their
operational state from the stored corpus and health fields without changing the snapshot schema.

## Chat: cited local generation

```bash
Expand Down
8 changes: 6 additions & 2 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,12 @@ authorized teammate explicitly needs a copy.

`team compare` previews up to 20 local-only, peer-only, and changed files and prints every
configuration difference plus ordered actions. `--json` returns the complete diff; `--strict` exits
with code 1 unless both ready indexes are synchronized. The command never changes source files or
decides which side is authoritative.
with code 1 unless both operational indexes are synchronized. `securityAdvisories`,
`localSecurityAdvisories`, and `peerSecurityAdvisories` keep privacy follow-ups visible without
turning matching operational indexes into `not-ready`. Review them with `rgr security-audit`; they
do not require deleting or rebuilding the index. Snapshots written by Ragmir v2.19.0 through
v2.19.2 remain compatible. The command never changes source files or decides which side is
authoritative.

## Safe upgrades

Expand Down
6 changes: 6 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ or shared folder, synchronize both workstations, run the recommended ingest or r
compare fresh snapshots. Keep tracked config based on stable directories or globs, and never share
an actively written `.ragmir/storage/` directory.

If the comparison reports `status=synchronized` together with security advisories, the indexed
bytes and operational configuration match. Review each side with `rgr security-audit`; do not run a
repair or rebuild only because an extractor or permission advisory remains. Ragmir also interprets
v2.19.0 through v2.19.2 snapshots from their stored health fields, so an older snapshot that marked
an advisory as `ready=false` can still be compared after upgrading.

The lower-level `corpusFingerprint` in `rgr status --json` remains a quick equality signal. A
different value identifies divergence, not which machine is correct.
If an index created before corpus fingerprints were introduced reports `null`, run `rgr ingest`
Expand Down
6 changes: 4 additions & 2 deletions packages/ragmir-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ Top-level `ingest`, `search`, `ask`, and `research` functions remain available f
- `rgr security-audit` checks permissions, Git ignore coverage, tracked private paths, redaction,
and local extractor authority.
- `rgr team snapshot` and `rgr team compare` explain configuration and per-file drift without
sharing source text or guessing which copy is authoritative.
sharing source text or guessing which copy is authoritative. Operational readiness remains
separate from non-blocking security advisories, including for existing v2.19 snapshots.
- After a package update, `rgr upgrade --check` previews compatibility; `rgr upgrade` safely stages
any required rebuild without deleting the active index first. Privacy warnings remain visible as
non-blocking advisories and can be handled separately with `rgr security-audit`.
Expand All @@ -105,6 +106,7 @@ Core installs and starts without either add-on. A hosted agent receives only pas
integration sends under that provider's data policy; use a local consumer when no passage may leave
the workstation. Teams synchronize their source folder and tracked configuration, then build one
local index per developer. Exchange an authorized metadata-only snapshot and run `rgr team compare`
to resolve exact drift. See the [team workflow](https://github.com/jcode-works/jcode-ragmir/blob/main/docs/agent-integration.md#team-knowledge-bases).
to resolve exact drift. Matching operational indexes remain synchronized when a privacy advisory
needs separate review with `rgr security-audit`. See the [team workflow](https://github.com/jcode-works/jcode-ragmir/blob/main/docs/agent-integration.md#team-knowledge-bases).

Ragmir Core is open source under the [MIT License](https://github.com/jcode-works/jcode-ragmir/blob/main/LICENSE).
3 changes: 3 additions & 0 deletions packages/ragmir-core/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,7 @@ teamCommand
console.log(`outputPath=${outputPath}`)
console.log(`label=${snapshot.label}`)
console.log(`ready=${snapshot.ready}`)
console.log(`securityAdvisories=${snapshot.health.securityWarnings}`)
console.log(`corpusFingerprint=${snapshot.corpus.fingerprint ?? "unavailable"}`)
console.log(`indexedFiles=${snapshot.corpus.indexedFiles}`)
console.log(`notice=${snapshot.notice}`)
Expand Down Expand Up @@ -2245,6 +2246,8 @@ function printTeamComparison(comparison: TeamComparison): void {
console.log(`summary=${comparison.summary}`)
console.log(`local=${comparison.localLabel}`)
console.log(`peer=${comparison.peerLabel}`)
console.log(`localSecurityAdvisories=${comparison.securityAdvisories.local}`)
console.log(`peerSecurityAdvisories=${comparison.securityAdvisories.peer}`)
console.log(`sameConfiguration=${comparison.sameConfiguration}`)
console.log(`sameCorpus=${comparison.sameCorpus}`)
console.log(`authorityDecisionRequired=${comparison.authorityDecisionRequired}`)
Expand Down
42 changes: 42 additions & 0 deletions packages/ragmir-core/src/team-diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { afterEach, describe, expect, it } from "vitest"
import { loadConfig } from "./config.js"
import { ingest } from "./ingest.js"
import { initProject } from "./init.js"
import { readIndexManifest, writeIndexManifest } from "./store.js"
import {
compareTeamSnapshots,
createTeamSnapshot,
Expand Down Expand Up @@ -128,6 +130,46 @@ describe("team diagnostics", () => {
})
})

it("should synchronize operational indexes while surfacing privacy advisories", async () => {
const [root] = await teamRoots()
await writeEvidence(root, "decision.md", "Approved release train.\n")
await ingest({ cwd: root })
const config = await loadConfig(root)
const manifest = await readIndexManifest(config)
if (!manifest?.health) {
throw new Error("Expected an index health snapshot.")
}
const warning = "A configured local extractor executes with operator authority."
await writeIndexManifest(
{
...manifest,
health: { ...manifest.health, securityWarnings: [warning] },
},
config,
manifest.indexedFiles,
)

const snapshot = await createTeamSnapshot({ cwd: root, label: "Alice" })
const legacySnapshot = { ...snapshot, runtimeRagmirVersion: "2.19.2", ready: false }
const comparison = compareTeamSnapshots(legacySnapshot, legacySnapshot)
const currentNotReadyComparison = compareTeamSnapshots(
{ ...snapshot, ready: false },
{ ...snapshot, ready: false },
)

expect(snapshot).toMatchObject({ ready: true, health: { securityWarnings: 1 } })
expect(comparison).toMatchObject({
status: "synchronized",
synchronized: true,
securityAdvisories: { local: 1, peer: 1 },
})
expect(comparison.summary).toContain("non-blocking security advisories")
expect(comparison.recommendedActions).toEqual([
"On Alice, review 1 non-blocking security advisory with `rgr security-audit`.",
])
expect(currentNotReadyComparison).toMatchObject({ status: "not-ready", synchronized: false })
})

it("should reject a snapshot when its file inventory was altered", async () => {
const [root] = await teamRoots()
await writeEvidence(root, "decision.md", "Approved release train.\n")
Expand Down
78 changes: 66 additions & 12 deletions packages/ragmir-core/src/team-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const MAX_TEAM_SNAPSHOT_BYTES = 64 * 1024 * 1024
const MAX_TEAM_LABEL_CHARACTERS = 80
const MAX_TEAM_PATH_CHARACTERS = 4_096
const SHA256_PATTERN = /^[0-9a-f]{64}$/u
const LEGACY_SECURITY_BLOCKING_SNAPSHOT_VERSIONS = new Set(["2.19.0", "2.19.1", "2.19.2"])

const teamSnapshotFileSchema = z
.object({
Expand Down Expand Up @@ -117,6 +118,10 @@ export interface TeamComparison {
summary: string
localLabel: string
peerLabel: string
securityAdvisories: {
local: number
peer: number
}
sameConfiguration: boolean
sameCorpus: boolean
authorityDecisionRequired: boolean
Expand Down Expand Up @@ -176,7 +181,7 @@ export async function createTeamSnapshot(
label: normalizeTeamLabel(options.label ?? "local"),
knowledgeBaseId: identity?.id ?? null,
runtimeRagmirVersion: VERSION,
ready: report.ready,
ready: report.readiness.operationalReady && report.readiness.indexPolicyCurrent,
freshnessWarning: report.indexFreshness.warning,
configuration: {
sources: sourceContract.sources,
Expand Down Expand Up @@ -307,16 +312,29 @@ export function compareTeamSnapshots(local: TeamSnapshot, peer: TeamSnapshot): T
localOnly.length === 0 &&
peerOnly.length === 0 &&
changed.length === 0
const synchronized =
validatedLocal.ready && validatedPeer.ready && sameConfiguration && sameCorpus
const localReady = isTeamSnapshotOperational(validatedLocal)
const peerReady = isTeamSnapshotOperational(validatedPeer)
const synchronized = localReady && peerReady && sameConfiguration && sameCorpus
const status = comparisonStatus(validatedLocal, validatedPeer, sameConfiguration, sameCorpus)
const advisoryCount =
validatedLocal.health.securityWarnings + validatedPeer.health.securityWarnings

return {
status,
synchronized,
summary: comparisonSummary(status, localOnly.length, peerOnly.length, changed.length),
summary: comparisonSummary(
status,
localOnly.length,
peerOnly.length,
changed.length,
advisoryCount,
),
localLabel: validatedLocal.label,
peerLabel: validatedPeer.label,
securityAdvisories: {
local: validatedLocal.health.securityWarnings,
peer: validatedPeer.health.securityWarnings,
},
sameConfiguration,
sameCorpus,
authorityDecisionRequired:
Expand Down Expand Up @@ -386,13 +404,33 @@ function assertTeamSnapshotIntegrity(snapshot: TeamSnapshot): void {
snapshot.health.missingFromIndex > 0 ||
snapshot.health.staleInIndex > 0 ||
snapshot.health.emptyTextFiles > 0 ||
snapshot.health.oversizedFiles > 0 ||
snapshot.health.securityWarnings > 0)
snapshot.health.oversizedFiles > 0)
) {
throw new Error("Team snapshot cannot be ready while freshness, coverage, or security fails.")
throw new Error("Team snapshot cannot be ready while freshness or coverage fails.")
}
}

function isTeamSnapshotOperational(snapshot: TeamSnapshot): boolean {
const storedHealthIsOperational =
snapshot.corpus.fingerprint !== null &&
snapshot.corpus.chunksIndexed > 0 &&
snapshot.freshnessWarning === null &&
snapshot.health.missingFromIndex === 0 &&
snapshot.health.staleInIndex === 0 &&
snapshot.health.emptyTextFiles === 0 &&
snapshot.health.oversizedFiles === 0
if (!storedHealthIsOperational) {
return false
}
if (snapshot.ready) {
return true
}
return (
snapshot.health.securityWarnings > 0 &&
LEGACY_SECURITY_BLOCKING_SNAPSHOT_VERSIONS.has(snapshot.runtimeRagmirVersion)
)
}

function fingerprintTeamFiles(files: TeamSnapshotFile[]): string {
return summarizeIndexedCorpus(
[...files].sort((left, right) =>
Expand Down Expand Up @@ -694,7 +732,7 @@ function comparisonStatus(
sameConfiguration: boolean,
sameCorpus: boolean,
): TeamComparisonStatus {
if (!local.ready || !peer.ready) {
if (!isTeamSnapshotOperational(local) || !isTeamSnapshotOperational(peer)) {
return "not-ready"
}
if (!sameConfiguration) {
Expand All @@ -708,9 +746,14 @@ function comparisonSummary(
localOnly: number,
peerOnly: number,
changed: number,
securityAdvisories: number,
): string {
if (status === "synchronized") {
return "Both ready indexes use the same configuration and indexed source bytes."
const advisoryNote =
securityAdvisories > 0
? ` ${securityAdvisories} non-blocking security ${securityAdvisories === 1 ? "advisory remains" : "advisories remain"} for separate review.`
: ""
return `Both operational indexes use the same configuration and indexed source bytes.${advisoryNote}`
}
if (status === "not-ready") {
return "At least one index is not ready. Repair readiness before deciding whether corpora match."
Expand All @@ -728,10 +771,12 @@ function recommendedActions(
sameCorpus: boolean,
): string[] {
const actions: string[] = []
if (!local.ready) {
const localReady = isTeamSnapshotOperational(local)
const peerReady = isTeamSnapshotOperational(peer)
if (!localReady) {
actions.push(`On ${local.label}, run \`rgr doctor --fix\` and \`rgr audit\` until ready=true.`)
}
if (!peer.ready) {
if (!peerReady) {
actions.push(`On ${peer.label}, run \`rgr doctor --fix\` and \`rgr audit\` until ready=true.`)
}
if (configurationDifferences.some((difference) => difference.scope === "runtime")) {
Expand All @@ -753,7 +798,16 @@ function recommendedActions(
)
actions.push("Run `rgr ingest` after source synchronization.")
}
if (actions.length > 0) {
const advisoryLabels = new Set<string>()
for (const snapshot of [local, peer]) {
if (snapshot.health.securityWarnings > 0 && !advisoryLabels.has(snapshot.label)) {
advisoryLabels.add(snapshot.label)
actions.push(
`On ${snapshot.label}, review ${snapshot.health.securityWarnings} non-blocking security ${snapshot.health.securityWarnings === 1 ? "advisory" : "advisories"} with \`rgr security-audit\`.`,
)
}
}
if (!localReady || !peerReady || configurationDifferences.length > 0 || !sameCorpus) {
actions.push("Export a fresh snapshot and compare again until status=synchronized.")
}
return actions
Expand Down