Skip to content

refactor(mcp): project system family output schemas from their owning module - #3014

Open
thymikee wants to merge 3 commits into
mainfrom
refactor/mcp-output-schemas-2819-system-index-schemas
Open

thymikee wants to merge 3 commits into
mainfrom
refactor/mcp-output-schemas-2819-system-index-schemas

Conversation

@thymikee

@thymikee thymikee commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Summary

Moves the 10 hand-authored MCP outputSchema entries for the system-family commands (back, home, orientation, app-switcher, fold, action-button, tv-remote, clipboard, appstate, keyboard) out of src/mcp/command-output-schemas.ts into a SYSTEM_COMMAND_OUTPUT_SCHEMAS map exported from the owning module, src/commands/system/index.ts, registered in PROJECTED_FAMILIES per the #2810 projection seam. Each family's schemas live next to the command they describe instead of in one large hand-authored file.

Test coverage for these schemas moves out of the legacy aggregations into a colocated command-tools-system-schemas.test.ts, including a reference-equality proof that the composed map holds the module's own object (not a copy) for every entry, and a bidirectional ownership check (module <-> registry) after review.

constSchema is now shared from src/commands/command-input.ts instead of being duplicated between this module and command-output-schemas.ts. FOLD_SCREEN_COORDINATE_SPACE's declaration moved to device-rotation.ts, which this module already loads through the @agent-device/contracts/device facade, so importing it no longer pulls fold-runtime.ts into the CLI's eager module closure; fold-runtime.ts still exports the same value on its published subpath (type-checked against the new declaration) for existing external consumers.

Part of #2819. 8 files touched, no behavior change.

Validation

Tested at c02b2c766e3b769eafcc4b8b0e57d08329b315ee.

  • pnpm check:affected --run: 554 test files / 4260 tests passed, all runnable checks passed (format, lint, typecheck, layering, fallow, build, vitest-related).
  • Not device-facing (pure MCP schema move); the issue's required proof is a byte-identical published-schema comparison instead. Dumped COMMAND_OUTPUT_SCHEMAS for all 10 migrated commands via a small Node script importing src/mcp/command-output-schemas.ts directly (Node 26 native TS), once on origin/main and once on this head: diff before.json after.json printed no differences ("IDENTICAL") -- the composed map is unchanged for all 10 keys.
  • Mutation check: temporarily made fold's spread entry a shallow copy instead of referencing the module's own object; this failed both the reference-equality test and the extended disjointness test, confirming the tests guard the "not a copy" invariant. Reverted before committing.
  • Coverage: the CI Coverage job failed on the previous head because src/commands/system/index.ts imported FOLD_SCREEN_COORDINATE_SPACE from @agent-device/contracts/fold-runtime, a module the CLI closure did not otherwise load (295 -> 296 modules, eager-closure-budgets.test.ts). Fixed by relocating the declaration to device-rotation.ts (already loaded via the device facade); fold-runtime.ts still exports the constant as a literal (not a re-export) so its own published subpath's closure does not grow either, and a type-only import of the new FoldScreenCoordinateSpace alias makes any drift between the two a compile error. pnpm check:affected --run passes the closure gate locally and Coverage is green on this head.
  • Compatibility: an automated review flagged that the first version of this fix dropped FOLD_SCREEN_COORDINATE_SPACE from the released @agent-device/contracts/fold-runtime subpath (in npm since v0.21.9), which would have broken existing importers. Fixed as described above -- the value ships from both subpaths, type-locked to one declaration.

No unresolved risk.

Review in cubic

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.88 MB 4.88 MB -76 B
Package (unpacked) 4.88 MB 4.88 MB -76 B
Package (download) 1.46 MB 1.46 MB +58 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.3 ms 28.3 ms -0.1 ms
CLI --help 80.2 ms 81.4 ms +1.2 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/commands/system/index.ts">

<violation number="1" location="src/commands/system/index.ts:87">
P3: The change is described as exporting a "frozen" map, but `satisfies` only constrains types — the object and every nested schema stay mutable at runtime, and the new reference-equality tests share the same reference so an in-place mutation would pass unnoticed. Either drop the "frozen" characterization or actually free the map (deep-`Object.freeze`) if immutability is the intent.</violation>

<violation number="2" location="src/commands/system/index.ts:188">
P1: The appstate schema rejects successful HarmonyOS responses. Use the same `android | harmonyos` platform vocabulary as `AppStateCommandResult` so MCP clients can validate HarmonyOS appstate results.</violation>

<violation number="3" location="src/commands/system/index.ts:199">
P1: The keyboard schema rejects HarmonyOS dismiss and enter results. Add `harmonyos` to the platform enum so the MCP output schema accepts every platform the keyboard runtime returns.</violation>

<violation number="4" location="src/commands/system/index.ts:210">
P2: The keyboard output schema omits the contract's `mechanism` field, so MCP consumers cannot discover the iOS dismiss-key disclosure from `outputSchema`. Add an optional `mechanism` enum for `dismissKey`.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

// packages/contracts/src/keyboard.ts — flat closed shape; `platform`/`action` always present.
keyboard: objectSchema(
{
platform: enumSchema(['android', 'ios']),

@cubic-dev-ai cubic-dev-ai Bot Sep 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The keyboard schema rejects HarmonyOS dismiss and enter results. Add harmonyos to the platform enum so the MCP output schema accepts every platform the keyboard runtime returns.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/system/index.ts, line 199:

<comment>The keyboard schema rejects HarmonyOS dismiss and enter results. Add `harmonyos` to the platform enum so the MCP output schema accepts every platform the keyboard runtime returns.</comment>

<file context>
@@ -64,6 +74,158 @@ const TV_REMOTE_LONGPRESS_PRESET_MS = 500;
+  // packages/contracts/src/keyboard.ts — flat closed shape; `platform`/`action` always present.
+  keyboard: objectSchema(
+    {
+      platform: enumSchema(['android', 'ios']),
+      action: enumSchema(['status', 'dismiss', 'enter']),
+      visible: booleanSchema(),
</file context>
Suggested change
platform: enumSchema(['android', 'ios']),
platform: enumSchema(['android', 'harmonyos', 'ios']),
Fix with cubic

),
objectSchema(
{
platform: constSchema('android'),

@cubic-dev-ai cubic-dev-ai Bot Sep 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The appstate schema rejects successful HarmonyOS responses. Use the same android | harmonyos platform vocabulary as AppStateCommandResult so MCP clients can validate HarmonyOS appstate results.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/system/index.ts, line 188:

<comment>The appstate schema rejects successful HarmonyOS responses. Use the same `android | harmonyos` platform vocabulary as `AppStateCommandResult` so MCP clients can validate HarmonyOS appstate results.</comment>

<file context>
@@ -64,6 +74,158 @@ const TV_REMOTE_LONGPRESS_PRESET_MS = 500;
+      ),
+      objectSchema(
+        {
+          platform: constSchema('android'),
+          package: stringSchema(),
+          activity: stringSchema(),
</file context>
Suggested change
platform: constSchema('android'),
platform: enumSchema(['android', 'harmonyos']),
Fix with cubic

inputMethodPackage: stringSchema(),
focusedPackage: stringSchema(),
focusedResourceId: stringSchema(),
inputOwner: enumSchema(['app', 'ime', 'unknown']),

@cubic-dev-ai cubic-dev-ai Bot Sep 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The keyboard output schema omits the contract's mechanism field, so MCP consumers cannot discover the iOS dismiss-key disclosure from outputSchema. Add an optional mechanism enum for dismissKey.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/system/index.ts, line 210:

<comment>The keyboard output schema omits the contract's `mechanism` field, so MCP consumers cannot discover the iOS dismiss-key disclosure from `outputSchema`. Add an optional `mechanism` enum for `dismissKey`.</comment>

<file context>
@@ -64,6 +74,158 @@ const TV_REMOTE_LONGPRESS_PRESET_MS = 500;
+      inputMethodPackage: stringSchema(),
+      focusedPackage: stringSchema(),
+      focusedResourceId: stringSchema(),
+      inputOwner: enumSchema(['app', 'ime', 'unknown']),
+      message: stringSchema(),
+    },
</file context>
Suggested change
inputOwner: enumSchema(['app', 'ime', 'unknown']),
inputOwner: enumSchema(['app', 'ime', 'unknown']),
mechanism: enumSchema(['dismissKey']),
Fix with cubic

Comment thread src/mcp/__tests__/command-tools-system-schemas.test.ts Outdated
* `additionalProperties: false`, so additive response fields such as `settle`/`cost` keep
* validating. `back`'s settle observation is grafted separately by the trait derivation pass.
*/
export const SYSTEM_COMMAND_OUTPUT_SCHEMAS = {

@cubic-dev-ai cubic-dev-ai Bot Sep 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The change is described as exporting a "frozen" map, but satisfies only constrains types — the object and every nested schema stay mutable at runtime, and the new reference-equality tests share the same reference so an in-place mutation would pass unnoticed. Either drop the "frozen" characterization or actually free the map (deep-Object.freeze) if immutability is the intent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/system/index.ts, line 87:

<comment>The change is described as exporting a "frozen" map, but `satisfies` only constrains types — the object and every nested schema stay mutable at runtime, and the new reference-equality tests share the same reference so an in-place mutation would pass unnoticed. Either drop the "frozen" characterization or actually free the map (deep-`Object.freeze`) if immutability is the intent.</comment>

<file context>
@@ -64,6 +74,158 @@ const TV_REMOTE_LONGPRESS_PRESET_MS = 500;
+ * `additionalProperties: false`, so additive response fields such as `settle`/`cost` keep
+ * validating. `back`'s settle observation is grafted separately by the trait derivation pass.
+ */
+export const SYSTEM_COMMAND_OUTPUT_SCHEMAS = {
+  back: objectSchema(
+    {
</file context>
Fix with cubic

Comment thread src/commands/system/index.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at 110cb57. The moved system-family schemas match the originals, but the new constSchema at src/commands/system/index.ts line 77 is a copy of the private helper at src/mcp/command-output-schemas.ts line 50. Each remaining #2819 family with a const discriminant would copy it again, and the copies could drift. Please export constSchema from src/commands/command-input.ts next to objectSchema and enumSchema, and use it at both sites.

Not blocking: the new test could pin Object.keys(SYSTEM_COMMAND_OUTPUT_SCHEMAS) with deepEqual, as the replay test does, to check ownership in both directions.

CI: checks are still queued.

@thymikee

Copy link
Copy Markdown
Member Author

CI at 110cb57: the Coverage failure is likely caused by this PR. The eager-closure budget test in scripts/__tests__/eager-closure-budgets.test.ts reports that src/cli.ts now loads 296 modules instead of 295, through src/commands/system/index.ts → @agent-device/contracts/fold-runtime. That import is new in this diff. Please take FOLD_SCREEN_COORDINATE_SPACE from a module the CLI closure already loads, or load it lazily, and rerun the test. The code finding above still applies.

@thymikee
thymikee added this pull request to stack #3043 September 28, 2026 13:44
@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at a595a82. This fixes the finding from the review at 110cb57: src/commands/system/index.ts now takes FOLD_SCREEN_COORDINATE_SPACE from @agent-device/contracts/device, which the CLI already loads, so the eager-closure budget should be back at its earlier size. I did not run the budget test myself.

Not blocking: packages/contracts/src/fold-runtime.ts:5 still re-exports the constant, so it has two import paths. platform-apple/src/foldable/pose.ts could import it from @agent-device/contracts/device, and then the re-export could go.

CI is still running. Coverage runs the eager-closure budget test on this exact import route, so a Coverage failure would be related to this change. Coverage and Repo Guards need to pass at a595a82.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 28, 2026
@thymikee
thymikee force-pushed the refactor/mcp-output-schemas-2819-system-index-schemas branch from a595a82 to 5c22d65 Compare September 28, 2026 17:56

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/contracts/src/fold-runtime.ts">

<violation number="1" location="packages/contracts/src/fold-runtime.ts:1">
P1: This removes `FOLD_SCREEN_COORDINATE_SPACE` from the published `@agent-device/contracts/fold-runtime` subpath, breaking existing consumers that import the value there. Preserve the compatibility re-export from `device-rotation.ts` while using the relocated constant internally.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

@@ -1,13 +1,10 @@
import type { FoldPose, SetFoldPoseInput } from './device-rotation.ts';
import type { FoldPose, FoldScreenCoordinateSpace, SetFoldPoseInput } from './device-rotation.ts';

@cubic-dev-ai cubic-dev-ai Bot Sep 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This removes FOLD_SCREEN_COORDINATE_SPACE from the published @agent-device/contracts/fold-runtime subpath, breaking existing consumers that import the value there. Preserve the compatibility re-export from device-rotation.ts while using the relocated constant internally.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/contracts/src/fold-runtime.ts, line 1:

<comment>This removes `FOLD_SCREEN_COORDINATE_SPACE` from the published `@agent-device/contracts/fold-runtime` subpath, breaking existing consumers that import the value there. Preserve the compatibility re-export from `device-rotation.ts` while using the relocated constant internally.</comment>

<file context>
@@ -1,13 +1,10 @@
-import type { FoldPose, SetFoldPoseInput } from './device-rotation.ts';
-import { FOLD_SCREEN_COORDINATE_SPACE } from './device-rotation.ts';
+import type { FoldPose, FoldScreenCoordinateSpace, SetFoldPoseInput } from './device-rotation.ts';
 import type { RuntimeOperationFact } from './platform-runtime.ts';
 
</file context>
Suggested change
import type { FoldPose, FoldScreenCoordinateSpace, SetFoldPoseInput } from './device-rotation.ts';
import type { FoldPose, FoldScreenCoordinateSpace, SetFoldPoseInput } from './device-rotation.ts';
export { FOLD_SCREEN_COORDINATE_SPACE } from './device-rotation.ts';
Fix with cubic

@thymikee

Copy link
Copy Markdown
Member Author

Addressed both review notes.

  • constSchema is now exported once from src/commands/command-input.ts and imported at both call sites, so the system module and the shared output-schemas file can't drift apart (5c22d65).
  • The ownership test now checks both directions: every command this module claims to own, and every command the registry attributes to this module, so a command missing from either side fails the test (5c22d65).
  • Coverage: confirmed the cause you called out. src/commands/system/index.ts pulled in fold-runtime.ts for FOLD_SCREEN_COORDINATE_SPACE, a module the CLI closure didn't otherwise load (295 -> 296 modules). Moved the constant to device-rotation.ts, which this file already loads through the device facade, and kept fold-runtime.ts importing it type-only so its own closure doesn't grow either (557cbd6). Coverage now passes on this head.

Pushed back on the automated review's HarmonyOS enum and keyboard mechanism field suggestions: checked both against the actual result types and the daemon handler, and they describe a real gap between the hand-written schema and what the runtime can return, but that gap already exists on main today, before this move. Adding it here would turn a proven byte-identical move into a behavior change and break the "not a copy" test this PR relies on for its validation. Left it as is so it can be fixed and verified as its own change.

CI: the earlier iOS and Android Smoke Tests failures on this PR (a simulator readiness timeout and an "adb: device offline" emulator disconnect) were infra flakes unrelated to this change, which only touches MCP schema definitions. Both have already passed on the rerun; the other Smoke Tests jobs are still running but were green before this push too.

Rebased and force-pushed the child branch on top of these two commits so its own diff stays clean.

@thymikee
thymikee force-pushed the refactor/mcp-output-schemas-2819-system-index-schemas branch from 5c22d65 to c02b2c7 Compare September 28, 2026 18:17
@thymikee

Copy link
Copy Markdown
Member Author

One more from the automated review: the last push dropped FOLD_SCREEN_COORDINATE_SPACE from @agent-device/contracts/fold-runtime, and that subpath has shipped the value since v0.21.9, so removing it would have broken existing consumers. Fixed (c02b2c7): fold-runtime.ts still exports the constant, now as a literal rather than a re-export so its own published closure does not grow, with a type-only import of the relocated declaration so the two values can't drift apart without a compile error. check:affected passes again on this head.

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at c02b2c7. FOLD_SCREEN_COORDINATE_SPACE is exported again from the fold-runtime subpath with the same value, and fold-runtime.ts now has only type imports, so it no longer pulls kernel/errors into the CLI closure. This stays ready for human review.

Not blocking: the constant is now declared in both device-rotation.ts and fold-runtime.ts; a small leaf module that both import from would keep one declaration.

Both Smoke Tests jobs were still queued at review time.

… module

Moves the 10 hand-authored MCP outputSchema entries for back, home,
orientation, app-switcher, fold, action-button, tv-remote, clipboard,
appstate, and keyboard out of command-output-schemas.ts into a frozen
SYSTEM_COMMAND_OUTPUT_SCHEMAS map exported from src/commands/system/index.ts,
following the #2810 projection seam. Registers the family in
PROJECTED_FAMILIES and moves its shape-assertion tests into a colocated
command-tools-system-schemas.test.ts, including a reference-equality proof
that accounts for the one entry (back) the settle-observation derivation
pass copies rather than passing through untouched.

Part of #2819.
…losure

FOLD_SCREEN_COORDINATE_SPACE lived in fold-runtime.ts, a module the CLI
closure did not otherwise load. Importing it directly from
src/commands/system/index.ts pulled fold-runtime.ts (and its own
dependents) into cli.ts's eager import graph, tripping the
eager-closure-budgets gate (295 -> 296 modules).

Move the constant's declaration to device-rotation.ts, which the same
file already loads via the @agent-device/contracts/device facade for
DEVICE_ROTATIONS/FOLD_POSES, and re-export it from the device facade so
importers reach it there.

fold-runtime.ts keeps publishing FOLD_SCREEN_COORDINATE_SPACE (an
external, released API on this subpath) as a literal rather than a
re-export, so importing it does not eagerly load device-rotation.ts and
its own eager closure does not grow either; a type-only import of the
new FoldScreenCoordinateSpace alias still makes any drift between the
two a compile error. Its runtime value consumer (platform-apple's fold
pose runtime) now takes the constant from the device facade it already
imports.
constSchema was hand-copied into src/commands/system/index.ts, drifting
from the identical private helper in command-output-schemas.ts. Export
it once from command-input.ts, next to the other schema-primitive
builders, and import it at both sites.

The ownership test for the projected family map only checked that each
of the module's own entries names the module as owner; it did not check
the inverse, so a system-family command the registry attributes to this
module but missing from SYSTEM_COMMAND_OUTPUT_SCHEMAS would still pass.
Compare the two sets directly.
@thymikee
thymikee force-pushed the refactor/mcp-output-schemas-2819-system-index-schemas branch from c02b2c7 to bc830f5 Compare September 28, 2026 20:45
@thymikee

Copy link
Copy Markdown
Member Author

Rebased onto the updated main; new head is bc830f5. The three commits replayed cleanly with no conflicts, and check:affected passes on it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant