stitchkit 0.91.1 → 0.92.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/agent-runtime-coding-tools.js +3 -3
  3. package/dist/agent-runtime-harness.js +6 -6
  4. package/dist/agent-runtime-sandbox.js +3 -3
  5. package/dist/agent-runtime.js +9 -9
  6. package/dist/cli.d.ts +6 -3
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +371 -50
  9. package/dist/contract/define.d.ts +84 -2
  10. package/dist/contract/define.d.ts.map +1 -1
  11. package/dist/contract/index.d.ts +1 -1
  12. package/dist/contract/index.d.ts.map +1 -1
  13. package/dist/{index-qrbdq8jk.js → index-2c2bjw3s.js} +3 -3
  14. package/dist/{index-ya611vx3.js → index-7n145nbe.js} +183 -109
  15. package/dist/{index-bh70rk41.js → index-bx75zwt7.js} +3 -3
  16. package/dist/{index-709nh7z7.js → index-cxqhvq21.js} +2 -1
  17. package/dist/{index-13cjtmrw.js → index-e0znv5gn.js} +31 -8
  18. package/dist/{index-f4p4b8f6.js → index-e42wzzz5.js} +3 -3
  19. package/dist/{index-13e1k7v9.js → index-f477k430.js} +1 -1
  20. package/dist/{index-qjeyx5t2.js → index-fq77yfzc.js} +1 -1
  21. package/dist/{index-fenaekmk.js → index-h9etkkyk.js} +37 -27
  22. package/dist/{index-1qvdwnp2.js → index-mzvgxp0n.js} +2 -2
  23. package/dist/{index-zsqy3xfn.js → index-n0f51dfn.js} +4 -4
  24. package/dist/{index-bv7mytab.js → index-sr93cwf9.js} +1 -1
  25. package/dist/{index-vsbzgd7b.js → index-tmwcrptx.js} +1 -0
  26. package/dist/{index-b971qjes.js → index-v5573g1k.js} +1 -1
  27. package/dist/{index-8z9we758.js → index-vjpvjqpn.js} +1 -1
  28. package/dist/{index-bx6yq2m4.js → index-vk3kq9sb.js} +1 -1
  29. package/dist/{index-137jjs4v.js → index-yewwh1w4.js} +1 -1
  30. package/dist/{index-mqhjmadk.js → index-yn8remb1.js} +61 -57
  31. package/dist/index.js +4 -4
  32. package/dist/internal/atomic-file.d.ts +21 -0
  33. package/dist/internal/atomic-file.d.ts.map +1 -0
  34. package/dist/internal/errors.d.ts +10 -0
  35. package/dist/internal/errors.d.ts.map +1 -1
  36. package/dist/node.js +5 -5
  37. package/dist/observability/audit.d.ts +5 -1
  38. package/dist/observability/audit.d.ts.map +1 -1
  39. package/dist/observability/changes.d.ts.map +1 -1
  40. package/dist/observability/context.d.ts +27 -6
  41. package/dist/observability/context.d.ts.map +1 -1
  42. package/dist/observability/event.d.ts +34 -6
  43. package/dist/observability/event.d.ts.map +1 -1
  44. package/dist/observability/index.d.ts +2 -1
  45. package/dist/observability/index.d.ts.map +1 -1
  46. package/dist/observability/index.js +51 -7
  47. package/dist/observability/work.d.ts +55 -0
  48. package/dist/observability/work.d.ts.map +1 -0
  49. package/dist/remote.js +3 -3
  50. package/dist/server/context-contribution.d.ts.map +1 -1
  51. package/dist/server/index.js +7 -7
  52. package/dist/server/types.d.ts +20 -6
  53. package/dist/server/types.d.ts.map +1 -1
  54. package/dist/testing/surface-manifest.d.ts.map +1 -1
  55. package/dist/testing.js +9 -7
  56. package/dist/tool-invoker.js +6 -6
  57. package/dist/tools/cli-checkpoint.d.ts +24 -0
  58. package/dist/tools/cli-checkpoint.d.ts.map +1 -0
  59. package/dist/tools/cli-format.d.ts +10 -0
  60. package/dist/tools/cli-format.d.ts.map +1 -1
  61. package/dist/tools/cli-invoke.d.ts +118 -0
  62. package/dist/tools/cli-invoke.d.ts.map +1 -0
  63. package/dist/tools/cli-manifest.d.ts +6 -9
  64. package/dist/tools/cli-manifest.d.ts.map +1 -1
  65. package/dist/tools/cli-signature.d.ts +81 -0
  66. package/dist/tools/cli-signature.d.ts.map +1 -0
  67. package/dist/tools/cli-stream.d.ts +93 -0
  68. package/dist/tools/cli-stream.d.ts.map +1 -0
  69. package/dist/tools/cli-update.d.ts +62 -0
  70. package/dist/tools/cli-update.d.ts.map +1 -1
  71. package/dist/tools/cli.d.ts +4 -13
  72. package/dist/tools/cli.d.ts.map +1 -1
  73. package/dist/tools/connections/index.js +2 -2
  74. package/dist/tools/execute.d.ts +21 -0
  75. package/dist/tools/execute.d.ts.map +1 -1
  76. package/dist/tools/mcp-progress.d.ts +46 -0
  77. package/dist/tools/mcp-progress.d.ts.map +1 -0
  78. package/dist/tools/mcp-round-policy.d.ts +25 -1
  79. package/dist/tools/mcp-round-policy.d.ts.map +1 -1
  80. package/dist/tools/mcp-round.d.ts +20 -1
  81. package/dist/tools/mcp-round.d.ts.map +1 -1
  82. package/dist/tools/mcp-trace.d.ts +9 -1
  83. package/dist/tools/mcp-trace.d.ts.map +1 -1
  84. package/dist/tools/mount-wait.d.ts.map +1 -1
  85. package/dist/tools.js +160 -51
  86. package/llms-full.txt +358 -8
  87. package/package.json +1 -1
package/llms-full.txt CHANGED
@@ -66,11 +66,11 @@ own, recorded as an ADR.
66
66
  | `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
67
67
  | `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
68
68
  | `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
69
- | `stitchkit/observability` | server | stable<br>_redefined in 1 of the 36 minors since 0.56.2, most recently 0.83.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
69
+ | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 37 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
70
70
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
71
71
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
72
72
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
73
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 20 of the 36 minors since 0.56.2, most recently 0.89.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
73
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 20 of the 37 minors since 0.56.2, most recently 0.89.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
74
74
  | `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
75
75
  | `stitchkit/agent-runtime/testing` | tests on Bun or Node | evolving | credential-free replay, scripted provider faults and deterministic race controls |
76
76
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
@@ -80,7 +80,7 @@ own, recorded as an ADR.
80
80
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
81
81
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
82
82
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
83
- | `stitchkit/application` | browser + server | evolving<br>_redefined in 7 of the 36 minors since 0.56.2, most recently 0.83.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
83
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 7 of the 37 minors since 0.56.2, most recently 0.83.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
84
84
  | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
85
85
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
86
86
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
@@ -3639,6 +3639,48 @@ never for authentication, RBAC, tenant selection or rate limiting. Verified
3639
3639
  identity still comes from `auth`; the `context(auth)` callback remains the
3640
3640
  application-context factory and receives no protocol metadata.
3641
3641
 
3642
+ ### Telling the host what a long call is doing — `ctx.reportProgress`
3643
+
3644
+ A tool that takes ten minutes tells a text host nothing: the call is made, and
3645
+ the next thing the host shows is the answer. The protocol has a channel for
3646
+ this — the host sends a `progressToken` with the call, the server relates
3647
+ progress notifications to it — and a handler reaches it through
3648
+ `ctx.reportProgress`:
3649
+
3650
+ ```ts
3651
+ render: async (ctx) => {
3652
+ await ctx.reportProgress?.({ message: 'queued' })
3653
+ const job = await jobs.start(ctx.input)
3654
+ await ctx.reportProgress?.({ message: 'rendering', progress: 1, total: 3 })
3655
+ return await jobs.finish(job)
3656
+ }
3657
+ ```
3658
+
3659
+ `reportProgress` is present on every **tool** call — MCP, agent and CLI — so a
3660
+ handler never branches on transport, and it does nothing wherever nobody is
3661
+ listening. It is absent on the HTTP transport, which has no channel for it, so a
3662
+ handler that also serves HTTP writes `ctx.reportProgress?.(…)`. It never throws and never
3663
+ rejects: a message about work must not be able to kill the work it describes, so
3664
+ a refused notification leaves the host exactly where a host that never asked for
3665
+ progress already is.
3666
+
3667
+ `progress` is a number the protocol requires and a handler often does not have —
3668
+ there is a stage to name and no scale to name it on. Omit it and the ordinal of
3669
+ the update is sent, with no `total`, so the host shows a counter rather than a
3670
+ bar stuck at 3%. That is a fact about what happened; a synthesised percentage
3671
+ would be a claim nobody measured.
3672
+
3673
+ The framework sends nothing on its own. Progress is meaningful only where an
3674
+ operation has observable stages, and manufacturing one per call would be
3675
+ inventing a second transport beside the one the protocol already provides. The
3676
+ one exception is the managed wait tool (`mountWait`), which already polls and
3677
+ therefore already knows: when the host asked for progress it reports each poll
3678
+ and the phase it last saw, which is the tool whose silence raised the question
3679
+ in the first place.
3680
+
3681
+ The token itself is on `ctx.mcp.progressToken` when a caller wants to know
3682
+ whether anyone is listening.
3683
+
3642
3684
  ### Multi-round tool input (`input_required`)
3643
3685
 
3644
3686
  An MCP-only operation can require typed user input before its final side effect:
@@ -3690,6 +3732,58 @@ ordinary unsupported result rather than a falsely advertised round. Signed
3690
3732
  state is tamper-resistant, not an exactly-once replay store; destructive
3691
3733
  handlers must still be idempotent where retries matter.
3692
3734
 
3735
+ #### Questions chosen from the arguments
3736
+
3737
+ The list above is fixed when the contract is written, which is right when the
3738
+ questions belong to the operation. It is not enough when they belong to the
3739
+ **arguments** — one model takes `aspect_ratio`, another `duration`, a third an
3740
+ input image — and there a declared list can only be empty. `inputRequired` also
3741
+ accepts a function of the parsed call:
3742
+
3743
+ ```ts
3744
+ mcp: {
3745
+ inputRequired: async ({ input }) => {
3746
+ const model = await catalog.get(input.model)
3747
+ return model.needsRatio
3748
+ ? [{ key: 'ratio', schema: RatioSchema, message: 'Which aspect ratio?' }]
3749
+ : []
3750
+ },
3751
+ }
3752
+ ```
3753
+
3754
+ It receives the same `params` / `input` the handler receives — parsed, so
3755
+ defaults and coercions are already applied. Returning an empty array means this
3756
+ call needs nothing and runs straight through. Everything else is unchanged: the
3757
+ state is still signed and bound to principal, operation and argument digest, the
3758
+ rounds are still counted against `maxRounds`, and a resolved list with duplicate
3759
+ keys or more rounds than allowed is refused.
3760
+
3761
+ **The plan is fingerprinted, and that matters more than it looks.** Between two
3762
+ rounds there is a full round trip to the host, and a resolver that reads
3763
+ anything outside its arguments — a model catalog, a feature flag — can answer
3764
+ differently the second time. Every other check would still pass: same principal,
3765
+ same operation, same arguments, round still in range. Round 2's question would
3766
+ be asked under round 1's key, and if the schemas happened to be compatible the
3767
+ user's answer would be accepted for a question nobody asked. So the resolved
3768
+ list is fingerprinted into the signed state and re-checked every round; a plan
3769
+ that moved is refused, not asked. A resolver that reads only its arguments never
3770
+ sees this.
3771
+
3772
+ **What a dynamic declaration costs.** One extra pass through the contract
3773
+ pipeline per round — `lifecycle.beforeHandle` runs once more and one more
3774
+ `toolPhase: 'input-round'` audit row is written. That is the price of ordering:
3775
+ the resolver is your code and may reach a network, so it runs **after**
3776
+ authorisation, never before, and the only thing that can authorise is a pipeline
3777
+ pass. Resolving itself is free — the arguments are parsed by the same pure
3778
+ function the call parses with, not by a second run of anything. A fixed list
3779
+ pays none of this; its call sequence is unchanged.
3780
+
3781
+ One thing does change for a fixed list: the signed round state now carries a
3782
+ fingerprint of the question plan, and it is checked on every round. A
3783
+ conversation already in flight when you deploy this has no fingerprint in its
3784
+ state and is refused once with `INVALID_REQUEST_STATE`; the host re-asks and the
3785
+ retry succeeds.
3786
+
3693
3787
  ### Guarding tools — `lifecycle`
3694
3788
 
3695
3789
  A tool call runs the same handler an HTTP request would. `lifecycle` makes it
@@ -7976,6 +8070,166 @@ Carry the build stamp inside the binary (`CliBuildStampSchema`,
7976
8070
  `formatCliBuildStamp`) so the tool can say what it is rather than leaving the
7977
8071
  reader to infer it from behaviour.
7978
8072
 
8073
+ ### Proving who built it — signing the manifest
8074
+
8075
+ The asset digest proves the bytes that arrived are the bytes the manifest named.
8076
+ It proves nothing about who named them: the manifest and the assets come from
8077
+ one origin, so whoever can replace one can replace the other. Authorship needs a
8078
+ key the build carries and the server never holds.
8079
+
8080
+ ```ts
8081
+ // Publishing — the only side that holds a private key.
8082
+ const signature = signCliManifest(manifest, { keyId: 'release-2026', privateKey })
8083
+ publish({ ...manifest, signature })
8084
+
8085
+ // Installing — the trust root is compiled into the binary. One fetched at run
8086
+ // time from the same origin as the manifest would prove nothing.
8087
+ const trust = { keys: { 'release-2026': PUBLISHED_KEY } }
8088
+ const check = await checkCliUpdate({ manifestUrl, currentVersion, trust })
8089
+ if (check.status === 'outdated' && check.asset) {
8090
+ await applyCliUpdate({ asset: check.asset, manifest: check.manifest, trust, backupPath })
8091
+ }
8092
+ ```
8093
+
8094
+ The signature covers `{name, version, commit, builtAt, assets[]}` with **every
8095
+ asset's digest**, so the chain closes on the file that will execute rather than
8096
+ on the document describing it; moving an asset to a new URL does not invalidate
8097
+ it, changing what an asset contains does.
8098
+
8099
+ Five verdicts, and `unenforced` is the one that earns its keep: a build with no
8100
+ pinned key keeps updating, and the fact that nothing was checked is *visible*
8101
+ rather than assumed. `missing`, `unknown-key` and `invalid` are distinguished
8102
+ because they need different answers — a key you forgot to pin is not a forged
8103
+ signature.
8104
+
8105
+ A bad verdict does not produce a fifth status. `outdated` is an instruction to
8106
+ install, and a manifest that failed its signature has not established that there
8107
+ is a newer build worth installing — only that a document claims one. The check
8108
+ answers `unknown` with the verdict in its reason, so it is never mistaken for a
8109
+ network failure. `applyCliUpdate` checks again and refuses **before** the
8110
+ download: a signature verified after the bytes are on the machine produces a
8111
+ cleanup problem rather than a refusal.
8112
+
8113
+ ### A way back — `backupPath` and `rollbackCliUpdate`
8114
+
8115
+ Rolling back is a property of updating, not of the application:
8116
+
8117
+ ```ts
8118
+ const applied = await applyCliUpdate({ asset, targetPath, backupPath })
8119
+ // …the new build turns out to be wrong. `backupSha256` is absent when there was
8120
+ // nothing to keep — a first install — and that is the case with nothing to roll
8121
+ // back to:
8122
+ if (applied.backupSha256) {
8123
+ rollbackCliUpdate({ targetPath, backupPath, expectedSha256: applied.backupSha256 })
8124
+ }
8125
+ ```
8126
+
8127
+ The copy is taken between "the new bytes verified" and the replacement — earlier
8128
+ would preserve a binary about to be replaced by a download that then failed its
8129
+ digest, later has nothing left to copy — and it carries the target's file mode,
8130
+ because a backup that cannot be executed is not a way back. A first install has
8131
+ nothing to keep, which is not a failure.
8132
+
8133
+ `expectedSha256` is required, not optional. A rollback that installs whatever
8134
+ happens to be at the backup path is a second install of an unverified binary,
8135
+ and the day it is used is the day nobody is in a position to check.
8136
+
8137
+ ### Two release tracks
8138
+
8139
+ `assertCliPublishable` takes one manifest **or every manifest already
8140
+ published**:
8141
+
8142
+ ```ts
8143
+ assertCliPublishable([stableManifest, betaManifest], next)
8144
+ ```
8145
+
8146
+ With a single argument and two tracks the check is silently useless: `1.2.3`
8147
+ goes to beta from one commit and to stable from another, the function is called
8148
+ with the track being published and sees no conflict, and the person who
8149
+ installed the beta is told they are current forever. Nothing on their machine
8150
+ looks wrong.
8151
+
8152
+ **The channel itself is not an argument of the framework, and will not become
8153
+ one.** The framework has no model of how your URLs are built and should not: an
8154
+ asset's address comes from the manifest, and the manifest's address belongs to
8155
+ your server. A channel is two documents at two addresses — which of them a build
8156
+ consults is your decision, and `assertCliPublishable` is told about all of them
8157
+ rather than taught the shape of any.
8158
+
8159
+ ## One operation per line — `createCliInvoker`
8160
+
8161
+ The most common way an agent drives a CLI is not one command: it is a stream —
8162
+ a JSON line per operation, an answer per line — and a resumable batch of the
8163
+ same. The mechanics contain nothing product-specific, and until now the
8164
+ framework left the one piece that cannot be written outside it: running an
8165
+ already-parsed call.
8166
+
8167
+ ```ts
8168
+ import { createCliInvoker, defineCliStreamCommand, defineCliBatchCommand } from 'stitchkit/cli'
8169
+
8170
+ const invoker = await createCliInvoker({ name: 'myapp', services })
8171
+
8172
+ const outcome = await invoker.invoke('create_item', { title: 'hello', tags: ['a'] })
8173
+ // { ok: true, exitCode: 0, data: { … } } — nothing written, nothing exited
8174
+ ```
8175
+
8176
+ `invoke` runs the same pipeline a typed command runs — the same validation, the
8177
+ same `lifecycle` gate, the same hooks — and returns the handler's validated
8178
+ output with **the exit code the printed path would give**, from the same table.
8179
+ `data` is typed `unknown`: the surface is resolved at run time from services and
8180
+ runtime tools, so there is no compile-time name to key a result type on. Narrow
8181
+ it with the contract's own output schema. That table used to
8182
+ live inside the function that prints, so the only way to learn a code was to
8183
+ print it.
8184
+
8185
+ Without this, a stream loop had to re-spawn the binary per line: arguments
8186
+ serialised back into `--flag value` strings, nested objects pushed through
8187
+ `JSON.stringify` into one argv slot and parsed again on the other side, the
8188
+ result read back out of stdout text. Three conversions of data the framework was
8189
+ already holding, plus a process start measured at 0.15 s — thirty seconds for a
8190
+ two-hundred-line manifest before any work begins. The ban on nesting one stream
8191
+ inside another was a consequence of that child process, not a rule anyone wanted.
8192
+
8193
+ ### Mounting the loop
8194
+
8195
+ ```ts
8196
+ await createCli({
8197
+ name: 'myapp', version, services,
8198
+ commands: [
8199
+ defineCliStreamCommand({ name: 'jsonl', invoker }),
8200
+ defineCliBatchCommand({ name: 'batch', invoker, checkpointPath: '.myapp-batch.json' }),
8201
+ ],
8202
+ })
8203
+ ```
8204
+
8205
+ They are factories, not framework-owned names. Reserving `jsonl` and `batch`
8206
+ would take two names out of a namespace that belongs to the application — one
8207
+ that already has a `batch` command would either fail at startup or find its own
8208
+ command silently shadowed — so you mount them under whatever you call them, like
8209
+ any other command of yours.
8210
+
8211
+ Each line is `{ id, command, args }`; each answer is the invocation result plus
8212
+ that `id`. A line that is not JSON, or has no `id`, is **answered** and the
8213
+ stream continues: a stream that goes quiet on one line leaves its consumer
8214
+ unable to tell which request it lost. Answers go to stdout, one object per line,
8215
+ and nothing else does.
8216
+
8217
+ ### Resuming a batch
8218
+
8219
+ The checkpoint records, per id, a digest of the line that produced the recorded
8220
+ answer, and is written after **every** line by atomic rename — a checkpoint that
8221
+ only survives a clean finish protects against exactly the case that does not
8222
+ happen.
8223
+
8224
+ A re-run replays what already ran. A line whose content changed under the same
8225
+ id is **refused**: replaying it would report success for an operation nobody
8226
+ ran, and re-running it would repeat a paid call. Both answer a question nobody
8227
+ asked.
8228
+
8229
+ What stays yours: what makes a repeat safe (an idempotency key derived from the
8230
+ line id), which operations a batch may contain, and how much of it runs at once.
8231
+ That is product knowledge, and the framework has no business guessing it.
8232
+
7979
8233
  ## Commands discovered from a running server
7980
8234
 
7981
8235
  A CLI compiled from contracts carries the surface of the build it was compiled
@@ -10453,7 +10707,7 @@ export const observability = createObservability({
10453
10707
  write,
10454
10708
  includePayload: false, // default: no Request.clone(), payload is null
10455
10709
  includeCancelled: false, // default: keep client closes in access logs only
10456
- filter: (event) => event.method !== 'GET',
10710
+ filter: (event) => event.method !== 'GET', // see the note on jobs below
10457
10711
  },
10458
10712
  tools: {
10459
10713
  write,
@@ -10639,21 +10893,56 @@ queryable across all three:
10639
10893
 
10640
10894
  | Field | Notes |
10641
10895
  |-------|-------|
10642
- | `source` | `http` \| `mcp` \| `agent` |
10643
- | `method` / `path` | the verb + path, or `TOOL` + `/{source}/{tool}` |
10896
+ | `source` | `http` \| `mcp` \| `agent` \| `job` |
10897
+ | `kind` | `request` \| `job` — written on **every** row, so a filter never has to read it off an absent `method` |
10898
+ | `name` | what the work is, on a job (`agent-loop`, `broadcast-send`); absent on a request, which its method and path name |
10899
+ | `method` / `path` | the verb + path, or `TOOL` + `/{source}/{tool}` — **absent** on a job, which arrived over no transport |
10644
10900
  | `serviceName` / `action` | stable contract identity of the operation (→ ADR 0022) — from the contract, not parsed from `path`; set on every surface, present even on a pre-handler 400 |
10645
10901
  | `toolName` | tool calls only |
10646
10902
  | `httpMethod` | the contract verb on **tool** events (their `method` is `TOOL`) — filter reads vs writes across both surfaces with `(event.httpMethod ?? event.method) !== 'GET'` |
10647
10903
  | `dimensions` | app-defined domain dimensions (tenant / project / entity id) — see [request context](#request-context) |
10648
10904
  | `traceId` / `spanId` / `parentSpanId` | [W3C trace context](#trace-context) |
10649
10905
  | `outcome` | optional `'cancelled'` on explicitly enabled HTTP client-close rows; ordinary rows omit it |
10650
- | `ok` / `statusCode` | legacy success bit plus real HTTP status, or `200`/`400` for a tool; an opted-in cancellation is `false` / `499` |
10906
+ | `ok` / `statusCode` | legacy success bit plus real HTTP status, or `200`/`400` for a tool; an opted-in cancellation is `false` / `499`. `statusCode` is **absent** on a job — it had no transport to have a status on, and inventing one would be the same defect as the `method: 'AGENT'` this replaced. A job's outcome is `ok`, plus `errorCode` when it failed |
10651
10907
  | `durationMs` / `startedAt` | timing |
10652
10908
  | `errorCode` / `errorMessage` / `errorDetail` | failures only — `errorDetail` carries the structure the message flattens (e.g. Zod issues) |
10653
10909
  | `payload` | sanitised tool arguments; HTTP is `null` unless request `includePayload` is enabled |
10654
10910
  | `resultSize` / `responseBytes` | result item count + serialised size |
10655
10911
  | `userId` / `ipAddress` / `userAgent` | identity |
10656
10912
 
10913
+ ### Work that is not a request — `runUnitOfWork`
10914
+
10915
+ An agent loop launched fire-and-forget, a scheduled broadcast, a scenario engine
10916
+ tick: each has a start, a duration and an outcome, and none of them arrived over
10917
+ a transport. Run one inside a context and it is audited like everything else:
10918
+
10919
+ ```ts
10920
+ await runUnitOfWork({ name: 'broadcast-send', observability }, async () => {
10921
+ setRequestDimensions({ projectId })
10922
+ await send()
10923
+ })
10924
+ ```
10925
+
10926
+ The row carries `kind: 'job'`, the `name`, the duration and the outcome, and
10927
+ carries **no** `method`, `path` or `statusCode`, because the work had none. That
10928
+ absence is the point. The alternative is what consuming code had to do before —
10929
+ write `method: 'AGENT'` into the field for verbs — and a row that names a verb
10930
+ nobody used is worse than a row that names none, because a filter reading
10931
+ `method !== 'GET'` counts it as a write and nothing looks wrong.
10932
+
10933
+ Which is why **a filter on `method` needs a decision about jobs.** `auditChanges`
10934
+ makes it explicitly: a row with no verb is kept, because whether it changed
10935
+ anything cannot be answered from the row, and an audit that drops what it did not
10936
+ examine reports zero and looks exactly like a system in which nothing happened.
10937
+ A hand-written `event.method !== 'GET'` keeps jobs too (`undefined !== 'GET'`);
10938
+ `event.kind === 'request' && event.method !== 'GET'` is how you exclude them on
10939
+ purpose.
10940
+
10941
+ Without an `observability` the work still runs inside a context, so trace ids,
10942
+ `setRequestDimensions` and the bounded logger behave — recording is a separate
10943
+ decision from having a context. `traceparent` continues the trace of whatever
10944
+ scheduled the work, instead of starting an unrelated one.
10945
+
10657
10946
  ### Write the calls that changed something
10658
10947
 
10659
10948
  `auditChanges` is the filter most projects end up writing, shipped so they do not
@@ -12132,6 +12421,52 @@ analytics policy belong to the application.
12132
12421
 
12133
12422
  # Upgrading stitchkit
12134
12423
 
12424
+ ## Released migration: 0.92.0
12425
+
12426
+ 1. **`RequestEvent.method`, `path` and `statusCode` are optional.** Work that did
12427
+ not arrive over a transport now records their **absence** instead of inventing
12428
+ values — a background loop used to write `method: 'AGENT'`, which is not a
12429
+ verb. **Who must act:** anyone whose `RequestEventSinkConfig.write` puts these
12430
+ into NOT NULL columns, or whose filter reads `event.method`.
12431
+
12432
+ ```ts
12433
+ // before
12434
+ write: (event) => db.audit.create({ data: { method: event.method, path: event.path, status: event.statusCode } })
12435
+ // after — a job has none of the three; `kind` says which kind of row it is
12436
+ write: (event) => db.audit.create({ data: {
12437
+ kind: event.kind,
12438
+ name: event.name ?? null,
12439
+ method: event.method ?? null,
12440
+ path: event.path ?? null,
12441
+ status: event.statusCode ?? null,
12442
+ } })
12443
+ ```
12444
+
12445
+ A filter written as `event.method !== 'GET'` keeps jobs (`undefined !== 'GET'`),
12446
+ which is usually what you want. To exclude them deliberately:
12447
+ `event.kind === 'request' && event.method !== 'GET'`.
12448
+
12449
+ 2. **`applyCliUpdate` needs the manifest when given a trust root.** The signature
12450
+ covers the manifest and every asset digest together, so an asset alone cannot
12451
+ be verified. **Who must act:** only callers adopting signature verification;
12452
+ the unsigned call is unchanged.
12453
+
12454
+ ```ts
12455
+ // before
12456
+ await applyCliUpdate({ asset })
12457
+ // after
12458
+ await applyCliUpdate({ asset, manifest: check.manifest, trust })
12459
+ ```
12460
+
12461
+ 3. **An MCP continuation minted before this release is refused once.** The signed
12462
+ round state now carries a fingerprint of the question plan, checked on every
12463
+ round, so a plan that moved between rounds is a refusal rather than a
12464
+ different question asked under the previous one's key. A conversation already
12465
+ in flight at deploy time has no fingerprint and gets
12466
+ `INVALID_REQUEST_STATE`; the host re-asks and the second attempt succeeds.
12467
+ **Who must act:** nobody — but expect a small number of these in the minutes
12468
+ after a deploy, and do not read them as tampering.
12469
+
12135
12470
  ## Released migration: 0.91.0
12136
12471
 
12137
12472
  1. `STITCH_ERROR_STATUS` gained `NOT_IMPLEMENTED` (501). An **exhaustive** error
@@ -15832,6 +16167,10 @@ from the root `stitchkit`.
15832
16167
  | `HandlerContext` | _type_ | the typed context seen by a handler, including optional typed `mcp` metadata |
15833
16168
  | `McpCallContext` | _type_ | validated metadata for the active managed MCP call (`era`, method, tool, client and multi-round fields) |
15834
16169
  | `McpClientInfo` | _type_ | self-reported MCP host name/version; attribution only, never application identity |
16170
+ | `McpInputRequiredCall` | _type_ | the parsed `params` / `input` an elicitation resolver chooses its questions from |
16171
+ | `McpInputRequiredResolver` | _type_ | `mcp.inputRequired` as a function of the parsed arguments — returns the rounds for this call, or none |
16172
+ | `McpProgressUpdate` | _type_ | one mid-call progress update — `message`, and `progress`/`total` when the operation knows a scale |
16173
+ | `McpReportProgress` | _type_ | `ctx.reportProgress` — sends one update to the host that asked for progress; a no-op when none did, and never throws |
15835
16174
  | `McpRoundOutcome` | _type_ | managed multi-round attempt outcome |
15836
16175
  | `EndpointHandlerContext` | _type_ | one endpoint handler's fully inferred params, input, files and runtime context |
15837
16176
  | `EndpointFn` | _type_ | the call signature of one client method |
@@ -16949,6 +17288,8 @@ audit event. See the [Observability guide](../guide/observability.md).
16949
17288
  | `DEFAULT_REDACT_PATHS` | constant | baseline credential/token paths added to the sanitizer's sensitive-key policy |
16950
17289
  | `BoundedLoggerBounds` / `BoundedLoggerOptions` | _type_ | per-value and total record ceilings (`stringLengthByKey` raises the string bound for named keys such as `stack`) plus sink/redaction configuration |
16951
17290
  | `RequestEvent` | _type_ | the normalised audit event handed to the sink; opt-in HTTP cancellation rows carry `outcome: 'cancelled'` |
17291
+ | `runUnitOfWork` / `RunUnitOfWorkOptions` | function / _type_ | run work that did not arrive over a transport — an agent loop, a scheduled broadcast — inside one context, with the same completion record a request gets |
17292
+ | `RequestContextKind` | _type_ | `'request'` \| `'job'` — what the row describes, written on every event so a filter never has to read it off an absent `method` |
16952
17293
  | `ObservabilityConfig` | _type_ | independent request and tool sink configuration |
16953
17294
  | `Observability` | _type_ | `{ request?, toolCall, getStatus(), flush(bound?): Promise<boolean>, close(bound?) }` with bounded sink lifecycle |
16954
17295
  | `ObservabilityDrainBound` | _type_ | `timeoutMs` and/or `signal` limiting how long `flush`/`close` wait; the wait ends, outstanding writes do not |
@@ -17690,7 +18031,11 @@ SDK nor the `ai` peer.
17690
18031
  | `renderCliInstaller` | function | generate the one-line installer from a build manifest, with the URL and digest substituted — [guide](../guide/cli.md#distribution-and-self-update) |
17691
18032
  | `checkCliUpdate` | function | bounded, interval-limited, never-throwing check for a newer published build |
17692
18033
  | `applyCliUpdate` | function | download, verify the decompressed digest and replace the binary by rename |
17693
- | `assertCliPublishable` | function | refuse republishing one version from a different commit |
18034
+ | `assertCliPublishable` | function | refuse republishing one version from a different commit — takes one manifest or every manifest already published, because the promise a version makes does not stop at a channel boundary |
18035
+ | `rollbackCliUpdate` / `CliRollbackConfig` / `RolledBackCliUpdate` | function / _types_ | put the kept previous build back, digest-checked first and written by the same atomic rename the update uses |
18036
+ | `signCliManifest` / `verifyCliManifest` / `cliManifestSigningPayload` | function | Ed25519 over `{name, version, commit, builtAt, assets[]}` — every asset's digest included, so the chain closes on the file that executes |
18037
+ | `cliSignatureAccepted` / `CliSignatureVerdict` | function / _type_ | `valid` \| `unenforced` \| `missing` \| `unknown-key` \| `invalid`; `unenforced` keeps an unpinned build updating while making the absence of a check visible |
18038
+ | `CliTrustRoot` / `CliSigningKey` / `CliBuildSignature` / `CliBuildSignatureSchema` | _types_ / schema | the keys a build trusts, by id, and the detached signature a manifest carries |
17694
18039
  | `selectCliBuildAsset` | function | the asset for one target, or `undefined` |
17695
18040
  | `currentCliBuildTarget` | function | `{ platform, arch }` of the running process |
17696
18041
  | `formatCliBuildStamp` | function | one line saying what the running build is |
@@ -17709,6 +18054,11 @@ SDK nor the `ai` peer.
17709
18054
  | `CliArgvRoute` | _type_ | `{ command, commandArgv, topLevelHelp, version, error? }` returned by `routeCliArgv` |
17710
18055
  | `pollUntilDone` | function | the generic `--wait` poller (advanced) |
17711
18056
  | `emitResult` | function | write a pretty or compact `ToolResult` record to stdout/stderr + exit code (advanced) |
18057
+ | `cliExitCode` | function | the exit code a result earns, from the same table, without writing anything — so an in-process run and the printed path cannot disagree |
18058
+ | `createCliInvoker` / `CliInvoker` / `CliInvokerConfig` / `CliInvocationResult` / `CliInvokerCommand` / `cliInvocationResult` | function / _types_ | compile the managed surface once and run parsed calls against it in process — no argv round-trip, no child process, no writing |
18059
+ | `defineCliStreamCommand` / `CliStreamCommandConfig` / `CliStreamAnswer` / `CliStreamHooks` / `runCliStream` | function / _types_ | one operation per JSON line of stdin, answered with the same `id`; a bad line answers and the stream continues |
18060
+ | `defineCliBatchCommand` / `CliBatchCommandConfig` | function / _type_ | the same loop, resumable: a recorded line replays, a line whose content changed under the same id is refused |
18061
+ | `readCliCheckpoint` / `writeCliCheckpoint` / `CliCheckpoint` / `CliCheckpointEntry` | function / _types_ | the batch's record — id plus a digest of the line, written by atomic rename after every line |
17712
18062
  | `DEFAULT_EXIT_CODES` | const | the default `ToolResult.code` → exit-code map |
17713
18063
  | `CliConfig` | _type_ | config for `createCli`; `defaultCommand`, `globalOptions`, `optionAliases` and `positionals` define the shared command presentation policy |
17714
18064
  | `CliPresentationPolicyConfig` | _type_ | shared command presentation-policy subset of `CliConfig` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.91.1",
3
+ "version": "0.92.0",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",