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
10 changes: 10 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# test/starter-prompt-docs.test.ts asserts the exact working-tree bytes of these
# files: the starter-prompt generator rejects a carriage return, the prompt
# assets are byte-compared against their pinned Git blobs, and
# local-credential-form.html is compared against the SHA-256 that users verify
# before they run the local credential helper. A core.autocrlf checkout would
# rewrite them and break those contracts, so pin the line endings here.
/docs/resources/starter-prompt.md text eol=lf
/docs/resources/prompt-assets/*.md text eol=lf
/docs/resources/local-credential-form.html text eol=lf

/skills/nemoclaw/** linguist-generated=true
/skills/nemoclaw/**/*.md diff=markdown
/tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/**/*.bundle linguist-generated=true -diff
15 changes: 15 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,21 @@ Install the following before you begin.
- Docker (running)
- [hadolint](https://github.com/hadolint/hadolint) (Dockerfile linter — `brew install hadolint` on macOS)

### Windows Line Endings

`.gitattributes` pins the files whose exact bytes the repository checks assert, so a new clone keeps LF even when `core.autocrlf` is `true`.

Git does not rewrite a file that is already in your working tree.
If you cloned before that rule existed, the file keeps CRLF, `git status` reports no change, and `npm run checks:repository` reports `use LF line endings`.

Commit or stash your work first, because the next commands discard uncommitted changes.
Then normalize the checkout once:

```bash
git rm --cached -r .
git reset --hard
```

## Getting Started

From the repository root, prepare the checkout with one command:
Expand Down
67 changes: 67 additions & 0 deletions test/starter-prompt-docs.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
import os from "node:os";
Expand All @@ -14,6 +15,7 @@ import {
renderStarterPromptSnippet,
runStarterPromptGenerator,
STARTER_PROMPT_GENERATED_PATH,
STARTER_PROMPT_SOURCE_PATH,
} from "../scripts/generate-starter-prompt.mts";
import {
createGitRunner,
Expand Down Expand Up @@ -1277,3 +1279,68 @@ describe("starter prompt docs CTA", () => {
expect(promptSource).toContain("nemo-deepagents onboard");
});
});

describe("starter prompt checkout line endings", () => {
// Point core.attributesFile at an absent path and set GIT_ATTR_NOSYSTEM so the
// result comes from the repository .gitattributes alone. Without that, a
// contributor's global "* text=auto" would decide the outcome.
const absentAttributesFile = path.join(os.tmpdir(), "nemoclaw-absent-gitattributes");

function checkoutEol(relativePath: string): string {
const result = spawnSync(
"git",
[
"-c",
`core.attributesFile=${absentAttributesFile}`,
"check-attr",
"eol",
"--",
relativePath,
],
{
cwd: repoRoot,
encoding: "utf8",
env: { ...process.env, GIT_ATTR_NOSYSTEM: "1" },
timeout: 10_000,
},
);
const diagnostic = [
`git check-attr did not run for ${relativePath}:`,
`status=${result.status}`,
`signal=${result.signal}`,
result.error?.message ?? "",
result.stderr ?? "",
]
.join(" ")
.trim();
expect(result.error, diagnostic).toBeUndefined();
expect(result.status, diagnostic).toBe(0);
return result.stdout;
}

// check-attr answers for any path, so require the file before trusting its
// attribute; otherwise a rename would leave these assertions passing.
function readCheckoutEol(relativePath: string): string {
expect(fs.existsSync(path.join(repoRoot, relativePath))).toBe(true);
return checkoutEol(relativePath);
}

// Every file whose exact working-tree bytes this suite asserts.
const bytePinnedPaths = [
STARTER_PROMPT_SOURCE_PATH,
...Object.values(promptAssets).map((asset) => asset.path),
"docs/resources/local-credential-form.html",
];

it.each(
bytePinnedPaths,
)("checks out %s with LF so an autocrlf clone keeps the bytes this suite asserts (#8648)", (relativePath) => {
expect(readCheckoutEol(relativePath)).toContain(`${relativePath}: eol: lf`);
});

// Asserts only the absence of the LF pin. Asserting "unspecified" would also
// forbid a repository-wide "* text=auto" rule, which is unrelated policy.
it("leaves a file without a byte-exact contract unpinned (#8648)", () => {
expect(readCheckoutEol("docs/resources/agent-skills.mdx")).not.toContain("eol: lf");
});
});
Loading