stitchkit 0.113.1 → 0.113.3

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 (79) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/contract/errors.d.ts +2 -0
  3. package/dist/contract/errors.d.ts.map +1 -1
  4. package/dist/entrypoints/agent-runtime/browser.js +2 -2
  5. package/dist/entrypoints/agent-runtime/coding-tools.js +4 -4
  6. package/dist/entrypoints/agent-runtime/harness.js +5 -5
  7. package/dist/entrypoints/agent-runtime/realtime.js +2 -2
  8. package/dist/entrypoints/agent-runtime/sandbox.js +4 -4
  9. package/dist/entrypoints/agent-runtime/sqlite/bun.js +6 -6
  10. package/dist/entrypoints/agent-runtime/sqlite/node.js +6 -6
  11. package/dist/entrypoints/agent-runtime/testing.js +6 -6
  12. package/dist/entrypoints/agent-runtime.js +11 -11
  13. package/dist/entrypoints/application/grammy.js +1 -1
  14. package/dist/entrypoints/application.js +2 -2
  15. package/dist/entrypoints/cli.js +8 -8
  16. package/dist/entrypoints/contract.js +2 -2
  17. package/dist/entrypoints/files.js +1 -1
  18. package/dist/entrypoints/index.js +5 -5
  19. package/dist/entrypoints/node.js +7 -7
  20. package/dist/entrypoints/observability.js +1 -1
  21. package/dist/entrypoints/realtime.js +3 -3
  22. package/dist/entrypoints/remote.js +1 -1
  23. package/dist/entrypoints/server.js +8 -8
  24. package/dist/entrypoints/testing.js +3 -3
  25. package/dist/entrypoints/tools/connections.js +5 -5
  26. package/dist/entrypoints/tools/invoker.js +5 -5
  27. package/dist/entrypoints/tools/mcp.js +7 -7
  28. package/dist/entrypoints/tools.js +13 -13
  29. package/dist/{index-8jjzfts8.js → index-095ya5hm.js} +5 -5
  30. package/dist/{index-sjww6myv.js → index-1csp1hwm.js} +4 -4
  31. package/dist/{index-xmgjtkvr.js → index-2bqf0a74.js} +1 -1
  32. package/dist/{index-ykdcnq1f.js → index-3za04re9.js} +1 -1
  33. package/dist/{index-yjnmac9b.js → index-523tq7y4.js} +1 -1
  34. package/dist/{index-mhrtwttb.js → index-7fc0c2np.js} +1 -1
  35. package/dist/{index-emrmgq3s.js → index-86nbfysb.js} +3 -3
  36. package/dist/{index-556g5hsh.js → index-98hv3sba.js} +1 -1
  37. package/dist/{index-h1spd9kv.js → index-9dyd8xzf.js} +1 -1
  38. package/dist/{index-gz9jf461.js → index-aykhzcnf.js} +1 -1
  39. package/dist/{index-86sxnvpr.js → index-bjcjqj46.js} +1 -1
  40. package/dist/{index-pzw0fpek.js → index-cqmqyjax.js} +1 -1
  41. package/dist/{index-nk1mh535.js → index-ega3yz4c.js} +1 -1
  42. package/dist/{index-k8fbzggr.js → index-fy4av0hq.js} +1 -1
  43. package/dist/{index-v6mzw27b.js → index-htxh46ej.js} +3 -3
  44. package/dist/{index-pvh3z03x.js → index-jk29rwkr.js} +1 -1
  45. package/dist/{index-fynr93w4.js → index-k42ckyzx.js} +1 -1
  46. package/dist/{index-fytsnhzj.js → index-k7z3j3hr.js} +211 -125
  47. package/dist/{index-j9gxbztg.js → index-qcn84byh.js} +2 -0
  48. package/dist/{index-jgwqc38k.js → index-rhzkndr5.js} +2 -2
  49. package/dist/{index-k0zfcxf5.js → index-tpwbct8j.js} +1 -1
  50. package/dist/{index-8asb62xn.js → index-v1tswj0z.js} +1 -1
  51. package/dist/{index-tg2q89ps.js → index-v6vb5qx8.js} +4 -4
  52. package/dist/{index-2qymcsq2.js → index-vtann8bz.js} +3 -3
  53. package/dist/{index-nn6607y3.js → index-vwhrnp67.js} +4 -4
  54. package/dist/{index-dsxrcseb.js → index-zktz242w.js} +2 -2
  55. package/dist/tools/cli/command-shape.d.ts +4 -0
  56. package/dist/tools/cli/command-shape.d.ts.map +1 -0
  57. package/dist/tools/cli/command.d.ts +2 -1
  58. package/dist/tools/cli/command.d.ts.map +1 -1
  59. package/dist/tools/cli/format.d.ts +5 -1
  60. package/dist/tools/cli/format.d.ts.map +1 -1
  61. package/dist/tools/cli/help.d.ts.map +1 -1
  62. package/dist/tools/cli/invoke.d.ts +1 -1
  63. package/dist/tools/cli/invoke.d.ts.map +1 -1
  64. package/dist/tools/cli/managed-surface.d.ts +3 -2
  65. package/dist/tools/cli/managed-surface.d.ts.map +1 -1
  66. package/dist/tools/cli/policy.d.ts +2 -0
  67. package/dist/tools/cli/policy.d.ts.map +1 -1
  68. package/dist/tools/cli/run-managed.d.ts.map +1 -1
  69. package/dist/tools/cli/run-native.d.ts.map +1 -1
  70. package/dist/tools/cli/session.d.ts +0 -1
  71. package/dist/tools/cli/session.d.ts.map +1 -1
  72. package/dist/tools/cli/unavailable.d.ts +10 -0
  73. package/dist/tools/cli/unavailable.d.ts.map +1 -0
  74. package/llms/cli.2.txt +282 -1
  75. package/llms/cli.txt +49 -287
  76. package/llms/server.4.txt +1 -1
  77. package/llms-full.txt +51 -8
  78. package/llms.txt +1 -1
  79. package/package.json +1 -1
package/llms/cli.txt CHANGED
@@ -213,9 +213,11 @@ const doctor = defineCliCommand({
213
213
  it the canonical JSON output is unchanged. `exitCode` classifies a successfully
214
214
  validated result and must return an integer in `0..255`; failed `ToolResult`
215
215
  envelopes and the application-wide `exitCodes` mapping remain authoritative for
216
- failures. A throwing callback, invalid status or non-string presenter becomes a
217
- normalized `INTERNAL_SERVER_ERROR`, never partial success output. Void native
218
- commands cannot declare either callback.
216
+ failures. A throwing or invalid exit-status callback becomes a normalized
217
+ `INTERNAL_SERVER_ERROR`. A throwing or non-string `present` preserves the validated
218
+ result on stdout and reports `CLI_PRESENTATION_FAILED` separately, as described
219
+ under [Aggregate views](#aggregate-views). Void native commands cannot declare
220
+ either callback.
219
221
 
220
222
  `--version`, a selected native command and its command help run before
221
223
  `resolveAuth`, services, context or runtime-tool factories. Top-level help is
@@ -430,6 +432,7 @@ newline-terminated JSON record on its respective stream; progress and CLI usage
430
432
  diagnostics remain ordinary stderr text. This keeps stdout pipeable and
431
433
  `2>/dev/null` clean. The process exit code carries the error class (`0` ok,
432
434
  `VALIDATION_ERROR → 1`, `UNAUTHORIZED → 2`, `FORBIDDEN → 3`, `NOT_FOUND → 4`,
435
+ `CLI_PRESENTATION_FAILED → 8`,
433
436
  …) — override per app with `exitCodes`.
434
437
 
435
438
  ### A narrower question than "all of them"
@@ -490,13 +493,42 @@ command name — validated against the declared schema, and kept out of every
490
493
  operation's arguments: `handoff_read` above receives `{ handoffId: 'u' }` and
491
494
  nothing else. The values reach `resolveAuth(globals)`, `context(auth, globals)`
492
495
  and a native command's `globals`. An invalid value is an argument error naming
493
- the flag; a name that collides with a framework option, or with a field of any
494
- command, is refused at startup rather than shadowing it silently. Past a bare
496
+ the flag. An application global that collides with a framework option is refused
497
+ at startup. Field collisions in authored commands are configuration errors;
498
+ runtime catalogue entries are isolated as described below. Past a bare
495
499
  `--` every token is a literal, so a positional value that reads like a global
496
500
  survives intact.
497
501
 
498
502
  Both help levels list them under `Application options:`.
499
503
 
504
+ ## When one runtime command cannot be represented in argv
505
+
506
+ A `runtimeTools` catalogue can change independently of the CLI application. An
507
+ entry whose input declares a framework option such as `json`, or a field shadowed
508
+ by an application global such as `caller`, cannot receive that field through argv.
509
+ Only that entry becomes unavailable. Other commands still run, and top-level,
510
+ filtered and command-specific help retain the entry and its full reason:
511
+
512
+ ```text
513
+ inspect Inspect an operation [unavailable]
514
+ Unavailable: [stitchkit] CLI command "inspect" declares field(s) shadowed by application global options: caller
515
+ ```
516
+
517
+ Calling it returns `CLI_COMMAND_UNAVAILABLE`, `retryable: false`, and the same
518
+ reason in `details.message` on stderr; the default exit code is 1. `--json`
519
+ compacts the error as usual; `exitCodes` and `errorHint` apply. The refusal precedes
520
+ command argument parsing, stdin, context, hooks and the handler; `--dry-run` cannot
521
+ turn it into success. A known unavailable `--wait` target returns the same refusal;
522
+ an unknown target keeps `NOT_FOUND`.
523
+
524
+ This isolates only reserved/global **field** conflicts in `runtimeTools`.
525
+ Native commands and contract-defined `services` still fail at startup for those
526
+ conflicts. Duplicate or reserved command names and invalid local aliases,
527
+ positionals or passthrough policies remain configuration errors for every entry.
528
+ `createCliInvoker` receives globals and command input separately and has no argv
529
+ projection, so its own `caller` or `json` input stays callable without renaming.
530
+ See [ADR 0273](../decisions/0273-a-runtime-cli-field-conflict-is-one-commands-unavailability.md).
531
+
500
532
  ## When the managed surface cannot resolve
501
533
 
502
534
  A CLI whose commands come from a running server declares them with a factory,
@@ -622,7 +654,7 @@ would answer a question nobody asked.
622
654
 
623
655
  Three rules worth knowing before you rely on them:
624
656
 
625
- - **A field the result does not carry is an argument error.** A group of zero
657
+ - **A field the result does not carry is a presentation error.** A group of zero
626
658
  over a misspelled field is indistinguishable from a true empty answer, and the
627
659
  caller reads it as data. The message names the fields that *are* there.
628
660
  - **An aggregate needs a collection** — the result itself when it is an array,
@@ -636,6 +668,17 @@ Three rules worth knowing before you rely on them:
636
668
  A failed call still reports its own error and exit code. An aggregate over an
637
669
  error is not an answer to the question that was asked.
638
670
 
671
+ If the command succeeds but its requested view cannot be built, the CLI writes
672
+ the complete original result to stdout, using the normal JSON format (`--json`
673
+ still selects compact output). It writes a separate structured
674
+ `CLI_PRESENTATION_FAILED` diagnostic to stderr and exits with `8`, configurable
675
+ through `exitCodes`. Its `details.operationSucceeded: true` and `retryable: false`
676
+ distinguish a presentation refusal from a failed operation; `details.cause`
677
+ preserves the safe reason. Recover the answer from stdout instead of repeating
678
+ the mutation. Domain retry hints are not appended to this diagnostic. CLI
679
+ argument errors detected before execution still exit with `2`
680
+ and have no successful result to preserve.
681
+
639
682
  ## Named profiles
640
683
 
641
684
  A CLI that talks to a deployed server needs an address and a credential per
@@ -681,284 +724,3 @@ await createCli({
681
724
  Files are written `0600` in a `0700` directory — and a profile file other users
682
725
  can read is refused with the `chmod` that fixes it, because it holds a
683
726
  credential.
684
-
685
- ## Distribution and self-update
686
-
687
- `createCli` ships no executable, and that is right — but the step after the
688
- executable is not application logic either. It is the same problem for every
689
- consumer, with the same three traps:
690
-
691
- 1. **The installer cannot parse the manifest.** A `curl … | sh` runs on a
692
- machine where nothing is installed yet, including `jq`. So the installer is
693
- generated *from* the manifest, server-side, with the URL and digest already
694
- substituted — it parses no JSON at all.
695
- 2. **Replacing a running binary is a rename, not a write.** Anything else can
696
- leave a half-written executable on someone's PATH when the connection drops.
697
- 3. **The digest covers the decompressed bytes** — the file that will actually be
698
- executed, not the archive that was transferred.
699
-
700
- The framework owns the manifest shape, the installer generation and the update
701
- primitive. The application owns where the assets live, which platforms it
702
- publishes and who may download them.
703
-
704
- ```ts
705
- import {
706
- CliBuildManifestSchema, assertCliPublishable, renderCliInstaller,
707
- selectCliBuildAsset, checkCliUpdate, applyCliUpdate,
708
- } from 'stitchkit/cli'
709
-
710
- // Publishing: refuse to republish one version from a different commit —
711
- // otherwise everyone who already installed it never receives the fix.
712
- assertCliPublishable(previous, next)
713
-
714
- // Serving: omit `asset` and one script covers every published target, selecting
715
- // by uname at run time — otherwise that dispatch is the last hand-written piece
716
- // of the install path, and every publisher writes the same x86_64 → x64 table.
717
- renderCliInstaller({ manifest, binaryName: 'myapp' })
718
- // Or pin one target explicitly:
719
- renderCliInstaller({ manifest, asset: selectCliBuildAsset(manifest, target), binaryName: 'myapp' })
720
-
721
- // Checking: bounded, at most once per interval, silent on any failure.
722
- const check = await checkCliUpdate({ manifestUrl, currentVersion, lastCheckedAt })
723
- if (check.status === 'outdated' && check.asset) {
724
- // Replacing is always an explicit command, never a side effect of a check.
725
- await applyCliUpdate({ asset: check.asset })
726
- }
727
- ```
728
-
729
- `checkCliUpdate` has **four** answers, not three: `skipped`, `current`,
730
- `outdated` and `unknown`. "Could not ask" is not "up to date" — collapsing them
731
- is how a tool goes quiet about its own staleness for months. It never throws,
732
- and a command still exits with the code it earned.
733
-
734
- Carry the build stamp inside the binary (`CliBuildStampSchema`,
735
- `formatCliBuildStamp`) so the tool can say what it is rather than leaving the
736
- reader to infer it from behaviour.
737
-
738
- #### A manifest inside your own network
739
-
740
- The check goes through the same SSRF boundary every outbound fetch does, so a
741
- manifest served from `10.x`, `localhost` or a `.internal` name is refused and
742
- the answer is `unknown`. For a self-hosted deployment that is a wrong answer
743
- dressed as a careful one, and the refusal says so — it names the field that
744
- lifts it rather than reading like a timeout:
745
-
746
- ```text
747
- refusing to fetch an internal host — the manifest endpoint is not public.
748
- Set `allowPrivateHosts` if it is your own deployment; the download keeps its
749
- own setting, because an asset URL comes from the document.
750
- ```
751
-
752
- ```ts
753
- const check = await checkCliUpdate({ manifestUrl, currentVersion, allowPrivateHosts: true })
754
- ```
755
-
756
- Set it from whether **your** endpoint is private, not as a blanket switch, and
757
- leave `applyCliUpdate` at its default. The two are not the same decision: the
758
- manifest URL is configuration you wrote, while an asset URL arrives inside a
759
- document the endpoint served, and a document is exactly what an SSRF boundary
760
- exists to distrust.
761
-
762
- ### Publishing a complete version
763
-
764
- `publishCli` is an opt-in publisher for a local application-owned distribution
765
- directory. It commits every target and its version manifest before atomically
766
- replacing the public `manifest.json`. It ships from its own evolving entrypoint,
767
- `stitchkit/cli/publish`: release infrastructure follows how distributions are
768
- published, so it is not held to the stable promise of `stitchkit/cli`
769
- ([ADR 0245](../decisions/0245-cli-publication-is-an-evolving-leaf.md)). The
770
- manifest, signatures, installer and updater it writes for stay in
771
- `stitchkit/cli`. Your application selects the CLI source identity and admits
772
- that snapshot at each phase:
773
-
774
- ```ts
775
- import { publishCli } from 'stitchkit/cli/publish'
776
-
777
- const result = await publishCli({
778
- storageRoot: '/srv/myapp/cli',
779
- name: 'myapp', version: cliVersion, commit: selectedCliCommit,
780
- baseUrl: 'https://downloads.example.com/cli/',
781
- targets: [{ platform: 'linux', arch: 'x64' }],
782
- signal,
783
- admit: ({ phase, identity, signal }) => assertSelectedSource({ phase, identity, signal }),
784
- build: ({ target, stamp, signal }) => compileSelectedSource({ target, stamp, signal }),
785
- })
786
- // result.outcome: 'published' | 'existing'; result.manifest is ready to serve.
787
- ```
788
-
789
- The builder returns `Uint8Array` or `ReadableStream<Uint8Array>` containing the
790
- executable. Every target receives the same immutable `{ version, commit,
791
- builtAt }` stamp; carry it inside the binary. The publisher owns staging,
792
- gzip assets, paths and manifests. Callbacks receive the cancellation signal and
793
- run under the publication lock. Admission phases are `prepare`, each target's
794
- `build`, `commit`, and `promote`; validate the selected source through commit
795
- and promotion. A backend release can explicitly select an unchanged older CLI
796
- identity. The helper never substitutes a different commit or rebuilds an
797
- existing version under a new identity.
798
-
799
- Assets live at `<version>/<name>-<platform>-<arch>.gz`, with an immutable
800
- `<version>/manifest.json` and a public `manifest.json`. A verified repeat keeps
801
- the exact bytes and `builtAt`, requires the same target set and base URL, and
802
- returns `existing` without invoking the builder. A version committed before a
803
- failed promotion is recovered and promoted without rebuilding. Conflicting
804
- same-version commits, downgrades, unsafe components, corrupt manifests and
805
- assets are refused. Version/name/target components are validated before any
806
- build; `storageRoot` is absolute and its existing parent must be trusted.
807
-
808
- All limits are finite positive safe integers; `limits` overrides these defaults:
809
-
810
- | Field | Default | Enforced boundary |
811
- | --- | --- | --- |
812
- | `maxTargets` | 16 | Requested and verified target count |
813
- | `maxManifestBytes` | 256 KiB | Read and written manifest bytes |
814
- | `maxAssetBytes` | 256 MiB | Builder and decompressed executable bytes |
815
- | `maxCompressedBytes` | 256 MiB | Archived asset bytes |
816
- | `maxDirectoryEntries` | 128 | Version layout, root scan and commit space |
817
- | `maxStoredVersions` | 16 | Verified publisher-owned history |
818
- | `lockTimeoutMs` | 10,000 | Wait to acquire the lock |
819
- | `timeoutMs` | 600,000 | Whole publication deadline |
820
-
821
- `retention` defaults to 2 and keeps the current and previous published versions.
822
- It must be at least 2 and less than `maxStoredVersions` (at least 3), leaving a
823
- recovery slot. Older versions are pruned only after their own identity, exact
824
- layout, raw size and digest are verified. Runtime folders and foreign semver
825
- directories remain untouched. A full or unverifiable store refuses further
826
- publication; repeated completed candidates stay inside the owned history cap.
827
- Cancelled or failed incomplete staging is removed only while its root and
828
- directory generations still match. The shared asynchronous gzip decoder also
829
- bounds updater expansion by the declared raw size and configured byte cap.
830
-
831
- The publisher rejects symlink roots/versions/assets, non-regular files and
832
- multiply linked files, and checks held-lock, root and prior-pointer generations
833
- before commit and promotion. This is cooperative publication under a trusted
834
- parent directory. Portable path checks do not provide an atomic comparison with
835
- a hostile process replacing ancestors between syscalls. Durability uses file
836
- and directory sync on POSIX; Windows publication is refused. A deadline stops
837
- callback/stream waits and blocks late promotion; trusted callbacks remain
838
- responsible for cancelling their own subprocesses and external effects.
839
-
840
- A failure after an immutable commit reports that the publication outcome must
841
- be checked. A failure after public-pointer rename may already have published;
842
- `AtomicFilePublicationError.published` remains available when the atomic writer
843
- reports that boundary. Read the durable manifest and retry the admitted same
844
- identity to recover; an error does not promise rollback. Optional `signing` and
845
- `trust` reuse the Ed25519 contract below. The generated shell installer checks
846
- raw digest and size; the updater checks manifest authorship when given a trust
847
- root.
848
-
849
- ### Proving who built it — signing the manifest
850
-
851
- The asset digest proves the bytes that arrived are the bytes the manifest named.
852
- It proves nothing about who named them: the manifest and the assets come from
853
- one origin, so whoever can replace one can replace the other. Authorship needs a
854
- key the build carries and the server never holds.
855
-
856
- ```ts
857
- // Publishing — the only side that holds a private key.
858
- const signature = signCliManifest(manifest, { keyId: 'release-2026', privateKey })
859
- publish({ ...manifest, signature })
860
-
861
- // Installing — the trust root is compiled into the binary. One fetched at run
862
- // time from the same origin as the manifest would prove nothing.
863
- const trust = { keys: { 'release-2026': PUBLISHED_KEY } }
864
- const check = await checkCliUpdate({ manifestUrl, currentVersion, trust })
865
- if (check.status === 'outdated' && check.asset) {
866
- await applyCliUpdate({ asset: check.asset, manifest: check.manifest, trust, backupPath })
867
- }
868
- ```
869
-
870
- The signature covers `{name, version, commit, builtAt, assets[]}` with **every
871
- asset's digest**, so the chain closes on the file that will execute rather than
872
- on the document describing it; moving an asset to a new URL does not invalidate
873
- it, changing what an asset contains does.
874
-
875
- Five verdicts, and `unenforced` is the one that earns its keep: a build with no
876
- pinned key keeps updating, and the fact that nothing was checked is *visible*
877
- rather than assumed. `missing`, `unknown-key` and `invalid` are distinguished
878
- because they need different answers — a key you forgot to pin is not a forged
879
- signature.
880
-
881
- A bad verdict does not produce a fifth status. `outdated` is an instruction to
882
- install, and a manifest that failed its signature has not established that there
883
- is a newer build worth installing — only that a document claims one. The check
884
- answers `unknown` with the verdict in its reason, so it is never mistaken for a
885
- network failure. `applyCliUpdate` checks again and refuses **before** the
886
- download: a signature verified after the bytes are on the machine produces a
887
- cleanup problem rather than a refusal.
888
-
889
- ### A way back — `backupPath` and `rollbackCliUpdate`
890
-
891
- Rolling back is a property of updating, not of the application:
892
-
893
- ```ts
894
- const applied = await applyCliUpdate({ asset, targetPath, backupPath })
895
- // …the new build turns out to be wrong. `backupSha256` is absent when there was
896
- // nothing to keep — a first install — and that is the case with nothing to roll
897
- // back to:
898
- if (applied.backupSha256) {
899
- rollbackCliUpdate({ targetPath, backupPath, expectedSha256: applied.backupSha256 })
900
- }
901
- ```
902
-
903
- The copy is taken between "the new bytes verified" and the replacement — earlier
904
- would preserve a binary about to be replaced by a download that then failed its
905
- digest, later has nothing left to copy — and it carries the target's file mode,
906
- because a backup that cannot be executed is not a way back. A first install has
907
- nothing to keep, which is not a failure.
908
-
909
- `expectedSha256` is required, not optional. A rollback that installs whatever
910
- happens to be at the backup path is a second install of an unverified binary,
911
- and the day it is used is the day nobody is in a position to check.
912
-
913
- ### Run it before it replaces anything — `verify`
914
-
915
- A digest proves the bytes are the ones published, not that they load. A tool
916
- that updates itself unattended — a daemon — and installs a build that does not
917
- start takes its own updater down with it, and every machine is then repaired by
918
- hand. `verify` is the step between "digest matched" and "target replaced":
919
-
920
- ```ts
921
- await applyCliUpdate({
922
- asset, targetPath, backupPath,
923
- verify: async (candidate, signal) => {
924
- const child = Bun.spawn(['bun', candidate, 'version'], { signal, stdout: 'pipe' })
925
- const printed = (await new Response(child.stdout).text()).trim()
926
- if ((await child.exited) !== 0 || printed !== check.version) {
927
- throw new Error(`printed ${printed}, expected ${check.version}`)
928
- }
929
- },
930
- verifyTimeoutMs: 15_000, // default 30 000
931
- })
932
- ```
933
-
934
- The verified bytes are written beside the target as an executable candidate
935
- with the target's extension, and `verify` gets its path — a path rather than a
936
- command, because a JavaScript bundle is started through its runtime. It runs
937
- before the backup and the replacement. If it throws or outlives
938
- `verifyTimeoutMs` (its `signal` aborts at the deadline), the candidate is
939
- removed, the target and any earlier backup are untouched, and the update is
940
- refused with the reason as its cause. The target is written from the checked
941
- bytes in memory, never from the candidate file, so nothing the candidate did
942
- while it ran can change what is installed.
943
-
944
- ### Two release tracks
945
-
946
- `assertCliPublishable` takes one manifest **or every manifest already
947
- published**:
948
-
949
- ```ts
950
- assertCliPublishable([stableManifest, betaManifest], next)
951
- ```
952
-
953
- With a single argument and two tracks the check is silently useless: `1.2.3`
954
- goes to beta from one commit and to stable from another, the function is called
955
- with the track being published and sees no conflict, and the person who
956
- installed the beta is told they are current forever. Nothing on their machine
957
- looks wrong.
958
-
959
- **The channel itself is not an argument of the framework, and will not become
960
- one.** The framework has no model of how your URLs are built and should not: an
961
- asset's address comes from the manifest, and the manifest's address belongs to
962
- your server. A channel is two documents at two addresses — which of them a build
963
- consults is your decision, and `assertCliPublishable` is told about all of them
964
- rather than taught the shape of any.
package/llms/server.4.txt CHANGED
@@ -553,7 +553,7 @@ core never models them (ADR 0002). But stitchkit itself emits a set of its own:
553
553
  `FILE_INSPECTION_REJECTED`, `FILE_TOO_LARGE`, `FILE_EXISTS`,
554
554
  `REALTIME_CONTRACT_VIOLATION`, `APPLICATION_NOT_ACCEPTING`, `NOT_IMPLEMENTED`,
555
555
  `WAIT_TIMEOUT`,
556
- `WAIT_FAILED`, `DOWNLOAD_NOT_FOUND`, `VIEW_HTTP_ERROR`,
556
+ `WAIT_FAILED`, `CLI_COMMAND_UNAVAILABLE`, `CLI_PRESENTATION_FAILED`, `DOWNLOAD_NOT_FOUND`, `VIEW_HTTP_ERROR`,
557
557
  `CONNECTION_TIMEOUT`, `CONNECTION_RESPONSE_TOO_LARGE`,
558
558
  `CONNECTION_REQUEST_FAILED`, `UPSTREAM_TOOL_ERROR`,
559
559
  `OPERATION_NOT_SUCCEEDED`, `STREAM_ITEM_INVALID`, `STREAM_FRAME_TOO_LARGE`,
package/llms-full.txt CHANGED
@@ -10012,9 +10012,11 @@ const doctor = defineCliCommand({
10012
10012
  it the canonical JSON output is unchanged. `exitCode` classifies a successfully
10013
10013
  validated result and must return an integer in `0..255`; failed `ToolResult`
10014
10014
  envelopes and the application-wide `exitCodes` mapping remain authoritative for
10015
- failures. A throwing callback, invalid status or non-string presenter becomes a
10016
- normalized `INTERNAL_SERVER_ERROR`, never partial success output. Void native
10017
- commands cannot declare either callback.
10015
+ failures. A throwing or invalid exit-status callback becomes a normalized
10016
+ `INTERNAL_SERVER_ERROR`. A throwing or non-string `present` preserves the validated
10017
+ result on stdout and reports `CLI_PRESENTATION_FAILED` separately, as described
10018
+ under [Aggregate views](#aggregate-views). Void native commands cannot declare
10019
+ either callback.
10018
10020
 
10019
10021
  `--version`, a selected native command and its command help run before
10020
10022
  `resolveAuth`, services, context or runtime-tool factories. Top-level help is
@@ -10229,6 +10231,7 @@ newline-terminated JSON record on its respective stream; progress and CLI usage
10229
10231
  diagnostics remain ordinary stderr text. This keeps stdout pipeable and
10230
10232
  `2>/dev/null` clean. The process exit code carries the error class (`0` ok,
10231
10233
  `VALIDATION_ERROR → 1`, `UNAUTHORIZED → 2`, `FORBIDDEN → 3`, `NOT_FOUND → 4`,
10234
+ `CLI_PRESENTATION_FAILED → 8`,
10232
10235
  …) — override per app with `exitCodes`.
10233
10236
 
10234
10237
  ### A narrower question than "all of them"
@@ -10289,13 +10292,42 @@ command name — validated against the declared schema, and kept out of every
10289
10292
  operation's arguments: `handoff_read` above receives `{ handoffId: 'u' }` and
10290
10293
  nothing else. The values reach `resolveAuth(globals)`, `context(auth, globals)`
10291
10294
  and a native command's `globals`. An invalid value is an argument error naming
10292
- the flag; a name that collides with a framework option, or with a field of any
10293
- command, is refused at startup rather than shadowing it silently. Past a bare
10295
+ the flag. An application global that collides with a framework option is refused
10296
+ at startup. Field collisions in authored commands are configuration errors;
10297
+ runtime catalogue entries are isolated as described below. Past a bare
10294
10298
  `--` every token is a literal, so a positional value that reads like a global
10295
10299
  survives intact.
10296
10300
 
10297
10301
  Both help levels list them under `Application options:`.
10298
10302
 
10303
+ ## When one runtime command cannot be represented in argv
10304
+
10305
+ A `runtimeTools` catalogue can change independently of the CLI application. An
10306
+ entry whose input declares a framework option such as `json`, or a field shadowed
10307
+ by an application global such as `caller`, cannot receive that field through argv.
10308
+ Only that entry becomes unavailable. Other commands still run, and top-level,
10309
+ filtered and command-specific help retain the entry and its full reason:
10310
+
10311
+ ```text
10312
+ inspect Inspect an operation [unavailable]
10313
+ Unavailable: [stitchkit] CLI command "inspect" declares field(s) shadowed by application global options: caller
10314
+ ```
10315
+
10316
+ Calling it returns `CLI_COMMAND_UNAVAILABLE`, `retryable: false`, and the same
10317
+ reason in `details.message` on stderr; the default exit code is 1. `--json`
10318
+ compacts the error as usual; `exitCodes` and `errorHint` apply. The refusal precedes
10319
+ command argument parsing, stdin, context, hooks and the handler; `--dry-run` cannot
10320
+ turn it into success. A known unavailable `--wait` target returns the same refusal;
10321
+ an unknown target keeps `NOT_FOUND`.
10322
+
10323
+ This isolates only reserved/global **field** conflicts in `runtimeTools`.
10324
+ Native commands and contract-defined `services` still fail at startup for those
10325
+ conflicts. Duplicate or reserved command names and invalid local aliases,
10326
+ positionals or passthrough policies remain configuration errors for every entry.
10327
+ `createCliInvoker` receives globals and command input separately and has no argv
10328
+ projection, so its own `caller` or `json` input stays callable without renaming.
10329
+ See [ADR 0273](../decisions/0273-a-runtime-cli-field-conflict-is-one-commands-unavailability.md).
10330
+
10299
10331
  ## When the managed surface cannot resolve
10300
10332
 
10301
10333
  A CLI whose commands come from a running server declares them with a factory,
@@ -10421,7 +10453,7 @@ would answer a question nobody asked.
10421
10453
 
10422
10454
  Three rules worth knowing before you rely on them:
10423
10455
 
10424
- - **A field the result does not carry is an argument error.** A group of zero
10456
+ - **A field the result does not carry is a presentation error.** A group of zero
10425
10457
  over a misspelled field is indistinguishable from a true empty answer, and the
10426
10458
  caller reads it as data. The message names the fields that *are* there.
10427
10459
  - **An aggregate needs a collection** — the result itself when it is an array,
@@ -10435,6 +10467,17 @@ Three rules worth knowing before you rely on them:
10435
10467
  A failed call still reports its own error and exit code. An aggregate over an
10436
10468
  error is not an answer to the question that was asked.
10437
10469
 
10470
+ If the command succeeds but its requested view cannot be built, the CLI writes
10471
+ the complete original result to stdout, using the normal JSON format (`--json`
10472
+ still selects compact output). It writes a separate structured
10473
+ `CLI_PRESENTATION_FAILED` diagnostic to stderr and exits with `8`, configurable
10474
+ through `exitCodes`. Its `details.operationSucceeded: true` and `retryable: false`
10475
+ distinguish a presentation refusal from a failed operation; `details.cause`
10476
+ preserves the safe reason. Recover the answer from stdout instead of repeating
10477
+ the mutation. Domain retry hints are not appended to this diagnostic. CLI
10478
+ argument errors detected before execution still exit with `2`
10479
+ and have no successful result to preserve.
10480
+
10438
10481
  ## Named profiles
10439
10482
 
10440
10483
  A CLI that talks to a deployed server needs an address and a credential per
@@ -13068,7 +13111,7 @@ core never models them (ADR 0002). But stitchkit itself emits a set of its own:
13068
13111
  `FILE_INSPECTION_REJECTED`, `FILE_TOO_LARGE`, `FILE_EXISTS`,
13069
13112
  `REALTIME_CONTRACT_VIOLATION`, `APPLICATION_NOT_ACCEPTING`, `NOT_IMPLEMENTED`,
13070
13113
  `WAIT_TIMEOUT`,
13071
- `WAIT_FAILED`, `DOWNLOAD_NOT_FOUND`, `VIEW_HTTP_ERROR`,
13114
+ `WAIT_FAILED`, `CLI_COMMAND_UNAVAILABLE`, `CLI_PRESENTATION_FAILED`, `DOWNLOAD_NOT_FOUND`, `VIEW_HTTP_ERROR`,
13072
13115
  `CONNECTION_TIMEOUT`, `CONNECTION_RESPONSE_TOO_LARGE`,
13073
13116
  `CONNECTION_REQUEST_FAILED`, `UPSTREAM_TOOL_ERROR`,
13074
13117
  `OPERATION_NOT_SUCCEEDED`, `STREAM_ITEM_INVALID`, `STREAM_FRAME_TOO_LARGE`,
@@ -23593,7 +23636,7 @@ SDK nor the `ai` peer.
23593
23636
 
23594
23637
  | Export | Kind | Summary |
23595
23638
  |--------|------|---------|
23596
- | `createCli` | function | build and run a CLI from contracts — [guide](../guide/cli.md) |
23639
+ | `createCli` | function | build and run a CLI from contracts; a runtime catalogue field conflicting with reserved/global argv options disables only that entry, retained in help with its reason; invocation returns `CLI_COMMAND_UNAVAILABLE` before side effects — [guide](../guide/cli.md#when-one-runtime-command-cannot-be-represented-in-argv) |
23597
23640
  | `defineCliCommand` | function | define one Zod-typed CLI-only executable command with optional validated-result presentation/exit policy |
23598
23641
  | `parseCliArgs` | function | argv → typed tool args against a schema; inline `--field:=JSON` assigns a whole JSON value, including null, while ordinary string flags remain lexical; named array options together with bare positionals require an explicit `positionals` policy (advanced) |
23599
23642
  | `routeCliArgv` | function | select the command out of argv without duplicating the global-option grammar (advanced) |
package/llms.txt CHANGED
@@ -18,7 +18,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
18
18
  - **stitchkit/tools/contract**: [llms/tools-contract.txt](llms/tools-contract.txt) 0.2 KB
19
19
  - **stitchkit/tools/invoker**: [llms/tools-invoker.txt](llms/tools-invoker.txt) 0.2 KB
20
20
  - **stitchkit/tools/connections**: [llms/tools-connections.txt](llms/tools-connections.txt) 3.9 KB
21
- - **stitchkit/cli**: [llms/cli.txt](llms/cli.txt) 45.3 KB → [llms/cli.2.txt](llms/cli.2.txt) 22.3 KB
21
+ - **stitchkit/cli**: [llms/cli.txt](llms/cli.txt) 33.9 KB → [llms/cli.2.txt](llms/cli.2.txt) 36.4 KB
22
22
  - **stitchkit/cli/publish**: [llms/cli-publish.txt](llms/cli-publish.txt) 1.5 KB
23
23
  - **stitchkit/remote**: [llms/remote.txt](llms/remote.txt) 1.0 KB
24
24
  - **stitchkit/contract**: [llms/contract.txt](llms/contract.txt) 31.9 KB
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.113.1",
3
+ "version": "0.113.3",
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",