diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..3c61d6942f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,34 @@ +## Summary + + + +- +- + +Closes # + +## Test plan + + + +- [ ] + +## Evidence + +**Claim class**: + + + +**Terminal test output**: + + + +**Verdict**: diff --git a/.github/workflows/duplicate-check.yml b/.github/workflows/duplicate-check.yml new file mode 100644 index 0000000000..cff5f6b3ec --- /dev/null +++ b/.github/workflows/duplicate-check.yml @@ -0,0 +1,55 @@ +name: Duplicate Code Check + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + duplicate-check: + name: Duplicate Code Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + + - name: Run jscpd and capture output + id: jscpd + # Always run (don't stop on non-zero exit) so we can write the summary first. + # The exit code is preserved in steps.jscpd.outputs.exit_code and re-applied after. + run: | + set +e + OUTPUT=$(pnpm check:duplicates 2>&1) + EXIT_CODE=$? + set -e + + echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" + + # Write job summary so clone details surface on the PR checks page + { + if [ $EXIT_CODE -ne 0 ]; then + echo "## ❌ Duplicate Code Check — threshold exceeded" + else + echo "## ✅ Duplicate Code Check — within threshold" + fi + echo "" + echo '```' + echo "$OUTPUT" + echo '```' + echo "" + echo "_Threshold: 3% of lines. Clones shown above even when passing._" + echo "_To suppress a false positive, add the files to \`.jscpd.json\` ignore list._" + } >> "$GITHUB_STEP_SUMMARY" + + # Re-apply the original exit code so the step fails when threshold is crossed + exit $EXIT_CODE diff --git a/.github/workflows/evidence-gate.yml b/.github/workflows/evidence-gate.yml new file mode 100644 index 0000000000..34fe311e9b --- /dev/null +++ b/.github/workflows/evidence-gate.yml @@ -0,0 +1,210 @@ +name: Evidence Gate + +on: + pull_request: + types: [opened, synchronize, edited, reopened] + +# Do not cancel in-progress runs: a second event (e.g. push + bot PR-body edit) +# would cancel the first job; GitHub surfaces that as a failed check on the same SHA. +concurrency: + group: evidence-gate-${{ github.event.pull_request.number }} + cancel-in-progress: false + +permissions: + pull-requests: read + +jobs: + evidence-gate: + name: Evidence Gate + runs-on: ubuntu-latest + # Skip entirely when PR is merged or closed — a merged PR stops receiving + # pull_request events so a stale failed check run cannot be overwritten. + # Evidence gate is a pre-merge gate; post-merge it has no function. + if: github.event.pull_request.merged == false && github.event.action != 'closed' + steps: + - uses: actions/checkout@v4.1.1 + + - name: Write PR body to temp file + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: | + printf '%s' "$PR_BODY" > "$RUNNER_TEMP/pr_body.txt" + echo "Body fetched: ${#PR_BODY} chars" + if [ ${#PR_BODY} -eq 0 ]; then + echo "PR body is empty — treating as no evidence bundle" + echo "skip=true" >> "$GITHUB_OUTPUT" + fi + id: write_body + + - name: Check for evidence bundle in PR body + id: check + run: | + if [ "${{ steps.write_body.outputs.skip }}" = "true" ]; then + echo "found=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + BODY=$(cat "$RUNNER_TEMP/pr_body.txt") + STRIPPED_BODY=$(printf '%s' "$BODY" | python3 -c 'import re, sys; sys.stdout.write(re.sub(r"", "", sys.stdin.read(), flags=re.S))') + + if printf '%s' "$STRIPPED_BODY" | grep -qi '^[[:space:]]*## evidence'; then + echo "found=true" >> "$GITHUB_OUTPUT" + else + echo "found=false" >> "$GITHUB_OUTPUT" + fi + + - name: Warn when no Evidence section found + if: steps.check.outputs.found == 'false' + run: | + echo "WARNING: No ## Evidence section found in PR body." + echo "" + echo "Consider adding an Evidence section to your PR body:" + echo "" + echo " ## Evidence" + echo " **Claim class**: unit | integration | feat | fix | refactor | docs | chore" + echo " **Verdict**: PASS | INSUFFICIENT" + echo "" + echo " Describe what was tested and how." + exit 1 + + - name: Extract and normalize claim class + id: claim + if: steps.check.outputs.found == 'true' + run: | + BODY=$(cat "$RUNNER_TEMP/pr_body.txt") + STRIPPED_BODY=$(printf '%s' "$BODY" | python3 -c 'import re, sys; sys.stdout.write(re.sub(r"", "", sys.stdin.read(), flags=re.S))') + + # Extract claim class — normalize to lowercase short form. + # Supports: **Claim class**: value (colon inside bold, space after colon) + # and list-bullet format "- **Claim class**: ...". + CLAIM=$(printf '%s' "$STRIPPED_BODY" \ + | grep -i '\*\*Claim class' | grep -v '^- ' | head -1 \ + | tr -d '*' \ + | sed 's/.*Claim class: *//I' \ + | sed 's/(.*//' \ + | tr '[:upper:]' '[:lower:]' \ + | tr ' ' '-' | tr -d '\t' \ + | sed 's/^[ \t-]*//;s/[ \t-]*$//') + + # Fallback: list-bullet format + if [ -z "$CLAIM" ]; then + CLAIM=$(printf '%s' "$STRIPPED_BODY" \ + | grep -i '^-.*\*\*Claim class' | head -1 \ + | tr -d '*' \ + | sed 's/.*Claim class: *//I' \ + | sed 's/(.*//' \ + | tr '[:upper:]' '[:lower:]' \ + | tr ' ' '-' | tr -d '\t' \ + | sed 's/^[ \t-]*//;s/[ \t-]*$//') + fi + + # Normalize long forms to canonical short forms + case "$CLAIM" in + unit-test-coverage|unit-test) CLAIM="unit" ;; + integration-test) CLAIM="integration" ;; + bug-fix) CLAIM="fix" ;; + feature) CLAIM="feat" ;; + esac + + echo "Claim class: $CLAIM" + echo "claim=$CLAIM" >> "$GITHUB_OUTPUT" + + - name: Enforce strong artifact evidence for integration claims + if: steps.check.outputs.found == 'true' + env: + CLAIM: ${{ steps.claim.outputs.claim }} + run: | + BODY=$(cat "$RUNNER_TEMP/pr_body.txt") + STRIPPED_BODY=$(printf '%s' "$BODY" | python3 -c 'import re, sys; sys.stdout.write(re.sub(r"", "", sys.stdin.read(), flags=re.S))') + + # integration + pipeline-e2e claims require rich artifacts; unit/fix/feat/docs/chore can use terminal evidence only. + if [ "$CLAIM" = "integration" ] || [ "$CLAIM" = "pipeline-e2e" ]; then + # Strip HTML comments before scanning for Evidence section. + EVIDENCE=$(printf '%s\n' "$STRIPPED_BODY" | awk ' + tolower($0) ~ /^[[:space:]]*##[[:space:]]+evidence([[:space:]]|$)/ { in_section=1; next } + in_section && /^[[:space:]]*##[[:space:]]/ { exit } + in_section { print } + ') + + # Warn on fabricated/placeholder evidence + if printf '%s' "$EVIDENCE" | grep -qiE '\bsimulated\b'; then + echo "WARNING: Evidence contains 'simulated' — fabricated output is not valid." + fi + if printf '%s' "$EVIDENCE" | grep -qiE 'https?://(www\.)?example\.com'; then + echo "WARNING: Evidence contains example.com placeholder URL — use a real screenshot URL." + fi + if printf '%s' "$EVIDENCE" | grep -qiE '|||\bTODO\b|\bTBD\b'; then + echo "WARNING: Evidence contains placeholder template text — fill in real values." + fi + + # Terminal/test output: fenced code block with a concrete test-command keyword. + HAS_OUTPUT=false + TTO_BLOCK=$(printf '%s' "$EVIDENCE" | awk ' + /\*\*Terminal test output(\*\*)?:/ { show=1 } + show && /\*\*UI media(\*\*)?:/ { exit } + show { print } + ') + if printf '%s' "$TTO_BLOCK" | grep -q '```'; then + BLOCK=$(printf '%s' "$TTO_BLOCK" | sed -n '/```/,/```/p' | tail -n +2 | sed '$d') + if printf '%s' "$BLOCK" | grep -qiE '\$[[:space:]]*(pnpm|npm|pytest|vitest|jest|go[[:space:]]+test)[[:space:]]'; then + HAS_OUTPUT=true + fi + fi + if [ "$HAS_OUTPUT" = "false" ] && printf '%s' "$TTO_BLOCK" | grep -qE '\*\*Terminal test output(\*\*)?:' && printf '%s' "$TTO_BLOCK" | grep -qE 'https://[^[:space:]]+'; then + HAS_OUTPUT=true + fi + + if [ "$HAS_OUTPUT" != "true" ]; then + echo "WARNING: Strong evidence standard not met for claim class '$CLAIM'." + echo "Recommended for integration/pipeline-e2e claims:" + echo " - **Terminal test output**: fenced code block with a concrete test command" + echo " (e.g. \`\`\` block containing: pnpm test, npm test, vitest, jest, etc.)" + exit 1 + else + echo "Strong evidence standard PASS for $CLAIM" + fi + else + echo "Strong artifact check not required for claim class: $CLAIM" + fi + + - name: Validate verdict is present and consistent + if: steps.check.outputs.found == 'true' + run: | + BODY=$(cat "$RUNNER_TEMP/pr_body.txt") + STRIPPED_BODY=$(printf '%s' "$BODY" | python3 -c 'import re, sys; sys.stdout.write(re.sub(r"", "", sys.stdin.read(), flags=re.S))') + + # Extract from "## Evidence" to EOF to avoid false matches before the section, + # then strip HTML comments so template placeholders () + # cannot satisfy the verdict check on an unfilled template. + EVIDENCE_SECTION=$(printf '%s\n' "$STRIPPED_BODY" | awk ' + tolower($0) ~ /^[[:space:]]*##[[:space:]]+evidence([[:space:]]|$)/ { in_section=1; next } + in_section && /^[[:space:]]*##[[:space:]]/ { exit } + in_section { print } + ') + + # Empty extraction means there was no real Evidence section after comment stripping. + if [ -z "$EVIDENCE_SECTION" ]; then + echo "WARNING: No ## Evidence section found in PR body after stripping HTML comments." + exit 1 + fi + + # Check for PASS verdict — scoped to Evidence section + if printf '%s' "$EVIDENCE_SECTION" | grep -qi '[Vv]erdict.*:.*[Pp][Aa][Ss][Ss]'; then + echo "Verdict: PASS — evidence gate passes" + + # Check for INSUFFICIENT verdict — gate passes; used when evidence is partial + elif printf '%s' "$EVIDENCE_SECTION" | grep -qi '[Vv]erdict.*:.*[Ii][Nn][Ss][Uu][Ff][Ff][Ii][Cc][Ii][Ee][Nn][Tt]'; then + echo "Verdict: INSUFFICIENT — gate passes (marks work-in-progress evidence)" + + # Check for FAIL verdict — gate passes with a warning + elif printf '%s' "$EVIDENCE_SECTION" | grep -qi '[Vv]erdict.*:.*[Ff][Aa][Ii][Ll]'; then + echo "Verdict: FAIL with present bundle — this bundle should be re-examined" + + # No verdict found — warn + else + echo "WARNING: No verdict found in evidence bundle." + echo "Consider adding one of the following to your ## Evidence section:" + echo " **Verdict**: PASS" + echo " **Verdict**: INSUFFICIENT" + exit 1 + fi diff --git a/.jscpd.json b/.jscpd.json new file mode 100644 index 0000000000..097ecc4c8a --- /dev/null +++ b/.jscpd.json @@ -0,0 +1,17 @@ +{ + "minLines": 8, + "minTokens": 50, + "threshold": 3, + "pattern": "**/*.{ts,tsx}", + "ignore": [ + "**/dist/**", + "**/node_modules/**", + "**/__tests__/**", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.spec.ts", + "**/*.spec.tsx" + ], + "reporters": ["console"], + "gitignore": true +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ff98cac34..7c6edffbe3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -254,10 +254,25 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for the full reference. The short chore: update vitest to v2 ``` -5. **Push and open a PR**. In the PR description: - - What changed and why - - How to test it - - Link to the issue it closes (e.g., `Closes #123`) +5. **Push and open a PR**. The PR template includes an **Evidence** section — fill it in: + + ```markdown + ## Evidence + + **Claim class**: unit | integration | feat | fix | refactor | docs | chore + + **Terminal test output**: + ``` + $ pnpm test + # ... test output ... + ``` + + **Verdict**: PASS | INSUFFICIENT + ``` + + The `evidence-gate` CI check enforces this. PRs without a `## Evidence` section or + without a `**Verdict**` line will fail the check. For `integration` and `pipeline-e2e` + claim classes, a fenced code block showing test command output is also required. 6. **Address review comments** — update the branch and push. Reply to comments when done. @@ -276,6 +291,39 @@ All PRs must pass: - `pnpm test` — all tests green - `pnpm lint` — no lint errors - Secret scanning — no leaked credentials +- `evidence-gate` — PR body must include a `## Evidence` section with a `**Verdict**` + +### Churn Guard (optional local hook) + +The churn-guard hook prevents creating duplicate PRs that modify the same files as an existing open PR. To install it locally (requires Claude Code): + +```bash +mkdir -p .claude/hooks +cp scripts/hooks/churn-guard.sh .claude/hooks/churn-guard.sh +``` + +Then add the following to `.claude/settings.json`: + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/churn-guard.sh", + "timeout": 60000 + } + ] + } + ] + } +} +``` + +The hook intercepts `gh pr create` commands and checks for file overlap with open PRs. To bypass, add `Supersedes #` to the PR body. --- diff --git a/package.json b/package.json index 5f31e5c264..fcfdd1b0ff 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "version-packages": "changeset version", "release": "pnpm -r build && changeset publish", "prepare": "husky", - "postinstall": "node scripts/rebuild-node-pty.js" + "postinstall": "node scripts/rebuild-node-pty.js", + "check:duplicates": "jscpd packages" }, "devDependencies": { "@changesets/cli": "^2.29.8", @@ -32,6 +33,7 @@ "@types/node": "^25.2.3", "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", + "jscpd": "^4.0.5", "husky": "^9.1.7", "prettier": "^3.8.1", "typescript-eslint": "^8.55.0" diff --git a/packages/core/src/__tests__/agent-workspace-hooks.test.ts b/packages/core/src/__tests__/agent-workspace-hooks.test.ts index a953b0d3a2..6b674b19ff 100644 --- a/packages/core/src/__tests__/agent-workspace-hooks.test.ts +++ b/packages/core/src/__tests__/agent-workspace-hooks.test.ts @@ -77,7 +77,7 @@ describe("setupPathWrapperWorkspace", () => { it("skips wrapper rewrite when version matches", async () => { mockReadFile - .mockResolvedValueOnce("0.2.0") // version marker matches + .mockResolvedValueOnce("0.3.0") // version marker matches .mockRejectedValueOnce(new Error("ENOENT")); // AGENTS.md doesn't exist await setupPathWrapperWorkspace("/workspace"); diff --git a/packages/core/src/agent-workspace-hooks.ts b/packages/core/src/agent-workspace-hooks.ts index c1f0bb47c7..58024c97da 100644 --- a/packages/core/src/agent-workspace-hooks.ts +++ b/packages/core/src/agent-workspace-hooks.ts @@ -32,7 +32,7 @@ function getAoBinDir(): string { } /** Current version of wrapper scripts — bump when scripts change */ -const WRAPPER_VERSION = "0.2.0"; +const WRAPPER_VERSION = "0.3.0"; // ============================================================================= // PATH Builder @@ -135,10 +135,11 @@ update_ao_metadata() { /** * gh wrapper — intercepts `gh pr create` and `gh pr merge` to auto-update - * session metadata. All other commands pass through transparently. + * session metadata, and runs the churn guard before PR creation. + * All other commands pass through transparently. */ export const GH_WRAPPER = `#!/usr/bin/env bash -# ao gh wrapper — auto-updates session metadata on PR operations +# ao gh wrapper — churn guard + session metadata on PR operations # Find real gh by removing our wrapper directory from PATH ao_bin_dir="\$(cd "\$(dirname "\$0")" && pwd)" @@ -167,10 +168,151 @@ fi # Source the metadata helper source "\$ao_bin_dir/ao-metadata-helper.sh" 2>/dev/null || true +# --------------------------------------------------------------------------- +# Churn guard — warn when open PRs already touch same files (non-blocking). +# Fail-open: any error (no python3, no git, API timeout) allows the PR. +# Override: include "Supersedes #N" in --body to suppress the warning. +# --------------------------------------------------------------------------- +churn_guard() { + command -v python3 &>/dev/null || return 0 + + # Extract --body / -b value from the already-parsed arg list + local body="" next_is_body=false + for arg in "\$@"; do + if \$next_is_body; then + body="\$arg" + next_is_body=false + continue + fi + case "\$arg" in + --body|-b) next_is_body=true ;; + --body=*) body="\${arg#--body=}" ;; + esac + done + + # Check for "Supersedes #N" override + if printf '%s' "\$body" | python3 -c " +import sys, re +sys.exit(0 if re.search(r'supersedes\s*#\s*[0-9]+', sys.stdin.read(), re.IGNORECASE) else 1) +" 2>/dev/null; then + return 0 + fi + + # Resolve repo from --repo / -R flag, then fall back to git remote + local repo="" next_is_repo=false + for arg in "\$@"; do + if \$next_is_repo; then + repo="\$arg" + next_is_repo=false + continue + fi + case "\$arg" in + --repo|-R) next_is_repo=true ;; + --repo=*) repo="\${arg#--repo=}" ;; + esac + done + if [[ -z "\$repo" ]]; then + repo="\$(git remote get-url origin 2>/dev/null | python3 -c " +import sys, re +m = re.search(r'github\\.com[/:]([^/]+/[\\w.-]+?)(?:\\.git)?\$', sys.stdin.read().strip()) +print(m.group(1) if m else '') +" 2>/dev/null)" + fi + [[ -z "\$repo" ]] && return 0 + + # Files changed in this branch vs origin/main + local changed_files + changed_files="\$(git diff --name-only origin/main...HEAD 2>/dev/null)" || return 0 + [[ -z "\$changed_files" ]] && return 0 + + # Check open PRs for file overlap (50s global deadline — fail open on timeout) + local overlap + overlap="\$(python3 - "\$repo" "\$changed_files" <<'PYEOF' +import sys, subprocess, json, time, re + +repo = sys.argv[1] +my_files = set(sys.argv[2].strip().split('\\n')) +deadline = time.monotonic() + 50 + +try: + br = subprocess.run(["git", "branch", "--show-current"], + capture_output=True, text=True, timeout=5) + my_branch = br.stdout.strip() if br.returncode == 0 else "" + + rr = subprocess.run(["git", "remote", "get-url", "origin"], + capture_output=True, text=True, timeout=5) + m = re.search(r'github\\.com[/:]([^/]+)/', rr.stdout.strip()) if rr.returncode == 0 else None + my_owner = m.group(1) if m else "" + + timeout = max(5, int(deadline - time.monotonic() - 10)) + pr_list = subprocess.run( + ["gh", "api", f"repos/{repo}/pulls", "--paginate", + "--jq", "[.[] | {number:.number, title:.title, branch:.head.ref, owner:(.head.repo.owner.login // \"\")}]"], + capture_output=True, text=True, timeout=timeout) + if pr_list.returncode != 0: + print("OK"); sys.exit(0) + + prs = [] + for line in pr_list.stdout.strip().split('\\n'): + if line.strip(): + try: prs.extend(json.loads(line.strip())) + except: pass + + overlapping = [] + for pr in prs: + if pr.get("branch") == my_branch and (not my_owner or pr.get("owner") == my_owner): + continue + if time.monotonic() > deadline: + break + remaining = max(5, int(deadline - time.monotonic())) + fr = subprocess.run( + ["gh", "api", f"repos/{repo}/pulls/{pr['number']}/files", + "--paginate", "--jq", ".[].filename"], + capture_output=True, text=True, timeout=remaining) + if fr.returncode != 0: + continue + common = my_files & set(f for f in fr.stdout.strip().split('\\n') if f) + if common: + files_str = ", ".join(list(common)[:3]) + if len(common) > 3: files_str += f" (+{len(common)-3} more)" + overlapping.append(f" PR #{pr['number']}: {pr['title']} — overlapping: {files_str}") + + if overlapping: + print("OVERLAP") + for line in overlapping: + print(line) + else: + print("OK") +except Exception: + print("OK") +PYEOF + )" || return 0 + + if printf '%s' "\$overlap" | grep -q "^OVERLAP"; then + echo "" >&2 + echo "============================================" >&2 + echo "WARNING: File overlap with existing open PRs" >&2 + echo "" >&2 + echo "Your branch changes files already modified by open PRs:" >&2 + printf '%s\\n' "\$overlap" | tail -n +2 >&2 + echo "" >&2 + echo " 1. Check if the existing PR already covers your fix" >&2 + echo " 2. If yes: consider commenting on that PR instead" >&2 + echo " 3. If no: coordinate with the existing PR author" >&2 + echo " 4. Add 'Supersedes #' to --body to suppress this warning." >&2 + echo "============================================" >&2 + fi + + return 0 +} + # Only capture output for commands we need to parse (pr/create, pr/merge). # All other commands pass through transparently without stream merging. case "\$1/\$2" in - pr/create|pr/merge) + pr/create) + # Run churn guard before creating — warns on file overlap but does not block + churn_guard "\$@" + tmpout="\$(mktemp)" trap 'rm -f "\$tmpout"' EXIT @@ -179,18 +321,24 @@ case "\$1/\$2" in if [[ \$exit_code -eq 0 ]]; then output="\$(cat "\$tmpout")" - case "\$1/\$2" in - pr/create) - pr_url="\$(echo "\$output" | grep -Eo 'https://github\\.com/[^/]+/[^/]+/pull/[0-9]+' | head -1)" - if [[ -n "\$pr_url" ]]; then - update_ao_metadata pr "\$pr_url" - update_ao_metadata status pr_open - fi - ;; - pr/merge) - update_ao_metadata status merged - ;; - esac + pr_url="\$(echo "\$output" | grep -Eo 'https://github\\.com/[^/]+/[^/]+/pull/[0-9]+' | head -1)" + if [[ -n "\$pr_url" ]]; then + update_ao_metadata pr "\$pr_url" + update_ao_metadata status pr_open + fi + fi + + exit \$exit_code + ;; + pr/merge) + tmpout="\$(mktemp)" + trap 'rm -f "\$tmpout"' EXIT + + "\$real_gh" "\$@" 2>&1 | tee "\$tmpout" + exit_code=\${PIPESTATUS[0]} + + if [[ \$exit_code -eq 0 ]]; then + update_ao_metadata status merged fi exit \$exit_code diff --git a/packages/plugins/agent-codex/src/index.test.ts b/packages/plugins/agent-codex/src/index.test.ts index fc4b83381e..b0deef5855 100644 --- a/packages/plugins/agent-codex/src/index.test.ts +++ b/packages/plugins/agent-codex/src/index.test.ts @@ -1460,7 +1460,7 @@ describe("setupWorkspaceHooks", () => { // Second call for AGENTS.md — file doesn't exist mockReadFile.mockImplementation((path: string) => { if (typeof path === "string" && path.endsWith(".ao-version")) { - return Promise.resolve("0.2.0"); + return Promise.resolve("0.3.0"); } // AGENTS.md read attempt return Promise.reject(new Error("ENOENT")); @@ -1494,7 +1494,7 @@ describe("setupWorkspaceHooks", () => { typeof call[0] === "string" && call[0].includes(".ao-version.tmp."), ); expect(versionWriteCall).toBeDefined(); - expect(versionWriteCall![1]).toBe("0.2.0"); + expect(versionWriteCall![1]).toBe("0.3.0"); const versionRenameCall = mockRename.mock.calls.find( (call: string[]) => typeof call[1] === "string" && call[1].endsWith(".ao-version"), @@ -1506,7 +1506,7 @@ describe("setupWorkspaceHooks", () => { // Version marker matches (skip wrapper install) mockReadFile.mockImplementation((path: string) => { if (typeof path === "string" && path.endsWith(".ao-version")) { - return Promise.resolve("0.2.0"); + return Promise.resolve("0.3.0"); } return Promise.reject(new Error("ENOENT")); }); @@ -1553,7 +1553,7 @@ describe("setupWorkspaceHooks", () => { it("writes .ao/AGENTS.md without modifying repo-tracked AGENTS.md", async () => { mockReadFile.mockImplementation((path: string) => { if (typeof path === "string" && path.endsWith(".ao-version")) { - return Promise.resolve("0.2.0"); + return Promise.resolve("0.3.0"); } return Promise.reject(new Error("ENOENT")); }); @@ -1654,7 +1654,8 @@ describe("shell wrapper content", () => { it("only captures output for pr/create and pr/merge", async () => { const content = await getWrapperContent("gh"); - expect(content).toContain("pr/create|pr/merge"); + expect(content).toContain("pr/create)"); + expect(content).toContain("pr/merge)"); }); it("uses exec for non-PR commands (transparent passthrough)", async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index defdd642e4..8fb23a29b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: husky: specifier: ^9.1.7 version: 9.1.7 + jscpd: + specifier: ^4.0.5 + version: 4.0.8 prettier: specifier: ^3.8.1 version: 3.8.1 @@ -886,6 +889,10 @@ packages: '@cloudflare/workers-types@4.20260214.0': resolution: {integrity: sha512-qb8rgbAdJR4BAPXolXhFL/wuGtecHLh1veOyZ1mK6QqWuCdI3vK1biKC0i3lzmzdLR/DZvsN3mNtpUE8zpWGEg==} + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + '@composio/mcp@1.0.3-0': resolution: {integrity: sha512-IpbfST0SSs/CEv+PIf6+EL0feNuJhQyUrOHJLPge8NhyLLrCyVqvujRIPyRUUjy0NDsked/Mm5VpJmYM6OACbg==} hasBin: true @@ -1518,6 +1525,21 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jscpd/badge-reporter@4.0.4': + resolution: {integrity: sha512-I9b4MmLXPM2vo0SxSUWnNGKcA4PjQlD3GzXvFK60z43cN/EIdLbOq3FVwCL+dg2obUqGXKIzAm7EsDFTg0D+mQ==} + + '@jscpd/core@4.0.4': + resolution: {integrity: sha512-QGMT3iXEX1fI6lgjPH+x8eyJwhwr2KkpSF5uBpjC0Z5Xloj0yFTFLtwJT+RhxP/Ob4WYrtx2jvpKB269oIwgMQ==} + + '@jscpd/finder@4.0.4': + resolution: {integrity: sha512-qVUWY7Nzuvfd5OIk+n7/5CM98LmFroLqblRXAI2gDABwZrc7qS+WH2SNr0qoUq0f4OqwM+piiwKvwL/VDNn/Cg==} + + '@jscpd/html-reporter@4.0.4': + resolution: {integrity: sha512-YiepyeYkeH74Kx59PJRdUdonznct0wHPFkf6FLQN+mCBoy6leAWCcOfHtcexnp+UsBFDlItG5nRdKrDSxSH+Kg==} + + '@jscpd/tokenizer@4.0.4': + resolution: {integrity: sha512-xxYYY/qaLah/FlwogEbGIxx9CjDO+G9E6qawcy26WwrflzJb6wsnhjwdneN6Wb0RNCDsqvzY+bzG453jsin4UQ==} + '@langchain/core@1.1.24': resolution: {integrity: sha512-u6l0dmMHN/2PCsY6stXoh9CH1OTlVR5Gjz0JjT1XRPuidAlu3kTq4ivW95xCog/PRhiAsCh6GCEC4/PqhNrcgQ==} engines: {node: '>=20'} @@ -1945,6 +1967,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/sarif@2.1.7': + resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} + '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} @@ -2156,6 +2181,11 @@ packages: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -2219,6 +2249,12 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assert-never@1.4.0: + resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2232,6 +2268,13 @@ packages: axios@1.13.5: resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + babel-walk@3.0.0-canary-5: + resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} + engines: {node: '>= 10.0.0'} + + badgen@3.2.3: + resolution: {integrity: sha512-svDuwkc63E/z0ky3drpUppB83s/nlgDciH9m+STwwQoWyq7yCgew1qEfJ+9axkKdNq7MskByptWUN9j1PGMwFA==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2250,6 +2293,10 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + blamer@1.0.7: + resolution: {integrity: sha512-GbBStl/EVlSWkiJQBZps3H1iARBrC7vt++Jb/TTmCNu/jZ04VW7tSN1nScbFXBUy1AN+jzeL7Zep9sbQxLhXKA==} + engines: {node: '>=8.9'} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -2266,6 +2313,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2278,6 +2329,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} @@ -2301,6 +2356,9 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-parser@2.2.0: + resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} + chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -2331,6 +2389,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -2349,6 +2411,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2361,6 +2427,10 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -2386,6 +2456,9 @@ packages: console-table-printer@2.15.0: resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==} + constantinople@4.0.1: + resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2457,6 +2530,9 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + doctypes@1.1.0: + resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -2488,6 +2564,9 @@ packages: encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -2614,6 +2693,10 @@ packages: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -2693,6 +2776,10 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2738,9 +2825,17 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + gitignore-to-glob@0.3.0: + resolution: {integrity: sha512-mk74BdnK7lIwDHnotHddx1wsjMOFIThpLY3cPNniJ/2fA/tlLzHnFxIdR+4sLOu5KGgQJdij4kjJ2RoUNnCNMA==} + engines: {node: '>=4.4 <5 || >=6.9'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2811,6 +2906,10 @@ packages: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -2852,11 +2951,18 @@ packages: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} hasBin: true + is-expression@4.0.0: + resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2888,6 +2994,17 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -2942,6 +3059,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-stringify@1.0.2: + resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} + js-tiktoken@1.0.21: resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} @@ -2962,6 +3082,13 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jscpd-sarif-reporter@4.0.6: + resolution: {integrity: sha512-b9Sm3IPZ3+m8Lwa4gZa+4/LhDhlc/ZLEsLXKSOy1DANQ6kx0ueqZT+fUHWEdQ6m0o3+RIVIa7DmvLSojQD05ng==} + + jscpd@4.0.8: + resolution: {integrity: sha512-d2VNT/2Hv4dxT2/59He8Lyda4DYOxPRyRG9zBaOpTZAqJCVf2xLrBlZkT8Va6Lo9u3X2qz8Bpq4HrDi4JsrQhA==} + hasBin: true + jsdom@25.0.1: resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} engines: {node: '>=18'} @@ -2996,6 +3123,12 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jstransformer@1.0.0: + resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3145,10 +3278,16 @@ packages: resolution: {integrity: sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==} engines: {node: ^20.17.0 || >=22.9.0} + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3165,6 +3304,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -3289,17 +3432,36 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-sarif-builder@3.4.0: + resolution: {integrity: sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==} + engines: {node: '>=20'} + nopt@9.0.0: resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} engines: {node: ^20.17.0 || >=22.9.0} hasBin: true + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -3412,6 +3574,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-root-regex@0.1.2: resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} engines: {node: '>=0.10.0'} @@ -3501,9 +3666,51 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pug-attrs@3.0.0: + resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} + + pug-code-gen@3.0.4: + resolution: {integrity: sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==} + + pug-error@2.1.0: + resolution: {integrity: sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==} + + pug-filters@4.0.0: + resolution: {integrity: sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==} + + pug-lexer@5.0.1: + resolution: {integrity: sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==} + + pug-linker@4.0.0: + resolution: {integrity: sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==} + + pug-load@3.0.0: + resolution: {integrity: sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==} + + pug-parser@6.0.0: + resolution: {integrity: sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==} + + pug-runtime@3.0.1: + resolution: {integrity: sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==} + + pug-strip-comments@2.0.0: + resolution: {integrity: sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==} + + pug-walk@2.0.0: + resolution: {integrity: sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==} + + pug@3.0.4: + resolution: {integrity: sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3541,6 +3748,13 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + reprism@0.0.11: + resolution: {integrity: sha512-VsxDR5QxZo08M/3nRypNlScw5r3rKeSOPdU/QhDmu3Ai3BJxHn/qgfXGWQp/tAxUtzwYNo9W6997JZR0tPLZsA==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3556,6 +3770,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -3635,6 +3854,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -3669,6 +3891,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + spark-md5@3.0.2: + resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -3713,6 +3938,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -3741,6 +3970,10 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -3817,6 +4050,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + token-stream@1.0.0: + resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -3888,6 +4124,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -4073,6 +4313,10 @@ packages: jsdom: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -4114,6 +4358,10 @@ packages: engines: {node: '>=8'} hasBin: true + with@7.0.2: + resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} + engines: {node: '>= 10.0.0'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4130,6 +4378,9 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -4530,6 +4781,9 @@ snapshots: '@cloudflare/workers-types@4.20260214.0': {} + '@colors/colors@1.5.0': + optional: true + '@composio/mcp@1.0.3-0': {} '@csstools/color-helpers@5.1.0': {} @@ -4995,6 +5249,41 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jscpd/badge-reporter@4.0.4': + dependencies: + badgen: 3.2.3 + colors: 1.4.0 + fs-extra: 11.3.4 + + '@jscpd/core@4.0.4': + dependencies: + eventemitter3: 5.0.4 + + '@jscpd/finder@4.0.4': + dependencies: + '@jscpd/core': 4.0.4 + '@jscpd/tokenizer': 4.0.4 + blamer: 1.0.7 + bytes: 3.1.2 + cli-table3: 0.6.5 + colors: 1.4.0 + fast-glob: 3.3.3 + fs-extra: 11.3.4 + markdown-table: 2.0.0 + pug: 3.0.4 + + '@jscpd/html-reporter@4.0.4': + dependencies: + colors: 1.4.0 + fs-extra: 11.3.4 + pug: 3.0.4 + + '@jscpd/tokenizer@4.0.4': + dependencies: + '@jscpd/core': 4.0.4 + reprism: 0.0.11 + spark-md5: 3.0.2 + '@langchain/core@1.1.24(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76))': dependencies: '@cfworker/json-schema': 4.1.1 @@ -5374,6 +5663,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/sarif@2.1.7': {} + '@types/uuid@10.0.0': {} '@types/wrap-ansi@3.0.0': {} @@ -5673,6 +5964,8 @@ snapshots: dependencies: acorn: 8.15.0 + acorn@7.4.1: {} + acorn@8.15.0: {} agent-base@7.1.4: {} @@ -5724,6 +6017,10 @@ snapshots: array-union@2.1.0: {} + asap@2.0.6: {} + + assert-never@1.4.0: {} + assertion-error@2.0.1: {} ast-v8-to-istanbul@0.3.12: @@ -5742,6 +6039,12 @@ snapshots: transitivePeerDependencies: - debug + babel-walk@3.0.0-canary-5: + dependencies: + '@babel/types': 7.29.0 + + badgen@3.2.3: {} + balanced-match@1.0.2: {} balanced-match@4.0.2: @@ -5756,6 +6059,11 @@ snapshots: dependencies: is-windows: 1.0.2 + blamer@1.0.7: + dependencies: + execa: 4.1.0 + which: 2.0.2 + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -5776,6 +6084,8 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + bytes@3.1.2: {} + cac@6.7.14: {} cacache@20.0.3: @@ -5797,6 +6107,11 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + camelcase@6.3.0: {} caniuse-lite@1.0.30001769: {} @@ -5818,6 +6133,10 @@ snapshots: chalk@5.6.2: {} + character-parser@2.2.0: + dependencies: + is-regex: 1.2.1 + chardet@0.7.0: {} chardet@2.1.1: {} @@ -5838,6 +6157,12 @@ snapshots: cli-spinners@2.9.2: {} + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + cli-width@4.1.0: {} client-only@0.0.1: {} @@ -5854,6 +6179,8 @@ snapshots: color-name@1.1.4: {} + colors@1.4.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -5862,6 +6189,8 @@ snapshots: commander@13.1.0: {} + commander@5.1.0: {} + commander@7.2.0: {} composio-core@0.5.39(@ai-sdk/openai@3.0.29(zod@3.25.76))(@cloudflare/workers-types@4.20260214.0)(@langchain/core@1.1.24(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76)))(@langchain/openai@1.2.7(@langchain/core@1.1.24(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76)))(ws@8.19.0))(ai@6.0.86(zod@3.25.76))(langchain@1.2.24(@langchain/core@1.1.24(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@3.25.76)))(openai@6.22.0(ws@8.19.0)(zod@3.25.76)): @@ -5902,6 +6231,11 @@ snapshots: dependencies: simple-wcswidth: 1.1.2 + constantinople@4.0.1: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + convert-source-map@2.0.0: {} cross-spawn@7.0.6: @@ -5952,6 +6286,8 @@ snapshots: dependencies: path-type: 4.0.0 + doctypes@1.1.0: {} + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -5979,6 +6315,10 @@ snapshots: iconv-lite: 0.6.3 optional: true + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 @@ -6155,6 +6495,18 @@ snapshots: eventsource-parser@3.0.6: {} + execa@4.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + expect-type@1.3.0: {} exponential-backoff@3.1.3: {} @@ -6229,6 +6581,12 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -6277,10 +6635,16 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + get-tsconfig@4.13.6: dependencies: resolve-pkg-maps: 1.0.0 + gitignore-to-glob@0.3.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -6357,6 +6721,8 @@ snapshots: human-id@4.1.3: {} + human-signals@1.1.1: {} + husky@9.1.7: {} iconv-lite@0.4.24: @@ -6392,8 +6758,17 @@ snapshots: ip-address@10.1.0: {} + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-docker@2.2.1: {} + is-expression@4.0.0: + dependencies: + acorn: 7.4.1 + object-assign: 4.1.1 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -6412,6 +6787,17 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@2.2.2: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-stream@2.0.1: {} + is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -6463,6 +6849,8 @@ snapshots: jiti@2.6.1: {} + js-stringify@1.0.2: {} + js-tiktoken@1.0.21: dependencies: base64-js: 1.5.1 @@ -6482,6 +6870,25 @@ snapshots: dependencies: argparse: 2.0.1 + jscpd-sarif-reporter@4.0.6: + dependencies: + colors: 1.4.0 + fs-extra: 11.3.4 + node-sarif-builder: 3.4.0 + + jscpd@4.0.8: + dependencies: + '@jscpd/badge-reporter': 4.0.4 + '@jscpd/core': 4.0.4 + '@jscpd/finder': 4.0.4 + '@jscpd/html-reporter': 4.0.4 + '@jscpd/tokenizer': 4.0.4 + colors: 1.4.0 + commander: 5.1.0 + fs-extra: 11.3.4 + gitignore-to-glob: 0.3.0 + jscpd-sarif-reporter: 4.0.6 + jsdom@25.0.1: dependencies: cssstyle: 4.6.0 @@ -6526,6 +6933,17 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jstransformer@1.0.0: + dependencies: + is-promise: 2.2.2 + promise: 7.3.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -6676,8 +7094,14 @@ snapshots: transitivePeerDependencies: - supports-color + markdown-table@2.0.0: + dependencies: + repeat-string: 1.6.1 + math-intrinsics@1.1.0: {} + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -6691,6 +7115,8 @@ snapshots: dependencies: mime-db: 1.52.0 + mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} min-indent@1.0.1: {} @@ -6811,14 +7237,33 @@ snapshots: node-releases@2.0.27: {} + node-sarif-builder@3.4.0: + dependencies: + '@types/sarif': 2.1.7 + fs-extra: 11.3.4 + nopt@9.0.0: dependencies: abbrev: 4.0.0 + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + nwsapi@2.2.23: {} + object-assign@4.1.1: {} + obug@2.1.1: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -6923,6 +7368,8 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + path-root-regex@0.1.2: {} path-root@0.1.1: @@ -6994,8 +7441,84 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + promise@7.3.1: + dependencies: + asap: 2.0.6 + proxy-from-env@1.1.0: {} + pug-attrs@3.0.0: + dependencies: + constantinople: 4.0.1 + js-stringify: 1.0.2 + pug-runtime: 3.0.1 + + pug-code-gen@3.0.4: + dependencies: + constantinople: 4.0.1 + doctypes: 1.1.0 + js-stringify: 1.0.2 + pug-attrs: 3.0.0 + pug-error: 2.1.0 + pug-runtime: 3.0.1 + void-elements: 3.1.0 + with: 7.0.2 + + pug-error@2.1.0: {} + + pug-filters@4.0.0: + dependencies: + constantinople: 4.0.1 + jstransformer: 1.0.0 + pug-error: 2.1.0 + pug-walk: 2.0.0 + resolve: 1.22.11 + + pug-lexer@5.0.1: + dependencies: + character-parser: 2.2.0 + is-expression: 4.0.0 + pug-error: 2.1.0 + + pug-linker@4.0.0: + dependencies: + pug-error: 2.1.0 + pug-walk: 2.0.0 + + pug-load@3.0.0: + dependencies: + object-assign: 4.1.1 + pug-walk: 2.0.0 + + pug-parser@6.0.0: + dependencies: + pug-error: 2.1.0 + token-stream: 1.0.0 + + pug-runtime@3.0.1: {} + + pug-strip-comments@2.0.0: + dependencies: + pug-error: 2.1.0 + + pug-walk@2.0.0: {} + + pug@3.0.4: + dependencies: + pug-code-gen: 3.0.4 + pug-filters: 4.0.0 + pug-lexer: 5.0.1 + pug-linker: 4.0.0 + pug-load: 3.0.0 + pug-parser: 6.0.0 + pug-runtime: 3.0.1 + pug-strip-comments: 2.0.0 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} pusher-js@8.4.0-rc2: @@ -7029,6 +7552,10 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + repeat-string@1.6.1: {} + + reprism@0.0.11: {} + require-directory@2.1.1: {} resolve-from@5.0.0: {} @@ -7039,6 +7566,12 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -7154,6 +7687,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} simple-wcswidth@1.1.2: {} @@ -7185,6 +7720,8 @@ snapshots: source-map-js@1.2.1: {} + spark-md5@3.0.2: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -7230,6 +7767,8 @@ snapshots: strip-bom@3.0.0: {} + strip-final-newline@2.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -7253,6 +7792,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + symbol-tree@3.2.4: {} tailwindcss@4.1.18: {} @@ -7312,6 +7853,8 @@ snapshots: dependencies: is-number: 7.0.0 + token-stream@1.0.0: {} + totalist@3.0.1: {} tough-cookie@5.1.2: @@ -7372,6 +7915,8 @@ snapshots: universalify@0.1.2: {} + universalify@2.0.1: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 @@ -7568,6 +8113,8 @@ snapshots: - tsx - yaml + void-elements@3.1.0: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -7617,6 +8164,13 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + with@7.0.2: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + assert-never: 1.4.0 + babel-walk: 3.0.0-canary-5 + word-wrap@1.2.5: {} wrap-ansi@6.2.0: @@ -7637,6 +8191,8 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + wrappy@1.0.2: {} + ws@7.5.10: {} ws@8.19.0: {} diff --git a/scripts/hooks/churn-guard.sh b/scripts/hooks/churn-guard.sh new file mode 100755 index 0000000000..39d0bc477a --- /dev/null +++ b/scripts/hooks/churn-guard.sh @@ -0,0 +1,282 @@ +#!/bin/bash +# churn-guard.sh — PreToolUse hook: block PR creation when open PRs already touch same files +# 8 PRs for metadata-updater.sh in 1hr on 2026-04-04 — 7 were wasted duplicates. +# Root cause: no file-level coordination gate at PR creation time. +# +# Intercepts: gh pr create, gh api repos/.../pulls --method POST, gh api .../pulls -f ... +# Checks: do any open PRs in the same repo touch files changed in the current branch? +# If overlap found: BLOCK with list of overlapping PRs. +# Override: include "Supersedes #N" in the PR body/command to bypass. +# Fail-open: if checks fail (no git, no gh, no python3, parse error), allow. + +set -euo pipefail + +# Fail open if python3 is missing +if ! command -v python3 &>/dev/null; then + exit 0 +fi + +INPUT=$(cat) + +# Only intercept Bash tool calls +TOOL=$(echo "$INPUT" | python3 -c " +import sys, json +try: + d = json.load(sys.stdin) + print(d.get('tool_name', '')) +except Exception: + print('') +" 2>/dev/null) || TOOL="" + +if [ "$TOOL" != "Bash" ]; then + exit 0 +fi + +CMD=$(echo "$INPUT" | python3 -c " +import sys, json +try: + d = json.load(sys.stdin) + print(d.get('tool_input', {}).get('command', '')) +except Exception: + print('') +" 2>/dev/null) || CMD="" + +if [ -z "$CMD" ]; then + exit 0 +fi + +# Detect PR creation commands +IS_PR_CREATE=$(echo "$CMD" | python3 -c " +import sys, re +cmd = sys.stdin.read() +# gh pr create +if re.search(r'\bgh\s+pr\s+create\b', cmd, re.IGNORECASE): + print('YES') + sys.exit(0) +# gh api with pulls collection endpoint (NOT /pulls/N/... nested paths) +has_gh_api = re.search(r'\bgh\s+api\b', cmd, re.IGNORECASE) +has_pulls_collection = re.search(r'repos/[^/]+/[^/]+/pulls(?!\s*/\d)(?:\s|\Z|[\x27\x22])', cmd, re.IGNORECASE) +if has_gh_api and has_pulls_collection: + # Explicit POST: --method POST, --method=POST, -X POST, -X=POST + if re.search(r'(?:--method|-X)[=\s]+POST', cmd, re.IGNORECASE): + print('YES') + sys.exit(0) + # Implicit POST via field flags: -f, -F, --field, --raw-field (can appear anywhere) + if re.search(r'(?:\s|^)(?:-[fF]\s|--field\s|--raw-field\s)', cmd): + print('YES') + sys.exit(0) + # Implicit POST via --input + if re.search(r'--input\s', cmd, re.IGNORECASE): + print('YES') + sys.exit(0) +print('NO') +" 2>/dev/null) || IS_PR_CREATE="NO" + +if [ "$IS_PR_CREATE" != "YES" ]; then + exit 0 +fi + +# Check for "Supersedes #N" override — only inside --body or -b payload, not the full command +HAS_SUPERSEDES=$(echo "$CMD" | python3 -c " +import sys, re +cmd = sys.stdin.read() +# Extract body content from --body '...' or --body \"...\" or -f body='...' or heredoc body +body = '' +# --body or -b flag with double-quoted value (supports --body=, --body , -b ) +m = re.search(r'(?:--body|-b)[=\s]\x22((?:[^\x22\\\\]|\\.)*)\x22', cmd, re.DOTALL) +if m: + body = m.group(1) +# --body or -b flag with single-quoted value (supports --body=, --body , -b ) +if not body: + m = re.search(r\"(?:--body|-b)[=\s]\x27([^\x27]*)\x27\", cmd, re.DOTALL) + if m: + body = m.group(1) +# -f body=... or --field body=... (double-quoted) +if not body: + m = re.search(r'(?:-[fF]|--field)\s+body=\x22((?:[^\x22\\\\]|\\.)*)\x22', cmd, re.DOTALL) + if m: + body = m.group(1) +# -f body=... (single-quoted) +if not body: + m = re.search(r\"(?:-[fF]|--field)\s+body=\x27([^\x27]*)\x27\", cmd, re.DOTALL) + if m: + body = m.group(1) +# heredoc: look for body content in EOF blocks +if not body: + m = re.search(r'<<[\x27]?EOF[\x27]?\n(.*?)\nEOF', cmd, re.DOTALL) + if m: + body = m.group(1) +if re.search(r'supersedes\s*#\s*\d+', body, re.IGNORECASE): + print('YES') +else: + print('NO') +" 2>/dev/null) || HAS_SUPERSEDES="NO" + +if [ "$HAS_SUPERSEDES" = "YES" ]; then + exit 0 +fi + +# --- PR creation detected, no override — check for file overlap --- + +# Get the repo (try --repo flag first, then git remote) +REPO=$(echo "$CMD" | python3 -c " +import sys, re +cmd = sys.stdin.read() +# Support: --repo VALUE, --repo=VALUE, -R VALUE +m = re.search(r'(?:--repo[=\s]|-R\s)(\S+)', cmd, re.IGNORECASE) +if m: + print(m.group(1).rstrip('/')) + sys.exit(0) +# Try repos/OWNER/REPO/pulls pattern +m = re.search(r'repos/([^/]+/[^/]+)/pulls', cmd) +if m: + print(m.group(1)) + sys.exit(0) +print('') +" 2>/dev/null) || REPO="" + +# Fall back to git remote +if [ -z "$REPO" ]; then + REPO=$(git remote get-url origin 2>/dev/null | python3 -c " +import sys, re +url = sys.stdin.read().strip() +m = re.search(r'github\.com[/:]([\w-]+/[\w.-]+?)(?:\.git)?$', url) +if m: + print(m.group(1)) +else: + print('') +" 2>/dev/null) || REPO="" +fi + +if [ -z "$REPO" ]; then + # Can't determine repo — fail open + exit 0 +fi + +# Get files changed in current branch vs main +CHANGED_FILES=$(git diff --name-only origin/main...HEAD 2>/dev/null) || CHANGED_FILES="" + +if [ -z "$CHANGED_FILES" ]; then + # No changed files detected (maybe not in a git repo or on main) — fail open + exit 0 +fi + +# Check open PRs for file overlap +OVERLAP=$(python3 - "$REPO" "$CHANGED_FILES" <<'PYEOF' +import sys, subprocess, json + +repo = sys.argv[1] +my_files = set(sys.argv[2].strip().split('\n')) + +try: + import time + # Global deadline: 50s total for the entire overlap check (hook timeout is 60s) + global_deadline = time.monotonic() + 50 + + # Get current branch to exclude our own PR + branch_result = subprocess.run( + ["git", "branch", "--show-current"], + capture_output=True, text=True, timeout=5 + ) + my_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else "" + + # Get current remote owner for cross-fork branch dedup + remote_result = subprocess.run( + ["git", "remote", "get-url", "origin"], + capture_output=True, text=True, timeout=5 + ) + import re as _re + _m = _re.search(r'github\.com[/:]([^/]+)/', remote_result.stdout.strip()) if remote_result.returncode == 0 else None + my_owner = _m.group(1) if _m else "" + + # List open PRs (with --paginate to get all pages) + pr_list_timeout = max(5, int(global_deadline - time.monotonic() - 10)) + result = subprocess.run( + ["gh", "api", f"repos/{repo}/pulls", "--paginate", + "--jq", "[.[] | {number: .number, title: .title, branch: .head.ref, owner: (.head.repo.owner.login // \"\")}]"], + capture_output=True, text=True, timeout=pr_list_timeout + ) + + if result.returncode != 0: + print("OK") + sys.exit(0) + + # --paginate outputs multiple JSON arrays; merge them + prs = [] + for line in result.stdout.strip().split('\n'): + line = line.strip() + if line: + try: + prs.extend(json.loads(line)) + except json.JSONDecodeError: + continue + + overlapping = [] + for pr in prs: + # Skip our own branch (match both owner and branch to avoid cross-fork collisions) + if pr.get("branch") == my_branch and (not my_owner or pr.get("owner") == my_owner): + continue + + # Bail if approaching global deadline — fail open + if time.monotonic() > global_deadline: + break + + # Get files for this PR (with --paginate for PRs with many files) + remaining = max(5, int(global_deadline - time.monotonic())) + files_result = subprocess.run( + ["gh", "api", f"repos/{repo}/pulls/{pr['number']}/files", + "--paginate", "--jq", ".[].filename"], + capture_output=True, text=True, timeout=remaining + ) + + if files_result.returncode != 0: + continue + + pr_files = set(f for f in files_result.stdout.strip().split('\n') if f) + common = my_files & pr_files + + if common: + overlapping.append({ + "number": pr["number"], + "title": pr["title"], + "files": list(common) + }) + + if overlapping: + lines = ["OVERLAP"] + for o in overlapping: + files_str = ", ".join(o["files"][:3]) + if len(o["files"]) > 3: + files_str += f" (+{len(o['files'])-3} more)" + lines.append(f" PR #{o['number']}: {o['title']} — overlapping: {files_str}") + print("\n".join(lines)) + else: + print("OK") + +except subprocess.TimeoutExpired: + print("OK") # fail open +except Exception: + print("OK") # fail open +PYEOF +) || OVERLAP="OK" + +if echo "$OVERLAP" | grep -q "^OVERLAP"; then + echo "" >&2 + echo "============================================" >&2 + echo "BLOCKED: File overlap with existing open PRs" >&2 + echo "" >&2 + echo "Your branch changes files that are already being modified by open PRs:" >&2 + echo "$OVERLAP" | tail -n +2 >&2 + echo "" >&2 + echo "To prevent churn (8 duplicate PRs for metadata-updater.sh on 2026-04-04):" >&2 + echo " 1. Check if the existing PR already covers your fix" >&2 + echo " 2. If yes: post your changes as a review comment on that PR instead" >&2 + echo " 3. If no: coordinate with the existing PR author" >&2 + echo " 4. Only create a new PR if the existing one is abandoned/stale (>24h no activity)" >&2 + echo "" >&2 + echo "To override: add 'Supersedes #' to the PR body (--body flag)." >&2 + echo "============================================" >&2 + exit 2 +fi + +exit 0