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
6 changes: 6 additions & 0 deletions references/policy/evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
60 changes: 60 additions & 0 deletions references/repository-context.md
Original file line number Diff line number Diff line change
@@ -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
```
180 changes: 180 additions & 0 deletions scripts/repository-context.mjs
Original file line number Diff line number Diff line change
@@ -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 <owner/repo|github-url> [--ref <branch>] [--path <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;
}
}
Loading
Loading