diff --git a/.github/workflows/provenance.yml b/.github/workflows/provenance.yml
index 35828c2db..77af369b1 100644
--- a/.github/workflows/provenance.yml
+++ b/.github/workflows/provenance.yml
@@ -166,7 +166,12 @@ jobs:
- uses: SocketDev/socket-registry/.github/actions/install@6096b06b1790f411714c89c40f72aade2eeaab7c # main
- - run: npm install -g npm@11.12.1
+ - name: Upgrade npm for trusted publishing
+ run: |
+ # Avoid npm self-upgrade (corrupts deps mid-install on Node 22).
+ NPM_DIR="$(npm config get prefix)/lib/node_modules/npm"
+ curl -sL https://registry.npmjs.org/npm/-/npm-11.12.1.tgz | tar xz -C "$NPM_DIR" --strip-components=1
+ echo "npm version: $(npm --version)"
# Get versions for lock-stepped and independent packages.
- name: Get versions
diff --git a/.github/workflows/weekly-update.yml b/.github/workflows/weekly-update.yml
index 9f9dc4f8c..a2b42e3c6 100644
--- a/.github/workflows/weekly-update.yml
+++ b/.github/workflows/weekly-update.yml
@@ -94,13 +94,20 @@ jobs:
with:
gpg-private-key: ${{ secrets.BOT_GPG_PRIVATE_KEY }}
- - name: Run updating skill with Claude Code
- id: claude
- timeout-minutes: 30
+ - name: Update dependencies (haiku — fast, cheap)
+ id: update
+ timeout-minutes: 10
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_ACTIONS: 'true'
run: |
+ if [ -n "$SFW_BIN" ]; then
+ mkdir -p /tmp/sfw-bin
+ printf '#!/bin/bash\nexec "%s" pnpm "$@"\n' "$SFW_BIN" > /tmp/sfw-bin/pnpm
+ chmod +x /tmp/sfw-bin/pnpm
+ export PATH="/tmp/sfw-bin:$PATH"
+ fi
+
if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "ANTHROPIC_API_KEY not set - skipping automated update"
echo "success=false" >> $GITHUB_OUTPUT
@@ -108,8 +115,10 @@ jobs:
fi
set +e
- pnpm exec claude --print --dangerously-skip-permissions \
- --model sonnet \
+ pnpm exec claude --print \
+ --allowedTools "Bash(pnpm:*)" "Bash(git add:*)" "Bash(git commit:*)" "Bash(git status:*)" "Bash(git diff:*)" "Bash(git log:*)" "Bash(git rev-parse:*)" "Read" "Write" "Edit" "Glob" "Grep" \
+ --model haiku \
+ --max-turns 15 \
"$(cat <<'PROMPT'
/updating
@@ -122,7 +131,7 @@ jobs:
Update all dependencies to their latest versions.
Create one atomic commit per dependency update with a conventional commit message.
Leave all changes local — the workflow handles pushing and PR creation.
- Skip running builds, tests, and type checks — CI runs those separately.
+ Do not run builds or tests — the next step handles that.
@@ -132,7 +141,7 @@ jobs:
PROMPT
)" \
- 2>&1 | tee claude-output.log
+ 2>&1 | tee claude-update.log
CLAUDE_EXIT=${PIPESTATUS[0]}
set -e
@@ -142,6 +151,126 @@ jobs:
echo "success=false" >> $GITHUB_OUTPUT
fi
+ - name: Run tests
+ id: tests
+ if: steps.update.outputs.success == 'true'
+ run: |
+ if [ -n "$SFW_BIN" ]; then
+ mkdir -p /tmp/sfw-bin
+ printf '#!/bin/bash\nexec "%s" pnpm "$@"\n' "$SFW_BIN" > /tmp/sfw-bin/pnpm
+ chmod +x /tmp/sfw-bin/pnpm
+ export PATH="/tmp/sfw-bin:$PATH"
+ fi
+
+ set +e
+ pnpm build 2>&1 | tee build.log
+ BUILD_EXIT=${PIPESTATUS[0]}
+
+ pnpm test 2>&1 | tee test.log
+ TEST_EXIT=${PIPESTATUS[0]}
+ set -e
+
+ if [ "$BUILD_EXIT" -eq 0 ] && [ "$TEST_EXIT" -eq 0 ]; then
+ echo "tests-passed=true" >> $GITHUB_OUTPUT
+ else
+ echo "tests-passed=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Fix test failures (sonnet — smarter, escalated)
+ id: claude
+ if: steps.update.outputs.success == 'true' && steps.tests.outputs.tests-passed == 'false'
+ timeout-minutes: 15
+ env:
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ GITHUB_ACTIONS: 'true'
+ run: |
+ if [ -n "$SFW_BIN" ]; then
+ mkdir -p /tmp/sfw-bin
+ printf '#!/bin/bash\nexec "%s" pnpm "$@"\n' "$SFW_BIN" > /tmp/sfw-bin/pnpm
+ chmod +x /tmp/sfw-bin/pnpm
+ export PATH="/tmp/sfw-bin:$PATH"
+ fi
+
+ FAILURE_LOG="$(cat build.log test.log 2>/dev/null)"
+
+ set +e
+ pnpm exec claude --print \
+ --allowedTools "Bash(pnpm:*)" "Bash(git add:*)" "Bash(git commit:*)" "Bash(git status:*)" "Bash(git diff:*)" "Bash(git log:*)" "Bash(git rev-parse:*)" "Read" "Write" "Edit" "Glob" "Grep" \
+ --model sonnet \
+ --max-turns 25 \
+ "$(cat <
+ You are an automated CI agent in a weekly dependency update workflow.
+ Git is configured with GPG signing. A branch has been created for you.
+ Dependencies were updated in the previous step but build/tests failed.
+
+
+
+ $FAILURE_LOG
+
+
+
+ The dependency updates above caused build or test failures.
+ Diagnose the failures from the logs and fix the code so it builds and tests pass.
+ Create one atomic commit per fix with a conventional commit message.
+ Run pnpm build && pnpm test to verify your fixes.
+ Leave all changes local — the workflow handles pushing and PR creation.
+
+
+
+ pnpm build succeeds.
+ pnpm test succeeds.
+ Each fix has its own commit.
+ No uncommitted changes remain in the working tree.
+
+ PROMPT
+ )" \
+ 2>&1 | tee claude-fix.log
+ CLAUDE_EXIT=${PIPESTATUS[0]}
+ set -e
+
+ if [ "$CLAUDE_EXIT" -eq 0 ]; then
+ echo "success=true" >> $GITHUB_OUTPUT
+ else
+ echo "success=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Set final status
+ id: final
+ if: always()
+ env:
+ UPDATE_SUCCESS: ${{ steps.update.outputs.success }}
+ TESTS_PASSED: ${{ steps.tests.outputs.tests-passed }}
+ FIX_SUCCESS: ${{ steps.claude.outputs.success }}
+ run: |
+ if [ "$UPDATE_SUCCESS" = "true" ] && [ "$TESTS_PASSED" = "true" ]; then
+ echo "success=true" >> $GITHUB_OUTPUT
+ elif [ "$UPDATE_SUCCESS" = "true" ] && [ "$FIX_SUCCESS" = "true" ]; then
+ echo "success=true" >> $GITHUB_OUTPUT
+ else
+ echo "success=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Validate changes
+ id: validate
+ if: steps.final.outputs.success == 'true'
+ run: |
+ UNEXPECTED=""
+ for file in $(git diff --name-only origin/main..HEAD); do
+ case "$file" in
+ package.json|*/package.json|pnpm-lock.yaml|*/pnpm-lock.yaml|.npmrc|pnpm-workspace.yaml) ;;
+ src/*|test/*) ;;
+ *.ts|*.mts|*.js|*.mjs) ;;
+ *) UNEXPECTED="$UNEXPECTED $file" ;;
+ esac
+ done
+ if [ -n "$UNEXPECTED" ]; then
+ echo "::error::Unexpected files modified by Claude:$UNEXPECTED"
+ echo "valid=false" >> $GITHUB_OUTPUT
+ else
+ echo "valid=true" >> $GITHUB_OUTPUT
+ fi
+
- name: Check for changes
id: changes
run: |
@@ -152,13 +281,13 @@ jobs:
fi
- name: Push branch
- if: steps.claude.outputs.success == 'true' && steps.changes.outputs.has-changes == 'true'
+ if: steps.final.outputs.success == 'true' && steps.validate.outputs.valid == 'true' && steps.changes.outputs.has-changes == 'true'
env:
BRANCH_NAME: ${{ steps.branch.outputs.branch }}
run: git push origin "$BRANCH_NAME"
- name: Create Pull Request
- if: steps.claude.outputs.success == 'true' && steps.changes.outputs.has-changes == 'true'
+ if: steps.final.outputs.success == 'true' && steps.validate.outputs.valid == 'true' && steps.changes.outputs.has-changes == 'true'
env:
GH_TOKEN: ${{ github.token }}
BRANCH_NAME: ${{ steps.branch.outputs.branch }}
@@ -187,7 +316,7 @@ jobs:
--base main
- name: Add job summary
- if: steps.claude.outputs.success == 'true' && steps.changes.outputs.has-changes == 'true'
+ if: steps.final.outputs.success == 'true' && steps.validate.outputs.valid == 'true' && steps.changes.outputs.has-changes == 'true'
env:
BRANCH_NAME: ${{ steps.branch.outputs.branch }}
run: |
@@ -202,7 +331,11 @@ jobs:
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: claude-output-${{ github.run_id }}
- path: claude-output.log
+ path: |
+ claude-update.log
+ claude-fix.log
+ build.log
+ test.log
retention-days: 7
- uses: SocketDev/socket-registry/.github/actions/cleanup-git-signing@6096b06b1790f411714c89c40f72aade2eeaab7c # main
diff --git a/package.json b/package.json
index 66682ddad..61bab4ef6 100644
--- a/package.json
+++ b/package.json
@@ -145,6 +145,7 @@
"aggregate-error": "catalog:",
"ansi-regex": "catalog:",
"brace-expansion": "catalog:",
+ "defu": ">=6.1.5",
"emoji-regex": "catalog:",
"es-define-property": "catalog:",
"es-set-tostringtag": "catalog:",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f04aa4446..788abd552 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -261,6 +261,7 @@ overrides:
aggregate-error: npm:@socketregistry/aggregate-error@^1.0.15
ansi-regex: 6.2.2
brace-expansion: 5.0.5
+ defu: '>=6.1.5'
emoji-regex: 10.6.0
es-define-property: npm:@socketregistry/es-define-property@^1.0.7
es-set-tostringtag: npm:@socketregistry/es-set-tostringtag@^1.0.10
@@ -2687,8 +2688,8 @@ packages:
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
engines: {node: '>=12'}
- defu@6.1.4:
- resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
+ defu@6.1.6:
+ resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==}
del-cli@6.0.0:
resolution: {integrity: sha512-9nitGV2W6KLFyya4qYt4+9AKQFL+c0Ehj5K7V7IwlxTc6RMCfQUGY9E9pLG6e8TQjtwXpuiWIGGZb3mfVxyZkw==}
@@ -6340,7 +6341,7 @@ snapshots:
define-lazy-prop@3.0.0: {}
- defu@6.1.4: {}
+ defu@6.1.6: {}
del-cli@6.0.0:
dependencies:
@@ -8018,7 +8019,7 @@ snapshots:
unconfig@7.4.1:
dependencies:
'@quansync/fs': 0.1.5
- defu: 6.1.4
+ defu: 6.1.6
jiti: 2.6.1
quansync: 0.2.11
unconfig-core: 7.4.1
@@ -8041,7 +8042,7 @@ snapshots:
unplugin-purge-polyfills@0.1.0:
dependencies:
- defu: 6.1.4
+ defu: 6.1.6
magic-string: 0.30.19
mlly: 1.8.0
unplugin: 2.3.10