Client dispatch: In Codex, read
codex-runtimeandconfigure/codex.md, execute that native procedure, and do not continue into the Claude procedure below.
Interaction rule: Every question, confirmation, or approval directed at the user — anywhere in this skill, including error recovery — MUST be asked with the
AskUserQuestiontool. Never ask in plain text. If an instruction says "ask the user" or "confirm", that meansAskUserQuestion.
Task
Help the user set up this project for the cenci plugin.
Parse $ARGUMENTS
All of $ARGUMENTS is optional user context (additional instructions or focus areas).
If empty, proceed normally with defaults.
Existing Config Detection
Resolve project configuration in this order:
.cenci/config.jsonis canonical. If present, read it asexistingConfig.- Otherwise read
.claude/config.jsonaslegacyConfig. Treat this as a migration run. - If neither exists, set
existingConfigto null.
On migration, preserve every unknown key from legacyConfig; new writes go only to
.cenci/config.json. If both files exist, recursively merge them into one
migrationBase with canonical values winning, then overlay managed answers. Use
"${CLAUDE_PLUGIN_ROOT}/scripts/migrate-project-core.sh" <root> to preview the exact
config/guidance diff:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/migrate-project-core.sh" <root> [--apply]
Rerun it with --apply only after approval, with <root> set to <worktree-path> (see
Create Worktree below — the worktree exists by the time any --apply can run). Never
rewrite or delete .claude/config.json; it is a read-only compatibility artifact.
When existingConfig is present, tell the user before starting questions:
"Found existing configuration. Each question will show your current setting as the default — select it to keep it unchanged."
Reconfiguration runs are how a project picks up configure features added since its config was written. The plugin's SessionStart hook (hooks/scripts/check-config-staleness.sh) compares existingConfig.configVersion against the installed flow plugin version and nudges the user to re-run this skill when the config is stale or unstamped; completing this run refreshes the stamp (step 6) and clears the nudge.
Create Worktree
/cenci:configure writes files into the repo (.cenci/config.json, AGENTS.md/CLAUDE.md, .mcp.json, .lsp.json, .gitignore, .claudeignore, .github/workflows/ci.yml, .cenci/Dockerfile, .lazyboards.yml, .codex/agents/) and updates .claude/settings.json. Like every other change in this repo, these ship as a PR — configure never writes directly to the main worktree (see cenci:worktrees and docs/git-workflow.md).
Create the worktree now, before any file is written (including a migration --apply above) and before the detection/question steps below, since none of them depend on it existing yet:
- Verify at least one commit exists:
git rev-parse HEAD 2>/dev/null. If the repository has no commits, create an initial commit as two standalone Bash calls (never compound&&— shell-rules: every segment of a compound is evaluated independently by the approval system):git add -A, thengit commit -m "chore: initial commit" --allow-empty. - Derive a slug:
initwhenexistingConfigis null (first-ever configure run),updatefor a plain reconfiguration, or a short kebab-case description of the user's focus when$ARGUMENTSnames one (e.g. "refresh MCP servers" →mcp-refresh). - Create the worktree:
git worktree add .worktrees/configure-<slug> -b chore/configure-<slug> main. If that branch/directory name is already taken by an unrelated prior run, append-2,-3, etc. until it's free.
From this point on, <worktree-path> is .worktrees/configure-<slug>. Every file this skill reads or writes below — .cenci/config.json, AGENTS.md, CLAUDE.md, .mcp.json, .lsp.json, .gitignore, .claudeignore, .claude/settings.json, .github/workflows/, .cenci/Dockerfile, .lazyboards.yml, .codex/, designs/ — and every "the repo root" / "the project root" reference in the steps below resolves against <worktree-path>, never the main checkout. Use absolute paths rooted at <worktree-path> for every Write/Edit; verify the CWD before Bash commands rather than relying on a single cd persisting across calls. gh label create / gh issue calls (step 3c) are GitHub API calls, not file writes, and run the same regardless of worktree.
Scripted Detection
The deterministic detections this skill needs (platform, container, package manager, MCP/LSP/dind/Playwright catalog triggers, plugin version) are scripted, not re-derived ad hoc. Run the bundled detector once, as its own Bash call (per cenci:shell-rules), from the repository root of the main checkout — it only reads, nothing is written:
bash "${CLAUDE_PLUGIN_ROOT}/skills/configure/scripts/detect-project.sh"
(the detector is scripts/detect-project.sh inside this skill; its tests live next to it). Parse its stdout as a JSON object and keep it as detection for the rest of this run:
detection.pluginVersion— installed flow plugin version; stamped into config asconfigVersion(step 6)detection.platform—{name, owner, repo}parsed from the git remote, ornulldetection.inContainer— cenci-sandbox container detection resultdetection.packageManager—pnpm/yarn/npmfrom lockfiles, ornulldetection.mcpServers— MCP catalog triggers found in the dependency scan (e.g.angular,primeng)detection.lspServers— LSP catalog triggers found (e.g.typescript,gopls)detection.dindDetected— Testcontainers/Docker-SDK trigger found (question 9b default)detection.playwrightTest—@playwright/testin rootdevDependenciesdetection.warnings— non-fatal detection notes; relay them to the user as informational messages
If the script exits non-zero or its output is not valid JSON, fall back to the manual fallback procedure kept in each consuming section below — a detection either comes from detection or from its manual fallback, never silently skipped. The script only detects: every question, catalog decision, and user choice stays in this skill.
Platform Detection
Use detection.platform: when non-null it carries the platform name plus extracted owner/repo; when null, fall back to manual questions.
Manual fallback (only if Scripted Detection failed): run git remote get-url origin 2>/dev/null and parse the result:
| Remote URL pattern | Platform | Extracted values |
|---|---|---|
git@github.com:OWNER/REPO.git |
github | owner, repo |
https://github.com/OWNER/REPO.git |
github | owner, repo |
| Unrecognized / no remote | — | Fall back to manual questions |
Strip trailing .git suffix from repo names.
If user context was provided, use it to steer the configuration (e.g., skip certain questions, pre-select options, focus on specific areas).
Container Detection
cenci runs inside sandbox's cenci-sandbox container with --dangerously-skip-permissions. The container is the security boundary — there is no host profile. Claude Code's own host sandbox stays disabled, and permissions.allow/deny are kept only as defense-in-depth for plain claude runs inside the container (e.g. cenci open --shell).
Use detection.inContainer (the detector checks CENCI_SANDBOX, then /.dockerenv).
Manual fallback (only if Scripted Detection failed) — detect the container (stop at the first match; run each check as its own Bash call, per cenci:shell-rules — never compound them):
CENCI_SANDBOXenv var (works for both Docker and Podman): runtest "${CENCI_SANDBOX:-}" = "1"as its own Bash call. Exit 0 → in container.- Docker fallback: run
test -f /.dockerenvas its own Bash call. Exit 0 → in container.
This detection is a non-blocking advisory — it never gates configuration and never uses AskUserQuestion:
- In container: emit an informational message: "Detected the cenci-sandbox container — the container is the security boundary. Claude Code's host sandbox stays disabled." Then continue normally.
- Not in container: emit an advisory and continue anyway: "cenci is designed to run inside the cenci-sandbox container (the security boundary). You appear to be running on the host — continuing, but running outside the container is unsupported." Proceed with the same container-shaped output.
Default values from existing config: When existingConfig is not null, each question below MUST present the existing value as the pre-selected default (list it first, marked "(current)"). The user can accept with one click or change it. New fields not in existingConfig (e.g., lspServers when upgrading from a pre-LSP config) have no default and are asked normally.
AskUserQuestion cannot pre-check multiSelect options — its options only carry label/description/preview, no "selected by default" field. So for the two multi-select questions (5. MCP Servers, 6. LSP Servers), "pre-select" cannot mean a pre-ticked checkbox — re-asking the full multi-select on every reconfigure would force re-clicking every already-enabled server one at a time. Instead, gate behind a single Keep/Change confirmation first (see Keep-or-change gate under each question below): when existingConfig already has a value for that field, ask one Yes/No question summarizing the current selections; only "No — let me change them" drops into the full multi-select. This is a single click to keep everything unchanged, instead of one click per server.
| Question | existingConfig field |
Default when field exists |
|---|---|---|
| 1. Tech stack | stack |
Pre-fill with formatted stack |
| 2. Project structure | isMonorepo |
Pre-select based on existing value |
| 3. Branching strategy | branchPattern |
Pre-fill with existing pattern |
| 5. MCP Servers | mcpServers |
Keep-or-change gate (see below); only "change" enters the multi-select, pre-sorted with enabled servers first |
| 5b. Pencil design | pencil |
Pre-select based on pencil.enabled; if field absent, ask normally |
| 6. LSP Servers | lspServers |
Keep-or-change gate (see below); only "change" enters the multi-select, pre-sorted with enabled servers first |
| 7. Auto-compact | autoCompactDisabled |
Pre-select Yes/No |
| 7b. Pin subagents to 200K | pinSubagents200K |
Pre-select Yes/No |
| 8. CI/CD pipeline | cicd |
Pre-select Yes/No based on cicd.enabled |
| 9. Sandbox Dockerfile | sandbox |
Pre-select Yes/No based on sandbox.enabled |
| 9b. Nested Docker (dind) | sandbox |
Pre-select Yes/No based on existingConfig.sandbox.dind (else Yes when a Testcontainers/Docker-SDK trigger was detected, No otherwise) |
| 9c. Azure CLI | sandbox |
Pre-select Yes/No based on existingConfig.sandbox.azure; if field absent, ask normally |
| 10. Board config (lazyboards) | lazyboards |
Only asked when no .lazyboards.yml exists; if one exists, suggest missing actions or skip (see Board Config) |
Ask these questions one at a time using the AskUserQuestion tool:
Tech stack: "What's your tech stack?" (capture for stack pack selection)
- Backend framework + version
- Frontend framework + version
- Test frameworks
- Any other key technologies
Project structure: "Is this a monorepo or single project?"
If monorepo, continue with Steps 2a and 2b below. Otherwise skip to question 3.
Step 2a — Auto-detect projects
Scan the repo for project directories using these strategies (try all, deduplicate):
- Node workspaces: Read
package.jsonworkspacesfield andpnpm-workspace.yamlpackagesfield - Lerna: Read
lerna.jsonpackagesfield - .NET solutions: Find
*.slnfiles and parseProject(...)references for.csprojpaths - Convention directories: Scan
packages/*/,apps/*/,projects/*/,src/*/for directories containingpackage.jsonor*.csproj
For each discovered project, detect:
- Path: relative directory (e.g.,
packages/api) - Stack: auto-detect from dependencies:
@angular/coreinpackage.json→ Angularreactinpackage.json→ Reactnextinpackage.json→ Next.jsvueinpackage.json→ Vue.csprojwithMicrosoft.NET.Sdk.Web→ .NET API.csprojwithMicrosoft.NET.Sdk→ .NET library- Fallback: read
package.jsonnameor directory name
- Build command: auto-detect (
dotnet buildfor .NET,npm run buildfor Node, etc.) - Test command: auto-detect (
dotnet testfor .NET,npm testfor Node, etc.) - Lint command: derive from the Stack-to-CI mapping table's Lint column (see the table under question 8 below); omit
lintCommandentirely when the detected stack has no Lint row (e.g.markdown-shell,docker-shell)
Present discovered projects for confirmation using AskUserQuestion: "Found these projects in the monorepo:
<path>—<path>— ... Are these correct? (You can add or remove projects)"
Step 2b — Per-project details
For each confirmed project, ask for a one-line description using AskUserQuestion: "Provide a short description for each project:"
<path>(): ___
Generate a slug for each project from its directory name (e.g., packages/api → api, apps/web-client → web-client).
- Branching strategy: "What's your branch naming convention?"
- Default suggestion:
feature/<id>-<description>
- Default suggestion:
(There is no question 4 — sandboxing is not asked. The cenci-sandbox container is the security boundary and Claude Code's host sandbox is always disabled; numbering of the remaining questions is kept for stability.)
Dependency Detection
The dependency scan is scripted: detection.mcpServers carries the MCP catalog triggers found, and detection.dindDetected is the dindDetected value question 9b uses below.
Manual fallback (only if Scripted Detection failed) — scan the project for framework dependencies:
- If
package.jsonexists in the repo root, readdependenciesanddevDependencies - If
.csprojfiles exist, readPackageReferenceentries - Store the detected package names for matching against the MCP catalog below
- Testcontainers/Docker-SDK detection (for question 9b below — nested Docker/dind): scan for these triggers and store the result as
dindDetected(trueif any match,falseotherwise):- npm:
testcontainers,@testcontainers/*, ordockerodeindependencies/devDependencies - NuGet:
Testcontainers*orDocker.DotNetin.csprojPackageReferenceentries - Python:
testcontainersin the dependency list (requirements.txt,pyproject.toml) — dep list only, no source scan - Go:
github.com/testcontainers/testcontainers-goingo.mod
- npm:
MCP Server Catalog
| Trigger Package | Server Name | Command | Args | Env Vars | Scope |
|---|---|---|---|---|---|
| (always available) | context7 | npx |
["-y", "@upstash/context7-mcp@3.2.5"] |
CONTEXT7_API_KEY |
project |
| (Pencil editor open) | pencil | (connected via editor) | — | — | editor |
@angular/core |
angular | npx |
["-y", "@angular/cli", "mcp"] |
— | project |
primeng |
primeng | npx |
["-y", "@primeng/mcp"] |
— | project |
Scope:
- project: Add to the project's root
.mcp.json. - editor: Provided by the Pencil editor over its own connection — nothing is written to
.mcp.json; enablement is driven bypencil.enabledin.cenci/config.json.
LSP Server Catalog
| Trigger | Server Name | Command | Args | Extension Map | Install Command |
|---|---|---|---|---|---|
typescript or @angular/core or react or next or vue in package.json |
typescript | typescript-language-server |
["--stdio"] |
{".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact"} |
npm install -g typescript-language-server typescript |
*.py files or pyproject.toml or requirements.txt |
pyright | pyright-langserver |
["--stdio"] |
{".py": "python"} |
pip install pyright or npm install -g pyright |
Cargo.toml present |
rust-analyzer | rust-analyzer |
[] |
{".rs": "rust"} |
See rust-analyzer docs |
*.csproj present |
csharp-ls | csharp-ls |
[] |
{".cs": "csharp"} |
dotnet tool install --global csharp-ls |
go.mod present |
gopls | gopls |
["serve"] |
{".go": "go"} |
go install golang.org/x/tools/gopls@latest |
MCP Servers: Match detected dependencies against the MCP catalog above. Build a suggestion list:
- Always include Context7 (general-purpose docs lookup)
- Add each MCP whose trigger package was found in the dependency scan
Keep-or-change gate: if
existingConfig.mcpServersis present, do not jump straight into the multi-select —AskUserQuestioncan't pre-check boxes, so re-asking it fresh would force re-clicking every already-enabled server. Instead present the current state and ask a plain Yes/No:"Current MCP servers:
<name>✓ enabled,<name>✗ disabled, … . Keep these settings?" Options: "Yes — keep current settings (Recommended)", "No — let me change them"- Yes: carry
existingConfig.mcpServersforward unchanged, skip the multi-select below entirely. - No: continue to the multi-select below.
If
existingConfig.mcpServersis absent (first-ever configure run, or a newly-detected MCP not previously offered), skip the gate and ask the multi-select directly.Present using AskUserQuestion with multiSelect=true (sort currently-enabled servers first when
existingConfig.mcpServersexists, so they're easiest to re-tick):"Based on your project dependencies, these MCP servers can enhance AI assistance. Which would you like to enable?"
Options (only show those whose trigger was detected, plus Context7 always):
- "Context7 — Live documentation lookup for any library (requires free API key from context7.com/dashboard)"
- "Angular — Official Angular AI tutor, best practices, and documentation search"
- "PrimeNG — Component documentation, props, events, theming, and examples"
If only Context7 is available (no framework-specific MCPs detected), still present it: "Do you want to enable Context7 for live documentation lookup? (Requires a free API key from context7.com/dashboard)"
Pencil Design Workflows
Condition: Only ask question 5b when a frontend framework is detected in the stack from question 1. Frontend frameworks include: Angular, React, Next.js, Vue, Svelte, or any UI framework.
If no frontend framework is detected, skip this section entirely (do not set pencil in config).
5b. Pencil design workflows: Present using AskUserQuestion:
"Your project includes <detected-frontend-framework>. Do you want to enable Pencil design workflows?
(Visual designs, auto-generated design specs with component mappings and tokens.
Requires the Pencil editor.)"
Options: "Yes — enable Pencil design workflows", "No — skip"
If Yes AND monorepo with multiple frontend projects (i.e., isMonorepo is true and more than one project in the projects array has a frontend stack):
"Should frontend projects share one design file, or have separate design files?"
Options: "Shared (single designs/ at repo root)", "Separate (per-project designs/)"
- Shared:
pencil.designPath = "designs/",pencil.shared = true - Separate: each frontend project entry in the
projectsarray gets its owndesignPath(e.g.,"<project-path>/designs/")
If Yes AND single project (or monorepo with only one frontend project):
pencil.designPath = "designs/",pencil.sharedis omitted
After the user confirms Yes (regardless of monorepo choice), detect pen interactive support:
Run pen interactive --help 2>/dev/null and check both the exit code and the
output: require exit 0 and the output containing --app. The --app flag is
specific to the npm CLI's (@pen.dev/cli) interactive subcommand help — a
desktop-app-installed pen symlink instead launches the GUI application on any
unrecognized argument, including --help, and can also exit 0 without ever
printing --app. Checking the exit code alone would misdetect that desktop-symlink
host as cli-app mode; the combined check is how cli-app vs. editor mode is
auto-detected without that false positive.
- Both conditions hold → Write
pencil.mode: "cli-app"to the config. Inform the user: "Pencilinteractivemode detected. Design skills will usepen interactiveto communicate with the Pencil editor — this is more token-efficient than the MCP server. For maximum token savings, you can disable the Pencil MCP server in your editor settings (Pencil → Preferences → MCP Server). cenci uses the CLI directly and does not need the MCP server." - Either condition fails, or the command is not found → Write
pencil.mode: "editor"to the config. Inform the user: "Pencilinteractivemode not available. Design skills will use the Pencil MCP server (requires the MCP connection to be active in your editor). For better token efficiency, install thepencommand (npm install -g @pen.dev/cli, or from within the Pencil app: File → Installpencommand into PATH) and re-run/cenci:configure— this switches tocli-appmode which avoids loading MCP tool schemas into every conversation."
Sandbox note (no extra config value needed): inside the cenci sandbox neither the
desktop editor nor its MCP server is reachable, so with either mode above the
pipeline's availability probe (implement's Design Context Loading) falls back at
runtime to pen interactive headless mode — the CLI's own editor engine, no
GUI — using the pen binary baked into the sandbox image by
sandbox/fragments/pencil.dockerfile (included when pencil.enabled is true; see
question 9). Headless auth comes from the host's ~/.pencil/session-cli.json
(created by pen login, staged into the container automatically) or a
PEN_CLI_KEY set in the host environment (forwarded per agent session, never baked
into the image). This headless fallback covers the pipeline's reads only:
design itself is host-only and refuses to run in-container — /cenci:design
fails fast with host-session guidance (see design/SKILL.md Phase 0.5).
cenci run design {number} --no-sandbox is how the generated board dispatches
it (see the D action below).
Playwright CLI Setup
Condition: Only ask this when a frontend framework is detected in the stack from question 1 AND detection.playwrightTest is true (manual fallback: @playwright/test found in root devDependencies).
If both conditions are met, present using AskUserQuestion:
"Your project uses Playwright Test. Do you want to set up Playwright CLI (@playwright/cli) for interactive browser automation during development?
(Screenshots, snapshots, form filling, network inspection — more token-efficient than Chrome MCP for agents.)"
Options: "Yes — install and configure Playwright CLI", "No — skip"
If Yes:
- Check if
playwright-cliis already installed:which playwright-cli 2>/dev/null- Found → "✓
playwright-clifound at<path>" - Not found → "Run
npm i -g @playwright/clito install, thenplaywright-cli install --skillsto set up agent skills."
- Found → "✓
- Set
playwrightCli: truein.cenci/config.json
If No: Set playwrightCli: false in .cenci/config.json (or omit the field)
If the conditions are not met, skip this section entirely (do not set playwrightCli in config).
LSP Detection
detection.lspServers carries the detected LSP catalog triggers directly.
Manual fallback (only if Scripted Detection failed) — reuse the dependency detection results from earlier and add file-type detection to match against the LSP Server Catalog:
typescript,@angular/core,react,next, orvueinpackage.jsondependencies → typescript*.pyfiles present, orpyproject.toml, orrequirements.txt→ pyrightCargo.tomlpresent → rust-analyzer*.csprojpresent → csharp-lsgo.modpresent → gopls
If no LSP servers are detected, skip question 6 entirely.
LSP Servers: If two or more LSP servers were detected above:
Keep-or-change gate: if
existingConfig.lspServersis present, do not jump straight into the multi-select —AskUserQuestioncan't pre-check boxes, so re-asking it fresh would force re-clicking every already-enabled server. Instead present the current state and ask a plain Yes/No:"Current LSP servers:
<name>✓ enabled,<name>✗ disabled, … . Keep these settings?" Options: "Yes — keep current settings (Recommended)", "No — let me change them"- Yes: carry
existingConfig.lspServersforward unchanged, skip the multi-select below entirely. - No: continue to the multi-select below.
If
existingConfig.lspServersis absent (first-ever configure run, or a newly-detected LSP server not previously offered), skip the gate and ask the multi-select directly.Present using AskUserQuestion with multiSelect=true (sort currently-enabled servers first when
existingConfig.lspServersexists, so they're easiest to re-tick):"LSP servers provide real-time diagnostics (type errors, unused variables, dead code) during implementation. Based on your project, which would you like to enable?"
Options (only show those whose trigger was detected):
- "typescript — TypeScript/JavaScript type checking and diagnostics"
- "pyright — Python type checking and diagnostics"
- "rust-analyzer — Rust type checking and diagnostics"
- "csharp-ls — C# type checking and diagnostics"
- "gopls — Go type checking and diagnostics"
If exactly one LSP server was detected, ask a plain Yes/No instead — a multi-select checkbox with a single option has no sensible "none" choice:
"Your project uses
<detected-language>. Do you want to enable<server-name>for real-time diagnostics (type errors, unused variables, dead code) during implementation?"Options: "Yes — enable
<server-name>", "No — skip"- Yes: carry
Binary Verification
For each LSP server the user selected, verify the binary is installed:
which <command>
- Found: Confirm with a checkmark: "✓
<command>found at<path>" - Not found: Warn with install command: "⚠
<command>not found. Install with:<install-command>. Server will activate once installed."
Include the server in .lsp.json regardless — it activates once the binary is installed.
- Disable auto-compact: "Do you want to disable Claude Code's auto-compact feature?
Auto-compact compresses conversation history as the context window fills,
which can lose important context during long sessions. (Recommended: Yes — disable it)"
- Default: Yes
- If Yes: set
"autoCompactEnabled": falsein~/.claude/settings.jsonusing jq (see step 5c for the exact jq/mv sequence, and its Write-then-mv variant when the file doesn't exist yet) - If No: remove the
autoCompactEnabledkey from~/.claude/settings.json(if present) - Either way, also remove any
CLAUDE_AUTOCOMPACT_PCT_OVERRIDEkey from theenvobject in~/.claude/settings.json. Earlier versions of this skill set it to"1"believing that meant manual-only compaction; Claude Code interprets it as "compact once 1% of the context window is used", so any leftover value causes constant compaction and must be purged.
7b. Pin subagents to 200K context: "Do you want to pin cenci's subagents to a 200K-context model? cenci delegates reviews to subagents, and on 1M-context sessions that delegation can be gated — every subagent inherits the session's 1M flag but not its extra-usage entitlement, so reviews fail with 'Usage credits required for 1M context' (Claude Code bug #51060). Pinning subagents to Sonnet 200K keeps reviews working while your main session keeps its 1M context. (Recommended: Yes if you run a 1M-context session; No otherwise — see the tiering caveat below)"
- Default: Yes when the session plausibly runs a 1M model; No otherwise
- If Yes: merge
{"env": {"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-5"}}into~/.claude/settings.jsonusing jq (see step 5c-bis for the exact jq/mv sequence, and its Write-then-mv variant when the file doesn't exist yet). This runs allTasksubagents on Sonnet 200K regardless of the main session model. (Pin Sonnet, not Opus — Opus is auto-upgraded to 1M on Max/Team/Enterprise plans and would re-trigger the gate.) - If No: remove the
CLAUDE_CODE_SUBAGENT_MODELkey from theenvobject in~/.claude/settings.json(if present) - Tiering caveat (state regardless of answer): the pin overrides every agent's
model:frontmatter — cenci's model tiering (opus refiner/planner/security-reviewer, haiku context-gatherer/structure-analyzer/lessons-collector) is flattened onto the pinned model while it is set. On a standard 200K session the 1M gate never fires, so answer No there to keep the tiering active. - Caveat (state regardless of answer): this only affects new sessions — restart after configuring. If subagent reviews still fail with the 1M gate even after pinning (the pin didn't strip
[1m]), run/model sonnetfor the current session, which always yields 200K.
CI/CD pipeline: "Do you want to generate a CI/CD pipeline?"
- Options: "Yes — generate a CI workflow", "No — skip"
- Default: No
- Platform: GitHub Actions
Conflict check: If the user selects Yes, scan for existing CI configuration:
- Use the
Globtool with patterns.github/workflows/*.ymland.github/workflows/*.yaml
If any files are found, present them and ask:
- Single file: "Found existing CI configuration at
<path>. What would you like to do?" - Multiple files: "Found existing CI workflows:\n -
<path1>\n -<path2>\n ...\nWhat would you like to do?"
Options: "Overwrite — generate
ci.yml(existing files are not deleted)", "Skip — keep existing files", "Show existing — display the current file contents"- If Skip: still record
cicdin config.json, don't write the file - If Show existing: read and display each file, then re-ask Overwrite/Skip
Stack-to-CI mapping: Use the detected stack from question 1 to select the appropriate lint, build, and test commands:
Stack Lint Build Test dotnet*dotnet format --verify-no-changesdotnet build --no-restoredotnet test --no-build --collect:"XPlat Code Coverage"gogolangci-lint run ./...go build ./...go test ./... -coverprofile=coverage.outpythonruff check .(none) pytest --cov=. --cov-report=xmlrustcargo clippy -- -D warningscargo buildcargo testangular*ng lintng build --configuration productionng test --watch=false --code-coveragereact/nextnpx eslint .npm run buildnpm test -- --coverage --watchAll=falsevuenpx eslint .npm run buildnpm run test:unit -- --coveragePackage manager detection (for Node-based stacks): use
detection.packageManager. Manual fallback:pnpm-lock.yaml→ pnpmyarn.lock→ yarnpackage-lock.json→ npm- .NET → NuGet cache on
**/*.csproj - Go → built-in cache in
actions/setup-go@v5
Version pinning:
- Node:
engines.nodefrompackage.json, fallback"20" - .NET: extract from stack token (
dotnet10→"10.x") - Go: first line of
go.mod - Python:
python-requiresfrompyproject.toml, fallback"3.12" - Rust:
rust-toolchain.tomlor"stable"
Sandbox Dockerfile: "Generate a sandbox Dockerfile for this repo? (Tailors the sandbox image to your stack; committed so your team shares it.)"
- Options: "Yes — generate
.cenci/Dockerfile", "No — skip" - Default: Yes
Mandatory agent runtime: Always include
node.dockerfile, regardless of the detected project stack. Claude Code and Codex are npm-distributed launchers installed by the isolated shared-volume updater, so generated images need Node.js but must not bake either agent CLI.Stack-to-fragment mapping: In addition to the mandatory Node runtime fragment, use the detected stack from question 1 (or, for monorepos, the union of every
projects[].stack.frameworkvalue) to select whichsandbox/fragments/*.dockerfileblocks to include:Detected stack Fragment dotnet*.NET SDK block (version from the token) angular*/react/next/vue/nodeNode block angular*/react/next/vuePlaywright block (Chromium, for verify-uiscreenshot capture)goGo block pythonPython + uv block rustRust block (none — config-selected) Docker block, when sandbox.dind: true(question 9b)(none — config-selected) Azure block, when sandbox.azure: true(question 9c)Playwright is scoped to the frontend-framework tokens, not plain
node— a Node-based backend/API project gets the Node block without paying Chromium's image-size cost for a visual-verification step it will never run.Pencil fragment (config-selected, not stack-token-selected): when the config being written enables Pencil design workflows (
pencil.enabled: true, question 5b), additionally includesandbox/fragments/pencil.dockerfile— it bakes the@pen.dev/clinpm package into the image soimplement/verify-uican run their design reads in the CLI's headless mode inside the sandbox, where the host's desktop editor and its MCP server are unreachable. Like Playwright, it is scoped to repos that actually need it rather than every Node image.Docker fragment (config-selected, not stack-token-selected): when the config being written enables nested Docker (
sandbox.dind: true, question 9b), additionally includesandbox/fragments/docker.dockerfile— it installs the Docker CLI, thedocker-ceengine andcontainerd.iosoentrypoint.shcan start an inner daemon undersysbox-runc. This block used to live inDockerfile.baseand so shipped in every image; since #831 it is selected only for repos that actually run nested Docker. Adind: truerepo whose.cenci/Dockerfileomits this fragment builds an image with nodockerd— the sandbox still boots and stays usable, but nested Docker is unavailable and~/.cenci-dockerd-startup-errorexplains why. So when Q9 is answered No (no.cenci/Dockerfilegenerated) and Q9b is answered Yes, tell the user their repo will use the sharedcenci-sandbox:latestmonolith, which carries the Docker block already — no action needed.Azure fragment (config-selected, not stack-token-selected): when the config being written enables the Azure CLI (
sandbox.azure: true, question 9c), additionally includesandbox/fragments/azure.dockerfile— it installsazfrom Microsoft's apt repo so an agent can check real command syntax withaz <group> <cmd> --helpinstead of guessing it. There is no stack token for this: an Azure repo can be written in any language, so the opt-in is the only signal. Unlike the Docker fragment there is no monolith fallback —cenci-sandbox:latestcarries noazeither — so a repo that answers Yes to Q9c but No to Q9 gets no Azure CLI at all; tell the user Q9 must also be Yes for the CLI to reach their sandbox.Monorepo: take the union of all
projects[].stack.frameworkvalues, deduplicated — e.g. a repo with a Go API project and a React web client project selects both the Go and Node fragments. Node is still emitted only once because the mandatory runtime set and stack-selected set are deduplicated.A stack token that matches no row above (e.g.
markdown-shell,docker-shell) contributes no additional project fragment. This is not an error — the generated Dockerfile still contains the mandatory Node runtime fragment..NET version substitution (the only row with a version-from-token adjustment):
sandbox/fragments/dotnet.dockerfileships withARG DOTNET_SDK_VERSION=10.0.100as its own default. When including this fragment, replace that default's version with<major>.0.100, where<major>is extracted from the stack token using the same extraction as the CI mapping's version-pinning table above (dotnet10→10) — e.g. adotnet8stack writesARG DOTNET_SDK_VERSION=8.0.100. Monorepo tie-break: when multiple projects map to the dotnet fragment with different major versions (e.g. one project ondotnet8, another ondotnet10), use the highest major version found across all matching projects. If no major version can be extracted from the token, leave the fragment's own default (10.0.100) unmodified — and add an inline comment immediately after theARG DOTNET_SDK_VERSIONline noting the version could not be auto-detected from the stack token and the fragment's default was used instead, e.g.# .NET version could not be auto-detected from the stack token — using fragment default. See sandbox/README.md to pin manually.(mirrors the unresolved-baseVersioncomment pattern in the baseVersion resolution above). The other fragments (node, playwright, go, python, rust, pencil, docker, azure) are included verbatim with their ownARGdefaults unmodified — every fragmentARG(includingDOTNET_SDK_VERSIONandBASE_VERSION) remains overridable at build time via--build-arg, so an unmodified default is never a hard lock-in.Sync obligation:
sandbox/fragments/*.dockerfileis the source of truth for these blocks; the mapping table above mirrors their content and existence, not their byte contents (generation reads the fragment files directly — see step 5e). If a fragment is added, removed, or renamed, this table needs a matching manual update. Low risk in practice — both live in the same monorepo and are maintained together — but currently unenforced by tooling.Trust / security note:
.cenci/Dockerfileis committed to the repo, so it is reviewed like any other file in the PR that adds or changes it. It only runsdocker buildsteps assembled fromsandbox/fragments/*.dockerfile— no arbitrary runtime hooks execute during configure or during the build it produces.- Options: "Yes — generate
9b. Nested Docker (dind): "Does this repo need Docker inside the sandbox — Testcontainers, docker build/docker run in tests, or a Docker SDK client?"
- Options: "Yes — enable nested Docker (
sandbox.dind)", "No — skip" - Default: Yes when
dindDetectedistrue(a Testcontainers/Docker-SDK trigger was found above), otherwise No - This question is independent of question 9 (Sandbox Dockerfile) — ask it regardless of how Q9 was answered, and record its answer separately (see the
sandbox.dindschema note below). - If Yes: inform the user that nested Docker requires the host to have Docker (not Podman) with the
sysbox-runccontainer runtime registered —cenci doctorreports this — and point atsandbox/README.md#nested-docker-sysboxfor host install instructions per distro. - If Yes and question 9 generated a
.cenci/Dockerfile: that Dockerfile must includesandbox/fragments/docker.dockerfile(see the mapping table's Docker rule under question 9). Because Q9b is asked after Q9, generate the Dockerfile only once this answer is known — or regenerate it here — so adind: truerepo never ends up with
…(truncated)