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
6 changes: 3 additions & 3 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ on:

jobs:
claude-review:
if: github.event.pull_request.author_association == 'OWNER'
if: github.event.pull_request.author_association == 'OWNER' && !github.event.pull_request.draft
runs-on: ubuntu-latest
permissions:
contents: read
contents: write
pull-requests: write
issues: read
id-token: write
Expand Down Expand Up @@ -57,7 +57,7 @@ jobs:
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review --comment'
track_progress: true
claude_args: '--allowedTools "Read,Write,Edit,Bash(git:*),mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr checkout:*),Bash(git log:*),Bash(bun run test:*),Bash(bun run lint:*),Bash(bun run build:*),Bash(bun test:*),Bash(npx tsc:*),Bash(bun run tsc:*),Bash(gh pr checks:*),Bash(npx biome check:*),Bash(git fetch:*),Bash(gh issue list:*),Bash(gh issue view:*)"'
claude_args: '--allowedTools "Read,Write,Edit,Bash(git:*),mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr checkout:*),Bash(git log:*),Bash(bun run test:*),Bash(bun run lint:*),Bash(bun run build:*),Bash(bun test:*),Bash(npx tsc:*),Bash(bun run tsc:*),Bash(bun run typecheck*),Bash(gh pr checks:*),Bash(npx biome check:*),Bash(git fetch:*),Bash(gh issue list:*),Bash(gh issue view:*)"'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

2 changes: 1 addition & 1 deletion .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,5 @@ jobs:
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
claude_args: '--allowedTools "Read,Write,Edit,Bash(git:*),mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr checkout:*),Bash(git log:*),Bash(bun run test:*),Bash(bun run lint:*),Bash(bun run build:*),Bash(bun test:*),Bash(npx tsc:*),Bash(bun run tsc:*),Bash(gh pr checks:*),Bash(npx biome check:*),Bash(git fetch:*),Bash(gh issue list:*),Bash(gh issue view:*)"'
claude_args: '--allowedTools "Read,Write,Edit,Bash(git:*),mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr checkout:*),Bash(git log:*),Bash(bun run test:*),Bash(bun run lint:*),Bash(bun run build:*),Bash(bun test:*),Bash(npx tsc:*),Bash(bun run tsc:*),Bash(bun run typecheck*),Bash(gh pr checks:*),Bash(npx biome check:*),Bash(git fetch:*),Bash(gh issue list:*),Bash(gh issue view:*)"'

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
config.json
.claude/settings.local.json
.claude/worktrees/
.claude-pr/
CLAUDE.local.md
lefthook-local.yml
.gitleaks-priv.toml

hook-test/fixtures/.state/
49 changes: 1 addition & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,54 +46,7 @@ See `config.example.json` for the full structure. The config maps space names to

**Including spaces from other configs:** Use `includeSpacesFrom` to import space definitions from other config files. This is useful for aggregating spaces from multiple projects into a central config, reducing the need to specify `--config` on CLI commands. Duplicate space names are not allowed.

**Plugins:** Use `plugins` to load parse plugins that read spaces from non-markdown sources. The built-in markdown plugin is always available without any declaration. Plugins are tried in order; the first to return a result wins. The `plugins` field is a map of plugin name to plugin config, and can be declared at the top level (applies to all spaces) or per-space (overrides the top level):

```json
{
"spaces": [
{
"name": "ProductX",
"path": "/path/to/space",
"plugins": {
"markdown": { "fieldMap": { "record_type": "type" } }
}
}
],
"plugins": {
"ost-tools-confluence": { "baseUrl": "https://example.atlassian.net" }
}
}
```

All plugin names must start with `ost-tools-` (the prefix is optional in config and normalised on load). The special name `markdown` refers to the built-in markdown plugin. External plugins are resolved in order: config-adjacent (`{configDir}/plugins/{name}`), then npm. Each plugin must export a `configSchema` JSON Schema; config is validated against it on load. Fields annotated `format: 'path'` in a plugin's `configSchema` are resolved relative to the config file directory.

**Markdown plugin config** fields (set under `plugins.markdown` per space):
- `templateDir` — directory containing template files (used by `template-sync`)
- `templatePrefix` — filename prefix for templates (default blank)
- `fieldMap` — maps file/frontmatter field names to canonical schema field names (e.g. `{ "record_type": "type" }`)

**Filter views:** Named filter expressions can be defined per space under `views`. Each view has an `expression` field using the filter expression syntax:

```json
{
"spaces": [
{
"name": "my-space",
"path": "/path/to/space",
"views": {
"active-solutions": {
"expression": "WHERE resolvedType='solution' and status='active'"
},
"solutions-under-active-opportunity": {
"expression": "WHERE resolvedType='solution' and $exists(ancestors[resolvedType='opportunity' and status='active'])"
}
}
}
]
}
```

Use a view name with `ost-tools show <space> --filter <view-name>`.
**Plugins and markdown plugin config:** See `ost-tools docs config` for the full reference including `fieldMap`, `typeInference`, `templateDir`, filter views, and plugin loading rules.

### Spaces

Expand Down
12 changes: 11 additions & 1 deletion docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,19 @@ Parsing behaviour for a space directory:
- Files declaring a `space node` type via frontmatter are included as nodes.
- Such files may also contain `embedded nodes` in their body, which are extracted and included.
- Files declaring a `tooling type` (e.g. `space_on_a_page`, `dashboard`) are excluded from the node set.
- Files without frontmatter, or without a `type` field, are excluded from the node set.
- Files without frontmatter, or without a `type` field, are excluded from the node set (unless **type inference** is configured — see below).
- Non-markdown files are not scanned.

#### Type inference

When `typeInference` is configured on the markdown plugin, files without an explicit `type` field in frontmatter can have their type inferred from their folder path. Explicit `type:` in frontmatter always takes precedence.

Two modes are available:

- **`folder-name`** (default) — the leaf directory name is matched case-insensitively against the schema's known type names and alias keys. For example, a file at `concept/page.md` is inferred as type `concept`; a file at `study/page.md` is inferred as `source` if `study` is an alias for `source` in the schema. A folder name that is neither a type name nor an alias key results in no inference.

- **`folderMap`** — an explicit map from folder path (relative to space root) to a type name or alias. Replaces auto-matching entirely; only folders listed in the map are inferred. Longest-prefix matching is used when folder paths overlap. An unresolvable value (not a known type or alias) is a hard error at parse time.

### Space on a page

**Space on a page** is a single-file backing format for a `space`. An entire planning tree is represented in one markdown document, using heading hierarchy, bullet point annotations, and `anchor` syntax. No separate per-node files are used. This format is most useful for the early development stages of a space, keeping information together in one file with less "boilerplate".
Expand Down
140 changes: 140 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# ost-tools Configuration Reference

## Config file location

ost-tools looks for its config file in this order:

1. `$OST_TOOLS_CONFIG` — explicit path override
2. `~/.config/ost-tools/config.json` (or `$XDG_CONFIG_HOME/ost-tools/config.json`)
3. `./config.json` in the current working directory

See `config.example.json` for the full structure. Paths in config files are resolved relative to the config file.

## Spaces

A space is a named directory or single file registered in the config. Example:

```json
{
"spaces": [
{
"name": "ProductX",
"path": "/path/to/space",
"schema": "general.json"
}
]
}
```

**`includeSpacesFrom`** — import space definitions from other config files. Useful for aggregating spaces from multiple projects into a central config. Duplicate space names are not allowed.

## Plugins

Use `plugins` to load parse plugins that read spaces from non-markdown sources. The built-in markdown plugin is always available without any declaration. Plugins are tried in order; the first to return a result wins. The `plugins` field is a map of plugin name to plugin config, and can be declared at the top level (applies to all spaces) or per-space (overrides the top level):

```json
{
"spaces": [
{
"name": "ProductX",
"path": "/path/to/space",
"plugins": {
"markdown": { "fieldMap": { "record_type": "type" } }
}
}
],
"plugins": {
"ost-tools-confluence": { "baseUrl": "https://example.atlassian.net" }
}
}
```

All plugin names must start with `ost-tools-` (the prefix is optional in config and normalised on load). The special name `markdown` refers to the built-in markdown plugin. External plugins are resolved in order: config-adjacent (`{configDir}/plugins/{name}`), then npm. Each plugin must export a `configSchema` JSON Schema; config is validated against it on load. Fields annotated `format: 'path'` in a plugin's `configSchema` are resolved relative to the config file directory.

## Markdown plugin config

Set under `plugins.markdown` per space.

### `fieldMap`

Maps file/frontmatter field names to canonical schema field names:

```json
{ "fieldMap": { "record_type": "type" } }
```

### `templateDir` and `templatePrefix`

- `templateDir` — directory containing template files (used by `template-sync` and excluded when parsing)
- `templatePrefix` — filename prefix for templates (default blank)

### `typeInference`

Automatically assigns a node type based on folder structure when no `type` field is present in frontmatter. Explicit `type:` always takes precedence.

**`mode`** — controls the matching strategy:
- `"folder-name"` (default) — matches the leaf directory name case-insensitively against schema type names and alias keys
- `"off"` — disables inference entirely

```json
{
"plugins": {
"markdown": {
"typeInference": { "mode": "folder-name" }
}
}
}
```

**`folderMap`** — explicit map from folder path (relative to space root) to type name or alias. When set, replaces auto-matching entirely; only folders listed in the map are inferred.

```json
{
"plugins": {
"markdown": {
"typeInference": {
"folderMap": {
"Research": "source",
"Personal": "note",
"topics/concepts": "concept"
}
}
}
}
}
```

Longest-prefix matching is used when keys overlap (e.g. `a/b` and `a/b/c` both present). Trailing slashes in keys are normalised. Values may be type aliases (resolved to canonical type). An unresolvable value throws a hard error at parse time.

## Filter views

Named filter expressions can be defined per space under `views`. Each view has an `expression` field:

```json
{
"spaces": [
{
"name": "my-space",
"path": "/path/to/space",
"views": {
"active-solutions": {
"expression": "WHERE resolvedType='solution' and status='active'"
},
"solutions-under-active-opportunity": {
"expression": "WHERE resolvedType='solution' and $exists(ancestors[resolvedType='opportunity' and status='active'])"
}
}
}
]
}
```

Use a view name with `ost-tools show <space> --filter <view-name>`.

See `ost-tools docs concepts` for full filter expression syntax.

## Security notice

**⚠️ Only use schemas and configuration files from trusted sources.**

The tool executes JSONata expressions defined in schema files for rule validation. A maliciously crafted schema could make JSONata access JavaScript's prototype chain and execute arbitrary code. Only use schemas you've created or reviewed personally.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
},
"files": [
"dist/",
"schemas/"
"schemas/",
"docs/concepts.md",
"docs/config.md",
"docs/schemas.md",
"docs/rules.md"
],
"exports": {
".": "./dist/index.js",
Expand Down
34 changes: 10 additions & 24 deletions plugin/skills/ost-tools/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ Before working with a space, use these to understand what's configured:
bunx ost-tools spaces --config <cfg> # per-space: path, schema, fieldMap, templates, miro
bunx ost-tools schemas show --space <name> --config <cfg> # entity types, properties, rules, enums + registry
bunx ost-tools schemas show <filename> # inspect a bundled partial (e.g. _ost_tools_base.json)
bunx ost-tools readme # full documentation if needed
bunx ost-tools docs # full README
bunx ost-tools docs config # plugin config reference (fieldMap, typeInference, etc.)
bunx ost-tools docs concepts # terminology reference
```

`spaces` is the starting point — it shows each space as a block with its schema name, `fieldMap`
Expand Down Expand Up @@ -108,31 +110,15 @@ what the rule actually sees in the `current` object, then adjust the rule in the

ost-tools supports **plugins** for extending capabilities. Currently, parse plugins allow reading spaces from sources other than markdown (which is a built-in plugin).

Declare plugins in config as a of plugin name → config object:

```json
{
"spaces": [
{
"name": "PDFSpace",
"path": "https://...",
"plugins": {
"ost-tools-pdf": { "baseUrl": "https://example.pdfstore.net" }
}
}
]
}
```

All plugin names must start with `ost-tools-` (the prefix is optional in config and normalised on load). External plugins are resolved in order: config-adjacent (`{configDir}/plugins/{name}`), then npm.
For full plugin and markdown plugin config reference (fieldMap, typeInference, templateDir, filter views), run:

**Markdown plugin config** (under `plugins.markdown` in a space entry):
- `templateDir` — directory for template files used by `template-sync`, and to exclude templates when parsing and validating
- `templatePrefix` — filename prefix for templates (default blank)
- `fieldMap` — maps file field names to canonical schema field names (e.g. `{ "record_type": "type" }`)
```bash
bunx ost-tools docs config
```

## References

- **`references/schema-authoring.md`** — schema file structure, `$metadata`, `fieldMap`, JSONata rules
- **`references/schema-authoring.md`** — schema file structure, `$metadata`, JSONata rules (run `ost-tools docs schema` for schema dialect reference)
- **`references/schema-design.md`** — process for designing a schema from existing content
- **`references/commands.md`** — detailed CLI usage and examples

For CLI and config reference, use `ost-tools docs <topic>` (topics: `concepts`, `config`, `schema`, `rules`).
24 changes: 22 additions & 2 deletions src/commands/readme.ts → src/commands/docs.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

export function readme(): void {
const content = readFileSync(join(import.meta.dir, '..', '..', 'README.md'), 'utf-8');
const TOPICS: Record<string, string> = {
concepts: 'concepts.md',
config: 'config.md',
schema: 'schemas.md',
rules: 'rules.md',
};

export function docs(topic?: string): void {
let filePath: string;
if (!topic) {
filePath = join(import.meta.dir, '..', '..', 'README.md');
} else {
const file = TOPICS[topic];
if (!file) {
const available = Object.keys(TOPICS).join(', ');
console.error(`Unknown topic "${topic}". Available: ${available}`);
process.exit(1);
}
filePath = join(import.meta.dir, '..', '..', 'docs', file);
}

const content = readFileSync(filePath, 'utf-8');
const cols = process.stdout.columns ?? 80;
const rendered = Bun.markdown.render(content, {
heading: (children, { level }) => {
Expand Down
Loading