This document describes the current architecture of strap as implemented in this repository.
DEFAULT TO NUSHELL. USE BABASHKA/CLOJURE WHEN NU GETS AWKWARD. USE JAVASCRIPT ONLY WHEN THE COMMAND NEEDS AN NPM SDK, A NODE-ONLY PACKAGE, OR A NODE-SPECIFIC RUNTIME SURFACE.
REST calls, JSON shaping, filesystem orchestration, provider request compilation, and command plumbing should be Nu first. Rich local data logic, auth/state transformations, SQLite-style local work, or code that benefits from immutable data structures should use Babashka/Clojure. JavaScript is not the default command language; it is an escape hatch for package/runtime dependency pressure.
strap is a filesystem-discovered command harness.
strap <command> [args...]
The runner resolves command directories, builds a standard environment, then spawns the command's run file.
| Root | Purpose | Default |
|---|---|---|
STRAP_ROOT |
Installed harness code | repository root in development |
STRAP_CONFIG |
Static harness config | config/strap when present, otherwise $STRAP_GLOBAL |
STRAP_GLOBAL |
Global home-directory artifacts | $XDG_CONFIG_HOME/strap |
STRAP_PROJECT |
Project-shared harness artifacts | nearest .strap, otherwise $STRAP_WORKSPACE/.strap |
STRAP_SESSION |
Active session directory | explicit env var; session-aware commands fail when it is absent |
STRAP_WORK |
User/agent-local mutable state | nearest .strap-user, otherwise $STRAP_WORKSPACE/.strap-user |
STRAP_WORKSPACE |
Filesystem workspace for tools | current working directory |
Use strap paths or strap commands roots to inspect resolution.
strap status reports the active roots, layers, and visible artifact overlays. strap artifact status --paths exposes the physical files when debugging is needed.
Commands and tools share a layered artifact model:
session: active-session overlay under$STRAP_SESSION/overlay.user: mutable user/agent overlay under.strap-user.project: shared project artifacts under.strap.global: home-directory artifacts under$STRAP_GLOBAL.root: installed harness defaults.
The session layer shadows lower layers when STRAP_SESSION is set. strap artifact workon <type> <name> copies a lower-layer artifact into the session layer, or into the user layer if STRAP_SESSION is unset. strap artifact promote <type> <name> moves one layer down by default: session to user, user to project, project to global, and global to root. strap artifact discard <type> <name> removes the session overlay first, then user overlay.
Supported artifact types are currently command, tool, model, agent, and skill. Zettelkasten notes use the same overlay grammar through strap zk workon/promote/discard/status because note identity and tombstones need note-specific handling.
Commands are searched in this order:
STRAP_COMMAND_PATH
$STRAP_SESSION/overlay/commands
$STRAP_WORK/commands
$STRAP_PROJECT/commands
$STRAP_GLOBAL/commands
$STRAP_ROOT/commands
The last matching name wins when commands are collected for listing, and the runner resolves from the same ordered roots so project/global commands can override built-ins.
Project and user commands are isolated capsules. They compose with other commands through the top-level strap router, not by importing repo-local implementation modules. All callers use stdin/stdout and strap <command> at command boundaries.
A command directory can contain:
command-name/
run executable public command
desc help text; first line is list summary
spec.yaml optional carapace completion spec
carapace-complete optional dynamic completion script
compgen optional shell completion script
inner/ private helper scripts
hide hide from default command listing
wd optional executable that prints command cwd
There are three command-surface levels:
- Public commands are visible in
strap commands listand form the user/agent surface, for examplestrap zk,strap provider, andstrap embed. - Hidden commands have a
hidefile. They are still callable asstrap <command>, but are omitted from the default command list. Use them for implementation commands that need command identity without becoming public UI, for exampleprovider-chatgptbehindstrap provider chatgpt .... - Inner helpers live under one command's
inner/directory and are callable only throughstrap inner <command> <helper>. Use them only for helpers that are private to that command's implementation, such asstrap inner zk index. Do not execute inner files directly.
Inspect and validate the command surface with:
strap commands list --json
strap commands manifest auth
strap commands validate --jsonAgent profiles are markdown files discovered from:
STRAP_AGENT_PATH
$STRAP_WORK/agents
$STRAP_PROJECT/agents
$STRAP_CONFIG/agents
$STRAP_ROOT/agents
Profiles support OpenCode-style frontmatter with fields like description, permission, and color, followed by instruction text. strap agents apply <name> writes the selected profile into a canonical-state actor, usually actors.assistant, so provider compilation uses it as the actor frame. strap agents import-opencode ~/.config/opencode/agents copies existing OpenCode profiles into the user overlay.
Skills are instruction bundles discovered from:
STRAP_SKILL_PATH
$STRAP_WORK/skills
$STRAP_PROJECT/skills
$STRAP_CONFIG/skills
$STRAP_ROOT/skills
Each skill is a directory containing SKILL.md, compatible with OpenCode-style skill directories. strap skills apply <name> attaches the skill to an actor without replacing the agent profile. Provider compilation renders applied skills as named instruction blocks after the actor's private instructions. strap skills import-opencode ~/.config/opencode/skills copies existing OpenCode skills into the user overlay.
The main command runner does not implement access-control rules. Restricted modes should be separate launch environments, for example strap sandbox run --profile readonly -- ..., restricted MCP/jsmcp profiles, or external Linux isolation tools.
The active state format is strap.state.v0.2.
See state-format.md for the canonical state format specification.
State is provider-agnostic and structured as:
actors: known humans, agents, and harness participants;root: a tree of events and scopes;- events: messages, tool requests, tool results, memory context, fork/fold records;
- scopes: branchable/collapsible regions that can hold summaries and hidden children.
Addressability is optional. strap state bookmark add attaches inline bookmarks to visible nodes by unique text match, and strap state fold --from <bookmark> --to <bookmark> folds a sibling range into a collapsed scope. This keeps the base state hand-editable while still giving agents stable handles when needed.
strap state extract --from <bookmark> --to <bookmark> emits strap.context.v0.1 for a visible sibling range without mutating state. strap context quote converts that extracted context into a normal strap.state.v0.2 history. strap context summarize <framing> runs a summarization loop over that history and prints summary text.
Provider request payloads are compiled projections. Provider continuation IDs or protocol-specific metadata are not the canonical state.
Nu is the preferred implementation surface for local structured data plumbing inside command capsules. There is intentionally no grouped native use strap module and no cross-command Nu import layer.
Every capsule exposes executable run with a shebang; this is the process entrypoint used by the top-level strap runner. For Nu-native capsules, run.nu is command-local implementation code. Additional .nu files are command-local modules for run.nu, not alternate entrypoints or public imports.
run.nu stays native inside its command capsule: it accepts and returns Nushell values, and does not read stdin, write stdout, or expose command-boundary text JSON flags. The executable run owns stdin/stdout text JSON handling before delegating to run.nu: it parses stdin when the command logically takes input, ignores stdin when it does not, and prints text JSON or plain text as the process result.
Command input does not use compatibility switches. The public surface is data-in/data-out over stdin/stdout.
Callers do not import another capsule's private files directly. State lens operations such as with-events, map-events, and with-extract accept explicit external process filters after --, or plain command words that default to strap <command> .... Process filters receive JSON on stdin and emit JSON on stdout.
Examples:
strap state with-events -- jq 'map(select(.from == "user"))' < state.json > next.json
strap state map-events -- jq '. + {"reviewed": true}' < state.json > next.json
strap state with-extract bm_start bm_end -- jq 'map(.text |= ascii_upcase)' < state.json > next.jsonThe intended boundary is: Nu owns local dataflow, filesystem work, JSON plumbing, and REST-only command capsules; Babashka owns richer local algorithms and state/auth logic that are awkward in Nu but do not need a package ecosystem; Node is reserved for MCP and real package/runtime pressure only.
Implementation files are intentionally small. The hard cap is 150 lines per source file, command run script, or helper; prefer even smaller files when a command has separable concepts. Command-local modules are the escape hatch for complexity, not shared repo libraries. Existing oversized files must appear in .strap/config/line-cap-exceptions.txt until they are split.
There is one loop surface:
strap loop: Nushell loop for provider/tool orchestration.strap one-shot: Nu one-shot runner that builds a temporary child state from optional state/context input and returns the first final assistant answer without mutating sessions.
The loop composes through public commands: strap llm complete for provider interaction and strap run-calls for tool execution.
Tool schemas are exposed through strap tools list --group <group> --json:
fsprocesswebagentjsmcpall
Script tools are executable files discovered from:
STRAP_TOOL_PATH
$STRAP_WORK/tools
$STRAP_PROJECT/tools
$STRAP_CONFIG/tools
$STRAP_ROOT/tools
Tool artifacts are grouped directories: tools/<group>/run, tools/<group>/desc, and either tools/<group>/meta.json or tools/<group>/meta/<action>.json. Provider-facing names are dotted, for example fs.read_file and jsmcp.list_servers. Action metadata can set process_state to none (default), own, or full; own-state tools read/write runtime.tools.<group> through an own_state envelope. Results use semantically equivalent content and structuredContent; content should be readable for the specific tool, and result metadata is not part of the contract. See docs/tool-contract.md for the exact envelope contract.
strap mcp <group> exposes Strap tools as stdio MCP servers for external MCP hosts. It lists schemas through strap tools and executes calls through strap run-calls. strap mcp all expands to local groups only; jsmcp must be requested explicitly.
The jsmcp bridge uses the local jsmcp HTTP API and exposes configured MCP servers as programmable Strap tools. It stores observed server/tool discovery in canonical state and rejects execute_code when cached discovery changes.
Model profiles live in layered models/ directories and describe model id, provider street address, API family, endpoint, auth, headers, and parameters. Runtime commands default to --model current, where current.json is a Linux symlink to the selected profile.
Model roots:
STRAP_MODEL_PATH
$STRAP_WORK/models
$STRAP_PROJECT/models
$STRAP_CONFIG/models
$STRAP_ROOT/models
Current provider adapter families:
- OpenAI public API with API-key auth.
- OpenRouter chat completions.
- Anthropic Messages.
- ChatGPT/Codex backend responses with local ChatGPT OAuth token cache.
Provider commands are split by provider. strap provider is a public dispatcher. The provider implementations are hidden commands:
provider-openai: Nushell REST capsule.provider-openrouter: Nushell REST capsule.provider-anthropic: Nushell REST capsule.provider-chatgpt: Babashka capsule for ChatGPT/Codex backend calls and OAuth token management.
strap embed is a provider-neutral facade. Local hash embeddings are implemented in embed; provider-backed embeddings delegate to strap provider <name> embed.
ChatGPT OAuth is managed by:
strap provider chatgpt auth login
strap provider chatgpt auth import-codex
strap provider chatgpt auth refresh
strap provider chatgpt auth show
strap provider chatgpt auth logoutThe token cache defaults to ~/.config/strap/auth/chatgpt.json.
strap work init creates both project-shared .strap and user-local .strap-user layouts.
strap session stores sessions under $STRAP_WORK/sessions/, which defaults to .strap-user/sessions/, with:
meta.jsonstate.jsontrace.jsonlprovider-requests/tool-results/overlay/.jj/
strap session copy <name> copies the active session into a new session. With --at <bookmark>, the copied state is truncated after that visible bookmark so the conversation can continue in a different direction from a specific point.
Actual session-aware commands rely on STRAP_SESSION. There is no current-session pointer fallback; commands that need a session fail when the env var is absent. Use strap with-session <session> <command> for one-off process selection. In Nushell, strap with-session <session> { ... } runs a block with STRAP_SESSION scoped to that block, while strap session select <session> is def --env and sets $env.STRAP_SESSION in the current shell.
Session mutations create jj snapshots in the session directory selected by STRAP_SESSION. strap history operates on that session directory rather than the project-user work root, and strap history ui launches jjui there.
Memory is explicit: agents and humans use strap zk or the zk script tool to search, create, and update zettelkasten notes. Session commands do not implicitly inject or write memory.
strap zk stores markdown notes under .strap-user/zettel for user/private memory or .strap/zettel for project-shared memory. The user layer is a transparent overlay on the project layer: user notes shadow project notes with the same id, workon copies a project note into the user layer, promote writes it back, and normal output hides physical .strap* paths. Inline [[wikilinks]] are the canonical link source; backlinks and ambiguity diagnostics are derived during normal reads/writes. SQLite/FTS/vector data under $STRAP_WORK/zettel is a derived cache rebuilt by strap zk reindex and on hybrid/vector searches.
The zettelkasten is a self-contained Babashka command capsule because markdown overlays, wikilinks, tombstones, search/index rebuilds, and tool-mode JSON handling are one conceptual subsystem. Its SQLite/FTS/vector index helper is command-private and reached through strap inner zk index; it is not part of the public command surface.
strap history wraps the session directory selected by STRAP_SESSION as a jj repo. Project-user overlays and private memory remain under $STRAP_WORK, but session-local state and session overlays get per-session history without touching the project workspace history.
Code is grouped by capability:
cli: Babashka main command dispatcher behindbin/strap.cli: built-in command capsules.
Use:
./bin/strap project-test
strap commands validate --jsonstrap project-test runs syntax/source checks and the smoke workflow.