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
3 changes: 3 additions & 0 deletions packages/coding-agent/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ interface PackageJson {
diffexConfig?: {
name?: string;
configDir?: string;
production?: boolean;
};
}

Expand All @@ -490,6 +491,8 @@ export const APP_NAME: string = diffexConfigName || "diffex";
export const APP_TITLE: string = diffexConfigName ? APP_NAME : "Diffex";
export const CONFIG_DIR_NAME: string = pkg.diffexConfig?.configDir || ".diffex";
export const VERSION: string = pkg.version || "0.0.0";
/** Set by distribution staging, independently of the user's NODE_ENV. */
export const IS_PRODUCTION_DISTRIBUTION: boolean = isBunBinary || pkg.diffexConfig?.production === true;

export const ENV_AGENT_DIR = "DIFFEX_CODING_AGENT_DIR";
export const ENV_SESSION_DIR = "DIFFEX_CODING_AGENT_SESSION_DIR";
Expand Down
6 changes: 5 additions & 1 deletion packages/coding-agent/src/core/prompt-templates.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
import { basename, dirname, join, resolve, sep } from "path";
import { CONFIG_DIR_NAME } from "../config.ts";
import { CONFIG_DIR_NAME, IS_PRODUCTION_DISTRIBUTION } from "../config.ts";
import { parseFrontmatter } from "../utils/frontmatter.ts";
import { resolvePath } from "../utils/paths.ts";
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.ts";
Expand Down Expand Up @@ -107,6 +107,7 @@ function loadTemplateFromFile(filePath: string, sourceInfo: SourceInfo): PromptT
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(rawContent);

const name = basename(filePath).replace(/\.md$/, "");
if (IS_PRODUCTION_DISTRIBUTION && name === "cl") return null;

// Get description from frontmatter or first non-empty line
let description = frontmatter.description || "";
Expand Down Expand Up @@ -273,6 +274,9 @@ export function expandPromptTemplate(text: string, templates: PromptTemplate[]):
if (!match) return text;

const templateName = match[1];
if (IS_PRODUCTION_DISTRIBUTION && templateName === "cl") {
throw new Error("/cl is unavailable in production Diffex installations.");
}
const argsString = match[2] ?? "";

const template = templates.find((t) => t.name === templateName);
Expand Down
19 changes: 11 additions & 8 deletions packages/coding-agent/src/core/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* System prompt construction and project context loading
*/

import { getDocsPath, getExamplesPath, getReadmePath } from "../config.ts";
import { existsSync } from "node:fs";
import { getDocsPath, getExamplesPath } from "../config.ts";
import { formatSkillsForPrompt, type Skill } from "./skills.ts";

export interface BuildSystemPromptOptions {
Expand Down Expand Up @@ -79,7 +80,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions): string {
}

// Get absolute paths to documentation and examples
const readmePath = getReadmePath();
const docsPath = getDocsPath();
const examplesPath = getExamplesPath();

Expand Down Expand Up @@ -136,13 +136,16 @@ Guidelines:
${guidelines}

Diffex documentation (read only when the user asks about Diffex itself, its SDK, extensions, themes, skills, or TUI):
- Main documentation: ${readmePath}
${
existsSync(docsPath)
? `- Product website: https://diffex.ai/
- Additional docs: ${docsPath}
- Examples: ${examplesPath} (extensions, custom tools, SDK)
- When reading Diffex docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), Diffex packages (docs/packages.md), environment variables (docs/environment-variables.md)
- When working on Diffex topics, read the docs and examples, and follow .md cross-references before implementing
- Always read Diffex.md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
${existsSync(examplesPath) ? `- Examples: ${examplesPath} (extensions, custom tools, SDK)\n- When reading Diffex docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory` : "- Resolve docs/... under Additional docs, not the current working directory; example source files are not included in this installation"}
- When asked about: extensions (docs/extensions.md), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), Diffex packages (docs/packages.md), environment variables (docs/environment-variables.md)
- When working on Diffex topics, read the docs and follow .md cross-references before implementing
- Always read Diffex.md files completely and follow links to related docs (e.g., tui.md for TUI API details)`
: "- Product website: https://diffex.ai/"
}`;

if (appendSection) {
prompt += appendSection;
Expand Down
49 changes: 49 additions & 0 deletions packages/coding-agent/test/production-prompt-templates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test, vi } from "vitest";
import { expandPromptTemplate, loadPromptTemplates } from "../src/core/prompt-templates.ts";

vi.mock("../src/config.ts", () => ({ CONFIG_DIR_NAME: ".diffex", IS_PRODUCTION_DISTRIBUTION: true }));

describe("production prompt templates", () => {
test.each(["project", "global", "directory", "file"])(
"hides /cl loaded from %s while preserving user prompts",
(source) => {
const root = mkdtempSync(join(tmpdir(), "diffex-production-prompts-"));
try {
const cwd = join(root, "project");
const agentDir = join(root, "agent");
const directory =
source === "project"
? join(cwd, ".diffex/prompts")
: source === "global"
? join(agentDir, "prompts")
: join(root, "explicit");
mkdirSync(directory, { recursive: true });
writeFileSync(join(directory, "cl.md"), "Audit repository changelogs");
writeFileSync(join(directory, "review.md"), "Review $ARGUMENTS");
const promptPaths =
source === "file"
? [join(directory, "cl.md"), join(directory, "review.md")]
: source === "directory"
? [directory]
: [];
const templates = loadPromptTemplates({ cwd, agentDir, promptPaths, includeDefaults: true });
expect(templates.map((template) => template.name)).toEqual(["review"]);
expect(expandPromptTemplate("/review changes", templates)).toBe("Review changes");
} finally {
rmSync(root, { recursive: true, force: true });
}
},
);

test.each(["/cl", "/cl audit", "/cl\naudit"])("rejects %s even without loaded templates", (text) => {
expect(() => expandPromptTemplate(text, [])).toThrow("/cl is unavailable in production Diffex installations.");
});

test("preserves ordinary text and similarly named commands", () => {
for (const text of ["Discuss /cl", "/clear", "/clone", "/changelog"])
expect(expandPromptTemplate(text, [])).toBe(text);
});
});
37 changes: 36 additions & 1 deletion packages/coding-agent/test/system-prompt.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,42 @@
import { describe, expect, test } from "vitest";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test, vi } from "vitest";
import { buildSystemPrompt } from "../src/core/system-prompt.ts";

describe("buildSystemPrompt", () => {
test("uses installed reference docs without pointing to an empty README or absent examples", () => {
const directory = mkdtempSync(join(tmpdir(), "diffex-package-docs-"));
try {
mkdirSync(join(directory, "docs"));
writeFileSync(join(directory, "README.md"), "");
vi.stubEnv("DIFFEX_PACKAGE_DIR", directory);
const prompt = buildSystemPrompt({ cwd: process.cwd() });
expect(prompt).toContain(`Additional docs: ${join(directory, "docs")}`);
expect(prompt).toContain("skills (docs/skills.md)");
expect(prompt).toContain("https://diffex.ai/");
expect(prompt).not.toContain("README.md");
expect(prompt).not.toContain("Examples:");
} finally {
vi.unstubAllEnvs();
rmSync(directory, { recursive: true, force: true });
}
});
test("uses the product website when packaged documentation is absent", () => {
const directory = mkdtempSync(join(tmpdir(), "diffex-package-docs-"));
try {
vi.stubEnv("DIFFEX_PACKAGE_DIR", directory);
const prompt = buildSystemPrompt({ cwd: process.cwd() });
expect(prompt).toContain("https://diffex.ai/");
expect(prompt).not.toContain("Main documentation:");
expect(prompt).not.toContain("Additional docs:");
expect(prompt).not.toContain("Examples:");
expect(prompt).not.toContain(directory);
} finally {
vi.unstubAllEnvs();
rmSync(directory, { recursive: true, force: true });
}
});
describe("empty tools", () => {
test("shows (none) for empty tools list", () => {
const prompt = buildSystemPrompt({
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-distribution-payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def inspect_archive(file, license_file, expected_name=None, expected_version=Non
root = roots[0]
if root and any(not name.startswith(root) for name in entries):
fail(str(file), 'file outside distribution root')
for notice in ['LICENSE', 'NOTICE', 'THIRD-PARTY-NOTICES.txt', 'distribution-components.json', 'distribution-files.json']:
for notice in ['LICENSE', 'THIRD-PARTY-NOTICES.txt', 'distribution-components.json', 'distribution-files.json']:
if not entries.get(root + notice, b'').strip():
fail(str(file), 'missing required notice: ' + notice)
if entries[root + 'LICENSE'] != Path(license_file).read_bytes():
Expand Down
7 changes: 7 additions & 0 deletions scripts/cli-distribution.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ export function cliDistribution(root = ".") {
throw new Error(`Bundled workspace version differs from CLI: ${pkg.manifest.name}`);
for (const name of Object.keys(dependencies)) delete optionalDependencies[name];
const manifest = structuredClone(cli.manifest);
manifest.homepage = "https://diffex.ai/";
manifest.diffexConfig = { ...manifest.diffexConfig, production: true };
delete manifest.repository;
delete manifest.bugs;
manifest.files = manifest.files.filter(
(file) => !["notice", "contributing.md"].includes(file.split("/")[0].toLowerCase()),
);
manifest.dependencies = Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)));
manifest.optionalDependencies = Object.fromEntries(
Object.entries(optionalDependencies).sort(([a], [b]) => a.localeCompare(b)),
Expand Down
6 changes: 5 additions & 1 deletion scripts/distribution-payload.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ test("accepts a tarball with readable JavaScript, declarations, and complete not

for (const notice of [
"LICENSE",
"NOTICE",
"THIRD-PARTY-NOTICES.txt",
"distribution-components.json",
"distribution-files.json",
Expand All @@ -91,6 +90,11 @@ for (const notice of [
});
}

test("accepts consolidated attribution without a separate NOTICE file", () => {
const result = inspect({}, "NOTICE");
assert.equal(result.status, 0, result.stderr);
});

for (const [name, content, reason] of [
["dist/implementation.ts", "export const value = 1", /prohibited source file/],
["dist/renamed.js", "export interface Hidden {", /TypeScript implementation content/],
Expand Down
10 changes: 4 additions & 6 deletions scripts/prepare-distribution.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ export function preparePackageDistribution(directory) {
notice: "LICENSE",
},
];
let notices =
"Diffex modifications copyright 2026 Luntraa, Inc.\nPi-derived portions retain the complete MIT terms in LICENSE.\n";
const notice =
"Diffex\nCopyright 2026 Luntraa, Inc.\n\nWhere this distribution contains Pi Agent Harness-derived portions,\nthose portions retain their MIT terms and copyright notice in LICENSE.\nThe Apache-2.0 package declaration does not replace third-party terms.\n";
let notices = notice;
if (pkg.name === "@diffexai/diffex") {
for (const workspace of ["evolution", "harness-trace"]) {
components.push({
Expand Down Expand Up @@ -95,10 +96,7 @@ export function preparePackageDistribution(directory) {
});
}
cpSync(join(root, "LICENSE"), join(absolute, "LICENSE"));
writeFileSync(
join(absolute, "NOTICE"),
`Diffex\nCopyright 2026 Luntraa, Inc.\n\nWhere this distribution contains Pi Agent Harness-derived portions,\nthose portions retain their MIT terms and copyright notice in LICENSE.\nThe Apache-2.0 package declaration does not replace third-party terms.\n`,
);
writeFileSync(join(absolute, "NOTICE"), notice);
writeFileSync(join(absolute, "THIRD-PARTY-NOTICES.txt"), notices);
for (const component of components) {
if (component.notice)
Expand Down
106 changes: 105 additions & 1 deletion scripts/prepare-distribution.test.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,114 @@
import assert from "node:assert/strict";
import { execFileSync, spawnSync } from "node:child_process";
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import { PROJECT_CHANGELOGS } from "./prepare-distribution.mjs";
import { cliDistribution } from "./cli-distribution.mjs";

test("packed CLI exposes the product homepage and preserves referenced documentation", () => {
const root = mkdtempSync(join(tmpdir(), "diffex-cli-presentation-"));
try {
const copy = (file) => {
mkdirSync(join(root, file, ".."), { recursive: true });
cpSync(file, join(root, file));
};
for (const script of [
"prepare-distribution.mjs",
"release-packages.mjs",
"release-distribution-manifest.mjs",
"package-workspaces.mjs",
"check-distribution-payload.py",
"cli-distribution.mjs",
"stage-cli-distribution.mjs",
]) copy(`scripts/${script}`);
for (const file of [
"LICENSE",
"docs/release/distribution-policy.json",
"legal/upstream/highlight-11.9.0.LICENSE",
"node_modules/marked/LICENSE",
"packages/coding-agent/npm-shrinkwrap.json",
"packages/coding-agent/install-lock/package.json",
"packages/coding-agent/install-lock/package-lock.json",
]) copy(file);
const directories = [
"packages/coding-agent",
"packages/evolution",
"packages/harness-trace",
...cliDistribution().bundled.map((pkg) => pkg.directory),
];
for (const directory of directories) {
copy(`${directory}/package.json`);
for (const folder of ["src", "dist", "docs"])
mkdirSync(join(root, directory, folder), { recursive: true });
writeFileSync(join(root, directory, "src/index.ts"), "export const value = 1;\n");
writeFileSync(join(root, directory, "dist/index.js"), "export const value = 1;\n");
for (const file of ["README.md", "CONTRIBUTING.md", "docs/skills.md", "CHANGELOG.md"])
writeFileSync(join(root, directory, file), "Repository documentation\n");
}
const pkg = join(root, "packages/coding-agent");
cpSync("packages/coding-agent/docs", join(pkg, "docs"), { recursive: true });
const excludedDocs = [
"development.md",
"docs.json",
"index.md",
"invocation-traces.md",
"images/doom-extension.png",
"images/exy.png",
];
const metadata = JSON.parse(readFileSync(join(pkg, "package.json"), "utf8"));
metadata.files.push("CONTRIBUTING.md");
writeFileSync(join(pkg, "package.json"), JSON.stringify(metadata));
for (const platform of ["darwin", "win32"])
mkdirSync(join(root, "packages/tui/native", platform, "prebuilds"), { recursive: true });
writeFileSync(
join(root, "prepare.mjs"),
'import { preparePackageDistribution } from "./scripts/prepare-distribution.mjs";\n' +
'import { stageCliDistribution } from "./scripts/stage-cli-distribution.mjs";\n' +
'preparePackageDistribution("packages/coding-agent");\n' +
'stageCliDistribution("packages/coding-agent", "stage");\n',
);
execFileSync("node", ["prepare.mjs"], {
cwd: root,
env: { ...process.env, DIFFEX_SOURCE_COMMIT: "a".repeat(40) },
});
const [packed] = JSON.parse(
execFileSync("npm", ["pack", "--ignore-scripts", "--json", "--cache", join(root, "cache")], {
cwd: join(root, "stage"),
encoding: "utf8",
}),
);
const archive = join(root, "stage", packed.filename);
const published = JSON.parse(execFileSync("tar", ["-xOf", archive, "package/package.json"], { encoding: "utf8" }));
assert.equal(published.homepage, "https://diffex.ai/");
assert.equal(published.repository, undefined);
assert.equal(published.bugs, undefined);
assert.equal(published.diffexConfig.production, true);
assert.equal(execFileSync("tar", ["-xOf", archive, "package/README.md"]).length, 0);
for (const file of readdirSync(join(pkg, "docs"), { recursive: true })) {
const source = join(pkg, "docs", file);
if (!statSync(source).isFile()) continue;
const path = `docs/${file.replaceAll("\\", "/")}`;
if (excludedDocs.includes(file.replaceAll("\\", "/"))) {
assert(!packed.files.some((entry) => entry.path === path), `${path} must not ship`);
} else {
assert.deepEqual(execFileSync("tar", ["-xOf", archive, `package/${path}`]), readFileSync(source), `${path} must remain intact`);
}
}
for (const file of excludedDocs) assert(existsSync(join(pkg, "docs", file)), `${file} must remain in source`);
for (const { path } of packed.files)
assert(!path.split("/").some((part) => ["notice", "contributing.md"].includes(part.toLowerCase())), path);
const attribution = execFileSync("tar", ["-xOf", archive, "package/THIRD-PARTY-NOTICES.txt"], { encoding: "utf8" });
assert(attribution.startsWith(readFileSync(join(pkg, "NOTICE"), "utf8")));
assert.match(attribution, /highlight\.js@11\.9\.0/);
assert.match(attribution, /marked@18\.0\.5/);
const result = spawnSync("python3", ["scripts/check-distribution-payload.py", archive], { cwd: root, encoding: "utf8" });
assert.equal(result.status, 0, result.stderr);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

test("actual npm tarball carries notices and excludes debug sources after preparation", () => {
const root = mkdtempSync(join(tmpdir(), "diffex-prepack-test-"));
Expand Down
Loading
Loading