From 7ebc53c2db977645d16a54fb34e60f5efae917e8 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:44:47 +0200 Subject: [PATCH 1/4] feat: add remote repository snapshot helper --- scripts/repository-context.mjs | 180 +++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 scripts/repository-context.mjs diff --git a/scripts/repository-context.mjs b/scripts/repository-context.mjs new file mode 100644 index 00000000..1597c64b --- /dev/null +++ b/scripts/repository-context.mjs @@ -0,0 +1,180 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { realpathSync } from "node:fs"; +import { pathToFileURL } from "node:url"; + +function run(command, args) { + try { + return execFileSync(command, args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + maxBuffer: 20 * 1024 * 1024, + }).trim(); + } catch (error) { + const stderr = error.stderr?.toString?.().trim(); + throw new Error( + `repository_context_command_failed:${command}${stderr ? `:${stderr}` : ""}`, + ); + } +} + +function parseJson(value, code) { + try { + return JSON.parse(value); + } catch { + throw new Error(code); + } +} + +function normalizeRepositoryParts(owner, repo) { + const cleanOwner = String(owner || "").trim(); + const cleanRepo = String(repo || "").trim().replace(/\.git$/i, ""); + const valid = /^[A-Za-z0-9_.-]+$/; + if (!valid.test(cleanOwner) || !valid.test(cleanRepo)) { + throw new Error("repository_specifier_invalid"); + } + return `${cleanOwner}/${cleanRepo}`; +} + +export function parseRepositorySpecifier(input) { + const value = String(input || "").trim(); + if (!value) throw new Error("repository_specifier_missing"); + + if (!value.includes("://")) { + const parts = value.split("/").filter(Boolean); + if (parts.length !== 2) throw new Error("repository_specifier_invalid"); + return normalizeRepositoryParts(parts[0], parts[1]); + } + + let url; + try { + url = new URL(value); + } catch { + throw new Error("repository_specifier_invalid_url"); + } + if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "github.com") { + throw new Error("repository_specifier_unsupported_host"); + } + const parts = url.pathname.split("/").filter(Boolean); + if (parts.length < 2) throw new Error("repository_specifier_invalid"); + return normalizeRepositoryParts(parts[0], parts[1]); +} + +export function resolveRepositorySnapshot(input, runner = run, requestedRef = null) { + const requestedRepo = parseRepositorySpecifier(input); + const metadata = parseJson( + runner("gh", [ + "repo", + "view", + requestedRepo, + "--json", + "nameWithOwner,defaultBranchRef,url", + ]), + "repository_metadata_invalid_json", + ); + + const repo = metadata?.nameWithOwner; + const defaultBranch = metadata?.defaultBranchRef?.name; + const url = metadata?.url; + if (!repo) throw new Error("repository_identity_missing"); + if (!defaultBranch) throw new Error("repository_default_branch_missing"); + if (!url) throw new Error("repository_url_missing"); + + const branch = requestedRef ? String(requestedRef).trim() : String(defaultBranch); + if (!branch) throw new Error("repository_snapshot_ref_missing"); + + const commit = parseJson( + runner("gh", [ + "api", + `repos/${repo}/commits/${encodeURIComponent(branch)}`, + ]), + "repository_snapshot_invalid_json", + ); + const sha = commit?.sha; + if (!sha || !/^[0-9a-f]{40,64}$/i.test(String(sha))) { + throw new Error("repository_snapshot_sha_missing"); + } + + return { + repo: String(repo), + defaultBranch: String(defaultBranch), + branch, + sha: String(sha), + url: String(url), + }; +} + +function encodeRepositoryPath(path) { + const value = String(path || "").trim().replace(/^\/+/, ""); + if (!value || value.includes("\0")) throw new Error("repository_path_invalid"); + return value.split("/").map(encodeURIComponent).join("/"); +} + +export function readRepositoryFile(snapshot, path, runner = run) { + const repo = parseRepositorySpecifier(snapshot?.repo); + const sha = String(snapshot?.sha || ""); + if (!/^[0-9a-f]{40,64}$/i.test(sha)) { + throw new Error("repository_snapshot_sha_missing"); + } + const encodedPath = encodeRepositoryPath(path); + return runner("gh", [ + "api", + `repos/${repo}/contents/${encodedPath}?ref=${sha}`, + "-H", + "Accept: application/vnd.github.raw+json", + ]); +} + +function parseArgs(argv) { + const out = { input: null, paths: [], ref: null }; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === "--ref") { + const ref = argv[++i]; + if (!ref) throw new Error("repository_snapshot_ref_missing"); + out.ref = ref; + } else if (arg === "--path") { + const path = argv[++i]; + if (!path) throw new Error("repository_path_missing"); + out.paths.push(path); + } else if (arg === "--help" || arg === "-h") { + out.help = true; + } else if (!out.input) { + out.input = arg; + } else { + throw new Error(`repository_context_unknown_arg:${arg}`); + } + } + return out; +} + +export function main(argv = process.argv.slice(2)) { + const args = parseArgs(argv); + if (args.help) { + console.log( + "Usage: node scripts/repository-context.mjs [--ref ] [--path ]...", + ); + return 0; + } + if (!args.input) throw new Error("repository_specifier_missing"); + + const snapshot = resolveRepositorySnapshot(args.input, run, args.ref); + const files = args.paths.map((path) => ({ + path, + content: readRepositoryFile(snapshot, path), + })); + console.log(JSON.stringify({ ...snapshot, files }, null, 2)); + return 0; +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href +) { + try { + process.exitCode = main(); + } catch (error) { + console.error(String(error?.message || error)); + process.exitCode = 2; + } +} From d541385ebadafdea73bd9faa2f2a191b20d1dfda Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:44:57 +0200 Subject: [PATCH 2/4] docs: define remote repository context evidence --- references/repository-context.md | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 references/repository-context.md diff --git a/references/repository-context.md b/references/repository-context.md new file mode 100644 index 00000000..65b17bad --- /dev/null +++ b/references/repository-context.md @@ -0,0 +1,60 @@ +# Remote Repository Context + +Use this companion when a GitHub Delivery workflow needs repository context but a useful local checkout is not already available. It is a read-only evidence-acquisition path, not a public lifecycle route and not mutation authority. + +## Goal + +Get enough repository context quickly without cloning by default, while preserving the evidence guarantees required by `references/policy/evidence.md`. + +## Snapshot contract + +1. Resolve repository identity from the provided `owner/repo` or GitHub URL. +2. Query repository metadata and resolve the repository's actual default branch. +3. Use the default branch unless the governing workflow selects another branch. Capture its exact commit SHA before substantive file reads. +4. Read README, `SKILL.md`, docs, or targeted source files against that exact SHA. +5. Record the repository, resolved branch, exact SHA, paths read, and any gaps that remain. + +Do not guess `main`, `master`, or `HEAD`. A moving branch name may be used only to resolve the snapshot SHA; evidence used for a substantive decision must then stay bound to that SHA until the workflow intentionally refreshes it. + +`scripts/repository-context.mjs` provides the deterministic `gh` path for repository identity, default-branch resolution, optional workflow-selected branch pinning, SHA capture, and exact-SHA file reads. A host-native GitHub connector may provide the same evidence when it exposes equivalent repository metadata and ref-pinned file reads. + +## Acquisition ladder + +Use the cheapest complete source first: + +1. repository metadata and exact default-branch SHA; +2. README / `SKILL.md` / relevant docs at the captured SHA; +3. targeted repository code search and exact-SHA file reads; +4. local fetch or clone only when the task requires history, runtime execution, exhaustive repository search, modification, or another capability that lightweight remote reads cannot prove. + +Semantic documentation search services such as gitmcp may be used as an optional adapter for discovery. They are never required and are never the sole authority for exhaustive or safety-sensitive claims. + +## Evidence limits + +Lightweight remote inspection does not prove an exhaustive codebase review. Search results are leads unless the selected workflow's required scope is demonstrably covered. Missing, truncated, stale, or unreadable evidence remains `unknown` under `GD-EVID-*`. + +Escalate to a checkout when any of these is material: + +- commit or blame history beyond the captured snapshot; +- runtime reproduction, tests, build, or generated output; +- exhaustive repository-wide analysis that remote search cannot prove complete; +- implementation, refactoring, conflict resolution, or any local modification; +- evidence whose completeness cannot be established through the available GitHub API or connector. + +## Refresh rule + +A snapshot stays reusable while its exact SHA and the relevant external inputs remain unchanged. Re-resolve only when the workflow intentionally asks for a newer repository state or new evidence shows the snapshot can no longer support the claim. Do not repeatedly refresh a stable snapshot merely because more files are discovered during the same analysis. + +## Output + +When this companion materially affects a workflow decision, preserve a compact record: + +```text +Repository: owner/repo +Branch: resolved-snapshot-branch +Snapshot: exact-commit-sha +Read: README.md, docs/..., src/... +Search: targeted queries used, if any +Gaps: none | exact unverified scope +Escalated: no | why a checkout became necessary +``` From 92d6f3075c140935f9b178c0ac5f1fdbdfd9e45d Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:45:14 +0200 Subject: [PATCH 3/4] test: cover remote repository context acquisition --- tests/unit/repository-context.test.mjs | 153 +++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 tests/unit/repository-context.test.mjs diff --git a/tests/unit/repository-context.test.mjs b/tests/unit/repository-context.test.mjs new file mode 100644 index 00000000..e1e50d35 --- /dev/null +++ b/tests/unit/repository-context.test.mjs @@ -0,0 +1,153 @@ +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import test from "node:test"; + +import { + parseRepositorySpecifier, + readRepositoryFile, + resolveRepositorySnapshot, +} from "../../scripts/repository-context.mjs"; + +test("accepts owner/repo and nested github.com URLs", () => { + assert.equal(parseRepositorySpecifier("acme/widgets"), "acme/widgets"); + assert.equal( + parseRepositorySpecifier("https://github.com/acme/widgets/blob/dev/README.md"), + "acme/widgets", + ); +}); + +test("rejects non-GitHub hosts instead of treating arbitrary URLs as repositories", () => { + assert.throws( + () => parseRepositorySpecifier("https://example.com/acme/widgets"), + /repository_specifier_unsupported_host/, + ); +}); + +test("resolves the repository default branch and captures its exact commit SHA", () => { + const calls = []; + const snapshot = resolveRepositorySnapshot("acme/widgets", (command, args) => { + calls.push([command, ...args]); + if (args[0] === "repo") { + return JSON.stringify({ + nameWithOwner: "Acme/Widgets", + defaultBranchRef: { name: "develop" }, + url: "https://github.com/Acme/Widgets", + }); + } + return JSON.stringify({ sha: "0123456789abcdef0123456789abcdef01234567" }); + }); + + assert.deepEqual(snapshot, { + repo: "Acme/Widgets", + defaultBranch: "develop", + branch: "develop", + sha: "0123456789abcdef0123456789abcdef01234567", + url: "https://github.com/Acme/Widgets", + }); + assert.deepEqual(calls, [ + ["gh", "repo", "view", "acme/widgets", "--json", "nameWithOwner,defaultBranchRef,url"], + ["gh", "api", "repos/Acme/Widgets/commits/develop"], + ]); +}); + +test("can pin a workflow-selected branch without confusing it with the default branch", () => { + const calls = []; + const snapshot = resolveRepositorySnapshot( + "acme/widgets", + (command, args) => { + calls.push([command, ...args]); + if (args[0] === "repo") { + return JSON.stringify({ + nameWithOwner: "acme/widgets", + defaultBranchRef: { name: "main" }, + url: "https://github.com/acme/widgets", + }); + } + return JSON.stringify({ sha: "abcdefabcdefabcdefabcdefabcdefabcdefabcd" }); + }, + "dev", + ); + + assert.equal(snapshot.defaultBranch, "main"); + assert.equal(snapshot.branch, "dev"); + assert.deepEqual(calls[1], ["gh", "api", "repos/acme/widgets/commits/dev"]); +}); + +test("fails closed when repository metadata or the resolved SHA is incomplete", () => { + assert.throws( + () => + resolveRepositorySnapshot("acme/widgets", (_command, args) => + args[0] === "repo" + ? JSON.stringify({ nameWithOwner: "acme/widgets", defaultBranchRef: null }) + : JSON.stringify({ sha: "abc" }), + ), + /repository_default_branch_missing/, + ); + + assert.throws( + () => + resolveRepositorySnapshot("acme/widgets", (_command, args) => + args[0] === "repo" + ? JSON.stringify({ + nameWithOwner: "acme/widgets", + defaultBranchRef: { name: "dev" }, + url: "https://github.com/acme/widgets", + }) + : JSON.stringify({}), + ), + /repository_snapshot_sha_missing/, + ); +}); + +test("reads repository files against the captured SHA, never a moving branch alias", () => { + let call = null; + const content = readRepositoryFile( + { repo: "acme/widgets", sha: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" }, + "docs/guide.md", + (command, args) => { + call = [command, ...args]; + return "guide"; + }, + ); + + assert.equal(content, "guide"); + assert.deepEqual(call, [ + "gh", + "api", + "repos/acme/widgets/contents/docs/guide.md?ref=deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "-H", + "Accept: application/vnd.github.raw+json", + ]); +}); + +test("repository context guidance keeps lightweight reads SHA-bound and bounded", () => { + const referenceUrl = new URL( + "../../references/repository-context.md", + import.meta.url, + ); + assert.ok(existsSync(referenceUrl), "expected repository context companion"); + const reference = readFileSync(referenceUrl, "utf8"); + + assert.match(reference, /resolve the repository's actual default branch/i); + assert.match(reference, /capture its exact commit SHA/i); + assert.match(reference, /exact SHA/i); + assert.match(reference, /Do not guess `main`, `master`, or `HEAD`/i); + assert.match(reference, /history.*runtime.*exhaustive.*modification/is); + assert.match(reference, /does not prove an exhaustive codebase review/i); + assert.match(reference, /optional adapter/i); +}); + +test("evidence policy conditionally composes the repository context companion", () => { + const evidence = readFileSync( + new URL("../../references/policy/evidence.md", import.meta.url), + "utf8", + ); + assert.match(evidence, /GD-EVID-007/); + assert.match(evidence, /references\/repository-context\.md/); + assert.match(evidence, /workflow-selected branch/i); + assert.match(evidence, /Do not guess `main`, `master`, or `HEAD`/i); + assert.match( + evidence, + /cannot by themselves prove history, runtime behavior, exhaustive repository coverage, or a modification result/i, + ); +}); From ceb8bceba408ae08999e470bd16d8345a04260cf Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:45:23 +0200 Subject: [PATCH 4/4] policy: bind remote context to immutable snapshots --- references/policy/evidence.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/references/policy/evidence.md b/references/policy/evidence.md index 2ec1aae1..87339c2e 100644 --- a/references/policy/evidence.md +++ b/references/policy/evidence.md @@ -25,3 +25,9 @@ Pending, missing, unrecognized, or unverifiable evidence is `unknown`, not pass. ### GD-EVID-006 — Finish with the authoritative gate Before claiming merge-ready or merging, the final `ship-gate.mjs` result must be `ready` on unchanged relevant heads/state. Component helpers diagnose; they do not overrule the authoritative gate. + +### GD-EVID-007 — Bind lightweight remote repository context to an immutable snapshot + +When a workflow needs GitHub repository context and no useful local checkout is already available, apply `references/repository-context.md`. Resolve the repository's actual default branch, or the workflow-selected branch when one is required, then capture its exact commit SHA and bind substantive remote file reads to that SHA. Do not guess `main`, `master`, or `HEAD`. + +Lightweight remote reads can establish targeted documentation or source evidence. They cannot by themselves prove history, runtime behavior, exhaustive repository coverage, or a modification result. Escalate to a local fetch/checkout when the selected workflow needs evidence that the remote path cannot prove complete.