${paste.content}diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index b19c87b8d..d5385a381 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -11,7 +11,7 @@
{
"name": "ecc",
"source": "./",
- "description": "Harness-native ECC operator layer - 68 agents, 292 skills, 94 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses",
+ "description": "Harness-native ECC operator layer - 68 agents, 293 skills, 94 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses",
"version": "2.2.2",
"author": {
"name": "Affaan Mustafa",
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 072edddfe..8871e37d2 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "ecc",
"version": "2.2.2",
- "description": "Harness-native ECC plugin for engineering teams - 68 agents, 292 skills, 94 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses",
+ "description": "Harness-native ECC plugin for engineering teams - 68 agents, 293 skills, 94 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses",
"author": {
"name": "Affaan Mustafa",
"url": "https://x.com/affaanmustafa"
diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md
index 7b9b2d670..d4984a2b1 100644
--- a/.gemini/GEMINI.md
+++ b/.gemini/GEMINI.md
@@ -4,7 +4,7 @@ This file provides Gemini CLI with the baseline ECC workflow, review standards,
## Overview
-Everything Claude Code (ECC) is a cross-harness coding system with 36 specialized agents, 142 skills, and 68 commands.
+Everything Claude Code (ECC) is a cross-harness coding system with 68 specialized agents, 293 skills, and 94 commands.
Gemini support is currently focused on a strong project-local instruction layer via `.gemini/GEMINI.md`, plus the shared MCP catalog and package-manager setup assets shipped by the installer.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cb0c531fd..84c854920 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,7 +20,7 @@ jobs:
test:
name: Test (${{ matrix.os }}, Node ${{ matrix.node }}, ${{ matrix.pm }})
runs-on: ${{ matrix.os }}
- timeout-minutes: 20
+ timeout-minutes: 30
strategy:
fail-fast: false
@@ -239,6 +239,44 @@ jobs:
run: node scripts/ci/validate-no-personal-paths.js
continue-on-error: false
+ pi-core:
+ name: Pi Core Profile
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Setup Node.js
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: '20.x'
+
+ # Rebuild runs frontmatter validation and the pi/core safety checks
+ # (no callable endpoints, no runtime downloads, no secrets, no absolute
+ # home paths, no symlinks, no duplicate skill names); the diff check
+ # proves the committed profile is up to date.
+ - name: Rebuild pi/core and verify it is up to date
+ run: |
+ node scripts/build-pi-core.js
+ git diff --exit-code pi/core
+
+ - name: Install Pi coding agent CLI
+ run: npm install --global --ignore-scripts --no-audit --no-fund @mariozechner/pi-coding-agent@0.73.1
+
+ # --extension only loads a package's pi.extensions entries; pi/core is a
+ # skills+prompts-only package, so the equivalent --skill/--prompt-template
+ # resource flags are used. The load-test script additionally asserts via
+ # RPC that every curated command actually loaded.
+ - name: Offline load test with the Pi CLI
+ run: |
+ PI_OFFLINE=1 pi --offline --mode rpc --no-session --no-context-files --no-extensions \
+ --skill pi/core/skills --prompt-template pi/core/commands /dev/null
+ node scripts/ci/pi-core-load-test.js
+
python-tests:
name: Python Lint, Type Check & Test
runs-on: ubuntu-latest
diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml
index 0a0db2f7f..ca724b328 100644
--- a/.github/workflows/maintenance.yml
+++ b/.github/workflows/maintenance.yml
@@ -48,7 +48,7 @@ jobs:
name: Stale Issues/PRs
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
+ - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
with:
stale-issue-message: 'This issue is stale due to inactivity.'
stale-pr-message: 'This PR is stale due to inactivity.'
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index bdad0d483..f71cf38a5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -5,6 +5,8 @@ on:
tags: ['v*']
permissions:
+ actions: read
+ checks: read
contents: read
jobs:
@@ -12,6 +14,8 @@ jobs:
name: Verify Release
runs-on: ubuntu-latest
outputs:
+ release_sha: ${{ steps.release_gate.outputs.release_sha }}
+ tag_object_sha: ${{ steps.release_gate.outputs.tag_object_sha }}
already_published: ${{ steps.npm_publish_state.outputs.already_published }}
dist_tag: ${{ steps.npm_publish_state.outputs.dist_tag }}
publish_tag: ${{ steps.npm_publish_state.outputs.publish_tag }}
@@ -43,6 +47,13 @@ jobs:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
+ - name: Verify signed tag and exact-SHA CI gates
+ id: release_gate
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ RELEASE_TAG: ${{ github.ref_name }}
+ run: RELEASE_SHA="$(git rev-parse HEAD)" node scripts/ci/verify-release-gates.js
+
- name: Install dependencies
run: npm ci --ignore-scripts
@@ -73,6 +84,22 @@ jobs:
exit 1
fi
+ - name: Verify VERSION matches tag
+ env:
+ TAG_NAME: ${{ github.ref_name }}
+ run: |
+ TAG_VERSION="${TAG_NAME#v}"
+ FILE_VERSION=$(tr -d '[:space:]' < VERSION)
+ if [ "$TAG_VERSION" != "$FILE_VERSION" ]; then
+ echo "::error::Tag version ($TAG_VERSION) does not match VERSION ($FILE_VERSION)"
+ exit 1
+ fi
+
+ # pi/core/package.json derives its version from VERSION, so a current
+ # profile also proves pi/core is in sync with the tag.
+ - name: Verify pi/core profile is current
+ run: node scripts/build-pi-core.js --check
+
- name: Verify release metadata stays in sync
run: node tests/plugin-manifest.test.js
@@ -194,6 +221,31 @@ jobs:
ECC_RELEASE_SHA256: ${{ needs.verify.outputs.package_sha256 }}
run: node -e "const crypto = require('crypto'); const fs = require('fs'); const file = process.env.ECC_RELEASE_PACKAGE; const expected = process.env.ECC_RELEASE_SHA256; if (!/^ecc-universal-[0-9A-Za-z.+-]+\.tgz$/.test(file || '')) throw new Error('Unexpected packed filename'); if (!/^[a-f0-9]{64}$/.test(expected || '')) throw new Error('Invalid packed SHA-256'); const archives = fs.readdirSync('.').filter(name => name.endsWith('.tgz')); if (archives.length !== 1 || archives[0] !== file) throw new Error('Expected exactly one downloaded archive'); const actual = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); if (actual !== expected) throw new Error('Downloaded publish artifact SHA-256 mismatch')"
+ - name: Bind gate source to triggering commit
+ env:
+ EVENT_SHA: ${{ github.sha }}
+ VERIFIED_RELEASE_SHA: ${{ needs.verify.outputs.release_sha }}
+ run: node -e "const actual = process.env.EVENT_SHA; const expected = process.env.VERIFIED_RELEASE_SHA; if (typeof actual !== 'string' || actual.length !== 40 || !/^[a-f0-9]{40}$/.test(actual) || expected !== actual) throw new Error('Verified release differs from triggering commit')"
+
+ - name: Checkout verified gate source
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ github.sha }}
+ path: release-gate-source
+ persist-credentials: false
+ sparse-checkout: scripts/ci/verify-release-gates.js
+ sparse-checkout-cone-mode: false
+
+ # This read-only API check uses the existing publish job token. It is a
+ # snapshot; preventing subsequent tag movement requires protected tags.
+ - name: Recheck verified tag before publish
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ RELEASE_TAG: ${{ github.ref_name }}
+ RELEASE_SHA: ${{ needs.verify.outputs.release_sha }}
+ RELEASE_TAG_OBJECT_SHA: ${{ needs.verify.outputs.tag_object_sha }}
+ run: node release-gate-source/scripts/ci/verify-release-gates.js --tag-only
+
- name: Publish npm package
if: needs.verify.outputs.already_published != 'true'
env:
diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml
index b038b1b8c..74ab4c003 100644
--- a/.github/workflows/reusable-release.yml
+++ b/.github/workflows/reusable-release.yml
@@ -18,6 +18,8 @@ on:
type: string
permissions:
+ actions: read
+ checks: read
contents: read
jobs:
@@ -25,6 +27,8 @@ jobs:
name: Verify Release
runs-on: ubuntu-latest
outputs:
+ release_sha: ${{ steps.release_gate.outputs.release_sha }}
+ tag_object_sha: ${{ steps.release_gate.outputs.tag_object_sha }}
already_published: ${{ steps.npm_publish_state.outputs.already_published }}
dist_tag: ${{ steps.npm_publish_state.outputs.dist_tag }}
publish_tag: ${{ steps.npm_publish_state.outputs.publish_tag }}
@@ -57,6 +61,13 @@ jobs:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
+ - name: Verify signed tag and exact-SHA CI gates
+ id: release_gate
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ RELEASE_TAG: ${{ inputs.tag }}
+ run: RELEASE_SHA="$(git rev-parse HEAD)" node scripts/ci/verify-release-gates.js
+
- name: Install dependencies
run: npm ci --ignore-scripts
@@ -208,6 +219,25 @@ jobs:
ECC_RELEASE_SHA256: ${{ needs.verify.outputs.package_sha256 }}
run: node -e "const crypto = require('crypto'); const fs = require('fs'); const file = process.env.ECC_RELEASE_PACKAGE; const expected = process.env.ECC_RELEASE_SHA256; if (!/^ecc-universal-[0-9A-Za-z.+-]+\.tgz$/.test(file || '')) throw new Error('Unexpected packed filename'); if (!/^[a-f0-9]{64}$/.test(expected || '')) throw new Error('Invalid packed SHA-256'); const archives = fs.readdirSync('.').filter(name => name.endsWith('.tgz')); if (archives.length !== 1 || archives[0] !== file) throw new Error('Expected exactly one downloaded archive'); const actual = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); if (actual !== expected) throw new Error('Downloaded publish artifact SHA-256 mismatch')"
+ - name: Checkout verified gate source
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ ref: ${{ needs.verify.outputs.release_sha }}
+ path: release-gate-source
+ persist-credentials: false
+ sparse-checkout: scripts/ci/verify-release-gates.js
+ sparse-checkout-cone-mode: false
+
+ # This read-only API check uses the existing publish job token. It is a
+ # snapshot; preventing subsequent tag movement requires protected tags.
+ - name: Recheck verified tag before publish
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ RELEASE_TAG: ${{ inputs.tag }}
+ RELEASE_SHA: ${{ needs.verify.outputs.release_sha }}
+ RELEASE_TAG_OBJECT_SHA: ${{ needs.verify.outputs.tag_object_sha }}
+ run: node release-gate-source/scripts/ci/verify-release-gates.js --tag-only
+
- name: Publish npm package
if: needs.verify.outputs.already_published != 'true'
env:
diff --git a/.mcp.json b/.mcp.json
index 045baea18..9860a4dbe 100644
--- a/.mcp.json
+++ b/.mcp.json
@@ -2,7 +2,7 @@
"mcpServers": {
"chrome-devtools": {
"command": "npx",
- "args": ["-y", "chrome-devtools-mcp@latest"]
+ "args": ["-y", "chrome-devtools-mcp@1.10.1"]
}
}
}
diff --git a/AGENTS.md b/AGENTS.md
index 17330b848..2107af90a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — Agent Instructions
-This is a **production-ready AI coding plugin** providing 68 specialized agents, 292 skills, 94 commands, and automated hook workflows for software development.
+This is a **production-ready AI coding plugin** providing 68 specialized agents, 293 skills, 94 commands, and automated hook workflows for software development.
**Version:** 2.2.2
@@ -48,6 +48,42 @@ This is a **production-ready AI coding plugin** providing 68 specialized agents,
| mle-reviewer | Production ML pipeline review | ML pipelines, evals, serving, monitoring, rollback |
| rag-pipeline-reviewer | RAG pipeline review | Retrieval quality, chunking, reranking, RAGAS evaluation coverage |
| typescript-reviewer | TypeScript/JavaScript code review | TypeScript/JavaScript projects |
+| react-reviewer | React/JSX code review | React component and hook changes |
+| react-build-resolver | React/Vite/Next.js/webpack build errors | React build failures |
+| vue-reviewer | Vue.js Composition API and reactivity review | Vue component, Pinia, and Nuxt changes |
+| swift-reviewer | Swift/iOS code review | Swift code changes |
+| swift-build-resolver | Swift/Xcode/SPM build errors | Swift build failures |
+| flutter-reviewer | Flutter/Dart widget and state review | Flutter app changes |
+| dart-build-resolver | Dart/Flutter build and pub dependency errors | Flutter compilation failures |
+| csharp-reviewer | C#/.NET async patterns, nullability, security | All C# code changes |
+| fastapi-reviewer | FastAPI async correctness, Pydantic, OpenAPI | FastAPI endpoint and schema changes |
+| php-reviewer | PHP/PSR-12, Eloquent, security review | PHP code changes |
+| harmonyos-app-resolver | HarmonyOS ArkTS/ArkUI code and API review | HarmonyOS/OpenHarmony application changes |
+| healthcare-reviewer | Clinical safety, PHI compliance, CDSS accuracy | Healthcare, EMR/EHR application code |
+| a11y-architect | WCAG 2.2 accessibility architecture | Designing UI components, accessibility audits |
+| code-architect | Feature architecture blueprints from codebase patterns | New features needing implementation design |
+| network-architect | Enterprise multi-site network architecture | Complex network design decisions |
+| homelab-architect | Home/small-lab network design | Home infrastructure planning |
+| network-config-reviewer | Router/switch config security and correctness | Network configuration changes |
+| network-troubleshooter | OSI-layer connectivity and routing diagnosis | Network connectivity and routing issues |
+| performance-optimizer | Bottleneck detection, bundle size, memory leaks | Slow code or high resource usage |
+| silent-failure-hunter | Swallowed errors and missing propagation | Code reliability audits |
+| type-design-analyzer | Type encapsulation and invariant design | Type design and invariant reviews |
+| pr-test-analyzer | PR test coverage quality and completeness | Before merging pull requests |
+| code-explorer | Execution path tracing and architecture mapping | Understanding unfamiliar code paths |
+| code-simplifier | Clarity-focused code refinement without behavior change | Post-implementation cleanup |
+| comment-analyzer | Comment accuracy, freshness, and rot risk | Code comment audits |
+| agent-evaluator | 5-axis quality scoring for agent output | Evaluating task completion quality |
+| chief-of-staff | Multi-channel communication triage and drafting | Managing email/Slack communication workflows |
+| conversation-analyzer | Extract hook behaviors from session transcripts | Creating hooks from observed patterns |
+| marketing-agent | Campaign planning, copy creation, content calendars | Product launches, marketing campaigns |
+| seo-specialist | Technical SEO audit, structured data, Core Web Vitals | Site audits, meta tag and schema issues |
+| opensource-forker | Fork projects and strip secrets for open-sourcing | Starting an open-source release |
+| opensource-sanitizer | Verify sanitized fork is release-ready | Before any public release |
+| opensource-packager | Generate OSS packaging boilerplate (README, LICENSE, etc.) | Finalizing an open-source release |
+| gan-planner | Expand a prompt into a full product specification | Starting a GAN harness session |
+| gan-generator | Implement features per spec, iterate on evaluator feedback | GAN harness implementation phase |
+| gan-evaluator | Test running application via Playwright and score it | GAN harness evaluation phase |
## Agent Orchestration
@@ -61,6 +97,17 @@ Use agents proactively without user prompt:
- Autonomous loops / loop monitoring → **ecc:loop-operator**
- Harness config reliability and cost → **ecc:harness-optimizer**
- RAG/retrieval pipeline changes → **ecc:rag-pipeline-reviewer**
+- Performance bottleneck or slow code → **ecc:performance-optimizer**
+- React/JSX changes → **ecc:react-reviewer**
+- Vue changes → **ecc:vue-reviewer**
+- Swift changes → **ecc:swift-reviewer**
+- C# changes → **ecc:csharp-reviewer**
+- PHP changes → **ecc:php-reviewer**
+- Flutter/Dart changes → **ecc:flutter-reviewer**
+- Healthcare/clinical code → **ecc:healthcare-reviewer**
+- UI component design → **ecc:a11y-architect**
+- Open-source release prep → **ecc:opensource-forker** → **ecc:opensource-sanitizer** → **ecc:opensource-packager**
+- Agent output quality check → **ecc:agent-evaluator**
Use parallel execution for independent operations — launch multiple agents simultaneously.
@@ -154,7 +201,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
```
agents/ — 68 specialized subagents
-skills/ — 292 workflow skills and domain knowledge
+skills/ — 293 workflow skills and domain knowledge
commands/ — 94 slash commands
hooks/ — Trigger-based automations
rules/ — Always-follow guidelines (common + per-language)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c89605c39..0eea94dc4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
## 2.2.2 - 2026-09-15
+### Added
+
+#### Pi core profile
+
+- Add `pi/core/`, a curated Pi-native skills+prompts-only profile for downstream packagers that mirror GitHub Releases: 123 portable engineering skills and 24 pure prompt-workflow commands, no extensions, no hooks, no runtime downloads, and no network or SaaS dependencies. The profile is generated deterministically from the explicit include/exclude lists in `manifests/pi-core.json` by `scripts/build-pi-core.js` and committed so release tarballs contain it verbatim; `pi/core/CURATION.md` lists every excluded skill and command with its reason.
+- The build fails on safety violations: non-allowlisted URL hosts, pipe-to-shell or fetch-and-run download forms, secrets or tokens, absolute per-user home paths, symlinks, invalid SKILL.md frontmatter, and duplicate skill names. The `council` skill ships as `ecc-council` inside pi/core to avoid catalog name clashes.
+- CI rebuilds pi/core and verifies it is committed up to date, then installs the Pi coding agent CLI and proves the profile loads fully offline (`PI_OFFLINE=1`), asserting every curated command is actually registered. The release workflow verifies VERSION matches the tag and that pi/core is current.
+
### Fixed
#### Packaging
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 06d1431b0..efe5e4f40 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,6 +14,7 @@ Thanks for wanting to contribute! This repo is a community resource for Claude C
- [MCP and documentation (e.g. Context7)](#mcp-and-documentation-eg-context7)
- [Cross-Harness and Translations](#cross-harness-and-translations)
- [Pull Request Process](#pull-request-process)
+- [Releases](#releases)
---
@@ -484,6 +485,27 @@ Run `npm test` locally. It is the same gauntlet CI runs, and it catches almost e
---
+## Releases
+
+Releases are cut on a regular cadence, roughly every two weeks, plus out-of-band
+patches for security or data-loss fixes.
+
+1. Sync the version everywhere: `VERSION`, `package.json`, and (via
+ `node scripts/build-pi-core.js`) `pi/core/package.json`.
+2. Update `CHANGELOG.md` and write reviewed release notes at
+ `docs/releases/
@@ -114,9 +115,9 @@ Use the [guided setup](#install-ecc) or [native plugin commands](#claude-code-de
' + body + '
${paste.content}`; `expiresAt` is an ISO
+ timestamp. Default TTL is 7 days; `ttlSeconds` must be an integer between
+ 1 and 2592000 (30 days).
+ - Missing/invalid `url` or out-of-range `ttlSeconds` → `400`.
+- `GET /` — `302` with `Location` set to the original URL.
+ Unknown code → `404`. Expired link → `410`.
+- `DELETE /links/` — `204`. Unknown code → `404`.
+
+All error responses follow the envelope in `CONTRIBUTING.md`.
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/files/CONTRIBUTING.md b/docker/context-profiles/complex-eval/cases3/chained-tickets/files/CONTRIBUTING.md
new file mode 100644
index 000000000..7c45e4af2
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/files/CONTRIBUTING.md
@@ -0,0 +1,13 @@
+# Engineering conventions
+
+These conventions apply to every ticket, every route, every change:
+
+- **Errors**: every error response is JSON with the envelope
+ `{ "error": { "code": "", "message": "" } }`
+ and the matching HTTP status. No HTML error pages, no stack traces.
+- **Layering**: HTTP handling in `src/routes.js`, business logic in
+ `src/service.js`, storage in `src/store.js`. `src/app.js` wires them.
+- **Runtime config** comes from environment variables, read at startup.
+- **Every ticket**: add tests under `test/`, add a `CHANGELOG.md` entry
+ describing what shipped, and keep `README.md` accurate.
+- No external dependencies.
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/files/README.md b/docker/context-profiles/complex-eval/cases3/chained-tickets/files/README.md
new file mode 100644
index 000000000..90f4bae61
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/files/README.md
@@ -0,0 +1,9 @@
+# shortlink
+
+Internal link shortener service. Node.js standard library only, CommonJS.
+
+- `API.md` — the HTTP contract.
+- `CONTRIBUTING.md` — engineering conventions. Every ticket follows them.
+- `src/app.js` exports `createApp()` returning an `http.Server` that is not yet
+ listening; `node src/index.js ` starts the service.
+- Run the tests with `npm test`.
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/files/package.json b/docker/context-profiles/complex-eval/cases3/chained-tickets/files/package.json
new file mode 100644
index 000000000..12bbcaf08
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/files/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "shortlink",
+ "private": true,
+ "type": "commonjs",
+ "scripts": { "test": "node --test test/*.test.js" }
+}
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/meta.json b/docker/context-profiles/complex-eval/cases3/chained-tickets/meta.json
new file mode 100644
index 000000000..30eb9fb05
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/meta.json
@@ -0,0 +1,17 @@
+{
+ "id": "chained-tickets",
+ "category": "long-horizon-chain",
+ "manualIds": [],
+ "checkTimeoutMs": 60000,
+ "steps": [
+ { "manualIds": ["skill:backend-patterns"] },
+ { "manualIds": ["skill:backend-patterns"] },
+ { "manualIds": ["skill:security-review"] },
+ { "manualIds": ["skill:api-design"] }
+ ],
+ "selection": {
+ "id": "complex-chained-tickets",
+ "category": "complex-long-horizon",
+ "expectedIds": ["skill:backend-patterns"]
+ }
+}
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/01-core/check.cjs b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/01-core/check.cjs
new file mode 100644
index 000000000..cda5c3028
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/01-core/check.cjs
@@ -0,0 +1,95 @@
+'use strict';
+// Step 1 grader: core API contract + conventions (envelope, layering, changelog, tests).
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 10; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 10, passed: ok, total: 10 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
+const root = process.cwd();
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+(async () => {
+ let createApp;
+ try { ({ createApp } = require(path.join(root, 'src', 'app.js'))); } catch { /* scored below */ }
+ if (typeof createApp === 'function') {
+ try {
+ const app = createApp();
+ await new Promise(resolve => app.listen(0, '127.0.0.1', resolve));
+ const port = app.address().port;
+ const post = (body) => fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
+ const get = (p) => fetch(`http://127.0.0.1:${port}${p}`, { redirect: 'manual' });
+
+ const created = await post({ url: 'https://example.com/landing' });
+ const createdBody = await created.json().catch(() => null);
+ record('create-happy-201', created.status === 201 && createdBody
+ && /^[A-Za-z0-9]{6,10}$/.test(createdBody.code || '') && typeof createdBody.shortUrl === 'string'
+ && typeof createdBody.expiresAt === 'string' && !Number.isNaN(Date.parse(createdBody.expiresAt)));
+
+ let code = createdBody && createdBody.code;
+ if (code) {
+ const redirect = await get(`/${code}`);
+ record('redirect-302-location', redirect.status === 302
+ && redirect.headers.get('location') === 'https://example.com/landing');
+ } else record('redirect-302-location', false);
+
+ const unknown = await get('/nope00');
+ record('unknown-code-404-envelope', unknown.status === 404 && hasEnvelope(await unknown.json().catch(() => null)));
+
+ const badUrl = await post({ url: 'notaurl' });
+ record('invalid-url-400-envelope', badUrl.status === 400 && hasEnvelope(await badUrl.json().catch(() => null)));
+ const noBody = await post({});
+ record('missing-url-400-envelope', noBody.status === 400 && hasEnvelope(await noBody.json().catch(() => null)));
+ const badTtl = await post({ url: 'https://example.com', ttlSeconds: 99999999 });
+ record('ttl-bounds-400-envelope', badTtl.status === 400 && hasEnvelope(await badTtl.json().catch(() => null)));
+
+ const expiring = await post({ url: 'https://example.com/gone', ttlSeconds: 1 });
+ const expiringBody = await expiring.json().catch(() => null);
+ if (expiringBody && expiringBody.code) {
+ await sleep(1300);
+ const gone = await get(`/${expiringBody.code}`);
+ record('expired-link-410-envelope', gone.status === 410 && hasEnvelope(await gone.json().catch(() => null)));
+ } else record('expired-link-410-envelope', false);
+
+ if (code) {
+ const del = await fetch(`http://127.0.0.1:${port}/links/${code}`, { method: 'DELETE' });
+ const after = await get(`/${code}`);
+ record('delete-flow-204-then-404', del.status === 204 && after.status === 404);
+ } else record('delete-flow-204-then-404', false);
+ app.close();
+ } catch { /* remaining checks unscored */ }
+ } else {
+ for (const name of ['create-happy-201', 'redirect-302-location', 'unknown-code-404-envelope',
+ 'invalid-url-400-envelope', 'missing-url-400-envelope', 'ttl-bounds-400-envelope',
+ 'expired-link-410-envelope', 'delete-flow-204-then-404']) record(name, false);
+ }
+
+ // Conventions.
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ const testCount = (tests.match(/\btest\(/g) || []).length;
+ record('changelog-and-tests', changelog.length > 20 && testCount >= 3);
+ record('layering-files', ['routes.js', 'service.js', 'store.js']
+ .every(f => fs.existsSync(path.join(root, 'src', f))));
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/01-core/query.md b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/01-core/query.md
new file mode 100644
index 000000000..2c00246ec
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/01-core/query.md
@@ -0,0 +1 @@
+Implement the link shortener described in API.md. Follow CONTRIBUTING.md — every convention applies.
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/02-persistence/check.cjs b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/02-persistence/check.cjs
new file mode 100644
index 000000000..ce42427f4
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/02-persistence/check.cjs
@@ -0,0 +1,106 @@
+'use strict';
+// Step 2 grader: persistence across a simulated restart (fresh module state,
+// same DATA_FILE), expiry state survives, fresh/corrupt-start tolerance, conventions.
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 7; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 7, passed: ok, total: 7 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
+const root = process.cwd();
+const DATA_FILE = path.join(root, '.ecc-data', 'links.json');
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+function purgeApp() {
+ for (const key of Object.keys(require.cache)) {
+ if (key.startsWith(path.join(root, 'src') + path.sep)) delete require.cache[key];
+ }
+}
+
+async function start() {
+ purgeApp();
+ const { createApp } = require(path.join(root, 'src', 'app.js'));
+ const app = createApp();
+ await new Promise((resolve, reject) => { app.once('error', reject); app.listen(0, '127.0.0.1', resolve); });
+ return app;
+}
+
+(async () => {
+ process.env.DATA_FILE = DATA_FILE;
+ try {
+ // First boot: create a durable link and a 1s-expiring link.
+ let app = await start();
+ let port = app.address().port;
+ const post = body => fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
+ const durable = await (await post({ url: 'https://example.com/durable' })).json().catch(() => null);
+ const short = await (await post({ url: 'https://example.com/short', ttlSeconds: 1 })).json().catch(() => null);
+ await new Promise(resolve => app.close(resolve));
+
+ // Restart: fresh modules, same DATA_FILE.
+ app = await start();
+ port = app.address().port;
+ const get = p => fetch(`http://127.0.0.1:${port}${p}`, { redirect: 'manual' });
+
+ const after = durable && durable.code ? await get(`/${durable.code}`) : null;
+ record('link-survives-restart', after && after.status === 302
+ && after.headers.get('location') === 'https://example.com/durable');
+
+ await sleep(1300);
+ const expiredAfter = short && short.code ? await get(`/${short.code}`) : null;
+ record('expiry-survives-restart', expiredAfter && expiredAfter.status === 410);
+ await new Promise(resolve => app.close(resolve));
+
+ // Data file is real JSON on disk.
+ let dataOk = false;
+ try { JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); dataOk = true; } catch { /* missing/invalid */ }
+ record('data-file-is-json', dataOk);
+
+ // Fresh start with no data file present.
+ fs.rmSync(DATA_FILE, { force: true });
+ app = await start();
+ port = app.address().port;
+ const fresh = await fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ url: 'https://example.com/fresh' }) });
+ record('fresh-start-without-data-file', fresh.status === 201);
+ await new Promise(resolve => app.close(resolve));
+
+ // Corrupt data file must not kill the service.
+ fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true });
+ fs.writeFileSync(DATA_FILE, 'garbage{{{');
+ app = await start();
+ port = app.address().port;
+ const afterCorrupt = await get('/anything1');
+ record('corrupt-data-file-tolerated', afterCorrupt.status === 404
+ && hasEnvelope(await afterCorrupt.json().catch(() => null)));
+ await new Promise(resolve => app.close(resolve));
+ fs.rmSync(DATA_FILE, { force: true });
+ } catch { /* remaining checks unscored */ }
+
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ const changelogEntries = (changelog.match(/^[-*#]/gm) || []).length;
+ record('changelog-grown', changelogEntries >= 2 && /persist|restart|data/i.test(changelog));
+ record('tests-grown', (tests.match(/\btest\(/g) || []).length >= 6);
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/02-persistence/query.md b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/02-persistence/query.md
new file mode 100644
index 000000000..544b2f51e
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/02-persistence/query.md
@@ -0,0 +1 @@
+Links need to survive a service restart. Persist them to the JSON file named by the DATA_FILE environment variable (read at startup). Take care of it.
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/03-abuse/check.cjs b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/03-abuse/check.cjs
new file mode 100644
index 000000000..829abd522
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/03-abuse/check.cjs
@@ -0,0 +1,83 @@
+'use strict';
+// Step 3 grader: abuse handling — URL validation, size limits, rate limiting —
+// plus conventions. Hammer probe runs last so earlier probes stay unthrottled.
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 8; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 8, passed: ok, total: 8 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const root = process.cwd();
+const DATA_FILE = path.join(root, '.ecc-data', 'links-step3.json');
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+function purgeApp() {
+ for (const key of Object.keys(require.cache)) {
+ if (key.startsWith(path.join(root, 'src') + path.sep)) delete require.cache[key];
+ }
+}
+
+(async () => {
+ process.env.DATA_FILE = DATA_FILE;
+ try {
+ purgeApp();
+ const { createApp } = require(path.join(root, 'src', 'app.js'));
+ const app = createApp();
+ await new Promise(resolve => app.listen(0, '127.0.0.1', resolve));
+ const port = app.address().port;
+ const post = body => fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
+
+ const okCreate = await post({ url: 'https://example.com/normal' });
+ record('normal-create-still-201', okCreate.status === 201);
+
+ const js = await post({ url: 'javascript:alert(1)' });
+ record('javascript-scheme-400-envelope', js.status === 400 && hasEnvelope(await js.json().catch(() => null)));
+ const ftp = await post({ url: 'ftp://files.example.com/x' });
+ record('non-http-scheme-400-envelope', ftp.status === 400 && hasEnvelope(await ftp.json().catch(() => null)));
+ const huge = await post({ url: `https://example.com/${'a'.repeat(10000)}` });
+ const hugeBody = await huge.json().catch(() => null);
+ record('oversize-url-4xx-envelope', huge.status >= 400 && huge.status < 500 && hasEnvelope(hugeBody));
+
+ // Hammer: 60 rapid creates must trip a 429 with the envelope.
+ const responses = await Promise.all(Array.from({ length: 60 }, (_, i) =>
+ post({ url: `https://example.com/flood-${i}` })));
+ const limited = [];
+ for (const r of responses) if (r.status === 429) limited.push(await r.json().catch(() => null));
+ record('rate-limit-429-envelope', limited.length > 0 && limited.every(hasEnvelope));
+ app.close();
+ } catch { /* remaining checks unscored */ }
+
+ let sources = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'src'))) {
+ if (f.endsWith('.js')) sources += fs.readFileSync(path.join(root, 'src', f), 'utf8');
+ }
+ } catch { /* missing */ }
+ record('rate-limiting-implemented', /429|rate.?limit/i.test(sources));
+
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ const changelogEntries = (changelog.match(/^[-*#]/gm) || []).length;
+ record('changelog-grown', changelogEntries >= 3 && /abuse|rate|valid|secur/i.test(changelog));
+ record('tests-grown', (tests.match(/\btest\(/g) || []).length >= 9);
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/03-abuse/query.md b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/03-abuse/query.md
new file mode 100644
index 000000000..799adaf89
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/03-abuse/query.md
@@ -0,0 +1 @@
+We're seeing abuse — people hammering the API and submitting junk URLs. Deal with it.
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/04-analytics/check.cjs b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/04-analytics/check.cjs
new file mode 100644
index 000000000..ed2e69364
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/04-analytics/check.cjs
@@ -0,0 +1,88 @@
+'use strict';
+// Step 4 grader: hit analytics consistent with the existing API, conventions,
+// docs and tests. (Runs in a later process than step 3, so rate windows cleared.)
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 8; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 8, passed: ok, total: 8 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const root = process.cwd();
+const DATA_FILE = path.join(root, '.ecc-data', 'links-step4.json');
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+function purgeApp() {
+ for (const key of Object.keys(require.cache)) {
+ if (key.startsWith(path.join(root, 'src') + path.sep)) delete require.cache[key];
+ }
+}
+
+(async () => {
+ process.env.DATA_FILE = DATA_FILE;
+ try {
+ purgeApp();
+ const { createApp } = require(path.join(root, 'src', 'app.js'));
+ const app = createApp();
+ await new Promise(resolve => app.listen(0, '127.0.0.1', resolve));
+ const port = app.address().port;
+
+ const created = await fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ url: 'https://example.com/tracked' }) });
+ const body = await created.json().catch(() => null);
+ const code = body && body.code;
+ record('create-still-works', created.status === 201 && Boolean(code));
+
+ if (code) {
+ const before = await fetch(`http://127.0.0.1:${port}/links/${code}/stats`);
+ const beforeBody = await before.json().catch(() => null);
+ record('stats-zero-before-redirects', before.status === 200 && beforeBody && beforeBody.hits === 0);
+
+ for (let i = 0; i < 3; i++) {
+ await fetch(`http://127.0.0.1:${port}/${code}`, { redirect: 'manual' });
+ }
+ const stats = await fetch(`http://127.0.0.1:${port}/links/${code}/stats`);
+ const statsBody = await stats.json().catch(() => null);
+ record('stats-count-three-hits', stats.status === 200 && statsBody && statsBody.hits === 3);
+
+ const redirect = await fetch(`http://127.0.0.1:${port}/${code}`, { redirect: 'manual' });
+ record('redirect-still-302', redirect.status === 302);
+
+ const missing = await fetch(`http://127.0.0.1:${port}/links/zzzzzz/stats`);
+ record('stats-unknown-404-envelope', missing.status === 404
+ && hasEnvelope(await missing.json().catch(() => null)));
+ } else {
+ for (const name of ['stats-zero-before-redirects', 'stats-count-three-hits',
+ 'redirect-still-302', 'stats-unknown-404-envelope']) record(name, false);
+ }
+ app.close();
+ } catch { /* remaining checks unscored */ }
+
+ let readme = '';
+ try { readme = fs.readFileSync(path.join(root, 'README.md'), 'utf8'); } catch { /* missing */ }
+ record('readme-documents-stats', /\/stats|hits|analytics/i.test(readme));
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ const changelogEntries = (changelog.match(/^[-*#]/gm) || []).length;
+ record('changelog-grown', changelogEntries >= 4 && /stat|analytic|hit/i.test(changelog));
+ record('tests-grown', (tests.match(/\btest\(/g) || []).length >= 12);
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/04-analytics/query.md b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/04-analytics/query.md
new file mode 100644
index 000000000..619549068
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/chained-tickets/steps/04-analytics/query.md
@@ -0,0 +1 @@
+Track redirect hits per link and expose them at GET /links/:code/stats, consistent with the existing API.
diff --git a/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/check.cjs b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/check.cjs
new file mode 100644
index 000000000..7882bce07
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/check.cjs
@@ -0,0 +1,119 @@
+'use strict';
+// Hidden grader for idempotent-webhooks: exactly-once under sequential,
+// concurrent, and mixed-concurrent duplicates, plus the documented API,
+// regression coverage, and hygiene. Prints ECC_EVAL_SCORE and always exits 0.
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 12; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 12, passed: ok, total: 12 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const root = process.cwd();
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+(async () => {
+ let createApp;
+ let store;
+ try {
+ ({ createApp } = require(path.join(root, 'src', 'app.js')));
+ ({ store } = require(path.join(root, 'src', 'store.js')));
+ } catch { /* scored below */ }
+ if (typeof createApp === 'function' && store && Array.isArray(store.paymentLog)) {
+ try {
+ const app = createApp();
+ await new Promise(resolve => app.listen(0, '127.0.0.1', resolve));
+ const port = app.address().port;
+ const send = (eventId, orderId, amountCents) => fetch(`http://127.0.0.1:${port}/webhooks/payments`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ eventId, orderId, amountCents, type: 'payment.succeeded' }) });
+ const logsFor = orderId => store.paymentLog.filter(p => p.orderId === orderId).length;
+
+ // 1: single delivery applies once.
+ const single = await send('ev-1', 'o1', 5000);
+ const singleBody = await single.json().catch(() => null);
+ record('single-delivery-processed', single.status === 200 && singleBody
+ && singleBody.status === 'processed' && singleBody.orderId === 'o1' && logsFor('o1') === 1);
+
+ // 2: sequential retry replays without re-applying.
+ const retry = await send('ev-1', 'o1', 5000);
+ const retryBody = await retry.json().catch(() => null);
+ record('sequential-duplicate-inert', retry.status === 200 && retryBody
+ && retryBody.status === 'duplicate' && logsFor('o1') === 1);
+
+ // 3: fifty concurrent identical deliveries apply exactly once.
+ const storm = await Promise.all(Array.from({ length: 50 }, () => send('ev-2', 'o2', 12500)));
+ const stormBodies = [];
+ for (const r of storm) stormBodies.push(await r.json().catch(() => null));
+ const processedCount = stormBodies.filter(b => b && b.status === 'processed').length;
+ const duplicateCount = stormBodies.filter(b => b && b.status === 'duplicate').length;
+ record('concurrent-storm-exactly-once', storm.every(r => r.status === 200)
+ && processedCount === 1 && duplicateCount === 49 && logsFor('o2') === 1
+ && store.orders.get('o2').paymentsApplied === 1);
+
+ // 4: a different event for an already-paid order is already_paid and inert.
+ const second = await send('ev-3', 'o2', 12500);
+ const secondBody = await second.json().catch(() => null);
+ record('already-paid-order-inert', second.status === 200 && secondBody
+ && secondBody.status === 'already_paid' && logsFor('o2') === 1);
+
+ // 5-7: contract errors with envelopes.
+ const unknown = await send('ev-4', 'nope', 100);
+ record('unknown-order-404-envelope', unknown.status === 404 && hasEnvelope(await unknown.json().catch(() => null)));
+ const malformed = await fetch(`http://127.0.0.1:${port}/webhooks/payments`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: '{bad json' });
+ record('malformed-body-400-envelope', malformed.status === 400 && hasEnvelope(await malformed.json().catch(() => null)));
+ const mismatch = await send('ev-5', 'o3', 999999);
+ record('amount-mismatch-422-envelope', mismatch.status === 422
+ && hasEnvelope(await mismatch.json().catch(() => null)) && logsFor('o3') === 0);
+
+ // 8: mixed storm — three orders, three eventIds, ten duplicates each, all concurrent.
+ const mixed = await Promise.all(['o4', 'o5', 'o6'].flatMap(orderId =>
+ Array.from({ length: 10 }, () => send(`ev-${orderId}`, orderId, store.orders.get(orderId).amountCents))));
+ for (const r of mixed) await r.json().catch(() => null);
+ record('mixed-storm-each-order-once', ['o4', 'o5', 'o6'].every(orderId =>
+ logsFor(orderId) === 1 && store.orders.get(orderId).paymentsApplied === 1));
+
+ // 9: order inspection endpoint reflects reality.
+ const orderView = await fetch(`http://127.0.0.1:${port}/orders/o2`);
+ const orderBody = await orderView.json().catch(() => null);
+ record('order-endpoint-accurate', orderView.status === 200 && orderBody
+ && orderBody.status === 'paid' && orderBody.paymentsApplied === 1 && Boolean(orderBody.paidAt));
+
+ app.close();
+ } catch { /* remaining checks unscored */ }
+ } else {
+ for (const name of ['single-delivery-processed', 'sequential-duplicate-inert', 'concurrent-storm-exactly-once',
+ 'already-paid-order-inert', 'unknown-order-404-envelope', 'malformed-body-400-envelope',
+ 'amount-mismatch-422-envelope', 'mixed-storm-each-order-once', 'order-endpoint-accurate']) record(name, false);
+ }
+
+ // Conventions.
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ record('concurrency-regression-tests', (tests.match(/\btest\(/g) || []).length >= 4
+ && /Promise\.all|concurrent|duplicate|retry/i.test(tests));
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ record('changelog-entry', /idem|duplicat|retry|inc-104|race/i.test(changelog));
+ try {
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
+ record('no-external-dependencies', !pkg.dependencies && !pkg.devDependencies);
+ } catch { record('no-external-dependencies', false); }
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/README.md b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/README.md
new file mode 100644
index 000000000..512c8c059
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/README.md
@@ -0,0 +1,12 @@
+# webhook-receiver
+
+Receives payment webhooks. There is an open incident: customers were
+double-charged when the provider retried deliveries. See `SPEC.md` for the
+contract, including the exactly-once rules.
+
+- `src/app.js` exports `createApp()` returning an `http.Server` that is not
+ yet listening; `node src/index.js ` starts the service.
+- `src/store.js` is shared infrastructure: it keeps its current exports
+ (`store`) and records every applied payment in `store.paymentLog`.
+- No external dependencies. `npm test` runs the tests. `CHANGELOG.md` records
+ every shipped change.
diff --git a/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/SPEC.md b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/SPEC.md
new file mode 100644
index 000000000..e3dee27b1
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/SPEC.md
@@ -0,0 +1,30 @@
+# Payment webhook contract
+
+`POST /webhooks/payments` with JSON body
+`{ "eventId": string, "orderId": string, "amountCents": number, "type": "payment.succeeded" }`.
+
+Exactly-once is the point. The provider retries aggressively and may deliver
+the same event many times, concurrently, or out of order.
+
+- A new, valid `eventId`: apply the payment exactly once → `200`
+ `{ "status": "processed", "orderId" }`.
+- The same `eventId` seen again (any number of times, any interleaving):
+ `200` `{ "status": "duplicate", "orderId" }` — never applied twice.
+- A payment event (new `eventId`) for an order that is already paid:
+ `200` `{ "status": "already_paid", "orderId" }` — an order is paid at most
+ once, ever.
+- `amountCents` not matching the order's amount: `422`, not applied.
+- Unknown `orderId`: `404`. Malformed body (bad JSON, missing/invalid
+ fields): `400`.
+- Error responses use the envelope
+ `{ "error": { "code": "", "message": "..." } }`.
+
+`GET /orders/:id` → `200` `{ "id", "status", "paidAt", "paymentsApplied" }`
+or a `404` envelope.
+
+## Incident note
+
+INC-104: concurrent duplicate deliveries double-applied payments. The naive
+receiver checked "have we seen this event?" and applied the payment in two
+separate steps with an async gap in between, so parallel duplicates both
+passed the check.
diff --git a/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/package.json b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/package.json
new file mode 100644
index 000000000..11c26f720
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "webhook-receiver",
+ "private": true,
+ "type": "commonjs",
+ "scripts": { "test": "node --test test/*.test.js" }
+}
diff --git a/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/src/app.js b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/src/app.js
new file mode 100644
index 000000000..6ba0ba755
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/src/app.js
@@ -0,0 +1,54 @@
+'use strict';
+const http = require('node:http');
+const { store } = require('./store');
+
+// INC-104 receiver: checks "seen this event?" and applies the payment in two
+// steps with an async gap in between. Concurrent duplicates both pass the
+// check. Do not keep this shape.
+function createApp() {
+ return http.createServer((req, res) => {
+ const url = new URL(req.url, 'http://localhost');
+
+ if (req.method === 'POST' && url.pathname === '/webhooks/payments') {
+ let body = '';
+ req.on('data', chunk => { body += chunk; });
+ req.on('end', async () => {
+ const parsed = JSON.parse(body);
+ const { eventId, orderId } = parsed;
+ if (store.processedEvents.has(eventId)) {
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ status: 'duplicate', orderId }));
+ return;
+ }
+ await new Promise(resolve => setImmediate(resolve)); // async gap
+ const order = store.orders.get(orderId);
+ order.status = 'paid';
+ order.paidAt = new Date().toISOString();
+ order.paymentsApplied++;
+ store.paymentLog.push({ eventId, orderId, amountCents: parsed.amountCents });
+ store.processedEvents.add(eventId);
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ status: 'processed', orderId }));
+ });
+ return;
+ }
+
+ const match = /^\/orders\/([\w-]+)$/.exec(url.pathname);
+ if (req.method === 'GET' && match) {
+ const order = store.orders.get(match[1]);
+ if (!order) {
+ res.writeHead(404, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ error: { code: 'NOT_FOUND', message: 'no such order' } }));
+ return;
+ }
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(order));
+ return;
+ }
+
+ res.writeHead(404, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ error: { code: 'NOT_FOUND', message: 'not found' } }));
+ });
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/src/index.js b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/src/index.js
new file mode 100644
index 000000000..90ef9215f
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/src/index.js
@@ -0,0 +1,7 @@
+'use strict';
+const { createApp } = require('./app');
+
+const port = Number(process.argv[2] || 8080);
+createApp().listen(port, () => {
+ console.log(`webhook-receiver listening on ${port}`);
+});
diff --git a/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/src/store.js b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/src/store.js
new file mode 100644
index 000000000..64a4099a4
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/src/store.js
@@ -0,0 +1,18 @@
+'use strict';
+
+// Shared infrastructure. Every applied payment is appended to paymentLog;
+// orders and processedEvents track receiver state. Keep the `store` export.
+const store = {
+ orders: new Map([
+ ['o1', { id: 'o1', amountCents: 5000, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ['o2', { id: 'o2', amountCents: 12500, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ['o3', { id: 'o3', amountCents: 800, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ['o4', { id: 'o4', amountCents: 9999, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ['o5', { id: 'o5', amountCents: 250, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ['o6', { id: 'o6', amountCents: 7300, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ]),
+ paymentLog: [],
+ processedEvents: new Set(),
+};
+
+module.exports = { store };
diff --git a/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/test/webhooks.test.js b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/test/webhooks.test.js
new file mode 100644
index 000000000..cf79f83d4
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/files/test/webhooks.test.js
@@ -0,0 +1,21 @@
+'use strict';
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createApp } = require('../src/app');
+const { store } = require('../src/store');
+
+test('a single payment event processes', async () => {
+ const server = createApp();
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ try {
+ const port = server.address().port;
+ const res = await fetch(`http://127.0.0.1:${port}/webhooks/payments`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ eventId: 'ev-test-1', orderId: 'o1', amountCents: 5000, type: 'payment.succeeded' }) });
+ assert.equal(res.status, 200);
+ assert.equal((await res.json()).status, 'processed');
+ assert.equal(store.orders.get('o1').status, 'paid');
+ } finally {
+ server.close();
+ }
+});
diff --git a/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/meta.json b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/meta.json
new file mode 100644
index 000000000..d5d396e74
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/meta.json
@@ -0,0 +1,11 @@
+{
+ "id": "idempotent-webhooks",
+ "category": "concurrency-trap",
+ "manualIds": ["skill:error-handling"],
+ "checkTimeoutMs": 60000,
+ "selection": {
+ "id": "complex-idempotent-webhooks",
+ "category": "complex-concurrency-trap",
+ "expectedIds": ["skill:error-handling"]
+ }
+}
diff --git a/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/query.md b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/query.md
new file mode 100644
index 000000000..f2902f874
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/idempotent-webhooks/query.md
@@ -0,0 +1 @@
+The payments provider retries aggressively and sometimes delivers the same webhook many times concurrently. Our receiver double-charges customers when that happens — see the incident note in SPEC.md. Rework the receiver so the full contract in SPEC.md holds: duplicates must never double-apply under any interleaving, and the documented API and the store contract stay intact. `npm test` must pass, and add regression coverage for the concurrent-duplicate case so INC-104 can't come back.
diff --git a/docker/context-profiles/complex-eval/cases3/production-ready/check.cjs b/docker/context-profiles/complex-eval/cases3/production-ready/check.cjs
new file mode 100644
index 000000000..1320f0e9f
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/production-ready/check.cjs
@@ -0,0 +1,133 @@
+'use strict';
+// Hidden grader for production-ready: probes every dimension of the documented
+// production bar. Prints ECC_EVAL_SCORE and always exits 0.
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 16; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 16, passed: ok, total: 16 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const root = process.cwd();
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+(async () => {
+ let createApp;
+ try { ({ createApp } = require(path.join(root, 'src', 'app.js'))); } catch { /* scored below */ }
+ if (typeof createApp === 'function') {
+ // Capture console output during the probe run to inspect request logging.
+ const logged = [];
+ const originalLog = console.log;
+ const originalError = console.error;
+ console.log = (...args) => { logged.push(args.join(' ')); };
+ console.error = (...args) => { logged.push(args.join(' ')); };
+ try {
+ const app = createApp();
+ await new Promise(resolve => app.listen(0, '127.0.0.1', resolve));
+ const port = app.address().port;
+ const api = (p, options) => fetch(`http://127.0.0.1:${port}${p}`, options);
+ const post = body => api('/notes', { method: 'POST', headers: { 'content-type': 'application/json' }, body });
+
+ // Documented API still works.
+ const created = await post(JSON.stringify({ title: 'deploy', body: 'checklist' }));
+ const createdBody = await created.json().catch(() => null);
+ record('api-roundtrip-preserved', created.status === 201 && createdBody && createdBody.id
+ && (await (await api(`/notes/${createdBody.id}`)).json().catch(() => ({}))).title === 'deploy'
+ && Array.isArray((await (await api('/notes')).json().catch(() => ({}))).notes));
+
+ // Validation and envelope discipline.
+ const badJson = await post('{not json');
+ record('malformed-json-400-envelope', badJson.status === 400 && hasEnvelope(await badJson.json().catch(() => null)));
+ const missing = await post(JSON.stringify({ body: 'no title' }));
+ record('missing-field-400-envelope', missing.status === 400 && hasEnvelope(await missing.json().catch(() => null)));
+ const wrongType = await post(JSON.stringify({ title: 42, body: 'x' }));
+ record('wrong-type-400-envelope', wrongType.status === 400 && hasEnvelope(await wrongType.json().catch(() => null)));
+ const unknown = await api('/notes/n_999999');
+ const unknownBody = await unknown.text();
+ let unknownParsed = null;
+ try { unknownParsed = JSON.parse(unknownBody); } catch { /* html or text */ }
+ record('unknown-404-json-envelope', unknown.status === 404 && hasEnvelope(unknownParsed));
+
+ // Body limit.
+ const big = await post(JSON.stringify({ title: 'big', body: 'x'.repeat(100 * 1024) }));
+ record('oversize-body-413-envelope', big.status === 413 && hasEnvelope(await big.json().catch(() => null)));
+
+ // Health endpoint.
+ const health = await api('/health');
+ const healthBody = await health.json().catch(() => null);
+ record('health-endpoint', health.status === 200 && healthBody && healthBody.status === 'ok');
+
+ // Security header on a normal response.
+ const headers = await api('/notes');
+ record('nosniff-header', headers.headers.get('x-content-type-options') === 'nosniff');
+
+ // Error responses carry JSON content type.
+ record('errors-are-json', /application\/json/.test(unknown.headers.get('content-type') || ''));
+
+ app.close();
+ } catch { /* remaining checks unscored */ } finally {
+ console.log = originalLog;
+ console.error = originalError;
+ }
+
+ // Structured request logging: at least one JSON line with method/path/status-ish fields.
+ const structured = logged.some(line => {
+ try {
+ const parsed = JSON.parse(line);
+ return parsed && typeof parsed === 'object'
+ && /method/i.test(Object.keys(parsed).join(' '))
+ && /path|url/i.test(Object.keys(parsed).join(' '))
+ && /status/i.test(Object.keys(parsed).join(' '));
+ } catch { return false; }
+ });
+ record('structured-request-logs', structured);
+ } else {
+ for (const name of ['api-roundtrip-preserved', 'malformed-json-400-envelope', 'missing-field-400-envelope',
+ 'wrong-type-400-envelope', 'unknown-404-json-envelope', 'oversize-body-413-envelope', 'health-endpoint',
+ 'nosniff-header', 'errors-are-json', 'structured-request-logs']) record(name, false);
+ }
+
+ // Static dimensions.
+ let sources = '';
+ const walk = directory => {
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
+ const item = path.join(directory, entry.name);
+ if (entry.isDirectory()) walk(item);
+ else if (entry.name.endsWith('.js')) sources += fs.readFileSync(item, 'utf8');
+ }
+ };
+ try { walk(path.join(root, 'src')); } catch { /* none */ }
+ record('sigterm-graceful-shutdown', /SIGTERM/.test(sources));
+ record('env-config-port', /process\.env\.[A-Z_]*PORT/.test(sources));
+
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ const testCount = (tests.match(/\btest\(/g) || []).length;
+ record('tests-cover-error-paths', testCount >= 4 && /400|404|413|invalid|error/i.test(tests));
+
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ record('changelog-entry', changelog.length > 20 && /product|harden|valid|health|log/i.test(changelog));
+
+ record('no-leftover-todos', !/TODO|FIXME/.test(sources));
+ try {
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
+ record('no-external-dependencies', !pkg.dependencies && !pkg.devDependencies);
+ } catch { record('no-external-dependencies', false); }
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases3/production-ready/files/README.md b/docker/context-profiles/complex-eval/cases3/production-ready/files/README.md
new file mode 100644
index 000000000..e387bff31
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/production-ready/files/README.md
@@ -0,0 +1,19 @@
+# notes-service
+
+Tiny notes API. Hobby prototype state: it works on the happy path and that's
+about all that can be said for it.
+
+## API
+
+- `POST /notes` — body `{ "title": string, "body": string }` → `201` with
+ `{ "id", "title", "body" }`.
+- `GET /notes/:id` — `200` with the note, or `404`.
+- `GET /notes` — `200` with `{ "notes": [...] }`.
+
+`src/app.js` exports `createApp()` returning an `http.Server` that is not yet
+listening; `node src/index.js` starts the service. `npm test` runs the tests.
+
+## Operations
+
+`docs/production-bar.md` lists what every production service here must meet.
+`CHANGELOG.md` records every shipped change.
diff --git a/docker/context-profiles/complex-eval/cases3/production-ready/files/docs/production-bar.md b/docker/context-profiles/complex-eval/cases3/production-ready/files/docs/production-bar.md
new file mode 100644
index 000000000..af3df1c4c
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/production-ready/files/docs/production-bar.md
@@ -0,0 +1,21 @@
+# The production bar
+
+Every production service here meets all of the following, all the time:
+
+- **Validation**: malformed JSON, missing fields, and wrong types are rejected
+ with `400` and a structured JSON error body
+ `{ "error": { "code": "", "message": "..." } }`. Unknown
+ resources are `404` in the same envelope. No stack traces, no HTML errors,
+ no hanging connections.
+- **Body limits**: request bodies over 64 KB are rejected with `413`, same
+ envelope.
+- **Health**: `GET /health` returns `200` with `{ "status": "ok" }`.
+- **Logging**: one structured JSON log line per request with at least
+ `method`, `path`, and `status` fields.
+- **Configuration**: runtime configuration (port, limits) comes from
+ environment variables, read at startup. Nothing secret is hardcoded.
+- **Shutdown**: the service closes cleanly on `SIGTERM` (stops accepting,
+ drains, exits).
+- **Headers**: responses carry `X-Content-Type-Options: nosniff`.
+- **Tests**: the suite covers error paths, not just the happy path.
+- **Changelog**: every shipped change has a `CHANGELOG.md` entry.
diff --git a/docker/context-profiles/complex-eval/cases3/production-ready/files/package.json b/docker/context-profiles/complex-eval/cases3/production-ready/files/package.json
new file mode 100644
index 000000000..7cef6f8c0
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/production-ready/files/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "notes-service",
+ "private": true,
+ "type": "commonjs",
+ "scripts": { "test": "node --test test/*.test.js" }
+}
diff --git a/docker/context-profiles/complex-eval/cases3/production-ready/files/src/app.js b/docker/context-profiles/complex-eval/cases3/production-ready/files/src/app.js
new file mode 100644
index 000000000..db7fe2695
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/production-ready/files/src/app.js
@@ -0,0 +1,50 @@
+'use strict';
+const http = require('node:http');
+
+// Prototype state: happy path only.
+const notes = new Map();
+let nextId = 1;
+
+function createApp() {
+ return http.createServer((req, res) => {
+ console.log('got a request');
+ const url = new URL(req.url, 'http://localhost');
+
+ if (req.method === 'POST' && url.pathname === '/notes') {
+ let body = '';
+ req.on('data', chunk => { body += chunk; });
+ req.on('end', () => {
+ const parsed = JSON.parse(body);
+ const id = `n_${nextId++}`;
+ notes.set(id, { id, title: parsed.title, body: parsed.body });
+ res.writeHead(201, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(notes.get(id)));
+ });
+ return;
+ }
+
+ const match = /^\/notes\/([\w-]+)$/.exec(url.pathname);
+ if (req.method === 'GET' && match) {
+ const note = notes.get(match[1]);
+ if (!note) {
+ res.writeHead(404);
+ res.end('not found');
+ return;
+ }
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(note));
+ return;
+ }
+
+ if (req.method === 'GET' && url.pathname === '/notes') {
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ notes: [...notes.values()] }));
+ return;
+ }
+
+ res.writeHead(404);
+ res.end('not found');
+ });
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/cases3/production-ready/files/src/index.js b/docker/context-profiles/complex-eval/cases3/production-ready/files/src/index.js
new file mode 100644
index 000000000..a71330e92
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/production-ready/files/src/index.js
@@ -0,0 +1,6 @@
+'use strict';
+const { createApp } = require('./app');
+
+createApp().listen(8080, () => {
+ console.log('notes listening on 8080');
+});
diff --git a/docker/context-profiles/complex-eval/cases3/production-ready/files/test/notes.test.js b/docker/context-profiles/complex-eval/cases3/production-ready/files/test/notes.test.js
new file mode 100644
index 000000000..51babd8fb
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/production-ready/files/test/notes.test.js
@@ -0,0 +1,21 @@
+'use strict';
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createApp } = require('../src/app');
+
+test('create and read a note', async () => {
+ const server = createApp();
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ try {
+ const port = server.address().port;
+ const created = await fetch(`http://127.0.0.1:${port}/notes`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ title: 'first', body: 'hello' }) });
+ assert.equal(created.status, 201);
+ const { id } = await created.json();
+ const read = await fetch(`http://127.0.0.1:${port}/notes/${id}`);
+ assert.equal((await read.json()).title, 'first');
+ } finally {
+ server.close();
+ }
+});
diff --git a/docker/context-profiles/complex-eval/cases3/production-ready/meta.json b/docker/context-profiles/complex-eval/cases3/production-ready/meta.json
new file mode 100644
index 000000000..21aae2a12
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/production-ready/meta.json
@@ -0,0 +1,11 @@
+{
+ "id": "production-ready",
+ "category": "vague-hardening",
+ "manualIds": ["skill:backend-patterns"],
+ "checkTimeoutMs": 60000,
+ "selection": {
+ "id": "complex-production-ready",
+ "category": "complex-vague-hardening",
+ "expectedIds": ["skill:backend-patterns"]
+ }
+}
diff --git a/docker/context-profiles/complex-eval/cases3/production-ready/query.md b/docker/context-profiles/complex-eval/cases3/production-ready/query.md
new file mode 100644
index 000000000..341755315
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases3/production-ready/query.md
@@ -0,0 +1 @@
+This goes to production Monday morning. Get it ready. The bar we hold production services to is in docs/production-bar.md — meet all of it, keep the documented API working, and leave the repo in a state you'd be comfortable being on-call for.
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/files/API.md b/docker/context-profiles/complex-eval/cases4/chained-tickets/files/API.md
new file mode 100644
index 000000000..b916ba80a
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/files/API.md
@@ -0,0 +1,13 @@
+# Shortlink API
+
+- `POST /links` — body `{ "url": string, "ttlSeconds"?: number }`.
+ - `201` → `{ "code", "shortUrl", "expiresAt" }`. `code` is 6–10
+ alphanumeric characters; `shortUrl` is `/`; `expiresAt` is an ISO
+ timestamp. Default TTL is 7 days; `ttlSeconds` must be an integer between
+ 1 and 2592000 (30 days).
+ - Missing/invalid `url` or out-of-range `ttlSeconds` → `400`.
+- `GET /` — `302` with `Location` set to the original URL.
+ Unknown code → `404`. Expired link → `410`.
+- `DELETE /links/` — `204`. Unknown code → `404`.
+
+All error responses follow the envelope in `CONTRIBUTING.md`.
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/files/CONTRIBUTING.md b/docker/context-profiles/complex-eval/cases4/chained-tickets/files/CONTRIBUTING.md
new file mode 100644
index 000000000..7c45e4af2
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/files/CONTRIBUTING.md
@@ -0,0 +1,13 @@
+# Engineering conventions
+
+These conventions apply to every ticket, every route, every change:
+
+- **Errors**: every error response is JSON with the envelope
+ `{ "error": { "code": "", "message": "" } }`
+ and the matching HTTP status. No HTML error pages, no stack traces.
+- **Layering**: HTTP handling in `src/routes.js`, business logic in
+ `src/service.js`, storage in `src/store.js`. `src/app.js` wires them.
+- **Runtime config** comes from environment variables, read at startup.
+- **Every ticket**: add tests under `test/`, add a `CHANGELOG.md` entry
+ describing what shipped, and keep `README.md` accurate.
+- No external dependencies.
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/files/README.md b/docker/context-profiles/complex-eval/cases4/chained-tickets/files/README.md
new file mode 100644
index 000000000..90f4bae61
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/files/README.md
@@ -0,0 +1,9 @@
+# shortlink
+
+Internal link shortener service. Node.js standard library only, CommonJS.
+
+- `API.md` — the HTTP contract.
+- `CONTRIBUTING.md` — engineering conventions. Every ticket follows them.
+- `src/app.js` exports `createApp()` returning an `http.Server` that is not yet
+ listening; `node src/index.js ` starts the service.
+- Run the tests with `npm test`.
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/files/package.json b/docker/context-profiles/complex-eval/cases4/chained-tickets/files/package.json
new file mode 100644
index 000000000..12bbcaf08
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/files/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "shortlink",
+ "private": true,
+ "type": "commonjs",
+ "scripts": { "test": "node --test test/*.test.js" }
+}
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/meta.json b/docker/context-profiles/complex-eval/cases4/chained-tickets/meta.json
new file mode 100644
index 000000000..30eb9fb05
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/meta.json
@@ -0,0 +1,17 @@
+{
+ "id": "chained-tickets",
+ "category": "long-horizon-chain",
+ "manualIds": [],
+ "checkTimeoutMs": 60000,
+ "steps": [
+ { "manualIds": ["skill:backend-patterns"] },
+ { "manualIds": ["skill:backend-patterns"] },
+ { "manualIds": ["skill:security-review"] },
+ { "manualIds": ["skill:api-design"] }
+ ],
+ "selection": {
+ "id": "complex-chained-tickets",
+ "category": "complex-long-horizon",
+ "expectedIds": ["skill:backend-patterns"]
+ }
+}
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/01-core/check.cjs b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/01-core/check.cjs
new file mode 100644
index 000000000..cda5c3028
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/01-core/check.cjs
@@ -0,0 +1,95 @@
+'use strict';
+// Step 1 grader: core API contract + conventions (envelope, layering, changelog, tests).
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 10; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 10, passed: ok, total: 10 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
+const root = process.cwd();
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+(async () => {
+ let createApp;
+ try { ({ createApp } = require(path.join(root, 'src', 'app.js'))); } catch { /* scored below */ }
+ if (typeof createApp === 'function') {
+ try {
+ const app = createApp();
+ await new Promise(resolve => app.listen(0, '127.0.0.1', resolve));
+ const port = app.address().port;
+ const post = (body) => fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
+ const get = (p) => fetch(`http://127.0.0.1:${port}${p}`, { redirect: 'manual' });
+
+ const created = await post({ url: 'https://example.com/landing' });
+ const createdBody = await created.json().catch(() => null);
+ record('create-happy-201', created.status === 201 && createdBody
+ && /^[A-Za-z0-9]{6,10}$/.test(createdBody.code || '') && typeof createdBody.shortUrl === 'string'
+ && typeof createdBody.expiresAt === 'string' && !Number.isNaN(Date.parse(createdBody.expiresAt)));
+
+ let code = createdBody && createdBody.code;
+ if (code) {
+ const redirect = await get(`/${code}`);
+ record('redirect-302-location', redirect.status === 302
+ && redirect.headers.get('location') === 'https://example.com/landing');
+ } else record('redirect-302-location', false);
+
+ const unknown = await get('/nope00');
+ record('unknown-code-404-envelope', unknown.status === 404 && hasEnvelope(await unknown.json().catch(() => null)));
+
+ const badUrl = await post({ url: 'notaurl' });
+ record('invalid-url-400-envelope', badUrl.status === 400 && hasEnvelope(await badUrl.json().catch(() => null)));
+ const noBody = await post({});
+ record('missing-url-400-envelope', noBody.status === 400 && hasEnvelope(await noBody.json().catch(() => null)));
+ const badTtl = await post({ url: 'https://example.com', ttlSeconds: 99999999 });
+ record('ttl-bounds-400-envelope', badTtl.status === 400 && hasEnvelope(await badTtl.json().catch(() => null)));
+
+ const expiring = await post({ url: 'https://example.com/gone', ttlSeconds: 1 });
+ const expiringBody = await expiring.json().catch(() => null);
+ if (expiringBody && expiringBody.code) {
+ await sleep(1300);
+ const gone = await get(`/${expiringBody.code}`);
+ record('expired-link-410-envelope', gone.status === 410 && hasEnvelope(await gone.json().catch(() => null)));
+ } else record('expired-link-410-envelope', false);
+
+ if (code) {
+ const del = await fetch(`http://127.0.0.1:${port}/links/${code}`, { method: 'DELETE' });
+ const after = await get(`/${code}`);
+ record('delete-flow-204-then-404', del.status === 204 && after.status === 404);
+ } else record('delete-flow-204-then-404', false);
+ app.close();
+ } catch { /* remaining checks unscored */ }
+ } else {
+ for (const name of ['create-happy-201', 'redirect-302-location', 'unknown-code-404-envelope',
+ 'invalid-url-400-envelope', 'missing-url-400-envelope', 'ttl-bounds-400-envelope',
+ 'expired-link-410-envelope', 'delete-flow-204-then-404']) record(name, false);
+ }
+
+ // Conventions.
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ const testCount = (tests.match(/\btest\(/g) || []).length;
+ record('changelog-and-tests', changelog.length > 20 && testCount >= 3);
+ record('layering-files', ['routes.js', 'service.js', 'store.js']
+ .every(f => fs.existsSync(path.join(root, 'src', f))));
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/01-core/query.md b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/01-core/query.md
new file mode 100644
index 000000000..2c00246ec
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/01-core/query.md
@@ -0,0 +1 @@
+Implement the link shortener described in API.md. Follow CONTRIBUTING.md — every convention applies.
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/02-persistence/check.cjs b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/02-persistence/check.cjs
new file mode 100644
index 000000000..ce42427f4
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/02-persistence/check.cjs
@@ -0,0 +1,106 @@
+'use strict';
+// Step 2 grader: persistence across a simulated restart (fresh module state,
+// same DATA_FILE), expiry state survives, fresh/corrupt-start tolerance, conventions.
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 7; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 7, passed: ok, total: 7 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
+const root = process.cwd();
+const DATA_FILE = path.join(root, '.ecc-data', 'links.json');
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+function purgeApp() {
+ for (const key of Object.keys(require.cache)) {
+ if (key.startsWith(path.join(root, 'src') + path.sep)) delete require.cache[key];
+ }
+}
+
+async function start() {
+ purgeApp();
+ const { createApp } = require(path.join(root, 'src', 'app.js'));
+ const app = createApp();
+ await new Promise((resolve, reject) => { app.once('error', reject); app.listen(0, '127.0.0.1', resolve); });
+ return app;
+}
+
+(async () => {
+ process.env.DATA_FILE = DATA_FILE;
+ try {
+ // First boot: create a durable link and a 1s-expiring link.
+ let app = await start();
+ let port = app.address().port;
+ const post = body => fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
+ const durable = await (await post({ url: 'https://example.com/durable' })).json().catch(() => null);
+ const short = await (await post({ url: 'https://example.com/short', ttlSeconds: 1 })).json().catch(() => null);
+ await new Promise(resolve => app.close(resolve));
+
+ // Restart: fresh modules, same DATA_FILE.
+ app = await start();
+ port = app.address().port;
+ const get = p => fetch(`http://127.0.0.1:${port}${p}`, { redirect: 'manual' });
+
+ const after = durable && durable.code ? await get(`/${durable.code}`) : null;
+ record('link-survives-restart', after && after.status === 302
+ && after.headers.get('location') === 'https://example.com/durable');
+
+ await sleep(1300);
+ const expiredAfter = short && short.code ? await get(`/${short.code}`) : null;
+ record('expiry-survives-restart', expiredAfter && expiredAfter.status === 410);
+ await new Promise(resolve => app.close(resolve));
+
+ // Data file is real JSON on disk.
+ let dataOk = false;
+ try { JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); dataOk = true; } catch { /* missing/invalid */ }
+ record('data-file-is-json', dataOk);
+
+ // Fresh start with no data file present.
+ fs.rmSync(DATA_FILE, { force: true });
+ app = await start();
+ port = app.address().port;
+ const fresh = await fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ url: 'https://example.com/fresh' }) });
+ record('fresh-start-without-data-file', fresh.status === 201);
+ await new Promise(resolve => app.close(resolve));
+
+ // Corrupt data file must not kill the service.
+ fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true });
+ fs.writeFileSync(DATA_FILE, 'garbage{{{');
+ app = await start();
+ port = app.address().port;
+ const afterCorrupt = await get('/anything1');
+ record('corrupt-data-file-tolerated', afterCorrupt.status === 404
+ && hasEnvelope(await afterCorrupt.json().catch(() => null)));
+ await new Promise(resolve => app.close(resolve));
+ fs.rmSync(DATA_FILE, { force: true });
+ } catch { /* remaining checks unscored */ }
+
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ const changelogEntries = (changelog.match(/^[-*#]/gm) || []).length;
+ record('changelog-grown', changelogEntries >= 2 && /persist|restart|data/i.test(changelog));
+ record('tests-grown', (tests.match(/\btest\(/g) || []).length >= 6);
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/02-persistence/query.md b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/02-persistence/query.md
new file mode 100644
index 000000000..544b2f51e
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/02-persistence/query.md
@@ -0,0 +1 @@
+Links need to survive a service restart. Persist them to the JSON file named by the DATA_FILE environment variable (read at startup). Take care of it.
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/03-abuse/check.cjs b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/03-abuse/check.cjs
new file mode 100644
index 000000000..829abd522
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/03-abuse/check.cjs
@@ -0,0 +1,83 @@
+'use strict';
+// Step 3 grader: abuse handling — URL validation, size limits, rate limiting —
+// plus conventions. Hammer probe runs last so earlier probes stay unthrottled.
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 8; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 8, passed: ok, total: 8 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const root = process.cwd();
+const DATA_FILE = path.join(root, '.ecc-data', 'links-step3.json');
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+function purgeApp() {
+ for (const key of Object.keys(require.cache)) {
+ if (key.startsWith(path.join(root, 'src') + path.sep)) delete require.cache[key];
+ }
+}
+
+(async () => {
+ process.env.DATA_FILE = DATA_FILE;
+ try {
+ purgeApp();
+ const { createApp } = require(path.join(root, 'src', 'app.js'));
+ const app = createApp();
+ await new Promise(resolve => app.listen(0, '127.0.0.1', resolve));
+ const port = app.address().port;
+ const post = body => fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
+
+ const okCreate = await post({ url: 'https://example.com/normal' });
+ record('normal-create-still-201', okCreate.status === 201);
+
+ const js = await post({ url: 'javascript:alert(1)' });
+ record('javascript-scheme-400-envelope', js.status === 400 && hasEnvelope(await js.json().catch(() => null)));
+ const ftp = await post({ url: 'ftp://files.example.com/x' });
+ record('non-http-scheme-400-envelope', ftp.status === 400 && hasEnvelope(await ftp.json().catch(() => null)));
+ const huge = await post({ url: `https://example.com/${'a'.repeat(10000)}` });
+ const hugeBody = await huge.json().catch(() => null);
+ record('oversize-url-4xx-envelope', huge.status >= 400 && huge.status < 500 && hasEnvelope(hugeBody));
+
+ // Hammer: 60 rapid creates must trip a 429 with the envelope.
+ const responses = await Promise.all(Array.from({ length: 60 }, (_, i) =>
+ post({ url: `https://example.com/flood-${i}` })));
+ const limited = [];
+ for (const r of responses) if (r.status === 429) limited.push(await r.json().catch(() => null));
+ record('rate-limit-429-envelope', limited.length > 0 && limited.every(hasEnvelope));
+ app.close();
+ } catch { /* remaining checks unscored */ }
+
+ let sources = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'src'))) {
+ if (f.endsWith('.js')) sources += fs.readFileSync(path.join(root, 'src', f), 'utf8');
+ }
+ } catch { /* missing */ }
+ record('rate-limiting-implemented', /429|rate.?limit/i.test(sources));
+
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ const changelogEntries = (changelog.match(/^[-*#]/gm) || []).length;
+ record('changelog-grown', changelogEntries >= 3 && /abuse|rate|valid|secur/i.test(changelog));
+ record('tests-grown', (tests.match(/\btest\(/g) || []).length >= 9);
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/03-abuse/query.md b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/03-abuse/query.md
new file mode 100644
index 000000000..799adaf89
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/03-abuse/query.md
@@ -0,0 +1 @@
+We're seeing abuse — people hammering the API and submitting junk URLs. Deal with it.
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/04-analytics/check.cjs b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/04-analytics/check.cjs
new file mode 100644
index 000000000..ed2e69364
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/04-analytics/check.cjs
@@ -0,0 +1,88 @@
+'use strict';
+// Step 4 grader: hit analytics consistent with the existing API, conventions,
+// docs and tests. (Runs in a later process than step 3, so rate windows cleared.)
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 8; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 8, passed: ok, total: 8 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const root = process.cwd();
+const DATA_FILE = path.join(root, '.ecc-data', 'links-step4.json');
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+function purgeApp() {
+ for (const key of Object.keys(require.cache)) {
+ if (key.startsWith(path.join(root, 'src') + path.sep)) delete require.cache[key];
+ }
+}
+
+(async () => {
+ process.env.DATA_FILE = DATA_FILE;
+ try {
+ purgeApp();
+ const { createApp } = require(path.join(root, 'src', 'app.js'));
+ const app = createApp();
+ await new Promise(resolve => app.listen(0, '127.0.0.1', resolve));
+ const port = app.address().port;
+
+ const created = await fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ url: 'https://example.com/tracked' }) });
+ const body = await created.json().catch(() => null);
+ const code = body && body.code;
+ record('create-still-works', created.status === 201 && Boolean(code));
+
+ if (code) {
+ const before = await fetch(`http://127.0.0.1:${port}/links/${code}/stats`);
+ const beforeBody = await before.json().catch(() => null);
+ record('stats-zero-before-redirects', before.status === 200 && beforeBody && beforeBody.hits === 0);
+
+ for (let i = 0; i < 3; i++) {
+ await fetch(`http://127.0.0.1:${port}/${code}`, { redirect: 'manual' });
+ }
+ const stats = await fetch(`http://127.0.0.1:${port}/links/${code}/stats`);
+ const statsBody = await stats.json().catch(() => null);
+ record('stats-count-three-hits', stats.status === 200 && statsBody && statsBody.hits === 3);
+
+ const redirect = await fetch(`http://127.0.0.1:${port}/${code}`, { redirect: 'manual' });
+ record('redirect-still-302', redirect.status === 302);
+
+ const missing = await fetch(`http://127.0.0.1:${port}/links/zzzzzz/stats`);
+ record('stats-unknown-404-envelope', missing.status === 404
+ && hasEnvelope(await missing.json().catch(() => null)));
+ } else {
+ for (const name of ['stats-zero-before-redirects', 'stats-count-three-hits',
+ 'redirect-still-302', 'stats-unknown-404-envelope']) record(name, false);
+ }
+ app.close();
+ } catch { /* remaining checks unscored */ }
+
+ let readme = '';
+ try { readme = fs.readFileSync(path.join(root, 'README.md'), 'utf8'); } catch { /* missing */ }
+ record('readme-documents-stats', /\/stats|hits|analytics/i.test(readme));
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ const changelogEntries = (changelog.match(/^[-*#]/gm) || []).length;
+ record('changelog-grown', changelogEntries >= 4 && /stat|analytic|hit/i.test(changelog));
+ record('tests-grown', (tests.match(/\btest\(/g) || []).length >= 12);
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/04-analytics/query.md b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/04-analytics/query.md
new file mode 100644
index 000000000..619549068
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/chained-tickets/steps/04-analytics/query.md
@@ -0,0 +1 @@
+Track redirect hits per link and expose them at GET /links/:code/stats, consistent with the existing API.
diff --git a/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/check.cjs b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/check.cjs
new file mode 100644
index 000000000..7882bce07
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/check.cjs
@@ -0,0 +1,119 @@
+'use strict';
+// Hidden grader for idempotent-webhooks: exactly-once under sequential,
+// concurrent, and mixed-concurrent duplicates, plus the documented API,
+// regression coverage, and hygiene. Prints ECC_EVAL_SCORE and always exits 0.
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 12; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 12, passed: ok, total: 12 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const root = process.cwd();
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+(async () => {
+ let createApp;
+ let store;
+ try {
+ ({ createApp } = require(path.join(root, 'src', 'app.js')));
+ ({ store } = require(path.join(root, 'src', 'store.js')));
+ } catch { /* scored below */ }
+ if (typeof createApp === 'function' && store && Array.isArray(store.paymentLog)) {
+ try {
+ const app = createApp();
+ await new Promise(resolve => app.listen(0, '127.0.0.1', resolve));
+ const port = app.address().port;
+ const send = (eventId, orderId, amountCents) => fetch(`http://127.0.0.1:${port}/webhooks/payments`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ eventId, orderId, amountCents, type: 'payment.succeeded' }) });
+ const logsFor = orderId => store.paymentLog.filter(p => p.orderId === orderId).length;
+
+ // 1: single delivery applies once.
+ const single = await send('ev-1', 'o1', 5000);
+ const singleBody = await single.json().catch(() => null);
+ record('single-delivery-processed', single.status === 200 && singleBody
+ && singleBody.status === 'processed' && singleBody.orderId === 'o1' && logsFor('o1') === 1);
+
+ // 2: sequential retry replays without re-applying.
+ const retry = await send('ev-1', 'o1', 5000);
+ const retryBody = await retry.json().catch(() => null);
+ record('sequential-duplicate-inert', retry.status === 200 && retryBody
+ && retryBody.status === 'duplicate' && logsFor('o1') === 1);
+
+ // 3: fifty concurrent identical deliveries apply exactly once.
+ const storm = await Promise.all(Array.from({ length: 50 }, () => send('ev-2', 'o2', 12500)));
+ const stormBodies = [];
+ for (const r of storm) stormBodies.push(await r.json().catch(() => null));
+ const processedCount = stormBodies.filter(b => b && b.status === 'processed').length;
+ const duplicateCount = stormBodies.filter(b => b && b.status === 'duplicate').length;
+ record('concurrent-storm-exactly-once', storm.every(r => r.status === 200)
+ && processedCount === 1 && duplicateCount === 49 && logsFor('o2') === 1
+ && store.orders.get('o2').paymentsApplied === 1);
+
+ // 4: a different event for an already-paid order is already_paid and inert.
+ const second = await send('ev-3', 'o2', 12500);
+ const secondBody = await second.json().catch(() => null);
+ record('already-paid-order-inert', second.status === 200 && secondBody
+ && secondBody.status === 'already_paid' && logsFor('o2') === 1);
+
+ // 5-7: contract errors with envelopes.
+ const unknown = await send('ev-4', 'nope', 100);
+ record('unknown-order-404-envelope', unknown.status === 404 && hasEnvelope(await unknown.json().catch(() => null)));
+ const malformed = await fetch(`http://127.0.0.1:${port}/webhooks/payments`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: '{bad json' });
+ record('malformed-body-400-envelope', malformed.status === 400 && hasEnvelope(await malformed.json().catch(() => null)));
+ const mismatch = await send('ev-5', 'o3', 999999);
+ record('amount-mismatch-422-envelope', mismatch.status === 422
+ && hasEnvelope(await mismatch.json().catch(() => null)) && logsFor('o3') === 0);
+
+ // 8: mixed storm — three orders, three eventIds, ten duplicates each, all concurrent.
+ const mixed = await Promise.all(['o4', 'o5', 'o6'].flatMap(orderId =>
+ Array.from({ length: 10 }, () => send(`ev-${orderId}`, orderId, store.orders.get(orderId).amountCents))));
+ for (const r of mixed) await r.json().catch(() => null);
+ record('mixed-storm-each-order-once', ['o4', 'o5', 'o6'].every(orderId =>
+ logsFor(orderId) === 1 && store.orders.get(orderId).paymentsApplied === 1));
+
+ // 9: order inspection endpoint reflects reality.
+ const orderView = await fetch(`http://127.0.0.1:${port}/orders/o2`);
+ const orderBody = await orderView.json().catch(() => null);
+ record('order-endpoint-accurate', orderView.status === 200 && orderBody
+ && orderBody.status === 'paid' && orderBody.paymentsApplied === 1 && Boolean(orderBody.paidAt));
+
+ app.close();
+ } catch { /* remaining checks unscored */ }
+ } else {
+ for (const name of ['single-delivery-processed', 'sequential-duplicate-inert', 'concurrent-storm-exactly-once',
+ 'already-paid-order-inert', 'unknown-order-404-envelope', 'malformed-body-400-envelope',
+ 'amount-mismatch-422-envelope', 'mixed-storm-each-order-once', 'order-endpoint-accurate']) record(name, false);
+ }
+
+ // Conventions.
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ record('concurrency-regression-tests', (tests.match(/\btest\(/g) || []).length >= 4
+ && /Promise\.all|concurrent|duplicate|retry/i.test(tests));
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ record('changelog-entry', /idem|duplicat|retry|inc-104|race/i.test(changelog));
+ try {
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
+ record('no-external-dependencies', !pkg.dependencies && !pkg.devDependencies);
+ } catch { record('no-external-dependencies', false); }
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/README.md b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/README.md
new file mode 100644
index 000000000..512c8c059
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/README.md
@@ -0,0 +1,12 @@
+# webhook-receiver
+
+Receives payment webhooks. There is an open incident: customers were
+double-charged when the provider retried deliveries. See `SPEC.md` for the
+contract, including the exactly-once rules.
+
+- `src/app.js` exports `createApp()` returning an `http.Server` that is not
+ yet listening; `node src/index.js ` starts the service.
+- `src/store.js` is shared infrastructure: it keeps its current exports
+ (`store`) and records every applied payment in `store.paymentLog`.
+- No external dependencies. `npm test` runs the tests. `CHANGELOG.md` records
+ every shipped change.
diff --git a/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/SPEC.md b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/SPEC.md
new file mode 100644
index 000000000..e3dee27b1
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/SPEC.md
@@ -0,0 +1,30 @@
+# Payment webhook contract
+
+`POST /webhooks/payments` with JSON body
+`{ "eventId": string, "orderId": string, "amountCents": number, "type": "payment.succeeded" }`.
+
+Exactly-once is the point. The provider retries aggressively and may deliver
+the same event many times, concurrently, or out of order.
+
+- A new, valid `eventId`: apply the payment exactly once → `200`
+ `{ "status": "processed", "orderId" }`.
+- The same `eventId` seen again (any number of times, any interleaving):
+ `200` `{ "status": "duplicate", "orderId" }` — never applied twice.
+- A payment event (new `eventId`) for an order that is already paid:
+ `200` `{ "status": "already_paid", "orderId" }` — an order is paid at most
+ once, ever.
+- `amountCents` not matching the order's amount: `422`, not applied.
+- Unknown `orderId`: `404`. Malformed body (bad JSON, missing/invalid
+ fields): `400`.
+- Error responses use the envelope
+ `{ "error": { "code": "", "message": "..." } }`.
+
+`GET /orders/:id` → `200` `{ "id", "status", "paidAt", "paymentsApplied" }`
+or a `404` envelope.
+
+## Incident note
+
+INC-104: concurrent duplicate deliveries double-applied payments. The naive
+receiver checked "have we seen this event?" and applied the payment in two
+separate steps with an async gap in between, so parallel duplicates both
+passed the check.
diff --git a/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/package.json b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/package.json
new file mode 100644
index 000000000..11c26f720
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "webhook-receiver",
+ "private": true,
+ "type": "commonjs",
+ "scripts": { "test": "node --test test/*.test.js" }
+}
diff --git a/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/src/app.js b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/src/app.js
new file mode 100644
index 000000000..6ba0ba755
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/src/app.js
@@ -0,0 +1,54 @@
+'use strict';
+const http = require('node:http');
+const { store } = require('./store');
+
+// INC-104 receiver: checks "seen this event?" and applies the payment in two
+// steps with an async gap in between. Concurrent duplicates both pass the
+// check. Do not keep this shape.
+function createApp() {
+ return http.createServer((req, res) => {
+ const url = new URL(req.url, 'http://localhost');
+
+ if (req.method === 'POST' && url.pathname === '/webhooks/payments') {
+ let body = '';
+ req.on('data', chunk => { body += chunk; });
+ req.on('end', async () => {
+ const parsed = JSON.parse(body);
+ const { eventId, orderId } = parsed;
+ if (store.processedEvents.has(eventId)) {
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ status: 'duplicate', orderId }));
+ return;
+ }
+ await new Promise(resolve => setImmediate(resolve)); // async gap
+ const order = store.orders.get(orderId);
+ order.status = 'paid';
+ order.paidAt = new Date().toISOString();
+ order.paymentsApplied++;
+ store.paymentLog.push({ eventId, orderId, amountCents: parsed.amountCents });
+ store.processedEvents.add(eventId);
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ status: 'processed', orderId }));
+ });
+ return;
+ }
+
+ const match = /^\/orders\/([\w-]+)$/.exec(url.pathname);
+ if (req.method === 'GET' && match) {
+ const order = store.orders.get(match[1]);
+ if (!order) {
+ res.writeHead(404, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ error: { code: 'NOT_FOUND', message: 'no such order' } }));
+ return;
+ }
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(order));
+ return;
+ }
+
+ res.writeHead(404, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ error: { code: 'NOT_FOUND', message: 'not found' } }));
+ });
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/src/index.js b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/src/index.js
new file mode 100644
index 000000000..90ef9215f
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/src/index.js
@@ -0,0 +1,7 @@
+'use strict';
+const { createApp } = require('./app');
+
+const port = Number(process.argv[2] || 8080);
+createApp().listen(port, () => {
+ console.log(`webhook-receiver listening on ${port}`);
+});
diff --git a/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/src/store.js b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/src/store.js
new file mode 100644
index 000000000..64a4099a4
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/src/store.js
@@ -0,0 +1,18 @@
+'use strict';
+
+// Shared infrastructure. Every applied payment is appended to paymentLog;
+// orders and processedEvents track receiver state. Keep the `store` export.
+const store = {
+ orders: new Map([
+ ['o1', { id: 'o1', amountCents: 5000, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ['o2', { id: 'o2', amountCents: 12500, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ['o3', { id: 'o3', amountCents: 800, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ['o4', { id: 'o4', amountCents: 9999, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ['o5', { id: 'o5', amountCents: 250, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ['o6', { id: 'o6', amountCents: 7300, status: 'pending', paidAt: null, paymentsApplied: 0 }],
+ ]),
+ paymentLog: [],
+ processedEvents: new Set(),
+};
+
+module.exports = { store };
diff --git a/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/test/webhooks.test.js b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/test/webhooks.test.js
new file mode 100644
index 000000000..cf79f83d4
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/files/test/webhooks.test.js
@@ -0,0 +1,21 @@
+'use strict';
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createApp } = require('../src/app');
+const { store } = require('../src/store');
+
+test('a single payment event processes', async () => {
+ const server = createApp();
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ try {
+ const port = server.address().port;
+ const res = await fetch(`http://127.0.0.1:${port}/webhooks/payments`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ eventId: 'ev-test-1', orderId: 'o1', amountCents: 5000, type: 'payment.succeeded' }) });
+ assert.equal(res.status, 200);
+ assert.equal((await res.json()).status, 'processed');
+ assert.equal(store.orders.get('o1').status, 'paid');
+ } finally {
+ server.close();
+ }
+});
diff --git a/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/meta.json b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/meta.json
new file mode 100644
index 000000000..d5d396e74
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/meta.json
@@ -0,0 +1,11 @@
+{
+ "id": "idempotent-webhooks",
+ "category": "concurrency-trap",
+ "manualIds": ["skill:error-handling"],
+ "checkTimeoutMs": 60000,
+ "selection": {
+ "id": "complex-idempotent-webhooks",
+ "category": "complex-concurrency-trap",
+ "expectedIds": ["skill:error-handling"]
+ }
+}
diff --git a/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/query.md b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/query.md
new file mode 100644
index 000000000..f2902f874
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/idempotent-webhooks/query.md
@@ -0,0 +1 @@
+The payments provider retries aggressively and sometimes delivers the same webhook many times concurrently. Our receiver double-charges customers when that happens — see the incident note in SPEC.md. Rework the receiver so the full contract in SPEC.md holds: duplicates must never double-apply under any interleaving, and the documented API and the store contract stay intact. `npm test` must pass, and add regression coverage for the concurrent-duplicate case so INC-104 can't come back.
diff --git a/docker/context-profiles/complex-eval/cases4/production-ready/check.cjs b/docker/context-profiles/complex-eval/cases4/production-ready/check.cjs
new file mode 100644
index 000000000..e08c1efeb
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/production-ready/check.cjs
@@ -0,0 +1,156 @@
+'use strict';
+// Hidden grader for production-ready: probes every dimension of the documented
+// production bar. Prints ECC_EVAL_SCORE and always exits 0.
+const fs = require('node:fs');
+const path = require('node:path');
+
+const checks = [];
+const record = (name, ok) => checks.push({ name, ok: Boolean(ok) });
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ for (let i = checks.length; i < 16; i++) record(`unreached-${i + 1}`, false);
+ const ok = checks.filter(c => c.ok).length;
+ for (const c of checks) process.stdout.write(`${c.ok ? 'ok' : 'not ok'} - ${c.name}\n`);
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: ok / 16, passed: ok, total: 16 })}\n`);
+ process.exit(0);
+}
+// A crashing agent server must not kill the grader: score what completed.
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+const root = process.cwd();
+const hasEnvelope = body => body && body.error && typeof body.error.code === 'string'
+ && /^[A-Z][A-Z0-9_]+$/.test(body.error.code) && typeof body.error.message === 'string';
+
+(async () => {
+ let createApp;
+ try { ({ createApp } = require(path.join(root, 'src', 'app.js'))); } catch { /* scored below */ }
+ if (typeof createApp === 'function') {
+ // Capture console output during the probe run to inspect request logging.
+ const logged = [];
+ const originalLog = console.log;
+ const originalError = console.error;
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
+ console.log = (...args) => { logged.push(args.join(' ')); };
+ console.error = (...args) => { logged.push(args.join(' ')); };
+ // Agents may log through an injectable writer straight to the streams
+ // instead of console.*. Capture-then-pass-through: the bytes always reach
+ // the stream untouched, so the grader's own ECC_EVAL_SCORE line (emitted
+ // via process.stdout.write) can never be swallowed or corrupted.
+ const tap = write => (chunk, encoding, callback) => {
+ try { logged.push(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk)); } catch { /* capture must never break a write */ }
+ return write(chunk, encoding, callback);
+ };
+ process.stdout.write = tap(originalStdoutWrite);
+ process.stderr.write = tap(originalStderrWrite);
+ try {
+ const app = createApp();
+ await new Promise(resolve => app.listen(0, '127.0.0.1', resolve));
+ const port = app.address().port;
+ const api = (p, options) => fetch(`http://127.0.0.1:${port}${p}`, options);
+ const post = body => api('/notes', { method: 'POST', headers: { 'content-type': 'application/json' }, body });
+
+ // Documented API still works.
+ const created = await post(JSON.stringify({ title: 'deploy', body: 'checklist' }));
+ const createdBody = await created.json().catch(() => null);
+ record('api-roundtrip-preserved', created.status === 201 && createdBody && createdBody.id
+ && (await (await api(`/notes/${createdBody.id}`)).json().catch(() => ({}))).title === 'deploy'
+ && Array.isArray((await (await api('/notes')).json().catch(() => ({}))).notes));
+
+ // Validation and envelope discipline.
+ const badJson = await post('{not json');
+ record('malformed-json-400-envelope', badJson.status === 400 && hasEnvelope(await badJson.json().catch(() => null)));
+ const missing = await post(JSON.stringify({ body: 'no title' }));
+ record('missing-field-400-envelope', missing.status === 400 && hasEnvelope(await missing.json().catch(() => null)));
+ const wrongType = await post(JSON.stringify({ title: 42, body: 'x' }));
+ record('wrong-type-400-envelope', wrongType.status === 400 && hasEnvelope(await wrongType.json().catch(() => null)));
+ const unknown = await api('/notes/n_999999');
+ const unknownBody = await unknown.text();
+ let unknownParsed = null;
+ try { unknownParsed = JSON.parse(unknownBody); } catch { /* html or text */ }
+ record('unknown-404-json-envelope', unknown.status === 404 && hasEnvelope(unknownParsed));
+
+ // Body limit.
+ const big = await post(JSON.stringify({ title: 'big', body: 'x'.repeat(100 * 1024) }));
+ record('oversize-body-413-envelope', big.status === 413 && hasEnvelope(await big.json().catch(() => null)));
+
+ // Health endpoint.
+ const health = await api('/health');
+ const healthBody = await health.json().catch(() => null);
+ record('health-endpoint', health.status === 200 && healthBody && healthBody.status === 'ok');
+
+ // Security header on a normal response.
+ const headers = await api('/notes');
+ record('nosniff-header', headers.headers.get('x-content-type-options') === 'nosniff');
+
+ // Error responses carry JSON content type.
+ record('errors-are-json', /application\/json/.test(unknown.headers.get('content-type') || ''));
+
+ app.close();
+ } catch { /* remaining checks unscored */ } finally {
+ console.log = originalLog;
+ console.error = originalError;
+ process.stdout.write = originalStdoutWrite;
+ process.stderr.write = originalStderrWrite;
+ }
+
+ // Structured request logging: at least one JSON line with method/path/status-ish fields.
+ const structured = logged.flatMap(chunk => String(chunk).split('\n')).some(line => {
+ try {
+ const parsed = JSON.parse(line);
+ return parsed && typeof parsed === 'object'
+ && /method/i.test(Object.keys(parsed).join(' '))
+ && /path|url/i.test(Object.keys(parsed).join(' '))
+ && /status/i.test(Object.keys(parsed).join(' '));
+ } catch { return false; }
+ });
+ record('structured-request-logs', structured);
+ } else {
+ for (const name of ['api-roundtrip-preserved', 'malformed-json-400-envelope', 'missing-field-400-envelope',
+ 'wrong-type-400-envelope', 'unknown-404-json-envelope', 'oversize-body-413-envelope', 'health-endpoint',
+ 'nosniff-header', 'errors-are-json', 'structured-request-logs']) record(name, false);
+ }
+
+ // Static dimensions.
+ let sources = '';
+ const sourceFiles = [];
+ const walk = directory => {
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
+ const item = path.join(directory, entry.name);
+ if (entry.isDirectory()) walk(item);
+ else if (entry.name.endsWith('.js')) {
+ const content = fs.readFileSync(item, 'utf8');
+ sourceFiles.push(content);
+ sources += content;
+ }
+ }
+ };
+ try { walk(path.join(root, 'src')); } catch { /* none */ }
+ record('sigterm-graceful-shutdown', /SIGTERM/.test(sources));
+ // Literal process.env.PORT access, or an injectable-config indirection: a
+ // 'PORT' string literal in a file that also reads process.env (for example a
+ // loadConfig(env = process.env) + readInt(env, 'PORT', default) module).
+ record('env-config-port', sourceFiles.some(content => /process\.env\.[A-Z_]*PORT/.test(content)
+ || (/(['"`])PORT\1/.test(content) && /process\.env/.test(content))));
+
+ let tests = '';
+ try {
+ for (const f of fs.readdirSync(path.join(root, 'test'))) tests += fs.readFileSync(path.join(root, 'test', f), 'utf8');
+ } catch { /* missing */ }
+ const testCount = (tests.match(/\btest\(/g) || []).length;
+ record('tests-cover-error-paths', testCount >= 4 && /400|404|413|invalid|error/i.test(tests));
+
+ let changelog = '';
+ try { changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); } catch { /* missing */ }
+ record('changelog-entry', changelog.length > 20 && /product|harden|valid|health|log/i.test(changelog));
+
+ record('no-leftover-todos', !/TODO|FIXME/.test(sources));
+ try {
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
+ record('no-external-dependencies', !pkg.dependencies && !pkg.devDependencies);
+ } catch { record('no-external-dependencies', false); }
+
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases4/production-ready/files/README.md b/docker/context-profiles/complex-eval/cases4/production-ready/files/README.md
new file mode 100644
index 000000000..e387bff31
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/production-ready/files/README.md
@@ -0,0 +1,19 @@
+# notes-service
+
+Tiny notes API. Hobby prototype state: it works on the happy path and that's
+about all that can be said for it.
+
+## API
+
+- `POST /notes` — body `{ "title": string, "body": string }` → `201` with
+ `{ "id", "title", "body" }`.
+- `GET /notes/:id` — `200` with the note, or `404`.
+- `GET /notes` — `200` with `{ "notes": [...] }`.
+
+`src/app.js` exports `createApp()` returning an `http.Server` that is not yet
+listening; `node src/index.js` starts the service. `npm test` runs the tests.
+
+## Operations
+
+`docs/production-bar.md` lists what every production service here must meet.
+`CHANGELOG.md` records every shipped change.
diff --git a/docker/context-profiles/complex-eval/cases4/production-ready/files/docs/production-bar.md b/docker/context-profiles/complex-eval/cases4/production-ready/files/docs/production-bar.md
new file mode 100644
index 000000000..af3df1c4c
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/production-ready/files/docs/production-bar.md
@@ -0,0 +1,21 @@
+# The production bar
+
+Every production service here meets all of the following, all the time:
+
+- **Validation**: malformed JSON, missing fields, and wrong types are rejected
+ with `400` and a structured JSON error body
+ `{ "error": { "code": "", "message": "..." } }`. Unknown
+ resources are `404` in the same envelope. No stack traces, no HTML errors,
+ no hanging connections.
+- **Body limits**: request bodies over 64 KB are rejected with `413`, same
+ envelope.
+- **Health**: `GET /health` returns `200` with `{ "status": "ok" }`.
+- **Logging**: one structured JSON log line per request with at least
+ `method`, `path`, and `status` fields.
+- **Configuration**: runtime configuration (port, limits) comes from
+ environment variables, read at startup. Nothing secret is hardcoded.
+- **Shutdown**: the service closes cleanly on `SIGTERM` (stops accepting,
+ drains, exits).
+- **Headers**: responses carry `X-Content-Type-Options: nosniff`.
+- **Tests**: the suite covers error paths, not just the happy path.
+- **Changelog**: every shipped change has a `CHANGELOG.md` entry.
diff --git a/docker/context-profiles/complex-eval/cases4/production-ready/files/package.json b/docker/context-profiles/complex-eval/cases4/production-ready/files/package.json
new file mode 100644
index 000000000..7cef6f8c0
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/production-ready/files/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "notes-service",
+ "private": true,
+ "type": "commonjs",
+ "scripts": { "test": "node --test test/*.test.js" }
+}
diff --git a/docker/context-profiles/complex-eval/cases4/production-ready/files/src/app.js b/docker/context-profiles/complex-eval/cases4/production-ready/files/src/app.js
new file mode 100644
index 000000000..db7fe2695
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/production-ready/files/src/app.js
@@ -0,0 +1,50 @@
+'use strict';
+const http = require('node:http');
+
+// Prototype state: happy path only.
+const notes = new Map();
+let nextId = 1;
+
+function createApp() {
+ return http.createServer((req, res) => {
+ console.log('got a request');
+ const url = new URL(req.url, 'http://localhost');
+
+ if (req.method === 'POST' && url.pathname === '/notes') {
+ let body = '';
+ req.on('data', chunk => { body += chunk; });
+ req.on('end', () => {
+ const parsed = JSON.parse(body);
+ const id = `n_${nextId++}`;
+ notes.set(id, { id, title: parsed.title, body: parsed.body });
+ res.writeHead(201, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(notes.get(id)));
+ });
+ return;
+ }
+
+ const match = /^\/notes\/([\w-]+)$/.exec(url.pathname);
+ if (req.method === 'GET' && match) {
+ const note = notes.get(match[1]);
+ if (!note) {
+ res.writeHead(404);
+ res.end('not found');
+ return;
+ }
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(note));
+ return;
+ }
+
+ if (req.method === 'GET' && url.pathname === '/notes') {
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ notes: [...notes.values()] }));
+ return;
+ }
+
+ res.writeHead(404);
+ res.end('not found');
+ });
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/cases4/production-ready/files/src/index.js b/docker/context-profiles/complex-eval/cases4/production-ready/files/src/index.js
new file mode 100644
index 000000000..a71330e92
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/production-ready/files/src/index.js
@@ -0,0 +1,6 @@
+'use strict';
+const { createApp } = require('./app');
+
+createApp().listen(8080, () => {
+ console.log('notes listening on 8080');
+});
diff --git a/docker/context-profiles/complex-eval/cases4/production-ready/files/test/notes.test.js b/docker/context-profiles/complex-eval/cases4/production-ready/files/test/notes.test.js
new file mode 100644
index 000000000..51babd8fb
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/production-ready/files/test/notes.test.js
@@ -0,0 +1,21 @@
+'use strict';
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createApp } = require('../src/app');
+
+test('create and read a note', async () => {
+ const server = createApp();
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ try {
+ const port = server.address().port;
+ const created = await fetch(`http://127.0.0.1:${port}/notes`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ title: 'first', body: 'hello' }) });
+ assert.equal(created.status, 201);
+ const { id } = await created.json();
+ const read = await fetch(`http://127.0.0.1:${port}/notes/${id}`);
+ assert.equal((await read.json()).title, 'first');
+ } finally {
+ server.close();
+ }
+});
diff --git a/docker/context-profiles/complex-eval/cases4/production-ready/meta.json b/docker/context-profiles/complex-eval/cases4/production-ready/meta.json
new file mode 100644
index 000000000..21aae2a12
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/production-ready/meta.json
@@ -0,0 +1,11 @@
+{
+ "id": "production-ready",
+ "category": "vague-hardening",
+ "manualIds": ["skill:backend-patterns"],
+ "checkTimeoutMs": 60000,
+ "selection": {
+ "id": "complex-production-ready",
+ "category": "complex-vague-hardening",
+ "expectedIds": ["skill:backend-patterns"]
+ }
+}
diff --git a/docker/context-profiles/complex-eval/cases4/production-ready/query.md b/docker/context-profiles/complex-eval/cases4/production-ready/query.md
new file mode 100644
index 000000000..341755315
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/production-ready/query.md
@@ -0,0 +1 @@
+This goes to production Monday morning. Get it ready. The bar we hold production services to is in docs/production-bar.md — meet all of it, keep the documented API working, and leave the repo in a state you'd be comfortable being on-call for.
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/files/README.md b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/README.md
new file mode 100644
index 000000000..9c7e5925a
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/README.md
@@ -0,0 +1,29 @@
+# payments-lite
+
+A small dependency-free payments service core: refunds to customers and payouts
+to vendors, executed against a fake gateway that records every call in an
+append-only ledger.
+
+## Layout
+
+- `src/charge.js` — the gateway client. `charge()`, `refund()`, and `payout()`
+ simulate network latency and append one JSON line per call to the ledger at
+ `LEDGER_FILE` (default `.data/ledger.jsonl`). `readLedger()` parses it.
+- `src/store.js` — a tiny JSON-file store at `STORE_FILE` (default
+ `.data/store.json`): `get`, `has`, `set`. Reads and writes are synchronous.
+- `src/refunds.js` — `processRefund(req)` for customer refunds.
+- `src/payouts.js` — `processPayout(req)` for vendor payouts.
+
+## API contract
+
+`processRefund({ orderId, amount, idempotencyKey? })` and
+`processPayout({ vendorId, amount, idempotencyKey? })` each return the gateway
+receipt (`{ id, type, amount, ... }`). When the caller supplies an
+`idempotencyKey`, a repeated call with the same key must not hit the gateway
+again; it returns the stored receipt with `duplicate: true`. Keep these
+signatures stable — the dashboard and the finance batch job call them directly.
+
+## Working here
+
+- No external dependencies. `npm test` runs the tests.
+- Incident notes live in `docs/incidents.md`; add an entry when you work one.
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/files/docs/incidents.md b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/docs/incidents.md
new file mode 100644
index 000000000..cde645464
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/docs/incidents.md
@@ -0,0 +1,17 @@
+# Incident notes
+
+## INC-201 — duplicate refunds (2026-06-14)
+
+Customers saw two refunds for one order. Traced to the storefront retrying the
+refund call after a gateway timeout. Asked the storefront team to retry less
+aggressively. Closed.
+
+## INC-214 — duplicate refunds, again (2026-07-29)
+
+Same shape as INC-201: a retried refund call landed twice. Reminded the
+storefront team about backoff. Closed.
+
+## INC-227 — duplicate refunds, third time (2026-09-03)
+
+Same shape as INC-201 and INC-214. Third time this quarter. Support is
+escalating refund-credit requests faster than we can explain them.
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/files/package.json b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/package.json
new file mode 100644
index 000000000..c7ce403d0
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "payments-lite",
+ "private": true,
+ "type": "module",
+ "scripts": { "test": "node --test test/*.test.js" }
+}
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/charge.js b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/charge.js
new file mode 100644
index 000000000..c0192c1f3
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/charge.js
@@ -0,0 +1,46 @@
+// Fake payment gateway. Every call is recorded as one JSON line in an
+// append-only ledger so side effects can be audited after the fact.
+import fs from 'node:fs';
+import path from 'node:path';
+import crypto from 'node:crypto';
+
+function ledgerPath() {
+ return process.env.LEDGER_FILE || path.join(process.cwd(), '.data', 'ledger.jsonl');
+}
+
+function append(entry) {
+ const file = ledgerPath();
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.appendFileSync(file, `${JSON.stringify({ ...entry, at: new Date().toISOString() })}\n`);
+}
+
+function latency() {
+ return new Promise(resolve => setTimeout(resolve, 5 + Math.floor(Math.random() * 10)));
+}
+
+export async function charge({ orderId, amount }) {
+ await latency();
+ const receipt = { id: `chg_${crypto.randomUUID()}`, type: 'charge', orderId, amount };
+ append(receipt);
+ return receipt;
+}
+
+export async function refund({ orderId, amount }) {
+ await latency();
+ const receipt = { id: `rfnd_${crypto.randomUUID()}`, type: 'refund', orderId, amount };
+ append(receipt);
+ return receipt;
+}
+
+export async function payout({ vendorId, amount }) {
+ await latency();
+ const receipt = { id: `pay_${crypto.randomUUID()}`, type: 'payout', vendorId, amount };
+ append(receipt);
+ return receipt;
+}
+
+export function readLedger(file = ledgerPath()) {
+ let text = '';
+ try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
+ return text.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
+}
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/payouts.js b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/payouts.js
new file mode 100644
index 000000000..4b09b6784
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/payouts.js
@@ -0,0 +1,14 @@
+import { payout } from './charge.js';
+import * as store from './store.js';
+
+// Processes a vendor payout. Finance's batch job calls this once per payout
+// run and has never retried, so the keyless path has never been exercised.
+export async function processPayout(req) {
+ const key = req.idempotencyKey ? `payout:${req.idempotencyKey}` : null;
+ if (key && store.has(key)) {
+ return { ...store.get(key), duplicate: true };
+ }
+ const receipt = await payout({ vendorId: req.vendorId, amount: req.amount });
+ if (key) store.set(key, receipt);
+ return receipt;
+}
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/refunds.js b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/refunds.js
new file mode 100644
index 000000000..b8217e506
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/refunds.js
@@ -0,0 +1,14 @@
+import { refund } from './charge.js';
+import * as store from './store.js';
+
+// Processes a customer refund. Callers that have one pass an idempotencyKey;
+// plenty of callers (the storefront retry loop among them) do not.
+export async function processRefund(req) {
+ const key = req.idempotencyKey ? `refund:${req.idempotencyKey}` : null;
+ if (key && store.has(key)) {
+ return { ...store.get(key), duplicate: true };
+ }
+ const receipt = await refund({ orderId: req.orderId, amount: req.amount });
+ if (key) store.set(key, receipt);
+ return receipt;
+}
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/store.js b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/store.js
new file mode 100644
index 000000000..3303c7588
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/src/store.js
@@ -0,0 +1,33 @@
+// Tiny JSON-file-backed key/value store. All operations are synchronous so a
+// check-and-set within one event-loop turn cannot interleave.
+import fs from 'node:fs';
+import path from 'node:path';
+
+function storePath() {
+ return process.env.STORE_FILE || path.join(process.cwd(), '.data', 'store.json');
+}
+
+function load() {
+ try { return JSON.parse(fs.readFileSync(storePath(), 'utf8')); } catch { return {}; }
+}
+
+function save(data) {
+ const file = storePath();
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.writeFileSync(file, JSON.stringify(data, null, 1));
+}
+
+export function get(key) {
+ return load()[key];
+}
+
+export function has(key) {
+ return Object.prototype.hasOwnProperty.call(load(), key);
+}
+
+export function set(key, value) {
+ const data = load();
+ data[key] = value;
+ save(data);
+ return value;
+}
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/files/test/payouts.test.js b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/test/payouts.test.js
new file mode 100644
index 000000000..9b51bd593
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/test/payouts.test.js
@@ -0,0 +1,30 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+function freshEnv(t) {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'payments-test-'));
+ process.env.LEDGER_FILE = path.join(dir, 'ledger.jsonl');
+ process.env.STORE_FILE = path.join(dir, 'store.json');
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
+}
+
+test('processPayout pays once and returns the gateway receipt', async (t) => {
+ freshEnv(t);
+ const { processPayout } = await import('../src/payouts.js');
+ const receipt = await processPayout({ vendorId: 'ven-1', amount: 5000 });
+ assert.equal(receipt.type, 'payout');
+ assert.equal(receipt.vendorId, 'ven-1');
+ assert.equal(receipt.amount, 5000);
+});
+
+test('processPayout with an explicit key returns the stored receipt on a repeat call', async (t) => {
+ freshEnv(t);
+ const { processPayout } = await import('../src/payouts.js');
+ const first = await processPayout({ vendorId: 'ven-2', amount: 7000, idempotencyKey: 'key-7' });
+ const second = await processPayout({ vendorId: 'ven-2', amount: 7000, idempotencyKey: 'key-7' });
+ assert.equal(second.duplicate, true);
+ assert.equal(second.id, first.id);
+});
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/files/test/refunds.test.js b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/test/refunds.test.js
new file mode 100644
index 000000000..163dc4a50
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/files/test/refunds.test.js
@@ -0,0 +1,30 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+function freshEnv(t) {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'payments-test-'));
+ process.env.LEDGER_FILE = path.join(dir, 'ledger.jsonl');
+ process.env.STORE_FILE = path.join(dir, 'store.json');
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
+}
+
+test('processRefund refunds once and returns the gateway receipt', async (t) => {
+ freshEnv(t);
+ const { processRefund } = await import('../src/refunds.js');
+ const receipt = await processRefund({ orderId: 'ord-1', amount: 1200 });
+ assert.equal(receipt.type, 'refund');
+ assert.equal(receipt.orderId, 'ord-1');
+ assert.equal(receipt.amount, 1200);
+});
+
+test('processRefund with an explicit key returns the stored receipt on a repeat call', async (t) => {
+ freshEnv(t);
+ const { processRefund } = await import('../src/refunds.js');
+ const first = await processRefund({ orderId: 'ord-2', amount: 900, idempotencyKey: 'key-2' });
+ const second = await processRefund({ orderId: 'ord-2', amount: 900, idempotencyKey: 'key-2' });
+ assert.equal(second.duplicate, true);
+ assert.equal(second.id, first.id);
+});
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/meta.json b/docker/context-profiles/complex-eval/cases4/recurring-incident/meta.json
new file mode 100644
index 000000000..15649835e
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/meta.json
@@ -0,0 +1,16 @@
+{
+ "id": "recurring-incident",
+ "category": "learning-loop-chain",
+ "manualIds": [],
+ "checkTimeoutMs": 60000,
+ "steps": [
+ { "manualIds": ["skill:error-handling"] },
+ { "manualIds": ["skill:error-handling"] },
+ { "manualIds": ["skill:continuous-learning"] }
+ ],
+ "selection": {
+ "id": "complex-recurring-incident",
+ "category": "complex-learning-loop",
+ "expectedIds": ["skill:continuous-learning"]
+ }
+}
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/01-refund-retry/check.cjs b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/01-refund-retry/check.cjs
new file mode 100644
index 000000000..f1b6e681d
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/01-refund-retry/check.cjs
@@ -0,0 +1,207 @@
+'use strict';
+// Step 1 grader (recurring-incident): a refund retried WITHOUT an idempotency
+// key must refund exactly once — in-process (0.20) and across a module reload
+// with the same store (0.20); a regression test wired into `npm test` must fail
+// when the fix is reverted in a scratch copy (0.30); a durable prevention doc
+// must exist (0.20); the mechanism must live in a shared helper module (0.10).
+// Graders cannot spawn child processes (--permission), so tests are executed
+// in-process via node:test's run({ isolation: 'none' }) with TMPDIR redirected
+// into the workspace.
+const fs = require('node:fs');
+const path = require('node:path');
+const { pathToFileURL } = require('node:url');
+
+const probes = [
+ { name: 'retry-same-process-refunds-once', weight: 0.20 },
+ { name: 'retry-after-reload-refunds-once', weight: 0.20 },
+ { name: 'regression-test-wired-and-bites', weight: 0.30 },
+ { name: 'prevention-doc-exists', weight: 0.20 },
+ { name: 'shared-idempotency-helper', weight: 0.10 },
+];
+const results = new Map();
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ let score = 0;
+ for (const probe of probes) {
+ const ok = results.get(probe.name) === true;
+ if (ok) score += probe.weight;
+ process.stdout.write(`${ok ? 'ok' : 'not ok'} - ${probe.name}\n`);
+ }
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: Math.round(score * 1000) / 1000 })}\n`);
+ process.exit(0);
+}
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+
+const root = process.cwd();
+const scratch = fs.mkdtempSync(path.join(root, '.ecc-g1-'));
+fs.mkdirSync(path.join(scratch, 'tmp'), { recursive: true });
+process.env.TMPDIR = path.join(scratch, 'tmp');
+
+// The fixture's original buggy refunds.js, embedded so the mutation probe can
+// revert the fix in a scratch copy and check the regression suite notices.
+const ORIGINAL_REFUNDS = [
+ "import { refund } from './charge.js';",
+ "import * as store from './store.js';",
+ '',
+ '// Processes a customer refund. Callers that have one pass an idempotencyKey;',
+ '// plenty of callers (the storefront retry loop among them) do not.',
+ 'export async function processRefund(req) {',
+ ' const key = req.idempotencyKey ? `refund:${req.idempotencyKey}` : null;',
+ ' if (key && store.has(key)) {',
+ ' return { ...store.get(key), duplicate: true };',
+ ' }',
+ ' const receipt = await refund({ orderId: req.orderId, amount: req.amount });',
+ ' if (key) store.set(key, receipt);',
+ ' return receipt;',
+ '}',
+ '',
+].join('\n');
+
+let importCounter = 0;
+function importFresh(relative) {
+ importCounter += 1;
+ return import(`${pathToFileURL(path.join(root, relative)).href}?cb=${importCounter}`);
+}
+
+function readLedger(file) {
+ let text = '';
+ try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
+ return text.split('\n').filter(line => line.trim()).map(line => {
+ try { return JSON.parse(line); } catch { return null; }
+ }).filter(Boolean);
+}
+
+function copyTree(from, to) {
+ fs.mkdirSync(to, { recursive: true });
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
+ const target = path.join(to, entry.name);
+ if (entry.isDirectory()) copyTree(path.join(from, entry.name), target);
+ else if (entry.isFile()) fs.copyFileSync(path.join(from, entry.name), target);
+ }
+}
+
+function findTestFiles(mustMatch) {
+ const found = [];
+ const walk = dir => {
+ let entries = [];
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
+ for (const entry of entries) {
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) { walk(full); continue; }
+ if (!/\.test\.(js|cjs|mjs)$/.test(entry.name)) continue;
+ let content = '';
+ try { content = fs.readFileSync(full, 'utf8'); } catch { continue; }
+ if (mustMatch.every(re => re.test(content))) found.push(full);
+ }
+ };
+ walk(root);
+ return found.sort();
+}
+
+function npmTestWired() {
+ try {
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
+ const script = (pkg.scripts && pkg.scripts.test) || '';
+ // `node --test test/` silently runs nothing on Node 24; that is not wired.
+ return /--test\b/.test(script) && !/--test\s+test\/?\s*$/.test(script.trim());
+ } catch { return false; }
+}
+
+async function countTestFailures(files) {
+ const { run } = require('node:test');
+ let failures = 0;
+ const stream = run({ files, isolation: 'none', concurrency: 1 });
+ stream.on('test:fail', () => { failures += 1; });
+ await new Promise((resolve, reject) => { stream.on('end', resolve); stream.on('error', reject); stream.resume(); });
+ return failures;
+}
+
+function markdownFiles() {
+ const found = [];
+ const walk = dir => {
+ let entries = [];
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
+ for (const entry of entries) {
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) walk(full);
+ else if (/\.(md|markdown|txt)$/i.test(entry.name)) found.push(full);
+ }
+ };
+ walk(root);
+ return found.sort();
+}
+
+function isPreventionDoc(file) {
+ let content = '';
+ try { content = fs.readFileSync(file, 'utf8'); } catch { return false; }
+ return /idempoten/i.test(content) && /prevent|runbook|playbook|checklist|post-?mortem|procedure/i.test(content);
+}
+
+(async () => {
+ // (a) Functional: a keyless refund retried in-process refunds exactly once.
+ try {
+ const ledger = path.join(scratch, 'ledger-a.jsonl');
+ process.env.LEDGER_FILE = ledger;
+ process.env.STORE_FILE = path.join(scratch, 'store-a.json');
+ const refunds = await importFresh('src/refunds.js');
+ await refunds.processRefund({ orderId: 'ord-g1-retry', amount: 2500 });
+ await refunds.processRefund({ orderId: 'ord-g1-retry', amount: 2500 });
+ const entries = readLedger(ledger).filter(e => e.type === 'refund' && e.orderId === 'ord-g1-retry');
+ results.set('retry-same-process-refunds-once', entries.length === 1);
+
+ // (a2) Durable: a fresh module instance (simulated restart) over the same
+ // store must still not refund again.
+ const reloaded = await importFresh('src/refunds.js');
+ await reloaded.processRefund({ orderId: 'ord-g1-retry', amount: 2500 });
+ const afterReload = readLedger(ledger).filter(e => e.type === 'refund' && e.orderId === 'ord-g1-retry');
+ results.set('retry-after-reload-refunds-once', entries.length === 1 && afterReload.length === 1);
+ } catch { /* both functional probes stay false */ }
+
+ // (b) Regression coverage: a refund/idempotency test exists, npm test is
+ // wired, the suite passes as-is, and it FAILS when the fix is reverted.
+ try {
+ const files = findTestFiles([/refund/i, /idempoten|retry|duplicat/i]);
+ let ok = files.length > 0 && npmTestWired();
+ if (ok) ok = (await countTestFailures(files)) === 0;
+ if (ok) {
+ const mut = path.join(scratch, 'mutation');
+ fs.mkdirSync(mut, { recursive: true });
+ copyTree(path.join(root, 'src'), path.join(mut, 'src'));
+ fs.copyFileSync(path.join(root, 'package.json'), path.join(mut, 'package.json'));
+ for (const file of files) {
+ const target = path.join(mut, path.relative(root, file));
+ fs.mkdirSync(path.dirname(target), { recursive: true });
+ fs.copyFileSync(file, target);
+ }
+ fs.writeFileSync(path.join(mut, 'src', 'refunds.js'), ORIGINAL_REFUNDS);
+ const mutated = files.map(file => path.join(mut, path.relative(root, file)));
+ ok = (await countTestFailures(mutated)) > 0;
+ }
+ results.set('regression-test-wired-and-bites', ok);
+ } catch { /* probe stays false */ }
+
+ // (c) A durable prevention artifact: some doc ties idempotency to a
+ // prevention procedure (runbook/playbook/checklist/postmortem).
+ try {
+ results.set('prevention-doc-exists', markdownFiles().some(isPreventionDoc));
+ } catch { /* probe stays false */ }
+
+ // (d) The mechanism lives in a shared helper module that refunds.js imports,
+ // not inline in refunds.js alone.
+ try {
+ const refundsSrc = fs.readFileSync(path.join(root, 'src', 'refunds.js'), 'utf8');
+ const helpers = fs.readdirSync(path.join(root, 'src'))
+ .filter(name => /idempoten/i.test(name) && /\.(js|cjs|mjs)$/.test(name));
+ const imported = /import[^'"]*from\s*['"][^'"]*idempoten[^'"]*['"]/.test(refundsSrc)
+ || /require\(\s*['"][^'"]*idempoten[^'"]*['"]\s*\)/.test(refundsSrc);
+ results.set('shared-idempotency-helper', helpers.length > 0 && imported);
+ } catch { /* probe stays false */ }
+
+ try { fs.rmSync(scratch, { recursive: true, force: true }); } catch { /* best effort */ }
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/01-refund-retry/query.md b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/01-refund-retry/query.md
new file mode 100644
index 000000000..84a617791
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/01-refund-retry/query.md
@@ -0,0 +1 @@
+Customers are reporting duplicate refunds again — see docs/incidents.md. When a refund request comes in without an idempotency key and the caller retries, we refund the customer twice. Fix it, and make sure this stops being a recurring incident — it's the third time this quarter.
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/02-payout-storm/check.cjs b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/02-payout-storm/check.cjs
new file mode 100644
index 000000000..619414014
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/02-payout-storm/check.cjs
@@ -0,0 +1,208 @@
+'use strict';
+// Step 2 grader (recurring-incident): a concurrent keyless payout retry storm
+// must pay exactly once and stay paid once across a module reload (0.40);
+// payouts.js must REUSE the same shared idempotency helper refunds.js imports,
+// with no second inline implementation (0.30); a payout regression test wired
+// into npm test must fail when the fix is reverted in a scratch copy (0.20);
+// the prevention doc must now cover payouts / this class of bug (0.10).
+const fs = require('node:fs');
+const path = require('node:path');
+const { pathToFileURL } = require('node:url');
+
+const probes = [
+ { name: 'payout-storm-pays-once', weight: 0.40 },
+ { name: 'reuses-shared-helper', weight: 0.30 },
+ { name: 'payout-regression-test-bites', weight: 0.20 },
+ { name: 'prevention-doc-covers-class', weight: 0.10 },
+];
+const results = new Map();
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ let score = 0;
+ for (const probe of probes) {
+ const ok = results.get(probe.name) === true;
+ if (ok) score += probe.weight;
+ process.stdout.write(`${ok ? 'ok' : 'not ok'} - ${probe.name}\n`);
+ }
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: Math.round(score * 1000) / 1000 })}\n`);
+ process.exit(0);
+}
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+
+const root = process.cwd();
+const scratch = fs.mkdtempSync(path.join(root, '.ecc-g2-'));
+fs.mkdirSync(path.join(scratch, 'tmp'), { recursive: true });
+process.env.TMPDIR = path.join(scratch, 'tmp');
+
+// The fixture's original payouts.js, embedded for the mutation probe.
+const ORIGINAL_PAYOUTS = [
+ "import { payout } from './charge.js';",
+ "import * as store from './store.js';",
+ '',
+ '// Processes a vendor payout. Finance\'s batch job calls this once per payout',
+ '// run and has never retried, so the keyless path has never been exercised.',
+ 'export async function processPayout(req) {',
+ ' const key = req.idempotencyKey ? `payout:${req.idempotencyKey}` : null;',
+ ' if (key && store.has(key)) {',
+ ' return { ...store.get(key), duplicate: true };',
+ ' }',
+ ' const receipt = await payout({ vendorId: req.vendorId, amount: req.amount });',
+ ' if (key) store.set(key, receipt);',
+ ' return receipt;',
+ '}',
+ '',
+].join('\n');
+
+let importCounter = 0;
+function importFresh(relative) {
+ importCounter += 1;
+ return import(`${pathToFileURL(path.join(root, relative)).href}?cb=${importCounter}`);
+}
+
+function readLedger(file) {
+ let text = '';
+ try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
+ return text.split('\n').filter(line => line.trim()).map(line => {
+ try { return JSON.parse(line); } catch { return null; }
+ }).filter(Boolean);
+}
+
+function copyTree(from, to) {
+ fs.mkdirSync(to, { recursive: true });
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
+ const target = path.join(to, entry.name);
+ if (entry.isDirectory()) copyTree(path.join(from, entry.name), target);
+ else if (entry.isFile()) fs.copyFileSync(path.join(from, entry.name), target);
+ }
+}
+
+function findTestFiles(mustMatch) {
+ const found = [];
+ const walk = dir => {
+ let entries = [];
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
+ for (const entry of entries) {
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) { walk(full); continue; }
+ if (!/\.test\.(js|cjs|mjs)$/.test(entry.name)) continue;
+ let content = '';
+ try { content = fs.readFileSync(full, 'utf8'); } catch { continue; }
+ if (mustMatch.every(re => re.test(content))) found.push(full);
+ }
+ };
+ walk(root);
+ return found.sort();
+}
+
+function npmTestWired() {
+ try {
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
+ const script = (pkg.scripts && pkg.scripts.test) || '';
+ return /--test\b/.test(script) && !/--test\s+test\/?\s*$/.test(script.trim());
+ } catch { return false; }
+}
+
+async function countTestFailures(files) {
+ const { run } = require('node:test');
+ let failures = 0;
+ const stream = run({ files, isolation: 'none', concurrency: 1 });
+ stream.on('test:fail', () => { failures += 1; });
+ await new Promise((resolve, reject) => { stream.on('end', resolve); stream.on('error', reject); stream.resume(); });
+ return failures;
+}
+
+function markdownFiles() {
+ const found = [];
+ const walk = dir => {
+ let entries = [];
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
+ for (const entry of entries) {
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) walk(full);
+ else if (/\.(md|markdown|txt)$/i.test(entry.name)) found.push(full);
+ }
+ };
+ walk(root);
+ return found.sort();
+}
+
+// The idempotency helper module specifier refunds.js imports, if any.
+function helperSpecifier() {
+ try {
+ const refundsSrc = fs.readFileSync(path.join(root, 'src', 'refunds.js'), 'utf8');
+ const match = /(?:from|require\()\s*['"]([^'"]*idempoten[^'"]*)['"]/i.exec(refundsSrc);
+ return match ? match[1] : null;
+ } catch { return null; }
+}
+
+(async () => {
+ // (a) Functional: 20 concurrent keyless retries pay exactly once, and a
+ // fresh module instance over the same store still does not pay again.
+ try {
+ const ledger = path.join(scratch, 'ledger-a.jsonl');
+ process.env.LEDGER_FILE = ledger;
+ process.env.STORE_FILE = path.join(scratch, 'store-a.json');
+ const payouts = await importFresh('src/payouts.js');
+ await Promise.all(Array.from({ length: 20 },
+ () => payouts.processPayout({ vendorId: 'ven-g2-storm', amount: 9000 }).catch(() => null)));
+ const afterStorm = readLedger(ledger).filter(e => e.type === 'payout' && e.vendorId === 'ven-g2-storm');
+ const reloaded = await importFresh('src/payouts.js');
+ await reloaded.processPayout({ vendorId: 'ven-g2-storm', amount: 9000 }).catch(() => null);
+ const afterReload = readLedger(ledger).filter(e => e.type === 'payout' && e.vendorId === 'ven-g2-storm');
+ results.set('payout-storm-pays-once', afterStorm.length === 1 && afterReload.length === 1);
+ } catch { /* probe stays false */ }
+
+ // (b) Reuse: payouts.js imports the SAME helper specifier as refunds.js and
+ // does not carry a second inline implementation (own key hashing or its own
+ // seen/inflight table).
+ try {
+ const specifier = helperSpecifier();
+ const payoutsSrc = fs.readFileSync(path.join(root, 'src', 'payouts.js'), 'utf8');
+ const importsSame = specifier !== null
+ && new RegExp(`(?:from|require\\()\\s*['"]${specifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`).test(payoutsSrc);
+ const inlineImplementation = /createHash|new Map\s*\(|new Set\s*\(|new WeakMap\s*\(/.test(payoutsSrc);
+ results.set('reuses-shared-helper', importsSame && !inlineImplementation);
+ } catch { /* probe stays false */ }
+
+ // (c) Regression coverage for payouts, same discipline as step 1.
+ try {
+ const files = findTestFiles([/payout/i, /idempoten|retry|duplicat|storm|concurrent/i]);
+ let ok = files.length > 0 && npmTestWired();
+ if (ok) ok = (await countTestFailures(files)) === 0;
+ if (ok) {
+ const mut = path.join(scratch, 'mutation');
+ fs.mkdirSync(mut, { recursive: true });
+ copyTree(path.join(root, 'src'), path.join(mut, 'src'));
+ fs.copyFileSync(path.join(root, 'package.json'), path.join(mut, 'package.json'));
+ for (const file of files) {
+ const target = path.join(mut, path.relative(root, file));
+ fs.mkdirSync(path.dirname(target), { recursive: true });
+ fs.copyFileSync(file, target);
+ }
+ fs.writeFileSync(path.join(mut, 'src', 'payouts.js'), ORIGINAL_PAYOUTS);
+ const mutated = files.map(file => path.join(mut, path.relative(root, file)));
+ ok = (await countTestFailures(mutated)) > 0;
+ }
+ results.set('payout-regression-test-bites', ok);
+ } catch { /* probe stays false */ }
+
+ // (d) The prevention doc now covers payouts / the whole class of bug.
+ try {
+ const covered = markdownFiles().some(file => {
+ let content = '';
+ try { content = fs.readFileSync(file, 'utf8'); } catch { return false; }
+ return /idempoten/i.test(content)
+ && /prevent|runbook|playbook|checklist|post-?mortem|procedure/i.test(content)
+ && /payout|vendor|class of|general|every payment|any payment/i.test(content);
+ });
+ results.set('prevention-doc-covers-class', covered);
+ } catch { /* probe stays false */ }
+
+ try { fs.rmSync(scratch, { recursive: true, force: true }); } catch { /* best effort */ }
+ finish();
+})();
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/02-payout-storm/query.md b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/02-payout-storm/query.md
new file mode 100644
index 000000000..b61f88e6e
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/02-payout-storm/query.md
@@ -0,0 +1 @@
+Finance just flagged that their payout batch job is about to start retrying on timeouts, and payout retries can double-pay vendors. Same family of problem as the refunds — handle it. One hard requirement: a retried payout must never pay a vendor twice, even if the service restarts between the attempts.
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/03-handoff/check.cjs b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/03-handoff/check.cjs
new file mode 100644
index 000000000..e495c15b0
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/03-handoff/check.cjs
@@ -0,0 +1,104 @@
+'use strict';
+// Step 3 grader (recurring-incident): the handoff note. A handoff doc must
+// exist (0.20); every file path it references must actually exist in the
+// workspace, with at least two concrete references (0.30); it must name the
+// shared idempotency helper and describe the prevention procedure (0.30); it
+// must cover both the refunds and the payouts incidents (0.20). Scored on the
+// best candidate when several handoff files exist.
+const fs = require('node:fs');
+const path = require('node:path');
+
+const probes = [
+ { name: 'handoff-exists', weight: 0.20 },
+ { name: 'referenced-paths-exist', weight: 0.30 },
+ { name: 'names-helper-and-procedure', weight: 0.30 },
+ { name: 'covers-both-incidents', weight: 0.20 },
+];
+const results = new Map();
+let finished = false;
+function finish() {
+ if (finished) return;
+ finished = true;
+ let score = 0;
+ for (const probe of probes) {
+ const ok = results.get(probe.name) === true;
+ if (ok) score += probe.weight;
+ process.stdout.write(`${ok ? 'ok' : 'not ok'} - ${probe.name}\n`);
+ }
+ process.stdout.write(`ECC_EVAL_SCORE ${JSON.stringify({ score: Math.round(score * 1000) / 1000 })}\n`);
+ process.exit(0);
+}
+process.on('uncaughtException', finish);
+process.on('unhandledRejection', finish);
+
+const root = process.cwd();
+
+function handoffFiles() {
+ const found = [];
+ const walk = dir => {
+ let entries = [];
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
+ for (const entry of entries) {
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) { walk(full); continue; }
+ if (/hand[ -]?off/i.test(entry.name) && /\.(md|markdown|txt)$/i.test(entry.name)) found.push(full);
+ }
+ };
+ walk(root);
+ return found.sort();
+}
+
+// Candidate file paths mentioned in prose: at least one path segment and a
+// file extension (src/refunds.js, docs/runbooks/idempotency.md, ...).
+function referencedPaths(content) {
+ const tokens = new Set();
+ for (const match of content.matchAll(/(?:[\w@+.-]+\/)+[\w@+.-]+\.[a-z0-9]{1,8}/gi)) {
+ const token = match[0].replace(/[.,;:'")\]`]+$/, '').replace(/^[^\w@+.-]+/, '');
+ if (token.includes('..') || /^https?/i.test(token)) continue;
+ tokens.add(token);
+ }
+ return [...tokens];
+}
+
+function helperBasename() {
+ try {
+ const refundsSrc = fs.readFileSync(path.join(root, 'src', 'refunds.js'), 'utf8');
+ const match = /(?:from|require\()\s*['"]([^'"]*idempoten[^'"]*)['"]/i.exec(refundsSrc);
+ return match ? path.basename(match[1]) : null;
+ } catch { return null; }
+}
+
+function scoreCandidate(content) {
+ const verdicts = new Map();
+ verdicts.set('handoff-exists', true);
+
+ const paths = referencedPaths(content);
+ verdicts.set('referenced-paths-exist', paths.length >= 2
+ && paths.every(token => fs.existsSync(path.join(root, token))));
+
+ const helper = helperBasename();
+ verdicts.set('names-helper-and-procedure', helper !== null
+ && content.includes(helper)
+ && /prevent|runbook|playbook|checklist|regression|npm test|procedure/i.test(content));
+
+ verdicts.set('covers-both-incidents', /refund/i.test(content) && /payout/i.test(content));
+ return verdicts;
+}
+
+try {
+ const candidates = handoffFiles();
+ if (candidates.length > 0) {
+ let best = null;
+ for (const file of candidates) {
+ let content = '';
+ try { content = fs.readFileSync(file, 'utf8'); } catch { continue; }
+ const verdicts = scoreCandidate(content);
+ const total = [...verdicts.values()].filter(Boolean).length;
+ if (!best || total > best.total) best = { verdicts, total };
+ }
+ if (best) for (const [name, ok] of best.verdicts) results.set(name, ok);
+ }
+} catch { /* everything stays false */ }
+
+finish();
diff --git a/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/03-handoff/query.md b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/03-handoff/query.md
new file mode 100644
index 000000000..a76859c00
--- /dev/null
+++ b/docker/context-profiles/complex-eval/cases4/recurring-incident/steps/03-handoff/query.md
@@ -0,0 +1 @@
+You're rolling off this area. Write the handoff note for whoever picks this up next.
diff --git a/docker/context-profiles/complex-eval/naive2/keccak-selector/src/selector.js b/docker/context-profiles/complex-eval/naive2/keccak-selector/src/selector.js
new file mode 100644
index 000000000..7d878cce5
--- /dev/null
+++ b/docker/context-profiles/complex-eval/naive2/keccak-selector/src/selector.js
@@ -0,0 +1,11 @@
+'use strict';
+// Deliberately naive control: confuses Keccak-256 with the finalized NIST
+// SHA3-256 (different padding suffix), so every vector is wrong.
+const crypto = require('node:crypto');
+
+function functionSelector(signature) {
+ if (typeof signature !== 'string') throw new TypeError('signature must be a string');
+ return `0x${crypto.createHash('sha3-256').update(signature, 'utf8').digest('hex').slice(0, 8)}`;
+}
+
+module.exports = { functionSelector };
diff --git a/docker/context-profiles/complex-eval/naive4/recurring-incident/HANDOFF.md b/docker/context-profiles/complex-eval/naive4/recurring-incident/HANDOFF.md
new file mode 100644
index 000000000..393321d57
--- /dev/null
+++ b/docker/context-profiles/complex-eval/naive4/recurring-incident/HANDOFF.md
@@ -0,0 +1,3 @@
+# Handoff
+
+Refunds were double-processing when clients retried. Fixed by remembering what we already refunded. — Sam
diff --git a/docker/context-profiles/complex-eval/naive4/recurring-incident/src/payouts.js b/docker/context-profiles/complex-eval/naive4/recurring-incident/src/payouts.js
new file mode 100644
index 000000000..d29a194d5
--- /dev/null
+++ b/docker/context-profiles/complex-eval/naive4/recurring-incident/src/payouts.js
@@ -0,0 +1,15 @@
+import { payout } from './charge.js';
+
+// Track in-flight payouts so a burst of retries only sends one.
+const pendingPayouts = new Map();
+
+export async function processPayout(req) {
+ const tag = `pay-${req.vendorId}-${req.amount}`;
+ if (pendingPayouts.has(tag)) {
+ const receipt = await pendingPayouts.get(tag);
+ return { ...receipt, duplicate: true };
+ }
+ const pending = payout({ vendorId: req.vendorId, amount: req.amount });
+ pendingPayouts.set(tag, pending);
+ return pending;
+}
diff --git a/docker/context-profiles/complex-eval/naive4/recurring-incident/src/refunds.js b/docker/context-profiles/complex-eval/naive4/recurring-incident/src/refunds.js
new file mode 100644
index 000000000..e0cddd01b
--- /dev/null
+++ b/docker/context-profiles/complex-eval/naive4/recurring-incident/src/refunds.js
@@ -0,0 +1,13 @@
+import { refund } from './charge.js';
+
+// Remember which refunds we already sent so we don't send them twice.
+const seenRefunds = new Set();
+
+export async function processRefund(req) {
+ const key = req.idempotencyKey || `${req.orderId}:${req.amount}`;
+ if (seenRefunds.has(key)) {
+ return { id: `dup_${key}`, type: 'refund', orderId: req.orderId, amount: req.amount, duplicate: true };
+ }
+ seenRefunds.add(key);
+ return refund({ orderId: req.orderId, amount: req.amount });
+}
diff --git a/docker/context-profiles/complex-eval/reference/incident-triage/INCIDENT.md b/docker/context-profiles/complex-eval/reference/incident-triage/INCIDENT.md
new file mode 100644
index 000000000..251ea9c51
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference/incident-triage/INCIDENT.md
@@ -0,0 +1,27 @@
+# Incident 2026-09-24: order totals off by one cent
+
+## Root cause
+
+**C-2** — the totals refactor in `src/totals.js`.
+
+The refactor replaced integer-cent arithmetic with a decimal discount factor
+(`priceCents * quantity * (1 - discountPercent / 100)`). Decimal factors such
+as 0.7 or 0.93 have no exact binary floating-point representation, so for
+line amounts whose exact discounted value lands precisely on a half-cent
+boundary (e.g. 165 cents at 30% off = 115.5), the float result lands just
+below the boundary and `Math.round` rounds down instead of half-up. Every
+affected order is undercharged by exactly one cent, matching the finance
+findings in `evidence/incident.txt`.
+
+## Evidence
+
+- `evidence/incident.txt`: every flagged order is off by exactly one cent in the
+ store's favor, and all of them appeared after the 2026-09-23 deploy.
+- C-1 (logging) and C-3 (inventory timeout) cannot change totals; C-2 touched
+ the totals computation itself.
+
+## Fix
+
+`src/totals.js` now computes line discounts with exact integer arithmetic:
+`floor((priceCents * quantity * (100 - discountPercent) + 50) / 100)`, which
+rounds half-up on exact cent boundaries with no floating-point error.
diff --git a/docker/context-profiles/complex-eval/reference/incident-triage/src/totals.js b/docker/context-profiles/complex-eval/reference/incident-triage/src/totals.js
new file mode 100644
index 000000000..398a1f132
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference/incident-triage/src/totals.js
@@ -0,0 +1,15 @@
+'use strict';
+
+// Fixed after the 2026-09-24 incident: totals use exact integer-cent
+// arithmetic. Per line: priceCents * quantity * (100 - discountPercent) / 100,
+// rounded half-up via (n + 50) / 100 floored — no floating point anywhere.
+function computeOrderTotal(order) {
+ let total = 0;
+ for (const line of order.lines) {
+ const numerator = line.priceCents * line.quantity * (100 - order.discountPercent);
+ total += Math.floor((numerator + 50) / 100);
+ }
+ return total;
+}
+
+module.exports = { computeOrderTotal };
diff --git a/docker/context-profiles/complex-eval/reference/sentinel-api/src/app.js b/docker/context-profiles/complex-eval/reference/sentinel-api/src/app.js
new file mode 100644
index 000000000..da0f88d96
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference/sentinel-api/src/app.js
@@ -0,0 +1,120 @@
+'use strict';
+const fs = require('node:fs');
+const path = require('node:path');
+const http = require('node:http');
+const config = require('./config');
+const store = require('./store');
+
+const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
+const escapeHtml = text => text.replace(/[&<>"']/g, char => HTML_ESCAPES[char]);
+
+function sendJson(res, status, value) {
+ res.writeHead(status, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(value));
+}
+
+function readBody(req, res, callback) {
+ const chunks = [];
+ let bytes = 0;
+ let rejected = false;
+ req.on('data', chunk => {
+ bytes += chunk.length;
+ if (bytes > config.MAX_BODY_BYTES && !rejected) {
+ rejected = true;
+ sendJson(res, 413, { error: 'payload too large' });
+ req.destroy();
+ return;
+ }
+ chunks.push(chunk);
+ });
+ req.on('end', () => { if (!rejected) callback(Buffer.concat(chunks).toString('utf8')); });
+}
+
+function page(paste) {
+ return `paste ${paste.id} `
+ + `${escapeHtml(paste.content)} `;
+}
+
+function createApp() {
+ const adminToken = process.env.ADMIN_TOKEN || null;
+
+ return http.createServer((req, res) => {
+ const url = new URL(req.url, 'http://localhost');
+
+ if (req.method === 'POST' && url.pathname === '/pastes') {
+ readBody(req, res, body => {
+ let parsed;
+ try { parsed = JSON.parse(body); } catch {
+ sendJson(res, 400, { error: 'invalid JSON body' });
+ return;
+ }
+ if (typeof parsed.content !== 'string') {
+ sendJson(res, 400, { error: 'content must be a string' });
+ return;
+ }
+ const paste = store.create(parsed.content);
+ sendJson(res, 201, { id: paste.id, deleteToken: paste.deleteToken });
+ });
+ return;
+ }
+
+ const pasteMatch = /^\/pastes\/([\w-]+)$/.exec(url.pathname);
+ if (pasteMatch && req.method === 'GET') {
+ const paste = store.get(pasteMatch[1]);
+ if (!paste) { sendJson(res, 404, { error: 'not found' }); return; }
+ sendJson(res, 200, { id: paste.id, content: paste.content });
+ return;
+ }
+ if (pasteMatch && req.method === 'DELETE') {
+ const paste = store.get(pasteMatch[1]);
+ if (!paste) { sendJson(res, 404, { error: 'not found' }); return; }
+ if (req.headers['x-delete-token'] !== paste.deleteToken) {
+ sendJson(res, 403, { error: 'bad delete token' });
+ return;
+ }
+ store.remove(paste.id);
+ res.writeHead(204);
+ res.end();
+ return;
+ }
+
+ const pageMatch = /^\/p\/([\w-]+)$/.exec(url.pathname);
+ if (pageMatch && req.method === 'GET') {
+ const paste = store.get(pageMatch[1]);
+ if (!paste) { sendJson(res, 404, { error: 'not found' }); return; }
+ res.writeHead(200, { 'content-type': 'text/html' });
+ res.end(page(paste));
+ return;
+ }
+
+ if (req.method === 'GET' && url.pathname === '/files') {
+ const name = url.searchParams.get('name') || '';
+ const resolved = path.resolve(config.FILES_DIR, name);
+ if (resolved !== config.FILES_DIR && !resolved.startsWith(config.FILES_DIR + path.sep)) {
+ sendJson(res, 400, { error: 'invalid file name' });
+ return;
+ }
+ try {
+ const content = fs.readFileSync(resolved);
+ res.writeHead(200, { 'content-type': 'text/plain' });
+ res.end(content);
+ } catch {
+ sendJson(res, 404, { error: 'not found' });
+ }
+ return;
+ }
+
+ if (req.method === 'GET' && url.pathname === '/admin/stats') {
+ if (!adminToken || req.headers['x-admin-token'] !== adminToken) {
+ sendJson(res, 401, { error: 'unauthorized' });
+ return;
+ }
+ sendJson(res, 200, store.stats());
+ return;
+ }
+
+ sendJson(res, 404, { error: 'not found' });
+ });
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/reference/sentinel-api/src/config.js b/docker/context-profiles/complex-eval/reference/sentinel-api/src/config.js
new file mode 100644
index 000000000..f36468899
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference/sentinel-api/src/config.js
@@ -0,0 +1,7 @@
+'use strict';
+const path = require('node:path');
+
+module.exports = {
+ MAX_BODY_BYTES: 64 * 1024,
+ FILES_DIR: path.join(__dirname, '..', 'data', 'files'),
+};
diff --git a/docker/context-profiles/complex-eval/reference/sentinel-api/src/store.js b/docker/context-profiles/complex-eval/reference/sentinel-api/src/store.js
new file mode 100644
index 000000000..88f194153
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference/sentinel-api/src/store.js
@@ -0,0 +1,28 @@
+'use strict';
+const crypto = require('node:crypto');
+
+// In-memory paste store. Delete tokens are cryptographically random and shown
+// once at creation.
+const pastes = new Map();
+let nextId = 1;
+
+function create(content) {
+ const id = `p_${nextId++}`;
+ const paste = { id, content, deleteToken: crypto.randomBytes(16).toString('hex') };
+ pastes.set(id, paste);
+ return paste;
+}
+
+function get(id) {
+ return pastes.get(id) || null;
+}
+
+function remove(id) {
+ return pastes.delete(id);
+}
+
+function stats() {
+ return { pastes: pastes.size, created: nextId - 1 };
+}
+
+module.exports = { create, get, remove, stats };
diff --git a/docker/context-profiles/complex-eval/reference/webhook-relay/src/app.js b/docker/context-profiles/complex-eval/reference/webhook-relay/src/app.js
new file mode 100644
index 000000000..c7c97d267
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference/webhook-relay/src/app.js
@@ -0,0 +1,73 @@
+'use strict';
+const http = require('node:http');
+const crypto = require('node:crypto');
+
+const MAX_ATTEMPTS = 5;
+const BASE_DELAY_MS = 100;
+
+function createRelay() {
+ const deliveries = new Map();
+
+ async function attempt(record) {
+ record.attempts += 1;
+ try {
+ const response = await fetch(record.url, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify(record.payload), signal: AbortSignal.timeout(5000) });
+ if (response.status >= 200 && response.status < 300) {
+ record.status = 'delivered';
+ record.lastError = null;
+ return;
+ }
+ record.lastError = `HTTP ${response.status}`;
+ } catch (error) {
+ record.lastError = error && error.message ? error.message : 'delivery failed';
+ }
+ if (record.attempts >= MAX_ATTEMPTS) {
+ record.status = 'dead';
+ return;
+ }
+ const delay = BASE_DELAY_MS * 2 ** (record.attempts - 1);
+ setTimeout(() => { void attempt(record); }, delay);
+ }
+
+ const server = http.createServer((req, res) => {
+ if (req.method === 'POST' && req.url === '/deliveries') {
+ let body = '';
+ req.on('data', chunk => { body += chunk; });
+ req.on('end', () => {
+ let parsed;
+ try { parsed = JSON.parse(body); } catch {
+ res.writeHead(400, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ error: 'invalid JSON body' }));
+ return;
+ }
+ const id = crypto.randomUUID();
+ const record = { id, url: parsed.url, payload: parsed.payload,
+ status: 'pending', attempts: 0, lastError: null };
+ deliveries.set(id, record);
+ void attempt(record);
+ res.writeHead(202, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ id }));
+ });
+ return;
+ }
+ const match = /^\/deliveries\/([0-9a-f-]+)$/.exec(req.url || '');
+ if (req.method === 'GET' && match) {
+ const record = deliveries.get(match[1]);
+ if (!record) {
+ res.writeHead(404, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ error: 'not found' }));
+ return;
+ }
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(record));
+ return;
+ }
+ res.writeHead(404, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ error: 'not found' }));
+ });
+ return server;
+}
+
+module.exports = { createRelay };
diff --git a/docker/context-profiles/complex-eval/reference2/event-stats-api/src/app.js b/docker/context-profiles/complex-eval/reference2/event-stats-api/src/app.js
new file mode 100644
index 000000000..2abfb2eff
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference2/event-stats-api/src/app.js
@@ -0,0 +1,87 @@
+'use strict';
+const http = require('node:http');
+const { events } = require('./data');
+
+// Indexed implementation: per-type arrays sorted by timestamp, with prefix
+// sums, built once at startup. Per query the range is located with binary
+// search; only the matching slice is touched.
+function buildIndex() {
+ const byType = new Map();
+ for (const event of events) {
+ if (!byType.has(event.type)) byType.set(event.type, []);
+ byType.get(event.type).push(event);
+ }
+ for (const rows of byType.values()) {
+ rows.sort((a, b) => a.ts - b.ts);
+ const prefix = new Float64Array(rows.length + 1);
+ for (let i = 0; i < rows.length; i++) prefix[i + 1] = prefix[i] + rows[i].value;
+ rows.prefixSums = prefix;
+ }
+ return byType;
+}
+
+function lowerBound(rows, ts) {
+ let lo = 0;
+ let hi = rows.length;
+ while (lo < hi) {
+ const mid = (lo + hi) >> 1;
+ if (rows[mid].ts < ts) lo = mid + 1; else hi = mid;
+ }
+ return lo;
+}
+
+function upperBound(rows, ts) {
+ let lo = 0;
+ let hi = rows.length;
+ while (lo < hi) {
+ const mid = (lo + hi) >> 1;
+ if (rows[mid].ts <= ts) lo = mid + 1; else hi = mid;
+ }
+ return lo;
+}
+
+const EMPTY = { count: 0, sum: 0, avg: null, p50: null, p95: null, p99: null, min: null, max: null };
+
+function summarize(index, type, from, to) {
+ const rows = index.get(type);
+ if (!rows) return EMPTY;
+ const lo = from === null ? 0 : lowerBound(rows, from);
+ const hi = to === null ? rows.length : upperBound(rows, to);
+ const count = hi - lo;
+ if (count <= 0) return EMPTY;
+ const sum = rows.prefixSums[hi] - rows.prefixSums[lo];
+ const values = new Array(count);
+ for (let i = 0; i < count; i++) values[i] = rows[lo + i].value;
+ values.sort((a, b) => a - b);
+ const rank = p => values[Math.ceil((p / 100) * count) - 1];
+ const avgCents = Math.floor((sum * 200 + count) / (count * 2));
+ return { count, sum, avg: avgCents / 100,
+ p50: rank(50), p95: rank(95), p99: rank(99), min: values[0], max: values[count - 1] };
+}
+
+function createApp() {
+ const index = buildIndex();
+ return http.createServer((req, res) => {
+ const url = new URL(req.url, 'http://localhost');
+ if (req.method === 'GET' && url.pathname === '/stats') {
+ const type = url.searchParams.get('type');
+ const hasFrom = url.searchParams.has('from');
+ const hasTo = url.searchParams.has('to');
+ const from = hasFrom ? Number(url.searchParams.get('from')) : null;
+ const to = hasTo ? Number(url.searchParams.get('to')) : null;
+ if ((hasFrom && !Number.isFinite(from)) || (hasTo && !Number.isFinite(to))
+ || (from !== null && to !== null && from > to)) {
+ res.writeHead(400, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ error: 'invalid bounds' }));
+ return;
+ }
+ res.writeHead(200, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ type, from, to, ...summarize(index, type, from, to) }));
+ return;
+ }
+ res.writeHead(404, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({ error: 'not found' }));
+ });
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/reference2/forge-cli/src/cli.js b/docker/context-profiles/complex-eval/reference2/forge-cli/src/cli.js
new file mode 100644
index 000000000..58301d426
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference2/forge-cli/src/cli.js
@@ -0,0 +1,98 @@
+'use strict';
+
+const NAME = /^[a-z0-9][a-z0-9-]*$/;
+const USAGE = 'usage: snippet \n';
+const ADD_USAGE = 'usage: add [--tags t1,t2] \n';
+
+const ok = (stdout = '') => ({ code: 0, stdout, stderr: '' });
+const fail = (code, stderr) => ({ code, stdout: '', stderr });
+
+function snippetsOf(state) {
+ if (!state.snippets || typeof state.snippets !== 'object') state.snippets = {};
+ return state.snippets;
+}
+
+function sortedNames(snippets, filter) {
+ return Object.keys(snippets).filter(filter).sort();
+}
+
+function run(argv, state) {
+ try {
+ const snippets = snippetsOf(state);
+ const [command, ...args] = argv;
+
+ if (command === 'add') {
+ let tags = [];
+ let rest = args;
+ const tagIndex = args.indexOf('--tags');
+ const name = args[0];
+ if (tagIndex !== -1) {
+ if (tagIndex < 1 || !args[tagIndex + 1]) return fail(2, ADD_USAGE);
+ tags = args[tagIndex + 1].split(',').filter(Boolean);
+ rest = [args[0], ...args.slice(tagIndex + 2)];
+ }
+ const text = rest.slice(1).join(' ');
+ if (!name || !text) return fail(2, ADD_USAGE);
+ if (!NAME.test(name)) return fail(2, `error: invalid snippet name '${name}'\n`);
+ if (snippets[name]) return fail(1, `error: snippet '${name}' already exists\n`);
+ snippets[name] = { text, tags: [...tags].sort() };
+ return ok(`created ${name}\n`);
+ }
+
+ if (command === 'get') {
+ const snippet = snippets[args[0]];
+ if (!snippet) return fail(2, `error: no snippet named '${args[0]}'\n`);
+ return ok(`${snippet.text}\n`);
+ }
+
+ if (command === 'remove') {
+ const snippet = snippets[args[0]];
+ if (!snippet) return fail(2, `error: no snippet named '${args[0]}'\n`);
+ delete snippets[args[0]];
+ return ok(`removed ${args[0]}\n`);
+ }
+
+ if (command === 'list') {
+ const tagIndex = args.indexOf('--tag');
+ const tag = tagIndex !== -1 ? args[tagIndex + 1] : null;
+ const names = sortedNames(snippets, name => tag === null || snippets[name].tags.includes(tag));
+ return ok(names.length ? `${names.join('\n')}\n` : 'no snippets\n');
+ }
+
+ if (command === 'search') {
+ const term = (args[0] || '').toLowerCase();
+ const names = sortedNames(snippets, name =>
+ name.toLowerCase().includes(term) || snippets[name].text.toLowerCase().includes(term));
+ return ok(names.length ? `${names.join('\n')}\n` : 'no matches\n');
+ }
+
+ if (command === 'export') {
+ const out = { snippets: {} };
+ for (const name of sortedNames(snippets, () => true)) {
+ out.snippets[name] = { text: snippets[name].text, tags: [...snippets[name].tags].sort() };
+ }
+ return ok(`${JSON.stringify(out)}\n`);
+ }
+
+ if (command === 'import') {
+ let parsed;
+ try { parsed = JSON.parse(args[0]); } catch { return fail(1, 'error: invalid JSON\n'); }
+ const incoming = parsed && typeof parsed === 'object' ? parsed.snippets : null;
+ if (!incoming || typeof incoming !== 'object') return fail(1, 'error: invalid JSON\n');
+ let imported = 0;
+ let skipped = 0;
+ for (const [name, value] of Object.entries(incoming)) {
+ if (snippets[name]) { skipped++; continue; }
+ snippets[name] = { text: value.text, tags: [...(value.tags || [])].sort() };
+ imported++;
+ }
+ return ok(`imported ${imported}, skipped ${skipped}\n`);
+ }
+
+ return fail(2, USAGE);
+ } catch {
+ return fail(2, USAGE);
+ }
+}
+
+module.exports = { run };
diff --git a/docker/context-profiles/complex-eval/reference2/keccak-selector/src/selector.js b/docker/context-profiles/complex-eval/reference2/keccak-selector/src/selector.js
new file mode 100644
index 000000000..0054fc2e0
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference2/keccak-selector/src/selector.js
@@ -0,0 +1,53 @@
+'use strict';
+// Keccak-256 (original Keccak padding 0x01, NOT the NIST SHA3-256 suffix 0x06).
+// Keccak-f[1600] permutation over 25 64-bit little-endian lanes as BigInts.
+const RC = [0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
+ 0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
+ 0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
+ 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
+ 0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
+ 0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n];
+const ROT = [[0, 36, 3, 41, 18], [1, 44, 10, 45, 2], [62, 6, 43, 15, 61],
+ [28, 55, 25, 21, 56], [27, 20, 39, 8, 14]];
+const MASK = 0xffffffffffffffffn;
+const rotl = (x, n) => n === 0n ? x : ((x << n) | (x >> (64n - n))) & MASK;
+
+function keccakF(s) {
+ for (let round = 0; round < 24; round++) {
+ const c = [];
+ const d = [];
+ for (let x = 0; x < 5; x++) c[x] = s[x] ^ s[x + 5] ^ s[x + 10] ^ s[x + 15] ^ s[x + 20];
+ for (let x = 0; x < 5; x++) d[x] = c[(x + 4) % 5] ^ rotl(c[(x + 1) % 5], 1n);
+ for (let y = 0; y < 5; y++) for (let x = 0; x < 5; x++) s[x + 5 * y] ^= d[x];
+ const b = new Array(25);
+ for (let y = 0; y < 5; y++) {
+ for (let x = 0; x < 5; x++) b[y + 5 * ((2 * x + 3 * y) % 5)] = rotl(s[x + 5 * y], BigInt(ROT[x][y]));
+ }
+ for (let y = 0; y < 5; y++) {
+ for (let x = 0; x < 5; x++) s[x + 5 * y] = b[x + 5 * y] ^ ((~b[(x + 1) % 5 + 5 * y] & MASK) & b[(x + 2) % 5 + 5 * y]);
+ }
+ s[0] ^= RC[round];
+ }
+}
+
+function keccak256(bytes) {
+ const rate = 136; // 1088-bit rate, 512-bit capacity
+ const state = new Array(25).fill(0n);
+ const q = rate - (bytes.length % rate);
+ const padded = Buffer.concat([bytes, Buffer.from([0x01]), Buffer.alloc(q - 1)]);
+ padded[padded.length - 1] |= 0x80;
+ for (let offset = 0; offset < padded.length; offset += rate) {
+ for (let i = 0; i < rate; i++) state[i >> 3] ^= BigInt(padded[offset + i]) << BigInt(8 * (i & 7));
+ keccakF(state);
+ }
+ const out = [];
+ for (let i = 0; i < 32; i++) out.push(Number((state[i >> 3] >> BigInt(8 * (i & 7))) & 0xffn));
+ return Buffer.from(out);
+}
+
+function functionSelector(signature) {
+ if (typeof signature !== 'string') throw new TypeError('signature must be a string');
+ return `0x${keccak256(Buffer.from(signature, 'utf8')).subarray(0, 4).toString('hex')}`;
+}
+
+module.exports = { functionSelector };
diff --git a/docker/context-profiles/complex-eval/reference3/chained-tickets/CHANGELOG.md b/docker/context-profiles/complex-eval/reference3/chained-tickets/CHANGELOG.md
new file mode 100644
index 000000000..e8cad2f0c
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/chained-tickets/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Changelog
+
+- 2026-09-25: Initial shortlink core — create, redirect, expiry, and delete per API.md.
+- 2026-09-25: Persistence — links survive restarts via the DATA_FILE JSON store; missing or corrupt data files start clean.
+- 2026-09-25: Abuse protection — URL validation (http/https only, length cap), request body limits, and per-client rate limiting with 429 responses.
+- 2026-09-25: Analytics — per-link redirect hit counts exposed at GET /links/:code/stats.
diff --git a/docker/context-profiles/complex-eval/reference3/chained-tickets/README.md b/docker/context-profiles/complex-eval/reference3/chained-tickets/README.md
new file mode 100644
index 000000000..3420482fe
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/chained-tickets/README.md
@@ -0,0 +1,14 @@
+# shortlink
+
+Internal link shortener service. Node.js standard library only, CommonJS.
+
+- `API.md` — the HTTP contract.
+- `CONTRIBUTING.md` — engineering conventions. Every ticket follows them.
+- `src/app.js` exports `createApp()` returning an `http.Server` that is not yet
+ listening; `node src/index.js ` starts the service.
+- Links persist to the JSON file named by the `DATA_FILE` environment variable
+ (default `./data/links.json`).
+- `GET /links//stats` returns `{ "code", "hits", "expiresAt" }` —
+ `hits` counts redirects.
+- The API is rate limited per client and validates URLs (http/https only).
+- Run the tests with `npm test`.
diff --git a/docker/context-profiles/complex-eval/reference3/chained-tickets/src/app.js b/docker/context-profiles/complex-eval/reference3/chained-tickets/src/app.js
new file mode 100644
index 000000000..c802a64fd
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/chained-tickets/src/app.js
@@ -0,0 +1,15 @@
+'use strict';
+const http = require('node:http');
+const path = require('node:path');
+const { createStore } = require('./store');
+const { createService } = require('./service');
+const { createRouter } = require('./routes');
+
+function createApp() {
+ const file = process.env.DATA_FILE || path.join(process.cwd(), 'data', 'links.json');
+ const store = createStore(file);
+ const service = createService(store);
+ return http.createServer(createRouter(service));
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/reference3/chained-tickets/src/index.js b/docker/context-profiles/complex-eval/reference3/chained-tickets/src/index.js
new file mode 100644
index 000000000..d37872b76
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/chained-tickets/src/index.js
@@ -0,0 +1,7 @@
+'use strict';
+const { createApp } = require('./app');
+
+const port = Number(process.env.PORT || process.argv[2] || 8080);
+createApp().listen(port, () => {
+ console.log(`shortlink listening on ${port}`);
+});
diff --git a/docker/context-profiles/complex-eval/reference3/chained-tickets/src/routes.js b/docker/context-profiles/complex-eval/reference3/chained-tickets/src/routes.js
new file mode 100644
index 000000000..7344146c6
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/chained-tickets/src/routes.js
@@ -0,0 +1,86 @@
+'use strict';
+const { HttpError } = require('./service');
+
+const MAX_BODY_BYTES = 64 * 1024;
+
+function sendJson(res, status, value) {
+ res.writeHead(status, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(value));
+}
+
+function sendError(res, error) {
+ const known = error instanceof HttpError;
+ sendJson(res, known ? error.status : 500, {
+ error: { code: known ? error.code : 'INTERNAL', message: known ? error.message : 'internal error' },
+ });
+}
+
+function readBody(req) {
+ return new Promise((resolve, reject) => {
+ let body = '';
+ let bytes = 0;
+ let settled = false;
+ req.on('data', chunk => {
+ if (settled) return;
+ bytes += chunk.length;
+ if (bytes > MAX_BODY_BYTES) {
+ settled = true;
+ reject(new HttpError(413, 'PAYLOAD_TOO_LARGE', 'request body too large'));
+ // Drain rather than destroy: the socket must live long enough to send the 413.
+ req.resume();
+ return;
+ }
+ body += chunk;
+ });
+ req.on('end', () => {
+ if (settled) return;
+ settled = true;
+ if (!body) { resolve({}); return; }
+ try { resolve(JSON.parse(body)); } catch { reject(new HttpError(400, 'INVALID_JSON', 'body must be valid JSON')); }
+ });
+ req.on('error', reject);
+ });
+}
+
+function createRouter(service) {
+ return async (req, res) => {
+ try {
+ const url = new URL(req.url, 'http://localhost');
+
+ if (req.method === 'POST' && url.pathname === '/links') {
+ service.assertRateLimit(req.socket.remoteAddress || 'unknown');
+ const link = service.createLink(await readBody(req));
+ sendJson(res, 201, { code: link.code, shortUrl: `/${link.code}`, expiresAt: link.expiresAt });
+ return;
+ }
+
+ const statsMatch = /^\/links\/([A-Za-z0-9]{1,20})\/stats$/.exec(url.pathname);
+ if (req.method === 'GET' && statsMatch) {
+ sendJson(res, 200, service.stats(statsMatch[1]));
+ return;
+ }
+
+ const linkMatch = /^\/links\/([A-Za-z0-9]{1,20})$/.exec(url.pathname);
+ if (req.method === 'DELETE' && linkMatch) {
+ service.deleteLink(linkMatch[1]);
+ res.writeHead(204);
+ res.end();
+ return;
+ }
+
+ const redirectMatch = /^\/([A-Za-z0-9]{1,20})$/.exec(url.pathname);
+ if (req.method === 'GET' && redirectMatch) {
+ const link = service.resolveLink(redirectMatch[1]);
+ res.writeHead(302, { location: link.url });
+ res.end();
+ return;
+ }
+
+ throw new HttpError(404, 'NOT_FOUND', 'not found');
+ } catch (error) {
+ sendError(res, error);
+ }
+ };
+}
+
+module.exports = { createRouter };
diff --git a/docker/context-profiles/complex-eval/reference3/chained-tickets/src/service.js b/docker/context-profiles/complex-eval/reference3/chained-tickets/src/service.js
new file mode 100644
index 000000000..f28167d8a
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/chained-tickets/src/service.js
@@ -0,0 +1,82 @@
+'use strict';
+const crypto = require('node:crypto');
+
+const MAX_URL_LENGTH = 2048;
+const DEFAULT_TTL_SECONDS = 604800;
+const MAX_TTL_SECONDS = 2592000;
+const RATE_LIMIT_WINDOW_MS = 60000;
+const RATE_LIMIT_MAX = 20;
+
+class HttpError extends Error {
+ constructor(status, code, message) {
+ super(message);
+ this.status = status;
+ this.code = code;
+ }
+}
+
+function validateUrl(url) {
+ if (typeof url !== 'string' || !url) throw new HttpError(400, 'INVALID_URL', 'url is required');
+ if (url.length > MAX_URL_LENGTH) throw new HttpError(400, 'INVALID_URL', 'url exceeds 2048 characters');
+ let parsed;
+ try { parsed = new URL(url); } catch { throw new HttpError(400, 'INVALID_URL', 'url must be a valid absolute URL'); }
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
+ throw new HttpError(400, 'INVALID_URL', 'only http and https URLs are allowed');
+ }
+ return url;
+}
+
+function validateTtl(ttlSeconds) {
+ if (ttlSeconds === undefined || ttlSeconds === null) return DEFAULT_TTL_SECONDS;
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1 || ttlSeconds > MAX_TTL_SECONDS) {
+ throw new HttpError(400, 'INVALID_TTL', 'ttlSeconds must be an integer between 1 and 2592000');
+ }
+ return ttlSeconds;
+}
+
+function createService(store) {
+ const buckets = new Map();
+
+ function assertRateLimit(key) {
+ const now = Date.now();
+ const windowHits = (buckets.get(key) || []).filter(at => now - at < RATE_LIMIT_WINDOW_MS);
+ if (windowHits.length >= RATE_LIMIT_MAX) throw new HttpError(429, 'RATE_LIMITED', 'too many requests, slow down');
+ windowHits.push(now);
+ buckets.set(key, windowHits);
+ }
+
+ function freshCode() {
+ let code = crypto.randomBytes(4).toString('hex');
+ while (store.get(code)) code = crypto.randomBytes(4).toString('hex');
+ return code;
+ }
+
+ return {
+ assertRateLimit,
+ createLink({ url, ttlSeconds } = {}) {
+ const validUrl = validateUrl(url);
+ const ttl = validateTtl(ttlSeconds);
+ const link = { code: freshCode(), url: validUrl,
+ expiresAt: new Date(Date.now() + ttl * 1000).toISOString(), hits: 0 };
+ store.set(link.code, link);
+ return link;
+ },
+ resolveLink(code) {
+ const link = store.get(code);
+ if (!link) throw new HttpError(404, 'NOT_FOUND', 'no link with that code');
+ if (Date.parse(link.expiresAt) <= Date.now()) throw new HttpError(410, 'GONE', 'link has expired');
+ store.incrementHits(code);
+ return link;
+ },
+ deleteLink(code) {
+ if (!store.delete(code)) throw new HttpError(404, 'NOT_FOUND', 'no link with that code');
+ },
+ stats(code) {
+ const link = store.get(code);
+ if (!link) throw new HttpError(404, 'NOT_FOUND', 'no link with that code');
+ return { code, hits: link.hits || 0, expiresAt: link.expiresAt };
+ },
+ };
+}
+
+module.exports = { createService, HttpError };
diff --git a/docker/context-profiles/complex-eval/reference3/chained-tickets/src/store.js b/docker/context-profiles/complex-eval/reference3/chained-tickets/src/store.js
new file mode 100644
index 000000000..7d5aa091b
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/chained-tickets/src/store.js
@@ -0,0 +1,28 @@
+'use strict';
+const fs = require('node:fs');
+const path = require('node:path');
+
+// JSON-file-backed link store. Missing or corrupt files start clean; every
+// mutation is flushed synchronously so a restart never loses a committed link.
+function createStore(file) {
+ let links = new Map();
+ try {
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
+ for (const [code, value] of Object.entries(raw.links || {})) links.set(code, value);
+ } catch { /* missing or corrupt: start empty */ }
+ const save = () => {
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.writeFileSync(file, `${JSON.stringify({ links: Object.fromEntries(links) }, null, 1)}\n`);
+ };
+ return {
+ get: code => links.get(code) || null,
+ set(code, value) { links.set(code, value); save(); },
+ delete(code) { const had = links.delete(code); if (had) save(); return had; },
+ incrementHits(code) {
+ const link = links.get(code);
+ if (link) { link.hits = (link.hits || 0) + 1; save(); }
+ },
+ };
+}
+
+module.exports = { createStore };
diff --git a/docker/context-profiles/complex-eval/reference3/chained-tickets/test/links.test.js b/docker/context-profiles/complex-eval/reference3/chained-tickets/test/links.test.js
new file mode 100644
index 000000000..1a358d43d
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/chained-tickets/test/links.test.js
@@ -0,0 +1,106 @@
+'use strict';
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createApp } = require('../src/app');
+
+process.env.DATA_FILE = require('node:path').join(require('node:os').tmpdir(),
+ `shortlink-test-${process.pid}.json`);
+
+let server;
+let port;
+test.before(async () => {
+ server = createApp();
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ port = server.address().port;
+});
+test.after(() => server.close());
+
+const post = body => fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
+const get = p => fetch(`http://127.0.0.1:${port}${p}`, { redirect: 'manual' });
+
+test('creates a link with default expiry', async () => {
+ const res = await post({ url: 'https://example.com/a' });
+ assert.equal(res.status, 201);
+ const body = await res.json();
+ assert.match(body.code, /^[A-Za-z0-9]{6,10}$/);
+ assert.ok(Date.parse(body.expiresAt) > Date.now());
+});
+
+test('redirects with 302 and location', async () => {
+ const { code } = await (await post({ url: 'https://example.com/b' })).json();
+ const res = await get(`/${code}`);
+ assert.equal(res.status, 302);
+ assert.equal(res.headers.get('location'), 'https://example.com/b');
+});
+
+test('unknown code is a 404 envelope', async () => {
+ const res = await get('/zzzzzz');
+ assert.equal(res.status, 404);
+ assert.equal((await res.json()).error.code, 'NOT_FOUND');
+});
+
+test('invalid url is a 400 envelope', async () => {
+ const res = await post({ url: 'notaurl' });
+ assert.equal(res.status, 400);
+ assert.equal((await res.json()).error.code, 'INVALID_URL');
+});
+
+test('javascript scheme rejected', async () => {
+ const res = await post({ url: 'javascript:alert(1)' });
+ assert.equal(res.status, 400);
+});
+
+test('ttl bounds enforced', async () => {
+ const res = await post({ url: 'https://example.com', ttlSeconds: 99999999 });
+ assert.equal(res.status, 400);
+ assert.equal((await res.json()).error.code, 'INVALID_TTL');
+});
+
+test('delete flow', async () => {
+ const { code } = await (await post({ url: 'https://example.com/c' })).json();
+ const del = await fetch(`http://127.0.0.1:${port}/links/${code}`, { method: 'DELETE' });
+ assert.equal(del.status, 204);
+ assert.equal((await get(`/${code}`)).status, 404);
+});
+
+test('stats start at zero and count redirects', async () => {
+ const { code } = await (await post({ url: 'https://example.com/d' })).json();
+ const zero = await (await fetch(`http://127.0.0.1:${port}/links/${code}/stats`)).json();
+ assert.equal(zero.hits, 0);
+ await get(`/${code}`);
+ await get(`/${code}`);
+ const two = await (await fetch(`http://127.0.0.1:${port}/links/${code}/stats`)).json();
+ assert.equal(two.hits, 2);
+});
+
+test('stats for unknown code are a 404 envelope', async () => {
+ const res = await fetch(`http://127.0.0.1:${port}/links/zzzzzz/stats`);
+ assert.equal(res.status, 404);
+ assert.equal((await res.json()).error.code, 'NOT_FOUND');
+});
+
+test('expired links are 410', async () => {
+ const { code } = await (await post({ url: 'https://example.com/e', ttlSeconds: 1 })).json();
+ await new Promise(resolve => setTimeout(resolve, 1200));
+ assert.equal((await get(`/${code}`)).status, 410);
+});
+
+test('malformed json is a 400 envelope', async () => {
+ const res = await fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: '{nope' });
+ assert.equal(res.status, 400);
+ assert.equal((await res.json()).error.code, 'INVALID_JSON');
+});
+
+test('error responses never leak html', async () => {
+ const res = await get('/zzzzzz');
+ assert.match(res.headers.get('content-type'), /application\/json/);
+});
+
+// Last: the flood exhausts the per-client rate-limit bucket.
+test('rate limiting kicks in under a flood', async () => {
+ const responses = await Promise.all(Array.from({ length: 30 }, (_, i) =>
+ post({ url: `https://example.com/flood-${i}` })));
+ assert.ok(responses.some(r => r.status === 429));
+});
diff --git a/docker/context-profiles/complex-eval/reference3/idempotent-webhooks/CHANGELOG.md b/docker/context-profiles/complex-eval/reference3/idempotent-webhooks/CHANGELOG.md
new file mode 100644
index 000000000..e0560c4a5
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/idempotent-webhooks/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Changelog
+
+- 2026-09-25: Fixed INC-104 — the receiver now claims each event id and applies
+ the payment synchronously in one event-loop turn, so concurrent duplicate
+ deliveries can never both pass the seen-check. Added idempotency regression
+ tests for concurrent duplicates, retries, and already-paid orders.
diff --git a/docker/context-profiles/complex-eval/reference3/idempotent-webhooks/src/app.js b/docker/context-profiles/complex-eval/reference3/idempotent-webhooks/src/app.js
new file mode 100644
index 000000000..57f29c250
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/idempotent-webhooks/src/app.js
@@ -0,0 +1,87 @@
+'use strict';
+const http = require('node:http');
+const { store } = require('./store');
+
+// Fixed after INC-104: all state checks and mutations happen synchronously in
+// one turn of the event loop — an event is claimed the instant its body is
+// parsed, before any await, so concurrent duplicates can never both pass.
+class HttpError extends Error {
+ constructor(status, code, message) {
+ super(message);
+ this.status = status;
+ this.code = code;
+ }
+}
+
+function sendJson(res, status, value) {
+ res.writeHead(status, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(value));
+}
+
+function sendError(res, error) {
+ const known = error instanceof HttpError;
+ sendJson(res, known ? error.status : 500, {
+ error: { code: known ? error.code : 'INTERNAL', message: known ? error.message : 'internal error' },
+ });
+}
+
+function readBody(req) {
+ return new Promise((resolve, reject) => {
+ let body = '';
+ req.on('data', chunk => { body += chunk; });
+ req.on('end', () => {
+ try { resolve(JSON.parse(body)); } catch { reject(new HttpError(400, 'INVALID_JSON', 'body must be valid JSON')); }
+ });
+ req.on('error', reject);
+ });
+}
+
+function validateEvent(parsed) {
+ if (!parsed || typeof parsed.eventId !== 'string' || !parsed.eventId
+ || typeof parsed.orderId !== 'string' || !parsed.orderId
+ || !Number.isInteger(parsed.amountCents) || parsed.amountCents <= 0
+ || parsed.type !== 'payment.succeeded') {
+ throw new HttpError(400, 'INVALID_EVENT', 'body must be a valid payment.succeeded event');
+ }
+ return parsed;
+}
+
+// Synchronous claim-and-apply: no awaits inside, so it is atomic.
+function applyEvent({ eventId, orderId, amountCents }) {
+ if (store.processedEvents.has(eventId)) return { status: 'duplicate', orderId };
+ const order = store.orders.get(orderId);
+ if (!order) throw new HttpError(404, 'NOT_FOUND', 'no such order');
+ if (order.amountCents !== amountCents) throw new HttpError(422, 'AMOUNT_MISMATCH', 'amountCents does not match the order');
+ if (order.status === 'paid') return { status: 'already_paid', orderId };
+ store.processedEvents.add(eventId);
+ order.status = 'paid';
+ order.paidAt = new Date().toISOString();
+ order.paymentsApplied++;
+ store.paymentLog.push({ eventId, orderId, amountCents });
+ return { status: 'processed', orderId };
+}
+
+function createApp() {
+ return http.createServer(async (req, res) => {
+ const url = new URL(req.url, 'http://localhost');
+ try {
+ if (req.method === 'POST' && url.pathname === '/webhooks/payments') {
+ const parsed = validateEvent(await readBody(req));
+ sendJson(res, 200, applyEvent(parsed));
+ return;
+ }
+ const match = /^\/orders\/([\w-]+)$/.exec(url.pathname);
+ if (req.method === 'GET' && match) {
+ const order = store.orders.get(match[1]);
+ if (!order) throw new HttpError(404, 'NOT_FOUND', 'no such order');
+ sendJson(res, 200, order);
+ return;
+ }
+ throw new HttpError(404, 'NOT_FOUND', 'not found');
+ } catch (error) {
+ sendError(res, error);
+ }
+ });
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/reference3/idempotent-webhooks/test/webhooks.test.js b/docker/context-profiles/complex-eval/reference3/idempotent-webhooks/test/webhooks.test.js
new file mode 100644
index 000000000..cdd102f49
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/idempotent-webhooks/test/webhooks.test.js
@@ -0,0 +1,60 @@
+'use strict';
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createApp } = require('../src/app');
+const { store } = require('../src/store');
+
+let server;
+let port;
+test.before(async () => {
+ server = createApp();
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ port = server.address().port;
+});
+test.after(() => server.close());
+
+const send = (eventId, orderId, amountCents) => fetch(`http://127.0.0.1:${port}/webhooks/payments`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ eventId, orderId, amountCents, type: 'payment.succeeded' }) });
+
+test('a single payment event processes', async () => {
+ const res = await send('ev-t-1', 'o1', 5000);
+ assert.equal(res.status, 200);
+ assert.equal((await res.json()).status, 'processed');
+ assert.equal(store.orders.get('o1').status, 'paid');
+});
+
+test('a sequential retry is an inert duplicate', async () => {
+ await send('ev-t-2', 'o3', 800);
+ const before = store.paymentLog.filter(p => p.orderId === 'o3').length;
+ const res = await send('ev-t-2', 'o3', 800);
+ assert.equal((await res.json()).status, 'duplicate');
+ assert.equal(store.paymentLog.filter(p => p.orderId === 'o3').length, before);
+});
+
+test('fifty concurrent duplicates apply exactly once (INC-104 regression)', async () => {
+ const storm = await Promise.all(Array.from({ length: 50 }, () => send('ev-t-storm', 'o4', 9999)));
+ const bodies = [];
+ for (const r of storm) bodies.push(await r.json());
+ assert.equal(bodies.filter(b => b.status === 'processed').length, 1);
+ assert.equal(bodies.filter(b => b.status === 'duplicate').length, 49);
+ assert.equal(store.orders.get('o4').paymentsApplied, 1);
+});
+
+test('a second event for a paid order is already_paid', async () => {
+ const res = await send('ev-t-3', 'o4', 9999);
+ assert.equal((await res.json()).status, 'already_paid');
+ assert.equal(store.orders.get('o4').paymentsApplied, 1);
+});
+
+test('amount mismatch is 422 and inert', async () => {
+ const res = await send('ev-t-4', 'o5', 1);
+ assert.equal(res.status, 422);
+ assert.equal(store.orders.get('o5').status, 'pending');
+});
+
+test('unknown order is a 404 envelope', async () => {
+ const res = await send('ev-t-5', 'nope', 100);
+ assert.equal(res.status, 404);
+ assert.equal((await res.json()).error.code, 'NOT_FOUND');
+});
diff --git a/docker/context-profiles/complex-eval/reference3/production-ready/CHANGELOG.md b/docker/context-profiles/complex-eval/reference3/production-ready/CHANGELOG.md
new file mode 100644
index 000000000..e0b0f3c6a
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/production-ready/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Changelog
+
+- 2026-09-25: Production hardening — request validation with structured JSON
+ error envelopes, 64 KB body limit with 413, /health endpoint, structured
+ JSON request logging, PORT from the environment, graceful SIGTERM shutdown,
+ nosniff headers, and error-path test coverage.
diff --git a/docker/context-profiles/complex-eval/reference3/production-ready/src/app.js b/docker/context-profiles/complex-eval/reference3/production-ready/src/app.js
new file mode 100644
index 000000000..ccdecd16e
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/production-ready/src/app.js
@@ -0,0 +1,100 @@
+'use strict';
+const http = require('node:http');
+
+const MAX_BODY_BYTES = Number(process.env.MAX_BODY_BYTES || 64 * 1024);
+
+class HttpError extends Error {
+ constructor(status, code, message) {
+ super(message);
+ this.status = status;
+ this.code = code;
+ }
+}
+
+function sendJson(res, status, value) {
+ res.writeHead(status, { 'content-type': 'application/json', 'x-content-type-options': 'nosniff' });
+ res.end(JSON.stringify(value));
+}
+
+function sendError(res, error) {
+ const known = error instanceof HttpError;
+ sendJson(res, known ? error.status : 500, {
+ error: { code: known ? error.code : 'INTERNAL', message: known ? error.message : 'internal error' },
+ });
+}
+
+function readBody(req) {
+ return new Promise((resolve, reject) => {
+ let body = '';
+ let bytes = 0;
+ let settled = false;
+ req.on('data', chunk => {
+ if (settled) return;
+ bytes += chunk.length;
+ if (bytes > MAX_BODY_BYTES) {
+ settled = true;
+ reject(new HttpError(413, 'PAYLOAD_TOO_LARGE', 'request body exceeds 64 KB'));
+ // Drain rather than destroy: the socket must live long enough to send the 413.
+ req.resume();
+ return;
+ }
+ body += chunk;
+ });
+ req.on('end', () => {
+ if (settled) return;
+ settled = true;
+ try { resolve(JSON.parse(body)); } catch { reject(new HttpError(400, 'INVALID_JSON', 'body must be valid JSON')); }
+ });
+ req.on('error', reject);
+ });
+}
+
+function validateNote(input) {
+ if (!input || typeof input.title !== 'string' || !input.title.trim()) {
+ throw new HttpError(400, 'INVALID_TITLE', 'title must be a non-empty string');
+ }
+ if (typeof input.body !== 'string') throw new HttpError(400, 'INVALID_BODY', 'body must be a string');
+ return { title: input.title, body: input.body };
+}
+
+function createApp() {
+ const notes = new Map();
+ let nextId = 1;
+
+ const server = http.createServer(async (req, res) => {
+ const url = new URL(req.url, 'http://localhost');
+ try {
+ if (req.method === 'GET' && url.pathname === '/health') {
+ sendJson(res, 200, { status: 'ok' });
+ return;
+ }
+ if (req.method === 'POST' && url.pathname === '/notes') {
+ const fields = validateNote(await readBody(req));
+ const id = `n_${nextId++}`;
+ notes.set(id, { id, ...fields });
+ sendJson(res, 201, notes.get(id));
+ return;
+ }
+ const match = /^\/notes\/([\w-]+)$/.exec(url.pathname);
+ if (req.method === 'GET' && match) {
+ const note = notes.get(match[1]);
+ if (!note) throw new HttpError(404, 'NOT_FOUND', 'no note with that id');
+ sendJson(res, 200, note);
+ return;
+ }
+ if (req.method === 'GET' && url.pathname === '/notes') {
+ sendJson(res, 200, { notes: [...notes.values()] });
+ return;
+ }
+ throw new HttpError(404, 'NOT_FOUND', 'not found');
+ } catch (error) {
+ sendError(res, error);
+ } finally {
+ console.log(JSON.stringify({ method: req.method, path: url.pathname,
+ status: res.statusCode, at: new Date().toISOString() }));
+ }
+ });
+ return server;
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/reference3/production-ready/src/index.js b/docker/context-profiles/complex-eval/reference3/production-ready/src/index.js
new file mode 100644
index 000000000..9b1d0a0d7
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/production-ready/src/index.js
@@ -0,0 +1,13 @@
+'use strict';
+const { createApp } = require('./app');
+
+const port = Number(process.env.PORT || 8080);
+const server = createApp();
+server.listen(port, () => {
+ console.log(JSON.stringify({ event: 'listening', port }));
+});
+
+process.on('SIGTERM', () => {
+ server.close(() => process.exit(0));
+ setTimeout(() => process.exit(1), 5000).unref();
+});
diff --git a/docker/context-profiles/complex-eval/reference3/production-ready/test/notes.test.js b/docker/context-profiles/complex-eval/reference3/production-ready/test/notes.test.js
new file mode 100644
index 000000000..65e4ca200
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference3/production-ready/test/notes.test.js
@@ -0,0 +1,58 @@
+'use strict';
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createApp } = require('../src/app');
+
+let server;
+let port;
+test.before(async () => {
+ server = createApp();
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ port = server.address().port;
+});
+test.after(() => server.close());
+
+const post = body => fetch(`http://127.0.0.1:${port}/notes`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body });
+
+test('create and read a note', async () => {
+ const created = await post(JSON.stringify({ title: 'first', body: 'hello' }));
+ assert.equal(created.status, 201);
+ const { id } = await created.json();
+ const read = await fetch(`http://127.0.0.1:${port}/notes/${id}`);
+ assert.equal((await read.json()).title, 'first');
+});
+
+test('malformed json is a 400 envelope', async () => {
+ const res = await post('{oops');
+ assert.equal(res.status, 400);
+ assert.equal((await res.json()).error.code, 'INVALID_JSON');
+});
+
+test('missing title is a 400 envelope', async () => {
+ const res = await post(JSON.stringify({ body: 'x' }));
+ assert.equal(res.status, 400);
+ assert.equal((await res.json()).error.code, 'INVALID_TITLE');
+});
+
+test('unknown note is a 404 envelope', async () => {
+ const res = await fetch(`http://127.0.0.1:${port}/notes/n_9999`);
+ assert.equal(res.status, 404);
+ assert.equal((await res.json()).error.code, 'NOT_FOUND');
+});
+
+test('oversize body is a 413 envelope', async () => {
+ const res = await post(JSON.stringify({ title: 'x', body: 'y'.repeat(100 * 1024) }));
+ assert.equal(res.status, 413);
+});
+
+test('health endpoint', async () => {
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
+ assert.equal(res.status, 200);
+ assert.equal((await res.json()).status, 'ok');
+});
+
+test('nosniff header present', async () => {
+ const res = await fetch(`http://127.0.0.1:${port}/notes`);
+ assert.equal(res.headers.get('x-content-type-options'), 'nosniff');
+});
diff --git a/docker/context-profiles/complex-eval/reference4/chained-tickets/CHANGELOG.md b/docker/context-profiles/complex-eval/reference4/chained-tickets/CHANGELOG.md
new file mode 100644
index 000000000..e8cad2f0c
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/chained-tickets/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Changelog
+
+- 2026-09-25: Initial shortlink core — create, redirect, expiry, and delete per API.md.
+- 2026-09-25: Persistence — links survive restarts via the DATA_FILE JSON store; missing or corrupt data files start clean.
+- 2026-09-25: Abuse protection — URL validation (http/https only, length cap), request body limits, and per-client rate limiting with 429 responses.
+- 2026-09-25: Analytics — per-link redirect hit counts exposed at GET /links/:code/stats.
diff --git a/docker/context-profiles/complex-eval/reference4/chained-tickets/README.md b/docker/context-profiles/complex-eval/reference4/chained-tickets/README.md
new file mode 100644
index 000000000..3420482fe
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/chained-tickets/README.md
@@ -0,0 +1,14 @@
+# shortlink
+
+Internal link shortener service. Node.js standard library only, CommonJS.
+
+- `API.md` — the HTTP contract.
+- `CONTRIBUTING.md` — engineering conventions. Every ticket follows them.
+- `src/app.js` exports `createApp()` returning an `http.Server` that is not yet
+ listening; `node src/index.js ` starts the service.
+- Links persist to the JSON file named by the `DATA_FILE` environment variable
+ (default `./data/links.json`).
+- `GET /links//stats` returns `{ "code", "hits", "expiresAt" }` —
+ `hits` counts redirects.
+- The API is rate limited per client and validates URLs (http/https only).
+- Run the tests with `npm test`.
diff --git a/docker/context-profiles/complex-eval/reference4/chained-tickets/src/app.js b/docker/context-profiles/complex-eval/reference4/chained-tickets/src/app.js
new file mode 100644
index 000000000..c802a64fd
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/chained-tickets/src/app.js
@@ -0,0 +1,15 @@
+'use strict';
+const http = require('node:http');
+const path = require('node:path');
+const { createStore } = require('./store');
+const { createService } = require('./service');
+const { createRouter } = require('./routes');
+
+function createApp() {
+ const file = process.env.DATA_FILE || path.join(process.cwd(), 'data', 'links.json');
+ const store = createStore(file);
+ const service = createService(store);
+ return http.createServer(createRouter(service));
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/reference4/chained-tickets/src/index.js b/docker/context-profiles/complex-eval/reference4/chained-tickets/src/index.js
new file mode 100644
index 000000000..d37872b76
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/chained-tickets/src/index.js
@@ -0,0 +1,7 @@
+'use strict';
+const { createApp } = require('./app');
+
+const port = Number(process.env.PORT || process.argv[2] || 8080);
+createApp().listen(port, () => {
+ console.log(`shortlink listening on ${port}`);
+});
diff --git a/docker/context-profiles/complex-eval/reference4/chained-tickets/src/routes.js b/docker/context-profiles/complex-eval/reference4/chained-tickets/src/routes.js
new file mode 100644
index 000000000..7344146c6
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/chained-tickets/src/routes.js
@@ -0,0 +1,86 @@
+'use strict';
+const { HttpError } = require('./service');
+
+const MAX_BODY_BYTES = 64 * 1024;
+
+function sendJson(res, status, value) {
+ res.writeHead(status, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(value));
+}
+
+function sendError(res, error) {
+ const known = error instanceof HttpError;
+ sendJson(res, known ? error.status : 500, {
+ error: { code: known ? error.code : 'INTERNAL', message: known ? error.message : 'internal error' },
+ });
+}
+
+function readBody(req) {
+ return new Promise((resolve, reject) => {
+ let body = '';
+ let bytes = 0;
+ let settled = false;
+ req.on('data', chunk => {
+ if (settled) return;
+ bytes += chunk.length;
+ if (bytes > MAX_BODY_BYTES) {
+ settled = true;
+ reject(new HttpError(413, 'PAYLOAD_TOO_LARGE', 'request body too large'));
+ // Drain rather than destroy: the socket must live long enough to send the 413.
+ req.resume();
+ return;
+ }
+ body += chunk;
+ });
+ req.on('end', () => {
+ if (settled) return;
+ settled = true;
+ if (!body) { resolve({}); return; }
+ try { resolve(JSON.parse(body)); } catch { reject(new HttpError(400, 'INVALID_JSON', 'body must be valid JSON')); }
+ });
+ req.on('error', reject);
+ });
+}
+
+function createRouter(service) {
+ return async (req, res) => {
+ try {
+ const url = new URL(req.url, 'http://localhost');
+
+ if (req.method === 'POST' && url.pathname === '/links') {
+ service.assertRateLimit(req.socket.remoteAddress || 'unknown');
+ const link = service.createLink(await readBody(req));
+ sendJson(res, 201, { code: link.code, shortUrl: `/${link.code}`, expiresAt: link.expiresAt });
+ return;
+ }
+
+ const statsMatch = /^\/links\/([A-Za-z0-9]{1,20})\/stats$/.exec(url.pathname);
+ if (req.method === 'GET' && statsMatch) {
+ sendJson(res, 200, service.stats(statsMatch[1]));
+ return;
+ }
+
+ const linkMatch = /^\/links\/([A-Za-z0-9]{1,20})$/.exec(url.pathname);
+ if (req.method === 'DELETE' && linkMatch) {
+ service.deleteLink(linkMatch[1]);
+ res.writeHead(204);
+ res.end();
+ return;
+ }
+
+ const redirectMatch = /^\/([A-Za-z0-9]{1,20})$/.exec(url.pathname);
+ if (req.method === 'GET' && redirectMatch) {
+ const link = service.resolveLink(redirectMatch[1]);
+ res.writeHead(302, { location: link.url });
+ res.end();
+ return;
+ }
+
+ throw new HttpError(404, 'NOT_FOUND', 'not found');
+ } catch (error) {
+ sendError(res, error);
+ }
+ };
+}
+
+module.exports = { createRouter };
diff --git a/docker/context-profiles/complex-eval/reference4/chained-tickets/src/service.js b/docker/context-profiles/complex-eval/reference4/chained-tickets/src/service.js
new file mode 100644
index 000000000..f28167d8a
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/chained-tickets/src/service.js
@@ -0,0 +1,82 @@
+'use strict';
+const crypto = require('node:crypto');
+
+const MAX_URL_LENGTH = 2048;
+const DEFAULT_TTL_SECONDS = 604800;
+const MAX_TTL_SECONDS = 2592000;
+const RATE_LIMIT_WINDOW_MS = 60000;
+const RATE_LIMIT_MAX = 20;
+
+class HttpError extends Error {
+ constructor(status, code, message) {
+ super(message);
+ this.status = status;
+ this.code = code;
+ }
+}
+
+function validateUrl(url) {
+ if (typeof url !== 'string' || !url) throw new HttpError(400, 'INVALID_URL', 'url is required');
+ if (url.length > MAX_URL_LENGTH) throw new HttpError(400, 'INVALID_URL', 'url exceeds 2048 characters');
+ let parsed;
+ try { parsed = new URL(url); } catch { throw new HttpError(400, 'INVALID_URL', 'url must be a valid absolute URL'); }
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
+ throw new HttpError(400, 'INVALID_URL', 'only http and https URLs are allowed');
+ }
+ return url;
+}
+
+function validateTtl(ttlSeconds) {
+ if (ttlSeconds === undefined || ttlSeconds === null) return DEFAULT_TTL_SECONDS;
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1 || ttlSeconds > MAX_TTL_SECONDS) {
+ throw new HttpError(400, 'INVALID_TTL', 'ttlSeconds must be an integer between 1 and 2592000');
+ }
+ return ttlSeconds;
+}
+
+function createService(store) {
+ const buckets = new Map();
+
+ function assertRateLimit(key) {
+ const now = Date.now();
+ const windowHits = (buckets.get(key) || []).filter(at => now - at < RATE_LIMIT_WINDOW_MS);
+ if (windowHits.length >= RATE_LIMIT_MAX) throw new HttpError(429, 'RATE_LIMITED', 'too many requests, slow down');
+ windowHits.push(now);
+ buckets.set(key, windowHits);
+ }
+
+ function freshCode() {
+ let code = crypto.randomBytes(4).toString('hex');
+ while (store.get(code)) code = crypto.randomBytes(4).toString('hex');
+ return code;
+ }
+
+ return {
+ assertRateLimit,
+ createLink({ url, ttlSeconds } = {}) {
+ const validUrl = validateUrl(url);
+ const ttl = validateTtl(ttlSeconds);
+ const link = { code: freshCode(), url: validUrl,
+ expiresAt: new Date(Date.now() + ttl * 1000).toISOString(), hits: 0 };
+ store.set(link.code, link);
+ return link;
+ },
+ resolveLink(code) {
+ const link = store.get(code);
+ if (!link) throw new HttpError(404, 'NOT_FOUND', 'no link with that code');
+ if (Date.parse(link.expiresAt) <= Date.now()) throw new HttpError(410, 'GONE', 'link has expired');
+ store.incrementHits(code);
+ return link;
+ },
+ deleteLink(code) {
+ if (!store.delete(code)) throw new HttpError(404, 'NOT_FOUND', 'no link with that code');
+ },
+ stats(code) {
+ const link = store.get(code);
+ if (!link) throw new HttpError(404, 'NOT_FOUND', 'no link with that code');
+ return { code, hits: link.hits || 0, expiresAt: link.expiresAt };
+ },
+ };
+}
+
+module.exports = { createService, HttpError };
diff --git a/docker/context-profiles/complex-eval/reference4/chained-tickets/src/store.js b/docker/context-profiles/complex-eval/reference4/chained-tickets/src/store.js
new file mode 100644
index 000000000..7d5aa091b
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/chained-tickets/src/store.js
@@ -0,0 +1,28 @@
+'use strict';
+const fs = require('node:fs');
+const path = require('node:path');
+
+// JSON-file-backed link store. Missing or corrupt files start clean; every
+// mutation is flushed synchronously so a restart never loses a committed link.
+function createStore(file) {
+ let links = new Map();
+ try {
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
+ for (const [code, value] of Object.entries(raw.links || {})) links.set(code, value);
+ } catch { /* missing or corrupt: start empty */ }
+ const save = () => {
+ fs.mkdirSync(path.dirname(file), { recursive: true });
+ fs.writeFileSync(file, `${JSON.stringify({ links: Object.fromEntries(links) }, null, 1)}\n`);
+ };
+ return {
+ get: code => links.get(code) || null,
+ set(code, value) { links.set(code, value); save(); },
+ delete(code) { const had = links.delete(code); if (had) save(); return had; },
+ incrementHits(code) {
+ const link = links.get(code);
+ if (link) { link.hits = (link.hits || 0) + 1; save(); }
+ },
+ };
+}
+
+module.exports = { createStore };
diff --git a/docker/context-profiles/complex-eval/reference4/chained-tickets/test/links.test.js b/docker/context-profiles/complex-eval/reference4/chained-tickets/test/links.test.js
new file mode 100644
index 000000000..1a358d43d
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/chained-tickets/test/links.test.js
@@ -0,0 +1,106 @@
+'use strict';
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createApp } = require('../src/app');
+
+process.env.DATA_FILE = require('node:path').join(require('node:os').tmpdir(),
+ `shortlink-test-${process.pid}.json`);
+
+let server;
+let port;
+test.before(async () => {
+ server = createApp();
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ port = server.address().port;
+});
+test.after(() => server.close());
+
+const post = body => fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
+const get = p => fetch(`http://127.0.0.1:${port}${p}`, { redirect: 'manual' });
+
+test('creates a link with default expiry', async () => {
+ const res = await post({ url: 'https://example.com/a' });
+ assert.equal(res.status, 201);
+ const body = await res.json();
+ assert.match(body.code, /^[A-Za-z0-9]{6,10}$/);
+ assert.ok(Date.parse(body.expiresAt) > Date.now());
+});
+
+test('redirects with 302 and location', async () => {
+ const { code } = await (await post({ url: 'https://example.com/b' })).json();
+ const res = await get(`/${code}`);
+ assert.equal(res.status, 302);
+ assert.equal(res.headers.get('location'), 'https://example.com/b');
+});
+
+test('unknown code is a 404 envelope', async () => {
+ const res = await get('/zzzzzz');
+ assert.equal(res.status, 404);
+ assert.equal((await res.json()).error.code, 'NOT_FOUND');
+});
+
+test('invalid url is a 400 envelope', async () => {
+ const res = await post({ url: 'notaurl' });
+ assert.equal(res.status, 400);
+ assert.equal((await res.json()).error.code, 'INVALID_URL');
+});
+
+test('javascript scheme rejected', async () => {
+ const res = await post({ url: 'javascript:alert(1)' });
+ assert.equal(res.status, 400);
+});
+
+test('ttl bounds enforced', async () => {
+ const res = await post({ url: 'https://example.com', ttlSeconds: 99999999 });
+ assert.equal(res.status, 400);
+ assert.equal((await res.json()).error.code, 'INVALID_TTL');
+});
+
+test('delete flow', async () => {
+ const { code } = await (await post({ url: 'https://example.com/c' })).json();
+ const del = await fetch(`http://127.0.0.1:${port}/links/${code}`, { method: 'DELETE' });
+ assert.equal(del.status, 204);
+ assert.equal((await get(`/${code}`)).status, 404);
+});
+
+test('stats start at zero and count redirects', async () => {
+ const { code } = await (await post({ url: 'https://example.com/d' })).json();
+ const zero = await (await fetch(`http://127.0.0.1:${port}/links/${code}/stats`)).json();
+ assert.equal(zero.hits, 0);
+ await get(`/${code}`);
+ await get(`/${code}`);
+ const two = await (await fetch(`http://127.0.0.1:${port}/links/${code}/stats`)).json();
+ assert.equal(two.hits, 2);
+});
+
+test('stats for unknown code are a 404 envelope', async () => {
+ const res = await fetch(`http://127.0.0.1:${port}/links/zzzzzz/stats`);
+ assert.equal(res.status, 404);
+ assert.equal((await res.json()).error.code, 'NOT_FOUND');
+});
+
+test('expired links are 410', async () => {
+ const { code } = await (await post({ url: 'https://example.com/e', ttlSeconds: 1 })).json();
+ await new Promise(resolve => setTimeout(resolve, 1200));
+ assert.equal((await get(`/${code}`)).status, 410);
+});
+
+test('malformed json is a 400 envelope', async () => {
+ const res = await fetch(`http://127.0.0.1:${port}/links`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: '{nope' });
+ assert.equal(res.status, 400);
+ assert.equal((await res.json()).error.code, 'INVALID_JSON');
+});
+
+test('error responses never leak html', async () => {
+ const res = await get('/zzzzzz');
+ assert.match(res.headers.get('content-type'), /application\/json/);
+});
+
+// Last: the flood exhausts the per-client rate-limit bucket.
+test('rate limiting kicks in under a flood', async () => {
+ const responses = await Promise.all(Array.from({ length: 30 }, (_, i) =>
+ post({ url: `https://example.com/flood-${i}` })));
+ assert.ok(responses.some(r => r.status === 429));
+});
diff --git a/docker/context-profiles/complex-eval/reference4/idempotent-webhooks/CHANGELOG.md b/docker/context-profiles/complex-eval/reference4/idempotent-webhooks/CHANGELOG.md
new file mode 100644
index 000000000..e0560c4a5
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/idempotent-webhooks/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Changelog
+
+- 2026-09-25: Fixed INC-104 — the receiver now claims each event id and applies
+ the payment synchronously in one event-loop turn, so concurrent duplicate
+ deliveries can never both pass the seen-check. Added idempotency regression
+ tests for concurrent duplicates, retries, and already-paid orders.
diff --git a/docker/context-profiles/complex-eval/reference4/idempotent-webhooks/src/app.js b/docker/context-profiles/complex-eval/reference4/idempotent-webhooks/src/app.js
new file mode 100644
index 000000000..57f29c250
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/idempotent-webhooks/src/app.js
@@ -0,0 +1,87 @@
+'use strict';
+const http = require('node:http');
+const { store } = require('./store');
+
+// Fixed after INC-104: all state checks and mutations happen synchronously in
+// one turn of the event loop — an event is claimed the instant its body is
+// parsed, before any await, so concurrent duplicates can never both pass.
+class HttpError extends Error {
+ constructor(status, code, message) {
+ super(message);
+ this.status = status;
+ this.code = code;
+ }
+}
+
+function sendJson(res, status, value) {
+ res.writeHead(status, { 'content-type': 'application/json' });
+ res.end(JSON.stringify(value));
+}
+
+function sendError(res, error) {
+ const known = error instanceof HttpError;
+ sendJson(res, known ? error.status : 500, {
+ error: { code: known ? error.code : 'INTERNAL', message: known ? error.message : 'internal error' },
+ });
+}
+
+function readBody(req) {
+ return new Promise((resolve, reject) => {
+ let body = '';
+ req.on('data', chunk => { body += chunk; });
+ req.on('end', () => {
+ try { resolve(JSON.parse(body)); } catch { reject(new HttpError(400, 'INVALID_JSON', 'body must be valid JSON')); }
+ });
+ req.on('error', reject);
+ });
+}
+
+function validateEvent(parsed) {
+ if (!parsed || typeof parsed.eventId !== 'string' || !parsed.eventId
+ || typeof parsed.orderId !== 'string' || !parsed.orderId
+ || !Number.isInteger(parsed.amountCents) || parsed.amountCents <= 0
+ || parsed.type !== 'payment.succeeded') {
+ throw new HttpError(400, 'INVALID_EVENT', 'body must be a valid payment.succeeded event');
+ }
+ return parsed;
+}
+
+// Synchronous claim-and-apply: no awaits inside, so it is atomic.
+function applyEvent({ eventId, orderId, amountCents }) {
+ if (store.processedEvents.has(eventId)) return { status: 'duplicate', orderId };
+ const order = store.orders.get(orderId);
+ if (!order) throw new HttpError(404, 'NOT_FOUND', 'no such order');
+ if (order.amountCents !== amountCents) throw new HttpError(422, 'AMOUNT_MISMATCH', 'amountCents does not match the order');
+ if (order.status === 'paid') return { status: 'already_paid', orderId };
+ store.processedEvents.add(eventId);
+ order.status = 'paid';
+ order.paidAt = new Date().toISOString();
+ order.paymentsApplied++;
+ store.paymentLog.push({ eventId, orderId, amountCents });
+ return { status: 'processed', orderId };
+}
+
+function createApp() {
+ return http.createServer(async (req, res) => {
+ const url = new URL(req.url, 'http://localhost');
+ try {
+ if (req.method === 'POST' && url.pathname === '/webhooks/payments') {
+ const parsed = validateEvent(await readBody(req));
+ sendJson(res, 200, applyEvent(parsed));
+ return;
+ }
+ const match = /^\/orders\/([\w-]+)$/.exec(url.pathname);
+ if (req.method === 'GET' && match) {
+ const order = store.orders.get(match[1]);
+ if (!order) throw new HttpError(404, 'NOT_FOUND', 'no such order');
+ sendJson(res, 200, order);
+ return;
+ }
+ throw new HttpError(404, 'NOT_FOUND', 'not found');
+ } catch (error) {
+ sendError(res, error);
+ }
+ });
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/reference4/idempotent-webhooks/test/webhooks.test.js b/docker/context-profiles/complex-eval/reference4/idempotent-webhooks/test/webhooks.test.js
new file mode 100644
index 000000000..cdd102f49
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/idempotent-webhooks/test/webhooks.test.js
@@ -0,0 +1,60 @@
+'use strict';
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createApp } = require('../src/app');
+const { store } = require('../src/store');
+
+let server;
+let port;
+test.before(async () => {
+ server = createApp();
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ port = server.address().port;
+});
+test.after(() => server.close());
+
+const send = (eventId, orderId, amountCents) => fetch(`http://127.0.0.1:${port}/webhooks/payments`, {
+ method: 'POST', headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ eventId, orderId, amountCents, type: 'payment.succeeded' }) });
+
+test('a single payment event processes', async () => {
+ const res = await send('ev-t-1', 'o1', 5000);
+ assert.equal(res.status, 200);
+ assert.equal((await res.json()).status, 'processed');
+ assert.equal(store.orders.get('o1').status, 'paid');
+});
+
+test('a sequential retry is an inert duplicate', async () => {
+ await send('ev-t-2', 'o3', 800);
+ const before = store.paymentLog.filter(p => p.orderId === 'o3').length;
+ const res = await send('ev-t-2', 'o3', 800);
+ assert.equal((await res.json()).status, 'duplicate');
+ assert.equal(store.paymentLog.filter(p => p.orderId === 'o3').length, before);
+});
+
+test('fifty concurrent duplicates apply exactly once (INC-104 regression)', async () => {
+ const storm = await Promise.all(Array.from({ length: 50 }, () => send('ev-t-storm', 'o4', 9999)));
+ const bodies = [];
+ for (const r of storm) bodies.push(await r.json());
+ assert.equal(bodies.filter(b => b.status === 'processed').length, 1);
+ assert.equal(bodies.filter(b => b.status === 'duplicate').length, 49);
+ assert.equal(store.orders.get('o4').paymentsApplied, 1);
+});
+
+test('a second event for a paid order is already_paid', async () => {
+ const res = await send('ev-t-3', 'o4', 9999);
+ assert.equal((await res.json()).status, 'already_paid');
+ assert.equal(store.orders.get('o4').paymentsApplied, 1);
+});
+
+test('amount mismatch is 422 and inert', async () => {
+ const res = await send('ev-t-4', 'o5', 1);
+ assert.equal(res.status, 422);
+ assert.equal(store.orders.get('o5').status, 'pending');
+});
+
+test('unknown order is a 404 envelope', async () => {
+ const res = await send('ev-t-5', 'nope', 100);
+ assert.equal(res.status, 404);
+ assert.equal((await res.json()).error.code, 'NOT_FOUND');
+});
diff --git a/docker/context-profiles/complex-eval/reference4/production-ready/CHANGELOG.md b/docker/context-profiles/complex-eval/reference4/production-ready/CHANGELOG.md
new file mode 100644
index 000000000..e0b0f3c6a
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/production-ready/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Changelog
+
+- 2026-09-25: Production hardening — request validation with structured JSON
+ error envelopes, 64 KB body limit with 413, /health endpoint, structured
+ JSON request logging, PORT from the environment, graceful SIGTERM shutdown,
+ nosniff headers, and error-path test coverage.
diff --git a/docker/context-profiles/complex-eval/reference4/production-ready/src/app.js b/docker/context-profiles/complex-eval/reference4/production-ready/src/app.js
new file mode 100644
index 000000000..ccdecd16e
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/production-ready/src/app.js
@@ -0,0 +1,100 @@
+'use strict';
+const http = require('node:http');
+
+const MAX_BODY_BYTES = Number(process.env.MAX_BODY_BYTES || 64 * 1024);
+
+class HttpError extends Error {
+ constructor(status, code, message) {
+ super(message);
+ this.status = status;
+ this.code = code;
+ }
+}
+
+function sendJson(res, status, value) {
+ res.writeHead(status, { 'content-type': 'application/json', 'x-content-type-options': 'nosniff' });
+ res.end(JSON.stringify(value));
+}
+
+function sendError(res, error) {
+ const known = error instanceof HttpError;
+ sendJson(res, known ? error.status : 500, {
+ error: { code: known ? error.code : 'INTERNAL', message: known ? error.message : 'internal error' },
+ });
+}
+
+function readBody(req) {
+ return new Promise((resolve, reject) => {
+ let body = '';
+ let bytes = 0;
+ let settled = false;
+ req.on('data', chunk => {
+ if (settled) return;
+ bytes += chunk.length;
+ if (bytes > MAX_BODY_BYTES) {
+ settled = true;
+ reject(new HttpError(413, 'PAYLOAD_TOO_LARGE', 'request body exceeds 64 KB'));
+ // Drain rather than destroy: the socket must live long enough to send the 413.
+ req.resume();
+ return;
+ }
+ body += chunk;
+ });
+ req.on('end', () => {
+ if (settled) return;
+ settled = true;
+ try { resolve(JSON.parse(body)); } catch { reject(new HttpError(400, 'INVALID_JSON', 'body must be valid JSON')); }
+ });
+ req.on('error', reject);
+ });
+}
+
+function validateNote(input) {
+ if (!input || typeof input.title !== 'string' || !input.title.trim()) {
+ throw new HttpError(400, 'INVALID_TITLE', 'title must be a non-empty string');
+ }
+ if (typeof input.body !== 'string') throw new HttpError(400, 'INVALID_BODY', 'body must be a string');
+ return { title: input.title, body: input.body };
+}
+
+function createApp() {
+ const notes = new Map();
+ let nextId = 1;
+
+ const server = http.createServer(async (req, res) => {
+ const url = new URL(req.url, 'http://localhost');
+ try {
+ if (req.method === 'GET' && url.pathname === '/health') {
+ sendJson(res, 200, { status: 'ok' });
+ return;
+ }
+ if (req.method === 'POST' && url.pathname === '/notes') {
+ const fields = validateNote(await readBody(req));
+ const id = `n_${nextId++}`;
+ notes.set(id, { id, ...fields });
+ sendJson(res, 201, notes.get(id));
+ return;
+ }
+ const match = /^\/notes\/([\w-]+)$/.exec(url.pathname);
+ if (req.method === 'GET' && match) {
+ const note = notes.get(match[1]);
+ if (!note) throw new HttpError(404, 'NOT_FOUND', 'no note with that id');
+ sendJson(res, 200, note);
+ return;
+ }
+ if (req.method === 'GET' && url.pathname === '/notes') {
+ sendJson(res, 200, { notes: [...notes.values()] });
+ return;
+ }
+ throw new HttpError(404, 'NOT_FOUND', 'not found');
+ } catch (error) {
+ sendError(res, error);
+ } finally {
+ console.log(JSON.stringify({ method: req.method, path: url.pathname,
+ status: res.statusCode, at: new Date().toISOString() }));
+ }
+ });
+ return server;
+}
+
+module.exports = { createApp };
diff --git a/docker/context-profiles/complex-eval/reference4/production-ready/src/index.js b/docker/context-profiles/complex-eval/reference4/production-ready/src/index.js
new file mode 100644
index 000000000..9b1d0a0d7
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/production-ready/src/index.js
@@ -0,0 +1,13 @@
+'use strict';
+const { createApp } = require('./app');
+
+const port = Number(process.env.PORT || 8080);
+const server = createApp();
+server.listen(port, () => {
+ console.log(JSON.stringify({ event: 'listening', port }));
+});
+
+process.on('SIGTERM', () => {
+ server.close(() => process.exit(0));
+ setTimeout(() => process.exit(1), 5000).unref();
+});
diff --git a/docker/context-profiles/complex-eval/reference4/production-ready/test/notes.test.js b/docker/context-profiles/complex-eval/reference4/production-ready/test/notes.test.js
new file mode 100644
index 000000000..65e4ca200
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/production-ready/test/notes.test.js
@@ -0,0 +1,58 @@
+'use strict';
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { createApp } = require('../src/app');
+
+let server;
+let port;
+test.before(async () => {
+ server = createApp();
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ port = server.address().port;
+});
+test.after(() => server.close());
+
+const post = body => fetch(`http://127.0.0.1:${port}/notes`, {
+ method: 'POST', headers: { 'content-type': 'application/json' }, body });
+
+test('create and read a note', async () => {
+ const created = await post(JSON.stringify({ title: 'first', body: 'hello' }));
+ assert.equal(created.status, 201);
+ const { id } = await created.json();
+ const read = await fetch(`http://127.0.0.1:${port}/notes/${id}`);
+ assert.equal((await read.json()).title, 'first');
+});
+
+test('malformed json is a 400 envelope', async () => {
+ const res = await post('{oops');
+ assert.equal(res.status, 400);
+ assert.equal((await res.json()).error.code, 'INVALID_JSON');
+});
+
+test('missing title is a 400 envelope', async () => {
+ const res = await post(JSON.stringify({ body: 'x' }));
+ assert.equal(res.status, 400);
+ assert.equal((await res.json()).error.code, 'INVALID_TITLE');
+});
+
+test('unknown note is a 404 envelope', async () => {
+ const res = await fetch(`http://127.0.0.1:${port}/notes/n_9999`);
+ assert.equal(res.status, 404);
+ assert.equal((await res.json()).error.code, 'NOT_FOUND');
+});
+
+test('oversize body is a 413 envelope', async () => {
+ const res = await post(JSON.stringify({ title: 'x', body: 'y'.repeat(100 * 1024) }));
+ assert.equal(res.status, 413);
+});
+
+test('health endpoint', async () => {
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
+ assert.equal(res.status, 200);
+ assert.equal((await res.json()).status, 'ok');
+});
+
+test('nosniff header present', async () => {
+ const res = await fetch(`http://127.0.0.1:${port}/notes`);
+ assert.equal(res.headers.get('x-content-type-options'), 'nosniff');
+});
diff --git a/docker/context-profiles/complex-eval/reference4/recurring-incident/docs/handoff.md b/docker/context-profiles/complex-eval/reference4/recurring-incident/docs/handoff.md
new file mode 100644
index 000000000..91d68d69f
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/recurring-incident/docs/handoff.md
@@ -0,0 +1,35 @@
+# Handoff: refunds & payouts idempotency
+
+## What happened
+
+Two incidents, one root cause family:
+
+- **Refunds** (INC-201, INC-214, INC-227 in docs/incidents.md): refund requests
+ arriving without an idempotency key were double-processed whenever the
+ storefront retried, refunding customers twice.
+- **Payouts**: finance's batch job is about to start retrying on timeouts, and
+ keyless payout retries would double-pay vendors the same way.
+
+## The fix
+
+Both entry points now route through a single shared helper,
+`src/idempotency.js` (`deriveKey` + `once`). `src/refunds.js` and
+`src/payouts.js` derive a stable key from the request payload when the caller
+sends none, claim it synchronously so concurrent retries share one execution,
+and persist the receipt in `src/store.js` so retries after a restart return the
+stored receipt. Gateway side effects all go through `src/charge.js`, so the
+ledger is the source of truth for "did this actually happen".
+
+## Regression coverage
+
+`test/idempotency.test.js` covers keyless refund retries, restart durability,
+and a 20-way concurrent payout storm. The pre-existing `test/refunds.test.js`
+and `test/payouts.test.js` still cover the keyed contract. Everything is wired
+into `npm test`; run it before touching any of this.
+
+## Prevention
+
+`docs/runbooks/idempotency.md` is the runbook: any new money-moving operation
+must go through `src/idempotency.js`, ship with a retry regression test, and
+log recurrences in `docs/incidents.md`. Do not bolt a second inline key-check
+into a new module — extend the helper instead.
diff --git a/docker/context-profiles/complex-eval/reference4/recurring-incident/docs/runbooks/idempotency.md b/docker/context-profiles/complex-eval/reference4/recurring-incident/docs/runbooks/idempotency.md
new file mode 100644
index 000000000..00c32cfa9
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/recurring-incident/docs/runbooks/idempotency.md
@@ -0,0 +1,35 @@
+# Runbook: idempotency for money-moving operations
+
+## The incident class
+
+INC-201, INC-214, INC-227 (refunds) and the payout double-pay risk flagged by
+finance are one class of bug: a caller retries a money-moving request that
+carries no idempotency key, and the service executes it again. Asking clients
+to retry less has failed three times; prevention must live in the service.
+
+## The pattern
+
+Every money-moving entry point routes through the shared helper in
+`src/idempotency.js`:
+
+- `deriveKey(scope, parts)` builds a stable key from the request payload when
+ the caller did not supply one.
+- `once(store, key, produce)` claims the key synchronously (concurrent retries
+ share one execution) and persists the receipt (retries after a restart get
+ the stored receipt back).
+
+`src/refunds.js` and `src/payouts.js` both use it. Do not add a second inline
+implementation of key derivation or seen-tracking in another module.
+
+## Prevention procedure
+
+For any new operation that moves money (charges, refunds, payouts, credits,
+adjustments):
+
+1. Route the side effect through `once()` from `src/idempotency.js` — never
+ call the gateway directly from the entry point.
+2. Add a regression test that retries the operation without a key (including
+ a concurrent retry storm) and asserts the ledger shows exactly one effect.
+3. Run `npm test` before merging.
+4. If this class of bug recurs anywhere, log it in `docs/incidents.md` and
+ extend this runbook instead of fixing silently.
diff --git a/docker/context-profiles/complex-eval/reference4/recurring-incident/src/idempotency.js b/docker/context-profiles/complex-eval/reference4/recurring-incident/src/idempotency.js
new file mode 100644
index 000000000..7f5eb0fc8
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/recurring-incident/src/idempotency.js
@@ -0,0 +1,31 @@
+// Shared idempotency helper for money-moving entry points. Any operation that
+// must not happen twice derives a stable key (from the caller's idempotencyKey
+// or from the request payload) and routes through once().
+import crypto from 'node:crypto';
+
+const inflight = new Map();
+
+export function deriveKey(scope, parts) {
+ const hash = crypto.createHash('sha256').update(JSON.stringify(parts)).digest('hex').slice(0, 24);
+ return `${scope}:${hash}`;
+}
+
+// Runs produce() at most once per key. The key is claimed synchronously, so
+// concurrent callers share one execution, and the receipt is persisted, so a
+// retry after a restart returns the stored receipt instead of re-running.
+export async function once(store, key, produce) {
+ const existing = store.get(key);
+ if (existing) return { ...existing, duplicate: true };
+ if (inflight.has(key)) return { ...(await inflight.get(key)), duplicate: true };
+ const pending = (async () => {
+ const receipt = await produce();
+ store.set(key, receipt);
+ return receipt;
+ })();
+ inflight.set(key, pending);
+ try {
+ return await pending;
+ } finally {
+ inflight.delete(key);
+ }
+}
diff --git a/docker/context-profiles/complex-eval/reference4/recurring-incident/src/payouts.js b/docker/context-profiles/complex-eval/reference4/recurring-incident/src/payouts.js
new file mode 100644
index 000000000..fffb428ae
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/recurring-incident/src/payouts.js
@@ -0,0 +1,12 @@
+import { payout } from './charge.js';
+import * as store from './store.js';
+import { deriveKey, once } from './idempotency.js';
+
+// Processes a vendor payout through the same shared idempotency helper as
+// refunds, so a retry storm can never double-pay a vendor.
+export async function processPayout(req) {
+ const key = req.idempotencyKey
+ ? `payout:${req.idempotencyKey}`
+ : deriveKey('payout', { vendorId: req.vendorId, amount: req.amount });
+ return once(store, key, () => payout({ vendorId: req.vendorId, amount: req.amount }));
+}
diff --git a/docker/context-profiles/complex-eval/reference4/recurring-incident/src/refunds.js b/docker/context-profiles/complex-eval/reference4/recurring-incident/src/refunds.js
new file mode 100644
index 000000000..a756f09bc
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/recurring-incident/src/refunds.js
@@ -0,0 +1,13 @@
+import { refund } from './charge.js';
+import * as store from './store.js';
+import { deriveKey, once } from './idempotency.js';
+
+// Processes a customer refund. Requests without an idempotencyKey get a key
+// derived from the payload, so a retried call can never refund twice — see
+// docs/runbooks/idempotency.md.
+export async function processRefund(req) {
+ const key = req.idempotencyKey
+ ? `refund:${req.idempotencyKey}`
+ : deriveKey('refund', { orderId: req.orderId, amount: req.amount });
+ return once(store, key, () => refund({ orderId: req.orderId, amount: req.amount }));
+}
diff --git a/docker/context-profiles/complex-eval/reference4/recurring-incident/test/idempotency.test.js b/docker/context-profiles/complex-eval/reference4/recurring-incident/test/idempotency.test.js
new file mode 100644
index 000000000..20135eb69
--- /dev/null
+++ b/docker/context-profiles/complex-eval/reference4/recurring-incident/test/idempotency.test.js
@@ -0,0 +1,53 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import { readLedger } from '../src/charge.js';
+
+function freshEnv(t) {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'payments-idem-'));
+ process.env.LEDGER_FILE = path.join(dir, 'ledger.jsonl');
+ process.env.STORE_FILE = path.join(dir, 'store.json');
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
+ return dir;
+}
+
+test('a refund retried without an idempotency key refunds exactly once', async (t) => {
+ const dir = freshEnv(t);
+ const { processRefund } = await import('../src/refunds.js');
+ await processRefund({ orderId: 'ord-retry', amount: 2500 });
+ await processRefund({ orderId: 'ord-retry', amount: 2500 });
+ const refunds = readLedger().filter(e => e.type === 'refund' && e.orderId === 'ord-retry');
+ assert.equal(refunds.length, 1);
+ assert.equal(fs.readdirSync(dir).includes('ledger.jsonl'), true);
+});
+
+test('refund idempotency survives a restart (fresh module, same store)', async (t) => {
+ freshEnv(t);
+ const first = await import('../src/refunds.js');
+ await first.processRefund({ orderId: 'ord-restart', amount: 3100 });
+ const reloaded = await import(`../src/refunds.js?restart=${Date.now()}`);
+ await reloaded.processRefund({ orderId: 'ord-restart', amount: 3100 });
+ const refunds = readLedger().filter(e => e.type === 'refund' && e.orderId === 'ord-restart');
+ assert.equal(refunds.length, 1);
+});
+
+test('a concurrent keyless payout retry storm pays exactly once', async (t) => {
+ freshEnv(t);
+ const { processPayout } = await import('../src/payouts.js');
+ await Promise.all(Array.from({ length: 20 },
+ () => processPayout({ vendorId: 'ven-storm', amount: 9000 })));
+ const payouts = readLedger().filter(e => e.type === 'payout' && e.vendorId === 'ven-storm');
+ assert.equal(payouts.length, 1);
+});
+
+test('payout idempotency survives a restart (fresh module, same store)', async (t) => {
+ freshEnv(t);
+ const first = await import('../src/payouts.js');
+ await first.processPayout({ vendorId: 'ven-restart', amount: 4000 });
+ const reloaded = await import(`../src/payouts.js?restart=${Date.now()}`);
+ await reloaded.processPayout({ vendorId: 'ven-restart', amount: 4000 });
+ const payouts = readLedger().filter(e => e.type === 'payout' && e.vendorId === 'ven-restart');
+ assert.equal(payouts.length, 1);
+});
diff --git a/docker/context-profiles/complex-eval/verify-checks.js b/docker/context-profiles/complex-eval/verify-checks.js
new file mode 100644
index 000000000..8c69d8d5e
--- /dev/null
+++ b/docker/context-profiles/complex-eval/verify-checks.js
@@ -0,0 +1,64 @@
+'use strict';
+// Development tool: validates the hidden graders end to end. For every task the
+// reference solution (referenceDir/ overlaid on the fixture) must score
+// 1.0; the as-shipped fixture and the optional naive control (naiveDir/)
+// must score strictly below 1.0. Uses the evaluator's own sandboxed grader
+// runner, so this exercises the real grading path.
+// Usage: node verify-checks.js [casesDir=cases] [referenceDir=reference] [naiveDir=naive]
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const { runScoredCheck } = require('../ai-eval-lib');
+
+const root = __dirname;
+const casesDir = path.join(root, process.argv[2] || 'cases');
+const referenceDir = path.join(root, process.argv[3] || 'reference');
+const naiveDir = path.join(root, process.argv[4] || 'naive');
+
+function stage(task, overlayDir) {
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), `ecc-complex-${task}-`));
+ const copy = (from, to) => {
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
+ const target = path.join(to, entry.name);
+ if (entry.isDirectory()) { fs.mkdirSync(target, { recursive: true }); copy(path.join(from, entry.name), target); }
+ else fs.copyFileSync(path.join(from, entry.name), target);
+ }
+ };
+ copy(path.join(casesDir, task, 'files'), cwd);
+ if (overlayDir && fs.existsSync(path.join(overlayDir, task))) copy(path.join(overlayDir, task), cwd);
+ return cwd;
+}
+
+let failed = false;
+for (const task of fs.readdirSync(casesDir).sort()) {
+ const meta = JSON.parse(fs.readFileSync(path.join(casesDir, task, 'meta.json'), 'utf8'));
+ const stepsDir = path.join(casesDir, task, 'steps');
+ if (fs.existsSync(stepsDir)) {
+ // Stepped task: graders run in order against one accumulating workspace.
+ const steps = fs.readdirSync(stepsDir).sort().map((name, index) => ({
+ check: fs.readFileSync(path.join(stepsDir, name, 'check.cjs'), 'utf8'),
+ timeoutMs: meta.steps?.[index]?.checkTimeoutMs || meta.checkTimeoutMs || 30000,
+ }));
+ const runChain = overlayDir => {
+ const cwd = stage(task, overlayDir);
+ return steps.map((step, index) => runScoredCheck(cwd, step.check, step.timeoutMs, index + 1).score);
+ };
+ const bare = runChain(null);
+ const solved = runChain(referenceDir);
+ const ok = solved.every(score => score === 1) && bare.some(score => score < 1);
+ if (!ok) failed = true;
+ console.log(`${ok ? 'ok' : 'FAIL'} - ${task}: fixture=[${bare.map(s => s.toFixed(2))}] reference=[${solved.map(s => s.toFixed(2))}]`);
+ continue;
+ }
+ const check = fs.readFileSync(path.join(casesDir, task, 'check.cjs'), 'utf8');
+ const timeoutMs = meta.checkTimeoutMs || 30000;
+ const bare = runScoredCheck(stage(task, null), check, timeoutMs);
+ const naive = fs.existsSync(path.join(naiveDir, task))
+ ? runScoredCheck(stage(task, naiveDir), check, timeoutMs) : null;
+ const solved = runScoredCheck(stage(task, referenceDir), check, timeoutMs);
+ const ok = solved.passed && solved.score === 1 && bare.score < 1 && (!naive || naive.score < 1);
+ if (!ok) failed = true;
+ console.log(`${ok ? 'ok' : 'FAIL'} - ${task}: fixture=${bare.score.toFixed(3)}`
+ + `${naive ? ` naive=${naive.score.toFixed(3)}` : ''} reference=${solved.score.toFixed(3)}`);
+}
+process.exit(failed ? 1 : 0);
diff --git a/docker/context-profiles/example-task.json b/docker/context-profiles/example-task.json
new file mode 100644
index 000000000..f45512f85
--- /dev/null
+++ b/docker/context-profiles/example-task.json
@@ -0,0 +1,7 @@
+{
+ "sessionId": "local-auto-canary",
+ "taskId": "python-patterns-explanation",
+ "revision": 1,
+ "phase": "explain",
+ "query": "Explain Python patterns for a short, readable list comprehension. Give one example and describe when a plain loop is clearer. Do not modify files or run commands."
+}
diff --git a/docker/context-profiles/legacy-source.json b/docker/context-profiles/legacy-source.json
new file mode 100644
index 000000000..096fe759a
--- /dev/null
+++ b/docker/context-profiles/legacy-source.json
@@ -0,0 +1,5 @@
+{
+ "ref": "origin/main",
+ "sha": "e482e579415fde18357cafce70f177ae19fd7f03",
+ "note": "Pre-ECC-029 ECC source for the ecc-legacy evaluation arm: the typical current user install (full skill library, no scoping layer). Pinned so runs are reproducible; advance deliberately."
+}
diff --git a/docker/context-profiles/native-probe.js b/docker/context-profiles/native-probe.js
new file mode 100644
index 000000000..ea74ea3ac
--- /dev/null
+++ b/docker/context-profiles/native-probe.js
@@ -0,0 +1,168 @@
+#!/usr/bin/env node
+'use strict';
+
+// Opt-in, credential-free native discovery. Never starts a thread or model turn.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const { spawn, spawnSync } = require('node:child_process');
+
+function run(command, args, options) {
+ const result = spawnSync(command, args, { ...options, encoding: 'utf8', timeout: 60000,
+ maxBuffer: 16 * 1024 * 1024 });
+ assert.equal(result.status, 0, `${command}: ${result.error || result.stderr || result.stdout}`);
+ return result.stdout.trim();
+}
+
+async function listSkills() {
+ const server = spawn(process.env.ECC_NATIVE_CODEX || 'codex', ['app-server', '--stdio'], {
+ cwd: process.cwd(), env: process.env, stdio: ['pipe', 'pipe', 'pipe'],
+ });
+ let buffer = '';
+ let stderr = '';
+ const pending = new Map();
+ let nextId = 0;
+ server.stderr.on('data', chunk => { stderr += chunk; });
+ server.stdout.on('data', chunk => {
+ buffer += chunk;
+ let end;
+ while ((end = buffer.indexOf('\n')) >= 0) {
+ const line = buffer.slice(0, end);
+ buffer = buffer.slice(end + 1);
+ if (!line.trim()) continue;
+ const message = JSON.parse(line);
+ const handler = pending.get(message.id);
+ if (handler) {
+ pending.delete(message.id);
+ if (message.error) handler.reject(new Error(JSON.stringify(message.error)));
+ else handler.resolve(message.result);
+ }
+ }
+ });
+ const fail = error => { for (const handler of pending.values()) handler.reject(error); };
+ server.on('error', fail);
+ server.on('exit', code => fail(new Error(`App server exited ${code}: ${stderr}`)));
+ const timer = setTimeout(() => { fail(new Error('Native discovery timed out')); server.kill(); }, 45000);
+ const request = (method, params) => new Promise((resolve, reject) => {
+ const id = ++nextId;
+ pending.set(id, { resolve, reject });
+ server.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
+ });
+ try {
+ const initialized = await request('initialize', {
+ clientInfo: { name: 'ecc-context-native-probe', version: '1.0.0' },
+ capabilities: { experimentalApi: true },
+ });
+ server.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
+ const skills = await request('skills/list', { cwds: [process.cwd()], forceReload: true });
+ process.stdout.write(`${JSON.stringify({ initialized, skills })}\n`);
+ } finally {
+ clearTimeout(timer);
+ server.kill();
+ }
+}
+
+function probe(options) {
+ const repoRoot = path.resolve(process.env.ECC_NATIVE_PACKAGE_ROOT || path.join(__dirname, '../..'));
+ const { planContextCarrier } = require(path.join(repoRoot, 'scripts/lib/context-carriers'));
+ const { compileContextProfile } = require(path.join(repoRoot, 'scripts/lib/context-profiles'));
+ // The independent structural oracle remains source-only test infrastructure.
+ const { withCarrierFixture } = require('../../tests/lib/helpers/context-carrier-fixture');
+ const artifact = planContextCarrier({ repoRoot, ...options });
+ const expectedPlan = compileContextProfile({ repoRoot, ...options });
+ return withCarrierFixture({ repoRoot, artifact, expectedPlan }, ({ root, verify }) => {
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-context-native-'));
+ try {
+ const home = path.join(temp, 'home');
+ const codexHome = path.join(home, '.codex');
+ const cwd = path.join(temp, 'project');
+ const marketplace = path.join(temp, 'marketplace');
+ for (const dir of [codexHome, cwd, path.join(marketplace, '.agents/plugins')]) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+ const env = { PATH: process.env.PATH, HOME: home, CODEX_HOME: codexHome,
+ CLAUDE_CONFIG_DIR: path.join(home, '.claude'), LANG: 'C.UTF-8',
+ DISABLE_TELEMETRY: '1', DISABLE_AUTOUPDATER: '1',
+ ECC_NATIVE_CODEX: process.env.ECC_NATIVE_CODEX || 'codex' };
+ const commandOptions = { cwd, env };
+ if (options.target === 'claude') {
+ const version = run('claude', ['--version'], commandOptions);
+ const validation = run('claude', ['plugin', 'validate', root], commandOptions);
+ const details = run('claude', ['--setting-sources', '', '--plugin-dir', root,
+ 'plugin', 'details', 'ecc-context-carrier'], commandOptions);
+ const names = details.match(/Skills \(\d+\)\s+([^\n]+)/);
+ assert.ok(names, 'Claude did not report the skill inventory');
+ const nativeNames = names[1].split(', ').sort();
+ assert.deepEqual(nativeNames, artifact.entries.map(skill => skill.name).sort());
+ for (const component of ['Agents', 'Hooks', 'MCP servers', 'LSP servers']) {
+ assert.ok(details.includes(`${component} (0)`), `Unexpected native ${component}`);
+ }
+ verify();
+ return { provider: version, profileId: artifact.profileId,
+ selectedIds: artifact.selectedIds, excludedIds: artifact.excludedIds,
+ nativeNames, discovery: 'verified-component-inventory',
+ validation, projectedTokens: details.match(/Always-on:\s+([^\n]+)/)?.[1],
+ carrierDigest: artifact.carrierDigest,
+ invocation: 'unobserved', modelCalls: 0, credentialsCopied: false };
+ }
+ const codex = env.ECC_NATIVE_CODEX;
+ const version = run(codex, ['--version'], commandOptions);
+ fs.cpSync(root, path.join(marketplace, 'carrier'), { recursive: true });
+ fs.writeFileSync(path.join(marketplace, '.agents/plugins/marketplace.json'), JSON.stringify({
+ name: 'ecc-context-probe', plugins: [{ name: 'ecc-context-carrier',
+ source: { source: 'local', path: './carrier' },
+ policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' } }],
+ }));
+ const added = JSON.parse(run(codex, ['plugin', 'marketplace', 'add', marketplace, '--json'], commandOptions));
+ const installed = JSON.parse(run(codex, ['plugin', 'add', 'ecc-context-carrier@ecc-context-probe', '--json'], commandOptions));
+ // Discovery must survive removal of the marketplace's source skill tree.
+ fs.rmSync(path.join(marketplace, 'carrier'), { recursive: true });
+ const observed = JSON.parse(run(process.execPath, [__filename, '--list-skills'], commandOptions));
+ assert.equal(observed.skills.data.length, 1);
+ const entry = observed.skills.data[0];
+ assert.deepEqual(entry.errors, [], 'Native parser rejected a selected skill');
+ const nativeSkills = entry.skills.filter(skill => skill.pluginId === 'ecc-context-carrier@ecc-context-probe');
+ const expectedNames = artifact.entries.map(skill => `ecc-context-carrier:${skill.name}`).sort();
+ const actualNames = nativeSkills.map(skill => skill.name).sort();
+ assert.deepEqual(actualNames, expectedNames, `Native skill selection mismatch: ${JSON.stringify(entry)}`);
+ let resourceCount = 0;
+ for (const skill of nativeSkills) {
+ assert.equal(skill.enabled, true);
+ assert.ok(skill.path.startsWith(`${fs.realpathSync(codexHome)}${path.sep}`), 'Skill escaped isolated Codex home');
+ const expected = artifact.entries.find(item => `ecc-context-carrier:${item.name}` === skill.name);
+ for (const file of artifact.files.filter(item => item.skillId === expected.id)) {
+ const relative = file.destinationPath.slice(`skills/${expected.name}/`.length);
+ const bytes = fs.readFileSync(path.join(path.dirname(skill.path), relative));
+ const digest = require('node:crypto').createHash('sha256').update(bytes).digest('hex');
+ assert.equal(digest, file.digest, 'Installed resource bytes changed');
+ resourceCount++;
+ }
+ }
+ verify();
+ assert.equal(fs.existsSync(path.join(codexHome, 'auth.json')), false);
+ return { provider: version, profileId: artifact.profileId, selectedIds: artifact.selectedIds,
+ excludedIds: artifact.excludedIds, discovery: 'verified', resources: resourceCount,
+ relocation: 'verified-after-source-removal', carrierDigest: artifact.carrierDigest,
+ nativeNames: actualNames, systemSkills: entry.skills.filter(skill => !skill.pluginId).map(skill => skill.name),
+ marketplaceAdded: !!added, installed: !!installed, invocation: 'unobserved',
+ modelCalls: 0, credentialsCopied: false };
+ } finally {
+ fs.rmSync(temp, { recursive: true, force: true });
+ }
+ });
+}
+
+if (process.argv.includes('--list-skills')) {
+ listSkills().catch(error => { console.error(error); process.exitCode = 1; });
+} else {
+ const cases = process.argv.includes('--claude') ? [
+ { profileId: 'lean@1', target: 'claude' },
+ { profileId: 'full@1', target: 'claude', exclude: ['skill:python-patterns'] },
+ ] : [
+ { profileId: 'lean@1', target: 'codex' },
+ { profileId: 'lean@1', target: 'codex', include: ['skill:angular-developer'] },
+ { profileId: 'full@1', target: 'codex', exclude: ['skill:python-patterns'] },
+ ];
+ for (const options of cases) process.stdout.write(`${JSON.stringify(probe(options))}\n`);
+}
diff --git a/docker/context-profiles/native-switch-probe.js b/docker/context-profiles/native-switch-probe.js
new file mode 100644
index 000000000..47b21810f
--- /dev/null
+++ b/docker/context-profiles/native-switch-probe.js
@@ -0,0 +1,43 @@
+#!/usr/bin/env node
+'use strict';
+
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const { applyStore, rollbackStore } = require('../../scripts/lib/context-profile-store');
+const { prepareNativeProfile, rollbackNativeProfile, getNativeProfileStatus, recoverNativeProfile } = require('../../scripts/lib/context-profile-native');
+
+const repoRoot = path.resolve(__dirname, '../..');
+const temp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-native-switch-')));
+const options = { stateRoot: path.join(temp, 'managed'), nativeRoot: path.join(temp, 'native'),
+ codexPath: process.env.ECC_NATIVE_CODEX || 'codex' };
+try {
+ const cases = []; let full;
+ for (const [index, profileId] of ['full@1', 'lean@1', 'full@1'].entries()) {
+ const managed = index === 2 ? rollbackStore({ stateRoot: options.stateRoot })
+ : applyStore({ repoRoot, stateRoot: options.stateRoot, target: 'codex', selectionMode: 'auto',
+ profileId, exclude: profileId === 'full@1' ? ['skill:python-patterns'] : [] });
+ const native = index === 2 ? rollbackNativeProfile(options) : prepareNativeProfile(options);
+ assert.equal(native.ready, true);
+ assert.equal(native.carrierDigest, managed.carrierDigest);
+ assert.equal(native.storeRevision, managed.revision);
+ assert.equal(native.active, false);
+ assert.equal(getNativeProfileStatus(options).ready, true);
+ if (index === 0) {
+ full = native;
+ fs.writeFileSync(path.join(full.home, 'unrelated.txt'), 'Unrelated user bytes');
+ }
+ if (index === 1) assert.notEqual(native.home, full.home);
+ if (index === 2) assert.equal(native.home, full.home);
+ assert.equal(fs.readFileSync(path.join(full.home, 'unrelated.txt'), 'utf8'), 'Unrelated user bytes');
+ cases.push({ profileId, storeRevision: native.storeRevision, nativeRevision: native.revision,
+ skills: native.selectedIds.length, carrierDigest: native.carrierDigest });
+ }
+ assert.equal(recoverNativeProfile(options).ready, true);
+ process.stdout.write(`${JSON.stringify({ kind: 'native-managed-switch', provider: 'codex-cli 0.154.0',
+ productAdapter: 'isolated-native-generations', cases, unrelatedBytesPreserved: true,
+ discovery: 'verified', modelCalls: 0, credentialsCopied: false, invocation: 'unobserved' })}\n`);
+} finally {
+ fs.rmSync(temp, { recursive: true, force: true });
+}
diff --git a/docker/context-profiles/packed-smoke.js b/docker/context-profiles/packed-smoke.js
new file mode 100644
index 000000000..2cc959374
--- /dev/null
+++ b/docker/context-profiles/packed-smoke.js
@@ -0,0 +1,143 @@
+#!/usr/bin/env node
+'use strict';
+
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const { spawnSync } = require('node:child_process');
+const { planContextCarrier } = require('../../scripts/lib/context-carriers');
+const { compileContextProfile } = require('../../scripts/lib/context-profiles');
+const { withCarrierFixture } = require('../../tests/lib/helpers/context-carrier-fixture');
+
+const repoRoot = path.resolve(__dirname, '../..');
+const temp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-packed-context-')));
+const expectedSource = process.env.ECC_EXPECTED_CARRIERS
+ ? JSON.parse(fs.readFileSync(process.env.ECC_EXPECTED_CARRIERS, 'utf8')) : null;
+
+function profileCommand(args, temp, env, expectedStatus = 0) {
+ const result = spawnSync(process.execPath, [path.join(repoRoot, 'scripts/ecc.js'), 'profile', ...args, '--json'], {
+ cwd: temp, env, encoding: 'utf8', timeout: 60000, maxBuffer: 16 * 1024 * 1024,
+ });
+ assert.equal(result.status, expectedStatus, result.stderr || result.stdout);
+ return JSON.parse(result.stdout);
+}
+
+function managedJourney(temp, env) {
+ const stateRoot = path.join(temp, 'managed');
+ const command = (args, status) => profileCommand(args, temp, env, status);
+ const store = (args, status) => command([...args, '--state-root', stateRoot], status);
+ assert.equal(store(['status']).store.status, 'unconfigured');
+ const preview = store(['set', 'full', '--dry-run']);
+ assert.equal(preview.store.proposedProfileId, 'full@1');
+ assert.equal(fs.existsSync(stateRoot), false);
+ const full = store(['set', 'full', '--exclude', 'skill:python-patterns', '--expected-revision', '0']).store;
+ assert.equal(full.profileId, 'full@1');
+ assert.equal(full.active, false);
+ assert.equal(full.revision, 1);
+ assert.equal(full.selectedIds.includes('skill:python-patterns'), false);
+ const lean = store(['set', 'lean', '--selection', 'auto', '--expected-revision', '1']).store;
+ assert.equal(lean.revision, 2);
+ assert.equal(lean.profileId, 'lean@1');
+ assert.equal(lean.selectedIds.length, 3);
+ assert.ok(fs.existsSync(path.join(lean.generationRoot, '.codex-plugin/plugin.json')));
+ const restored = store(['rollback', '--expected-revision', '2']).store;
+ assert.equal(restored.revision, 3);
+ assert.equal(restored.carrierDigest, full.carrierDigest);
+ const repeated = store(['set', 'full', '--exclude', 'skill:python-patterns']).store;
+ assert.equal(repeated.revision, 3, 'Repeated configuration should be idempotent');
+ store(['set', 'lean', '--expected-revision', '1'], 1);
+ assert.equal(store(['status']).store.revision, 3);
+ assert.equal(store(['recover']).store.revision, 3);
+
+ const taskPath = path.join(temp, 'task.json');
+ const task = { sessionId: 'packed-probe', taskId: 'python-step', revision: 1, phase: 'implement',
+ query: 'python-patterns', proposedIds: ['skill:python-patterns'] };
+ fs.writeFileSync(taskPath, JSON.stringify(task));
+ const resolve = args => command(['resolve', 'lean', '--task-input', taskPath, ...args]).selection;
+ const selected = resolve(['--selection', 'auto']);
+ assert.deepEqual(selected.selectedIds, ['skill:python-patterns']);
+ assert.deepEqual(selected.loadedIds, []);
+ const loaded = resolve(['--selection', 'auto', '--load', '--expected-digest', selected.receipt.selectionDigest]);
+ assert.deepEqual(loaded.loadedIds, ['skill:python-patterns']);
+ assert.ok(loaded.resources.every(resource => resource.content.length > 0));
+ assert.deepEqual(resolve(['--selection', 'suggest', '--load']).loadedIds, []);
+ assert.deepEqual(resolve(['--selection', 'manual', '--load']).loadedIds, []);
+ assert.deepEqual(resolve(['--selection', 'auto', '--load', '--dry-run']).loadedIds, []);
+ const launch = profileCommand(['run', 'lean', '--task-input', taskPath, '--dry-run'], temp,
+ { ...env, PATH: temp }).launch;
+ assert.equal(launch.status, 'proposed');
+ assert.equal(launch.exitCode, null);
+ assert.deepEqual(launch.selection.loadedIds, []);
+ fs.writeFileSync(taskPath, JSON.stringify({ ...task, explicitIds: ['skill:python-patterns'] }));
+ const excluded = command(['resolve', '--state-root', stateRoot, '--task-input', taskPath, '--load'], 1);
+ assert.match(excluded.summary, /excluded/);
+ fs.writeFileSync(taskPath, JSON.stringify(task));
+ const receiptPath = path.join(temp, 'receipt.json');
+ fs.writeFileSync(receiptPath, JSON.stringify(loaded.receipt));
+ fs.writeFileSync(taskPath, JSON.stringify({ ...task, proposedIds: [], query: 'unrelated wording' }));
+ assert.equal(resolve(['--previous', receiptPath, '--load']).reused, true);
+ fs.writeFileSync(taskPath, JSON.stringify({ ...task, revision: 2, noWorkflow: true }));
+ const reset = resolve(['--previous', receiptPath, '--load']);
+ assert.equal(reset.reason, 'no-workflow-needed');
+ assert.deepEqual(reset.loadedIds, []);
+ const nativeRoot = path.join(temp, 'native-cli');
+ const nativeArgs = ['--state-root', stateRoot, '--native-root', nativeRoot];
+ const proposedNative = command(['prepare-native', ...nativeArgs, '--dry-run']).native;
+ assert.equal(proposedNative.ready, false);
+ assert.equal(fs.existsSync(nativeRoot), false);
+ const preparedNative = command(['prepare-native', ...nativeArgs]).native;
+ assert.equal(preparedNative.ready, true);
+ const nativeStatus = command(['native-status', ...nativeArgs]).native;
+ assert.equal(nativeStatus.ready, true);
+ assert.equal(nativeStatus.storeRevision, 3);
+ const nativeLaunch = profileCommand(['run', '--task-input', taskPath, ...nativeArgs, '--dry-run'], temp,
+ { ...env, PATH: temp }).launch;
+ assert.equal(nativeLaunch.status, 'proposed');
+ assert.equal(nativeLaunch.command, preparedNative.executable);
+ assert.equal(nativeLaunch.providerConfiguration, 'isolated-native-generation');
+ assert.equal(command(['native-recover', ...nativeArgs]).native.ready, true);
+ assert.equal(fs.existsSync(env.HOME), false, 'Managed commands changed the caller home');
+ return { kind: 'packed-managed-and-auto', transitions: ['full', 'lean', 'rollback-full'],
+ finalRevision: 3, idempotency: 'verified', staleRevision: 'rejected',
+ autoLoaded: loaded.loadedIds, suggestLoaded: [], manualLoaded: [],
+ dryRunLoaded: [], launcherDryRun: 'verified-with-no-provider-on-PATH', savedExclusions: 'enforced',
+ pinnedReuse: 'verified', noWorkflowReset: 'verified', nativeCliPreparation: 'verified',
+ nativePinnedLaunchDryRun: 'verified', existingSessionActivation: 'unchanged' };
+}
+
+try {
+ const env = { PATH: process.env.PATH, HOME: path.join(temp, 'home'), LANG: 'C.UTF-8' };
+ const results = [];
+ for (const target of ['claude', 'codex', 'pi', 'opencode', 'cursor']) {
+ for (const profileId of ['lean@1', 'full@1']) {
+ const options = { repoRoot, profileId, target, selectionMode: 'auto' };
+ const expectedPlan = compileContextProfile(options);
+ const artifact = planContextCarrier(options);
+ if (expectedSource) {
+ assert.deepEqual(artifact, expectedSource.find(item => item.target === target && item.profileId === profileId),
+ 'Packed carrier differs from source artifact');
+ }
+ const cli = spawnSync(process.execPath, [path.join(repoRoot, 'scripts/ecc.js'),
+ 'profile', 'carrier', profileId, '--target', target, '--json'],
+ { cwd: temp, env, encoding: 'utf8', timeout: 60000, maxBuffer: 16 * 1024 * 1024 });
+ assert.equal(cli.status, 0, cli.stderr);
+ assert.deepEqual(JSON.parse(cli.stdout).carrier, artifact);
+ const evidence = withCarrierFixture({ repoRoot, artifact, expectedPlan }, ({ verify }) => verify());
+ results.push({ target, profileId, selected: artifact.selectedIds.length, files: evidence.fileCount });
+ }
+ }
+ assert.deepEqual(fs.readdirSync(temp), [], 'Preview changed the disposable caller home');
+ process.stdout.write(`${JSON.stringify({ kind: 'packed-cli-and-structural', node: process.version,
+ platform: `${process.platform}/${process.arch}`, cases: results })}\n`);
+ process.stdout.write(`${JSON.stringify(managedJourney(temp, env))}\n`);
+ for (const script of ['native-probe.js', 'native-switch-probe.js']) {
+ const native = spawnSync(process.execPath, [path.join(__dirname, script)], {
+ cwd: temp, env, encoding: 'utf8', timeout: 180000, maxBuffer: 16 * 1024 * 1024,
+ });
+ assert.equal(native.status, 0, native.stderr || native.stdout);
+ process.stdout.write(native.stdout);
+ }
+} finally {
+ fs.rmSync(temp, { recursive: true, force: true });
+}
diff --git a/docker/context-profiles/run-podman.js b/docker/context-profiles/run-podman.js
new file mode 100644
index 000000000..c58cca450
--- /dev/null
+++ b/docker/context-profiles/run-podman.js
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+'use strict';
+
+const assert = require('node:assert/strict');
+const crypto = require('node:crypto');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const { spawnSync } = require('node:child_process');
+
+const repoRoot = path.resolve(__dirname, '../..');
+const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-context-podman-'));
+const image = `localhost/ecc-context-profiles:${process.pid}-${Date.now()}`;
+function run(command, args, capture = false) {
+ const result = spawnSync(command, args, { cwd: repoRoot, encoding: 'utf8',
+ timeout: 600000, maxBuffer: 32 * 1024 * 1024, stdio: capture ? 'pipe' : 'inherit' });
+ assert.equal(result.status, 0, `${command}: ${result.error || result.stderr || result.stdout}`);
+ return result.stdout;
+}
+try {
+ const packed = JSON.parse(run('npm', ['pack', '--json', '--pack-destination', temp], true));
+ const { planContextCarrier } = require('../../scripts/lib/context-carriers');
+ const expected = [];
+ for (const target of ['claude', 'codex', 'pi', 'opencode', 'cursor']) {
+ for (const profileId of ['lean@1', 'full@1']) {
+ expected.push(planContextCarrier({ repoRoot, target, profileId, selectionMode: 'auto' }));
+ }
+ }
+ const archivePaths = new Set(packed[0].files.map(file => file.path));
+ const missing = expected[1].files.filter(file => file.kind === 'copy' && !archivePaths.has(file.sourcePath));
+ assert.deepEqual(missing, [], 'Packed archive omitted canonical skill resources');
+ fs.writeFileSync(path.join(temp, 'expected-carriers.json'), JSON.stringify(expected));
+ fs.renameSync(path.join(temp, packed[0].filename), path.join(temp, 'package.tgz'));
+ for (const file of ['Dockerfile', 'native-probe.js', 'native-switch-probe.js', 'packed-smoke.js']) {
+ fs.copyFileSync(path.join(__dirname, file), path.join(temp, file));
+ }
+ fs.copyFileSync(path.join(repoRoot, 'tests/lib/helpers/context-carrier-fixture.js'),
+ path.join(temp, 'context-carrier-fixture.js'));
+ const packageDigest = crypto.createHash('sha256').update(fs.readFileSync(path.join(temp, 'package.tgz'))).digest('hex');
+ process.stdout.write(`${JSON.stringify({ packageDigest, image })}\n`);
+ const args = ['build', '--tag', image];
+ if (process.env.ECC_CONTEXT_NODE_IMAGE) args.push('--build-arg', `NODE_IMAGE=${process.env.ECC_CONTEXT_NODE_IMAGE}`);
+ args.push(temp);
+ run('podman', args);
+ run('podman', ['run', '--rm', '--network=none', '--cap-drop=all', '--security-opt=no-new-privileges', image]);
+} finally {
+ // Only the image and temporary directory created by this invocation are removed.
+ spawnSync('podman', ['image', 'rm', image], { stdio: 'ignore', timeout: 60000 });
+ fs.rmSync(temp, { recursive: true, force: true });
+}
diff --git a/docker/context-profiles/run-sandbox.js b/docker/context-profiles/run-sandbox.js
new file mode 100644
index 000000000..c429f6c81
--- /dev/null
+++ b/docker/context-profiles/run-sandbox.js
@@ -0,0 +1,284 @@
+#!/usr/bin/env node
+'use strict';
+
+// The installed tier router owns provisioning and cleanup. This acceptance
+// driver transfers only an npm archive and a fixed verifier into the VM.
+const assert = require('node:assert/strict');
+const crypto = require('node:crypto');
+const fs = require('node:fs');
+const http = require('node:http');
+const net = require('node:net');
+const os = require('node:os');
+const path = require('node:path');
+const { spawn } = require('node:child_process');
+
+const NODE_VERSION = '22.18.0';
+const NODE_SHA = '2c12913cba67af77ded8a399df3fd91c2e7f8628c7079da40bb9ff33bf00dfc0';
+const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
+const quote = text => `'${String(text).replace(/'/g, `'"'"'`)}'`;
+
+function command(executable, args, cwd, timeout = 900000) {
+ return new Promise((resolve, reject) => {
+ const child = spawn(executable, args, { cwd, env: process.env, stdio: ['ignore', 'pipe', 'pipe'], shell: false });
+ let stdout = ''; let stderr = ''; let size = 0; let termination = null; let settled = false;
+ const stop = reason => {
+ if (!termination) termination = reason;
+ child.kill('SIGKILL');
+ };
+ const timer = setTimeout(() => stop('timeout'), timeout);
+ const collect = key => chunk => {
+ size += chunk.length;
+ if (size > 24 * 1024 * 1024) { stop('output-limit'); return; }
+ if (key === 'stdout') stdout += chunk; else stderr += chunk;
+ };
+ child.stdout.on('data', collect('stdout')); child.stderr.on('data', collect('stderr'));
+ child.once('error', error => {
+ if (settled) return;
+ settled = true; clearTimeout(timer); reject(error);
+ });
+ child.once('close', (code, signal) => {
+ if (settled) return;
+ settled = true; clearTimeout(timer); resolve({ code, signal, stdout, stderr, termination });
+ });
+ });
+}
+
+function fingerprintSandboxCli(executable) {
+ const resolved = fs.realpathSync(executable);
+ fs.accessSync(resolved, fs.constants.X_OK);
+ const before = fs.statSync(resolved);
+ assert.ok(before.isFile() && before.size > 0 && before.size <= 64 * 1024 * 1024,
+ 'Sandbox CLI must be a bounded executable file');
+ const bytes = fs.readFileSync(resolved);
+ const after = fs.statSync(resolved);
+ assert.equal(after.dev, before.dev, 'Sandbox CLI changed during fingerprinting');
+ assert.equal(after.ino, before.ino, 'Sandbox CLI changed during fingerprinting');
+ assert.equal(after.size, before.size, 'Sandbox CLI changed during fingerprinting');
+ assert.equal(after.mtimeMs, before.mtimeMs, 'Sandbox CLI changed during fingerprinting');
+ const executableDigest = digest(bytes);
+ const sourceRoot = path.basename(path.dirname(resolved)) === 'sandbox' ? path.dirname(resolved) : null;
+ if (!sourceRoot) return { path: resolved, bytes: bytes.length, digest: executableDigest,
+ implementation: { root: null, files: 1, bytes: bytes.length, digest: executableDigest } };
+ const files = [];
+ function visit(directory) {
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
+ const file = path.join(directory, entry.name);
+ assert.equal(entry.isSymbolicLink(), false, 'Sandbox CLI implementation must not contain symbolic links');
+ if (entry.isDirectory()) visit(file);
+ else {
+ assert.equal(entry.isFile(), true, 'Sandbox CLI implementation must contain regular files only');
+ files.push(file);
+ assert.ok(files.length <= 512, 'Sandbox CLI implementation exceeds the file bound');
+ }
+ }
+ }
+ visit(sourceRoot);
+ const hash = crypto.createHash('sha256'); let total = 0;
+ for (const file of files) {
+ const content = fs.readFileSync(file);
+ total += content.length;
+ assert.ok(total <= 32 * 1024 * 1024, 'Sandbox CLI implementation exceeds the byte bound');
+ hash.update(path.relative(sourceRoot, file).split(path.sep).join('/')).update('\0').update(content);
+ }
+ return { path: resolved, bytes: bytes.length, digest: executableDigest,
+ implementation: { root: sourceRoot, files: files.length, bytes: total, digest: hash.digest('hex') } };
+}
+
+function resolveSandboxCli(commandName = 'ecc-sandbox') {
+ const candidates = path.isAbsolute(commandName) ? [commandName]
+ : (process.env.PATH || '').split(path.delimiter).filter(directory => path.isAbsolute(directory))
+ .map(directory => path.join(directory, commandName));
+ const executable = candidates.find(candidate => {
+ try { fs.accessSync(candidate, fs.constants.X_OK); return true; } catch { return false; }
+ });
+ assert.ok(executable, 'Sandbox CLI executable was not found');
+ return fingerprintSandboxCli(executable);
+}
+
+function verifySandboxCli(binding) {
+ const current = fingerprintSandboxCli(binding.path);
+ assert.deepEqual(current, binding, 'Sandbox CLI changed after acceptance was staged');
+ return current;
+}
+
+function validateReport(stdout, { tier, manifest }) {
+ try {
+ const report = JSON.parse(stdout);
+ assert.ok(report && typeof report === 'object' && !Array.isArray(report));
+ assert.equal(report.result, 'pass');
+ assert.equal(report.backend, tier === 1 ? 'podman' : 'lume');
+ assert.equal(report.tier, tier);
+ assert.equal(report.execution_mode, 'real');
+ const installDiff = report.install_diff;
+ assert.ok(installDiff && typeof installDiff === 'object' && !Array.isArray(installDiff));
+ for (const key of ['files_added', 'files_changed', 'files_deleted', 'path_changes',
+ 'services_registered', 'dotfiles_touched']) assert.ok(Array.isArray(installDiff[key]));
+ if (tier === 1) assert.equal(installDiff.complete, true);
+ else {
+ assert.equal(installDiff.method, 'scan');
+ assert.equal(installDiff.complete, false);
+ assert.ok(report.notes?.includes('VM install diff is a bounded best-effort path scan, not a complete disk diff'));
+ }
+ assert.equal(report.assertions?.length, manifest.steps.assert.length);
+ for (let index = 0; index < manifest.steps.assert.length; index++) {
+ assert.deepEqual(report.assertions[index], { cmd: manifest.steps.assert[index], pass: true });
+ }
+ const assertion = manifest.steps.assert.at(-1);
+ const step = report.steps?.findLast(item => item?.cmd === assertion);
+ assert.equal(step?.exit, 0);
+ assert.equal(typeof step.stdout_tail, 'string');
+ const smoke = JSON.parse(step.stdout_tail.trim());
+ assert.equal(smoke?.schemaVersion, 'ecc.context-sandbox-smoke.v1');
+ assert.equal(smoke.passed, true);
+ assert.equal(smoke.os, tier === 1 ? 'linux' : 'darwin');
+ assert.equal(smoke.arch, 'arm64');
+ assert.equal(smoke.authenticated, false);
+ assert.equal(smoke.taskOutcomes, 'unobserved');
+ assert.equal(smoke.matrix?.length, 10);
+ const layouts = smoke.matrix.map(item => `${item.target}/${item.profile}`).sort();
+ assert.deepEqual(layouts, ['claude/full', 'claude/lean', 'codex/full', 'codex/lean',
+ 'cursor/full', 'cursor/lean', 'opencode/full', 'opencode/lean', 'pi/full', 'pi/lean']);
+ return { report, smoke };
+ } catch {
+ throw new Error('Sandbox acceptance report or final smoke payload is invalid');
+ }
+}
+
+function manifestFor({ tier, archiveDigest, verifierDigest, url, runName }) {
+ assert.ok([1, 2].includes(tier));
+ for (const value of [archiveDigest, verifierDigest]) assert.match(value, /^[a-f0-9]{64}$/);
+ assert.match(runName, /^[a-z0-9-]+$/);
+ const guestRoot = tier === 1 ? `/home/ecc/${runName}` : `/tmp/${runName}`;
+ const setup = [`mkdir -m 700 ${quote(guestRoot)}`];
+ let runtime = '';
+ if (tier === 2) {
+ const parsed = new URL(url);
+ assert.equal(parsed.protocol, 'http:');
+ assert.equal(parsed.username, ''); assert.equal(parsed.password, '');
+ assert.equal(net.isIP(parsed.hostname), 4, 'Artifact URL requires an IPv4 address');
+ setup.push(`curl -fsS --max-time 120 https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-arm64.tar.gz -o ${quote(`${guestRoot}/node.tgz`)} && test "$(shasum -a 256 ${quote(`${guestRoot}/node.tgz`)} | cut -d ' ' -f 1)" = ${NODE_SHA} && tar -xzf ${quote(`${guestRoot}/node.tgz`)} -C ${quote(guestRoot)}`);
+ runtime = `export PATH=${quote(`${guestRoot}/node-v${NODE_VERSION}-darwin-arm64/bin`)}:$PATH; `;
+ for (const file of ['package.tgz', 'sandbox-smoke.js']) {
+ setup.push(`curl -fsS --max-time 120 ${quote(`${url}/${file}`)} -o ${quote(`${guestRoot}/${file}`)}`);
+ }
+ } else {
+ setup.push(`cp /workspace/source/package.tgz /workspace/source/sandbox-smoke.js ${quote(guestRoot)}/`);
+ }
+ const check = `const fs=require('fs'),c=require('crypto'); for(const [f,h] of ${JSON.stringify([['package.tgz', archiveDigest], ['sandbox-smoke.js', verifierDigest]])}) {if(c.createHash('sha256').update(fs.readFileSync(f)).digest('hex')!==h)throw Error('Input digest mismatch')}`;
+ setup.push(`${runtime}cd ${quote(guestRoot)} && node -e ${quote(check)} && npm install --ignore-scripts --omit=dev --no-audit --no-fund --fetch-timeout=30000 --fetch-retries=1 --prefix consumer ./package.tgz && npm install --ignore-scripts --no-audit --no-fund --fetch-timeout=30000 --fetch-retries=1 --prefix tools @openai/codex@0.154.0 ${quote(`@openai/codex-${tier === 2 ? 'darwin' : 'linux'}-arm64@npm:@openai/codex@0.154.0-${tier === 2 ? 'darwin' : 'linux'}-arm64`)}`);
+ const assertion = `${runtime}export PATH=${quote(`${guestRoot}/tools/node_modules/.bin`)}:$PATH; node ${quote(`${guestRoot}/sandbox-smoke.js`)} ${quote(`${guestRoot}/consumer/node_modules/ecc-universal`)} ${quote(guestRoot)}`;
+ const manifest = { name: runName, needs: { os: [tier === 1 ? 'linux' : 'macos'], arch: ['arm64'],
+ capabilities: ['clean-home', 'pkg-install', 'network:*'], trust: 'first-party', native: tier === 2 },
+ resources: { cpu: 2, memory: tier === 1 ? '1GB' : '2GB', timeout: 900 },
+ steps: { setup, assert: [assertion] }, report: 'install-diff' };
+ for (const step of [...setup, assertion]) assert.ok(step.length <= 8192);
+ return manifest;
+}
+
+async function serveInputs(files, host) {
+ assert.equal(net.isIP(host), 4, 'Artifact host must be an explicit IPv4 address');
+ const token = crypto.randomBytes(24).toString('hex');
+ const requests = [];
+ const server = http.createServer((request, response) => {
+ const file = request.url?.startsWith(`/${token}/`) ? request.url.slice(token.length + 2) : '';
+ if (request.method !== 'GET' || !Object.hasOwn(files, file) || requests.length >= 12) {
+ response.writeHead(404).end(); return;
+ }
+ const bytes = files[file]; requests.push({ file, bytes: bytes.length, digest: digest(bytes) });
+ response.writeHead(200, { 'Content-Length': bytes.length, 'Content-Type': 'application/octet-stream', 'Cache-Control': 'no-store' });
+ response.end(bytes);
+ });
+ server.requestTimeout = 150000; server.headersTimeout = 10000;
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, host, resolve); });
+ return { url: `http://${host}:${server.address().port}/${token}`, requests,
+ close: () => new Promise(resolve => { server.close(resolve); server.closeAllConnections(); }) };
+}
+
+async function run(options) {
+ assert.ok([1, 2].includes(options.tier), 'Choose --tier 1 or --tier 2');
+ assert.equal(process.arch, 'arm64', 'This acceptance currently certifies arm64 only');
+ const repoRoot = path.resolve(__dirname, '../..');
+ if (options.sandboxCli) assert.ok(path.isAbsolute(options.sandboxCli), '--sandbox-cli must be an absolute trusted executable');
+ const sandboxBinding = resolveSandboxCli(options.sandboxCli || 'ecc-sandbox');
+ const sandboxCli = sandboxBinding.path;
+ const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-profile-sandbox-'));
+ const resultRoot = path.resolve(options.output);
+ fs.mkdirSync(resultRoot, { recursive: true, mode: 0o700 });
+ const runName = `ecc-profile-tier${options.tier}-${crypto.randomUUID()}`;
+ let server;
+ const receipt = { schemaVersion: 'ecc.context-sandbox-acceptance.v1', runName, tier: options.tier,
+ sourceRevision: (await command('git', ['rev-parse', 'HEAD'], repoRoot, 10000)).stdout.trim(),
+ sourceDirty: (await command('git', ['status', '--porcelain'], repoRoot, 10000)).stdout.length > 0,
+ sandboxCli, sandboxCliDigest: sandboxBinding.digest,
+ sandboxImplementationDigest: sandboxBinding.implementation.digest, reportValidated: false,
+ credentialsTransferred: false, artifactServerClosed: false, stageRemoved: false };
+ try {
+ const packed = await command('npm', ['pack', '--json', '--pack-destination', stage], repoRoot);
+ assert.equal(packed.code, 0, packed.stderr);
+ const pack = JSON.parse(packed.stdout)[0];
+ const archive = fs.readFileSync(path.join(stage, pack.filename));
+ assert.ok(archive.length < 64 * 1024 * 1024, 'Package exceeds transfer bound');
+ const verifier = fs.readFileSync(path.join(__dirname, 'sandbox-smoke.js'));
+ assert.ok(verifier.length < 65536);
+ const files = { 'package.tgz': archive, 'sandbox-smoke.js': verifier };
+ fs.writeFileSync(path.join(stage, 'package.tgz'), archive, { mode: 0o600 });
+ fs.writeFileSync(path.join(stage, 'sandbox-smoke.js'), verifier, { mode: 0o600 });
+ receipt.packageDigest = digest(archive); receipt.verifierDigest = digest(verifier);
+ if (options.tier === 2) {
+ const host = options.artifactHost || Object.values(os.networkInterfaces()).flat()
+ .find(address => address.address === '192.168.64.1')?.address;
+ assert.ok(host, 'Specify --artifact-host with a host IP reachable from the guest');
+ server = await serveInputs(files, host);
+ }
+ const manifest = manifestFor({ tier: options.tier, archiveDigest: receipt.packageDigest,
+ verifierDigest: receipt.verifierDigest, url: server?.url, runName });
+ receipt.manifestDigest = digest(Buffer.from(JSON.stringify(manifest)));
+ const manifestPath = path.join(stage, 'sandbox.json');
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest), { mode: 0o600 });
+ fs.copyFileSync(manifestPath, path.join(resultRoot, `${runName}.manifest.json`));
+ verifySandboxCli(sandboxBinding);
+ const preview = await command(sandboxCli, ['run', manifestPath, '--local-only', '--dry-run'], stage, 30000);
+ fs.writeFileSync(path.join(resultRoot, `${runName}.preview.json`), preview.stdout, { mode: 0o600 });
+ assert.equal(preview.code, 0, preview.stdout || preview.stderr);
+ const routes = JSON.parse(preview.stdout).routes;
+ assert.equal(routes?.length, 1, 'Expected exactly one admitted sandbox route');
+ assert.equal(routes[0].result, 'routable');
+ assert.equal(routes[0].tier, options.tier, 'Router chose a different tier');
+ assert.equal(routes[0].backend, options.tier === 1 ? 'podman' : 'lume', 'Router chose a different backend');
+ process.stderr.write(`Starting ${runName}; package ${receipt.packageDigest}\n`);
+ verifySandboxCli(sandboxBinding);
+ const result = await command(sandboxCli, ['run', manifestPath, '--local-only'], stage, 960000);
+ receipt.exitCode = result.code; receipt.signal = result.signal;
+ fs.writeFileSync(path.join(resultRoot, `${runName}.report.json`), result.stdout, { mode: 0o600 });
+ fs.writeFileSync(path.join(resultRoot, `${runName}.stderr.log`), result.stderr, { mode: 0o600 });
+ receipt.reportPath = path.join(resultRoot, `${runName}.report.json`);
+ assert.equal(result.code, 0, result.stdout || result.stderr);
+ verifySandboxCli(sandboxBinding);
+ const validated = validateReport(result.stdout, { tier: options.tier, manifest });
+ receipt.reportValidated = true;
+ receipt.smokeDigest = digest(Buffer.from(JSON.stringify(validated.smoke)));
+ if (server) receipt.transfers = server.requests;
+ return receipt;
+ } finally {
+ if (server) { await server.close(); receipt.artifactServerClosed = true; }
+ else receipt.artifactServerClosed = true;
+ fs.rmSync(stage, { recursive: true, force: true }); receipt.stageRemoved = !fs.existsSync(stage);
+ fs.writeFileSync(path.join(resultRoot, `${runName}.driver.json`), JSON.stringify(receipt, null, 2), { mode: 0o600 });
+ }
+}
+
+if (require.main === module) {
+ const args = process.argv.slice(2); const options = {};
+ for (let i = 0; i < args.length; i++) {
+ if (args[i] === '--tier') options.tier = Number(args[++i]);
+ else if (args[i] === '--output') options.output = args[++i];
+ else if (args[i] === '--artifact-host') options.artifactHost = args[++i];
+ else if (args[i] === '--sandbox-cli') options.sandboxCli = args[++i];
+ else throw new Error(`Unknown option: ${args[i]}`);
+ }
+ if (!options.output) throw new Error('--output is required');
+ run(options).then(receipt => { process.stdout.write(`${JSON.stringify(receipt, null, 2)}\n`); process.exitCode = receipt.exitCode === 0 ? 0 : 1; })
+ .catch(error => { process.stderr.write(`${error.stack}\n`); process.exitCode = 1; });
+}
+module.exports = { command, manifestFor, resolveSandboxCli, serveInputs, validateReport,
+ verifySandboxCli, run };
diff --git a/docker/context-profiles/sandbox-smoke.js b/docker/context-profiles/sandbox-smoke.js
new file mode 100644
index 000000000..ed68fe694
--- /dev/null
+++ b/docker/context-profiles/sandbox-smoke.js
@@ -0,0 +1,180 @@
+#!/usr/bin/env node
+'use strict';
+
+// Runs only inside the disposable acceptance environment. The supervisor owns
+// the verdict and resource cleanup; this script supplies independently checked
+// file and public-CLI assertions, not a production-readiness assertion.
+const assert = require('node:assert/strict');
+const crypto = require('node:crypto');
+const fs = require('node:fs');
+const path = require('node:path');
+const { spawnSync } = require('node:child_process');
+
+const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
+
+function discoverPublishedSkills(packageRoot) {
+ const skillsRoot = path.join(packageRoot, 'skills');
+ const nativeNames = new Set();
+ return fs.readdirSync(skillsRoot, { withFileTypes: true }).filter(entry => {
+ if (!entry.isDirectory()) return false;
+ assert.equal(entry.isSymbolicLink(), false, 'Published skill directory must not be a symlink');
+ return fs.existsSync(path.join(skillsRoot, entry.name, 'SKILL.md'));
+ }).map(entry => {
+ assert.match(entry.name, NAME, 'Canonical skill directory has an invalid name');
+ const source = fs.readFileSync(path.join(skillsRoot, entry.name, 'SKILL.md'), 'utf8')
+ .replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
+ const frontmatter = source.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
+ assert.ok(frontmatter, `Missing skill metadata: ${entry.name}`);
+ const names = frontmatter[1].split('\n').map(line => line.match(/^name:[ \t]*([a-z0-9]+(?:-[a-z0-9]+)*)[ \t]*$/))
+ .filter(Boolean).map(match => match[1]);
+ assert.equal(names.length, 1, `Skill requires one plain native name: ${entry.name}`);
+ assert.equal(nativeNames.has(names[0]), false, `Duplicate native skill name: ${names[0]}`);
+ nativeNames.add(names[0]);
+ return { id: `skill:${entry.name}`, sourceName: entry.name, nativeName: names[0] };
+ }).sort((left, right) => left.id.localeCompare(right.id));
+}
+
+function smoke(packageRoot, workspace) {
+ const cli = path.join(packageRoot, 'scripts/ecc.js');
+ // macOS exposes /tmp as a system symlink to /private/tmp. Canonicalize the
+ // newly created directory so the production store can keep rejecting
+ // symlinked managed paths without rejecting this isolated acceptance root.
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(workspace, 'lifecycle-')));
+ const stateRoot = path.join(root, 'store');
+ const nativeRoot = path.join(root, 'native');
+ const sentinel = path.join(root, 'user-owned.txt');
+ fs.writeFileSync(sentinel, 'preserve unrelated user content\n');
+ const checks = [];
+ function invoke(args, expected = 0) {
+ const child = spawnSync(process.execPath, [cli, 'profile', ...args, '--json'], {
+ cwd: root, encoding: 'utf8', timeout: 90000, maxBuffer: 16 * 1024 * 1024,
+ });
+ assert.equal(child.error, undefined, child.error?.message);
+ assert.equal(child.status, expected, child.stderr || child.stdout);
+ return JSON.parse(child.stdout);
+ }
+ function profile(args, expected) { return invoke([...args, '--state-root', stateRoot], expected); }
+ const preview = profile(['set', 'lean', '--dry-run']);
+ assert.equal(preview.status, 'success');
+ assert.equal(fs.existsSync(stateRoot), false);
+ checks.push('dry-run-does-not-create-state');
+
+ const full = profile(['set', 'full', '--exclude', 'skill:python-testing']).store;
+ assert.ok(full.selectedIds.length > 200);
+ assert.ok(!full.selectedIds.includes('skill:python-testing'));
+ const verify = value => {
+ const carrier = JSON.parse(fs.readFileSync(path.join(path.dirname(value.generationRoot), 'carrier.json')));
+ for (const file of carrier.files) {
+ const bytes = fs.readFileSync(path.join(value.generationRoot, file.destinationPath));
+ assert.equal(bytes.length, file.bytes);
+ assert.equal(crypto.createHash('sha256').update(bytes).digest('hex'), file.digest);
+ }
+ return carrier.files.length;
+ };
+ const fullFiles = verify(full);
+ const repeated = profile(['set', 'full', '--exclude', 'skill:python-testing']).store;
+ assert.equal(repeated.revision, full.revision);
+ profile(['set', 'lean', '--expected-revision', '0'], 1);
+ assert.equal(profile(['status']).store.revision, full.revision);
+ checks.push('idempotent-install-and-stale-revision-rejection');
+
+ const lean = profile(['set', 'lean']).store;
+ assert.equal(lean.selectedIds.length, 3);
+ const leanFiles = verify(lean);
+ assert.equal(profile(['status']).store.carrierDigest, lean.carrierDigest);
+ const restored = profile(['rollback']).store;
+ assert.equal(restored.carrierDigest, full.carrierDigest);
+ assert.deepEqual(restored.selectedIds, full.selectedIds);
+ checks.push('full-lean-full-byte-verified-rollback');
+
+ // Independent layout oracle: do not import the carrier generator or its tests.
+ const allSkills = discoverPublishedSkills(packageRoot);
+ const kernel = new Set(['skill:configure-ecc', 'skill:context-budget', 'skill:ecc-guide']);
+ const layouts = { claude: 'skills', codex: 'skills', pi: 'skills',
+ opencode: '.opencode/skills', cursor: '.cursor/skills' };
+ const manifests = { claude: ['.claude-plugin/plugin.json', { name: 'ecc-context-carrier', skills: ['./skills/'] }],
+ codex: ['.codex-plugin/plugin.json', { name: 'ecc-context-carrier', skills: './skills/' }],
+ pi: ['package.json', { name: 'ecc-context-carrier', private: true, pi: { skills: ['./skills'] } }] };
+ const walk = (directory, prefix = '') => fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => {
+ assert.equal(entry.isSymbolicLink(), false, 'Carrier resource must not be a symlink');
+ const relative = path.posix.join(prefix, entry.name);
+ return entry.isDirectory() ? walk(path.join(directory, entry.name), relative) : [relative];
+ }).sort();
+ const matrix = [];
+ for (const [target, skillRoot] of Object.entries(layouts)) {
+ for (const base of ['lean', 'full']) {
+ const value = invoke(['set', base, '--target', target,
+ '--state-root', path.join(root, `matrix-${target}-${base}`)]).store;
+ const expected = base === 'lean' ? allSkills.filter(skill => kernel.has(skill.id)) : allSkills;
+ assert.deepEqual(value.selectedIds, expected.map(skill => skill.id));
+ const expectedFiles = [];
+ for (const skill of expected) {
+ const source = path.join(packageRoot, 'skills', skill.sourceName);
+ for (const relative of walk(source)) {
+ const destination = path.posix.join(skillRoot, skill.nativeName, relative);
+ expectedFiles.push(destination);
+ assert.deepEqual(fs.readFileSync(path.join(value.generationRoot, destination)), fs.readFileSync(path.join(source, relative)));
+ }
+ }
+ if (manifests[target]) {
+ const [filename, expectedManifest] = manifests[target];
+ expectedFiles.push(filename);
+ assert.deepEqual(JSON.parse(fs.readFileSync(path.join(value.generationRoot, filename))), expectedManifest);
+ }
+ assert.deepEqual(walk(value.generationRoot), expectedFiles.sort(), 'Unexpected, missing, or authority-bearing carrier file');
+ matrix.push({ target, profile: base, skills: expected.length, files: verify(value), nativeInvocation: 'unobserved' });
+ }
+ }
+ checks.push('ten-packed-carrier-layouts-exact-resource-bytes-and-file-set');
+
+ profile(['set', 'lean', '--selection', 'auto']);
+ const taskFile = path.join(root, 'task.json');
+ const task = { sessionId: 'acceptance', taskId: 'task', revision: 1, phase: 'implement',
+ query: 'Use Python patterns to explain a list comprehension.', explicitIds: ['skill:python-patterns'] };
+ fs.writeFileSync(taskFile, JSON.stringify(task));
+ const loaded = profile(['resolve', '--task-input', taskFile, '--load']).selection;
+ assert.deepEqual(loaded.loadedIds, ['skill:python-patterns']);
+ assert.ok(loaded.resources.length > 0);
+ profile(['mode', 'suggest']);
+ assert.deepEqual(profile(['resolve', '--task-input', taskFile, '--load']).selection.loadedIds, []);
+ profile(['mode', 'manual']);
+ fs.writeFileSync(taskFile, JSON.stringify({ ...task, explicitIds: [] }));
+ assert.deepEqual(profile(['resolve', '--task-input', taskFile, '--load']).selection.loadedIds, []);
+ profile(['mode', 'auto']);
+ const pending = profile(['resolve', '--task-input', taskFile]).selection;
+ assert.equal(pending.receipt.decision, 'pending');
+ assert.deepEqual(pending.loadedIds, []);
+ checks.push('auto-manual-suggest-and-pending-admission');
+
+ const native = profile(['prepare-native', '--native-root', nativeRoot]).native;
+ assert.equal(native.ready, true);
+ assert.equal(native.credentialsCopied, false);
+ assert.equal(native.selectedIds.length, 3);
+ const nativeDry = profile(['run', '--native-root', nativeRoot, '--task-input', taskFile, '--dry-run']).launch;
+ assert.equal(nativeDry.status, 'proposed');
+ assert.deepEqual(nativeDry.selection.loadedIds, []);
+ checks.push('isolated-native-discovery-and-pinned-launch-preview');
+ const interactive = profile(['start', '--native-root', nativeRoot, '--dry-run']).interactive;
+ assert.equal(interactive.status, 'proposed');
+ assert.equal(interactive.launched, false);
+ checks.push('interactive-start-preview-without-authentication');
+
+ // A user edit inside managed content must block a switch, preserving bytes.
+ const current = profile(['status']).store;
+ const ownedFile = path.join(current.generationRoot, 'skills/ecc-guide/SKILL.md');
+ fs.appendFileSync(ownedFile, '\nUser customization\n');
+ profile(['set', 'full'], 1);
+ assert.match(fs.readFileSync(ownedFile, 'utf8'), /User customization/);
+ assert.equal(fs.readFileSync(sentinel, 'utf8'), 'preserve unrelated user content\n');
+ checks.push('modified-managed-and-unrelated-files-preserved');
+ return { schemaVersion: 'ecc.context-sandbox-smoke.v1', passed: true, os: process.platform,
+ arch: process.arch, node: process.version, packageVersion: require(path.join(packageRoot, 'package.json')).version,
+ fullSkills: full.selectedIds.length, fullFiles, leanSkills: lean.selectedIds.length, leanFiles,
+ nativeVersion: native.providerVersion, matrix, checks, authenticated: false, taskOutcomes: 'unobserved' };
+}
+
+if (require.main === module) {
+ try { process.stdout.write(`${JSON.stringify(smoke(path.resolve(process.argv[2]), path.resolve(process.argv[3])))}\n`); }
+ catch (error) { process.stderr.write(`${error.stack}\n`); process.exitCode = 1; }
+}
+module.exports = { discoverPublishedSkills, smoke };
diff --git a/docs/ATLAS-CLOUD-GUIDE.md b/docs/ATLAS-CLOUD-GUIDE.md
index 83163c5b7..efefcfd7d 100644
--- a/docs/ATLAS-CLOUD-GUIDE.md
+++ b/docs/ATLAS-CLOUD-GUIDE.md
@@ -1,5 +1,7 @@
# Atlas Cloud — LLM Provider Guide
+> Sponsor status: Atlas Cloud is a past sponsor. This status change leaves the documented integration unchanged.
+
[Atlas Cloud](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=everything-claude-code) is a full-modal AI inference platform providing an OpenAI-compatible API for 59+ LLM models, image generation, and video generation.
> Run or self-host any open-source model instead of using a managed API. Itô is ECC's preferred compute sponsor: [open the Itô dashboard to sign in and rent or manage GPUs](https://compute.itomarkets.com). Any GPU provider works. That sponsorship link is passive: it does not invoke an RFQ, reserve capacity, provision compute, or configure serving. Separately, the opt-in `ecc ito find` bridge invokes the explicitly configured canonical Itô CLI and submits a live authenticated RFQ; it does not reserve capacity. Managed inference through Itô is not live yet.
diff --git a/docs/COMMAND-REGISTRY.json b/docs/COMMAND-REGISTRY.json
index 4f7918cfc..158b3e641 100644
--- a/docs/COMMAND-REGISTRY.json
+++ b/docs/COMMAND-REGISTRY.json
@@ -40,7 +40,7 @@
},
{
"command": "code-review",
- "description": "Code review — local uncommitted changes or GitHub PR (pass PR number/URL for PR mode)",
+ "description": "Code review — local uncommitted changes or GitHub PR (pass PR number/URL for PR mode). Use for a step-by-step PRP-style checklist review; for a multi-agent pass use /review-pr, and for the adversarially-verified Workflow pass use /orch-review.",
"type": "testing",
"primaryAgents": [],
"allAgents": [],
@@ -49,7 +49,7 @@
},
{
"command": "cost-report",
- "description": "Generate a local Claude Code cost report from the ECC cost-tracker metrics log.",
+ "description": "Generate a local Claude Code cost report from the ECC cost-tracker metrics log. Use for a terminal summary or CSV export of tracked spend; the cost-tracking skill covers the same metrics log for on-demand cost/budget questions asked in conversation.",
"type": "testing",
"primaryAgents": [],
"allAgents": [],
@@ -101,7 +101,7 @@
},
{
"command": "ecc-guide",
- "description": "Navigate ECC's current agents, skills, commands, hooks, install profiles, and docs from the live repository surface.",
+ "description": "Navigate ECC's current agents, skills, commands, hooks, install profiles, and docs from the live repository surface. This is the slash-command entrypoint for that navigation; the ecc-guide skill covers the same map for on-demand use in conversation.",
"type": "review",
"primaryAgents": [],
"allAgents": [],
@@ -325,7 +325,7 @@
},
{
"command": "harness-audit",
- "description": "Run a deterministic repository harness audit and return a prioritized scorecard.",
+ "description": "Run a deterministic repository harness audit and return a prioritized scorecard. Use for a deterministic overall repo-readiness scorecard; for a security-specific audit use the security-scan command.",
"type": "testing",
"primaryAgents": [],
"allAgents": [],
@@ -361,7 +361,7 @@
},
{
"command": "hookify",
- "description": "Create hooks to prevent unwanted behaviors from conversation analysis or explicit instructions",
+ "description": "Create hooks to prevent unwanted behaviors from conversation analysis or explicit instructions. Use to generate a new hook rule file from conversation analysis or a described behavior; the hookify-rules skill covers hookify rule syntax and patterns for authoring or editing rules directly.",
"type": "general",
"primaryAgents": [],
"allAgents": [],
@@ -464,7 +464,7 @@
},
{
"command": "learn",
- "description": "Extract reusable patterns from the current session and save them as candidate skills or guidance.",
+ "description": "Extract reusable patterns from the current session and save them as candidate skills or guidance. Use to review a session on demand and persist an approved skill file; continuous-learning-v2 is a separate, configurable Stop/PreToolUse/PostToolUse hook-observation and instinct-evolution system (background observer disabled by default) and is not an automatic equivalent of this command.",
"type": "review",
"primaryAgents": [],
"allAgents": [],
@@ -473,7 +473,7 @@
},
{
"command": "loop-start",
- "description": "Start a managed autonomous loop pattern with safety defaults and explicit stop conditions.",
+ "description": "Prepare a managed autonomous loop pattern with safety defaults and explicit stop conditions, then print the commands to launch and monitor it. Use to set up sequential, continuous-pr, rfc-dag, or infinite loop patterns with safety gates before starting one; the continuous-agent-loop skill covers the same pattern selection and quality-gate guidance for in-conversation use (supersedes the deprecated autonomous-loops skill).",
"type": "testing",
"primaryAgents": [],
"allAgents": [],
@@ -491,7 +491,7 @@
},
{
"command": "marketing-campaign",
- "description": "Plan and execute a full marketing campaign. Accepts a product brief and returns positioning, landing page copy, email sequence, social posts, ad variants, video scripts, and a content calendar. Can also review existing copy for conversion quality.",
+ "description": "Plan and execute a full marketing campaign. Accepts a product brief and returns positioning, landing page copy, email sequence, social posts, ad variants, video scripts, and a content calendar. Can also review existing copy for conversion quality. This is the slash-command entrypoint that delegates to the marketing-agent; prefer the marketing-campaign skill for the same end-to-end workflow in conversation.",
"type": "testing",
"primaryAgents": [],
"allAgents": [],
@@ -538,7 +538,7 @@
},
{
"command": "multi-plan",
- "description": "Create a multi-model implementation plan without modifying production code.",
+ "description": "Create a multi-model (Codex + Antigravity) implementation plan without modifying production code. Requires the external ccg-workflow runtime, not part of the base ECC install (see Prerequisite below). Use when the user explicitly wants dual-model plan drafts; for a single-model plan with no extra runtime use /plan.",
"type": "orchestration",
"primaryAgents": [],
"allAgents": [],
@@ -619,7 +619,7 @@
},
{
"command": "orch-review",
- "description": "Run the orch-review native Workflow over a diff (local changes or a GitHub PR) and report blocking vs advisory findings. Surface for the orch-review workflow.",
+ "description": "Run the orch-review native Workflow over a diff (local changes or a GitHub PR) and report blocking vs advisory findings. Surface for the orch-review workflow. Use for a native-Workflow, adversarially-verified multi-dimension review with fan-out and dedup; for the step-by-step checklist pass use /code-review, and for the multi-agent pass use /review-pr.",
"type": "review",
"primaryAgents": [],
"allAgents": [],
@@ -628,7 +628,7 @@
},
{
"command": "plan-canvas",
- "description": "Open a plan or HTML artifact in the browser Plan Canvas for annotate-and-approve review",
+ "description": "Open a plan or HTML artifact in the browser Plan Canvas for annotate-and-approve review. This is a thin slash-command entrypoint over the plan-canvas skill, which covers the full workflow and rules.",
"type": "review",
"primaryAgents": [],
"allAgents": [],
@@ -648,7 +648,7 @@
},
{
"command": "plan",
- "description": "Restate requirements, assess risks, and create step-by-step implementation plan. WAIT for user CONFIRM before touching any code.",
+ "description": "Restate requirements, assess risks, and create step-by-step implementation plan. WAIT for user CONFIRM before touching any code. Use for a single-model inline or PRD-driven implementation plan; for a dual-model (Codex/Antigravity) plan use /multi-plan, and for visual annotate-and-approve review of the resulting plan use /plan-canvas.",
"type": "testing",
"primaryAgents": [
"planner"
@@ -681,7 +681,7 @@
},
{
"command": "project-init",
- "description": "Detect a project's stack and produce a dry-run ECC onboarding plan using the repository's install manifests and stack mappings.",
+ "description": "Detect a project's stack and produce a dry-run ECC onboarding plan using the repository's install manifests and stack mappings. Use to onboard ECC into a target project via a reviewable dry-run plan; for general feature discovery and navigation use the ecc-guide skill or /ecc-guide command instead.",
"type": "testing",
"primaryAgents": [],
"allAgents": [],
@@ -759,7 +759,7 @@
},
{
"command": "prune",
- "description": "Delete pending instincts older than 30 days that were never promoted",
+ "description": "Delete pending instincts older than 30 days that were never promoted. Thin CLI wrapper for continuous-learning-v2's instinct-cli.py prune command; use to clean up stale pending instincts that were never reviewed or promoted.",
"type": "review",
"primaryAgents": [],
"allAgents": [],
@@ -862,7 +862,7 @@
},
{
"command": "review-pr",
- "description": "Comprehensive PR review using specialized agents",
+ "description": "Comprehensive PR review using specialized agents (code-reviewer, comment-analyzer, pr-test-analyzer, silent-failure-hunter, type-design-analyzer, code-simplifier). Use for a multi-agent PR review pass; for the adversarially-verified Workflow pass use /orch-review, and for the standalone step-by-step checklist review use /code-review.",
"type": "testing",
"primaryAgents": [],
"allAgents": [],
@@ -914,7 +914,7 @@
},
{
"command": "santa-loop",
- "description": "Adversarial dual-review convergence loop — two independent model reviewers must both approve before code ships.",
+ "description": "Adversarial dual-review convergence loop — two independent model reviewers must both approve before code ships. Use for the CLI-driven version of this loop; wraps the santa-method skill.",
"type": "review",
"primaryAgents": [],
"allAgents": [],
@@ -932,7 +932,7 @@
},
{
"command": "security-scan",
- "description": "Run AgentShield against agent, hook, MCP, permission, and secret surfaces.",
+ "description": "Run AgentShield against agent, hook, MCP, permission, and secret surfaces. This is the slash-command entrypoint for that audit; prefer the security-scan skill for the same AgentShield audit in conversation.",
"type": "review",
"primaryAgents": [
"security-reviewer"
@@ -965,7 +965,7 @@
},
{
"command": "skill-create",
- "description": "Analyze local git history to extract coding patterns and generate SKILL.md files. Local version of the Skill Creator GitHub App.",
+ "description": "Analyze local git history to extract coding patterns and generate SKILL.md files. Local version of the Skill Creator GitHub App. Use to generate new skills from local git history on demand; check the skill-scout skill first to avoid duplicating an existing local, marketplace, or GitHub skill.",
"type": "testing",
"primaryAgents": [],
"allAgents": [],
@@ -974,7 +974,7 @@
},
{
"command": "skill-health",
- "description": "Show skill portfolio health dashboard with charts and analytics",
+ "description": "Show skill portfolio health dashboard with charts and analytics. Use for the quantitative usage/success-rate dashboard; for a qualitative compliance or quality audit use the skill-stocktake skill.",
"type": "review",
"primaryAgents": [],
"allAgents": [],
diff --git a/docs/SKILL-DEVELOPMENT-GUIDE.md b/docs/SKILL-DEVELOPMENT-GUIDE.md
index fc1fb0690..f29f8cf11 100644
--- a/docs/SKILL-DEVELOPMENT-GUIDE.md
+++ b/docs/SKILL-DEVELOPMENT-GUIDE.md
@@ -908,6 +908,7 @@ npm run test:e2e
## Additional Resources
+- [DevScratchpad AI Skill Studio](https://www.devscratchpad.tech/ai-skill-studio/claude-skills) - Third-party editor for drafting Claude Code `SKILL.md` files.
- [CONTRIBUTING.md](../CONTRIBUTING.md) - General contribution guidelines
- [project-guidelines-template](./examples/project-guidelines-template.md) - Project-specific skill template
- [coding-standards](../skills/coding-standards/SKILL.md) - Example of standards skill
diff --git a/docs/de-DE/README.md b/docs/de-DE/README.md
index 07542e977..ff00c4fa7 100644
--- a/docs/de-DE/README.md
+++ b/docs/de-DE/README.md
@@ -1,4 +1,4 @@
-**Sprache:** [English](../../README.md) | [Deutsch](README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md)
+**Sprache:** [English](../../README.md) | [Deutsch](README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
# ECC
@@ -28,7 +28,7 @@
**Language / 语言 / 語言 / Dil / Язык / Ngôn ngữ**
[English](../../README.md) | [**Deutsch**](README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md)
- | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md)
+ | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
diff --git a/docs/design/context-carriers.md b/docs/design/context-carriers.md
new file mode 100644
index 000000000..ee4f61212
--- /dev/null
+++ b/docs/design/context-carriers.md
@@ -0,0 +1,79 @@
+# Skill-only context carriers
+
+Status: P2a/P2b/P2c implemented and focused checks passed, following the read-only foundation in [PR #3037](https://github.com/affaan-m/ECC/pull/3037). This is a source implementation contract, not an installation, activation, or native discovery certificate.
+
+M1 context profiles determine proposed discovery. Carrier layouts map that proposal into a portable file inventory. Sandbox authority, hooks, tool permissions, task routing, and user settings remain separate. See the [profile contract](context-profiles.md) for Lean/Full and selection semantics.
+
+## Three bounded slices
+
+| Slice | Contract | Boundary |
+| --- | --- | --- |
+| P2a resource declarations | Registry and plan entries preserve sorted explicit `requiredResources` | `sourcePath` is the mandatory entrypoint; empty declarations do not prove resource or workflow closure |
+| P2b carrier planning | `planContextCarrier(options)` emits `ecc.context-carrier.v1` | Pure read-only file projection; no output destination, installed-state probe, or native activation |
+| P2c acceptance fixtures | An independently checked disposable tree demonstrates structural materialization | Test-only writer owns its temporary parent; observed file equality does not prove native discovery or invocation |
+
+The generated registry/plan v1 shapes gain an additive `requiredResources` field. Existing profile IDs and declaration schemas retain their meanings. Inspection consumers should tolerate additional output fields. A new carrier consumer must reject an older object missing declaration metadata instead of interpreting it as an empty declaration.
+
+`sourcePath` remains required even when absent from the explicit declaration list. An explicit declaration of `SKILL.md` remains visible. The effective required set is their union, while `resources` inventories all included bundled files. Resource-content digests retain their exact byte semantics; registry and plan provenance also bind declaration changes.
+
+## User-facing preview
+
+```sh
+node scripts/ecc.js profile carrier lean@1 --target codex --json
+node scripts/ecc.js profile carrier lean@1 --target claude --include skill:security-review --json
+node scripts/ecc.js profile carrier full@1 --target pi --exclude skill:python-patterns --selection manual --json
+```
+
+The packaged command uses `ecc profile carrier` with the same arguments. Defaults match profile preview: Lean, Codex, and Auto selection intent. Auto remains recorded intent only. The JSON inspection envelope reports a warning and unobserved activation; its `carrier` object lists exact proposed files and source bindings. No files are written. Destination and hook flags are rejected.
+
+The [carrier library](../../scripts/lib/context-carriers.js) accepts the same source/profile/target/selection options as compilation. It compiles from canonical sources, verifies the loaded registry matches the compiled plan, and rejects externally supplied replacement plans or unknown options. Its output is checked against the [carrier schema](../../schemas/context-carrier.schema.json).
+
+The schema validates output shape and rejects unknown fields. Semantic relationships such as exact target/layout agreement and resource completeness are enforced by the generator and independent fixture verifier. Schema validation alone cannot certify a supplied artifact.
+
+## Layouts preserve the exact selection
+
+| Target | Skill root within a future isolated carrier | Generated discovery manifest |
+| --- | --- | --- |
+| Claude | `skills/` | `.claude-plugin/plugin.json` |
+| Codex | `skills/` | `.codex-plugin/plugin.json` |
+| Pi | `skills/` | `package.json` with the narrow Pi skills declaration |
+| OpenCode | `.opencode/skills/` | None; use the native project skills convention |
+| Cursor | `.cursor/skills/` | None; use the native project skills convention |
+
+These are implemented layout proposals, not five certified runtime integrations. Other recognized target IDs return `status: unsupported` with an empty file list and retained proposal inventory; unknown target IDs fail. A legacy install-module declaration gap remains visible independently of layout availability.
+
+Every selected skill contributes its complete bundled tree. Canonical IDs remain stable; destination directories use validated native metadata names, which can differ from canonical directory IDs. Full honors explicit exclusions. Routed and excluded skills contribute no carrier files; routed retrieval remains future work rather than an extra undisclosed bootstrap skill. Generated manifests use a narrow field allowlist and never inherit ECC's monolithic hooks, MCP configuration, agents, commands, or broad instruction lists.
+
+Copy operations retain binary byte digests and sizes rather than embedding decoded bodies. Generated manifests bind exact UTF-8 bytes. Required resources must exist in the selected inventory. Duplicate native names, case-colliding paths, unsafe paths, nested case-insensitive skill entrypoints, or source-plan drift fail before a carrier can be returned.
+
+Preserved skill files can contain their own authority-related metadata, including `allowed-tools`. Planning treats those bytes as data and grants no authority. Before native activation, resolve skill-level metadata against retained user consent and trusted policy; omitting hook and MCP manifest fields is insufficient for that gate.
+
+The artifact binds the source registry, profile, compiler, plan, and adapter implementation/schema digests. `carrierDigest` binds the full proposed artifact before adding its own digest. Hashes are content bindings, not signatures or attestations. No runtime execution or executable-mode preservation is certified.
+
+## Acceptance evidence has a narrow meaning
+
+The source-only fixture helper creates its own temporary parent, stages pinned source bytes, and compares an independently expected tree with observed files. It does not accept a user destination. Tests cover resource omission, extra or changed bytes, binary preservation, source drift, symlink substitution, failed-write cleanup, and unrelated sentinel preservation. Generated content must match its independently compiled expectation; a carrier's self-reported digest cannot redefine acceptance.
+
+Structural evidence and native evidence are distinct:
+
+| Claim | Required evidence |
+| --- | --- |
+| Materialized file set and byte integrity | Fixture comparison against independent expected source and generated content |
+| Bundled resource completeness and relocation | All selected resources present; verification still works after source removal |
+| Native visible IDs and exclusions | Future fresh-session probe for a named provider version and install path |
+| Skill loading and useful workflow execution | Future native invocation and task-outcome checks |
+| Activation, reload, rollback, hooks, whole-context cost | Later dedicated lifecycle, consent, and measurement gates |
+
+No structural result may set native discovery, invocation, activation, or token usage to verified. Whole bundled trees also do not prove complete cross-skill or external runtime dependency closure.
+
+## Contributor and provider provenance
+
+The architecture reuses Jeffrey Montoya's [#2788](https://github.com/affaan-m/ECC/pull/2788) ideas of whole-skill copying and one preview/build inventory. Ownership receipts and staging/rollback mechanics remain queued for P3. Its extra catalog bootstrap and copying of all unselected skills are not carried forward because they would change the approved selection or leak exclusions.
+
+LovePlayCode's [#2844](https://github.com/affaan-m/ECC/pull/2844) grouping and deterministic selection ideas inform the shared inventory. Its broad Full directory projection cannot preserve explicit exclusions, so the carrier uses the canonical selected IDs instead. These source contributions remain independently reviewable with attribution; this work does not merge or close their PRs.
+
+Codex and Pi layout fields are grounded in ECC's existing native manifests; provider mirrors are not used as canonical resources. Claude's [documented path rules](https://code.claude.com/docs/en/plugins-reference#path-behavior-rules) require install-path-specific exclusion tests because default discovery can be additive. OpenCode's [skill-name rules](https://opencode.ai/docs/skills/#validate-names) require the native directory name to match metadata. These constraints inform projection fixtures and do not substitute for fresh-session observations.
+
+## Next gate
+
+Earn native discovery and exclusion evidence using isolated homes and exact provider versions. Then implement transactional activation and recovery using the accepted ownership/receipt contract. Task routing, automatic switching, hook consent integration, and release-default changes remain behind their later gates.
diff --git a/docs/design/context-carriers.tdd.md b/docs/design/context-carriers.tdd.md
new file mode 100644
index 000000000..8655fee37
--- /dev/null
+++ b/docs/design/context-carriers.tdd.md
@@ -0,0 +1,78 @@
+# ECC-029 carrier slice evidence
+
+Date: September 8, 2026. Milestone: M1 canonical context profiles. The P2a/P2b/P2c stack follows [PR #3037](https://github.com/affaan-m/ECC/pull/3037), based on main `5064474d4d762dc9640234a41617cccb79185cec`. Environment: macOS 26.6.2 arm64, Node 24.9.0, ECC 2.2.1. This source-only report records local development evidence. The packed [carrier contract](context-carriers.md) defines the public boundaries.
+
+## Test-first slices and review regressions
+
+| Slice or regression | RED checkpoint | GREEN checkpoint and evidence |
+| --- | --- | --- |
+| P2a explicit required-resource output | `3b3a7c72`: 3 resource cases passed, 10 failed for missing declarations | `935861ac`: 13 resource cases pass; resource byte digests retain their meaning, while declaration changes affect provenance |
+| P2b pure five-layout file planner | `09ec70d9`: 20 cases fail for the intended missing public module | `bdb317eb`: 22 planner cases pass, including subsequent path-alias regressions |
+| Read-only carrier CLI journey | `3b3a7c72`: 1 CLI case passed, 6 failed for missing command behavior | `bdb317eb`: 7 cases pass; deterministic JSON, five layouts, exclusions, unsupported targets, argument rejection, unchanged temporary caller state |
+| Packed public surface | `a2963136`: both publish-surface cases fail for the missing carrier contract | `bdb317eb`: 2 cases pass with the library, schema and public contract included |
+| Portable path collision rejection | `337c560c`: 20 planner cases passed, 2 failed for case/NFC-equivalent directory prefixes | `bdb317eb`: all 22 pass; aliases with different child names fail before returning an artifact |
+| P2c disposable acceptance fixture | `09ec70d9`: the intended helper entry point is absent | `fccadba2`: 19 fixture cases pass, including independent expected-plan and manifest checks, source removal, binary bytes, tampering, symlinks and cleanup |
+| Fixture aliases fail before writes | `9454a0d5`: 17 cases passed, 2 failed because staging performed 6 writes before rejection | `fccadba2`: both adversarial cases reject with zero writes |
+
+Preserve the RED/GREEN commits. Independent security/code review checked the file planner and CLI, reproduced the portable ancestor collision, and approved the corrected implementation. The acceptance helper received separate review and remains test-only. Source files and skill bodies are data during these checks; scripts are copied but never executed. Narrow manifests omit hooks and MCP settings, while preserved authority-related skill metadata remains a separate pre-activation policy gate.
+
+## Focused checks and coverage
+
+```sh
+./node_modules/.bin/c8 --all \
+ --include='scripts/lib/context*.js' \
+ --include='scripts/profile.js' \
+ --include='scripts/ci/validate-context-profiles.js' \
+ --reporter=text --reporter=json-summary \
+ --reports-dir=/tmp/ecc-029-carrier-coverage \
+ --check-coverage --lines=80 --functions=80 --branches=80 --statements=80 \
+ node --test tests/lib/context-pack-registry.test.js \
+ tests/lib/context-profiles.test.js tests/lib/context-resources.test.js \
+ tests/lib/context-carriers.test.js tests/lib/context-carrier-fixture.test.js \
+ tests/scripts/profile.test.js tests/scripts/profile-carrier.test.js \
+ tests/ci/context-profiles.test.js
+node tests/scripts/npm-publish-surface.test.js
+npm run lint
+npm test
+git diff --check
+```
+
+Focused results: 119 logical cases passed, zero failed or skipped. The breakdown is 18 registry, 12 compiler, 13 resource, 22 carrier, 19 fixture, 25 original CLI, 7 carrier CLI and 3 CI cases. Node's outer TAP summary reports 93 because the original CLI and CI files each wrap their own cases.
+
+Runtime coverage: 98.33% statements/lines, 91.16% branches and 100% functions. All thresholds pass. A separate test-helper-inclusive review run reports 100% statements/lines/functions and 90.54% branches for that helper. Runtime coverage excludes test infrastructure.
+
+## Real inventory and package verification
+
+All ten source-tree Lean/Full combinations across Claude, Codex, Pi, OpenCode and Cursor passed disposable structural verification against the actual canonical inventory. Full contains 286 skills and 464 bundled files. Claude, Codex and Pi add one narrow manifest, giving 465 files; OpenCode and Cursor retain 464. Lean contains 3 skills and 3 source files, plus a manifest where applicable.
+
+At implementation head `d52d3430`, the full `npm test` exited 0 and its legacy aggregate reported 4,423 passed and zero failed. That aggregate does not separately count the new node:test cases, which are reported explicitly above. Full ESLint/Markdown lint and whitespace checks passed before this source-only evidence update.
+
+A real `npm pack` ran the normal prepack build. The archive SHA-256 was `dd0577889bfa09071cbd87b430b200f8d0eaf036c6b0fb583dc71ae2f855fd78`. A disposable consumer installed it with `npm install --offline --ignore-scripts --omit=dev --no-audit --no-fund --userconfig=/dev/null`, using a task-local cache explicitly primed online during the preceding PR-readiness check. This proves an offline cached install, not a dependency-free install.
+
+The installed public dispatcher produced all ten Lean/Full carrier objects with deep equality to the checkout, including their complete digests. Each installed artifact then passed structural materialization using the installed package's own canonical skill resources and an independently compiled expected plan. Full's 464 bundled resources were verified in every layout. The isolated subprocess environment was allowlisted and its disposable home remained absent. Packed runtime resolution confirmed js-yaml 4.3.2.
+
+A separate policy simulation denying Windows file symlinks passed all 54 new resource/carrier/fixture cases with zero skips. Directory links use junctions on Windows. This simulation supplies no native Windows filesystem or provider evidence.
+
+Hosted review of the prerequisite PR subsequently identified dry-run argument ordering and directory-enumeration bounds. Fixes and their dependent-stack revalidation follow; the `d52d3430` results remain a pinned earlier checkpoint.
+
+## September 9 review hardening and final verification
+
+The stack inherits the prerequisite PR's global dry-run fix `9b5e3934` and bounded-reader fix `5f9503e6`. Their RED checkpoints are `c373b7fe` (27 CLI passes, 4 failures) and `ea00894d` (7 support-test failures). The reader keeps all file-byte and identity protections and now limits incremental directory enumeration. Public context-profile documentation describes the exact limits. Source-reader extraction received independent security review; its largest function is 20 lines.
+
+Carrier checkpoint `ebd43bef` independently reproduced the global flag failure: 6 CLI cases passed and 1 failed. Merging the prerequisite fixes in `072a3160` makes all 7 carrier CLI cases pass, including a leading global flag and a flag between an option and its value.
+
+The first merged focused run passed 98 outer tests and failed 2 alias regressions because their old `readdirSync` mocks no longer supplied synthetic alias names to the incremental reader. Test-only correction `46924366` models those same source directories through `opendirSync` instead. Both case/NFC spellings and the mandatory zero-staging-write assertions remain unchanged; independent review reran all 19 fixture cases successfully. No runtime change was needed.
+
+Final focused execution uses the coverage command above plus `tests/lib/context-profile-support.test.js`. It passes 132 logical cases, zero failures or skips: 18 registry, 7 support, 12 compiler, 13 resource, 22 carrier, 19 fixture, 31 original CLI, 7 carrier CLI and 3 CI. Outer TAP reports 100 passes. Runtime coverage is 98.37% statements/lines, 91.43% branches and 100% functions, with every threshold passing.
+
+Both prerequisite and carrier full-suite commands exited 0 with legacy aggregates of 4,429 passed and zero failed. The carrier run began at `072a3160`; its test-only mock correction was applied before the runner reached that fixture file, whose final 19/19 result was observed in the complete run. Runtime and packed files remained unchanged throughout. The final focused run independently exercised the corrected tests. Later changes update source-only evidence.
+
+The rebuilt carrier archive at runtime revision `072a3160` has SHA-256 `45ef651dfab1a9da9af7b7b4b4546c84bc6b325a31a95dac47d52def060649e6`. Its offline cached install and all ten installed-provider-layout Lean/Full parity and structural checks passed again. The archive has 2,628 entries; none of these checks launches a provider. A Git diff verifies final runtime, schemas, manifests, package declarations, lockfiles and packed contracts are byte-identical to that revision.
+
+The prerequisite runtime at `e54fd44c` separately passes 71 focused cases, 98.49% statements/lines, 90.46% branches and 100% functions, plus the full 4,429 aggregate. Its rebuilt offline-consumer archive has SHA-256 `e96826df9b336e180408c7765dcd4e09fca2fb7eb7252cbf84f2ff99d036b1a7`. Later prerequisite commit `be393cb0` only reconciles the source-only dependency evidence. Hosted CI is still pending for the latest PR revision.
+
+Lower-priority review suggestions remain explicit follow-ups: failing projection labels, one exported supported-profile list, richer budget-failure inspection and preserving dual CLI/snapshot diagnostics. Process-lifetime compiler caching is deferred until an immutable snapshot and invalidation contract exists. The current schema fixes the budget at 8,000; alternate ceilings are rejected. Private fixtures currently have only synchronous callers, and noncanonical skill-root directories remain rejected under the existing inventory policy.
+
+## Claims deliberately left unobserved
+
+Native discovery, exact native exclusions, invocation, executable-mode needs, workflow outcomes, activation, hook consent, rollback, automatic routing and actual token savings still require their own gates. Schema validation checks shape; it cannot certify supplied artifact semantics. The independently compiled fixture checks exact layout, selection, file set and bytes. It uses a trusted private temporary parent and does not certify an arbitrary-destination transaction writer against hostile concurrent mutation. No native provider, model, container or VM was launched, and no package was published.
diff --git a/docs/design/context-profile-ai-evaluation.md b/docs/design/context-profile-ai-evaluation.md
new file mode 100644
index 000000000..f9d790df7
--- /dev/null
+++ b/docs/design/context-profile-ai-evaluation.md
@@ -0,0 +1,128 @@
+# Context profile AI evaluation
+
+This development-only evaluator measures whether Lean with Auto selection completes real
+coding tasks as well as Full. It lives in `docker/context-profiles/` and is not part of
+the published npm package. No provider call occurs without an injected test provider or
+the explicit `--allow-real-provider` flag. Reports never approve a release on their own.
+
+## What it compares
+
+`docker/context-profiles/ai-corpus.json` fixes 30 small coding tasks and at least 30
+selection probes before execution. Each task is a tiny CommonJS workspace with a bug or
+missing behavior; about two thirds benefit from a specific ECC skill and the rest need
+none, including tasks with misleading workflow vocabulary. Each task carries a hidden
+grader that the agent never sees.
+
+Every task runs in all three arms, in separate fresh workspaces with identical files.
+Arm order rotates by task and repeat to reduce fixed ordering effects.
+
+| Arm | Codex install | ECC task context |
+| --- | --- | --- |
+| Full | Real Full install: every skill natively discoverable | None; the host chooses from its own catalog |
+| manual Lean | Real Lean install: three-entry core | The task's preregistered skill, loaded by the launcher |
+| Auto Lean | Same Lean install | The resolver's shortlist plus one bounded agent proposal |
+
+Both installs are prepared through the isolated native adapter (`applyStore` then
+`prepareNativeProfile`), the same path users get. Before every call the evaluator
+re-verifies the install's recorded inventory and stops with `environment-drift` if
+Codex changed discovery configuration or skill bytes. Full therefore measures today's
+native experience, including its real startup context, rather than a simulated catalog.
+
+## Hidden grading
+
+After the agent exits, the evaluator writes the grader into the workspace and runs it
+with Node. Exit zero passes. An agent that plants its own grader file fails. On Node 20
+and later the grader runs under Node's permission model with read access limited to the
+workspace, so it cannot write files, spawn processes or start workers. Network access is
+not restricted by that model; run live evaluations inside the Tier 1 sandbox when that
+matters. Provider exit status and claimed success alone never pass a task.
+
+`tests/lib/context-profile-eval-corpus.test.js` proves every grader fails on the initial
+files and passes on an independent reference solution kept in
+`tests/fixtures/context-eval-references.json`, which is never shown to the agent.
+
+## Setup with a ChatGPT subscription
+
+The Codex adapter supports exactly Codex 0.154.0 and 0.155.1. Install a pinned copy
+next to, not over, your everyday Codex:
+
+```sh
+npm install --prefix ~/.ecc-eval/codex @openai/codex@0.155.1
+```
+
+Create a dedicated login home and sign in once. The file credential store keeps the
+login in `auth.json`, which the evaluator can lease:
+
+```sh
+mkdir -m 700 -p ~/.ecc-eval/auth
+CODEX_HOME=~/.ecc-eval/auth ~/.ecc-eval/codex/node_modules/.bin/codex login \
+ -c 'cli_auth_credentials_store="file"'
+chmod 600 ~/.ecc-eval/auth/auth.json
+```
+
+For each call, the evaluator copies `auth.json` into the isolated install's
+`CODEX_HOME`, runs Codex, writes any refreshed tokens back to the login home, and always
+deletes the copy. It refuses a login home that is your own `~/.codex` or `CODEX_HOME`,
+or that other users can read. It never reads your everyday Codex home. Calls run
+sequentially, so refreshed tokens cannot race. Usage counts against your subscription's
+rate limits. `CODEX_API_KEY` remains an alternative when no `--auth-home` is given.
+
+## Running
+
+Register first, then execute against the retained registration:
+
+```sh
+CODEX=$(realpath ~/.ecc-eval/codex/node_modules/@openai/codex/bin/codex.js)
+node docker/context-profiles/ai-eval.js --plan \
+ --executable "$CODEX" --model YOUR_PINNED_MODEL > /tmp/ecc-ai-registration.json
+node docker/context-profiles/ai-eval.js --allow-real-provider \
+ --registration /tmp/ecc-ai-registration.json \
+ --executable "$CODEX" --model YOUR_PINNED_MODEL \
+ --auth-home ~/.ecc-eval/auth > /tmp/ecc-ai-metrics.json
+```
+
+The registration binds corpus bytes, registry resource digests, both profile plans,
+evaluator, launcher, resolver and native adapter digests, model and executable
+fingerprints, case order, repeats and analysis thresholds. A changed source stops
+execution. Repeated sampling requires the same `--repeats N` at registration and
+execution. A changed corpus is a new experiment, never a silent replacement for failed
+cases.
+
+Defaults are 300 provider calls, a one-hour overall deadline and five minutes per task
+call. Hard limits are 2,000 calls, four hours and ten minutes per call. Proposal calls
+retain the launcher's tighter timeout. A single pass of the bundled corpus makes about
+90 task calls plus up to one proposal call per Auto task and selection probe. Every
+scheduled outcome remains in the denominator after a budget, deadline, provider, drift
+or grading failure. Workspaces and installs are removed in `finally`.
+
+## Metrics and statistical limits
+
+The JSON report is built from an allowlist: case IDs, arm, repeat, pass/fail, controlled
+failure codes, selected skill IDs, digests, call counts, elapsed time, numeric usage,
+install skill counts and the authentication mode. Transcripts, prompts, paths, stderr
+and credentials are never emitted or persisted. Valid usage requires one
+`turn.completed` record with nonnegative integer input, cached-input and output
+counters. Missing or malformed usage is unknown, never zero.
+
+Selection accuracy includes a descriptive 95% Wilson interval. Paired pass-rate
+differences against Full use a conservative bounded Hoeffding interval with Bonferroni
+correction across the two comparisons. Repeats are averaged within distinct task IDs
+first, so repeating tasks never creates new independent tasks. The corpus is purposive,
+so no production population generalization is justified.
+
+The preregistered minimum is 30 distinct tasks and 30 selection cases, with a
+five-percentage-point noninferiority margin. With 30 tasks the Hoeffding interval is
+still wide, so a first live run is expected to report `review-required` without
+supporting noninferiority. Use its observed variance to size the next corpus.
+
+## Deterministic verification
+
+```sh
+node --test tests/lib/context-profile-eval.test.js tests/lib/context-profile-eval-corpus.test.js
+node docker/context-profiles/ai-eval.js --plan
+```
+
+Injected providers validate the measurement path, isolation, grading, lease handling
+and sanitization. A passing synthetic run validates the framework, never model quality.
+A valid CLI report exits zero even when cases fail or the sample is insufficient;
+consumers must inspect case results and the gate.
diff --git a/docs/design/context-profile-delivery.md b/docs/design/context-profile-delivery.md
new file mode 100644
index 000000000..82d6151fe
--- /dev/null
+++ b/docs/design/context-profile-delivery.md
@@ -0,0 +1,91 @@
+# Lean, Full, and task selection delivery
+
+ECC-029 advances M1: a canonical `lean@1` / `full@1` context contract. This development branch adds managed generations, experimental task selection, an opt-in isolated Codex session, and a preregistered outcome-evaluation pilot. Public release defaults remain governed by the M1 release gate.
+
+## Development sequence and acceptance
+
+| Stage | Deliverable | Acceptance |
+| --- | --- | --- |
+| Registry and compiler | One source-backed registry, Lean/Full plans, exact exclusions | Deterministic digests, resource closure, invalid-input fixtures |
+| Native carriers | Complete skill trees and allowlisted native manifests | Fresh Claude/Codex inventory, exclusion and relocated resource readback |
+| Managed state | Explicit private store, immutable generations, receipts, rollback and recovery | Full to Lean to Full, injected interruption, source drift, ownership and concurrency checks |
+| Task selection | Manual, suggest and Auto over a stable base | Explicit IDs, bounded agent proposals, exclusions, manual-only rules, source-bound decisions, output budget |
+| Interactive session | Receipt-bound bootstrap in an isolated native Codex home | Exact source and executable identity, bounded stdin resolution, refresh after binary or source drift |
+| Disposable acceptance | Packed install in tiered clean environments | All ten layout/profile combinations, native Codex discovery, functional store and resolver |
+| Release promotion | Certified activation adapters and outcome evidence | Provider invocation, measured whole-context budget, paired task quality, upgrade/uninstall matrix, reviewed PRs |
+
+The first five stages are the local development target. Release promotion requires its own evidence and must retain explicit unsupported or unobserved states.
+
+## User interface
+
+```text
+ecc profile preview lean --target codex --json
+ecc profile set lean --state-root /absolute/dedicated/profile-store --selection auto --dry-run --json
+ecc profile set lean --state-root /absolute/dedicated/profile-store --selection auto --json
+ecc profile status --state-root /absolute/dedicated/profile-store --json
+ecc profile mode suggest --state-root /absolute/dedicated/profile-store --json
+ecc profile rollback --state-root /absolute/dedicated/profile-store --expected-revision 2 --json
+ecc profile recover --state-root /absolute/dedicated/profile-store --json
+ecc profile resolve lean --task-input task.json|- --json
+ecc profile resolve lean --task-input task.json|- --load --json
+ecc profile resolve --state-root /absolute/dedicated/profile-store --task-input task.json --load --json
+ecc profile run --state-root /absolute/dedicated/profile-store --task-input task.json --dry-run --json
+ecc profile prepare-native --state-root /absolute/dedicated/profile-store --native-root /absolute/dedicated/native-store --json
+ecc profile native-status --state-root /absolute/dedicated/profile-store --native-root /absolute/dedicated/native-store --json
+ecc profile run --state-root /absolute/dedicated/profile-store --native-root /absolute/dedicated/native-store --task-input task.json --dry-run --json
+ecc profile start --state-root /absolute/dedicated/profile-store --native-root /absolute/dedicated/native-store
+```
+
+`set` materializes a verified generation and records the configured choice. `generationRoot` identifies the provider-shaped payload. A configured generation does not claim a running provider loaded it. Provider-owned skills can remain visible alongside ECC skills.
+
+`resolve --state-root` uses the saved base, mode and exclusions. It rejects overrides and stale source generations. `mode` preserves the configured profile and explicit selections while recording the new mode transactionally.
+
+A task input contains caller-assigned `sessionId`, `taskId`, positive integer `revision`, and `phase`. Optional fields are `query`, `explicitIds`, `proposedIds`, and `noWorkflow`. Increment revision for material task changes. A changed query, including rewording, also invalidates selection reuse. Task prose is consumed locally and omitted from returned receipts.
+
+```json
+{
+ "sessionId": "session-1",
+ "taskId": "feature-1",
+ "revision": 1,
+ "phase": "implement",
+ "explicitIds": ["skill:python-patterns"]
+}
+```
+
+Auto uses explicit user IDs first, then a completed pinned decision, an unambiguous ranked match, one cited skill name, or admitted agent-proposed IDs. Ambiguous free text shortlists up to five candidates for a bounded proposal. Manual uses explicit IDs; suggest emits a proposal without bodies. `--load` returns selected UTF-8 instructions and declared required resources, capped at 32,000 bytes across at most eight skills. `--task-input -` accepts one UTF-8 JSON object on standard input, capped at 65,536 bytes. These byte caps are output and transport bounds, not native tokenizer results.
+
+Save the returned `selection.receipt` as a separate JSON document to use `--previous receipt.json`. `--expected-digest` can bind a load to a prior selection digest. Source, trigger content, routing-policy version, profile, mode, exclusions, session, task revision, phase, and a digest of the query invalidate stale reuse. A pending proposal cannot be reused as a completed decision. Receipts are integrity checks for local operation, not an authorization signature.
+
+An agent can call the resolver at task boundaries and read the returned context. This integration is prompt-advisory. Returning a body never grants tools, invokes shell interpolation, starts a native skill, changes hooks or installs dependencies. Native manual-only flags and authority-bearing metadata are checked before selection. Base profiles remain stable during task routing.
+
+`run` is the explicit task-launch boundary. Ambiguous Auto routing makes one provider proposal call over candidate IDs and descriptions. It accepts zero or one known candidate, then rechecks source bindings, saved state, exclusions and admission policy before loading bodies. Invalid or stale proposals stop before task execution. The proposal has a 30-second timeout and 64 KiB output bound. Codex uses an ephemeral, filesystem-read-only agent session with inherited tools and configuration; the prompt's request to avoid tools is advisory, not enforced tool isolation. Claude disables tools and session persistence for this proposal. Task text is sent to the configured provider, so its normal authentication and data-handling policy apply.
+
+The task call sends the query and selected reference content on standard input to `codex exec -` or `claude --print`, with no added task permissions or hook overrides. Current-provider launches inherit the provider process environment. An isolated native launch passes only the pinned home paths, `PATH`, a fixed locale, a private temporary directory, and required Windows system root; caller credentials, proxy settings, runtime injection and unrelated secrets are excluded. Its timeout is 90 seconds after a proposal or 120 seconds without one, uses an uncatchable termination signal, and captures at most 1 MiB. Dry run reports the pending proposal without a provider call. A zero provider exit code records process completion; task success and native skill invocation remain unverified. Routine interactive turns outside this launcher do not gain automatic routing.
+
+## Isolated native Codex generations
+
+`prepare-native` registers the managed carrier in a fresh ECC-owned home, verifies exact discovery through the allowlisted Codex 0.154.0 or 0.155.1 binary, and only then selects that native generation. It writes a bounded `AGENTS.md` bootstrap bound to the installed CLI source, managed roots, carrier, executable and receipt. It copies no credentials or user configuration and never rewrites the user's provider home. `native-status` checks the recorded generation, executable fingerprint, bootstrap source identity and managed-store binding. A launch pins that verified binary instead of resolving a different executable from PATH. Explicit preparation can refresh a changed executable or installed-source binding while preserving the prior generation and receipts.
+
+`profile start` is an explicit terminal-only boundary. It revalidates the store and native generation, then launches the pinned Codex binary with inherited terminal capabilities and the isolated home. The bootstrap tells the active agent to resolve context at material task boundaries through bounded structured stdin. It remains prompt-advisory, grants no tools or permissions, and persists no task prose or selected skill bodies. Authentication must be completed separately inside the isolated home; the start path does not inherit or copy provider credentials.
+
+Switching the managed profile makes the old native generation stale until `prepare-native` succeeds. To undo a switch, first `rollback` the managed store, then use `native-rollback` with both roots. `native-recover` handles a retained interruption journal without deleting provider data. Existing sessions retain their original context. These commands support isolated Codex generations, not migration of an existing global installation or native activation for other providers.
+
+Discovery evidence comes from the generation's empty project. Task launch inherits the caller's task working directory, whose repository instructions and native configuration may add context or affect policy. Native readiness attests the isolated home's recorded inventory and integrity, not the complete context or permissions of every possible task directory.
+
+## Outcome-evaluation pilot
+
+`docker/context-profiles/ai-eval.js` is a development-only evaluator; it lives outside the published package. It preregisters a fixed corpus before any provider call, binding the corpus, profile plans, registry, implementation, Node runtime, dependency versions, model and executable digests. It supports isolated Claude skill installs for five arms, including a pinned legacy skill-library comparator, and isolated Codex Lean/Full installs without that legacy arm. A hidden grader enters each workspace only after the agent exits and runs read-only where Node supports its permission model.
+
+Real execution requires an explicit flag and provider authentication. Codex uses a dedicated subscription login home (`--auth-home`) or `CODEX_API_KEY`; Claude uses its configured token or Keychain login. A Codex subscription login is leased into each isolated call home, refreshed tokens are returned to the login home, and the leased copy is always removed. The evaluator never reads or copies the user's own Codex home. Results contain allowlisted metrics and hidden-check verdicts, not prompts, transcripts, paths or credentials. See `context-profile-ai-evaluation.md` for the setup, measurement contract and statistical limits.
+
+## Community integration
+
+Jeffrey Montoya's [#2788](https://github.com/affaan-m/ECC/pull/2788) informed whole-tree staging, ownership receipts and reversible generations. LovePlayCode's [#2844](https://github.com/affaan-m/ECC/pull/2844) informed deterministic grouping and explicit exclusion. Jeffrey's [#2945](https://github.com/affaan-m/ECC/pull/2945) informed bounded ID/description ranking and deterministic ties. Canonical source digests replace independent routing-cache authority. [#2740](https://github.com/affaan-m/ECC/pull/2740) remains aligned with native context meters and truthful measurement labels.
+
+These are attributed adaptations of concepts; contributor commits have not been silently relabeled as our implementation. Source PR disposition remains separate.
+
+## Remaining release gates
+
+The store recovers actual process exits at five durable boundaries: prepared journal, file publication, generation publication, receipt publication and state publication. An interruption before the initial ownership marker is published, or a corrupted partial kernel write, is preserved for inspection. These cases do not receive an automatic recovery claim.
+
+Small authenticated Claude pilots now provide task and token observations, but they are descriptive and the evaluation gate remains `review-required`. Adequately powered task-quality canaries and whole-context measurements need additional evidence. The opt-in interactive bootstrap has local source, discovery and terminal-start evidence, but authenticated task behavior and native skill invocation remain unobserved. Isolated Codex registration, switching, refresh and rollback have local native evidence; changing a live user installation still requires its own ownership and recovery contract. Fresh-install default changes, existing-user migration, other-provider activation, hook plans, ECC Tools compatibility, hosted rollout and package publication remain outside this local preview.
diff --git a/docs/design/context-profile-delivery.tdd.md b/docs/design/context-profile-delivery.tdd.md
new file mode 100644
index 000000000..725eb94a2
--- /dev/null
+++ b/docs/design/context-profile-delivery.tdd.md
@@ -0,0 +1,91 @@
+# ECC-029 verification ledger
+
+September 13 baseline branch: `feat/ecc-029-profile-delivery`, incorporating upstream main `8321021c` and the previous carrier branch. The September 21 continuation is recorded below. This report describes local development and packed evidence, not a public release.
+
+## Reproduced failures and fixes
+
+| Failure | RED evidence | Fix and GREEN evidence |
+| --- | --- | --- |
+| Windows profile CI identity fixtures | Synthetic inode `2 ** 60` reproduces missing-exception assertions because adding one does not change the Number | Guaranteed distinct test inode; host and large-inode fixtures pass |
+| npm resource mismatch | Source inventory contains nested `.gitignore` omitted by npm | Publication-control files excluded from canonical resources; ten packed plans match source |
+| Implicit-invocation policy race | Change `agents/openai.yaml` after compile and before policy read | Policy bytes revalidated against registry digests; preview/load reject drift |
+| Windows managed-root parsing | Drive/UNC decomposition loses root separator | Platform-aware root preservation; drive/UNC tests pass |
+| Interactive setup fixture race | Delayed startup sends blank answers and EOF before prompt | Prompt-driven PTY and final input closure; 30 tests and 36 existing-install combinations pass |
+| Overconfident keyword Auto | Realistic JS review, RAG research and npm release queries select unrelated top scores | Names and generic scores only shortlist; loading requires explicit IDs or a separately admitted agent proposal |
+| Native state and executable drift | Reviewed receipt resealing, stale revision, symlink/FIFO and binary replacement cases | Immutable transition binding, bounded regular-file reads, prepublication checks and pinned binary checks |
+| Packaged native binary layout | Linux npm wrapper differs from assumed vendor path | Resolve and fingerprint the actual pinned platform binary; regression and real Podman pass |
+
+New feature tests were introduced before their implementations. Independent review covered ownership, source races, exclusion/dependency policy, Windows paths, command validation, inherited authority, native provenance and failure propagation.
+
+## Final focused verification
+
+```sh
+node --experimental-test-coverage --test \
+ --test-coverage-include='scripts/lib/context-profile-*.js' \
+ --test-coverage-include='scripts/lib/context-selection.js' \
+ tests/lib/context-profile-*.test.js tests/lib/context-selection.test.js \
+ tests/scripts/profile-selection.test.js
+```
+
+140 tests pass, zero failures. Aggregate coverage for the listed runtime files: 92.73% lines, 81.74% branches, 96.00% functions. This includes the lightly unit-instrumented native discovery subprocess adapter, which also has real-provider conformance below. These percentages are aggregate, not per-file or repository-wide guarantees. Native unit tests account for 25 cases; launcher/proposal/CLI review accounts for 35.
+
+Final `npm test`, `npm run lint` and `git diff --check` all exit zero. The full runner reports 4,726 legacy-format passes and zero failures, and also executes the new native `node:test` files successfully. Its summary parser counts only `Passed:` output, so the separately measured 140-case focused result above is the precise native-runner count, not a claim that the full-suite summary includes every test format.
+
+## Final fresh packed consumer
+
+Command: `node docker/context-profiles/run-podman.js`. Final frozen run exits zero.
+
+Tested npm archive SHA-256:
+
+```text
+34346621a1062358f96b1a3ce2f07ac6fe72067cd735771e30d06e1dc202335e
+```
+
+Linux arm64, Node 22.23.1, Codex 0.154.0. Normal packed installation completed during image build. The runtime container used the unprivileged node user, networking disabled, all capabilities dropped, no privilege escalation, no host mounts and no copied credentials. Task containers, image and temporary build directory were removed. The exact archive and acceptance log were retained separately; ordinary dependency build caches may remain.
+
+- All ten Lean/Full target combinations match source plans and independent resource expectations. Lean has three skills. Full has 292 skills and 583 source resource files, plus one generated manifest for Claude, Codex and Pi.
+- The packed managed CLI verifies Full to Lean to rollback Full, revision checks, idempotency, exclusions, Auto loading, suggest/manual/dry-run boundaries, receipt reuse and no-workflow reset.
+- Packed `prepare-native`, `native-status` and `native-recover` pass. Isolated launch dry-run uses the pinned executable even with no provider on PATH.
+- Native Codex discovery matches Lean, Lean plus Angular and Full excluding Python patterns. Resource digests survive marketplace carrier source removal. Six provider-owned system skills are reported separately.
+- Actual managed/native product APIs switch 291 ECC skills to three and roll back to 291, preserving the Full exclusion and unrelated prior-home bytes. Every native preparation and rollback uses a fresh app-server and verifies discovery before pointer publication.
+- Earlier isolated Claude Code 2.1.247 conformance validates and lists exact Lean/Full-with-exclusion inventory with zero hooks, agents, MCP and LSP components. Its projected token counter is not provider usage.
+
+## Evidence boundaries
+
+No authenticated model calls were made. Auto proposal and task transport, admission failures, executable pinning and state drift are tested with injected executable fixtures. Dry-run and native discovery are tested through actual packed provider executables. Model-driven task success, native skill invocation and token savings remain unobserved; there is no certified routing-quality percentage.
+
+Native readiness attests the isolated generation and discovery in its empty project. Task launch inherits the actual working directory and its repository controls, so complete task-context equivalence is unverified. Codex proposal execution is filesystem-read-only but inherits provider tools; tool avoidance in its prompt is advisory. Claude proposal tools are disabled. Task execution inherits provider policy and requires normal authentication.
+
+The store recovers actual process exits at five durable boundaries. Initial creation interrupted before its ownership marker, corrupted partial writes and numeric filesystem identity precision retain explicit limitations. Live installer migration, other-provider activation, interactive Auto bootstrap, whole-context outcome evaluation and default/release changes remain delivery gates. Native status never claims that an existing session changed context.
+
+## September 21 production-acceptance continuation
+
+Branch: `feat/ecc-029-production-acceptance`, with the working integration snapshot updated to upstream main `43b3a01e`. The writer session stopped at its provider usage limit after integrating the interactive and evaluation slices. A replacement session recovered the exact tmux transcript, process state, task log and worktree before continuing. No test process was still running and no conflicting writer remained active.
+
+Additional RED/GREEN cases cover gaps found during review:
+
+- Complete skill names in questions, quoted data or negated requests previously triggered implicit loading. Names now create candidates only; a user explicit ID or admitted agent proposal is required.
+- A pending receipt could previously be reused and skip the provider decision. Receipts now bind routing-policy version and `selected`, `none` or `pending` decision state; only completed decisions can be reused.
+- A changed or removed pinned Codex executable could leave native preparation unable to refresh. Explicit preparation may create a newly verified generation while preserving the old receipt and pointer until publication. Ordinary status and start remain fail-closed.
+- Isolated native task launch previously inherited every caller environment variable. It now passes only pinned home paths, `PATH`, a fixed locale, a private temporary directory and the required Windows system root. Regression coverage proves unrelated cloud credentials, API keys, proxy settings and `NODE_OPTIONS` are absent.
+- The Auto authority check previously missed the shipped `tools` frontmatter field. Scalar and array forms now require manual selection. Malformed task JSON now returns a fixed error without echoing task bytes.
+- Provider and sandbox timeouts previously used a catchable termination signal. Launch, proposal, native discovery and sandbox supervision now use `SIGKILL`; a real subprocess that ignores `SIGTERM` verifies the sandbox bound.
+- The acceptance driver previously trusted only the sandbox exit code. It now binds the executable and its complete implementation tree, rechecks both identities across preview and execution, and validates backend, tier, real execution, assertion commands, final smoke payload, architecture, layout matrix and evidence boundaries.
+
+The opt-in interactive slice adds bounded UTF-8 task JSON on stdin, receipt-bound bootstrap instructions, installed-source and executable identity checks, exact Codex 0.154.0/0.155.1 version admission, safe refresh, and `profile start`. A real macOS arm64 Codex 0.155.1 run verified Lean, an explicit include, Full with an exclusion, relocated resource digests, stdin resolution, bootstrap visibility, sign-in-screen startup and removed-binary refresh. No credential was copied and no authenticated task turn was made.
+
+The source-only AI pilot fixes 13 selection probes and eight paired artifact tasks before execution. Registration binds corpus, registry, plans, implementation, Node runtime, pinned parser and validator dependency versions, model and binary. The provider adapter uses disposable homes, explicit opt-in, `CODEX_API_KEY`, bounded JSONL, deadlines and call counts. Independent artifact assertions and sanitized metrics are implemented. Synthetic tests validate the measurement path; they do not establish model quality. The 13/8 pilot remains below the 30/30 gate and therefore reports `insufficient-sample` even if every case passes.
+
+Current combined verification after recovery:
+
+- Focused registry, carrier, store, native, interactive, resolver, admission, evaluation, sandbox and CLI suites pass, including the review regressions above.
+- The final focused `node:test` run passes 182/182. Claude migration and setup compatibility suites pass 16/16 and 30/30. The complete repository runner passes 4,940/4,940; lint, diff checks and the production dependency audit all pass with zero vulnerabilities.
+- The integration snapshot is current with upstream main `43b3a01e`. The latest-main Claude setup change removed obsolete install flags; migration dry-run and setup expectations now match the shipped command while retaining separate settings preservation.
+- Clean commit `cda9c4bf` produced package SHA-256 `2ebc804ffc4f4c89fcf4b5ea0a9f644613618c1508292ef9199928157aa228d1`; both final driver receipts record that exact revision with `sourceDirty: false`.
+- Real Tier 1 run `ecc-profile-tier1-89ead327-f193-4959-aff4-67cf8d381df3` passes on rootless Podman with a validated final smoke payload, a complete 10,758-added/4-changed layer diff, no credentials and exact cleanup.
+- Real Tier 2 run `ecc-profile-tier2-fd4654a2-18b2-45f4-ba87-b8d0cd8bc488` passes on a disposable native macOS arm64 Lume clone with the same package digest. It validates all ten layouts, isolated Codex discovery, no credential transfer, stopped-guest cleanup and artifact-server cleanup. Lume v1 reports a bounded path scan with 49 added and nine changed files; it explicitly does not claim a complete disk diff.
+- The initial Tier 2 attempt exposed `/tmp` as the standard macOS symlink to `/private/tmp`. The acceptance verifier now canonicalizes its newly created private directory while the production managed-store guard continues to reject symlinked roots. A second guest run proved the corrected path.
+- The default sandbox checkout's 5,000-path capture limit truncated a real Tier 1 install diff and failed closed. The reviewed ECC-029 sandbox implementation raises the bounded cap to 50,000, passes its 26-case boundary suite, and produced both final reports. The driver receipt binds its 51-file implementation digest `a84e09ab848b8cd05f33792c13734f7aabe16bfe16d50d8f8292eb5261a93c3a`.
+- No real AI outcome call ran because `CODEX_API_KEY` was absent. Host ChatGPT authentication was neither copied nor exposed to the disposable evaluator.
+
+These boundaries keep the shipped behavior distinct from the M1 release gate. Authenticated outcome observations, a complete Tier 2 disk diff, live-install migration, other-provider activation, whole-context token truth and release defaults remain unverified until their explicit prerequisites are available.
diff --git a/docs/design/context-profiles.md b/docs/design/context-profiles.md
new file mode 100644
index 000000000..5c67246eb
--- /dev/null
+++ b/docs/design/context-profiles.md
@@ -0,0 +1,153 @@
+# Context profiles: read-only foundation
+
+Status: accepted first development slice, P0/P1, September 8, 2026. This document describes the source implementation and its contributor contract. It does not announce a released runtime capability or a change to installation defaults.
+
+ECC context profiles separate the skill-discovery proposal from installation, runtime authority, and measurement. The first slice inventories canonical skills, validates versioned declarations, and produces deterministic read-only plans. It does not yet scope the complete host system prompt.
+
+## Keep the controls separate
+
+| Control | Meaning | Compatibility rule |
+| --- | --- | --- |
+| Existing install `--profile` | Selects install modules using [install profiles](../../manifests/install-profiles.json) | `minimal`, `opencode`, `core`, `developer`, `security`, `research`, and `full` keep their existing meanings |
+| Context profile `lean@1` or `full@1` | Proposes which canonical skill metadata is selected for discovery | No automatic mapping from an install profile; `full@1` is a skill projection, not the complete ECC installation |
+| Selection `manual`, `suggest`, or `auto` | Records selection intent in a proposed context plan | No task classifier, agent-directed switching, or automatic application exists in this slice |
+| Existing hook profile | Controls existing hook policy through [hook flags](../../scripts/lib/hook-flags.js) | `minimal`, `standard`, and `strict` remain separate; preview never changes hook consent |
+| Runtime and capabilities | Execution isolation, tool permissions, secrets, and side effects | A context selection grants no authority and chooses no sandbox |
+
+There is no new `use`, `apply`, or `mode` mutation command. The existing install interface is preserved rather than repurposed.
+
+## Inspect the proposal
+
+From a source checkout, use the existing [ECC dispatcher](../../scripts/ecc.js):
+
+```sh
+node scripts/ecc.js profile show --json
+node scripts/ecc.js profile show lean@1 --json
+node scripts/ecc.js profile preview lean@1 --target codex --selection auto --json
+node scripts/ecc.js profile preview full@1 --target claude --selection manual --json
+node scripts/ecc.js profile preview lean@1 --target codex --include skill:security-review --exclude skill:python-patterns --json
+node scripts/ecc.js profile explain skill:security-review --target codex --json
+```
+
+The packaged CLI uses the same `ecc profile ...` arguments. `show` reads profile definitions; `preview` compiles a proposal; `explain` looks up one exact canonical skill ID and reports its source, resources, ownership, and target declarations. These commands neither invoke skills nor write installed settings. The CLI reads its own package sources, independently of the caller's working directory.
+
+CLI preview defaults are `lean@1`, target `codex`, and selection intent `auto`. These are preview defaults, not detected user preferences. The library compiler defaults selection intent to `manual`; consumers should pass the intended value explicitly. Both `lean` and `full` are accepted aliases for the versioned profile IDs.
+
+JSON responses use `ecc.profile-inspection.v1`, including `status`, `summary`, `activation`, `next_actions`, and `artifacts`. A successful preview deliberately reports `status: "warning"` with exit code 0 because runtime activation remains `unobserved`. Invalid requests return an error and exit code 1. A plan reports `active: false` and `disposition: "proposed"`; these fields must survive downstream presentation.
+
+## Public sources and APIs
+
+The source manifests have numeric `schemaVersion: 1`. Generated registry and plan objects identify their output shapes as `ecc.context-registry.v1` and `ecc.context-plan.v1` respectively.
+
+| Source | Responsibility |
+| --- | --- |
+| [Profile schema](../../schemas/context-profile.schema.json) | Versioned profile ID, registry binding, eager and required selection, and metadata budget |
+| [Registry declaration schema](../../schemas/context-pack-registry.schema.json) | Canonical inventory source and explicit per-skill dependency/resource overrides |
+| [Lean manifest](../../manifests/context-profiles/lean@1.json) and [Full manifest](../../manifests/context-profiles/full@1.json) | Reviewable selection and budget policy |
+| [Skill registry declaration](../../manifests/context-packs/skill-registry@1.json) | Binds the inventory to existing install-module ownership and the canonical skills directory |
+| [Registry library](../../scripts/lib/context-pack-registry.js) | Inventory, metadata validation, source hashing, dependency validation, and exact explanation |
+| [Profile library](../../scripts/lib/context-profiles.js) | Profile loading, deterministic selection, target projection, and metadata estimation |
+| [Shared support](../../scripts/lib/context-profile-support.js) | Bounded source reads, portable paths, schema validation, canonical serialization, and compiler digest |
+| [Profile CLI](../../scripts/profile.js) | Read-only inspection envelope and argument validation |
+
+Contributor entry points are:
+
+```js
+loadContextRegistry({ repoRoot });
+explainContextEntry({ repoRoot, id: 'skill:security-review', target: 'codex' });
+loadContextProfile('lean@1', { repoRoot });
+compileContextProfile({
+ repoRoot,
+ profileId: 'lean@1',
+ target: 'codex',
+ selectionMode: 'auto',
+ include: ['skill:security-review'],
+ exclude: ['skill:python-patterns'],
+});
+```
+
+The first two functions are exported by the registry library; the profile library exports the last two and re-exports `explainContextEntry`. The registry also exports `projectionFor(entry, target)` for already validated entries and targets. Consumers should use the loading and compilation APIs instead of duplicating source parsing or building another profile authority.
+
+## Inventory and selection semantics
+
+Each canonical `skills//SKILL.md` becomes `skill:`. Its skill directory must have exactly one owner in [install modules](../../manifests/install-modules.json). The owning module supplies `ownerModuleId`, the initial `packId`, and `declaredInstallTargets`. This reuses existing ownership without treating installer module dependencies as skill workflow dependencies.
+
+Lean currently selects three required candidate entries: `skill:configure-ecc`, `skill:context-budget`, and `skill:ecc-guide`. Other canonical skills remain labeled `routed` unless explicitly included or excluded. Here, `routed` means available in the catalog for future discovery integration; it does not mean a router has run or a native host can already retrieve the skill.
+
+Full derives `all` from the current canonical inventory. The September 8 baseline contains 286 skills, but 286 is a snapshot, not a hardcoded profile limit. Explicit exclusions can narrow a Full proposal, except for required entries and dependencies needed by retained selections.
+
+Includes add exact IDs and their transitively declared dependencies. Exclusions cannot remove required profile entries or break that declared closure. Unknown IDs, duplicate selectors, overlapping include/exclude requests, unknown targets, and invalid selection modes fail. Profiles must include their declared required entries in the eager selection.
+
+Dependencies come only from `overrides[].dependencies` in the registry declaration. The current manifest has no overrides, and entries report `dependencyCoverage: "declared-only-unreviewed"`. An empty dependency array means no declaration exists; it does not prove that a workflow is self-contained. References in skill prose are not followed, interpreted, or promoted into dependency edges.
+
+`overrides[].requiredResources` can assert that files exist within that skill's own directory. Unknown override IDs, duplicate ownership, missing resources, unknown dependencies, cycles, malformed metadata, unsafe paths, and symbolic links within the source tree are rejected. Reads are bounded at 4 MiB per file, 16 MiB per source reader, 10,000 files, and 32 levels of recursive directory depth. Directory enumeration is incremental, with at most 10,000 accepted names per directory and 20,000 traversal operations per reader. Every directory open and enumerated entry consumes that shared budget, including empty directories and excluded names; detecting overflow may inspect one extra entry. Generated Python caches, `.git`, and `node_modules` are excluded; an explicitly required excluded resource is rejected.
+
+P2a adds sorted explicit `requiredResources` to registry and plan entries. The mandatory `sourcePath` entrypoint remains distinct; effective required paths are their union. Empty declarations do not establish resource closure, and carriers must not infer that arbitrary subsets are sufficient. The first carrier implementation projects all bundled files for selected skills; see the [P2 carrier contract](context-carriers.md).
+
+Source reads revalidate ancestor and file identities before consuming bytes and after reading. These consistency checks reject the tested concurrent symlink substitution; they do not provide an atomic repository snapshot. Use immutable source artifacts for downstream execution. Skill and profile metadata reject terminal controls; CLI text also renders controls inert in error paths.
+
+## Provenance without eager instruction loading
+
+The registry reads and hashes skill bodies and bundled resource bytes to bind source identity. It does not evaluate scripts, follow instructions in prose, or emit those bodies as model context. Discovery metadata and resource descriptors are separate from instruction loading. Future native carriers must preserve on-demand loading of selected skill bodies and required resources; this first slice implements no native loader.
+
+| Digest | What it binds |
+| --- | --- |
+| Resource `digest` | Exact bytes of one source file |
+| Entry `contentDigest` | Ordered resource descriptors, including paths, byte counts, and resource digests |
+| `registryDigest` | Portable registry output, including inventory-source digests, ownership, metadata, and resource descriptors |
+| `profileDigest` | Normalized profile manifest, with selection arrays sorted |
+| `compilerDigest` | Source digests for the three compiler library files, two declaration schemas, and the existing install-manifest module supplying target IDs |
+| `planDigest` | Complete portable proposed-plan object before adding `planDigest` itself |
+
+These are SHA-256 content bindings, not signatures, runtime attestations, or a complete execution-environment identity. Digests deliberately exclude caller-specific absolute paths and timestamps. Equivalent selector ordering produces identical plans; changing a skill body changes provenance even when its discovery-metadata estimate stays constant.
+
+## The 8K check is a metadata fixture gate
+
+`estimate.surface` is `skill-discovery-metadata`. Method `utf8-bytes-div-4@1` renders each selected entry as canonical JSON containing `harness`, `type`, `name`, and `description`, adds a newline, divides UTF-8 bytes by four, rounds each entry up, and sums the results. The ledger exposes per-entry costs.
+
+Lean rejects estimates above 8,000 using `CONTEXT_PROFILE_BUDGET_EXCEEDED`; a library caller can inspect the rejected proposal on `error.plan`. Exactly 8,000 passes the estimator check; 8,001 fails. Full uses the same reference budget in report-only mode.
+
+This heuristic is an early rejection and regression fixture, not a tokenizer, measured lower bound, or whole-prompt certification. Passing cannot establish the production Lean startup ceiling. `nativeTokens`, `wrapperTokens`, and `wholeScopeTokens` remain `null` until appropriate observation exists.
+
+The registry explicitly excludes agents, commands, rules, hooks, MCP schemas, harness wrappers, and learned skills. Skill bodies and bundled resources are hashed but excluded from the discovery estimate. Other plugin context, host overhead, repeated prompts, and task execution costs are also unmeasured. Report observed native counters separately and avoid deriving savings claims from this ledger alone.
+
+## Target declarations are not runtime certification
+
+The registry recognizes the current 15 install target IDs plus Pi. For a requested target, `projection.installSupport` reports `declared` or `not-declared` according to the owning module. `projection.nativeSupport` remains `unobserved` in both cases.
+
+Target selection does not silently drop skills lacking an installer declaration. The same explicit skill selection is projected for every recognized target, so consumers can inspect gaps rather than mistake them for successful installation. Native discovery, invocation, resource access, reload behavior, exclusion enforcement, and whole-context cost require adapter-specific evidence in later slices.
+
+## Rationale and alternatives
+
+The read-only boundary makes the selection contract reviewable before it can alter user state. Versioned manifests and source digests provide shared inputs for adapters, grouping work, routing, and measurement. Keeping existing install ownership avoids a second independently maintained inventory.
+
+Alternatives considered:
+
+- Reuse install profile names for runtime scope. Rejected because installed files, visible context, hooks, and permissions are separate controls with existing compatibility obligations.
+- Start by rewriting plugin caches or installed discovery files. Deferred until carrier ownership, fresh-session behavior, receipts, rollback, and user-edit preservation have evidence.
+- Treat a task classifier or system prompt as the enforcement boundary. Rejected. Future agent proposals must be validated against deterministic contracts and retained consent.
+- Infer complete workflow closure from Markdown prose. Rejected as an unreviewed authority source. Explicit declarations are auditable; the current dependency coverage remains incomplete.
+- Declare 8K compliance from a character or byte estimate. Rejected. Metadata fixtures help catch regressions while native host measurements remain a separate gate.
+
+## Contributor integration lanes
+
+These related PRs are integration inputs, not claims that their proposed behavior has shipped. Preserve contributor attribution and verify each change against the shared contract before adoption.
+
+| Contribution | Intended integration | Boundary |
+| --- | --- | --- |
+| [#2788](https://github.com/affaan-m/ECC/pull/2788) | Native discovery carriers and associated ownership/receipt work | Consume this registry and plan; carrier generation and activation belong to later slices |
+| [#2844](https://github.com/affaan-m/ECC/pull/2844) | Catalog grouping, deterministic selection fixtures, and listing projection | Reuse canonical IDs and pack ownership instead of introducing competing profile authority |
+| [#2945](https://github.com/affaan-m/ECC/pull/2945) | Task routing and automatic-selection proposals | Future structured task resolver; `selectionMode: "auto"` alone implements none of this |
+| [#2740](https://github.com/affaan-m/ECC/pull/2740) | Native context counters and bounded diagnostics | Keep observed measurements separate from fixture estimates and scan assumptions |
+| [#3030](https://github.com/affaan-m/ECC/pull/3030) | Contributor skill-quality validation | Content-quality checks complement inventory validation; they do not prove runtime activation or workflow outcomes |
+| [#3032](https://github.com/affaan-m/ECC/pull/3032) | Existing js-yaml dependency security update | Verify contributor integration before release; retain both lockfiles and rerun dependency and regression checks |
+
+The original September 8 dependency baseline pinned js-yaml 4.3.1, affected by [GHSA-2883-xcg3-v3hh](https://github.com/nodeca/js-yaml/security/advisories/GHSA-2883-xcg3-v3hh). PR preparation exposed that existing finding in hosted CI. This branch now includes Myles Agnew's exact 4.3.2 upgrade from #3032 as an attributed prerequisite commit, updating the runtime pin, overrides, resolutions, and both lockfiles. Runtime audit reports zero vulnerabilities after installation. The original contributor PR remains independently reviewable. This registry's `JSON_SCHEMA` excludes the advisory's merge behavior, but upgrading also protects existing default-schema parsers.
+
+## Follow-on gates and verification
+
+P2 now has resource-complete read-only carrier projections and disposable structural acceptance fixtures. Native fresh-session discovery and invocation remain unobserved. P3 adds transactional activation, receipts, ownership, migration, recovery, and rollback. P4 adds structured task selection, agent proposals, and bounded automatic routing. P5 integrates hook plans with explicit, separately retained consent. P6 earns release-default changes through package, operating-system, harness, compatibility, and recovery tests. None of those later stages is implied by a successful preview.
+
+The first-slice checks live in [registry tests](../../tests/lib/context-pack-registry.test.js), [profile tests](../../tests/lib/context-profiles.test.js), [CLI tests](../../tests/scripts/profile.test.js), and the [context-profile validator](../../scripts/ci/validate-context-profiles.js). They cover source and selection validation, deterministic provenance, metadata boundaries, and read-only behavior. Those fixtures do not replace native fresh-session, activation, workflow, or whole-system measurement evidence.
+
+In a source checkout, see the [TDD evidence record](context-profiles.tdd.md) and test files linked above for executed checks, checkpoints, coverage, and known gaps. Test sources and the evidence record are intentionally outside the reduced npm runtime surface.
diff --git a/docs/design/context-profiles.tdd.md b/docs/design/context-profiles.tdd.md
new file mode 100644
index 000000000..01d332afa
--- /dev/null
+++ b/docs/design/context-profiles.tdd.md
@@ -0,0 +1,89 @@
+# ECC-029 read-only context profile evidence
+
+Date: September 8, 2026. Scope: the first P0/P1 implementation slice for M1, canonical context profiles. Baseline: main `5064474d4d762dc9640234a41617cccb79185cec`, ECC 2.2.1. Environment: macOS 26.6.2, Apple M4 Pro, Node 24.9.0. This is local development evidence, not a release or native-host certification.
+
+Source intent: the accepted ECC-029 production and economics planning canvases in the maintainer workspace. Their approved first-slice journeys and boundaries are carried into the portable [implementation contract](context-profiles.md). Planning text was treated as design input; validation used reviewed local test, lint, package, and inspection commands. No activation, remote installer, publication, or credential-handling instruction was adopted. The project detector selected unavailable Bun; the actual test scripts run standalone Node, so Node and npm ran them without changing package-manager preferences.
+
+## Journeys and test specification
+
+| Approved journey and guarantee | Test target | Type | RED evidence | GREEN evidence |
+| --- | --- | --- | --- | --- |
+| Inspect versioned profiles and exact skill IDs without invoking skills or changing caller state | [CLI tests](../../tests/scripts/profile.test.js) | CLI journey/integration | `cd3950d3`: 24 failures for the missing command, entrypoint, and package inclusion | 25 passed, including later terminal-control regression; temporary home and workspace snapshots remain unchanged |
+| Build one portable canonical skill inventory with validated ownership, explicit declarations, and resource digests | [Registry tests](../../tests/lib/context-pack-registry.test.js) | Unit/integration | `4c1b938b`: intended registry module absent | 15 passed, including source safety and repository inventory |
+| Compile deterministic Lean/Full proposals with exact selectors, declared dependency closure, and honest metadata estimates | [Profile tests](../../tests/lib/context-profiles.test.js) | Unit/integration | `4c1b938b`: intended compiler module absent | 12 passed; 8,000 passes and 8,001 blocks the Lean metadata estimator, while native totals remain unknown |
+| Gate every recognized target and register validation in the normal test workflow | [CI tests](../../tests/ci/context-profiles.test.js) | Integration | `5fcd9e08`: 3 failures for missing validation and registration | 3 passed; 2 profiles across 16 target IDs |
+| Reject redirected source reads, unsafe metadata controls, and unstable cache-derived provenance | Registry and profile tests above | Security/regression | `f01d3366`: 23 passed and 3 expected failures during review | Same regressions pass; redirected descriptor receives zero byte reads in the substitution fixture |
+| Keep user-supplied terminal controls inert in CLI error output | CLI tests above | Security/CLI | `254a6cc1`: 24 passed, 1 failed for raw OSC output | 25 passed |
+| Ship the entrypoint, libraries, schemas, manifests, and contract together | [Publish-surface tests](../../tests/scripts/npm-publish-surface.test.js) | Packaging/integration | Existing explicit publish allowlist initially reported 1 pass and 1 failure | Updated expected public surface passes, plus real offline package smoke below |
+
+The module-absence RED runs exercised the intended new public entry points; they were not failures of an unrelated dependency installation. The initial library checkpoint contained 20 cases; boundary and security review grew the focused library suite to 27. All listed checkpoints are local commits on `plan/ecc-029-harness-scoping`, reachable from the GREEN implementation commit. Preserve this record if later integration squashes those checkpoints. No separate refactor stage was performed after final GREEN validation.
+
+## Executed checks
+
+```sh
+node --test tests/lib/context-pack-registry.test.js tests/lib/context-profiles.test.js
+node tests/scripts/profile.test.js
+node tests/ci/context-profiles.test.js
+node tests/scripts/npm-publish-surface.test.js
+npm run context-profiles:check
+npm test
+npm run lint
+git diff --check
+```
+
+Final focused coverage execution also runs the first four feature test targets together:
+
+```sh
+./node_modules/.bin/c8 --all \
+ --include='scripts/lib/context*.js' \
+ --include='scripts/profile.js' \
+ --include='scripts/ci/validate-context-profiles.js' \
+ --reporter=text --reporter=json-summary \
+ --reports-dir=/tmp/ecc-029-context-coverage \
+ --check-coverage --lines=80 --functions=80 --branches=80 --statements=80 \
+ node --test tests/lib/context-pack-registry.test.js \
+ tests/lib/context-profiles.test.js tests/scripts/profile.test.js \
+ tests/ci/context-profiles.test.js
+```
+
+Results: 27 library cases, 25 CLI cases, and 3 CI cases passed. Node's outer TAP summary reports 29 because the CLI and CI files each wrap their own cases. New-code coverage is 98.43% statements and lines, 90% branches, and 100% functions. Coverage thresholds all pass; no focused cases were skipped. Uncovered lines include a defensive source-error path and the single-profile text rendering branch.
+
+The complete `npm test` command exited 0 and its legacy aggregate reported `Total Tests: 4423`, `Passed: 4423`, `Failed: 0`. Its aggregate does not separately count the new node:test library cases, which have their explicit result above. Existing platform-dependent tests can skip on macOS; this run supplies no Windows or Linux execution evidence. Full ESLint/Markdown lint, catalog/command validators, and whitespace checks passed.
+
+## Packed offline user journey
+
+Ran `npm pack` with the real prepack build into a disposable directory, followed by `npm install --offline --ignore-scripts --omit=dev --no-audit --no-fund --userconfig=/dev/null` into a disposable consumer. The install succeeded using cached dependencies. No package was published or globally installed.
+
+The packaged dispatcher produced Lean and Full Codex previews, and the packaged direct entrypoint explained an exact skill ID. Both full proposed-plan objects were deeply equal to their checkout counterparts, including registry, profile, compiler, and plan digests. The subprocess environment used an explicit allowlist and a disposable user-home path, which remained absent after all three calls. This checks the real archive and runtime dependencies independently of the checkout's module resolution.
+
+At this baseline, Codex Lean selects 3 entries and leaves 283 routed; Full selects all 286. The descriptor estimator reports 221 tokens from 879 bytes for Lean and 26,145 tokens from 104,168 bytes for Full. These are reproducible fixture estimates, not observed native startup tokens or demonstrated task savings.
+
+## Review findings and remaining gates
+
+Independent review reproduced ancestor substitution and terminal-control issues before fixes, then rechecked the fixes and approved the read-only boundary. Source identity checks do not create an atomic filesystem snapshot. The initial checkpoint lacked an independent directory listing bound; the hosted-review follow-up below closes that gap. Dependency coverage remains explicit-declarations-only and unreviewed. Required-resource annotations need a distinct output contract before selective P2 carriers can safely omit resources.
+
+The js-yaml integration prerequisite from contributor [PR #3032](https://github.com/affaan-m/ECC/pull/3032) is satisfied on this branch by the attributed 4.3.2 upgrade, fresh install, zero-vulnerability runtime audit and packed-consumer verification described below. Its original PR remains open; final hosted CI and release qualification are separate gates. See the [contract's dependency gate](context-profiles.md#contributor-integration-lanes).
+
+Native carriers, active discovery, actual skill invocation, transactional activation, hook consent, automatic task routing, recovery, real-host token counters, broader context surfaces, cross-platform conformance, and default migration remain follow-on work. No provider calls, container or VM launches, or runtime profile changes were used to establish these results.
+
+## PR-readiness follow-up
+
+Independent exact-head review approved the read-only implementation and identified privilege-sensitive symlink fixtures. Review's original permission-denial injection produced 12 passes and 3 failures. Checkpoint `88f5a996` added a failing portable directory-link contract: 15 passes and 1 expected failure. The fix uses Windows junctions for directory cases, separates unconditional ownership and mocked leaf-link rejection from the real file-link integration case, and explicitly skips only that extra file-link case on Windows EPERM/EACCES. No runtime code changed.
+
+Final local focused checks now pass 30 library, 25 CLI, and 3 CI cases. A bounded simulation of Windows file-link denial, keeping the local temporary directory fixed and emulating directory junctions, passes 17 registry cases and explicitly skips 1 real file-link case. It is a test-policy simulation, not native Windows evidence. The source-read substitution and zero-byte-read assertions remain mandatory.
+
+An isolated Git archive passed `YARN_ENABLE_HARDENED_MODE=1 YARN_ENABLE_SCRIPTS=false yarn install --immutable --mode=skip-build`; both package manifest and Yarn lockfile remained byte-identical. The initially attempted immutable/update-lockfile combination was rejected by Yarn as incompatible before installation; the immutable skip-build run is the applicable successful CI check. Dependency declarations remain unchanged. Source-only evidence/test links in the shipped contract are now labeled explicitly.
+
+### Contributor security prerequisite
+
+Hosted CI for PR #3037 at `78cbd01c` reproduced the existing js-yaml high-severity advisory in its runtime audit. The branch incorporated contributor Myles Agnew's exact commit `5674661fc30ab1d3f3fcae22d72bfb4ab3059822` from #3032 using an attributed cherry-pick (`77872972`). No contributor PR was merged or closed. A fresh dependency install resolved js-yaml 4.3.2, and `npm audit --omit=dev --audit-level=high` reports zero vulnerabilities.
+
+The local npm 11 install unexpectedly rewrote the Yarn lock into its legacy format. Only that task-induced rewrite was restored to the committed contributor bytes before subsequent validation. This is installation-tool behavior, not an intended lockfile change. The full test run started on the preceding revision overlapped the dependency update and is excluded from exact-final-head evidence; final PR checks must bind to the updated head.
+
+### Hosted review regressions
+
+The global dry-run parser regression was reproduced before implementation in `c373b7fe`: 27 CLI cases passed and 4 failed. Fix `9b5e3934` removes exact global `--dry-run` flags before command/value parsing, without mutating caller arguments or weakening other validation. All 31 CLI cases and seven independent parser probes pass. Both public entrypoints retain unobserved activation.
+
+Checkpoint `ea00894d` adds seven source-reader regressions for incremental enumeration, the exact per-directory boundary, empty-directory breadth, excluded cache names, handle cleanup and directory identity changes. The corrected reader accepts at most 10,000 names per directory and charges every directory open and enumerated entry against a 20,000-operation reader budget, allowing one lookahead to detect overflow. It retains the file, cumulative-byte and depth bounds. Focused support/registry/compiler checks pass 37/37, including the mandatory ancestor-substitution test with zero redirected file-byte reads.
+
+The source reader was split into focused helpers below 50 lines. Directory handles close in `finally`, and identities are revalidated before and after enumeration. Independent review checked that descriptor no-follow flags, identity checks before the first file byte, post-read checks and exact byte digests survive the extraction. This remains a bounded consistency check, not an atomic filesystem snapshot.
diff --git a/docs/es/README.md b/docs/es/README.md
index 242adb358..1809c17cc 100644
--- a/docs/es/README.md
+++ b/docs/es/README.md
@@ -1,4 +1,4 @@
-**Idioma:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | **Español** | [Українська](../uk-UA/README.md)
+**Idioma:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | **Español** | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
# ECC
@@ -28,7 +28,7 @@
**Language / 语言 / 語言 / Dil / Язык / Ngôn ngữ / Idioma**
[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md)
- | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | **Español** | [Українська](../uk-UA/README.md)
+ | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | **Español** | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
diff --git a/docs/es/SPONSORS.md b/docs/es/SPONSORS.md
index 54a9e0acc..51f2f4bbd 100644
--- a/docs/es/SPONSORS.md
+++ b/docs/es/SPONSORS.md
@@ -18,10 +18,15 @@ Gracias a todos los que financian el trabajo de código abierto de ECC. Tu patro
| Patrocinador | Desde |
|---------|-------|
-| [Mike Morgan](https://github.com/mikejmorgan-ai) | 2026 |
*[Conviértete en patrocinador Team](https://github.com/sponsors/affaan-m) para obtener un logo pequeño y 5 asientos de ECC Pro.*
+## Patrocinadores anteriores
+
+| Patrocinador | Período activo |
+|---------|---------------|
+| [Mike Morgan](https://github.com/mikejmorgan-ai) (inactivo) | 2026 |
+
## Patrocinadores Pro — $50/mes
*[Conviértete en patrocinador Pro](https://github.com/sponsors/affaan-m) para aparecer aquí con tu nombre en la fila de patrocinadores del README principal.*
diff --git a/docs/es/agents/chief-of-staff.md b/docs/es/agents/chief-of-staff.md
index 6963978c2..1e12ac3e4 100644
--- a/docs/es/agents/chief-of-staff.md
+++ b/docs/es/agents/chief-of-staff.md
@@ -2,7 +2,7 @@
name: chief-of-staff
description: Jefe de comunicaciones personal que gestiona el correo electrónico, Slack, LINE y Messenger. Clasifica mensajes en 4 niveles (skip/info_only/meeting_info/action_required), genera borradores de respuesta y refuerza el seguimiento post-envío mediante hooks. Usar para gestionar flujos de trabajo de comunicación multi-canal.
tools: ["Read", "Grep", "Glob", "Bash", "Edit", "Write"]
-model: opus
+model: sonnet
---
## Línea de Base de Defensa de Prompts
diff --git a/docs/es/agents/database-reviewer.md b/docs/es/agents/database-reviewer.md
index 808b4d706..02ec9bce7 100644
--- a/docs/es/agents/database-reviewer.md
+++ b/docs/es/agents/database-reviewer.md
@@ -1,7 +1,7 @@
---
name: database-reviewer
description: Especialista en bases de datos PostgreSQL para optimización de consultas, diseño de esquemas, seguridad y rendimiento. Usar PROACTIVAMENTE al escribir SQL, crear migraciones, diseñar esquemas o solucionar problemas de rendimiento de base de datos. Incorpora mejores prácticas de Supabase.
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
+tools: ["Read", "Grep", "Glob", "Bash"]
model: sonnet
---
diff --git a/docs/es/agents/docs-lookup.md b/docs/es/agents/docs-lookup.md
index 7a8625278..8625de573 100644
--- a/docs/es/agents/docs-lookup.md
+++ b/docs/es/agents/docs-lookup.md
@@ -2,7 +2,7 @@
name: docs-lookup
description: Cuando el usuario pregunta cómo usar una biblioteca, framework o API, o necesita ejemplos de código actualizados, usar Context7 MCP para obtener documentación actual y devolver respuestas con ejemplos. Invocar para preguntas sobre docs/API/configuración.
tools: ["Read", "Grep", "mcp__context7__resolve-library-id", "mcp__context7__query-docs"]
-model: sonnet
+model: haiku
---
## Línea de Base de Defensa de Prompts
diff --git a/docs/es/agents/security-reviewer.md b/docs/es/agents/security-reviewer.md
index 13a893a8d..cb99eb30d 100644
--- a/docs/es/agents/security-reviewer.md
+++ b/docs/es/agents/security-reviewer.md
@@ -1,7 +1,7 @@
---
name: security-reviewer
description: Especialista en detección y remediación de vulnerabilidades de seguridad. Usar PROACTIVAMENTE después de escribir código que maneja entrada de usuarios, autenticación, endpoints de API o datos sensibles. Detecta secretos, SSRF, inyección, criptografía insegura y vulnerabilidades del OWASP Top 10.
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
+tools: ["Read", "Grep", "Glob", "Bash"]
model: sonnet
---
diff --git a/docs/ja-JP/README.md b/docs/ja-JP/README.md
index ec59cf67a..9596adb8d 100644
--- a/docs/ja-JP/README.md
+++ b/docs/ja-JP/README.md
@@ -31,7 +31,8 @@
ไทย |
Deutsch |
Español |
- Українська
+ Українська |
+ Polski
@@ -136,13 +137,13 @@ plan -> test -> implement -> review -> verify -> remember -> improve
ECC は MIT ライセンスのオープンソースです。現時点では Claude Code で最もよく機能し、サポート対象の Codex 同期パスを備え、Cursor、OpenCode、Gemini、Zed、GitHub Copilot、Antigravity、Qwen、その他のハーネス向けには機能が限定されたアダプターを提供しています。機能の同等性を前提にする前に、[サポート状況マトリクス](#プラットフォームサポート)を確認してください。
-68 の agents、292 の skills、95 のレガシー command シムに加えて、hooks、rules、メモリ、継続的学習、AgentShield セキュリティスキャンを利用できます。agents は計画、レビュー、ビルド修復、セキュリティ、アーキテクチャ、ドメイン作業に特化しています。
+68 の agents、293 の skills、94 のレガシー command シムに加えて、hooks、rules、メモリ、継続的学習、AgentShield セキュリティスキャンを利用できます。agents は計画、レビュー、ビルド修復、セキュリティ、アーキテクチャ、ドメイン作業に特化しています。
| 含まれるもの | 数 | 得られるもの |
| ---------------- | ----------: | ------------------------------------------------------------------------------------ |
| Agents | 68 agents | 計画、レビュー、ビルド修復、セキュリティ、アーキテクチャ、ドメイン作業 |
-| Skills | 292 skills | TDD、リサーチ、セキュリティ、ドキュメント、フロントエンド、データ、ML、運用など |
-| Commands | 95 commands | ECC が skills ファーストの構成へ移行する間の便利なエントリーポイント |
+| Skills | 293 skills | TDD、リサーチ、セキュリティ、ドキュメント、フロントエンド、データ、ML、運用など |
+| Commands | 94 commands | ECC が skills ファーストの構成へ移行する間の便利なエントリーポイント |
| Hooks とメモリ | ランタイム | 強制、セッションサマリー、継続的学習、instincts、コンテキスト制御 |
| Rules | 選択式 | 言語やプロジェクトごとに選ぶ、常時ロードされる標準 |
| AgentShield | 同梱 | プロンプト、hooks、MCP 設定、パーミッション、シークレット、agent ファイルのスキャン |
@@ -376,7 +377,7 @@ cd ECC
| Qwen CLI | `./install.sh --profile minimal --target qwen` | [Qwen ガイド](../QWEN-GUIDE.md)を参照 |
| Hermes | `./install.sh --profile minimal --target hermes` | [Hermes セットアップガイド](../HERMES-SETUP.md)を参照 |
| OpenClaw | `./install.sh --profile minimal --target openclaw` | 管理されたホームディレクトリインストール |
-| Kimi Code CLI | `./install.sh --profile minimal --target kimi` | プロジェクトローカルの `.kimi-code/` インストール · [Kimi Code を入手](https://www.kimi.com/code?aff=ecc) |
+| Kimi Code CLI | `./install.sh --profile minimal --target kimi` | プロジェクトローカルの `.kimi-code/` インストール · [Kimi Code を入手](https://www.kimi.ai/code?aff=ecc) |
| CodeBuddy | `./install.sh --profile minimal --target codebuddy` | プロジェクトローカルの `.codebuddy/` インストール |
| JoyCode | `./install.sh --profile minimal --target joycode` | プロジェクトローカルの `.joycode/` インストール |
@@ -794,7 +795,7 @@ Kimi Code はインストールされた `.kimi-code/AGENTS.md` の指示と `.k
```text
ECC/
|-- agents/ # 委譲用の 68 の専門サブエージェント
-|-- skills/ # オンデマンドで読み込まれる 292 の再利用可能なワークフロー
+|-- skills/ # オンデマンドで読み込まれる 293 の再利用可能なワークフロー
|-- commands/ # メンテナンスされている 94 のスラッシュコマンドシム
|-- rules/ # オプトインの共通標準と言語別標準
|-- hooks/ # ランタイムの自動化と強制
diff --git a/docs/ja-JP/agents/build-error-resolver.md b/docs/ja-JP/agents/build-error-resolver.md
index 6362ac24a..5eda1ba50 100644
--- a/docs/ja-JP/agents/build-error-resolver.md
+++ b/docs/ja-JP/agents/build-error-resolver.md
@@ -2,7 +2,7 @@
name: build-error-resolver
description: ビルドおよびTypeScriptエラー解決のスペシャリスト。ビルドが失敗した際やタイプエラーが発生した際に積極的に使用してください。最小限の差分でビルド/タイプエラーのみを修正し、アーキテクチャの変更は行いません。ビルドを迅速に成功させることに焦点を当てます。
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: sonnet
---
# ビルドエラーリゾルバー
diff --git a/docs/ja-JP/agents/chief-of-staff.md b/docs/ja-JP/agents/chief-of-staff.md
index d62cc0be0..13ad1f21c 100644
--- a/docs/ja-JP/agents/chief-of-staff.md
+++ b/docs/ja-JP/agents/chief-of-staff.md
@@ -2,7 +2,7 @@
name: chief-of-staff
description: メール、Slack、LINE、Messengerをトリアージするパーソナルコミュニケーションチーフオブスタッフ。メッセージを4つのティア(skip/info_only/meeting_info/action_required)に分類し、返信ドラフトを生成し、送信後のフォロースルーをフックで強制します。マルチチャネルコミュニケーションワークフローの管理時に使用します。
tools: ["Read", "Grep", "Glob", "Bash", "Edit", "Write"]
-model: opus
+model: sonnet
---
## プロンプト防御ベースライン
diff --git a/docs/ja-JP/agents/code-reviewer.md b/docs/ja-JP/agents/code-reviewer.md
index b5c5c5d72..bf26d6a5a 100644
--- a/docs/ja-JP/agents/code-reviewer.md
+++ b/docs/ja-JP/agents/code-reviewer.md
@@ -2,7 +2,7 @@
name: code-reviewer
description: 専門コードレビュースペシャリスト。品質、セキュリティ、保守性のためにコードを積極的にレビューします。コードの記述または変更直後に使用してください。すべてのコード変更に対して必須です。
tools: ["Read", "Grep", "Glob", "Bash"]
-model: opus
+model: sonnet
---
あなたはコード品質とセキュリティの高い基準を確保するシニアコードレビュアーです。
diff --git a/docs/ja-JP/agents/comment-analyzer.md b/docs/ja-JP/agents/comment-analyzer.md
index 1db18900e..63255e383 100644
--- a/docs/ja-JP/agents/comment-analyzer.md
+++ b/docs/ja-JP/agents/comment-analyzer.md
@@ -1,7 +1,7 @@
---
name: comment-analyzer
description: コードコメントの正確性、完全性、保守性、コメント劣化リスクを分析します。
-model: sonnet
+model: haiku
tools: [Read, Grep, Glob]
---
diff --git a/docs/ja-JP/agents/conversation-analyzer.md b/docs/ja-JP/agents/conversation-analyzer.md
index bc8ddb8e0..26e31f7ca 100644
--- a/docs/ja-JP/agents/conversation-analyzer.md
+++ b/docs/ja-JP/agents/conversation-analyzer.md
@@ -1,7 +1,7 @@
---
name: conversation-analyzer
description: 会話のトランスクリプトを分析し、フックで防止すべき動作を見つけるためにこのエージェントを使用します。引数なしの/hookifyでトリガーされます。
-model: sonnet
+model: haiku
tools: [Read, Grep]
---
diff --git a/docs/ja-JP/agents/database-reviewer.md b/docs/ja-JP/agents/database-reviewer.md
index 30d814b82..f76e50e95 100644
--- a/docs/ja-JP/agents/database-reviewer.md
+++ b/docs/ja-JP/agents/database-reviewer.md
@@ -1,8 +1,8 @@
---
name: database-reviewer
description: クエリ最適化、スキーマ設計、セキュリティ、パフォーマンスのためのPostgreSQLデータベーススペシャリスト。SQL作成、マイグレーション作成、スキーマ設計、データベースパフォーマンスのトラブルシューティング時に積極的に使用してください。Supabaseのベストプラクティスを組み込んでいます。
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+tools: ["Read", "Grep", "Glob", "Bash"]
+model: sonnet
---
# データベースレビューアー
diff --git a/docs/ja-JP/agents/doc-updater.md b/docs/ja-JP/agents/doc-updater.md
index c54876458..adf807954 100644
--- a/docs/ja-JP/agents/doc-updater.md
+++ b/docs/ja-JP/agents/doc-updater.md
@@ -2,7 +2,7 @@
name: doc-updater
description: ドキュメントとコードマップのスペシャリスト。コードマップとドキュメントの更新に積極的に使用してください。/update-codemapsと/update-docsを実行し、docs/CODEMAPS/*を生成し、READMEとガイドを更新します。
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: haiku
---
# ドキュメント & コードマップスペシャリスト
diff --git a/docs/ja-JP/agents/docs-lookup.md b/docs/ja-JP/agents/docs-lookup.md
index e18c0e55a..721baf95c 100644
--- a/docs/ja-JP/agents/docs-lookup.md
+++ b/docs/ja-JP/agents/docs-lookup.md
@@ -2,7 +2,7 @@
name: docs-lookup
description: ユーザーがライブラリ、フレームワーク、APIの使い方を質問したり、最新のコード例が必要な場合に、Context7 MCPを使用して最新のドキュメントを取得し、例付きの回答を返します。ドキュメント/API/セットアップの質問時に呼び出します。
tools: ["Read", "Grep", "mcp__context7__resolve-library-id", "mcp__context7__query-docs"]
-model: sonnet
+model: haiku
---
## プロンプト防御ベースライン
diff --git a/docs/ja-JP/agents/e2e-runner.md b/docs/ja-JP/agents/e2e-runner.md
index e6eb35f87..03cc2890c 100644
--- a/docs/ja-JP/agents/e2e-runner.md
+++ b/docs/ja-JP/agents/e2e-runner.md
@@ -2,7 +2,7 @@
name: e2e-runner
description: Vercel Agent Browser(推奨)とPlaywrightフォールバックを使用するエンドツーエンドテストスペシャリスト。E2Eテストの生成、メンテナンス、実行に積極的に使用してください。テストジャーニーの管理、不安定なテストの隔離、アーティファクト(スクリーンショット、ビデオ、トレース)のアップロード、重要なユーザーフローの動作確認を行います。
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: sonnet
---
# E2Eテストランナー
diff --git a/docs/ja-JP/agents/gan-evaluator.md b/docs/ja-JP/agents/gan-evaluator.md
index 8e2e1b8b6..7dd200268 100644
--- a/docs/ja-JP/agents/gan-evaluator.md
+++ b/docs/ja-JP/agents/gan-evaluator.md
@@ -1,8 +1,8 @@
---
name: gan-evaluator
description: "GANハーネス — エバリュエーターエージェント。Playwrightを使用してライブ実行中のアプリケーションをテストし、ルーブリックに対してスコアリングし、ジェネレーターに実行可能なフィードバックを提供します。"
-tools: ["Read", "Write", "Bash", "Grep", "Glob"]
-model: opus
+tools: ["Read", "Write", "Bash", "Grep", "Glob", "mcp__playwright__browser_navigate", "mcp__playwright__browser_click", "mcp__playwright__browser_take_screenshot", "mcp__playwright__browser_snapshot", "mcp__playwright__browser_type", "mcp__playwright__browser_fill_form", "mcp__playwright__browser_resize", "mcp__playwright__browser_press_key"]
+model: sonnet
color: red
---
diff --git a/docs/ja-JP/agents/gan-generator.md b/docs/ja-JP/agents/gan-generator.md
index f31d4c02c..9a3862608 100644
--- a/docs/ja-JP/agents/gan-generator.md
+++ b/docs/ja-JP/agents/gan-generator.md
@@ -2,7 +2,7 @@
name: gan-generator
description: "GANハーネス — ジェネレーターエージェント。仕様に従って機能を実装し、エバリュエーターのフィードバックを読み、品質閾値を満たすまでイテレーションします。"
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: sonnet
color: green
---
diff --git a/docs/ja-JP/agents/gan-planner.md b/docs/ja-JP/agents/gan-planner.md
index 084bb60b4..acf2752d2 100644
--- a/docs/ja-JP/agents/gan-planner.md
+++ b/docs/ja-JP/agents/gan-planner.md
@@ -2,7 +2,7 @@
name: gan-planner
description: "GANハーネス — プランナーエージェント。1行のプロンプトを、機能、スプリント、評価基準、デザイン方向を含む完全な製品仕様に展開します。"
tools: ["Read", "Write", "Grep", "Glob"]
-model: opus
+model: sonnet
color: purple
---
diff --git a/docs/ja-JP/agents/go-build-resolver.md b/docs/ja-JP/agents/go-build-resolver.md
index 4f360fce3..64f2f7df5 100644
--- a/docs/ja-JP/agents/go-build-resolver.md
+++ b/docs/ja-JP/agents/go-build-resolver.md
@@ -2,7 +2,7 @@
name: go-build-resolver
description: Goビルド、vet、コンパイルエラー解決スペシャリスト。最小限の変更でビルドエラー、go vet問題、リンターの警告を修正します。Goビルドが失敗したときに使用してください。
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: sonnet
---
# Goビルドエラーリゾルバー
diff --git a/docs/ja-JP/agents/go-reviewer.md b/docs/ja-JP/agents/go-reviewer.md
index abab6fe57..0dd66d876 100644
--- a/docs/ja-JP/agents/go-reviewer.md
+++ b/docs/ja-JP/agents/go-reviewer.md
@@ -4,7 +4,7 @@ description: 慣用的なGo、並行処理パターン、エラー処理、パ
コード変更に使用してください。Goプロジェクトに必須です。
tools: ["Read", "Grep", "Glob", "Bash"]
-model: opus
+model: sonnet
---
あなたは慣用的なGoとベストプラクティスの高い基準を確保するシニアGoコードレビュアーです。
diff --git a/docs/ja-JP/agents/opensource-forker.md b/docs/ja-JP/agents/opensource-forker.md
index c1c21dd9a..30a81d7ae 100644
--- a/docs/ja-JP/agents/opensource-forker.md
+++ b/docs/ja-JP/agents/opensource-forker.md
@@ -2,7 +2,7 @@
name: opensource-forker
description: あらゆるプロジェクトをオープンソース化のためにフォークします。ファイルのコピー、シークレットと認証情報の除去(20以上のパターン)、内部参照のプレースホルダー置換、.env.exampleの生成、git履歴のクリーンアップを行います。opensource-pipelineスキルの第1ステージです。
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: sonnet
+model: haiku
---
## プロンプト防御ベースライン
diff --git a/docs/ja-JP/agents/opensource-packager.md b/docs/ja-JP/agents/opensource-packager.md
index 6916ed6c8..916147b04 100644
--- a/docs/ja-JP/agents/opensource-packager.md
+++ b/docs/ja-JP/agents/opensource-packager.md
@@ -2,7 +2,7 @@
name: opensource-packager
description: サニタイズ済みプロジェクトの完全なオープンソースパッケージングを生成します。CLAUDE.md、setup.sh、README.md、LICENSE、CONTRIBUTING.md、GitHubイシューテンプレートを作成します。あらゆるリポジトリをClaude Codeですぐに使えるようにします。opensource-pipelineスキルの第3ステージです。
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: sonnet
+model: haiku
---
## プロンプト防御ベースライン
diff --git a/docs/ja-JP/agents/python-reviewer.md b/docs/ja-JP/agents/python-reviewer.md
index 06059fea3..a8ffe16db 100644
--- a/docs/ja-JP/agents/python-reviewer.md
+++ b/docs/ja-JP/agents/python-reviewer.md
@@ -2,7 +2,7 @@
name: python-reviewer
description: PEP 8準拠、Pythonイディオム、型ヒント、セキュリティ、パフォーマンスを専門とする専門Pythonコードレビュアー。すべてのPythonコード変更に使用してください。Pythonプロジェクトに必須です。
tools: ["Read", "Grep", "Glob", "Bash"]
-model: opus
+model: sonnet
---
あなたはPythonicコードとベストプラクティスの高い基準を確保するシニアPythonコードレビュアーです。
diff --git a/docs/ja-JP/agents/refactor-cleaner.md b/docs/ja-JP/agents/refactor-cleaner.md
index e378ba949..a6757490d 100644
--- a/docs/ja-JP/agents/refactor-cleaner.md
+++ b/docs/ja-JP/agents/refactor-cleaner.md
@@ -2,7 +2,7 @@
name: refactor-cleaner
description: デッドコードクリーンアップと統合スペシャリスト。未使用コード、重複の削除、リファクタリングに積極的に使用してください。分析ツール(knip、depcheck、ts-prune)を実行してデッドコードを特定し、安全に削除します。
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: sonnet
---
# リファクタ&デッドコードクリーナー
diff --git a/docs/ja-JP/agents/security-reviewer.md b/docs/ja-JP/agents/security-reviewer.md
index a9367460d..d2285a3e3 100644
--- a/docs/ja-JP/agents/security-reviewer.md
+++ b/docs/ja-JP/agents/security-reviewer.md
@@ -1,8 +1,8 @@
---
name: security-reviewer
description: セキュリティ脆弱性検出および修復のスペシャリスト。ユーザー入力、認証、APIエンドポイント、機密データを扱うコードを書いた後に積極的に使用してください。シークレット、SSRF、インジェクション、安全でない暗号、OWASP Top 10の脆弱性を検出します。
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+tools: ["Read", "Grep", "Glob", "Bash"]
+model: sonnet
---
# セキュリティレビューアー
diff --git a/docs/ja-JP/agents/tdd-guide.md b/docs/ja-JP/agents/tdd-guide.md
index 7726ce367..79bac72df 100644
--- a/docs/ja-JP/agents/tdd-guide.md
+++ b/docs/ja-JP/agents/tdd-guide.md
@@ -2,7 +2,7 @@
name: tdd-guide
description: テスト駆動開発スペシャリストで、テストファースト方法論を強制します。新しい機能の記述、バグの修正、コードのリファクタリング時に積極的に使用してください。80%以上のテストカバレッジを確保します。
tools: ["Read", "Write", "Edit", "Bash", "Grep"]
-model: opus
+model: sonnet
---
あなたはテスト駆動開発(TDD)スペシャリストで、すべてのコードがテストファーストの方法論で包括的なカバレッジをもって開発されることを確保します。
diff --git a/docs/ja-JP/skills/scientific-db-pubmed-database/SKILL.md b/docs/ja-JP/skills/scientific-db-pubmed-database/SKILL.md
index 22ba0f37f..2e0394375 100644
--- a/docs/ja-JP/skills/scientific-db-pubmed-database/SKILL.md
+++ b/docs/ja-JP/skills/scientific-db-pubmed-database/SKILL.md
@@ -1,5 +1,5 @@
---
-name: pubmed-database
+name: scientific-db-pubmed-database
description: 生物医学文献、MeSH クエリ、PMID 検索、引用取得、および API を利用した文献モニタリングのための PubMed および NCBI E-utilities の直接検索ワークフロー。
origin: community
---
diff --git a/docs/ja-JP/skills/scientific-db-uspto-database/SKILL.md b/docs/ja-JP/skills/scientific-db-uspto-database/SKILL.md
index 67783cc01..2826a3332 100644
--- a/docs/ja-JP/skills/scientific-db-uspto-database/SKILL.md
+++ b/docs/ja-JP/skills/scientific-db-uspto-database/SKILL.md
@@ -1,5 +1,5 @@
---
-name: uspto-database
+name: scientific-db-uspto-database
description: 公式記録の検索、PatentSearch クエリ、TSDR チェック、譲渡データ、および再現可能な IP 調査ログのための USPTO 特許・商標データワークフロー。
origin: community
---
diff --git a/docs/ja-JP/skills/scientific-pkg-gget/SKILL.md b/docs/ja-JP/skills/scientific-pkg-gget/SKILL.md
index b8edca572..bae76ad58 100644
--- a/docs/ja-JP/skills/scientific-pkg-gget/SKILL.md
+++ b/docs/ja-JP/skills/scientific-pkg-gget/SKILL.md
@@ -1,5 +1,5 @@
---
-name: gget
+name: scientific-pkg-gget
description: ゲノムデータベースへのクイック検索、配列検索、BLAST スタイルの検索、エンリッチメントチェック、および再現可能なバイオインフォマティクス証拠ログのための gget CLI および Python ワークフロー。
origin: community
---
diff --git a/docs/ja-JP/skills/scientific-thinking-literature-review/SKILL.md b/docs/ja-JP/skills/scientific-thinking-literature-review/SKILL.md
index c5c065f63..d5b997b0b 100644
--- a/docs/ja-JP/skills/scientific-thinking-literature-review/SKILL.md
+++ b/docs/ja-JP/skills/scientific-thinking-literature-review/SKILL.md
@@ -1,5 +1,5 @@
---
-name: literature-review
+name: scientific-thinking-literature-review
description: 学術、生物医学、技術、科学的なトピックに対するシステマティックな文献レビューワークフロー。検索計画、ソースのスクリーニング、統合、引用確認、証拠ログを含む。
origin: community
---
diff --git a/docs/ja-JP/skills/scientific-thinking-scholar-evaluation/SKILL.md b/docs/ja-JP/skills/scientific-thinking-scholar-evaluation/SKILL.md
index 9533fd205..28dd79b6f 100644
--- a/docs/ja-JP/skills/scientific-thinking-scholar-evaluation/SKILL.md
+++ b/docs/ja-JP/skills/scientific-thinking-scholar-evaluation/SKILL.md
@@ -1,5 +1,5 @@
---
-name: scholar-evaluation
+name: scientific-thinking-scholar-evaluation
description: 論文、提案書、文献レビュー、方法論セクション、証拠の質、引用サポート、研究論文フィードバックのための構造化された学術的作業評価。
origin: community
---
diff --git a/docs/ko-KR/README.md b/docs/ko-KR/README.md
index 9adc19ea1..3d20673fc 100644
--- a/docs/ko-KR/README.md
+++ b/docs/ko-KR/README.md
@@ -1,4 +1,4 @@
-**언어:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | 한국어 | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+**언어:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | 한국어 | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
# Everything Claude Code
@@ -24,7 +24,7 @@
**Language / 语言 / 語言 / 언어 / Dil / Язык / Ngôn ngữ**
-[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
diff --git a/docs/ko-KR/agents/database-reviewer.md b/docs/ko-KR/agents/database-reviewer.md
index a5023cbd1..bac391711 100644
--- a/docs/ko-KR/agents/database-reviewer.md
+++ b/docs/ko-KR/agents/database-reviewer.md
@@ -1,7 +1,7 @@
---
name: database-reviewer
description: PostgreSQL 데이터베이스 전문가. 쿼리 최적화, 스키마 설계, 보안, 성능을 다룹니다. SQL 작성, 마이그레이션 생성, 스키마 설계, 데이터베이스 성능 트러블슈팅 시 사용하세요. Supabase 모범 사례를 포함합니다.
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
+tools: ["Read", "Grep", "Glob", "Bash"]
model: sonnet
---
diff --git a/docs/ko-KR/agents/security-reviewer.md b/docs/ko-KR/agents/security-reviewer.md
index 49dcff92e..5370ae126 100644
--- a/docs/ko-KR/agents/security-reviewer.md
+++ b/docs/ko-KR/agents/security-reviewer.md
@@ -1,7 +1,7 @@
---
name: security-reviewer
description: 보안 취약점 감지 및 수정 전문가. 사용자 입력 처리, 인증, API 엔드포인트, 민감한 데이터를 다루는 코드 작성 후 사용하세요. 시크릿, SSRF, 인젝션, 안전하지 않은 암호화, OWASP Top 10 취약점을 플래그합니다.
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
+tools: ["Read", "Grep", "Glob", "Bash"]
model: sonnet
---
diff --git a/docs/pl/GLOSSARY.md b/docs/pl/GLOSSARY.md
new file mode 100644
index 000000000..8fe10c585
--- /dev/null
+++ b/docs/pl/GLOSSARY.md
@@ -0,0 +1,65 @@
+# Glosariusz / Glossary
+
+Ujednolicona terminologia polskiego tłumaczenia ECC.
+
+Nazwy powierzchni ECC oraz powszechne terminy techniczne pozostają po angielsku, gdy ich
+spolszczenie utrudniałoby powiązanie tekstu z nazwą katalogu, poleceniem lub interfejsem.
+W zwykłym opisie można użyć polskiego odpowiednika wskazanego poniżej.
+
+| English | Polski | Uwagi |
+|---|---|---|
+| Agent | Agent | nazwa powierzchni `agents/`; liczba mnoga: Agenty |
+| Skill | Skill | nazwa powierzchni `skills/`; liczba mnoga: Skille |
+| Hook | Hook | nazwa powierzchni `hooks/`; liczba mnoga: Hooki |
+| Command | Command | nazwa powierzchni `commands/`; ogólnie: „polecenie” |
+| Rule | Rule | nazwa powierzchni `rules/`; ogólnie: „reguła” |
+| Harness | środowisko agenta | „Harness” dopuszczalne w kontekście nazwy technicznej |
+| Instinct | Instinct | termin funkcji Continuous Learning |
+| Plugin | plugin | |
+| Marketplace | marketplace | nazwa powierzchni produktu |
+| Worktree | worktree | termin Git |
+| Subagent | subagent | |
+| Frontmatter | frontmatter | nazwy pól YAML pozostają po angielsku |
+| Continuous Learning | Continuous Learning | nazwa funkcji; opisowo: „ciągłe uczenie” |
+| Memory | pamięć | jako nazwa funkcji może pozostać po angielsku |
+| Context window | okno kontekstu | |
+| Token | token | |
+| Coverage | pokrycie testami | |
+| Test-Driven Development | programowanie sterowane testami | zachowaj skrót TDD |
+| Code review | przegląd kodu | |
+| Refactoring | refaktoryzacja | |
+| Pull request | pull request | zachowaj skrót PR |
+| Commit | commit | |
+| Branch | gałąź | |
+| Merge | scalenie | jako czasownik: „scalić” |
+| Build | build | opisowo: „kompilacja” lub „artefakt” zależnie od kontekstu |
+| Deploy | wdrożenie | |
+| Pipeline | pipeline | |
+| Orchestration | orkiestracja | |
+| Repository | repozytorium | skrót: repo |
+| Dependency | zależność | |
+| Edge case | przypadek brzegowy | |
+| Best practice | dobra praktyka | |
+| Anti-pattern | antywzorzec | |
+| Middleware | middleware | |
+| Endpoint | endpoint | |
+| Schema | schemat | |
+| Payload | payload | opisowo: „dane żądania” |
+| Callback | callback | |
+| Checkpoint | punkt kontrolny | |
+| Linter | linter | |
+| Formatter | formatter | |
+| Staging | środowisko testowe | zależnie od kontekstu także „staging” |
+| Production | produkcja | „środowisko produkcyjne” |
+| Debugging | debugowanie | |
+| Logging | logowanie | nie mylić z logowaniem użytkownika; w razie potrzeby „rejestrowanie zdarzeń” |
+| Monitoring | monitoring | |
+| Rate limit | limit żądań | |
+| Retry | ponowienie | |
+| Fallback | rozwiązanie zapasowe | |
+| Sandboxing | izolacja w sandboxie | |
+| Sanitization | sanityzacja | |
+| Selective install | instalacja selektywna | |
+| Profile | profil | profil instalacji |
+| Component | komponent | komponent instalatora |
+| Module | moduł | moduł instalatora |
diff --git a/docs/pl/README.md b/docs/pl/README.md
new file mode 100644
index 000000000..582549f6b
--- /dev/null
+++ b/docs/pl/README.md
@@ -0,0 +1,193 @@
+**Język:** [English](../../README.md) | **Polski** | [Deutsch](../de-DE/README.md) | [Español](../es/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md)
+
+# ECC
+
+
+
+> Tłumaczenie obejmuje przewodnik startowy i najważniejsze powierzchnie ECC. Źródło angielskie:
+> commit `8321021c54d670126ce3b2969d5deb880b4b0c2a` z gałęzi `main`.
+> Pełny, aktualny katalog pozostaje w [angielskim README](../../README.md); kolejne obszary będą
+> tłumaczone etapami, aby ograniczyć rozmiar i ryzyko nieaktualnych zmian.
+
+---
+
+**Natywny dla środowisk agentów system operacyjny do pracy agentowej.**
+
+ECC to nie tylko zestaw konfiguracji. Łączy gotowe do użycia Agenty, Skille, Hooki, Rules,
+konfiguracje MCP i warstwę zgodności ze starszymi Commands. System powstał na podstawie
+rzeczywistych przepływów pracy i działa w wielu środowiskach: **Claude Code**, **Codex**,
+**Cursor**, **OpenCode**, **Gemini**, **Zed**, **GitHub Copilot** i innych.
+
+## Oficjalne źródła
+
+Instaluj ECC wyłącznie ze zweryfikowanych kanałów:
+
+- repozytorium [github.com/affaan-m/ECC](https://github.com/affaan-m/ECC),
+- pakiety npm [`ecc-universal`](https://www.npmjs.com/package/ecc-universal) i
+ [`ecc-agentshield`](https://www.npmjs.com/package/ecc-agentshield),
+- aplikacja [ECC Tools dla GitHub](https://github.com/apps/ecc-tools),
+- identyfikator pluginu `ecc@ecc`,
+- witryna [ecc.tools](https://ecc.tools).
+
+Nieoficjalne kopie i mirrory nie są utrzymywane ani sprawdzane przez projekt.
+
+## Szybki start
+
+Wybierz **jedną** ścieżkę instalacji. Łączenie instalacji pluginu z pełną instalacją ręczną
+jest najczęstszą przyczyną zduplikowanych Agentów, Skilli i Hooków.
+
+### Uniwersalna konfiguracja prowadzona
+
+```bash
+npx ecc-universal@2.2.1 setup
+```
+
+Możesz także użyć właściwego menedżera pakietów:
+
+```bash
+pnpm dlx ecc-universal@2.2.1 setup
+yarn dlx ecc-universal@2.2.1 setup
+bunx ecc-universal@2.2.1 setup
+```
+
+Przed uruchomieniem kodu pakietu sprawdź źródło wydania i integralność rejestru.
+
+### Claude Code
+
+W Claude Code dodaj marketplace i zainstaluj plugin:
+
+```text
+/plugin marketplace add https://github.com/affaan-m/ECC
+/plugin install ecc@ecc
+```
+
+Następnie zacznij od `rules/common` oraz tylko tych pakietów językowych lub frameworków,
+których rzeczywiście używasz. Po instalacji pluginu nie uruchamiaj dodatkowo pełnego
+`./install.sh --profile full`.
+
+### Codex
+
+```bash
+codex plugin marketplace add affaan-m/ECC
+codex plugin add ecc@ecc
+```
+
+W Codex użyj `$configure-ecc`, aby przejść przez konfigurację dostosowaną do dostawcy.
+
+### Inne środowiska
+
+Polecenia uruchamiaj z głównego katalogu pobranego repozytorium. Wiersz OpenCode wybiera
+pełny profil i jawnie włącza automatyczne Hooki.
+
+| Środowisko | Polecenie instalacji z repozytorium |
+|---|---|
+| Cursor | `./install.sh --profile minimal --target cursor` |
+| Gemini CLI | `./install.sh --profile minimal --target gemini` |
+| Zed | `./install.sh --profile minimal --target zed` |
+| OpenCode | `npm install && npm run build:opencode && ./install.sh --profile full --target opencode --enable-hooks` |
+| Hermes | `./install.sh --profile minimal --target hermes` |
+| OpenClaw | `./install.sh --profile minimal --target openclaw` |
+| Kimi Code CLI | `./install.sh --profile minimal --target kimi` |
+
+Pełna macierz środowisk i wymagania znajdują się w
+[angielskiej sekcji Platform Support](../../README.md#platform-support).
+
+## Instalacja polskiej dokumentacji
+
+Polski używa kodu `pl`; akceptowany jest także alias `pl-PL`. Użyj lokalnej kopii
+repozytorium zawierającej katalog `docs/pl/` i komponent `locale:pl`. Uruchom poniższe
+polecenia z głównego katalogu tej kopii, po zainstalowaniu zależności repozytorium.
+Wymagany jest Node.js 18 lub nowszy. Wydanie `ecc-universal@2.2.1` nie zawiera jeszcze
+polskiej dokumentacji.
+
+Najpierw sprawdź plan bez zapisywania plików instalacji:
+
+```bash
+node scripts/install-apply.js --target claude --locale pl --dry-run
+```
+
+Następnie zainstaluj polską dokumentację w `~/.claude/docs/pl/`:
+
+```bash
+node scripts/install-apply.js --target claude --locale pl
+```
+
+Ten wybór instaluje moduł dokumentacji `docs-pl`; nie instaluje pełnego profilu ECC
+ani automatycznych Hooków.
+
+## Co zawiera ECC
+
+| Powierzchnia | Rola |
+|---|---|
+| `agents/` | wyspecjalizowane Agenty do planowania, implementacji, przeglądu i diagnostyki |
+| `skills/` | modułowe procedury i wiedza aktywowane zależnie od zadania |
+| `hooks/` | automatyzacje uruchamiane przy zdarzeniach środowiska |
+| `rules/` | stałe zasady wspólne oraz reguły języków i frameworków |
+| `commands/` | starsza warstwa zgodności dla poleceń slash |
+| `mcp-configs/` | konfiguracje serwerów Model Context Protocol |
+| `manifests/` | deklaratywne moduły, komponenty i profile instalatora |
+
+Kierunek projektu jest **skills-first**: nowe przepływy pracy powinny trafiać najpierw do
+`skills/`; `commands/` pozostaje powierzchnią zgodności tam, gdzie nadal jest potrzebna.
+
+## Najważniejsze pojęcia
+
+### Agenty
+
+Agent ma określoną rolę, zestaw narzędzi i sposób pracy. Przykłady obejmują planistę,
+recenzentów kodu dla konkretnych języków oraz specjalistów od rozwiązywania błędów kompilacji.
+
+### Skille
+
+Skill przechowuje skoncentrowaną procedurę lub wiedzę dziedzinową. Dzięki temu kontekst jest
+ładowany tylko wtedy, gdy pasuje do zadania, zamiast powiększać każdy prompt systemowy.
+
+### Hooki
+
+Hook reaguje na zdarzenia takie jak rozpoczęcie sesji lub użycie narzędzia. Hooki muszą być
+przenośne i bezpieczne; nie kopiuj ich drugi raz do ustawień po instalacji pluginu, ponieważ
+nowe wersje Claude Code ładują `hooks/hooks.json` automatycznie.
+
+### Rules
+
+Rules opisują zawsze obowiązujące konwencje. Instaluj wspólny rdzeń i tylko pasujące pakiety,
+aby nie obciążać okna kontekstu nieistotnymi regułami.
+
+Sposób tłumaczenia terminów ECC opisuje [polski glosariusz](GLOSSARY.md).
+
+## Bezpieczeństwo
+
+- Nie zapisuj kluczy API, haseł ani tokenów w repozytorium.
+- Przeglądaj skrypty i źródła pakietów przed uruchomieniem.
+- Nie łącz wielu metod instalacji.
+- Nie kopiuj surowego `hooks/hooks.json` do `~/.claude/settings.json` po instalacji pluginu.
+- Używaj minimalnych uprawnień i weryfikuj wejścia na granicach systemu.
+
+Szczegółowe informacje znajdują się w [sekcji Security](../../README.md#security).
+
+## Aktualizowanie tłumaczenia
+
+Tłumaczenia są utrzymywane według zasady „best effort”. Każdy PR powinien podawać:
+
+1. commit angielskiego źródła,
+2. dokładny zakres przetłumaczonej treści,
+3. zmiany w terminologii względem [GLOSSARY.md](GLOSSARY.md),
+4. wykonane sprawdzenia linków, Markdownu i manifestów instalatora.
+
+Kolejne PR-y powinny być małe i podzielone według domen, na przykład `commands/`, `agents/`,
+`rules/` i `skills/`. Pozwala to uniknąć nakładających się tłumaczeń i ułatwia synchronizację
+z szybko zmieniającym się źródłem angielskim.
+
+## Współtworzenie
+
+Przed rozpoczęciem większego tłumaczenia sprawdź istniejące issues i PR-y, aby uniknąć
+równoległej pracy nad tym samym zakresem. Zasady tworzenia zmian i opisów PR znajdują się w
+[CONTRIBUTING.md](../../CONTRIBUTING.md).
+
+## Licencja
+
+MIT — możesz swobodnie używać i dostosowywać projekt oraz dzielić się ulepszeniami.
+
+---
+
+**Jeśli ECC Ci pomaga, zostaw gwiazdkę. Przeczytaj przewodniki. Zbuduj coś świetnego.**
diff --git a/docs/pt-BR/README.md b/docs/pt-BR/README.md
index e33eff641..e0ecf8d41 100644
--- a/docs/pt-BR/README.md
+++ b/docs/pt-BR/README.md
@@ -1,4 +1,4 @@
-**Idioma:** [English](../../README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | Português (Brasil) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+**Idioma:** [English](../../README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | Português (Brasil) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
# Everything Claude Code
@@ -24,7 +24,7 @@
**Idioma / Language / 语言 / Dil / Язык / Ngôn ngữ**
-[**English**](../../README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Português (Brasil)](README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+[**English**](../../README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Português (Brasil)](README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
---
diff --git a/docs/pt-BR/agents/database-reviewer.md b/docs/pt-BR/agents/database-reviewer.md
index 31b05e0a0..c88abe9eb 100644
--- a/docs/pt-BR/agents/database-reviewer.md
+++ b/docs/pt-BR/agents/database-reviewer.md
@@ -1,7 +1,7 @@
---
name: database-reviewer
description: Especialista em banco de dados PostgreSQL para otimização de queries, design de schema, segurança e performance. Use PROATIVAMENTE ao escrever SQL, criar migrações, projetar schemas ou solucionar problemas de performance. Incorpora boas práticas do Supabase.
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
+tools: ["Read", "Grep", "Glob", "Bash"]
model: sonnet
---
diff --git a/docs/pt-BR/agents/security-reviewer.md b/docs/pt-BR/agents/security-reviewer.md
index 54e456753..3355ff84c 100644
--- a/docs/pt-BR/agents/security-reviewer.md
+++ b/docs/pt-BR/agents/security-reviewer.md
@@ -1,7 +1,7 @@
---
name: security-reviewer
description: Especialista em detecção e remediação de vulnerabilidades de segurança. Use PROATIVAMENTE após escrever código que trata input de usuário, autenticação, endpoints de API ou dados sensíveis. Sinaliza segredos, SSRF, injection, criptografia insegura e vulnerabilidades OWASP Top 10.
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
+tools: ["Read", "Grep", "Glob", "Bash"]
model: sonnet
---
diff --git a/docs/releases/2.2.2/release-notes.md b/docs/releases/2.2.2/release-notes.md
new file mode 100644
index 000000000..1ba5ff5ff
--- /dev/null
+++ b/docs/releases/2.2.2/release-notes.md
@@ -0,0 +1,61 @@
+# ECC 2.2.2
+
+ECC 2.2.2 adds `pi/core`, a curated Pi-native skills+prompts-only profile built for
+downstream packagers that mirror GitHub Releases, plus a set of packaging, memory,
+hooks, and Windows compatibility fixes.
+
+## Pi core profile
+
+`pi/core/` is a self-contained package (`ecc-pi-core`) that mirrors of the release
+tarball can copy directly: no build step, no extensions, no hooks, no runtime
+downloads, and no network or SaaS dependencies.
+
+- 123 curated skills (language and framework patterns, testing/TDD, code review,
+ non-offensive security review, planning, refactoring, docs, and git/PR workflows)
+ and 24 prompt commands that are pure prompt workflows.
+- Generated deterministically from the explicit include/exclude lists in
+ `manifests/pi-core.json` by `scripts/build-pi-core.js` and committed, so the
+ release tarball contains it verbatim. `pi/core/CURATION.md` lists every excluded
+ skill and command with its reason; the `council` skill ships as `ecc-council`
+ inside pi/core to avoid catalog name clashes.
+- The build fails on safety violations: non-allowlisted URL hosts, pipe-to-shell
+ or fetch-and-run download forms, secrets or tokens, absolute per-user home
+ paths, symlinks, invalid SKILL.md frontmatter, and duplicate skill names.
+- CI rebuilds pi/core on every PR and verifies it is up to date, then installs
+ the Pi coding agent CLI and proves the profile loads fully offline
+ (`PI_OFFLINE=1`), asserting that every curated command actually registers.
+- The release workflow additionally verifies that `VERSION` matches the tag and
+ that pi/core is current before publishing.
+
+Downstream consumption: poll `/releases`, download
+`archive/refs/tags/vX.Y.Z.tar.gz`, pin its sha256, copy `pi/core/`, and load it
+offline with `pi --offline --skill pi/core/skills --prompt-template pi/core/commands`.
+
+## Packaging
+
+- The compiled OpenCode payload is explicitly included in the npm package, and
+ packing is verified from a clean state with lifecycle scripts enabled.
+
+## Memory and MCP
+
+- Incomplete memory reads are distinguished from missing records, and directory
+ traversal failures are classified.
+- The reserved `_meta` parameter is accepted on memory MCP ping requests.
+
+## Hooks and Windows compatibility
+
+- `hooks.json` stays within Claude Code's schema; stable hook metadata moved into
+ a validated sidecar.
+- The no-verify guard handles stuck optional values and long-option prefixes.
+- Windows linter paths and ESLint 9 are supported, and settings updates tolerate
+ missing Windows device IDs while retaining full-precision inode checks.
+
+## Workflow guidance, catalog, and dependency security
+
+- Epic sync filters issues by label; dependency bumps no longer document
+ auto-merge; naming and Boolean guidance is language-neutral; the `prp-pr`
+ command alias is distinguished; Rails skill discovery, invoice tax calculation
+ order, and framework documentation were corrected; Serply and Squish catalog
+ entries were removed.
+- `lru` updated to 0.18.2 (RUSTSEC-2026-0253) and `js-yaml` to 4.3.2
+ (GHSA-2883-xcg3-v3hh).
diff --git a/docs/ru/README.md b/docs/ru/README.md
index 537770e85..64f6f0f6d 100644
--- a/docs/ru/README.md
+++ b/docs/ru/README.md
@@ -1,4 +1,4 @@
-**Язык:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | **Русский** | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+**Язык:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | **Русский** | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
# Everything Claude Code
@@ -27,7 +27,7 @@
**Язык / 语言 / 語言 / Dil / Ngôn ngữ**
-[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | **Русский** | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | **Русский** | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
diff --git a/docs/th/README.md b/docs/th/README.md
index 01e48b871..2110b5e77 100644
--- a/docs/th/README.md
+++ b/docs/th/README.md
@@ -1,4 +1,4 @@
-**ภาษา:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | **ไทย** | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+**ภาษา:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | **ไทย** | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
# Everything Claude Code
@@ -18,7 +18,7 @@
**ภาษา / Language / 语言 / 語言 / Dil / Язык / Ngôn ngữ**
-[English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | **ไทย** | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+[English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | **ไทย** | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
diff --git a/docs/tr/AGENTS.md b/docs/tr/AGENTS.md
index a67004d7b..53e74b8f1 100644
--- a/docs/tr/AGENTS.md
+++ b/docs/tr/AGENTS.md
@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — Agent Talimatları
-Bu, yazılım geliştirme için 68 özel agent, 292 skill, 94 command ve otomatik hook iş akışları sağlayan **üretime hazır bir AI kodlama eklentisidir**.
+Bu, yazılım geliştirme için 68 özel agent, 293 skill, 94 command ve otomatik hook iş akışları sağlayan **üretime hazır bir AI kodlama eklentisidir**.
**Sürüm:** 2.2.2
@@ -142,7 +142,7 @@ Başarısızlık sorunlarını giderin: test izolasyonunu kontrol edin → mockl
```
agents/ — 68 özel subagent
-skills/ — 292 iş akışı skillleri ve alan bilgisi
+skills/ — 293 iş akışı skillleri ve alan bilgisi
commands/ — 94 slash command
hooks/ — Tetikleyici tabanlı otomasyonlar
rules/ — Her zaman uyulması gereken kurallar (ortak + dile özel)
diff --git a/docs/tr/README.md b/docs/tr/README.md
index 1fc5e2f5b..00046afc6 100644
--- a/docs/tr/README.md
+++ b/docs/tr/README.md
@@ -23,7 +23,7 @@
**Dil / Language / 语言 / 語言 / Язык / Ngôn ngữ**
-[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [**Türkçe**](README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [**Türkçe**](README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
diff --git a/docs/tr/agents/chief-of-staff.md b/docs/tr/agents/chief-of-staff.md
index f7924608f..d41816d58 100644
--- a/docs/tr/agents/chief-of-staff.md
+++ b/docs/tr/agents/chief-of-staff.md
@@ -2,7 +2,7 @@
name: chief-of-staff
description: Personal communication chief of staff that triages email, Slack, LINE, and Messenger. Classifies messages into 4 tiers (skip/info_only/meeting_info/action_required), generates draft replies, and enforces post-send follow-through via hooks. Use when managing multi-channel communication workflows.
tools: ["Read", "Grep", "Glob", "Bash", "Edit", "Write"]
-model: opus
+model: sonnet
---
Tüm iletişim kanallarını — e-posta, Slack, LINE, Messenger ve takvim — birleşik bir triyaj hattı üzerinden yöneten kişisel bir başkan yardımcısısınız.
diff --git a/docs/tr/agents/database-reviewer.md b/docs/tr/agents/database-reviewer.md
index c1cc651b9..cae06aa28 100644
--- a/docs/tr/agents/database-reviewer.md
+++ b/docs/tr/agents/database-reviewer.md
@@ -1,7 +1,7 @@
---
name: database-reviewer
description: PostgreSQL database specialist for query optimization, schema design, security, and performance. Use PROACTIVELY when writing SQL, creating migrations, designing schemas, or troubleshooting database performance. Incorporates Supabase best practices.
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
+tools: ["Read", "Grep", "Glob", "Bash"]
model: sonnet
---
diff --git a/docs/tr/agents/docs-lookup.md b/docs/tr/agents/docs-lookup.md
index 942d97091..9e2afc1a6 100644
--- a/docs/tr/agents/docs-lookup.md
+++ b/docs/tr/agents/docs-lookup.md
@@ -2,7 +2,7 @@
name: docs-lookup
description: Kullanıcı bir kütüphaneyi, framework'ü veya API'yi nasıl kullanacağını sorduğunda veya güncel kod örneklerine ihtiyaç duyduğunda, güncel dokümantasyon getirmek ve örneklerle cevaplar döndürmek için Context7 MCP kullanın. Docs/API/kurulum soruları için çağrılır.
tools: ["Read", "Grep", "mcp__context7__resolve-library-id", "mcp__context7__query-docs"]
-model: sonnet
+model: haiku
---
Bir dokümantasyon specialistisiniz. Kütüphaneler, framework'ler ve API'ler hakkındaki soruları Context7 MCP (resolve-library-id ve query-docs) aracılığıyla getirilen güncel dokümantasyonu kullanarak cevaplarsınız, eğitim verilerini değil.
diff --git a/docs/tr/agents/security-reviewer.md b/docs/tr/agents/security-reviewer.md
index 8beb9e1c4..dd9d415f5 100644
--- a/docs/tr/agents/security-reviewer.md
+++ b/docs/tr/agents/security-reviewer.md
@@ -1,7 +1,7 @@
---
name: security-reviewer
description: Güvenlik açığı tespit ve düzeltme specialisti. Kullanıcı girdisi, kimlik doğrulama, API endpoint'leri veya hassas veri işleyen kod yazdıktan sonra PROAKTİF olarak kullanın. Secret'ları, SSRF, injection, güvensiz kriptografiyi ve OWASP Top 10 güvenlik açıklarını işaretler.
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
+tools: ["Read", "Grep", "Glob", "Bash"]
model: sonnet
---
diff --git a/docs/tr/the-longform-guide.md b/docs/tr/the-longform-guide.md
index b9f23cc56..c1b78a472 100644
--- a/docs/tr/the-longform-guide.md
+++ b/docs/tr/the-longform-guide.md
@@ -1,12 +1,12 @@
# Claude Code'un Her Şeyine Dair Uzun Kılavuz
-
+
---
> **Ön Koşul**: Bu kılavuz [Claude Code'un Her Şeyine Dair Kısa Kılavuz](./the-shortform-guide.md) üzerine kuruludur. Skill'leri, hook'ları, subagent'ları, MCP'leri ve plugin'leri henüz kurmadıysanız önce onu okuyun.
-
+
*Kısa Kılavuz - önce onu okuyun*
Kısa kılavuzda, temel kurulumu ele aldım: etkili bir Claude Code iş akışının omurgasını oluşturan skill'ler ve command'lar, hook'lar, subagent'lar, MCP'ler, plugin'ler ve yapılandırma desenleri. Bu kurulum kılavuzu ve temel altyapıydı.
@@ -39,7 +39,7 @@ Lazy loading ile, context window sorunu çoğunlukla çözülmüştür. Ancak to
Oturumlar arasında memory paylaşımı için, ilerlemeyi özetleyen ve kontrol eden, ardından `.claude` klasörünüzde bir `.tmp` dosyasına kaydeden ve oturumunuz sonuna kadar ona ekleyen bir skill veya command en iyi bahistir. Ertesi gün bunu context olarak kullanabilir ve kaldığı yerden devam edebilir, her oturum için yeni bir dosya oluşturun böylece eski context'i yeni işe kirletmezsiniz.
-
+
*Oturum depolama örneği -> *
Claude mevcut durumu özetleyen bir dosya oluşturur. İnceleyin, gerekirse düzenlemeler isteyin, ardından yeniden başlayın. Yeni konuşma için, sadece dosya yolunu sağlayın. Özellikle context limitlerini aşarken ve karmaşık işi sürdürmeniz gerektiğinde kullanışlıdır. Bu dosyalar şunları içermelidir:
@@ -110,7 +110,7 @@ Kullandığınız araçları optimize edin ve görev için yeterli olan en ucuz
**Model Seçimi Hızlı Referans:**
-
+
*Çeşitli yaygın görevlerde subagent'ların varsayımsal kurulumu ve seçimlerin arkasındaki akıl yürütme*
| Görev Türü | Model | Neden |
@@ -128,14 +128,14 @@ Kodlama görevlerinin %90'ı için Sonnet'i varsayılan yapın. İlk deneme baş
**Fiyatlandırma Referansı:**
-
+
*Kaynak: *
**Araca Özgü Optimizasyonlar:**
grep'i mgrep ile değiştirin - geleneksel grep veya ripgrep'e kıyasla ortalama ~%50 token azaltması:
-
+
*50 görevlik benchmark'ımızda, mgrep + Claude Code, grep tabanlı iş akışlarına kıyasla benzer veya daha iyi değerlendirilen kalitede ~2 kat daha az token kullandı. Kaynak: @mixedbread-ai tarafından mgrep*
**Modüler Kod Tabanı Faydaları:**
@@ -181,7 +181,7 @@ Kod değişiklikleri için ana sohbet, kod tabanı ve mevcut durumu hakkında so
**Keyfi Terminal Sayıları Üzerine:**
-
+
*Boris (Anthropic) birden fazla Claude instance'ı çalıştırma üzerine*
Boris'in paralelleştirme hakkında ipuçları var. 5 Claude instance'ını yerel olarak ve 5'ini upstream çalıştırmak gibi şeyler önerdi. Keyfi terminal miktarları belirlemeye karşı tavsiyede bulunurum. Bir terminalin eklenmesi gerçek bir zorunluluktan olmalıdır.
@@ -202,7 +202,7 @@ cd ../project-feature-a && claude
Instance'larınızı ölçeklendirmeye başlıyorsanız VE birbirleriyle örtüşen kod üzerinde çalışan birden fazla Claude instance'ınız varsa, git worktree'leri kullanmanız ve her biri için çok iyi tanımlanmış bir plana sahip olmanız zorunludur. Tüm sohbetlerinizi adlandırmak için `/rename ` kullanın.
-
+
*Başlangıç Kurulumu: Kodlama için Sol Terminal, Sorular için Sağ Terminal - /rename ve /fork kullanın*
**Cascade Yöntemi:**
@@ -314,7 +314,7 @@ alias q='cd ~/Desktop/projects'
## Kilometre Taşı
-
+
*Bir haftadan kısa sürede 25.000+ GitHub yıldızı*
---
diff --git a/docs/tr/the-security-guide.md b/docs/tr/the-security-guide.md
index 516be6a9a..df812958f 100644
--- a/docs/tr/the-security-guide.md
+++ b/docs/tr/the-security-guide.md
@@ -18,13 +18,13 @@ Saldırı vektörleri esasen herhangi bir etkileşim giriş noktasıdır. Agent'
### Saldırı Zinciri ve Dahil Olan Düğümler / Bileşenler
-
+
Örneğin, agent'ım bir gateway katmanı aracılığıyla WhatsApp'a bağlı. Bir rakip WhatsApp numaranızı biliyor. Mevcut bir jailbreak kullanarak bir prompt injection denemesi yapıyorlar. Sohbette jailbreak spam'i yapıyorlar. Agent mesajı okuyor ve bunu talimat olarak alıyor. Özel bilgileri ifşa eden bir yanıt yürütüyor. Agent'ınızın root erişimi, geniş dosya sistemi erişimi veya yüklü yararlı kimlik bilgileri varsa, tehlikeye girdiniz.
İnsanların güldüğü bu Good Rudi jailbreak klipleri bile (komik ngl) aynı sorun sınıfına işaret ediyor: tekrarlanan denemeler, sonunda hassas bir ifşa, yüzeyde eğlenceli ancak altta yatan arıza ciddi - yani sonuçta çocuklar için tasarlanmış, bundan biraz çıkarım yapın ve bunun neden felaket olabileceği sonucuna hızla varırsınız. Aynı desen, model gerçek araçlara ve gerçek izinlere bağlandığında çok daha ileri gider.
-[Video: Bad Rudi Exploit](../assets/images/security/badrudi-exploit.mp4) — good rudi (çocuklar için grok animasyonlu AI karakteri) hassas bilgileri ifşa etmek için tekrarlanan denemelerden sonra bir prompt jailbreak ile exploit edilir. eğlenceli bir örnek ama yine de olasılıklar çok daha ileri gider.
+Bad Rudi örneği (video bu depoda mevcut değil) — good rudi (çocuklar için grok animasyonlu AI karakteri) hassas bilgileri ifşa etmek için tekrarlanan denemelerden sonra bir prompt jailbreak ile exploit edilir. eğlenceli bir örnek ama yine de olasılıklar çok daha ileri gider.
WhatsApp sadece bir örnek. E-posta ekleri büyük bir vektör. Bir saldırgan gömülü bir prompt'lu PDF gönderiyor; agent'ınız eki işin bir parçası olarak okuyor ve şimdi yardımcı veri olarak kalması gereken metin kötü niyetli talimata dönüştü. Üzerlerinde OCR yapıyorsanız ekran görüntüleri ve taramalar da aynı derecede kötü. Anthropic'in kendi prompt injection çalışması, gizli metin ve manipüle edilmiş görüntüleri açıkça gerçek saldırı malzemesi olarak adlandırıyor.
@@ -111,9 +111,9 @@ Belirli sayılar değişmeye devam edecek. Önemli olan seyahat yönü (olaylar
Root erişimi tehlikelidir. Geniş yerel erişim tehlikelidir. Aynı makinede uzun ömürlü kimlik bilgileri tehlikelidir. "YOLO, Claude beni koruyor" burada doğru yaklaşım değildir. Cevap izolasyondur.
-
+
-
+
İlke basittir: agent tehlikeye girerse, patlama yarıçapının küçük olması gerekir.
@@ -195,7 +195,7 @@ Bir iş akışının sadece bir repo okuması ve testleri çalıştırması gere
Bir LLM'nin okuduğu her şey çalıştırılabilir context'tir. Metin context window'a girdiğinde "veri" ve "talimatlar" arasında anlamlı bir ayrım yoktur. Sanitizasyon kozmetik değildir; runtime sınırının bir parçasıdır.
-
+
### Gizli Unicode ve Yorum Payload'ları
@@ -278,7 +278,7 @@ OWASP'nin en az ayrıcalık etrafındaki dili agent'lara temiz bir şekilde eşl
Agent'ın neyi okuduğunu, hangi aracı çağırdığını ve hangi ağ hedefine gitmeye çalıştığını göremezseniz, onu güvenli hale getiremezsiniz (bu bariz olmalı, yine de bir ralph döngüsünde claude --dangerously-skip-permissions'ı çalıştırdığınızı ve hiçbir endişe olmadan uzaklaştığınızı görüyorum). Sonra karmaşık bir kod tabanıyla geri geliyorsunuz, agent'ın ne yaptığını bulmaya iş yapmaktan daha fazla zaman harcıyorsunuz.
-
+
En azından bunları logla:
- araç adı
@@ -311,7 +311,7 @@ Zarif ve sert kill'ler arasındaki farkı bilin. `SIGTERM` sürecine temizlik i
Ayrıca, sadece parent'ı değil, süreç grubunu kill edin. Sadece parent'ı kill ederseniz, çocuklar çalışmaya devam edebilir. (bu aynı zamanda bazen sabah ghostty sekmelerinize baktığınızda bir şekilde 100GB RAM tükettiğinizi ve bilgisayarınızda sadece 64GB varken sürecin duraklatıldığını görmenizin nedenidir, bir sürü çocuk süreç kapandığını düşündüğünüzde kontrolden çıkmış)
-
+
Node örneği:
diff --git a/docs/tr/the-shortform-guide.md b/docs/tr/the-shortform-guide.md
index 6a894a175..90519eb48 100644
--- a/docs/tr/the-shortform-guide.md
+++ b/docs/tr/the-shortform-guide.md
@@ -1,6 +1,6 @@
# Claude Code'un Her Şeyine Dair Kısa Kılavuz
-
+
---
@@ -16,7 +16,7 @@ Skill'ler, belirli kapsamlar ve iş akışlarıyla sınırlandırılmış kurall
Opus 4.5 ile uzun bir kodlama oturumundan sonra ölü kodu ve gevşek .md dosyalarını temizlemek mi istiyorsunuz? `/refactor-clean` çalıştırın. Test mi gerekli? `/tdd`, `/e2e`, `/test-coverage`. Skill'ler ayrıca codemap'leri de içerebilir - Claude'un keşfe context harcamadan kod tabanınızda hızlıca gezinmesi için bir yöntem.
-
+
*Command'ları zincirleme*
Command'lar, slash command'lar aracılığıyla yürütülen skill'lerdir. Örtüşürler ancak farklı şekilde saklanırlar:
@@ -66,7 +66,7 @@ Hook'lar, belirli olaylarda tetiklenen otomasyonlardır. Skill'lerin aksine, ara
}
```
-
+
*PostToolUse hook çalıştırırken Claude Code'da aldığınız geri bildirimin örneği*
**Pro ipucu:** JSON'u manuel yazmak yerine hook'ları konuşarak oluşturmak için `hookify` plugin'ini kullanın. `/hookify` çalıştırın ve ne istediğinizi açıklayın.
@@ -129,7 +129,7 @@ MCP'ler Claude'u doğrudan harici hizmetlere bağlar. API'lerin yerini tutmaz -
**Örnek:** Supabase MCP, Claude'un belirli verileri çekmesine, SQL'i kopyala-yapıştır olmadan doğrudan upstream çalıştırmasına izin verir. Veritabanları, dağıtım platformları vb. için de aynı.
-
+
*Supabase MCP'nin public şemasındaki tabloları listeleyen örneği*
**Claude'da Chrome:** Claude'un tarayıcınızı özerk olarak kontrol etmesine izin veren yerleşik bir plugin MCP'sidir - işlerin nasıl çalıştığını görmek için etrafta tıklar.
@@ -138,7 +138,7 @@ MCP'ler Claude'u doğrudan harici hizmetlere bağlar. API'lerin yerini tutmaz -
MCP'lerle seçici olun. Tüm MCP'leri kullanıcı yapılandırmasında tutarım ancak **kullanılmayan her şeyi devre dışı bırakırım**. `/plugins`'e gidin ve aşağı kaydırın veya `/mcp` çalıştırın.
-
+
*/plugins kullanarak MCP'lere giderek şu anda hangi MCP'lerin yüklü olduğunu ve durumlarını görme*
Sıkıştırmadan önce 200k context window'unuz, çok fazla araç etkinleştirilmişse sadece 70k olabilir. Performans önemli ölçüde düşer.
@@ -168,7 +168,7 @@ claude plugin marketplace add https://github.com/mixedbread-ai/mgrep
# Claude'u açın, /plugins çalıştırın, yeni marketplace'i bulun, oradan yükleyin
```
-
+
*Yeni yüklenen Mixedbread-Grep marketplace'i gösterme*
**LSP Plugin'leri**, Claude Code'u sık sık editör dışında çalıştırıyorsanız özellikle kullanışlıdır. Language Server Protocol, Claude'a IDE açık olmadan gerçek zamanlı tip kontrolü, tanıma gitme ve akıllı tamamlamalar verir.
@@ -239,7 +239,7 @@ mgrep --web "Next.js 15 app router changes" # Web araması
PR'larınızda GitHub Actions ile kod incelemesi kurun. Claude yapılandırıldığında PR'ları otomatik olarak inceleyebilir.
-
+
*Claude bir bug düzeltme PR'ını onaylıyor*
### Sandboxing
@@ -264,7 +264,7 @@ Ben [Zed](https://zed.dev) kullanıyorum - Rust ile yazılmış, bu nedenle ger
- **Minimal Kaynak Kullanımı** - Ağır işlemler sırasında Claude ile RAM/CPU için rekabet etmez. Opus çalıştırırken önemli
- **Vim Modu** - Bu sizin tarzınızsa tam vim keybinding'leri
-
+
*CMD+Shift+R kullanarak özel komutlar açılır menüsü olan Zed Editor. Following modu sağ altta hedef işareti olarak gösterilmiş.*
**Editörden Bağımsız İpuçları:**
@@ -279,7 +279,7 @@ Ben [Zed](https://zed.dev) kullanıyorum - Rust ile yazılmış, bu nedenle ger
Bu da geçerli bir seçimdir ve Claude Code ile iyi çalışır. LSP işlevselliğini etkinleştiren `\ide` ile editörünüzle otomatik senkronizasyon ile terminal formatında kullanabilirsiniz (artık plugin'lerle biraz gereksiz). Veya Editor ile daha entegre olan ve eşleşen bir UI'ya sahip extension'ı tercih edebilirsiniz.
-
+
*VS Code extension, doğrudan IDE'nize entegre edilmiş Claude Code için native bir grafik arayüz sağlar.*
---
@@ -363,7 +363,7 @@ Bu anahtar - 14 MCP yapılandırılmış ancak proje başına sadece ~5-6'sı et
Kullanıcı, dizin, kirli göstergeli git branch, kalan context %, model, zaman ve todo sayısını gösterir:
-
+
*Mac root dizinimde örnek statusline*
```
diff --git a/docs/uk-UA/README.md b/docs/uk-UA/README.md
index 7c8f28f88..f76959dc6 100644
--- a/docs/uk-UA/README.md
+++ b/docs/uk-UA/README.md
@@ -16,7 +16,8 @@
ไทย |
Deutsch |
Español |
- Українська
+ Українська |
+ Polski
@@ -103,13 +104,13 @@
-Спонсори спільноти: Mike Morgan · @jasonwu513 · @1anter · @massimotodaro · @meadmccabe
+Спонсори спільноти: @jasonwu513 · @1anter · @massimotodaro · @meadmccabe
+Минулі спонсори: Atlas Cloud · Mike Morgan (неактивний)
Стати спонсором · Рівні спонсорства · Програма спонсорства
@@ -736,6 +737,7 @@ ECC також постачає розширені керовані адапте
- **Ціль встановлення Kimi Code** (`--target kimi`): ECC встановлюється нативно в Kimi Code CLI від [Moonshot AI](https://www.moonshot.ai)
- **Самостійний хостинг на GPU**: перевірений шлях з [Itô](https://compute.itomarkets.com), бажаним обчислювальним спонсором ECC, включно з опційним мостом RFQ `ecc ito find` (деталі та розкриття вище в опціях встановлення)
- **Moonshot AI (Kimi), Itô та Atlas Cloud** тепер публічні спонсори
+- **Поточний статус спонсорства:** Atlas Cloud є минулим спонсором. Оголошення вище збережено як історичний запис випуску 2.1.
- **Цілі встановлення Hermes + OpenClaw**, посібник з навігації Codex, консолідовані хуки PostToolUse та зміцнення ланцюжка поставок
### Поточна розробка: Уніфікованe сховище пам'яті
diff --git a/docs/ur/README.md b/docs/ur/README.md
index 32185989e..d98a0dc4c 100644
--- a/docs/ur/README.md
+++ b/docs/ur/README.md
@@ -1,4 +1,4 @@
-**زبان:** [English](../../README.md) | [اردو](README.md) | [Deutsch](../de-DE/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md)
+**زبان:** [English](../../README.md) | [اردو](README.md) | [Deutsch](../de-DE/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
# ECC
@@ -27,7 +27,7 @@
**زبان / Language / 语言**
-[English](../../README.md) | [**اردو**](README.md) | [Deutsch](../de-DE/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md)
+[English](../../README.md) | [**اردو**](README.md) | [Deutsch](../de-DE/README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
diff --git a/docs/vi-VN/README.md b/docs/vi-VN/README.md
index 4c9b3d8f7..aff90f2cb 100644
--- a/docs/vi-VN/README.md
+++ b/docs/vi-VN/README.md
@@ -1,4 +1,4 @@
-**Ngôn ngữ:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | **Tiếng Việt** | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+**Ngôn ngữ:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | **Tiếng Việt** | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
# Everything Claude Code
@@ -18,7 +18,7 @@
**Ngôn ngữ / Language / 语言 / 語言 / Dil / Язык**
-[English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | **Tiếng Việt** | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+[English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | **Tiếng Việt** | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md
index 31e1a3817..555599e2e 100644
--- a/docs/zh-CN/AGENTS.md
+++ b/docs/zh-CN/AGENTS.md
@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — 智能体指令
-这是一个**生产就绪的 AI 编码插件**,提供 68 个专业代理、292 项技能、94 条命令以及自动化钩子工作流,用于软件开发。
+这是一个**生产就绪的 AI 编码插件**,提供 68 个专业代理、293 项技能、94 条命令以及自动化钩子工作流,用于软件开发。
**版本:** 2.2.2
@@ -147,7 +147,7 @@
```
agents/ — 68 个专业子代理
-skills/ — 292 个工作流技能和领域知识
+skills/ — 293 个工作流技能和领域知识
commands/ — 94 个斜杠命令
hooks/ — 基于触发的自动化
rules/ — 始终遵循的指导方针(通用 + 每种语言)
diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md
index 3228c6159..2ce4ef8e5 100644
--- a/docs/zh-CN/README.md
+++ b/docs/zh-CN/README.md
@@ -1,4 +1,4 @@
-**语言:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md)
+**语言:** [English](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
# Everything Claude Code
@@ -25,7 +25,7 @@
**语言 / Language / 語言 / Dil / Язык / Ngôn ngữ**
-[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md)
+[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
@@ -260,7 +260,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
/plugin list ecc@ecc
```
-**搞定!** 你现在可以使用 68 个智能体、292 项技能和 94 个命令了。
+**搞定!** 你现在可以使用 68 个智能体、293 项技能和 94 个命令了。
***
@@ -1174,7 +1174,7 @@ opencode
|---------|---------------|----------|--------|
| 智能体 | PASS: 68 个 | PASS: 12 个 | **Claude Code 领先** |
| 命令 | PASS: 94 个 | PASS: 35 个 | **Claude Code 领先** |
-| 技能 | PASS: 292 项 | PASS: 37 项 | **Claude Code 领先** |
+| 技能 | PASS: 293 项 | PASS: 37 项 | **Claude Code 领先** |
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
@@ -1282,7 +1282,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|---------|-----------------------|------------|-----------|----------|
| **智能体** | 68 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
| **命令** | 94 | 共享 | 基于指令 | 35 |
-| **技能** | 292 | 共享 | 10 (原生格式) | 37 |
+| **技能** | 293 | 共享 | 10 (原生格式) | 37 |
| **钩子事件** | 8 种类型 | 15 种类型 | SessionStart(1 种类型) | 11 种类型 |
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | 1 个 SessionStart 引导脚本 | 插件钩子 |
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
diff --git a/docs/zh-CN/agents/chief-of-staff.md b/docs/zh-CN/agents/chief-of-staff.md
index 157c84fb1..733f97545 100644
--- a/docs/zh-CN/agents/chief-of-staff.md
+++ b/docs/zh-CN/agents/chief-of-staff.md
@@ -2,7 +2,7 @@
name: chief-of-staff
description: 个人通讯首席参谋,负责筛选电子邮件、Slack、LINE和Messenger中的消息。将消息分为4个等级(跳过/仅信息/会议信息/需要行动),生成草稿回复,并通过钩子强制执行发送后的跟进。适用于管理多渠道通讯工作流程时。
tools: ["Read", "Grep", "Glob", "Bash", "Edit", "Write"]
-model: opus
+model: sonnet
---
你是一位个人幕僚长,通过一个统一的分类处理管道管理所有通信渠道——电子邮件、Slack、LINE、Messenger 和日历。
diff --git a/docs/zh-CN/agents/comment-analyzer.md b/docs/zh-CN/agents/comment-analyzer.md
index ba1dff182..b2b1e68b5 100644
--- a/docs/zh-CN/agents/comment-analyzer.md
+++ b/docs/zh-CN/agents/comment-analyzer.md
@@ -1,7 +1,7 @@
---
name: comment-analyzer
description: 分析代码注释的准确性、完整性、可维护性和注释腐烂风险。
-model: sonnet
+model: haiku
tools: [Read, Grep, Glob]
---
diff --git a/docs/zh-CN/agents/conversation-analyzer.md b/docs/zh-CN/agents/conversation-analyzer.md
index a91ed543f..e54a87d53 100644
--- a/docs/zh-CN/agents/conversation-analyzer.md
+++ b/docs/zh-CN/agents/conversation-analyzer.md
@@ -1,7 +1,7 @@
---
name: conversation-analyzer
description: 使用此代理分析对话记录,以找到值得通过钩子预防的行为。由不带参数的 /hookify 触发。
-model: sonnet
+model: haiku
tools: [Read, Grep]
---
diff --git a/docs/zh-CN/agents/database-reviewer.md b/docs/zh-CN/agents/database-reviewer.md
index f7a4dd6a0..d4d8fcd33 100644
--- a/docs/zh-CN/agents/database-reviewer.md
+++ b/docs/zh-CN/agents/database-reviewer.md
@@ -1,7 +1,7 @@
---
name: database-reviewer
description: PostgreSQL 数据库专家,专注于查询优化、模式设计、安全性和性能。在编写 SQL、创建迁移、设计模式或排查数据库性能问题时,请主动使用。融合了 Supabase 最佳实践。
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
+tools: ["Read", "Grep", "Glob", "Bash"]
model: sonnet
---
diff --git a/docs/zh-CN/agents/docs-lookup.md b/docs/zh-CN/agents/docs-lookup.md
index 2a6188dbf..bb98cfa0b 100644
--- a/docs/zh-CN/agents/docs-lookup.md
+++ b/docs/zh-CN/agents/docs-lookup.md
@@ -2,7 +2,7 @@
name: docs-lookup
description: 当用户询问如何使用库、框架或API,或需要最新的代码示例时,使用Context7 MCP获取当前文档,并返回带有示例的答案。针对文档/API/设置问题调用。
tools: ["Read", "Grep", "mcp__context7__resolve-library-id", "mcp__context7__query-docs"]
-model: sonnet
+model: haiku
---
你是一名文档专家。你使用通过 Context7 MCP(resolve-library-id 和 query-docs)获取的当前文档来回答关于库、框架和 API 的问题,而不是使用训练数据。
diff --git a/docs/zh-CN/agents/gan-evaluator.md b/docs/zh-CN/agents/gan-evaluator.md
index b48f4fe57..70eff2ff3 100644
--- a/docs/zh-CN/agents/gan-evaluator.md
+++ b/docs/zh-CN/agents/gan-evaluator.md
@@ -1,8 +1,8 @@
---
name: gan-evaluator
description: "GAN Harness — Evaluator agent. Tests the live running application via Playwright, scores against rubric, and provides actionable feedback to the Generator."
-tools: ["Read", "Write", "Bash", "Grep", "Glob"]
-model: opus
+tools: ["Read", "Write", "Bash", "Grep", "Glob", "mcp__playwright__browser_navigate", "mcp__playwright__browser_click", "mcp__playwright__browser_take_screenshot", "mcp__playwright__browser_snapshot", "mcp__playwright__browser_type", "mcp__playwright__browser_fill_form", "mcp__playwright__browser_resize", "mcp__playwright__browser_press_key"]
+model: sonnet
color: red
---
diff --git a/docs/zh-CN/agents/gan-generator.md b/docs/zh-CN/agents/gan-generator.md
index 63d99d7f8..d1e8367b7 100644
--- a/docs/zh-CN/agents/gan-generator.md
+++ b/docs/zh-CN/agents/gan-generator.md
@@ -2,7 +2,7 @@
name: gan-generator
description: "GAN Harness — Generator agent. Implements features according to the spec, reads evaluator feedback, and iterates until quality threshold is met."
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: sonnet
color: green
---
diff --git a/docs/zh-CN/agents/gan-planner.md b/docs/zh-CN/agents/gan-planner.md
index f08c015fc..ceb3dbe1b 100644
--- a/docs/zh-CN/agents/gan-planner.md
+++ b/docs/zh-CN/agents/gan-planner.md
@@ -2,7 +2,7 @@
name: gan-planner
description: "GAN Harness — Planner agent. Expands a one-line prompt into a full product specification with features, sprints, evaluation criteria, and design direction."
tools: ["Read", "Write", "Grep", "Glob"]
-model: opus
+model: sonnet
color: purple
---
diff --git a/docs/zh-CN/agents/opensource-forker.md b/docs/zh-CN/agents/opensource-forker.md
index c8f3e1fca..122ed9a76 100644
--- a/docs/zh-CN/agents/opensource-forker.md
+++ b/docs/zh-CN/agents/opensource-forker.md
@@ -2,7 +2,7 @@
name: opensource-forker
description: 分叉任何项目以进行开源。复制文件,剥离机密和凭据(20多种模式),用占位符替换内部引用,生成.env.example,并清理git历史。这是opensource-pipeline技能的第一阶段。
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: sonnet
+model: haiku
---
# 开源分叉工具
diff --git a/docs/zh-CN/agents/opensource-packager.md b/docs/zh-CN/agents/opensource-packager.md
index 480247b33..c3b091774 100644
--- a/docs/zh-CN/agents/opensource-packager.md
+++ b/docs/zh-CN/agents/opensource-packager.md
@@ -2,7 +2,7 @@
name: opensource-packager
description: 为经过清理的项目生成完整的开源打包文件。生成 CLAUDE.md、setup.sh、README.md、LICENSE、CONTRIBUTING.md 和 GitHub 问题模板。使任何仓库都能立即与 Claude Code 配合使用。这是 opensource-pipeline 技能的第三阶段。
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: sonnet
+model: haiku
---
# 开源打包工具
diff --git a/docs/zh-CN/agents/security-reviewer.md b/docs/zh-CN/agents/security-reviewer.md
index f2067a56c..75f0bdd6d 100644
--- a/docs/zh-CN/agents/security-reviewer.md
+++ b/docs/zh-CN/agents/security-reviewer.md
@@ -1,7 +1,7 @@
---
name: security-reviewer
description: 安全漏洞检测与修复专家。在编写处理用户输入、身份验证、API端点或敏感数据的代码后主动使用。标记密钥、SSRF、注入、不安全的加密以及OWASP Top 10漏洞。
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
+tools: ["Read", "Grep", "Glob", "Bash"]
model: sonnet
---
diff --git a/docs/zh-CN/agents/seo-specialist.md b/docs/zh-CN/agents/seo-specialist.md
index e1625f769..9b220ebb8 100644
--- a/docs/zh-CN/agents/seo-specialist.md
+++ b/docs/zh-CN/agents/seo-specialist.md
@@ -1,7 +1,7 @@
---
name: seo-specialist
description: SEO专家,负责技术SEO审计、页面优化、结构化数据、核心网页指标以及内容/关键词映射。用于网站审计、元标签审查、架构标记、站点地图和robots问题以及SEO修复计划。
-tools: ["Read", "Grep", "Glob", "Bash", "WebSearch", "WebFetch"]
+tools: ["Read", "Grep", "Glob", "WebSearch", "WebFetch"]
model: sonnet
---
diff --git a/docs/zh-TW/README.md b/docs/zh-TW/README.md
index 4d46dfce2..194e98af5 100644
--- a/docs/zh-TW/README.md
+++ b/docs/zh-TW/README.md
@@ -13,7 +13,7 @@
**Language / 语言 / 語言 / Dil / Язык / Ngôn ngữ**
-[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | **繁體中文** | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md)
+[**English**](../../README.md) | [Português (Brasil)](../pt-BR/README.md) | [简体中文](../../README.zh-CN.md) | **繁體中文** | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) | [Türkçe](../tr/README.md) | [Русский](../ru/README.md) | [Tiếng Việt](../vi-VN/README.md) | [ไทย](../th/README.md) | [Deutsch](../de-DE/README.md) | [Українська](../uk-UA/README.md) | [Polski](../pl/README.md)
diff --git a/docs/zh-TW/agents/build-error-resolver.md b/docs/zh-TW/agents/build-error-resolver.md
index 412479019..9aca9d21f 100644
--- a/docs/zh-TW/agents/build-error-resolver.md
+++ b/docs/zh-TW/agents/build-error-resolver.md
@@ -2,7 +2,7 @@
name: build-error-resolver
description: Build and TypeScript error resolution specialist. Use PROACTIVELY when build fails or type errors occur. Fixes build/type errors only with minimal diffs, no architectural edits. Focuses on getting the build green quickly.
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: sonnet
---
# 建置錯誤解決專家
diff --git a/docs/zh-TW/agents/code-reviewer.md b/docs/zh-TW/agents/code-reviewer.md
index 2a732d0d8..fa8aaeb10 100644
--- a/docs/zh-TW/agents/code-reviewer.md
+++ b/docs/zh-TW/agents/code-reviewer.md
@@ -2,7 +2,7 @@
name: code-reviewer
description: Expert code review specialist. Proactively reviews code for quality, security, and maintainability. Use immediately after writing or modifying code. MUST BE USED for all code changes.
tools: ["Read", "Grep", "Glob", "Bash"]
-model: opus
+model: sonnet
---
您是一位資深程式碼審查員,確保程式碼品質和安全性的高標準。
diff --git a/docs/zh-TW/agents/database-reviewer.md b/docs/zh-TW/agents/database-reviewer.md
index 1e8c2ad71..8e8e8a734 100644
--- a/docs/zh-TW/agents/database-reviewer.md
+++ b/docs/zh-TW/agents/database-reviewer.md
@@ -1,8 +1,8 @@
---
name: database-reviewer
description: PostgreSQL database specialist for query optimization, schema design, security, and performance. Use PROACTIVELY when writing SQL, creating migrations, designing schemas, or troubleshooting database performance. Incorporates Supabase best practices.
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+tools: ["Read", "Grep", "Glob", "Bash"]
+model: sonnet
---
# 資料庫審查員
diff --git a/docs/zh-TW/agents/doc-updater.md b/docs/zh-TW/agents/doc-updater.md
index c2df8b51a..13c4054ed 100644
--- a/docs/zh-TW/agents/doc-updater.md
+++ b/docs/zh-TW/agents/doc-updater.md
@@ -2,7 +2,7 @@
name: doc-updater
description: Documentation and codemap specialist. Use PROACTIVELY for updating codemaps and documentation. Runs /update-codemaps and /update-docs, generates docs/CODEMAPS/*, updates READMEs and guides.
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: haiku
---
# 文件與程式碼地圖專家
diff --git a/docs/zh-TW/agents/e2e-runner.md b/docs/zh-TW/agents/e2e-runner.md
index a88b0c1e3..037889b1f 100644
--- a/docs/zh-TW/agents/e2e-runner.md
+++ b/docs/zh-TW/agents/e2e-runner.md
@@ -2,7 +2,7 @@
name: e2e-runner
description: End-to-end testing specialist using Vercel Agent Browser (preferred) with Playwright fallback. Use PROACTIVELY for generating, maintaining, and running E2E tests. Manages test journeys, quarantines flaky tests, uploads artifacts (screenshots, videos, traces), and ensures critical user flows work.
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: sonnet
---
# E2E 測試執行器
diff --git a/docs/zh-TW/agents/go-build-resolver.md b/docs/zh-TW/agents/go-build-resolver.md
index 217b7bdcc..91361d6c8 100644
--- a/docs/zh-TW/agents/go-build-resolver.md
+++ b/docs/zh-TW/agents/go-build-resolver.md
@@ -2,7 +2,7 @@
name: go-build-resolver
description: Go build, vet, and compilation error resolution specialist. Fixes build errors, go vet issues, and linter warnings with minimal changes. Use when Go builds fail.
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: sonnet
---
# Go 建置錯誤解決專家
diff --git a/docs/zh-TW/agents/go-reviewer.md b/docs/zh-TW/agents/go-reviewer.md
index b6a96b880..2e0af2d4a 100644
--- a/docs/zh-TW/agents/go-reviewer.md
+++ b/docs/zh-TW/agents/go-reviewer.md
@@ -2,7 +2,7 @@
name: go-reviewer
description: Expert Go code reviewer specializing in idiomatic Go, concurrency patterns, error handling, and performance. Use for all Go code changes. MUST BE USED for Go projects.
tools: ["Read", "Grep", "Glob", "Bash"]
-model: opus
+model: sonnet
---
您是一位資深 Go 程式碼審查員,確保慣用 Go 和最佳實務的高標準。
diff --git a/docs/zh-TW/agents/refactor-cleaner.md b/docs/zh-TW/agents/refactor-cleaner.md
index b5f3a9154..02f5c0255 100644
--- a/docs/zh-TW/agents/refactor-cleaner.md
+++ b/docs/zh-TW/agents/refactor-cleaner.md
@@ -2,7 +2,7 @@
name: refactor-cleaner
description: Dead code cleanup and consolidation specialist. Use PROACTIVELY for removing unused code, duplicates, and refactoring. Runs analysis tools (knip, depcheck, ts-prune) to identify dead code and safely removes it.
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+model: sonnet
---
# 重構與無用程式碼清理專家
diff --git a/docs/zh-TW/agents/security-reviewer.md b/docs/zh-TW/agents/security-reviewer.md
index 4acd77f6c..0b6255d44 100644
--- a/docs/zh-TW/agents/security-reviewer.md
+++ b/docs/zh-TW/agents/security-reviewer.md
@@ -1,8 +1,8 @@
---
name: security-reviewer
description: Security vulnerability detection and remediation specialist. Use PROACTIVELY after writing code that handles user input, authentication, API endpoints, or sensitive data. Flags secrets, SSRF, injection, unsafe crypto, and OWASP Top 10 vulnerabilities.
-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
-model: opus
+tools: ["Read", "Grep", "Glob", "Bash"]
+model: sonnet
---
# 安全性審查員
diff --git a/docs/zh-TW/agents/tdd-guide.md b/docs/zh-TW/agents/tdd-guide.md
index 256c2e3fc..3dbac8bfa 100644
--- a/docs/zh-TW/agents/tdd-guide.md
+++ b/docs/zh-TW/agents/tdd-guide.md
@@ -2,7 +2,7 @@
name: tdd-guide
description: Test-Driven Development specialist enforcing write-tests-first methodology. Use PROACTIVELY when writing new features, fixing bugs, or refactoring code. Ensures 80%+ test coverage.
tools: ["Read", "Write", "Edit", "Bash", "Grep"]
-model: opus
+model: sonnet
---
您是一位 TDD(測試驅動開發)專家,確保所有程式碼都以測試先行的方式開發,並具有全面的覆蓋率。
diff --git a/eslint.config.js b/eslint.config.js
index 788a502b5..22f924aff 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -30,5 +30,11 @@ module.exports = [
languageOptions: {
sourceType: 'module'
}
+ },
+ {
+ files: ['docker/context-profiles/complex-eval/**/recurring-incident/**/*.js'],
+ languageOptions: {
+ sourceType: 'module'
+ }
}
];
diff --git a/hooks/hooks.json b/hooks/hooks.json
index 8ec470673..ea3960496 100644
--- a/hooks/hooks.json
+++ b/hooks/hooks.json
@@ -70,7 +70,7 @@
]
},
{
- "matcher": ".*",
+ "matcher": "^mcp__",
"hooks": [
{
"type": "command",
@@ -145,7 +145,7 @@
],
"PostToolUseFailure": [
{
- "matcher": ".*",
+ "matcher": "^mcp__",
"hooks": [
{
"type": "command",
diff --git a/hooks/hooks.metadata.json b/hooks/hooks.metadata.json
index 1c3a54c8a..c5986e44c 100644
--- a/hooks/hooks.metadata.json
+++ b/hooks/hooks.metadata.json
@@ -40,7 +40,7 @@
{
"id": "pre:mcp-health-check",
"description": "Check MCP server health before MCP tool execution and block unhealthy MCP calls",
- "fingerprint": "922686364d01"
+ "fingerprint": "a68881c27a83"
},
{
"id": "pre:edit-write:gateguard-fact-force",
@@ -83,7 +83,7 @@
{
"id": "post:mcp-health-check",
"description": "Track failed MCP tool calls, mark unhealthy servers, and attempt reconnect",
- "fingerprint": "9e25549c1229"
+ "fingerprint": "edbf5346288f"
},
{
"id": "post:skill:track",
diff --git a/install.sh b/install.sh
index c69a11dd7..8d6aaa9c5 100755
--- a/install.sh
+++ b/install.sh
@@ -4,6 +4,14 @@
# This wrapper resolves the real repo/package root when invoked through a
# symlinked npm bin, then delegates to the Node-based installer runtime.
+# Re-exec under bash if invoked via a POSIX sh (e.g. `sh install.sh`), since
+# the rest of this script relies on bash features (`set -o pipefail`, `[[`).
+# Probe actual shell capability instead of trusting $BASH_VERSION, which is
+# just an environment variable and could be inherited/spoofed under sh.
+if ! (eval '[[ 1 == 1 ]]' >/dev/null 2>&1); then
+ exec bash "$0" "$@"
+fi
+
set -euo pipefail
SCRIPT_PATH="$0"
@@ -25,7 +33,7 @@ fi
# On MSYS2/Git Bash, convert the POSIX path to a Windows path so Node.js
# (a native Windows binary) receives a valid path instead of a doubled one
# like G:\g\projects\... that results from Git Bash's auto path conversion.
-if command -v cygpath &>/dev/null; then
+if command -v cygpath >/dev/null 2>&1; then
NODE_SCRIPT="$(cygpath -w "$SCRIPT_DIR/scripts/install-apply.js")"
else
NODE_SCRIPT="$SCRIPT_DIR/scripts/install-apply.js"
diff --git a/manifests/context-packs/skill-registry@1.json b/manifests/context-packs/skill-registry@1.json
new file mode 100644
index 000000000..08f0d6351
--- /dev/null
+++ b/manifests/context-packs/skill-registry@1.json
@@ -0,0 +1,9 @@
+{
+ "schemaVersion": 1,
+ "id": "skill-registry@1",
+ "inventory": {
+ "source": "manifests/install-modules.json",
+ "skillsRoot": "skills"
+ },
+ "overrides": []
+}
diff --git a/manifests/context-packs/skill-triggers@1.json b/manifests/context-packs/skill-triggers@1.json
new file mode 100644
index 000000000..757fc0218
--- /dev/null
+++ b/manifests/context-packs/skill-triggers@1.json
@@ -0,0 +1 @@
+{"coverage":{"skills":293,"withTriggers":32},"generatedAt":"2026-09-24T23:51:22.784Z","id":"skill-triggers@1","model":{"effort":null,"id":"hand-seeded","source":"manual-curation-pending-regeneration"},"registryDigest":"05b0793ddaad74a794b6f7fcb4f4704b0c1ddeba4edec8347859eebbc394c47e","schemaVersion":1,"triggers":{"skill:api-connector-builder":["add api integration","new provider connector","match existing integration pattern"],"skill:api-design":["rest endpoint design","pagination api","status codes","api versioning","rate limiting api","resource naming","filtering api","api error responses","offset pagination","limit query parameter","pagination defaults"],"skill:backend-patterns":["express api","node backend architecture","nextjs api routes","server side patterns","data access layer","static file server","url path handling","file server"],"skill:browser-qa":["deployed feature test","visual regression screenshots","core web vitals check","axe accessibility audit","ship do not ship","staging verification"],"skill:canary-watch":["post deploy monitoring","smoke test url","production url check","console errors production","sse stream check","after deploy verification"],"skill:code-tour":["onboarding walkthrough","explain subsystem","architecture tour","pr walkthrough","rca tour"],"skill:coding-standards":["code review standards","naming conventions","readability review","immutability conventions","fix naming typo","export naming","consistent exports"],"skill:content-hash-cache-pattern":["cache file processing","content addressed cache","sha256 hash cache"],"skill:database-migrations":["zero downtime migration","schema change production","add column large table","backfill data","expand contract","concurrent index","migration rollback","prisma migration","django migration"],"skill:deployment-patterns":["ci cd setup","dockerize app","health checks","rollback strategy","production readiness","deploy pipeline","containerize application"],"skill:design-system":["design tokens","visual consistency audit","css custom properties","ui audit","design system bootstrap"],"skill:django-patterns":["django orm","drf api","django rest framework","django caching","django signals","django middleware"],"skill:django-security":["django authentication","csrf protection","sql injection prevention","xss prevention","django deployment security","role based access control","authorization middleware","permissions checks"],"skill:docker-patterns":["dockerfile review","docker compose setup","container security","multi service orchestration"],"skill:error-handling":["error types","retry logic","circuit breaker","user facing errors","exception handling patterns","typed errors","error boundaries","go error handling","custom error class","error codes","config validation"],"skill:evm-token-decimals":["token decimals","wei conversion","erc20 balance off","bridge token precision"],"skill:frontend-a11y":["aria attributes","screen reader support","focus management","semantic html","form labeling","keyboard navigation react","a11y lint errors"],"skill:git-workflow":["merge vs rebase","commit conventions","resolve merge conflict","branching strategy","clean up commits","pull request cleanup","git history tidy"],"skill:hexagonal-architecture":["ports and adapters","dependency injection boundaries","decouple domain from io"],"skill:kubernetes-patterns":["kubernetes manifests","kubectl debugging","pod probes","k8s rbac","autoscaling config","configmap secrets"],"skill:orch-fix-defect":["fix a bug","broken behavior","regression fix","reproduce bug","defect repair"],"skill:postgres-patterns":["slow postgres query","query optimization","index design","rls policies","supabase schema","postgres indexing","database performance","schema design postgres","postgres driver","node postgres","query planner"],"skill:python-patterns":["pythonic code","pep 8","type hints python","python code review","idiomatic python"],"skill:python-testing":["pytest fixtures","mocking python","parametrized tests","coverage python","tdd python"],"skill:redis-patterns":["cache aside pattern","distributed lock","redis rate limiting","cache invalidation"],"skill:regex-vs-llm-structured-text":["parse invoice","extract receipt data","text extraction pipeline","parse form fields","cheap document parser","extract table data","parse log lines","parse access logs","common log format","log line parsing"],"skill:rust-patterns":["rust ownership","borrow checker","rust error handling","traits rust","rust concurrency","idiomatic rust"],"skill:search-first":["find existing library","npm package research","before writing custom code","evaluate existing tools","add dependency research"],"skill:security-review":["security audit","authentication review","sanitize user input","secrets handling","payment security checklist","prevent injection attacks","secure api endpoints","authn authz review","vulnerability checklist","input validation security","parameterized queries","sql injection"],"skill:security-scan":["audit claude config","claudemd security","mcp server audit","agentshield scan","hook configuration audit","settings json security"],"skill:tdd-workflow":["write test first","failing test","red green refactor","test driven development","regression test first","write a regression test"],"skill:verification-loop":["pre pr checks","verification report","quality gates","build lint test coverage","before creating a pr"]},"triggersDigest":"25b97a9e06fc336c7cf95ab854ed1a41033a54bcd6e1fb1cf69dc906332462aa"}
diff --git a/manifests/context-profiles/full@1.json b/manifests/context-profiles/full@1.json
new file mode 100644
index 000000000..df8c92f60
--- /dev/null
+++ b/manifests/context-profiles/full@1.json
@@ -0,0 +1,12 @@
+{
+ "schemaVersion": 1,
+ "id": "full@1",
+ "description": "Proposed complete canonical skill discovery projection. Agents, commands, rules, hooks and tool schemas remain outside this projection; native activation is unobserved.",
+ "registryId": "skill-registry@1",
+ "selection": {
+ "eager": "all",
+ "required": ["skill:configure-ecc", "skill:context-budget", "skill:ecc-guide"],
+ "remainder": "routed"
+ },
+ "budget": { "tokens": 8000, "mode": "report-only" }
+}
diff --git a/manifests/context-profiles/lean@1.json b/manifests/context-profiles/lean@1.json
new file mode 100644
index 000000000..8127d6a21
--- /dev/null
+++ b/manifests/context-profiles/lean@1.json
@@ -0,0 +1,12 @@
+{
+ "schemaVersion": 1,
+ "id": "lean@1",
+ "description": "Proposed three-skill ECC discovery kernel. Remaining skills are routed; this profile does not activate or modify a harness.",
+ "registryId": "skill-registry@1",
+ "selection": {
+ "eager": ["skill:configure-ecc", "skill:context-budget", "skill:ecc-guide"],
+ "required": ["skill:configure-ecc", "skill:context-budget", "skill:ecc-guide"],
+ "remainder": "routed"
+ },
+ "budget": { "tokens": 8000, "mode": "blocking" }
+}
diff --git a/manifests/install-components.json b/manifests/install-components.json
index 8a6205bbd..5f8970c75 100644
--- a/manifests/install-components.json
+++ b/manifests/install-components.json
@@ -677,6 +677,14 @@
"modules": [
"docs-uk-ua"
]
+ },
+ {
+ "id": "locale:pl",
+ "family": "locale",
+ "description": "Polish (pl) translated reference docs installed to ~/.claude/docs/pl/.",
+ "modules": [
+ "docs-pl"
+ ]
}
]
}
diff --git a/manifests/install-modules.json b/manifests/install-modules.json
index 884c7d39d..b61d4e20f 100644
--- a/manifests/install-modules.json
+++ b/manifests/install-modules.json
@@ -173,6 +173,7 @@
"skills/fastapi-patterns",
"skills/frontend-design-direction",
"skills/frontend-patterns",
+ "skills/i18n-sync",
"skills/frontend-slides",
"skills/make-interfaces-feel-better",
"skills/golang-patterns",
@@ -1177,6 +1178,22 @@
"defaultInstall": false,
"cost": "heavy",
"stability": "stable"
+ },
+ {
+ "id": "docs-pl",
+ "kind": "docs",
+ "description": "Polish (pl) getting-started and core-concepts guide with a terminology glossary.",
+ "paths": [
+ "docs/pl"
+ ],
+ "targets": [
+ "claude",
+ "claude-project"
+ ],
+ "dependencies": [],
+ "defaultInstall": false,
+ "cost": "heavy",
+ "stability": "stable"
}
]
}
diff --git a/manifests/pi-core.json b/manifests/pi-core.json
new file mode 100644
index 000000000..cb0f09dc2
--- /dev/null
+++ b/manifests/pi-core.json
@@ -0,0 +1,482 @@
+{
+ "description": "Curated Pi-native profile for ECC: portable engineering skills and pure prompt workflow commands, regenerated into pi/core/ by scripts/build-pi-core.js.",
+ "profile": {
+ "dir": "pi/core",
+ "packageName": "ecc-pi-core",
+ "license": "MIT",
+ "keywords": [
+ "pi-package",
+ "skills"
+ ]
+ },
+ "curationRules": {
+ "include": [
+ "language/framework skills",
+ "testing/TDD",
+ "code review",
+ "security review (non-offensive)",
+ "planning",
+ "refactoring",
+ "docs",
+ "git/PR workflows",
+ "prompt commands that are pure prompt workflows"
+ ],
+ "exclude": [
+ "makes network calls, needs API keys, or downloads at runtime (npx, pip install, curl|sh)",
+ "wraps third-party SaaS (exa, context7, fal, videodb, x-api, social/marketing/SEO/outreach)",
+ "integrates commercial products (ECC Tools, ecc.tools, AgentShield, billing/ops skills)",
+ "depends on Claude-Code-only mechanics (hooks, agent rosters/orch-*, continuous-learning, loops, hookify, cost tracking, session save/resume, gateguard)",
+ "venture-specific (ito-*, prediction-market-*, hermes-*, nasiko-*, openclaw-*, x402)",
+ "niche domain pack (healthcare/HIPAA, supply chain/logistics, homelab, scientific, visa/legal docs) or offensive security (bug bounty)"
+ ]
+ },
+ "skills": {
+ "include": [
+ "accessibility",
+ "agent-architecture-audit",
+ "agent-eval",
+ "agent-harness-construction",
+ "agent-introspection-debugging",
+ "agent-self-evaluation",
+ "agentic-engineering",
+ "ai-first-engineering",
+ "android-clean-architecture",
+ "angular-developer",
+ "api-connector-builder",
+ "api-design",
+ "architecture-decision-records",
+ "backend-patterns",
+ "benchmark",
+ "benchmark-optimization-loop",
+ "blueprint",
+ "bun-runtime",
+ "click-path-audit",
+ "clickhouse-io",
+ "code-tour",
+ "codebase-onboarding",
+ "coding-standards",
+ "compose-multiplatform-patterns",
+ "content-hash-cache-pattern",
+ "contract-first",
+ "cost-aware-llm-pipeline",
+ "council",
+ "cpp-coding-standards",
+ "cpp-testing",
+ "csharp-testing",
+ "dart-flutter-patterns",
+ "dashboard-builder",
+ "data-throughput-accelerator",
+ "database-migrations",
+ "deployment-patterns",
+ "design-system",
+ "dev-team",
+ "django-celery",
+ "django-patterns",
+ "django-security",
+ "django-tdd",
+ "django-verification",
+ "docker-patterns",
+ "dotnet-patterns",
+ "e2e-testing",
+ "error-handling",
+ "fastapi-patterns",
+ "flutter-dart-code-review",
+ "foundation-models-on-device",
+ "frontend-a11y",
+ "frontend-patterns",
+ "fsharp-testing",
+ "git-workflow",
+ "golang-patterns",
+ "golang-testing",
+ "hexagonal-architecture",
+ "inherit-legacy-style",
+ "intent-driven-development",
+ "java-coding-standards",
+ "jpa-patterns",
+ "kotlin-coroutines-flows",
+ "kotlin-exposed-patterns",
+ "kotlin-ktor-patterns",
+ "kotlin-patterns",
+ "kotlin-testing",
+ "kubernetes-patterns",
+ "laravel-patterns",
+ "laravel-security",
+ "laravel-tdd",
+ "laravel-verification",
+ "latency-critical-systems",
+ "liquid-glass-design",
+ "living-docs-governance",
+ "make-interfaces-feel-better",
+ "mcp-server-patterns",
+ "ml-adoption-playbook",
+ "mle-workflow",
+ "motion-advanced",
+ "motion-foundations",
+ "motion-patterns",
+ "mysql-patterns",
+ "nestjs-patterns",
+ "nextjs-turbopack",
+ "nuxt4-patterns",
+ "parallel-execution-optimizer",
+ "perl-patterns",
+ "perl-security",
+ "perl-testing",
+ "postgres-patterns",
+ "prisma-patterns",
+ "product-capability",
+ "product-lens",
+ "production-audit",
+ "python-patterns",
+ "python-testing",
+ "pytorch-patterns",
+ "quarkus-patterns",
+ "quarkus-security",
+ "quarkus-tdd",
+ "quarkus-verification",
+ "rails-patterns",
+ "react-native-patterns",
+ "react-patterns",
+ "react-performance",
+ "react-testing",
+ "redis-patterns",
+ "regex-vs-llm-structured-text",
+ "rust-patterns",
+ "rust-testing",
+ "security-review",
+ "springboot-patterns",
+ "springboot-security",
+ "springboot-tdd",
+ "springboot-verification",
+ "swift-actor-persistence",
+ "swift-concurrency-6-2",
+ "swift-protocol-di-testing",
+ "swiftui-patterns",
+ "tdd-workflow",
+ "verification-loop",
+ "vite-patterns",
+ "vue-patterns"
+ ],
+ "rename": {
+ "council": "ecc-council"
+ },
+ "exclude": {
+ "agent-payment-x402": "venture-specific x402 payments; wallet and network runtime",
+ "agent-sort": "ECC install planner over the full ECC catalog; not portable",
+ "agentic-os": "Claude-Code-only persistent OS mechanics (slash commands, memory, schedules)",
+ "ai-regression-testing": "workflow creates Claude Code custom slash commands (.claude/commands)",
+ "article-writing": "content/marketing writing, not engineering",
+ "automation-audit-ops": "ECC ops audit of hooks/connectors/MCP surfaces",
+ "autonomous-agent-harness": "Claude-Code-only autonomous harness (hooks, scheduling, computer use)",
+ "autonomous-loops": "Claude-Code-only autonomous loops",
+ "benchmark-methodology": "competitive/marketing benchmarking",
+ "blender-motion-state-inspection": "niche 3D/Blender domain pack",
+ "brand-discovery": "brand/marketing content",
+ "brand-voice": "marketing/outreach voice profiling",
+ "browser-qa": "requires a browser automation MCP server at runtime",
+ "canary-watch": "makes network calls to deployed URLs",
+ "carrier-relationship-management": "supply chain/logistics domain pack",
+ "cisco-ios-patterns": "network-device ops niche domain",
+ "ck": "Claude-Code-only persistent memory commands",
+ "claude-devfleet": "multi-agent orchestration via external DevFleet product",
+ "codehealth-mcp": "wraps CodeScene MCP SaaS",
+ "competitive-platform-analysis": "competitive/marketing analysis",
+ "competitive-report-structure": "competitive/marketing reporting",
+ "config-gc": "Claude-Code-only config garbage collection (~/.claude)",
+ "configure-ecc": "ECC-specific setup wizard",
+ "connections-optimizer": "social/outreach (X and LinkedIn)",
+ "content-engine": "social/marketing content system",
+ "context-budget": "Claude-Code-only context window audit",
+ "continuous-agent-loop": "continuous agent loops",
+ "continuous-learning": "Claude-Code-only hooks-based continuous learning (deprecated)",
+ "continuous-learning-v2": "Claude-Code-only hooks-based continuous learning",
+ "cost-tracking": "Claude Code cost tracking",
+ "council-multi-model": "requires external Codex CLI at runtime",
+ "counterparty-channel-discipline": "agent messaging ops policy",
+ "crosspost": "social/marketing distribution",
+ "customer-billing-ops": "billing/ops skill over connected billing tools",
+ "customs-trade-compliance": "customs/trade niche domain",
+ "data-scraper-agent": "scheduled network scraping agent",
+ "deep-research": "requires firecrawl and exa SaaS MCP tools",
+ "defi-amm-security": "crypto/DeFi niche domain",
+ "delivery-gate": "Claude-Code-only stop hook",
+ "dmux-workflows": "multi-agent orchestration via dmux",
+ "documentation-lookup": "wraps Context7 SaaS MCP",
+ "dynamic-workflow-mode": "Claude dynamic workflow mode mechanics",
+ "ecc-guide": "ECC repository self-reference",
+ "ecc-recipes": "ECC command catalog self-reference",
+ "ecc-tools-cost-audit": "ECC Tools commercial billing/ops",
+ "email-ops": "mailbox ops skill",
+ "energy-procurement": "energy procurement niche domain",
+ "enterprise-agent-ops": "agent runtime ops, not portable engineering",
+ "esign-field-placement": "niche e-sign browser automation",
+ "eval-harness": "reads and writes .claude/evals (Claude-Code-only)",
+ "evm-token-decimals": "crypto/EVM niche domain",
+ "exa-search": "wraps Exa SaaS",
+ "fal-ai-media": "wraps fal.ai SaaS",
+ "finance-billing-ops": "billing/ops skill",
+ "flox-environments": "runtime installer flow (curl|sh) for Flox",
+ "frontend-design-direction": "ECC-specific design direction",
+ "frontend-slides": "presentation/content production, not engineering",
+ "gan-style-harness": "Claude-Code-only generator/evaluator harness",
+ "gateguard": "Claude-Code-only PreToolUse gate",
+ "generating-python-installer": "niche Windows installer packaging with runtime downloads",
+ "github-ops": "makes GitHub API calls via gh at runtime",
+ "google-workspace-ops": "wraps Google Workspace SaaS",
+ "growth-log": "ECC learning-log workflow",
+ "healthcare-cdss-patterns": "healthcare niche domain pack",
+ "healthcare-emr-patterns": "healthcare niche domain pack",
+ "healthcare-eval-harness": "healthcare niche domain pack",
+ "healthcare-phi-compliance": "healthcare/PHI niche domain pack",
+ "hermes-imports": "venture-specific hermes-*",
+ "hipaa-compliance": "HIPAA niche domain pack",
+ "homelab-network-readiness": "homelab niche domain pack",
+ "homelab-network-setup": "homelab niche domain pack",
+ "homelab-pihole-dns": "homelab niche domain pack",
+ "homelab-vlan-segmentation": "homelab niche domain pack",
+ "homelab-wireguard-vpn": "homelab niche domain pack",
+ "hookify-rules": "hookify (Claude-Code-only hooks)",
+ "i18n-sync": "built around a third-party npm CLI (locakit) with external source links; not self-contained",
+ "inventory-demand-planning": "supply chain niche domain",
+ "investor-materials": "fundraising/marketing content",
+ "investor-outreach": "fundraising outreach",
+ "ios-icon-gen": "Iconify API network calls at runtime",
+ "iterative-retrieval": "multi-agent subagent context mechanics; links to external social post",
+ "ito-baskets": "venture-specific ito-*",
+ "ito-compute": "venture-specific ito-*",
+ "ito-inference": "venture-specific ito-*",
+ "ito-training": "venture-specific ito-*",
+ "jira-integration": "wraps Jira SaaS API",
+ "knowledge-ops": "ops skill over MCP memory and vector stores",
+ "laravel-plugin-discovery": "wraps LaraPlugins.io SaaS MCP",
+ "lead-intelligence": "sales outreach pipeline",
+ "llm-trading-agent-security": "trading-agent niche domain",
+ "logistics-exception-management": "logistics niche domain",
+ "loop-design-check": "agent loop design mechanics",
+ "mailtrap-email-integration": "wraps Mailtrap SaaS API",
+ "manim-video": "niche video production; pip installs at runtime",
+ "market-research": "web research over network sources",
+ "marketing-campaign": "marketing",
+ "master-agreement-generator": "legal docs niche",
+ "messages-ops": "messaging ops skill",
+ "nanoclaw-repl": "ECC product-specific REPL",
+ "nasiko-control-plane": "venture-specific nasiko-*",
+ "netmiko-ssh-automation": "network-device ops niche; SSH at runtime",
+ "network-bgp-diagnostics": "network ops niche domain",
+ "network-config-validation": "network ops niche domain",
+ "network-interface-health": "network ops niche domain",
+ "nodejs-keccak256": "crypto/EVM niche domain",
+ "nutrient-document-processing": "wraps Nutrient DWS SaaS API",
+ "openclaw-persona-forge": "venture-specific openclaw-*",
+ "opensource-pipeline": "multi-agent roster pipeline",
+ "operator-approval-loop": "ECC ops approval contract",
+ "orch-add-feature": "Claude-Code-only orch-* orchestration",
+ "orch-build-mvp": "Claude-Code-only orch-* orchestration",
+ "orch-change-feature": "Claude-Code-only orch-* orchestration",
+ "orch-fix-defect": "Claude-Code-only orch-* orchestration",
+ "orch-pipeline": "Claude-Code-only orch-* orchestration",
+ "orch-refine-code": "Claude-Code-only orch-* orchestration",
+ "plan-canvas": "Claude-Code-only plan canvas server",
+ "plan-orchestrate": "ECC orchestration prompt generator over the full catalog",
+ "plankton-code-quality": "Claude-Code-only write-time hooks",
+ "prediction-market-oracle-research": "venture-specific prediction-market-*",
+ "prediction-market-risk-review": "venture-specific prediction-market-*",
+ "production-scheduling": "manufacturing niche domain",
+ "project-flow-ops": "ops over GitHub/Linear SaaS",
+ "prompt-optimizer": "ECC command/agent catalog self-reference",
+ "quality-nonconformance": "regulated manufacturing niche",
+ "ralphinho-rfc-pipeline": "multi-agent DAG orchestration",
+ "recsys-pipeline-architect": "installs upstream package via npx skills add",
+ "recursive-decision-ledger": "recursive prompting loops",
+ "remotion-video-creation": "video/media production niche",
+ "repo-scan": "downloads an external skill at runtime",
+ "research-ops": "ECC ops research workflow with network enrichment",
+ "returns-reverse-logistics": "logistics niche domain",
+ "rules-distill": "Claude-Code-only rules distillation mechanics",
+ "safety-guard": "Claude-Code-only PreToolUse hooks",
+ "santa-method": "multi-agent adversarial review roster",
+ "scientific-db-pubmed-database": "scientific niche domain pack",
+ "scientific-db-uspto-database": "scientific niche domain pack",
+ "scientific-pkg-gget": "scientific niche domain pack",
+ "scientific-thinking-literature-review": "scientific niche domain pack",
+ "scientific-thinking-scholar-evaluation": "scientific niche domain pack",
+ "search-first": "network searches (npm/PyPI/GitHub) at runtime",
+ "security-bounty-hunter": "offensive security (bug bounty)",
+ "security-scan": "wraps AgentShield commercial product",
+ "seo": "SEO/marketing",
+ "skill-comply": "runs agent rosters for compliance checks",
+ "skill-scout": "network searches of skill marketplaces",
+ "skill-stocktake": "subagent-based Claude skill audit",
+ "social-graph-ranker": "social graph (X and LinkedIn)",
+ "social-publisher": "wraps SocialClaw SaaS",
+ "strategic-compact": "Claude Code session compaction mechanics",
+ "taste": "media/creative-direction niche pack",
+ "taste-application": "media generation via fal.ai SaaS",
+ "taste-distillation": "media analysis paired with fal.ai SaaS",
+ "tasteforge-video": "media generation workflow over fal.ai SaaS",
+ "team-agent-orchestration": "agent squad orchestration",
+ "team-builder": "Claude agents roster picker",
+ "terminal-opener": "ECC harness utility, not engineering content",
+ "terminal-ops": "ECC ops workflow",
+ "tinystruct-patterns": "niche single-framework pack",
+ "token-budget-advisor": "session/token mechanics",
+ "ui-demo": "Playwright video recording with runtime installs",
+ "ui-to-vue": "runs an npx converter package at runtime",
+ "uncloud": "niche cluster ops",
+ "unified-memory": "ECC memory vault (session save/resume family)",
+ "unified-notifications-ops": "ECC notifications ops",
+ "video-editing": "media production niche",
+ "videodb": "wraps VideoDB SaaS",
+ "visa-doc-translate": "visa/legal docs niche; OCR network calls",
+ "windows-desktop-e2e": "niche Windows desktop automation; pip installs at runtime",
+ "workspace-surface-audit": "ECC harness surface audit",
+ "x-api": "wraps X/Twitter SaaS API"
+ }
+ },
+ "commands": {
+ "include": [
+ "aside.md",
+ "build-fix.md",
+ "code-review.md",
+ "cpp-test.md",
+ "fastapi-review.md",
+ "feature-dev.md",
+ "flutter-test.md",
+ "go-test.md",
+ "gradle-build.md",
+ "kotlin-test.md",
+ "plan-prd.md",
+ "plan.md",
+ "pr.md",
+ "prp-commit.md",
+ "prp-implement.md",
+ "prp-plan.md",
+ "prp-pr.md",
+ "prp-prd.md",
+ "react-test.md",
+ "refactor-clean.md",
+ "rust-test.md",
+ "test-coverage.md",
+ "update-codemaps.md",
+ "update-docs.md"
+ ],
+ "exclude": {
+ "auto-update.md": "ECC self-update/reinstall",
+ "checkpoint.md": "writes .claude/checkpoints.log (Claude-Code-only)",
+ "cost-report.md": "Claude Code cost tracking",
+ "cpp-build.md": "invokes ECC agent roster (cpp-build-resolver)",
+ "cpp-review.md": "invokes ECC agent roster (cpp-reviewer)",
+ "ecc-guide.md": "ECC repository self-reference",
+ "epic-claim.md": "GitHub epic ops over ECC coordination state; network",
+ "epic-decompose.md": "GitHub epic ops over ECC coordination state; network",
+ "epic-publish.md": "GitHub epic ops over ECC coordination state; network",
+ "epic-review.md": "GitHub epic ops over ECC coordination state; network",
+ "epic-sync.md": "GitHub epic ops over ECC coordination state; network",
+ "epic-unblock.md": "GitHub epic ops over ECC coordination state; network",
+ "epic-validate.md": "GitHub epic ops over ECC coordination state; network",
+ "evolve.md": "instincts/continuous-learning mechanics",
+ "flutter-build.md": "invokes ECC agent roster (dart-build-resolver)",
+ "flutter-review.md": "invokes ECC agent roster (flutter-reviewer)",
+ "gan-build.md": "GAN generator/evaluator loop harness",
+ "gan-design.md": "GAN generator/evaluator loop harness",
+ "go-build.md": "invokes ECC agent roster (go-build-resolver)",
+ "go-review.md": "invokes ECC agent roster (go-reviewer)",
+ "harness-audit.md": "ECC harness audit",
+ "hookify-configure.md": "hookify (Claude-Code-only hooks)",
+ "hookify-help.md": "hookify (Claude-Code-only hooks)",
+ "hookify-list.md": "hookify (Claude-Code-only hooks)",
+ "hookify.md": "hookify (Claude-Code-only hooks)",
+ "instinct-export.md": "instincts (continuous-learning)",
+ "instinct-import.md": "instincts (continuous-learning)",
+ "instinct-status.md": "instincts (continuous-learning)",
+ "jira.md": "wraps Jira SaaS API",
+ "kotlin-build.md": "invokes ECC agent roster (kotlin-build-resolver)",
+ "kotlin-review.md": "invokes ECC agent roster (kotlin-reviewer)",
+ "learn-eval.md": "continuous-learning session extraction",
+ "learn.md": "continuous-learning session extraction",
+ "loop-start.md": "autonomous loops",
+ "loop-status.md": "autonomous loops",
+ "marketing-campaign.md": "marketing",
+ "model-route.md": "ECC model routing/cost mechanics",
+ "multi-backend.md": "multi-model orchestration",
+ "multi-execute.md": "multi-model orchestration",
+ "multi-frontend.md": "multi-model orchestration",
+ "multi-plan.md": "multi-model orchestration",
+ "multi-workflow.md": "multi-model orchestration",
+ "orch-add-feature.md": "Claude-Code-only orch-* orchestration",
+ "orch-build-mvp.md": "Claude-Code-only orch-* orchestration",
+ "orch-change-feature.md": "Claude-Code-only orch-* orchestration",
+ "orch-fix-defect.md": "Claude-Code-only orch-* orchestration",
+ "orch-refine-code.md": "Claude-Code-only orch-* orchestration",
+ "orch-review.md": "Claude-Code-only orch-* orchestration",
+ "plan-canvas.md": "Claude-Code-only plan canvas server",
+ "pm2.md": "PM2 runtime process manager ops",
+ "project-init.md": "ECC install-manifest onboarding plan",
+ "projects.md": "instincts (continuous-learning)",
+ "promote.md": "instincts (continuous-learning)",
+ "prune.md": "instincts (continuous-learning)",
+ "python-review.md": "invokes ECC agent roster (python-reviewer)",
+ "quality-gate.md": "drives the ECC PostToolUse formatter hook script",
+ "react-build.md": "invokes ECC agent roster (react-build-resolver)",
+ "react-review.md": "invokes ECC agent roster (react-reviewer)",
+ "resume-session.md": "Claude Code session save/resume (~/.claude/session-data)",
+ "review-pr.md": "invokes ECC agent roster (specialized review agents)",
+ "rust-build.md": "invokes ECC agent roster (rust-build-resolver)",
+ "rust-review.md": "invokes ECC agent roster (rust-reviewer)",
+ "santa-loop.md": "multi-agent adversarial review loop",
+ "save-session.md": "Claude Code session save/resume (~/.claude/session-data)",
+ "security-scan.md": "wraps AgentShield commercial product",
+ "sessions.md": "Claude Code session management",
+ "setup-pm.md": "runs an ECC repo script (scripts/setup-package-manager.js)",
+ "skill-create.md": "Claude Code skill authoring plus instincts",
+ "skill-health.md": "ECC skill analytics dashboard",
+ "vue-review.md": "invokes ECC agent roster (vue-reviewer)"
+ }
+ },
+ "safety": {
+ "semantics": "URLs: documentation hosts in urlAllowlistHosts, placeholder hosts (example.com/org/net and subdomains), and non-FQDN internal hostnames (localhost, docker service names) are allowed; everything else fails. Runtime downloads: pipe-to-shell (curl|sh, wget|sh) and fetch-and-run npx forms (-y/--yes, pkg@version, create-*, degit, \"skills add\") fail. Local-first npx and standard project dependency installation (pip install ) are the reader's own project workflow and are allowed; skills whose own operation downloads tooling are excluded above. Absolute per-user home paths fail; portable tilde references are allowed.",
+ "urlAllowlistHosts": [
+ "aka.ms",
+ "angular.dev",
+ "aws.amazon.com",
+ "bloclibrary.dev",
+ "cli.github.com",
+ "developer.android.com",
+ "developer.apple.com",
+ "developers.cloudflare.com",
+ "dart.dev",
+ "docs.flutter.dev",
+ "external-secrets.io",
+ "github.com",
+ "isocpp.github.io",
+ "kotlinlang.org",
+ "modelcontextprotocol.io",
+ "nextjs.org",
+ "owasp.org",
+ "portswigger.net",
+ "pub.dev",
+ "riverpod.dev",
+ "supabase.com",
+ "vite.dev",
+ "www.cisecurity.org",
+ "www.speedscope.app",
+ "www.terraform.io",
+ "www.w3.org"
+ ],
+ "defaultAllowedHosts": [
+ "localhost",
+ "127.0.0.1",
+ "[::1]",
+ "0.0.0.0",
+ "example.com",
+ "example.org",
+ "example.net"
+ ],
+ "scanAllowlist": [
+ {
+ "path": "skills/tdd-workflow/SKILL.md",
+ "contains": "curl ... | sh` must be rejected",
+ "reason": "anti-pattern warning telling reviewers to reject pipe-to-shell; not an instruction"
+ }
+ ]
+ }
+}
diff --git a/package.json b/package.json
index 6a53ed2f5..57357af0e 100644
--- a/package.json
+++ b/package.json
@@ -84,9 +84,13 @@
"docs/COMMAND-AGENT-MAP.md",
"docs/ROADMAP.md",
"docs/design/ecc-memory-vault.md",
+ "docs/design/context-profiles.md",
+ "docs/design/context-carriers.md",
+ "docs/design/context-profile-delivery.md",
"docs/ja-JP/",
"docs/ko-KR/",
"docs/pt-BR/",
+ "docs/pl/",
"docs/ru/",
"docs/tr/",
"docs/uk-UA/",
@@ -106,6 +110,7 @@
"scripts/ci/scan-supply-chain-iocs.js",
"scripts/ci/supply-chain-advisory-sources.js",
"scripts/consult.js",
+ "scripts/profile.js",
"scripts/auto-update.js",
"scripts/claw.js",
"scripts/control-pane.js",
@@ -248,6 +253,7 @@
"skills/homelab-network-readiness/",
"skills/homelab-network-setup/",
"skills/hookify-rules/",
+ "skills/i18n-sync/",
"skills/inventory-demand-planning/",
"skills/ito-baskets/",
"skills/ito-compute/",
@@ -468,6 +474,7 @@
"scripts": {
"welcome": "echo '\\n ecc-universal installed!\\n Run: ecc typescript\\n Compat: ecc-install typescript\\n Docs: https://github.com/affaan-m/ECC\\n Run or self-host any open-source model.\\n Compute: Itô is the preferred compute sponsor — https://compute.itomarkets.com\\n Any GPU provider works. This sponsorship link is passive: it does not invoke an RFQ, reserve capacity, provision compute, or configure serving.\\n Separately, the opt-in ecc ito find bridge invokes the explicitly configured canonical Itô CLI and submits a live authenticated RFQ; it does not reserve capacity.\\n Managed inference through Itô is not live yet.\\n'",
"catalog:check": "node scripts/ci/catalog.js --text",
+ "context-profiles:check": "node scripts/ci/validate-context-profiles.js",
"catalog:sync": "node scripts/ci/catalog.js --write --text",
"command-registry:generate": "node scripts/ci/generate-command-registry.js",
"command-registry:write": "node scripts/ci/generate-command-registry.js --write",
@@ -491,7 +498,7 @@
"orchestrate:status": "node scripts/orchestration-status.js",
"orchestrate:worker": "bash scripts/orchestrate-codex-worker.sh",
"orchestrate:tmux": "node scripts/orchestrate-worktrees.js",
- "test": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/check-hooks-schema-keys.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && npm run command-registry:check && node tests/run-all.js",
+ "test": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/check-hooks-schema-keys.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-context-profiles.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && npm run command-registry:check && node tests/run-all.js",
"coverage": "c8 --all --include=\"scripts/**/*.js\" --include=\"scripts/**/*.mjs\" --check-coverage --lines 80 --functions 80 --branches 79 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js",
"build:opencode": "node scripts/build-opencode.js",
"prepack": "npm run build:opencode",
diff --git a/pi/core/CURATION.md b/pi/core/CURATION.md
new file mode 100644
index 000000000..df1345897
--- /dev/null
+++ b/pi/core/CURATION.md
@@ -0,0 +1,270 @@
+# Curation
+
+pi/core includes 123 of 293 skills and 24 of 94 commands from the root of ECC.
+Everything excluded is listed here with its reason.
+
+## Rules
+
+Include: language/framework skills; testing/TDD; code review; security review (non-offensive); planning; refactoring; docs; git/PR workflows; prompt commands that are pure prompt workflows.
+
+Exclude anything that:
+- makes network calls, needs API keys, or downloads at runtime (npx, pip install, curl|sh)
+- wraps third-party SaaS (exa, context7, fal, videodb, x-api, social/marketing/SEO/outreach)
+- integrates commercial products (ECC Tools, ecc.tools, AgentShield, billing/ops skills)
+- depends on Claude-Code-only mechanics (hooks, agent rosters/orch-*, continuous-learning, loops, hookify, cost tracking, session save/resume, gateguard)
+- venture-specific (ito-*, prediction-market-*, hermes-*, nasiko-*, openclaw-*, x402)
+- niche domain pack (healthcare/HIPAA, supply chain/logistics, homelab, scientific, visa/legal docs) or offensive security (bug bounty)
+
+## Excluded skills
+
+| Skill | Reason |
+|---|---|
+| `agent-payment-x402` | venture-specific x402 payments; wallet and network runtime |
+| `agent-sort` | ECC install planner over the full ECC catalog; not portable |
+| `agentic-os` | Claude-Code-only persistent OS mechanics (slash commands, memory, schedules) |
+| `ai-regression-testing` | workflow creates Claude Code custom slash commands (.claude/commands) |
+| `article-writing` | content/marketing writing, not engineering |
+| `automation-audit-ops` | ECC ops audit of hooks/connectors/MCP surfaces |
+| `autonomous-agent-harness` | Claude-Code-only autonomous harness (hooks, scheduling, computer use) |
+| `autonomous-loops` | Claude-Code-only autonomous loops |
+| `benchmark-methodology` | competitive/marketing benchmarking |
+| `blender-motion-state-inspection` | niche 3D/Blender domain pack |
+| `brand-discovery` | brand/marketing content |
+| `brand-voice` | marketing/outreach voice profiling |
+| `browser-qa` | requires a browser automation MCP server at runtime |
+| `canary-watch` | makes network calls to deployed URLs |
+| `carrier-relationship-management` | supply chain/logistics domain pack |
+| `cisco-ios-patterns` | network-device ops niche domain |
+| `ck` | Claude-Code-only persistent memory commands |
+| `claude-devfleet` | multi-agent orchestration via external DevFleet product |
+| `codehealth-mcp` | wraps CodeScene MCP SaaS |
+| `competitive-platform-analysis` | competitive/marketing analysis |
+| `competitive-report-structure` | competitive/marketing reporting |
+| `config-gc` | Claude-Code-only config garbage collection (~/.claude) |
+| `configure-ecc` | ECC-specific setup wizard |
+| `connections-optimizer` | social/outreach (X and LinkedIn) |
+| `content-engine` | social/marketing content system |
+| `context-budget` | Claude-Code-only context window audit |
+| `continuous-agent-loop` | continuous agent loops |
+| `continuous-learning` | Claude-Code-only hooks-based continuous learning (deprecated) |
+| `continuous-learning-v2` | Claude-Code-only hooks-based continuous learning |
+| `cost-tracking` | Claude Code cost tracking |
+| `council-multi-model` | requires external Codex CLI at runtime |
+| `counterparty-channel-discipline` | agent messaging ops policy |
+| `crosspost` | social/marketing distribution |
+| `customer-billing-ops` | billing/ops skill over connected billing tools |
+| `customs-trade-compliance` | customs/trade niche domain |
+| `data-scraper-agent` | scheduled network scraping agent |
+| `deep-research` | requires firecrawl and exa SaaS MCP tools |
+| `defi-amm-security` | crypto/DeFi niche domain |
+| `delivery-gate` | Claude-Code-only stop hook |
+| `dmux-workflows` | multi-agent orchestration via dmux |
+| `documentation-lookup` | wraps Context7 SaaS MCP |
+| `dynamic-workflow-mode` | Claude dynamic workflow mode mechanics |
+| `ecc-guide` | ECC repository self-reference |
+| `ecc-recipes` | ECC command catalog self-reference |
+| `ecc-tools-cost-audit` | ECC Tools commercial billing/ops |
+| `email-ops` | mailbox ops skill |
+| `energy-procurement` | energy procurement niche domain |
+| `enterprise-agent-ops` | agent runtime ops, not portable engineering |
+| `esign-field-placement` | niche e-sign browser automation |
+| `eval-harness` | reads and writes .claude/evals (Claude-Code-only) |
+| `evm-token-decimals` | crypto/EVM niche domain |
+| `exa-search` | wraps Exa SaaS |
+| `fal-ai-media` | wraps fal.ai SaaS |
+| `finance-billing-ops` | billing/ops skill |
+| `flox-environments` | runtime installer flow (curl\|sh) for Flox |
+| `frontend-design-direction` | ECC-specific design direction |
+| `frontend-slides` | presentation/content production, not engineering |
+| `gan-style-harness` | Claude-Code-only generator/evaluator harness |
+| `gateguard` | Claude-Code-only PreToolUse gate |
+| `generating-python-installer` | niche Windows installer packaging with runtime downloads |
+| `github-ops` | makes GitHub API calls via gh at runtime |
+| `google-workspace-ops` | wraps Google Workspace SaaS |
+| `growth-log` | ECC learning-log workflow |
+| `healthcare-cdss-patterns` | healthcare niche domain pack |
+| `healthcare-emr-patterns` | healthcare niche domain pack |
+| `healthcare-eval-harness` | healthcare niche domain pack |
+| `healthcare-phi-compliance` | healthcare/PHI niche domain pack |
+| `hermes-imports` | venture-specific hermes-* |
+| `hipaa-compliance` | HIPAA niche domain pack |
+| `homelab-network-readiness` | homelab niche domain pack |
+| `homelab-network-setup` | homelab niche domain pack |
+| `homelab-pihole-dns` | homelab niche domain pack |
+| `homelab-vlan-segmentation` | homelab niche domain pack |
+| `homelab-wireguard-vpn` | homelab niche domain pack |
+| `hookify-rules` | hookify (Claude-Code-only hooks) |
+| `i18n-sync` | built around a third-party npm CLI (locakit) with external source links; not self-contained |
+| `inventory-demand-planning` | supply chain niche domain |
+| `investor-materials` | fundraising/marketing content |
+| `investor-outreach` | fundraising outreach |
+| `ios-icon-gen` | Iconify API network calls at runtime |
+| `iterative-retrieval` | multi-agent subagent context mechanics; links to external social post |
+| `ito-baskets` | venture-specific ito-* |
+| `ito-compute` | venture-specific ito-* |
+| `ito-inference` | venture-specific ito-* |
+| `ito-training` | venture-specific ito-* |
+| `jira-integration` | wraps Jira SaaS API |
+| `knowledge-ops` | ops skill over MCP memory and vector stores |
+| `laravel-plugin-discovery` | wraps LaraPlugins.io SaaS MCP |
+| `lead-intelligence` | sales outreach pipeline |
+| `llm-trading-agent-security` | trading-agent niche domain |
+| `logistics-exception-management` | logistics niche domain |
+| `loop-design-check` | agent loop design mechanics |
+| `mailtrap-email-integration` | wraps Mailtrap SaaS API |
+| `manim-video` | niche video production; pip installs at runtime |
+| `market-research` | web research over network sources |
+| `marketing-campaign` | marketing |
+| `master-agreement-generator` | legal docs niche |
+| `messages-ops` | messaging ops skill |
+| `nanoclaw-repl` | ECC product-specific REPL |
+| `nasiko-control-plane` | venture-specific nasiko-* |
+| `netmiko-ssh-automation` | network-device ops niche; SSH at runtime |
+| `network-bgp-diagnostics` | network ops niche domain |
+| `network-config-validation` | network ops niche domain |
+| `network-interface-health` | network ops niche domain |
+| `nodejs-keccak256` | crypto/EVM niche domain |
+| `nutrient-document-processing` | wraps Nutrient DWS SaaS API |
+| `openclaw-persona-forge` | venture-specific openclaw-* |
+| `opensource-pipeline` | multi-agent roster pipeline |
+| `operator-approval-loop` | ECC ops approval contract |
+| `orch-add-feature` | Claude-Code-only orch-* orchestration |
+| `orch-build-mvp` | Claude-Code-only orch-* orchestration |
+| `orch-change-feature` | Claude-Code-only orch-* orchestration |
+| `orch-fix-defect` | Claude-Code-only orch-* orchestration |
+| `orch-pipeline` | Claude-Code-only orch-* orchestration |
+| `orch-refine-code` | Claude-Code-only orch-* orchestration |
+| `plan-canvas` | Claude-Code-only plan canvas server |
+| `plan-orchestrate` | ECC orchestration prompt generator over the full catalog |
+| `plankton-code-quality` | Claude-Code-only write-time hooks |
+| `prediction-market-oracle-research` | venture-specific prediction-market-* |
+| `prediction-market-risk-review` | venture-specific prediction-market-* |
+| `production-scheduling` | manufacturing niche domain |
+| `project-flow-ops` | ops over GitHub/Linear SaaS |
+| `prompt-optimizer` | ECC command/agent catalog self-reference |
+| `quality-nonconformance` | regulated manufacturing niche |
+| `ralphinho-rfc-pipeline` | multi-agent DAG orchestration |
+| `recsys-pipeline-architect` | installs upstream package via npx skills add |
+| `recursive-decision-ledger` | recursive prompting loops |
+| `remotion-video-creation` | video/media production niche |
+| `repo-scan` | downloads an external skill at runtime |
+| `research-ops` | ECC ops research workflow with network enrichment |
+| `returns-reverse-logistics` | logistics niche domain |
+| `rules-distill` | Claude-Code-only rules distillation mechanics |
+| `safety-guard` | Claude-Code-only PreToolUse hooks |
+| `santa-method` | multi-agent adversarial review roster |
+| `scientific-db-pubmed-database` | scientific niche domain pack |
+| `scientific-db-uspto-database` | scientific niche domain pack |
+| `scientific-pkg-gget` | scientific niche domain pack |
+| `scientific-thinking-literature-review` | scientific niche domain pack |
+| `scientific-thinking-scholar-evaluation` | scientific niche domain pack |
+| `search-first` | network searches (npm/PyPI/GitHub) at runtime |
+| `security-bounty-hunter` | offensive security (bug bounty) |
+| `security-scan` | wraps AgentShield commercial product |
+| `seo` | SEO/marketing |
+| `skill-comply` | runs agent rosters for compliance checks |
+| `skill-scout` | network searches of skill marketplaces |
+| `skill-stocktake` | subagent-based Claude skill audit |
+| `social-graph-ranker` | social graph (X and LinkedIn) |
+| `social-publisher` | wraps SocialClaw SaaS |
+| `strategic-compact` | Claude Code session compaction mechanics |
+| `taste` | media/creative-direction niche pack |
+| `taste-application` | media generation via fal.ai SaaS |
+| `taste-distillation` | media analysis paired with fal.ai SaaS |
+| `tasteforge-video` | media generation workflow over fal.ai SaaS |
+| `team-agent-orchestration` | agent squad orchestration |
+| `team-builder` | Claude agents roster picker |
+| `terminal-opener` | ECC harness utility, not engineering content |
+| `terminal-ops` | ECC ops workflow |
+| `tinystruct-patterns` | niche single-framework pack |
+| `token-budget-advisor` | session/token mechanics |
+| `ui-demo` | Playwright video recording with runtime installs |
+| `ui-to-vue` | runs an npx converter package at runtime |
+| `uncloud` | niche cluster ops |
+| `unified-memory` | ECC memory vault (session save/resume family) |
+| `unified-notifications-ops` | ECC notifications ops |
+| `video-editing` | media production niche |
+| `videodb` | wraps VideoDB SaaS |
+| `visa-doc-translate` | visa/legal docs niche; OCR network calls |
+| `windows-desktop-e2e` | niche Windows desktop automation; pip installs at runtime |
+| `workspace-surface-audit` | ECC harness surface audit |
+| `x-api` | wraps X/Twitter SaaS API |
+
+## Excluded commands
+
+| Command | Reason |
+|---|---|
+| `auto-update` | ECC self-update/reinstall |
+| `checkpoint` | writes .claude/checkpoints.log (Claude-Code-only) |
+| `cost-report` | Claude Code cost tracking |
+| `cpp-build` | invokes ECC agent roster (cpp-build-resolver) |
+| `cpp-review` | invokes ECC agent roster (cpp-reviewer) |
+| `ecc-guide` | ECC repository self-reference |
+| `epic-claim` | GitHub epic ops over ECC coordination state; network |
+| `epic-decompose` | GitHub epic ops over ECC coordination state; network |
+| `epic-publish` | GitHub epic ops over ECC coordination state; network |
+| `epic-review` | GitHub epic ops over ECC coordination state; network |
+| `epic-sync` | GitHub epic ops over ECC coordination state; network |
+| `epic-unblock` | GitHub epic ops over ECC coordination state; network |
+| `epic-validate` | GitHub epic ops over ECC coordination state; network |
+| `evolve` | instincts/continuous-learning mechanics |
+| `flutter-build` | invokes ECC agent roster (dart-build-resolver) |
+| `flutter-review` | invokes ECC agent roster (flutter-reviewer) |
+| `gan-build` | GAN generator/evaluator loop harness |
+| `gan-design` | GAN generator/evaluator loop harness |
+| `go-build` | invokes ECC agent roster (go-build-resolver) |
+| `go-review` | invokes ECC agent roster (go-reviewer) |
+| `harness-audit` | ECC harness audit |
+| `hookify-configure` | hookify (Claude-Code-only hooks) |
+| `hookify-help` | hookify (Claude-Code-only hooks) |
+| `hookify-list` | hookify (Claude-Code-only hooks) |
+| `hookify` | hookify (Claude-Code-only hooks) |
+| `instinct-export` | instincts (continuous-learning) |
+| `instinct-import` | instincts (continuous-learning) |
+| `instinct-status` | instincts (continuous-learning) |
+| `jira` | wraps Jira SaaS API |
+| `kotlin-build` | invokes ECC agent roster (kotlin-build-resolver) |
+| `kotlin-review` | invokes ECC agent roster (kotlin-reviewer) |
+| `learn-eval` | continuous-learning session extraction |
+| `learn` | continuous-learning session extraction |
+| `loop-start` | autonomous loops |
+| `loop-status` | autonomous loops |
+| `marketing-campaign` | marketing |
+| `model-route` | ECC model routing/cost mechanics |
+| `multi-backend` | multi-model orchestration |
+| `multi-execute` | multi-model orchestration |
+| `multi-frontend` | multi-model orchestration |
+| `multi-plan` | multi-model orchestration |
+| `multi-workflow` | multi-model orchestration |
+| `orch-add-feature` | Claude-Code-only orch-* orchestration |
+| `orch-build-mvp` | Claude-Code-only orch-* orchestration |
+| `orch-change-feature` | Claude-Code-only orch-* orchestration |
+| `orch-fix-defect` | Claude-Code-only orch-* orchestration |
+| `orch-refine-code` | Claude-Code-only orch-* orchestration |
+| `orch-review` | Claude-Code-only orch-* orchestration |
+| `plan-canvas` | Claude-Code-only plan canvas server |
+| `pm2` | PM2 runtime process manager ops |
+| `project-init` | ECC install-manifest onboarding plan |
+| `projects` | instincts (continuous-learning) |
+| `promote` | instincts (continuous-learning) |
+| `prune` | instincts (continuous-learning) |
+| `python-review` | invokes ECC agent roster (python-reviewer) |
+| `quality-gate` | drives the ECC PostToolUse formatter hook script |
+| `react-build` | invokes ECC agent roster (react-build-resolver) |
+| `react-review` | invokes ECC agent roster (react-reviewer) |
+| `resume-session` | Claude Code session save/resume (~/.claude/session-data) |
+| `review-pr` | invokes ECC agent roster (specialized review agents) |
+| `rust-build` | invokes ECC agent roster (rust-build-resolver) |
+| `rust-review` | invokes ECC agent roster (rust-reviewer) |
+| `santa-loop` | multi-agent adversarial review loop |
+| `save-session` | Claude Code session save/resume (~/.claude/session-data) |
+| `security-scan` | wraps AgentShield commercial product |
+| `sessions` | Claude Code session management |
+| `setup-pm` | runs an ECC repo script (scripts/setup-package-manager.js) |
+| `skill-create` | Claude Code skill authoring plus instincts |
+| `skill-health` | ECC skill analytics dashboard |
+| `vue-review` | invokes ECC agent roster (vue-reviewer) |
+
+## Renames
+
+- `council` is shipped as `ecc-council` inside pi/core (the root skill keeps its original name).
diff --git a/pi/core/LICENSE b/pi/core/LICENSE
new file mode 100644
index 000000000..b832b6f64
--- /dev/null
+++ b/pi/core/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Affaan Mustafa
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/pi/core/README.md b/pi/core/README.md
new file mode 100644
index 000000000..f3df41414
--- /dev/null
+++ b/pi/core/README.md
@@ -0,0 +1,40 @@
+# ecc-pi-core
+
+A curated, Pi-native profile of ECC (Everything Claude Code): 123 portable
+engineering skills and 24 pure prompt-workflow commands, with no extensions,
+no hooks, no runtime downloads, and no network or SaaS dependencies.
+
+## Contents
+
+- `skills/` - language, framework, testing/TDD, code review, security review,
+ planning, refactoring, docs, and git/PR workflow skills.
+- `commands/` - prompt commands that are pure prompt workflows.
+- `CURATION.md` - every excluded skill and command with its reason.
+
+## Use
+
+Copy this directory into your project (or pin a release tarball) and load it with the
+Pi coding agent:
+
+```sh
+pi --no-extensions --extension pi/core
+```
+
+Offline load test (as run in CI):
+
+```sh
+PI_OFFLINE=1 pi --offline --mode rpc --no-session --no-context-files --no-extensions \
+ --extension pi/core /dev/null
+```
+
+## Regenerate
+
+`pi/core` is generated from `manifests/pi-core.json` and committed so release
+tarballs contain it verbatim. After changing the manifest or any included source
+content, run:
+
+```sh
+node scripts/build-pi-core.js
+```
+
+and commit the result. CI verifies the committed profile is up to date.
diff --git a/pi/core/commands/aside.md b/pi/core/commands/aside.md
new file mode 100644
index 000000000..9fa0881d1
--- /dev/null
+++ b/pi/core/commands/aside.md
@@ -0,0 +1,164 @@
+---
+description: Answer a quick side question without interrupting or losing context from the current task. Resume work automatically after answering.
+---
+
+# Aside Command
+
+Ask a question mid-task and get an immediate, focused answer — then continue right where you left off. The current task, files, and context are never modified.
+
+## When to Use
+
+- You're curious about something while Claude is working and don't want to lose momentum
+- You need a quick explanation of code Claude is currently editing
+- You want a second opinion or clarification on a decision without derailing the task
+- You need to understand an error, concept, or pattern before Claude proceeds
+- You want to ask something unrelated to the current task without starting a new session
+
+## Usage
+
+```
+/aside
+/aside what does this function actually return?
+/aside is this pattern thread-safe?
+/aside why are we using X instead of Y here?
+/aside what's the difference between foo() and bar()?
+/aside should we be worried about the N+1 query we just added?
+```
+
+## Process
+
+### Step 1: Freeze the current task state
+
+Before answering anything, mentally note:
+- What is the active task? (what file, feature, or problem was being worked on)
+- What step was in progress at the moment `/aside` was invoked?
+- What was about to happen next?
+
+Do NOT touch, edit, create, or delete any files during the aside.
+
+### Step 2: Answer the question directly
+
+Answer the question in the most concise form that is still complete and useful.
+
+- Lead with the answer, not the reasoning
+- Keep it short — if a full explanation is needed, offer to go deeper after the task
+- If the question is about the current file or code being worked on, reference it precisely (file path and line number if relevant)
+- If answering requires reading a file, read it — but read only, never write
+
+Format the response as:
+
+```
+ASIDE: [restate the question briefly]
+
+[Your answer here]
+
+— Back to task: [one-line description of what was being done]
+```
+
+### Step 3: Resume the main task
+
+After delivering the answer, immediately continue the active task from the exact point it was paused. Do not ask for permission to resume unless the aside answer revealed a blocker or a reason to reconsider the current approach (see Edge Cases).
+
+---
+
+## Edge Cases
+
+**No question provided (`/aside` with nothing after it):**
+Respond:
+```
+ASIDE: no question provided
+
+What would you like to know? (ask your question and I'll answer without losing the current task context)
+
+— Back to task: [one-line description of what was being done]
+```
+
+**Question reveals a potential problem with the current task:**
+Flag it clearly before resuming:
+```
+ASIDE: [answer]
+
+WARNING: Note: This answer suggests [issue] with the current approach. Want to address this before continuing, or proceed as planned?
+```
+Wait for the user's decision before resuming.
+
+**Question is actually a task redirect (not a side question):**
+If the question implies changing what is being built (e.g., `/aside actually, let's use Redis instead`), clarify:
+```
+ASIDE: That sounds like a direction change, not just a side question.
+Do you want to:
+ (a) Answer this as information only and keep the current plan
+ (b) Pause the current task and change approach
+```
+Wait for the user's answer — do not make assumptions.
+
+**Question is about the currently open file or code:**
+Answer from the live context. If the file was read earlier in the session, reference it directly. If not, read it now (read-only) and answer with a file:line reference.
+
+**No active task (nothing in progress when `/aside` is invoked):**
+Still use the standard wrapper so the response shape stays consistent:
+```
+ASIDE: [restate the question briefly]
+
+[Your answer here]
+
+— Back to task: no active task to resume
+```
+
+**Question requires a long answer:**
+Give the essential answer concisely, then offer:
+```
+That's the short version. Want a deeper explanation after we finish [current task]?
+```
+
+**Multiple `/aside` questions in a row:**
+Answer each one in sequence. After the last answer, resume the main task. Do not lose task state across a chain of asides.
+
+**Aside answer implies a code change is needed:**
+Note the change needed but do not make it during the aside:
+```
+ASIDE: [answer]
+
+ Worth fixing: [what should be changed]. I'll flag this after the current task unless you want to address it now.
+```
+
+**Question is ambiguous or too vague:**
+Ask one clarifying question — the shortest question that gets the information needed to answer. Do not ask multiple questions.
+
+---
+
+## Example Output
+
+```
+User: /aside what does fetchWithRetry() actually do?
+
+ASIDE: what does fetchWithRetry() do?
+
+fetchWithRetry() (src/api/retry.ts:12) attempts the request up to 3 times with
+exponential backoff (250ms → 500ms → 1s). It only retries on 5xx responses and
+network errors — 4xx errors are treated as final and not retried.
+
+— Back to task: refactoring the auth middleware in src/middleware/auth.ts
+```
+
+```
+User: /aside is the approach we're taking thread-safe?
+
+ASIDE: is the current approach thread-safe?
+
+No — the shared cache object in src/cache/store.ts:34 is mutated without locking.
+Under concurrent requests this is a race condition. It's low risk in a single-process
+Node.js server but would be a real problem with worker threads or clustering.
+
+WARNING: Note: This could affect the feature we're building. Want to address this now or continue and fix it in a follow-up?
+```
+
+---
+
+## Notes
+
+- Never modify files during an aside — read-only access only
+- The aside is a conversation pause, not a new task — the original task must always resume
+- Keep answers focused: the goal is to unblock the user quickly, not to deliver a lecture
+- If an aside sparks a larger discussion, finish the current task first unless the aside reveals a blocker
+- Asides are not saved to session files unless explicitly relevant to the task outcome
diff --git a/pi/core/commands/build-fix.md b/pi/core/commands/build-fix.md
new file mode 100644
index 000000000..568351bbc
--- /dev/null
+++ b/pi/core/commands/build-fix.md
@@ -0,0 +1,66 @@
+---
+description: Detect the project build system and incrementally fix build/type errors with minimal safe changes.
+---
+
+# Build and Fix
+
+Incrementally fix build and type errors with minimal, safe changes.
+
+## Step 1: Detect Build System
+
+Identify the project's build tool and run the build:
+
+| Indicator | Build Command |
+|-----------|---------------|
+| `package.json` with `build` script | `npm run build` or `pnpm build` |
+| `tsconfig.json` (TypeScript only) | `npx tsc --noEmit` |
+| `Cargo.toml` | `cargo build 2>&1` |
+| `pom.xml` | `mvn compile` |
+| `build.gradle` | `./gradlew compileJava` |
+| `go.mod` | `go build ./...` |
+| `pyproject.toml` | `python -m compileall -q .` or `mypy .` |
+
+## Step 2: Parse and Group Errors
+
+1. Run the build command and capture stderr
+2. Group errors by file path
+3. Sort by dependency order (fix imports/types before logic errors)
+4. Count total errors for progress tracking
+
+## Step 3: Fix Loop (One Error at a Time)
+
+For each error:
+
+1. **Read the file** — Use Read tool to see error context (10 lines around the error)
+2. **Diagnose** — Identify root cause (missing import, wrong type, syntax error)
+3. **Fix minimally** — Use Edit tool for the smallest change that resolves the error
+4. **Re-run build** — Verify the error is gone and no new errors introduced
+5. **Move to next** — Continue with remaining errors
+
+## Step 4: Guardrails
+
+Stop and ask the user if:
+- A fix introduces **more errors than it resolves**
+- The **same error persists after 3 attempts** (likely a deeper issue)
+- The fix requires **architectural changes** (not just a build fix)
+- Build errors stem from **missing dependencies** (need `npm install`, `cargo add`, etc.)
+
+## Step 5: Summary
+
+Show results:
+- Errors fixed (with file paths)
+- Errors remaining (if any)
+- New errors introduced (should be zero)
+- Suggested next steps for unresolved issues
+
+## Recovery Strategies
+
+| Situation | Action |
+|-----------|--------|
+| Missing module/import | Check if package is installed; suggest install command |
+| Type mismatch | Read both type definitions; fix the narrower type |
+| Circular dependency | Identify cycle with import graph; suggest extraction |
+| Version conflict | Check `package.json` / `Cargo.toml` for version constraints |
+| Build tool misconfiguration | Read config file; compare with working defaults |
+
+Fix one error at a time for safety. Prefer minimal diffs over refactoring.
diff --git a/pi/core/commands/code-review.md b/pi/core/commands/code-review.md
new file mode 100644
index 000000000..3e5e8ee61
--- /dev/null
+++ b/pi/core/commands/code-review.md
@@ -0,0 +1,289 @@
+---
+description: Code review — local uncommitted changes or GitHub PR (pass PR number/URL for PR mode). Use for a step-by-step PRP-style checklist review; for a multi-agent pass use /review-pr, and for the adversarially-verified Workflow pass use /orch-review.
+argument-hint: [pr-number | pr-url | blank for local review]
+---
+
+# Code Review
+
+> PR review mode adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
+
+**Input**: $ARGUMENTS
+
+---
+
+## Mode Selection
+
+If `$ARGUMENTS` contains a PR number, PR URL, or `--pr`:
+→ Jump to **PR Review Mode** below.
+
+Otherwise:
+→ Use **Local Review Mode**.
+
+---
+
+## Local Review Mode
+
+Comprehensive security and quality review of uncommitted changes.
+
+### Phase 1 — GATHER
+
+```bash
+git diff --name-only HEAD
+```
+
+If no changed files, stop: "Nothing to review."
+
+### Phase 2 — REVIEW
+
+Read each changed file in full. Check for:
+
+**Security Issues (CRITICAL):**
+- Hardcoded credentials, API keys, tokens
+- SQL injection vulnerabilities
+- XSS vulnerabilities
+- Missing input validation
+- Insecure dependencies
+- Path traversal risks
+
+**Code Quality (HIGH):**
+- Functions > 50 lines
+- Files > 800 lines
+- Nesting depth > 4 levels
+- Missing error handling
+- console.log statements
+- TODO/FIXME comments
+- Missing JSDoc for public APIs
+
+**Best Practices (MEDIUM):**
+- Mutation patterns (use immutable instead)
+- Emoji usage in code/comments
+- Missing tests for new code
+- Accessibility issues (a11y)
+
+### Phase 3 — REPORT
+
+Generate report with:
+- Severity: CRITICAL, HIGH, MEDIUM, LOW
+- File location and line numbers
+- Issue description
+- Suggested fix
+
+Block commit if CRITICAL or HIGH issues found.
+Never approve code with security vulnerabilities.
+
+---
+
+## PR Review Mode
+
+Comprehensive GitHub PR review — fetches diff, reads full files, runs validation, posts review.
+
+### Phase 1 — FETCH
+
+Parse input to determine PR:
+
+| Input | Action |
+|---|---|
+| Number (e.g. `42`) | Use as PR number |
+| URL (`github.com/.../pull/42`) | Extract PR number |
+| Branch name | Find PR via `gh pr list --head ` |
+
+```bash
+gh pr view --json number,title,body,author,baseRefName,headRefName,changedFiles,additions,deletions
+gh pr diff
+```
+
+If PR not found, stop with error. Store PR metadata for later phases.
+
+### Phase 2 — CONTEXT
+
+Build review context:
+
+1. **Project rules** — Read `CLAUDE.md`, `.claude/docs/`, and any contributing guidelines
+2. **Planning artifacts** — Check `.claude/prds/`, `.claude/plans/`, `.claude/reviews/`, and legacy `.claude/PRPs/{prds,plans,reports,reviews}/` for context related to this PR
+3. **PR intent** — Parse PR description for goals, linked issues, test plans
+4. **Changed files** — List all modified files and categorize by type (source, test, config, docs)
+
+### Phase 3 — REVIEW
+
+Read each changed file **in full** (not just the diff hunks — you need surrounding context).
+
+For PR reviews, fetch the full file contents at the PR head revision:
+```bash
+gh pr diff --name-only | while IFS= read -r file; do
+ gh api "repos/{owner}/{repo}/contents/$file?ref=" --jq '.content' | base64 -d
+done
+```
+
+Apply the review checklist across 7 categories:
+
+| Category | What to Check |
+|---|---|
+| **Correctness** | Logic errors, off-by-ones, null handling, edge cases, race conditions |
+| **Type Safety** | Type mismatches, unsafe casts, `any` usage, missing generics |
+| **Pattern Compliance** | Matches project conventions (naming, file structure, error handling, imports) |
+| **Security** | Injection, auth gaps, secret exposure, SSRF, path traversal, XSS |
+| **Performance** | N+1 queries, missing indexes, unbounded loops, memory leaks, large payloads |
+| **Completeness** | Missing tests, missing error handling, incomplete migrations, missing docs |
+| **Maintainability** | Dead code, magic numbers, deep nesting, unclear naming, missing types |
+
+Assign severity to each finding:
+
+| Severity | Meaning | Action |
+|---|---|---|
+| **CRITICAL** | Security vulnerability or data loss risk | Must fix before merge |
+| **HIGH** | Bug or logic error likely to cause issues | Should fix before merge |
+| **MEDIUM** | Code quality issue or missing best practice | Fix recommended |
+| **LOW** | Style nit or minor suggestion | Optional |
+
+### Phase 4 — VALIDATE
+
+Run available validation commands:
+
+Detect the project type from config files (`package.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, etc.), then run the appropriate commands:
+
+**Node.js / TypeScript** (has `package.json`):
+```bash
+npm run typecheck 2>/dev/null || npx tsc --noEmit 2>/dev/null # Type check
+npm run lint # Lint
+npm test # Tests
+npm run build # Build
+```
+
+**Rust** (has `Cargo.toml`):
+```bash
+cargo clippy -- -D warnings # Lint
+cargo test # Tests
+cargo build # Build
+```
+
+**Go** (has `go.mod`):
+```bash
+go vet ./... # Lint
+go test ./... # Tests
+go build ./... # Build
+```
+
+**Python** (has `pyproject.toml` / `setup.py`):
+```bash
+pytest # Tests
+```
+
+Run only the commands that apply to the detected project type. Record pass/fail for each.
+
+### Phase 5 — DECIDE
+
+Form recommendation based on findings:
+
+| Condition | Decision |
+|---|---|
+| Zero CRITICAL/HIGH issues, validation passes | **APPROVE** |
+| Only MEDIUM/LOW issues, validation passes | **APPROVE** with comments |
+| Any HIGH issues or validation failures | **REQUEST CHANGES** |
+| Any CRITICAL issues | **BLOCK** — must fix before merge |
+
+Special cases:
+- Draft PR → Always use **COMMENT** (not approve/block)
+- Only docs/config changes → Lighter review, focus on correctness
+- Explicit `--approve` or `--request-changes` flag → Override decision (but still report all findings)
+
+### Phase 6 — REPORT
+
+Create review artifact at `.claude/reviews/pr--review.md` unless the repo already uses legacy `.claude/PRPs/reviews/` for this workstream:
+
+```markdown
+# PR Review: # —
+
+**Reviewed**:
+**Author**:
+**Branch**: →
+**Decision**: APPROVE | REQUEST CHANGES | BLOCK
+
+## Summary
+<1-2 sentence overall assessment>
+
+## Findings
+
+### CRITICAL
+
+
+### HIGH
+
+
+### MEDIUM
+
+
+### LOW
+
+
+## Validation Results
+
+| Check | Result |
+|---|---|
+| Type check | Pass / Fail / Skipped |
+| Lint | Pass / Fail / Skipped |
+| Tests | Pass / Fail / Skipped |
+| Build | Pass / Fail / Skipped |
+
+## Files Reviewed
+
+```
+
+### Phase 7 — PUBLISH
+
+Post the review to GitHub:
+
+```bash
+# If APPROVE
+gh pr review --approve --body ""
+
+# If REQUEST CHANGES
+gh pr review --request-changes --body ""
+
+# If COMMENT only (draft PR or informational)
+gh pr review --comment --body ""
+```
+
+For inline comments on specific lines, use the GitHub review comments API:
+```bash
+gh api "repos/{owner}/{repo}/pulls//comments" \
+ -f body="" \
+ -f path="" \
+ -F line= \
+ -f side="RIGHT" \
+ -f commit_id="$(gh pr view --json headRefOid --jq .headRefOid)"
+```
+
+Alternatively, post a single review with multiple inline comments at once:
+```bash
+gh api "repos/{owner}/{repo}/pulls//reviews" \
+ -f event="COMMENT" \
+ -f body="" \
+ --input comments.json # [{"path": "file", "line": N, "body": "comment"}, ...]
+```
+
+### Phase 8 — OUTPUT
+
+Report to user:
+
+```
+PR #:
+Decision:
+
+Issues: critical, high, medium, low
+Validation: / checks passed
+
+Artifacts:
+ Review: .claude/reviews/pr--review.md
+ GitHub:
+
+Next steps:
+ -
+```
+
+---
+
+## Edge Cases
+
+- **No `gh` CLI**: Fall back to local-only review (read the diff, skip GitHub publish). Warn user.
+- **Diverged branches**: Suggest `git fetch origin && git rebase origin/ ` before review.
+- **Large PRs (>50 files)**: Warn about review scope. Focus on source changes first, then tests, then config/docs.
diff --git a/pi/core/commands/cpp-test.md b/pi/core/commands/cpp-test.md
new file mode 100644
index 000000000..0a4b0ab01
--- /dev/null
+++ b/pi/core/commands/cpp-test.md
@@ -0,0 +1,251 @@
+---
+description: Enforce TDD workflow for C++. Write GoogleTest tests first, then implement. Verify coverage with gcov/lcov.
+---
+
+# C++ TDD Command
+
+This command enforces test-driven development methodology for C++ code using GoogleTest/GoogleMock with CMake/CTest.
+
+## What This Command Does
+
+1. **Define Interfaces**: Scaffold class/function signatures first
+2. **Write Tests**: Create comprehensive GoogleTest test cases (RED)
+3. **Run Tests**: Verify tests fail for the right reason
+4. **Implement Code**: Write minimal code to pass (GREEN)
+5. **Refactor**: Improve while keeping tests green
+6. **Check Coverage**: Ensure 80%+ coverage
+
+## When to Use
+
+Use `/cpp-test` when:
+- Implementing new C++ functions or classes
+- Adding test coverage to existing code
+- Fixing bugs (write failing test first)
+- Building critical business logic
+- Learning TDD workflow in C++
+
+## TDD Cycle
+
+```
+RED → Write failing GoogleTest test
+GREEN → Implement minimal code to pass
+REFACTOR → Improve code, tests stay green
+REPEAT → Next test case
+```
+
+## Example Session
+
+````
+User: /cpp-test I need a function to validate email addresses
+
+Agent:
+# TDD Session: Email Validator
+
+## Step 1: Define Interface
+
+```cpp
+// validator/email.hpp
+#pragma once
+#include
+#include
+
+enum class EmailError {
+ Empty,
+ InvalidFormat
+};
+
+std::expected validate_email(const std::string& email);
+```
+
+## Step 2: Write Tests (RED)
+
+```cpp
+// validator/email_test.cpp
+#include
+#include "email.hpp"
+
+TEST(ValidateEmail, AcceptsSimpleEmail) {
+ auto result = validate_email("user@example.com");
+ EXPECT_TRUE(result.has_value());
+}
+
+TEST(ValidateEmail, AcceptsSubdomain) {
+ EXPECT_TRUE(validate_email("user@mail.example.com").has_value());
+}
+
+TEST(ValidateEmail, AcceptsPlus) {
+ EXPECT_TRUE(validate_email("user+tag@example.com").has_value());
+}
+
+TEST(ValidateEmail, RejectsEmpty) {
+ auto result = validate_email("");
+ ASSERT_FALSE(result.has_value());
+ EXPECT_EQ(result.error(), EmailError::Empty);
+}
+
+TEST(ValidateEmail, RejectsNoAtSign) {
+ EXPECT_FALSE(validate_email("userexample.com").has_value());
+}
+
+TEST(ValidateEmail, RejectsNoDomain) {
+ EXPECT_FALSE(validate_email("user@").has_value());
+}
+
+TEST(ValidateEmail, RejectsNoLocalPart) {
+ EXPECT_FALSE(validate_email("@example.com").has_value());
+}
+```
+
+## Step 3: Run Tests - Verify FAIL
+
+```bash
+$ cmake --build build && ctest --test-dir build --output-on-failure
+
+1/1 Test #1: email_validator_test .....***Failed
+ --- undefined reference to `validate_email`
+
+FAIL
+```
+
+✓ Tests fail as expected (unimplemented).
+
+## Step 4: Implement Minimal Code (GREEN)
+
+```cpp
+// validator/email.cpp
+#include "email.hpp"
+#include
+
+std::expected validate_email(const std::string& email) {
+ if (email.empty()) {
+ return std::unexpected(EmailError::Empty);
+ }
+ static const std::regex pattern(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})");
+ if (!std::regex_match(email, pattern)) {
+ return std::unexpected(EmailError::InvalidFormat);
+ }
+ return {};
+}
+```
+
+## Step 5: Run Tests - Verify PASS
+
+```bash
+$ cmake --build build && ctest --test-dir build --output-on-failure
+
+1/1 Test #1: email_validator_test ..... Passed 0.01 sec
+
+100% tests passed.
+```
+
+✓ All tests passing!
+
+## Step 6: Check Coverage
+
+```bash
+$ cmake -DCMAKE_CXX_FLAGS="--coverage" -B build && cmake --build build
+$ ctest --test-dir build
+$ lcov --capture --directory build --output-file coverage.info
+$ lcov --list coverage.info
+
+validator/email.cpp | 100%
+```
+
+✓ Coverage: 100%
+
+## TDD Complete!
+````
+
+## Test Patterns
+
+### Basic Tests
+```cpp
+TEST(SuiteName, TestName) {
+ EXPECT_EQ(add(2, 3), 5);
+ EXPECT_NE(result, nullptr);
+ EXPECT_TRUE(is_valid);
+ EXPECT_THROW(func(), std::invalid_argument);
+}
+```
+
+### Fixtures
+```cpp
+class DatabaseTest : public ::testing::Test {
+protected:
+ void SetUp() override { db_ = create_test_db(); }
+ void TearDown() override { db_.reset(); }
+ std::unique_ptr db_;
+};
+
+TEST_F(DatabaseTest, InsertsRecord) {
+ db_->insert("key", "value");
+ EXPECT_EQ(db_->get("key"), "value");
+}
+```
+
+### Parameterized Tests
+```cpp
+class PrimeTest : public ::testing::TestWithParam> {};
+
+TEST_P(PrimeTest, ChecksPrimality) {
+ auto [input, expected] = GetParam();
+ EXPECT_EQ(is_prime(input), expected);
+}
+
+INSTANTIATE_TEST_SUITE_P(Primes, PrimeTest, ::testing::Values(
+ std::make_pair(2, true),
+ std::make_pair(4, false),
+ std::make_pair(7, true)
+));
+```
+
+## Coverage Commands
+
+```bash
+# Build with coverage
+cmake -DCMAKE_CXX_FLAGS="--coverage" -DCMAKE_EXE_LINKER_FLAGS="--coverage" -B build
+
+# Run tests
+cmake --build build && ctest --test-dir build
+
+# Generate coverage report
+lcov --capture --directory build --output-file coverage.info
+lcov --remove coverage.info '/usr/*' --output-file coverage.info
+genhtml coverage.info --output-directory coverage_html
+```
+
+## Coverage Targets
+
+| Code Type | Target |
+|-----------|--------|
+| Critical business logic | 100% |
+| Public APIs | 90%+ |
+| General code | 80%+ |
+| Generated code | Exclude |
+
+## TDD Best Practices
+
+**DO:**
+- Write test FIRST, before any implementation
+- Run tests after each change
+- Use `EXPECT_*` (continues) over `ASSERT_*` (stops) when appropriate
+- Test behavior, not implementation details
+- Include edge cases (empty, null, max values, boundary conditions)
+
+**DON'T:**
+- Write implementation before tests
+- Skip the RED phase
+- Test private methods directly (test through public API)
+- Use `sleep` in tests
+- Ignore flaky tests
+
+## Related Commands
+
+- `/cpp-build` - Fix build errors
+- `/cpp-review` - Review code after implementation
+- `verification-loop` skill - Run full verification loop
+
+## Related
+
+- Skill: `skills/cpp-testing/`
+- Skill: `skills/tdd-workflow/`
diff --git a/pi/core/commands/fastapi-review.md b/pi/core/commands/fastapi-review.md
new file mode 100644
index 000000000..9d730c6e9
--- /dev/null
+++ b/pi/core/commands/fastapi-review.md
@@ -0,0 +1,39 @@
+---
+description: Review a FastAPI application for architecture, async correctness, dependency injection, Pydantic schemas, security, performance, and testability.
+---
+
+# FastAPI Review
+
+Invoke the `fastapi-reviewer` agent for a focused FastAPI review.
+
+## Usage
+
+```text
+/fastapi-review [file-or-directory]
+```
+
+## Review Areas
+
+- App factory, router boundaries, middleware, and exception handlers.
+- Pydantic request and response schema separation.
+- Dependency injection for database sessions, auth, pagination, and settings.
+- Async database and external HTTP patterns.
+- CORS, auth, rate limits, logging, and secret handling.
+- OpenAPI metadata and documented response models.
+- Test client setup and dependency overrides.
+
+## Expected Output
+
+```text
+[SEVERITY] Short issue title
+File: path/to/file.py:42
+Issue: What is wrong and why it matters.
+Fix: Concrete change to make.
+```
+
+## Related
+
+- Agent: `fastapi-reviewer`
+- Skill: `fastapi-patterns`
+- Command: `/python-review`
+- Skill: `security-scan`
diff --git a/pi/core/commands/feature-dev.md b/pi/core/commands/feature-dev.md
new file mode 100644
index 000000000..808c1e737
--- /dev/null
+++ b/pi/core/commands/feature-dev.md
@@ -0,0 +1,49 @@
+---
+description: Guided feature development with codebase understanding and architecture focus
+---
+
+A structured feature-development workflow that emphasizes understanding existing code before writing new code.
+
+## Phases
+
+### 1. Discovery
+
+- read the feature request carefully
+- identify requirements, constraints, and acceptance criteria
+- ask clarifying questions if the request is ambiguous
+
+### 2. Codebase Exploration
+
+- use `code-explorer` to analyze the relevant existing code
+- trace execution paths and architecture layers
+- understand integration points and conventions
+
+### 3. Clarifying Questions
+
+- present findings from exploration
+- ask targeted design and edge-case questions
+- wait for user response before proceeding
+
+### 4. Architecture Design
+
+- use `code-architect` to design the feature
+- provide the implementation blueprint
+- wait for approval before implementing
+
+### 5. Implementation
+
+- implement the feature following the approved design
+- prefer TDD where appropriate
+- keep commits small and focused
+
+### 6. Quality Review
+
+- use `code-reviewer` to review the implementation
+- address critical and important issues
+- verify test coverage
+
+### 7. Summary
+
+- summarize what was built
+- list follow-up items or limitations
+- provide testing instructions
diff --git a/pi/core/commands/flutter-test.md b/pi/core/commands/flutter-test.md
new file mode 100644
index 000000000..db0724f2b
--- /dev/null
+++ b/pi/core/commands/flutter-test.md
@@ -0,0 +1,144 @@
+---
+description: Run Flutter/Dart tests, report failures, and incrementally fix test issues. Covers unit, widget, golden, and integration tests.
+---
+
+# Flutter Test
+
+This command runs the Flutter test suite and reports results. When failures occur, it diagnoses and fixes issues incrementally.
+
+## What This Command Does
+
+1. **Run Tests**: Execute `flutter test` (or scope to changed files)
+2. **Parse Failures**: Identify failing tests by type and cause
+3. **Fix Incrementally**: One failure at a time where possible
+4. **Verify**: Re-run after each fix
+5. **Report**: Show coverage summary and remaining failures
+
+## When to Use
+
+Use `/flutter-test` when:
+- After implementing a feature to verify nothing broke
+- After running `/flutter-build` to ensure tests pass
+- To check test coverage on new code
+- When a specific test file is failing
+- Before submitting a PR
+
+## Commands Run
+
+```bash
+# Run all tests
+flutter test 2>&1
+
+# Run with coverage
+flutter test --coverage 2>&1
+
+# Run specific test file
+flutter test test/unit/domain/usecases/get_user_test.dart 2>&1
+
+# Run tests matching a name pattern
+flutter test --name "CartBloc" 2>&1
+
+# Run integration tests (requires device/emulator)
+flutter test integration_test/ 2>&1
+
+# Update golden files when intentional visual changes are made
+flutter test --update-goldens 2>&1
+```
+
+## Example Session
+
+````text
+User: /flutter-test
+
+Agent:
+# Flutter Test Run
+
+## Test Execution
+
+```
+$ flutter test
+00:05 +42 -2: Some tests failed.
+
+FAILED: test/unit/data/user_repository_test.dart
+ UserRepository getById
+ returns null when user not found
+ Expected: null
+ Actual: User(id: 'test-id', name: 'Test User')
+
+FAILED: test/widget/cart_page_test.dart
+ CartPage
+ shows empty state when cart is empty
+ Expected: exactly one widget with text 'Your cart is empty'
+ Found: no widget with text 'Your cart is empty'
+```
+
+## Fix 1: Repository Test
+
+File: test/unit/data/user_repository_test.dart
+Failure: `getById` returns a user when it should return null for missing ID
+
+Root cause: Test setup adds a user with ID 'test-id' but queries with 'missing-id'.
+Fix: Updated test to query with 'missing-id' — setup was correct, query was wrong.
+
+```
+$ flutter test test/unit/data/user_repository_test.dart
+1 test passed.
+```
+
+## Fix 2: Widget Test
+
+File: test/widget/cart_page_test.dart
+Failure: Empty state text widget not found
+
+Root cause: Empty state message was renamed from 'Your cart is empty' to 'Cart is empty' in the widget.
+Fix: Updated test string to match current widget copy.
+
+```
+$ flutter test test/widget/cart_page_test.dart
+1 test passed.
+```
+
+## Final Run
+
+```
+$ flutter test --coverage
+All 44 tests passed.
+Coverage: 84.2% (target: 80%)
+```
+
+## Summary
+
+| Metric | Value |
+|--------|-------|
+| Total tests | 44 |
+| Passed | 44 |
+| Failed | 0 |
+| Coverage | 84.2% |
+
+Test Status: PASS ✓
+````
+
+## Common Test Failures
+
+| Failure | Typical Fix |
+|---------|-------------|
+| `Expected: Actual: ` | Update assertion or fix implementation |
+| `Widget not found` | Fix finder selector or update test after widget rename |
+| `Golden file not found` | Run `flutter test --update-goldens` to generate |
+| `Golden mismatch` | Inspect diff; run `--update-goldens` if change was intentional |
+| `MissingPluginException` | Mock platform channel in test setup |
+| `LateInitializationError` | Initialize `late` fields in `setUp()` |
+| `pumpAndSettle timed out` | Replace with explicit `pump(Duration)` calls |
+
+## Related Commands
+
+- `/flutter-build` — Fix build errors before running tests
+- `/flutter-review` — Review code after tests pass
+- `tdd-workflow` skill — Test-driven development workflow
+
+## Related
+
+- Agent: `agents/flutter-reviewer.md`
+- Agent: `agents/dart-build-resolver.md`
+- Skill: `skills/flutter-dart-code-review/`
+- Rules: `rules/dart/testing.md`
diff --git a/pi/core/commands/go-test.md b/pi/core/commands/go-test.md
new file mode 100644
index 000000000..8f592fb20
--- /dev/null
+++ b/pi/core/commands/go-test.md
@@ -0,0 +1,268 @@
+---
+description: Enforce TDD workflow for Go. Write table-driven tests first, then implement. Verify 80%+ coverage with go test -cover.
+---
+
+# Go TDD Command
+
+This command enforces test-driven development methodology for Go code using idiomatic Go testing patterns.
+
+## What This Command Does
+
+1. **Define Types/Interfaces**: Scaffold function signatures first
+2. **Write Table-Driven Tests**: Create comprehensive test cases (RED)
+3. **Run Tests**: Verify tests fail for the right reason
+4. **Implement Code**: Write minimal code to pass (GREEN)
+5. **Refactor**: Improve while keeping tests green
+6. **Check Coverage**: Ensure 80%+ coverage
+
+## When to Use
+
+Use `/go-test` when:
+- Implementing new Go functions
+- Adding test coverage to existing code
+- Fixing bugs (write failing test first)
+- Building critical business logic
+- Learning TDD workflow in Go
+
+## TDD Cycle
+
+```
+RED → Write failing table-driven test
+GREEN → Implement minimal code to pass
+REFACTOR → Improve code, tests stay green
+REPEAT → Next test case
+```
+
+## Example Session
+
+````
+User: /go-test I need a function to validate email addresses
+
+Agent:
+# TDD Session: Email Validator
+
+## Step 1: Define Interface
+
+```go
+// validator/email.go
+package validator
+
+// ValidateEmail checks if the given string is a valid email address.
+// Returns nil if valid, error describing the issue otherwise.
+func ValidateEmail(email string) error {
+ panic("not implemented")
+}
+```
+
+## Step 2: Write Table-Driven Tests (RED)
+
+```go
+// validator/email_test.go
+package validator
+
+import (
+ "testing"
+)
+
+func TestValidateEmail(t *testing.T) {
+ tests := []struct {
+ name string
+ email string
+ wantErr bool
+ }{
+ // Valid emails
+ {"simple email", "user@example.com", false},
+ {"with subdomain", "user@mail.example.com", false},
+ {"with plus", "user+tag@example.com", false},
+ {"with dots", "first.last@example.com", false},
+
+ // Invalid emails
+ {"empty string", "", true},
+ {"no at sign", "userexample.com", true},
+ {"no domain", "user@", true},
+ {"no local part", "@example.com", true},
+ {"double at", "user@@example.com", true},
+ {"spaces", "user @example.com", true},
+ {"no tld", "user@example", true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := ValidateEmail(tt.email)
+ if tt.wantErr && err == nil {
+ t.Errorf("ValidateEmail(%q) = nil; want error", tt.email)
+ }
+ if !tt.wantErr && err != nil {
+ t.Errorf("ValidateEmail(%q) = %v; want nil", tt.email, err)
+ }
+ })
+ }
+}
+```
+
+## Step 3: Run Tests - Verify FAIL
+
+```bash
+$ go test ./validator/...
+
+--- FAIL: TestValidateEmail (0.00s)
+ --- FAIL: TestValidateEmail/simple_email (0.00s)
+ panic: not implemented
+
+FAIL
+```
+
+✓ Tests fail as expected (panic).
+
+## Step 4: Implement Minimal Code (GREEN)
+
+```go
+// validator/email.go
+package validator
+
+import (
+ "errors"
+ "regexp"
+)
+
+var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
+
+var (
+ ErrEmailEmpty = errors.New("email cannot be empty")
+ ErrEmailInvalid = errors.New("email format is invalid")
+)
+
+func ValidateEmail(email string) error {
+ if email == "" {
+ return ErrEmailEmpty
+ }
+ if !emailRegex.MatchString(email) {
+ return ErrEmailInvalid
+ }
+ return nil
+}
+```
+
+## Step 5: Run Tests - Verify PASS
+
+```bash
+$ go test ./validator/...
+
+PASS
+ok project/validator 0.003s
+```
+
+✓ All tests passing!
+
+## Step 6: Check Coverage
+
+```bash
+$ go test -cover ./validator/...
+
+PASS
+coverage: 100.0% of statements
+ok project/validator 0.003s
+```
+
+✓ Coverage: 100%
+
+## TDD Complete!
+````
+
+## Test Patterns
+
+### Table-Driven Tests
+```go
+tests := []struct {
+ name string
+ input InputType
+ want OutputType
+ wantErr bool
+}{
+ {"case 1", input1, want1, false},
+ {"case 2", input2, want2, true},
+}
+
+for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := Function(tt.input)
+ // assertions
+ })
+}
+```
+
+### Parallel Tests
+```go
+for _, tt := range tests {
+ tt := tt // Capture
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ // test body
+ })
+}
+```
+
+### Test Helpers
+```go
+func setupTestDB(t *testing.T) *sql.DB {
+ t.Helper()
+ db := createDB()
+ t.Cleanup(func() { db.Close() })
+ return db
+}
+```
+
+## Coverage Commands
+
+```bash
+# Basic coverage
+go test -cover ./...
+
+# Coverage profile
+go test -coverprofile=coverage.out ./...
+
+# View in browser
+go tool cover -html=coverage.out
+
+# Coverage by function
+go tool cover -func=coverage.out
+
+# With race detection
+go test -race -cover ./...
+```
+
+## Coverage Targets
+
+| Code Type | Target |
+|-----------|--------|
+| Critical business logic | 100% |
+| Public APIs | 90%+ |
+| General code | 80%+ |
+| Generated code | Exclude |
+
+## TDD Best Practices
+
+**DO:**
+- Write test FIRST, before any implementation
+- Run tests after each change
+- Use table-driven tests for comprehensive coverage
+- Test behavior, not implementation details
+- Include edge cases (empty, nil, max values)
+
+**DON'T:**
+- Write implementation before tests
+- Skip the RED phase
+- Test private functions directly
+- Use `time.Sleep` in tests
+- Ignore flaky tests
+
+## Related Commands
+
+- `/go-build` - Fix build errors
+- `/go-review` - Review code after implementation
+- `verification-loop` skill - Run full verification loop
+
+## Related
+
+- Skill: `skills/golang-testing/`
+- Skill: `skills/tdd-workflow/`
diff --git a/pi/core/commands/gradle-build.md b/pi/core/commands/gradle-build.md
new file mode 100644
index 000000000..541ca1b68
--- /dev/null
+++ b/pi/core/commands/gradle-build.md
@@ -0,0 +1,70 @@
+---
+description: Fix Gradle build errors for Android and KMP projects
+---
+
+# Gradle Build Fix
+
+Incrementally fix Gradle build and compilation errors for Android and Kotlin Multiplatform projects.
+
+## Step 1: Detect Build Configuration
+
+Identify the project type and run the appropriate build:
+
+| Indicator | Build Command |
+|-----------|---------------|
+| `build.gradle.kts` + `composeApp/` (KMP) | `./gradlew composeApp:compileKotlinMetadata 2>&1` |
+| `build.gradle.kts` + `app/` (Android) | `./gradlew app:compileDebugKotlin 2>&1` |
+| `settings.gradle.kts` with modules | `./gradlew assemble 2>&1` |
+| Detekt configured | `./gradlew detekt 2>&1` |
+
+Also check `gradle.properties` and `local.properties` for configuration.
+
+## Step 2: Parse and Group Errors
+
+1. Run the build command and capture output
+2. Separate Kotlin compilation errors from Gradle configuration errors
+3. Group by module and file path
+4. Sort: configuration errors first, then compilation errors by dependency order
+
+## Step 3: Fix Loop
+
+For each error:
+
+1. **Read the file** — Full context around the error line
+2. **Diagnose** — Common categories:
+ - Missing import or unresolved reference
+ - Type mismatch or incompatible types
+ - Missing dependency in `build.gradle.kts`
+ - Expect/actual mismatch (KMP)
+ - Compose compiler error
+3. **Fix minimally** — Smallest change that resolves the error
+4. **Re-run build** — Verify fix and check for new errors
+5. **Continue** — Move to next error
+
+## Step 4: Guardrails
+
+Stop and ask the user if:
+- Fix introduces more errors than it resolves
+- Same error persists after 3 attempts
+- Error requires adding new dependencies or changing module structure
+- Gradle sync itself fails (configuration-phase error)
+- Error is in generated code (Room, SQLDelight, KSP)
+
+## Step 5: Summary
+
+Report:
+- Errors fixed (module, file, description)
+- Errors remaining
+- New errors introduced (should be zero)
+- Suggested next steps
+
+## Common Gradle/KMP Fixes
+
+| Error | Fix |
+|-------|-----|
+| Unresolved reference in `commonMain` | Check if the dependency is in `commonMain.dependencies {}` |
+| Expect declaration without actual | Add `actual` implementation in each platform source set |
+| Compose compiler version mismatch | Align Kotlin and Compose compiler versions in `libs.versions.toml` |
+| Duplicate class | Check for conflicting dependencies with `./gradlew dependencies` |
+| KSP error | Run `./gradlew kspCommonMainKotlinMetadata` to regenerate |
+| Configuration cache issue | Check for non-serializable task inputs |
diff --git a/pi/core/commands/kotlin-test.md b/pi/core/commands/kotlin-test.md
new file mode 100644
index 000000000..bfbc82241
--- /dev/null
+++ b/pi/core/commands/kotlin-test.md
@@ -0,0 +1,312 @@
+---
+description: Enforce TDD workflow for Kotlin. Write Kotest tests first, then implement. Verify 80%+ coverage with Kover.
+---
+
+# Kotlin TDD Command
+
+This command enforces test-driven development methodology for Kotlin code using Kotest, MockK, and Kover.
+
+## What This Command Does
+
+1. **Define Types/Interfaces**: Scaffold function signatures first
+2. **Write Kotest Tests**: Create comprehensive test specs (RED)
+3. **Run Tests**: Verify tests fail for the right reason
+4. **Implement Code**: Write minimal code to pass (GREEN)
+5. **Refactor**: Improve while keeping tests green
+6. **Check Coverage**: Ensure 80%+ coverage with Kover
+
+## When to Use
+
+Use `/kotlin-test` when:
+- Implementing new Kotlin functions or classes
+- Adding test coverage to existing Kotlin code
+- Fixing bugs (write failing test first)
+- Building critical business logic
+- Learning TDD workflow in Kotlin
+
+## TDD Cycle
+
+```
+RED -> Write failing Kotest test
+GREEN -> Implement minimal code to pass
+REFACTOR -> Improve code, tests stay green
+REPEAT -> Next test case
+```
+
+## Example Session
+
+````text
+User: /kotlin-test I need a function to validate user registration requests
+
+Agent:
+# TDD Session: Registration Validator
+
+## Step 1: Define Interface
+
+```kotlin
+// validator/RegistrationValidator.kt
+package com.example.validator
+
+data class RegistrationRequest(
+ val name: String,
+ val email: String,
+ val password: String,
+)
+
+sealed class ValidationResult {
+ data object Valid : ValidationResult()
+ data class Invalid(val errors: List) : ValidationResult()
+}
+
+fun validateRegistration(request: RegistrationRequest): ValidationResult {
+ TODO("not implemented")
+}
+```
+
+## Step 2: Write Kotest Tests (RED)
+
+```kotlin
+// validator/RegistrationValidatorTest.kt
+package com.example.validator
+
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.types.shouldBeInstanceOf
+
+class RegistrationValidatorTest : FunSpec({
+ test("valid registration returns Valid") {
+ val request = RegistrationRequest(
+ name = "Alice",
+ email = "alice@example.com",
+ password = "SecureP@ss1",
+ )
+
+ val result = validateRegistration(request)
+
+ result.shouldBeInstanceOf()
+ }
+
+ test("blank name returns Invalid") {
+ val request = RegistrationRequest(
+ name = "",
+ email = "alice@example.com",
+ password = "SecureP@ss1",
+ )
+
+ val result = validateRegistration(request)
+
+ val invalid = result.shouldBeInstanceOf()
+ invalid.errors shouldBe listOf("Name is required")
+ }
+
+ test("invalid email returns Invalid") {
+ val request = RegistrationRequest(
+ name = "Alice",
+ email = "not-an-email",
+ password = "SecureP@ss1",
+ )
+
+ val result = validateRegistration(request)
+
+ val invalid = result.shouldBeInstanceOf()
+ invalid.errors shouldBe listOf("Invalid email format")
+ }
+
+ test("short password returns Invalid") {
+ val request = RegistrationRequest(
+ name = "Alice",
+ email = "alice@example.com",
+ password = "short",
+ )
+
+ val result = validateRegistration(request)
+
+ val invalid = result.shouldBeInstanceOf()
+ invalid.errors shouldBe listOf("Password must be at least 8 characters")
+ }
+
+ test("multiple errors returns all errors") {
+ val request = RegistrationRequest(
+ name = "",
+ email = "bad",
+ password = "short",
+ )
+
+ val result = validateRegistration(request)
+
+ val invalid = result.shouldBeInstanceOf()
+ invalid.errors.size shouldBe 3
+ }
+})
+```
+
+## Step 3: Run Tests - Verify FAIL
+
+```bash
+$ ./gradlew test
+
+RegistrationValidatorTest > valid registration returns Valid FAILED
+ kotlin.NotImplementedError: An operation is not implemented
+
+FAILED (5 tests, 0 passed, 5 failed)
+```
+
+✓ Tests fail as expected (NotImplementedError).
+
+## Step 4: Implement Minimal Code (GREEN)
+
+```kotlin
+// validator/RegistrationValidator.kt
+package com.example.validator
+
+private val EMAIL_REGEX = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
+private const val MIN_PASSWORD_LENGTH = 8
+
+fun validateRegistration(request: RegistrationRequest): ValidationResult {
+ val errors = buildList {
+ if (request.name.isBlank()) add("Name is required")
+ if (!EMAIL_REGEX.matches(request.email)) add("Invalid email format")
+ if (request.password.length < MIN_PASSWORD_LENGTH) add("Password must be at least $MIN_PASSWORD_LENGTH characters")
+ }
+
+ return if (errors.isEmpty()) ValidationResult.Valid
+ else ValidationResult.Invalid(errors)
+}
+```
+
+## Step 5: Run Tests - Verify PASS
+
+```bash
+$ ./gradlew test
+
+RegistrationValidatorTest > valid registration returns Valid PASSED
+RegistrationValidatorTest > blank name returns Invalid PASSED
+RegistrationValidatorTest > invalid email returns Invalid PASSED
+RegistrationValidatorTest > short password returns Invalid PASSED
+RegistrationValidatorTest > multiple errors returns all errors PASSED
+
+PASSED (5 tests, 5 passed, 0 failed)
+```
+
+✓ All tests passing!
+
+## Step 6: Check Coverage
+
+```bash
+$ ./gradlew koverHtmlReport
+
+Coverage: 100.0% of statements
+```
+
+✓ Coverage: 100%
+
+## TDD Complete!
+````
+
+## Test Patterns
+
+### StringSpec (Simplest)
+
+```kotlin
+class CalculatorTest : StringSpec({
+ "add two positive numbers" {
+ Calculator.add(2, 3) shouldBe 5
+ }
+})
+```
+
+### BehaviorSpec (BDD)
+
+```kotlin
+class OrderServiceTest : BehaviorSpec({
+ Given("a valid order") {
+ When("placed") {
+ Then("should be confirmed") { /* ... */ }
+ }
+ }
+})
+```
+
+### Data-Driven Tests
+
+```kotlin
+class ParserTest : FunSpec({
+ context("valid inputs") {
+ withData("2026-01-15", "2026-12-31", "2000-01-01") { input ->
+ parseDate(input).shouldNotBeNull()
+ }
+ }
+})
+```
+
+### Coroutine Testing
+
+```kotlin
+class AsyncServiceTest : FunSpec({
+ test("concurrent fetch completes") {
+ runTest {
+ val result = service.fetchAll()
+ result.shouldNotBeEmpty()
+ }
+ }
+})
+```
+
+## Coverage Commands
+
+```bash
+# Run tests with coverage
+./gradlew koverHtmlReport
+
+# Verify coverage thresholds
+./gradlew koverVerify
+
+# XML report for CI
+./gradlew koverXmlReport
+
+# Open HTML report
+open build/reports/kover/html/index.html
+
+# Run specific test class
+./gradlew test --tests "com.example.UserServiceTest"
+
+# Run with verbose output
+./gradlew test --info
+```
+
+## Coverage Targets
+
+| Code Type | Target |
+|-----------|--------|
+| Critical business logic | 100% |
+| Public APIs | 90%+ |
+| General code | 80%+ |
+| Generated code | Exclude |
+
+## TDD Best Practices
+
+**DO:**
+- Write test FIRST, before any implementation
+- Run tests after each change
+- Use Kotest matchers for expressive assertions
+- Use MockK's `coEvery`/`coVerify` for suspend functions
+- Test behavior, not implementation details
+- Include edge cases (empty, null, max values)
+
+**DON'T:**
+- Write implementation before tests
+- Skip the RED phase
+- Test private functions directly
+- Use `Thread.sleep()` in coroutine tests
+- Ignore flaky tests
+
+## Related Commands
+
+- `/kotlin-build` - Fix build errors
+- `/kotlin-review` - Review code after implementation
+- `verification-loop` skill - Run full verification loop
+
+## Related
+
+- Skill: `skills/kotlin-testing/`
+- Skill: `skills/tdd-workflow/`
diff --git a/pi/core/commands/plan-prd.md b/pi/core/commands/plan-prd.md
new file mode 100644
index 000000000..192295785
--- /dev/null
+++ b/pi/core/commands/plan-prd.md
@@ -0,0 +1,162 @@
+---
+description: "Generate a lean, problem-first PRD and hand off to /plan for implementation planning."
+argument-hint: "[product/feature idea] (blank = start with questions)"
+---
+
+# PRD Command
+
+Produces a **Product Requirements Document** — the requirements-phase artifact of the SDLC. Captures *what* must be true for success and *why*, and stops before *how*. Implementation decomposition is delegated to `/plan`.
+
+**Input**: `$ARGUMENTS`
+
+## Scope of this command
+
+| This command does | This command does NOT do |
+|---|---|
+| Frame the problem and users | Design the architecture |
+| Capture success criteria and scope | Pick files or write patterns |
+| List open questions and risks | Enumerate implementation tasks |
+| Write `.claude/prds/{name}.prd.md` | Produce an implementation plan — that's `/plan` |
+
+If you find yourself writing implementation detail, stop and cut it. It belongs in `/plan`.
+
+**Anti-fluff rule**: When information is missing, write `TBD — needs validation via {method}`. Never invent plausible-sounding requirements.
+
+## Workflow
+
+Four phases. Each phase is a single gate — ask the questions, wait for the user, then move on. No nested loops, no parallel research ceremony.
+
+### Phase 1 — FRAME
+
+If `$ARGUMENTS` is empty, ask:
+
+> What do you want to build? One or two sentences.
+
+If provided, restate in one sentence and ask:
+
+> I understand: *{restated}*. Correct, or should I adjust?
+
+Then ask the framing questions in a single set:
+
+> 1. **Who** has this problem? (specific role or segment)
+> 2. **What** is the observable pain? (describe behavior, not assumed needs)
+> 3. **Why** can't they solve it with what exists today?
+> 4. **Why now?** — what changed that makes this worth doing?
+
+Wait for the user. Do not proceed without answers (or explicit "skip").
+
+### Phase 2 — GROUND
+
+Ask for evidence. This is the shortest phase and the most load-bearing:
+
+> What evidence do you have that this problem is real and worth solving? (user quotes, support tickets, metrics, observed behavior, failed workarounds — anything concrete)
+
+If the user has none, record the PRD's Evidence section as `Assumption — needs validation via {user research | analytics | prototype}`. This keeps the PRD honest.
+
+### Phase 3 — DECIDE
+
+Scope and hypothesis in a single set:
+
+> 1. **Hypothesis** — Complete: *We believe **{capability}** will **{solve problem}** for **{users}**. We'll know we're right when **{measurable outcome}**.*
+> 2. **MVP** — The minimum needed to test the hypothesis?
+> 3. **Out of scope** — What are you explicitly **not** building (even if users ask)?
+> 4. **Open questions** — Uncertainties that could change the approach?
+
+Wait for responses.
+
+### Phase 4 — GENERATE & HAND OFF
+
+Create the directory if needed, write the PRD, and report.
+
+```bash
+mkdir -p .claude/prds
+```
+
+**Output path**: `.claude/prds/{kebab-case-name}.prd.md`
+
+#### PRD Template
+
+```markdown
+# {Product / Feature Name}
+
+## Problem
+{2–3 sentences: who has what problem, and what's the cost of leaving it unsolved?}
+
+## Evidence
+- {User quote, data point, or observation}
+- {OR: "Assumption — needs validation via {method}"}
+
+## Users
+- **Primary**: {role, context, what triggers the need}
+- **Not for**: {who this explicitly excludes}
+
+## Hypothesis
+We believe **{capability}** will **{solve problem}** for **{users}**.
+We'll know we're right when **{measurable outcome}**.
+
+## Success Metrics
+| Metric | Target | How measured |
+|---|---|---|
+| {primary} | {number} | {method} |
+
+## Scope
+**MVP** — {the minimum to test the hypothesis}
+
+**Out of scope**
+- {item} — {why deferred}
+
+## Delivery Milestones
+
+
+
+| # | Milestone | Outcome | Status | Plan |
+|---|---|---|---|---|
+| 1 | {name} | {user-visible change} | pending | — |
+| 2 | {name} | {user-visible change} | pending | — |
+
+## Open Questions
+- [ ] {question that could change scope or approach}
+
+## Risks
+| Risk | Likelihood | Impact | Mitigation |
+|---|---|---|---|
+
+---
+*Status: DRAFT — requirements only. Implementation planning pending via /plan.*
+```
+
+#### Report to user
+
+```
+PRD created: .claude/prds/{name}.prd.md
+
+Problem: {one line}
+Hypothesis: {one line}
+MVP: {one line}
+
+Validation status:
+ Problem {validated | assumption}
+ Users {concrete | generic — refine}
+ Metrics {defined | TBD}
+
+Open questions: {count}
+
+Next step: /plan .claude/prds/{name}.prd.md
+ → /plan will pick the next pending milestone and produce an implementation plan.
+```
+
+## Integration
+
+- `/plan ` — consume the PRD and produce an implementation plan for the next pending milestone.
+- `tdd-workflow` skill — implement the plan test-first.
+- `/pr` — open a PR that references the PRD and plan.
+
+## Success criteria
+
+- **PROBLEM_CLEAR**: problem is specific and evidenced (or flagged as assumption).
+- **USER_CONCRETE**: primary user is a specific role, not "users".
+- **HYPOTHESIS_TESTABLE**: measurable outcome included.
+- **SCOPE_BOUNDED**: explicit MVP and explicit out-of-scope.
+- **NO_IMPLEMENTATION_DETAIL**: file paths, libraries, or task breakdowns are absent — if they appeared, move them to the `/plan` step.
+
+Background on the staged markdown flow: [docs/PLAN-PRD-PATTERN.md](../docs/PLAN-PRD-PATTERN.md).
diff --git a/pi/core/commands/plan.md b/pi/core/commands/plan.md
new file mode 100644
index 000000000..f9ac7db6e
--- /dev/null
+++ b/pi/core/commands/plan.md
@@ -0,0 +1,206 @@
+---
+description: Restate requirements, assess risks, and create step-by-step implementation plan. WAIT for user CONFIRM before touching any code. Use for a single-model inline or PRD-driven implementation plan; for a dual-model (Codex/Antigravity) plan use /multi-plan, and for visual annotate-and-approve review of the resulting plan use /plan-canvas.
+argument-hint: "[feature description | path/to/*.prd.md]"
+---
+
+# Plan Command
+
+This command creates a comprehensive implementation plan before writing any code. It accepts either free-form requirements or a PRD markdown file.
+
+Run inline by default. Do not call the Task tool or any subagent by default. This keeps `/plan` usable from plugin installs that ship commands without agent files.
+
+## What This Command Does
+
+1. **Restate Requirements** - Clarify what needs to be built
+2. **Identify Risks** - Surface potential issues and blockers
+3. **Create Step Plan** - Break down implementation into phases
+4. **Wait for Confirmation** - MUST receive user approval before proceeding
+
+## When to Use
+
+Use `/plan` when:
+- Starting a new feature
+- Making significant architectural changes
+- Working on complex refactoring
+- Multiple files/components will be affected
+- Requirements are unclear or ambiguous
+
+## How It Works
+
+The assistant will:
+
+1. **Analyze the request** and restate requirements in clear terms
+2. **Ground the plan** in relevant codebase patterns when the repo is available
+3. **Break down into phases** with specific, actionable steps
+4. **Identify dependencies** between components
+5. **Assess risks** and potential blockers
+6. **Estimate complexity** (High/Medium/Low)
+7. **Present the plan** and WAIT for your explicit confirmation
+
+## Input Modes
+
+| Input | Mode | Behavior |
+|---|---|---|
+| `path/to/name.prd.md` | PRD artifact mode | Read the PRD, pick the next pending delivery milestone or implementation phase, and write `.claude/plans/{name}.plan.md` |
+| Any other markdown path | Reference mode | Read the file as context and produce an inline plan |
+| Free-form text | Conversational mode | Produce an inline plan |
+| Empty input | Clarification mode | Ask what should be planned |
+
+In PRD artifact mode, create `.claude/plans/` if needed. If the PRD contains a `Delivery Milestones` table, update only the selected row from `pending` to `in-progress` and set its `Plan` cell to the generated plan path. If the PRD uses the legacy `.claude/PRPs/prds/` format with `Implementation Phases`, read it without migrating paths.
+
+## Pattern Grounding
+
+Before writing the plan, search the codebase for conventions the implementation should mirror. Capture the top example for each relevant category with file references:
+
+| Category | What to capture |
+|---|---|
+| Naming | File, function, type, command, or script naming in the affected area |
+| Error handling | How failures are raised, returned, logged, or handled gracefully |
+| Logging | Levels, format, and what gets logged |
+| Data access | Repository, service, query, or filesystem patterns |
+| Tests | Test file location, framework, fixtures, and assertion style |
+
+If no similar code exists, state that explicitly. Do not invent a pattern.
+
+## PRD Artifact Output
+
+When called with a `.prd.md` file, write the plan to `.claude/plans/{kebab-case-name}.plan.md` using this structure:
+
+````markdown
+# Plan: {Feature Name}
+
+**Source PRD**: {path}
+**Selected Milestone**: {milestone or phase name}
+**Complexity**: {Small | Medium | Large}
+
+## Summary
+{2-3 sentences}
+
+## Patterns to Mirror
+| Category | Source | Pattern |
+|---|---|---|
+| Naming | `path:line` | {short description} |
+| Errors | `path:line` | {short description} |
+| Tests | `path:line` | {short description} |
+
+## Files to Change
+| File | Action | Why |
+|---|---|---|
+| `path` | CREATE / UPDATE / DELETE | {reason} |
+
+## Tasks
+### Task 1: {name}
+- **Action**: {what to do}
+- **Mirror**: {pattern to follow}
+- **Validate**: {command that proves correctness}
+
+## Validation
+```bash
+{project-specific validation commands}
+```
+
+## Risks
+| Risk | Likelihood | Mitigation |
+|---|---|---|
+
+## Acceptance
+- [ ] All tasks complete
+- [ ] Validation passes
+- [ ] Patterns mirrored, not reinvented
+````
+
+After writing the artifact, report its path and WAIT for confirmation before writing code.
+
+> **Visual review:** instead of asking for a typed confirmation, you can open the
+> artifact in the browser Plan Canvas (`/plan-canvas`, or the `plan-canvas` skill):
+> the user annotates the plan in place and clicks **Approve plan** or **Request
+> changes**, which arrives as your confirmation signal.
+
+## Example Usage
+
+```
+User: /plan I need to add real-time notifications when markets resolve
+
+Assistant:
+# Implementation Plan: Real-Time Market Resolution Notifications
+
+## Requirements Restatement
+- Send notifications to users when markets they're watching resolve
+- Support multiple notification channels (in-app, email, webhook)
+- Ensure notifications are delivered reliably
+- Include market outcome and user's position result
+
+## Implementation Phases
+
+### Phase 1: Database Schema
+- Add notifications table with columns: id, user_id, market_id, type, status, created_at
+- Add user_notification_preferences table for channel preferences
+- Create indexes on user_id and market_id for performance
+
+### Phase 2: Notification Service
+- Create notification service in lib/notifications.ts
+- Implement notification queue using BullMQ/Redis
+- Add retry logic for failed deliveries
+- Create notification templates
+
+### Phase 3: Integration Points
+- Hook into market resolution logic (when status changes to "resolved")
+- Query all users with positions in market
+- Enqueue notifications for each user
+
+### Phase 4: Frontend Components
+- Create NotificationBell component in header
+- Add NotificationList modal
+- Implement real-time updates via Supabase subscriptions
+- Add notification preferences page
+
+## Dependencies
+- Redis (for queue)
+- Email service (SendGrid/Resend)
+- Supabase real-time subscriptions
+
+## Risks
+- HIGH: Email deliverability (SPF/DKIM required)
+- MEDIUM: Performance with 1000+ users per market
+- MEDIUM: Notification spam if markets resolve frequently
+- LOW: Real-time subscription overhead
+
+## Estimated Complexity: MEDIUM
+- Backend: 4-6 hours
+- Frontend: 3-4 hours
+- Testing: 2-3 hours
+- Total: 9-13 hours
+
+**WAITING FOR CONFIRMATION**: Proceed with this plan? (yes/no/modify)
+```
+
+## Important Notes
+
+**CRITICAL**: This command will **NOT** write any code until you explicitly confirm the plan with "yes" or "proceed" or similar affirmative response.
+
+If you want changes, respond with:
+- "modify: [your changes]"
+- "different approach: [alternative]"
+- "skip phase 2 and do phase 3 first"
+
+## Integration with Other Commands
+
+After planning:
+- Use `/plan-canvas` to run the confirmation gate visually in the browser (annotate + approve)
+- Use the `tdd-workflow` skill to implement with test-driven development
+- Use `/build-fix` if build errors occur
+- Use `/code-review` to review completed implementation
+- Use `/pr` or `/prp-pr` to open a pull request
+
+> **Need requirements first?** Use `/plan-prd` for a lean PRD at `.claude/prds/{name}.prd.md`.
+>
+> **Need the legacy PRP flow?** Use `/prp-plan` for deep PRP planning with `.claude/PRPs/` artifacts. Use `/prp-implement` to execute those plans with rigorous validation loops.
+
+## Optional Planner Agent
+
+ECC also provides a `planner` agent for manual installs that include agent files. Use it only when the local runtime already exposes that subagent and the user explicitly asks you to delegate planning.
+
+If the `planner` subagent is unavailable, continue planning inline instead of surfacing an "Agent type 'planner' not found" error.
+
+For manual installs, the source file lives at:
+`agents/planner.md`
diff --git a/pi/core/commands/pr.md b/pi/core/commands/pr.md
new file mode 100644
index 000000000..264ec3fff
--- /dev/null
+++ b/pi/core/commands/pr.md
@@ -0,0 +1,184 @@
+---
+description: "Create a GitHub PR from current branch with unpushed commits — discovers templates, analyzes changes, pushes"
+argument-hint: "[base-branch] (default: main)"
+---
+
+# Create Pull Request
+
+**Input**: `$ARGUMENTS` — optional, may contain a base branch name and/or flags (e.g., `--draft`).
+
+**Parse `$ARGUMENTS`**:
+- Extract any recognized flags (`--draft`)
+- Treat remaining non-flag text as the base branch name
+- Default base branch to `main` if none specified
+
+---
+
+## Phase 1 — VALIDATE
+
+Check preconditions:
+
+```bash
+git branch --show-current
+git status --short
+git log origin/ ..HEAD --oneline
+```
+
+| Check | Condition | Action if Failed |
+|---|---|---|
+| Not on base branch | Current branch ≠ base | Stop: "Switch to a feature branch first." |
+| Clean working directory | No uncommitted changes | Warn: "You have uncommitted changes. Commit or stash first." |
+| Has commits ahead | `git log origin/ ..HEAD` not empty | Stop: "No commits ahead of ` `. Nothing to PR." |
+| No existing PR | `gh pr list --head --json number` is empty | Stop: "PR already exists: #. Use `gh pr view --web` to open it." |
+
+If all checks pass, proceed.
+
+---
+
+## Phase 2 — DISCOVER
+
+### PR Template
+
+Search for PR template in order:
+
+1. `.github/PULL_REQUEST_TEMPLATE/` directory — if exists, list files and let user choose (or use `default.md`)
+2. `.github/PULL_REQUEST_TEMPLATE.md`
+3. `.github/pull_request_template.md`
+4. `docs/pull_request_template.md`
+
+If found, read it and use its structure for the PR body.
+
+### Commit Analysis
+
+```bash
+git log origin/ ..HEAD --format="%h %s" --reverse
+```
+
+Analyze commits to determine:
+- **PR title**: Use conventional commit format with type prefix — `feat: ...`, `fix: ...`, etc.
+ - If multiple types, use the dominant one
+ - If single commit, use its message as-is
+- **Change summary**: Group commits by type/area
+
+### File Analysis
+
+```bash
+git diff origin/ ..HEAD --stat
+git diff origin/ ..HEAD --name-only
+```
+
+Categorize changed files: source, tests, docs, config, migrations.
+
+### Planning Artifacts
+
+Check for related artifacts produced by `/plan-prd`, `/plan`, or the legacy PRP workflow:
+- `.claude/prds/` — PRDs this PR implements a milestone of
+- `.claude/plans/` — Plans executed by this PR
+- `.claude/PRPs/prds/` — legacy PRP PRDs
+- `.claude/PRPs/plans/` — legacy PRP implementation plans
+- `.claude/PRPs/reports/` — legacy PRP implementation reports
+
+Reference these in the PR body if they exist.
+
+---
+
+## Phase 3 — PUSH
+
+```bash
+git push -u origin HEAD
+```
+
+If push fails due to divergence:
+```bash
+git fetch origin
+git rebase origin/
+git push -u origin HEAD
+```
+
+If rebase conflicts occur, stop and inform the user.
+
+---
+
+## Phase 4 — CREATE
+
+### With Template
+
+If a PR template was found in Phase 2, fill in each section using the commit and file analysis. Preserve all template sections — leave sections as "N/A" if not applicable rather than removing them.
+
+### Without Template
+
+Use this default format:
+
+```markdown
+## Summary
+
+<1-2 sentence description of what this PR does and why>
+
+## Changes
+
+
+
+## Files Changed
+
+
+
+## Testing
+
+
+
+## Related Issues
+
+
+```
+
+### Create the PR
+
+```bash
+gh pr create \
+ --title "" \
+ --base \
+ --body ""
+ # Add --draft if the --draft flag was parsed from $ARGUMENTS
+```
+
+---
+
+## Phase 5 — VERIFY
+
+```bash
+gh pr view --json number,url,title,state,baseRefName,headRefName,additions,deletions,changedFiles
+gh pr checks --json name,status,conclusion 2>/dev/null || true
+```
+
+---
+
+## Phase 6 — OUTPUT
+
+Report to user:
+
+```
+PR #:
+URL:
+Branch: →
+Changes: + - across files
+
+CI Checks:
+
+Artifacts referenced:
+ -
+
+Next steps:
+ - gh pr view --web → open in browser
+ - /code-review → review the PR
+ - gh pr merge → merge when ready
+```
+
+---
+
+## Edge Cases
+
+- **No `gh` CLI**: Stop with: "GitHub CLI (`gh`) is required. Install: "
+- **Not authenticated**: Stop with: "Run `gh auth login` first."
+- **Force push needed**: If remote has diverged and rebase was done, use `git push --force-with-lease` (never `--force`).
+- **Multiple PR templates**: If `.github/PULL_REQUEST_TEMPLATE/` has multiple files, list them and ask user to choose.
+- **Large PR (>20 files)**: Warn about PR size. Suggest splitting if changes are logically separable.
diff --git a/pi/core/commands/prp-commit.md b/pi/core/commands/prp-commit.md
new file mode 100644
index 000000000..85935b8c5
--- /dev/null
+++ b/pi/core/commands/prp-commit.md
@@ -0,0 +1,112 @@
+---
+description: "Quick commit with natural language file targeting — describe what to commit in plain English"
+argument-hint: "[target description] (blank = all changes)"
+---
+
+# Smart Commit
+
+> Adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
+
+**Input**: $ARGUMENTS
+
+---
+
+## Phase 1 — ASSESS
+
+```bash
+git status --short
+```
+
+If output is empty → stop: "Nothing to commit."
+
+Show the user a summary of what's changed (added, modified, deleted, untracked).
+
+---
+
+## Phase 2 — INTERPRET & STAGE
+
+Interpret `$ARGUMENTS` to determine what to stage:
+
+| Input | Interpretation | Git Command |
+|---|---|---|
+| *(blank / empty)* | Stage everything | `git add -A` |
+| `staged` | Use whatever is already staged | *(no git add)* |
+| `*.ts` or `*.py` etc. | Stage matching glob | `git add '*.ts'` |
+| `except tests` | Stage all, then unstage tests | `git add -A && git reset -- '**/*.test.*' '**/*.spec.*' '**/test_*' 2>/dev/null \|\| true` |
+| `only new files` | Stage untracked files only | `git ls-files --others --exclude-standard \| grep . && git ls-files --others --exclude-standard \| xargs git add` |
+| `the auth changes` | Interpret from status/diff — find auth-related files | `git add ` |
+| Specific filenames | Stage those files | `git add ` |
+
+For natural language inputs (like "the auth changes"), cross-reference the `git status` output and `git diff` to identify relevant files. Show the user which files you're staging and why.
+
+```bash
+git add
+```
+
+After staging, verify:
+```bash
+git diff --cached --stat
+```
+
+If nothing staged, stop: "No files matched your description."
+
+---
+
+## Phase 3 — COMMIT
+
+Craft a single-line commit message in imperative mood:
+
+```
+{type}: {description}
+```
+
+Types:
+- `feat` — New feature or capability
+- `fix` — Bug fix
+- `refactor` — Code restructuring without behavior change
+- `docs` — Documentation changes
+- `test` — Adding or updating tests
+- `chore` — Build, config, dependencies
+- `perf` — Performance improvement
+- `ci` — CI/CD changes
+
+Rules:
+- Imperative mood ("add feature" not "added feature")
+- Lowercase after the type prefix
+- No period at the end
+- Under 72 characters
+- Describe WHAT changed, not HOW
+
+```bash
+git commit -m "{type}: {description}"
+```
+
+---
+
+## Phase 4 — OUTPUT
+
+Report to user:
+
+```
+Committed: {hash_short}
+Message: {type}: {description}
+Files: {count} file(s) changed
+
+Next steps:
+ - git push → push to remote
+ - /prp-pr → create a pull request
+ - /code-review → review before pushing
+```
+
+---
+
+## Examples
+
+| You say | What happens |
+|---|---|
+| `/prp-commit` | Stages all, auto-generates message |
+| `/prp-commit staged` | Commits only what's already staged |
+| `/prp-commit *.ts` | Stages all TypeScript files, commits |
+| `/prp-commit except tests` | Stages everything except test files |
+| `/prp-commit the database migration` | Finds DB migration files from status, stages them |
+| `/prp-commit only new files` | Stages untracked files only |
diff --git a/pi/core/commands/prp-implement.md b/pi/core/commands/prp-implement.md
new file mode 100644
index 000000000..9a729cf21
--- /dev/null
+++ b/pi/core/commands/prp-implement.md
@@ -0,0 +1,385 @@
+---
+description: Execute an implementation plan with rigorous validation loops
+argument-hint:
+---
+
+> Adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
+
+# PRP Implement
+
+Execute a plan file step-by-step with continuous validation. Every change is verified immediately — never accumulate broken state.
+
+**Core Philosophy**: Validation loops catch mistakes early. Run checks after every change. Fix issues immediately.
+
+**Golden Rule**: If a validation fails, fix it before moving on. Never accumulate broken state.
+
+---
+
+## Phase 0 — DETECT
+
+### Package Manager Detection
+
+| File Exists | Package Manager | Runner |
+|---|---|---|
+| `bun.lockb` | bun | `bun run` |
+| `pnpm-lock.yaml` | pnpm | `pnpm run` |
+| `yarn.lock` | yarn | `yarn` |
+| `package-lock.json` | npm | `npm run` |
+| `pyproject.toml` or `requirements.txt` | uv / pip | `uv run` or `python -m` |
+| `Cargo.toml` | cargo | `cargo` |
+| `go.mod` | go | `go` |
+
+### Validation Scripts
+
+Check `package.json` (or equivalent) for available scripts:
+
+```bash
+# For Node.js projects
+cat package.json | grep -A 20 '"scripts"'
+```
+
+Note available commands for: type-check, lint, test, build.
+
+---
+
+## Phase 1 — LOAD
+
+Read the plan file:
+
+```bash
+cat "$ARGUMENTS"
+```
+
+Extract these sections from the plan:
+- **Summary** — What is being built
+- **Patterns to Mirror** — Code conventions to follow
+- **Files to Change** — What to create or modify
+- **Step-by-Step Tasks** — Implementation sequence
+- **Validation Commands** — How to verify correctness
+- **Acceptance Criteria** — Definition of done
+
+If the file doesn't exist or isn't a valid plan:
+```
+Error: Plan file not found or invalid.
+Run /prp-plan to create a plan first.
+```
+
+**CHECKPOINT**: Plan loaded. All sections identified. Tasks extracted.
+
+---
+
+## Phase 2 — PREPARE
+
+### Git State
+
+```bash
+git branch --show-current
+git status --porcelain
+```
+
+### Branch Decision
+
+| Current State | Action |
+|---|---|
+| On feature branch | Use current branch |
+| On main, clean working tree | Create feature branch: `git checkout -b feat/{plan-name}` |
+| On main, dirty working tree | **STOP** — Ask user to stash or commit first |
+| In a git worktree for this feature | Use the worktree |
+
+### Sync Remote
+
+```bash
+git pull --rebase origin $(git branch --show-current) 2>/dev/null || true
+```
+
+**CHECKPOINT**: On correct branch. Working tree ready. Remote synced.
+
+---
+
+## Phase 3 — EXECUTE
+
+Process each task from the plan sequentially.
+
+### Per-Task Loop
+
+For each task in **Step-by-Step Tasks**:
+
+1. **Read MIRROR reference** — Open the pattern file referenced in the task's MIRROR field. Understand the convention before writing code.
+
+2. **Implement** — Write the code following the pattern exactly. Apply GOTCHA warnings. Use specified IMPORTS.
+
+3. **Validate immediately** — After EVERY file change:
+ ```bash
+ # Run type-check (adjust command per project)
+ [type-check command from Phase 0]
+ ```
+ If type-check fails → fix the error before moving to the next file.
+
+4. **Track progress** — Log: `[done] Task N: [task name] — complete`
+
+### Handling Deviations
+
+If implementation must deviate from the plan:
+- Note **WHAT** changed
+- Note **WHY** it changed
+- Continue with the corrected approach
+- These deviations will be captured in the report
+
+**CHECKPOINT**: All tasks executed. Deviations logged.
+
+---
+
+## Phase 4 — VALIDATE
+
+Run all validation levels from the plan. Fix issues at each level before proceeding.
+
+### Level 1: Static Analysis
+
+```bash
+# Type checking — zero errors required
+[project type-check command]
+
+# Linting — fix automatically where possible
+[project lint command]
+[project lint-fix command]
+```
+
+If lint errors remain after auto-fix, fix manually.
+
+### Level 2: Unit Tests
+
+Write tests for every new function (as specified in the plan's Testing Strategy).
+
+```bash
+[project test command for affected area]
+```
+
+- Every function needs at least one test
+- Cover edge cases listed in the plan
+- If a test fails → fix the implementation (not the test, unless the test is wrong)
+
+### Level 3: Build Check
+
+```bash
+[project build command]
+```
+
+Build must succeed with zero errors.
+
+### Level 4: Integration Testing (if applicable)
+
+```bash
+# Start server, run tests, stop server
+[project dev server command] &
+SERVER_PID=$!
+
+# Wait for server to be ready (adjust port as needed)
+SERVER_READY=0
+for i in $(seq 1 30); do
+ if curl -sf http://localhost:PORT/health >/dev/null 2>&1; then
+ SERVER_READY=1
+ break
+ fi
+ sleep 1
+done
+
+if [ "$SERVER_READY" -ne 1 ]; then
+ kill "$SERVER_PID" 2>/dev/null || true
+ echo "ERROR: Server failed to start within 30s" >&2
+ exit 1
+fi
+
+[integration test command]
+TEST_EXIT=$?
+
+kill "$SERVER_PID" 2>/dev/null || true
+wait "$SERVER_PID" 2>/dev/null || true
+
+exit "$TEST_EXIT"
+```
+
+### Level 5: Edge Case Testing
+
+Run through edge cases from the plan's Testing Strategy checklist.
+
+**CHECKPOINT**: All 5 validation levels pass. Zero errors.
+
+---
+
+## Phase 5 — REPORT
+
+### Create Implementation Report
+
+```bash
+mkdir -p .claude/PRPs/reports
+```
+
+Write report to `.claude/PRPs/reports/{plan-name}-report.md`:
+
+```markdown
+# Implementation Report: [Feature Name]
+
+## Summary
+[What was implemented]
+
+## Assessment vs Reality
+
+| Metric | Predicted (Plan) | Actual |
+|---|---|---|
+| Complexity | [from plan] | [actual] |
+| Confidence | [from plan] | [actual] |
+| Files Changed | [from plan] | [actual count] |
+
+## Tasks Completed
+
+| # | Task | Status | Notes |
+|---|---|---|---|
+| 1 | [task name] | [done] Complete | |
+| 2 | [task name] | [done] Complete | Deviated — [reason] |
+
+## Validation Results
+
+| Level | Status | Notes |
+|---|---|---|
+| Static Analysis | [done] Pass | |
+| Unit Tests | [done] Pass | N tests written |
+| Build | [done] Pass | |
+| Integration | [done] Pass | or N/A |
+| Edge Cases | [done] Pass | |
+
+## Files Changed
+
+| File | Action | Lines |
+|---|---|---|
+| `path/to/file` | CREATED | +N |
+| `path/to/file` | UPDATED | +N / -M |
+
+## Deviations from Plan
+[List any deviations with WHAT and WHY, or "None"]
+
+## Issues Encountered
+[List any problems and how they were resolved, or "None"]
+
+## Tests Written
+
+| Test File | Tests | Coverage |
+|---|---|---|
+| `path/to/test` | N tests | [area covered] |
+
+## Next Steps
+- [ ] Code review via `/code-review`
+- [ ] Create PR via `/prp-pr`
+```
+
+### Update PRD (if applicable)
+
+If this implementation was for a PRD phase:
+1. Update the phase status from `in-progress` to `complete`
+2. Add report path as reference
+
+### Archive Plan
+
+```bash
+mkdir -p .claude/PRPs/plans/completed
+mv "$ARGUMENTS" .claude/PRPs/plans/completed/
+```
+
+**CHECKPOINT**: Report created. PRD updated. Plan archived.
+
+---
+
+## Phase 6 — OUTPUT
+
+Report to user:
+
+```
+## Implementation Complete
+
+- **Plan**: [plan file path] → archived to completed/
+- **Branch**: [current branch name]
+- **Status**: [done] All tasks complete
+
+### Validation Summary
+
+| Check | Status |
+|---|---|
+| Type Check | [done] |
+| Lint | [done] |
+| Tests | [done] (N written) |
+| Build | [done] |
+| Integration | [done] or N/A |
+
+### Files Changed
+- [N] files created, [M] files updated
+
+### Deviations
+[Summary or "None — implemented exactly as planned"]
+
+### Artifacts
+- Report: `.claude/PRPs/reports/{name}-report.md`
+- Archived Plan: `.claude/PRPs/plans/completed/{name}.plan.md`
+
+### PRD Progress (if applicable)
+| Phase | Status |
+|---|---|
+| Phase 1 | [done] Complete |
+| Phase 2 | [next] |
+| ... | ... |
+
+> Next step: Run `/prp-pr` to create a pull request, or `/code-review` to review changes first.
+```
+
+---
+
+## Handling Failures
+
+### Type Check Fails
+1. Read the error message carefully
+2. Fix the type error in the source file
+3. Re-run type-check
+4. Continue only when clean
+
+### Tests Fail
+1. Identify whether the bug is in the implementation or the test
+2. Fix the root cause (usually the implementation)
+3. Re-run tests
+4. Continue only when green
+
+### Lint Fails
+1. Run auto-fix first
+2. If errors remain, fix manually
+3. Re-run lint
+4. Continue only when clean
+
+### Build Fails
+1. Usually a type or import issue — check error message
+2. Fix the offending file
+3. Re-run build
+4. Continue only when successful
+
+### Integration Test Fails
+1. Check server started correctly
+2. Verify endpoint/route exists
+3. Check request format matches expected
+4. Fix and re-run
+
+---
+
+## Success Criteria
+
+- **TASKS_COMPLETE**: All tasks from the plan executed
+- **TYPES_PASS**: Zero type errors
+- **LINT_PASS**: Zero lint errors
+- **TESTS_PASS**: All tests green, new tests written
+- **BUILD_PASS**: Build succeeds
+- **REPORT_CREATED**: Implementation report saved
+- **PLAN_ARCHIVED**: Plan moved to `completed/`
+
+---
+
+## Next Steps
+
+- Run `/code-review` to review changes before committing
+- Run `/prp-commit` to commit with a descriptive message
+- Run `/prp-pr` to create a pull request
+- Run `/prp-plan ` if the PRD has more phases
diff --git a/pi/core/commands/prp-plan.md b/pi/core/commands/prp-plan.md
new file mode 100644
index 000000000..7d7e06c4a
--- /dev/null
+++ b/pi/core/commands/prp-plan.md
@@ -0,0 +1,502 @@
+---
+description: Create comprehensive feature implementation plan with codebase analysis and pattern extraction
+argument-hint:
+---
+
+> Adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
+
+# PRP Plan
+
+Create a detailed, self-contained implementation plan that captures all codebase patterns, conventions, and context needed to implement a feature in a single pass.
+
+**Core Philosophy**: A great plan contains everything needed to implement without asking further questions. Every pattern, every convention, every gotcha — captured once, referenced throughout.
+
+**Golden Rule**: If you would need to search the codebase during implementation, capture that knowledge NOW in the plan.
+
+---
+
+## Phase 0 — DETECT
+
+Determine input type from `$ARGUMENTS`:
+
+| Input Pattern | Detection | Action |
+|---|---|---|
+| Path ending in `.prd.md` | File path to PRD | Parse PRD, find next pending phase |
+| Path to `.md` with "Implementation Phases" | PRD-like document | Parse phases, find next pending |
+| Path to any other file | Reference file | Read file for context, treat as free-form |
+| Free-form text | Feature description | Proceed directly to Phase 1 |
+| Empty / blank | No input | Ask user what feature to plan |
+
+### PRD Parsing (when input is a PRD)
+
+1. Read the PRD file with `cat "$PRD_PATH"`
+2. Parse the **Implementation Phases** section
+3. Find phases by status:
+ - Look for `pending` phases
+ - Check dependency chains (a phase may depend on prior phases being `complete`)
+ - Select the **next eligible pending phase**
+4. Extract from the selected phase:
+ - Phase name and description
+ - Acceptance criteria
+ - Dependencies on prior phases
+ - Any scope notes or constraints
+5. Use the phase description as the feature to plan
+
+If no pending phases remain, report that all phases are complete.
+
+---
+
+## Phase 1 — PARSE
+
+Extract and clarify the feature requirements.
+
+### Feature Understanding
+
+From the input (PRD phase or free-form description), identify:
+
+- **What** is being built (concrete deliverable)
+- **Why** it matters (user value)
+- **Who** uses it (target user/system)
+- **Where** it fits (which part of the codebase)
+
+### User Story
+
+Format as:
+```
+As a [type of user],
+I want [capability],
+So that [benefit].
+```
+
+### Complexity Assessment
+
+| Level | Indicators | Typical Scope |
+|---|---|---|
+| **Small** | Single file, isolated change, no new dependencies | 1-3 files, <100 lines |
+| **Medium** | Multiple files, follows existing patterns, minor new concepts | 3-10 files, 100-500 lines |
+| **Large** | Cross-cutting concerns, new patterns, external integrations | 10+ files, 500+ lines |
+| **XL** | Architectural changes, new subsystems, migration needed | 20+ files, consider splitting |
+
+### Ambiguity Gate
+
+If any of these are unclear, **STOP and ask the user** before proceeding:
+
+- The core deliverable is vague
+- Success criteria are undefined
+- There are multiple valid interpretations
+- Technical approach has major unknowns
+
+Do NOT guess. Ask. A plan built on assumptions fails during implementation.
+
+---
+
+## Phase 2 — EXPLORE
+
+Gather deep codebase intelligence. Search the codebase directly for each category below.
+
+### Codebase Search (8 Categories)
+
+For each category, search using grep, find, and file reading:
+
+1. **Similar Implementations** — Find existing features that resemble the planned one. Look for analogous patterns, endpoints, components, or modules.
+
+2. **Naming Conventions** — Identify how files, functions, variables, classes, and exports are named in the relevant area of the codebase.
+
+3. **Error Handling** — Find how errors are caught, propagated, logged, and returned to users in similar code paths.
+
+4. **Logging Patterns** — Identify what gets logged, at what level, and in what format.
+
+5. **Type Definitions** — Find relevant types, interfaces, schemas, and how they're organized.
+
+6. **Test Patterns** — Find how similar features are tested. Note test file locations, naming, setup/teardown patterns, and assertion styles.
+
+7. **Configuration** — Find relevant config files, environment variables, and feature flags.
+
+8. **Dependencies** — Identify packages, imports, and internal modules used by similar features.
+
+### Codebase Analysis (5 Traces)
+
+Read relevant files to trace:
+
+1. **Entry Points** — How does a request/action enter the system and reach the area you're modifying?
+2. **Data Flow** — How does data move through the relevant code paths?
+3. **State Changes** — What state is modified and where?
+4. **Contracts** — What interfaces, APIs, or protocols must be honored?
+5. **Patterns** — What architectural patterns are used (repository, service, controller, etc.)?
+
+### Unified Discovery Table
+
+Compile findings into a single reference:
+
+| Category | File:Lines | Pattern | Key Snippet |
+|---|---|---|---|
+| Naming | `src/services/userService.ts:1-5` | camelCase services, PascalCase types | `export class UserService` |
+| Error | `src/middleware/errorHandler.ts:10-25` | Custom AppError class | `throw new AppError(...)` |
+| ... | ... | ... | ... |
+
+---
+
+## Phase 3 — RESEARCH
+
+If the feature involves external libraries, APIs, or unfamiliar technology:
+
+1. Search the web for official documentation
+2. Find usage examples and best practices
+3. Identify version-specific gotchas
+
+Format each finding as:
+
+```
+KEY_INSIGHT: [what you learned]
+APPLIES_TO: [which part of the plan this affects]
+GOTCHA: [any warnings or version-specific issues]
+```
+
+If the feature uses only well-understood internal patterns, skip this phase and note: "No external research needed — feature uses established internal patterns."
+
+---
+
+## Phase 4 — DESIGN
+
+### UX Transformation (if applicable)
+
+Document the before/after user experience:
+
+**Before:**
+```
+┌─────────────────────────────┐
+│ [Current user experience] │
+│ Show the current flow, │
+│ what the user sees/does │
+└─────────────────────────────┘
+```
+
+**After:**
+```
+┌─────────────────────────────┐
+│ [New user experience] │
+│ Show the improved flow, │
+│ what changes for the user │
+└─────────────────────────────┘
+```
+
+### Interaction Changes
+
+| Touchpoint | Before | After | Notes |
+|---|---|---|---|
+| ... | ... | ... | ... |
+
+If the feature is purely backend/internal with no UX change, note: "Internal change — no user-facing UX transformation."
+
+---
+
+## Phase 5 — ARCHITECT
+
+### Strategic Design
+
+Define the implementation approach:
+
+- **Approach**: High-level strategy (e.g., "Add new service layer following existing repository pattern")
+- **Alternatives Considered**: What other approaches were evaluated and why they were rejected
+- **Scope**: Concrete boundaries of what WILL be built
+- **NOT Building**: Explicit list of what is OUT OF SCOPE (prevents scope creep during implementation)
+
+---
+
+## Phase 6 — GENERATE
+
+Write the full plan document using the template below. Save to `.claude/PRPs/plans/{kebab-case-feature-name}.plan.md`.
+
+Create the directory if it doesn't exist:
+```bash
+mkdir -p .claude/PRPs/plans
+```
+
+### Plan Template
+
+````markdown
+# Plan: [Feature Name]
+
+## Summary
+[2-3 sentence overview]
+
+## User Story
+As a [user], I want [capability], so that [benefit].
+
+## Problem → Solution
+[Current state] → [Desired state]
+
+## Metadata
+- **Complexity**: [Small | Medium | Large | XL]
+- **Source PRD**: [path or "N/A"]
+- **PRD Phase**: [phase name or "N/A"]
+- **Estimated Files**: [count]
+
+---
+
+## UX Design
+
+### Before
+[ASCII diagram or "N/A — internal change"]
+
+### After
+[ASCII diagram or "N/A — internal change"]
+
+### Interaction Changes
+| Touchpoint | Before | After | Notes |
+|---|---|---|---|
+
+---
+
+## Mandatory Reading
+
+Files that MUST be read before implementing:
+
+| Priority | File | Lines | Why |
+|---|---|---|---|
+| P0 (critical) | `path/to/file` | 1-50 | Core pattern to follow |
+| P1 (important) | `path/to/file` | 10-30 | Related types |
+| P2 (reference) | `path/to/file` | all | Similar implementation |
+
+## External Documentation
+
+| Topic | Source | Key Takeaway |
+|---|---|---|
+| ... | ... | ... |
+
+---
+
+## Patterns to Mirror
+
+Code patterns discovered in the codebase. Follow these exactly.
+
+### NAMING_CONVENTION
+// SOURCE: [file:lines]
+[actual code snippet showing the naming pattern]
+
+### ERROR_HANDLING
+// SOURCE: [file:lines]
+[actual code snippet showing error handling]
+
+### LOGGING_PATTERN
+// SOURCE: [file:lines]
+[actual code snippet showing logging]
+
+### REPOSITORY_PATTERN
+// SOURCE: [file:lines]
+[actual code snippet showing data access]
+
+### SERVICE_PATTERN
+// SOURCE: [file:lines]
+[actual code snippet showing service layer]
+
+### TEST_STRUCTURE
+// SOURCE: [file:lines]
+[actual code snippet showing test setup]
+
+---
+
+## Files to Change
+
+| File | Action | Justification |
+|---|---|---|
+| `path/to/file.ts` | CREATE | New service for feature |
+| `path/to/existing.ts` | UPDATE | Add new method |
+
+## NOT Building
+
+- [Explicit item 1 that is out of scope]
+- [Explicit item 2 that is out of scope]
+
+---
+
+## Step-by-Step Tasks
+
+### Task 1: [Name]
+- **ACTION**: [What to do]
+- **IMPLEMENT**: [Specific code/logic to write]
+- **MIRROR**: [Pattern from Patterns to Mirror section to follow]
+- **IMPORTS**: [Required imports]
+- **GOTCHA**: [Known pitfall to avoid]
+- **VALIDATE**: [How to verify this task is correct]
+
+### Task 2: [Name]
+- **ACTION**: ...
+- **IMPLEMENT**: ...
+- **MIRROR**: ...
+- **IMPORTS**: ...
+- **GOTCHA**: ...
+- **VALIDATE**: ...
+
+[Continue for all tasks...]
+
+---
+
+## Testing Strategy
+
+### Unit Tests
+
+| Test | Input | Expected Output | Edge Case? |
+|---|---|---|---|
+| ... | ... | ... | ... |
+
+### Edge Cases Checklist
+- [ ] Empty input
+- [ ] Maximum size input
+- [ ] Invalid types
+- [ ] Concurrent access
+- [ ] Network failure (if applicable)
+- [ ] Permission denied
+
+---
+
+## Validation Commands
+
+### Static Analysis
+```bash
+# Run type checker
+[project-specific type check command]
+```
+EXPECT: Zero type errors
+
+### Unit Tests
+```bash
+# Run tests for affected area
+[project-specific test command]
+```
+EXPECT: All tests pass
+
+### Full Test Suite
+```bash
+# Run complete test suite
+[project-specific full test command]
+```
+EXPECT: No regressions
+
+### Database Validation (if applicable)
+```bash
+# Verify schema/migrations
+[project-specific db command]
+```
+EXPECT: Schema up to date
+
+### Browser Validation (if applicable)
+```bash
+# Start dev server and verify
+[project-specific dev server command]
+```
+EXPECT: Feature works as designed
+
+### Manual Validation
+- [ ] [Step-by-step manual verification checklist]
+
+---
+
+## Acceptance Criteria
+- [ ] All tasks completed
+- [ ] All validation commands pass
+- [ ] Tests written and passing
+- [ ] No type errors
+- [ ] No lint errors
+- [ ] Matches UX design (if applicable)
+
+## Completion Checklist
+- [ ] Code follows discovered patterns
+- [ ] Error handling matches codebase style
+- [ ] Logging follows codebase conventions
+- [ ] Tests follow test patterns
+- [ ] No hardcoded values
+- [ ] Documentation updated (if needed)
+- [ ] No unnecessary scope additions
+- [ ] Self-contained — no questions needed during implementation
+
+## Risks
+| Risk | Likelihood | Impact | Mitigation |
+|---|---|---|---|
+| ... | ... | ... | ... |
+
+## Notes
+[Any additional context, decisions, or observations]
+```
+
+---
+
+## Output
+
+### Save the Plan
+
+Write the generated plan to:
+```
+.claude/PRPs/plans/{kebab-case-feature-name}.plan.md
+```
+
+### Update PRD (if input was a PRD)
+
+If this plan was generated from a PRD phase:
+1. Update the phase status from `pending` to `in-progress`
+2. Add the plan file path as a reference in the phase
+
+### Report to User
+
+```
+## Plan Created
+
+- **File**: .claude/PRPs/plans/{kebab-case-feature-name}.plan.md
+- **Source PRD**: [path or "N/A"]
+- **Phase**: [phase name or "standalone"]
+- **Complexity**: [level]
+- **Scope**: [N files, M tasks]
+- **Key Patterns**: [top 3 discovered patterns]
+- **External Research**: [topics researched or "none needed"]
+- **Risks**: [top risk or "none identified"]
+- **Confidence Score**: [1-10] — likelihood of single-pass implementation
+
+> Next step: Run `/prp-implement .claude/PRPs/plans/{name}.plan.md` to execute this plan.
+```
+
+---
+
+## Verification
+
+Before finalizing, verify the plan against these checklists:
+
+### Context Completeness
+- [ ] All relevant files discovered and documented
+- [ ] Naming conventions captured with examples
+- [ ] Error handling patterns documented
+- [ ] Test patterns identified
+- [ ] Dependencies listed
+
+### Implementation Readiness
+- [ ] Every task has ACTION, IMPLEMENT, MIRROR, and VALIDATE
+- [ ] No task requires additional codebase searching
+- [ ] Import paths are specified
+- [ ] GOTCHAs documented where applicable
+
+### Pattern Faithfulness
+- [ ] Code snippets are actual codebase examples (not invented)
+- [ ] SOURCE references point to real files and line numbers
+- [ ] Patterns cover naming, errors, logging, data access, and tests
+- [ ] New code will be indistinguishable from existing code
+
+### Validation Coverage
+- [ ] Static analysis commands specified
+- [ ] Test commands specified
+- [ ] Build verification included
+
+### UX Clarity
+- [ ] Before/after states documented (or marked N/A)
+- [ ] Interaction changes listed
+- [ ] Edge cases for UX identified
+
+### No Prior Knowledge Test
+A developer unfamiliar with this codebase should be able to implement the feature using ONLY this plan, without searching the codebase or asking questions. If not, add the missing context.
+
+---
+
+## Next Steps
+
+- Run `/prp-implement ` to execute this plan
+- Run `/plan` for quick conversational planning without artifacts
+- Run `/prp-prd` to create a PRD first if scope is unclear
+````
diff --git a/pi/core/commands/prp-pr.md b/pi/core/commands/prp-pr.md
new file mode 100644
index 000000000..2016ec90b
--- /dev/null
+++ b/pi/core/commands/prp-pr.md
@@ -0,0 +1,184 @@
+---
+description: "Alias of /pr for the PRP workflow series. Use when creating a pull request mid-PRP workflow; otherwise use /pr."
+argument-hint: "[base-branch] (default: main)"
+---
+
+# Create Pull Request
+
+> Adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
+
+**Input**: `$ARGUMENTS` — optional, may contain a base branch name and/or flags (e.g., `--draft`).
+
+**Parse `$ARGUMENTS`**:
+- Extract any recognized flags (`--draft`)
+- Treat remaining non-flag text as the base branch name
+- Default base branch to `main` if none specified
+
+---
+
+## Phase 1 — VALIDATE
+
+Check preconditions:
+
+```bash
+git branch --show-current
+git status --short
+git log origin/ ..HEAD --oneline
+```
+
+| Check | Condition | Action if Failed |
+|---|---|---|
+| Not on base branch | Current branch ≠ base | Stop: "Switch to a feature branch first." |
+| Clean working directory | No uncommitted changes | Warn: "You have uncommitted changes. Commit or stash first. Use `/prp-commit` to commit." |
+| Has commits ahead | `git log origin/ ..HEAD` not empty | Stop: "No commits ahead of ` `. Nothing to PR." |
+| No existing PR | `gh pr list --head --json number` is empty | Stop: "PR already exists: #. Use `gh pr view --web` to open it." |
+
+If all checks pass, proceed.
+
+---
+
+## Phase 2 — DISCOVER
+
+### PR Template
+
+Search for PR template in order:
+
+1. `.github/PULL_REQUEST_TEMPLATE/` directory — if exists, list files and let user choose (or use `default.md`)
+2. `.github/PULL_REQUEST_TEMPLATE.md`
+3. `.github/pull_request_template.md`
+4. `docs/pull_request_template.md`
+
+If found, read it and use its structure for the PR body.
+
+### Commit Analysis
+
+```bash
+git log origin/ ..HEAD --format="%h %s" --reverse
+```
+
+Analyze commits to determine:
+- **PR title**: Use conventional commit format with type prefix — `feat: ...`, `fix: ...`, etc.
+ - If multiple types, use the dominant one
+ - If single commit, use its message as-is
+- **Change summary**: Group commits by type/area
+
+### File Analysis
+
+```bash
+git diff origin/ ..HEAD --stat
+git diff origin/ ..HEAD --name-only
+```
+
+Categorize changed files: source, tests, docs, config, migrations.
+
+### PRP Artifacts
+
+Check for related PRP artifacts:
+- `.claude/PRPs/reports/` — Implementation reports
+- `.claude/PRPs/plans/` — Plans that were executed
+- `.claude/PRPs/prds/` — Related PRDs
+
+Reference these in the PR body if they exist.
+
+---
+
+## Phase 3 — PUSH
+
+```bash
+git push -u origin HEAD
+```
+
+If push fails due to divergence:
+```bash
+git fetch origin
+git rebase origin/
+git push -u origin HEAD
+```
+
+If rebase conflicts occur, stop and inform the user.
+
+---
+
+## Phase 4 — CREATE
+
+### With Template
+
+If a PR template was found in Phase 2, fill in each section using the commit and file analysis. Preserve all template sections — leave sections as "N/A" if not applicable rather than removing them.
+
+### Without Template
+
+Use this default format:
+
+```markdown
+## Summary
+
+<1-2 sentence description of what this PR does and why>
+
+## Changes
+
+
+
+## Files Changed
+
+
+
+## Testing
+
+
+
+## Related Issues
+
+
+```
+
+### Create the PR
+
+```bash
+gh pr create \
+ --title "" \
+ --base \
+ --body ""
+ # Add --draft if the --draft flag was parsed from $ARGUMENTS
+```
+
+---
+
+## Phase 5 — VERIFY
+
+```bash
+gh pr view --json number,url,title,state,baseRefName,headRefName,additions,deletions,changedFiles
+gh pr checks --json name,status,conclusion 2>/dev/null || true
+```
+
+---
+
+## Phase 6 — OUTPUT
+
+Report to user:
+
+```
+PR #:
+URL:
+Branch: →
+Changes: + - across files
+
+CI Checks:
+
+Artifacts referenced:
+ -
+
+Next steps:
+ - gh pr view --web → open in browser
+ - /code-review → review the PR
+ - gh pr merge → merge when ready
+```
+
+---
+
+## Edge Cases
+
+- **No `gh` CLI**: Stop with: "GitHub CLI (`gh`) is required. Install: "
+- **Not authenticated**: Stop with: "Run `gh auth login` first."
+- **Force push needed**: If remote has diverged and rebase was done, use `git push --force-with-lease` (never `--force`).
+- **Multiple PR templates**: If `.github/PULL_REQUEST_TEMPLATE/` has multiple files, list them and ask user to choose.
+- **Large PR (>20 files)**: Warn about PR size. Suggest splitting if changes are logically separable.
diff --git a/pi/core/commands/prp-prd.md b/pi/core/commands/prp-prd.md
new file mode 100644
index 000000000..5292c38c4
--- /dev/null
+++ b/pi/core/commands/prp-prd.md
@@ -0,0 +1,447 @@
+---
+description: "Interactive PRD generator - problem-first, hypothesis-driven product spec with back-and-forth questioning"
+argument-hint: "[feature/product idea] (blank = start with questions)"
+---
+
+# Product Requirements Document Generator
+
+> Adapted from PRPs-agentic-eng by Wirasm. Part of the PRP workflow series.
+
+**Input**: $ARGUMENTS
+
+---
+
+## Your Role
+
+You are a sharp product manager who:
+- Starts with PROBLEMS, not solutions
+- Demands evidence before building
+- Thinks in hypotheses, not specs
+- Asks clarifying questions before assuming
+- Acknowledges uncertainty honestly
+
+**Anti-pattern**: Don't fill sections with fluff. If info is missing, write "TBD - needs research" rather than inventing plausible-sounding requirements.
+
+---
+
+## Process Overview
+
+```
+QUESTION SET 1 → GROUNDING → QUESTION SET 2 → RESEARCH → QUESTION SET 3 → GENERATE
+```
+
+Each question set builds on previous answers. Grounding phases validate assumptions.
+
+---
+
+## Phase 1: INITIATE - Core Problem
+
+**If no input provided**, ask:
+
+> **What do you want to build?**
+> Describe the product, feature, or capability in a few sentences.
+
+**If input provided**, confirm understanding by restating:
+
+> I understand you want to build: {restated understanding}
+> Is this correct, or should I adjust my understanding?
+
+**GATE**: Wait for user response before proceeding.
+
+---
+
+## Phase 2: FOUNDATION - Problem Discovery
+
+Ask these questions (present all at once, user can answer together):
+
+> **Foundation Questions:**
+>
+> 1. **Who** has this problem? Be specific - not just "users" but what type of person/role?
+>
+> 2. **What** problem are they facing? Describe the observable pain, not the assumed need.
+>
+> 3. **Why** can't they solve it today? What alternatives exist and why do they fail?
+>
+> 4. **Why now?** What changed that makes this worth building?
+>
+> 5. **How** will you know if you solved it? What would success look like?
+
+**GATE**: Wait for user responses before proceeding.
+
+---
+
+## Phase 3: GROUNDING - Market & Context Research
+
+After foundation answers, conduct research:
+
+**Research market context:**
+
+1. Find similar products/features in the market
+2. Identify how competitors solve this problem
+3. Note common patterns and anti-patterns
+4. Check for recent trends or changes in this space
+
+Compile findings with direct links, key insights, and any gaps in available information.
+
+**If a codebase exists, explore it in parallel:**
+
+1. Find existing functionality relevant to the product/feature idea
+2. Identify patterns that could be leveraged
+3. Note technical constraints or opportunities
+
+Record file locations, code patterns, and conventions observed.
+
+**Summarize findings to user:**
+
+> **What I found:**
+> - {Market insight 1}
+> - {Competitor approach}
+> - {Relevant pattern from codebase, if applicable}
+>
+> Does this change or refine your thinking?
+
+**GATE**: Brief pause for user input (can be "continue" or adjustments).
+
+---
+
+## Phase 4: DEEP DIVE - Vision & Users
+
+Based on foundation + research, ask:
+
+> **Vision & Users:**
+>
+> 1. **Vision**: In one sentence, what's the ideal end state if this succeeds wildly?
+>
+> 2. **Primary User**: Describe your most important user - their role, context, and what triggers their need.
+>
+> 3. **Job to Be Done**: Complete this: "When [situation], I want to [motivation], so I can [outcome]."
+>
+> 4. **Non-Users**: Who is explicitly NOT the target? Who should we ignore?
+>
+> 5. **Constraints**: What limitations exist? (time, budget, technical, regulatory)
+
+**GATE**: Wait for user responses before proceeding.
+
+---
+
+## Phase 5: GROUNDING - Technical Feasibility
+
+**If a codebase exists, perform two parallel investigations:**
+
+Investigation 1 — Explore feasibility:
+1. Identify existing infrastructure that can be leveraged
+2. Find similar patterns already implemented
+3. Map integration points and dependencies
+4. Locate relevant configuration and type definitions
+
+Record file locations, code patterns, and conventions observed.
+
+Investigation 2 — Analyze constraints:
+1. Trace how existing related features are implemented end-to-end
+2. Map data flow through potential integration points
+3. Identify architectural patterns and boundaries
+4. Estimate complexity based on similar features
+
+Document what exists with precise file:line references. No suggestions.
+
+**If no codebase, research technical approaches:**
+
+1. Find technical approaches others have used
+2. Identify common implementation patterns
+3. Note known technical challenges and pitfalls
+
+Compile findings with citations and gap analysis.
+
+**Summarize to user:**
+
+> **Technical Context:**
+> - Feasibility: {HIGH/MEDIUM/LOW} because {reason}
+> - Can leverage: {existing patterns/infrastructure}
+> - Key technical risk: {main concern}
+>
+> Any technical constraints I should know about?
+
+**GATE**: Brief pause for user input.
+
+---
+
+## Phase 6: DECISIONS - Scope & Approach
+
+Ask final clarifying questions:
+
+> **Scope & Approach:**
+>
+> 1. **MVP Definition**: What's the absolute minimum to test if this works?
+>
+> 2. **Must Have vs Nice to Have**: What 2-3 things MUST be in v1? What can wait?
+>
+> 3. **Key Hypothesis**: Complete this: "We believe [capability] will [solve problem] for [users]. We'll know we're right when [measurable outcome]."
+>
+> 4. **Out of Scope**: What are you explicitly NOT building (even if users ask)?
+>
+> 5. **Open Questions**: What uncertainties could change the approach?
+
+**GATE**: Wait for user responses before generating.
+
+---
+
+## Phase 7: GENERATE - Write PRD
+
+**Output path**: `.claude/PRPs/prds/{kebab-case-name}.prd.md`
+
+Create directory if needed: `mkdir -p .claude/PRPs/prds`
+
+### PRD Template
+
+```markdown
+# {Product/Feature Name}
+
+## Problem Statement
+
+{2-3 sentences: Who has what problem, and what's the cost of not solving it?}
+
+## Evidence
+
+- {User quote, data point, or observation that proves this problem exists}
+- {Another piece of evidence}
+- {If none: "Assumption - needs validation through [method]"}
+
+## Proposed Solution
+
+{One paragraph: What we're building and why this approach over alternatives}
+
+## Key Hypothesis
+
+We believe {capability} will {solve problem} for {users}.
+We'll know we're right when {measurable outcome}.
+
+## What We're NOT Building
+
+- {Out of scope item 1} - {why}
+- {Out of scope item 2} - {why}
+
+## Success Metrics
+
+| Metric | Target | How Measured |
+|--------|--------|--------------|
+| {Primary metric} | {Specific number} | {Method} |
+| {Secondary metric} | {Specific number} | {Method} |
+
+## Open Questions
+
+- [ ] {Unresolved question 1}
+- [ ] {Unresolved question 2}
+
+---
+
+## Users & Context
+
+**Primary User**
+- **Who**: {Specific description}
+- **Current behavior**: {What they do today}
+- **Trigger**: {What moment triggers the need}
+- **Success state**: {What "done" looks like}
+
+**Job to Be Done**
+When {situation}, I want to {motivation}, so I can {outcome}.
+
+**Non-Users**
+{Who this is NOT for and why}
+
+---
+
+## Solution Detail
+
+### Core Capabilities (MoSCoW)
+
+| Priority | Capability | Rationale |
+|----------|------------|-----------|
+| Must | {Feature} | {Why essential} |
+| Must | {Feature} | {Why essential} |
+| Should | {Feature} | {Why important but not blocking} |
+| Could | {Feature} | {Nice to have} |
+| Won't | {Feature} | {Explicitly deferred and why} |
+
+### MVP Scope
+
+{What's the minimum to validate the hypothesis}
+
+### User Flow
+
+{Critical path - shortest journey to value}
+
+---
+
+## Technical Approach
+
+**Feasibility**: {HIGH/MEDIUM/LOW}
+
+**Architecture Notes**
+- {Key technical decision and why}
+- {Dependency or integration point}
+
+**Technical Risks**
+
+| Risk | Likelihood | Mitigation |
+|------|------------|------------|
+| {Risk} | {H/M/L} | {How to handle} |
+
+---
+
+## Implementation Phases
+
+
+
+| # | Phase | Description | Status | Parallel | Depends | PRP Plan |
+|---|-------|-------------|--------|----------|---------|----------|
+| 1 | {Phase name} | {What this phase delivers} | pending | - | - | - |
+| 2 | {Phase name} | {What this phase delivers} | pending | - | 1 | - |
+| 3 | {Phase name} | {What this phase delivers} | pending | with 4 | 2 | - |
+| 4 | {Phase name} | {What this phase delivers} | pending | with 3 | 2 | - |
+| 5 | {Phase name} | {What this phase delivers} | pending | - | 3, 4 | - |
+
+### Phase Details
+
+**Phase 1: {Name}**
+- **Goal**: {What we're trying to achieve}
+- **Scope**: {Bounded deliverables}
+- **Success signal**: {How we know it's done}
+
+**Phase 2: {Name}**
+- **Goal**: {What we're trying to achieve}
+- **Scope**: {Bounded deliverables}
+- **Success signal**: {How we know it's done}
+
+{Continue for each phase...}
+
+### Parallelism Notes
+
+{Explain which phases can run in parallel and why}
+
+---
+
+## Decisions Log
+
+| Decision | Choice | Alternatives | Rationale |
+|----------|--------|--------------|-----------|
+| {Decision} | {Choice} | {Options considered} | {Why this one} |
+
+---
+
+## Research Summary
+
+**Market Context**
+{Key findings from market research}
+
+**Technical Context**
+{Key findings from technical exploration}
+
+---
+
+*Generated: {timestamp}*
+*Status: DRAFT - needs validation*
+```
+
+---
+
+## Phase 8: OUTPUT - Summary
+
+After generating, report:
+
+```markdown
+## PRD Created
+
+**File**: `.claude/PRPs/prds/{name}.prd.md`
+
+### Summary
+
+**Problem**: {One line}
+**Solution**: {One line}
+**Key Metric**: {Primary success metric}
+
+### Validation Status
+
+| Section | Status |
+|---------|--------|
+| Problem Statement | {Validated/Assumption} |
+| User Research | {Done/Needed} |
+| Technical Feasibility | {Assessed/TBD} |
+| Success Metrics | {Defined/Needs refinement} |
+
+### Open Questions ({count})
+
+{List the open questions that need answers}
+
+### Recommended Next Step
+
+{One of: user research, technical spike, prototype, stakeholder review, etc.}
+
+### Implementation Phases
+
+| # | Phase | Status | Can Parallel |
+|---|-------|--------|--------------|
+{Table of phases from PRD}
+
+### To Start Implementation
+
+Run: `/prp-plan .claude/PRPs/prds/{name}.prd.md`
+
+This will automatically select the next pending phase and create an implementation plan.
+```
+
+---
+
+## Question Flow Summary
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ INITIATE: "What do you want to build?" │
+└─────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────┐
+│ FOUNDATION: Who, What, Why, Why now, How to measure │
+└─────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────┐
+│ GROUNDING: Market research, competitor analysis │
+└─────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────┐
+│ DEEP DIVE: Vision, Primary user, JTBD, Constraints │
+└─────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────┐
+│ GROUNDING: Technical feasibility, codebase exploration │
+└─────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────┐
+│ DECISIONS: MVP, Must-haves, Hypothesis, Out of scope │
+└─────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────┐
+│ GENERATE: Write PRD to .claude/PRPs/prds/ │
+└─────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Integration with ECC
+
+After PRD generation:
+- Use `/prp-plan` to create implementation plans from PRD phases
+- Use `/plan` for simpler planning without PRD structure
+- Use `/save-session` to preserve PRD context across sessions
+
+## Success Criteria
+
+- **PROBLEM_VALIDATED**: Problem is specific and evidenced (or marked as assumption)
+- **USER_DEFINED**: Primary user is concrete, not generic
+- **HYPOTHESIS_CLEAR**: Testable hypothesis with measurable outcome
+- **SCOPE_BOUNDED**: Clear must-haves and explicit out-of-scope
+- **QUESTIONS_ACKNOWLEDGED**: Uncertainties are listed, not hidden
+- **ACTIONABLE**: A skeptic could understand why this is worth building
diff --git a/pi/core/commands/react-test.md b/pi/core/commands/react-test.md
new file mode 100644
index 000000000..5cff72077
--- /dev/null
+++ b/pi/core/commands/react-test.md
@@ -0,0 +1,265 @@
+---
+description: Enforce TDD workflow for React. Write React Testing Library tests first (behavior-focused, accessibility-first), then implement components. Detects Vitest or Jest and verifies coverage targets.
+---
+
+# React TDD Command
+
+This command enforces test-driven development for React using React Testing Library plus Vitest or Jest, detected at runtime.
+
+## What This Command Does
+
+1. **Define Component Signature**: Scaffold the component, prop type, and exports
+2. **Write Behavior Tests First**: RTL queries (role-first), `userEvent`, MSW for network — RED
+3. **Run Tests**: Verify they fail for the right reason
+4. **Implement Minimal Code**: Just enough to pass — GREEN
+5. **Refactor**: Improve while keeping tests green
+6. **Check Coverage**: Hit the targets in [rules/react/testing.md](../rules/react/testing.md)
+
+## When to Use
+
+Use `/react-test` when:
+
+- Implementing a new React component or custom hook
+- Adding test coverage to an untested component
+- Fixing a bug (write failing test first that reproduces it)
+- Building forms, state machines, or accessibility-critical UI
+- Onboarding to RTL + Vitest/Jest workflow
+
+## TDD Cycle
+
+```
+RED -> Write failing test for the next behavior
+GREEN -> Implement minimal component code to pass
+REFACTOR -> Improve component, tests stay green
+REPEAT -> Next behavior
+```
+
+## Runner Detection
+
+```bash
+test -f vitest.config.ts -o -f vitest.config.js -o -f vite.config.ts # Vitest
+grep -l '"jest"' package.json # Jest
+```
+
+Prefer Vitest for new Vite-based projects; respect Jest for existing setups.
+
+## Example Session
+
+````text
+User: /react-test I need a SearchInput component with debounced search
+
+Agent:
+# TDD Session: SearchInput
+
+## Step 1: Define Signature
+
+```tsx
+// src/components/SearchInput.tsx
+type Props = {
+ onSearch: (query: string) => void;
+ placeholder?: string;
+ debounceMs?: number;
+};
+
+export function SearchInput({ onSearch, placeholder, debounceMs }: Props) {
+ throw new Error("not implemented");
+}
+```
+
+## Step 2: Write Behavior Tests (RED)
+
+```tsx
+// src/components/SearchInput.test.tsx
+import { describe, expect, test, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { SearchInput } from "./SearchInput";
+
+describe("SearchInput", () => {
+ test("renders with placeholder", () => {
+ render( {}} placeholder="Search users" />);
+ expect(screen.getByPlaceholderText("Search users")).toBeInTheDocument();
+ });
+
+ test("calls onSearch after typing", async () => {
+ vi.useFakeTimers();
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+ const onSearch = vi.fn();
+ render( );
+
+ await user.type(screen.getByRole("textbox"), "alice");
+
+ expect(onSearch).not.toHaveBeenCalled(); // before debounce
+ vi.advanceTimersByTime(300);
+ expect(onSearch).toHaveBeenCalledWith("alice"); // after debounce
+
+ vi.useRealTimers();
+ });
+
+ test("does not call onSearch when typing pauses then continues", async () => {
+ vi.useFakeTimers();
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+ const onSearch = vi.fn();
+ render( );
+
+ await user.type(screen.getByRole("textbox"), "ali");
+ vi.advanceTimersByTime(200); // mid-debounce
+ await user.type(screen.getByRole("textbox"), "ce");
+ vi.advanceTimersByTime(300);
+
+ expect(onSearch).toHaveBeenCalledTimes(1);
+ expect(onSearch).toHaveBeenCalledWith("alice");
+
+ vi.useRealTimers();
+ });
+
+ test("is keyboard reachable and accessible", () => {
+ render( {}} />);
+ const input = screen.getByRole("textbox");
+ input.focus();
+ expect(input).toHaveFocus();
+ });
+});
+```
+
+## Step 3: Run Tests — Verify FAIL
+
+```bash
+$ vitest run src/components/SearchInput.test.tsx
+
+× src/components/SearchInput.test.tsx (4 tests) ✘ Error: not implemented
+```
+
+✓ Tests fail as expected.
+
+## Step 4: Implement Minimal Code (GREEN)
+
+```tsx
+import { useEffect, useState } from "react";
+
+export function SearchInput({ onSearch, placeholder, debounceMs = 300 }: Props) {
+ const [query, setQuery] = useState("");
+
+ useEffect(() => {
+ const id = setTimeout(() => onSearch(query), debounceMs);
+ return () => clearTimeout(id);
+ }, [query, onSearch, debounceMs]);
+
+ return (
+ setQuery(e.target.value)}
+ />
+ );
+}
+```
+
+## Step 5: Run Tests — Verify PASS
+
+```bash
+$ vitest run src/components/SearchInput.test.tsx
+
+✓ src/components/SearchInput.test.tsx (4 tests) 47ms
+```
+
+## Step 6: Coverage
+
+```bash
+$ vitest run --coverage src/components/SearchInput.test.tsx
+
+% Stmts: 100 % Branch: 100 % Funcs: 100 % Lines: 100
+```
+
+## TDD Complete!
+````
+
+## Test Patterns
+
+### Behavior, not implementation
+
+Use `getByRole`, `getByLabelText`, `getByText`. Avoid `container.querySelector` and asserting on component state.
+
+### `userEvent.setup()` per test
+
+```tsx
+const user = userEvent.setup();
+await user.click(screen.getByRole("button", { name: /save/i }));
+```
+
+### MSW for network
+
+```tsx
+beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
+afterEach(() => server.resetHandlers());
+afterAll(() => server.close());
+
+server.use(http.post("/api/users", () => HttpResponse.json({ id: "1" }, { status: 201 })));
+```
+
+### Custom hooks
+
+```tsx
+const { result } = renderHook(() => useCounter(0));
+act(() => result.current.increment());
+expect(result.current.count).toBe(1);
+```
+
+### Accessibility
+
+```tsx
+import { axe } from "vitest-axe";
+expect(await axe(container)).toHaveNoViolations();
+```
+
+## Coverage Targets
+
+| Layer | Target |
+|---|---|
+| Pure utilities | >=90% |
+| Custom hooks | >=85% |
+| Presentational components | >=80% |
+| Container components | >=70% |
+| Pages | E2E covered separately |
+
+Configure in `vitest.config.ts` / `jest.config.js` to enforce thresholds in CI.
+
+## Anti-Patterns to Avoid
+
+- `container.querySelector(...)` — bypasses accessibility queries
+- Asserting on render count
+- Mocking `react` itself (`jest.mock("react", ...)`)
+- Mocking child components by default (mock only when child has heavy side effects)
+- Ignoring `act()` warnings — they signal real bugs
+- Snapshot tests of rendered components (brittle, rubber-stamped) — use Playwright/Cypress visual diff instead
+
+## Test Commands
+
+```bash
+# Vitest
+vitest # watch
+vitest run # one-shot
+vitest run --coverage # with coverage
+vitest run path/to/file.test.tsx # single file
+
+# Jest
+jest --watch
+jest --coverage
+jest path/to/file.test.tsx
+
+# CI mode
+CI=true vitest run --coverage
+```
+
+## Related Commands
+
+- `/react-build` — fix build errors before running tests
+- `/react-review` — review after implementation
+- `verification-loop` skill — full verification loop
+
+## Related
+
+- Skills: `skills/react-testing/`, `skills/tdd-workflow/`, `skills/accessibility/`, `skills/e2e-testing/`
+- Rules: `rules/react/testing.md`
+- Agents: `react-reviewer` (reviews test quality), `tdd-guide` (enforces TDD process)
diff --git a/pi/core/commands/refactor-clean.md b/pi/core/commands/refactor-clean.md
new file mode 100644
index 000000000..781a57853
--- /dev/null
+++ b/pi/core/commands/refactor-clean.md
@@ -0,0 +1,84 @@
+---
+description: Safely identify and remove dead code with verification after each change.
+---
+
+# Refactor Clean
+
+Safely identify and remove dead code with test verification at every step.
+
+## Step 1: Detect Dead Code
+
+Run analysis tools based on project type:
+
+| Tool | What It Finds | Command |
+|------|--------------|---------|
+| knip | Unused exports, files, dependencies | `npx knip` |
+| depcheck | Unused npm dependencies | `npx depcheck` |
+| ts-prune | Unused TypeScript exports | `npx ts-prune` |
+| vulture | Unused Python code | `vulture src/` |
+| deadcode | Unused Go code | `deadcode ./...` |
+| cargo-udeps | Unused Rust dependencies | `cargo +nightly udeps` |
+
+If no tool is available, use Grep to find exports with zero imports:
+```
+# Find exports, then check if they're imported anywhere
+```
+
+## Step 2: Categorize Findings
+
+Sort findings into safety tiers:
+
+| Tier | Examples | Action |
+|------|----------|--------|
+| **SAFE** | Unused utilities, test helpers, internal functions | Delete with confidence |
+| **CAUTION** | Components, API routes, middleware | Verify no dynamic imports or external consumers |
+| **DANGER** | Config files, entry points, type definitions | Investigate before touching |
+
+## Step 3: Safe Deletion Loop
+
+For each SAFE item:
+
+1. **Run full test suite** — Establish baseline (all green)
+2. **Delete the dead code** — Use Edit tool for surgical removal
+3. **Re-run test suite** — Verify nothing broke
+4. **If tests fail** — Immediately revert with `git checkout -- ` and skip this item
+5. **If tests pass** — Move to next item
+
+## Step 4: Handle CAUTION Items
+
+Before deleting CAUTION items:
+- Search for dynamic imports: `import()`, `require()`, `__import__`
+- Search for string references: route names, component names in configs
+- Check if exported from a public package API
+- Verify no external consumers (check dependents if published)
+
+## Step 5: Consolidate Duplicates
+
+After removing dead code, look for:
+- Near-duplicate functions (>80% similar) — merge into one
+- Redundant type definitions — consolidate
+- Wrapper functions that add no value — inline them
+- Re-exports that serve no purpose — remove indirection
+
+## Step 6: Summary
+
+Report results:
+
+```
+Dead Code Cleanup
+──────────────────────────────
+Deleted: 12 unused functions
+ 3 unused files
+ 5 unused dependencies
+Skipped: 2 items (tests failed)
+Saved: ~450 lines removed
+──────────────────────────────
+All tests passing PASS:
+```
+
+## Rules
+
+- **Never delete without running tests first**
+- **One deletion at a time** — Atomic changes make rollback easy
+- **Skip if uncertain** — Better to keep dead code than break production
+- **Don't refactor while cleaning** — Separate concerns (clean first, refactor later)
diff --git a/pi/core/commands/rust-test.md b/pi/core/commands/rust-test.md
new file mode 100644
index 000000000..ce671be5f
--- /dev/null
+++ b/pi/core/commands/rust-test.md
@@ -0,0 +1,308 @@
+---
+description: Enforce TDD workflow for Rust. Write tests first, then implement. Verify 80%+ coverage with cargo-llvm-cov.
+---
+
+# Rust TDD Command
+
+This command enforces test-driven development methodology for Rust code using `#[test]`, rstest, proptest, and mockall.
+
+## What This Command Does
+
+1. **Define Types/Traits**: Scaffold function signatures with `todo!()`
+2. **Write Tests**: Create comprehensive test module (RED)
+3. **Run Tests**: Verify tests fail for the right reason
+4. **Implement Code**: Write minimal code to pass (GREEN)
+5. **Refactor**: Improve while keeping tests green
+6. **Check Coverage**: Ensure 80%+ coverage with cargo-llvm-cov
+
+## When to Use
+
+Use `/rust-test` when:
+- Implementing new Rust functions, methods, or traits
+- Adding test coverage to existing Rust code
+- Fixing bugs (write failing test first)
+- Building critical business logic
+- Learning TDD workflow in Rust
+
+## TDD Cycle
+
+```
+RED -> Write failing test first
+GREEN -> Implement minimal code to pass
+REFACTOR -> Improve code, tests stay green
+REPEAT -> Next test case
+```
+
+## Example Session
+
+````text
+User: /rust-test I need a function to validate user registration
+
+Agent:
+# TDD Session: Registration Validator
+
+## Step 1: Define Interface
+
+```rust
+// src/validator.rs
+pub struct RegistrationRequest {
+ pub name: String,
+ pub email: String,
+ pub password: String,
+}
+
+pub enum ValidationResult {
+ Valid,
+ Invalid(Vec),
+}
+
+pub fn validate_registration(request: &RegistrationRequest) -> ValidationResult {
+ todo!()
+}
+```
+
+## Step 2: Write Tests (RED)
+
+```rust
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn valid_registration_returns_valid() {
+ let request = RegistrationRequest {
+ name: "Alice".into(),
+ email: "alice@example.com".into(),
+ password: "SecureP@ss1".into(),
+ };
+ assert!(matches!(validate_registration(&request), ValidationResult::Valid));
+ }
+
+ #[test]
+ fn blank_name_returns_invalid() {
+ let request = RegistrationRequest {
+ name: "".into(),
+ email: "alice@example.com".into(),
+ password: "SecureP@ss1".into(),
+ };
+ match validate_registration(&request) {
+ ValidationResult::Invalid(errors) => {
+ assert!(errors.contains(&"Name is required".to_string()));
+ }
+ ValidationResult::Valid => panic!("expected Invalid"),
+ }
+ }
+
+ #[test]
+ fn invalid_email_returns_invalid() {
+ let request = RegistrationRequest {
+ name: "Alice".into(),
+ email: "not-an-email".into(),
+ password: "SecureP@ss1".into(),
+ };
+ assert!(matches!(
+ validate_registration(&request),
+ ValidationResult::Invalid(_)
+ ));
+ }
+
+ #[test]
+ fn short_password_returns_invalid() {
+ let request = RegistrationRequest {
+ name: "Alice".into(),
+ email: "alice@example.com".into(),
+ password: "short".into(),
+ };
+ assert!(matches!(
+ validate_registration(&request),
+ ValidationResult::Invalid(_)
+ ));
+ }
+}
+```
+
+## Step 3: Run Tests — Verify FAIL
+
+```bash
+$ cargo test
+running 4 tests
+test tests::valid_registration_returns_valid ... FAILED (not yet implemented)
+test tests::blank_name_returns_invalid ... FAILED
+test tests::invalid_email_returns_invalid ... FAILED
+test tests::short_password_returns_invalid ... FAILED
+```
+
+Tests fail as expected (todo! panic).
+
+## Step 4: Implement Minimal Code (GREEN)
+
+```rust
+pub fn validate_registration(request: &RegistrationRequest) -> ValidationResult {
+ let mut errors = Vec::new();
+
+ if request.name.trim().is_empty() {
+ errors.push("Name is required".into());
+ }
+ if !request.email.contains('@') {
+ errors.push("Invalid email format".into());
+ }
+ if request.password.len() < 8 {
+ errors.push("Password must be at least 8 characters".into());
+ }
+
+ if errors.is_empty() {
+ ValidationResult::Valid
+ } else {
+ ValidationResult::Invalid(errors)
+ }
+}
+```
+
+## Step 5: Run Tests — Verify PASS
+
+```bash
+$ cargo test
+running 4 tests
+test tests::valid_registration_returns_valid ... ok
+test tests::blank_name_returns_invalid ... ok
+test tests::invalid_email_returns_invalid ... ok
+test tests::short_password_returns_invalid ... ok
+
+test result: ok. 4 passed; 0 failed
+```
+
+All tests passing!
+
+## Step 6: Check Coverage
+
+```bash
+$ cargo llvm-cov
+Coverage: 100.0% of lines
+```
+
+Coverage: 100%
+
+## TDD Complete!
+````
+
+## Test Patterns
+
+### Unit Tests
+
+```rust
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn adds_two_numbers() {
+ assert_eq!(add(2, 3), 5);
+ }
+
+ #[test]
+ fn handles_error() -> Result<(), Box> {
+ let result = parse_config(r#"port = 8080"#)?;
+ assert_eq!(result.port, 8080);
+ Ok(())
+ }
+}
+```
+
+### Parameterized Tests with rstest
+
+```rust
+use rstest::{rstest, fixture};
+
+#[rstest]
+#[case("hello", 5)]
+#[case("", 0)]
+#[case("rust", 4)]
+fn test_string_length(#[case] input: &str, #[case] expected: usize) {
+ assert_eq!(input.len(), expected);
+}
+```
+
+### Async Tests
+
+```rust
+#[tokio::test]
+async fn fetches_data_successfully() {
+ let client = TestClient::new().await;
+ let result = client.get("/data").await;
+ assert!(result.is_ok());
+}
+```
+
+### Property-Based Tests
+
+```rust
+use proptest::prelude::*;
+
+proptest! {
+ #[test]
+ fn encode_decode_roundtrip(input in ".*") {
+ let encoded = encode(&input);
+ let decoded = decode(&encoded).unwrap();
+ assert_eq!(input, decoded);
+ }
+}
+```
+
+## Coverage Commands
+
+```bash
+# Summary report
+cargo llvm-cov
+
+# HTML report
+cargo llvm-cov --html
+
+# Fail if below threshold
+cargo llvm-cov --fail-under-lines 80
+
+# Run specific test
+cargo test test_name
+
+# Run with output
+cargo test -- --nocapture
+
+# Run without stopping on first failure
+cargo test --no-fail-fast
+```
+
+## Coverage Targets
+
+| Code Type | Target |
+|-----------|--------|
+| Critical business logic | 100% |
+| Public API | 90%+ |
+| General code | 80%+ |
+| Generated / FFI bindings | Exclude |
+
+## TDD Best Practices
+
+**DO:**
+- Write test FIRST, before any implementation
+- Run tests after each change
+- Use `assert_eq!` over `assert!` for better error messages
+- Use `?` in tests that return `Result` for cleaner output
+- Test behavior, not implementation
+- Include edge cases (empty, boundary, error paths)
+
+**DON'T:**
+- Write implementation before tests
+- Skip the RED phase
+- Use `#[should_panic]` when `Result::is_err()` works
+- Use `sleep()` in tests — use channels or `tokio::time::pause()`
+- Mock everything — prefer integration tests when feasible
+
+## Related Commands
+
+- `/rust-build` - Fix build errors
+- `/rust-review` - Review code after implementation
+- `verification-loop` skill - Run full verification loop
+
+## Related
+
+- Skill: `skills/rust-testing/`
+- Skill: `skills/rust-patterns/`
diff --git a/pi/core/commands/test-coverage.md b/pi/core/commands/test-coverage.md
new file mode 100644
index 000000000..0af60e88b
--- /dev/null
+++ b/pi/core/commands/test-coverage.md
@@ -0,0 +1,73 @@
+---
+description: Analyze coverage, identify gaps, and generate missing tests toward the target threshold.
+---
+
+# Test Coverage
+
+Analyze test coverage, identify gaps, and generate missing tests to reach 80%+ coverage.
+
+## Step 1: Detect Test Framework
+
+| Indicator | Coverage Command |
+|-----------|-----------------|
+| `jest.config.*` or `package.json` jest | `npx jest --coverage --coverageReporters=json-summary` |
+| `vitest.config.*` | `npx vitest run --coverage` |
+| `pytest.ini` / `pyproject.toml` pytest | `pytest --cov=src --cov-report=json` |
+| `Cargo.toml` | `cargo llvm-cov --json` |
+| `pom.xml` with JaCoCo | `mvn test jacoco:report` |
+| `go.mod` | `go test -coverprofile=coverage.out ./...` |
+
+## Step 2: Analyze Coverage Report
+
+1. Run the coverage command
+2. Parse the output (JSON summary or terminal output)
+3. List files **below 80% coverage**, sorted worst-first
+4. For each under-covered file, identify:
+ - Untested functions or methods
+ - Missing branch coverage (if/else, switch, error paths)
+ - Dead code that inflates the denominator
+
+## Step 3: Generate Missing Tests
+
+For each under-covered file, generate tests following this priority:
+
+1. **Happy path** — Core functionality with valid inputs
+2. **Error handling** — Invalid inputs, missing data, network failures
+3. **Edge cases** — Empty arrays, null/undefined, boundary values (0, -1, MAX_INT)
+4. **Branch coverage** — Each if/else, switch case, ternary
+
+### Test Generation Rules
+
+- Place tests adjacent to source: `foo.ts` → `foo.test.ts` (or project convention)
+- Use existing test patterns from the project (import style, assertion library, mocking approach)
+- Mock external dependencies (database, APIs, file system)
+- Each test should be independent — no shared mutable state between tests
+- Name tests descriptively: `test_create_user_with_duplicate_email_returns_409`
+
+## Step 4: Verify
+
+1. Run the full test suite — all tests must pass
+2. Re-run coverage — verify improvement
+3. If still below 80%, repeat Step 3 for remaining gaps
+
+## Step 5: Report
+
+Show before/after comparison:
+
+```
+Coverage Report
+──────────────────────────────
+File Before After
+src/services/auth.ts 45% 88%
+src/utils/validation.ts 32% 82%
+──────────────────────────────
+Overall: 67% 84% PASS:
+```
+
+## Focus Areas
+
+- Functions with complex branching (high cyclomatic complexity)
+- Error handlers and catch blocks
+- Utility functions used across the codebase
+- API endpoint handlers (request → response flow)
+- Edge cases: null, undefined, empty string, empty array, zero, negative numbers
diff --git a/pi/core/commands/update-codemaps.md b/pi/core/commands/update-codemaps.md
new file mode 100644
index 000000000..a0670aed0
--- /dev/null
+++ b/pi/core/commands/update-codemaps.md
@@ -0,0 +1,76 @@
+---
+description: Scan project structure and generate token-lean architecture codemaps.
+---
+
+# Update Codemaps
+
+Analyze the codebase structure and generate token-lean architecture documentation.
+
+## Step 1: Scan Project Structure
+
+1. Identify the project type (monorepo, single app, library, microservice)
+2. Find all source directories (src/, lib/, app/, packages/)
+3. Map entry points (main.ts, index.ts, app.py, main.go, etc.)
+
+## Step 2: Generate Codemaps
+
+Create or update codemaps in `docs/CODEMAPS/` (or `.reports/codemaps/`):
+
+| File | Contents |
+|------|----------|
+| `architecture.md` | High-level system diagram, service boundaries, data flow |
+| `backend.md` | API routes, middleware chain, service → repository mapping |
+| `frontend.md` | Page tree, component hierarchy, state management flow |
+| `data.md` | Database tables, relationships, migration history |
+| `dependencies.md` | External services, third-party integrations, shared libraries |
+
+### Codemap Format
+
+Each codemap should be token-lean — optimized for AI context consumption:
+
+```markdown
+# Backend Architecture
+
+## Routes
+POST /api/users → UserController.create → UserService.create → UserRepo.insert
+GET /api/users/:id → UserController.get → UserService.findById → UserRepo.findById
+
+## Key Files
+src/services/user.ts (business logic, 120 lines)
+src/repos/user.ts (database access, 80 lines)
+
+## Dependencies
+- PostgreSQL (primary data store)
+- Redis (session cache, rate limiting)
+- Stripe (payment processing)
+```
+
+## Step 3: Diff Detection
+
+1. If previous codemaps exist, calculate the diff percentage
+2. If changes > 30%, show the diff and request user approval before overwriting
+3. If changes <= 30%, update in place
+
+## Step 4: Add Metadata
+
+Add a freshness header to each codemap:
+
+```markdown
+
+```
+
+## Step 5: Save Analysis Report
+
+Write a summary to `.reports/codemap-diff.txt`:
+- Files added/removed/modified since last scan
+- New dependencies detected
+- Architecture changes (new routes, new services, etc.)
+- Staleness warnings for docs not updated in 90+ days
+
+## Tips
+
+- Focus on **high-level structure**, not implementation details
+- Prefer **file paths and function signatures** over full code blocks
+- Keep each codemap under **1000 tokens** for efficient context loading
+- Use ASCII diagrams for data flow instead of verbose descriptions
+- Run after major feature additions or refactoring sessions
diff --git a/pi/core/commands/update-docs.md b/pi/core/commands/update-docs.md
new file mode 100644
index 000000000..a75f2332c
--- /dev/null
+++ b/pi/core/commands/update-docs.md
@@ -0,0 +1,88 @@
+---
+description: Sync documentation from source-of-truth files such as scripts, schemas, routes, and exports.
+---
+
+# Update Documentation
+
+Sync documentation with the codebase, generating from source-of-truth files.
+
+## Step 1: Identify Sources of Truth
+
+| Source | Generates |
+|--------|-----------|
+| `package.json` scripts | Available commands reference |
+| `.env.example` | Environment variable documentation |
+| `openapi.yaml` / route files | API endpoint reference |
+| Source code exports | Public API documentation |
+| `Dockerfile` / `docker-compose.yml` | Infrastructure setup docs |
+
+## Step 2: Generate Script Reference
+
+1. Read `package.json` (or `Makefile`, `Cargo.toml`, `pyproject.toml`)
+2. Extract all scripts/commands with their descriptions
+3. Generate a reference table:
+
+```markdown
+| Command | Description |
+|---------|-------------|
+| `npm run dev` | Start development server with hot reload |
+| `npm run build` | Production build with type checking |
+| `npm test` | Run test suite with coverage |
+```
+
+## Step 3: Generate Environment Documentation
+
+1. Read `.env.example` (or `.env.template`, `.env.sample`)
+2. Extract all variables with their purposes
+3. Categorize as required vs optional
+4. Document expected format and valid values
+
+```markdown
+| Variable | Required | Description | Example |
+|----------|----------|-------------|---------|
+| `DATABASE_URL` | Yes | PostgreSQL connection string | `postgres://user:pass@host:5432/db` |
+| `LOG_LEVEL` | No | Logging verbosity (default: info) | `debug`, `info`, `warn`, `error` |
+```
+
+## Step 4: Update Contributing Guide
+
+Generate or update `docs/CONTRIBUTING.md` with:
+- Development environment setup (prerequisites, install steps)
+- Available scripts and their purposes
+- Testing procedures (how to run, how to write new tests)
+- Code style enforcement (linter, formatter, pre-commit hooks)
+- PR submission checklist
+
+## Step 5: Update Runbook
+
+Generate or update `docs/RUNBOOK.md` with:
+- Deployment procedures (step-by-step)
+- Health check endpoints and monitoring
+- Common issues and their fixes
+- Rollback procedures
+- Alerting and escalation paths
+
+## Step 6: Staleness Check
+
+1. Find documentation files not modified in 90+ days
+2. Cross-reference with recent source code changes
+3. Flag potentially outdated docs for manual review
+
+## Step 7: Show Summary
+
+```
+Documentation Update
+──────────────────────────────
+Updated: docs/CONTRIBUTING.md (scripts table)
+Updated: docs/ENV.md (3 new variables)
+Flagged: docs/DEPLOY.md (142 days stale)
+Skipped: docs/API.md (no changes detected)
+──────────────────────────────
+```
+
+## Rules
+
+- **Single source of truth**: Always generate from code, never manually edit generated sections
+- **Preserve manual sections**: Only update generated sections; leave hand-written prose intact
+- **Mark generated content**: Use `` markers around generated sections
+- **Don't create docs unprompted**: Only create new doc files if the command explicitly requests it
diff --git a/pi/core/package.json b/pi/core/package.json
new file mode 100644
index 000000000..7662a619c
--- /dev/null
+++ b/pi/core/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "ecc-pi-core",
+ "version": "2.2.2",
+ "license": "MIT",
+ "keywords": [
+ "pi-package",
+ "skills"
+ ],
+ "pi": {
+ "skills": [
+ "./skills"
+ ],
+ "prompts": [
+ "./commands"
+ ]
+ }
+}
diff --git a/pi/core/skills/accessibility/SKILL.md b/pi/core/skills/accessibility/SKILL.md
new file mode 100644
index 000000000..0685394ee
--- /dev/null
+++ b/pi/core/skills/accessibility/SKILL.md
@@ -0,0 +1,146 @@
+---
+name: accessibility
+description: Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels, accessibility traits and hints, focus management, contrast, target size, and screen-reader support. Use when building or auditing UI for accessibility compliance, keyboard navigation, or screen-reader support.
+metadata:
+ origin: ECC
+---
+
+# Accessibility (WCAG 2.2)
+
+This skill ensures that digital interfaces are Perceivable, Operable, Understandable, and Robust (POUR) for all users, including those using screen readers, switch controls, or keyboard navigation. It focuses on the technical implementation of WCAG 2.2 success criteria.
+
+## When to Use
+
+- Defining UI component specifications for Web, iOS, or Android.
+- Auditing existing code for accessibility barriers or compliance gaps.
+- Implementing new WCAG 2.2 standards like Target Size (Minimum) and Focus Appearance.
+- Mapping high-level design requirements to technical attributes (ARIA roles, traits, hints).
+
+## Core Concepts
+
+- **POUR Principles**: The foundation of WCAG (Perceivable, Operable, Understandable, Robust).
+- **Semantic Mapping**: Using native elements over generic containers to provide built-in accessibility.
+- **Accessibility Tree**: The representation of the UI that assistive technologies actually "read."
+- **Focus Management**: Controlling the order and visibility of the keyboard/screen reader cursor.
+- **Labeling & Hints**: Providing context through `aria-label`, `accessibilityLabel`, and `contentDescription`.
+
+## How It Works
+
+### Step 1: Identify the Component Role
+
+Determine the functional purpose (e.g., Is this a button, a link, or a tab?). Use the most semantic native element available before resorting to custom roles.
+
+### Step 2: Define Perceivable Attributes
+
+- Ensure text contrast meets **4.5:1** (normal) or **3:1** (large/UI).
+- Add text alternatives for non-text content (images, icons).
+- Implement responsive reflow (up to 400% zoom without loss of function).
+
+### Step 3: Implement Operable Controls
+
+- Ensure a minimum **24x24 CSS pixel** target size (WCAG 2.2 SC 2.5.8).
+- Verify all interactive elements are reachable via keyboard and have a visible focus indicator (SC 2.4.11).
+- Provide single-pointer alternatives for dragging movements.
+
+### Step 4: Ensure Understandable Logic
+
+- Use consistent navigation patterns.
+- Provide descriptive error messages and suggestions for correction (SC 3.3.3).
+- Implement "Redundant Entry" (SC 3.3.7) to prevent asking for the same data twice.
+
+### Step 5: Verify Robust Compatibility
+
+- Use correct `Name, Role, Value` patterns.
+- Implement `aria-live` or live regions for dynamic status updates.
+
+## Accessibility Architecture Diagram
+
+```mermaid
+flowchart TD
+ UI["UI Component"] --> Platform{Platform?}
+ Platform -->|Web| ARIA["WAI-ARIA + HTML5"]
+ Platform -->|iOS| SwiftUI["Accessibility Traits + Labels"]
+ Platform -->|Android| Compose["Semantics + ContentDesc"]
+
+ ARIA --> AT["Assistive Technology (Screen Readers, Switches)"]
+ SwiftUI --> AT
+ Compose --> AT
+```
+
+## Cross-Platform Mapping
+
+| Feature | Web (HTML/ARIA) | iOS (SwiftUI) | Android (Compose) |
+| :----------------- | :----------------------- | :----------------------------------- | :---------------------------------------------------------- |
+| **Primary Label** | `aria-label` / `