-
Notifications
You must be signed in to change notification settings - Fork 0
Release & Context Tooling Scripts
This page covers scripts/ — code that no end user ever executes inside the shipping app, with one deliberate exception: the context CLI and the agent-tool entry are TS sources under src/core/ that get bundled into committed artifacts under scripts/ and out/tools/, and those artifacts do ship to agents at runtime. Everything else (release.sh, check-originality.*, agent-tools-smoke.cjs, the space E2E harness) is maintainer-only and never enters the packaged app.
That distinction drives the main invariant to internalize before editing anything here: never hand-edit scripts/termsprawl-context.mjs or out/tools/agent-tool-entry.mjs. They are build outputs of pnpm run build:cli, and the next rebuild silently discards your changes.
release.sh is a linear, fail-fast orchestration script (set -euo pipefail). It does not build or publish anything by default — it hands off to a self-hosted builder. Its job is to get a correctly-versioned, gate-passing tag pushed to three remotes, and to stop there.
flowchart TD
A["release.sh <version> [--from b | --merge-all] [--local-dist]"] --> V{"version matches ^[0-9]+\\.[0-9]+\\.[0-9]+$ ?"}
V -- no --> X1["exit 2"]
V -- yes --> C{"git status --porcelain empty?"}
C -- no --> X2["exit 1: working tree not clean"]
C -- yes --> M["optional merge into main (--from / --merge-all)"]
M --> B["bump package.json version + README 'Current version: **x.y.z**'"]
B --> G["gates: pnpm typecheck, pnpm test, ./scripts/check-originality.sh"]
G -- fail --> X3["set -e aborts with tree left dirty"]
G -- pass --> K["commit 'chore: bump to x.y.z' (skipped if nothing staged)"]
K --> P["push main to origin, gitea, github"]
P --> T["tag vX.Y.Z (reuse if already at HEAD) + push to 3 remotes"]
T --> D{"--local-dist?"}
D -- no --> W["builder flow: Gitea Actions release job builds + publishes; verify by gh release view"]
D -- yes --> L["pnpm run dist → stage in /tmp/termsprawl-release-<ver> → notes → gh release create → read-back verify"]
Key nodes:
-
Version/tag derivation (
L42-L51): the positional argument is the bare version; the tag is alwaysv<version>. A leadingvin the argument is a hard error, not a normalization. -
Merge stage (
L64-L87):--merge-allwalks everyrefs/heads/*exceptmainand only merges branches whosemerge-basewithmainequals the current main tip — i.e. branches that are strictly ahead of main. Extraneous branches are skipped silently. Merges prefer--ff-only, fall back to--no-ffwith amerge: <branch>message, and abort on conflict. With neither flag and a non-mainHEAD, the script refuses to run. -
Gates (
L101-L105): this is the only place the script delegates to the other tools on this page.check-originality.shis invoked here; the algorithm itself lives in the Python file (see §2). -
Bump commit and resume semantics (
L107-L116): the bump is committed before any push. On a rerun where the bump already landed,git diff --cached --quietis true and the commit is skipped rather than aborting — this is what makes a retry after a mid-push failure safe. Note the asymmetry: the clean-tree guard atL58runs first, so a run that died after the bump but before the commit cannot simply be re-run; the tree is dirty and the guard fires. -
Tag handling (
L121-L134): an existing tag pointing at a different commit aborts; one pointing at HEAD is reused. Pushing the tag to all three remotes is what triggers the builder. -
--local-distescape hatch (L139-L179): builds locally, stages into/tmp/termsprawl-release-<version>, hard-fails ifdist/termsprawl-<version>.AppImageis missing (the.debandlatest-linux.ymlare best-effort copies), generatesRELEASE_NOTES.mdfromgit log --oneline <previous-tag>..<tag>(falling back tov0.0.0), thengh release createfollowed by a read-back viagh release view --json.
Boundary conditions worth knowing before changing it:
- Remote names (
origin,gitea,github) and the GitHub slug (dazeb/termsprawl) are hardcoded; the pushes are sequential, so a failure mid-way leaves remotes diverged and requires the resume path above. -
--skip-distis now accepted and ignored with a warning (L36) — it exists only for backward compatibility. - Unknown
-*options exit 2 (L37), but unknown positional arguments silently overwriteNEW_VER. Only the last positional wins. - The default flow deliberately ends with no release assets present yet; verification of the builder's output is a manual copy-paste step printed at
L181-L183.
release.sh calls the shell wrapper; the comparison logic is entirely in Python. The design goal is stated in the docstring: catch path-renamed copies, not just same-path duplicates, by comparing every scanned source file against every file in a prior project tree.
Mechanism:
-
code_files()collects files by extension (CODE_EXTS:.ts .tsx .js .jsx .css .mjs .cjs .py) under the source dir (defaultsrc) and the prior dir (default../nodeterm-linux). -
meaningful_lines()drops blank lines, lines shorter thanMIN_LINE_LEN(8) after right-stripping, and lines matching theTRIVIALpunctuation-only regex. Order is preserved, because ordering is treated as part of copied expression. - Every window of
MIN_BLOCK(env-overridable, default 5) consecutive meaningful lines in the prior tree becomes a tuple key inblock_index;setdefaultkeeps the first prior file that produced the block. - For each of our files, the scan breaks at the first matching window, so a file is reported at most once regardless of how many copies it contains.
- Matches are classified against an explicit
KNOWN_BENIGNset of exact tuples (currently a generic button-chrome CSS block). Benign hits are printed asREVIEWED ... BENIGNand do not fail; anything else prints asSUSPICIOUSand returns exit 1.
Extension points and blind spots:
-
Tuning knobs are all at the top:
MIN_BLOCK,MIN_LINE_LEN,TRIVIAL,CODE_EXTS. RaisingMIN_LINE_LENor extendingTRIVIALnarrows detection. -
Allowlisting is exact-tuple based on purpose — the comment (
L75-L90) frames it as "reusable generic styling concept", and the block must match verbatim, so the allowlist cannot be widened by accident via a substring. -
Fail-open on missing inputs: if the source dir doesn't exist the check prints OK, and if the prior tree isn't present it prints WARN and returns 0 (
L55-L60). In a CI checkout without the sibling../nodeterm-linuxtree, the gate passes vacuously. If you relocate either tree, the gate's meaning changes, not just its output. - Only line-identical, same-order, same-indentation blocks are caught (only trailing whitespace is stripped). Reformatting, renaming identifiers, or breaking a copied block with short lines defeats it — this is a plagiarism tripwire, not a provenance guarantee. Documentation formats (
.md,.json,.sh) are not scanned at all.
Two files collaborate here, and the relationship is the important part: scripts/build-context-cli.mjs is the producer, scripts/termsprawl-context.mjs is the committed product. The producer uses Vite's programmatic lib build (configFile: false, ssr: true so node: builtins stay real imports, target: 'node22', minify: false, rollupOptions.external: [] to inline everything except builtins), emits under scripts/, then rmSync/renameSync normalizes the SSR default filename to termsprawl-context.mjs (build-context-cli.mjs#L16-L41). Because the output is committed, pnpm test and packaged copies execute a stable artifact with no install step — and that is exactly why editing the .mjs directly is a trap. The same script performs a second, independent build of src/core/agent-tool-entry.ts into out/tools/agent-tool-entry.mjs, described as a dependency-free MCP/CLI client executed with the app's bundled Electron runtime (L44-L57).
The bundled runtime logic (visible inline in the committed artifact) resolves linked context like this:
flowchart LR
A["--cwd, --self"] --> B["peersOf(cwd, self)"]
B --> C[".termsprawl/links/*.json"]
C --> D["parseLinkFile: version==1, safe a/b, a!=b"]
D --> E["other side of the link = peer"]
E --> F[".termsprawl/transcripts/<peer>.json → path + version check"]
F --> G["readTranscriptTurns(path): JSONL user/assistant text"]
G --> H["formatLinkedContext: '# linked context from <title> (<id>)'"]
H --> I["stdout, blocks joined by ---"]
J[".termsprawl/project.json"] --> H
-
Argument contract (
termsprawl-context.mjs#L115-L132):--cwdand--selfare both required; any other argument is an error; parse failures print to stderr and exit 2. -
Link store: undirected pairs of project/node ids stored as
.termsprawl/links/*.jsonwithversion: 1. Both ids must satisfy/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; self-links are rejected. The filename is ignored, so the link's identity is the pair itself. Any parse failure, unreadable file, or version mismatch yieldsnulland the link is dropped silently. -
Transcript index:
.termsprawl/transcripts/<nodeId>.jsonmaps a node to a transcriptpathwithversion: 1. ThenodeIdis validated by the same safe-id pattern before being interpolated into the filename — that validation is the path-traversal guard for the index lookup. The recordedpathitself is read verbatim (L54-L65), so if you change who writes these index files, treat that field as trusted input with real filesystem reach. -
Turn extraction (
L66-L107): line-delimited JSON, skipping malformed lines; onlytype: "user" | "assistant"entries with objectmessagesurvive;contentmay be a string or an array of{type: "text", text}blocks (non-text blocks are dropped, and an array with no text returnsnull). Each turn is truncated to 2000 chars, and only the last 40 turns are kept. -
Output behaviour:
runContextClireturns 0 with no output when there are no peers, no transcript paths, or no turns. Empty output plus exit 0 is the intended "nothing to add" signal for prompt assembly; do not change it to a non-zero exit or error text without checking every caller. -
Testability seam:
runContextCli(opts, io)takes anioobject (peers,transcriptPath,nodeTitle,turns,print), andcreateRealContextIO(cwd)is only one implementation (L133-L170). Injecting a fakeiois the supported way to test resolution and formatting without a.termsprawldirectory.
To change any of the above, edit src/core/context-cli-entry.ts and rerun the build — the constants LINK_FILE_VERSION and INDEX_VERSION are the format contract. Note that a version bump makes all existing files invisible rather than invalid: links go dark silently, and the CLI still exits 0.
This one runs under Electron, after pnpm run build, and its defining constraint is isolation: it creates a temp root via mkdtempSync with home, data, project, and bin subdirectories, uses fixture agent CLIs from that bin, and per its header comment never sends model requests or touches the user's real agent configuration. It pulls in Electron's app/BrowserWindow, a local node:http server, spawn/execFileSync, and pathToFileURL to load built ESM output. TERMSPRAWL_SMOKE_APP_ROOT overrides the app root, which is what lets it run against a built tree outside the repo.
The excerpt available for this page stops before the assertion sequence, so the individual steps it verifies are not documented here — read the file itself before modifying what it asserts. What is safe to say: the isolated-home/isolated-project/isolated-bin setup is the invariant that must be preserved when adding cases, because that is what makes it safe to run on a developer machine with live CLI auth present.
Evidence gap: the wiki outline lists a space E2E harness under this page, but no source excerpt for it was provided. Its entry point, fixtures, and pass criteria are intentionally not described here rather than guessed.
| To change… | Edit | Then |
|---|---|---|
| Add a release gate |
scripts/release.sh step 3 (L101-L105) |
ensure the new script exits non-zero on failure — the pipeline relies on set -e
|
| Add a release flag | option loop at release.sh#L31-L40
|
remember -* is the catch-all and positionals overwrite NEW_VER
|
| Change copy-detection sensitivity |
check-originality.py constants / KNOWN_BENIGN
|
keep allowlist entries as exact tuples; re-verify the prior tree is present or the gate fails open |
| Change linked-context resolution or output format | src/core/context-cli-entry.ts |
pnpm run build:cli, commit the regenerated scripts/termsprawl-context.mjs
|
| Change agent-tool bundle contents | src/core/agent-tool-entry.ts |
pnpm run build:cli also rewrites out/tools/agent-tool-entry.mjs
|
Sources: scripts/release.sh, scripts/check-originality.py, scripts/termsprawl-context.mjs, scripts/build-context-cli.mjs, scripts/agent-tools-smoke.cjs
Generated from termsprawl at 0d4393be54c6200beedd91bb636e5296c30472c5.
App Shell & Platform Foundations
- Electron Main Process & Window Lifecycle
- Preload Bridge & IPC Contract
- Shared Domain Types and File/URL Helpers
- Renderer Bootstrap & App Composition
- Build Targets & TypeScript Configuration
Canvas, Nodes & Renderer State
- Infinite Canvas Surface & Viewport Interaction
- Workspace, Project & Tab State
- Node Links, Edges & Link Inspector
- Sticky, Group, Editor & Diff Nodes
- Keyboard Canvas Navigation & Cross-Panel Requests
- Theme, Accent & Visual Language
- Boot Overlay, Onboarding & Shared UI Kit
Terminals & Session Continuity
- PTY Lifecycle & Terminal Sessions
- tmux Session Naming & Reattach
- Scrollback Snapshots & Cold Replay
- Terminal Node Rendering (xterm.js)
- SSH Remote Projects, Terminals & Files
Persistence, Projects & Files
- Workspace Store & Project File Layout
- Project Scope, Deletion & Worktree Registry
- Workspace Bundle Export/Import
- File Service & File Tree UI
Agent Runtime & Tooling
- Agent Status Model & Hook Normalization
- Hook Server & CLI Hook Installers
- Agent Launch, CLI Probing & Managed Accounts
- Agent Tool Protocol & In-Process Server
- Agent Tool Client, CLI & MCP Entry
- Transcripts, Context Discovery & Context CLI
- Agent Canvas State & Status Badges
Chat Nodes & Model Providers
- Chat Runtime, Conversation & Cost
- Model Provider Adapters & Streaming
- Chat Tool Calling & Project Tools
- Chat Node UI
Git & Source Control
Embedded Browser Nodes
- Browser Manager & Guest Runtime
- CDP Facade & Browser Agent Server
- Browser Navigation Policy & Node UI
Server Edition
- Server Bootstrap & HTTP/WebSocket Entry
- RPC Dispatch, Handlers & Service Bridges
- Renderer Shim & Server Boundary
- Server Auth & Security Boundary
Relay & Remote Access
- Relay Hub & WebSocket Frame Routing
- Relay End-to-End Cryptography
- Relay Auth, Invites, Store & Admin API
- Relay Client, Pairing & Terminal Tunneling
- Relay Trust UI
Integrations & Secondary Surfaces
- Telegram Bot, Commands & Pairing
- A2A Peers: Protocol, Client & Server
- Node Link Engine, Registry & Scheduler
- Cloud Spaces, Snapshots & Sync
Settings, Updates & Maintenance