Skip to main content

Developing Open Forge

Use this guide to build the CLI locally, contribute to the Framework, and check a change before review. For everyday use, start with the README or CLI guide.

Enter the workspace​

Read AGENTS.md and .agents/loader.md, then select the scopes relevant to your change. The Sources Of Truth map helps locate the file that defines each affected question.

LocationWhat belongs there
src/open-forge/Installable Framework files
src/extensions/First-party Extension packages
src/cli/Native CLI implementation and tests
src/docusaurus/The documentation site
demos/Demo projects for trying Open Forge with an agent
scripts/Repository build, delivery, package, and agent tooling
.agents/This repository's own rules, current knowledge, and work context
docs/ and README.mdPublic introductions and practical guides
artifacts/Generated build, publication, and verification output

Keep changes in their defining sources. Generated output and a machine's installed CLI do not establish what a worktree contains.

Build and test​

The delivery CLI guide is the quick reference for setup, stage selection, target-specific packaging and publication. The CLI is the shared entry point for local development and CI. It runs directly as TypeScript with the required Node version, so a fresh checkout can run setup before dependencies or a compiled build tool exist. Run npx forge from the repository root. The private root package registers the local command, so no global link or bootstrap compilation is needed. Arguments after forge go directly to the tool without an extra -- separator. Root npm scripts remain short aliases for the same CLI:

npx forge --help
npx forge setup
npx forge build
npx forge version patch
npx forge dist --help
npx forge dist --no-restore --plan

The command catalog is scripts/delivery/commands.ts. It defines each command's implementation, description and supported options. Focused task modules own execution, and dist-plan.ts declares the pipeline and routes flags to their stages. Use either the unified CLI or an alias such as npm run build. Small TypeScript scripts coordinate native tests and packaging. Standard npm and .NET commands own version increments, restore, compilation and uploads. Repository tooling uses Node and npm, including its tests.

Package scripts invoke workspace tools directly: tsc, eslint, and prettier. tsc uses TypeScript 7. The @typescript/native npm alias supplies that compiler. The typescript alias supplies Microsoft's TypeScript 6 API package for ESLint. This follows the supported side-by-side installation. npm run build:launcher compiles only the thin npm launcher. Delivery and local linking use that same command when they need emitted JavaScript.

Delivery scripts live together under scripts/delivery/. Each task has a direct entry point, such as build.ts, restore.ts or pack.ts. Shared capabilities stay beside their consumers, with package preparation under npm/ and release coordination under release/. Repository agent tools remain separate under scripts/agent-tooling/.

One root tsconfig.json checks the repository tooling TypeScript in scripts/ and eslint.config.ts, including tests. The documentation site and the brownfield demo app have their own configurations. The launcher's emitting configuration includes only its runtime sources. Tests remain independently runnable through their package commands. Compiler configuration does not determine which test tier executes.

The replacement C# implementation follows the current CLI Architecture and active Plan. Source and projects live below src/cli/: the executable project in root/, the layer projects in framework/, shell/, operations/, rendering/, and output-text/, and the test projects in tests/. The repository root owns OpenForge.Cli.slnx, global.json, NuGet.Config, Directory.Build.props, and Directory.Packages.props. All .NET output goes to the ignored root artifacts/ directory.

Install Node.js 22.18 or later, as package.json requires. CI uses Node 24, but contributors need nothing newer unless a change depends on a newer API. Also install the stable .NET 10 SDK selected by global.json. Native publishing also needs the platform's native toolchain: Clang and development libraries on Linux, Xcode command-line tools on macOS, or Visual Studio Build Tools with the C++ workload on Windows. See the .NET Native AOT prerequisites.

Set up dependencies once, then use the same commands from any supported host:

npm run setup
npm run build
npm test
npm run cli:dev -- --help

setup installs locked npm dependencies and restores .NET dependencies. npm run build and npm test restore .NET dependencies first, then run dotnet build without restoring, in Release configuration. Native builds restore once during their managed build step. The native publications that follow reuse the restored dependencies. Use -- --no-restore after an explicit restore, or -- --offline to restore only from cached dependencies. These options are mutually exclusive. The test command builds the managed solution and runs its unit, integration and public-command tests. IDEs can continue using the root .NET solution directly.

Use npm run check:delivery for delivery TypeScript, lint and formatting, npm run check:dotnet for C# formatting and warning-level diagnostics, and npm run test:delivery for the delivery helpers. npm run test:agent-tooling checks the separate repository agent tools.

Use npm run check:docs for the offline documentation gate. It reuses the pinned .NET Markdown parser to check local links and anchors in README.md, current Markdown under docs/ except docs/cli-experience-fixtures/ and docs/extension-candidates/, public Docusaurus docs under src/docusaurus/docs/ including demos, Extension READMEs under src/extensions/, .agents/maps/, the current CLI contracts, layers and technical designs, and the Workspace State Files decision. The gate does not fetch external HTTP links. The Docusaurus build checks site routes, assets and rendered anchors. Destinations in excluded source trees are still checked, unsupported local anchors fail instead of being silently accepted, and Docusaurus /docs/, /guides/ and /img/ routes delegate to the site build.

After a build, invoke that worktree's development artifact directly:

./artifacts/publish/open-forge-dev/Release/open-forge-dev --help
./artifacts/publish/open-forge-dev/Release/open-forge-dev context
./artifacts/publish/open-forge-dev/Release/open-forge-dev doctor

On Windows, use open-forge-dev.exe. Use --help on the artifact built in this worktree when checking documentation examples.

Building the CLI project directly, through the solution, or through the EndToEnd project publishes the local managed development executable as artifacts/publish/open-forge-dev/<Configuration>/open-forge-dev[.exe] and writes open-forge-dev.version. EndToEnd tests discover that artifact directly, so local terminal, Visual Studio, and VS Code test runs require no environment variables. Use -p:OpenForgeSkipDevelopmentPublish=true only for a build that deliberately does not need this artifact. A dotnet test --no-build run requires the artifact selected by an earlier build. CI and explicit Native AOT evidence compile the EndToEnd project for one supported target RID and use the corresponding artifacts/publish/<RID>/open-forge/OpenForge.Cli[.exe] publication.

When all dependencies are already cached, npm run setup -- --offline or npm run restore -- --offline prepares the workspace without contacting package feeds. Offline .NET restore does not perform a fresh vulnerability audit. A normal connected restore is required for that evidence.

Build a copyable native package​

Run the same preparation, checks and native journey used by CI on the host you want to support:

npm run setup
npm run verify
npm run dist -- --no-restore

verify runs formatting, lint, type checking and delivery/package-layout tests. dist prints its selected host, stages and effective commands, then builds the native CLI and its test executables, runs the managed/native suites, and packs the portable archive plus main and host npm packages. The installed-package journey checks the exact native payload through the launcher. No upload or global installation occurs. Use -- --sha for a commit-qualified development version, -- --offline for cached restore, or -- --no-restore after preparation.

CI and focused local work can run the individual steps:

npm run build:native -- --no-restore
npm run test:built
npm run pack

The native build defaults to the current OS and architecture. --rid can state that target explicitly, such as npm run build:native -- --rid linux-x64. Use a matching native host. Native AOT does not support building Windows executables on Linux or other cross-OS compilation. The CI matrix supplies all six matching hosts. See .NET cross-compilation.

build:native produces the native CLI and the managed/native test executables. test:built runs the existing artifacts without compiling them. Its six suites are six execution modes on one host: managed unit and integration tests, managed public tests against the managed CLI, native integration and public tests, and managed public tests against the native CLI. Integration tests therefore run in two modes and public tests in three. This does not run other OS targets.

By default, pack requires passing test:built reports for the existing artifacts. It creates the portable archive, the main npm package, and the current host's native npm package, then checks the installed npm launcher against the native binary. The commands print their output locations under artifacts/delivery/<RID>/. The current reports are in reports/, and the packages are in packages/. package-path.txt is written only after packaging succeeds.

To package locally when tests are failing or have not been run:

npm run pack -- --skip-tests # existing matching build
npm run dist -- --skip-tests # build, then package
npm run dist -- --skip-tests --plan # inspect without executing

--skip-tests bypasses .NET qualification and the npm installation/invocation test. It does not bypass source, artifact-hash or license checks. The native build still compiles the test executables. Packages record tested: false. Native publication and complete release collection reject them. To qualify later, run npm run test:built and npm run pack without the flag.

Stages print START/PASS/FAIL with their names. A failed .NET suite reports its name, log location and the end of its failure output. Later stages stop.

Copy the open-forge-<version>-<RID>.tar.gz archive and its checksums to a supported machine with the same OS and architecture. Extract it, then run ./open-forge --help or ./open-forge.exe --help on Windows. The native executable needs neither Node nor an installed .NET runtime. The npm launcher requires Node. Native operating-system requirements still apply. Matching the architecture alone does not make an older unsupported OS compatible.

Builds and tests also use the normal artifacts/bin, artifacts/obj, and artifacts/publish directories. Run these commands sequentially in one worktree. Generated archives and reports are build artifacts, not authored source.

Build and publish the wrapper separately​

The wrapper needs Node/npm dependencies but no .NET SDK or native artifacts:

npm ci --ignore-scripts
npm run dist:wrapper
npm run publish:wrapper -- --tag preview --dry-run

dist:wrapper compiles the launcher and packs it with the license and exact versioned optional dependencies for the selected platforms (all six by default). Its output is artifacts/delivery/wrapper/packages/. --sha uses the same optional commit-qualified version convention as native builds.

After dist succeeds, preview publication of only the host's native package:

npm run publish:native -- --tag preview --dry-run

Both publishers validate the existing tarball against its source, version, hash and license. The dry run is entirely local and does not invoke npm publish or inspect credentials. Removing --dry-run explicitly uploads that one public npm package using the configured registry and account. An actual upload requires committed matching source. A dirty trial can still preview its selection. Before any upload, the shared publisher queries npm for each selected exact version. Existing versions print a warning and are skipped. Missing versions are published. Authentication, network and unexpected lookup errors stop the run before uploads. Rerun the same command after an interrupted publication. The release checks every selected native package and the wrapper first and publishes remaining native packages before the wrapper. Existing versions keep their npm tags. Skipping checks availability, not whether remote bytes match a local rebuild. Dry runs remain offline and cannot report which versions already exist.

Both commands require a tag, and prerelease versions cannot use latest as that tag. Until a package has a stable version, publishing a prerelease also moves latest to it, because the npm package page shows the latest version. Changing source or version requires rebuilding the affected distribution.

Publish each native package at the synchronized version, then publish the main wrapper once. No publisher needs all native tarballs locally. The wrapper's optional dependencies do need to exist in the registry for their platforms to work. A release is complete for its recorded selection when every listed native package and its wrapper are published. The platform package alone contains no npm command mapping. The main package provides the open-forge command.

Select targets for one version​

Choose the wrapper's exact dependencies when packing it:

npx forge dist:wrapper --targets linux-x64,osx-x64,win-x64
npx forge publish:wrapper --tag preview --dry-run

This wrapper version lists exactly those three x64 packages. It has no ARM dependencies and requires no native builds locally. Publish its three native packages separately before publishing the wrapper. A target selection belongs to one version: adding ARM later requires a new wrapper version. If that version already exists with different dependencies, publication fails before any upload instead of skipping an incompatible wrapper.

For collection and publication of a selected release, pass the same list to pack (or dist) on each selected host, then to collection:

npx forge pack --targets linux-x64,osx-x64,win-x64
npx forge release:collect artifacts/release-input artifacts/release --targets linux-x64,osx-x64,win-x64
npx forge publish:release --tag preview --dry-run

The input contains each selected host's package-<RID> directory. Collection requires their tested packages and matching wrapper dependencies. It records the selection in release.json. Publication uses that exact graph and uploads the wrapper last. Omitted target artifacts are neither required nor uploaded. Changing selection requires repacking, but not rebuilding native executables. Without --targets, packaging and collection select all six. The existing Actions workflows continue to build and release all six by default.

Clean generated outputs​

npm run clean removes owned .NET outputs and current/legacy delivery output directories. It preserves source, node_modules, dependency caches, the offline feed, staged local npm links and unrelated artifact directories. Output paths with symlinked ancestors are rejected before deletion.

Ordinary builds preserve incremental compiler outputs. Native builds replace their selected target's publish and delivery directories, while tests and packs replace their current reports and packages. Old runs are no longer archived automatically. Failure diagnostics remain until the next run or explicit clean. Run build, test, pack, link and clean commands sequentially in one worktree. After clean, build/test/dist restore by default. Do not pass --no-restore until restoration has completed again. Directory.Build.props centralizes outputs but does not own cleanup of TypeScript-created packages and reports.

Change the product version​

The root package.json owns the product version. Use one command to calculate the next version and update the .NET version:

npm run version:bump -- patch
npm run version:bump -- minor
npm run version:bump -- major
npm run version:bump -- prerelease --preid beta
npm run version:bump -- 0.1.0-beta.1

These are alternative examples. The command uses npm's normal version handling, which updates package.json and its lockfile. Its version lifecycle hook projects that value to one property in Directory.Build.props for direct .NET/IDE builds. The informational version derives from it. Staging generates all seven npm manifests and their exact dependency versions from the selected version and platform definitions, so no per-platform manifests need editing. It creates no commit, Git tag, or release. Review and commit the resulting diff normally. If you edit the root version manually, run npm run version to refresh the .NET property before using direct dotnet or IDE builds.

Builds use the committed product version by default. For a development artifact identified by the current commit, use npm run build:native -- --sha. For example, 0.1.0-beta.1 becomes 0.1.0-beta.1.sha-<commit> in the artifacts. The tracked version stays unchanged. Packaging reads the built version. Changing the version afterward requires a new build and test run.

CI and releases​

Every workflow command has a local path through forge. ci:checks runs the Build workflow's shared setup and verify steps. ci:job --rid <RID> runs one matrix job with the same stage order and logs. docs:build installs, type-checks and builds the documentation site. Add --plan to preview any of these jobs. The delivery guide maps individual workflow steps to their local commands.

build.yml runs setup and verify once for shared checks. Its single native matrix covers Linux, macOS and Windows on x64 and ARM64. Each runner uses ci:prepare before SDK installation, then setup followed by explicit build:native -- --no-restore, ci:bundle, test:built and pack steps on the same machine. These are the stages shown by ci:job --rid <RID> --plan. CI passes one shared RID and never supplies --skip-tests. GitHub shows the failed stage directly and retains separate build/test/pack logs. The shared --log <file> option prints and records stdout and stderr while preserving failures. Windows uses runner-default PowerShell to preserve the filesystem tests' ACL denial evidence. Hosted tool setup, artifact transfer, secret-backed publication and Pages deployment remain in the workflows. The explicit matrix RID is an assertion that the host matches the target. The build uploads finished packages and diagnostics from stable output paths. There are no intermediate build/test transfers or platform packaging workflows.

Each test suite stops after 20 minutes, lists running processes and fails. With the slowest build and suites observed so far, a stall still leaves diagnostics before the 90-minute job limit. Suites print progress every two minutes and name tests running longer than two minutes. In GitHub Actions, failed and hung tests become annotations that ci:watch prints. npx forge ci:timings compares step times across recent Build runs and lists failed or unusually slow steps. The delivery guide describes these safeguards.

The release.yml workflow coordinates complete public releases. It can run manually with a source ref or commit, a destination (github, npm, or all), and optionally an existing successful build run for that exact commit. Without a supplied run, it reuses the most recent successful build.yml run for the exact commit with all six package artifacts present and unexpired. Only push and manual runs in this repository count, never pull request or fork runs. A supplied run passes the same checks. If none qualifies, it runs the reusable build workflow once. Direct pushes also skip duplicate Builds when another branch's run for the same commit has passed. When that run is still in progress, the push waits for it and builds itself if it fails. Pull requests, manual runs and Release calls always build. Retry a failed host with GitHub's Re-run failed jobs, then let Release reuse the completed run. The release job downloads the finished package-<RID> artifacts from the selected run and calls npm run release:collect -- artifacts/release-input artifacts/release. The default collection checks all six targets and replaces its generated release output. Older build runs containing only intermediate test artifacts cannot be reused. Release does not compile, repack or change the product version.

After collection, release:checkpoint prints the complete publication plan and waits 60 seconds before either destination. Cancelling during this wait uploads nothing. During npm publication, publish:release handles SIGINT and SIGTERM, starts no further upload or tag change, and lets the current effect settle. It reports published, interrupted and unstarted packages and exits non-zero. Earlier uploads remain published.

To restore public npm channels, preview npx forge release:restore-channels --version <bad> --to <previous>. It defaults to latest plus the bad version's prerelease channel. Use --channels <tags> to choose tags and --apply to apply the plan after checking that the previous version exists for all seven packages. It never unpublishes or deprecates versions. GitHub release changes are manual. The delivery guide describes cancellation and recovery at both destinations.

Pushing a version tag such as v0.1.0-beta.1 selects automatic release. The tag must match the source version. Automatic runs use the RELEASE_TARGET repository variable, defaulting to all. Merely bumping the local version does not release anything.

For the default complete release, all six platform packages must be prepared before publication begins. npm publishes the platform packages before the main package that references them. GitHub receives one release containing the portable archives and checksums. Prereleases use a prerelease channel, and also become latest until the first stable release. Stable releases use latest. Selecting all requests both destinations, but there is no transaction across GitHub and npm. A failed publication must be inspected before retrying its remaining work.

Put the workflow on the default branch to expose GitHub's Run workflow button. The first beta used REST dispatch of the registered workflow at its version tag while that button was unavailable. Its release record retains the setup and successful run. When retrying publication, keep the qualified source ref and build run and select only the unfinished destination.

Configure NPM_TOKEN as a repository Actions secret. Its npm granular token needs package publishing access to @thelithiumforge and Bypass 2FA enabled. Organization-management access alone does not grant package publishing access. See npm's token instructions. Local build/test/package commands do not publish, create GitHub releases, or require publication credentials. The explicit publish:native and publish:wrapper commands upload individual packages independently. publish:release -- --tag <channel> consumes the collected release and uses the same package validation and publication code, with all native packages before the wrapper. --dry-run previews the complete npm publication locally. It needs no .NET SDK, native test output or matching target host. All packages are validated before any upload begins.

The npm tooling under scripts/delivery/npm/ can prepare the native package for the current host on Linux (glibc), macOS, or Windows, on x64 or ARM64. The accepted distribution contains all six target packages. Qualify package layout, packing, native execution, and installed-launcher journeys on all six matching hosts before each release. See CLI Distribution.

The root link command publishes the current host in Release mode without restoring, stages the product version with the full Git commit SHA, and links the platform package through the main package globally. This is an explicit maintainer workflow that creates global links, not a verification prerequisite.

Restore the locked .NET workspace before the first link or after its dependencies change. Then link and invoke the current native CLI:

npm run restore
npm run cli:link
open-forge --version

The command uses ordinary npm package links and therefore creates npm's normal global package links and a link between the staged packages. It does not save a dependency, update the lockfile, run package scripts, contact the registry, or publish a package. Generated JavaScript, native publications, and staged package files remain below the ignored artifacts/ directory.

Ordinary projects use the machine-global open-forge command. Development, review, and acceptance in an Open Forge worktree must instead invoke an artifact built and published from that same worktree, such as artifacts/publish/open-forge-dev/<Configuration>/open-forge-dev[.exe] or the selected artifacts/publish/<RID>/open-forge/OpenForge.Cli[.exe]. Never use the machine-global CLI as worktree evidence. A package-manager-specific user-local PATH bridge may be needed when shell-facing shims differ from npm's active bin, but that bridge does not establish artifact identity.

Remove the known global npm links when they are no longer needed:

npm run cli:unlink

That command removes the known npm links. Remove an explicitly owned user-local PATH bridge only after resolving and revalidating its exact target.

The public command name is open-forge.

Framework and Extension changes​

Treat the shipped Markdown as the product a workspace will read. Keep it understandable without repository-only explanations or the CLI. When shared meaning changes, update the defining source and its current maintenance explanation, then align the repository's own copy while preserving intentional local specialization.

Check the affected questions, rules, frontmatter, links, generated Entries, and installed relationships. A wording edit must preserve conditions, authority, scope, and requirement strength. Extension checks should inspect the assembled result with its dependencies, as described in the Extension guide.

Use proportionate verification. With a current artifact available, index --dry-run can expose navigation drift and doctor can check structure without applying changes. These commands support review. They do not decide whether a sentence preserves its meaning.

Review the final diff and check whitespace:

git diff --check
git diff

Measure context size​

Context measurements count the Markdown in src/open-forge/, including hidden files, frontmatter, and generated Entries. The current base has 19 Markdown files, with 12 read at startup through AGENTS.md.

The README uses the CLI's estimate method: divide each file's character count by four, round up, and sum. The current base gives about 7.4k estimated tokens for the 12 startup files and about 18.6k for all 19. The open-forge-cli Skill entry file accounts for about 0.9k and is explicitly required by the Skills entrypoint at startup. Its reference files load only when the Skill points to them.

open-forge status reports the loading-tag closure: 11 files, about 6.4k tokens. It does not interpret required-read instructions in prose. open-forge context skills/open-forge-cli includes that closure and the required Skill in one batch, providing the 12-file source set used for the startup estimate. Account for any further reads required by your workspace's loaded rules. Installed Extensions and your own content can change these numbers.

Reference tokenizer counts use tiktoken with two named encodings, o200k_base and cl100k_base. The counts below predate the optional-content extraction and are historical, not counts for the current payload. Fresh tiktoken totals have not been measured.

Historical source setFileso200k_base tokenscl100k_base tokens
Default startup context196,8236,863
Complete base Framework238,2678,309

For startup, start with the canonical AGENTS.md handoff and loader, then follow exposed LoadNow and KeepInMind entries through loaded parents in listed order. Include adjacent overwrites where present. The current default set leaves the archived and Templates entrypoints unloaded. Adaptive Collaboration is now supplied on demand by the optional Collaboration Extension. The alternative CLAUDE.md bridge is included in the complete-file count.

Count each file's raw UTF-8 text independently with len(encoding.encode(text)), then sum the results. The measurements use tiktoken 0.14.0. They exclude tool response wrappers, file separators, system and conversation context, project-specific files, and Extensions. Actual model and harness costs can differ.

Recalculate after changing the shipped files or their loading policy. Report the tokenizer, included source set, and final source revision with the verification record. Keep Core, Memory, and the complete Framework distinct when describing the result.

Documentation voice​

Use Project Voice for READMEs and introductions: direct, concrete, and written like an engineer sharing ideas they're excited about. Give rules and reference text the precise, conversational voice described in the Writing Standard. Both share the same accuracy and terminology requirements. Choose the voice for the passage's purpose, so an opinionated introduction can lead into exact setup instructions.

The README should quickly explain what Open Forge is, why it helps, how to start, and where to learn more. Public guides explain use in more detail. Current repository documents preserve the complete accepted subjects, and maintenance files explain how their sources stay aligned. Link between them instead of maintaining competing detailed explanations.

Keep category questions synchronized between the README and their defining files. A description helps a reader decide whether to open a file. An optional responsibility helps an editor decide what belongs in it.

Follow the Writing Directive and Dictionary. The Framework itself must remain understandable without these repository documents.

Documentation site​

The public documentation site is a Docusaurus project in src/docusaurus/. Its own pages, covering getting started, concepts, the demos, and a file-by-file reference for every first-party Extension, live in src/docusaurus/docs/. The site also publishes this guide, the CLI guide, and the Extension guide directly from docs/, so each guide keeps one source. At build time, a relative link that leaves the published pages becomes a link to the file on GitHub.

The site has its own package.json and lockfile, separate from the repository tooling. From src/docusaurus/:

npm ci
npm start

The framework diagram has one data source, src/components/framework-map/framework-map-data.ts. The site renders it directly, and npm run diagram regenerates the light and dark SVGs that the README shows. Run it after changing the data.

npm start serves the site with live reload. npm run build writes the static site to src/docusaurus/build/ and fails on broken links or anchors. npm run typecheck checks the site's TypeScript.

The Documentation workflow type-checks and builds the site for pull requests that touch it or the three published guides, and publishes it to GitHub Pages whenever those changes reach main. The repository's Pages settings must use GitHub Actions as the build source. GitHub serves a project repository's site below its name, so the default address is https://thelithiumforge.github.io/open-forge/. The workflow reads the actual address from the Pages settings, so adding a custom domain moves the site to / without a configuration change. Local builds use the default address.

The demos live in the repository's demos/ folder, and the site's Demos pages describe them. They are small projects with seed requests at four levels of detail and a checklist for the result. The brownfield app runs its own tests with npm test from its app/ folder.

The site's pages explain shipped behavior, so they need the same care as the README. When a Framework or Extension change alters what a page or a demo describes, such as an installed file, a loading tag, a package name, or an install command, update it in the same change.