@bridge_gpt/mcp-server 0.2.55 → 0.2.57
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.
- package/README.md +26 -10
- package/build/agent-capabilities/types.js +4 -0
- package/build/agent-registry.js +20 -3
- package/build/codex-bridge-profile.js +151 -0
- package/build/codex-home.js +7 -0
- package/build/codex-launch-scope.js +54 -0
- package/build/codex-skill-adapter.js +8 -5
- package/build/command-host.js +9 -0
- package/build/command-provisioning.js +35 -4
- package/build/commands.generated.js +5 -5
- package/build/doctor.js +9 -1
- package/build/epic-implementer-bin.js +3 -3
- package/build/index.js +121 -116
- package/build/init.js +2 -1
- package/build/install-bridge.js +69 -10
- package/build/mcp-host-config.js +4 -0
- package/build/mcp-host-targets.js +21 -11
- package/build/mcp-provisioning.js +1 -1
- package/build/mcp-registration-doctor.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +50 -35
- package/build/start-tickets.js +335 -58
- package/build/version.generated.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -242,6 +242,20 @@ npx -y @bridge_gpt/mcp-server doctor
|
|
|
242
242
|
`doctor` is strictly read-only. It reports what it found — configs, registrations,
|
|
243
243
|
credential availability, prerequisites — and changes nothing.
|
|
244
244
|
|
|
245
|
+
**Codex approval profile.** When selecting Codex during `install`, opt in at the
|
|
246
|
+
prompt or pass `--tools=codex --codex-profile=on` in a scripted install. Bridge
|
|
247
|
+
creates `$CODEX_HOME/bridge.config.toml` (default `~/.codex/bridge.config.toml`)
|
|
248
|
+
with approval settings for the tools used by its review and implementation
|
|
249
|
+
workflows. Bridge-spawned Codex workers then select it automatically, including
|
|
250
|
+
the fresh implementation session after review. Pass `--codex-profile=off` to
|
|
251
|
+
decline; the installer leaves any existing profile content intact. If a profile
|
|
252
|
+
already contains user-authored or conflicting content, the installer leaves it
|
|
253
|
+
untouched and reports that workers cannot use it. Run `doctor --agent codex` to
|
|
254
|
+
inspect the current status. To use the profile in a Codex session you open
|
|
255
|
+
yourself, run `codex --profile bridge`. Bridge `--auto` controls recipe gates;
|
|
256
|
+
Codex MCP tool approvals are a separate setting. The profile does not change
|
|
257
|
+
global `sandbox_mode` or `approval_policy`.
|
|
258
|
+
|
|
245
259
|
</details>
|
|
246
260
|
|
|
247
261
|
<details>
|
|
@@ -351,7 +365,7 @@ later boot.
|
|
|
351
365
|
"mcpServers": {
|
|
352
366
|
"bridge": {
|
|
353
367
|
"command": "npx",
|
|
354
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
368
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],
|
|
355
369
|
"env": {
|
|
356
370
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
357
371
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -373,7 +387,7 @@ later boot.
|
|
|
373
387
|
"bridge": {
|
|
374
388
|
"type": "stdio",
|
|
375
389
|
"command": "npx",
|
|
376
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
390
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],
|
|
377
391
|
"env": {
|
|
378
392
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
379
393
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -395,7 +409,7 @@ later boot.
|
|
|
395
409
|
"bridge": {
|
|
396
410
|
"type": "stdio",
|
|
397
411
|
"command": "npx",
|
|
398
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
412
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],
|
|
399
413
|
"env": {
|
|
400
414
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
401
415
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -418,7 +432,7 @@ Windsurf only supports global MCP configuration.
|
|
|
418
432
|
"mcpServers": {
|
|
419
433
|
"bridge": {
|
|
420
434
|
"command": "npx",
|
|
421
|
-
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
435
|
+
"args": ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"],
|
|
422
436
|
"env": {
|
|
423
437
|
"BAPI_BASE_URL": "https://bridgegpt-api.com",
|
|
424
438
|
"BAPI_REPO_NAME": "your-repo",
|
|
@@ -437,7 +451,7 @@ Windsurf only supports global MCP configuration.
|
|
|
437
451
|
```toml
|
|
438
452
|
[mcp_servers.bridge]
|
|
439
453
|
command = "npx"
|
|
440
|
-
args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
454
|
+
args = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.57", "serve"]
|
|
441
455
|
|
|
442
456
|
[mcp_servers.bridge.env]
|
|
443
457
|
BAPI_BASE_URL = "https://bridgegpt-api.com"
|
|
@@ -985,9 +999,9 @@ npx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]
|
|
|
985
999
|
| Flag | Default | Meaning |
|
|
986
1000
|
|---|---|---|
|
|
987
1001
|
| `--agent claude\|cursor-agent` | `claude` | Agent command to launch in each worktree |
|
|
988
|
-
| `--workflow implement\|review-and-implement` | `implement` | Slash command each spawned worktree runs. `
|
|
1002
|
+
| `--workflow implement\|review-and-implement` | `implement` | Slash command each spawned worktree runs. `review-and-implement` reviews the ticket, then hands approval to a fresh implementation session in the same worktree. `--auto` applies to both phases. |
|
|
989
1003
|
| `--rounds 1\|2` | unset | Review round count forwarded to the review phase. Review-only — valid only with `--workflow review-and-implement`. |
|
|
990
|
-
| `--tier cheap\|basic\|premium` | unset (difficulty routing) | Coarse model-routing override. Bypasses **only** the per-ticket difficulty/tier lookup (`GET /jira/tickets/{KEY}/model-tier`) and applies this one tier to every ticket;
|
|
1004
|
+
| `--tier cheap\|basic\|premium` | unset (difficulty routing) | Coarse model-routing override. Bypasses **only** the per-ticket difficulty/tier lookup (`GET /jira/tickets/{KEY}/model-tier`) and applies this one tier to every ticket; Claude/Cursor use their registry aliases and repository overrides, while Codex uses the authenticated Bridge catalog. It is **not** a raw `--model` alias and never carries an API key or credential. A malformed value fails open to premium routing when its alias is available. Used by the `/review-and-implement` handoff to reuse the review-time tier. |
|
|
991
1005
|
| `--terminal terminal\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |
|
|
992
1006
|
| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |
|
|
993
1007
|
| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |
|
|
@@ -997,15 +1011,17 @@ npx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]
|
|
|
997
1011
|
| `--conductor` | off | Opt into the Conductor system (per-worker `BAPI_CONDUCTOR_*` env + Claude hook injection, lifecycle ledger events, and a supervisor peer tab). **Default off** — a plain run spawns `cd <worktree> && <agent> '/implement-ticket <KEY>'`. |
|
|
998
1012
|
| `-h`, `--help` | — | Show usage |
|
|
999
1013
|
|
|
1000
|
-
Each `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switches each worktree up front (throttled by `--max-parallel`), then opens one tab/session per successful worktree running the selected agent
|
|
1014
|
+
Each `KEY` must match `[A-Z]+-[0-9]+` (e.g., `BAPI-248`). The CLI creates/switches each worktree up front (throttled by `--max-parallel`), then opens one tab/session per successful worktree running the selected agent. Claude is the default. Cursor with `--auto` runs headlessly after a bounded authentication/readiness check; manual Cursor runs remain interactive for human review gates. To launch Cursor Agent instead of Claude Code:
|
|
1001
1015
|
|
|
1002
1016
|
```
|
|
1003
1017
|
npx -y @bridge_gpt/mcp-server start-tickets --agent cursor-agent BAPI-248
|
|
1004
1018
|
```
|
|
1005
1019
|
|
|
1006
|
-
**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket's `difficulty` (1-2 → cheap, 3-6 → basic, 7-10 → premium) and
|
|
1020
|
+
**Difficulty-based model routing.** Before launching each agent, the CLI selects an implementation **model tier** from the ticket's `difficulty` (1-2 → cheap, 3-6 → basic, 7-10 → premium) and may inject `--model` at the spawn boundary. The Python backend returns only the coarse tier (`GET /jira/tickets/{KEY}/model-tier`, computing + caching difficulty on demand). Claude and Cursor map that tier through their agent registry aliases; Cursor validates against `cursor-agent --list-models`. Routing is gated per repo by `difficulty_model_routing_enabled` (default **ON**). Claude/Cursor may use `difficulty_model_tier_overrides` (tier → alias) and retain their premium fallback behavior on an unknown tier or transient failure. When an alias cannot be validated, the CLI omits `--model` and launches the agent default. `--dry-run` resolves routing read-only to preview the command without creating worktrees or opening tabs.
|
|
1021
|
+
|
|
1022
|
+
For Codex, `GET /jira/codex-model-catalog?repo_name=...` is the sole tier-to-model authority. Bridge derives its three bare CLI IDs and SHA-256 mapping revision at request time from `MODEL_REGISTRY`; the launcher fetches and validates that catalog once per invocation. Repository `difficulty_model_tier_overrides` do not apply to Codex. A failed access, config, tier, catalog, revision, or alias check omits `--model` and launches the configured Codex default. The catalog expresses **Bridge provider policy only**: it cannot confirm that the local Codex login or subscription can use a model. The CLI has no supported non-interactive entitlement probe, so routing emits that limitation as a diagnostic and never uses Bridge's OpenAI API key as proof of local access.
|
|
1007
1023
|
|
|
1008
|
-
**Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses
|
|
1024
|
+
**Coarse `--tier` override.** Passing `--tier cheap|basic|premium` bypasses only the per-ticket difficulty lookup and applies that tier to every ticket. Claude/Cursor use the same alias resolution as normal routing; Codex uses the fetched catalog. It is never a raw model alias, and no API key or credential belongs in the spawned command. A malformed tier requests the premium fallback when its alias is available; otherwise the agent default launches. `/review-and-implement` uses this flag to hand its review-time tier snapshot to the fresh implementation session.
|
|
1009
1025
|
|
|
1010
1026
|
**Conductor observability (opt-in via `--conductor`, BAPI-394).** Conductor is **off by default**. With `--conductor`, a run mints a conductor `run_id`, emits events into the local ledger (`~/.config/bridge/events.db`), opens a supervisor peer tab, and (for Claude Code) injects a secret-free lifecycle hook into each worktree; it also sets `BRIDGE_MCP_PROFILE=conductor` so workers get the conductor MCP tools. Observability is best-effort — a conductor failure never blocks a spawn, and `--dry-run` performs no conductor side effects. (Epic-tick dispatch always enables conductor internally.) See **[CONDUCTOR.md](./docs/CONDUCTOR.md)** for the full observability, git-hook, done-gate, and auto-merge reference.
|
|
1011
1027
|
|
|
@@ -5,6 +5,10 @@ export const AGENT_AUTH = {
|
|
|
5
5
|
note: "Claude Code authenticates via keychain/OAuth — cannot be verified from the environment.",
|
|
6
6
|
},
|
|
7
7
|
"cursor-agent": { kind: "env", varName: "CURSOR_API_KEY" },
|
|
8
|
+
codex: {
|
|
9
|
+
kind: "interactive",
|
|
10
|
+
note: "Codex authenticates via login/OAuth — cannot be verified from the environment.",
|
|
11
|
+
},
|
|
8
12
|
};
|
|
9
13
|
/** Default hard timeout per headless probe run (ms). The cursor `-p` hang bug is version-sensitive. */
|
|
10
14
|
export const DEFAULT_PROBE_TIMEOUT_MS = 90_000;
|
package/build/agent-registry.js
CHANGED
|
@@ -26,7 +26,7 @@ export function isModelTier(value) {
|
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* The registry: the ONLY place mapping an agent name to its command/spec. Seeded
|
|
29
|
-
* with
|
|
29
|
+
* with `claude` (default), `cursor-agent`, and `codex`. `as const satisfies` keeps
|
|
30
30
|
* the literal keys (so `AgentName` is a precise union) while type-checking each
|
|
31
31
|
* value against `AgentSpec`.
|
|
32
32
|
*/
|
|
@@ -108,6 +108,20 @@ export const AGENT_REGISTRY = {
|
|
|
108
108
|
premium: "claude-opus-4-8-thinking-high",
|
|
109
109
|
},
|
|
110
110
|
},
|
|
111
|
+
codex: {
|
|
112
|
+
name: "codex",
|
|
113
|
+
command: "codex",
|
|
114
|
+
promptArgStyle: "positional",
|
|
115
|
+
installHint: {
|
|
116
|
+
darwin: "npm install -g @openai/codex",
|
|
117
|
+
linux: "npm install -g @openai/codex",
|
|
118
|
+
win32: "npm install -g @openai/codex",
|
|
119
|
+
},
|
|
120
|
+
authNote: "Codex authenticates interactively on first run — follow its login prompt if asked.",
|
|
121
|
+
// The launch boundary validates a fresh Bridge catalog before injection.
|
|
122
|
+
supportsModelOverride: true,
|
|
123
|
+
modelFlag: "--model",
|
|
124
|
+
},
|
|
111
125
|
};
|
|
112
126
|
/** The default agent used when `--agent` is omitted. */
|
|
113
127
|
export const DEFAULT_AGENT_NAME = "claude";
|
|
@@ -130,7 +144,7 @@ export function resolveAgentSpec(name) {
|
|
|
130
144
|
return null;
|
|
131
145
|
return AGENT_REGISTRY[resolved];
|
|
132
146
|
}
|
|
133
|
-
/** Comma-delimited
|
|
147
|
+
/** Comma-delimited registry names for CLI validation errors (`claude, cursor-agent, codex`). */
|
|
134
148
|
export function formatValidAgentNames() {
|
|
135
149
|
return listAgentNames().join(", ");
|
|
136
150
|
}
|
|
@@ -154,10 +168,13 @@ export function resolveModelAlias(agent, tier, overrides) {
|
|
|
154
168
|
return null;
|
|
155
169
|
if (!tier)
|
|
156
170
|
return null;
|
|
171
|
+
// Codex has no static or repository-override model authority.
|
|
172
|
+
if (agent.name === "codex")
|
|
173
|
+
return null;
|
|
157
174
|
const override = overrides?.[tier];
|
|
158
175
|
const candidate = typeof override === "string" && override.trim().length > 0
|
|
159
176
|
? override.trim()
|
|
160
|
-
: agent.tierModels[tier];
|
|
177
|
+
: agent.tierModels?.[tier];
|
|
161
178
|
if (typeof candidate !== "string" || !isValidModelAlias(candidate))
|
|
162
179
|
return null;
|
|
163
180
|
if (agent.staticModelAliasAllowlist && !agent.staticModelAliasAllowlist.includes(candidate)) {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** Secret-free, opt-in approval settings for Bridge-spawned Codex workers. */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { codexBridgeTableNames } from "./mcp-host-config.js";
|
|
5
|
+
import { MCP_SERVER_NAME } from "./mcp-identity.js";
|
|
6
|
+
export { codexHome } from "./codex-home.js";
|
|
7
|
+
export const BRIDGE_CODEX_PROFILE = "bridge";
|
|
8
|
+
const PROFILE_FILE = "bridge.config.toml";
|
|
9
|
+
const STATE_FILE = "bridge.profile-state.json";
|
|
10
|
+
const MARKER = "# Bridge-managed Codex approval profile (BAPI-1214)";
|
|
11
|
+
/** Tools called by review-and-start, review-and-implement, and implement-ticket. */
|
|
12
|
+
export const BRIDGE_WORKFLOW_TOOLS = [
|
|
13
|
+
"ping", "get_ticket", "get_pipeline_recipe", "request_ticket_review",
|
|
14
|
+
"wait_for_ticket_review", "fresh_base", "generate_decision_page",
|
|
15
|
+
"capture_review_decisions", "request_plan_generation", "get_plan",
|
|
16
|
+
"get_phase_context", "record_phase_result", "create_pull_request",
|
|
17
|
+
"update_jira_status", "config_field", "resolve_ci_checks", "poll_ci_checks",
|
|
18
|
+
"wait_for_ci_checks",
|
|
19
|
+
];
|
|
20
|
+
export function codexProfilePaths(home) {
|
|
21
|
+
return {
|
|
22
|
+
profile: path.join(home, PROFILE_FILE),
|
|
23
|
+
state: path.join(home, STATE_FILE),
|
|
24
|
+
config: path.join(home, "config.toml"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function renderBridgeCodexProfile(key) {
|
|
28
|
+
return [
|
|
29
|
+
MARKER,
|
|
30
|
+
`# Applies only when selected with codex --profile ${BRIDGE_CODEX_PROFILE}.`,
|
|
31
|
+
...BRIDGE_WORKFLOW_TOOLS.flatMap((tool) => [
|
|
32
|
+
`[mcp_servers.${key}.tools.${tool}]`,
|
|
33
|
+
'approval_mode = "auto"',
|
|
34
|
+
"",
|
|
35
|
+
]),
|
|
36
|
+
].join("\n") + "\n";
|
|
37
|
+
}
|
|
38
|
+
function fingerprint(value) {
|
|
39
|
+
return createHash("sha256").update(value).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
async function optionalRead(readFile, filePath) {
|
|
42
|
+
try {
|
|
43
|
+
return await readFile(filePath);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return error.code === "ENOENT" ? null : undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Only a matching managed file and explicit enabled state can activate workers. */
|
|
50
|
+
export async function inspectBridgeCodexProfile(home, readFile) {
|
|
51
|
+
const paths = codexProfilePaths(home);
|
|
52
|
+
const [config, profile, state] = await Promise.all([
|
|
53
|
+
optionalRead(readFile, paths.config),
|
|
54
|
+
optionalRead(readFile, paths.profile),
|
|
55
|
+
optionalRead(readFile, paths.state),
|
|
56
|
+
]);
|
|
57
|
+
if (config === undefined || profile === undefined || state === undefined)
|
|
58
|
+
return { status: "unreadable" };
|
|
59
|
+
const names = codexBridgeTableNames(config ?? "");
|
|
60
|
+
if (names.length > 1)
|
|
61
|
+
return { status: "conflict" };
|
|
62
|
+
const key = names[0] ?? MCP_SERVER_NAME;
|
|
63
|
+
if (state === null)
|
|
64
|
+
return { status: profile === null ? "missing" : "conflict", key };
|
|
65
|
+
let parsed;
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(state);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return { status: "conflict", key };
|
|
71
|
+
}
|
|
72
|
+
if (!parsed || typeof parsed !== "object" || !("enabled" in parsed) || typeof parsed.enabled !== "boolean") {
|
|
73
|
+
return { status: "conflict", key };
|
|
74
|
+
}
|
|
75
|
+
if (!parsed.enabled)
|
|
76
|
+
return { status: "declined", key };
|
|
77
|
+
if (profile === null || !("hash" in parsed) || typeof parsed.hash !== "string")
|
|
78
|
+
return { status: "conflict", key };
|
|
79
|
+
if (profile !== renderBridgeCodexProfile(key) || fingerprint(profile) !== parsed.hash)
|
|
80
|
+
return { status: "conflict", key };
|
|
81
|
+
return { status: "active", key };
|
|
82
|
+
}
|
|
83
|
+
export async function configureBridgeCodexProfile(home, choice, deps) {
|
|
84
|
+
const paths = codexProfilePaths(home);
|
|
85
|
+
const current = await inspectBridgeCodexProfile(home, deps.readFile);
|
|
86
|
+
if (choice === "decline") {
|
|
87
|
+
const oldState = await optionalRead(deps.readFile, paths.state);
|
|
88
|
+
if (oldState === undefined)
|
|
89
|
+
return { status: "unreadable" };
|
|
90
|
+
let existingHash;
|
|
91
|
+
if (oldState !== null) {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(oldState);
|
|
94
|
+
if (!parsed || typeof parsed.enabled !== "boolean")
|
|
95
|
+
return { status: "conflict" };
|
|
96
|
+
if (typeof parsed.hash === "string")
|
|
97
|
+
existingHash = parsed.hash;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { status: "conflict" };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await deps.mkdir(home, { recursive: true });
|
|
104
|
+
await deps.writeFile(paths.state, JSON.stringify({ enabled: false, ...(existingHash ? { hash: existingHash } : {}) }) + "\n");
|
|
105
|
+
return { status: "declined", key: current.key };
|
|
106
|
+
}
|
|
107
|
+
if (current.status === "unreadable")
|
|
108
|
+
return current;
|
|
109
|
+
const config = await optionalRead(deps.readFile, paths.config);
|
|
110
|
+
if (config === undefined)
|
|
111
|
+
return { status: "unreadable" };
|
|
112
|
+
const names = codexBridgeTableNames(config ?? "");
|
|
113
|
+
if (names.length > 1)
|
|
114
|
+
return { status: "conflict" };
|
|
115
|
+
const key = names[0] ?? MCP_SERVER_NAME;
|
|
116
|
+
const existing = await optionalRead(deps.readFile, paths.profile);
|
|
117
|
+
const oldState = await optionalRead(deps.readFile, paths.state);
|
|
118
|
+
const desired = renderBridgeCodexProfile(key);
|
|
119
|
+
if (existing === undefined || oldState === undefined)
|
|
120
|
+
return { status: "unreadable", key };
|
|
121
|
+
if (oldState !== null) {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(oldState);
|
|
124
|
+
if (!parsed || typeof parsed.enabled !== "boolean" ||
|
|
125
|
+
(parsed.enabled && (typeof parsed.hash !== "string" || existing === null || parsed.hash !== fingerprint(existing)))) {
|
|
126
|
+
return { status: "conflict", key };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return { status: "conflict", key };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Existing user content is never modified or implicitly adopted.
|
|
134
|
+
if (existing !== null && existing !== desired) {
|
|
135
|
+
let managed = false;
|
|
136
|
+
if (oldState !== null && existing.startsWith(MARKER)) {
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(oldState);
|
|
139
|
+
managed = typeof parsed?.enabled === "boolean" && parsed?.hash === fingerprint(existing);
|
|
140
|
+
}
|
|
141
|
+
catch { /* malformed state cannot authorize a rewrite */ }
|
|
142
|
+
}
|
|
143
|
+
if (!managed)
|
|
144
|
+
return { status: "conflict", key };
|
|
145
|
+
}
|
|
146
|
+
await deps.mkdir(home, { recursive: true });
|
|
147
|
+
if (existing !== desired)
|
|
148
|
+
await deps.writeFile(paths.profile, desired);
|
|
149
|
+
await deps.writeFile(paths.state, JSON.stringify({ enabled: true, hash: fingerprint(desired) }) + "\n");
|
|
150
|
+
return { status: "active", key };
|
|
151
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
/** Codex's user config root; only an absolute CODEX_HOME is accepted. */
|
|
4
|
+
export function codexHome(env, homedir = os.homedir()) {
|
|
5
|
+
const override = env.CODEX_HOME?.trim();
|
|
6
|
+
return override && path.isAbsolute(override) ? override : path.join(homedir, ".codex");
|
|
7
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Per-process Codex MCP scope, derived from the same manifest and shim as worktree provisioning. */
|
|
2
|
+
import { readBridgeConfig } from "./bridge-config.js";
|
|
3
|
+
import { codexBridgeTableNames } from "./mcp-host-config.js";
|
|
4
|
+
import { codexHome } from "./codex-home.js";
|
|
5
|
+
import { codexProfilePaths, inspectBridgeCodexProfile, BRIDGE_CODEX_PROFILE } from "./codex-bridge-profile.js";
|
|
6
|
+
import { buildMcpServerEntriesForManifest, normalizeWorktreePathForRegistration, } from "./mcp-provisioning.js";
|
|
7
|
+
import { MCP_SERVER_NAME } from "./mcp-identity.js";
|
|
8
|
+
/** A missing Bridge target is a launch failure, never a fallback to user-scoped config. */
|
|
9
|
+
export async function resolveCodexLaunchScope(root, deps, codexConfigPath = codexProfilePaths(codexHome(deps.env ?? process.env, deps.homedir?.())).config) {
|
|
10
|
+
const normalized = normalizeWorktreePathForRegistration(root, deps);
|
|
11
|
+
if (!normalized.ok)
|
|
12
|
+
return { ok: false, error: "Codex MCP scope unavailable: invalid repository path." };
|
|
13
|
+
let manifest;
|
|
14
|
+
try {
|
|
15
|
+
const read = await readBridgeConfig(normalized.path, { readFile: deps.readFile });
|
|
16
|
+
if (!read.ok)
|
|
17
|
+
return { ok: false, error: "Codex MCP scope unavailable: .bridge/config is missing or invalid." };
|
|
18
|
+
manifest = read.manifest;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { ok: false, error: "Codex MCP scope unavailable: could not read .bridge/config." };
|
|
22
|
+
}
|
|
23
|
+
const built = buildMcpServerEntriesForManifest(manifest, normalized.path, deps.mcpServerInvocation, deps.bridgeApiBaseUrl);
|
|
24
|
+
const bridge = built.entries[MCP_SERVER_NAME];
|
|
25
|
+
if (!bridge)
|
|
26
|
+
return { ok: false, error: "Codex MCP scope unavailable: .bridge/config has no Bridge target." };
|
|
27
|
+
let existingConfig = "";
|
|
28
|
+
try {
|
|
29
|
+
existingConfig = await deps.readFile(codexConfigPath);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code !== "ENOENT") {
|
|
33
|
+
return { ok: false, error: "Codex MCP scope unavailable: could not inspect Codex host config." };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const registeredNames = codexBridgeTableNames(existingConfig);
|
|
37
|
+
if (registeredNames.length > 1) {
|
|
38
|
+
return { ok: false, error: "Codex MCP scope unavailable: duplicate Bridge registrations in Codex host config." };
|
|
39
|
+
}
|
|
40
|
+
const bridgeKey = registeredNames[0] ?? MCP_SERVER_NAME;
|
|
41
|
+
const home = codexHome(deps.env ?? process.env, deps.homedir?.());
|
|
42
|
+
const profile = await inspectBridgeCodexProfile(home, deps.readFile);
|
|
43
|
+
const argv = ["-C", normalized.path];
|
|
44
|
+
if (profile.status === "active" && profile.key === bridgeKey)
|
|
45
|
+
argv.push("--profile", BRIDGE_CODEX_PROFILE);
|
|
46
|
+
for (const [name, entry] of Object.entries(built.entries)) {
|
|
47
|
+
const key = name === MCP_SERVER_NAME ? bridgeKey : name;
|
|
48
|
+
// Field-level overrides retain the profile's mcp_servers.<key>.tools policy.
|
|
49
|
+
argv.push("-c", `mcp_servers.${key}.command=${JSON.stringify(entry.command)}`);
|
|
50
|
+
argv.push("-c", `mcp_servers.${key}.args=${JSON.stringify(entry.args)}`);
|
|
51
|
+
argv.push("-c", `mcp_servers.${key}.enabled=true`);
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, argv };
|
|
54
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Codex discovers repository-local skills from `.agents/skills/<name>/SKILL.md`.
|
|
5
5
|
* Bridge command sources use a standalone `$ARGUMENTS` marker, which is a
|
|
6
6
|
* Claude/Cursor command convention rather than a Codex input variable. This
|
|
7
|
-
* adapter replaces
|
|
8
|
-
*
|
|
7
|
+
* adapter replaces that marker with the explicit-skill input contract and
|
|
8
|
+
* translates references to shipped slash commands into Codex skill names.
|
|
9
9
|
*/
|
|
10
10
|
export const CODEX_SKILLS_DIR = ".agents/skills";
|
|
11
11
|
function commandStem(filename) {
|
|
@@ -19,7 +19,7 @@ export function codexSkillNameForCommand(filename) {
|
|
|
19
19
|
return `bridge-${commandStem(filename)}`;
|
|
20
20
|
}
|
|
21
21
|
/** Render the one Codex-native skill artifact for a canonical command. */
|
|
22
|
-
export function renderCodexSkill(filename, command) {
|
|
22
|
+
export function renderCodexSkill(filename, command, knownCommands = [commandStem(filename)]) {
|
|
23
23
|
const stem = commandStem(filename);
|
|
24
24
|
const skillName = codexSkillNameForCommand(filename);
|
|
25
25
|
const invocation = `$${skillName}`;
|
|
@@ -28,10 +28,12 @@ export function renderCodexSkill(filename, command) {
|
|
|
28
28
|
`Treat the text after \`${invocation}\` in the user's message as this workflow's input.`,
|
|
29
29
|
"If no input follows the invocation, follow the canonical workflow's existing usage/error behavior.",
|
|
30
30
|
].join("\n");
|
|
31
|
+
const commandPattern = new RegExp(`(?<![A-Za-z0-9_./-])/(${knownCommands.join("|")})(?=[^A-Za-z0-9-]|$)`, "g");
|
|
31
32
|
const adaptedCommand = command
|
|
32
33
|
.split("\n")
|
|
33
34
|
.map((line) => (line.trim() === "$ARGUMENTS" ? inputContract : line))
|
|
34
|
-
.join("\n")
|
|
35
|
+
.join("\n")
|
|
36
|
+
.replace(commandPattern, (_match, name) => `$bridge-${name}`);
|
|
35
37
|
return {
|
|
36
38
|
skillName,
|
|
37
39
|
relativePath: `${CODEX_SKILLS_DIR}/${skillName}/SKILL.md`,
|
|
@@ -49,7 +51,8 @@ export function renderCodexSkill(filename, command) {
|
|
|
49
51
|
}
|
|
50
52
|
/** Build the complete, deterministically ordered Codex skill bundle. */
|
|
51
53
|
export function codexSkillAssets(commands) {
|
|
54
|
+
const names = Object.keys(commands).map(commandStem).sort((a, b) => b.length - a.length);
|
|
52
55
|
return Object.entries(commands)
|
|
53
56
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
54
|
-
.map(([filename, command]) => renderCodexSkill(filename, command));
|
|
57
|
+
.map(([filename, command]) => renderCodexSkill(filename, command, names));
|
|
55
58
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Specialize the shared front-door command for a known installation host. */
|
|
2
|
+
export function renderCommandForHost(filename, content, host) {
|
|
3
|
+
if (filename !== "review-and-start.md" || host !== "cursor")
|
|
4
|
+
return content;
|
|
5
|
+
const marker = "**Command host default: none.**";
|
|
6
|
+
if (!content.includes(marker))
|
|
7
|
+
throw new Error("review-and-start host default marker is missing");
|
|
8
|
+
return content.replace(marker, "**Command host default: cursor.**");
|
|
9
|
+
}
|
|
@@ -26,9 +26,11 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import path from "path";
|
|
28
28
|
import { COMMANDS } from "./commands.generated.js";
|
|
29
|
+
import { codexSkillAssets } from "./codex-skill-adapter.js";
|
|
29
30
|
import { ensureGitInfoExcluded } from "./git-ignore-utils.js";
|
|
30
31
|
/** The exact exclude entry appended for the command directory (POSIX-relative). */
|
|
31
32
|
const COMMAND_DIR_EXCLUDE_ENTRY = ".claude/commands/";
|
|
33
|
+
const CODEX_SKILLS_EXCLUDE_ENTRY = ".agents/skills/";
|
|
32
34
|
/** Bounded, secret-free error surfaced when the packaged bundle is empty. */
|
|
33
35
|
const EMPTY_BUNDLE_ERROR = "Command provisioning failed: the packaged command bundle is empty — reinstall or rebuild the MCP server package.";
|
|
34
36
|
/**
|
|
@@ -51,7 +53,7 @@ function isEnoentError(err) {
|
|
|
51
53
|
* for an expected filesystem/Git failure, and never leaks raw exception text,
|
|
52
54
|
* command output, or file contents.
|
|
53
55
|
*/
|
|
54
|
-
export async function provisionCommandsForWorktree(worktreeRoot, deps) {
|
|
56
|
+
export async function provisionCommandsForWorktree(worktreeRoot, deps, includeCodexSkills = false) {
|
|
55
57
|
const api = pathApiForCommandProvisioningPlatform(deps.platform);
|
|
56
58
|
const normalizedRoot = api.isAbsolute(worktreeRoot)
|
|
57
59
|
? api.normalize(worktreeRoot)
|
|
@@ -97,6 +99,26 @@ export async function provisionCommandsForWorktree(worktreeRoot, deps) {
|
|
|
97
99
|
"Command provisioning failed: could not write one or more packaged command assets.";
|
|
98
100
|
}
|
|
99
101
|
}
|
|
102
|
+
// Codex launch requires the full bundle. Fill only, preserving customer assets.
|
|
103
|
+
if (!fillError && includeCodexSkills) {
|
|
104
|
+
try {
|
|
105
|
+
for (const asset of codexSkillAssets(COMMANDS)) {
|
|
106
|
+
const target = api.join(normalizedRoot, asset.relativePath);
|
|
107
|
+
try {
|
|
108
|
+
await deps.readFile(target);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (!isEnoentError(err))
|
|
112
|
+
throw err;
|
|
113
|
+
await deps.mkdir(api.dirname(target), { recursive: true });
|
|
114
|
+
await deps.writeFile(target, asset.content);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
fillError = "Command provisioning failed: could not write one or more Codex skill assets.";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
100
122
|
// Phase 3 — ALWAYS attempt exclusion, even after a partial or failed fill, so
|
|
101
123
|
// partially bootstrapped files are never left visible to Git. The primary fill
|
|
102
124
|
// failure is preserved if both the fill and the exclusion fail.
|
|
@@ -109,10 +131,19 @@ export async function provisionCommandsForWorktree(worktreeRoot, deps) {
|
|
|
109
131
|
runCommand: deps.runCommand,
|
|
110
132
|
platform: deps.platform,
|
|
111
133
|
});
|
|
134
|
+
if (includeCodexSkills) {
|
|
135
|
+
await ensureGitInfoExcluded(normalizedRoot, CODEX_SKILLS_EXCLUDE_ENTRY, {
|
|
136
|
+
readFile: deps.readFile,
|
|
137
|
+
writeFile: deps.writeFile,
|
|
138
|
+
mkdir: deps.mkdir,
|
|
139
|
+
runCommand: deps.runCommand,
|
|
140
|
+
platform: deps.platform,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
112
143
|
}
|
|
113
144
|
catch {
|
|
114
145
|
excludeError =
|
|
115
|
-
"Command provisioning failed: could not add
|
|
146
|
+
"Command provisioning failed: could not add generated command assets to the worktree Git exclude file.";
|
|
116
147
|
}
|
|
117
148
|
if (fillError)
|
|
118
149
|
return { ok: false, error: fillError };
|
|
@@ -129,7 +160,7 @@ export async function provisionCommandsForWorktree(worktreeRoot, deps) {
|
|
|
129
160
|
* proceed. A defensive per-row catch guarantees one unexpected failure cannot
|
|
130
161
|
* abort later rows or reject the overall call.
|
|
131
162
|
*/
|
|
132
|
-
export async function provisionCommandsForCreatedWorktrees(rows, deps) {
|
|
163
|
+
export async function provisionCommandsForCreatedWorktrees(rows, deps, includeCodexSkills = false) {
|
|
133
164
|
const out = [];
|
|
134
165
|
for (const row of rows) {
|
|
135
166
|
if (row.status !== "created" || !row.path) {
|
|
@@ -137,7 +168,7 @@ export async function provisionCommandsForCreatedWorktrees(rows, deps) {
|
|
|
137
168
|
continue;
|
|
138
169
|
}
|
|
139
170
|
try {
|
|
140
|
-
const result = await provisionCommandsForWorktree(row.path, deps);
|
|
171
|
+
const result = await provisionCommandsForWorktree(row.path, deps, includeCodexSkills);
|
|
141
172
|
if (result.ok) {
|
|
142
173
|
out.push(row);
|
|
143
174
|
}
|