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
9 changes: 9 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Prefer the repo gh policy shim so coding agents cannot undraft with raw
# `gh pr ready` (use `pnpm pr:ready`). Installed by scripts/install-git-hooks.mjs.
PATH_add .tools/bin

# Default bare `gh` commands to the fork, not gh's upstream-parent default
# (`pingdotgg/t3code`). Without this, `gh pr view`/`create`/`checks` resolve
# against upstream and miss the fork's PRs. Explicit `--repo` always overrides,
# so the stack tooling (which always passes `--repo`) is unaffected.
export GH_REPO="${T3CODE_FORK_REPOSITORY:-patroza/t3code}"
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,9 @@ node_modules/
*.log
.env*
!.env.example
!.envrc
# direnv
.direnv
# agent gh shim + local run state (ship-gate cache, etc.)
.tools/
.run/
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pnpm lint-staged
7 changes: 7 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Agent-only ship gate (humans: no-op). See scripts/agent-pre-push.mjs.
# Draft / no PR: free agent push. Ready PR: vp check + typecheck + tests.
# Publish: pnpm pr:ready (not raw gh pr ready). Agent gh shim: .tools/bin/gh.
#
# Humans: SKIP_AGENT_PREPUSH=1 git push
# Agents: never SKIP_AGENT_PREPUSH / never --no-verify
pnpm exec node scripts/agent-pre-push.mjs
213 changes: 109 additions & 104 deletions AGENTS.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions docs/internals/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ authenticated.
- `vp run typecheck`: Strict TypeScript checks for all packages.
- `vp run test`: Runs workspace tests.
- `vp run lint:mobile`: Mobile native static analysis (`scripts/mobile-native-static-check.ts`).
- `pnpm pr:ready`: Agent publish path — runs the ship gate (`vp check`, `vpr typecheck`,
`vp run test`), then marks the open draft PR ready. Do not use raw `gh pr ready` from coding
agents; the `.tools/bin/gh` shim blocks undraft side channels (installed by
`scripts/install-git-hooks.mjs` on `prepare`).
- `pnpm test:agent-gate`: Unit tests for the agent pre-push / PR-state / gh-policy helpers.
- Husky: `pre-commit` runs `pnpm lint-staged` (`vp fmt` on staged files + `vp lint --fix` on staged
code files);
`pre-push` runs `scripts/agent-pre-push.mjs` (agents only: free push on draft / no PR; full ship
gate on ready PRs; SHA cache in `.run/agent-ship-gate.json`).
- `node apps/server/scripts/t3-sqlite-state.ts <query|exec> --base-dir <path> ...`: Inspects or seeds
an isolated T3 SQLite database; writes create a private backup first.

Expand Down
11 changes: 11 additions & 0 deletions lint-staged.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** @type {import("lint-staged").Configuration} */
export default {
// Keep in sync with vite.config.ts `staged`.
// Commit runs format + lint; the heavier typecheck + tests stay in the agent
// ship gate (pre-push on ready PRs / `pnpm pr:ready`).
// `--no-error-on-unmatched-pattern`: a commit whose staged files are all
// unformattable (e.g. only *.nix) must not fail pre-commit.
"*": "vp fmt --no-error-on-unmatched-pattern",
// Lint (with autofix) only the code files oxlint understands.
"*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}": "vp lint --fix",
};
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"private": true,
"type": "module",
"scripts": {
"prepare": "node scripts/clean-tsgo-backups.mjs && effect-tsgo patch && vp config --no-agent",
"prepare": "node scripts/clean-tsgo-backups.mjs && effect-tsgo patch && vp config --no-agent && node scripts/install-git-hooks.mjs",
"pr:ready": "node scripts/agent-pr-ready.mjs",
"test:agent-gate": "vp test run scripts/agent-pre-push.test.mjs",
"dev": "node scripts/dev-runner.ts dev",
"dev:share": "node scripts/dev-runner.ts dev --share",
"dev:server": "node scripts/dev-runner.ts dev:server",
Expand Down Expand Up @@ -55,6 +57,8 @@
"@oxlint/plugins": "^1.63.0",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"vite-plus": "catalog:"
},
"engines": {
Expand Down
77 changes: 77 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions scripts/agent-gh.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env node
/**
* Agent-facing `gh` policy shim.
*
* Installed at `.tools/bin/gh` by `scripts/install-git-hooks.mjs`.
* When coding-agent env markers are set, blocks undraft side channels
* (`gh pr ready`, ready_for_review API). Use `pnpm pr:ready` instead
* (sets AGENT_PR_SHIP=1 for the real call).
*
* Humans / non-agents: transparent pass-through to the next `gh` on PATH.
*/
import * as NodeChildProcess from "node:child_process";
import * as NodePath from "node:path";
import * as NodeProcess from "node:process";
import * as NodeURL from "node:url";
import { isCodingAgent } from "./lib/agent-env.mjs";
import { findRealGh, inspectAgentGhCommand } from "./lib/agent-gh-policy.mjs";

const selfPath = NodeURL.fileURLToPath(import.meta.url);
const argv = NodeProcess.argv.slice(2);

if (isCodingAgent()) {
const decision = inspectAgentGhCommand(argv);
if (decision.blocked) {
console.error(`agent gh: blocked: ${decision.reason}`);
NodeProcess.exit(1);
}
}

const realGh = findRealGh({ selfPath });
if (!realGh) {
console.error("agent gh: could not resolve real `gh` binary (set AGENT_GH_REAL)");
NodeProcess.exit(127);
}

// Avoid re-entering this shim if PATH still prefers us.
const env = { ...NodeProcess.env };
const toolsBin = NodePath.resolve(NodePath.dirname(selfPath), "..", ".tools", "bin");
const pathParts = (env["PATH"] ?? "").split(NodePath.delimiter).filter(Boolean);
env["PATH"] = pathParts.filter((p) => NodePath.resolve(p) !== toolsBin).join(NodePath.delimiter);
env["AGENT_GH_REAL"] = realGh;

const result = NodeChildProcess.spawnSync(realGh, argv, {
stdio: "inherit",
env,
shell: false,
});
NodeProcess.exit(result.status === null ? 1 : result.status);
62 changes: 62 additions & 0 deletions scripts/agent-pr-ready.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env node
/**
* Agent (and human) publish path: run the ship gate, then mark the PR ready.
*
* pnpm pr:ready
*
* Agents must not call `gh pr ready` directly — the agent gh shim blocks it
* unless AGENT_PR_SHIP=1 (set only here for the undraft step).
*/
import * as NodeChildProcess from "node:child_process";
import * as NodeProcess from "node:process";
import { runAgentShipGate } from "./agent-pre-push.mjs";
import { resolveOpenPrState } from "./lib/agent-pr-state.mjs";

const root = NodeProcess.cwd();
const prState = resolveOpenPrState({ cwd: root });

if (prState.mode === "none") {
console.error(
"agent pr:ready: no open PR for this branch — open a draft first (`gh pr create --draft`)",
);
NodeProcess.exit(1);
}

if (prState.mode === "unknown") {
console.error(`agent pr:ready: cannot resolve PR state (${prState.detail ?? "gh failed"})`);
NodeProcess.exit(1);
}

if (prState.mode === "ready") {
console.error(
`agent pr:ready: PR${prState.pr?.number != null ? ` #${prState.pr.number}` : ""} is already ready — running ship gate only`,
);
await runAgentShipGate({ root });
NodeProcess.exit(0);
}

// draft → gate then undraft
console.error(
`agent pr:ready: ship gate then ready PR${prState.pr?.number != null ? ` #${prState.pr.number}` : ""}`,
);
await runAgentShipGate({ root });

const env = { ...NodeProcess.env, AGENT_PR_SHIP: "1" };
const readyArgs =
prState.pr?.number != null ? ["pr", "ready", String(prState.pr.number)] : ["pr", "ready"];

console.error("agent pr:ready: marking PR ready for review");
const result = NodeChildProcess.spawnSync("gh", readyArgs, {
stdio: "inherit",
cwd: root,
env,
shell: false,
});
const status = result.status === null ? 1 : result.status;
if (status !== 0) {
console.error(`agent pr:ready: gh pr ready failed (exit ${status})`);
NodeProcess.exit(status);
}

console.error("agent pr:ready: ok — PR is ready; CI will run the full suite");
NodeProcess.exit(0);
Loading
Loading