# Cross-CLI Contract (Claude Code · Copilot CLI · Codex CLI)

> **Non-negotiable**. Any change that breaks this contract blocks merge. Validated by `smoke-cross-cli-behavior.sh`.

**Purpose**: every pipeline command must produce identical artifacts (state, logs, outputs) and respect identical placeholder vocabulary regardless of which of the three host CLIs invokes it. This file is the source of truth for "what must stay the same."

---

## 1. Command Inventory (53 files, 49 live commands)

```
analysis, analysis-resolve, autopilot, build-optimize, channels,
complaint-analysis, create-jira, design-check, dev, dev-autopilot, dev-local,
dev-local-autopilot, diff-explain, feedback, forget, garbage-collect, help,
ios-coding-standard, issue, jira, kill, language, local, local-autopilot,
log, manual-test, prune-logs, prune-prompts, purge, refactor, resume,
resume-local, review, review-analysis, review-issue, review-jira, routines,
save, scan, search, setup, stack, status, store-ready, sync, test,
test-accessibility, test-dark-mode, test-dynamic-type, test-screenshots,
testflight-validation, uninstall, update
```

Categories:

- **Interactive pickers** (single-purpose, not modes): `jira`, `issue`
- **Issue generator** (one-shot, no worktree, asks type Task/Bug/Story, hard approval gate before create): `create-jira`
- **Pipeline entries**: `autopilot`, `local`, `local-autopilot` (plus the bare `/multi-agent` in the dispatcher). Depth is not a command: `/multi-agent` and `local` ask Full or Short at Phase 0 Step 7.5; the two autopilot entries never ask and always run Full.
- **Retired stubs** (v16.0.0, deleted next minor  -  they print a redirect and run no phase): `dev`, `dev-local` redirect to the picker entries with Short; `dev-autopilot`, `dev-local-autopilot` have no equivalent, because fast-plus-unattended no longer exists
- **Tail modes** (run the pipeline tail over already-done local work): `resume-local`
- **Ops commands** (one-shot, no worktree): `status`, `log`, `kill`, `purge`, `uninstall`, `resume`, `review`, `review-jira`, `review-issue`, `analysis`, `analysis-resolve`, `complaint-analysis`, `build-optimize`, `channels`, `scan`, `search`, `diff-explain`, `garbage-collect`, `prune-logs`, `prune-prompts`
- **Local audits** (worktree only to build; no commit, push, PR or channels): `design-check`, `testflight-validation`, `ios-coding-standard`. `testflight-validation` additionally never invokes `altool --upload-app`  -  a validation run must not be able to ship a build by accident.
- **Meta-ops**: `setup`, `sync`, `update`, `help`, `refactor`, `test`, `stack`, `manual-test`, `language`
- **Routines** (user-defined routine registry; the routines they create are local-only and never synced): `save`, `routines`, `forget`

The count is 51 files and 47 live commands until the four stubs are deleted, at which point both numbers become 47. A stub is still installed and still invocable, so counting it as absent would be wrong; counting it as a command would be worse.

> **Inventory drift is a contract violation.** Adding a slash command under `pipeline/commands/multi-agent/` without updating this list + its counterpart Copilot dir (`pipeline/skills/shared/core/multi-agent-<cmd>/`) is a merge blocker. `smoke-commands-skills-parity.sh` enforces command ↔ skill directory parity; `smoke-cross-cli-behavior.sh` enforces behavior parity. This doc is the authoritative command list  -  bump the count + table together.

### 1.1 Figma / component work (plugin-based on Claude Code; NOT parity-enforced)

Component work is **no longer a bundled pipeline skill set**. Claude Code dispatches `taskType === "component"` to the enabled `ai-<platform>-toolkit` **marketplace plugin** (`create-component`, fallback `create-ui-component`) via the Skill tool  -  full contract in `$HOME/.claude/multi-agent-refs/component-dispatch.md`. The pipeline no longer ships `pipeline/skills/figma-ios|figma-android|figma-common`; the pipeline-unique component skills (iterate loops, performance harness, validate/review, commit, adapters, wiki) were absorbed into the `ai-ios-toolkit` plugin so component skills live in one place.

**How each host receives the plugin's skills.** Only Claude Code loads the marketplace
plugin natively. The other two are served by the installer, so all three end up with the
same active skill set:

| Host | Delivery | Discovery |
|---|---|---|
| Claude Code | the marketplace plugin, loaded natively | matched on description |
| Copilot CLI | `install/copilot.mjs` copies the **enabled** stack plugin's authored skills (`index`, `reference/`, `workflow/`, `tools/`) into `~/.copilot/skills/` | matched on description |
| Codex CLI | `install/codex.mjs` copies the same set **plus** `skills/shared/external` into `~/.codex/multi-agent-refs/skills/` as reference files | located by path; the AGENTS.md block carries the `ls` / `grep` recipe |

Three rules make that work:

1. **`knowledge/` is never copied to Copilot.** A stack plugin's `knowledge/` tree is
   generated from this pipeline's `skills/shared/external`, which the installer already
   lays down there. Copying it again would duplicate ~5 MB the host has.
2. **Only the ENABLED plugins are delivered.** `fix-bug` and `branch-and-pr` exist in
   four stack plugins, `component`, `state` and `create-component` in three, and the
   destination is flat  -  delivering every stack would be last-write-wins, so an iOS
   repo could end up running the Android `create-component`. The enabled list in
   `~/.claude/settings.json` is the source of truth, exactly as on Claude Code;
   `--platform` is the fallback when no such list exists.
3. **Codex takes refs, not list entries.** Measured on 0.145: one plugin declaring 142
   skills surfaced 75 and evicted an unrelated user skill, and every plugin-provided
   skill renders with an empty description, so a block entry buys nothing even when it
   fits. Parity on that host is **reachability**, not an identical listing.

> This section previously claimed Copilot "retains its own standalone
> `~/.copilot/skills/figma-*` copies as a frozen fallback". `install/copilot.mjs` pruned
> exactly those directories, so the fallback did not exist: `create-screen`,
> `figma-validate`, `figma-review`, `component`, `state` and `navigation` were all
> absent, and a component task on Copilot had nothing to dispatch to. The contract
> documented a safety net the installer deleted.

What still MUST match across CLIs for component tasks: the `taskType === "component"` classification, the `state.phases["3"].subphases[]` shape the dispatch layer writes, and the Short-run elision semantics. Resolution/routing is Claude-plugin vs Copilot-local-copy by design.

### 1.2 Store-compliance skills

Two parallel shared/core skills under `pipeline/skills/shared/core/` wrap external tooling for pre-submission store compliance:

| Skill | Path | Tool wrapper | Rules |
|---|---|---|---|
| `apple-archive-compliance` | `pipeline/skills/shared/core/apple-archive-compliance/` | `ios_app_store_audit` MCP tool (in `@mmerterden/multi-agent-toolkit-mcp` ≥ v3.0.0) | 18 (Apple ITMS + App Store Review Guidelines) |
| `google-play-compliance` | `pipeline/skills/shared/core/google-play-compliance/` | bundletool + aapt2 + apksigner | 21 (4 categories: Technical / Security / Privacy / Hygiene) |

Both skills are wired to 4 consumers: `/multi-agent:test "store-ready"` (primary), Phase 4 Security Auditor (`pipeline/agents/security-auditor.md`), `/multi-agent:review` + SKILL.md counterpart, `/multi-agent:channels` PR-body auto-augmentation. Contract enforced by `smoke-compliance-skills.sh`.

### 1.3 Figma-skill routing from multi-agent (superseded)

Superseded by section 1.1: Claude Code resolves component dispatch to the marketplace plugin (dual-name `create-component`/`create-ui-component`; full contract in `$HOME/.claude/multi-agent-refs/component-dispatch.md`); Copilot CLI receives the enabled plugin's authored skills through `install/copilot.mjs` (section 1.1), not a standalone `figma-*` tree  -  the installer prunes those. No shared filesystem-path routing table or figma-skill frontmatter/inventory parity remains under enforcement.

---

## 2. Canonical Placeholder Vocabulary

**Problem we prevent**: same concept referenced with different placeholder names in Claude vs Copilot files (e.g., `{github-username}` vs `{owner}`). Every new command must pull from this table.

### 2.1 Identity / account placeholders

| Placeholder | Meaning | Example real value |
|---|---|---|
| `{owner}` | Public GitHub org/user owning the pipeline repo | `mmerterden` |
| `{npm-scope}` | npm scope for publishing | `@mmerterden` |
| `${USER}` | Local OS username, used for per-user keychain keys | `alice` |
| `{author-name}` | Git author name (comes from preferences.identities[].name) | `Full Name` |
| `{author-email}` | Git author email | `foo@example.com` |

### 2.2 Host / URL placeholders

| Placeholder | Meaning | Example |
|---|---|---|
| `{jira-host}` | Jira server hostname | `jira.company.com` |
| `{bitbucket-host}` | Bitbucket server hostname | `bitbucket.company.com` |
| `{confluence-host}` | Confluence server hostname | `confluence.company.com` |
| `{corp-domain}` | Corporate email domain | `company.com` |
| `{website-host}` | Public website hostname | `company.dev` |

### 2.3 Project name placeholders

Use these specific names (from the Copilot set)  -  **never invent new ones**. They correspond to realistic example project types.

| Placeholder | Represents |
|---|---|
| `my-ios-app` | An iOS native project |
| `my-figma-app` | A figma-driven SwiftUI component project |
| `my-ui-components` | A UI component library (cross-platform possible) |

### 2.4 Task / tracker placeholders

| Placeholder | Meaning |
|---|---|
| `{JIRA_KEY}` | Jira project key (e.g., `PROJ`, `MOBILE`) |
| `PROJ-12345` | Example Jira issue (placeholder, always use `PROJ-12345` for examples) |
| `#N` | GitHub issue number placeholder |

### 2.5 Deprecated placeholders (DO NOT USE)

Legacy names found during the v3.7 audit  -  replaced per this contract.

| Deprecated | Replace with | Reason |
|---|---|---|
| `{github-username}` | `{owner}` | Shorter, consistent with `{owner}` pattern |
| `{website-repo}` | `website` (literal, since the repo name IS "website" under `{owner}`) | Hardcoding the repo name inside `{owner}` scope is fine |
| `{your-website}` | `{website-host}` | Match `{*-host}` convention |
| `mmerterden` (hardcoded in examples) | `{owner}` | Personal handle, public but must be a placeholder in generic docs |
| `my-project`, `my-figma-project`, `my-mobile-app` | `my-ui-components`, `my-figma-app`, `my-ios-app` | Canonical project names per 2.3 |

---

## 2.6 Intentional structural divergence  -  thin dispatcher vs inlined orchestrator

The top-level orchestrator has two deliberately different shapes. This is **not
drift**  -  it reflects a real capability gap between the host CLIs, and auditors
must not flag it as a parity violation.

| Surface | File | Shape | Why |
|---|---|---|---|
| Claude Code (colon-form) | `pipeline/commands/multi-agent/SKILL.md` | ~300 lines  -  thin dispatcher that routes to `$HOME/.claude/multi-agent-refs/phases/phase-N-*.md` on demand | Claude Code lazy-loads reference files, so only the active phase's docs enter the context window. Saves tokens. |
| Copilot CLI (dash-form) | `pipeline/skills/shared/core/multi-agent/SKILL.md` | ~830 lines  -  full inline orchestrator with all 8 phase specs embedded | Copilot CLI loads the whole SKILL.md once the skill is dispatched; it has no equivalent of Claude's ref-file loading. Embedding keeps behavior identical without relying on a feature Copilot lacks. |
| Codex CLI | installed as `~/.codex/skills/multi-agent/SKILL.md`, generated from the Claude dispatcher | thin dispatcher, refs under `~/.codex/multi-agent-refs/` | Codex reads files on demand, so it takes the Claude shape  -  **and it has to.** See below. |

### Why Codex takes the thin-dispatcher shape, and must keep it

Codex assembles every discovered skill's name + description into a single prompt
block and **silently drops entries when that block overflows**. Measured against
Codex 0.145 during the install design: installing one plugin that declares 142
skills took the block from 11 skills / 4,710 bytes to 83 skills / 22,111 bytes  -
only **75 of the 142** surfaced, **and an unrelated user-scope skill was evicted**.
Removing the plugin brought it back.

So shipping every sub-command as a peer skill on Codex would silently lose
pipeline commands next to any stack toolkit, with no error anywhere. The pipeline
therefore contributes **exactly one** skill on Codex (`multi-agent`) and keeps every
sub-command spec as a reference file that costs nothing until read.

**Do not "fix" this by adding per-command skills on Codex.** The layout is
capability-derived, and `smoke-install-layout.sh` fails if the Codex skills tree
gains a second pipeline entry.

### Parity axis differs per host

Claude Code and Copilot CLI are compared on their **skill directory sets**. Codex is
compared on its **ref set**: every command spec must exist under
`~/.codex/multi-agent-refs/commands/<cmd>/SKILL.md`, and
`smoke-codex-install.sh` asserts the count against the source tree. Comparing Codex
on skill directories would demand exactly the layout that breaks it.

**What must stay identical** (byte-level) across the two files:

- Phase canonical labels (Phase 0-7)
- Input parsing table (Section 5 of this doc)
- Routing decisions for each input type
- Placeholder vocabulary (Section 2 of this doc)
- Mode flag semantics (Section 5 of this doc)
- Output artifact schemas (Section 6 of this doc)

**What's allowed to differ**:

- File length and section grouping
- Whether phase docs are inlined vs referenced
- Language of operational commentary (TR helpers in dash form are OK; key identifiers / commit messages / PR bodies stay English everywhere)

Future changes that break an item in the "stay identical" list must update **both** files in the same commit. `smoke-cross-cli-behavior.sh` enforces the identity-preserving axis (input parsing, routing, output shape); structural differences are left to manual review because enforcing them would require forcing the files to the same shape, which we intentionally don't want.

## 3. Frontmatter Transform Rules (Claude ↔ Copilot)

Each file has a different frontmatter schema. The sync flow transforms between them:

### 3.1 Claude Code  -  `commands/multi-agent/{cmd}.md`

```yaml
---
description: "<one-line purpose>"
allowed-tools: <comma-separated tool list>
---
```

### 3.2 Copilot CLI  -  `skills/multi-agent-{cmd}/SKILL.md`

```yaml
---
name: multi-agent-{cmd}
description: "<one-line purpose>"
user-invocable: true
argument-hint: "<input hint>"
---
```

### 3.3 Transform Contract

| Direction | Action |
|---|---|
| Claude → Copilot | Add `name`, `user-invocable: true`, `argument-hint`; remove `allowed-tools`; `description` stays English (localization happens only in the installed Claude tree via `description-tr` + `localize-commands.mjs`, never in repo or Copilot files) |
| Copilot → Claude | Remove `name`, `user-invocable`, `argument-hint`; add `allowed-tools` (inferred from command's actual tool calls) |

**Invariant**: the English `description` must have the same meaning on both sides, and a `description-tr` line (when present) must be a faithful translation of it. Semantic drift is not allowed. `description-en` sidecars are an installed-tree artifact and must never appear in repo or Copilot files.

---

## 4. Progress Signalling Parity

### 4.1 TaskCreate ↔ phase-tracker.sh

| Concept | Claude Code | Copilot CLI | Codex CLI |
|---|---|---|---|
| Register a phase | `TaskCreate` tool call with subject/description | `phase-tracker.sh add <N> <name>` | `update_plan` step, `status: pending` |
| Mark a phase in-progress | `TaskUpdate` → `in_progress` | `phase-tracker.sh update <N> in_progress` | `update_plan` step → `in_progress` |
| Mark a phase complete | `TaskUpdate` → `completed` | `phase-tracker.sh update <N> completed` | `update_plan` step → `completed` |
| Mark a phase failed | `TaskUpdate` → `completed` + log failure in agent-log | `phase-tracker.sh update <N> failed` | `update_plan` step → `completed` + failure in agent-log |
| Sub-phase progress | TaskCreate with `addBlockedBy` | `phase-tracker.sh sub <N> <sub> <name> <status>` | `phase-tracker.sh sub ...` (the plan tool has no nesting) |

`update_plan` takes the FULL step list, not a delta, so each boundary rewrites the
whole plan. It must never be called in parallel with another tool, and it is
unavailable in Codex plan mode  -  fall back to `phase-tracker.sh render` there.

### 4.2 Contract

Every phase boundary MUST call EITHER TaskCreate/Update (Claude) OR `phase-tracker.sh` (Copilot). `phase-tracker.sh` is the cross-CLI source of truth (writes to `~/.claude/logs/multi-agent/<task_id>/tracker-state.json`). Claude Code users get BOTH: TaskCreate tiles AND tracker state.

### 4.3 Banner Parity

`phase-banner.sh` runs on both CLIs. Same output format, no Claude-only or Copilot-only flair.

---

## 5. Argument Parsing Invariants

Given the same user input, both CLIs must produce identical parsed state:

| Input pattern | Parsed as |
|---|---|
| `PROJ-12345` (bare Jira ID) | `input.type = "jira-id"`, `input.jiraId = "PROJ-12345"` |
| `https://{jira-host}/browse/PROJ-12345` | `input.type = "jira-url"`, `input.jiraId = "PROJ-12345"` |
| `https://github.com/{owner}/{repo}/issues/42` | `input.type = "github-issue-url"`, `input.issueNo = 42`, `input.owner = "{owner}"`, `input.repo = "{repo}"` |
| `#42` or bare `42` | `input.type = "github-issue-number"`, `input.issueNo = 42` |
| Any other string | `input.type = "free-text"`, `input.summary = <string>` |

Modifier flags are orthogonal and compose:
- `autopilot`  -  skip confirmations
- Short depth  -  init → dev → review → test → commit → report (Phase 0 Step 7.5 picker, not a flag)
- `--local`  -  no worktree
- Combinations: `--local autopilot`. Short plus autopilot is NOT a combination: autopilot never asks the depth question and always runs Full.

---

## 6. Output Format Expectations

| Artifact | Both CLIs must produce |
|---|---|
| `agent-state.json` | Same JSON schema (see `~/.claude/schemas/agent-state.schema.json`); same field names; same values for identical input |
| `agent-log.md` | Same section order, same phase headers (`📊 Phase 1`, `🧠 Phase 2`, etc.), same timeline columns |
| PR body | Same template; no CLI-specific headers |
| Jira comment | Same template; real newlines (no literal `\n`); no HTML entities (no `&amp;` / `&lt;` / `&quot;`) |
| Commit messages | Format `{type}(scope): description [{JIRA_KEY}-{id}]`; identical on both CLIs (see `~/.claude/rules/git-conventions.md`) |

---

## 7. Platform Guards (macOS / Linux / WSL)

All three CLIs run on macOS, Linux and Windows, so any command may execute on any of them. (This line used to say Claude Code was macOS-only; it has not been true for some time, and the assumption it invited - that a Windows user necessarily arrives through Copilot or Codex - produces wrong conclusions about which paths need to be portable.) Shell code in `pipeline/skills/shared/core/` command files and in `pipeline/lib` / `pipeline/scripts` must be portable.

For Keychain I/O the canonical path is **`~/.claude/lib/credential-store.sh`** (or `~/.copilot/lib/credential-store.sh` / `~/.codex/lib/credential-store.sh` on those installs). The shell driver detects platform internally and auto-delegates to `keychain.py` (Python helper, macOS / Linux) or PowerShell `CredentialManager` (Windows). Call sites stay platform-agnostic  -  no per-OS branching needed.

| Purpose | Canonical (cross-platform) | Underlying backend (for reference / debugging) |
|---|---|---|
| Keychain read | `~/.claude/lib/credential-store.sh get <key>` | macOS: `security find-generic-password -a "$USER" -s <key> -w` · Linux: `secret-tool lookup account "$USER" service <key>` · Windows: PowerShell `Get-StoredCredential` |
| Keychain write | `~/.claude/lib/credential-store.sh set <key> <value>` (or `keychain.py set <key> -` to stream the secret over stdin) | macOS: `security add-generic-password -a "$USER" -s <key> -w "$VAL"` · Linux: `secret-tool store --label=<key> account "$USER" service <key>` · Windows: `New-StoredCredential` |
| Clipboard read |  -  (still needs a per-platform branch  -  there is no helper) | `pbpaste` (macOS) · `wl-paste` (Wayland) · `xclip -selection clipboard -o` (X11) |
| Clipboard clear |  -  | `pbcopy < /dev/null` (macOS) · `printf '' \| wl-copy` (Wayland) · `printf '' \| xclip -selection clipboard` (X11) |

For clipboard ops, callers still gate with `if command -v pbpaste >/dev/null; then ...; elif command -v wl-paste ...; elif command -v xclip ...; fi`. A future `pipeline/lib/clipboard.sh` could absorb that pattern; until it exists, keep the inline guard in skill bash blocks.

---

## 8. Enforcement

This contract is validated by:

- `smoke-cross-cli-behavior.sh`  -  asserts every command behaves identically, pulls from Section 2 (placeholder vocab), Section 5 (argument parsing), Section 6 (output formats); also regression-locks the 8-persona agent deployment
- `smoke-commands-skills-parity.sh` (two assertions per command)  -  enforces colon-form command ↔ dash-form skill directory parity
- `smoke-compliance-skills.sh`  -  enforces store-compliance skill catalog + 4 consumer wiring
- `smoke-personal-data.sh`  -  extended in 0.5.5 to treat deprecated placeholders (`{github-username}`, `{your-website}`, `{website-repo}`) as leaks; adds `mmerterden` to public-handle blocklist for generic docs
- `pre-push-check.sh`  -  runs the cross-CLI + personal-data smoke tests before any push that touches `pipeline/skills/shared/core/` commands
- `sync.md` (Section 2 REPO step)  -  genericization lookup table MUST match Section 2 of this contract

---

## 9. Change Control

To change anything in this contract:

1. Amend this file with rationale
2. Update `sync.md` genericization table (if Section 2 changes)
3. Update `smoke-cross-cli-behavior.sh` fixtures (if Section 5/6 changes)
4. Run `smoke-cross-cli-behavior.sh` and `smoke-personal-data.sh` locally  -  both must pass
5. Announce in pipeline repo `CHANGELOG.md` under `## [Unreleased]` → `### Contract`

No silent changes. No exceptions.
