Compare commits

..

12 Commits

Author SHA1 Message Date
Affaan Mustafa
08a5ae6784 fix: resolve stop hooks without plugin root env 2026-03-29 21:26:12 -04:00
Affaan Mustafa
dd675d4258 Merge pull request #1007 from AndriyKalashnykov/chore/pin-actions-and-update-claude-md
chore: pin actions to commit SHAs and add Skills section to CLAUDE.md
2026-03-29 21:16:09 -04:00
Affaan Mustafa
db12d3d838 Merge pull request #1004 from ohashi-mizuki/fix/pre-push-skip-branch-deletion
fix: skip pre-push checks on branch deletion
2026-03-29 21:16:01 -04:00
Andriy Kalashnykov
46f37ae4fb chore: pin actions to commit SHAs and add Skills section to CLAUDE.md
Pin all GitHub Actions to commit SHAs instead of mutable version tags
across ci.yml, release.yml, maintenance.yml, and all reusable workflows.
This prevents supply-chain attacks via tag hijacking.

Add the required Skills section to CLAUDE.md mapping project files
(README.md, .github/workflows/*.yml) to their respective review skills.
2026-03-29 17:16:56 -04:00
ohashi-mizuki
0c166e14da fix: skip pre-push checks on branch deletion
The pre-push hook runs lint/typecheck/test/build checks on every push,
including `git push origin --delete <branch>`. Branch deletion does not
push any code, so verification checks are unnecessary and block the
delete operation.

Detect deletion pushes by reading stdin (local sha is all zeros for
deletes) and exit early.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:09:26 +09:00
Affaan Mustafa
527c79350c Merge pull request #1000 from affaan-m/fix/codex-context7-compat-tests
fix(codex): broaden context7 config checks
2026-03-29 00:26:32 -04:00
Affaan Mustafa
bec1ebf76d Merge pull request #999 from affaan-m/fix/clv2-config-override-rebase
fix(clv2): honor CLV2_CONFIG in start-observer
2026-03-29 00:22:23 -04:00
Affaan Mustafa
be76918850 fix(clv2): honor CLV2_CONFIG in start-observer 2026-03-29 00:21:55 -04:00
Affaan Mustafa
99a154a908 Merge pull request #998 from affaan-m/fix/token-budget-advisor-trigger-clarity
fix(skills): clarify token-budget-advisor triggers
2026-03-29 00:20:29 -04:00
Affaan Mustafa
ebf0f135bb fix(skills): clarify token-budget-advisor triggers 2026-03-29 00:20:04 -04:00
Affaan Mustafa
2d27da52e2 Merge pull request #997 from affaan-m/fix/readme-agent-count-tree
docs(readme): fix agent count in repo tree
2026-03-29 00:20:01 -04:00
Affaan Mustafa
65c4a0f6ba docs(readme): fix agent count in repo tree 2026-03-29 00:19:52 -04:00
14 changed files with 164 additions and 71 deletions

View File

@@ -34,10 +34,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Node.js ${{ matrix.node }} - name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
@@ -68,7 +68,7 @@ jobs:
- name: Cache npm - name: Cache npm
if: matrix.pm == 'npm' if: matrix.pm == 'npm'
uses: actions/cache@v4 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with: with:
path: ${{ steps.npm-cache-dir.outputs.dir }} path: ${{ steps.npm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ matrix.node }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-node-${{ matrix.node }}-npm-${{ hashFiles('**/package-lock.json') }}
@@ -83,7 +83,7 @@ jobs:
- name: Cache pnpm - name: Cache pnpm
if: matrix.pm == 'pnpm' if: matrix.pm == 'pnpm'
uses: actions/cache@v4 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with: with:
path: ${{ steps.pnpm-cache-dir.outputs.dir }} path: ${{ steps.pnpm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ matrix.node }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-node-${{ matrix.node }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
@@ -104,7 +104,7 @@ jobs:
- name: Cache yarn - name: Cache yarn
if: matrix.pm == 'yarn' if: matrix.pm == 'yarn'
uses: actions/cache@v4 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with: with:
path: ${{ steps.yarn-cache-dir.outputs.dir }} path: ${{ steps.yarn-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ matrix.node }}-yarn-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-node-${{ matrix.node }}-yarn-${{ hashFiles('**/yarn.lock') }}
@@ -113,7 +113,7 @@ jobs:
- name: Cache bun - name: Cache bun
if: matrix.pm == 'bun' if: matrix.pm == 'bun'
uses: actions/cache@v4 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with: with:
path: ~/.bun/install/cache path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
@@ -146,7 +146,7 @@ jobs:
# Upload test artifacts on failure # Upload test artifacts on failure
- name: Upload test artifacts - name: Upload test artifacts
if: failure() if: failure()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: test-results-${{ matrix.os }}-node${{ matrix.node }}-${{ matrix.pm }} name: test-results-${{ matrix.os }}-node${{ matrix.node }}-${{ matrix.pm }}
path: | path: |
@@ -160,10 +160,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: '20.x' node-version: '20.x'
@@ -205,10 +205,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: '20.x' node-version: '20.x'
@@ -223,10 +223,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: '20.x' node-version: '20.x'

View File

@@ -15,8 +15,8 @@ jobs:
name: Check Dependencies name: Check Dependencies
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: '20.x' node-version: '20.x'
- name: Check for outdated packages - name: Check for outdated packages
@@ -26,8 +26,8 @@ jobs:
name: Security Audit name: Security Audit
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: '20.x' node-version: '20.x'
- name: Run security audit - name: Run security audit
@@ -43,7 +43,7 @@ jobs:
name: Stale Issues/PRs name: Stale Issues/PRs
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
with: with:
stale-issue-message: 'This issue is stale due to inactivity.' stale-issue-message: 'This issue is stale due to inactivity.'
stale-pr-message: 'This PR is stale due to inactivity.' stale-pr-message: 'This PR is stale due to inactivity.'

View File

@@ -14,7 +14,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@@ -27,10 +27,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ inputs.node-version }} node-version: ${{ inputs.node-version }}
@@ -59,7 +59,7 @@ jobs:
- name: Cache npm - name: Cache npm
if: inputs.package-manager == 'npm' if: inputs.package-manager == 'npm'
uses: actions/cache@v4 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with: with:
path: ${{ steps.npm-cache-dir.outputs.dir }} path: ${{ steps.npm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ inputs.node-version }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-node-${{ inputs.node-version }}-npm-${{ hashFiles('**/package-lock.json') }}
@@ -74,7 +74,7 @@ jobs:
- name: Cache pnpm - name: Cache pnpm
if: inputs.package-manager == 'pnpm' if: inputs.package-manager == 'pnpm'
uses: actions/cache@v4 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with: with:
path: ${{ steps.pnpm-cache-dir.outputs.dir }} path: ${{ steps.pnpm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ inputs.node-version }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} key: ${{ runner.os }}-node-${{ inputs.node-version }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
@@ -95,7 +95,7 @@ jobs:
- name: Cache yarn - name: Cache yarn
if: inputs.package-manager == 'yarn' if: inputs.package-manager == 'yarn'
uses: actions/cache@v4 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with: with:
path: ${{ steps.yarn-cache-dir.outputs.dir }} path: ${{ steps.yarn-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ inputs.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-node-${{ inputs.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }}
@@ -104,7 +104,7 @@ jobs:
- name: Cache bun - name: Cache bun
if: inputs.package-manager == 'bun' if: inputs.package-manager == 'bun'
uses: actions/cache@v4 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with: with:
path: ~/.bun/install/cache path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
@@ -134,7 +134,7 @@ jobs:
- name: Upload test artifacts - name: Upload test artifacts
if: failure() if: failure()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: test-results-${{ inputs.os }}-node${{ inputs.node-version }}-${{ inputs.package-manager }} name: test-results-${{ inputs.os }}-node${{ inputs.node-version }}-${{ inputs.package-manager }}
path: | path: |

View File

@@ -17,10 +17,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with: with:
node-version: ${{ inputs.node-version }} node-version: ${{ inputs.node-version }}

View File

@@ -59,3 +59,14 @@ Follow the formats in CONTRIBUTING.md:
- Hooks: JSON with matcher and hooks array - Hooks: JSON with matcher and hooks array
File naming: lowercase with hyphens (e.g., `python-reviewer.md`, `tdd-workflow.md`) File naming: lowercase with hyphens (e.g., `python-reviewer.md`, `tdd-workflow.md`)
## Skills
Use the following skills when working on related files:
| File(s) | Skill |
|---------|-------|
| `README.md` | `/readme` |
| `.github/workflows/*.yml` | `/ci-workflow` |
When spawning subagents, always pass conventions from the respective skill into the agent's prompt.

View File

@@ -295,7 +295,7 @@ everything-claude-code/
| |-- plugin.json # Plugin metadata and component paths | |-- plugin.json # Plugin metadata and component paths
| |-- marketplace.json # Marketplace catalog for /plugin marketplace add | |-- marketplace.json # Marketplace catalog for /plugin marketplace add
| |
|-- agents/ # 29 specialized subagents for delegation |-- agents/ # 30 specialized subagents for delegation
| |-- planner.md # Feature implementation planning | |-- planner.md # Feature implementation planning
| |-- architect.md # System design decisions | |-- architect.md # System design decisions
| |-- tdd-guide.md # Test-driven development | |-- tdd-guide.md # Test-driven development

View File

@@ -259,7 +259,7 @@
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"stop:check-console-log\" \"scripts/hooks/check-console-log.js\" \"standard,strict\"" "command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const hookId='stop:check-console-log';const scriptRel='scripts/hooks/check-console-log.js';const profiles='standard,strict';const rel=path.join('scripts','hooks','run-with-flags.js');const hasRunnerRoot=candidate=>{const value=typeof candidate==='string'?candidate.trim():'';return value.length>0&&fs.existsSync(path.join(path.resolve(value),rel));};const root=(()=>{const envRoot=process.env.CLAUDE_PLUGIN_ROOT||'';if(hasRunnerRoot(envRoot))return path.resolve(envRoot.trim());const home=require('os').homedir();const claudeDir=path.join(home,'.claude');if(hasRunnerRoot(claudeDir))return claudeDir;for(const candidate of [path.join(claudeDir,'plugins','everything-claude-code'),path.join(claudeDir,'plugins','everything-claude-code@everything-claude-code'),path.join(claudeDir,'plugins','marketplace','everything-claude-code')]){if(hasRunnerRoot(candidate))return candidate;}try{const cacheBase=path.join(claudeDir,'plugins','cache','everything-claude-code');for(const org of fs.readdirSync(cacheBase,{withFileTypes:true})){if(!org.isDirectory())continue;for(const version of fs.readdirSync(path.join(cacheBase,org.name),{withFileTypes:true})){if(!version.isDirectory())continue;const candidate=path.join(cacheBase,org.name,version.name);if(hasRunnerRoot(candidate))return candidate;}}}catch{}return claudeDir;})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,hookId,scriptRel,profiles],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});const stdout=typeof result.stdout==='string'?result.stdout:'';if(stdout)process.stdout.write(stdout);else process.stdout.write(raw);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[Hook] inline runner failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);}process.stderr.write('[Hook] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);\""
} }
], ],
"description": "Check for console.log in modified files after each response" "description": "Check for console.log in modified files after each response"
@@ -269,7 +269,7 @@
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"stop:session-end\" \"scripts/hooks/session-end.js\" \"minimal,standard,strict\"", "command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const hookId='stop:session-end';const scriptRel='scripts/hooks/session-end.js';const profiles='minimal,standard,strict';const rel=path.join('scripts','hooks','run-with-flags.js');const hasRunnerRoot=candidate=>{const value=typeof candidate==='string'?candidate.trim():'';return value.length>0&&fs.existsSync(path.join(path.resolve(value),rel));};const root=(()=>{const envRoot=process.env.CLAUDE_PLUGIN_ROOT||'';if(hasRunnerRoot(envRoot))return path.resolve(envRoot.trim());const home=require('os').homedir();const claudeDir=path.join(home,'.claude');if(hasRunnerRoot(claudeDir))return claudeDir;for(const candidate of [path.join(claudeDir,'plugins','everything-claude-code'),path.join(claudeDir,'plugins','everything-claude-code@everything-claude-code'),path.join(claudeDir,'plugins','marketplace','everything-claude-code')]){if(hasRunnerRoot(candidate))return candidate;}try{const cacheBase=path.join(claudeDir,'plugins','cache','everything-claude-code');for(const org of fs.readdirSync(cacheBase,{withFileTypes:true})){if(!org.isDirectory())continue;for(const version of fs.readdirSync(path.join(cacheBase,org.name),{withFileTypes:true})){if(!version.isDirectory())continue;const candidate=path.join(cacheBase,org.name,version.name);if(hasRunnerRoot(candidate))return candidate;}}}catch{}return claudeDir;})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,hookId,scriptRel,profiles],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});const stdout=typeof result.stdout==='string'?result.stdout:'';if(stdout)process.stdout.write(stdout);else process.stdout.write(raw);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[Hook] inline runner failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);}process.stderr.write('[Hook] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);\"",
"async": true, "async": true,
"timeout": 10 "timeout": 10
} }
@@ -281,7 +281,7 @@
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"stop:evaluate-session\" \"scripts/hooks/evaluate-session.js\" \"minimal,standard,strict\"", "command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const hookId='stop:evaluate-session';const scriptRel='scripts/hooks/evaluate-session.js';const profiles='minimal,standard,strict';const rel=path.join('scripts','hooks','run-with-flags.js');const hasRunnerRoot=candidate=>{const value=typeof candidate==='string'?candidate.trim():'';return value.length>0&&fs.existsSync(path.join(path.resolve(value),rel));};const root=(()=>{const envRoot=process.env.CLAUDE_PLUGIN_ROOT||'';if(hasRunnerRoot(envRoot))return path.resolve(envRoot.trim());const home=require('os').homedir();const claudeDir=path.join(home,'.claude');if(hasRunnerRoot(claudeDir))return claudeDir;for(const candidate of [path.join(claudeDir,'plugins','everything-claude-code'),path.join(claudeDir,'plugins','everything-claude-code@everything-claude-code'),path.join(claudeDir,'plugins','marketplace','everything-claude-code')]){if(hasRunnerRoot(candidate))return candidate;}try{const cacheBase=path.join(claudeDir,'plugins','cache','everything-claude-code');for(const org of fs.readdirSync(cacheBase,{withFileTypes:true})){if(!org.isDirectory())continue;for(const version of fs.readdirSync(path.join(cacheBase,org.name),{withFileTypes:true})){if(!version.isDirectory())continue;const candidate=path.join(cacheBase,org.name,version.name);if(hasRunnerRoot(candidate))return candidate;}}}catch{}return claudeDir;})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,hookId,scriptRel,profiles],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});const stdout=typeof result.stdout==='string'?result.stdout:'';if(stdout)process.stdout.write(stdout);else process.stdout.write(raw);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[Hook] inline runner failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);}process.stderr.write('[Hook] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);\"",
"async": true, "async": true,
"timeout": 10 "timeout": 10
} }
@@ -293,7 +293,7 @@
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"stop:cost-tracker\" \"scripts/hooks/cost-tracker.js\" \"minimal,standard,strict\"", "command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const hookId='stop:cost-tracker';const scriptRel='scripts/hooks/cost-tracker.js';const profiles='minimal,standard,strict';const rel=path.join('scripts','hooks','run-with-flags.js');const hasRunnerRoot=candidate=>{const value=typeof candidate==='string'?candidate.trim():'';return value.length>0&&fs.existsSync(path.join(path.resolve(value),rel));};const root=(()=>{const envRoot=process.env.CLAUDE_PLUGIN_ROOT||'';if(hasRunnerRoot(envRoot))return path.resolve(envRoot.trim());const home=require('os').homedir();const claudeDir=path.join(home,'.claude');if(hasRunnerRoot(claudeDir))return claudeDir;for(const candidate of [path.join(claudeDir,'plugins','everything-claude-code'),path.join(claudeDir,'plugins','everything-claude-code@everything-claude-code'),path.join(claudeDir,'plugins','marketplace','everything-claude-code')]){if(hasRunnerRoot(candidate))return candidate;}try{const cacheBase=path.join(claudeDir,'plugins','cache','everything-claude-code');for(const org of fs.readdirSync(cacheBase,{withFileTypes:true})){if(!org.isDirectory())continue;for(const version of fs.readdirSync(path.join(cacheBase,org.name),{withFileTypes:true})){if(!version.isDirectory())continue;const candidate=path.join(cacheBase,org.name,version.name);if(hasRunnerRoot(candidate))return candidate;}}}catch{}return claudeDir;})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,hookId,scriptRel,profiles],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});const stdout=typeof result.stdout==='string'?result.stdout:'';if(stdout)process.stdout.write(stdout);else process.stdout.write(raw);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[Hook] inline runner failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);}process.stderr.write('[Hook] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);\"",
"async": true, "async": true,
"timeout": 10 "timeout": 10
} }
@@ -305,7 +305,7 @@
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"stop:desktop-notify\" \"scripts/hooks/desktop-notify.js\" \"standard,strict\"", "command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const hookId='stop:desktop-notify';const scriptRel='scripts/hooks/desktop-notify.js';const profiles='standard,strict';const rel=path.join('scripts','hooks','run-with-flags.js');const hasRunnerRoot=candidate=>{const value=typeof candidate==='string'?candidate.trim():'';return value.length>0&&fs.existsSync(path.join(path.resolve(value),rel));};const root=(()=>{const envRoot=process.env.CLAUDE_PLUGIN_ROOT||'';if(hasRunnerRoot(envRoot))return path.resolve(envRoot.trim());const home=require('os').homedir();const claudeDir=path.join(home,'.claude');if(hasRunnerRoot(claudeDir))return claudeDir;for(const candidate of [path.join(claudeDir,'plugins','everything-claude-code'),path.join(claudeDir,'plugins','everything-claude-code@everything-claude-code'),path.join(claudeDir,'plugins','marketplace','everything-claude-code')]){if(hasRunnerRoot(candidate))return candidate;}try{const cacheBase=path.join(claudeDir,'plugins','cache','everything-claude-code');for(const org of fs.readdirSync(cacheBase,{withFileTypes:true})){if(!org.isDirectory())continue;for(const version of fs.readdirSync(path.join(cacheBase,org.name),{withFileTypes:true})){if(!version.isDirectory())continue;const candidate=path.join(cacheBase,org.name,version.name);if(hasRunnerRoot(candidate))return candidate;}}}catch{}return claudeDir;})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,hookId,scriptRel,profiles],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});const stdout=typeof result.stdout==='string'?result.stdout:'';if(stdout)process.stdout.write(stdout);else process.stdout.write(raw);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[Hook] inline runner failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);}process.stderr.write('[Hook] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);\"",
"async": true, "async": true,
"timeout": 10 "timeout": 10
} }
@@ -319,7 +319,7 @@
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"session:end:marker\" \"scripts/hooks/session-end-marker.js\" \"minimal,standard,strict\"", "command": "node -e \"const fs=require('fs');const path=require('path');const {spawnSync}=require('child_process');const raw=fs.readFileSync(0,'utf8');const hookId='session:end:marker';const scriptRel='scripts/hooks/session-end-marker.js';const profiles='minimal,standard,strict';const rel=path.join('scripts','hooks','run-with-flags.js');const hasRunnerRoot=candidate=>{const value=typeof candidate==='string'?candidate.trim():'';return value.length>0&&fs.existsSync(path.join(path.resolve(value),rel));};const root=(()=>{const envRoot=process.env.CLAUDE_PLUGIN_ROOT||'';if(hasRunnerRoot(envRoot))return path.resolve(envRoot.trim());const home=require('os').homedir();const claudeDir=path.join(home,'.claude');if(hasRunnerRoot(claudeDir))return claudeDir;for(const candidate of [path.join(claudeDir,'plugins','everything-claude-code'),path.join(claudeDir,'plugins','everything-claude-code@everything-claude-code'),path.join(claudeDir,'plugins','marketplace','everything-claude-code')]){if(hasRunnerRoot(candidate))return candidate;}try{const cacheBase=path.join(claudeDir,'plugins','cache','everything-claude-code');for(const org of fs.readdirSync(cacheBase,{withFileTypes:true})){if(!org.isDirectory())continue;for(const version of fs.readdirSync(path.join(cacheBase,org.name),{withFileTypes:true})){if(!version.isDirectory())continue;const candidate=path.join(cacheBase,org.name,version.name);if(hasRunnerRoot(candidate))return candidate;}}}catch{}return claudeDir;})();const script=path.join(root,rel);if(fs.existsSync(script)){const result=spawnSync(process.execPath,[script,hookId,scriptRel,profiles],{input:raw,encoding:'utf8',env:process.env,cwd:process.cwd(),timeout:30000});const stdout=typeof result.stdout==='string'?result.stdout:'';if(stdout)process.stdout.write(stdout);else process.stdout.write(raw);if(result.stderr)process.stderr.write(result.stderr);if(result.error||result.status===null||result.signal){const reason=result.error?result.error.message:(result.signal?'signal '+result.signal:'missing exit status');process.stderr.write('[Hook] inline runner failed: '+reason+String.fromCharCode(10));process.exit(1);}process.exit(Number.isInteger(result.status)?result.status:0);}process.stderr.write('[Hook] WARNING: could not resolve ECC plugin root; skipping hook'+String.fromCharCode(10));process.stdout.write(raw);\"",
"async": true, "async": true,
"timeout": 10 "timeout": 10
} }

View File

@@ -16,6 +16,20 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
exit 0 exit 0
fi fi
# Skip checks for branch deletion pushes (e.g., git push origin --delete <branch>).
# The pre-push hook receives lines on stdin: <local ref> <local sha> <remote ref> <remote sha>.
# For deletions, the local sha is the zero OID.
is_delete_only=true
while read -r _local_ref local_sha _remote_ref _remote_sha; do
if [[ "$local_sha" != "0000000000000000000000000000000000000000" ]]; then
is_delete_only=false
break
fi
done
if [[ "$is_delete_only" == "true" ]]; then
exit 0
fi
ran_any_check=0 ran_any_check=0
log() { log() {

View File

@@ -36,7 +36,11 @@ PYTHON_CMD="${CLV2_PYTHON_CMD:-}"
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
CONFIG_DIR="${HOME}/.claude/homunculus" CONFIG_DIR="${HOME}/.claude/homunculus"
if [ -n "${CLV2_CONFIG:-}" ]; then
CONFIG_FILE="$CLV2_CONFIG"
else
CONFIG_FILE="${SKILL_ROOT}/config.json" CONFIG_FILE="${SKILL_ROOT}/config.json"
fi
# PID file is project-scoped so each project can have its own observer # PID file is project-scoped so each project can have its own observer
PID_FILE="${PROJECT_DIR}/.observer.pid" PID_FILE="${PROJECT_DIR}/.observer.pid"
LOG_FILE="${PROJECT_DIR}/observer.log" LOG_FILE="${PROJECT_DIR}/observer.log"

View File

@@ -1,20 +1,18 @@
--- ---
name: token-budget-advisor name: token-budget-advisor
description: >- description: >-
Intercepts the response flow to offer the user an informed choice about Offers the user an informed choice about how much response depth to
how much depth/tokens to consume — BEFORE responding. Use this skill consume before answering. Use this skill when the user explicitly
when the user wants to control token consumption, adjust response depth, wants to control response length, depth, or token budget.
choose between short/long answers, or optimize their prompt. TRIGGER when: "token budget", "token count", "token usage", "token limit",
TRIGGER when: "token budget", "response token budget", "token count", "response length", "answer depth", "short version", "brief answer",
"token usage", "response length", "response depth", "brief answer", "detailed answer", "exhaustive answer", "respuesta corta vs larga",
"short answer", "detailed answer", "full answer", "cuántos tokens", "ahorrar tokens", "responde al 50%", "dame la versión
"respuesta corta vs larga", "cuántos tokens", "ahorrar tokens", corta", "quiero controlar cuánto usas", or clear variants where the
"responde al 50%", "dame la versión corta", "quiero controlar cuánto usas", user is explicitly asking to control answer size or depth.
"75%", "100%", "at 25%", "at 50%", "at 75%", "at 100%",
"give me the full answer", or any variant where the user wants
to control length, depth, or token usage — even without mentioning tokens.
DO NOT TRIGGER when: user has already specified a level in the current DO NOT TRIGGER when: user has already specified a level in the current
session (maintain it) or the request is clearly a one-word answer. session (maintain it), the request is clearly a one-word answer, or
"token" refers to auth/session/payment tokens rather than response size.
origin: community origin: community
--- ---
@@ -35,12 +33,14 @@ Intercept the response flow to offer the user a choice about response depth **be
### Step 1 — Estimate input tokens ### Step 1 — Estimate input tokens
Use the repository's canonical estimation guidance from `skills/context-budget`. Use the repository's canonical context-budget heuristics to estimate the prompt's token count mentally.
- Prose-first prompts: `input_tokens ≈ word_count × 1.3` Use the same calibration guidance as [context-budget](../context-budget/SKILL.md):
- Code-heavy or mixed prompts: `input_tokens ≈ char_count / 4`
For mixed content, prefer the code-heavy estimate as the conservative default. - prose: `words × 1.3`
- code-heavy or mixed/code blocks: `chars / 4`
For mixed content, use the dominant content type and keep the estimate heuristic.
### Step 2 — Estimate response size by complexity ### Step 2 — Estimate response size by complexity
@@ -72,7 +72,7 @@ Choose your depth level:
[3] Detailed (75%) -> ~[tokens] Full answer with alternatives [3] Detailed (75%) -> ~[tokens] Full answer with alternatives
[4] Exhaustive (100%) -> ~[tokens] Everything, no limits [4] Exhaustive (100%) -> ~[tokens] Everything, no limits
Which level? (1-4 or say "25%", "50%", "75%", "100%") Which level? (1-4 or say "25% depth", "50% depth", "75% depth", "100% depth")
Precision: heuristic estimate ~85-90% accuracy (±15%). Precision: heuristic estimate ~85-90% accuracy (±15%).
``` ```
@@ -98,10 +98,10 @@ If the user already signals a level, respond at that level immediately without a
| What they say | Level | | What they say | Level |
|----------------------------------------------------|-------| |----------------------------------------------------|-------|
| "1" / "25%" / "short answer" / "brief" / "tldr" / "one-liner" | 25% | | "1" / "25% depth" / "short version" / "brief answer" / "tldr" | 25% |
| "2" / "50%" / "moderate detail" / "balanced answer" | 50% | | "2" / "50% depth" / "moderate depth" / "balanced answer" | 50% |
| "3" / "75%" / "detailed answer" / "thorough explanation" | 75% | | "3" / "75% depth" / "detailed answer" / "thorough answer" | 75% |
| "4" / "100%" / "exhaustive" / "everything" / "full answer" | 100% | | "4" / "100% depth" / "exhaustive answer" / "full deep dive" | 100% |
If the user set a level earlier in the session, **maintain it silently** for subsequent responses unless they change it. If the user set a level earlier in the session, **maintain it silently** for subsequent responses unless they change it.
@@ -113,19 +113,21 @@ This skill uses heuristic estimation — no real tokenizer. Accuracy ~85-90%, va
### Triggers ### Triggers
- "Give me the brief answer first." - "Give me the short version first."
- "How many tokens will your response use?" - "How many tokens will your answer use?"
- "Respond at 50% depth." - "Respond at 50% depth."
- "I want the full answer." - "I want the exhaustive answer, not the summary."
- "Dame la version corta." - "Dame la version corta y luego la detallada."
### Does Not Trigger ### Does Not Trigger
- "Explain OAuth token refresh flow." (`token` here is domain language, not a budget request) - "What is a JWT token?"
- "Why is this JWT token invalid?" (security/domain usage, not response sizing) - "The checkout flow uses a payment token."
- "What is 2 + 2?" (trivially short answer) - "Is this normal?"
- "Complete the refactor."
- Follow-up questions after the user already chose a depth for the session
## Source ## Source
Standalone skill from [TBA — Token Budget Advisor for Claude Code](https://github.com/Xabilimon1/Token-Budget-Advisor-Claude-Code-). Standalone skill from [TBA — Token Budget Advisor for Claude Code](https://github.com/Xabilimon1/Token-Budget-Advisor-Claude-Code-).
The upstream project includes an optional estimator script, but this ECC skill intentionally stays zero-dependency and heuristic-only. Original project also ships a Python estimator script, but this repository keeps the skill self-contained and heuristic-only.

View File

@@ -1965,7 +1965,7 @@ async function runTests() {
passed++; passed++;
else failed++; else failed++;
if ( if (
test('script references use CLAUDE_PLUGIN_ROOT variable or safe SessionStart inline resolver', () => { test('script references use CLAUDE_PLUGIN_ROOT variable or a safe inline resolver', () => {
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json'); const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8')); const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
@@ -1973,9 +1973,8 @@ async function runTests() {
for (const entry of hookArray) { for (const entry of hookArray) {
for (const hook of entry.hooks) { for (const hook of entry.hooks) {
if (hook.type === 'command' && hook.command.includes('scripts/hooks/')) { if (hook.type === 'command' && hook.command.includes('scripts/hooks/')) {
// Check for the literal string "${CLAUDE_PLUGIN_ROOT}" in the command const isInlineResolver = hook.command.startsWith('node -e') && hook.command.includes('run-with-flags.js');
const isSessionStartInlineResolver = hook.command.startsWith('node -e') && hook.command.includes('session:start') && hook.command.includes('run-with-flags.js'); const hasPluginRoot = hook.command.includes('${CLAUDE_PLUGIN_ROOT}') || isInlineResolver;
const hasPluginRoot = hook.command.includes('${CLAUDE_PLUGIN_ROOT}') || isSessionStartInlineResolver;
assert.ok(hasPluginRoot, `Script paths should use CLAUDE_PLUGIN_ROOT: ${hook.command.substring(0, 80)}...`); assert.ok(hasPluginRoot, `Script paths should use CLAUDE_PLUGIN_ROOT: ${hook.command.substring(0, 80)}...`);
} }
} }
@@ -1990,6 +1989,24 @@ async function runTests() {
passed++; passed++;
else failed++; else failed++;
if (
test('Stop and SessionEnd lifecycle hooks use inline resolvers when plugin root may be unset', () => {
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
const lifecycleHooks = [...(hooks.hooks.Stop || []), ...(hooks.hooks.SessionEnd || [])];
for (const entry of lifecycleHooks) {
for (const hook of entry.hooks || []) {
assert.ok(hook.command.startsWith('node -e "'), `Expected inline resolver for lifecycle hook: ${hook.command.substring(0, 80)}...`);
assert.ok(hook.command.includes("const hookId='"), 'Inline lifecycle resolver should embed hook identifiers');
assert.ok(hook.command.includes('plugins'), 'Inline lifecycle resolver should probe known plugin roots');
}
}
})
)
passed++;
else failed++;
if ( if (
test('InsAIts hook is opt-in and scoped to high-signal tool inputs', () => { test('InsAIts hook is opt-in and scoped to high-signal tool inputs', () => {
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json'); const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');

View File

@@ -179,6 +179,22 @@ function cleanupTestDir(testDir) {
fs.rmSync(testDir, { recursive: true, force: true }); fs.rmSync(testDir, { recursive: true, force: true });
} }
function linkPluginCacheInstall(homeDir) {
const installRoot = path.join(
homeDir,
'.claude',
'plugins',
'cache',
'everything-claude-code',
'affaan-m',
'1.9.0'
);
fs.mkdirSync(path.dirname(installRoot), { recursive: true });
fs.symlinkSync(REPO_ROOT, installRoot, process.platform === 'win32' ? 'junction' : 'dir');
return installRoot;
}
function getHookCommandByDescription(hooks, lifecycle, descriptionText) { function getHookCommandByDescription(hooks, lifecycle, descriptionText) {
const hookGroup = hooks.hooks[lifecycle]?.find( const hookGroup = hooks.hooks[lifecycle]?.find(
entry => entry.description && entry.description.includes(descriptionText) entry => entry.description && entry.description.includes(descriptionText)
@@ -267,6 +283,35 @@ async function runTests() {
assert.strictEqual(payload.hookSpecificOutput.hookEventName, 'SessionStart'); assert.strictEqual(payload.hookSpecificOutput.hookEventName, 'SessionStart');
})) passed++; else failed++; })) passed++; else failed++;
if (await asyncTest('Stop hooks resolve plugin root without CLAUDE_PLUGIN_ROOT', async () => {
const hookCommand = getHookCommandByDescription(
hooks,
'Stop',
'Check for console.log in modified files'
);
const testHome = createTestDir();
try {
linkPluginCacheInstall(testHome);
const result = await runHookCommand(
hookCommand,
{ tool_input: { file_path: 'src/example.js' } },
{
CLAUDE_PLUGIN_ROOT: '',
HOME: testHome,
USERPROFILE: testHome
}
);
assert.strictEqual(result.code, 0, 'Stop hook should exit 0 when plugin root is resolved from plugin cache');
assert.ok(!result.stderr.includes('/scripts/hooks/run-with-flags.js'), 'Should not fall back to an invalid absolute root path');
assert.ok(!result.stderr.includes('MODULE_NOT_FOUND'), 'Should not fail to locate run-with-flags.js');
} finally {
cleanupTestDir(testHome);
}
})) passed++; else failed++;
if (await asyncTest('PreCompact hook logs to stderr', async () => { if (await asyncTest('PreCompact hook logs to stderr', async () => {
const result = await runHookWithInput(path.join(scriptsDir, 'pre-compact.js'), {}); const result = await runHookWithInput(path.join(scriptsDir, 'pre-compact.js'), {});
assert.ok(result.stderr.includes('[PreCompact]'), 'Should output to stderr with prefix'); assert.ok(result.stderr.includes('[PreCompact]'), 'Should output to stderr with prefix');