@aperant/framework 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -1
- package/agents/{apt-pr-review-xplatform.md → apt-pr-review-crossplatform.md} +1 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +30 -1
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/pr-review/detect-specialists.d.mts +5 -5
- package/dist/cli/commands/pr-review/detect-specialists.d.mts.map +1 -1
- package/dist/cli/commands/pr-review/detect-specialists.mjs +49 -45
- package/dist/cli/commands/pr-review/detect-specialists.mjs.map +1 -1
- package/dist/cli/commands/spar-resolve-partner.d.mts +9 -0
- package/dist/cli/commands/spar-resolve-partner.d.mts.map +1 -0
- package/dist/cli/commands/spar-resolve-partner.mjs +82 -0
- package/dist/cli/commands/spar-resolve-partner.mjs.map +1 -0
- package/dist/cli/config/gitignore-drift.d.mts.map +1 -1
- package/dist/cli/config/gitignore-drift.mjs +1 -0
- package/dist/cli/config/gitignore-drift.mjs.map +1 -1
- package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
- package/dist/cli/config/post-merge-sweep.mjs +1 -0
- package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +2 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +1 -0
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/host/codex-config.d.mts +78 -0
- package/dist/cli/host/codex-config.d.mts.map +1 -0
- package/dist/cli/host/codex-config.mjs +169 -0
- package/dist/cli/host/codex-config.mjs.map +1 -0
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +1 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/wfrun/run.d.mts.map +1 -1
- package/dist/cli/wfrun/run.mjs +1 -0
- package/dist/cli/wfrun/run.mjs.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +2 -2
- package/dist/plugin/agents/{apt-pr-review-xplatform.md → apt-pr-review-crossplatform.md} +1 -1
- package/dist/plugin/skills/apt-pr-review/SKILL.md +13 -13
- package/dist/plugin/skills/apt-spar/SKILL.md +14 -6
- package/package.json +1 -1
- package/skills/apt-pr-review/SKILL.md +13 -13
- package/skills/apt-spar/SKILL.md +14 -6
- package/src/cli/commands/init.mjs +29 -0
- package/src/cli/commands/pr-review/detect-specialists.mjs +51 -45
- package/src/cli/commands/spar-resolve-partner.mjs +85 -0
- package/src/cli/config/gitignore-drift.mjs +1 -0
- package/src/cli/config/post-merge-sweep.mjs +1 -0
- package/src/cli/dispatch.mjs +2 -0
- package/src/cli/help.mjs +1 -0
- package/src/cli/host/codex-config.mjs +165 -0
- package/src/cli/install/legacy-paths.mjs +1 -0
- package/src/cli/wfrun/run.mjs +1 -0
- /package/prompts/github/{pr_xplatform_agent.md → pr_crossplatform_agent.md} +0 -0
|
@@ -137,6 +137,8 @@ Do NOT proceed; self-sparring is sycophancy by construction (the host cannot obj
|
|
|
137
137
|
|
|
138
138
|
Walk the ladder in order; pick the first rung with `detected: true` (or `"runtime-host-only"` for MCP). **MCP rung has a hard timeout** — default 900000ms (15 min), configurable via `apt-spar.mcp_timeout_ms` in `.aperant/config.json`. The MCP rung is a structured tool call, NOT a Bash invocation, so the Bash tool's 600000ms ceiling does NOT apply — the MCP rung can carry heavy-research partner runs (e.g. Claude Code partner doing multiple minutes of Read/Grep/Bash) where the Bash rungs would hit the host ceiling. On MCP timeout, **automatically fall through to rung 2 or rung 3** — do NOT fail the whole spar. Treat MCP timeout as "rung unavailable for this invocation."
|
|
139
139
|
|
|
140
|
+
**Codex walk order (anti-staleness — do NOT infer it from prose).** The codex MCP rung is the ONLY codex transport that forces the agent to NAME a model, and a named model is a staleness vector (the upstream MCP server has no access to the user's config and silently substitutes a stale default; the agent could also substitute one from training memory). The self-resolving rungs (plugin shim → raw CLI) pass NO `--model` and let `codex exec` inherit `~/.codex/config.toml`, so they are staleness-proof by construction. Therefore, for the **codex** partner specifically, prefer the self-resolving plugin → CLI rungs in the common case and demote the MCP rung to **LAST**. Do NOT hand-order the rungs from this prose — run `apt-tools spar-resolve-partner --partner codex` and FOLLOW the preference it returns: walk in the order of its `recommended_walk` array (`["plugin","cli","mcp"]`) and treat `prefer_self_resolving: true` as authoritative. The generic `host.partner_transports.codex` array stays MCP-first (it is data shared with other consumers, not a spar walk order); the resolver's preference overrides it for codex only. **claude / gemini stay MCP-first** — their MCP rungs take only `{prompt}` and need no model, so there is no staleness vector and no walk change.
|
|
141
|
+
|
|
140
142
|
If all three rungs are unavailable (every rung `detected: false`, or MCP `runtime-host-only` timed out AND rungs 2+3 are `detected: false`), print:
|
|
141
143
|
|
|
142
144
|
```
|
|
@@ -183,7 +185,7 @@ The table below shows the canonical command shape per (partner × rung); the act
|
|
|
183
185
|
|
|
184
186
|
| Partner | Rung | Command | Notes |
|
|
185
187
|
|---|---|---|---|
|
|
186
|
-
| codex | mcp | tool call: `mcp__codex-mcp__codex({prompt, model})` where `model` is
|
|
188
|
+
| codex | mcp | tool call: `mcp__codex-mcp__codex({prompt, model})` where `model` is the `model` field returned by `apt-tools spar-resolve-partner --partner codex` (see §5a below). | **Demoted to LAST for codex** (see the codex walk-order note in §3) — attempt this rung only when the resolver returns `available: true`. 900000ms (15 min) timeout (configurable `apt-spar.mcp_timeout_ms`); on timeout fall through to plugin then CLI. When the resolver returns `available: false`, treat the MCP rung as UNAVAILABLE and fall through to plugin/CLI — never invent a default model (the MCP server silently substitutes a stale hardcoded default model id, which is exactly the bug we are fixing). |
|
|
187
189
|
| codex | plugin | `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | No `--model` flag — `codex-companion.mjs` invokes `codex exec` which inherits `~/.codex/config.toml` automatically. **NO `--write`. NO `--effort` override.** Per SPAR-04 — spar is conversation, not patching. |
|
|
188
190
|
| codex | cli | `codex exec "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | Precedent: `apt-pr-review/SKILL.md:1084`. No `--model` flag — `codex exec` inherits `~/.codex/config.toml` automatically. Verify with `codex --help`. |
|
|
189
191
|
| claude | mcp | tool call: `mcp__claude-code-mcp__claude_code({prompt})` | Preferred. Same 900000ms (15 min) timeout + fallthrough rule. |
|
|
@@ -199,15 +201,21 @@ Set the Bash tool's `timeout` parameter to `--timeout-ms` (default 300000).
|
|
|
199
201
|
|
|
200
202
|
## 5a. Codex MCP model resolution
|
|
201
203
|
|
|
202
|
-
The codex MCP rung (table row above) requires an explicit `model` arg sourced from the user's `~/.codex/config.toml`. The MCP server (`codex-mcp-server`) does NOT read `~/.codex/config.toml` itself — when no `model` is passed it
|
|
204
|
+
The codex MCP rung (table row above) requires an explicit `model` arg sourced from the user's `~/.codex/config.toml`. The MCP server (`codex-mcp-server`) does NOT read `~/.codex/config.toml` itself — when no `model` is passed it silently substitutes a stale hardcoded default model id, overriding the user's actual configured model. **Aperant holds zero model names** — resolve the model LIVE via the deterministic resolver exactly once per spar invocation, before the rung-table walk:
|
|
203
205
|
|
|
204
206
|
```bash
|
|
205
|
-
# Resolve the codex
|
|
206
|
-
|
|
207
|
-
#
|
|
207
|
+
# Resolve the codex partner model + transport preference LIVE, once per spar
|
|
208
|
+
# invocation, before the rung-table walk. apt-tools reads ~/.codex/config.toml
|
|
209
|
+
# fresh and never bakes a default.
|
|
210
|
+
apt-tools spar-resolve-partner --partner codex
|
|
208
211
|
```
|
|
209
212
|
|
|
210
|
-
|
|
213
|
+
The command emits a JSON envelope `{available, model, source, reason, prefer_self_resolving, recommended_walk}`:
|
|
214
|
+
|
|
215
|
+
- When `available: true`, pass the returned `model` field **VERBATIM** into the codex MCP tool call's `model` arg — do NOT transform it, and do NOT substitute a model from your own training memory.
|
|
216
|
+
- When `available: false` (the envelope carries a machine `reason` of `config_missing` / `no_model_key` / `unparseable`), treat the codex MCP rung as **UNAVAILABLE** and fall through to plugin → CLI. **Never invent a default** — substituting any model name is exactly the staleness bug this resolution path exists to fix. The plugin and raw-CLI rungs pass no `--model` and let `codex exec` self-resolve from the user's live config, so the fall-through is always staleness-proof.
|
|
217
|
+
|
|
218
|
+
Follow the envelope's `recommended_walk` / `prefer_self_resolving` for the rung order (see the codex walk-order note in §3) rather than ordering the rungs by hand.
|
|
211
219
|
|
|
212
220
|
**Prompt-quoting safety.** The prompt is user-controlled (it comes from `[topic]` and/or conversation context) and may legitimately contain double quotes, dollar signs, backticks, or `$(...)` substitution that bash would otherwise expand or break on. NEVER interpolate `<prompt>` directly inside a double-quoted shell argument. Instead, pipe via a here-document so the shell treats the body as opaque text:
|
|
213
221
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aperant/framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "AI coding framework — composable skills for planning, executing, verifying, and reviewing code with any LLM provider. Works as Claude Code commands, Codex tasks, or programmatically.",
|
|
5
5
|
"author": "Mikalsen AI <hello@mikalsen.ai>",
|
|
6
6
|
"type": "module",
|
|
@@ -480,12 +480,12 @@ The detector returns:
|
|
|
480
480
|
```json
|
|
481
481
|
{
|
|
482
482
|
"i18n": { "engage": true, "signals": [{ "kind": "...", "detail": "...", "tier": "strong|weak" }], "reason": "strong: locale-file src/locales/fr.json" },
|
|
483
|
-
"
|
|
483
|
+
"crossplatform": { "engage": false, "signals": [], "reason": "no signals" }
|
|
484
484
|
}
|
|
485
485
|
```
|
|
486
486
|
|
|
487
487
|
Engagement precedence (the detector encodes it; you just read the result):
|
|
488
|
-
config `pr_review.specialists.{i18n,
|
|
488
|
+
config `pr_review.specialists.{i18n,crossplatform}` ∈ `auto|always|never` (default
|
|
489
489
|
`auto`) is checked FIRST and is absolute — `always` engages with no scan,
|
|
490
490
|
`never` skips with no scan (even when the diff introduces the first locale
|
|
491
491
|
file). Otherwise `auto` defers to the STRONG/WEAK tier scan.
|
|
@@ -500,17 +500,17 @@ verbatim, e.g. `config override: never`):
|
|
|
500
500
|
// metadata.json (additive key)
|
|
501
501
|
"conditional_specialists": {
|
|
502
502
|
"i18n": { "engaged": true, "reason": "strong: locale-file src/locales/fr.json" },
|
|
503
|
-
"
|
|
503
|
+
"crossplatform": { "engaged": false, "reason": "no signals" }
|
|
504
504
|
}
|
|
505
505
|
```
|
|
506
506
|
|
|
507
507
|
```
|
|
508
508
|
i18n: ENGAGED (strong: locale-file src/locales/fr.json)
|
|
509
|
-
|
|
509
|
+
crossplatform: SKIPPED (no platform signals; config: auto)
|
|
510
510
|
```
|
|
511
511
|
|
|
512
|
-
Store `result.i18n.engage` and `result.
|
|
513
|
-
`
|
|
512
|
+
Store `result.i18n.engage` and `result.crossplatform.engage` as `ENGAGE_I18N` /
|
|
513
|
+
`ENGAGE_CROSSPLATFORM` for Phase 3.
|
|
514
514
|
|
|
515
515
|
**R3: pin the reviewed head SHA (PR reviews only).** After `gh pr diff` fetches into `pr-review-${REVIEW_ID}`, capture the SHA and pass it to `pr-review start-round`. All subsequent evidence reads within this round should reference this pinned SHA, not a fresh `gh pr view` call — otherwise a mid-round force-push invalidates cited line numbers.
|
|
516
516
|
|
|
@@ -839,7 +839,7 @@ DO NOT return full error output in your response -- write it to the file.
|
|
|
839
839
|
node packages/framework/bin/apt-tools.mjs workflow advance . --id ${REVIEW_ID} --phase specialist-review
|
|
840
840
|
```
|
|
841
841
|
|
|
842
|
-
**CRITICAL: Spawn all SELECTED review agents (6-8) in ONE message using parallel Agent tool calls.** The 6 core specialists ALWAYS spawn. Additionally append the **i18n** agent when `ENGAGE_I18N` is true and the **cross-platform** agent when `
|
|
842
|
+
**CRITICAL: Spawn all SELECTED review agents (6-8) in ONE message using parallel Agent tool calls.** The 6 core specialists ALWAYS spawn. Additionally append the **i18n** agent when `ENGAGE_I18N` is true and the **cross-platform** agent when `ENGAGE_CROSSPLATFORM` is true (from Step 4b). So the set is 6, 7, or 8 agents — all in the SAME message.
|
|
843
843
|
|
|
844
844
|
Each agent receives:
|
|
845
845
|
1. The changed file list
|
|
@@ -909,10 +909,10 @@ Agent(
|
|
|
909
909
|
prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
|
|
910
910
|
)
|
|
911
911
|
|
|
912
|
-
# Append ONLY if
|
|
912
|
+
# Append ONLY if ENGAGE_CROSSPLATFORM is true:
|
|
913
913
|
Agent(
|
|
914
|
-
name: "
|
|
915
|
-
subagent_type: "apt-pr-review-
|
|
914
|
+
name: "crossplatform-reviewer",
|
|
915
|
+
subagent_type: "apt-pr-review-crossplatform",
|
|
916
916
|
description: "Cross-platform review of PR changes",
|
|
917
917
|
prompt: "{CONSTRUCTED_PROMPT_WITH_FILLED_PLACEHOLDERS}",
|
|
918
918
|
)
|
|
@@ -1027,7 +1027,7 @@ Review Directory: {REVIEW_DIR}
|
|
|
1027
1027
|
|
|
1028
1028
|
Read all finding files from {REVIEW_DIR}/findings/ (excluding validated.md if it exists).
|
|
1029
1029
|
The core finding files are: security.md, quality.md, logic.md, codebase-fit.md, test-integrity.md, performance.md.
|
|
1030
|
-
Additionally read i18n.md and/or
|
|
1030
|
+
Additionally read i18n.md and/or crossplatform.md when present (the conditional specialists write them only when engaged).
|
|
1031
1031
|
|
|
1032
1032
|
For each finding in each file, run the hypothesis-validation process from your instructions.
|
|
1033
1033
|
|
|
@@ -1935,7 +1935,7 @@ Fix agents self-report their outcome via a status line (`FIXED: X | FAILED: Y |
|
|
|
1935
1935
|
| `review.max_iterations` | number | 4 | Maximum fix-review iterations before stopping. |
|
|
1936
1936
|
| `review.optional_agents` | string[] | [] | Reserved for future use. Additional specialist agents to spawn beyond the core 6. |
|
|
1937
1937
|
| `pr_review.specialists.i18n` | `"auto"\|"always"\|"never"` | `"auto"` | Override for the conditional i18n specialist. `always` forces engage, `never` forces skip (over the detector AND over a diff-introduced signal), `auto`/absent defers to `detect-specialists`. Override is the TOP of the precedence chain (ID-01a). |
|
|
1938
|
-
| `pr_review.specialists.
|
|
1938
|
+
| `pr_review.specialists.crossplatform` | `"auto"\|"always"\|"never"` | `"auto"` | Override for the conditional cross-platform specialist. Same semantics as `pr_review.specialists.i18n`. |
|
|
1939
1939
|
|
|
1940
1940
|
---
|
|
1941
1941
|
|
|
@@ -1964,7 +1964,7 @@ Agent(
|
|
|
1964
1964
|
| Test-integrity reviewer | `.claude/agents/apt-pr-review-test-integrity.md` | Read, Write, Grep, Glob | 3 |
|
|
1965
1965
|
| Performance reviewer | `.claude/agents/apt-pr-review-performance.md` | Read, Write, Grep, Glob | 3 |
|
|
1966
1966
|
| i18n reviewer **(conditional)** | `.claude/agents/apt-pr-review-i18n.md` | Read, Write, Grep, Glob | 3 — spawned only when `detect-specialists` engages i18n (or config `always`) |
|
|
1967
|
-
| Cross-platform reviewer **(conditional)** | `.claude/agents/apt-pr-review-
|
|
1967
|
+
| Cross-platform reviewer **(conditional)** | `.claude/agents/apt-pr-review-crossplatform.md` | Read, Write, Grep, Glob | 3 — spawned only when `detect-specialists` engages crossplatform (or config `always`) |
|
|
1968
1968
|
| Finding validator | `.claude/agents/apt-pr-review-validator.md` | Read, Write, Grep, Glob | 4 |
|
|
1969
1969
|
| Fix agent (x3) | `.claude/agents/apt-pr-review-fixer.md` | Read, Write, Edit, Bash, Grep, Glob | 6 |
|
|
1970
1970
|
| Self-reviewer | `.claude/agents/apt-pr-review-self-reviewer.md` | Read, Write, Edit, Bash, Grep, Glob | 7 |
|
package/skills/apt-spar/SKILL.md
CHANGED
|
@@ -137,6 +137,8 @@ Do NOT proceed; self-sparring is sycophancy by construction (the host cannot obj
|
|
|
137
137
|
|
|
138
138
|
Walk the ladder in order; pick the first rung with `detected: true` (or `"runtime-host-only"` for MCP). **MCP rung has a hard timeout** — default 900000ms (15 min), configurable via `apt-spar.mcp_timeout_ms` in `.aperant/config.json`. The MCP rung is a structured tool call, NOT a Bash invocation, so the Bash tool's 600000ms ceiling does NOT apply — the MCP rung can carry heavy-research partner runs (e.g. Claude Code partner doing multiple minutes of Read/Grep/Bash) where the Bash rungs would hit the host ceiling. On MCP timeout, **automatically fall through to rung 2 or rung 3** — do NOT fail the whole spar. Treat MCP timeout as "rung unavailable for this invocation."
|
|
139
139
|
|
|
140
|
+
**Codex walk order (anti-staleness — do NOT infer it from prose).** The codex MCP rung is the ONLY codex transport that forces the agent to NAME a model, and a named model is a staleness vector (the upstream MCP server has no access to the user's config and silently substitutes a stale default; the agent could also substitute one from training memory). The self-resolving rungs (plugin shim → raw CLI) pass NO `--model` and let `codex exec` inherit `~/.codex/config.toml`, so they are staleness-proof by construction. Therefore, for the **codex** partner specifically, prefer the self-resolving plugin → CLI rungs in the common case and demote the MCP rung to **LAST**. Do NOT hand-order the rungs from this prose — run `apt-tools spar-resolve-partner --partner codex` and FOLLOW the preference it returns: walk in the order of its `recommended_walk` array (`["plugin","cli","mcp"]`) and treat `prefer_self_resolving: true` as authoritative. The generic `host.partner_transports.codex` array stays MCP-first (it is data shared with other consumers, not a spar walk order); the resolver's preference overrides it for codex only. **claude / gemini stay MCP-first** — their MCP rungs take only `{prompt}` and need no model, so there is no staleness vector and no walk change.
|
|
141
|
+
|
|
140
142
|
If all three rungs are unavailable (every rung `detected: false`, or MCP `runtime-host-only` timed out AND rungs 2+3 are `detected: false`), print:
|
|
141
143
|
|
|
142
144
|
```
|
|
@@ -183,7 +185,7 @@ The table below shows the canonical command shape per (partner × rung); the act
|
|
|
183
185
|
|
|
184
186
|
| Partner | Rung | Command | Notes |
|
|
185
187
|
|---|---|---|---|
|
|
186
|
-
| codex | mcp | tool call: `mcp__codex-mcp__codex({prompt, model})` where `model` is
|
|
188
|
+
| codex | mcp | tool call: `mcp__codex-mcp__codex({prompt, model})` where `model` is the `model` field returned by `apt-tools spar-resolve-partner --partner codex` (see §5a below). | **Demoted to LAST for codex** (see the codex walk-order note in §3) — attempt this rung only when the resolver returns `available: true`. 900000ms (15 min) timeout (configurable `apt-spar.mcp_timeout_ms`); on timeout fall through to plugin then CLI. When the resolver returns `available: false`, treat the MCP rung as UNAVAILABLE and fall through to plugin/CLI — never invent a default model (the MCP server silently substitutes a stale hardcoded default model id, which is exactly the bug we are fixing). |
|
|
187
189
|
| codex | plugin | `node "${CLAUDE_PLUGIN_ROOT}/scripts/codex-companion.mjs" task "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | No `--model` flag — `codex-companion.mjs` invokes `codex exec` which inherits `~/.codex/config.toml` automatically. **NO `--write`. NO `--effort` override.** Per SPAR-04 — spar is conversation, not patching. |
|
|
188
190
|
| codex | cli | `codex exec "$(cat <<'APT_SPAR_PROMPT_EOF' ... APT_SPAR_PROMPT_EOF\n)"` | Precedent: `apt-pr-review/SKILL.md:1084`. No `--model` flag — `codex exec` inherits `~/.codex/config.toml` automatically. Verify with `codex --help`. |
|
|
189
191
|
| claude | mcp | tool call: `mcp__claude-code-mcp__claude_code({prompt})` | Preferred. Same 900000ms (15 min) timeout + fallthrough rule. |
|
|
@@ -199,15 +201,21 @@ Set the Bash tool's `timeout` parameter to `--timeout-ms` (default 300000).
|
|
|
199
201
|
|
|
200
202
|
## 5a. Codex MCP model resolution
|
|
201
203
|
|
|
202
|
-
The codex MCP rung (table row above) requires an explicit `model` arg sourced from the user's `~/.codex/config.toml`. The MCP server (`codex-mcp-server`) does NOT read `~/.codex/config.toml` itself — when no `model` is passed it
|
|
204
|
+
The codex MCP rung (table row above) requires an explicit `model` arg sourced from the user's `~/.codex/config.toml`. The MCP server (`codex-mcp-server`) does NOT read `~/.codex/config.toml` itself — when no `model` is passed it silently substitutes a stale hardcoded default model id, overriding the user's actual configured model. **Aperant holds zero model names** — resolve the model LIVE via the deterministic resolver exactly once per spar invocation, before the rung-table walk:
|
|
203
205
|
|
|
204
206
|
```bash
|
|
205
|
-
# Resolve the codex
|
|
206
|
-
|
|
207
|
-
#
|
|
207
|
+
# Resolve the codex partner model + transport preference LIVE, once per spar
|
|
208
|
+
# invocation, before the rung-table walk. apt-tools reads ~/.codex/config.toml
|
|
209
|
+
# fresh and never bakes a default.
|
|
210
|
+
apt-tools spar-resolve-partner --partner codex
|
|
208
211
|
```
|
|
209
212
|
|
|
210
|
-
|
|
213
|
+
The command emits a JSON envelope `{available, model, source, reason, prefer_self_resolving, recommended_walk}`:
|
|
214
|
+
|
|
215
|
+
- When `available: true`, pass the returned `model` field **VERBATIM** into the codex MCP tool call's `model` arg — do NOT transform it, and do NOT substitute a model from your own training memory.
|
|
216
|
+
- When `available: false` (the envelope carries a machine `reason` of `config_missing` / `no_model_key` / `unparseable`), treat the codex MCP rung as **UNAVAILABLE** and fall through to plugin → CLI. **Never invent a default** — substituting any model name is exactly the staleness bug this resolution path exists to fix. The plugin and raw-CLI rungs pass no `--model` and let `codex exec` self-resolve from the user's live config, so the fall-through is always staleness-proof.
|
|
217
|
+
|
|
218
|
+
Follow the envelope's `recommended_walk` / `prefer_self_resolving` for the rung order (see the codex walk-order note in §3) rather than ordering the rungs by hand.
|
|
211
219
|
|
|
212
220
|
**Prompt-quoting safety.** The prompt is user-controlled (it comes from `[topic]` and/or conversation context) and may legitimately contain double quotes, dollar signs, backticks, or `$(...)` substitution that bash would otherwise expand or break on. NEVER interpolate `<prompt>` directly inside a double-quoted shell argument. Instead, pipe via a here-document so the shell treats the body as opaque text:
|
|
213
221
|
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
mkdirSync,
|
|
24
24
|
readdirSync,
|
|
25
25
|
readFileSync,
|
|
26
|
+
unlinkSync,
|
|
26
27
|
writeFileSync,
|
|
27
28
|
} from 'node:fs'
|
|
28
29
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
@@ -1276,6 +1277,34 @@ export async function cmdInit(projectDir, extraArgs = []) {
|
|
|
1276
1277
|
}
|
|
1277
1278
|
}
|
|
1278
1279
|
|
|
1280
|
+
// Orphan-removal sweep — the cross-platform PR-review agent was renamed
|
|
1281
|
+
// from `apt-pr-review-xplatform` to `apt-pr-review-crossplatform`. The
|
|
1282
|
+
// install pipeline writes the current agent set + a fresh manifest but
|
|
1283
|
+
// has NO stale-removal pass (readManifest is consumed only by
|
|
1284
|
+
// skill-installs.mjs + check-version.mjs, never pipeline.mjs), so a
|
|
1285
|
+
// rename leaves the OLD-named file orphaned in every active runtime
|
|
1286
|
+
// root. Targeted, best-effort unlink of that ONE basename from each
|
|
1287
|
+
// requested runtime's install root (the agent lands under different
|
|
1288
|
+
// per-runtime subdirs: claude→agents, pi→skills, windsurf→workflows).
|
|
1289
|
+
// Mirrors runLegacyCleanup's conservative idempotent posture; ENOENT
|
|
1290
|
+
// and any other unlink error is swallowed.
|
|
1291
|
+
const ORPHANED_AGENT_BASENAME = 'apt-pr-review-xplatform.md'
|
|
1292
|
+
const ORPHAN_AGENT_SUBDIRS = ['agents', 'skills', 'workflows', 'rules']
|
|
1293
|
+
for (const runtimeId of effectiveAnswers.runtimes) {
|
|
1294
|
+
const runtime = getRuntime(runtimeId)
|
|
1295
|
+
if (!runtime?.installRoot) continue
|
|
1296
|
+
for (const subdir of ORPHAN_AGENT_SUBDIRS) {
|
|
1297
|
+
const orphanPath = join(installTarget, runtime.installRoot, subdir, ORPHANED_AGENT_BASENAME)
|
|
1298
|
+
if (existsSync(orphanPath)) {
|
|
1299
|
+
try {
|
|
1300
|
+
unlinkSync(orphanPath)
|
|
1301
|
+
} catch {
|
|
1302
|
+
// best-effort — a missing/locked orphan must not fail init
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1279
1308
|
const results = await installRuntimes(
|
|
1280
1309
|
effectiveAnswers.runtimes,
|
|
1281
1310
|
canonicalRoot,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* commands/pr-review/detect-specialists.mjs — deterministic detector for the
|
|
3
3
|
* two CONDITIONAL /apt:pr-review specialists (i18n + cross-platform).
|
|
4
4
|
*
|
|
5
|
-
* Restores the 2.8 `i18n-reviewer` + `
|
|
5
|
+
* Restores the 2.8 `i18n-reviewer` + `crossplatform` perspectives but gates their
|
|
6
6
|
* Phase-3 spawn on a reproducible, language/platform/ecosystem-agnostic scan
|
|
7
7
|
* with a config override on top. The SKILL orchestrator NEVER decides
|
|
8
8
|
* engagement by reading the diff itself — it calls this detector and obeys the
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Engagement model (spec ID-01/ID-01a/ID-02/ID-02a/ID-03/ID-03a/ID-03b):
|
|
12
12
|
*
|
|
13
13
|
* 1. Config check FIRST and short-circuit (ID-01a). `.aperant/config.json`
|
|
14
|
-
* pr_review.specialists.{i18n,
|
|
14
|
+
* pr_review.specialists.{i18n,crossplatform} ∈ {auto|always|never}, default
|
|
15
15
|
* auto. `always` → engage (no scan); `never` → skip (no scan); only
|
|
16
16
|
* `auto`/absent proceeds. Config is the TOP of the precedence chain — a
|
|
17
17
|
* diff-introduced STRONG signal does NOT override `never`.
|
|
@@ -44,7 +44,7 @@ import { err, ok } from '../../util/result.mjs'
|
|
|
44
44
|
* @typedef {{ engage: boolean, signals: Signal[], reason: string }} SpecialistDecision
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
|
-
// Manifests scanned for dependency-substring i18n/
|
|
47
|
+
// Manifests scanned for dependency-substring i18n/crossplatform library markers.
|
|
48
48
|
// No YAML/TOML parse — substring/regex on the raw text, mirroring
|
|
49
49
|
// toolchain-detect.mjs:isBerryNodeLinker (R2).
|
|
50
50
|
const MANIFEST_FILES = [
|
|
@@ -334,10 +334,10 @@ function scanSourceForI18nTokens(detailLabel, text) {
|
|
|
334
334
|
return found
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
// ──
|
|
337
|
+
// ── crossplatform matchers ──────────────────────────────────────────────────────
|
|
338
338
|
|
|
339
339
|
// Cross-platform app frameworks (STRONG) matched as manifest substrings.
|
|
340
|
-
const
|
|
340
|
+
const CROSSPLATFORM_FRAMEWORK_SUBSTRINGS = [
|
|
341
341
|
'electron',
|
|
342
342
|
'@tauri-apps',
|
|
343
343
|
'react-native',
|
|
@@ -358,13 +358,13 @@ const XPL_FRAMEWORK_SUBSTRINGS = [
|
|
|
358
358
|
* @param {string} text
|
|
359
359
|
* @returns {Array<{kind:string, detail:string, tier:string}>}
|
|
360
360
|
*/
|
|
361
|
-
function
|
|
361
|
+
function scanManifestForCrossplatform(manifestName, text) {
|
|
362
362
|
const found = []
|
|
363
363
|
const lower = text.toLowerCase()
|
|
364
|
-
for (const fw of
|
|
364
|
+
for (const fw of CROSSPLATFORM_FRAMEWORK_SUBSTRINGS) {
|
|
365
365
|
if (lower.includes(fw)) {
|
|
366
366
|
found.push({
|
|
367
|
-
kind: '
|
|
367
|
+
kind: 'crossplatform-framework',
|
|
368
368
|
detail: `${fw} in ${manifestName}`,
|
|
369
369
|
tier: 'strong',
|
|
370
370
|
})
|
|
@@ -378,7 +378,7 @@ function scanManifestForXplatform(manifestName, text) {
|
|
|
378
378
|
|
|
379
379
|
// Compile-time platform guards (STRONG) — source-token regexes spanning
|
|
380
380
|
// Rust / Go / C/C++ / CMake / Swift.
|
|
381
|
-
const
|
|
381
|
+
const CROSSPLATFORM_COMPILE_GUARD_RES = [
|
|
382
382
|
{ re: /#\[cfg\(target_os/, label: '#[cfg(target_os' },
|
|
383
383
|
{ re: /\bcfg!\s*\(\s*windows\b/, label: 'cfg!(windows)' },
|
|
384
384
|
{ re: /\/\/go:build\s+(windows|darwin|linux)/, label: '//go:build <os>' },
|
|
@@ -393,7 +393,7 @@ const XPL_COMPILE_GUARD_RES = [
|
|
|
393
393
|
]
|
|
394
394
|
|
|
395
395
|
// Lone runtime platform checks (WEAK).
|
|
396
|
-
const
|
|
396
|
+
const CROSSPLATFORM_WEAK_TOKEN_RES = [
|
|
397
397
|
{ re: /\bprocess\.platform\b/, label: 'process.platform' },
|
|
398
398
|
{ re: /\bos\.platform\s*\(/, label: 'os.platform()' },
|
|
399
399
|
{ re: /\bsys\.platform\b/, label: 'sys.platform' },
|
|
@@ -405,28 +405,28 @@ const XPL_WEAK_TOKEN_RES = [
|
|
|
405
405
|
]
|
|
406
406
|
|
|
407
407
|
/**
|
|
408
|
-
* Scan one source blob for
|
|
408
|
+
* Scan one source blob for crossplatform compile-guard (STRONG) + lone runtime
|
|
409
409
|
* (WEAK) tokens.
|
|
410
410
|
*
|
|
411
411
|
* @param {string} detailLabel
|
|
412
412
|
* @param {string} text
|
|
413
413
|
* @returns {Array<{kind:string, detail:string, tier:string}>}
|
|
414
414
|
*/
|
|
415
|
-
function
|
|
415
|
+
function scanSourceForCrossplatformTokens(detailLabel, text) {
|
|
416
416
|
const found = []
|
|
417
|
-
for (const { re, label } of
|
|
417
|
+
for (const { re, label } of CROSSPLATFORM_COMPILE_GUARD_RES) {
|
|
418
418
|
if (re.test(text)) {
|
|
419
419
|
found.push({
|
|
420
|
-
kind: '
|
|
420
|
+
kind: 'crossplatform-guard',
|
|
421
421
|
detail: `${label} in ${detailLabel}`,
|
|
422
422
|
tier: 'strong',
|
|
423
423
|
})
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
|
-
for (const { re, label } of
|
|
426
|
+
for (const { re, label } of CROSSPLATFORM_WEAK_TOKEN_RES) {
|
|
427
427
|
if (re.test(text)) {
|
|
428
428
|
found.push({
|
|
429
|
-
kind: '
|
|
429
|
+
kind: 'crossplatform-runtime',
|
|
430
430
|
detail: `${label} in ${detailLabel}`,
|
|
431
431
|
tier: 'weak',
|
|
432
432
|
})
|
|
@@ -496,7 +496,7 @@ function detectCiMatrix(workflowName, text) {
|
|
|
496
496
|
}
|
|
497
497
|
if (osFamilies.size > 1) {
|
|
498
498
|
return {
|
|
499
|
-
kind: '
|
|
499
|
+
kind: 'crossplatform-ci-matrix',
|
|
500
500
|
detail: `multi-OS CI matrix in ${workflowName} (${osFamilies.size} OS families)`,
|
|
501
501
|
tier: 'strong',
|
|
502
502
|
}
|
|
@@ -519,7 +519,7 @@ function detectPlatformDirSiblings(relPaths) {
|
|
|
519
519
|
const hasLinuxDir = relPaths.some((p) => p.split('/').includes('linux'))
|
|
520
520
|
if (hasWindowsDir && hasMacosDir && hasLinuxDir) {
|
|
521
521
|
found.push({
|
|
522
|
-
kind: '
|
|
522
|
+
kind: 'crossplatform-dir-siblings',
|
|
523
523
|
detail: 'platform dir siblings windows/ + macos/ + linux/ co-present',
|
|
524
524
|
tier: 'strong',
|
|
525
525
|
})
|
|
@@ -531,7 +531,7 @@ function detectPlatformDirSiblings(relPaths) {
|
|
|
531
531
|
})
|
|
532
532
|
if (hasXcodeproj && hasGradle) {
|
|
533
533
|
found.push({
|
|
534
|
-
kind: '
|
|
534
|
+
kind: 'crossplatform-dir-siblings',
|
|
535
535
|
detail: '*.xcodeproj + build.gradle co-presence',
|
|
536
536
|
tier: 'strong',
|
|
537
537
|
})
|
|
@@ -545,12 +545,14 @@ function detectPlatformDirSiblings(relPaths) {
|
|
|
545
545
|
* @param {string[]} relPaths
|
|
546
546
|
* @returns {Array<{kind:string, detail:string, tier:string}>}
|
|
547
547
|
*/
|
|
548
|
-
function
|
|
548
|
+
function detectWeakCrossplatformFiles(relPaths) {
|
|
549
549
|
const found = []
|
|
550
550
|
for (const p of relPaths) {
|
|
551
551
|
const ext = extname(p.toLowerCase())
|
|
552
|
-
if (ext === '.plist')
|
|
553
|
-
|
|
552
|
+
if (ext === '.plist')
|
|
553
|
+
found.push({ kind: 'crossplatform-file', detail: `plist ${p}`, tier: 'weak' })
|
|
554
|
+
else if (ext === '.rc')
|
|
555
|
+
found.push({ kind: 'crossplatform-file', detail: `rc ${p}`, tier: 'weak' })
|
|
554
556
|
}
|
|
555
557
|
return found
|
|
556
558
|
}
|
|
@@ -731,7 +733,7 @@ function applyScope(baselineSignals, diffSignals, diffTouchedPaths, scope) {
|
|
|
731
733
|
* Read `.aperant/config.json:pr_review.specialists` (best-effort).
|
|
732
734
|
*
|
|
733
735
|
* @param {string} root
|
|
734
|
-
* @returns {{ i18n?: string,
|
|
736
|
+
* @returns {{ i18n?: string, crossplatform?: string }}
|
|
735
737
|
*/
|
|
736
738
|
function readSpecialistConfig(root) {
|
|
737
739
|
try {
|
|
@@ -759,15 +761,15 @@ function normalizeMode(v) {
|
|
|
759
761
|
/**
|
|
760
762
|
* The core detector. Pure over its inputs (reads the filesystem at `root` and
|
|
761
763
|
* the supplied `diffText`); returns the full
|
|
762
|
-
* `{ i18n,
|
|
764
|
+
* `{ i18n, crossplatform }` decision object.
|
|
763
765
|
*
|
|
764
766
|
* @param {object} opts
|
|
765
767
|
* @param {string} opts.root absolute project dir
|
|
766
768
|
* @param {string} [opts.diffText] unified-diff text (optional)
|
|
767
769
|
* @param {'repo'|'diff'} [opts.scope] default 'repo'
|
|
768
|
-
* @param {{i18n?:string,
|
|
770
|
+
* @param {{i18n?:string, crossplatform?:string}} [opts.config] override config
|
|
769
771
|
* (read from .aperant/config.json when omitted)
|
|
770
|
-
* @returns {{ i18n: SpecialistDecision,
|
|
772
|
+
* @returns {{ i18n: SpecialistDecision, crossplatform: SpecialistDecision }}
|
|
771
773
|
*/
|
|
772
774
|
export function detectSpecialists(opts) {
|
|
773
775
|
const root = resolve(opts.root)
|
|
@@ -776,7 +778,7 @@ export function detectSpecialists(opts) {
|
|
|
776
778
|
const diffText = opts.diffText ?? ''
|
|
777
779
|
|
|
778
780
|
const i18nMode = normalizeMode(config.i18n)
|
|
779
|
-
const
|
|
781
|
+
const crossplatformMode = normalizeMode(config.crossplatform)
|
|
780
782
|
|
|
781
783
|
// Parse diff once (shared by both specialists).
|
|
782
784
|
const diff = diffText ? parseUnifiedDiff(diffText) : { touchedPaths: [], addedText: '' }
|
|
@@ -791,17 +793,17 @@ export function detectSpecialists(opts) {
|
|
|
791
793
|
collectDiff: collectI18nDiff,
|
|
792
794
|
})
|
|
793
795
|
|
|
794
|
-
// ──
|
|
795
|
-
const
|
|
796
|
-
mode:
|
|
796
|
+
// ── crossplatform ───────────────────────────────────────────────────────────
|
|
797
|
+
const crossplatform = decideSpecialist({
|
|
798
|
+
mode: crossplatformMode,
|
|
797
799
|
root,
|
|
798
800
|
diff,
|
|
799
801
|
scope,
|
|
800
|
-
collectBaseline:
|
|
801
|
-
collectDiff:
|
|
802
|
+
collectBaseline: collectCrossplatformBaseline,
|
|
803
|
+
collectDiff: collectCrossplatformDiff,
|
|
802
804
|
})
|
|
803
805
|
|
|
804
|
-
return { i18n,
|
|
806
|
+
return { i18n, crossplatform }
|
|
805
807
|
}
|
|
806
808
|
|
|
807
809
|
/**
|
|
@@ -880,7 +882,7 @@ function collectI18nDiff(diff) {
|
|
|
880
882
|
return dedupeSignals(signals)
|
|
881
883
|
}
|
|
882
884
|
|
|
883
|
-
function
|
|
885
|
+
function collectCrossplatformBaseline(root) {
|
|
884
886
|
const signals = []
|
|
885
887
|
const files = walkFiles(root)
|
|
886
888
|
|
|
@@ -889,12 +891,12 @@ function collectXplatformBaseline(root) {
|
|
|
889
891
|
const abs = join(root, manifest)
|
|
890
892
|
if (!existsSync(abs)) continue
|
|
891
893
|
const text = readSafe(abs)
|
|
892
|
-
if (text) signals.push(...
|
|
894
|
+
if (text) signals.push(...scanManifestForCrossplatform(manifest, text))
|
|
893
895
|
}
|
|
894
896
|
// pubspec.yaml present → Flutter (its presence alone is the flutter marker).
|
|
895
897
|
if (existsSync(join(root, 'pubspec.yaml'))) {
|
|
896
898
|
signals.push({
|
|
897
|
-
kind: '
|
|
899
|
+
kind: 'crossplatform-framework',
|
|
898
900
|
detail: 'flutter (pubspec.yaml present)',
|
|
899
901
|
tier: 'strong',
|
|
900
902
|
})
|
|
@@ -930,9 +932,13 @@ function collectXplatformBaseline(root) {
|
|
|
930
932
|
) {
|
|
931
933
|
const text = readSafe(join(root, rel))
|
|
932
934
|
if (text) {
|
|
933
|
-
for (const { re, label } of
|
|
935
|
+
for (const { re, label } of CROSSPLATFORM_COMPILE_GUARD_RES) {
|
|
934
936
|
if (re.test(text)) {
|
|
935
|
-
signals.push({
|
|
937
|
+
signals.push({
|
|
938
|
+
kind: 'crossplatform-guard',
|
|
939
|
+
detail: `${label} in ${rel}`,
|
|
940
|
+
tier: 'strong',
|
|
941
|
+
})
|
|
936
942
|
}
|
|
937
943
|
}
|
|
938
944
|
}
|
|
@@ -941,25 +947,25 @@ function collectXplatformBaseline(root) {
|
|
|
941
947
|
|
|
942
948
|
// Platform dir siblings + weak files.
|
|
943
949
|
signals.push(...detectPlatformDirSiblings(files))
|
|
944
|
-
signals.push(...
|
|
950
|
+
signals.push(...detectWeakCrossplatformFiles(files))
|
|
945
951
|
|
|
946
952
|
return dedupeSignals(signals)
|
|
947
953
|
}
|
|
948
954
|
|
|
949
|
-
function
|
|
955
|
+
function collectCrossplatformDiff(diff) {
|
|
950
956
|
const signals = []
|
|
951
957
|
if (diff.addedText) {
|
|
952
958
|
const lowerAdded = diff.addedText.toLowerCase()
|
|
953
|
-
for (const fw of
|
|
959
|
+
for (const fw of CROSSPLATFORM_FRAMEWORK_SUBSTRINGS) {
|
|
954
960
|
if (lowerAdded.includes(fw)) {
|
|
955
961
|
signals.push({
|
|
956
|
-
kind: '
|
|
962
|
+
kind: 'crossplatform-framework',
|
|
957
963
|
detail: `${fw} (diff added line)`,
|
|
958
964
|
tier: 'strong',
|
|
959
965
|
})
|
|
960
966
|
}
|
|
961
967
|
}
|
|
962
|
-
signals.push(...
|
|
968
|
+
signals.push(...scanSourceForCrossplatformTokens('diff', diff.addedText))
|
|
963
969
|
}
|
|
964
970
|
// CI matrix introduced in the diff.
|
|
965
971
|
for (const rel of diff.touchedPaths) {
|
|
@@ -968,7 +974,7 @@ function collectXplatformDiff(diff) {
|
|
|
968
974
|
if (m) signals.push(m)
|
|
969
975
|
}
|
|
970
976
|
}
|
|
971
|
-
signals.push(...
|
|
977
|
+
signals.push(...detectWeakCrossplatformFiles(diff.touchedPaths))
|
|
972
978
|
return dedupeSignals(signals)
|
|
973
979
|
}
|
|
974
980
|
|
|
@@ -1018,6 +1024,6 @@ export function cmdDetectSpecialists(projectDir, extraArgs = []) {
|
|
|
1018
1024
|
command: 'pr-review-detect-specialists',
|
|
1019
1025
|
scope,
|
|
1020
1026
|
i18n: result.i18n,
|
|
1021
|
-
|
|
1027
|
+
crossplatform: result.crossplatform,
|
|
1022
1028
|
})
|
|
1023
1029
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands/spar-resolve-partner.mjs — live partner-model resolver for `apt:spar`.
|
|
3
|
+
*
|
|
4
|
+
* Layer 1 cmd (returns { envelope, exitCode } via ok()/err() — never calls
|
|
5
|
+
* process.exit / output() / fail() directly; the Layer 2 wrapper in dispatch.mjs
|
|
6
|
+
* writes stdout and exits).
|
|
7
|
+
*
|
|
8
|
+
* `apt-tools spar-resolve-partner --partner <codex|claude|gemini>` returns the
|
|
9
|
+
* model the codex MCP rung must pass, read LIVE from `~/.codex/config.toml` with
|
|
10
|
+
* no baked default (Decision B + C). For claude / gemini the MCP rung needs no
|
|
11
|
+
* model arg, so the command returns `reason: 'self_resolving'` + `model: null`.
|
|
12
|
+
*
|
|
13
|
+
* Each envelope carries an EXPLICIT transport preference (plan-gate spar
|
|
14
|
+
* refinement #2): codex returns `prefer_self_resolving: true` +
|
|
15
|
+
* `recommended_walk: ['plugin','cli','mcp']` so the SKILL agent walks the
|
|
16
|
+
* self-resolving rungs first and only tries the model-naming MCP rung last. The
|
|
17
|
+
* `detect.mjs` `partner_transports` array stays MCP-first (ID-02); the agent
|
|
18
|
+
* follows THIS returned preference rather than inferring rung order from prose.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
22
|
+
import { homedir } from 'node:os'
|
|
23
|
+
import { resolveCodexSparModel } from '../host/codex-config.mjs'
|
|
24
|
+
import { err, ok } from '../util/result.mjs'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse `--partner <id>` from the positional args. Defaults to `codex`
|
|
28
|
+
* (the only partner whose MCP rung requires a model arg).
|
|
29
|
+
*
|
|
30
|
+
* @param {string[]} args
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function parsePartner(args) {
|
|
34
|
+
if (!Array.isArray(args)) return 'codex'
|
|
35
|
+
const idx = args.indexOf('--partner')
|
|
36
|
+
if (idx === -1 || idx === args.length - 1) return 'codex'
|
|
37
|
+
return String(args[idx + 1]).toLowerCase()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string[]} [args]
|
|
42
|
+
* @returns {{ envelope: object, exitCode: number }}
|
|
43
|
+
*/
|
|
44
|
+
export function cmdSparResolvePartner(args = []) {
|
|
45
|
+
const partner = parsePartner(args)
|
|
46
|
+
|
|
47
|
+
if (partner === 'codex') {
|
|
48
|
+
const { available, model, source, reason } = resolveCodexSparModel({
|
|
49
|
+
home: homedir(),
|
|
50
|
+
existsSync,
|
|
51
|
+
readFileSync,
|
|
52
|
+
})
|
|
53
|
+
return ok({
|
|
54
|
+
status: 'ok',
|
|
55
|
+
command: 'spar-resolve-partner',
|
|
56
|
+
partner: 'codex',
|
|
57
|
+
available,
|
|
58
|
+
model,
|
|
59
|
+
source,
|
|
60
|
+
reason,
|
|
61
|
+
// Refinement #2 — explicit transport preference the SKILL FOLLOWS.
|
|
62
|
+
prefer_self_resolving: true,
|
|
63
|
+
recommended_walk: ['plugin', 'cli', 'mcp'],
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (partner === 'claude' || partner === 'gemini') {
|
|
68
|
+
return ok({
|
|
69
|
+
status: 'ok',
|
|
70
|
+
command: 'spar-resolve-partner',
|
|
71
|
+
partner,
|
|
72
|
+
available: true,
|
|
73
|
+
model: null,
|
|
74
|
+
source: null,
|
|
75
|
+
reason: 'self_resolving',
|
|
76
|
+
prefer_self_resolving: false,
|
|
77
|
+
recommended_walk: ['mcp', 'plugin', 'cli'],
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return err(`Unknown partner: ${partner}. Expected codex, claude, or gemini.`, 1, {
|
|
82
|
+
command: 'spar-resolve-partner',
|
|
83
|
+
partner,
|
|
84
|
+
})
|
|
85
|
+
}
|