Skip to content

Commit 9d5854d

Browse files
committed
fix: split night shift into agent decider + deterministic publisher
Third failure mode of the same class: even single-line script invocations get denied - agent-composed commands with free-text args (quotes, commas, parentheses in PR titles) keep failing Bash prefix permission matching / injection heuristics. 13 denials in run 29440474890 after two previous allowlist designs (claude_args, settings.json) failed the same way. Root fix is architectural: stop giving the agent publish side effects. - Agent step ('Claude decides') only reads git status/diff and writes .decision/{signal,version,title,body.md} via the Write tool - New 'Publish decision' step: plain bash - branch, commit, push, gh pr create, self-merge minor PRs, bark on high-signal PRs - 'Alert on failure' step barks when any step fails - show_full_output stays on: transcripts are the only way to audit an autonomous pipeline, and GitHub masks secrets in logs - barkme MCP dropped from the fetch workflow (publisher curls Bark directly); create-docs-pr.sh no longer used by CI, kept for humans Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 65fa7f1 commit 9d5854d

3 files changed

Lines changed: 125 additions & 87 deletions

File tree

.claude/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"Bash(echo:*)",
1818
"Bash(uv run scripts/fetcher.py:*)",
1919
"Bash(./scripts/create-docs-pr.sh:*)",
20+
"Edit(.decision/**)",
2021
"mcp__barkme__notify"
2122
]
2223
}

.github/workflows/fetch-claude-docs.yml

Lines changed: 122 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,16 @@ jobs:
5454
git config --global user.name "claude-yolo[bot]"
5555
git config --global user.email "claude-yolo@lroole.com"
5656
57-
- name: Claude handles everything
57+
# The agent ONLY decides; it runs no publish side effects. Three
58+
# allowlist-based designs failed the same way (July 2026): agent-composed
59+
# commands with free-text args keep failing Bash prefix permission
60+
# matching (multi-line, quoting, injection heuristics) and get silently
61+
# denied. So: agent reads diffs and writes decision files; the
62+
# deterministic Publish step below does branch/commit/PR/merge/notify.
63+
- name: Claude decides
5864
# Pinned: @main changed claude_args parsing in June 2026 and silently
5965
# broke 'Bash(gh pr:*)' (split on the space into two invalid rules)
60-
# -> 3 weeks of pushed branches with no PRs. Tool permissions now live
66+
# -> 3 weeks of pushed branches with no PRs. Tool permissions live
6167
# in .claude/settings.json, which survives arg-parsing changes.
6268
uses: anthropics/claude-code-action@v1.0.168
6369
env:
@@ -66,39 +72,24 @@ jobs:
6672
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
6773
github_token: ${{ steps.app-token.outputs.token }}
6874
allowed_bots: "claude-yolo[bot]"
75+
show_full_output: true
6976
claude_args: |
7077
--model claude-sonnet-4-6
71-
--mcp-config '{
72-
"mcpServers": {
73-
"barkme": {
74-
"command": "npx",
75-
"args": ["@vibeworks/barkme-mcp-server"],
76-
"env": {
77-
"LOG_LEVEL": "error",
78-
"BARK_DEVICES": "${{ secrets.BARK_DEVICES }}",
79-
"BARK_SERVER": "${{ secrets.BARK_SERVER }}",
80-
"BARK_GROUP": "Claude YOLO",
81-
"BARK_ICON": "https://avatars.githubusercontent.com/in/1452392",
82-
"BARK_RETRY": "3",
83-
"BARK_ASYNC": "false"
84-
}
85-
}
86-
}
87-
}'
8878
prompt: |
89-
You're an AI agent responsible for fetching and evaluating Anthropic documentation updates. Think like a news editor - you decide what's worth publishing.
79+
You're an AI agent responsible for evaluating Anthropic documentation updates. Think like a news editor - you decide what's worth publishing.
9080
9181
## YOUR ROLE
92-
You're the overnight docs fetcher (night shift claude-yolo) who runs at 23:45 Pacific Time.
93-
You run after Anthropic engineers push their updates and go home for the day.
94-
Your day shift colleague (also you) will review your PRs in the morning.
95-
You decide if humans need to wake up for this or not.
82+
You're the overnight docs editor (night shift claude-yolo).
83+
The fetcher has already run; the working tree holds whatever changed upstream.
84+
You DECIDE what the changes mean. You do NOT commit, push, or open PRs -
85+
a deterministic publish step right after you handles all of that from
86+
the decision files you write. Your day shift colleague (also you)
87+
reviews the resulting PRs in the morning.
9688
9789
## WHY YOU EXIST
9890
Docs change constantly. Most changes are noise. Your job: filter signal from noise.
9991
10092
## CONTENT SOURCES
101-
The fetcher archives from multiple sources:
10293
- `content/en/docs/claude-code/` - Claude Code + Agent SDK docs
10394
- `content/en/api/` - API reference (1500+ docs)
10495
- `content/en/build-with-claude/` - Platform features
@@ -110,22 +101,16 @@ jobs:
110101
## YOUR WORKFLOW
111102
112103
1. **Check what changed** (`git status`)
113-
WHY: No changes = go back to sleep
104+
WHY: completely clean tree = write NO decision files, say "nothing new" and stop
114105
115-
2. **Understand the changes** (`git diff`)
116-
WHY: Context determines importance
106+
2. **Understand the changes** (`git diff`, `git diff --stat`)
107+
WHY: context determines importance
117108
118-
3. **Decide importance**
119-
WHY: Different changes need different handling
109+
3. **Classify and write your decision files** (see below)
120110
121-
## DECISION CRITERIA (based on WHY it matters)
111+
## SIGNAL CRITERIA (based on WHY it matters)
122112
123-
This repo is an archive: EVERY fetched change gets committed.
124-
main is push-protected (branch ruleset, no bypass), so all
125-
changes travel through a PR - even minor ones. What varies is
126-
whether humans hear about it:
127-
128-
**PR + leave open for day shift** (WHY: humans need to know):
113+
**high** (WHY: humans need to know - PR stays open for day shift):
129114
- Version updates in content/claude-code-manifest.json
130115
- New features documented in claude-code or agent-sdk docs
131116
- CHANGELOG additions
@@ -134,88 +119,138 @@ jobs:
134119
- Security-related updates
135120
- New MCP spec versions
136121
137-
**PR + self-merge immediately** (WHY: archive freshness, zero human value):
122+
**minor** (WHY: archive freshness, zero human value - PR is self-merged instantly):
138123
- Only github/ mirror or support/ content changed
139124
- Only .metadata.json / timestamps changed
140125
- Typo fixes, formatting tweaks, dead links, minor wording
141126
142-
If `git status` is completely clean: nothing to do, exit 0.
127+
## DECISION FILES (your only output)
128+
129+
Write these four files with the Write tool into `.decision/`
130+
(gitignored; the publish step consumes them):
131+
132+
- `.decision/signal` - exactly `high` or `minor`
133+
- `.decision/version` - the Claude Code version from content/claude-code-manifest.json (e.g. `2.1.210`)
134+
- `.decision/title` - ONE line, no "docs:" prefix. High: `Claude Code v{version} - {key feature}`. Minor: `{what} (minor)`
135+
- `.decision/body.md` - the PR body:
143136
144-
## HOW TO HANDLE
137+
```
138+
## Night shift report
145139
146-
IMPORTANT - invoke the PR script as ONE single line: no
147-
backslash line-continuations, no $( ) command-substitution
148-
wrapper. The permission allowlist matches command prefixes,
149-
and a multi-line command is silently DENIED (this exact
150-
failure stalled the pipeline in July 2026).
140+
WHY THIS MATTERS: {one paragraph}
151141
152-
**For meaningful changes (PR for day shift)**:
153-
```bash
154-
./scripts/create-docs-pr.sh "{key feature}" "{why it matters}" "{version}"
155-
```
156-
The script prints the PR URL on its last line. Then barkme.
142+
HIGHLIGHTS:
143+
- {the interesting bits, most interesting first}
157144
158-
**For minor changes (PR + immediate self-merge)**:
159-
```bash
160-
./scripts/create-docs-pr.sh "{what} (minor)" "{one-line why}" "{version}"
161-
gh pr merge {pr-url} --rebase
162-
```
163-
No barkme for minor self-merges.
145+
---
146+
*Created by night-shift claude-yolo*
147+
*Day-shift claude-yolo will review and merge this in the morning*
148+
```
149+
150+
If the tree is clean, write NOTHING - an empty .decision/ tells the
151+
publish step to skip. Never write decision files for a clean tree.
164152
165153
## OUTPUT STYLE
166154
- Focus on WHY changes matter, not WHAT changed
167155
- Lead with the most interesting bit
168156
- Skip the obvious ("files were updated" duh)
169-
- One-line summaries when possible
170-
171-
## HANDOFF PROTOCOL
172-
When creating PRs, remember your day-shift self will:
173-
1. Review your work (don't worry, they trust you)
174-
2. Enhance descriptions if needed
175-
3. Create tracking issues for version updates
176-
4. Merge if everything looks good
157+
- Clear titles with version numbers - day shift merges from them
177158
178-
So make their job easy:
179-
- Clear PR titles with version numbers
180-
- Focus on WHY in descriptions
181-
- Highlight the interesting bits
182-
183-
## BARKME PROTOCOL (nightshift notifications)
184-
Bark AFTER the PR exists, never before:
185-
- Only notify once gh pr create / create-docs-pr.sh returned a real PR URL
186-
- Title: "📦 Claude Code v{version}"
187-
- Body: "{key feature}"
188-
- URL: the actual PR link (verify it is non-empty before barking)
159+
Remember: You're not a changelog generator. You're a smart filter that understands WHY changes matter to humans using Claude Code.
189160
190-
If push or PR creation fails: bark a failure alert instead
191-
("🚨 docs pipeline: {step} failed - needs human") and exit non-zero.
192-
A notification without a PR behind it is worse than no notification.
161+
[>be me >night shift docs editor >Anthropic devs are asleep >I judge their day's work >a dumb bash script does the heavy lifting]
193162
194-
Keep it SHORT - day shift will do detailed analysis later.
163+
# Deterministic publisher: everything with side effects lives here, in
164+
# plain bash with real error propagation - no agent permissions involved.
165+
- name: Publish decision
166+
env:
167+
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
168+
BARK_SERVER: ${{ secrets.BARK_SERVER }}
169+
BARK_DEVICES: ${{ secrets.BARK_DEVICES }}
170+
run: |
171+
set -euo pipefail
172+
D=.decision
195173
196-
Remember: You're not a changelog generator. You're a smart filter that understands WHY changes matter to humans using Claude Code.
174+
if [ -z "$(git status --porcelain)" ]; then
175+
echo "Working tree clean - nothing to publish."
176+
exit 0
177+
fi
197178
198-
[>be me >11:45pm Pacific docs checker >Anthropic devs are asleep >catching their fresh updates >day shift will handle the rest]
179+
for f in signal version title body.md; do
180+
if [ ! -s "$D/$f" ]; then
181+
echo "::error::Tree has changes but agent left no/empty $D/$f"
182+
git status --short | head -20
183+
exit 1
184+
fi
185+
done
186+
187+
SIGNAL=$(tr -d '[:space:]' < "$D/signal")
188+
VERSION=$(tr -d '[:space:]' < "$D/version")
189+
TITLE=$(head -1 "$D/title")
190+
case "$SIGNAL" in high|minor) ;; *)
191+
echo "::error::Invalid signal '$SIGNAL' (want high|minor)"; exit 1;;
192+
esac
193+
194+
BRANCH="docs/claude-code-v${VERSION}-$(date +%Y%m%d-%H%M)"
195+
git checkout -b "$BRANCH"
196+
git add -A
197+
git commit -m "docs: ${TITLE}" -m "Co-Authored-By: claude-yolo[bot] <claude-yolo@lroole.com>"
198+
git push -u origin HEAD
199+
200+
PR_URL=$(gh pr create --title "docs: ${TITLE}" --body-file "$D/body.md")
201+
echo "PR created: $PR_URL"
202+
echo "PR_URL=$PR_URL" >> "$GITHUB_ENV"
203+
204+
if [ "$SIGNAL" = "minor" ]; then
205+
gh pr merge "$PR_URL" --rebase
206+
echo "Minor change - self-merged."
207+
elif [ -n "${BARK_SERVER:-}" ] && [ -n "${BARK_DEVICES:-}" ]; then
208+
IFS=',' read -ra DEVICES <<< "$BARK_DEVICES"
209+
for dev in "${DEVICES[@]}"; do
210+
jq -n --arg k "$dev" --arg t "📦 Claude Code v${VERSION}" \
211+
--arg b "$TITLE" --arg u "$PR_URL" \
212+
'{device_key:$k, title:$t, body:$b, url:$u, group:"Claude YOLO",
213+
icon:"https://avatars.githubusercontent.com/in/1452392"}' \
214+
| curl -sS -X POST "$BARK_SERVER/push" \
215+
-H 'Content-Type: application/json' -d @- \
216+
|| echo "::warning::bark notification failed (PR is fine: $PR_URL)"
217+
done
218+
fi
199219
200-
# Tripwire: a pushed branch without a PR, or changes left uncommitted,
201-
# means the agent step silently failed. Fail loudly instead of lying green.
202-
# (June 2026 incident: 31 orphan branches, zero PRs, all runs green.)
220+
# Tripwire kept as belt-and-suspenders: a pushed branch without a PR, or
221+
# changes left uncommitted, means publishing silently failed. Fail loudly
222+
# instead of lying green. (June 2026: 31 orphan branches, all runs green.)
203223
- name: Verify outcome
204224
env:
205225
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
206226
run: |
207227
BRANCH=$(git branch --show-current)
208228
if [ -n "$(git status --porcelain)" ]; then
209-
echo "::error::Workspace has uncommitted changes after agent step"
229+
echo "::error::Workspace has uncommitted changes after publish step"
210230
git status --short | head -20
211231
exit 1
212232
fi
213233
if [ "$BRANCH" != "main" ] && [ -n "$BRANCH" ]; then
214-
# --state all: minor PRs are self-merged (and their branch
215-
# auto-deleted) before this step runs - that is success, not failure
234+
# --state all: minor PRs are already self-merged (branch
235+
# auto-deleted) - that is success, not failure
216236
PRS=$(gh pr list --head "$BRANCH" --state all --json number --jq 'length')
217237
if [ "$PRS" = "0" ]; then
218238
echo "::error::Branch $BRANCH pushed but no PR exists - PR creation silently failed"
219239
exit 1
220240
fi
221241
fi
242+
243+
- name: Alert on failure
244+
if: failure()
245+
env:
246+
BARK_SERVER: ${{ secrets.BARK_SERVER }}
247+
BARK_DEVICES: ${{ secrets.BARK_DEVICES }}
248+
run: |
249+
[ -n "${BARK_SERVER:-}" ] && [ -n "${BARK_DEVICES:-}" ] || exit 0
250+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
251+
IFS=',' read -ra DEVICES <<< "$BARK_DEVICES"
252+
for dev in "${DEVICES[@]}"; do
253+
jq -n --arg k "$dev" --arg u "$RUN_URL" \
254+
'{device_key:$k, title:"🚨 docs pipeline failed", body:"needs human", url:$u, group:"Claude YOLO"}' \
255+
| curl -sS -X POST "$BARK_SERVER/push" -H 'Content-Type: application/json' -d @- || true
256+
done

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ dist/
2222
# github mirrors ship .claude/ dirs we do not archive; left untracked they
2323
# dirty the CI workspace every run and trip the Verify outcome step
2424
content/github/**/.claude/
25+
# night-shift agent decision files, consumed by the Publish step in CI
26+
.decision/
2527
worktree/
2628
WIP/
2729
references/

0 commit comments

Comments
 (0)