Skills and rules for the NanoClaw host agent (Claude Code on Mac). Tile promotion, container management, staging checks, repo chain safety, and public sync.
97
97%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Standard lifecycle for a code change in this monorepo. Follows the fork chain defined in repo-chain.md: changes land on private first, then appropriate commits cherry-pick to public. Never push to main on either side.
main.repo-chain.md — never touch upstream repos, always use the --repo flag.git push -u origin <branch>
gh pr create --repo jbaruch/nanoclaw --base main \
--title "<type>(<scope>): <imperative summary>" \
--body "$(cat <<'EOF'
## Summary
<what + why; link issues if any>
## Test plan
- [x] <what you verified>
- [ ] <manual steps remaining>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"Copilot auto-reviews some PRs but not all. Always request explicitly.
Important gotcha: the REST endpoint POST /requested_reviewers silently drops bot reviewers — it returns 201 but requested_reviewers stays empty, and Copilot never posts a review. The correct path is the GraphQL requestReviews mutation with botIds:
# One-time: resolve the Copilot bot's node ID (BOT_kgDOCnlnWA at the time of writing).
# If unknown, fish it out of a past PR review:
# gh api graphql -f query='{ repository(owner:"jbaruch",name:"nanoclaw") {
# pullRequest(number: 45) { reviews(first:5) { nodes {
# author { login ... on Bot { id } } } } } } }'
PR_ID=$(gh api graphql -f query='{ repository(owner:"jbaruch",name:"nanoclaw") {
pullRequest(number: <N>) { id } } }' --jq .data.repository.pullRequest.id)
gh api graphql -f query='
mutation($prId: ID!, $botIds: [ID!]!) {
requestReviews(input: { pullRequestId: $prId, botIds: $botIds, union: true }) {
pullRequest { number }
}
}' -F prId="$PR_ID" -F 'botIds[]=BOT_kgDOCnlnWA'Verify with gh api repos/jbaruch/nanoclaw/pulls/<N> --jq '.requested_reviewers[].login' — you should see Copilot.
Don't rush. Let both complete:
gh pr checks <N> --repo jbaruch/nanoclaw
gh api repos/jbaruch/nanoclaw/pulls/<N>/reviews \
--jq '.[] | select(.user.login | contains("opilot")) | {state, body: .body[:120]}'
gh api repos/jbaruch/nanoclaw/pulls/<N>/comments \
--jq '.[] | {path, line, body: .body[:200]}'# Accepted:
gh api "repos/jbaruch/nanoclaw/pulls/<N>/comments/<COMMENT_ID>/replies" \
-X POST -f body="Fixed in <sha> — <what changed>."
# Declined:
gh api "repos/jbaruch/nanoclaw/pulls/<N>/comments/<COMMENT_ID>/replies" \
-X POST -f body="Declining — <reason: out of scope / intentional / conflicts with X>."Push fixes to the same branch. CI re-runs automatically.
Only when CI is green and all Copilot threads are replied to:
gh pr merge <N> --repo jbaruch/nanoclaw --merge --delete-branch
git checkout main
git pull --ff-only origin main
git branch -d <branch> # -d refuses if not merged — safety
git remote prune origin # clean stale remote-tracking refsNot every private commit goes public. Apply the scrub rules from repo-chain.md and scripts/sync-to-public.sh.
Skip the commit if it touches any of:
scripts/trakt-auth.py, scripts/audible-backup.sh — private integration scriptssrc/hubitat-listener.ts — private integrationsync_tripit, fetch_trakt_history, sessionize_*, audible_backup) in src/ipc.ts or container/agent-runner/src/ipc-mcp-stdio.tsgroups/main/, groups/telegram_*/ — personal group contentdocs/OPERATIONS.md — private ops notes.env, dist/, any secret — neverShip if the commit is a generic improvement (bug fix, feature, refactor, test) touching shared code.
The authoritative scrub list is in scripts/sync-to-public.sh (EXCLUDES array + the two in-file Python regex lists). Read it before shipping if uncertain.
Work in the public clone (default ~/Projects/nanoclaw-public):
cd ~/Projects/nanoclaw-public
git checkout main && git pull --ff-only origin main
git checkout -b <same-branch-name-as-private>
git cherry-pick <private-commit-sha> # or a rangeIf cherry-pick conflicts on private-only code: resolve by dropping those hunks — they must not reach public. Verify with:
git diff main...HEAD -- scripts/ src/ container/ | grep -E 'hubitat|sync_tripit|fetch_trakt|sessionize|audible_backup|trakt-auth' && echo "LEAK — fix before pushing" || echo "clean"Then re-run Phase A (steps 1–5) against jbaruch/nanoclaw-public — same PR template, same Copilot request, same CI/review cycle, same merge + cleanup. Replace every jbaruch/nanoclaw with jbaruch/nanoclaw-public in the gh commands.
Copilot reviews each repo independently. The private and public PR for "the same" change can get different comments — either because one got summoned and the other didn't, or because Copilot noticed something different the second time. Decision tree:
| Case | What to do |
|---|---|
| Public Copilot catches a real bug that private Copilot missed (or was never asked) | Fix on public as normal. Then push the same fix to private. If private is merged, a direct push to main is OK only when the diff is bit-for-bit identical to what public Copilot already approved. Otherwise open a tiny private PR. |
| Private Copilot caught something and you've already fixed it there, public Copilot asks about the same thing | Reply on public with a pointer to the private thread/commit: "Applied on private in <sha> — porting the same fix here in <sha>." |
| Public Copilot asks for something you explicitly declined on private | Reply with the private rationale verbatim, link the private thread. Don't re-litigate. |
| Public Copilot proposes something orthogonal (only relevant to public) | Handle locally. Don't propagate to private. |
Rule of thumb: the two repos must stay semantically identical on any file that isn't in the scrub list. If a Copilot-validated fix lands on one, it lands on both — otherwise the next sync-to-public.sh or update-from-public will surface the drift as a churn commit.
When you push the fix to private directly (rather than via PR), the commit message should reference the public PR and explain why the PR gate is being bypassed:
test: clarify TRIGGER_PATTERN comment — sync from nanoclaw-public#22
Copilot review on the public counterpart pointed out that the
test name/comment said "word boundary" but the actual gate is
`(?:^|\s)` — `\b` is satisfied at end-of-string regardless.
Applying the same wording here so the two repos don't drift.--repo. gh defaults to upstream (qwibitai) on forks and will leak. No exceptions.main on either side.qwibitai/* repos without explicit user permission.This skill is for per-change ship. When a batch of private-only improvements has accumulated and you want a bulk scrubbed export, use the sync-to-public skill instead — it runs the full rsync + scrub pipeline in one go. Don't mix: cherry-pick OR batch sync for a given set of changes, not both.