create-stitchkit 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,84 @@ step is overwritten by the next release.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.5.0] — 2026-09-01
16
+
17
+ ### ⚠️ Breaking changes
18
+
19
+ **Who must act:** two audiences. Anyone holding a generated **Agent** project
20
+ changes one key in one object, and nothing reports it when it goes wrong.
21
+ **Everyone** re-reads the Zod item: an API that accepted a timestamp without
22
+ seconds stops accepting it, which is a change in what your endpoints admit, not
23
+ in what your code compiles to.
24
+
25
+ - **The Agent approval policy keys `edit_file`, not `apply_patch`.** Stitchkit
26
+ 0.71.0 replaced `apply_patch` with a one-call `edit_file`, and `toolApproval`
27
+ names tools by string. A key matching no tool is not an error — an unlisted
28
+ tool falls through to "no approval required", so the effect of leaving the old
29
+ name is not that editing breaks. It is that editing stops asking: the one tool
30
+ the policy existed to gate now runs unattended, and the only visible sign is an
31
+ approval prompt that never appears.
32
+ `// before: toolApproval: { apply_patch: 'user-approval' }` →
33
+ `// after: toolApproval: { edit_file: 'user-approval' }`
34
+
35
+ - **Zod 4.5 makes seconds mandatory in `z.iso.datetime()`.** The template moves
36
+ to `zod@4.5.4`, and the tightening travels with it: `2026-08-08T00:15Z`
37
+ validated before and is refused now. Nothing in the generated code changes —
38
+ what changes is the set of inputs your API accepts, so a client that omitted
39
+ seconds starts receiving `BAD_REQUEST`. If you need the old latitude, say so
40
+ in the schema rather than by holding the version back:
41
+ `// before: z.iso.datetime()` → `// after: z.iso.datetime() // seconds now required`
42
+ The same release regenerates the committed **surface snapshots**: 4.5 encodes
43
+ a nullable as `type: ['string','null']` where 4.4 wrote `anyOf`, so every
44
+ affected shape fingerprint moves. A project of your own carrying
45
+ `surface.snapshot.json` regenerates it with `bun run surface:snapshot` and
46
+ reviews the diff — the hashes move, the operations do not.
47
+
48
+ ### Changed
49
+
50
+ - **The generated project targets the published `stitchkit@0.71.0` line.** The
51
+ catalog target and the frozen lockfile move together, so a fresh scaffold
52
+ receives `edit_file`, `list_directory`, `glob`, the typed coding-tool refusals
53
+ a model can act on, the context usage a step reports, and the peer-free
54
+ `stitchkit/telegram` leaf.
55
+ - **The whole toolchain moves to its current releases** — Zod 4.5.4 (see the
56
+ breaking note above), Biome 2.5.11, `@types/node` 26.4.0, and `ai` 7.0.87 in
57
+ the Agent template. One Zod resolves across the repository and both templates,
58
+ which is now a gate rather than a coincidence: two minors of Zod are two
59
+ incompatible type systems, and the error that surfaces names neither Zod nor
60
+ the file that moved it.
61
+ - **The Agent approval policy is exhaustive again.** 0.71.0 added
62
+ `list_directory` and `glob`; both are read-only and both were therefore
63
+ running under the framework's default rather than under the project's own
64
+ policy. They are now named `'approved'` beside `read_file` and `search_files`.
65
+ Nothing about what happens changes — what changes is that the file says so,
66
+ which is the whole reason the map is written out rather than defaulted.
67
+
68
+ ## [0.4.4] — 2026-08-30
69
+
70
+ ### Changed
71
+
72
+ - **The generated project now targets the published `stitchkit@0.70.1` line and includes the
73
+ maintained `stitchkit-tui` Agent profile.** The catalog and frozen lockfile move together, so a
74
+ fresh scaffold receives the macOS contained-file fix, bounded realtime shutdown and the terminal
75
+ harness package already validated by the same release train.
76
+
77
+ ### Added
78
+
79
+ - **An explicit Agent template opens a durable OpenRouter coding session in the terminal.**
80
+ `bun create stitchkit my-agent --template agent` generates an OpenTUI host over the published
81
+ headless harness, Bun SQLite storage, lazy skills, direct coding tools, signed approvals and
82
+ recovery. The default production application and its repository overlay are unchanged. The
83
+ Agent development source follows framework HEAD locally, while generated projects receive the
84
+ application starter's one canonical Stitchkit catalog target. Startup requires only an API key:
85
+ a bounded picker reads the current popular tool-capable models and their provider-owned context
86
+ windows directly from OpenRouter. → ADR 0132.
87
+ - **The Agent template now composes the official terminal package instead of copying a product
88
+ shell.** `stitchkit.agent.ts` is the editable typed entrypoint; `/model`, durable sessions,
89
+ approvals, scrolling and local `send`/`interrupt` attachment come from `stitchkit-tui`. The
90
+ generated manifest receives portable catalog dependencies while the repository fixture tests
91
+ both packages from source. → ADR 0133.
92
+
15
93
  ## [0.4.3] — 2026-08-28
16
94
 
17
95
  ### Fixed
package/README.md CHANGED
@@ -13,6 +13,25 @@ The generated Bun workspace contains a Next.js frontend, a separate Stitchkit
13
13
  API, Prisma/PostgreSQL, typed shared contracts, Socket.IO, MCP, CLI tools and a
14
14
  complete production UI system.
15
15
 
16
+ To start from a terminal coding agent instead:
17
+
18
+ ```bash
19
+ bun create stitchkit my-agent --template agent
20
+ cd my-agent
21
+ cp .env.example .env
22
+ # Set OPENROUTER_API_KEY, then choose a live model in the terminal.
23
+ bun run dev
24
+ ```
25
+
26
+ This opt-in profile is deliberately smaller. It composes the published headless
27
+ harness, Bun SQLite store, OpenRouter adapter, lazy filesystem skills and direct
28
+ coding tools behind an OpenTUI shell. Reads/search run directly; writes, edits,
29
+ patches and shell calls require a signed durable `Y`/`N` approval. `bun --watch`
30
+ restarts source while the ignored `.stitchkit/` directory retains conversation
31
+ and recovery state. The bounded startup picker reads current tool-capable models and their context
32
+ windows from OpenRouter instead of duplicating provider metadata in environment variables. The
33
+ workspace path is a containment boundary, not an OS sandbox.
34
+
16
35
  It uses one conventional `packages/*` namespace: `backend`, `frontend`,
17
36
  `config`, `db` and `shared`. The destination name becomes the generated slug;
18
37
  `--display-name` sets the human title. Both are recorded once in
package/UPGRADING.md CHANGED
@@ -60,6 +60,71 @@ the first scaffolder release with a migration channel of its own.
60
60
 
61
61
  ---
62
62
 
63
+ ## Released migration: 0.5.0
64
+
65
+ ### the approval policy names a tool that no longer exists
66
+
67
+ One code edit, and one thing to do to a machine that was left mid-question.
68
+
69
+ 1. **Rename the key.** In `src/runtime.ts`, inside `loop.toolApproval`, replace
70
+ `apply_patch: 'user-approval'` with `edit_file: 'user-approval'`. While you
71
+ are in that object, add `list_directory: 'approved'` and `glob: 'approved'`
72
+ beside `read_file` and `search_files` — 0.71.0 added both, they are read-only,
73
+ and an unlisted tool is governed by the framework's default rather than by
74
+ this file.
75
+
76
+ Do this **before** you start the upgraded agent, not after. The failure mode
77
+ is silent in the direction that costs you: a key matching no tool does not
78
+ raise, it simply stops gating, so the first edit after the upgrade is applied
79
+ without asking and looks exactly like an edit you approved.
80
+
81
+ 2. **Operator step — a durable session left waiting on the old tool cannot be
82
+ answered.** If an agent was interrupted while a `apply_patch` approval was
83
+ pending, that approval names a call for a tool the framework no longer
84
+ defines: approving it cannot execute anything, and the run will not move.
85
+ Interrupt that run, or drop the local state directory — `.stitchkit/` holds
86
+ `agent.sqlite`, the approval secret and the TUI logs, all of it local and
87
+ regenerated on the next start. There is nothing durable in it that a server
88
+ owns.
89
+
90
+ A session with no pending approval needs none of this and resumes normally.
91
+
92
+ 3. **The framework's own move is separate.** Going from a `0.70.x` line to
93
+ `0.71.0` is a Stitchkit upgrade with its own breaking notes — the coding-tool
94
+ surface changed for anyone calling it directly, not only through this
95
+ template. Read
96
+ [`docs/guide/upgrading.md`](../../docs/guide/upgrading.md), section
97
+ `Released migration: 0.71.0`.
98
+
99
+ ### the timestamp your API accepts got narrower
100
+
101
+ Zod moved to 4.5, and `z.iso.datetime()` now requires seconds. This is not a
102
+ code change — it is a change in what your endpoints admit, and it arrives the
103
+ moment you install.
104
+
105
+ 1. **Decide before you deploy, not after.** If any client sends
106
+ `2026-08-08T00:15Z`, it now receives `BAD_REQUEST` where it used to receive
107
+ `200`. Search your schemas for `z.iso.datetime()` and check who fills those
108
+ fields. A machine-to-machine caller you control is a one-line fix on its
109
+ side; a caller you do not control is a decision, and the honest form of it is
110
+ an explicit schema that accepts what you mean to accept — not a pinned Zod
111
+ version, which only moves the same day to a later one.
112
+
113
+ 2. **Regenerate your surface snapshot in the same commit.** 4.5 encodes a
114
+ nullable as `type: ["string","null"]` where 4.4 wrote `anyOf`, so the shape
115
+ fingerprints in `packages/backend/src/surface.snapshot.json` move without any
116
+ operation changing. Run `bun run surface:snapshot` and read the diff: only
117
+ `inputShape` / `outputShape` values may differ. If an operation appeared,
118
+ vanished, or changed its HTTP or tool exposure, that is **your** change and
119
+ Zod did not cause it.
120
+
121
+ 3. **Move one Zod, not several.** If your project vendors or links packages that
122
+ depend on Zod themselves, they must resolve the same minor. Two minors are
123
+ two incompatible type systems, and what surfaces is
124
+ `TS2589: Type instantiation is excessively deep` in a file you did not touch.
125
+ No operator step: nothing about a running machine changes.
126
+
127
+
63
128
  ## Released migration: 0.4.1
64
129
 
65
130
  ### a release refuses a stale artifact, and cleanup is bounded
package/dist/cli.js CHANGED
@@ -207,10 +207,11 @@ function withIdentity(declaration, identity) {
207
207
  var HELP = `Create a production-shaped Stitchkit application.
208
208
 
209
209
  Usage:
210
- bun create stitchkit <directory> [--display-name "Product Name"] [--example repository] [--no-install]
210
+ bun create stitchkit <directory> [--template application|agent] [--display-name "Product Name"] [--example repository] [--no-install]
211
211
 
212
212
  Options:
213
213
  --no-install Generate files without installing dependencies
214
+ --template Select the project shape (default: application)
214
215
  --example Add an isolated runnable example (supported: repository)
215
216
  --display-name Set the initial public application name
216
217
  --help Show this help
@@ -221,7 +222,7 @@ function helpText() {
221
222
  function parseOptions(args) {
222
223
  if (args.includes("--help") || args.includes("-h"))
223
224
  return "help";
224
- const unknown = args.filter((arg) => arg.startsWith("-") && arg !== "--no-install" && arg !== "--example" && arg !== "--display-name");
225
+ const unknown = args.filter((arg) => arg.startsWith("-") && arg !== "--no-install" && arg !== "--template" && arg !== "--example" && arg !== "--display-name");
225
226
  if (unknown.length > 0) {
226
227
  throw new Error(`Unknown option: ${unknown[0]}`);
227
228
  }
@@ -233,14 +234,26 @@ function parseOptions(args) {
233
234
  if (example !== undefined && example !== "repository") {
234
235
  throw new Error(`Unknown example: ${example}`);
235
236
  }
237
+ const templateFlagIndex = args.indexOf("--template");
238
+ const template = templateFlagIndex === -1 ? "application" : args[templateFlagIndex + 1];
239
+ if (templateFlagIndex !== -1 && template === undefined) {
240
+ throw new Error("--template requires a value");
241
+ }
242
+ if (template !== "application" && template !== "agent") {
243
+ throw new Error(`Unknown template: ${template}`);
244
+ }
245
+ if (template === "agent" && example !== undefined) {
246
+ throw new Error("--example is only supported by the application template");
247
+ }
236
248
  const displayNameFlagIndex = args.indexOf("--display-name");
237
249
  const displayName = displayNameFlagIndex === -1 ? undefined : args[displayNameFlagIndex + 1];
238
250
  if (displayNameFlagIndex !== -1 && displayName === undefined) {
239
251
  throw new Error("--display-name requires a value");
240
252
  }
241
253
  const consumedExampleIndex = exampleFlagIndex === -1 ? -1 : exampleFlagIndex + 1;
254
+ const consumedTemplateIndex = templateFlagIndex === -1 ? -1 : templateFlagIndex + 1;
242
255
  const consumedDisplayNameIndex = displayNameFlagIndex === -1 ? -1 : displayNameFlagIndex + 1;
243
- const positionals = args.filter((arg, index) => !arg.startsWith("-") && index !== consumedExampleIndex && index !== consumedDisplayNameIndex);
256
+ const positionals = args.filter((arg, index) => !arg.startsWith("-") && index !== consumedExampleIndex && index !== consumedTemplateIndex && index !== consumedDisplayNameIndex);
244
257
  if (positionals.length !== 1) {
245
258
  throw new Error("Exactly one destination directory is required");
246
259
  }
@@ -250,6 +263,7 @@ function parseOptions(args) {
250
263
  return {
251
264
  destination,
252
265
  install: !args.includes("--no-install"),
266
+ template,
253
267
  ...example && { example },
254
268
  ...displayName && { displayName }
255
269
  };
@@ -283,9 +297,15 @@ var TEMPLATE_RENAMES = new Map([
283
297
  ["_env.example.append", ".env.example"],
284
298
  ["_gitignore", ".gitignore"]
285
299
  ]);
286
- var RootManifestSchema = z2.looseObject({ name: z2.string().min(1) });
300
+ var RootManifestSchema = z2.looseObject({
301
+ name: z2.string().min(1),
302
+ catalog: z2.record(z2.string(), z2.string()).optional(),
303
+ dependencies: z2.record(z2.string(), z2.string()).optional(),
304
+ devDependencies: z2.record(z2.string(), z2.string()).optional()
305
+ });
287
306
  var IGNORED_DIRECTORIES = new Set([
288
307
  ".next",
308
+ ".stitchkit",
289
309
  "coverage",
290
310
  "dist",
291
311
  "node_modules",
@@ -401,16 +421,43 @@ async function scaffoldProject(templateDirectory, destination, options = {}) {
401
421
  if (options.overlayDirectory) {
402
422
  await writeMaterialisedFiles(resolvedDestination, await materialiseTemplateFiles(options.overlayDirectory));
403
423
  }
424
+ if (options.lockfile === false) {
425
+ await rm(join(resolvedDestination, "bun.lock"), { force: true });
426
+ }
404
427
  const declarationPath = join(resolvedDestination, "project.json");
405
428
  const declaration = withIdentity(JSON.parse(await readFile(declarationPath, "utf8")), identity);
406
429
  await writeFile(declarationPath, `${JSON.stringify(declaration, undefined, 2)}
407
430
  `);
408
- const identityPath = join(resolvedDestination, APP_IDENTITY_PATH);
409
- await mkdir(dirname(identityPath), { recursive: true });
410
- await writeFile(identityPath, renderAppIdentityModule(declaration.identity));
431
+ if (options.identityModule !== false) {
432
+ const identityPath = join(resolvedDestination, APP_IDENTITY_PATH);
433
+ await mkdir(dirname(identityPath), { recursive: true });
434
+ await writeFile(identityPath, renderAppIdentityModule(declaration.identity));
435
+ }
411
436
  const manifestPath = join(resolvedDestination, "package.json");
412
437
  const manifest = RootManifestSchema.parse(JSON.parse(await readFile(manifestPath, "utf8")));
413
- await writeFile(manifestPath, `${JSON.stringify({ ...manifest, name: identity.slug }, undefined, 2)}
438
+ const catalog = options.stitchkitCatalogTarget ? { ...manifest.catalog ?? {}, stitchkit: options.stitchkitCatalogTarget } : manifest.catalog;
439
+ const replaceLocalPackages = (dependencies) => {
440
+ if (!dependencies)
441
+ return dependencies;
442
+ return {
443
+ ...dependencies,
444
+ ...dependencies.stitchkit?.startsWith("file:") && { stitchkit: "catalog:" },
445
+ ...dependencies["stitchkit-tui"]?.startsWith("file:") && {
446
+ "stitchkit-tui": "catalog:"
447
+ }
448
+ };
449
+ };
450
+ await writeFile(manifestPath, `${JSON.stringify({
451
+ ...manifest,
452
+ name: identity.slug,
453
+ ...catalog && { catalog },
454
+ ...manifest.dependencies && {
455
+ dependencies: replaceLocalPackages(manifest.dependencies)
456
+ },
457
+ ...manifest.devDependencies && {
458
+ devDependencies: replaceLocalPackages(manifest.devDependencies)
459
+ }
460
+ }, undefined, 2)}
414
461
  `);
415
462
  } catch (error) {
416
463
  if (!destinationExisted) {
@@ -432,11 +479,17 @@ async function run(args) {
432
479
  return 0;
433
480
  }
434
481
  const destination = resolve2(options.destination);
435
- const templateDirectory = resolve2(import.meta.dir, "../template");
482
+ const applicationTemplateDirectory = resolve2(import.meta.dir, "../template");
483
+ const templateDirectory = options.template === "agent" ? resolve2(import.meta.dir, "../templates/agent") : applicationTemplateDirectory;
436
484
  const overlayDirectory = options.example ? resolve2(import.meta.dir, `../examples/${options.example}`) : undefined;
437
485
  await scaffoldProject(templateDirectory, destination, {
438
486
  ...overlayDirectory && { overlayDirectory },
439
- ...options.displayName && { displayName: options.displayName }
487
+ ...options.displayName && { displayName: options.displayName },
488
+ ...options.template === "agent" && {
489
+ identityModule: false,
490
+ lockfile: false,
491
+ stitchkitCatalogTarget: await readStitchkitCatalogTarget(applicationTemplateDirectory)
492
+ }
440
493
  });
441
494
  if (options.install) {
442
495
  const install = spawn(["bun", "install"], {
@@ -449,7 +502,7 @@ async function run(args) {
449
502
  if (exitCode !== 0)
450
503
  throw new Error(`bun install failed with exit code ${exitCode}`);
451
504
  }
452
- const mode = options.example ? ` with the ${options.example} example` : "";
505
+ const mode = options.template === "agent" ? " from the agent template" : options.example ? ` with the ${options.example} example` : "";
453
506
  process.stdout.write(`
454
507
  Created ${options.displayName ?? basename3(destination)}${mode}
455
508
 
@@ -471,6 +524,13 @@ Created ${options.displayName ?? basename3(destination)}${mode}
471
524
  return 1;
472
525
  }
473
526
  }
527
+ async function readStitchkitCatalogTarget(templateDirectory) {
528
+ const manifest = JSON.parse(await readFile2(join2(templateDirectory, "package.json"), "utf8"));
529
+ if (typeof manifest !== "object" || manifest === null || !("catalog" in manifest) || typeof manifest.catalog !== "object" || manifest.catalog === null || !("stitchkit" in manifest.catalog) || typeof manifest.catalog.stitchkit !== "string") {
530
+ throw new Error("Application template is missing catalog.stitchkit");
531
+ }
532
+ return manifest.catalog.stitchkit;
533
+ }
474
534
  if (import.meta.main) {
475
535
  process.exitCode = await run(Bun.argv.slice(2));
476
536
  }
@@ -6,7 +6,7 @@
6
6
  "hasInput": false,
7
7
  "hasOutput": true,
8
8
  "inputShape": null,
9
- "outputShape": "49c1f81a53ca82ad",
9
+ "outputShape": "05d1386c30d82f6a",
10
10
  "http": [
11
11
  {
12
12
  "method": "GET",
@@ -26,7 +26,7 @@
26
26
  "hasInput": false,
27
27
  "hasOutput": true,
28
28
  "inputShape": null,
29
- "outputShape": "49c1f81a53ca82ad",
29
+ "outputShape": "05d1386c30d82f6a",
30
30
  "http": [
31
31
  {
32
32
  "method": "POST",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-stitchkit",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Create a production-shaped Stitchkit application",
5
5
  "license": "MIT",
6
6
  "author": "Max Listov <maxlistov@gmail.com>",
@@ -16,11 +16,13 @@
16
16
  "files": [
17
17
  "dist",
18
18
  "template/**/*",
19
+ "templates/**/*",
19
20
  "examples/**/*",
20
21
  "!template/**/.env",
21
22
  "!template/**/.build-stamp.json",
22
23
  "!template/**/node_modules/**",
23
24
  "!template/**/.next/**",
25
+ "!template/**/.stitchkit/**",
24
26
  "!template/**/dist/**",
25
27
  "!template/**/playwright-report/**",
26
28
  "!template/**/test-results/**",
@@ -29,10 +31,25 @@
29
31
  "!template/**/*.log",
30
32
  "!template/**/*.tsbuildinfo",
31
33
  "!template/**/coverage/**",
34
+ "!templates/**/.env",
35
+ "!templates/**/.build-stamp.json",
36
+ "!templates/**/node_modules/**",
37
+ "!templates/**/.next/**",
38
+ "!templates/**/.stitchkit/**",
39
+ "!templates/**/dist/**",
40
+ "!templates/**/playwright-report/**",
41
+ "!templates/**/test-results/**",
42
+ "!templates/**/next-env.d.ts",
43
+ "!templates/**/src/generated/**",
44
+ "!templates/**/*.log",
45
+ "!templates/**/*.tsbuildinfo",
46
+ "!templates/**/coverage/**",
47
+ "!templates/agent/bun.lock",
32
48
  "!examples/**/.env",
33
49
  "!examples/**/.build-stamp.json",
34
50
  "!examples/**/node_modules/**",
35
51
  "!examples/**/.next/**",
52
+ "!examples/**/.stitchkit/**",
36
53
  "!examples/**/dist/**",
37
54
  "!examples/**/coverage/**",
38
55
  "!examples/**/playwright-report/**",
@@ -59,7 +76,14 @@
59
76
  "zod": "^4.4.3"
60
77
  },
61
78
  "devDependencies": {
79
+ "@opentui/core": "^0.5.9",
80
+ "@opentui/react": "^0.5.9",
81
+ "@openrouter/ai-sdk-provider": "^3.0.0",
62
82
  "@types/bun": "^1.4.0",
83
+ "@types/react": "^19.2.18",
84
+ "ai": "^7.0.84",
85
+ "react": "^19.2.8",
86
+ "stitchkit": "0.71.0",
63
87
  "typescript": "^7.0.2"
64
88
  },
65
89
  "engines": {
package/template/bun.lock CHANGED
@@ -11,16 +11,16 @@
11
11
  "@app/config": "workspace:*",
12
12
  "@app/shared": "workspace:*",
13
13
  "@axe-core/playwright": "^4.13.0",
14
- "@biomejs/biome": "^2.5.10",
14
+ "@biomejs/biome": "^2.5.11",
15
15
  "@modelcontextprotocol/client": "^2.0.0",
16
16
  "@playwright/test": "^1.62.1",
17
17
  "@types/bun": "^1.4.0",
18
- "@types/node": "^26.3.0",
18
+ "@types/node": "^26.4.0",
19
19
  "oxc-parser": "^0.147.0",
20
20
  "socket.io-client": "^4.8.3",
21
21
  "stitchkit": "catalog:",
22
22
  "typescript": "^7.0.2",
23
- "zod": "^4.4.3",
23
+ "zod": "^4.5.4",
24
24
  },
25
25
  },
26
26
  "packages/backend": {
@@ -139,7 +139,7 @@
139
139
  },
140
140
  },
141
141
  "catalog": {
142
- "stitchkit": "^0.68.6",
142
+ "stitchkit": "^0.71.0",
143
143
  },
144
144
  "packages": {
145
145
  "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.63", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@ai-sdk/provider-utils": "5.0.29", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-D7BogSRg61QfTdr7AEcYn9h0I/e4QHvFXwIV1RW+DZZGJu1wSiX2cH06szZSYyKi7Eat50V4s4J8vggZUEs7eg=="],
@@ -168,23 +168,23 @@
168
168
 
169
169
  "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
170
170
 
171
- "@biomejs/biome": ["@biomejs/biome@2.5.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.10", "@biomejs/cli-darwin-x64": "2.5.10", "@biomejs/cli-linux-arm64": "2.5.10", "@biomejs/cli-linux-arm64-musl": "2.5.10", "@biomejs/cli-linux-x64": "2.5.10", "@biomejs/cli-linux-x64-musl": "2.5.10", "@biomejs/cli-win32-arm64": "2.5.10", "@biomejs/cli-win32-x64": "2.5.10" }, "bin": { "biome": "bin/biome" } }, "sha512-WRKXARA3kTuiV5sxqTpobJ/I0MVd4vk3pOL6wnp5az4LntFIhWTj1RWZq3DI9PCEN3lXcqy7p5aqUHzvq8AXyQ=="],
171
+ "@biomejs/biome": ["@biomejs/biome@2.5.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.11", "@biomejs/cli-darwin-x64": "2.5.11", "@biomejs/cli-linux-arm64": "2.5.11", "@biomejs/cli-linux-arm64-musl": "2.5.11", "@biomejs/cli-linux-x64": "2.5.11", "@biomejs/cli-linux-x64-musl": "2.5.11", "@biomejs/cli-win32-arm64": "2.5.11", "@biomejs/cli-win32-x64": "2.5.11" }, "bin": { "biome": "bin/biome" } }, "sha512-Tj0dnkLPdW0ASjHfj2D/ZkkvPU2wrFmnE1jWTD2xzV1ycapV1DutbYXk4NDnR3rYTi1ZCbNFD4G2gRMEY65WaA=="],
172
172
 
173
- "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ItCrxKK6SXVT6flYs0qIuBd4AA3TTTl4d66Re6YI2FuGZnN85NmuYNzkiTJUyYw8qBLv69L5zTUB6uyWd++h3Q=="],
173
+ "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6SGZxoKbXvUjMn1t6A98HqWISPnGNbYs0R/Rt2JarmXBSev+lva4QxUMWEBX9lX1Wo1XTJ78uk5xVDtG58SRZg=="],
174
174
 
175
- "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-yLsPU9pAmtChXDu8vhKAzErqe+LeeYuwuUB2FZMkRitsmdodxsYRa9KHrFispsUHzzOu+9HB3nP/TQxyia+Sjw=="],
175
+ "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-nYkXY7tLBEgnGbYapDKAyKzgt44ZEyG+AKalvTXtCWKYgepI9dw327q+cVgedxm+Udi1ZzHKUyZrIusHi/KQbw=="],
176
176
 
177
- "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-VG8uQW/86a1roLaIFvtIbEigxIdzdJ190oGyg1tV7VYeQtOS+x10sflk7WbuXgw91EtZX5DlIIIej1YqkNLlcg=="],
177
+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-3PVLSTD9RR73rvVPt5G3T1gc+ycggWEGfTD7RvzzbtcDPD27NxgxBbAFfpm7DXJKW6VLHWE1lLMGvFt2Qxjcow=="],
178
178
 
179
- "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-t1QAKZwQJRB4dvgJSgFiQ4BNfNPChg69BNonz854qLVxnjT3UvDzQg9mbkTJRu35ZqU0Rw10A73J8Urgbg2RPw=="],
179
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-qhyZUMyCbWYFV2bAwRNVvfMVZ+hv7WYl6mossGrxC+uiQQXhvsuWWU8zz6jYX0mChZd9MgQZbm4vozTmG/5iGw=="],
180
180
 
181
- "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-4O6T0eq2heoHZN0a9UX+rWQoxXEBaKf+lRi2hbsGlHneUz9BWXM76nEWMK7Eeq8gzMxR1khQB6BFpAASpeXqGg=="],
181
+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-JOytptlsgM33B2MMFUg8iBrb4IKpbD5JnJrSeYiaFEeAj4vuXx0iQSQZ4qK7sqyMtfjZxxPdNdMZZVL4y/mFyA=="],
182
182
 
183
- "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-pgDDqp9JybHm2I0KRgzN6i4+lt8xu4iqxUwLzglUMmOmyRTU1AYBGKzh9sNMOtIjah7xoWvKHlLVetvyifzoiQ=="],
183
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-oRRlrchG5EfrEL/EmtT1qUjSNHk3/5LGeZhQqADBBAJF1b1ET6964xEKe7aGlGARzDfza8H/seEsFJl7S6Ql9w=="],
184
184
 
185
- "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-pxAbxduPO4xq/Cvgaa2lOrs9BB0hEXmmDqfMNP4ZOffGOkUrD1/QGw9UAMpFQpX2P8MqTIIRuQKcmetum4Oa6A=="],
185
+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-e49E6K9hzH/ohJNx8Y26mY8HaV4I4ZViIeoqhKsmoXLKHhQnMeBAVqCgsGf2Wa3lXlS7RkporDXMHHWkzvZzFw=="],
186
186
 
187
- "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-M+2dgBsl3lXRiTfgPVc2p3anS4Tocojke4rzFLScZ2Y/wmF+36dRb1iHCLiyGqOzQGyTplZH1HnEYviiAqi3nA=="],
187
+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.11", "", { "os": "win32", "cpu": "x64" }, "sha512-QSQr/KjOgXA7OzXJUWS+oguKyAZ3Q0l/lnlDGbu397eKo83atuWUjBPJrsqbKNF6CARGw8XXJLGzpHC8Ryhd4Q=="],
188
188
 
189
189
  "@electric-sql/pglite": ["@electric-sql/pglite@0.4.3", "", {}, "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ=="],
190
190
 
@@ -620,7 +620,7 @@
620
620
 
621
621
  "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
622
622
 
623
- "@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="],
623
+ "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
624
624
 
625
625
  "@types/pg": ["@types/pg@8.23.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A=="],
626
626
 
@@ -1118,7 +1118,7 @@
1118
1118
 
1119
1119
  "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
1120
1120
 
1121
- "stitchkit": ["stitchkit@0.68.6", "", { "dependencies": { "ky": "^2.0.2" }, "peerDependencies": { "@modelcontextprotocol/ext-apps": "^1.7.2", "@modelcontextprotocol/server": "^2.0.0", "@openrouter/ai-sdk-provider": "^3.0.0", "@opentelemetry/api": "^1.9.0", "@socket.io/bun-engine": "^0.1.1", "@socket.io/component-emitter": "^3.1.2", "@tanstack/react-query": ">=5", "@types/bun": "^1.3.14", "ai": "^7.0.0", "grammy": "^1.45.1", "react": ">=18", "react-query-kit": "^3.3.3", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "srvx": "^0.12.5", "zod": "^4.4.3" }, "optionalPeers": ["@modelcontextprotocol/ext-apps", "@modelcontextprotocol/server", "@openrouter/ai-sdk-provider", "@opentelemetry/api", "@socket.io/bun-engine", "@socket.io/component-emitter", "@tanstack/react-query", "@types/bun", "ai", "grammy", "react", "react-query-kit", "socket.io", "socket.io-client", "srvx"] }, "sha512-fnniuyge5JEN55TVdI3qH+nN1XyGJ6MglwOraWeOk6FkYs7mv6ZZyEo1kkFVT1bcsYj4HZW7KJCiKwnsjDoyfw=="],
1121
+ "stitchkit": ["stitchkit@0.71.0", "", { "dependencies": { "ky": "^2.0.2" }, "peerDependencies": { "@modelcontextprotocol/ext-apps": "^1.7.2", "@modelcontextprotocol/server": "^2.0.0", "@openrouter/ai-sdk-provider": "^3.0.0", "@opentelemetry/api": "^1.9.0", "@socket.io/bun-engine": "^0.1.1", "@socket.io/component-emitter": "^3.1.2", "@tanstack/react-query": ">=5", "@types/bun": "^1.3.14", "ai": "^7.0.0", "grammy": "^1.45.1", "react": ">=18", "react-query-kit": "^3.3.3", "socket.io": "^4.8.3", "socket.io-client": "^4.8.3", "srvx": "^0.12.5", "zod": "^4.4.3" }, "optionalPeers": ["@modelcontextprotocol/ext-apps", "@modelcontextprotocol/server", "@openrouter/ai-sdk-provider", "@opentelemetry/api", "@socket.io/bun-engine", "@socket.io/component-emitter", "@tanstack/react-query", "@types/bun", "ai", "grammy", "react", "react-query-kit", "socket.io", "socket.io-client", "srvx"] }, "sha512-HXqTD1Sv534rWt+KKJV1Gp535NTRzbGFxNMuRAvo9TzuU0kZxBDF18gu+WexF9O8Z4jkhlfvTjXeRy/oQrnHwA=="],
1122
1122
 
1123
1123
  "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
1124
1124
 
@@ -1186,10 +1186,13 @@
1186
1186
 
1187
1187
  "zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="],
1188
1188
 
1189
- "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
1189
+ "zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="],
1190
1190
 
1191
1191
  "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
1192
1192
 
1193
+
1194
+
1195
+
1193
1196
  "@prisma/adapter-pg/@types/pg": ["@types/pg@8.21.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-AYdtudzabjLZgVgRZmAnU8bAnVUXzuJX2IYHeSIiIHm68olD+LgQYCGWdtcNYnP0uq9c4S4NibVG3Ni7VbKW7Q=="],
1194
1197
 
1195
1198
  "@prisma/adapter-pg/pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="],
@@ -1220,6 +1223,8 @@
1220
1223
 
1221
1224
  "@types/cors/@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
1222
1225
 
1226
+ "@types/pg/@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="],
1227
+
1223
1228
  "@types/ws/@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
1224
1229
 
1225
1230
  "@visx/vendor/@types/d3-array": ["@types/d3-array@3.0.3", "", {}, "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ=="],
@@ -1236,6 +1241,8 @@
1236
1241
 
1237
1242
  "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
1238
1243
 
1244
+ "bun-types/@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="],
1245
+
1239
1246
  "engine.io/@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
1240
1247
 
1241
1248
  "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
@@ -7,7 +7,7 @@
7
7
  "packages/*"
8
8
  ],
9
9
  "catalog": {
10
- "stitchkit": "^0.68.6"
10
+ "stitchkit": "^0.71.0"
11
11
  },
12
12
  "scripts": {
13
13
  "dev": "bun scripts/dev.ts",
@@ -42,16 +42,16 @@
42
42
  "@app/config": "workspace:*",
43
43
  "@app/shared": "workspace:*",
44
44
  "@axe-core/playwright": "^4.13.0",
45
- "@biomejs/biome": "^2.5.10",
45
+ "@biomejs/biome": "^2.5.11",
46
46
  "@modelcontextprotocol/client": "^2.0.0",
47
47
  "@playwright/test": "^1.62.1",
48
48
  "@types/bun": "^1.4.0",
49
- "@types/node": "^26.3.0",
49
+ "@types/node": "^26.4.0",
50
50
  "oxc-parser": "^0.147.0",
51
51
  "socket.io-client": "^4.8.3",
52
52
  "stitchkit": "catalog:",
53
53
  "typescript": "^7.0.2",
54
- "zod": "^4.4.3"
54
+ "zod": "^4.5.4"
55
55
  },
56
56
  "engines": {
57
57
  "bun": ">=1.2.0",
@@ -30,4 +30,19 @@ describe('ensureLocalEnvironment', () => {
30
30
  await rm(root, { recursive: true, force: true });
31
31
  }
32
32
  });
33
+
34
+ test('a clean framework source tree reads the pre-scaffold example name', async () => {
35
+ const root = await mkdtemp(join(tmpdir(), 'sk-source-env-'));
36
+ try {
37
+ await writeFile(
38
+ join(root, '_env.example'),
39
+ 'DATABASE_URL=postgresql://USER:PASSWORD@127.0.0.1:5432/stitchkit_starter\n',
40
+ );
41
+ ensureLocalEnvironment(root);
42
+ const databaseName = appDeclaration.identity.slug.replaceAll('-', '_');
43
+ expect(await readFile(join(root, '.env'), 'utf8')).toContain(`5432/${databaseName}`);
44
+ } finally {
45
+ await rm(root, { recursive: true, force: true });
46
+ }
47
+ });
33
48
  });
@@ -3,8 +3,11 @@ import { resolve } from 'node:path';
3
3
  import { appIdentity } from '../packages/config/src/app-identity.generated';
4
4
 
5
5
  /**
6
- * Create `.env` from `.env.example` on first run, rendering the application
7
- * identity into the database name.
6
+ * Create `.env` from the public `.env.example` on first run, rendering the
7
+ * application identity into the database name. The framework repository keeps
8
+ * that same source as `_env.example` until the scaffolder performs its rename,
9
+ * so clean source-tree checks use it directly instead of depending on an
10
+ * ignored developer `.env`.
8
11
  *
9
12
  * Identity, not the whole declaration: this needs one slug, and the identity
10
13
  * module carries no dependencies. That matters here more than elsewhere —
@@ -19,7 +22,11 @@ import { appIdentity } from '../packages/config/src/app-identity.generated';
19
22
  export function ensureLocalEnvironment(root: string): void {
20
23
  const destination = resolve(root, '.env');
21
24
  if (existsSync(destination)) return;
22
- const example = readFileSync(resolve(root, '.env.example'), 'utf8');
25
+ const publicExample = resolve(root, '.env.example');
26
+ const example = readFileSync(
27
+ existsSync(publicExample) ? publicExample : resolve(root, '_env.example'),
28
+ 'utf8',
29
+ );
23
30
  const databaseName = appIdentity.slug.replaceAll('-', '_');
24
31
  writeFileSync(destination, example.replaceAll('stitchkit_starter', databaseName));
25
32
  }
@@ -43,16 +43,23 @@ describe('the termination budget is an upper bound, not an estimate', () => {
43
43
  });
44
44
 
45
45
  test('the steps share one budget rather than each getting a full one', async () => {
46
+ const started: string[] = [];
47
+ const hangingClose = (name: string) => () => {
48
+ started.push(name);
49
+ return new Promise<void>(() => undefined);
50
+ };
51
+ const names = Array.from({ length: 10 }, (_, index) => `resource-${index + 1}`);
46
52
  const result = await closeWithinBudget(
47
- [
48
- { name: 'MCP', close: () => new Promise<void>(() => undefined) },
49
- { name: 'database', close: () => new Promise<void>(() => undefined) },
50
- ],
53
+ names.map((name) => ({ name, close: hangingClose(name) })),
51
54
  25,
52
55
  );
53
- expect(result.unfinished).toEqual(['MCP', 'database']);
54
- // Two steps, one budget: about one budget of wall clock, not two.
55
- expect(result.durationMs).toBeLessThan(50);
56
+ expect(result.unfinished).toEqual(names);
57
+ // Timer rounding may let one close start on a sub-millisecond remainder,
58
+ // so no exact boundary is contractual. What a fresh per-step budget would
59
+ // do is start every close; one shared deadline must skip at least one. This
60
+ // proves the state transition directly without treating scheduler latency
61
+ // as a product failure.
62
+ expect(started.length).toBeLessThan(names.length);
56
63
  });
57
64
 
58
65
  test('a close that fails is a failure, and keeps its cause', async () => {
@@ -0,0 +1,41 @@
1
+ # Stitchkit Agent
2
+
3
+ A small, real terminal coding agent. The official TUI package owns terminal interaction while
4
+ Stitchkit owns durable messages, runs, direct typed tools, approvals, recovery and resources.
5
+
6
+ ## Start
7
+
8
+ ```bash
9
+ cp .env.example .env
10
+ # Fill OPENROUTER_API_KEY.
11
+ bun run dev
12
+ ```
13
+
14
+ Use `/model` to choose any live tool-capable model. Weekly popularity and benchmark facts stay
15
+ separate in the catalog. File reads and searches run directly. Writes, patches and shell commands
16
+ show an approval card bound to the exact durable tool call; press `Y` or `N`.
17
+
18
+ Source edits restart the terminal host through `bun --watch`, while `.stitchkit/agent.sqlite`
19
+ retains durable conversations and recovery evidence. Every launch opens a fresh conversation;
20
+ use `/resume` to return to an earlier one. `/clear` starts clean without deleting the conversation
21
+ you are leaving.
22
+
23
+ ## Shape
24
+
25
+ - `stitchkit.agent.ts` — the small, typed composition point for theme, catalog and runtime policy.
26
+ - `src/runtime.ts` — host policy and composition of published Stitchkit primitives.
27
+ - `stitchkit-tui` — commands, transcript, model/session pickers and local attach protocol.
28
+ - `instructions/` — eager instructions with explicit provenance.
29
+ - `skills/*/SKILL.md` — lazily discoverable skills read through the direct `read_resource` tool.
30
+ - `.stitchkit/` — ignored local durable state, session descriptors, bounded metadata diagnostics
31
+ and approval secret.
32
+
33
+ While the TUI is open, `/status` shows its session ID. Another local process can submit through
34
+ the same controller without racing the runtime:
35
+
36
+ ```bash
37
+ bunx stitchkit-agent send --session SESSION_ID -- "Inspect the current project"
38
+ ```
39
+
40
+ The coding root is a path boundary, not an operating-system sandbox. Run the process in a container
41
+ or another isolated environment before granting it access to untrusted projects or executables.
@@ -0,0 +1,4 @@
1
+ # Create an API key at https://openrouter.ai/settings/keys
2
+ OPENROUTER_API_KEY=
3
+ # Optional: preselect this exact row when the live model picker opens.
4
+ OPENROUTER_MODEL=
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .stitchkit/
5
+ coverage/
6
+ *.log
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
3
+ "files": {
4
+ "includes": ["**", "!!node_modules", "!!dist", "!!project.json", "!!**/.stitchkit"]
5
+ },
6
+ "formatter": {
7
+ "enabled": true,
8
+ "indentStyle": "space",
9
+ "indentWidth": 2,
10
+ "lineWidth": 96
11
+ },
12
+ "javascript": {
13
+ "formatter": {
14
+ "quoteStyle": "single",
15
+ "jsxQuoteStyle": "single",
16
+ "trailingCommas": "all",
17
+ "semicolons": "always"
18
+ }
19
+ },
20
+ "linter": {
21
+ "enabled": true,
22
+ "rules": {
23
+ "preset": "recommended",
24
+ "suspicious": {
25
+ "noExplicitAny": "error",
26
+ "noUnknownAttribute": "off"
27
+ }
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,9 @@
1
+ # Agent workspace
2
+
3
+ You are a coding agent working inside this generated project.
4
+
5
+ - Inspect existing files before changing them.
6
+ - Keep edits inside the workspace root.
7
+ - Use direct file and shell tools; never claim a command succeeded without reading its result.
8
+ - Ask for approval through the tool protocol when a write, patch, edit or shell command requires it.
9
+ - Prefer the smallest coherent implementation and leave the workspace in a checked state.
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "stitchkit-agent-starter",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "catalog": {
7
+ "stitchkit-tui": "^0.1.1"
8
+ },
9
+ "scripts": {
10
+ "dev": "bun --watch src/index.ts",
11
+ "start": "bun src/index.ts",
12
+ "check": "bun x tsgo --noEmit",
13
+ "lint": "bun x biome check --error-on-warnings .",
14
+ "lint:fix": "bun x biome check --write .",
15
+ "test": "bun test",
16
+ "build": "bun build src/index.ts --outdir dist --target bun --packages external"
17
+ },
18
+ "dependencies": {
19
+ "@openrouter/ai-sdk-provider": "^3.0.0",
20
+ "ai": "^7.0.87",
21
+ "stitchkit": "file:../../../core",
22
+ "stitchkit-tui": "file:../../../tui",
23
+ "zod": "4.5.4"
24
+ },
25
+ "devDependencies": {
26
+ "@typescript/native-preview": "7.0.0-dev.20260707.2",
27
+ "@types/bun": "^1.4.0",
28
+ "@types/react": "^19.2.14",
29
+ "typescript": "^7.0.2"
30
+ },
31
+ "engines": {
32
+ "bun": ">=1.3.0"
33
+ },
34
+ "packageManager": "bun@1.3.14"
35
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "kind": "application",
4
+ "identity": {
5
+ "slug": "stitchkit-agent-starter",
6
+ "name": "Stitchkit Agent Starter",
7
+ "version": "0.1.0",
8
+ "description": {
9
+ "en": "A durable terminal coding agent built with Stitchkit."
10
+ }
11
+ },
12
+ "roles": [
13
+ {
14
+ "name": "agent",
15
+ "workingDirectory": ".",
16
+ "commands": {
17
+ "development": {
18
+ "executable": "bun",
19
+ "args": ["--watch", "src/index.ts"]
20
+ },
21
+ "production": {
22
+ "executable": "bun",
23
+ "args": ["dist/index.js"]
24
+ }
25
+ },
26
+ "drainFloorMs": 1000
27
+ }
28
+ ],
29
+ "requires": [],
30
+ "release": {},
31
+ "env": {
32
+ "variables": [
33
+ {
34
+ "name": "OPENROUTER_API_KEY",
35
+ "shape": "string",
36
+ "required": true
37
+ },
38
+ {
39
+ "name": "OPENROUTER_MODEL",
40
+ "shape": "string",
41
+ "required": false
42
+ }
43
+ ]
44
+ }
45
+ }
@@ -0,0 +1,10 @@
1
+ ---
2
+ name: verify
3
+ description: Check a completed change with the project's own Bun scripts before reporting success.
4
+ ---
5
+
6
+ # Verify
7
+
8
+ Read `package.json`, choose the narrowest relevant script, run it with the direct shell tool and
9
+ report the exact result. Use `bun run check` and `bun test` when the change affects shared types or
10
+ behavior. Do not invent a green result from code inspection alone.
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+
3
+ const EnvironmentSchema = z
4
+ .object({
5
+ OPENROUTER_API_KEY: z.string().min(1, 'OPENROUTER_API_KEY is required'),
6
+ OPENROUTER_MODEL: z.string().min(1).optional(),
7
+ })
8
+ .loose();
9
+
10
+ export interface AgentConfig {
11
+ apiKey: string;
12
+ preferredModelId?: string;
13
+ }
14
+
15
+ export function readAgentConfig(environment: Record<string, string | undefined>): AgentConfig {
16
+ const result = EnvironmentSchema.safeParse(environment);
17
+ if (!result.success) {
18
+ const fields = [
19
+ ...new Set(
20
+ result.error.issues
21
+ .map((issue) => issue.path[0])
22
+ .filter((field) => field !== undefined),
23
+ ),
24
+ ];
25
+ throw new Error(`Missing or invalid configuration: ${fields.join(', ')}`);
26
+ }
27
+ const parsed = result.data;
28
+ return {
29
+ apiKey: parsed.OPENROUTER_API_KEY,
30
+ ...(parsed.OPENROUTER_MODEL && { preferredModelId: parsed.OPENROUTER_MODEL }),
31
+ };
32
+ }
@@ -0,0 +1,4 @@
1
+ import { runAgentTui } from 'stitchkit-tui';
2
+ import config from '../stitchkit.agent';
3
+
4
+ await runAgentTui(config);
@@ -0,0 +1,141 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ type AgentLanguageModelProvider,
5
+ type AgentModelCatalog,
6
+ type AgentModelSelectionStore,
7
+ createAgentObservability,
8
+ defineAgentProtocol,
9
+ } from 'stitchkit/agent-runtime';
10
+ import { createAgentCodingTools } from 'stitchkit/agent-runtime/coding-tools';
11
+ import {
12
+ createAgentHarnessFileResources,
13
+ createHeadlessAgentHarness,
14
+ } from 'stitchkit/agent-runtime/harness';
15
+ import { openRouterProvider } from 'stitchkit/agent-runtime/openrouter';
16
+ import { createBunSqliteAgentRuntimeStore } from 'stitchkit/agent-runtime/sqlite/bun';
17
+ import { mountAgent } from 'stitchkit/tools';
18
+ import type { AgentTuiDiagnostics } from 'stitchkit-tui';
19
+ import { z } from 'zod';
20
+ import type { AgentConfig } from './config';
21
+
22
+ const InputMetadataSchema = z.object({ modelId: z.string().min(1) }).strict();
23
+
24
+ async function persistentApprovalSecret(stateDirectory: string): Promise<string> {
25
+ const filename = path.join(stateDirectory, 'approval-secret');
26
+ await mkdir(stateDirectory, { recursive: true });
27
+ try {
28
+ return (await readFile(filename, 'utf8')).trim();
29
+ } catch (error) {
30
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
31
+ }
32
+ const secret = crypto.randomUUID();
33
+ try {
34
+ await writeFile(filename, `${secret}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
35
+ return secret;
36
+ } catch (error) {
37
+ if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST')) throw error;
38
+ return (await readFile(filename, 'utf8')).trim();
39
+ }
40
+ }
41
+
42
+ export async function createStarterHarness(
43
+ config: AgentConfig,
44
+ workspace: string,
45
+ options: {
46
+ catalog: AgentModelCatalog;
47
+ selections: AgentModelSelectionStore;
48
+ provider?: AgentLanguageModelProvider;
49
+ diagnostics: AgentTuiDiagnostics;
50
+ },
51
+ ) {
52
+ const stateDirectory = path.join(workspace, '.stitchkit');
53
+ await mkdir(stateDirectory, { recursive: true });
54
+ const resources = createAgentHarnessFileResources({
55
+ roots: [
56
+ { id: 'instructions', path: path.join(workspace, 'instructions'), kind: 'instruction' },
57
+ { id: 'skills', path: path.join(workspace, 'skills'), kind: 'skill' },
58
+ ],
59
+ });
60
+ const codingTools = createAgentCodingTools({
61
+ root: workspace,
62
+ authorize: () => true,
63
+ executables: {
64
+ bun: process.execPath,
65
+ git: Bun.which('git') ?? '/usr/bin/git',
66
+ rg: Bun.which('rg') ?? '/usr/bin/rg',
67
+ },
68
+ });
69
+ const provider = options.provider ?? openRouterProvider({ apiKey: config.apiKey });
70
+ const sqlite = createBunSqliteAgentRuntimeStore({
71
+ filename: path.join(stateDirectory, 'agent.sqlite'),
72
+ initialize: true,
73
+ });
74
+ const observability = createAgentObservability({
75
+ includeInternalCause: true,
76
+ write: (event) => options.diagnostics.write(event),
77
+ });
78
+ const harness = createHeadlessAgentHarness({
79
+ protocol: defineAgentProtocol({
80
+ context: z.object({}),
81
+ inputMetadata: InputMetadataSchema,
82
+ terminalAcceptance: 'require-output',
83
+ }),
84
+ store: sqlite.store,
85
+ observe: observability,
86
+ models: {
87
+ async resolve({ conversationId, run, snapshot }) {
88
+ const input = snapshot.messages.find(({ id }) => id === run.inputMessageIds[0]);
89
+ const metadata = InputMetadataSchema.safeParse(input?.metadata);
90
+ const selected = metadata.success
91
+ ? metadata.data.modelId
92
+ : (await options.selections.load(conversationId))?.modelId;
93
+ const entry = options.catalog.models.find(({ id }) => id === selected);
94
+ if (!entry) throw new Error('The selected model is stale or unavailable');
95
+ return {
96
+ descriptor: entry.descriptor,
97
+ model: provider.create(entry.descriptor.modelId),
98
+ ...(provider.normalizeUsage && { normalizeUsage: provider.normalizeUsage }),
99
+ };
100
+ },
101
+ },
102
+ resources: { load: () => resources.load() },
103
+ promptBudget: ({ contextWindow }) => ({
104
+ contextWindow,
105
+ reservedOutput: Math.min(8_192, Math.floor(contextWindow / 4)),
106
+ toolSchemas: { provenance: 'unavailable' },
107
+ attachments: { value: 0, provenance: 'measured' },
108
+ providerOverhead: { provenance: 'unavailable' },
109
+ }),
110
+ tools: (context) =>
111
+ mountAgent([], {
112
+ runtimeTools: [...codingTools, ...resources.runtimeTools],
113
+ lifecycle: context.toolFenceLifecycle,
114
+ }),
115
+ loop: {
116
+ maxSteps: 50,
117
+ checkpointEveryEvents: 10,
118
+ toolApproval: {
119
+ read_file: 'approved',
120
+ search_files: 'approved',
121
+ list_directory: 'approved',
122
+ glob: 'approved',
123
+ read_resource: 'approved',
124
+ write_file: 'user-approval',
125
+ edit_file: 'user-approval',
126
+ run_command: 'user-approval',
127
+ },
128
+ toolApprovalSecret: await persistentApprovalSecret(stateDirectory),
129
+ },
130
+ });
131
+ const managedHarness = {
132
+ ...harness,
133
+ async close(closeOptions?: Parameters<typeof harness.close>[0]) {
134
+ const result = await harness.close(closeOptions);
135
+ await observability.close();
136
+ await sqlite.close();
137
+ return result;
138
+ },
139
+ };
140
+ return { harness: managedHarness, conversations: sqlite.conversations };
141
+ }
@@ -0,0 +1,15 @@
1
+ import { openRouterModelCatalog } from 'stitchkit/agent-runtime/openrouter';
2
+ import { defineAgentTui } from 'stitchkit-tui';
3
+ import { readAgentConfig } from './src/config';
4
+ import { createStarterHarness } from './src/runtime';
5
+
6
+ const environment = readAgentConfig(Bun.env);
7
+
8
+ export default defineAgentTui({
9
+ title: 'Stitchkit agent',
10
+ context: () => ({}),
11
+ modelCatalog: openRouterModelCatalog({ apiKey: environment.apiKey }),
12
+ ...(environment.preferredModelId && { preferredModelId: environment.preferredModelId }),
13
+ createRuntime: ({ catalog, selections, diagnostics }) =>
14
+ createStarterHarness(environment, process.cwd(), { catalog, selections, diagnostics }),
15
+ });
@@ -0,0 +1,17 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { readAgentConfig } from '../src/config';
3
+
4
+ describe('agent config', () => {
5
+ test('requires only an OpenRouter credential and accepts an optional preferred model', () => {
6
+ expect(() => readAgentConfig({})).toThrow(
7
+ 'Missing or invalid configuration: OPENROUTER_API_KEY',
8
+ );
9
+ expect(readAgentConfig({ OPENROUTER_API_KEY: 'secret' })).toEqual({ apiKey: 'secret' });
10
+ expect(
11
+ readAgentConfig({
12
+ OPENROUTER_API_KEY: 'secret',
13
+ OPENROUTER_MODEL: 'provider/model',
14
+ }),
15
+ ).toEqual({ apiKey: 'secret', preferredModelId: 'provider/model' });
16
+ });
17
+ });
@@ -0,0 +1,90 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { simulateReadableStream } from 'ai';
6
+ import { MockLanguageModelV4 } from 'ai/test';
7
+ import {
8
+ AgentModelCatalogSchema,
9
+ createMemoryAgentModelSelectionStore,
10
+ } from 'stitchkit/agent-runtime';
11
+ import { createStarterHarness } from '../src/runtime';
12
+
13
+ const roots: string[] = [];
14
+
15
+ afterEach(async () => {
16
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
17
+ });
18
+
19
+ describe('Agent starter runtime', () => {
20
+ test('runs one model turn and reopens its durable transcript', async () => {
21
+ const workspace = await mkdtemp(path.join(tmpdir(), 'stitchkit-agent-starter-'));
22
+ roots.push(workspace);
23
+ await mkdir(path.join(workspace, 'instructions'));
24
+ await mkdir(path.join(workspace, 'skills'));
25
+ await writeFile(path.join(workspace, 'instructions/AGENTS.md'), 'Answer directly.\n');
26
+ const usage = {
27
+ inputTokens: { total: 1, noCache: 1, cacheRead: undefined, cacheWrite: undefined },
28
+ outputTokens: { total: 1, text: 1, reasoning: undefined },
29
+ };
30
+ const model = new MockLanguageModelV4({
31
+ doStream: {
32
+ stream: simulateReadableStream({
33
+ chunks: [
34
+ { type: 'text-start', id: 'answer' },
35
+ { type: 'text-delta', id: 'answer', delta: 'Ready to build.' },
36
+ { type: 'text-end', id: 'answer' },
37
+ { type: 'finish', finishReason: { unified: 'stop', raw: undefined }, usage },
38
+ ],
39
+ }),
40
+ },
41
+ });
42
+ const config = { apiKey: 'fixture' };
43
+ const descriptor = {
44
+ provider: 'fixture',
45
+ modelId: 'fixture/model',
46
+ contextWindow: 32_000,
47
+ capabilities: ['tools'],
48
+ };
49
+ const catalog = AgentModelCatalogSchema.parse({
50
+ schemaVersion: 1,
51
+ source: 'fixture',
52
+ observedAt: '2026-08-30T00:00:00.000Z',
53
+ completeness: 'complete',
54
+ diagnostics: [],
55
+ models: [{ id: 'fixture/model', name: 'Fixture', descriptor, metrics: [] }],
56
+ });
57
+ const selections = createMemoryAgentModelSelectionStore();
58
+ await selections.save('main', {
59
+ modelId: 'fixture/model',
60
+ selectedAt: '2026-08-30T00:00:00.000Z',
61
+ });
62
+ const provider = { create: () => model };
63
+ const diagnostics = { write: () => undefined };
64
+ const first = await createStarterHarness(config, workspace, {
65
+ catalog,
66
+ selections,
67
+ provider,
68
+ diagnostics,
69
+ });
70
+ const result = await first.harness.submit({
71
+ conversationId: 'main',
72
+ idempotencyKey: 'first',
73
+ context: {},
74
+ parts: [{ type: 'text', text: 'Hello' }],
75
+ metadata: { modelId: 'fixture/model' },
76
+ }).result;
77
+ expect(result.reason).toBe('success');
78
+ expect(result.message.parts).toContainEqual({ type: 'text', text: 'Ready to build.' });
79
+ await first.harness.close();
80
+
81
+ const reopened = await createStarterHarness(config, workspace, {
82
+ catalog,
83
+ selections,
84
+ provider,
85
+ diagnostics,
86
+ });
87
+ expect((await reopened.harness.snapshot('main')).messages).toHaveLength(2);
88
+ await reopened.harness.close();
89
+ });
90
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["ESNext", "DOM"],
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "jsxImportSource": "@opentui/react",
9
+ "strict": true,
10
+ "noUncheckedIndexedAccess": true,
11
+ "exactOptionalPropertyTypes": true,
12
+ "skipLibCheck": true,
13
+ "types": ["bun"]
14
+ },
15
+ "include": ["src", "tests"]
16
+ }