This is the StrayMark project repository. It contains two main components, plus one experimental:
- Framework (
dist/): documentation templates, governance policies, and agent directives - CLI (
cli/): thestraymarkRust binary that manages the framework in user projects - Loom (
experiment-loom/, EXPERIMENTAL): knowledge-graph/architecture visualization server — seeexperiment-loom/README.mdandexperiment-loom/CHARTER-01-loom-server.md
The Rust code is a Cargo workspace (root Cargo.toml, members core + cli): straymark-core holds the shared document model and knowledge graph (one parser for the CLI and Loom); Cargo.lock and [profile.release] live at the workspace root; build artifacts land in the root target/.
StrayMark adopts StrayMark for its own development (decision: AIDEC-2026-07-13-001, installed 2026-07-13 in PR #362). Two .straymark/-shaped things exist in this repo — never confuse them:
dist/.straymark/= the framework DISTRIBUTION SOURCE — product under edit. This is the shipped template. Edit it as product (it's what adopters receive). Never read it as governance-in-force for this repo, and never write generated artifacts (AILOGs/AIDECs/telemetry) into it. Never run a mutatingstraymarkcommand with cwd or--pathinsidedist/— the CLI now refuses (safeguard S1), but the intent is yours to hold./.straymark/(repo root) = the installed framework — governance IN FORCE. It is pinned to the last release ("yesterday's tail"), so it may lagdist/. That is the governance you follow and where your artifacts go (AILOGs, decisions, Charters);straymark statusshows the skew (safeguard S3).
Rule of thumb: dist/ is what we ship; /.straymark/ is what governs us. They are one framework version apart on purpose.
straymark/
├── Cargo.toml # Workspace root (members: core, cli) + release profile
├── Cargo.lock
├── core/ # straymark-core: shared document model + typed knowledge graph
│ └── src/
│ ├── document.rs # DocType, Frontmatter, parse_document, discover_documents
│ └── graph.rs # Typed, bidirectional, orphan-preserving graph builder
├── cli/ # Rust CLI source code
│ ├── src/
│ │ ├── main.rs # Entry point, command routing
│ │ ├── commands/ # Subcommands: init, update, remove, status, repair, validate, new, compliance, metrics, analyze, audit, explore, about
│ │ ├── tui/ # Terminal UI for `explore` (ratatui + crossterm)
│ │ ├── analysis_engine.rs # Code complexity analysis (arborist-metrics)
│ │ ├── config.rs # StrayMarkConfig, Checksums, ComplexityConfig
│ │ ├── download.rs # GitHub API, ZIP downloads
│ │ ├── inject.rs # Directive injection system
│ │ ├── manifest.rs # dist-manifest.yml parser
│ │ ├── platform.rs # OS/arch detection
│ │ ├── self_update.rs # CLI auto-update
│ │ └── utils.rs # Output helpers, file hashing
│ ├── tests/ # Integration tests
│ └── Cargo.toml
├── experiment-loom/ # Loom (EXPERIMENTAL): straymark-loom server crate
│ ├── src/ # axum server + notify watcher
│ ├── web/ # Sigma.js + graphology frontend (Vite/TS, built in CI only)
│ ├── specs/ # SpecKit sets (001 knowledge graph, 002 architecture plan)
│ └── CHARTER-01-loom-server.md
├── dist/ # Framework distribution files
│ ├── .straymark/ # Templates, governance, config
│ ├── STRAYMARK.md # Unified governance rules
│ └── dist-manifest.yml # What gets installed
├── docs/ # Project documentation (EN + ES + zh-CN)
├── .github/workflows/ # CI/CD
│ ├── release-cli.yml # Build + release CLI binaries (publishes straymark-core first)
│ ├── release-loom.yml # Build + release Loom binaries (loom-* tags; npm build step; no crates.io)
│ └── release-framework.yml
└── README.md
StrayMark uses independent versions for framework and CLI:
| Component | Tag format | Current | Example |
|---|---|---|---|
| Framework | fw-X.Y.Z |
fw-4.2.0 | fw-4.2.0 |
| CLI | cli-X.Y.Z |
cli-3.2.2 | cli-3.2.2 |
Follow semver:
- Major: breaking changes
- Minor: new features (e.g., new command)
- Patch: bug fixes, small improvements
Edit cli/Cargo.toml:
version = "X.Y.Z"Run cargo check at the repo root to update the workspace Cargo.lock.
Update version references in all docs that mention version numbers:
docs/adopters/CLI-REFERENCE.md(EN — versioning table + example outputs)docs/i18n/es/adopters/CLI-REFERENCE.md(ES — same)docs/i18n/zh-CN/adopters/CLI-REFERENCE.md(zh-CN — same)README.md(versioning table)docs/i18n/es/README.md(ES — versioning table)docs/i18n/zh-CN/README.md(zh-CN — versioning table)
Update CHANGELOG.md (root) following Keep a Changelog format:
- Add a new
## CLI X.Y.Zsection at the top (below the header) - Use subsections:
### Added (CLI),### Changed (CLI),### Fixed (CLI),### Removedas applicable - Describe user-facing changes concisely
git checkout -b chore/bump-cli-X.Y.Z
git add cli/Cargo.toml Cargo.lock docs/
git commit -m "chore: bump CLI version to X.Y.Z"
# Push, create PR, merge to maingit tag cli-X.Y.Z
git push origin cli-X.Y.ZThe release-cli.yml workflow triggers automatically:
- Verifies
Cargo.tomlversion matches the tag - Compiles for 4 platforms in parallel:
x86_64-unknown-linux-gnu(Ubuntu)x86_64-apple-darwin(macOS Intel)aarch64-apple-darwin(macOS ARM)x86_64-pc-windows-msvc(Windows)
- Packages each as
.tar.gz(Unix) or.zip(Windows) - Creates the GitHub release and uploads all binaries
If CI needs re-running, trigger manually:
gh workflow run release-cli.yml -f tag=cli-X.Y.Zgh release view cli-X.Y.Z --json assets --jq '.assets[].name'
# Should show 4 binariesUsers can now run straymark update-cli to get the new version.
Framework releases are automated via release-framework.yml. The workflow triggers on tag push (fw-*), packages dist/ as a ZIP, and creates the GitHub release with the asset.
Edit dist/dist-manifest.yml:
version: "X.Y.Z"Update version references in docs:
docs/adopters/CLI-REFERENCE.md(EN — versioning table)docs/i18n/es/adopters/CLI-REFERENCE.md(ES — versioning table)docs/i18n/zh-CN/adopters/CLI-REFERENCE.md(zh-CN — versioning table)README.md,docs/i18n/es/README.md, anddocs/i18n/zh-CN/README.md(versioning tables)dist/.straymark/00-governance/QUICK-REFERENCE.md(EN + ES + zh-CN footer)dist/.straymark/00-governance/AGENT-RULES.md(EN + ES + zh-CN footer)dist/.straymark/00-governance/DOCUMENTATION-POLICY.md(EN + ES + zh-CN footer)dist/.straymark/00-governance/C4-DIAGRAM-GUIDE.md(EN + ES + zh-CN footer)
Update CHANGELOG.md (root) following Keep a Changelog format:
- Add a new
## Framework X.Y.Zsection at the top (below the header) - Use subsections:
### Added (Framework),### Changed (Framework),### Fixed,### Removedas applicable - If both CLI and Framework are bumped together, combine into
## Framework X.Y.Z / CLI X.Y.Z
git checkout -b chore/bump-fw-X.Y.Z
git add dist/ docs/ README.md
git commit -m "chore: bump Framework version to X.Y.Z"
# Push, create PR, merge to maingit tag fw-X.Y.Z
git push origin fw-X.Y.ZThe release-framework.yml workflow triggers automatically:
- Verifies
dist-manifest.ymlversion matches the tag - Packages
dist/contents intostraymark-fw-X.Y.Z.zip - Creates the GitHub release with the ZIP as asset
If CI needs re-running, trigger manually:
gh workflow run release-framework.yml -f tag=fw-X.Y.Zgh release view fw-X.Y.Z --json assets --jq '.assets[].name'
# Should show: straymark-fw-X.Y.Z.zipUsers can now run straymark update-framework to get the new version.
Every release-*.yml workflow prunes its own component's releases to the newest 5 (KEEP=5). Tags are never deleted — they are the historical record, they cost nothing to keep, and git checkout <tag> keeps working for every version ever shipped.
- Only the last 5 versions are downloadable. Anything older still has its tag and its source, but no built assets: no
straymark update-frameworkrollback to it, and no verifying a migration against the release it actually shipped in. That is why the window exists at all; it wasKEEP=1until 2026-08-06, which made both impossible. - A tag without a release is normal, not a failure. Anything older than the newest 5 will be exactly that. Use
gh release listto see what is downloadable andgit tag -lto see what was ever released — they answer different questions. - Tags deleted before 2026-08-06 are gone for good. The old
KEEP=1step used--cleanup-tag, so it removed tags along with releases. Only versions published from that date forward are guaranteed to have a tag.
Loom releases are GitHub-release-only (no crates.io while experimental). Tag format: loom-X.Y.Z.
- Bump
versioninexperiment-loom/Cargo.tomland updateexperiment-loom/CHANGELOG.md. - Commit via PR (same rules as any change).
git tag loom-X.Y.Z && git push origin loom-X.Y.Z—release-loom.ymlverifies the version matches the tag, builds the frontend (npm ci && npm run buildinexperiment-loom/web/, embedded via rust-embed), compiles the same 4-platform matrix as the CLI, and uploadsstraymark-loom-vX.Y.Z-<target>.{tar.gz,zip}assets. The release is marked--latest=falseso CLI update flows are unaffected.- Verify:
gh release view loom-X.Y.Z --json assets --jq '.assets[].name'(4 binaries).straymark loom servepicks the new version automatically (version marker in~/.straymark/bin/).
- Never commit directly to
main. All changes go through feature/fix/chore branches and pull requests (mirrors the rule shipped to adopters indist/STRAYMARK.md §5). - Branch prefixes:
feat/,fix/,chore/,docs/,refactor/,test/. - Conventional commit subjects:
feat:,fix:,docs:,refactor:,chore:(release bumps usechore: bump fw-X.Y.Zorchore: bump cli-X.Y.Z). - Squash merge is the project default. Visible in
git log: each merged PR yields one squash commit onmain(e.g.2ae3802 feat(framework): fw-4.13.0 — TDE activation trigger (closes #128) (#129)).
Lesson from the #129 / #131 / #133 incident (2026-05-11): when you stack PR B on top of PR A's branch (base of B is A's head, not main), and A is squash-merged to main, B's content gets stranded:
- The squash merge of A creates a new commit on
mainwith content equivalent to A but a different SHA than A's original commits. - B's branch still points at A's original commits, which now have no descendant on
main. - When B is merged, GitHub merges it into A's branch (its declared base), not
main. The "merge to main" never happens — even though GitHub UI shows B asMERGED. - A direct PR from A's branch →
mainafterwards will surface as CONFLICTING because git sees the same lines touched in different ways (A's original commits vs A's squash on main).
Prevention (pick one):
- Sequential, not stacked. Wait for PR A to merge to
main, then rebase B ontomainand open B as a standalone PR withbase = main. Slower but bulletproof. - If you must stack, use merge commits (not squash) for the parent. A merge commit preserves shared history, so subsequent merges of stacked PRs into
mainresolve cleanly. Pay the cost of a noisiergit logfor the stacked-PR safety.
Recovery if you find yourself in this state (B stranded on A's branch, B's content not in main):
git checkout -b chore/sync-<B-content>-to-main maingit cherry-pick <B's merge commit SHA>— should be clean because B touches files outside A's conflict zone (which is why it could be stacked in the first place).- Push, open PR with
base = main, head = chore/sync-.... Cherry-pick produces a fresh commit on top ofmain, so no conflicts. - The branch protection may require
--adminmerge if the content was already reviewed in B (the original PR) — sync PRs are purely procedural.
- The user owns the repo.
gh pr merge --adminis acceptable for procedural sync PRs where the content was already reviewed in a separate PR (e.g. recovering from a stacked-PR mishap as above). - For substantive PRs, do not bypass review — wait for the user to merge via UI or explicit instruction.
- Tags are created on
mainHEAD after the relevant PR is merged. - Before pushing a tag, verify the in-file version matches:
grep '^version' dist/dist-manifest.ymlforfw-*tags;grep '^version' cli/Cargo.tomlforcli-*tags. CI'srelease-cli.ymlandrelease-framework.ymlboth refuse mismatches. - Both tags can be pushed in a single command when releasing framework + CLI together:
git push origin fw-X.Y.Z cli-X.Y.Z.
| Command | Description |
|---|---|
straymark init [path] |
Initialize StrayMark in a project |
straymark update |
Update both framework and CLI |
straymark update-framework |
Update only the framework |
straymark update-cli |
Update the CLI binary |
straymark remove [--full] |
Remove StrayMark from project |
straymark status [path] |
Show installation health and doc stats |
straymark status --where [path] [--out DIR] |
EXPERIMENTAL (Loom A1.4) — textual "you are here": load architecture/model.yml, project per-layer/per-component state (active/in-progress/implemented/has-debt/uncharted) from governance signals (charters + drift + open TDEs + on-disk inventory) via the pure core::architecture::project, highlight active components, and print the §8 "Where are we" summary. Degrades to a hint when no model exists. --out overrides the default .straymark/architecture/ (lets you dogfood a non-installed repo) |
straymark repair [path] |
Restore missing directories and framework files |
straymark validate [path] [--staged] |
Validate documents for compliance and correctness |
straymark new [path] [-t type] [--title] |
Create a new StrayMark document from a template |
straymark compliance [path] |
Check regulatory compliance (EU AI Act, ISO 42001, NIST) |
straymark metrics [path] |
Show governance metrics and documentation statistics |
straymark analyze [path] |
Analyze code complexity (cognitive + cyclomatic metrics) |
straymark analyze declared-vs-wired [path] |
Flag declared symbols (IPC/RPC proxy methods) with no implemented wiring counterpart — config-driven set-difference (POLISH-CHARTER-PATTERN sub-class 5) |
straymark followups list [--bucket] [--status] [--severity] [--label] |
List follow-ups registry entries with filters |
straymark followups status [FU-NNN] |
Registry pulse (counters recomputed on the fly) or entry detail |
straymark followups drift [--apply] [--scan-all] |
Detect/extract AILOGs with unextracted follow-up content (native, anti-noise suspected-closed, recomputes CLI-owned counters even with zero extractions, upgrades v0→v1) |
straymark followups recount |
Recompute the CLI-owned counters after a manual-triage session (no AILOG scan, idempotent) |
straymark followups promote FU-NNN [--premise-verified] |
Elevate an entry to a TDE document with promoted_from_followup traceability; surfaces the premise re-check, stamps Verified-at with the flag |
straymark followups verify FU-NNN [--premise "..."] [--verified] [--at DATE] |
Re-verify a dated hypothesis's premise at execution: surface/record the premise, stamp Verified-at (AIDEC-2026-07-18-001) |
straymark followups note FU-NNN "<text>" [--source ID] |
Append a dated annotation to an entry's Notes in one validated edit (#355) |
straymark followups set-status FU-NNN <status> |
Change an entry's status and recompute the CLI-owned counters in the same step — no recount to forget (#355) |
straymark followups new --title … --origin "CHARTER-NN §Scope" [--bucket …] [--premise …] |
Create an entry declared ex-ante, at Charter-declaration time, with an atomically-assigned id (#360) |
straymark architecture generate [path] [--force] [--out DIR] |
EXPERIMENTAL (Loom A1.2) — write a first-draft architecture/model.yml + plan.drawio by mining codebase structure (top-level source dirs → components) enriched with ADR C4 diagrams + "Affected Components" tables. --force overwrites; --out overrides the default .straymark/architecture/ |
straymark architecture sync [path] [--out DIR] [--apply] |
EXPERIMENTAL (Loom A1.3) — append-only: detect new top-level source dirs / ADR components not yet covered by the model and append them to model.yml + plan.drawio (never clobbers human edits/geometry). Dry-run by default; --apply writes |
straymark architecture validate [path] [--out DIR] [--output FMT] |
EXPERIMENTAL (Loom A1.3) — report model↔plan.drawio integrity signals (undrawn/unmodeled/empty) via core::architecture::validate_model. --output text|json|markdown; exits 1 when any signal is found (CI-gateable) |
straymark audit [path] |
Generate audit trail reports with timeline and traceability |
straymark explore [path] |
Interactive TUI documentation browser |
straymark loom serve [path] [--port] [--no-open] |
Launch Loom, the EXPERIMENTAL knowledge-graph visualization server (binary downloaded on demand from loom-* releases, cached in ~/.straymark/bin/) |
straymark about |
Show version and license info |
cd cli
cargo build # Debug
cargo build --release # Release
cargo build --no-default-features # Without TUIcargo test # Full workspace suiteCLI integration tests (cli/tests/) spawn the binary via assert_cmd. Use the macro cargo_bin_cmd!("straymark") (import use assert_cmd::cargo_bin_cmd;), not the function Command::cargo_bin("straymark").unwrap(). The function is deprecated since assert_cmd 2.1 (incompatible with a custom cargo build-dir) and the macro returns a Command directly — no .unwrap().
| Feature | Default | Description |
|---|---|---|
tui |
Yes | Terminal UI for explore (ratatui + crossterm + pulldown-cmark) |
| Crate | Purpose |
|---|---|
clap |
CLI argument parsing |
colored |
Terminal colors |
ratatui |
TUI framework (optional) |
crossterm |
Terminal backend (optional) |
pulldown-cmark |
Markdown parser (optional) |
reqwest |
HTTP client for downloads |
serde_yaml |
YAML parsing |
chrono |
Date parsing (metrics, audit) |
anyhow |
Error handling |
Public marketing + docs + blog site, served at https://straymark.dev. Source under website/. Stack: Docusaurus 3.10, TypeScript, React 19, Node ≥ 20. Engine of record: GitHub Pages with the official Actions pipeline (build_type=workflow), not the legacy gh-pages branch.
website/
├── docusaurus.config.ts # Site config: title, url, baseUrl, i18n, plugins, theme
├── sidebars.ts # Docs sidebar structure
├── package.json # npm scripts: start/build/sync:docs/migrate:blog
├── blog/ # Blog posts (canonical EN). 14 posts.
├── src/ # React components (custom landing, Hero, etc.)
├── static/ # Static assets served at /, incl. CNAME
├── scripts/
│ ├── sync-docs-i18n.ts # Copies docs/i18n/{locale}/ → website/i18n/{locale}/...
│ ├── migrate-blog.ts # One-off migration helper (Aparador → blog/)
│ └── blog-excerpts.json # Hand-authored blog excerpts (overrides auto-truncation)
├── i18n/ # Per-locale overrides (UI strings + blog translations + docs mirror)
└── (build/ + .docusaurus/ # Generated, gitignored)
Canonical docs live at the repo root in docs/ (read by the path: '../docs' setting in the classic preset config). Translations of those docs live in docs/i18n/{locale}/ and get mirrored into website/i18n/{locale}/docusaurus-plugin-content-docs/current/ by the sync:docs script — Docusaurus reads them from the mirror at build time. Edit canonical content in docs/, edit translated content in docs/i18n/{locale}/, never edit website/i18n/.../current/ directly (it's clobbered on every build).
| Script | What it does |
|---|---|
npm run start |
Dev server with hot reload (prestart runs sync:docs first). |
npm run build |
Production build into build/. prebuild runs sync:docs. |
npm run serve |
Serve the production build locally for smoke-testing. |
npm run sync:docs |
Mirror docs/i18n/{locale}/ into website/i18n/{locale}/.../current/. Idempotent. |
npm run migrate:blog |
One-off — only used during the initial Aparador → Docusaurus migration. |
npm run clear |
Wipe .docusaurus/ cache + build/. |
npm run typecheck |
tsc over the website's TypeScript. |
npm run write-translations |
Extract translatable strings from JSX/markdown into i18n/{locale}/code.json skeletons. Use when adding a new locale or new translatable UI strings. |
Configured in docusaurus.config.ts under i18n:
i18n: {
defaultLocale: 'en',
locales: ['en', 'es', 'zh-CN'],
localeConfigs: {
en: {label: 'English'},
es: {label: 'Español'},
'zh-CN': {label: '简体中文'},
},
}For each non-default locale you need three things:
- The locale code in
locales+ alocaleConfigs[code]entry (label shown in the dropdown). website/i18n/{code}/code.json— UI strings (hero, workflow, features section). Generate skeleton withnpm run write-translations -- --locale {code}and translate themessagefields. Theesfile is the reference shape.website/i18n/{code}/docusaurus-plugin-content-blog/— blog post translations +authors.yml. One file per canonical post (same filename).docs/i18n/{code}/— doc translations, copied automatically intowebsite/i18n/.../current/bysync:docs. Also add the locale toLOCALESarray inwebsite/scripts/sync-docs-i18n.ts.- Optionally
website/i18n/{code}/docusaurus-theme-classic/{navbar,footer}.jsonfor theme overrides.
Workflow: .github/workflows/deploy-website.yml. Triggers on push to main when files under website/**, docs/**, or the workflow itself change. Two jobs:
- build — checkout,
npm ci(cache keyed onwebsite/package-lock.json),npm run build,actions/configure-pages+actions/upload-pages-artifactupload thewebsite/build/dir as a Pages artifact. - deploy —
actions/deploy-pagesconsumes the artifact, publishes to thegithub-pagesenvironment, surfaces the URL on the PR/run.
Permissions required (already set in the workflow): contents: read, pages: write, id-token: write.
Repo-side Pages config (one-time setup, can be inspected with gh api repos/StrangeDaysTech/straymark/pages):
| Field | Value | Why |
|---|---|---|
build_type |
workflow |
Required by actions/deploy-pages. Was legacy until the migration to the official pipeline (PR #169) — see the "Pages source migration gotcha" note below. |
cname |
straymark.dev |
Custom domain. Must be set via gh api -X PUT .../pages -f cname=straymark.dev once. Auto-detection from website/static/CNAME only works in legacy build_type. |
https_enforced |
true |
Let's Encrypt cert auto-provisions after the cname is set. |
website/static/CNAME carries straymark.dev into the published artifact (used by Pages for the legacy redirect from strangedaystech.github.io/straymark/). The authoritative source of the custom domain when running under build_type: workflow is the Pages API cname field — not the file.
To change the domain: update website/static/CNAME, update url: in docusaurus.config.ts, and gh api -X PUT .../pages -f cname=<new-domain>. Then redeploy. DNS must already point at GitHub Pages or the cert provisioning will fail.
- Docusaurus build has a slow tail. After the production build is logically complete (artifacts written to
build/), the Node process can take 5–20 extra seconds to exit cleanly (workers, fs watchers). The command is not hung — wait it out. Markers of real completion:build/index.htmlandbuild/docs/intro/index.htmlboth exist. - Never run two builds in parallel. Both write to the same
build/dir and race. Ifnpm run buildseems stuck, kill it first (pkill -f "docusaurus build"); do not relaunch on top of it. - Avoid
npm run build | tail -Nfor monitoring.tail -Nwaits for EOF on its stdin, and Docusaurus's slow tail makes that close late — making the whole command appear hung when it isn't. Either run the build without a pipe, or redirect to a file (npm run build > build.log 2>&1) andtailthe file separately. excludesemantics. Patterns in the docs plugin'sexcludeare matched relative topath: '../docs'. Use'**/decisions/**'(with leading**), not'decisions/**', when you want to exclude a sub-tree across all i18n mirrors as well.- Pages source migration gotcha. Switching the workflow from
peaceiris/actions-gh-pagesto the officialactions/deploy-pagesrequiresbuild_type=workflowat the repo level. If you migrate the workflow without flipping the repo setting, thedeployjob fails with zero visible steps (the environment can't bind to a legacy source). Fix:gh api -X PUT repos/<org>/<repo>/pages -f build_type=workflow.
# Pages config is sane
gh api repos/StrangeDaysTech/straymark/pages --jq '{build_type, cname, html_url, https_enforced}'
# Workflow run succeeded
gh run list --workflow=deploy-website.yml --branch=main --limit 1 --json conclusion --jq '.[0].conclusion'
# Live site responds
curl -sI https://straymark.dev/ | head -1 # HTTP/2 200
curl -sL https://straymark.dev/ | grep -oE '<title>[^<]*</title>' # site title present
curl -sIL https://strangedaystech.github.io/straymark/ | head -2 # 301 → straymark.devRead and follow the rules in STRAYMARK.md. That file contains all StrayMark documentation governance rules for this project.