@alexkroman1/aai-cli 5.8.1 → 5.10.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 (46) hide show
  1. package/README.md +5 -1
  2. package/bin.mjs +32 -0
  3. package/dist/{_agent-2nVugrN3.mjs → _agent-DS2PUJcl.mjs} +1 -1
  4. package/dist/{_bundler-Cjaxa2wi.mjs → _bundler-DC17suWN.mjs} +2 -2
  5. package/dist/_config-DMyolIk9.mjs +242 -0
  6. package/dist/_config.d.ts +28 -9
  7. package/dist/_dev-env.d.ts +38 -0
  8. package/dist/{_dev-server-BB5N8kdh.mjs → _dev-server-B3ivqyEd.mjs} +75 -55
  9. package/dist/_dev-server.d.ts +2 -2
  10. package/dist/{_init-Dei2JRmn.mjs → _init-CYLvYU-B.mjs} +3 -3
  11. package/dist/{_ui-8kOEB-JH.mjs → _output-CC300DzW.mjs} +1 -42
  12. package/dist/{_server-common-CnaP_Urf.mjs → _server-common-DX8Bfrf5.mjs} +1 -1
  13. package/dist/{_slug-api-CGJSST9B.mjs → _slug-api-fRNNR8tz.mjs} +1 -1
  14. package/dist/{_templates-BfyRyE7z.mjs → _templates-BWJOiWOO.mjs} +2 -2
  15. package/dist/{_typecheck-gate-DvE8S3aQ.mjs → _typecheck-gate-DB-PY0A3.mjs} +2 -1
  16. package/dist/_ui-DfwfDbT-.mjs +68 -0
  17. package/dist/_ui.d.ts +19 -0
  18. package/dist/{_utils-Ch0J4s6a.mjs → _utils-8KKw-bzi.mjs} +9 -2
  19. package/dist/_utils.d.ts +9 -2
  20. package/dist/{build-BXwDB78d.mjs → build-CACFbdQ4.mjs} +5 -4
  21. package/dist/cli.mjs +21 -20
  22. package/dist/{client-bundler-yiWoXrgb.mjs → client-bundler-DONm-khu.mjs} +1 -1
  23. package/dist/client-bundler.mjs +1 -1
  24. package/dist/{delete-DRNfvczK.mjs → delete-DEZ7u3u4.mjs} +6 -4
  25. package/dist/delete.d.ts +5 -0
  26. package/dist/{deploy-Cp-wgME3.mjs → deploy-Ch0d_jje.mjs} +8 -7
  27. package/dist/{dev-C4KyxouE.mjs → dev-CdeRcYiQ.mjs} +7 -6
  28. package/dist/{init-BpBbttOv.mjs → init-Nc6fB774.mjs} +6 -5
  29. package/dist/{login-AA_UdRI-.mjs → login-BeFUiU6M.mjs} +9 -9
  30. package/dist/scaffold/CLAUDE.md +39 -14
  31. package/dist/scaffold/package.json +3 -3
  32. package/dist/{secret-Dr0qnxeb.mjs → secret-4_dYyrpA.mjs} +3 -2
  33. package/dist/{storage-CoQB8d-u.mjs → storage-BTfErOOW.mjs} +3 -2
  34. package/dist/{studio-_UBBvh5V.mjs → studio-LNvXtWak.mjs} +7 -6
  35. package/dist/templates/dispatch-center/agent.ts +4 -6
  36. package/dist/templates/retail/agent.ts +4 -6
  37. package/dist/templates/retail/registry.test.ts +5 -3
  38. package/dist/templates/retail/resolve.test.ts +6 -4
  39. package/dist/templates/retail/seed.test.ts +21 -13
  40. package/dist/templates/retail/shared.test.ts +9 -5
  41. package/dist/templates/solo-rpg/agent.test.ts +7 -5
  42. package/dist/templates/solo-rpg/shared.ts +2 -2
  43. package/dist/{test-3Gq4pqH_.mjs → test-C-V98oC-.mjs} +4 -4
  44. package/dist/typecheck.mjs +65 -14
  45. package/package.json +5 -4
  46. package/dist/_config-Y5V-5Krn.mjs +0 -173
@@ -97,8 +97,9 @@ export default agent({
97
97
  silencePrompt?: string; // instruction injected on silence timeout (requires silenceTimeoutMs)
98
98
  minBargeInWords?: number; // pipeline only — words before user speech interrupts the reply (default 2)
99
99
  interruptionMinDurationMs?: number; // pipeline only — sustained speech (ms) before an interim barge-in interrupts (default 500; 0 disables)
100
- holdPhrase?: string; // pipeline only — spoken before a silent tool-call turn (default "One moment."; "" disables)
101
- falseInterruptionTimeoutMs?: number; // pipeline only — resume an interrupted reply if no user turn commits (default 2000; 0 disables)
100
+ deadAirCoverMs?: number; // pipeline only — speak a short filler after this much silence in a turn (default 5000; 0 disables)
101
+ resumeFalseInterruption?: boolean; // pipeline only — resume an interrupted reply if no user turn commits (default true)
102
+ preemptiveGeneration?: boolean; // pipeline only — start the reply from a high-confidence interim (default true; false opts out)
102
103
  state?: () => S; // per-session mutable state, exposed as ctx.state
103
104
  // (S is inferred; see "Typing ctx.state")
104
105
  syncState?: (state: S) => unknown; // push a projection of state to the client
@@ -250,9 +251,26 @@ land). End-of-turn detection (how long a pause ends the user's turn)
250
251
  belongs to the STT provider: `assemblyAIStt({ minTurnSilenceMs })` (default
251
252
  2000 ms) / `deepgram({ endpointing })` (default 1500 ms), so mid-utterance
252
253
  pauses don't split a request.
253
- `holdPhrase` is spoken when a turn opens with a tool call and no speech.
254
- `falseInterruptionTimeoutMs` resumes an interrupted reply when a barge-in
255
- turns out to be noise (no user turn commits within the window).
254
+ `deadAirCoverMs` is how long a turn may go silent before the transport speaks
255
+ a short filler, so a long tool chain doesn't sound like a dropped call. It is
256
+ measured silence, not a guess about the turn's shape, so a reply that arrives
257
+ promptly pays nothing; `0` disables it. The wording is not yours to set — the
258
+ filler must be purely declarative and never a request for patience, or the
259
+ caller answers it and the answer barges in.
260
+ `resumeFalseInterruption` (default `true`) resumes an interrupted reply when
261
+ a barge-in turns out to be noise — no user turn ever commits. The wait is not
262
+ configurable: the resume fires once the transcript stream goes quiet with no
263
+ final, so it can never race a real turn the STT is still endpointing.
264
+ `preemptiveGeneration` (default `true`) starts generating the reply as soon
265
+ as transcription is confident the caller has finished, and uses that
266
+ already-running answer if the committed transcript matches. It can shorten the
267
+ pause before the agent speaks, and **how much is unmeasured** — nobody has
268
+ shown the size of the saving. What bounds the downside is that a speculation
269
+ never speaks, calls a tool, or enters history until the real turn adopts it, so
270
+ the worst case is a wasted request and a turn that behaves exactly as it would
271
+ with the flag off. Set `preemptiveGeneration: false` to opt out — worth doing
272
+ on a tool-heavy agent, where a speculation that reaches a tool call is thrown
273
+ away, so it is cost with no upside.
256
274
 
257
275
  ## Providers
258
276
 
@@ -651,10 +669,11 @@ interpolate values into the SQL string. The rows come back as plain objects;
651
669
  A query returning more than 1000 rows throws — always bound reads with
652
670
  `LIMIT` (paginate with `LIMIT`/`OFFSET`).
653
671
 
654
- **Storage must be enabled** or accessing `ctx.db` throws:
672
+ **The database must be enabled** or accessing `ctx.db` throws:
655
673
 
656
674
  - CLI: `aai storage enable`
657
- - Studio: the Storage toggle
675
+ - Studio: Settings pane → Database → Enable database (covers both the
676
+ preview and published agents, each with its own schema)
658
677
  - `aai dev`: set `DATABASE_URL` in the project `.env`
659
678
 
660
679
  Create tables lazily from tool code and upsert with `on conflict`:
@@ -929,8 +948,10 @@ Never hardcode secrets in agent code.
929
948
  `ctx.env`.
930
949
  - **Production:** `npx @alexkroman1/aai-cli secret put NAME`
931
950
  - **Access:** `ctx.env.MY_KEY` in tool execute functions.
932
- - **AssemblyAI key:** CLI prompts on first use, stores globally. No `.env`
933
- entry needed. For CI, set `ASSEMBLYAI_API_KEY` env var.
951
+ - **AssemblyAI key:** `npx @alexkroman1/aai-cli login` links your account and
952
+ stores the key globally the only way the CLI authenticates. No `.env`
953
+ entry needed. For CI, point `AAI_CONFIG_DIR` at a config dir holding a
954
+ logged-in key (an exported `ASSEMBLYAI_API_KEY` does not authenticate).
934
955
 
935
956
  ## Voice rules for systemPrompt
936
957
 
@@ -984,9 +1005,11 @@ Common mistakes when working in aai projects:
984
1005
  platform's Modal/Deno sandbox; the self-hosted `aai dev` server has no
985
1006
  sandbox, so there `run_code` refuses with an error result. Deploy to test
986
1007
  it end-to-end, or use the `calculate` builtin for simple arithmetic in dev.
987
- - **`ctx.db` throws until storage is enabled.** Enable it with
988
- `aai storage enable` (CLI), the Storage toggle (studio), or `DATABASE_URL`
989
- in `.env` (`aai dev`) before shipping tools that persist data.
1008
+ - **`ctx.db` throws until the database is enabled.** Enable it with
1009
+ `aai storage enable` (CLI), Settings Database (studio), or
1010
+ `DATABASE_URL` in `.env` (`aai dev`) before shipping tools that persist
1011
+ data. In the studio it takes effect when each agent next deploys — the
1012
+ preview redeploys itself, production on the next publish.
990
1013
  - **The database is per-app.** Rows are shared by every session of one
991
1014
  deployment — key them yourself if sessions must not see each other's data
992
1015
  (or keep session-scoped data in `ctx.state`).
@@ -1002,7 +1025,9 @@ Common mistakes when working in aai projects:
1002
1025
  - Agent code runs in a sandboxed worker — use `fetch` for HTTP, `ctx.env`
1003
1026
  for secrets
1004
1027
  - Tool execution timeout: 30 seconds
1005
- - `maxSteps` limits tool calls per turn (default 10) — increase for
1006
- multi-tool workflows
1028
+ - `maxSteps` limits tool calls per turn (default 10) — lower it for a
1029
+ latency-sensitive agent. On reaching the cap the agent spends one more LLM
1030
+ step with tools switched off, so it answers with what it has instead of
1031
+ going silent mid-chain
1007
1032
  - Tool returns `undefined` if execute function has no return statement —
1008
1033
  always return a value
@@ -11,15 +11,15 @@
11
11
  "publish:agent": "aai publish"
12
12
  },
13
13
  "dependencies": {
14
- "@alexkroman1/aai": "^5.8.1",
15
- "@alexkroman1/aai-ui": "^5.8.1",
14
+ "@alexkroman1/aai": "^5.10.0",
15
+ "@alexkroman1/aai-ui": "^5.10.0",
16
16
  "react": "^19.2.8",
17
17
  "react-dom": "^19.2.8",
18
18
  "tailwindcss": "^4.0.0",
19
19
  "zod": "^4.4.3"
20
20
  },
21
21
  "devDependencies": {
22
- "@alexkroman1/aai-cli": "^5.8.1",
22
+ "@alexkroman1/aai-cli": "^5.10.0",
23
23
  "@tailwindcss/vite": "^4.3.3",
24
24
  "@types/node": "^26.1.1",
25
25
  "@types/react": "^19.2.17",
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { a as unwrapCancel, n as log, s as fail, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { t as slugRequest } from "./_slug-api-CGJSST9B.mjs";
2
+ import { a as ok, n as fail } from "./_output-CC300DzW.mjs";
3
+ import { n as log, o as unwrapCancel } from "./_ui-DfwfDbT-.mjs";
4
+ import { t as slugRequest } from "./_slug-api-fRNNR8tz.mjs";
4
5
  import * as p from "@clack/prompts";
5
6
  import { text } from "node:stream/consumers";
6
7
  //#region secret.ts
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { n as log, s as fail, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { t as slugRequest } from "./_slug-api-CGJSST9B.mjs";
2
+ import { a as ok, n as fail } from "./_output-CC300DzW.mjs";
3
+ import { n as log } from "./_ui-DfwfDbT-.mjs";
4
+ import { t as slugRequest } from "./_slug-api-fRNNR8tz.mjs";
4
5
  import * as p from "@clack/prompts";
5
6
  //#region storage.ts
6
7
  async function storageRequest(cwd, init, server) {
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { n as log, o as CliError, t as fmtUrl, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { s as updateProjectConfig } from "./_config-Y5V-5Krn.mjs";
4
- import { t as resolveServerEnv } from "./_server-common-CnaP_Urf.mjs";
5
- import { i as resolveDeployTarget } from "./_agent-2nVugrN3.mjs";
6
- import { layerScaffold } from "./_templates-BfyRyE7z.mjs";
2
+ import { a as ok, t as CliError } from "./_output-CC300DzW.mjs";
3
+ import { n as log, t as fmtUrl } from "./_ui-DfwfDbT-.mjs";
4
+ import { s as updateProjectConfig } from "./_config-DMyolIk9.mjs";
5
+ import { t as resolveServerEnv } from "./_server-common-DX8Bfrf5.mjs";
6
+ import { i as resolveDeployTarget } from "./_agent-DS2PUJcl.mjs";
7
+ import { layerScaffold } from "./_templates-BWJOiWOO.mjs";
7
8
  import { n as apiRequest } from "./_api-client-B-upMGkc.mjs";
8
9
  import path from "node:path";
9
10
  import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
@@ -290,7 +291,7 @@ async function syncEnvSecrets(cwd, serverUrl, apiKey, slug) {
290
291
  }
291
292
  async function executePublish(opts) {
292
293
  if (!opts.skipTypecheck) {
293
- const { assertTypechecks } = await import("./_typecheck-gate-DvE8S3aQ.mjs");
294
+ const { assertTypechecks } = await import("./_typecheck-gate-DB-PY0A3.mjs");
294
295
  await assertTypechecks(opts.cwd);
295
296
  }
296
297
  const pushed = await pushProject(opts);
@@ -1,5 +1,4 @@
1
1
  import { agent } from "@alexkroman1/aai";
2
- import { assemblyAIStt } from "@alexkroman1/aai/stt";
3
2
  import { dashboardView } from "./shared.ts";
4
3
  import systemPrompt from "./system-prompt.md?raw";
5
4
  import { incidentAddNote } from "./tools/incident_add_note.ts";
@@ -20,11 +19,10 @@ export default agent({
20
19
  // One projection replaces eleven `ctx.send("incidents", ...)` calls, and
21
20
  // is the single place that decides caller PII stays server-side.
22
21
  syncState: dashboardView,
23
- // Only the STT stage is declared (LLM and TTS stay on the AssemblyAI
24
- // defaults): a dispatcher reads addresses and unit numbers in bursts with
25
- // pauses inside one message, so end-of-turn silence has to be longer than
26
- // the default or "unit twelve respond to" splits in two.
27
- stt: assemblyAIStt({ minTurnSilenceMs: 2200 }),
22
+ // A dispatcher reads addresses and unit numbers in bursts with pauses inside
23
+ // one message ("unit twelve respond to"). The default pipeline's
24
+ // `max_turn_silence` already tolerates that; reach for
25
+ // `assemblyAIStt({ maxTurnSilenceMs })` only if your callers pause longer.
28
26
  systemPrompt,
29
27
  greeting:
30
28
  "Dispatch Command Center online. Restoring operational state. I'm ready to take incoming calls, manage active incidents, or run dispatch operations. Say 'dashboard' for a full status report. What do we have.",
@@ -1,5 +1,4 @@
1
1
  import { agent } from "@alexkroman1/aai";
2
- import { assemblyAIStt } from "@alexkroman1/aai/stt";
3
2
  import type { StateSlot } from "./shared.ts";
4
3
  import { storeView } from "./shared.ts";
5
4
  import { createDefaultState } from "./store.ts";
@@ -41,11 +40,10 @@ export default agent({
41
40
  // authenticated one may reach the browser.
42
41
  syncState: storeView,
43
42
 
44
- // Callers read order numbers and ten-digit item numbers in bursts with
45
- // pauses inside one utterance, so end-of-turn silence has to be longer than
46
- // the default or "W seven six seven … eight oh seven two" splits in two.
47
- stt: assemblyAIStt({ minTurnSilenceMs: 2200 }),
48
-
43
+ // Callers read order numbers and ten-digit item numbers in bursts with pauses
44
+ // inside one utterance ("W seven six seven eight oh seven two"). The default
45
+ // pipeline's `max_turn_silence` already tolerates that; reach for
46
+ // `assemblyAIStt({ maxTurnSilenceMs })` only if your callers pause longer.
49
47
  systemPrompt,
50
48
  greeting:
51
49
  "Thanks for calling. Before I can look anything up I'll need to find your account — " +
@@ -105,16 +105,18 @@ describe("tool registry", () => {
105
105
  );
106
106
  });
107
107
 
108
+ // Both sweep the registry, so they assert softly: adding a batch of tools
109
+ // should list every one still missing its entry, not just the first.
108
110
  test("every registered tool has sample args, so the sweeps below cover it", () => {
109
111
  for (const [name] of registry) {
110
- expect(SAMPLE_ARGS[name], `add SAMPLE_ARGS["${name}"]`).toBeDefined();
112
+ expect.soft(SAMPLE_ARGS[name], `add SAMPLE_ARGS["${name}"]`).toBeDefined();
111
113
  }
112
114
  });
113
115
 
114
116
  test("every tool declares a description the model can act on", () => {
115
117
  for (const [name, def] of registry) {
116
- expect(def.description, name).toBeTruthy();
117
- expect(def.description.length, name).toBeGreaterThan(40);
118
+ expect.soft(def.description, name).toBeTruthy();
119
+ expect.soft(def.description.length, name).toBeGreaterThan(40);
118
120
  }
119
121
  });
120
122
  });
@@ -19,7 +19,9 @@ describe("normalizeOrderId", () => {
19
19
  "5866402",
20
20
  " #w 586 6402 ",
21
21
  ]) {
22
- expect(normalizeOrderId(spoken), spoken).toBe("#W5866402");
22
+ // Soft: a narrowed normalizer drops a whole family of spoken forms, and
23
+ // which ones it drops is what says where the pattern went wrong.
24
+ expect.soft(normalizeOrderId(spoken), spoken).toBe("#W5866402");
23
25
  }
24
26
  });
25
27
  });
@@ -37,7 +39,7 @@ describe("resolveOrder — canonical ids", () => {
37
39
  const state = stateFor("olivia_ito_3591");
38
40
  for (const spoken of ["#W5866402", "W 5866402", "5866402"]) {
39
41
  const order = resolveOrder(state, spoken);
40
- expect(isError(order) ? null : order.order_id, spoken).toBe("#W5866402");
42
+ expect.soft(isError(order) ? null : order.order_id, spoken).toBe("#W5866402");
41
43
  }
42
44
  });
43
45
 
@@ -52,7 +54,7 @@ describe("resolveOrder — shorthand", () => {
52
54
  const state = stateFor("olivia_ito_3591");
53
55
  for (const spoken of ["the delivered one", "my delivered order", "delivered"]) {
54
56
  const order = resolveOrder(state, spoken);
55
- expect(isError(order) ? null : order.order_id, spoken).toBe("#W5866402");
57
+ expect.soft(isError(order) ? null : order.order_id, spoken).toBe("#W5866402");
56
58
  }
57
59
  });
58
60
 
@@ -61,7 +63,7 @@ describe("resolveOrder — shorthand", () => {
61
63
  const result = resolveOrder(state, "my pending order");
62
64
  if (!isError(result)) throw new Error("expected ambiguity");
63
65
  for (const id of ["#W5442520", "#W7941031", "#W3657213"]) {
64
- expect(result.error).toContain(id);
66
+ expect.soft(result.error, `ambiguity message omits ${id}`).toContain(id);
65
67
  }
66
68
  });
67
69
 
@@ -90,18 +90,25 @@ describe("seed shape", () => {
90
90
  expect(Object.keys(seed.orders)).toHaveLength(22);
91
91
  });
92
92
 
93
+ // These four sweep the whole corpus, so they assert SOFTLY: one hand-edited
94
+ // order usually breaks several records at once, and a hard failure reports
95
+ // the first and hides how far the damage runs.
93
96
  test("every order belongs to a seeded user and is listed on that user", () => {
94
97
  for (const order of Object.values(seed.orders)) {
95
98
  const user = seed.users[order.user_id];
96
- expect(user, `order ${order.order_id} has no seeded user`).toBeDefined();
97
- expect(user?.orders).toContain(order.order_id);
99
+ expect.soft(user, `order ${order.order_id} has no seeded user`).toBeDefined();
100
+ expect
101
+ .soft(user?.orders ?? [], `${order.order_id} is not listed on its user`)
102
+ .toContain(order.order_id);
98
103
  }
99
104
  });
100
105
 
101
106
  test("every user's listed orders are all seeded", () => {
102
107
  for (const user of Object.values(seed.users)) {
103
108
  for (const orderId of user.orders) {
104
- expect(seed.orders[orderId], `${user.user_id} lists unseeded ${orderId}`).toBeDefined();
109
+ expect
110
+ .soft(seed.orders[orderId], `${user.user_id} lists unseeded ${orderId}`)
111
+ .toBeDefined();
105
112
  }
106
113
  }
107
114
  });
@@ -110,11 +117,10 @@ describe("seed shape", () => {
110
117
  for (const order of Object.values(seed.orders)) {
111
118
  for (const item of order.items) {
112
119
  const variant = seed.products[item.product_id]?.variants[item.item_id];
113
- expect(
114
- variant,
115
- `${order.order_id}: ${item.item_id} missing from its product`,
116
- ).toBeDefined();
117
- expect(variant?.price).toBe(item.price);
120
+ expect
121
+ .soft(variant, `${order.order_id}: ${item.item_id} missing from its product`)
122
+ .toBeDefined();
123
+ expect.soft(variant?.price, `${order.order_id}: ${item.item_id} price`).toBe(item.price);
118
124
  }
119
125
  }
120
126
  });
@@ -123,10 +129,12 @@ describe("seed shape", () => {
123
129
  for (const order of Object.values(seed.orders)) {
124
130
  const user = seed.users[order.user_id];
125
131
  for (const payment of order.payment_history) {
126
- expect(
127
- user?.payment_methods[payment.payment_method_id],
128
- `${order.order_id}: unknown method ${payment.payment_method_id}`,
129
- ).toBeDefined();
132
+ expect
133
+ .soft(
134
+ user?.payment_methods[payment.payment_method_id],
135
+ `${order.order_id}: unknown method ${payment.payment_method_id}`,
136
+ )
137
+ .toBeDefined();
130
138
  }
131
139
  }
132
140
  });
@@ -148,7 +156,7 @@ describe("seed coverage — each datum backs a specific test in agent.test.ts",
148
156
  test("every status the tools branch on is present", () => {
149
157
  const statuses = new Set(Object.values(seed.orders).map((o) => o.status));
150
158
  for (const s of ["pending", "processed", "delivered", "cancelled"]) {
151
- expect(statuses, `no ${s} order seeded`).toContain(s);
159
+ expect.soft(statuses, `no ${s} order seeded`).toContain(s);
152
160
  }
153
161
  });
154
162
 
@@ -57,7 +57,9 @@ describe("storeView", () => {
57
57
  "#W4316152",
58
58
  "#W1840144",
59
59
  ]) {
60
- expect(json, `projection leaked ${leaked}`).not.toContain(leaked);
60
+ // Soft: a widened projection leaks several fields at once, and the full
61
+ // list is what says how much of the other customers' records got out.
62
+ expect.soft(json, `projection leaked ${leaked}`).not.toContain(leaked);
61
63
  }
62
64
  });
63
65
 
@@ -84,15 +86,17 @@ describe("DEMO_PERSONAS", () => {
84
86
  const users = Object.values((structuredClone(seedJson) as unknown as Store).users);
85
87
  for (const persona of DEMO_PERSONAS) {
86
88
  const user = users.find((u) => u.email === persona.email);
87
- expect(user, `no seeded customer has email ${persona.email}`).toBeDefined();
88
- expect(`${user?.name.first_name} ${user?.name.last_name}`).toBe(persona.name);
89
- expect(user?.address.zip).toBe(persona.zip);
89
+ expect.soft(user, `no seeded customer has email ${persona.email}`).toBeDefined();
90
+ expect
91
+ .soft(`${user?.name.first_name} ${user?.name.last_name}`, persona.email)
92
+ .toBe(persona.name);
93
+ expect.soft(user?.address.zip, persona.email).toBe(persona.zip);
90
94
  }
91
95
  });
92
96
 
93
97
  test("every persona carries a hint, so a user can pick a path deliberately", () => {
94
98
  for (const persona of DEMO_PERSONAS) {
95
- expect(persona.hint.length, persona.name).toBeGreaterThan(20);
99
+ expect.soft(persona.hint.length, persona.name).toBeGreaterThan(20);
96
100
  }
97
101
  });
98
102
  });
@@ -167,11 +167,13 @@ describe("applyConsequences MISS matrix", () => {
167
167
  ] as const) {
168
168
  const state = playingState();
169
169
  const { deltas, clockEvents } = applyConsequences(state, miss, position, "standard", null);
170
- expect(state.health).toBe(5 - dmg);
171
- expect(deltas.health).toBe(-dmg);
172
- expect(state.clocks[0]?.filled).toBe(ticks);
173
- expect(deltas.clockTicks).toBe(ticks);
174
- expect(clockEvents).toHaveLength(0); // 4-segment clock not full yet
170
+ // Each row starts from a fresh state, so they are independent — assert
171
+ // softly and a rebalanced table reports the whole matrix in one run.
172
+ expect.soft(state.health, position).toBe(5 - dmg);
173
+ expect.soft(deltas.health, position).toBe(-dmg);
174
+ expect.soft(state.clocks[0]?.filled, position).toBe(ticks);
175
+ expect.soft(deltas.clockTicks, position).toBe(ticks);
176
+ expect.soft(clockEvents, position).toHaveLength(0); // 4-segment clock not full yet
175
177
  }
176
178
  });
177
179
 
@@ -341,8 +341,8 @@ export function saveGameState(ctx: ToolContext, state: GameState): void {
341
341
 
342
342
  // ── Persistent save slots (ctx.db) ───────────────────────────────────────────
343
343
  // save_game / load_game are genuine cross-session persistence, so they use
344
- // the app's SQL database. Requires storage: `aai storage enable` (or the
345
- // Storage toggle in the studio); under `aai dev`, set DATABASE_URL in .env.
344
+ // the app's SQL database. Requires storage: `aai storage enable` (or
345
+ // Settings Database in the studio); under `aai dev`, set DATABASE_URL in .env.
346
346
  //
347
347
  // Slots are keyed by name alone — the whole point of a save is loading it in
348
348
  // a LATER session, whose sessionId differs, so the key can't embed one. The
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { n as log, s as fail, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { a as errorMessage, n as binFromPackageJson, r as errorCode } from "./_utils-Ch0J4s6a.mjs";
2
+ import { a as ok, n as fail } from "./_output-CC300DzW.mjs";
3
+ import { n as log } from "./_ui-DfwfDbT-.mjs";
4
+ import { a as errorMessage, n as binFromPackageJson, r as errorCode } from "./_utils-8KKw-bzi.mjs";
4
5
  import { createRequire } from "node:module";
5
6
  import { existsSync } from "node:fs";
6
7
  import path from "node:path";
@@ -50,8 +51,7 @@ function runVitest(cwd) {
50
51
  testFile
51
52
  ], {
52
53
  cwd,
53
- stdio: "inherit",
54
- env: { NODE_OPTIONS: "--experimental-strip-types" }
54
+ stdio: "inherit"
55
55
  });
56
56
  return true;
57
57
  }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as errorMessage, n as binFromPackageJson } from "./_utils-Ch0J4s6a.mjs";
3
- import { existsSync } from "node:fs";
2
+ import { a as errorMessage, n as binFromPackageJson } from "./_utils-8KKw-bzi.mjs";
3
+ import { existsSync, readFileSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { spawn } from "node:child_process";
6
6
  //#region typecheck.ts
@@ -56,13 +56,69 @@ function findTypescriptPackage(cwd) {
56
56
  dir = parent;
57
57
  }
58
58
  }
59
- /** Resolve the project's TypeScript compiler entry (its own `tsc` bin). */
60
- function resolveTscEntry(cwd) {
59
+ /**
60
+ * Resolve the project's TypeScript compiler entry (its own `tsc` bin), plus
61
+ * its major version — see {@link tscArgs} for what the version decides.
62
+ */
63
+ function resolveTsc(cwd) {
61
64
  const dir = findTypescriptPackage(cwd);
62
65
  if (dir === void 0) throw new Error("no typescript package in the project's node_modules");
63
- const bin = binFromPackageJson(path.join(dir, "package.json"), "tsc");
66
+ const manifest = path.join(dir, "package.json");
67
+ const bin = binFromPackageJson(manifest, "tsc");
64
68
  if (!bin) throw new Error("installed typescript package declares no tsc bin");
65
- return bin;
69
+ return {
70
+ entry: bin,
71
+ major: readMajor(manifest)
72
+ };
73
+ }
74
+ /**
75
+ * The resolved compiler's major version, or 0 when it can't be read.
76
+ *
77
+ * 0 is the safe answer: {@link tscArgs} only ADDS flags above a version
78
+ * floor, so an unreadable manifest degrades to the flag set every TypeScript
79
+ * accepts rather than failing a build over a cosmetic read.
80
+ */
81
+ function readMajor(manifest) {
82
+ try {
83
+ const { version } = JSON.parse(readFileSync(manifest, "utf-8"));
84
+ return typeof version === "string" ? Number.parseInt(version, 10) || 0 : 0;
85
+ } catch {
86
+ return 0;
87
+ }
88
+ }
89
+ /**
90
+ * The `tsc` argv for one project check.
91
+ *
92
+ * `--singleThreaded` is the interesting one, and it is a SPEEDUP here rather
93
+ * than a throttle. TypeScript 7 parallelizes parse/check/emit by default,
94
+ * which pays off on a repo-sized program and costs on a single agent project —
95
+ * and this function only ever checks one of those. Measured on the templates
96
+ * project (14 agents, the closest in-repo analogue of a studio workspace):
97
+ *
98
+ * - pinned to 1 core: 2.4–2.9s parallel vs 1.2–1.4s single — ~2x faster
99
+ * - on 4 cores: 1.21–1.24s parallel vs 1.01–1.04s single
100
+ *
101
+ * The 1-core number is the one that matters: a guest sandbox RESERVES one CPU
102
+ * (`SANDBOX_CPU`) and this same check runs after every settled write burst in
103
+ * the studio (`aai-guest/studio-write-diagnostics.ts`), where the whole design
104
+ * rests on it finishing in well under a second. Parallelism inside a one-core
105
+ * reservation is oversubscription: the threads exist, contend, and the wall
106
+ * clock doubles.
107
+ *
108
+ * Gated on major >= 7 because the flag is TS 7's, and an unknown compiler
109
+ * option is a HARD error (TS5023) — a project pinning an older TypeScript
110
+ * would fail its build on a flag it never asked for. `engines`/scaffold pin
111
+ * `^7`, so in practice this floor is always met; it exists so a project that
112
+ * pins otherwise degrades instead of breaking.
113
+ */
114
+ function tscArgs(entry, major) {
115
+ return [
116
+ entry,
117
+ "--noEmit",
118
+ "--pretty",
119
+ "false",
120
+ ...major >= 7 ? ["--singleThreaded"] : []
121
+ ];
66
122
  }
67
123
  /**
68
124
  * Typecheck the project at `cwd` with its own tsconfig + compiler.
@@ -76,9 +132,9 @@ async function typecheckProject(cwd) {
76
132
  ok: true,
77
133
  skipped: true
78
134
  };
79
- let tscEntry;
135
+ let tsc;
80
136
  try {
81
- tscEntry = resolveTscEntry(cwd);
137
+ tsc = resolveTsc(cwd);
82
138
  } catch (err) {
83
139
  return {
84
140
  ok: false,
@@ -86,12 +142,7 @@ async function typecheckProject(cwd) {
86
142
  };
87
143
  }
88
144
  const result = await new Promise((resolve, reject) => {
89
- const child = spawn(process.execPath, [
90
- tscEntry,
91
- "--noEmit",
92
- "--pretty",
93
- "false"
94
- ], {
145
+ const child = spawn(process.execPath, tscArgs(tsc.entry, tsc.major), {
95
146
  cwd,
96
147
  timeout: TYPECHECK_TIMEOUT_MS,
97
148
  stdio: [
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-cli",
3
- "version": "5.8.1",
3
+ "version": "5.10.0",
4
4
  "type": "module",
5
5
  "bin": {
6
- "aai": "dist/cli.mjs"
6
+ "aai": "bin.mjs"
7
7
  },
8
8
  "exports": {
9
9
  "./client-bundler": {
@@ -23,6 +23,7 @@
23
23
  }
24
24
  },
25
25
  "files": [
26
+ "bin.mjs",
26
27
  "dist"
27
28
  ],
28
29
  "dependencies": {
@@ -37,8 +38,8 @@
37
38
  "p-timeout": "^7.0.1",
38
39
  "vite": "^8.1.5",
39
40
  "zod": "^4.4.3",
40
- "@alexkroman1/aai-ui": "5.8.1",
41
- "@alexkroman1/aai": "5.8.1"
41
+ "@alexkroman1/aai": "5.10.0",
42
+ "@alexkroman1/aai-ui": "5.10.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "playwright": "^1.61.1",