Skip to content

Release & Context Tooling Scripts

dazeb edited this page Sep 17, 2026 · 1 revision

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.

1. The release pipeline (scripts/release.sh)

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"]
Loading

Key nodes:

  • Version/tag derivation (L42-L51): the positional argument is the bare version; the tag is always v<version>. A leading v in the argument is a hard error, not a normalization.
  • Merge stage (L64-L87): --merge-all walks every refs/heads/* except main and only merges branches whose merge-base with main equals 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-ff with a merge: <branch> message, and abort on conflict. With neither flag and a non-main HEAD, 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.sh is 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 --quiet is 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 at L58 runs 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-dist escape hatch (L139-L179): builds locally, stages into /tmp/termsprawl-release-<version>, hard-fails if dist/termsprawl-<version>.AppImage is missing (the .deb and latest-linux.yml are best-effort copies), generates RELEASE_NOTES.md from git log --oneline <previous-tag>..<tag> (falling back to v0.0.0), then gh release create followed by a read-back via gh 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-dist is now accepted and ignored with a warning (L36) — it exists only for backward compatibility.
  • Unknown -* options exit 2 (L37), but unknown positional arguments silently overwrite NEW_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.

2. The originality gate (scripts/check-originality.py, invoked via check-originality.sh)

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:

  1. code_files() collects files by extension (CODE_EXTS: .ts .tsx .js .jsx .css .mjs .cjs .py) under the source dir (default src) and the prior dir (default ../nodeterm-linux).
  2. meaningful_lines() drops blank lines, lines shorter than MIN_LINE_LEN (8) after right-stripping, and lines matching the TRIVIAL punctuation-only regex. Order is preserved, because ordering is treated as part of copied expression.
  3. Every window of MIN_BLOCK (env-overridable, default 5) consecutive meaningful lines in the prior tree becomes a tuple key in block_index; setdefault keeps the first prior file that produced the block.
  4. 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.
  5. Matches are classified against an explicit KNOWN_BENIGN set of exact tuples (currently a generic button-chrome CSS block). Benign hits are printed as REVIEWED ... BENIGN and do not fail; anything else prints as SUSPICIOUS and returns exit 1.

Extension points and blind spots:

  • Tuning knobs are all at the top: MIN_BLOCK, MIN_LINE_LEN, TRIVIAL, CODE_EXTS. Raising MIN_LINE_LEN or extending TRIVIAL narrows 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-linux tree, 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.

3. Context CLI: one TS implementation, one committed bundle

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/&lt;peer&gt;.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
Loading
  • Argument contract (termsprawl-context.mjs#L115-L132): --cwd and --self are 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/*.json with version: 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 yields null and the link is dropped silently.
  • Transcript index: .termsprawl/transcripts/<nodeId>.json maps a node to a transcript path with version: 1. The nodeId is 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 recorded path itself 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; only type: "user" | "assistant" entries with object message survive; content may be a string or an array of {type: "text", text} blocks (non-text blocks are dropped, and an array with no text returns null). Each turn is truncated to 2000 chars, and only the last 40 turns are kept.
  • Output behaviour: runContextCli returns 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 an io object (peers, transcriptPath, nodeTitle, turns, print), and createRealContextIO(cwd) is only one implementation (L133-L170). Injecting a fake io is the supported way to test resolution and formatting without a .termsprawl directory.

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.

4. Agent-tool smoke test (scripts/agent-tools-smoke.cjs)

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.

5. Change map

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

termsprawl

App Shell & Platform Foundations

Canvas, Nodes & Renderer State

Terminals & Session Continuity

Persistence, Projects & Files

Agent Runtime & Tooling

Chat Nodes & Model Providers

Git & Source Control

Embedded Browser Nodes

Server Edition

Relay & Remote Access

Integrations & Secondary Surfaces

Settings, Updates & Maintenance

Clone this wiki locally