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
5 changes: 5 additions & 0 deletions .changeset/tidy-wikis-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openwiki": minor
---

feat: add configurable `manage` and `preserve` policies for code-mode root agent files
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,21 @@ Locally the setup wizard saves this to `~/.openwiki/.env`. In CI, set it as a re

Everything OpenWiki writes is plain Markdown you own and version alongside your code.

- **Agents read it as memory.** On each `code` run, OpenWiki maintains an `AGENTS.md` and `CLAUDE.md` at the repo root that point your coding agent at the wiki. It only rewrites its own `<!-- OPENWIKI:START -->…<!-- OPENWIKI:END -->` block and leaves the rest of each file untouched.
- **Agents read it as memory.** On each `code` run, OpenWiki maintains an `AGENTS.md` and `CLAUDE.md` at the repo root that point your coding agent at the wiki. It only rewrites its own `<!-- OPENWIKI:START -->…<!-- OPENWIKI:END -->` block and leaves the rest of each file untouched. Repositories that own those files themselves can select the `preserve` policy below.
- **You set the brief.** Repository-specific instructions live in `openwiki/INSTRUCTIONS.md`, a user-authored file OpenWiki reads for scope and priorities but never rewrites during normal runs.
- **No-op runs are free.** After a run, OpenWiki snapshots the `openwiki/` directory and only records new metadata when something actually changed, so scheduled workflows never churn.
- **Local, private config.** Provider choice, keys, and optional LangSmith tracing are saved to `~/.openwiki/.env` on your machine.

Agent-file ownership is committed in `openwiki.config.yaml`. The default policy is `manage`; set `preserve` to leave existing root files byte-for-byte unchanged and keep missing files absent during init, update, chat, and scheduled runs:

```yaml
codeMode:
agentFiles:
policy: preserve
```

Use `--agent-files-policy <manage|preserve>` to override that policy for one code-mode run without rewriting the config. Resolution order is CLI override, committed config, then the `manage` default. The `agentFiles` mapping leaves room for future path configuration without changing the meaning of the policy setting. A newly generated workflow reads the committed config on later runs and omits `AGENTS.md` and `CLAUDE.md` from its pull request paths when the committed policy is `preserve`.

## Open Knowledge Format

OpenWiki emits [Google Open Knowledge Format (OKF) v0.1](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) bundles in both modes, so your wiki is portable to any OKF-aware tool.
Expand Down Expand Up @@ -359,6 +369,7 @@ openwiki "generate docs" # start with an initial request
openwiki -p "what can you do?" # one-shot, print, and exit
openwiki --init # initialize code docs (personal: openwiki personal --init)
openwiki --update # update code docs (personal: openwiki personal --update)
openwiki code --update --agent-files-policy preserve # preserve root agent files for one run
openwiki visualize # interactive graph + live reader
openwiki auth <provider> # authenticate a connector (slack, gmail, x, notion)
openwiki ingest <source> # run connector ingestion (all, or a connector/instance)
Expand Down
11 changes: 8 additions & 3 deletions src/cli/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { getDisplayModelId, isExitMessage } from "../format.js";
import { appendRunLogEvent } from "../run-log/reducer.js";
import type { RunLogItem } from "../run-log/types.js";
import {
getCodeModeRepoSetupOptions,
getRunModeCwd,
getRunModeOutputMode,
shouldAutoExitStartupRun,
Expand Down Expand Up @@ -431,9 +432,13 @@ export function App({ command }: AppProps) {
telemetryContext,
async () => {
if (runMode === "code") {
await ensureCodeModeRepoSetup(runtimeCwd, {
createWorkflow: resolvedCommand === "init",
});
await ensureCodeModeRepoSetup(
runtimeCwd,
getCodeModeRepoSetupOptions({
...command,
command: resolvedCommand,
}),
);
}

await scheduler.yield();
Expand Down
59 changes: 59 additions & 0 deletions src/cli/commands.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { isValidModelId, normalizeModelId } from "../config/constants.js";
import {
isCodeModeAgentFilesPolicy,
type CodeModeAgentFilesPolicy,
} from "../config/code-mode.js";
import type { OpenWikiCommand } from "../agent/types.js";
import { resolveLanguage } from "../platform/language.js";
import { isAuthProviderId } from "../auth/providers.js";
Expand Down Expand Up @@ -71,6 +75,7 @@ export type CliCommand =
dryRun: boolean;
language: string | null;
languageWarning: string | null;
agentFilesPolicy: CodeModeAgentFilesPolicy | null;
mode: OpenWikiRunMode;
modeSource: OpenWikiRunModeSource;
modelId: string | null;
Expand Down Expand Up @@ -408,6 +413,7 @@ function parseRunCommand(
initialMode: OpenWikiRunMode,
initialModeSource: OpenWikiRunModeSource,
): CliCommand {
let agentFilesPolicy: CodeModeAgentFilesPolicy | null = null;
let dryRun = false;
let language: string | null = null;
let mode = initialMode;
Expand Down Expand Up @@ -444,6 +450,43 @@ function parseRunCommand(
continue;
}

if (arg === "--agent-files-policy") {
const nextArg = argv[index + 1];

if (!nextArg || nextArg.startsWith("-")) {
return {
kind: "error",
exitCode: 1,
message: "--agent-files-policy requires manage or preserve.",
};
}
if (!isCodeModeAgentFilesPolicy(nextArg)) {
return {
kind: "error",
exitCode: 1,
message: `Invalid --agent-files-policy value: ${nextArg}. Expected manage or preserve.`,
};
}

agentFilesPolicy = nextArg;
index += 1;
continue;
}

if (arg.startsWith("--agent-files-policy=")) {
const [, value = ""] = arg.split("=", 2);
if (!isCodeModeAgentFilesPolicy(value)) {
return {
kind: "error",
exitCode: 1,
message: `Invalid --agent-files-policy value: ${value}. Expected manage or preserve.`,
};
}

agentFilesPolicy = value;
continue;
}

if (arg === "--debug") {
// isDebugMode() reads OPENWIKI_DEBUG; setting it at parse time is the
// least-invasive way to opt into full credential/error diagnostics.
Expand Down Expand Up @@ -643,6 +686,14 @@ function parseRunCommand(
mode = "code";
}

if (agentFilesPolicy !== null && mode !== "code") {
return {
kind: "error",
exitCode: 1,
message: "--agent-files-policy is only available in code mode.",
};
}

if (print && !shouldStart) {
return {
kind: "error",
Expand All @@ -654,6 +705,7 @@ function parseRunCommand(
return {
kind: "run",
exitCode: 0,
agentFilesPolicy,
command,
dryRun,
language: resolvedLanguage.language ?? null,
Expand Down Expand Up @@ -736,6 +788,7 @@ export const helpContent: HelpContent = {
usage: [
"openwiki [--init|--update] [message]",
"openwiki code [--init|--update] [message]",
"openwiki code --agent-files-policy <manage|preserve> [--init|--update] [message]",
"openwiki personal [--init|--update] [message]",
"openwiki --mode <personal|code> [--init|--update] [message]",
"openwiki [--language <locale>] [--init|--update] [message]",
Expand Down Expand Up @@ -843,6 +896,11 @@ export const helpContent: HelpContent = {
label: "-p, --print",
description: "Run once and print the final assistant output.",
},
{
label: "--agent-files-policy <manage|preserve>",
description:
"Override root agent-file handling for this code-mode run (default: config, then manage).",
},
{
label: "--debug",
description:
Expand Down Expand Up @@ -884,6 +942,7 @@ export const helpContent: HelpContent = {
"openwiki personal --init",
"openwiki code --init",
"openwiki --update",
"openwiki code --update --agent-files-policy preserve",
"openwiki --update --mode personal",
'openwiki "What can you do?"',
'openwiki -p "Summarize what OpenWiki can do"',
Expand Down
11 changes: 11 additions & 0 deletions src/cli/run-mode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { OpenWikiOutputMode } from "../agent/types.js";
import { openWikiLocalWikiDir } from "../config/openwiki-home.js";
import type { CodeModeRepoSetupOptions } from "../ingestion/code-mode.js";
import type { CliCommand, OpenWikiRunMode } from "./commands.js";

/**
Expand Down Expand Up @@ -54,6 +55,16 @@ export function getRunModeOutputMode(
return mode === "code" ? "repository" : "local-wiki";
}

/** Maps a parsed code-mode command to repository setup behavior. */
export function getCodeModeRepoSetupOptions(
command: Extract<CliCommand, { kind: "run" }>,
): CodeModeRepoSetupOptions {
return {
agentFilesPolicy: command.agentFilesPolicy,
createWorkflow: command.command === "init",
};
}

/**
* Reports whether a startup run should auto-exit when finished: a real
* (non-dry-run, non-print) init or update run that was asked to start.
Expand Down
13 changes: 9 additions & 4 deletions src/cli/runners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ import type { CliCommand } from "./commands.js";
import { isDebugMode } from "./debug.js";
import { getAuthFix, getAuthFixSteps } from "./diagnostics/auth-fix.js";
import { getErrorDiagnostics } from "./diagnostics/error-diagnostics.js";
import { getRunModeCwd, getRunModeOutputMode } from "./run-mode.js";
import {
getCodeModeRepoSetupOptions,
getRunModeCwd,
getRunModeOutputMode,
} from "./run-mode.js";
import {
formatPowerScheduleStatus,
formatScheduleHeader,
Expand Down Expand Up @@ -283,9 +287,10 @@ export async function runPrintCommand(
telemetryContext,
async () => {
if (command.mode === "code") {
await ensureCodeModeRepoSetup(runtimeCwd, {
createWorkflow: command.command === "init",
});
await ensureCodeModeRepoSetup(
runtimeCwd,
getCodeModeRepoSetupOptions(command),
);
}

// Code-mode connectors (e.g. langsmith) pull their evidence and augment
Expand Down
124 changes: 124 additions & 0 deletions src/config/code-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { parse } from "yaml";
import { isFileNotFoundError } from "../platform/fs-errors.js";

export const CODE_MODE_CONFIG_FILENAME = "openwiki.config.yaml";
export const DEFAULT_CODE_MODE_AGENT_FILES_POLICY = "manage";

export type CodeModeAgentFilesPolicy = "manage" | "preserve";
export type CodeModeAgentFilesPolicySource = "cli" | "config" | "default";

export interface ResolvedCodeModeAgentFilesPolicy {
/** Policy applied to the current run. */
policy: CodeModeAgentFilesPolicy;
/** Where the current run's policy came from. */
source: CodeModeAgentFilesPolicySource;
/** Committed policy used by later runs, or null when the default applies. */
configuredPolicy: CodeModeAgentFilesPolicy | null;
}

export function isCodeModeAgentFilesPolicy(
value: string,
): value is CodeModeAgentFilesPolicy {
return value === "manage" || value === "preserve";
}

/**
* Resolve the current code-mode agent-file policy without mutating repository
* configuration. CLI overrides win over committed config, which wins over the
* backward-compatible `manage` default.
*/
export async function resolveCodeModeAgentFilesPolicy(
cwd: string,
cliOverride: CodeModeAgentFilesPolicy | null = null,
): Promise<ResolvedCodeModeAgentFilesPolicy> {
const configuredPolicy = await readConfiguredAgentFilesPolicy(cwd);

if (cliOverride !== null) {
return { configuredPolicy, policy: cliOverride, source: "cli" };
}

if (configuredPolicy !== null) {
return {
configuredPolicy,
policy: configuredPolicy,
source: "config",
};
}

return {
configuredPolicy: null,
policy: DEFAULT_CODE_MODE_AGENT_FILES_POLICY,
source: "default",
};
}

async function readConfiguredAgentFilesPolicy(
cwd: string,
): Promise<CodeModeAgentFilesPolicy | null> {
let text: string;

try {
text = await readFile(path.join(cwd, CODE_MODE_CONFIG_FILENAME), "utf8");
} catch (error) {
if (isFileNotFoundError(error)) {
return null;
}
throw error;
}

let config: unknown;
try {
config = parse(text) as unknown;
} catch (error) {
throw invalidConfig(errorMessage(error));
}

if (config === null || config === undefined) {
return null;
}
if (!isRecord(config)) {
throw invalidConfig("the document root must be a mapping.");
}

const codeMode = config.codeMode;
if (codeMode === undefined) {
return null;
}
if (!isRecord(codeMode)) {
throw invalidConfig("codeMode must be a mapping.");
}

const agentFiles = codeMode.agentFiles;
if (agentFiles === undefined) {
return null;
}
if (!isRecord(agentFiles)) {
throw invalidConfig("codeMode.agentFiles must be a mapping.");
}

const policy = agentFiles.policy;
if (policy === undefined) {
return null;
}
if (typeof policy !== "string" || !isCodeModeAgentFilesPolicy(policy)) {
throw invalidConfig(
"codeMode.agentFiles.policy must be manage or preserve.",
);
}

return policy;
}

function invalidConfig(message: string): Error {
return new Error(`Invalid ${CODE_MODE_CONFIG_FILENAME}: ${message}`);
}

function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
Loading