Skip to content
Open
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
22 changes: 22 additions & 0 deletions src/agent/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import {
UpdateMetadata,
} from "./types.js";

const EMPTY_PROMPT_SECTION = "";
const REPOSITORY_CI_GROUNDING_INSTRUCTION = `Repository evidence grounding discipline:
- When documenting this repository's CI, scheduled jobs, or OpenWiki integration, read and cite the checked-out workflow/config files from this repository before changing those docs.
- Treat this repository's workflow files, package scripts, configuration files, and existing user-authored briefs as the source of truth for this repository's actual automation.
- The OpenWiki CLI reference and OpenWiki's own README/examples describe the tool's defaults and upstream examples; use them only for generic OpenWiki behavior, never as a substitute for this repository's checked-out configuration.
- If the repository configuration conflicts with OpenWiki's defaults or examples, document the repository configuration and call the upstream default/example out only when that contrast is directly relevant.`;

function formatLastUpdate(lastUpdate: UpdateMetadata | null): string {
if (lastUpdate === null) {
return "No previous OpenWiki update metadata was found.";
Expand Down Expand Up @@ -96,6 +103,8 @@ Existing documentation discipline:
- Summarize and link to existing docs when they are still useful instead of duplicating them wholesale.
- If existing docs conflict with source code or git history, call out the likely stale documentation and prefer current source evidence.

${output.repositoryEvidenceGroundingInstruction}

${output.rootAgentInstructions}

OpenWiki CLI reference:
Expand Down Expand Up @@ -291,6 +300,9 @@ Start with ${output.quickstartPath} as the entrypoint. Then create section direc
Wiki brief:
${formatWikiGoal(context.wikiGoal)}

Repository automation context:
${formatRepositoryCiSummary(context.ciSummary)}

Git context:
${context.gitSummary}
`.trim(),
Expand All @@ -310,6 +322,9 @@ ${formatLastUpdate(context.lastUpdate)}
Wiki brief:
${formatWikiGoal(context.wikiGoal)}

Repository automation context:
${formatRepositoryCiSummary(context.ciSummary)}

Git change summary:
${context.gitSummary}
`.trim(),
Expand All @@ -321,6 +336,10 @@ function formatWikiGoal(wikiGoal: string | undefined): string {
return wikiGoal?.trim() || "(not provided)";
}

function formatRepositoryCiSummary(ciSummary: string | undefined): string {
return ciSummary?.trim() || "(not applicable)";
}

type OutputPromptConfig = {
canonicalLocationInstruction: string;
docsLocation: string;
Expand All @@ -334,6 +353,7 @@ type OutputPromptConfig = {
quickstartPath: string;
removePlanCommand: string;
rootAgentInstructions: string;
repositoryEvidenceGroundingInstruction: string;
searchBoundaryInstruction: string;
sectionDirectoryInstruction: string;
subjectLabel: string;
Expand Down Expand Up @@ -422,6 +442,7 @@ function getOutputPromptConfig(
planPath: "/_plan.md",
quickstartPath: "/quickstart.md",
removePlanCommand: "rm -f ./_plan.md",
repositoryEvidenceGroundingInstruction: EMPTY_PROMPT_SECTION,
rootAgentInstructions:
"Root agent instruction files:\n- Repository /AGENTS.md and /CLAUDE.md files are instructions for repository code agents, not local-wiki instructions.\n- When inspecting a configured local repository as evidence, do not read or follow those files unless the user explicitly asks about their contents.\n- Local wiki mode does not manage repository /AGENTS.md or /CLAUDE.md files.\n- Do not create or edit agent instruction files unless the user explicitly asks for that as a separate repository documentation task.",
searchBoundaryInstruction:
Expand Down Expand Up @@ -463,6 +484,7 @@ function getOutputPromptConfig(
planPath: "/openwiki/_plan.md",
quickstartPath: "/openwiki/quickstart.md",
removePlanCommand: "rm -f ./openwiki/_plan.md",
repositoryEvidenceGroundingInstruction: REPOSITORY_CI_GROUNDING_INSTRUCTION,
rootAgentInstructions: `Root agent instruction files:
- Do not create or update repository /AGENTS.md or /CLAUDE.md files during normal code wiki runs.
- Keep generated wiki content under the repository /openwiki directory.
Expand Down
1 change: 1 addition & 0 deletions src/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type UpdateMetadata = {
};

export type RunContext = {
ciSummary?: string;
lastUpdate: UpdateMetadata | null;
gitSummary: string;
wikiGoal?: string;
Expand Down
150 changes: 149 additions & 1 deletion src/agent/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
import {
lstat,
mkdir,
readdir,
readFile,
realpath,
rm,
writeFile,
} from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { OPEN_WIKI_DIR, UPDATE_METADATA_PATH } from "../constants.js";
Expand All @@ -24,6 +32,17 @@ import type { Dirent } from "node:fs";
const execFileAsync = promisify(execFile);
const LOCAL_WIKI_METADATA_PATH = ".last-update.json";
const TEMPORARY_PLAN_FILE = "_plan.md";
const GITHUB_WORKFLOWS_DIR = ".github/workflows";
const YAML_EXTENSIONS = new Set([".yaml", ".yml"]);
const REPOSITORY_CI_FILE_PATHS = [".gitlab-ci.yml", "bitbucket-pipelines.yml"];
const REPOSITORY_CI_CONTEXT_HEADING =
"Repository automation context (checked-out CI configuration)";
const NO_REPOSITORY_CI_CONFIG_MESSAGE =
"No checked-out CI configuration files were found in this repository.";
const BYTES_PER_KIB = 1024;
const REPOSITORY_CI_CONTEXT_MAX_BYTES = 64 * BYTES_PER_KIB;
const TRUNCATED_CONTEXT_PREFIX = "[...truncated, ";
const TRUNCATED_CONTEXT_SUFFIX = " more bytes]";

export type OpenWikiContentSnapshot = string;

Expand Down Expand Up @@ -68,11 +87,140 @@ export async function createRunContext(

return {
lastUpdate,
ciSummary: await createRepositoryCiSummary(cwd),
gitSummary: await createGitSummary(command, cwd, lastUpdate),
wikiGoal,
};
}

async function createRepositoryCiSummary(cwd: string): Promise<string> {
const ciFilePaths = await findRepositoryCiFilePaths(cwd);

if (ciFilePaths.length === 0) {
return NO_REPOSITORY_CI_CONFIG_MESSAGE;
}

const fileBlocks = await Promise.all(
ciFilePaths.map(async (relativePath) => {
const content = await readFile(path.join(cwd, relativePath), "utf8");

return [`--- ${relativePath} ---`, content.trimEnd()].join("\n");
}),
);

return truncateTextByBytes(
[REPOSITORY_CI_CONTEXT_HEADING, ...fileBlocks].join("\n\n"),
REPOSITORY_CI_CONTEXT_MAX_BYTES,
);
}

async function findRepositoryCiFilePaths(cwd: string): Promise<string[]> {
const githubWorkflowPaths = await findGitHubWorkflowPaths(cwd);
const directCiPaths = await filterExistingFiles(
cwd,
Comment thread
corridor-security[bot] marked this conversation as resolved.
REPOSITORY_CI_FILE_PATHS,
);

return [...githubWorkflowPaths, ...directCiPaths].sort((left, right) =>
left.localeCompare(right),
);
}

async function findGitHubWorkflowPaths(cwd: string): Promise<string[]> {
const workflowsDir = path.join(cwd, GITHUB_WORKFLOWS_DIR);

try {
const workflowsDirStat = await lstat(workflowsDir);

if (!workflowsDirStat.isDirectory()) {
return [];
}

const realCwd = await realpath(cwd);
const realWorkflowsDir = await realpath(workflowsDir);

if (!isPathInsideDirectory(realWorkflowsDir, realCwd)) {
return [];
}

const entries = await readdir(workflowsDir, { withFileTypes: true });

return entries
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.filter((fileName) => YAML_EXTENSIONS.has(path.extname(fileName)))
.map((fileName) => path.posix.join(GITHUB_WORKFLOWS_DIR, fileName));
} catch (error) {
if (isFileNotFoundError(error)) {
return [];
}

throw error;
}
}

async function filterExistingFiles(
cwd: string,
relativePaths: string[],
): Promise<string[]> {
const realCwd = await realpath(cwd);
const existingPaths = await Promise.all(
relativePaths.map(async (relativePath) => {
try {
const filePath = path.join(cwd, relativePath);
const fileStat = await lstat(filePath);

if (!fileStat.isFile()) {
return null;
}

const realFilePath = await realpath(filePath);

return isPathInsideDirectory(realFilePath, realCwd)
? relativePath
: null;
} catch (error) {
if (isFileNotFoundError(error)) {
return null;
}

throw error;
}
}),
);

return existingPaths.filter((relativePath) => relativePath !== null);
}

function isPathInsideDirectory(
filePath: string,
directoryPath: string,
): boolean {
const relativePath = path.relative(directoryPath, filePath);

return (
relativePath.length > 0 &&
!relativePath.startsWith("..") &&
!path.isAbsolute(relativePath)
);
}

function truncateTextByBytes(text: string, maxBytes: number): string {
const buffer = Buffer.from(text, "utf8");

if (buffer.byteLength <= maxBytes) {
return text;
}

const remainingBytes = buffer.byteLength - maxBytes;
const truncatedText = buffer.subarray(0, maxBytes).toString("utf8").trimEnd();

return [
truncatedText,
`${TRUNCATED_CONTEXT_PREFIX}${remainingBytes}${TRUNCATED_CONTEXT_SUFFIX}`,
].join("\n\n");
}

async function readRunWikiGoal(
cwd: string,
outputMode: OpenWikiOutputMode,
Expand Down
20 changes: 20 additions & 0 deletions test/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import {
createSystemPrompt,
} from "../src/agent/prompt.ts";

const REPOSITORY_CI_GROUNDING_RULE =
"When documenting this repository's CI, scheduled jobs, or OpenWiki integration";
const OPENWIKI_REFERENCE_BOUNDARY =
"The OpenWiki CLI reference and OpenWiki's own README/examples describe the tool's defaults";

/**
* Guards against the 0.2 regression where the shared "Canonical wiki location"
* and "Wiki-first question answering" blocks hardcoded ~/.openwiki/wiki and
Expand Down Expand Up @@ -125,3 +130,18 @@ describe("createSystemPrompt diagram guidance", () => {
expect(init).not.toContain("adding one is a valuable improvement");
});
});

describe("createSystemPrompt repository evidence grounding", () => {
test("grounds repository update CI documentation in checked-out workflow files", () => {
const prompt = createSystemPrompt("update", "repository");

expect(prompt).toContain(REPOSITORY_CI_GROUNDING_RULE);
expect(prompt).toContain(OPENWIKI_REFERENCE_BOUNDARY);
});

test("does not add repository CI grounding rules to local wiki runs", () => {
const prompt = createSystemPrompt("update", "local-wiki");

expect(prompt).not.toContain(REPOSITORY_CI_GROUNDING_RULE);
});
});
Loading