Skip to main content

Generate, lint, and parse OmniPlan .oplx files. Companion to the oplx-format spec.

Project description

oplx-tools

Python tooling for OmniPlan .oplx documents. Generate, lint, and parse .oplx files without OmniPlan running or installed.

Naming note: oplx is the file extension OmniPlan uses (.oplx). This project is not related to Yamaha OPL audio synthesis chips (OPL2/OPL3/OPL4) which share a similar string in some retro-audio communities.

Ecosystem

This repo is one of three working together:

Repo What it is When to use
๐Ÿ“– oplx-format The file-format specification (CC-BY-4.0) Read this if you're writing any .oplx tool in any language
๐Ÿ oplx-tools (this repo) Python implementation: generate / lint / parse โ€” no OmniPlan needed Headless .oplx workflows: CI/CD, batch generation, ETL
๐Ÿค– omniplan-mcp MCP server for live OmniPlan automation (requires OmniPlan running) Conversational task management with Claude โ€” "schedule a task tomorrow at 2pm"
๐Ÿ“ฆ lash The installer used to wire oplx-tools's lint hook into Claude Code One-shot setup for the agent integration

Picking between live MCP and headless oplx-tools:

  • Use omniplan-mcp when you want conversational interaction with the running app (open documents, query/edit live tasks, schedule changes).
  • Use this repo when you want to read, create, or edit .oplx files without running OmniPlan at all โ€” agents on Linux, CI pipelines, batch generation from spreadsheets, headless tests, mass migration, etc. No GUI, no license, no Mac required.

Prior art

  • liyanage/omniplan-python โ€” older Python library focused on read-side data access. Different scope from oplx-tools (this repo emphasizes from-scratch file generation, lint, and silent-corruption detection). Use both if you need both reading and writing.

PRs that improve interop with existing libraries are welcome.

What's here

  • oplx generate โ€” build a minimum-viable .oplx from a YAML/JSON description (no OmniPlan required)
  • oplx lint โ€” validate a .oplx (zip or directory) against the format spec; catch silent-corruption patterns before they bite
  • oplx parse โ€” extract tasks/resources/dependencies/assignments from an .oplx for downstream tools (BI, ETL, integrations)

The spec lives in a separate repo (oplx-format) so it can be referenced by tool authors in any language. This repo is the Python reference implementation.

Use cases

These all work without OmniPlan running or installed:

1. Generate .oplx files from external sources

Drive OmniPlan's task tree from your existing data. Turn a CSV, JSON dataset, or YAML manifest into a valid .oplx your stakeholders can open in OmniPlan to review:

oplx generate roadmap.yaml --out roadmap.oplx

Useful for: project bootstrapping from templates, generating per-customer plans, converting Jira/Linear/GitHub issue exports into a Gantt chart your PMs can review and edit, weekly auto-regenerated baselines.

2. Headless lint in CI/CD

Run on Linux runners with no Mac, no OmniPlan license:

# .github/workflows/oplx-lint.yml
- run: uv tool install oplx-tools
- run: oplx lint plans/*.oplx   # CRITICAL findings exit non-zero, fail the build

Catches silent-corruption patterns the moment they're committed (uppercase <type>, lowercase kind=, units="0", orphaned tasks not reachable from <top-task>). See silent-corruption catalog.

3. Bulk-edit existing files without touching OmniPlan

Read in, mutate, write out:

from oplx import parse, generate
from oplx.models import TaskType

doc = parse("project.oplx")
for task in doc.actual.tasks:
    if "[milestone]" in task.title:
        task.type = TaskType.MILESTONE
        task.title = task.title.replace("[milestone]", "").strip()
generate(doc, "project-fixed.oplx")

Works on a stack of files in a loop. No GUI thrash, no per-file dialog acceptance.

4. Parse for downstream BI / ETL

OmniPlan's CSV export is lossy (no dependency kinds, no constraint dates, no custom data types). The XML form preserves everything:

from oplx import parse

doc = parse("project.oplx")
total_effort = sum(t.effort or 0 for t in doc.actual.tasks)
critical_path = [t for t in doc.actual.tasks if t.total_slack == 0]

Pipe into your warehouse, dashboard, or Slack digest.

5. Generate baselines programmatically

The OmniPlan UI's "Set Baseline" button has no API equivalent in OmniPlan's AppleScript dictionary or Omni Automation surface. oplx generate (with the Project.baselines= field) creates a multi-scenario doc directly โ€” useful for capturing weekly snapshots automatically without UI clicking.

6. Agent-driven workflows

With the Claude Code lint hook installed, agents can read/create/edit .oplx files freely; any silent-corruption pattern they introduce is immediately surfaced in their next turn so they self-correct. This composes naturally with omniplan-mcp when the live app is also available โ€” the MCP for runtime queries, this repo for file mutation.

Status

  • Verified against: OmniPlan 4.10.2
  • Spec version: 0.1.0
  • Maturity: alpha โ€” works for the cases tested in the research repo (cycles 0011โ€“0023). Not yet battle-tested in production.
  • Python: 3.11+

Install

Quick install (CLI only)

Homebrew (macOS):

brew tap johntrandall/tap
brew install oplx-tools

uv (Python tool, cross-platform):

uv tool install oplx-tools
# Or from a local clone:
uv tool install ~/dev/oplx-tools

The CLI command is oplx. The full install path below adds the Claude Code lint hook on top of the CLI.

Full install (CLI + Claude Code lint hook)

If you're using Claude Code (or any tool with the same PostToolUse hook surface) and you want agents to be auto-corrected the moment they introduce a silent-corruption pattern, install via lash โ€” a tiny manifest-driven installer.

lash install reads the lash.json manifest in this repo and:

  1. Installs the oplx CLI (uv tool install).
  2. Symlinks hooks/oplx-lint.sh โ†’ ~/.claude/hooks/oplx-lint.sh.
  3. Patches ~/.claude/settings.json with a PostToolUse Edit|Write|MultiEdit block that runs the hook.
# One-time prerequisite โ€” zero-dep installer (single Python script):
uv tool install lash-installer
# Or from a clone:  uv tool install git+https://github.com/johntrandall/lash

# Clone this repo and install:
git clone https://github.com/johntrandall/oplx-tools ~/dev/oplx-tools
cd ~/dev/oplx-tools
lash install                       # runs all 3 operations above

After install, restart Claude Code (or open /hooks once) so the settings watcher picks up the new entry.

lash install is idempotent โ€” re-run safely after git pull to refresh. See lash README for full manifest semantics.

lash status                        # see what's installed
lash uninstall                     # reverse all operations cleanly

What the hook does

The oplx-lint.sh hook fires after Edit/Write/MultiEdit when the file path resolves to inside an .oplx directory bundle. It runs oplx lint <bundle> on that bundle and emits any findings to stderr โ€” non-blocking (exits 0 even on findings; the agent sees them in the next turn and decides whether to self-correct).

Severity tiers (from silent-corruption.md):

  • CRITICAL โ€” file-level rejection (e.g., <type>MILESTONE</type> uppercase silently makes OmniPlan refuse to open the doc)
  • HIGH โ€” content silently dropped on save (e.g., orphaned tasks, units="0" deleting an assignment)
  • MEDIUM โ€” value normalized to default (e.g., <recalculate>none</recalculate> โ†’ duration)
  • LOW โ€” cosmetic / metadata

To disable the hook without uninstalling the CLI: lash uninstall && uv tool install oplx-tools.

Quick examples

Generate a minimal doc

cat > project.yaml <<'EOF'
title: My Project
start_date: 2026-06-01T13:00:00Z
tasks:
  - id: t1
    title: Plan
    effort: 14400         # 4 hours
  - id: t2
    title: Build
    effort: 28800
    depends_on: [t1]
  - id: t3
    title: Ship
    type: milestone
    depends_on: [t2]
EOF

oplx generate project.yaml --out project.oplx
open -a OmniPlan project.oplx

Lint an existing doc

oplx lint project.oplx
# Checks for: orphaned tasks, uppercase <type>, lowercase kind=, units="0",
# unreachable child-task chains, etc.

Parse for downstream use

oplx parse project.oplx --format json | jq '.tasks[].title'
from oplx import parse

doc = parse("project.oplx")
for task in doc.actual.tasks:
    print(task.title, task.effort, [d.idref for d in task.prerequisites])

Project layout

src/oplx/
โ”œโ”€โ”€ __init__.py
โ”œโ”€โ”€ cli.py              # Entry point: `oplx` command
โ”œโ”€โ”€ models.py           # Dataclasses for Project, Scenario, Task, Resource, Dependency
โ”œโ”€โ”€ generate.py         # YAML/dict โ†’ .oplx zip
โ”œโ”€โ”€ lint.py             # .oplx โ†’ list of warnings/errors
โ”œโ”€โ”€ parse.py            # .oplx โ†’ models
โ””โ”€โ”€ xml/                # Element-by-element serializers/deserializers

Coverage vs spec

oplx generate:

  • โœ… All 4 task types (with hammock via XML hand-write since omniJS rejects)
  • โœ… All 4 dependency kinds with optional lead-time (duration or percentage)
  • โœ… All 4 date constraints
  • โœ… Multi-resource assignment with units
  • โœ… Custom data (string values; other types not yet wired)
  • โœ… Per-resource schedule overrides (basic)
  • โœ… Multi-baseline scenarios
  • โœ… Zip variant output (default) or directory bundle
  • โš ๏ธ 3-pt estimation: emit <min/expected/max-estimate> and let OmniPlan PERT-compute <effort>
  • โŒ Note rich-text formatting (bold/color/alignment) โ€” plain text only
  • โŒ <filter> saved-filter generation (would need NSPredicate bplist construction)

oplx lint:

  • โœ… Silent-corruption catalog (all CRITICAL + HIGH tier patterns)
  • โœ… Element-ordering rules
  • โœ… Element-presence requirements (every task reachable from t-1)
  • โš ๏ธ XML schema validation (loose; relies on examples not a formal XSD/Relax-NG)

oplx parse:

  • โœ… Reads zip variant and directory bundle
  • โœ… Resolves dependencies, assignments, schedules
  • โœ… Handles multi-scenario docs (Actual + baselines)
  • โš ๏ธ Note rich-text: returns concatenated <lit> text; structure preserved as raw XML if requested
  • โŒ Filter bplist decode (use plistlib directly if needed)

License

MIT (see LICENSE). The format itself is described in the oplx-format repo under CC-BY-4.0.

Contributing

This is alpha software. Expected places needing work:

  • src/oplx/xml/ โ€” more thorough element handling (each element gets its own ser/de)
  • tests/ โ€” round-trip tests against oplx-format examples
  • oplx lint โ€” additional silent-corruption patterns as they emerge
  • Note rich-text formatting (bold/color/alignment) โ€” currently plain-only

PRs welcome. Please include a fixture (tests/fixtures/) demonstrating the case.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

oplx_tools-0.1.1.tar.gz (22.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

oplx_tools-0.1.1-py3-none-any.whl (22.2 kB view details)

Uploaded Python 3

File details

Details for the file oplx_tools-0.1.1.tar.gz.

File metadata

  • Download URL: oplx_tools-0.1.1.tar.gz
  • Upload date:
  • Size: 22.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.16

File hashes

Hashes for oplx_tools-0.1.1.tar.gz
Algorithm Hash digest
SHA256 653fff4395fde8ac3a740f0081ddbf5ff30c89d473df0f27dc558b13f0046d7c
MD5 0d571a7b8f4490e9b594b23535e95626
BLAKE2b-256 72500e78f3f129002032c6739d3d576ee4dddba537c86657b3900e97834cb85d

See more details on using hashes here.

File details

Details for the file oplx_tools-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: oplx_tools-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 22.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.16

File hashes

Hashes for oplx_tools-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e5f44f16ada8d9e9eedfe3f07c58e8f064ed763233d66ce69c4d6b6f0743a07d
MD5 a1923d1000ecdd94e3982418a61f15a8
BLAKE2b-256 fda2f65cdecd14afb56042b992565684b5ccdfd2d6857901a21e6f937d84c029

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page