all systems operationalv0.17.10
stech/

Agent starter templates

stech init --template=<id> scaffolds a new agent project from a curated starter so a fresh user can get a working agent in one command instead of filling out TODO stubs from an empty agent.ts. Templates are static tarballs published to releases.stech.com on every CLI release tag, SHA-pinned in a manifest the CLI verifies before extracting.

Quick start #

stech auth login                                            # cache org slug + api key
stech init my-bot --template=customer-support               # fetch + extract template
cd my-bot
stech dev "what's our refund policy?"                       # runs locally
stech deploy && stech query my-bot "what's our refund policy?"

Without --template the command falls back to the empty scaffold (the previous default — an agent.ts with TODO-stub tools).

Catalog #

Four templates ship in v1. Each lives at cli/templates/<id>/ in this repo and ships its own README.md with a sample transcript, provider-wiring notes, and the exact tool surface.

customer-support #

RAG over the org's knowledge corpus plus a ticket-create stub. The knowledge_lookup tool wraps KnowledgeMemory.search() from @stech/agent (hybrid BM25 + vector); ticket_create returns a synthetic id you replace with a fetch() to your CRM. Self-contained: works after stech auth login plus uploading at least one document at /knowledge. Model: claude-sonnet-4-6. Most common ask we hear — a good first agent.

code-reviewer #

Reads a GitHub PR diff via the gh CLI source with auth_mode='user_oauth' and posts a single review comment. Demos per-user OAuth (the v0.16.0 marquee feature) — every member of the org connects their own GitHub at /settings/cli-credentials, and agent runs fork gh with the caller's token. Requires the org admin to register gh per the walkthrough in cli-tool-sources.md § Walk-through: registering gh as a user_oauth source before any member can use the template end-to-end. Model: claude-sonnet-4-6.

log-triage #

Haiku-tier log classifier — summarises recent log lines and files a stub ticket on anomalies. Demos cheap-model routing for the SRE persona. Both tools (fetch_recent_logs, file_ticket) are stubs with realistic shapes that you wire to your observability stack (Loki / Datadog / CloudWatch / Splunk) and incident tracker (Linear / Jira / PagerDuty / OpsGenie). The template's README has a worked Loki example for fetch_recent_logs. Model: claude-haiku-4-5.

knowledge-assistant #

Q&A over your uploaded docs. The librarian variant of customer-support — pure read path with knowledge_search (hybrid search) plus cite_chunk (full-text fetch by chunk id when a snippet is truncated mid-sentence). System tone is "answer from the docs, refuse politely when the corpus is silent" rather than "always escalate". Self-contained: works after stech auth login plus uploading one doc. Model: claude-sonnet-4-6.

The --template flag #

stech init <name> [--template=<id>]

Without --template, behavior is unchanged — scaffolds the empty-shell agent from cli/src/templates.ts.

With --template=<id> the CLI:

  1. Fetches https://releases.stech.com/templates/index.json.
  2. Resolves <id> against the catalog. Unknown id → exits 2 with the list of available ids.
  3. Checks min_cli_version on the entry. Older CLI → exits 2 with a run \stech update` first` hint. Doesn't silently fall back.
  4. Fetches <entry>.tarball_url (a .tar.gz under releases.stech.com/templates/<id>/).
  5. Verifies SHA-256 against the manifest's sha256 (constant-time compare). Mismatch = refuse to extract; the destination dir stays untouched.
  6. Extracts over the scaffolded dir. Template files overwrite base scaffold files where they collide (intentional — the template's real agent.ts replaces the empty one).
  7. Replaces Mustache-shaped {{agentName}} and {{orgSlug}} (cached from stech auth login) in every text file. Tokens with no value are left in place so you can fill them in by hand.
  8. Re-vendors the SDK into node_modules/@stech/agent/. This runs LAST so a stale template can't smuggle in an old SDK copy that mismatches what this CLI version exports.

Failure during the template phase rolls back the half-scaffolded dir before exiting, so the next stech init doesn't trip on a leftover ./<name>/.

Exit codes #

  • 0 — success.
  • 1 — extract / SHA / network failure (transient or recoverable).
  • 2 — usage error: missing name, invalid name, dir already exists, unknown template id, CLI too old for the template's min_cli_version.

Examples #

stech init my-bot                                 # empty scaffold (no --template)
stech init my-bot --template=customer-support     # working agent on first run
stech init bad --template=does-not-exist          # exit 2: lists available ids
stech init bad --template=customer-support        # exit 2 if your CLI < min_cli_version

Registry shape #

Templates are static artifacts — no DB schema, no API endpoint. The catalog is one JSON file plus one tarball per (template, version) on the same R2 bucket that hosts the CLI binaries.

R2 layout #

releases.stech.com/
  v0.17.4/                         # existing — pinned binary downloads
  latest/                          # existing — latest binary downloads
  templates/
    index.json                     # catalog manifest
    customer-support/
      v0.17.0.tar.gz               # one tarball per template per CLI version
      v0.17.4.tar.gz
      latest.tar.gz                # bytewise copy of the newest version's tarball
    code-reviewer/
      v0.17.0.tar.gz
      latest.tar.gz
    log-triage/
      v0.17.4.tar.gz
      latest.tar.gz
    knowledge-assistant/
      v0.17.4.tar.gz
      latest.tar.gz

index.json schema #

{
  "schema_version": 1,
  "templates": [
    {
      "id": "customer-support",
      "name": "Customer Support",
      "description": "RAG over a knowledge corpus + ticket triage. Wraps KnowledgeMemory.search and ships a ticket-create stub.",
      "latest_version": "v0.17.4",
      "min_cli_version": "v0.16.7",
      "tarball_url": "https://releases.stech.com/templates/customer-support/v0.17.4.tar.gz",
      "sha256": "<64-hex>"
    }
  ]
}

The CLI's loader (cli/src/runtime/template-fetch.ts::TemplateManifest) and the build script (cli/scripts/build-templates.ts::PublishedManifestEntry) keep this shape in lockstep — adding a field on one side without the other is the breakage shape here. The same schema feeds the marketing gallery at /templates (see web/lib/templates.ts).

Tarball shape #

Each tarball expands directly over the customer's project dir — the build script tars from inside the template dir (tar -czf … -C <dir> .) so the archive contains ./agent.ts rather than ./customer-support/agent.ts. No --strip-components=1 needed on the client side.

Contents of every template tarball:

  • agent.tsdefineAgent config.
  • tools.ts — tool implementations.
  • template.json — metadata; carried into the customer's project as a marker for future template-aware tooling (e.g. stech upgrade --from-template=...).
  • README.md — sample transcript + provider-wiring notes.
  • .gitignore — standard scaffold ignore list.

No node_modules/@stech/agent/ ever ships in a tarball — the CLI re-vendors the SDK after the extract step (per the locked order in cli/src/index.ts::init()). A template that bundled its own copy would be wiped out and is rejected by template-content.test.ts's parity check.

Verification posture #

Mirrors the same defenses web/public/install.sh applies to the binary downloads — the tarball is third-party-substitutable bytes, so the SHA is the trust anchor.

  • HTTPS-only fetch with redirect: "manual". A 3xx response is treated as a failure; an attacker who registers a public host that 302s to a private IP can't bounce the CLI there.
  • DNS-resolution-time SSRF guard. The hostname is resolved, and any RFC1918 / loopback / link-local / IPv6 ULA / IPv4-mapped IPv6 address is refused. Closes the DNS-rebinding gap a validate-time hostname check alone leaves open. Same routine as runtime/cli-bootstrap.ts for parity.
  • SHA-256 verified before extract (constant-time compare against the manifest entry). Mismatch = refuse to extract; the destination dir is untouched.
  • 30s fetch timeout via AbortController so a hung CDN doesn't hang stech init.
  • Path-traversal guard on extracted entries. After tar extracts into a stage dir, the loader walks the tree and refuses any symlink or any entry whose resolved path escapes the stage root. The SHA pin already prevents third-party substitution, but a first-party template author could accidentally ship ../ segments.

Versioning #

Per-CLI-version, with latest as a copy of the newest tagged tarball. latest.tar.gz is bytewise identical to the newest versioned tarball — a separate file rather than a symlink because R2 serves it through a CDN that doesn't resolve symlinks transparently.

The CLI doesn't fetch latest.tar.gz directly — it reads the manifest entry's tarball_url, which the build script always writes as the versioned URL. latest.tar.gz exists for curl users and for the build pipeline's atomic-upgrade story (write the versioned tarball first, then overwrite latest.tar.gz; readers who get an in-flight manifest see consistent bytes).

The reason for per-version: SDK churn is real — every PR-A/B/C/D in cli/src/sdk/index.ts's history has added new exports. A v0.10.x CLI fetching a template that uses KnowledgeMemory.search() (added v0.14.x) would crash at stech dev time with a confusing module-resolution error. min_cli_version is the gate that prevents that crash; per-version tarballs let us back-port a hotfix to an older template without touching the live latest for newer CLIs.

Authoring a new template #

Templates today are stech-curated only — every template appears on the public /templates gallery and is a public-facing endorsement, so quality + security review happens via PR before merge. Community contributions are deferred to v2 (likely as a separate marketplace surface, not the curated catalog).

To add one:

1. Scaffold the directory #

cli/templates/<id>/
  agent.ts
  tools.ts
  template.json
  README.md
  .gitignore

The dir name must match template.json::id exactly — the build script asserts this because the URL is built from the dir name, not the id, and a mismatch silently breaks fetch.

2. Write template.json #

{
  "id": "your-template-id",
  "name": "Display name for /templates page",
  "description": "One-paragraph summary; surfaced on the gallery card.",
  "model": "claude-sonnet-4-6",
  "tools": ["tool_name_1", "tool_name_2"],
  "min_cli_version": "v0.17.4"
}

Fields:

  • id — stable identifier used as --template=<id>. Must match the dir name.
  • name — display name for the marketing surface.
  • description — one-line summary, shown on the /templates gallery card. Keep it under ~120 chars; long descriptions get clipped.
  • model — recommended model for the template's agent.ts. Informational only — the user can change it after init.
  • tools — names of the tools the template registers. Informational; surfaced as chips on the gallery card.
  • min_cli_version — minimum CLI version that exports every SDK symbol the template's agent.ts / tools.ts imports. Bump this whenever the template starts using a newly-added export — older CLIs hitting the manifest exit cleanly with run \stech update` firstrather than crashing atbun run agent.ts` time.

3. Use placeholders sparingly #

The CLI substitutes Mustache-shaped tokens in every text file after extraction. The supported set is:

Token Source
{{agentName}} The <name> arg passed to stech init.
{{orgSlug}} Cached from stech auth login (otherwise left as-is).

That's it for v1. Region, env, and any other variable is hardcoded in the template until a user need surfaces. Optional whitespace inside the braces ({{ agentName }}) is tolerated; tokens with no entry in the substitution map are left in place so the user can fill them in.

Binary files (.png, .woff2, .zip, etc. — full list in template-fetch.ts::BINARY_EXTENSIONS) are skipped so a chance hex match doesn't corrupt them.

4. Don't bundle the SDK #

Templates must not ship node_modules/@stech/agent/ — the CLI re-vendors the SDK after the extract step. A bundled copy would be wiped out by step 3 of init(), and template-content.test.ts's parity check fails CI on any node_modules/ under cli/templates/.

Import only from @stech/agent and relative paths. Every named import must have a matching export in cli/src/templates.ts::sdkIndexTs() (the vendored SDK string-builder) — the parity check fails CI if not.

5. Write a README.md with a sample transcript #

Every existing template's README follows the same shape: what's in the box → first run → sample transcript → provider-wiring notes (for stub tools) → deploying. Match that shape so users get a consistent experience.

For user_oauth CLI tools (the code-reviewer template's gh source), make the README unambiguous that the org admin still has to register the source per the cli-tool-sources.md walkthrough, AND each member has to connect their credential at /settings/cli-credentials, before the template runs end-to-end. Otherwise users hit MissingUserCliCredentialError on first turn and bounce.

6. Build + verify locally #

bun run --filter '@stech/cli' build:templates

This invokes cli/scripts/build-templates.ts, which discovers every template, validates template.json, tar-czfs the contents, computes SHA-256, and emits cli/dist/templates/<id>/<version>.tar.gz plus cli/dist/templates/index.json. No upload — that's the release workflow's job. Smoke-test by extracting a tarball into a temp dir and checking the file list.

The version per template defaults to STECH_BUILD_VERSION (the release workflow sets this to $GITHUB_REF_NAME, e.g. v0.17.5). For local builds without the env set, the version is dev so the tarball lands at <id>/dev.tar.gz.

7. Open a PR against main #

The release workflow's publish-templates job (in .github/workflows/release.yml) runs on every v* tag, builds the manifest, and uploads to R2 alongside the binaries. A new template becomes fetchable by stech init --template=<id> only after the next tag ships.

Risks and gotchas #

  • Min-CLI-version drift. If you forget to bump min_cli_version when your template starts using a newly-added SDK export, older CLIs hitting the manifest will fetch successfully and crash at bun run agent.ts time with a confusing module-resolution error. The template-content.test.ts parity check catches most import mismatches at PR time; the runtime check is your seatbelt.
  • R2 catalog availability. If releases.stech.com/templates/index.json is unreachable (Cloudflare outage, DNS issue), stech init --template=... fails with a clear error and a 30s ceiling. Falls back cleanly — no hang.
  • Per-user OAuth credentials are NOT bundled. The code-reviewer template's gh source needs the admin to register it once and each member to connect once. The template's README must be unambiguous about both steps.
  • Template files OVERWRITE base scaffold files during extract. This is intentional (the template's real agent.ts replaces the empty one), but it means a template can clobber any of agent.ts, tools.ts, stech.config.ts, tsconfig.json, .gitignore, README.md. The SDK files written in step 3 of init() are outside this surface (different dir) and are safe.