@intentic/sandbox-contract 1.222.0 → 1.224.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 (86) hide show
  1. package/dist/chores/chores.js +1 -1
  2. package/dist/chores/chores.js.map +1 -1
  3. package/dist/chores/prompt.d.ts.map +1 -1
  4. package/dist/chores/prompt.js +3 -3
  5. package/dist/chores/prompt.js.map +1 -1
  6. package/dist/contracts/ports.contract.d.ts +11 -0
  7. package/dist/contracts/ports.contract.d.ts.map +1 -1
  8. package/dist/index.d.ts +11 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/schemas.d.ts +22 -0
  11. package/dist/schemas.d.ts.map +1 -1
  12. package/dist/schemas.js +3 -0
  13. package/dist/schemas.js.map +1 -1
  14. package/package.json +4 -4
  15. package/src/agent-catalog.ts +78 -78
  16. package/src/agent-run-model.ts +8 -8
  17. package/src/capability-env.ts +3 -3
  18. package/src/capability-secrets.ts +5 -5
  19. package/src/chores/chores.ts +67 -67
  20. package/src/chores/digest.ts +3 -3
  21. package/src/chores/extension-update.ts +3 -3
  22. package/src/chores/fix-deps.ts +3 -3
  23. package/src/chores/probes.ts +16 -16
  24. package/src/chores/prompt.ts +9 -9
  25. package/src/chores/stack.ts +17 -17
  26. package/src/chores/verdict.ts +23 -23
  27. package/src/contract-lock.ts +7 -7
  28. package/src/contracts/activity.contract.ts +1 -1
  29. package/src/contracts/agent.contract.ts +2 -2
  30. package/src/contracts/agents.contract.ts +11 -11
  31. package/src/contracts/automations.contract.ts +5 -5
  32. package/src/contracts/capabilities.contract.ts +7 -7
  33. package/src/contracts/chores.contract.ts +3 -3
  34. package/src/contracts/ci.contract.ts +2 -2
  35. package/src/contracts/claude.contract.ts +3 -3
  36. package/src/contracts/drafts.contract.ts +1 -1
  37. package/src/contracts/endpoints.contract.ts +2 -2
  38. package/src/contracts/extensions.contract.ts +8 -8
  39. package/src/contracts/git.contract.ts +6 -6
  40. package/src/contracts/grok.contract.ts +4 -4
  41. package/src/contracts/host.contract.ts +5 -5
  42. package/src/contracts/intentic.contract.ts +2 -2
  43. package/src/contracts/logs.contract.ts +1 -1
  44. package/src/contracts/loops.contract.ts +8 -8
  45. package/src/contracts/personas.contract.ts +8 -8
  46. package/src/contracts/ports.contract.ts +1 -1
  47. package/src/contracts/prepush.contract.ts +2 -2
  48. package/src/contracts/providers.contract.ts +4 -4
  49. package/src/contracts/public.contract.ts +1 -1
  50. package/src/contracts/push.contract.ts +2 -2
  51. package/src/contracts/secrets.contract.ts +2 -2
  52. package/src/contracts/sessions.contract.ts +2 -2
  53. package/src/contracts/settings.contract.ts +3 -3
  54. package/src/contracts/share.contract.ts +1 -1
  55. package/src/contracts/skills.contract.ts +2 -2
  56. package/src/contracts/system.contract.ts +9 -9
  57. package/src/contracts/usage.contract.ts +1 -1
  58. package/src/contracts/vpn.contract.ts +5 -5
  59. package/src/contracts/workflows.contract.ts +12 -12
  60. package/src/contracts/workspace.contract.ts +13 -13
  61. package/src/conversation-ids.ts +7 -7
  62. package/src/events.ts +136 -136
  63. package/src/history-state.ts +12 -12
  64. package/src/host-protocol.ts +7 -7
  65. package/src/hostnames.ts +15 -15
  66. package/src/index.ts +8 -8
  67. package/src/listener-protocol.ts +12 -12
  68. package/src/model-order.ts +30 -30
  69. package/src/output-fields.ts +9 -9
  70. package/src/path-refs.ts +6 -6
  71. package/src/publish-drafts.ts +6 -6
  72. package/src/quick-model.ts +15 -15
  73. package/src/routes.ts +19 -19
  74. package/src/runtime-state.ts +22 -22
  75. package/src/schemas.ts +930 -918
  76. package/src/search-globs.ts +6 -6
  77. package/src/session-names.ts +5 -5
  78. package/src/share-paths.ts +7 -7
  79. package/src/sse.ts +1 -1
  80. package/src/state-portability.ts +8 -8
  81. package/src/terminal-protocol.ts +3 -3
  82. package/src/title.ts +20 -20
  83. package/src/tunnel-ids.ts +7 -7
  84. package/src/versions.ts +5 -5
  85. package/src/workflow-faults.ts +11 -11
  86. package/src/workspace-state.ts +119 -119
@@ -1,14 +1,14 @@
1
1
  import type { StateFile } from "./state-portability.js";
2
2
 
3
- /* WHAT LIVES ON /history the second half of the daemon's state, and the half nothing declared until an
3
+ /* WHAT LIVES ON /history, the second half of the daemon's state, and the half nothing declared until an
4
4
  * export had to reason about it.
5
5
  *
6
6
  * `WORKSPACE_STATE_FILES` covers `<workspace>/.intentic/`, which is where the manifests live. It is not where
7
- * the machinery lives. Every repo's REAL git dir is here (a repo's in-tree `.git` is a pointer file see
7
+ * the machinery lives. Every repo's REAL git dir is here (a repo's in-tree `.git` is a pointer file, see
8
8
  * git/repo-git-dirs.ts for the invariant that forces it), and so are the fleet registry, the turn journal, the
9
9
  * ledgers, the checkpoint scopes and the isolated agents' checkouts. A "workspace export" that took `/work`
10
- * alone would carry a tree of repos with dangling gitdir pointers every git command in the restored sandbox
11
- * answering `fatal: not a git repository` and an empty agent board.
10
+ * alone would carry a tree of repos with dangling gitdir pointers, every git command in the restored sandbox
11
+ * answering `fatal: not a git repository`, and an empty agent board.
12
12
  *
13
13
  * The two tables stay separate rather than becoming one keyed by volume, because they answer different
14
14
  * questions. A `.intentic` entry also declares which browser QUERY it makes stale, since the file watcher
@@ -17,7 +17,7 @@ import type { StateFile } from "./state-portability.js";
17
17
  * imported rather than duplicated.
18
18
  *
19
19
  * `history-state-coverage.test.ts` fails when a daemon store builds a `/history` path this list doesn't carry, in both
20
- * directions the same shape-recognizing guard that covers the workspace table.
20
+ * directions, the same shape-recognizing guard that covers the workspace table.
21
21
  */
22
22
 
23
23
  // Paths are historyRoot-relative, forward-slash, matched by PREFIX; a directory entry keeps its trailing slash
@@ -27,7 +27,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
27
27
 
28
28
  /* THE ONE THAT MAKES A BUNDLE A WORKSPACE. Every repo's real git dir, including the /work root's own
29
29
  * ("root"), keyed by URI-encoded repo id. Carrying the working tree without this hands the target files
30
- * whose `.git` points at a path that does not exist there which is not a degraded repo but a broken one,
30
+ * whose `.git` points at a path that does not exist there, which is not a degraded repo but a broken one,
31
31
  * and it takes the Changes review, the diff, land and every agent branch with it. The agent BRANCHES live
32
32
  * in here too, which is what lets the checkouts below be left out. */
33
33
  { path: "gits/", portability: "carry" },
@@ -38,7 +38,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
38
38
  { path: "agents.json", portability: "carry" },
39
39
  { path: "turns/", portability: "carry" },
40
40
  { path: "transcripts/", portability: "carry" },
41
- // What each message can be put back to a workspace checkpoint, or an isolated conversation's own commits.
41
+ // What each message can be put back to, a workspace checkpoint, or an isolated conversation's own commits.
42
42
  // Carried WITH the transcripts and the scopes above, because it is the join between them: without it a
43
43
  // restored conversation reads back whole and offers no way back into it, even though both the messages and
44
44
  // the states they name travelled.
@@ -48,7 +48,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
48
48
  * Carried, and it is the entry with the most to say for itself: the PAGES live in the workspace's outbox
49
49
  * (`public/`), so they travel with `/work` whatever this says. Leaving the index behind would restore a
50
50
  * sandbox that is still serving somebody's conversation on the internet with nothing in the app that knows
51
- * it no row, no link, and no way to stop sharing short of deleting files by hand. The index is what makes
51
+ * it, no row, no link, and no way to stop sharing short of deleting files by hand. The index is what makes
52
52
  * a published page withdrawable, so it goes wherever the pages go. */
53
53
  { path: "shares.json", portability: "carry" },
54
54
  { path: "activity.jsonl", portability: "carry" },
@@ -58,7 +58,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
58
58
  // Explicit first-time dependency setup requests. Carrying the worklist preserves the owner's decision when
59
59
  // an export interrupts the queue before its terminal starts; fulfilled entries remove themselves.
60
60
  { path: "dependency-requests.json", portability: "carry" },
61
- // The deploy engine's own ledgers a run's events and the check results the Pipelines view reads back.
61
+ // The deploy engine's own ledgers, a run's events and the check results the Pipelines view reads back.
62
62
  { path: "apply-events.ndjson", portability: "carry" },
63
63
  { path: "check-events/", portability: "carry" },
64
64
 
@@ -68,7 +68,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
68
68
  *
69
69
  * A conversation's worktree is a full checkout of the monorepo per agent (plus its overlay upper dir), and
70
70
  * there can be a hundred of them. None of it is unique: the branch it holds is in `gits/` above, and the
71
- * registry entry naming it travels in agents.json so an imported conversation arrives in exactly the
71
+ * registry entry naming it travels in agents.json, so an imported conversation arrives in exactly the
72
72
  * shape the system already has a name for. `attached()` reports its checkout as absent, the board renders
73
73
  * it, and the next turn's `ensure()` re-creates it from the recorded composition, which is the same path an
74
74
  * archived agent takes when it runs again. The boot sweep's `git worktree prune` clears the stale admin
@@ -84,7 +84,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
84
84
  { path: ".isolation-probe", portability: "derived" },
85
85
  /* The finished bundles themselves. `derived` is doing real work here rather than describing leftovers: an
86
86
  * export that carried the export directory would pack every previous bundle into the new one, and the next
87
- * export would pack THAT each one a multiple of the last. Living on this volume is the other half of the
87
+ * export would pack THAT, each one a multiple of the last. Living on this volume is the other half of the
88
88
  * same guard; under `/work` the file would also be watched, indexed by iq, and snapshotted into history. */
89
89
  { path: "exports/", portability: "derived" },
90
90
 
@@ -107,7 +107,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
107
107
  /* ---- identity: what binds this sandbox to its owner, its browsers and its host ---- */
108
108
 
109
109
  /* Signs every browser session cookie. Carrying it would let a bundle's holder mint sessions against the
110
- * target an export becomes a credential and the target minting its own costs exactly one sign-in. */
110
+ * target, an export becomes a credential, and the target minting its own costs exactly one sign-in. */
111
111
  { path: "session-secret", portability: "identity", note: "Sign in again — the target signs its own sessions." },
112
112
  {
113
113
  path: "browser-access-disabled",
@@ -1,36 +1,36 @@
1
1
  import { z } from "zod";
2
2
 
3
- /* The handshake on /system/hosts/connect the ONE message that is not oRPC.
3
+ /* The handshake on /system/hosts/connect, the ONE message that is not oRPC.
4
4
  *
5
5
  * Everything a connected computer is asked lives in `hostContract` (contracts/host.contract.ts), spoken over
6
6
  * this socket by oRPC's websocket adapter: the machine hosts the server, the daemon holds the client. But a
7
7
  * socket has to prove whose it is before it can be given a typed client, and that proof cannot itself be an
8
- * oRPC call the daemon has nothing to call yet, and would be attaching a link to a stranger.
8
+ * oRPC call, the daemon has nothing to call yet, and would be attaching a link to a stranger.
9
9
  *
10
10
  * So the machine's first act is this frame, in plain JSON. The daemon verifies the token, learns which
11
11
  * capability the socket belongs to, and only then attaches the link; from that message on, every byte on the
12
12
  * wire is oRPC. Anything arriving before the link exists is either this frame or a closed socket. */
13
13
 
14
14
  // The MCP protocol revision the machine's tool server implements. Shared because the daemon answers the
15
- // handshake ITSELF when the machine is asleep (hosts/host.routes.ts) two spellings of this would mean an
15
+ // handshake ITSELF when the machine is asleep (hosts/host.routes.ts), two spellings of this would mean an
16
16
  // offline machine negotiating a different protocol than the same machine awake.
17
17
  export const MCP_PROTOCOL_VERSION = "2025-06-18";
18
18
 
19
19
  export const HostHelloSchema = z.object({
20
20
  type: z.literal("hello"),
21
- /* The machine's enrollment token in the FIRST FRAME, never in the URL. A WebSocket has no headers to put
21
+ /* The machine's enrollment token, in the FIRST FRAME, never in the URL. A WebSocket has no headers to put
22
22
  * it in, and the obvious `?token=` would write a durable key to somebody's laptop into Cloudflare's edge
23
23
  * logs, the connector's logs and every proxy in between (the reasoning that moved the browser's upgrades
24
- * onto one-shot tickets auth/ws-tickets.ts). A frame is body, not URL, so it is logged nowhere. Until this
24
+ * onto one-shot tickets, auth/ws-tickets.ts). A frame is body, not URL, so it is logged nowhere. Until this
25
25
  * arrives the socket is anonymous and short-lived: the daemon closes it in seconds if it never does. */
26
26
  token: z.string(),
27
- // The @intentic/host build the machine is running surfaced per machine so an old binary is visible rather
27
+ // The @intentic/host build the machine is running, surfaced per machine so an old binary is visible rather
28
28
  // than mysteriously missing a tool. What the machine IS (`describe`) is not here: it is pulled over the
29
29
  // typed link a moment later, so there is one definition of those facts rather than two.
30
30
  version: z.string(),
31
31
  });
32
32
  export type HostHello = z.infer<typeof HostHelloSchema>;
33
33
 
34
- // The URL the machine's agent dials, given the sandbox's public URL. Carries no credential the token rides
34
+ // The URL the machine's agent dials, given the sandbox's public URL. Carries no credential, the token rides
35
35
  // the hello frame. One place builds it, so the agent and the daemon route can't disagree about where it lives.
36
36
  export const hostConnectUrl = (sandboxUrl: string): string => `${sandboxUrl.replace(/^http/, "ws").replace(/\/$/, "")}/system/hosts/connect`;
package/src/hostnames.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // The single source for the tunnel/preview hostname + ingress scheme, shared by the CLI, platform API, daemon,
2
2
  // AND the browser. Pure string builders/parsers with NO node imports (unlike ./tunnel-ids, which needs
3
- // node:crypto for the digest) so the web bundle can import them and derive identical names. The caller supplies
3
+ // node:crypto for the digest), so the web bundle can import them and derive identical names. The caller supplies
4
4
  // the 12-hex id (sandboxIdFromToken/hostSshIdFromToken in node; WebCrypto in the browser).
5
5
  //
6
6
  // All four apps MUST agree on these strings: a divergence resolves to NXDOMAIN that resolvers negative-cache for
@@ -18,7 +18,7 @@ export const sshHostname = (id: string, zone: string): string => `ssh-${id}.${zo
18
18
  *
19
19
  * A public DNS name for a private address looks odd until you ask what the alternative is. A browser on the
20
20
  * same machine as the sandbox can reach its daemon in microseconds instead of crossing to a Cloudflare edge
21
- * and back but only over HTTPS, because Safari refuses http://127.0.0.1 from an HTTPS page as mixed content
21
+ * and back, but only over HTTPS, because Safari refuses http://127.0.0.1 from an HTTPS page as mixed content
22
22
  * (WebKit 171934, open since 2017), and HTTPS needs a name a public CA will certify. An IP literal cannot have
23
23
  * one; this can. The daemon holds the key and gets the certificate by proving control of the zone over
24
24
  * DNS-01 (there is nothing on the public internet for a CA to connect to).
@@ -36,36 +36,36 @@ export const hostSshTunnelName = (id: string): string => `host-ssh-${id}`;
36
36
  // The proxied-CNAME target every tunnel points its DNS record at.
37
37
  export const cfargotunnelCname = (tunnelId: string): string => `${tunnelId}.cfargotunnel.com`;
38
38
 
39
- // The cloudflared ingress catch-all must be the LAST rule.
39
+ // The cloudflared ingress catch-all, must be the LAST rule.
40
40
  export const CATCH_ALL = { service: "http_status:404" } as const;
41
41
 
42
- // Preview scheme: `preview-<panel>-<sandboxId>.<zone>` one DNS label (the free Universal SSL `*.<zone>` cert
42
+ // Preview scheme: `preview-<panel>-<sandboxId>.<zone>`, one DNS label (the free Universal SSL `*.<zone>` cert
43
43
  // covers exactly one level), where <panel> is `<repo>` or `<repo>--<app>` and <sandboxId> pins the hostname to
44
44
  // this sandbox (the shared intentic zone hosts many sandboxes; without the id two users' panels would collide).
45
- // Port-forward scheme: `port-<slot>-<sandboxId>.<zone>` the same shape with a `port-` prefix, where <slot>
45
+ // Port-forward scheme: `port-<slot>-<sandboxId>.<zone>`, the same shape with a `port-` prefix, where <slot>
46
46
  // is one of the sandbox's forward slots (portSlotsFromToken in ./tunnel-ids), not the port number itself:
47
47
  // slots keep the intentic-provided path's minted routes bounded and warm while dev servers churn ephemeral
48
48
  // ports. The slot labels are salted with the connect token rather than being the letters a…h, so a forwarded
49
- // port's hostname is not derivable from the (public) sandbox id alone see tunnel-ids for why that matters.
49
+ // port's hostname is not derivable from the (public) sandbox id alone, see tunnel-ids for why that matters.
50
50
  //
51
- // Outbox scheme: `public-<slot>-<sandboxId>.<zone>` the same shape again, serving the workspace's `public/`
51
+ // Outbox scheme: `public-<slot>-<sandboxId>.<zone>`, the same shape again, serving the workspace's `public/`
52
52
  // directory as static files. <slot> is publicSlotFromToken (./tunnel-ids), salted for the same reason the port
53
53
  // slots are, and one record per sandbox rather than a pool: there is one outbox, and its link has to stay good
54
54
  // for as long as the file does.
55
55
  //
56
56
  // A *label* is the first-DNS-label prefix before `-<sandboxId>` (`preview-<panel>` / `port-<slot>` /
57
- // `public-<slot>`) the unit the platform's /sandbox/preview-route mints, so one endpoint serves all three.
57
+ // `public-<slot>`), the unit the platform's /sandbox/preview-route mints, so one endpoint serves all three.
58
58
  export const previewLabel = (panel: string): string => `preview-${panel}`;
59
59
  export const portLabel = (slot: string): string => `port-${slot}`;
60
60
  export const publicLabel = (slot: string): string => `public-${slot}`;
61
61
 
62
- // The hostname a label resolves to what the platform's /sandbox/preview-route mints from the label alone.
62
+ // The hostname a label resolves to, what the platform's /sandbox/preview-route mints from the label alone.
63
63
  export const labelHostname = (label: string, id: string, zone: string): string => `${label}-${id}.${zone}`;
64
64
  export const previewHostname = (panel: string, id: string, zone: string): string => labelHostname(previewLabel(panel), id, zone);
65
65
  export const portHostname = (slot: string, id: string, zone: string): string => labelHostname(portLabel(slot), id, zone);
66
66
  export const publicHostname = (slot: string, id: string, zone: string): string => labelHostname(publicLabel(slot), id, zone);
67
67
 
68
- // A label's public URL undefined unless the sandbox has both a zone and an id (headless/loopback sandboxes
68
+ // A label's public URL, undefined unless the sandbox has both a zone and an id (headless/loopback sandboxes
69
69
  // have neither and advertise nothing). One builder, three vocabularies: a panel's preview, a forwarded port's,
70
70
  // and the outbox's.
71
71
  const labelUrl = (label: string, zone: string | undefined, sandboxId: string | undefined): string | undefined =>
@@ -79,7 +79,7 @@ export const publicUrl = (slot: string, zone: string | undefined, sandboxId: str
79
79
 
80
80
  // The key after `<prefix>` from a request's Host header. The first DNS label must carry the prefix (the
81
81
  // own-Cloudflare wildcard also catches stray subdomains → undefined → the caller's 404) and, when the sandbox
82
- // has an id, the exact `-<sandboxId>` suffix a fixed-length match, so keys containing `-` stay unambiguous.
82
+ // has an id, the exact `-<sandboxId>` suffix, a fixed-length match, so keys containing `-` stay unambiguous.
83
83
  // Without an id the bare label is the key (loopback tests and provider-deployed workspaces, which front the
84
84
  // proxy themselves).
85
85
  const keyFromHost = (prefix: string, hostHeader: string | undefined, sandboxId: string | undefined): string | undefined => {
@@ -103,7 +103,7 @@ export const publicSlotFromHost = (hostHeader: string | undefined, sandboxId: st
103
103
  keyFromHost("public-", hostHeader, sandboxId);
104
104
 
105
105
  // The sandbox's identity AS THE USER SEES IT: the leading DNS label of its public URL, minus the `sandbox-`
106
- // prefix `https://sandbox-0f310c3c4db4.intentic.dev` → `0f310c3c4db4`, i.e. sandboxIdFromToken's digest read
106
+ // prefix, `https://sandbox-0f310c3c4db4.intentic.dev` → `0f310c3c4db4`, i.e. sandboxIdFromToken's digest read
107
107
  // back off the wire by anyone holding only the URL. On the own-Cloudflare path the label is whatever subdomain
108
108
  // the owner chose, so that is the id there. undefined until the sandbox has a URL at all.
109
109
  export const sandboxIdFromUrl = (url: string | undefined): string | undefined => {
@@ -124,8 +124,8 @@ export const sandboxIdFromUrl = (url: string | undefined): string | undefined =>
124
124
  };
125
125
 
126
126
  // The LOCAL folder desktop sync mirrors /work into: `~/intentic/<sandbox name>-<sandboxIdFromUrl>`. Both halves
127
- // are strings the user already has in front of them the name in the sandbox switcher, the id in the address
128
- // bar so the folder on disk and the sandbox it mirrors read as ONE identity: `~/intentic/shop-0f310c3c4db4`
127
+ // are strings the user already has in front of them, the name in the sandbox switcher, the id in the address
128
+ // bar, so the folder on disk and the sandbox it mirrors read as ONE identity: `~/intentic/shop-0f310c3c4db4`
129
129
  // belongs to `https://sandbox-0f310c3c4db4.intentic.dev` and nothing else. Keyed on the URL rather than the
130
130
  // name alone for the same reason the hostname is: a torn-down sandbox recreated under the same name gets a new
131
131
  // id, hence its own fresh folder instead of reusing the dead one's (which cleanup never deletes) and colliding
@@ -142,7 +142,7 @@ export const syncFolder = (name: string, url: string | undefined): string => {
142
142
 
143
143
  // The Cloudflare zone from a sandbox public URL (https://sandbox-<id>.<zone> → <zone>): the hostname minus its
144
144
  // first DNS label. undefined when the URL is unparsable OR the hostname has fewer than three labels (no zone
145
- // suffix to strip e.g. a 2-label host would otherwise yield a bare TLD). Accepts scheme-less input too, so it
145
+ // suffix to strip, e.g. a 2-label host would otherwise yield a bare TLD). Accepts scheme-less input too, so it
146
146
  // works whether the caller passes `https://…` (daemon/CLI) or a bare host. This is the single reconciled
147
147
  // implementation of what used to be the daemon's `zoneFromPublicUrl` and the web's `zoneFromDaemonUrl`.
148
148
  export const zoneFromUrl = (url: string | undefined): string | undefined => {
package/src/index.ts CHANGED
@@ -53,7 +53,7 @@ export { gitContract } from "./contracts/git.contract.js";
53
53
  export { grokContract } from "./contracts/grok.contract.js";
54
54
  export { historyContract } from "./contracts/history.contract.js";
55
55
  /* Deliberately NOT part of `sandboxContract` below: that map is the daemon's own HTTP surface, and this one is
56
- * spoken the other way round over a connected computer's WebSocket, with the MACHINE implementing it. */
56
+ * spoken the other way round, over a connected computer's WebSocket, with the MACHINE implementing it. */
57
57
  export { hostContract } from "./contracts/host.contract.js";
58
58
  export { intenticContract } from "./contracts/intentic.contract.js";
59
59
  export { inventoryContract } from "./contracts/inventory.contract.js";
@@ -82,13 +82,13 @@ export * from "./routes.js";
82
82
  /* THE CONTAINER'S FIXED DIRECTORY LAYOUT, re-exported so extensions can reach it.
83
83
  *
84
84
  * The names are defined once in @intentic/constants, which sits at the bottom of the dependency graph. An
85
- * EXTENSION may not import that package the boundary rule (.oxlintrc.json, _extensions/README.md) allows
85
+ * EXTENSION may not import that package, the boundary rule (.oxlintrc.json, _extensions/README.md) allows
86
86
  * only the SDK halves and this contract, so that an extension cannot couple itself to app or engine internals.
87
87
  * That rule is right, and it left extensions with no way to name the workspace root except by spelling it.
88
88
  *
89
89
  * Re-exporting here is what closes that gap without widening the boundary: the layout is exactly the kind of
90
- * thing this package already carries shared vocabulary both sides of the wire must agree on, alongside the
91
- * state-file table below and there is still one definition, in one place, that everything resolves to. */
90
+ * thing this package already carries, shared vocabulary both sides of the wire must agree on, alongside the
91
+ * state-file table below, and there is still one definition, in one place, that everything resolves to. */
92
92
  export { HISTORY_ROOT, HOST_STATE_ROOT, STATE_DIR, WORKSPACE_ROOT } from "@intentic/constants";
93
93
  export * from "./workspace-state.js";
94
94
  export * from "./runtime-state.js";
@@ -114,7 +114,7 @@ export * from "./title.js";
114
114
  export * from "./versions.js";
115
115
  export * from "./workflow-faults.js";
116
116
 
117
- // The aggregated contract implemented on the server by the per-domain route factories and consumed by the
117
+ // The aggregated contract, implemented on the server by the per-domain route factories and consumed by the
118
118
  // browser's typed oRPC client (ContractRouterClient<typeof sandboxContract>). The wire paths it declares are
119
119
  // mounted at the sandbox root, so /health and /workspace/raw (plain Hono routes) sit alongside it.
120
120
  export const sandboxContract = {
@@ -157,14 +157,14 @@ export const sandboxContract = {
157
157
  };
158
158
 
159
159
  // Every route in THIS build of the contract, and the names the daemon advertises on its hello frame. Bound here
160
- // rather than in routes.ts so that module stays a pure function of whatever contract it is handed importing
160
+ // rather than in routes.ts so that module stays a pure function of whatever contract it is handed, importing
161
161
  // `sandboxContract` from there would close a load-time cycle back through this file. See routes.ts for why a
162
162
  // daemon advertises its route surface at all.
163
163
  export const SANDBOX_ROUTES: readonly ContractRoute[] = contractRoutes(sandboxContract);
164
164
  export const SANDBOX_ROUTE_NAMES: readonly string[] = SANDBOX_ROUTES.map((route) => route.name);
165
165
 
166
166
  /* And the SHAPE of each of those routes, advertised beside the names for the failure the names cannot describe:
167
- * a route both builds have, answering a payload only one of them expects. Computed once at module load it
167
+ * a route both builds have, answering a payload only one of them expects. Computed once at module load, it
168
168
  * walks every contract schema through `z.toJSONSchema`, which is far too much work to repeat per connection and
169
169
  * exactly the kind of thing that never changes for the life of a process. See routes.ts. */
170
170
  export const SANDBOX_ROUTE_SHAPES: Readonly<Record<string, string>> = routeShapes(sandboxContract);
@@ -174,7 +174,7 @@ export const sandboxRouteName = (method: string, pathWithQuery: string): string
174
174
  routeNameForRequest(SANDBOX_ROUTES, method, pathWithQuery);
175
175
 
176
176
  // The method and concrete path a TYPED call is about to put on the wire, bound to this build's route table.
177
- // Undefined when the procedure is not one this contract declares, which a typed caller cannot reach the host
177
+ // Undefined when the procedure is not one this contract declares, which a typed caller cannot reach, the host
178
178
  // gate treats it as a refusal rather than assuming it is harmless.
179
179
  export const sandboxRequestFor = (procedure: readonly string[], input: unknown): { method: string; path: string } | undefined => {
180
180
  const route = routeForProcedure(SANDBOX_ROUTES, procedure);
@@ -3,14 +3,14 @@ import { ActivityStatusSchema } from "./schemas.js";
3
3
 
4
4
  /* The wire between the daemon and an extension's realtime-listener GATEWAY process (ext-discord, ext-slack,
5
5
  * ext-telegram, ext-whatsapp, ext-imap): the four provider-scoped routes app.ts mounts under
6
- * /listeners/:provider state, dispatch, failure, status. These shapes used to live daemon-side only, with
6
+ * /listeners/:provider, state, dispatch, failure, status. These shapes used to live daemon-side only, with
7
7
  * every gateway hand-writing its own copy of the payloads as untyped literals; a field rename broke five
8
- * producers silently. They live in the contract now so BOTH ends compile against one declaration the daemon
8
+ * producers silently. They live in the contract now so BOTH ends compile against one declaration, the daemon
9
9
  * parses with the schemas, the gateways (via @intentic/connector-runtime) type against the inferred types. */
10
10
 
11
- // One normalized inbound event serialized as a JSON line in the automation's payload, and the JSON body a
11
+ // One normalized inbound event, serialized as a JSON line in the automation's payload, and the JSON body a
12
12
  // realtime source POSTs to /listeners/<provider>/dispatch. A zod schema (not a bare interface) because it's
13
- // parsed from an extension gateway's request; `provider` and `type` are open strings the source is
13
+ // parsed from an extension gateway's request; `provider` and `type` are open strings, the source is
14
14
  // extension-declared (contributes.listener), not a core enum.
15
15
  export const ListenerMessageSchema = z.object({
16
16
  provider: z.string().min(1),
@@ -22,7 +22,7 @@ export const ListenerMessageSchema = z.object({
22
22
  // Discord message: it @mentions one of our bots or replies to a bot's message. Voice events never set it.
23
23
  mentioned: z.boolean().optional(),
24
24
  // CI pipeline event: the ref it ran on. Top-level rather than inside `extra` for the same reason
25
- // `mentioned` is the dispatcher MATCHES on it, and a narrowing axis the trigger can name has to be
25
+ // `mentioned` is, the dispatcher MATCHES on it, and a narrowing axis the trigger can name has to be
26
26
  // a field of the message rather than a key in a provider's opaque bag.
27
27
  branch: z.string().optional(),
28
28
  // Prior channel messages (chronological) fetched when a bot is tagged, so the agent can reason about why.
@@ -45,13 +45,13 @@ export const ListenerMessageSchema = z.object({
45
45
  });
46
46
  export type ListenerMessage = z.infer<typeof ListenerMessageSchema>;
47
47
 
48
- // One ndjson frame of a /listeners/<provider>/dispatch?stream=1 response a text delta for one automation's
48
+ // One ndjson frame of a /listeners/<provider>/dispatch?stream=1 response, a text delta for one automation's
49
49
  // reply, the provider's own failure sentence, or that automation's terminal marker. A type, not a schema: the
50
50
  // DAEMON produces these (listener.routes.ts), so nothing parses them from untrusted input.
51
51
  export interface ListenerDispatchFrame {
52
52
  readonly automationId: string;
53
53
  readonly delta?: string;
54
- // The turn refused or broke, in the provider's own words forwarded verbatim because a gateway delivers
54
+ // The turn refused or broke, in the provider's own words, forwarded verbatim because a gateway delivers
55
55
  // into the owner's own channel, where the actual sentence is the useful thing.
56
56
  readonly failed?: string;
57
57
  readonly end?: boolean;
@@ -61,16 +61,16 @@ export interface ListenerDispatchFrame {
61
61
  *
62
62
  * A CODE IS NOT THE ONLY THING WORTH SAYING, and publishing only codes is what made a phone that had never
63
63
  * linked read as connected: the seconds before the first code, a gateway that just restarted, and a number
64
- * WhatsApp refused all looked identical from the daemon's side an absent code so the card fell through to
64
+ * WhatsApp refused all looked identical from the daemon's side, an absent code, so the card fell through to
65
65
  * "ready" and the owner was sent away from the one screen that could have shown them the step. Each of those is
66
66
  * its own state here, and every one of them means NOT PAIRED YET.
67
67
  *
68
68
  * `since` stamps the CURRENT code. WhatsApp closes an unpaired socket after a minute or so and each reopen mints
69
- * a fresh code, so a code is a thing with an age the card says how old the one on screen is rather than
69
+ * a fresh code, so a code is a thing with an age, the card says how old the one on screen is rather than
70
70
  * leaving the owner to type a dead one twice. */
71
71
  export const ListenerPairingSchema = z.object({
72
72
  // waiting: a socket is up and the code hasn't arrived (or the last one died with its socket).
73
- // code: `code` is live type it on the phone. failed: `detail` says what WhatsApp refused.
73
+ // code: `code` is live, type it on the phone. failed: `detail` says what WhatsApp refused.
74
74
  state: z.enum(["waiting", "code", "failed"]),
75
75
  code: z.string().optional(),
76
76
  detail: z.string().optional(),
@@ -79,11 +79,11 @@ export const ListenerPairingSchema = z.object({
79
79
  export type ListenerPairing = z.infer<typeof ListenerPairingSchema>;
80
80
 
81
81
  // Push-based listener status: a gateway process POSTs its live connection/voice snapshot to
82
- // /listeners/<provider>/status, and the activity route reads it from there the daemon holds no provider
82
+ // /listeners/<provider>/status, and the activity route reads it from there, the daemon holds no provider
83
83
  // connection of its own to probe. The body IS the ActivityStatus the /activity/status probe used to build from
84
84
  // in-process discord singletons, plus the per-gateway extras that ride the same channel: whether whisper is
85
85
  // present (discord's voice-pending signal) and each unpaired capability's ceremony by id (whatsapp's
86
- // link-a-device flow the capability card renders it as the step the owner is standing in front of).
86
+ // link-a-device flow, the capability card renders it as the step the owner is standing in front of).
87
87
  export const ListenerStatusSchema = ActivityStatusSchema.extend({
88
88
  whisperReady: z.boolean().optional(),
89
89
  pairing: z.record(z.string(), ListenerPairingSchema).optional(),
@@ -1,35 +1,35 @@
1
- /* HOW A MODEL CATALOG IS ORDERED one rule for every provider, because only one provider publishes an order
1
+ /* HOW A MODEL CATALOG IS ORDERED, one rule for every provider, because only one provider publishes an order
2
2
  * worth keeping.
3
3
  *
4
4
  * Anthropic's REST /v1/models answers newest-first: that IS a provider opinion, and Claude's catalog rides it
5
- * (claude-models.ts). Every other provider here is read through an OpenAI-compatible /v1/models Codex and
6
- * Gemini and Kimi via the bundled translator or out of xAI's "Did you mean" rejection, and those
5
+ * (claude-models.ts). Every other provider here is read through an OpenAI-compatible /v1/models. Codex and
6
+ * Gemini and Kimi via the bundled translator, or out of xAI's "Did you mean" rejection, and those
7
7
  * endpoints publish a SET, not a ranking: they hand the ids back in whatever order their registry iterates,
8
8
  * which in practice is alphabetical. Reading that as a preference is what put "GPT 5.4 Mini" at the head of the
9
9
  * Codex group with GPT 5.6 below it, and what made a fresh Codex conversation start on whichever id happened to
10
- * sort first models[0] is the provider default.
10
+ * sort first, models[0] is the provider default.
11
11
  *
12
12
  * So for those providers the order is DERIVED from the id, out of the only two facts an id reliably carries:
13
- * which TIER the model is (the adjective) and which RELEASE it is (the numbers). Both are provider-agnostic
14
- * every vendor names its models the same way which is what lets the daemon's four catalog services and the
13
+ * which TIER the model is (the adjective) and which RELEASE it is (the numbers). Both are provider-agnostic,
14
+ * every vendor names its models the same way, which is what lets the daemon's four catalog services and the
15
15
  * web's picker share one rule instead of each inventing a local one. */
16
16
 
17
17
  // A version-ish segment: digits and dots, optionally prefixed by the vendor's version marker (`4`, `5.1`, `v2`,
18
18
  // `k3`, `k2.7`, `20251001`). Kimi is the one provider that fuses the marker with the generation; treating `k3`
19
19
  // as a name made the current flagship look unversioned, so K2.x sorted above it. Everything else is a NAME
20
- // segment and belongs to the family which is what makes the split below exhaustive.
20
+ // segment and belongs to the family, which is what makes the split below exhaustive.
21
21
  const VERSION_SEGMENT = /^(?:v|k)?[\d.]+$/i;
22
22
 
23
23
  // A date stamp rather than a version component: six digits or more (20251001, 250514). The distinction is not
24
- // cosmetic claude-opus-4-1-20250805 (Opus 4.1) and claude-opus-4-20250514 (Opus 4.0) compare as (4,1) vs (4)
25
- // with the stamps held apart, and as (4,1,20250805) vs (4,20250514) the OLDER model winning without.
24
+ // cosmetic, claude-opus-4-1-20250805 (Opus 4.1) and claude-opus-4-20250514 (Opus 4.0) compare as (4,1) vs (4)
25
+ // with the stamps held apart, and as (4,1,20250805) vs (4,20250514), the OLDER model winning, without.
26
26
  const DATE_SEGMENT = /^\d{6,}$/;
27
27
 
28
28
  const segmentsOf = (id: string): string[] => id.split(/[-_]/);
29
29
 
30
- // A model's FAMILY its id with every version-ish segment dropped, so claude-opus-5 and claude-opus-4-8 land
30
+ // A model's FAMILY, its id with every version-ish segment dropped, so claude-opus-5 and claude-opus-4-8 land
31
31
  // together (as do gpt-5.1/gpt-5, and claude-haiku-4-5-20251001 with its date suffix). Derived, never listed: a
32
- // family that ships tomorrow groups itself. The id is the stable key here labels get renamed, ids don't.
32
+ // family that ships tomorrow groups itself. The id is the stable key here, labels get renamed, ids don't.
33
33
  export const familyOf = (id: string): string => {
34
34
  const stem = segmentsOf(id)
35
35
  .filter((segment) => !VERSION_SEGMENT.test(segment))
@@ -43,7 +43,7 @@ export interface ModelRelease {
43
43
  // id (kimi-latest, gemini-pro-agent), which therefore reads as the oldest of its tier: a rolling alias names
44
44
  // no release, and inventing one for it would outrank the models that do name theirs.
45
45
  readonly version: readonly number[];
46
- // The id's date stamp, 0 for none the tiebreak between two builds of the SAME version.
46
+ // The id's date stamp, 0 for none, the tiebreak between two builds of the SAME version.
47
47
  readonly date: number;
48
48
  }
49
49
 
@@ -75,14 +75,14 @@ const compareRelease = (left: ModelRelease, right: ModelRelease): number => {
75
75
 
76
76
  /* THE ONE CURATED FACT in this file, and the only one the providers publish nowhere the app can read: which tier
77
77
  * is the frontier and which is the cheap one. It ranks FAMILIES, never models, and it is a vocabulary of tier
78
- * ADJECTIVES rather than a table of ids that scoping is the whole point, because a per-model ranking table
78
+ * ADJECTIVES rather than a table of ids, that scoping is the whole point, because a per-model ranking table
79
79
  * failed here once already. The words are the ones every vendor reaches for, so a release that ships tomorrow
80
80
  * ranks itself as long as it is named like its predecessors, and a release named some other way ranks as unknown.
81
81
  *
82
82
  * An UNKNOWN family LEADS rather than sinks, and that direction is the point: the ranking this replaced sank
83
83
  * unrecognized ids to a floor below the everyday tier, so a brand-new flagship sorted beneath the model it
84
- * replaced. An id carrying no tier word at all is the provider's BASE line (gpt-5.6, grok-4, kimi-k2) which is
85
- * exactly the line a user reaches for and a family nobody here has heard of is far likelier to be the next
84
+ * replaced. An id carrying no tier word at all is the provider's BASE line (gpt-5.6, grok-4, kimi-k2), which is
85
+ * exactly the line a user reaches for, and a family nobody here has heard of is far likelier to be the next
86
86
  * flagship than the next budget tier. Being wrong costs one row's position; being wrong the other way hides a
87
87
  * launch. */
88
88
  const TIER_RANK: Readonly<Record<string, number>> = {
@@ -113,7 +113,7 @@ const UNRANKED = -1;
113
113
 
114
114
  /* Some providers name a capability ladder INSIDE one release instead of using the cross-release adjectives
115
115
  * above. Codex 5.6's Sol/Terra/Luna rows are that shape: they must remain together ahead of the older 5.5 line,
116
- * but their order is not an arbitrary id tiebreak Sol is the strongest, followed by Terra, then Luna. Keeping
116
+ * but their order is not an arbitrary id tiebreak. Sol is the strongest, followed by Terra, then Luna. Keeping
117
117
  * this as a separate rank lets release recency still win across generations (a future GPT 5.7 base model must
118
118
  * not be buried under a recognized 5.6 suffix), while the three siblings sort by their real tier. */
119
119
  const RELEASE_TIER_RANK: Readonly<Record<string, number>> = {
@@ -135,23 +135,23 @@ const lastRankOf = (family: string, ranks: Readonly<Record<string, number>>): nu
135
135
 
136
136
  const releaseTierRankOf = (family: string): number => lastRankOf(family, RELEASE_TIER_RANK);
137
137
 
138
- /* HOW HARD AN ID SAYS IT WILL THINK. A routed catalog does not publish one row per model it publishes one row
138
+ /* HOW HARD AN ID SAYS IT WILL THINK. A routed catalog does not publish one row per model, it publishes one row
139
139
  * per model PER THINKING LEVEL, spelling the level into the id: `gemini-3.6-flash-high` and
140
140
  * `gemini-3.5-flash-extra-low` are the same Flash at opposite ends of its reasoning budget.
141
141
  *
142
142
  * Which the ranking above cannot see at all, and that blind spot has a direction: `high` and `low` are both
143
- * unrecognized words, so two variants of one model tie on tier and the RELEASE tiebreak settles them and the
143
+ * unrecognized words, so two variants of one model tie on tier and the RELEASE tiebreak settles them, and the
144
144
  * newest variant a channel publishes is routinely the high one. The quick model, whose entire job is to be the
145
145
  * cheap rung, would therefore reach for the most expensive reading of the cheapest model it can find.
146
146
  *
147
147
  * That is not a small mis-sort. Thinking is the difference between a commit message that is in the box before
148
- * the user has finished reading the file list and one that arrives half a minute later measured at ~2s versus
148
+ * the user has finished reading the file list and one that arrives half a minute later, measured at ~2s versus
149
149
  * ~27s on the same model and the same diff (agent/one-shot.ts, which disables thinking for the rungs where a
150
150
  * request parameter can). For a routed rung there is no such parameter: the id IS the setting, so this ranking
151
151
  * is the only place the choice can be made.
152
152
  *
153
153
  * Read ONLY by the cheap-end order. A picker orders a catalog by what a person reaches for, and a person
154
- * pinning `-high` on purpose means it see compareCheapestFirst for the seam this belongs to. */
154
+ * pinning `-high` on purpose means it, see compareCheapestFirst for the seam this belongs to. */
155
155
  const THINKING_RANK: Readonly<Record<string, number>> = {
156
156
  minimal: 0,
157
157
  none: 0,
@@ -164,7 +164,7 @@ const THINKING_RANK: Readonly<Record<string, number>> = {
164
164
  thinking: 4,
165
165
  };
166
166
 
167
- /* An id naming NO level sits between the two ends rather than at either the provider's own default, which for
167
+ /* An id naming NO level sits between the two ends rather than at either, the provider's own default, which for
168
168
  * a model that can think is usually some thinking. Ranking it cheapest would seat a silent default ahead of an
169
169
  * id that explicitly says `minimal`, and ranking it dearest would bury every model whose channel publishes no
170
170
  * variants at all (Anthropic's, Kimi's) beneath one that does. Neither is what the id claims. */
@@ -180,7 +180,7 @@ const thinkingRankOf = (family: string): number => {
180
180
  export const tierRankOf = (family: string): number => lastRankOf(family, TIER_RANK);
181
181
 
182
182
  // The canonical order of two model ids: broad tier first, then release, then a tier declared within that release.
183
- // Hand it straight to Array#toSorted that sort is stable, so two ids this rule cannot separate keep the order
183
+ // Hand it straight to Array#toSorted, that sort is stable, so two ids this rule cannot separate keep the order
184
184
  // they arrived in (for Claude, the provider's own).
185
185
  export const compareModelIds = (left: string, right: string): number => {
186
186
  const leftFamily = familyOf(left);
@@ -192,28 +192,28 @@ export const compareModelIds = (left: string, right: string): number => {
192
192
  );
193
193
  };
194
194
 
195
- /* The order for a catalog its endpoint published as a SET Codex, Gemini, Kimi and Grok, i.e. everything but
195
+ /* The order for a catalog its endpoint published as a SET. Codex, Gemini, Kimi and Grok, i.e. everything but
196
196
  * Anthropic's ranked list. Falling back on arrival order is what the rule above does with a tie, and for a RANKED
197
197
  * catalog that is exactly right: the tie is the provider's own opinion, so claude-opus-5 stays ahead of
198
198
  * claude-fable-5. For a set there is no opinion to keep, and the header of this file assumed the leftover order
199
- * was at least alphabetical it is not. A subscription can hand tied rows back in whatever order its registry
199
+ * was at least alphabetical, it is not. A subscription can hand tied rows back in whatever order its registry
200
200
  * iterated THIS request, so the tie decides which model a fresh conversation opens on and can flip between
201
201
  * catalog refreshes.
202
202
  *
203
- * So a set breaks its own ties on the id. Which sibling that seats first is arbitrary but it is the same
203
+ * So a set breaks its own ties on the id. Which sibling that seats first is arbitrary, but it is the same
204
204
  * arbitrary answer every refresh, which is the property `default` actually needs. */
205
205
  export const compareUnrankedModelIds = (left: string, right: string): number => compareModelIds(left, right) || left.localeCompare(right);
206
206
 
207
207
  /* THE SAME TIER SCALE READ FROM THE OTHER END, for the one caller that wants the WEAKEST model rather than the
208
208
  * strongest: the quick model behind an automatic helper (the commit message written at land time). A picker
209
209
  * orders a catalog by what a user reaches for; this orders it by what a helper should spend, and the two are
210
- * exact opposites so they share TIER_RANK rather than each naming its own list of cheap ids.
210
+ * exact opposites, so they share TIER_RANK rather than each naming its own list of cheap ids.
211
211
  *
212
212
  * The direction of UNRANKED is the reason this can't just be compareModelIds reversed. There, an unrecognized
213
213
  * family LEADS, because an id carrying no tier word is the provider's base line and a family nobody here has
214
214
  * heard of is likelier the next flagship than the next budget tier. Reversing would therefore seat exactly that
215
- * unknown-probably-flagship id as the cheap pick. So unknown sinks to LAST here too both orders agree it is
216
- * not the efficient rung and the cheap end is only ever a family whose tier word is actually recognized.
215
+ * unknown-probably-flagship id as the cheap pick. So unknown sinks to LAST here too, both orders agree it is
216
+ * not the efficient rung, and the cheap end is only ever a family whose tier word is actually recognized.
217
217
  * Falling off the end of a catalog with no efficient tier at all (Kimi publishes none) is then honest: the
218
218
  * newest of what it does publish, chosen by the release tiebreak below. */
219
219
  export const compareCheapestFirst = (left: string, right: string): number => {
@@ -223,14 +223,14 @@ export const compareCheapestFirst = (left: string, right: string): number => {
223
223
  tierRankOf(rightFamily) - tierRankOf(leftFamily) ||
224
224
  // BEFORE release, and that placement is the point: two rows of one model differing only in thinking
225
225
  // level are the same model, so recency has nothing to say between them, and letting it speak is what
226
- // seated the high variant. Tier still wins over both a thinking Haiku is cheaper than a silent Opus.
226
+ // seated the high variant. Tier still wins over both, a thinking Haiku is cheaper than a silent Opus.
227
227
  thinkingRankOf(leftFamily) - thinkingRankOf(rightFamily) ||
228
228
  compareRelease(releaseOf(left), releaseOf(right)) ||
229
229
  releaseTierRankOf(rightFamily) - releaseTierRankOf(leftFamily)
230
230
  );
231
231
  };
232
232
 
233
- /* WOULD RUNNING THIS ID MAKE THE MODEL THINK as far as its name admits, which for a routed catalog is as far
233
+ /* WOULD RUNNING THIS ID MAKE THE MODEL THINK, as far as its name admits, which for a routed catalog is as far
234
234
  * as anyone can tell without running it. True only for an id that spells out a level ABOVE the quiet end, so an
235
235
  * ordinary id nobody has annotated (claude-haiku-4-5, kimi-k2) is never accused of it.
236
236
  *