Skip to content

Local skills and commands: codex impl#3788

Open
lmtr0 wants to merge 2 commits into
pingdotgg:mainfrom
lmtr0:local-skills-and-commands/codex
Open

Local skills and commands: codex impl#3788
lmtr0 wants to merge 2 commits into
pingdotgg:mainfrom
lmtr0:local-skills-and-commands/codex

Conversation

@lmtr0

@lmtr0 lmtr0 commented Jul 8, 2026

Copy link
Copy Markdown

Important

This is the Codex implementation adapter. To make the PR small and easy to review I have created different branches for the other adapters. This branch contains all the codex code and #3787 contains the shared implementation. If they both get merged, I'll create PRs for claude code, opencode, and cursor which I already have implemented, but I would rather not spam the repository with tons of PRs.

Why

Same as #3787

What Changed

Adds codex provider implementation for project-scoped skills and slash-commands discovery.

Code Changes

  • Implemented project capability probes for Codex, Claude, Cursor, and OpenCode.

Testing

  • vp run typecheck
  • vp check

UI Changes

Same as the parent branch

Checklist

  • This PR is small and focused
  • explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes
    • No animations or interaction effects

Note

Medium Risk
Composer menus and skill highlighting now depend on Codex app-server subprocess probes per cwd; failures surface to users instead of silent empty lists, and first-load latency is visible despite caching.

Overview
Adds Codex as the first driver with live project-scoped skill discovery, wired through the shared providers.projectCapabilities RPC and composer hooks from the parent work.

On the server, CodexDriver exposes composerCapabilities.list, which spawns the Codex app-server for the request cwd, calls skills/list, and returns skills with empty slash commands. Results are cached per cwd for 30s (capacity 64), with forceReload bypassing the cache. Probes run under an 8s timeout and map failures to ProviderProjectCapabilitiesError / ServerProviderProjectCapabilitiesError.

listCodexProjectSkills is extracted in CodexProvider.ts (with exported parseCodexSkillsListResponse). ProviderRegistry gains listProjectCapabilities, returning empty arrays for disabled, unavailable, or drivers without composerCapabilities, and surfacing probe errors for enabled Codex instances.

Web and mobile composers stop using static provider snapshot skills/commands for / and $ menus. They use useProviderProjectCapabilities (environment, instance, thread/worktree cwd), show loading while the RPC is pending, and pass errors into ComposerCommandPopover. The command popover can appear even with zero matches; mobile drops the built-in /model entry from the slash menu. Thread feed skills for markdown highlighting follow the same capability source.

Reviewed by Cursor Bugbot for commit 091e39e. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add provider project capabilities (skills and slash commands) via Codex RPC endpoint

  • Introduces a providers.projectCapabilities WebSocket RPC that fetches project-scoped skills and slash commands for a given provider instance and cwd, backed by ProviderRegistry.listProjectCapabilities.
  • Implements listCodexProjectSkills in CodexDriver, which spawns the Codex app-server, runs the skills/list protocol, and caches results per cwd with a 30-second TTL and optional forceReload.
  • Adds useProviderProjectCapabilities hooks (web and mobile) that query the new RPC, expose skills, slashCommands, isPending, error, and a refresh function with snapshot fallback when querying is unavailable.
  • Wires the capabilities into ChatComposer, ThreadComposer, and timeline/feed views, replacing static provider-snapshot lookups with live, environment- and cwd-scoped data.
  • Adds ExactNonBlankString, ServerProviderProjectCapabilitiesInput/Result/Error contract schemas with strict non-blank cwd validation and an 8-second probe timeout.
📊 Macroscope summarized 5c85692. 27 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fe09da93-239d-4ddc-bd79-1ed2d48dbc07

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 8, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One Effect service convention issue found in the new provider project-capabilities error. See the inline comment.

Posted via Macroscope — Effect Service Conventions

Comment on lines +169 to +174
detail: Schema.String,
cause: Schema.optional(Schema.Defect()),
},
) {
override get message(): string {
return `Provider '${this.provider}' failed to list project capabilities for instance '${this.instanceId}' in '${this.cwd}': ${this.detail}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

detail merely copies cause.detail/cause.message (via describeCapabilityProbeFailure inside makeProviderProjectCapabilitiesError), and the message getter is then built from this.detail. The error conventions forbid adding a detail field that copies cause.message and using it to construct the wrapper message; the message must be derived exclusively from stable structural attributes (provider, instanceId, cwd), while the underlying failure is preserved only as cause.

Drop detail and derive the message from the structural fields:

   {
     provider: Schema.String,
     instanceId: Schema.String,
     cwd: Schema.String,
-    detail: Schema.String,
     cause: Schema.optional(Schema.Defect()),
   },
 ) {
   override get message(): string {
-    return `Provider '${this.provider}' failed to list project capabilities for instance '${this.instanceId}' in '${this.cwd}': ${this.detail}`;
+    return `Provider '${this.provider}' failed to list project capabilities for instance '${this.instanceId}' in '${this.cwd}'.`;
   }
 }

Then simplify makeProviderProjectCapabilitiesError to construct the error directly with cause and remove describeCapabilityProbeFailure.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/provider/Drivers/CodexDriver.ts
Comment thread apps/mobile/src/features/threads/ThreadComposer.tsx
Comment thread apps/server/src/provider/Layers/CursorProvider.ts Outdated
Comment thread apps/web/src/state/queries.ts
Comment thread apps/server/src/provider/Layers/CodexProvider.ts
Comment thread apps/web/src/state/queries.ts
Comment thread apps/mobile/src/state/queries.ts
@lmtr0 lmtr0 force-pushed the local-skills-and-commands/codex branch from 5c85692 to 091e39e Compare July 8, 2026 01:57
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:XXL 1,000+ changed lines (additions + deletions). labels Jul 8, 2026
@lmtr0 lmtr0 mentioned this pull request Jul 8, 2026
4 tasks
)?.skills ?? [],
[selectedEnvironmentServerConfig, selectedModel?.instanceId],
);
const selectedProviderCapabilities = useProviderProjectCapabilities({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium threads/new-task-flow-provider.tsx:384

When workspaceMode is switched back to "local", the provider capabilities query still uses the previously selected selectedWorktreePath instead of the project's workspaceRoot, so the composer shows skills and slash-commands for the old worktree rather than the local project. setWorkspaceMode preserves selectedWorktreePath when updating the draft, so switching to local doesn't clear it, and the query at line 387 passes selectedWorktreePath || selectedProject?.workspaceRoot, which picks up the stale worktree path. Consider passing workspaceMode === "worktree" ? selectedWorktreePath : selectedProject?.workspaceRoot so capabilities match the active workspace.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/new-task-flow-provider.tsx around line 384:

When `workspaceMode` is switched back to `"local"`, the provider capabilities query still uses the previously selected `selectedWorktreePath` instead of the project's `workspaceRoot`, so the composer shows skills and slash-commands for the old worktree rather than the local project. `setWorkspaceMode` preserves `selectedWorktreePath` when updating the draft, so switching to local doesn't clear it, and the query at line 387 passes `selectedWorktreePath || selectedProject?.workspaceRoot`, which picks up the stale worktree path. Consider passing `workspaceMode === "worktree" ? selectedWorktreePath : selectedProject?.workspaceRoot` so capabilities match the active workspace.

? provider
: null;
}, [queryTarget, target.providerInstanceId, target.providers]);
useEffect(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium state/queries.ts:162

After calling refresh(), every subsequent fetch for that target keeps forceReload: true applied indefinitely, so the cache is bypassed on every later render/remount and the live capability probe re-runs repeatedly. refresh() sets forceReloadTargetKey to targetKey, but the cleanup effect at lines 162–169 only clears it when the target changes or queryTarget becomes null — never after the forced reload succeeds. The effect needs a condition that resets forceReloadTargetKey once the forced query is no longer pending.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/state/queries.ts around line 162:

After calling `refresh()`, every subsequent fetch for that target keeps `forceReload: true` applied indefinitely, so the cache is bypassed on every later render/remount and the live capability probe re-runs repeatedly. `refresh()` sets `forceReloadTargetKey` to `targetKey`, but the cleanup effect at lines 162–169 only clears it when the target changes or `queryTarget` becomes `null` — never after the forced reload succeeds. The effect needs a condition that resets `forceReloadTargetKey` once the forced query is no longer pending.

@macroscopeapp

macroscopeapp Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

5 blocking correctness issues found. This PR introduces new project-scoped provider capabilities - a substantial feature addition with new RPC endpoints, caching infrastructure, and changed runtime behavior for skill/command resolution. Multiple unresolved review comments identify bugs including stale worktree paths, missing fallbacks on RPC failure, and accidental removal of the /model command.

You can customize Macroscope's approvability policy. Learn more.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 091e39e. Configure here.

};
}

export function useProviderProjectCapabilities(target: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing cwd shows wrong skills

Low Severity

When cwd is missing or blank, useProviderProjectCapabilities skips the project RPC but still falls back to the provider snapshot’s skills and slashCommands. Those snapshot lists come from Codex status probes against process.cwd(), not the user’s project or worktree, so composer hints can show the wrong project’s skills.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 091e39e. Configure here.

Comment on lines +251 to +260
const snapshotCapabilities = useMemo(() => {
const provider =
target.providers?.find((candidate) => candidate.instanceId === target.providerInstanceId) ??
null;
return queryTarget === null &&
provider !== null &&
isProviderProjectCapabilitiesProviderQueryable(provider)
? provider
: null;
}, [queryTarget, target.providerInstanceId, target.providers]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium state/queries.ts:251

When the project-scoped capabilities RPC fails, useProviderProjectCapabilities returns EMPTY_PROVIDER_SKILLS and EMPTY_PROVIDER_SLASH_COMMANDS instead of falling back to the provider's existing skills and slashCommands. This means a transient RPC error (transport failure, unsupported cwd, etc.) makes all / commands and skills disappear, even though the provider snapshot still has them.

The snapshotCapabilities fallback is only applied when queryTarget === null, so any non-null query that results in a Failure drops the snapshot data. Consider applying the snapshot fallback whenever result.data is absent, not only when there is no active query.

Suggested change
const snapshotCapabilities = useMemo(() => {
const provider =
target.providers?.find((candidate) => candidate.instanceId === target.providerInstanceId) ??
null;
return queryTarget === null &&
provider !== null &&
isProviderProjectCapabilitiesProviderQueryable(provider)
? provider
: null;
}, [queryTarget, target.providerInstanceId, target.providers]);
const snapshotCapabilities = useMemo(() => {
const provider =
target.providers?.find((candidate) => candidate.instanceId === target.providerInstanceId) ??
null;
return provider !== null && isProviderProjectCapabilitiesProviderQueryable(provider)
? provider
: null;
}, [queryTarget, target.providerInstanceId, target.providers]);
Also found in 1 other location(s)

apps/mobile/src/features/threads/ThreadDetailScreen.tsx:276

useProviderProjectCapabilities at ThreadDetailScreen line 276 replaces the previous direct serverConfig.providers[].skills fallback with an RPC-backed lookup that returns [] on the first project-capabilities failure. Because ThreadFeed now receives selectedProviderCapabilities.skills, any transient RPC error or unsupported projectCapabilities response strips all known skill metadata from the feed, so existing skill references in messages stop rendering as skills even though props.serverConfig still has the provider's advertised skills.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/state/queries.ts around lines 251-260:

When the project-scoped capabilities RPC fails, `useProviderProjectCapabilities` returns `EMPTY_PROVIDER_SKILLS` and `EMPTY_PROVIDER_SLASH_COMMANDS` instead of falling back to the provider's existing `skills` and `slashCommands`. This means a transient RPC error (transport failure, unsupported cwd, etc.) makes all `/` commands and skills disappear, even though the provider snapshot still has them.

The `snapshotCapabilities` fallback is only applied when `queryTarget === null`, so any non-null query that results in a `Failure` drops the snapshot data. Consider applying the snapshot fallback whenever `result.data` is absent, not only when there is no active query.

Also found in 1 other location(s):
- apps/mobile/src/features/threads/ThreadDetailScreen.tsx:276 -- `useProviderProjectCapabilities` at `ThreadDetailScreen` line 276 replaces the previous direct `serverConfig.providers[].skills` fallback with an RPC-backed lookup that returns `[]` on the first project-capabilities failure. Because `ThreadFeed` now receives `selectedProviderCapabilities.skills`, any transient RPC error or unsupported `projectCapabilities` response strips all known skill metadata from the feed, so existing skill references in messages stop rendering as skills even though `props.serverConfig` still has the provider's advertised `skills`.

const providerSnapshot = (yield* Ref.get(providersRef)).find(
(provider) => provider.instanceId === input.providerInstanceId,
);
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium Layers/ProviderRegistry.ts:501

listProjectCapabilities only gates on installed, enabled, and availability, so an unauthenticated-but-installed instance still reaches instance.composerCapabilities.list. The live probe raises ProviderProjectCapabilitiesError instead of returning empty capabilities, so composer capability lookups fail for every unauthenticated instance until the user logs in. Consider adding an auth/readiness guard (e.g. checking providerSnapshot.auth !== "authenticated") before calling the live probe, or document why unauthenticated instances should be allowed to reach it.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/ProviderRegistry.ts around line 501:

`listProjectCapabilities` only gates on `installed`, `enabled`, and `availability`, so an unauthenticated-but-installed instance still reaches `instance.composerCapabilities.list`. The live probe raises `ProviderProjectCapabilitiesError` instead of returning empty capabilities, so composer capability lookups fail for every unauthenticated instance until the user logs in. Consider adding an auth/readiness guard (e.g. checking `providerSnapshot.auth !== "authenticated"`) before calling the live probe, or document why unauthenticated instances should be allowed to reach it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium threads/ThreadComposer.tsx:354

The built-in /model slash command was removed from allBuiltIn in composerMenuItems, so typing /m or /mo in the composer now returns No matching commands. instead of surfacing /model for autocompletion. This regresses a core composer command on mobile — users can no longer discover or switch models via the slash menu. Consider re-adding the /model entry to allBuiltIn.

       const allBuiltIn = [
+        {
+          id: "cmd:model",
+          type: "slash-command" as const,
+          command: "model",
+          label: "/model",
+          description: "Switch model",
+        },
         {
           id: "cmd:plan",
           type: "slash-command" as const,
           command: "plan",
           label: "/plan",
           description: "Switch to plan mode",
         },
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadComposer.tsx around lines 354-360:

The built-in `/model` slash command was removed from `allBuiltIn` in `composerMenuItems`, so typing `/m` or `/mo` in the composer now returns `No matching commands.` instead of surfacing `/model` for autocompletion. This regresses a core composer command on mobile — users can no longer discover or switch models via the slash menu. Consider re-adding the `/model` entry to `allBuiltIn`.

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

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant