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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ Scheduled/CI runs send anonymous reliability telemetry. See [Telemetry](#telemet
for what is collected and how to turn it off (uncomment `OPENWIKI_TELEMETRY_DISABLED`
in the example workflow).

## Open Knowledge Format compatibility

OpenWiki emits [Google Open Knowledge Format (OKF) v0.1](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) bundles in both code and personal modes.

- Every non-reserved Markdown concept has YAML front matter with a non-empty
`type`; all other standard fields are optional.
- Valid `timestamp` values and producer-defined extension fields are accepted
and preserved during updates and migrations.
- `index.md` and `log.md` are reserved documents rather than concepts. Nested
indexes contain no front matter, while the root index declares
`okf_version: "0.1"`.
- Standard Markdown links between concept documents express relationships.

## Usage

Start the interactive CLI in code mode for the current repository:
Expand Down
4 changes: 1 addition & 3 deletions openwiki/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
---
type: Documentation Index
title: "OpenWiki"
description: "Files and subdirectories in OpenWiki."
okf_version: "0.1"
---

# Files
Expand Down
12 changes: 7 additions & 5 deletions skills/migrate-wiki-to-okf/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ Add or correct OKF front matter across the existing wiki without changing accura

Each subagent must:

- Inspect every non-generated Markdown file directly in its assigned directory.
- Inspect every non-reserved Markdown concept file directly in its assigned directory.
- Leave already compliant files unchanged.
- Add or correct only the leading YAML front matter when needed. Preserve the existing Markdown body.
- Use a descriptive, self-explanatory `type`. Infer `title` and a one to two sentence `description` (this should be optimized for search & retrieval) from the document when useful. Add `resource` or `tags` only when supported by the document.
- Never add `timestamp` or fields outside this formatter:
- Preserve all valid existing front matter fields, including `timestamp` and producer-defined extension fields. Never delete an unknown field merely because OpenWiki did not create it.
- Require only a non-empty, descriptive `type`. Infer recommended `title` and one to two sentence `description` values when useful. Add `resource`, `tags`, or `timestamp` only when supported by the document and available evidence.
- Use this standard-field formatter while retaining any existing producer extensions:

```yaml
---
Expand All @@ -32,11 +33,12 @@ title: <Optional display name>
description: <Optional one to two sentence summary (optimized for search & retrieval)>
resource: <Optional canonical URI for the underlying asset>
tags: [<tag>, <tag>]
timestamp: <Optional ISO 8601 datetime>
---
```

- Do not edit `index.md`; OpenWiki regenerates directory indexes deterministically after the run.
- `index.md` and `log.md` are reserved OKF documents. Do not add concept front matter to them or process them as concepts; OpenWiki regenerates directory indexes deterministically after the run.
- Report the files checked, the files changed, and any file whose metadata could not be inferred confidently.
- The description field here is very important as retrieval tools will rely on it when searching through documents. Ensure your descriptions are clear, detailed, and optimized for search.
- The description field is important for retrieval tools. When present, make it clear, detailed, and optimized for search.

Do not create, delete, move, or reorganize wiki pages during this migration.
19 changes: 12 additions & 7 deletions src/agent/frontmatter-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ import { parse } from "yaml";
import { MUTATION_PATH_METADATA_KEY } from "./docs-only-backend.js";
import type { OpenWikiOutputMode } from "./types.js";

const OKF_STRING_FIELDS = ["type", "title", "description", "resource"];
const OKF_FIELDS = new Set([...OKF_STRING_FIELDS, "tags"]);
const OKF_STRING_FIELDS = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we losing tags here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, tags is still validated — it just isn't part of the OKF_STRING_FIELDS list because it's a list, not a string. It has its own dedicated check further down (unchanged by this PR):

if (
  Object.hasOwn(fields, "tags") &&
  (!Array.isArray(fields.tags) ||
    fields.tags.some((tag) => typeof tag !== "string" || !tag.trim()))
) {
  issues.push(issue("invalid_tags", "Field `tags` must be a YAML list of non-empty strings."));
}

What went away is the old OKF_FIELDS allowlist that produced the unsupported_field error. That removal is intentional: OKF v0.1 permits producer-defined extension fields, so we can no longer reject unknown keys.

One conscious trade-off worth flagging: without the allowlist, a typo in a standard field (e.g. descriptoin:) now passes silently as an "extension" instead of erroring. Erroring would violate the spec, so I left it as-is — happy to open a follow-up issue for a heuristic "did you mean" warning if you think it's worth it.

"type",
"title",
"description",
"resource",
"timestamp",
];
const OKF_RESERVED_FILES = new Set(["index.md", "log.md"]);
const WRITE_TOOLS = new Set(["write_file", "edit_file"]);

interface FrontmatterIssue {
Expand All @@ -24,7 +30,7 @@ export type FrontmatterValidation =
valid: false;
};

/** Parses and validates leading YAML front matter against the supported OKF fields. */
/** Parses and validates OKF front matter while tolerating producer extensions. */
export function validateOkfFrontmatter(content: string): FrontmatterValidation {
const lines = content.split(/\r?\n/u);
if (lines[0] !== "---") {
Expand Down Expand Up @@ -57,9 +63,7 @@ export function validateOkfFrontmatter(content: string): FrontmatterValidation {
return invalid("invalid_yaml_root", "Front matter must be a YAML mapping.");
}

const issues = Object.keys(fields)
.filter((key) => !OKF_FIELDS.has(key))
.map((key) => issue("unsupported_field", `Unsupported field \`${key}\`.`));
const issues: FrontmatterIssue[] = [];

if (!Object.hasOwn(fields, "type")) {
issues.push(issue("missing_type", "Required field `type` is missing."));
Expand Down Expand Up @@ -156,7 +160,7 @@ function getToolMessages(result: unknown): ToolMessage[] {
: [];
}

/** Checks whether a path targets a Markdown file inside the configured wiki. */
/** Checks whether a path targets an OKF concept document inside the wiki. */
function isWikiMarkdownPath(
filePath: string,
outputMode: OpenWikiOutputMode,
Expand All @@ -166,6 +170,7 @@ function isWikiMarkdownPath(
);
return (
path.posix.extname(normalized).toLowerCase() === ".md" &&
!OKF_RESERVED_FILES.has(path.posix.basename(normalized).toLowerCase()) &&
(outputMode === "local-wiki" || normalized.startsWith("/openwiki/"))
);
}
Expand Down
28 changes: 11 additions & 17 deletions src/agent/index-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import { addFrontmatterWarning } from "./frontmatter-validator.js";
import type { OpenWikiOutputMode } from "./types.js";

const INDEX_FILE = "index.md";
const EXCLUDED_FILES = new Set([INDEX_FILE, "_plan.md", "INSTRUCTIONS.md"]);
const LOG_FILE = "log.md";
const EXCLUDED_FILES = new Set([
INDEX_FILE,
LOG_FILE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice - good catch.

"_plan.md",
"INSTRUCTIONS.md",
]);

interface Directory {
entries: FileInfo[];
Expand Down Expand Up @@ -113,11 +119,7 @@ async function synchronizeDirectory(
}

const indexPath = path.posix.join(directory.path, INDEX_FILE);
const title =
directory.path === root
? "OpenWiki"
: titleFromSlug(path.posix.basename(directory.path));
const content = renderIndex(title, files, directories);
const content = renderIndex(files, directories, directory.path === root);
const existing = directory.entries.some(
(entry) => !entry.is_dir && entryName(entry) === INDEX_FILE,
)
Expand All @@ -135,17 +137,18 @@ async function synchronizeDirectory(

/** Renders a complete deterministic index document. */
function renderIndex(
title: string,
files: Link[],
directories: Link[],
isRoot: boolean,
): string {
const sections = [
renderLinks("Files", files, true),
renderLinks("Directories", directories, false),
]
.filter(Boolean)
.join("\n\n");
return `---\ntype: Documentation Index\ntitle: ${JSON.stringify(title)}\ndescription: ${JSON.stringify(`Files and subdirectories in ${title}.`)}\n---\n\n${sections}\n`;
const version = isRoot ? '---\nokf_version: "0.1"\n---\n\n' : "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also a great catch.

return `${version}${sections || "# Files"}\n`;
}

/** Renders a sorted Markdown section for files or subdirectories. */
Expand Down Expand Up @@ -236,15 +239,6 @@ function entryName(entry: FileInfo): string {
return path.posix.basename(entry.path.replace(/\/$/u, ""));
}

/** Converts a directory slug into a human-readable title. */
function titleFromSlug(slug: string): string {
return slug
.split(/[-_\s]+/u)
.filter(Boolean)
.map((word) => word[0].toUpperCase() + word.slice(1))
.join(" ");
}

/** Escapes a value for use as a Markdown link label. */
function escapeLabel(value: string): string {
return value
Expand Down
22 changes: 13 additions & 9 deletions src/agent/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,10 @@ OKF relationship modeling:
- Prefer links to existing canonical concepts over duplicating their explanations. Do not mint thin concepts merely to create more nodes or edges.

Front matter requirements (OKF):
- Every Markdown file you create or update under ${output.docsLocation}, including the temporary ${output.planPath} file, MUST begin with OKF-compliant YAML front matter.
- The front matter MUST follow the Google Knowledge Catalog OKF schema
- Use this exact formatter at the very beginning of each file, replacing placeholders with real values and omitting optional fields that do not apply:
- Every non-reserved Markdown concept file you create or update under ${output.docsLocation}, including the temporary ${output.planPath} file, MUST begin with OKF-compliant YAML front matter.
- The front matter MUST follow the Google Knowledge Catalog OKF v0.1 schema.
- \`index.md\` and \`log.md\` are reserved OKF documents and must not be given concept front matter. Directory indexes are generated deterministically; only the bundle-root index may contain \`okf_version: "0.1"\` front matter.
- Use this formatter at the very beginning of concept files, replacing placeholders with real values and omitting optional fields that do not apply:

<okf_front_matter>
---
Expand All @@ -162,15 +163,18 @@ title: <Optional display name>
description: <Optional one to two sentence summary (optimized for search & retrieval)>
resource: <Optional canonical URI for the underlying asset>
tags: [<tag>, <tag>, …] # Optional
timestamp: <Optional ISO 8601 datetime>
# Producer-defined extension fields are allowed.
---
</okf_front_matter>

- \`type\` is required. Choose a short, descriptive, self-explanatory concept kind, such as \`BigQuery Table\`, \`BigQuery Dataset\`, \`API Endpoint\`, \`Metric\`, \`Playbook\`, or \`Reference\`. Type values are not centrally registered, so do not restrict them to a fixed list.
- Required fields are: \`title\`, a human-readable display name; \`description\`, a one to two sentence summary (this should be optimized for search & retrieval); and \`tags\`, a YAML list of short cross-cutting category strings.
- Recommended field(s), in priority order, are: \`resource\`, the canonical URI of the underlying asset when one exists (e.g. file path to specific code file in a repo).
- Produce valid YAML. Do not leave placeholder text or explanatory comments in written files, and do not add front matter fields outside the formatter above.
- The description field here is very important as retrieval tools will rely on it when searching through documents. Ensure your descriptions are clear, detailed, and optimized for search.
- When updating an existing Markdown file, preserve accurate content but add or correct its opening front matter as part of that update so the resulting file complies with this requirement. - Only update front matter when necessary. You do not need to update every time, only when key file components change.
- Only \`type\` is required. Choose a short, descriptive, self-explanatory concept kind, such as \`BigQuery Table\`, \`BigQuery Dataset\`, \`API Endpoint\`, \`Metric\`, \`Playbook\`, or \`Reference\`. Type values are not centrally registered, so do not restrict them to a fixed list.
- Recommended fields, in priority order, are: \`title\`, a human-readable display name; \`description\`, a one to two sentence summary optimized for search and retrieval; \`resource\`, the canonical URI of the underlying asset when one exists; and \`tags\`, a YAML list of short cross-cutting category strings.
- \`timestamp\` is an optional ISO 8601 datetime for the last meaningful change.
- Produce valid YAML. Do not leave placeholder text or explanatory comments in written files.
- Preserve all existing producer-defined front matter fields when updating a concept. Unknown extension fields are valid OKF and must survive round trips. Change metadata only when the underlying fact or meaningful content changes.
- The description field is especially useful for retrieval tools. When present, make it clear, detailed, and optimized for search.
- When updating an existing Markdown concept, preserve accurate body content and correct its opening front matter only when needed for compliance or accuracy.

Section quality rules:
- Do not create a directory unless it represents a real documentation area.
Expand Down
35 changes: 32 additions & 3 deletions test/frontmatter-validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ describe("validateOkfFrontmatter", () => {
).toEqual({ valid: true });
});

test("accepts OKF timestamp and producer-defined extension fields", () => {
expect(
validateOkfFrontmatter(
markdown(
[
"type: Reference",
'timestamp: "2026-07-16T20:00:00Z"',
"author: steve",
"confidence: 0.95",
"status: verified",
].join("\n"),
),
),
).toEqual({ valid: true });
});

test("reports deterministic delimiter and required-field issues", () => {
expect(validateOkfFrontmatter("# Page")).toEqual({
issues: [
Expand Down Expand Up @@ -94,24 +110,25 @@ describe("validateOkfFrontmatter", () => {
expect(malformed.issues[0].message).toContain("line 3");
});

test("reports unsupported and mistyped fields", () => {
test("reports mistyped standard fields", () => {
const result = validateOkfFrontmatter(
markdown(
[
"type: Reference",
"timestamp: 2026-07-13",
"timestamp: [Not a string]",
"title: [Not a string]",
"description: 123",
"tags: docs, api",
"producer_extension: preserved",
].join("\n"),
),
);

expect(result).toMatchObject({
issues: [
{ code: "unsupported_field" },
{ code: "invalid_title" },
{ code: "invalid_description" },
{ code: "invalid_timestamp" },
{ code: "invalid_tags" },
],
valid: false,
Expand Down Expand Up @@ -166,6 +183,18 @@ describe("addFrontmatterWarning", () => {
expect(outsideBackend.readRaw).not.toHaveBeenCalled();
});

test("does not validate reserved index and log documents as concepts", async () => {
for (const fileName of ["index.md", "log.md"]) {
const backend = backendWith("# Reserved OKF document");
const message = mutationMessage(`/openwiki/architecture/${fileName}`);

await addFrontmatterWarning(message, backend, "repository", "write_file");

expect(backend.readRaw).not.toHaveBeenCalled();
expect(message.content).toBe("Successfully wrote file.");
}
});

test("edits tool messages nested in Command results", async () => {
const message = mutationMessage();
const command = { update: { messages: [message] } };
Expand Down
52 changes: 50 additions & 2 deletions test/index-middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ describe("synchronizeWikiIndexes", () => {
"utf8",
);

expect(rootIndex).toContain("type: Documentation Index");
expect(rootIndex).toContain('okf_version: "0.1"');
expect(rootIndex).not.toContain("type: Documentation Index");
expect(rootIndex).not.toMatch(/^tags:/mu);
expect(rootIndex).toContain("- [Quickstart](quickstart.md) - Start here.");
expect(rootIndex).toContain(
Expand All @@ -55,6 +56,33 @@ describe("synchronizeWikiIndexes", () => {
);
});

test("uses OKF version frontmatter only at the bundle root", async () => {
const { backend, rootDir } = await setup();
await backend.write(
"/openwiki/quickstart.md",
document("Quickstart", "Start here."),
);
await backend.write(
"/openwiki/architecture/overview.md",
document("Architecture", "System structure."),
);

await synchronizeWikiIndexes(backend, "repository");

const rootIndex = await readFile(
path.join(rootDir, "openwiki/index.md"),
"utf8",
);
const nestedIndex = await readFile(
path.join(rootDir, "openwiki/architecture/index.md"),
"utf8",
);
expect(rootIndex).toMatch(/^---\nokf_version: "0\.1"\n---\n\n# Files/mu);
expect(rootIndex).not.toContain("type: Documentation Index");
expect(nestedIndex).toMatch(/^# Files/mu);
expect(nestedIndex).not.toMatch(/^---/u);
});

test("does not rewrite an index that is already current", async () => {
const { backend } = await setup();
await backend.write(
Expand Down Expand Up @@ -95,6 +123,26 @@ describe("synchronizeWikiIndexes", () => {
expect(repaired).not.toContain("_plan.md");
});

test("does not index the reserved OKF log document", async () => {
const { backend, rootDir } = await setup();
await backend.write(
"/openwiki/page.md",
document("Page", "Current description."),
);
await backend.write(
"/openwiki/log.md",
"# Directory Update Log\n\n## 2026-07-16\n- **Update**: Changed page.\n",
);

await synchronizeWikiIndexes(backend, "repository");

const index = await readFile(
path.join(rootDir, "openwiki/index.md"),
"utf8",
);
expect(index).not.toContain("log.md");
});

test("indexes a valid OKF file without an optional description", async () => {
const { backend, rootDir } = await setup();
await backend.write(
Expand Down Expand Up @@ -190,6 +238,6 @@ describe("synchronizeWikiIndexes", () => {
).resolves.toContain("- [empty](empty/)");
await expect(
readFile(path.join(rootDir, "empty/index.md"), "utf8"),
).resolves.toContain('title: "Empty"');
).resolves.toBe("# Files\n");
});
});
21 changes: 21 additions & 0 deletions test/prompt-okf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, test } from "vitest";
import { createSystemPrompt } from "../src/agent/prompt.ts";

describe("createSystemPrompt OKF guidance", () => {
test("describes Google OKF v0.1 frontmatter and preservation rules", () => {
const prompt = createSystemPrompt("init", "repository");

expect(prompt).toContain("Only `type` is required");
expect(prompt).toContain("`timestamp` is an optional ISO 8601 datetime");
expect(prompt).toContain(
"Preserve all existing producer-defined front matter fields",
);
expect(prompt).toContain(
"`index.md` and `log.md` are reserved OKF documents",
);
expect(prompt).not.toContain("Required fields are: `title`");
expect(prompt).not.toContain(
"do not add front matter fields outside the formatter above",
);
});
});
Loading