Jev for Git workflows: commits, pull requests, and CI gates
Use Jev for bounded semantic decisions around Git commits, pull requests, and CI without giving a probabilistic model permission to push, merge, or bypass branch protection.
1. Decide what Jev owns—and what it never owns
Use Jev where exact Git data is insufficient: classify a change, select a review area, estimate a risk band, or decide whether a human review is required. File existence, tests, permissions, required reviewers, and protected-branch policy remain deterministic checks. This tutorial never lets the model push, merge, tag, release, or bypass an approval.
2. Create a dependency-free PR decision script
The example uses Node 20 built-ins and the official POST /v1/systemone endpoint. Locally it reads the staged diff; in CI, JEV_BASE points it at the pull request base branch. It skips oversized patches and checks sensitive paths and common secret patterns before upload. These checks cannot guarantee all credentials are detected; inspect inputs before real use.
// Node 20+. Report only: never merges, publishes, or authorizes an action.
import { appendFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { pathToFileURL } from 'node:url';
export function parsePaths(output) {
const fields = output.split('\0');
if (fields.pop() !== '') throw new Error('Invalid NUL-terminated Git output');
const paths = [];
while (fields.length) {
const status = fields.shift();
if (!/^[ACDMRTUXB][0-9]*$/.test(status)) throw new Error('Invalid Git status');
const count = /^[RC]/.test(status) ? 2 : 1;
for (let i = 0; i < count; i++) {
const path = fields.shift();
if (!path) throw new Error('Missing Git path');
paths.push(path);
}
}
return paths;
}
export function containsSensitiveInput(paths, patch) {
const sensitivePath = /(^|\/)(?:\.env(?:\.[^/]*)?|\.npmrc|\.pypirc|credentials|id_(?:rsa|ed25519|ecdsa))$|\.(?:pem|key|p12|pfx)$/i;
// Defense in depth, not a guarantee: review the input before any real upload.
const sensitiveContent = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{16,}|sk-[A-Za-z0-9_-]{16,}|AKIA[A-Z0-9]{16})\b|\bBearer\s+\S+|\b(?:[A-Z_]*(?:API_KEY|TOKEN|SECRET|PASSWORD))\s*[:=]\s*\S+/i;
return paths.some(path => sensitivePath.test(path)) || sensitiveContent.test(patch);
}
export const questions = {
change_type: {
type: 'choice', instructions: 'What is the primary purpose of this Git change?',
criteria: { feature: 'Adds user-visible behavior', fix: 'Corrects broken behavior',
maintenance: 'Refactor, documentation, dependencies or tooling', unknown: 'Insufficient evidence' },
},
risk: {
type: 'score', instructions: 'How risky is this change to merge?',
criteria: ['Low: isolated and reversible', 'Moderate: bounded shared behavior',
'High: data, auth, payment or deployment changes', 'Critical: credible irreversible or security impact'],
},
needs_human_review: {
type: 'noul', instructions: 'Does this change require deliberate human review before merge?',
criteria: { true: 'Ambiguous, high-impact or security-sensitive', false: 'Low-impact and reversible' },
},
};
const inRange = (value, max) => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= max;
export function validAnswers(answers) {
const type = answers?.change_type;
return type?.type === 'choice' && typeof type.choice === 'string'
&& Object.hasOwn(questions.change_type.criteria, type.choice)
&& inRange(type.confidence, 1) && answers?.risk?.type === 'score'
&& inRange(answers.risk.score, 3) && answers?.needs_human_review?.type === 'noul'
&& inRange(answers.needs_human_review.noul, 1);
}
export async function reviewDiff({ names, patch, apiKey, fetchImpl = fetch }) {
let paths;
try { paths = parsePaths(names); } catch { return { status: 'invalid_git_input' }; }
if (containsSensitiveInput(paths, patch)) return { status: 'sensitive_input' };
if (!patch.trim()) return { status: 'no_changes' };
if (!apiKey) return { status: 'missing_credentials' };
// Do not silently judge an incomplete patch or send a huge file list.
if (patch.length > 14_000 || paths.length > 200) return { status: 'input_too_large' };
try {
const response = await fetchImpl('https://api.typesafe.ai/v1/systemone', {
method: 'POST',
headers: { authorization: 'Bearer ' + apiKey, 'content-type': 'application/json' },
body: JSON.stringify({ model: 'jev-latest', state: { changed_files: paths, patch }, questions }),
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) return { status: 'provider_failure' };
const { answers } = await response.json();
if (!validAnswers(answers)) return { status: 'invalid_response' };
return { status: 'report_only', answers };
} catch { return { status: 'provider_failure' }; }
}
export function formatReport(result) {
const lines = ['## Jev PR report', '- Status: ' + result.status,
'- Advisory only. Keep tests and human approval; this does not authorize merging.'];
if (result.status === 'report_only') {
const a = result.answers;
lines.push('- Change type: ' + a.change_type.choice,
'- Type confidence: ' + a.change_type.confidence.toFixed(2),
'- Risk score: ' + a.risk.score.toFixed(2) + ' / 3',
'- P(human review): ' + a.needs_human_review.noul.toFixed(2));
}
// No patch, key, exception message, or provider response text in logs.
return lines.join('\n');
}
export function exitCode(result, mode = 'report') {
if (mode === 'report') return 0;
// Conservative gate: any real change or uncertainty requires existing human approval.
// No model answer grants permission to merge. Configure review in the repository rules.
if (mode === 'blocking') return result.status === 'no_changes' ? 0 : 1;
return 1;
}
export async function main(env = process.env) {
const git = (...args) => execFileSync('git', args, { encoding: 'utf8', maxBuffer: 2_000_000 });
let result;
try {
const base = env.JEV_BASE?.trim();
if (base && !/^[A-Za-z0-9][A-Za-z0-9/_.-]*$/.test(base)) throw new Error('Invalid ref');
const range = base ? [base + '...HEAD'] : ['--cached'];
result = await reviewDiff({
names: git('diff', '--name-status', '-z', '--no-ext-diff', '--no-textconv', ...range, '--'),
patch: git('diff', '--unified=1', '--no-ext-diff', '--no-textconv', ...range, '--'),
apiKey: env.TYPESAFE_API_KEY,
});
} catch { result = { status: 'git_input_failure' }; }
const report = formatReport(result);
console.log(report);
if (env.GITHUB_STEP_SUMMARY) appendFileSync(env.GITHUB_STEP_SUMMARY, report + '\n');
process.exitCode = exitCode(result, env.JEV_MODE || 'report');
return result;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await main();
Download complete script (Node 20+) Node 20 includes fetch. Save the download as scripts/jev-pr-review.mjs. JEV_MODE=report is the default and records sanitized provider failures. Optional JEV_MODE=blocking exits nonzero for every change or uncertainty, requiring existing repository human-approval procedures; no model score auto-approves a merge. Checked with offline mocks, not a live API.
3. Ask several bounded questions in one request
The script sends one state and three independent questions: Choice for change type, Score for ordered risk, and Noul for whether human review is required. TypeSafe documents that questions against the same state are evaluated independently, so application policy must combine the answers after the response arrives.
Choicechange_typeCan only return feature, fix, maintenance, or unknown.
ScoreriskReturns a position on the four-level risk scale, including between levels.
Noulneeds_human_reviewReturns P(true) for the proposition and has no separate confidence field.
4. Validate the response before applying policy
A typed response still needs runtime validation. Check that Choice returned an allowed key, Score is finite and inside the declared range, and Noul is between zero and one. Then map the validated values to a visible policy; do not hide authority inside prompt wording.
| Condition | Report-only phase | Calibrated gate |
|---|---|---|
| API timeout or invalid response | Record provider_failure; CI continues | Escalate by team policy; never auto-approve |
| High risk or review probability | Highlight in the job summary | Require the existing human-approval rule |
| Low risk | Still run tests and normal review | Does not imply permission to auto-merge |
5. Run locally in report-only mode
Start with staged changes so a developer can inspect the exact evidence before committing. Report the result without blocking. This reveals oversized diffs, ambiguous labels, sensitive paths, and question-design problems before the workflow affects anyone else.
# Review exactly what is staged for the next commit.
export TYPESAFE_API_KEY='your-server-side-key'
node scripts/jev-pr-review.mjs
# Inspect the same range a pull request would use.
git fetch origin main
JEV_BASE=origin/main node scripts/jev-pr-review.mjs 6. Add a GitHub Actions reporter
The workflow checks out full history, fetches the pull request base, runs the same script, and appends the result to the job summary. Keep permissions read-only and store TYPESAFE_API_KEY as an Actions secret. Reporting first also gives you data for later calibration.
name: Jev PR report
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Fetch base branch
env:
BASE_BRANCH: "${{ github.base_ref }}"
run: git fetch origin "$BASE_BRANCH"
- name: Skip fork report
if: github.event.pull_request.head.repo.full_name != github.repository
run: echo "Fork PR — remote analysis skipped."
- name: Build Jev report
if: github.event.pull_request.head.repo.full_name == github.repository
env:
TYPESAFE_API_KEY: "${{ secrets.TYPESAFE_API_KEY }}"
JEV_BASE: "origin/${{ github.base_ref }}"
run: node scripts/jev-pr-review.mjs 7. Calibrate before enabling a gate
Replay labeled historical pull requests and measure false approvals, unnecessary reviews, coverage, latency, and provider failures. Choose thresholds from observed error cost, not intuition. If you later make the job blocking, fail closed only for the specific high-risk condition your team has validated.
- Prepare a human-labeled historical PR set with low-, medium-, and high-risk examples.
- Log question version, model version, full distributions, thresholds, final human outcome, and overrides.
- Measure false approvals and unnecessary blocks separately; their costs are rarely equal.
- Move from report-only to required human review; do not begin with auto-merge.
- Define fallbacks for provider failure, oversized diffs, sensitive paths, and truncated state.