diff --git a/bridge/package-lock.json b/bridge/package-lock.json index ae2f9b7..e713d99 100644 --- a/bridge/package-lock.json +++ b/bridge/package-lock.json @@ -1,12 +1,12 @@ { "name": "ftown-bridge", - "version": "0.19.8", + "version": "0.19.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ftown-bridge", - "version": "0.19.8", + "version": "0.19.9", "license": "MIT", "dependencies": { "@xterm/addon-serialize": "^0.14.0", diff --git a/bridge/package.json b/bridge/package.json index 2df0057..af89865 100644 --- a/bridge/package.json +++ b/bridge/package.json @@ -1,6 +1,6 @@ { "name": "ftown-bridge", - "version": "0.19.8", + "version": "0.19.9", "description": "CLI bridge for ftown — generic PTY-over-Centrifugo relay", "type": "module", "main": "dist/index.js", diff --git a/bridge/pi-extension/ftown.js b/bridge/pi-extension/ftown.js index 551f356..4fb322f 100644 --- a/bridge/pi-extension/ftown.js +++ b/bridge/pi-extension/ftown.js @@ -211,7 +211,36 @@ export function registerFtownPiExtension(pi, options = {}) { throw new Error(`Session not found: ${normalized}`); } + function hasOwn(value, key) { + return value !== null && typeof value === 'object' + && Object.prototype.hasOwnProperty.call(value, key); + } + + function requireString(params, field, label = field) { + const value = params?.[field]; + if (typeof value !== 'string' || !value.trim()) throw new Error(`${label} is required`); + } + + function requireProperty(params, field, label = field) { + if (!hasOwn(params, field)) throw new Error(`${label} is required`); + } + + function requireOperation(params, allowed) { + if (!allowed.includes(params?.operation)) { + throw new Error(`operation must be one of: ${allowed.join(', ')}`); + } + } + + function validateMailParams(params) { + requireOperation(params, ['send', 'read']); + if (params.operation === 'send') { + requireString(params, 'target'); + requireString(params, 'body'); + } + } + async function runMail(params) { + validateMailParams(params); if (params.operation === 'read') { if (!ftownSessionId) throw new Error('FTOWN_SESSION_ID is unavailable'); const query = new URLSearchParams({ wait: '0' }); @@ -261,31 +290,19 @@ export function registerFtownPiExtension(pi, options = {}) { label: 'ftown mail', description: 'Send durable mail to another ftown session, or read this session inbox.', parameters: { - anyOf: [ - { - type: 'object', - properties: { - operation: { const: 'send' }, - target: { type: 'string', description: 'Session id/name, or "parent".' }, - body: { type: 'string', minLength: 1 }, - type: { enum: ['message', 'task', 'result', 'escalation'] }, - threadId: { type: 'string' }, - }, - required: ['operation', 'target', 'body'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - operation: { const: 'read' }, - peek: { type: 'boolean', description: 'Do not mark messages delivered.' }, - all: { type: 'boolean', description: 'Include already-delivered messages.' }, - limit: { type: 'integer', minimum: 1, maximum: 100 }, - }, - required: ['operation'], - additionalProperties: false, - }, - ], + type: 'object', + properties: { + operation: { type: 'string', enum: ['send', 'read'] }, + target: { type: 'string', description: 'Session id/name, or "parent".' }, + body: { type: 'string', minLength: 1 }, + type: { type: 'string', enum: ['message', 'task', 'result', 'escalation'] }, + threadId: { type: 'string' }, + peek: { type: 'boolean', description: 'Do not mark messages delivered.' }, + all: { type: 'boolean', description: 'Include already-delivered messages.' }, + limit: { type: 'integer', minimum: 1, maximum: 100 }, + }, + required: ['operation'], + additionalProperties: false, }, async execute(toolCallId, params) { try { @@ -322,6 +339,9 @@ export function registerFtownPiExtension(pi, options = {}) { }); async function runSessions(params) { + requireOperation(params, ['list', 'archive', 'get', 'usage', 'running', 'screen', 'grep']); + if (!['list', 'archive'].includes(params.operation)) requireString(params, 'target'); + if (params.operation === 'grep') requireString(params, 'pattern'); if (params.operation === 'archive') { return requestJson('/api/archive', { method: 'GET' }); } @@ -369,30 +389,19 @@ export function registerFtownPiExtension(pi, options = {}) { label: 'ftown sessions', description: 'List or inspect ftown sessions, running state, archive, token usage, terminal screen, and terminal log matches.', parameters: { - anyOf: [ - { - type: 'object', properties: { operation: { enum: ['list', 'archive'] } }, - required: ['operation'], additionalProperties: false, - }, - ...['get', 'usage', 'running'].map((operation) => ({ - type: 'object', properties: { operation: { const: operation }, target: targetProperty }, - required: ['operation', 'target'], additionalProperties: false, - })), - { - type: 'object', - properties: { operation: { const: 'screen' }, target: targetProperty, ...pageProperties }, - required: ['operation', 'target'], additionalProperties: false, - }, - { - type: 'object', - properties: { - operation: { const: 'grep' }, target: targetProperty, - pattern: { type: 'string', minLength: 1 }, ...pageProperties, - context: { type: 'integer', minimum: 0, maximum: 10 }, - }, - required: ['operation', 'target', 'pattern'], additionalProperties: false, + type: 'object', + properties: { + operation: { + type: 'string', + enum: ['list', 'archive', 'get', 'usage', 'running', 'screen', 'grep'], }, - ], + target: targetProperty, + pattern: { type: 'string', minLength: 1 }, + ...pageProperties, + context: { type: 'integer', minimum: 0, maximum: 10 }, + }, + required: ['operation'], + additionalProperties: false, }, async execute(_toolCallId, params) { try { @@ -422,6 +431,7 @@ export function registerFtownPiExtension(pi, options = {}) { type: 'object', properties: { shell: { + type: 'string', enum: ['claude', 'cursor', 'codex', 'grok', 'pi', 'kimi-code', 'opencode', 'shell', 'zai', 'kimi', 'deepseek', 'fireworks'], }, prompt: { type: 'string', minLength: 1 }, @@ -467,6 +477,10 @@ export function registerFtownPiExtension(pi, options = {}) { }); async function runSessionManage(params) { + requireOperation(params, ['stop', 'remove', 'revive', 'rename', 'reparent']); + requireString(params, 'target'); + if (params.operation === 'rename') requireString(params, 'name'); + if (params.operation === 'reparent') requireProperty(params, 'parent'); if (params.operation === 'revive') { const payload = await requestJson('/api/archive', { method: 'GET' }); const archived = Array.isArray(payload?.archived) ? payload.archived : []; @@ -517,28 +531,17 @@ export function registerFtownPiExtension(pi, options = {}) { label: 'manage ftown session', description: 'Stop, rename, reparent, remove, or revive an ftown session.', parameters: { - anyOf: [ - ...['stop', 'remove', 'revive'].map((operation) => ({ - type: 'object', properties: { operation: { const: operation }, target: targetProperty }, - required: ['operation', 'target'], additionalProperties: false, - })), - { - type: 'object', - properties: { - operation: { const: 'rename' }, target: targetProperty, - name: { type: 'string', minLength: 1 }, - }, - required: ['operation', 'target', 'name'], additionalProperties: false, - }, - { - type: 'object', - properties: { - operation: { const: 'reparent' }, target: targetProperty, - parent: { type: ['string', 'null'], description: 'Parent id/name; null clears it.' }, - }, - required: ['operation', 'target', 'parent'], additionalProperties: false, + type: 'object', + properties: { + operation: { + type: 'string', enum: ['stop', 'remove', 'revive', 'rename', 'reparent'], }, - ], + target: targetProperty, + name: { type: 'string', minLength: 1 }, + parent: { type: ['string', 'null'], description: 'Parent id/name; null clears it.' }, + }, + required: ['operation', 'target'], + additionalProperties: false, }, async execute(toolCallId, params) { try { @@ -567,41 +570,54 @@ export function registerFtownPiExtension(pi, options = {}) { } const loopScheduleProperty = { - anyOf: [ - { - type: 'object', - properties: { - kind: { const: 'interval' }, - everyMs: { type: 'integer', minimum: 1000 }, - }, - required: ['kind', 'everyMs'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - kind: { const: 'cron' }, - expression: { type: 'string', minLength: 1 }, - tz: { type: 'string', minLength: 1 }, - }, - required: ['kind', 'expression'], - additionalProperties: false, - }, - ], + type: 'object', + properties: { + kind: { type: 'string', enum: ['interval', 'cron'] }, + everyMs: { type: 'integer', minimum: 1000 }, + expression: { type: 'string', minLength: 1 }, + tz: { type: 'string', minLength: 1 }, + }, + required: ['kind'], + additionalProperties: false, }; const loopDraftProperties = { name: { type: 'string', minLength: 1 }, task: { type: 'string', minLength: 1 }, schedule: loopScheduleProperty, - shell: { enum: ['claude', 'cursor', 'codex', 'grok', 'pi', 'kimi-code', 'opencode', 'shell'] }, + shell: { type: 'string', enum: ['claude', 'cursor', 'codex', 'grok', 'pi', 'kimi-code', 'opencode', 'shell'] }, workdir: { type: 'string' }, model: { type: 'string' }, enabled: { type: 'boolean' }, - overlapPolicy: { enum: ['skip', 'allow'] }, + overlapPolicy: { type: 'string', enum: ['skip', 'allow'] }, retention: { type: ['integer', 'null'], minimum: 0 }, maxRuntimeMs: { type: 'integer', minimum: 1000 }, group: { type: 'string' }, }; + const loopDraftFields = Object.keys(loopDraftProperties); + + function validateLoopSchedule(schedule) { + requireProperty({ schedule }, 'schedule'); + if (schedule === null || typeof schedule !== 'object') throw new Error('schedule is required'); + requireOperation({ operation: schedule.kind }, ['interval', 'cron']); + if (schedule.kind === 'interval') requireProperty(schedule, 'everyMs', 'schedule.everyMs'); + if (schedule.kind === 'cron') requireString(schedule, 'expression', 'schedule.expression'); + } + + function validateLoopParams(params) { + requireOperation(params, ['list', 'get', 'runs', 'run_now', 'delete', 'create', 'update']); + if (['get', 'runs', 'run_now', 'delete', 'update'].includes(params.operation)) { + requireString(params, 'target'); + } + if (params.operation === 'create') { + requireString(params, 'name'); + requireString(params, 'task'); + requireProperty(params, 'schedule'); + } + if (params.operation === 'update' && !loopDraftFields.some((field) => hasOwn(params, field))) { + throw new Error('At least one field to update is required'); + } + if (hasOwn(params, 'schedule')) validateLoopSchedule(params.schedule); + } function loopBody(params, create = false) { const body = {}; @@ -626,35 +642,21 @@ export function registerFtownPiExtension(pi, options = {}) { label: 'ftown loops', description: 'List, inspect, create, update, delete, or run scheduled ftown loops.', parameters: { - anyOf: [ - { - type: 'object', properties: { operation: { const: 'list' } }, - required: ['operation'], additionalProperties: false, - }, - ...['get', 'runs', 'run_now', 'delete'].map((operation) => ({ - type: 'object', - properties: { operation: { const: operation }, target: { type: 'string' } }, - required: ['operation', 'target'], additionalProperties: false, - })), - { - type: 'object', - properties: { operation: { const: 'create' }, ...loopDraftProperties }, - required: ['operation', 'name', 'task', 'schedule'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - operation: { const: 'update' }, target: { type: 'string' }, ...loopDraftProperties, - }, - required: ['operation', 'target'], - minProperties: 3, - additionalProperties: false, + type: 'object', + properties: { + operation: { + type: 'string', + enum: ['list', 'get', 'runs', 'run_now', 'delete', 'create', 'update'], }, - ], + target: { type: 'string' }, + ...loopDraftProperties, + }, + required: ['operation'], + additionalProperties: false, }, async execute(toolCallId, params) { try { + validateLoopParams(params); if (params.operation === 'create') { return await executeOnce('ftown_loops', toolCallId, async () => toolResult(await requestJson('/api/loops', { diff --git a/bridge/src/pi-extension.test.ts b/bridge/src/pi-extension.test.ts index 61a8be1..1a55fc2 100644 --- a/bridge/src/pi-extension.test.ts +++ b/bridge/src/pi-extension.test.ts @@ -4,6 +4,80 @@ import assert from 'node:assert/strict'; import { registerFtownPiExtension } from '../pi-extension/ftown.js'; describe('ftown Pi extension', () => { + it('registers provider-compatible object schemas for every ftown tool', () => { + const tools: any[] = []; + const pi = { + on() {}, sendUserMessage() {}, registerCommand() {}, + registerTool(tool: any) { tools.push(tool); }, + }; + + registerFtownPiExtension(pi, { + env: {}, + fetch: async () => ({ ok: true, json: async () => ({}) }), + readBridgePointer: async () => null, + }); + + assert.deepEqual(tools.map((tool) => tool.name), [ + 'ftown_mail', + 'ftown_sessions', + 'ftown_session_create', + 'ftown_session_manage', + 'ftown_loops', + ]); + for (const tool of tools) { + assert.equal(tool.parameters.type, 'object', `${tool.name} must have an object schema`); + for (const keyword of ['oneOf', 'anyOf', 'allOf', 'enum', 'const', 'not']) { + assert.equal( + Object.hasOwn(tool.parameters, keyword), + false, + `${tool.name} must not use top-level ${keyword}`, + ); + } + } + }); + + it('rejects incomplete operation-specific arguments before making bridge requests', async () => { + const tools = new Map(); + let requestCount = 0; + const pi = { + on() {}, sendUserMessage() {}, registerCommand() {}, + registerTool(tool: any) { tools.set(tool.name, tool); }, + }; + + registerFtownPiExtension(pi, { + env: { FTOWN_SESSION_ID: 'self' }, + fetch: async () => { + requestCount += 1; + return { ok: true, json: async () => ({}) }; + }, + readBridgePointer: async () => null, + }); + + const cases = [ + ['ftown_mail', { operation: 'send', target: 'worker' }, /body is required/], + ['ftown_sessions', { operation: 'grep', target: 'worker' }, /pattern is required/], + ['ftown_session_manage', { operation: 'rename', target: 'worker' }, /name is required/], + ['ftown_session_manage', { operation: 'reparent', target: 'worker' }, /parent is required/], + ['ftown_loops', { operation: 'create', name: 'Review', task: 'Review work' }, /schedule is required/], + ['ftown_loops', { operation: 'update', target: 'Review' }, /field to update is required/], + ['ftown_loops', { + operation: 'create', name: 'Review', task: 'Review work', + schedule: { kind: 'interval' }, + }, /schedule.everyMs is required/], + ['ftown_loops', { + operation: 'create', name: 'Review', task: 'Review work', + schedule: { kind: 'cron' }, + }, /schedule.expression is required/], + ] as const; + + for (const [toolName, params, expected] of cases) { + const result = await tools.get(toolName).execute(`invalid-${toolName}`, params); + assert.equal(result.isError, true, `${toolName} should reject incomplete arguments`); + assert.match(result.details.error, expected); + } + assert.equal(requestCount, 0); + }); + it('forwards native lifecycle metadata and turns pending mail into a follow-up', async () => { const handlers = new Map Promise>(); const followUps: string[] = []; diff --git a/docs/investigations/2026-08-09-pi-ftown-tool-schema-rejected.md b/docs/investigations/2026-08-09-pi-ftown-tool-schema-rejected.md new file mode 100644 index 0000000..90bb8b9 --- /dev/null +++ b/docs/investigations/2026-08-09-pi-ftown-tool-schema-rejected.md @@ -0,0 +1,147 @@ +--- +type: investigation +symptom: "OpenAI rejects the Pi ftown_mail tool because its parameters are not a top-level object schema" +slug: pi-ftown-tool-schema-rejected +date: 2026-08-09T17:59:12-03:00 +investigator: Foad Kesheh +git_commit: 3ffac51800ad72a96467fdb917d7b7dc77a2d664 +branch: feat/pi-ftown-tools +repository: fmktech/ftown +status: resolved +hypotheses_formed: 3 +hypotheses_rejected: 2 +hypotheses_proven: 1 +related: + - docs/presubmit-report.md +--- + +# Pi ftown tool schema is rejected by OpenAI + +## Symptom + +- **Observed**: `Error: 400: {"message":"Invalid schema for function 'ftown_mail': schema must be a JSON Schema of 'type: \"object\"', got 'type: null'.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}` +- **Expected**: Pi should send a prompt with `ftown_mail` enabled and receive a model response. +- **Delta**: the provider rejects the request before inference because `ftown_mail.parameters` has no top-level `type: "object"` and uses a top-level union. + +## Reproduction + +Environment: Pi 0.83.0, Node 24.12.0, published `ftown-bridge@0.19.8`, OpenAI `gpt-4.1-mini`. + +1. Load only the shipped extension and enable only `ftown_mail`: + + ```sh + pi --provider openai --model gpt-4.1-mini \ + --extension ./bridge/pi-extension/ftown.js \ + --no-extensions --no-skills --no-context-files --no-session \ + --tools ftown_mail --print 'Reply only OK.' + ``` + +2. Verified on 2026-08-09: + + ```text + OpenAI API error (400): {"message":"Invalid schema for function 'ftown_mail': schema must be a JSON Schema of 'type: \"object\"', got 'type: \"None\"'.","type":"invalid_request_error","param":"tools[0].parameters","code":"invalid_function_parameters"} + ``` + +3. Capturing registered tool schemas before any provider serialization produces: + + ```json + [ + { "name": "ftown_mail", "type": null, "hasAnyOf": true }, + { "name": "ftown_sessions", "type": null, "hasAnyOf": true }, + { "name": "ftown_session_create", "type": "object", "hasAnyOf": false }, + { "name": "ftown_session_manage", "type": null, "hasAnyOf": true }, + { "name": "ftown_loops", "type": null, "hasAnyOf": true } + ] + ``` + +## Hypotheses + +#### H1: Operation-discriminated tools use provider-incompatible top-level `anyOf` schemas instead of one top-level object + +- **Layer**: dependency/integration +- **Prediction**: The raw registered schema will have no top-level object type; adding only the type will expose a top-level-union rejection; replacing the top-level union with a flat object will let the identical real-provider request proceed. +- **Verification method**: inspect `bridge/pi-extension/ftown.js:259-289`, capture schemas through a mock registration boundary, then run two one-variable counterfactual extensions against OpenAI. +- **Evidence**: + + ```text + Raw: { "name": "ftown_mail", "type": null, "hasAnyOf": true } + Type-only counterfactual: Invalid schema for function 'ftown_mail': schema must have type 'object' and not have 'oneOf'/'anyOf'/'allOf'/'enum'/'const'/'not' at the top level. + Flat-object counterfactual: OK + ``` + +- **Verdict**: PROVEN +- **Rationale**: The live OpenAI request changes from two deterministic schema errors to success only when the top-level union is replaced by an object schema. + +#### H2: Pi strips a valid top-level `type` while registering or serializing the extension tool + +- **Layer**: tooling/build +- **Prediction**: The extension source will declare `type: "object"`, but a capture after `registerTool` or provider serialization will lose it. +- **Verification method**: intercept `registerTool` directly, before Pi or provider serialization. +- **Evidence**: + + ```text + bridge/pi-extension/ftown.js:263 + parameters: { + anyOf: [ + + captured ftown_mail type: null + ``` + +- **Verdict**: REJECTED +- **Rationale**: The type is absent in the extension-owned object before Pi receives it; Pi cannot be the component that removed it. + +#### H3: The published 0.19.8 package contains an older schema than the reviewed repository source + +- **Layer**: state/data +- **Prediction**: The npm tarball extension hash or schema will differ from the branch copy. +- **Verification method**: pack `ftown-bridge@0.19.8`, hash its extension, and compare it with the branch file. +- **Evidence**: + + ```text + repository: 3eea1c9285bb16a5bfc93a9b65368ff16e219291765505678901609eb809b5f4 + npm tarball: 3eea1c9285bb16a5bfc93a9b65368ff16e219291765505678901609eb809b5f4 + tarball schema begins: parameters: { anyOf: [ + ``` + +- **Verdict**: REJECTED +- **Rationale**: The published and repository extension bytes are identical, so publication skew does not explain the failure. + +## 5 Whys + +Symptom: OpenAI rejects `ftown_mail` before inference. +Why 1? Its parameters schema has no top-level object type and has `anyOf` at the top level. +Why 2? Multi-operation tools were represented as discriminated unions of operation-specific objects. +Why 3? The extension optimized for local validation precision without applying the provider-facing tool-schema compatibility constraints. +Why 4? Tests captured mocked `registerTool` objects and exercised handlers, but did not validate every schema or make a real provider request. +Why 5? The new extension had no cross-provider schema contract gate or native Pi/provider smoke test in the release path. + +## Falsification + +- **Check performed**: counterfactual isolation against the real OpenAI provider. +- **Result**: adding `type: "object"` while retaining top-level `anyOf` still failed with the more specific top-level-combinator error. Flattening only `ftown_mail` into one object schema made the same Pi/OpenAI command return `OK` with exit code 0. +- **Conclusion**: H1 survived and was refined: both the missing object type and the top-level union shape are causal; merely adding a type is insufficient. + +## Root Cause + +- **Immediate cause**: four model-facing tools declare operation variants with top-level `anyOf` (`bridge/pi-extension/ftown.js:264`, `:372`, `:520`, `:629`), which is outside OpenAI's accepted function-parameter root shape. +- **Architectural root**: provider portability was not encoded as a testable schema invariant, and the registration tests stopped at a permissive mock boundary. +- **Rejected H2**: direct registration capture proves the missing type originates in extension source, not Pi serialization. +- **Rejected H3**: the npm artifact and branch extension have identical SHA-256 hashes and schema bytes. +- **Falsification result**: the actual provider accepted the flat-object counterfactual and rejected both the original and type-only variants. + +## Fix + +- Replace each top-level operation union with one `type: "object"` schema whose `operation` is a string enum and whose operation-specific fields are optional at the provider boundary. +- Preserve operation-specific required-field validation in `execute` before any request or mutation. +- Add a regression test that asserts every registered tool has a top-level object schema without top-level combinators, plus operation-validation tests for required fields. +- Re-run the exact Pi/OpenAI reproduction with every ftown tool enabled. + +The confirmed test seams are the public Pi extension registration boundary (`registerTool`), the public tool execution boundary (`execute`), and the native Pi-to-provider request. The user's provider error directly identified the third seam as the missing release-level coverage. + +## Resolution + +- **Diff summary**: all five tools now expose provider-compatible root object schemas; operation variants use string enums and optional operation-specific properties. Conditional required fields and interval/cron schedule requirements are validated before any bridge request. The package version is 0.19.9. +- **Regression tests**: `bridge/src/pi-extension.test.ts` asserts every registered tool schema has an object root without forbidden root combinators and verifies incomplete operation arguments fail before network access. +- **Native verification**: the exact original Pi/OpenAI command fails on 0.19.8. With the corrected extension, Pi 0.83.0 and OpenAI `gpt-4.1-mini` return `OK` with all five ftown tools enabled. +- **Suite verification**: 14 focused extension tests pass; the full bridge suite passes with 571 tests; TypeScript build and `npm pack --dry-run` pass; the package manifest reports `ftown-bridge-0.19.9.tgz`. +- **Follow-up**: keep the provider-compatible schema invariant in the extension test suite so future tools cannot regress to a root union. diff --git a/docs/presubmit-report.md b/docs/presubmit-report.md index e820a07..c01291e 100644 --- a/docs/presubmit-report.md +++ b/docs/presubmit-report.md @@ -8,9 +8,10 @@ Scope: native Pi harness support, lifecycle hooks and token usage, bundled model | Check | Result | Evidence | | --- | --- | --- | -| Bridge unit tests | ✅ | `npm test`: 568 passed, 0 failed. | +| Bridge unit tests | ✅ | `npm test`: 571 passed, 0 failed. | | Bridge typecheck/build | ✅ | `npm run build`: TypeScript compilation completed successfully. | -| Bridge package | ✅ | `npm pack --dry-run`: version 0.19.8 includes `pi-extension/ftown.js` and `pi-extension/API.md`. | +| Bridge package | ✅ | `npm pack --dry-run`: version 0.19.9 includes `pi-extension/ftown.js` and `pi-extension/API.md`. | +| Native Pi/OpenAI schema smoke | ✅ | Pi 0.83.0 with OpenAI `gpt-4.1-mini` accepted all five ftown tools and returned `OK`; the same command deterministically reproduces the 0.19.8 schema rejection. | | UI unit tests | ✅ | `npm test -- --run`: 133 passed, 0 failed across 12 files. | | UI production build | ✅ | The E2E-environment production build completed successfully. | | E2E typecheck | ✅ | The E2E TypeScript check completed without errors. |