Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 5 additions & 1 deletion .agents/skills/nemoclaw-contributor-create-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ Confirm that the complete validation execution surface is byte-for-byte identica

Do not infer executable identity from a package name or version. Do not use a branch-defined validator as independent evidence. If any surface differs, is unavailable, or cannot be traced, do not execute the candidate validator or publish. Report the path or executable and canonical base SHA.

Run `npm run validate:pr` before every agent-managed push only after that comparison succeeds. Do not push when it fails or is inconclusive. If it changes a tracked file, inspect and commit the validator-created local diff. Record the new commit as the expected publication SHA. Do not reuse review evidence from the earlier commit for that later change. Before the first push, repeat the initial-publication review step for the new commit, including a self-review of the validator-created diff. For an open PR, preserve the completed remote disposition record and inspect the validator-created local diff as new pre-publication review evidence without recollecting the unchanged remote candidate. Refresh and resolve the trusted base, reestablish the trusted validation surface, and rerun validation. Use `npm run check` for repository-wide validation changes, such as hooks, formatter configuration, generated-check scripts, or coverage baselines.
Run `npm run validate:pr` before every agent-managed push only after that comparison succeeds. Do not push when it fails or is inconclusive. Complete formatting and generation before the final commit; publication validation checks tracked files without applying fixes. If validation reports a required change, repair it, commit it, inspect the new diff, refresh the trusted base, and validate again. For an open PR, preserve the completed disposition record for the unchanged remote candidate and review the local repair without recollecting that remote candidate.

The shared compiler-check runner may reuse a successful local result only when the candidate and base commits, source bytes, installed dependency bytes, resolved executable, execution environment, and required generated outputs still match. Missing, unreadable, stale, or failed evidence must execute the check. A dirty worktree or external Node loader prevents reuse. This local optimization does not establish independent review, CI success, or publication authorization. Keep the trusted-validation comparison above.

Use `npm run check` for repository-wide validation changes, such as hooks, formatter configuration, generated-check scripts, or coverage baselines.

A maintainer may unblock unavailable trusted-base validation only with recorded evidence identifying the base and candidate SHAs, isolated environment, trusted validator entry point and resolved executables, exact command and result, and publication authorization. The environment must not give candidate code contributor-host credentials.

Expand Down
13 changes: 7 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,11 @@ repos:

- id: repository-checks
name: Repository checks
entry: npm run checks:repository
entry: node --import tsx scripts/checks/run.mts --files
language: system
files: ^(\.pre-commit-config\.yaml$|\.github/workflows/e2e\.yaml$|Dockerfile(?:\.base)?$|agents/openclaw/manifest\.yaml$|agents/hermes/(?:Dockerfile(?:\.base)?|manifest\.yaml|mcp-config-transaction\.py)$|ci/e2e-assertion-budget\.json$|docs/resources/starter-prompt\.md$|nemoclaw-blueprint/blueprint\.yaml$|nemoclaw/package\.json$|package\.json$|scripts/(?:brev-launchable-ci-cpu|check-installer-hash|install-openshell|update-hermes-agent)\.sh$|src/lib/actions/sandbox/openshell-child-visible-credentials\.v[0-9]+\.[0-9]+\.[0-9]+\.json$|bin/.*\.(cjs|js|mjs)$|src/.*\.(cts|mts|ts|tsx)$|scripts/.*\.(cjs|cts|js|mjs|mts|ts|tsx)$|test/.*\.(cjs|cts|js|mjs|mts|ts|tsx)$|nemoclaw/src/.*\.(cts|mts|ts|tsx)$)
pass_filenames: false
always_run: true
pass_filenames: true
require_serial: true
priority: 10

- id: env-var-docs
Expand Down Expand Up @@ -255,7 +256,7 @@ repos:
hooks:
- id: tsc-plugin
name: TypeScript (plugin)
entry: npm --prefix nemoclaw run typecheck
entry: npx tsx scripts/checks/cached-command.mts tsc-plugin npm --prefix nemoclaw run typecheck
language: system
pass_filenames: false
files: ^nemoclaw/
Expand All @@ -264,7 +265,7 @@ repos:

- id: tsc-js
name: TypeScript (JS config)
entry: bash -c 'npm run build:cli && npx tsc -p jsconfig.json'
entry: npx tsx scripts/checks/cached-command.mts tsc-js bash -c 'npm run build:cli && npx tsc -p jsconfig.json'
language: system
pass_filenames: false
files: ^(bin|test|scripts)/.*\.js$|^(jsconfig\.json|package(-lock)?\.json)$
Expand All @@ -273,7 +274,7 @@ repos:

- id: tsc-cli
name: TypeScript (CLI)
entry: npm run typecheck:cli -- --incremental
entry: npx tsx scripts/checks/cached-command.mts tsc-cli npm run typecheck:cli -- --incremental
language: system
pass_filenames: false
files: ^(agents/hermes|bin|scripts|src|test|tools|nemoclaw-blueprint/scripts)/.*\.(ts|tsx|mts|cts|json)$|^\.agents/skills/nemoclaw-maintainer-day/scripts/(check-gates|shared)\.ts$|^nemoclaw/src/(lib/subprocess-env|blueprint/private-networks)\.ts$|^(package(-lock)?\.json|tsconfig\.cli\.json|vitest\.config\.ts)$
Expand Down
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ All hooks managed by [prek](https://prek.j178.dev/) (installed via `npm install`
| **commit-msg** | commitlint (Conventional Commits) |
| **pre-push** | Path-scoped incremental CLI/plugin TypeScript checks and checked-JavaScript checks |

`npm run validate:pr` requires a clean committed tree and runs read-only formatting checks.
The repository-check runner reports durations and selects checks from changed paths, including deletions.
Compiler hooks share content-based local result reuse with explicit validation. Changed or unavailable
inputs require execution; reuse does not replace trusted validation or independent CI.

## Working with This Repo

### Before Making Changes
Expand Down
7 changes: 6 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,12 @@ npm run test:changed

Normal Git hooks validate committed changes. If hooks were skipped or unavailable, commit the
changes, run `git fetch origin main`, then run `npm run validate:pr`. This command compares committed
changes with `origin/main`; it does not validate uncommitted changes.
changes with `origin/main` and requires a clean worktree. It checks formatting without applying fixes.
Complete formatting and generated-file updates before the final commit.

Repository checks report per-check durations and select checks from the hook file list, including
deletions. `npm run checks:repository` still runs every repository check. Compiler checks reuse a
successful local result only while the recorded inputs and required generated outputs remain unchanged.

Use the commands defined in [`package.json`](package.json) for component type-checking, builds,
documentation validation, or focused test projects. Use repository-wide validation only when the
Expand Down
2 changes: 1 addition & 1 deletion nemoclaw/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"lint:fix": "oxlint --fix --type-aware src",
"format": "npm --prefix .. run format",
"format:check": "npm --prefix .. run format:check",
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
"typecheck": "tsc --noEmit --incremental --tsBuildInfoFile node_modules/.cache/typecheck-production.tsbuildinfo -p tsconfig.json && tsc --noEmit --incremental --tsBuildInfoFile node_modules/.cache/typecheck-tests.tsbuildinfo -p tsconfig.test.json",
"check": "npm run lint && npm run format:check && npm run typecheck",
"clean": "rm -rf dist/"
},
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"test:titles:check": "tsx scripts/checks/test-title-style.mts",
"bench": "tsx scripts/bench/run.mts",
"check": "npx prek run --all-files --stage pre-commit && npx prek run --all-files --stage manual",
"validate:pr": "npx prek run --from-ref origin/main --to-ref HEAD --stage pre-commit && npx commitlint --from origin/main --to HEAD && npx prek run --from-ref origin/main --to-ref HEAD --stage pre-push",
"validate:pr": "tsx scripts/checks/validate-pr.mts",
"check:diff": "npm run validate:pr",
"checks:repository": "tsx scripts/checks/run.mts",
"checks": "node -e \"console.error('npm run checks runs only narrow repository checks. Use npm run validate:pr for routine PR validation or npm run checks:repository for the narrow runner.')\" && npm run checks:repository",
Expand All @@ -72,7 +72,7 @@
"format:ts": "cd nemoclaw && npm run format",
"check:installer-hash": "bash scripts/check-installer-hash.sh",
"typecheck": "tsc -p jsconfig.json",
"build:policy-boundary": "tsc -p nemoclaw/tsconfig.shared.json",
"build:policy-boundary": "tsc --build nemoclaw/tsconfig.shared.json",
"catalog:compile": "tsx src/lib/inference/serving/generate-catalog.ts",
"catalog:check": "tsx src/lib/inference/serving/generate-catalog.ts --check",
"config-schema:generate": "npm run build:policy-boundary && tsx scripts/config/generate-nemoclaw-config-schema.mts",
Expand Down
272 changes: 272 additions & 0 deletions scripts/checks/cached-command.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/** Reuses successful local compiler checks only while their input bytes are unchanged. */
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { fileURLToPath } from "node:url";

function git(root: string, args: string[]): string {
const result = spawnSync("git", args, { cwd: root, encoding: "utf8" });
if (result.error || result.status !== 0)
throw new Error("Could not identify validation inputs", { cause: result.error });
return result.stdout;
}

function hashPaths(root: string, files: readonly string[], excludeCaches = false): string {
const hash = createHash("sha256");
const activeLinks = new Set<string>();
function visit(file: string): void {
if (excludeCaches && (path.basename(file) === ".cache" || file.endsWith(".tsbuildinfo")))
return;
hash.update(file).update("\0");
const absolute = path.resolve(root, file);
let stat: fs.Stats;
try {
stat = fs.lstatSync(absolute);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
hash.update("missing\0");
return;
}
hash.update(String(stat.mode)).update("\0");
if (stat.isSymbolicLink()) {
hash.update(fs.readlinkSync(absolute));
// Hash the destination too; a stable executable symlink is not tool identity.
const resolved = fs.realpathSync(absolute);
if (fs.statSync(resolved).isFile())
hash.update(String(fs.statSync(resolved).mode)).update(fs.readFileSync(resolved));
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
else {
const relative = path.relative(root, resolved);
if (relative.startsWith("..") || path.isAbsolute(relative) || activeLinks.has(resolved))
throw new Error("External or cyclic directory symlinks prevent validation reuse");
activeLinks.add(resolved);
visit(resolved);
activeLinks.delete(resolved);
}
} else if (stat.isDirectory()) {
for (const entry of fs.readdirSync(absolute).sort()) visit(path.join(file, entry));
} else if (stat.isFile()) hash.update(fs.readFileSync(absolute));
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
else throw new Error("Unsupported validation input");
hash.update("\0");
}
for (const file of [...new Set(files)].sort()) visit(file);
return hash.digest("hex");
}

export function validationEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
// npm and prek inject invocation metadata. Remove it from execution as well as
// hashing so explicit validation and the installed hook have the same inputs.
const result = Object.fromEntries(
Object.entries(env).filter(
([key]) =>
(!/^npm_/i.test(key) || /^npm_config_/i.test(key)) &&
!/^PRE_COMMIT_/.test(key) &&
!["_", "SHLVL", "INIT_CWD", "GIT_PREFIX"].includes(key),
),
);
// The installed Git hook prepends Git's own helper directory. Preserve custom
// overrides, but remove the default injection from both execution and identity.
if (env.GIT_EXEC_PATH) {
const defaultPath = spawnSync("git", ["--exec-path"], {
env: { ...env, GIT_EXEC_PATH: undefined },
encoding: "utf8",
});
if (defaultPath.status === 0 && defaultPath.stdout.trim() === env.GIT_EXEC_PATH) {
delete result.GIT_EXEC_PATH;
result.PATH = result.PATH?.split(path.delimiter)
.filter((entry) => entry !== env.GIT_EXEC_PATH)
.join(path.delimiter);
}
}
if (result.PATH)
result.PATH = [...new Set(result.PATH.split(path.delimiter))].join(path.delimiter);
// Compiler checks use installed tools; npx must never install a missing tool.
result.npm_config_yes = "false";
return Object.fromEntries(
Object.entries(result).sort(([left], [right]) => left.localeCompare(right)),
);
}

export function validationFingerprint(
root: string,
command: readonly string[],
env: NodeJS.ProcessEnv,
outputPaths: readonly string[] = ["dist", "nemoclaw/dist", "nemoclaw/runner-dist"],
): { inputs: string; outputs: string } {
const files = git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"])
.split("\0")
.filter(Boolean);
const refs = git(root, ["rev-parse", "HEAD", "origin/main"]);
const executable =
process.platform === "win32" && /^(?:npm|npx|tsx)$/.test(command[0])
? `${command[0]}.cmd`
: command[0];
const resolved = path.isAbsolute(executable)
? executable
: (env.PATH ?? "")
.split(path.delimiter)
.map((directory) => path.resolve(root, directory, executable))
.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
if (!resolved) throw new Error("Could not resolve the validation executable");
const npm = (env.PATH ?? "")
.split(path.delimiter)
.map((directory) =>
path.resolve(root, directory, process.platform === "win32" ? "npm.cmd" : "npm"),
)
.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
const npmInputs = npm
? [
process.platform === "win32"
? path.join(path.dirname(npm), "node_modules/npm")
: path.resolve(fs.realpathSync(npm), "../.."),
env.npm_config_userconfig ?? path.join(env.HOME ?? env.USERPROFILE ?? root, ".npmrc"),
env.npm_config_globalconfig ??
path.join(env.npm_config_prefix ?? path.resolve(process.execPath, "../.."), "etc/npmrc"),
]
: [];
for (const config of [".npmrc", "nemoclaw/.npmrc", ...npmInputs.slice(1)]) {
const absolute = path.resolve(root, config);
if (
fs.existsSync(absolute) &&
/^\s*(?:script-shell|node-options)\s*=/m.test(fs.readFileSync(absolute, "utf8"))
)
throw new Error("External npm execution configuration prevents validation reuse");
}
const inputs = createHash("sha256")
.update(
JSON.stringify({
root,
command,
env,
refs,
outputPaths,
platform: process.platform,
arch: process.arch,
}),
)
.update(
hashPaths(root, [
...files,
// Compiler globs also include ignored source files inside these trees.
"src",
"test",
"bin",
"scripts",
"agents",
"tools",
".agents",
"nemoclaw/src",
"nemoclaw-blueprint",
process.execPath,
resolved,
".npmrc",
"nemoclaw/.npmrc",
...npmInputs,
]),
)
.update(hashPaths(root, ["node_modules", "nemoclaw/node_modules"], true))
.digest("hex");
const outputs = hashPaths(root, outputPaths);
return { inputs, outputs };
}

type CachedCommandOptions = {
root: string;
label: string;
command: string[];
env?: NodeJS.ProcessEnv;
outputPaths?: readonly string[];
execute?: (env: NodeJS.ProcessEnv) => number;
report?: (message: string) => void;
};

export function runCachedCommand(options: CachedCommandOptions): number {
const { root, label, command } = options;
if (!/^[a-z][a-z0-9-]*$/.test(label) || command.length === 0)
throw new Error("Expected a check name and command");
const env = validationEnvironment(options.env ?? process.env);
const report = options.report ?? console.log;
const started = performance.now();
const cacheDirectory = path.resolve(
root,
git(root, ["rev-parse", "--git-path", "nemoclaw-validation"]).trim(),
);
const receipt = path.join(cacheDirectory, `${label}.json`);
const clean = git(root, ["status", "--porcelain", "--untracked-files=all"]).length === 0;
// External Node loaders can read files outside the repository and tool tree.
const cacheable =
clean &&
!env.NODE_PATH &&
!Object.entries(env).some(
([key, value]) => /^npm_config_(?:node_options|script_shell)$/i.test(key) && value,
) &&
!/--(?:require|import|loader|experimental-loader)\b|(?:^|\s)-r/.test(env.NODE_OPTIONS ?? "");
let before: ReturnType<typeof validationFingerprint> | undefined;
try {
if (cacheable) before = validationFingerprint(root, command, env, options.outputPaths);
if (before && fs.readFileSync(receipt, "utf8") === JSON.stringify(before)) {
report(
`${label}: reused successful validation (${Math.round(performance.now() - started)} ms)`,
);
return 0;
}
} catch {
/* Unavailable or malformed evidence requires executing the check. */
}
try {
fs.rmSync(receipt, { force: true });
} catch {
before = undefined;
}
const status = options.execute ? options.execute(env) : executeCommand(root, command, env);
if (status === 0 && before) {
try {
const after = validationFingerprint(root, command, env, options.outputPaths);
if (
before.inputs === after.inputs &&
git(root, ["status", "--porcelain", "--untracked-files=all"]).length === 0
) {
fs.mkdirSync(cacheDirectory, { recursive: true, mode: 0o700 });
const temporary = `${receipt}.${process.pid}.tmp`;
fs.writeFileSync(temporary, JSON.stringify(after), { mode: 0o600 });
fs.renameSync(temporary, receipt);
}
} catch {
/* A successful check remains valid even when it cannot be cached. */
}
}
report(
`${label}: ${status === 0 ? "passed" : "failed"} (${Math.round(performance.now() - started)} ms)`,
);
return status;
}

function executeCommand(root: string, command: string[], env: NodeJS.ProcessEnv): number {
const executable = command[0];
const windowsShim =
process.platform === "win32" && /^(?:npm|npx|tsx)(?:\.cmd)?$/.test(executable);
const result = spawnSync(
windowsShim ? (env.ComSpec ?? "cmd.exe") : executable,
windowsShim
? ["/d", "/s", "/c", `${executable.replace(/\.cmd$/, "")}.cmd`, ...command.slice(1)]
: command.slice(1),
{ cwd: root, env, stdio: "inherit" },
);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
if (result.error) console.error(result.error.message);
return result.status ?? 1;
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const [label, ...command] = process.argv.slice(2);
process.exitCode = runCachedCommand({
root: path.resolve(import.meta.dirname, "../.."),
label,
command,
// Plugin type checks consume source and dependencies, without compiled artifacts.
outputPaths: label === "tsc-plugin" ? [] : undefined,
});
}
Loading
Loading