Initialize / onboard this agentic-QA plugin onto a deployment. Installs deps, then asks the operator only what genuinely shapes the config — the environment NAME, the bug tracker (Jira / Azure Boards), the code host (GitHub / Azure Repos), and an auth preference per axis (PAT recommended, else browser/CLI login). Everything else — whether it is a native-platform or a CLIENT project, the client org, the contribution mode, the fork account — is DERIVED from the token + the filled env + a live m...
Scanned 9/20/2026
Install to Claude Code
npx -y skills add VirtoCommerce/vc-mcp-testing-module --skill project-init --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Project Init?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/virtocommerce-project-init-vc-mcp-testing-module)More formats (shields.io, HTML) on the badges page.
---
name: project-init
description: "Initialize / onboard this agentic-QA plugin onto a deployment. Installs deps, then asks the operator only what genuinely shapes the config — the environment NAME, the bug tracker (Jira / Azure Boards), the code host (GitHub / Azure Repos), and an auth preference per axis (PAT recommended, else browser/CLI login). Everything else — whether it is a native-platform or a CLIENT project, the client org, the contribution mode, the fork account — is DERIVED from the token + the filled env + a live module/repo scan. Writes project-profile.json + .env.<env> + .env.local + .mcp.json and verifies access. The whole point is to make /qa-fix route each bug to the RIGHT repo (client custom code vs native platform) and file to the RIGHT tracker. Use when standing the plugin up on a new machine or for a new customer. Day-2 modes skip the interview: `--add-env` adds another environment (URLs + per-env access keys) to an already-onboarded project; `--check` reconciles an existing profile to the current schema then verifies."
---
# /project-init — deploy & wire this QA plugin for a customer
Stand up this plugin against a customer's infrastructure and produce a single
**deployment profile** (`project-profile.json`) that every other skill reads to
know what it's working with. **Focus: get `/qa-fix` working first** — it must
understand the infrastructure and route a found bug to the correct repository
(client module / theme / storefront fork **or** the native VirtoCommerce
platform) and to the correct bug tracker.
> **Ask only what shapes the config; DERIVE the rest.** The interview is three
> answers — **env name · tracker · code host** — plus a soft auth preference per
> axis. `projectType` (native-platform vs client), the client org, the
> contribution mode (fork vs direct), and the fork account are **not asked** —
> they are derived from the token's permissions + the filled `.env` + a live
> module/repo scan. Ask again only on a *genuine* ambiguity.
> **Additive, never destructive.** This skill only *adds* a profile + config. With
> no profile, the plugin keeps its original behaviour (native-platform, Jira,
> GitHub). Nothing existing is rewritten.
> **Two day-2 modes** (skip the interview): **`--add-env`** — add another environment
> (URLs + per-env access creds) to an already-onboarded project (§`--add-env` below);
> **`--check`** — reconcile an existing profile to the current schema after a plugin
> upgrade, then verify (§`--check`).
## What it produces
| Artifact | Purpose |
|----------|---------|
| `project-profile.json` (gitignored) | the deployment profile — read by `config.js` (→ every skill via `env.PROFILE`), `ci/lib/repo-router.ts` (client-vs-platform routing), `ci/lib/trackers/*` (which tracker) |
| `.env.<env>` + `.env.local` | Both scaffolded as **commented templates** the operator fills in — no values are asked in the interview. `scaffold-env.mjs` writes `.env.<env>` (Bucket #2: URLs/identifiers/tracker connection); `scaffold-secrets.mjs` writes `.env.local` (Bucket #3: secrets, per-env creds `_<ENV>`-suffixed). Each placeholder carries what/where comments. |
| `.mcp.json` + `.claude/settings.local.json` | MCP servers enabled for the chosen tracker/VCS (via `gen-mcp.mjs`). The Playwright servers are configured entirely via CLI flags (`--browser` / `--isolated` / `--viewport-size` / `--output-dir`) — no config files are shipped or copied. Only `playwright-chrome` is enabled by default; `playwright-firefox` / `playwright-edge` stay defined for opt-in cross-browser runs. |
## Pipeline
```
0 preconditions (confirm dir · BOTH self-diagnostics consents FIRST + write flag · install) → 2 interview (env name · tracker · code host · auth pref)
→ 3 scaffold BOTH env templates + operator fills + pause
→ 3d normalize + validate the filled .env.<env> (normalize-env) — STOP on exit 1
→ 4 discover repos (ALWAYS) → projectType · clientOrg · repo split · storefront
→ 4b discover tracker (Azure) → per-type states · role→state map · apiBase · projectId
→ 5 derive block (derive-context) → auth-fact · contributionMode · forkAccount · operator
→ 6 write profile (gen-profile: repos-json + tracker-json + derived flags) → 7 MCP (gen-mcp)
→ 8 verify (verify-access) → 9 done
```
All scripts live in the plugin's own `skills/project-init/` directory and are
**non-interactive** — you (the model) collect the interview answers, run the scan +
derive scripts, then call the writers with the results as flags.
### Output discipline — the operator is onboarding, not reading a report
Onboarding is the operator's FIRST contact with the plugin. Every extra table teaches them that
this tool is heavy, and it buries the two or three things they actually have to decide. Three
rules, applied at every step:
1. **A question goes immediately after the thing it asks about.** Never table → table → table →
question: the operator has to scroll back, so they approve unread. One table, one question,
nothing in between.
2. **Show what the operator can DECIDE. Summarise what they cannot.** A scan result they can
correct earns a table. A derived fact (a token's permission, an API base, a state map that
resolved cleanly) earns ONE line — or nothing. Internals that only feed `/qa-fix`
(`apiBase`, `projectId`, `forkAccount`, `upstreamRef`, local-verify commands) go into the
profile, not onto the screen.
3. **Exceptions get the space, happy paths get a line.** Anything that resolved cleanly is one
line; anything WARN/missing/unverified gets its own line or small table and says what to do.
That contrast is what makes a real problem visible.
Never print the same fact twice in two shapes (a "provenance" table repeating a Notes column, a
role grid restating "all roles mapped"). If it is already on screen, reference it.
### Output FORMAT — two shapes, used consistently
Two client-side constraints decide this format; both were confirmed on a live run, not assumed:
- **Colour is unavailable.** This output is Markdown and the client paints it — an ANSI escape
never reaches the screen.
- **`---` does NOT render as a rule** in the Claude Code terminal — it prints as three literal
dashes. Do not use it as a separator. **Box-drawing characters always render**, because they
are ordinary text, not markup.
- **Markdown collapses adjacent lines into one paragraph**, so a multi-line frame only survives
inside a fenced code block — which is also what gives it a real border on both surfaces.
Reliable everywhere: box-drawing (`┌─┐│└┘`), CAPS, **bold**, `code`, emoji, fenced blocks.
**1. Step header — a framed, iconed, CAPS name in a fenced block.**
````markdown
```
┌──────────────────────────────┐
│ ⚙ ENVIRONMENT NAME │
└──────────────────────────────┘
```
````
- Size the frame to the text (a couple of spaces of padding); do not pad to a fixed width — a
long step name in a narrow box wraps and the frame breaks.
- **The frame replaces the separator.** It already stops the eye, so no rule is needed above it.
- **Icon per step kind** — one consistent glyph, so a returning operator recognises the phase
before reading it:
| Icon | Step kind | Examples |
|---|---|---|
| 📁 | preconditions / where things land | confirm directory |
| 🔒 | consent + credentials | self-diagnostics consents, auth preference |
| ⚙ | interview / configuration | environment name, tracker + code host |
| 📝 | generating files | env templates, profile, `.mcp.json` |
| 🔍 | scanning / deriving | repo split, tracker states, derive block |
| ✅ | verification | readiness table |
| 🎉 | finished | the wrap-up |
- **No step numbers.** `2a` / `4b` / `0c` are THIS file's internal numbering; they mean nothing
to the operator. (They stay here in the doc — the model and the reviewer navigate by them.)
**2. A question asked in PLAIN CHAT — question first, WAITING last.**
```markdown
**What should this environment be named?**
It becomes `TEST_ENV` and the suffix of the env file (`.env.<name>`). Typical: `qa`, `dev`,
`staging`, or a customer short name like `acme_qa`. I normalise it to `[a-z0-9_]+`.
⏸️ **WAITING FOR YOU** — reply with the name.
```
Why this order:
- **The question comes FIRST, as its own bold line.** The original banner said only "waiting"
while the actual ask sat buried mid-paragraph above it — a cue with no question attached, so
the operator scrolled past both.
- **Context in the middle**, ≤2 lines: what the value becomes, typical answers, what you do to it.
- **`⏸️ WAITING FOR YOU` is the LAST line**, and names the action (`reply with the name`,
`reply "done"`). It is the hand-off, so it belongs at the hand-off point — the operator's eye
lands there last and knows exactly what to type.
- **Nothing follows it.** No further prose, no tool call — the turn ends so the ask stays on
screen.
- No progress counter (operator's call — noise on a 3-question interview).
**When the question is an `AskUserQuestion`, drop the WAITING line.** That tool renders its own
picker, so the banner is duplicate chrome. Framed step header, one line of context if the
options need it, then the tool call.
### Where things go — read this once (two roots, kept separate)
- **Plugin install directory** (`$CLAUDE_PLUGIN_ROOT` — the versioned marketplace cache
when the plugin is installed, or this checkout when developing): holds the read-only
scripts, templates, and source MCP configs. **Invoke every generator by its ABSOLUTE
path here** — `node "$CLAUDE_PLUGIN_ROOT/skills/project-init/<name>.mjs" …`. Do **not**
use a bare relative `node skills/…` path: your Bash working directory is the deployment
project, not the plugin, so a relative path won't resolve once the plugin is installed
from the marketplace. (The bash examples below show the bare path for brevity — always
prefix it with `$CLAUDE_PLUGIN_ROOT/`.)
- **Deployment project directory** (your Bash cwd — the folder Claude Code was launched
in): where **all generated state lands** — `project-profile.json`, `.env.<env>`,
`.env.local`, `.mcp.json`, and `.claude/settings.local.json`. The generators default
their output there
automatically (symmetric with the readers: `config.js` dotenv-loads `.env.*` and
`loadProjectProfile()` reads the profile from cwd). You do **not** pass an output path in
the normal flow. `VC_FIX_HOME=<dir>` overrides the output root only for out-of-project /
CI callers — the interactive flow never sets it.
**Every generator flag you need is documented in the steps below — never inspect the
`.mjs` scripts (and never pass `--help`: unrecognised flags are treated as booleans and
the script runs anyway, writing a default file).**
---
## 0. Preconditions — confirm the working directory, then detect AND install tooling
### 0a. Confirm the deployment project directory (everything lands here)
Run `pwd` and show the operator the current directory. **This is your deployment project
home** — everything `/project-init` generates (profile, env files, `.mcp.json`, MCP
configs) is written here, and every later run (`/qa-fix`, `/qa-bug`) reads its config from
here. Confirm with `AskUserQuestion`:
- **Yes — initialize here (`<cwd>`)** (Recommended) → proceed.
- **No — wrong folder** → **STOP.** Tell the operator to relaunch Claude Code from their
deployment project directory, then re-run `/project-init`. (The Bash working directory is
fixed to wherever Claude Code was launched and cannot be moved outside it mid-session, so
the project home must be chosen at launch — not with `cd`.)
Then resolve the **plugin directory** once, so you can invoke the generators by absolute
path: `echo "$CLAUDE_PLUGIN_ROOT"`. If it prints a path, use it. If empty, locate the
installed plugin (e.g. under `~/.claude/plugins/`, or the absolute directory this SKILL.md
loaded from). Use it as the `$CLAUDE_PLUGIN_ROOT/skills/project-init/…` prefix for every
generator call in the steps below.
### 0b. Self-diagnostics capture consent — ask FIRST, write the flag NOW
**This is the FIRST interactive decision of the whole run** — before installing tooling
and before the interview. Reason: the passive session-telemetry hook captures **opt-in**
— it is a full no-op until `project-profile.json` explicitly sets `selfDiagnostics: true`.
`/project-init` writes its profile only at the *end* of onboarding, so if we waited, its
OWN run (the most failure-prone skill a client runs) would never be captured. Asking now +
writing the flag immediately makes the rest of this run observable from this point on.
Ask with **`AskUserQuestion`** — a single question, wording taken **verbatim** from
`reconcile-profile.mjs` `MANAGED_FIELDS.selfDiagnostics.question` so the fresh interview and
`/project-init --check` never diverge (make the first option the `default`):
> *"Enable vc-fix self-diagnostics for this project? The passive session-telemetry hook
> records how the plugin's OWN skills ran (to `<project>/.vc-fix/`, gitignored) so
> `/vc-self-check` can spot plugin quality issues. It never sends anything without a separate
> consent step and never touches your code."*
- **Yes (recommended)** → **immediately** write a stub profile carrying just the flag, so
capture turns on right away (`gen-profile` layers over `PROFILE_DEFAULTS`, so this writes a
complete-but-default profile with `selfDiagnostics: true` — step 6 rewrites it fully from the
interview + scan, so the stub is disposable):
```bash
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/gen-profile.mjs" --self-diagnostics true --print
```
- **No** → write nothing now; capture stays off for this run (opt-in). Remember the answer.
**Carry the answer to step 6** as `--self-diagnostics <true|false>` (step 6's authoritative
write re-passes it, so the final profile always reflects this decision). This is the ONLY place
`/project-init` asks the capture opt-in — no other command asks it; they just read the flag.
(Note: the `session_start` record still misses this run — `SessionStart` fired before the flag
existed — but every span + the finalize verdict are captured from the write onward. Accepted.)
### 0c. Upstream-delivery consent is NOT asked here (PR #172 item 4)
> There used to be a second consent step here for `feedback.mode` (ask / auto / off). It was
> **removed**. Asking at onboarding meant the operator had to decide how a finding should be
> contributed **before any finding existed** — a context-free question, minutes after the capture
> opt-in, about a decision only meaningful when there is actually something to send.
The delivery flow now asks **once, per finding, at the moment a BROKEN/DEGRADED finding exists**:
`/vc-self-check` spawns the diagnostician, and if it returns a routable finding the orchestrator
asks a single binary *file the issue in Virto — yes/no?* — with the exact payload on screen. So
`feedback.mode` needs no onboarding question:
- It stays at its **`ask`** default (`PROFILE_DEFAULTS`) — meaning "ask each time, per finding".
- Hand-edit `project-profile.json` (or `gen-profile --feedback-mode <v>`) to set **`auto`** (CI /
standing consent — file directly) or **`off`** (kill switch — nothing ever leaves the machine).
- `reconcile-profile.mjs` no longer lists `feedback` as a MANAGED "ask" field, so
`/project-init --check` fills it as a safe default and never surfaces it as a pending decision.
**Do not pass `--feedback-mode` from the interview.** §0b's `--self-diagnostics` is the ONLY
self-diagnostics answer carried to step 6.
### 0d. Detect AND install the required tooling
Run a detection pass, then **install whatever is missing** — do not just report a
gap and move on (that leaves `/qa-fix` unable to open PRs).
- **Node 18+** and **git** — hard prerequisites. If absent, STOP and ask the
operator to install them (cannot be auto-installed reliably).
- **`gh` (GitHub CLI)** — **required** (platform upstream + client GitHub
PRs/issues). Install now if missing.
- **`az` (Azure CLI)** — required **only** if the operator picks Azure
Boards/Repos in step 2. Install it then (or now if you already know).
Install commands (pick by OS). System installs need `sudo`, which prompts — have
the operator run them via `!` in the prompt (e.g. `! sudo pacman -S github-cli`):
| Tool | Debian/Ubuntu | Arch/CachyOS | macOS (brew) | Windows (winget) |
|------|---------------|--------------|--------------|------------------|
| `gh` | `sudo apt install gh` | `sudo pacman -S github-cli` | `brew install gh` | `winget install GitHub.cli` |
| `az` | `curl -sL https://aka.ms/InstallAzureCLIDeb \| sudo bash` | `sudo pacman -S azure-cli` | `brew install azure-cli` | `winget install Microsoft.AzureCLI` |
Detect with `command -v gh` / `command -v az`; re-check after install before
proceeding.
## 1. Install dependencies
Install the plugin's own package dependencies + the Playwright browsers:
```bash
( cd "$CLAUDE_PLUGIN_ROOT" && npm install ) # into the PLUGIN's node_modules (subshell — your cwd is unchanged)
npx playwright install chromium firefox # browser binaries (global cache — cwd-independent)
```
(Edge uses the system `msedge` channel; WebKit is not used on Windows.) This step
must run before any generator/verify script. **Install inside the plugin (`( cd
"$CLAUDE_PLUGIN_ROOT" && npm install )`), NOT a bare `npm install`** — several scripts
(`discover-repos`, `derive-context`, `verify-access`, `ensure-session`) import `dotenv`,
and Node resolves that bare specifier from the *script's own* directory (the plugin), not
your project cwd. A bare `npm install` would install into the project and leave those
scripts unable to find `dotenv`. The subshell `( … )` keeps your project cwd unchanged
(don't use a bare `cd`, which the harness resets when it leaves the project).
## 2. Interview — three answers + an auth preference, THEN scaffold
> **No mid-interview reconnaissance.** Do **not** read files, inspect the `.mjs`
> scripts, or run any other Bash **between** the interview steps. Ask the questions,
> then run step 3. Only these answers are collected — **no operator / projectType /
> contribution-mode question** (those are derived in steps 4–5).
### 2a. ENV_NAME — one plain chat question
Do **not** use `AskUserQuestion` (always ≥2 option buttons — no option-less input) or a
`show_widget` input (unreliable) for this — a plain chat question is the stable way to collect
one free-text value. Emit exactly this (§Output FORMAT):
````markdown
```
┌──────────────────────────────┐
│ ⚙ ENVIRONMENT NAME │
└──────────────────────────────┘
```
**What should this environment be named?**
It becomes `TEST_ENV` and the suffix of the env file (`.env.<name>`). Typical: `qa`, `dev`,
`staging`, or a customer short name like `acme_qa`. I normalise it to `[a-z0-9_]+`.
⏸️ **WAITING FOR YOU** — reply with the environment name.
````
End the turn there — nothing after. On the reply, normalise mixed case / spaces / hyphens to
`[a-z0-9_]+` (e.g. `My QA` → `my_qa`) and tell the operator what you used.
### 2b. Tracker + code host — one `AskUserQuestion` block
Ask these two enums together:
1. **tracker** — `jira` or `azure` (Azure Boards). What the bug-tracker skills work
against + which tracker MCP is enabled.
2. **code host** — `github` or `azure-repos`. Where the **client's own** code lives
(drives routing + how a client PR is opened). Tracker and code host are
**independent** (Azure Boards + GitHub is fine). The platform upstream is
**always** GitHub, whatever the client host.
### 2c. Existing-env guard — check `.env.<name>` BEFORE scaffolding
After you have the normalised name, **check whether `.env.<name>` already exists**
(`test -f .env.<name>`, or list `.env.*`). This must happen before step 3.
- **Not found** → proceed.
- **Found** → surface it with `AskUserQuestion` (two options), never auto-pick:
1. **Use the existing environment** — keep the name; scaffolding is idempotent
and only *adds* missing keys, never clobbers filled values.
2. **Create a new one with a different name** — ask again in plain chat; re-run
this 2c guard on the new name.
Show the operator the set of keys the existing file already contains (values
masked) so they can decide — never print secret values.
### 2d. Auth preference — one `AskUserQuestion` block (PAT recommended)
Now that tracker + host are known, ask an auth preference **only for the applicable
axes**, each as a question in a single `AskUserQuestion` call. **A PAT is the
recommended option** (more reliable / non-interactive); the alternative is a browser
/ CLI login. **You are NOT collecting the token in chat** — the choice only decides
whether step 3 emits a *token placeholder to fill in `.env.local`* (PAT) or *no token
line + a login to run* (session). verify-access (step 8) confirms whichever is
actually present.
Applicable axes:
| Axis | When applicable | Options |
|------|-----------------|---------|
| **GitHub** | **always** (the platform upstream is always GitHub; and the client host may be GitHub) | `PAT` (Recommended) → `GITHUB_FIX_BUGS_TOKEN` placeholder · `Browser login` → `gh auth login` |
| **Azure DevOps** | tracker = `azure` **OR** code host = `azure-repos` | `PAT` (Recommended) → `ADO_PAT` placeholder · `Browser login` → `az login` |
| **Jira** | tracker = `jira` | `API token` (Recommended) → `JIRA_API_TOKEN` placeholder · `Atlassian MCP OAuth` (no token line) |
Map the answers to the scaffold flags: `--github-auth pat|gh-cli`, `--ado-auth
pat|az-login`, `--jira-auth token|oauth`.
## 3. Scaffold the two env templates, then hand off for filling
Using the env name + tracker + code host + auth preferences, create **both** env
files as commented templates. No values are collected here — the operator fills them.
**Do NOT pass `--project-type` / `--client-org`** — projectType and the client org are
derived by the scan (step 4), not chosen; leaving them off means scaffold-env emits no
`CLIENT_REPO_ORG` placeholder (it is derived).
### 3a. `.env.<env>` template (`scaffold-env.mjs`)
```bash
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/scaffold-env.mjs" --env myqa --tracker jira --client-vcs github --print
# Azure Repos client on Jira → ADO_ORG/ADO_PROJECT are emitted for the code host too:
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/scaffold-env.mjs" --env acme --tracker jira --client-vcs azure-repos --print
```
Flags: `--env <name>` (required), `--tracker jira|azure`, `--client-vcs
github|azure-repos`. `ENV_RISK` and `ADMIN` are pre-filled with safe defaults;
everything else is empty. `ADO_ORG`/`ADO_PROJECT` are emitted when the tracker is
Azure **or** the code host is Azure Repos. Idempotent — a re-run only adds missing
keys, never clobbers.
### 3b. `.env.local` template (`scaffold-secrets.mjs`)
```bash
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/scaffold-secrets.mjs" \
--env myqa --tracker jira --jira-auth token \
--client-vcs github --github-auth pat --print
# client on Azure with session auth for both axes → app passwords + optional MCP keys only:
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/scaffold-secrets.mjs" \
--env acme --tracker azure --client-vcs azure-repos \
--ado-auth az-login --github-auth gh-cli --print
```
Flags — **one per auth axis, from step 2d**: `--jira-auth token|oauth` · `--ado-auth
pat|az-login` (`az-login` ⇒ no `ADO_PAT`) · `--github-auth pat|gh-cli` (`gh-cli` ⇒ no
`GITHUB_FIX_BUGS_TOKEN`). App test-user passwords (`ADMIN_PASSWORD`/`USER_PASSWORD`,
`_<ENV>`-suffixed) are always emitted. **`POSTMAN_API_KEY` + `CONTEXT7_API_KEY` are
always emitted too, but as OPTIONAL placeholders** (blank ⇒ that MCP server stays
disabled; not needed for `/qa-fix`) — each with a "which tool it powers" comment.
(`--extras` is retained for back-compat but no longer gates them.) Idempotent.
### 3c. Tell the operator: two files created — fill them, then pause
Under a framed `📝 FILL IN THE TWO ENV FILES` step header (§Output FORMAT), print:
- **two files were created**: `.env.<env>` (non-secret URLs/identifiers) and `.env.local`
(secrets, gitignored) — with the placeholder keys each emitted;
- that the inline comments say what each value is and where to get it;
- for any **browser-login** choice (github `gh auth login`, ado `az login`, Jira Atlassian MCP
OAuth) there is no token line — remind the operator to run that login instead (step 8's
`ensure-session.mjs` drives it).
Then the plain-chat question block (§Output FORMAT), which is what makes the pause impossible
to scroll past:
```markdown
**Filled in both files?**
I normalise and validate them first (trailing slashes, quotes, unfilled placeholders), then
scan your repos and verify every credential.
⏸️ **WAITING FOR YOU** — reply "done" when both files are filled.
```
Nothing after the block — no further tool calls; the turn ends there so the ask is the last
thing on screen. This is the same format every blocking question uses.
Note the env name (e.g. `myqa`) — that's your `TEST_ENV` for every later run.
### 3d. On "done" — NORMALIZE + VALIDATE the filled `.env.<env>` (MANDATORY, before the scan)
The very first thing you do on the operator's "done" — **before** the repo scan in step 4 and
before any access probe:
```bash
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/normalize-env.mjs" --env <env>
```
It rewrites the file **in place** and prints, line by line, exactly what it corrected: surrounding
quotes and padding stripped, **every trailing slash removed** from `FRONT_URL` / `BACK_URL` /
`JIRA_BASE_URL`, a pasted `https://dev.azure.com/<org>` reduced to the bare `ADO_ORG` slug. It
**exits 1** on an unfilled placeholder, a URL with no `http(s)://` scheme, or an `ADO_ORG`/
`ADO_PROJECT` that is still path-shaped, and WARNs when `FRONT_URL`/`BACK_URL` carries a path
component. Comments, ordering, and any variable the operator added themselves are untouched.
**On exit 1: STOP.** Show the errors verbatim, then re-ask with the plain-chat question block
(§Output FORMAT — *"Fixed them? Reply 'done'."*), and wait — do not
scan, do not probe. Every downstream check would otherwise fail for a reason the operator cannot
see: a stray `/` used to be stripped only *in memory* by `verify-access.mjs` while the file stayed
wrong, and every runtime `${BACK_URL}/api/...` template produced `//api/...` (VCST-5582 B). The
rules are not restated in this skill — `scaffold-env.mjs` `CATALOG` declares each key's type and
requiredness, and `normalize-env.mjs` reads them from there.
Relay the printed fixes to the operator in one line ("normalized N value(s): …") so they know the
file changed under them, then continue to step 4.
## 4. Discover the repo split — ALWAYS run; it is the source of projectType
**After the files are filled and normalized (§3d)**, run the scan **unconditionally** (it needs no
`--client-org` — clients are recognised by module owner / id-namespace). It classifies
installed modules client-vs-platform, scans the client's code host for the
storefront/theme repo, and **derives `projectType` + `clientOrg`**:
```bash
TEST_ENV=<env> node "$CLAUDE_PLUGIN_ROOT/skills/project-init/discover-repos.mjs" \
--client-vcs <github|azure-repos> --out .local-env/repos.json --print
# (add --insecure only for a QA env with a self-signed cert)
```
It writes `{ projectType, clientOrg, client:[…], platform:[…] }`:
- **projectType** = `client` if any client module was found, the host is `azure-repos`
(native VC always lives on GitHub), or a clientOrg resolved; else `platform` (native
VC — the original default, every repo platform-owned).
- **clientOrg** = the owner of the discovered client modules (`ADO_ORG` for an
azure-repos host).
- The **storefront/theme/frontend** repo is not a platform module; the scan lists the
client's repos (Azure Repos via `ADO_PAT` / `az`, or GitHub via the fix token) and
adds any name matching the theme/frontend heuristic as `kind:"frontend"`. It also
captures each repo's **`defaultBranch`** (so `checkoutForFix` doesn't blind-guess `main`)
and **best-effort provenance** for a storefront fork — `upstream` (the vc-frontend repo it
was forked from) + `upstreamRef`. **`upstreamRef` is now a CONCRETE, VERIFIED upstream tag
— the fork line's BASE (e.g. `2.49.0`), not the bare MAJOR.MINOR line label.** The scan
takes the fork's `package.json` `version` (kept as `forkVersion`, e.g. `"2.49.7"`), reduces
it to its `MAJOR.MINOR` line (`2.49`), then `git ls-remote --tags <upstream>` and picks the
smallest existing tag on that line (its base) — the guaranteed common ancestor ≤ the fork
(the fork's own patch has no upstream tag; the bare line label isn't a git ref — both 422 on
`vc-frontend`). It records `upstreamRefResolved: true`. If the line was never tagged it falls
back to the highest earlier tag; if ls-remote fails (offline / no token) it keeps the line
label with `upstreamRefResolved: false` and ASKS. The resolved baseline appears both in the
scan map (`… @ 2.49.0 (verified)`) and in the `verify-access` readiness table (**"Storefront
upstream ref"** row). **This is what lets `/qa-fix` Gate 1b tell a client customization from
an unmodified-platform bug** — if it couldn't be derived/verified, ASK the operator for the
vc-frontend line base tag and set `repos.client[].upstreamRef` (e.g. `2.49` → `2.49.0`).
### 4a. Show the map and ask RIGHT THERE — one table, one question, nothing between them
The operator confirms the repo map **immediately after seeing it**. Do **not** run step 4b or 5
first: a table separated from its question by two more sections forces the operator to scroll
back, and they will approve it unread. Sequence is exactly: repo table → `AskUserQuestion` →
(only then) 4b.
**ONE table. Columns: `Repo · Kind · Branch · Notes`** — and nothing else.
- **Branch** = `default → PR target` in one cell (`main → dev`). Two columns for two branch
names is padding.
- **Notes** = only what is repo-SPECIFIC: the toolchain, and for a storefront fork its
provenance in one phrase — `fork of vc-frontend 2.49.7, base 2.49.0`. Do **NOT** render a
separate "fork provenance" table: `upstream` / `upstreamRef` / `forkVersion` are the same
three facts, and the operator cannot verify them anyway — they are `/qa-fix` Gate-1b inputs,
not decisions. If `upstreamRefResolved: false`, that IS a decision → say so in Notes and ask.
- **Drop `Host` and `Auth`** when every client repo shares them (the usual case) — the operator
chose both in the interview two minutes ago. State once above the table:
`2 client repos on azure-repos (ADO_PAT) · 53 platform repos`. Keep a `Host` column only for a
genuinely mixed set.
- **Never list the 53 platform repos.** A count is the whole signal; they are not the operator's
decision and the list buries the two rows that are.
- Local-verify facts (`yarn dev`, port, cert) are `/qa-fix` plumbing — omit from the table.
Then ask with `AskUserQuestion`, and **put the actual discrepancy in the question text**, not
just in an option label — e.g. *"`frontend` defaults to `main` but the scan set PR target `dev`.
Correct?"* Options: accept as scanned · fix the one thing named · something else.
**Genuine-ambiguity asks (only these — everything else is confirm-or-correct):**
- host = `github`, **no** client modules found, org not resolvable → ask the operator:
native platform (no client repos), or name the client GitHub org.
- **no** storefront/theme repo matched → ask the operator to name it; add it to
`repos.client` as `kind:"frontend"`.
- `upstreamRefResolved: false` → ask for the vc-frontend line base tag (see above).
## 4b. Discover the tracker status model — Azure Boards only
**This step is what lets `/qa-fix` transition tickets by role WITHOUT asking the operator.**
Scan the tracker's work-item TYPES and, per type, its allowed STATES (a custom process is
common — Bug/Task/User story can each differ), and derive a `role → System.State` map. Skip
for Jira (transitions are discovered live at runtime; the scan would add nothing):
```bash
# Azure Boards:
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/discover-tracker.mjs" \
--tracker azure --org "$ADO_ORG" --project "$ADO_PROJECT" \
--types "Bug,Task,User story" [--team "<team>"] --out .local-env/tracker.json --print
# Jira (format facts only — no state scan needed):
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/discover-tracker.mjs" --tracker jira --out .local-env/tracker.json
```
The Azure scan also DISCOVERS the `team` whose current sprint `/qa-bug` will stamp (`tracker.azure.team`):
it enumerates the project's teams and picks the one that owns a **date-valid** current sprint (the
project's DEFAULT team is often dormant — its `timeFrame:"current"` flag points at a long-dead sprint).
Pass **`--team "<name>"`** to override the discovery (or to disambiguate when several teams have a
current sprint — the scan leaves the team unset and asks for one). An unset team is safe: the runtime
resolver in `ado.mjs` re-validates and can still auto-select the right team at bug-create time.
It writes `.local-env/tracker.json`: `{ kind, ticketKeyFormat, crossLinkToken, apiBase,
projectId, workItemTypes:{<Type>:{states:[…]}}, roleStates:{in-progress,in-review,
ready-for-test,done} }`. Auth = `ADO_PAT` (Basic) or an `az login` session — the same creds
the operator filled in step 3; if neither is present yet, this step FAILS loudly (fix the ADO
auth, don't skip it — an empty `roleStates` makes `/qa-fix` fall back to asking on every
transition). Step 6 ingests it via `--tracker-json`.
**Reporting — scale it to whether the operator has anything to decide:**
- **`roleStatesComplete: true` (every role mapped) → render the role→state grid as a TABLE and
CONFIRM it** with `AskUserQuestion` (options: **"Accept as scanned"** / **"Correct a role"**) — the
same shape §4a already mandates for the repo map. `roleStatesComplete: true` only means every role
got *a* state, **not** that the picks match this team's workflow. Because step 6 flips
`transitionPolicy=auto`, a mis-picked role moves real customer work items **silently, with no
further prompt** — so a custom process's mapping is exactly the kind of fact the operator must be
able to see and approve (D4). Render one row per role → picked `System.State`, and add a context
line listing the **unused** states — the alternatives the operator is implicitly approving against,
e.g. `Unused: New, On Dev, On hold, HotFixed, Resolved, On UAT`. On **"Correct a role"**, ask which
role, offer that board's states, and persist with
`reconcile-profile.mjs --write --set 'tracker.azure.roleStates.<role>=<state>'`.
- **Exception — the one-liner is fine ONLY when the picks are unambiguous:** the board's state set
is the STOCK one (New/Active/Resolved/Closed) with **no unused candidate state** a role could
plausibly have taken instead. Then: `Board states mapped (stock process, no ambiguity).
Transitions will be silent.` State counts per work-item type, `apiBase`, `projectId`,
`ticketKeyFormat` and the cross-link token stay internals: **do not print them.**
- **A role is MISSING or looks wrong** → show the table of the affected roles only and ask (as
above). A MISSING role is the one that must be resolved before `/qa-fix` can transition by it; a
complete-but-custom map is confirmed, not blocked.
Correct a mismapped role by hand-editing `.local-env/tracker.json` (or the profile) before
continuing.
Also captured here: the **bug FIELD CONTRACT** per work-item type (VCST-5582 E-a) —
`tracker.fields.<Type>[]` — plus the two lists that drive the FIRST bug creation:
**`operatorQuestions`** (every required field the operator must supply a value for, whether or not it
maps to a semantic slot) and **`unmappedRequired`** (only the subset that maps to no slot). **The
happy-path one-liner MUST state the TOTAL number of first-run questions — `operatorQuestions.length`
— and name them**, e.g. `Bug field contract: 16 fields, 8 required — /qa-bug's first run will ask 3
values (Environment, Reported by, Type of bug), then persist them`. Do **NOT** report only
`unmappedRequired`: on a live run it showed "1 question" (Value Area) while `operatorQuestions` held
3 more that were never surfaced, so the operator was told to expect 1 and got 4. A field the board
already answers — a `defaultValue` that is a member of a closed `allowedValues` set — is filled from
that default and is **not** a question (D1); it appears in neither list. **Surface the
`operatorQuestions` list to the operator now — never silently defer it to `/qa-bug`.** Show a table
only when `unmappedRequired` is non-empty (a required field with no semantic slot — a genuine mapping
gap to review).
`--out` is optional (accepts `--out <path>`; the default flag set here writes it so step 6 can
read it). If you omit `--out`, capture the printed JSON and pass its path to step 6 another way.
## 5. Derive the rest — no questions
Run the **derive block**: it reads the filled env + live sessions and derives the auth
actually present, the contribution mode, the fork account, and the operator role:
```bash
TEST_ENV=<env> node "$CLAUDE_PLUGIN_ROOT/skills/project-init/derive-context.mjs" \
--tracker <jira|azure> --client-vcs <github|azure-repos>
```
It prints ONE JSON object on stdout (notes on stderr):
```json
{ "auth": { "github": "pat|gh-cli|none", "ado": "pat|az-login|none|n/a", "jira": "token|none|n/a" },
"github": { "login": "...", "upstreamPerm": "push|pull...", "contributionMode": "direct|fork",
"forkAccount": "...", "via": "PAT|gh CLI",
"tokenKind": "classic|fine-grained|gh-cli|none", "forkCapable": "yes|no|unknown" },
"operator": "virto-engineer|client", "upstreamOrg": "VirtoCommerce" }
```
- **contributionMode / operator** — from the token's permission on
`VirtoCommerce/vc-platform`: `push`/`maintain`/`admin` ⇒ `direct` / `virto-engineer`;
otherwise ⇒ `fork` / `client` (you PR from your own fork). No token / offline ⇒
safe default `fork` (verify-access confirms).
- **forkAccount** — the GitHub token owner's login (PR head = `<forkAccount>:<branch>`;
only used when `contributionMode=fork`).
- **tokenKind / forkCapable** — the token's PROBED type (VCST-5582 A). The recommended
credential is **ONE classic PAT with `repo`** (or `gh auth login`): it covers both the
client's own repos and the upstream, so onboarding asks for a single value.
`contributionMode: "fork"` says WHERE a platform PR goes; `forkCapable` says whether this
credential can actually get it there. A **fine-grained** PAT is bound to one resource owner
and is read-only on public repos it does not own, so it reads `vc-platform` fine, is
classified `fork`, and then 403s on fork / fork-PR / issue-create. Only `forkCapable: "yes"` is
trusted — `"unknown"` is never assumed capable. Both land in the profile
(`vcs.githubTokenKind` / `vcs.githubForkCapable`) so `/qa-fix` Gate 1 and
`/vc-self-check deliver` refuse an impossible route up front, and `verify-access` shows
the **GitHub token kind / upstream capability** row with the exact remedy.
Capture these values for step 6.
**Reporting — this step has NO question in it, so it gets no table.** Everything here is
derived, not chosen: the operator cannot approve or correct a token's permission on
`vc-platform`. Print **ONE line**:
`Derived: classic PAT (Dan-BV) with push on vc-platform → direct PRs upstream, no fork · ADO: PAT`
Then **only the exceptions**, one line each — these are the whole point of showing anything:
- `forkCapable` is not `yes` while the plan needs the upstream → the WARN + remedy (VCST-5582 A);
- an auth axis came back `none` → say which, and that it must be fixed before `/qa-fix`;
- `contributionMode` fell back to `fork` because nothing could be probed (offline / no token) →
say it is a default, not a measurement.
`forkAccount`, `upstreamOrg`, the raw `auth` triple and the JSON itself are internals — they go
into the profile, not onto the screen.
## 6. Write the deployment profile
Write the profile from the **scan output + derived values** (projectType + clientOrg
come from the repos-json — no flags for them). Read the tracker connection back from the
filled `.env.<env>`:
```bash
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/gen-profile.mjs" \
--repos-json .local-env/repos.json \
--tracker-json .local-env/tracker.json \
--runtime-mode plugin \
--tracker jira --tracker-base-url https://acme.atlassian.net --tracker-project ABC \
--client-vcs github \
--operator <derived> --contribution-mode <derived> \
--upstream-account <forkAccount, only if fork> \
--github-token-kind <derived: github.tokenKind> --github-fork-capable <derived: github.forkCapable> \
--self-diagnostics <true|false, from step 0b> \
# NB: NO --feedback-mode (item 4) — it stays at the `ask` default; the interview never sets it. \
--vcs-auth <derived: client host's auth — github⇒gh-cli|pat, azure-repos⇒az-login|pat> --print
# Azure Boards + Azure Repos:
# ... --tracker azure --azure-org acme --azure-project Web --client-vcs azure-repos --vcs-auth pat ...
```
`--repos-json` supplies `projectType`, `vcs.clientOrg`, and the client/platform repo
lists in one shot. **`--tracker-json .local-env/tracker.json`** (from step 4b) bakes the
tracker status model (per-type states, `roleStates`, `apiBase`, `projectId`, `ticketKeyFormat`,
`crossLinkToken`) and flips `transitionPolicy` to `auto` — WITHOUT it the Azure profile has an
empty `roleStates` and `/qa-fix` falls back to asking on every transition. **`--runtime-mode
plugin`** is REQUIRED when running from an installed plugin: gen-profile's env auto-detect
(`$CLAUDE_PLUGIN_ROOT`) is unreliable because that var is not exported into the node process,
so pass the flag explicitly — this sets `runtime.mode=plugin` / `helpersRunnable=false` so the
interactive commands read the baked profile facts. (Developing directly in the agent repo?
Omit the flag → `agent-project`.) Explicit `--project-type` / `--client-org` flags still
override, but in the normal flow you do **not** pass them — the scan is authoritative.
If step 4 surfaced a storefront/theme repo the scan couldn't classify, hand-edit
`project-profile.json` `repos.client` to add it (or fix any miscategorised entry).
**No plugin path is baked into the profile — it is resolved at runtime.** gen-profile does
NOT write `paths.pluginRoot` (a versioned cache dir `…/vc-fix/<version>` would freeze to a
stale/deleted version on the next upgrade). Instead every command's `node "$pluginRoot/skills/…"`
resolves `$pluginRoot` = the ACTIVE (enabled) install at call time via `claude plugin list --json`
(fallback: a highest-semver scan of `~/.claude/plugins/cache/*/vc-fix/`). See
[`knowledge/execution/plugin-root.md`](../../knowledge/execution/plugin-root.md). Result:
**no re-run of `/project-init` after an upgrade**, no version-stamped path, no stale link.
## 7. Generate `.mcp.json`
```bash
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/gen-mcp.mjs" --tracker jira --client-vcs github \
--with context7 --warm-cache # add postman,figma,devtools as needed
```
`--warm-cache` pre-fetches the pinned npx packages into the npm cache (with an IPv4-first
DNS hint) so the first MCP start doesn't pay a registry round-trip — the #220 startup-timeout
guard. Best-effort + timeboxed; drop it to skip the network step.
Enables playwright×3 + github + the tracker's MCP (atlassian for Jira; azure-mcp
for Azure). A `--with` extra whose API key is an OPTIONAL placeholder the operator left blank
(`postman`, `context7`) is **defined but NOT enabled** — the same "blank ⇒ stays disabled"
contract `scaffold-secrets.mjs` states, so passing `--with context7` unconditionally (as above) is
safe: it enables the server only once the key exists. gen-mcp prints one info line per dormant
extra; filling the key in `.env.local` and re-running enables it. **Remind the operator to restart
the MCP servers** (reload the IDE) for the new config to take effect.
**Secrets never land in `.mcp.json` (VCST-5774).** A resolved credential is written there as a
`${VAR}` **indirection**; the VALUE goes into `.claude/settings.local.json` `env`, which Claude
Code applies to every session and its subprocesses — and that is what feeds `${VAR}` expansion in
`.mcp.json` `headers`/`env`. So `.mcp.json` is safe to read, diff and share. Before writing
anything, gen-mcp adds `.mcp.json`, `.claude/settings.local.json`, `.env.local`, `.env.*.local`,
`project-profile.json` and `.vc-fix/` to the project's `.gitignore` (creating it if absent), so
the file never exists un-ignored even briefly. There is **no `gh auth token` fallback** — with no
PAT the placeholder stays unresolved and the github server drops the header and uses interactive
OAuth, rather than persisting the operator's CLI session to disk.
Two consequences worth stating to the operator:
- The value being in settings `env` means it is exported to **every** session subprocess, not just
the one MCP client. That is the mechanism, not an accident — treat `.claude/settings.local.json`
as a secret file.
- **`--inline-secrets`** restores the legacy literal substitution into `.mcp.json` (and then writes
no second copy to settings). Opt-in only, for a host that cannot apply settings `env`. It makes
`.mcp.json` itself a secret; the §8 hygiene rows will say so.
## 8. Verify access — full readiness checkup
Run with the env selected (so per-env creds resolve). **Pass `FORCE_COLOR=1`** —
the table colours the Status column, but the script auto-disables colour when stdout
is not a TTY and the harness runs this through a pipe, so without `FORCE_COLOR=1` the
operator only ever sees a plain table.
```bash
FORCE_COLOR=1 TEST_ENV=<env> node "$CLAUDE_PLUGIN_ROOT/skills/project-init/verify-access.mjs"
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/assert-profile.mjs"
```
Prints a bordered readiness table + a **READY / NOT READY** verdict for `/qa-fix`.
Checks (PASS / FAIL / WARN / SKIP): deployment profile · **plugin root** (`claude plugin list --json`
resolves the active vc-fix install and `skills/qa-fix-routing/ado.mjs` is present under it;
WARN if the `claude` CLI isn't on PATH) · core env vars · storefront
URL · admin/platform URL · **admin login** (real `POST {BACK_URL}/connect/token`
password grant) · storefront user login (the REAL store-scoped OAuth grant → PASS/FAIL, not a
"verify manually" WARN) · tracker token (Jira `GET /myself`
or a **real ADO org probe**) · **GitHub fix token / gh session** (validates the token and
its permission on the upstream — shared with the derive block via `probe-lib.mjs`, so
what verify reports and what the profile stored can't drift) · **client repos** (for a
client deployment, each `repos.client` entry is probed for reach on its own host —
GitHub `permissions.push` or an Azure Repos `_apis/git/repositories/<repo>` JSON hit — so a
dead client-repo token surfaces here, not at Gate 2; native platform ⇒ SKIP) · **Storefront
upstream ref** (a client frontend fork's `upstreamRef` resolves in `vc-frontend` — PASS; missing /
`upstreamRefResolved:false` / doesn't resolve — WARN, since Gate 1b reconstructs/asks; no fork ⇒
SKIP).
**WRITE-capability probes (the axes `/qa-fix` must write to).** A PAT/session with only
READ scope passes every read probe above but 401s the moment `/qa-fix` transitions a
ticket or pushes a branch — the gap found live in a client deployment (ADO PAT: `whoami` /
`get-workitem` / `list-refs` 200, but transition / comment / push 401). So the table also
probes write, **without mutating anything** — it sends a deliberately-invalid write request
and reads the status split (`401/403` = no write scope; `400/409/422` = scope present, the
bad request was rejected at validation; anything else ⇒ "write scope unverified"):
| Row | Axis | Probe (non-mutating) | Verdict |
|-----|------|----------------------|---------|
| **Azure Boards write (transition)** | tracker (Azure Boards only) | `PATCH _apis/wit/workitems/<known id>` with an invalid JSON-Patch body | present ⇒ PASS · no scope / ACL-403 / inconclusive ⇒ **WARN** |
| **GitHub auth (direct/fork PR)** | platform upstream (GitHub) | existing `permissions.push` on the upstream repo | fork (own fork) ⇒ PASS · no push on the proxy repo / unreadable perm ⇒ **WARN** |
| **Client repo `<name>`** | client code host | GitHub `permissions.push`; Azure Repos `POST _apis/git/repositories/<repo>/pushes` with an empty body | reachable + write ⇒ PASS · reachable but no write / inconclusive ⇒ **WARN** |
A missing write scope is a **WARN, never a NOT-READY FAIL** (`probe-lib.writeProbeSeverity`):
refusing to finish onboarding over a token that reaches the resource but lacks one write scope is
too heavy — the WARN explains exactly what to grant, the operator grants it before running
`/qa-fix`, and `/qa-fix` Gate 1 re-checks the ACTUAL routed repo anyway. The `To resolve:` block
names the exact scopes — **Azure: Work Items (Read & Write) + Code (Read & Write) + Pull Request
(contribute); GitHub: repo/PR write** — and never prints the token. Only **fundamentals** FAIL →
NOT READY (missing core env, unreachable `FRONT_URL`/`BACK_URL`, bad admin login, a totally
absent/rejected credential that can't even reach the resource, or an **exposed credential** — see
the hygiene rows below); **WARN** is non-blocking; **SKIP** means a feature isn't configured.
**Secret-hygiene rows (VCST-5774).** Two rows — `Secret hygiene — .mcp.json` and
`Secret hygiene — .claude/settings.local.json` — audit the files §7 generated. Both are checked,
because the redesign MOVED the credential: guarding only `.mcp.json` would leave the value's new
home unguarded. The walk covers the whole server def — `headers`, `env`, `args[]`, `url`, nested
bags — because the generator substitutes placeholders at every leaf. It reports KEY PATHS only; a
credential value never reaches the table or the telemetry.
**Two nets, two confidence levels**, and that split is what keeps the row honest:
- **CERTAIN** — the known-token-shape matcher shared with `hooks/redact.mjs` (`ghp_`, `glpat-`,
`AKIA`, JWT, …). A hit is a credential whatever key it hides under, so it may block readiness.
- **SUSPECTED** — a credential-shaped key (or a `--api-key`-style flag, or a URL with inline
credentials) whose value is opaque. This is the net that catches a token type nobody has invented
yet, but it cannot tell a secret from a filename — so it **only ever WARNs**. That ceiling is
what lets the key vocabulary stay wide instead of being narrowed until real names fall out of it.
Obvious non-credentials (a path, a filename, a bare number, a short enum word) are filtered out.
Grading is by **actual exposure**:
| Situation | Row |
|---|---|
| **Certain** credential **and** the file is committable (not gitignored, **or already tracked**) | **FAIL** — blocks readiness, names the fix and says to rotate |
| **Suspected** credential, however exposed | **WARN** — never blocks; says it may equally be a filename or an id |
| Certain literal in `.mcp.json`, file not committable | WARN — re-run `/project-init`, or keep it via `--inline-secrets` |
| Certain credential in `settings.local.json`, file not committable | **PASS** — that is the target state |
| Clean but committable, or unparsable JSON | WARN |
| Outside a git repo | never FAIL on ignore-state — there is nothing to commit to |
An **already-tracked** file is the case to read carefully: `git check-ignore` reports a tracked
path as NOT ignored, and adding a `.gitignore` rule does not untrack it — so that FAIL tells the
operator to `git rm --cached <file>` and commit, then rotate. Re-running `/project-init` would not
fix it.
**Session auth is really probed, not assumed.** For an `az-login` / `gh-cli` axis the
check mints a real token and hits the org / upstream — an active session that is not a
member (or is in a different tenant) is reported **FAIL**, not a false PASS. On a session
FAIL, do NOT make the operator hand-craft commands — run **`ensure-session.mjs`** (it
auto-discovers the ADO org's tenant and drives `az login --tenant <guid>` /
`gh auth login --web`, then re-probes):
```bash
TEST_ENV=<env> node "$CLAUDE_PLUGIN_ROOT/skills/project-init/ensure-session.mjs" # establish
TEST_ENV=<env> node "$CLAUDE_PLUGIN_ROOT/skills/project-init/ensure-session.mjs" --check # probe only
```
`az login` / `gh auth login` are **blocking** — **launch `ensure-session.mjs` in the
background** and let the operator complete the single browser consent, then re-run
verify-access. For a fully non-interactive setup use a PAT instead.
**After the readiness table it also prints the MCP-servers section** — `OK` (local) ·
`AUTHORIZED` (token/key/az present) · `NEEDS OAUTH` (atlassian / figma) · `NO KEY`
(optional server, key unset).
**Surface the result in your reply — do NOT leave it only in the script's stdout.**
The table is tool output, which many clients collapse or hide. **Restate the readiness
table AND the MCP-server status as Markdown tables in your chat message**, mapping each
row to the surface it proves — **front** = `FRONT_URL` (+ storefront user login),
**back** = `BACK_URL` + admin login, **Jira/tracker** = tracker token, **Git** = GitHub
token + gh CLI — and end with the `N PASS · N FAIL · N WARN · N SKIP` line and the
READY / NOT READY verdict.
## 9. Done
Present this as an explicit, labelled **Step 9** in your reply (the operator tracks the
pipeline by step number). Summarise what was written (profile path, tracker, VCS, repo
counts, derived projectType / contributionMode), list the remaining **manual** actions
(reload the IDE for `.mcp.json`; any interactive OAuth still pending, e.g. Atlassian),
and point the operator at the first run:
```
/qa-fix VCST-1234 # (or your tracker's key) — now routes to the right repo
```
**Signal completion (self-diagnostics).** This is now emitted **automatically** — `verify-access.mjs`
(Step 8, the last script every path runs) fires the terminal-step marker itself, best-effort, so the
clean self-check line prints reliably without depending on you remembering a trailing command. You only
need to run it **manually as a fallback** if the run ended BEFORE reaching Step 8 verify-access (e.g. a
hard precondition bail at Step 0), so the collector still knows the run finished:
```bash
node "$CLAUDE_PLUGIN_ROOT/hooks/session-telemetry.mjs" complete --skill "project-init"
```
It tells the vc-fix self-diagnostics collector this run has finished, so its one-line
clean/health status prints **exactly once** (right after this step) instead of on every
interview / file-fill / verify pause. Emit it even when the run ended early (a NOT READY
precondition bail, or "nothing to do") — a correct early exit is still a completed run. (See
[`knowledge/diagnostics/skill-expectations.md`](../../knowledge/diagnostics/skill-expectations.md)
§Signal completion.)
---
## `--add-env` — add another environment to an already-onboarded project
`/project-init --add-env` is for a deployment that is ALREADY onboarded (a
`project-profile.json` exists) and you want to point the plugin at **another deployment
target** — a second QA env, staging, a customer's second storefront — that shares the
**same project topology**.
**Why this is short (and touches almost nothing).** The deployment profile is
**env-agnostic**: `tracker.kind`, `vcs.clientHost`, `repos.*`, `contributionMode`, the
tracker role model — all describe the PROJECT (where bugs are filed, where code lives, which
repos exist), not a single deployment. An *environment* is only a **URL set + its access
creds** (`.env.<name>` + the `_<ENV>`-suffixed passwords in `.env.local`), selected at
runtime by `TEST_ENV`. So `--add-env`:
- **does NOT** re-run the interview, re-scan repos (`discover-repos`), re-derive
(`derive-context`), rewrite `project-profile.json`, or regenerate `.mcp.json` — all
env-agnostic and already done;
- **only** scaffolds the new env's two files (reusing the profile's tracker/host), then
verifies the new env.
> A different **tracker or code host** is a different *project*, not an environment — those
> live in `project-profile.json`. For that, run a fresh full `/project-init` in its own
> project directory; don't try to bend `--add-env` to it.
### Step A — precondition + read the project topology (no questions)
1. `project-profile.json` must exist in the cwd. If it doesn't → this project was never
onboarded: run the **full interview** (`/project-init`, no args) instead. Confirm the cwd
(§0a) — everything still lands here.
2. Read `tracker.kind` and `vcs.clientHost` from `project-profile.json` (e.g.
`node -e "const p=require('./project-profile.json');console.log(p.tracker.kind,p.vcs.clientHost)"`,
or just read the file). **Do NOT ask them again** — they are project-level and already
decided. These become the `--tracker` / `--client-vcs` flags below.
### Step B — ask the env name, then the existing-env guard
- **Ask the env name** as a **plain chat** question (like §2a — *"what should the new
environment be named?"*; not `AskUserQuestion`/widget, and no positional arg). The operator
replies; normalise to `[a-z0-9_]+` (e.g. `Staging 2` → `staging_2`) and tell them what you used.
- **Existing-env guard** (§2c): check `.env.<name>` first. Found → `AskUserQuestion` to
**reuse** it (scaffolding is add-only, never clobbers filled values) or **pick a new name**
(re-ask the plain-chat name question + re-check). Not found → proceed.
### Step C — scaffold the new env's two files, then pause
Same scaffolders as the full flow (§3), reusing the tracker/host read from the profile:
```bash
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/scaffold-env.mjs" --env <new> --tracker <kind> --client-vcs <host> --print
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/scaffold-secrets.mjs" --env <new> --tracker <kind> --client-vcs <host> --print
```
- `scaffold-env` writes `.env.<new>` — the new deployment's URLs/identifiers (`FRONT_URL`,
`BACK_URL`, store/culture, `ADO_ORG`/`ADO_PROJECT` if Azure). All empty for the operator to fill.
It also re-emits the **project-level** connection placeholders (`JIRA_BASE_URL`/`JIRA_PROJECT_KEY`,
or `ADO_ORG`/`ADO_PROJECT`, or `CLIENT_REPO_ORG`). **Those are safe to leave empty in `.env.<new>`
— they are already in `project-profile.json`, and every runtime consumer falls back to the profile
(`process.env.X || profile.…`).** Only the per-env **URLs** genuinely need filling; don't ask the
operator to re-enter tracker/org values they already gave at onboarding.
- `scaffold-secrets` adds ONLY the `_<ENV>`-suffixed per-env app passwords
(`ADMIN_PASSWORD_<NEW>` / `USER_PASSWORD_<NEW>`) to the shared `.env.local`. **Cross-env
tokens** (`GITHUB_FIX_BUGS_TOKEN` / `JIRA_API_TOKEN` / `ADO_PAT`) are single-instance and
already present from the first env — idempotent add-only leaves them untouched. (A new env
reachable only with a *different* tracker/GitHub token is the "different project" case above.)
Tell the operator both were touched and what genuinely needs filling — **the new env's URLs + its
admin/user passwords** (the empty project-level tracker/org placeholders are covered by the profile) —
then **pause** with the unmistakable waiting banner (§3c) — no tool calls after it.
### Step D — verify the new environment
```bash
FORCE_COLOR=1 TEST_ENV=<new> node "$CLAUDE_PLUGIN_ROOT/skills/project-init/verify-access.mjs"
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/assert-profile.mjs"
```
Run with `TEST_ENV=<new>` so the per-env creds resolve. This confirms the new env's URLs +
**real admin login** + the (cross-env) tracker/GitHub tokens all work for this target.
`project-profile.json` and `.mcp.json` are **unchanged** (env-agnostic). **Restate the
readiness table** in your reply (§8). Then present **Done**: `TEST_ENV=<new>` (per run, or via
your shell) selects the new environment for `/qa-fix`, `/qa-bug`, `/qa-verify-fix`.
Completion is signalled **automatically** by `verify-access.mjs` above (it fires the terminal-step
marker itself). Only if this path ended before running verify-access, run it manually as a fallback:
`node "$CLAUDE_PLUGIN_ROOT/hooks/session-telemetry.mjs" complete --skill "project-init"`.
---
## `--check` — reconcile an existing profile, then verify
`/project-init --check` is for a deployment that is ALREADY onboarded — most often
**after a plugin upgrade**. The schema (`PROFILE_DEFAULTS` in `scripts/lib/project-profile.mjs`)
evolves between versions — fields are **added** (e.g. `selfDiagnostics`), **removed**
(e.g. the old baked `pluginRoot`), or need a fresh **rescan** (repos / tracker role model
drift) — but the on-disk `project-profile.json` was written once and goes stale. `--check`
brings it back in line with the current schema, then runs the readiness table. It does
**NOT** re-run the interview.
### Step A — reconcile the profile (deterministic, dry-run first)
```bash
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/reconcile-profile.mjs" --print
```
`reconcile-profile.mjs` diffs the profile against the current schema and prints a JSON
report — no writes, no scans, no questions of its own:
- **`added`** — schema fields missing from the profile that have a SAFE default (filled
automatically on `--write`). Discriminated blocks (`tracker.azure`, `vcs.azure`) follow
the same rule `gen-profile` uses, so a Jira+GitHub profile is never re-grown an `azure:{}`.
- **`removed`** — obsolete fields no longer in the schema (pruned on `--write`). **Open
maps** (`tracker.azure.roleStates`/`stateMap`/`workItemTypes`) and **arrays** (`repos.*`)
are kept wholesale — their contents are data, not schema.
- **`pending`** — fields whose value is the OPERATOR's decision (a privacy / behaviour
opt-in, e.g. `selfDiagnostics`). Each carries its own `question` + `options`; **never**
auto-filled.
- **`rescan`** — fields that should be re-derived from a live scan (repos / tracker).
`status:"no-profile"` ⇒ never onboarded — run the **full interview** instead, don't reconcile.
`status:"current"` ⇒ nothing stale — skip to **Step C**.
### Step B — resolve decisions + rescans, then write
- For each **`pending`** entry, ask the operator with **`AskUserQuestion`**, using the
entry's own `question` + `options` (make the `default` value the Recommended option).
- For each **`rescan`** entry, re-run the matching discovery (`discover-repos` /
`discover-tracker`) and fold the result back via `gen-profile --merge` (§ Re-running). An Azure
profile with an **empty `tracker.fields`** is reported here as `{ path: "tracker.fields", source:
"discover-tracker" }` (VCST-5582 E5) — an upgraded install whose original scan predates the
`$expand=all` fix re-derives the bug field contract on `--check`, no full re-onboarding needed.
- Then apply everything in one write, one `--set path=value` per resolved decision:
```bash
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/reconcile-profile.mjs" --write \
--set selfDiagnostics=<true|false>
```
`selfDiagnostics` is the CAPTURE opt-in and the ONLY self-diagnostics decision `--check` asks.
`feedback.mode` (VCST-5509) — the DELIVERY consent — is **no longer a managed "ask" field**
(item 4): it fills as a safe default (`ask`) and is never surfaced as a pending decision. To
change it, hand-edit `project-profile.json` or re-run `gen-profile --feedback-mode <auto|ask|off>`.
`ask` = the per-finding binary offer at delivery time; `auto` = file the Issue directly; `off` =
nothing ever leaves the machine.
`--write` applies the structural adds/removes plus any `--set` decisions. Unresolved
`pending`/`rescan` fields are left **absent** — safe, because a missing field reads as its
safe default (no `selfDiagnostics` ⇒ capture stays **OFF** — the opt-in gate; set
`selfDiagnostics:true` to opt in; no `feedback` ⇒ delivery falls back to `ask` — a per-finding
offer + confirm, never an unattended send) — and stay in the report so a later `--check` can finish
them. Reconciling is **idempotent**: once done, the report is `current`.
If a `--write` would remove **≥5 fields** it returns `status:"needs-force"` and writes
nothing — that many removals usually means a schema mismatch (reconciling against a leaner
schema than the one that wrote the profile), not stale fields. **Review the `removed` list**;
only if the removals are genuinely intended, re-run with `--force`.
### Step C — normalize the env file, then verify access
First re-run the §3d normalizer — an env file edited by hand since onboarding can have
re-acquired a trailing slash or a quoted value, and every probe below would then fail for an
invisible reason:
```bash
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/normalize-env.mjs" --env <env>
```
Then the readiness table (§8), so a stale token / URL / login surfaces too:
```bash
FORCE_COLOR=1 TEST_ENV=<env> node "$CLAUDE_PLUGIN_ROOT/skills/project-init/verify-access.mjs"
node "$CLAUDE_PLUGIN_ROOT/skills/project-init/assert-profile.mjs"
```
**Restate all three** — the reconciliation summary (added / removed / decided), any value the
normalizer corrected, and the readiness table — in your reply. A normalizer exit 1 (unfilled
placeholder / missing scheme) is a **STOP**: report it and skip the readiness table, which would
only produce misleading failures.
Completion is signalled **automatically** by `verify-access.mjs` above (it fires the terminal-step
marker itself). Only if this path ended before running verify-access, run it manually as a fallback:
`node "$CLAUDE_PLUGIN_ROOT/hooks/session-telemetry.mjs" complete --skill "project-init"`.
---
## How this makes `/qa-fix` route correctly
Once the profile exists, the fix pipeline uses it automatically:
- **Ownership** — `repoOwnership(repo)` (from `repo-router.ts`) returns `client`
for repos under the client org / in `repos.client`, else `platform`.
- **Client bug** → fix + PR into the client repo (GitHub or Azure Repos per
`vcs.clientHost`).
- **Platform bug, fixable** (passes the G0/G1 gates) → fix + PR to the VirtoCommerce
repo (a **fork-PR** when `contributionMode=fork`, a direct PR for a Virto engineer
with write access).
- **Platform bug, NOT fixable** (multi-module / complex / breaking) → **file a
GitHub issue** on the open upstream repo (no PR).
- **Tracker** — comments + status transitions go to Jira or Azure Boards per
`tracker.kind`.
Client-code containment (`.claude/rules/quality-gates.md` §2a) is enforced regardless:
client code never leaves the client project. Gate ladder is unchanged. Never auto-merges.
## Re-running
Safe to re-run. `gen-profile`/`gen-mcp` overwrite their outputs; `--merge` layers
onto the existing profile. To reconfigure one dimension, re-run `gen-profile`
with just those flags + `--merge`. To re-derive after a token/session change, re-run
`discover-repos` + `discover-tracker` + `derive-context` and regenerate the profile.
**Existing profiles** written before the verified-`upstreamRef` change are safe to leave —
Gate 1b reconstructs a resolvable ref on the fly. A `/project-init` re-run (or just re-running
`discover-repos`) refreshes them to a concrete tag; alternatively an operator can hand-fix
`repos.client[].upstreamRef` from the bare line label to that line's base tag (e.g. `2.49` →
`2.49.0`).
## Scripts
| Script | Role |
|--------|------|
| `scaffold-env.mjs` | write a commented `.env.<env>` **template** (non-secret URL/identifier/tracker placeholders + what/example comments); topology-driven, idempotent. **Calls `lib/gitignore.mjs` `ensureProjectIgnores()` BEFORE it writes**, so the file it is about to create is already covered (VCST-5774). |
| `scaffold-secrets.mjs` | write a commented `.env.local` **template** (secret placeholders + what/why/where per secret); topology-driven, idempotent. **Calls `lib/gitignore.mjs` `ensureProjectIgnores()` BEFORE it writes**, so the file it is about to create is already covered (VCST-5774). |
| `lib/gitignore.mjs` | the ONE list of what onboarding generates that must never be committed, plus the append-only writer. Shared because FOUR scripts create such files at four different steps and each must protect its own **before** creating it — while only `gen-mcp` (§7) wrote the block, `.env.local` (§3b) sat unignored through the operator's fill-in pause and through any run that aborted before §7. Idempotent, so four calls cost one read and produce one block |
| `normalize-env.mjs` | **run on the operator's "done" (§3d) and in `--check` Step C** — normalize the hand-filled `.env.<env>` IN PLACE (quotes / padding / **all** trailing slashes; a pasted `dev.azure.com/<org>` → the bare slug) and validate it: exit 1 on an unfilled placeholder, a URL with no `http(s)://`, or a path-shaped `ADO_ORG`/`ADO_PROJECT`; WARN on a path component in `FRONT_URL`/`BACK_URL`. Prints every fix. Rules come from `scaffold-env.mjs` `CATALOG` (`type` + no-`def`), never a private copy |
| `write-env.mjs` | (non-interactive helper) write `.env.<env>` / `.env.local` from a JSON answer object on STDIN when values ARE known programmatically; idempotent |
| `discover-repos.mjs` | ALWAYS-run scan: Platform API modules → client/platform split, client-host scan for the storefront repo, and **derives projectType + clientOrg**; bakes per-repo `contribution`/`integrationBranch`/`toolchain`/`localVerify`; emits `{ projectType, clientOrg, client, platform }` |
| `discover-tracker.mjs` | **Azure-only** scan of work-item types → per-type `states` + a `role→state` map (`roleStates`), plus `apiBase`/`projectId`/`ticketKeyFormat`/`crossLinkToken`; emits `.local-env/tracker.json` for `gen-profile --tracker-json`. Jira: format facts only (transitions discovered live). Enables `/qa-fix`'s silent role-based transitions |
| `derive-context.mjs` | the **derive block**: reads the filled env + sessions, probes the upstream permission, emits JSON — auth actually present per axis, contributionMode, forkAccount, operator |
| `probe-lib.mjs` | shared side-effect-free probes (GitHub-upstream permission, ADO tenant/auth) used by BOTH `verify-access` and `derive-context` so their results can't drift |
| `gen-profile.mjs` | write/merge `project-profile.json` from the repos-json (projectType/clientOrg/repos) + derived flags (operator/contributionMode/upstream-account/vcs-auth) + tracker connection |
| `reconcile-profile.mjs` | **`--check` migration**: diff an existing profile against the current `PROFILE_DEFAULTS` schema → JSON report of `added` (safe-default) / `removed` (obsolete, open-maps+arrays preserved) / `pending` (operator-decision fields with `question`+`options`, e.g. `selfDiagnostics`) / `rescan` (re-derive live). Deterministic, dry-run by default; `--write` applies structural changes + `--set path=value` decisions. Idempotent. Mirrors `gen-profile`'s `tracker.azure`/`vcs.azure` discriminated pruning |
| `gen-mcp.mjs` | write `.mcp.json` (OS-aware) into the project + enable servers for the tracker/VCS. **Credentials are written as `${VAR}` refs; the VALUE goes to `.claude/settings.local.json` `env`. `--inline-secrets` opts back into a literal (and `--inline-secrets false` correctly turns it OFF). Refuses to write at all when the settings file is unparsable or already git-tracked — a rewrite would delete the operator's keys, and no `.gitignore` rule can untrack a tracked file (VCST-5774).** Playwright servers are flags-only (`--browser` / `--isolated` / `--viewport-size` / `--output-dir`) — no config files; only `playwright-chrome` is enabled by default |
| `lib/paths.mjs` | shared path helper — `outputRoot()` (`VC_FIX_HOME` \|\| `process.cwd()`, where generated state goes) + `pluginRoot()` (`CLAUDE_PLUGIN_ROOT` \|\| resolved from `import.meta.url`, used by a running script to find its own read-only plugin assets). Keeps every generator writing to the project and reading templates from the plugin. (Commands resolve their launch path via `claude plugin list --json` — see `knowledge/execution/plugin-root.md`.) |
| `verify-access.mjs` | full `/qa-fix` readiness table + verdict, incl. the two **secret-hygiene** rows that audit `.mcp.json` + `.claude/settings.local.json` for an exposed credential (graded by committability: tracked/not-ignored ⇒ FAIL); prints an untruncated "To resolve" block (incl. an auto-discovered `az login --tenant <guid>`). Also **reports every non-PASS row as self-diagnostics telemetry** (`lib/diag-obs.mjs` → the collector's `obs` subcommand) — the table used to be rendered and discarded, so a WARN the operator could plainly read was invisible to `/vc-self-check` and the run self-diagnosed "no plugin issues detected" (VCST-5582 H). Exit code is unchanged: 0 unless a hard FAIL |
| `assert-profile.mjs` | asserts the **SHAPE** of the profile just written and records each degradation as a `degraded_artifact` observation: empty `tracker.fields` (⇒ `/qa-bug` sends "unverified defaults"), `roleStatesComplete:false`, unmapped required fields, empty `repos.client` on a client project, an unresolved storefront `upstreamRef`, `githubForkCapable != "yes"` while the upstream path is needed. Complements `verify-access` (which probes ACCESS): a scan can return empty with no HTTP error, and what `/qa-fix` reads at runtime is the persisted shape. Read-only, **always exits 0** — a diagnostic, not a second readiness gate |
| `ensure-session.mjs` | establish the browser-login sessions WITHOUT hand-crafted commands: auto-discovers the ADO org tenant and drives `az login --tenant <guid>` / `gh auth login --web`; `--check` probes only. Run in the background (the login blocks on the browser). |
> The interview asks only **env name · tracker · code host** + an auth preference
> per axis. The **self-diagnostics capture opt-in** (`selfDiagnostics`) is asked FIRST, as
> step 0b, and the flag is written immediately on Yes — the ONE self-diagnostics decision
> `--check` also surfaces. The **`feedback.mode` upstream-delivery consent is NOT asked** (item 4):
> it stays at its `ask` default and the delivery flow asks once, per finding, at send time.
> Both env files are scaffolded as commented templates the operator fills;
> the scan (step 4) derives projectType + clientOrg, the derive block (step 5) derives
> contribution mode + fork account + operator, and verify-access (step 8) confirms.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!