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:
- Fetches
https://releases.stech.com/templates/index.json. - Resolves
<id>against the catalog. Unknown id → exits 2 with the list of available ids. - Checks
min_cli_versionon the entry. Older CLI → exits 2 with arun \stech update` first` hint. Doesn't silently fall back. - Fetches
<entry>.tarball_url(a.tar.gzunderreleases.stech.com/templates/<id>/). - Verifies SHA-256 against the manifest's
sha256(constant-time compare). Mismatch = refuse to extract; the destination dir stays untouched. - Extracts over the scaffolded dir. Template files overwrite base
scaffold files where they collide (intentional — the template's real
agent.tsreplaces the empty one). - Replaces Mustache-shaped
{{agentName}}and{{orgSlug}}(cached fromstech auth login) in every text file. Tokens with no value are left in place so you can fill them in by hand. - 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'smin_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_versionRegistry 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.gzindex.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.ts—defineAgentconfig.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.tsfor 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
AbortControllerso a hung CDN doesn't hangstech 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
.gitignoreThe 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/templatesgallery card. Keep it under ~120 chars; long descriptions get clipped.model— recommended model for the template'sagent.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'sagent.ts/tools.tsimports. Bump this whenever the template starts using a newly-added export — older CLIs hitting the manifest exit cleanly withrun \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:templatesThis 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_versionwhen your template starts using a newly-added SDK export, older CLIs hitting the manifest will fetch successfully and crash atbun run agent.tstime with a confusing module-resolution error. Thetemplate-content.test.tsparity check catches most import mismatches at PR time; the runtime check is your seatbelt. - R2 catalog availability. If
releases.stech.com/templates/index.jsonis 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-reviewertemplate'sghsource 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.tsreplaces the empty one), but it means a template can clobber any ofagent.ts,tools.ts,stech.config.ts,tsconfig.json,.gitignore,README.md. The SDK files written in step 3 ofinit()are outside this surface (different dir) and are safe.
Related #
- CLI tool sources —
code-reviewerlifts itsghrecipe from the Walk-through section. cli/src/runtime/template-fetch.ts— the fetch + verify + extract loader.cli/scripts/build-templates.ts— the build script.cli/templates/— the curated catalog.web/lib/templates.ts— the marketing gallery's manifest fetcher (mirrors the CLI's schema).