Compare commits

..

1 Commits

Author SHA1 Message Date
Affaan Mustafa
e50e64ac05 fix(skills): tighten token-budget-advisor triggers 2026-03-29 00:19:10 -04:00
16 changed files with 90 additions and 241 deletions

View File

@@ -86,8 +86,7 @@ startup_timeout_sec = 30
# args = ["-y", "@cloudflare/mcp-server-cloudflare"]
[features]
# Codex multi-agent collaboration is stable and on by default in current builds.
# Keep the explicit toggle here so the repo documents its expectation clearly.
# Codex multi-agent support is experimental as of March 2026.
multi_agent = true
# Profiles — switch with `codex -p <name>`
@@ -102,9 +101,6 @@ sandbox_mode = "workspace-write"
web_search = "live"
[agents]
[agents]
# Multi-agent role limits and local role definitions.
# These map to `.codex/agents/*.toml` and mirror the repo's explorer/reviewer/docs workflow.
max_threads = 6
max_depth = 1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@
"args": ["-y", "@upstash/context7-mcp@2.1.4"]
},
"exa": {
"type": "http",
"url": "https://mcp.exa.ai/mcp"
},
"memory": {

View File

@@ -59,14 +59,3 @@ Follow the formats in CONTRIBUTING.md:
- Hooks: JSON with matcher and hooks array
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

@@ -16,20 +16,6 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
exit 0
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
log() {

View File

@@ -112,25 +112,10 @@ if [[ -f "$CONFIG_FILE" ]]; then
fi
done
has_context7_legacy=0
has_context7_current=0
if rg -n '^\[mcp_servers\.context7\]' "$CONFIG_FILE" >/dev/null 2>&1; then
has_context7_legacy=1
fi
if rg -n '^\[mcp_servers\.context7-mcp\]' "$CONFIG_FILE" >/dev/null 2>&1; then
has_context7_current=1
fi
if [[ "$has_context7_legacy" -eq 1 || "$has_context7_current" -eq 1 ]]; then
ok "MCP section [mcp_servers.context7] or [mcp_servers.context7-mcp] exists"
warn "Legacy [mcp_servers.context7-mcp] exists (context7 is preferred)"
else
fail "MCP section [mcp_servers.context7] or [mcp_servers.context7-mcp] missing"
fi
if [[ "$has_context7_legacy" -eq 1 && "$has_context7_current" -eq 1 ]]; then
warn "Both [mcp_servers.context7] and [mcp_servers.context7-mcp] exist; prefer one name"
ok "No legacy [mcp_servers.context7-mcp] section"
fi
fi

View File

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

View File

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

View File

@@ -25,22 +25,6 @@ const configPath = path.join(repoRoot, '.codex', 'config.toml');
const config = fs.readFileSync(configPath, 'utf8');
const codexAgentsDir = path.join(repoRoot, '.codex', 'agents');
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function getTomlSection(text, sectionName) {
const escapedSection = escapeRegExp(sectionName);
const headerPattern = new RegExp(`^\\s*\\[${escapedSection}\\]\\s*$`, 'm');
const headerMatch = headerPattern.exec(text);
assert.ok(headerMatch, `Expected TOML section to exist: [${sectionName}]`);
const afterHeader = text.slice(headerMatch.index + headerMatch[0].length);
const nextHeaderIndex = afterHeader.search(/^\s*\[/m);
return nextHeaderIndex === -1 ? afterHeader : afterHeader.slice(0, nextHeaderIndex);
}
let passed = 0;
let failed = 0;
@@ -63,37 +47,6 @@ if (
passed++;
else failed++;
if (
test('reference config enables Codex multi-agent support', () => {
assert.ok(
/^\s*multi_agent\s*=\s*true\s*$/m.test(config),
'Expected `.codex/config.toml` to opt into Codex multi-agent collaboration',
);
})
)
passed++;
else failed++;
if (
test('reference config wires the sample Codex role files', () => {
for (const roleFile of ['explorer.toml', 'reviewer.toml', 'docs-researcher.toml']) {
const rolePath = path.join(codexAgentsDir, roleFile);
const roleSection = roleFile.replace(/\.toml$/, '').replace(/-/g, '_');
const sectionBody = getTomlSection(config, `agents.${roleSection}`);
assert.ok(fs.existsSync(rolePath), `Expected role config to exist: ${roleFile}`);
assert.ok(
new RegExp(`^\\s*config_file\\s*=\\s*"agents\\/${escapeRegExp(roleFile)}"\\s*$`, 'm').test(
sectionBody,
),
`Expected \`.codex/config.toml\` to reference ${roleFile} inside [agents.${roleSection}]`,
);
}
})
)
passed++;
else failed++;
if (
test('sample Codex role configs do not use o4-mini', () => {
const roleFiles = fs.readdirSync(codexAgentsDir).filter(file => file.endsWith('.toml'));

View File

@@ -216,20 +216,6 @@ test('.mcp.json includes at least github, context7, and exa servers', () => {
assert.ok(servers.includes('exa'), 'Expected exa MCP server');
});
test('.mcp.json HTTP transport servers declare type=http when they use url', () => {
for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
if (!server || typeof server !== 'object' || !('url' in server)) {
continue;
}
assert.strictEqual(
server.type,
'http',
`Expected ${name} MCP server to declare type=http when using url transport`,
);
}
});
// ── Codex marketplace file ────────────────────────────────────────────────────
// Per official docs: repo marketplace lives at $REPO_ROOT/.agents/plugins/marketplace.json
console.log('\n=== .agents/plugins/marketplace.json ===\n');

View File

@@ -10,8 +10,7 @@ const { spawnSync } = require('child_process');
const repoRoot = path.join(__dirname, '..', '..');
const installScript = path.join(repoRoot, 'scripts', 'codex', 'install-global-git-hooks.sh');
const syncScript = path.join(repoRoot, 'scripts', 'sync-ecc-to-codex.sh');
const checkScript = path.join(repoRoot, 'scripts', 'codex', 'check-codex-global-state.sh');
const installSource = fs.readFileSync(installScript, 'utf8');
function test(name, fn) {
try {
@@ -33,15 +32,25 @@ function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function toBashPath(filePath) {
if (process.platform !== 'win32') {
return filePath;
}
return String(filePath)
.replace(/^([A-Za-z]):/, (_, driveLetter) => `/${driveLetter.toLowerCase()}`)
.replace(/\\/g, '/');
}
function runBash(scriptPath, args = [], env = {}, cwd = repoRoot) {
return spawnSync('bash', [scriptPath, ...args], {
return spawnSync('bash', [toBashPath(scriptPath), ...args], {
cwd,
env: {
...process.env,
...env,
...env
},
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
stdio: ['pipe', 'pipe', 'pipe']
});
}
@@ -49,14 +58,24 @@ let passed = 0;
let failed = 0;
if (
test('install-global-git-hooks.sh handles quoted hook paths without shell injection', () => {
test('install-global-git-hooks.sh does not use eval and executes argv directly', () => {
assert.ok(!installSource.includes('eval "$*"'), 'Expected installer to avoid eval');
assert.ok(installSource.includes(' "$@"'), 'Expected installer to execute argv directly');
assert.ok(installSource.includes(`printf ' %q' "$@"`), 'Expected dry-run logging to shell-escape argv');
})
)
passed++;
else failed++;
if (
test('install-global-git-hooks.sh handles shell-sensitive hook paths without shell injection', () => {
const homeDir = createTempDir('codex-hooks-home-');
const weirdHooksDir = path.join(homeDir, 'git-hooks "quoted"');
const weirdHooksDir = path.join(homeDir, "git-hooks 'quoted' & spaced");
try {
const result = runBash(installScript, [], {
HOME: homeDir,
ECC_GLOBAL_HOOKS_DIR: weirdHooksDir,
HOME: toBashPath(homeDir),
ECC_GLOBAL_HOOKS_DIR: toBashPath(weirdHooksDir)
});
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
@@ -70,66 +89,6 @@ if (
passed++;
else failed++;
if (
test('sync and global sanity checks accept the legacy context7 MCP section', () => {
const homeDir = createTempDir('codex-sync-home-');
const codexDir = path.join(homeDir, '.codex');
const configPath = path.join(codexDir, 'config.toml');
const config = [
'approval_policy = "on-request"',
'sandbox_mode = "workspace-write"',
'web_search = "live"',
'persistent_instructions = ""',
'',
'[features]',
'multi_agent = true',
'',
'[profiles.strict]',
'approval_policy = "on-request"',
'sandbox_mode = "read-only"',
'web_search = "cached"',
'',
'[profiles.yolo]',
'approval_policy = "never"',
'sandbox_mode = "workspace-write"',
'web_search = "live"',
'',
'[mcp_servers.context7]',
'command = "npx"',
'args = ["-y", "@upstash/context7-mcp"]',
'',
'[mcp_servers.github]',
'command = "npx"',
'args = ["-y", "@modelcontextprotocol/server-github"]',
'',
'[mcp_servers.memory]',
'command = "npx"',
'args = ["-y", "@modelcontextprotocol/server-memory"]',
'',
'[mcp_servers.sequential-thinking]',
'command = "npx"',
'args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]',
'',
].join('\n');
try {
fs.mkdirSync(codexDir, { recursive: true });
fs.writeFileSync(configPath, config);
const syncResult = runBash(syncScript, [], { HOME: homeDir, CODEX_HOME: codexDir });
assert.strictEqual(syncResult.status, 0, syncResult.stderr || syncResult.stdout);
const checkResult = runBash(checkScript, [], { HOME: homeDir, CODEX_HOME: codexDir });
assert.strictEqual(checkResult.status, 0, checkResult.stderr || checkResult.stdout);
assert.match(checkResult.stdout, /MCP section \[mcp_servers\.context7\] or \[mcp_servers\.context7-mcp\] exists/);
} finally {
cleanup(homeDir);
}
})
)
passed++;
else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);