Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"enabledPlugins": {
"beads@beads-marketplace": true
},
"hooks": {
"SessionStart": [
{
"hooks": [
{
"command": "bd prime --hook-json",
"type": "command"
}
],
"matcher": ""
}
]
}
}
239 changes: 239 additions & 0 deletions .github/workflows/upstream-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
name: Upstream Sync

# Software factory: watch voska/qbo-cli for new commits, merge them into a
# sync branch, run the quality gates, and open a PR for human review.
# Clean merges that pass the gates open a ready PR directly. Conflicts or
# test failures are handed to Claude Code in CI to resolve; its result is
# re-verified deterministically before a PR is opened. Human merge is the
# ship gate — nothing lands on main automatically.

on:
schedule:
- cron: "0 12 * * 1-5" # weekdays ~06:00 America/Chicago
workflow_dispatch:

permissions:
contents: write
pull-requests: write
issues: write

concurrency:
group: upstream-sync
cancel-in-progress: false

env:
UPSTREAM_REPO: https://github.com/voska/qbo-cli.git
REVIEWER: bcarlso-lt

jobs:
sync:
runs-on: ubuntu-latest
outputs:
status: ${{ steps.status.outputs.status }}
branch: ${{ steps.detect.outputs.branch }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Detect upstream changes
id: detect
run: |
git remote add upstream "$UPSTREAM_REPO"
git fetch upstream main
count=$(git rev-list --count HEAD..upstream/main)
echo "count=$count" >> "$GITHUB_OUTPUT"
# key the branch on the upstream SHA, not the date: idempotent per
# upstream state and immune to same-day rerun collisions
echo "branch=sync/upstream-$(git rev-parse --short upstream/main)" >> "$GITHUB_OUTPUT"
echo "Upstream commits not in main: $count"

- name: Check for existing sync PR
id: existing
if: steps.detect.outputs.count != '0'
env:
GH_TOKEN: ${{ github.token }}
run: |
open=$(gh pr list --state open --json headRefName \
--jq '[.[] | select(.headRefName | startswith("sync/upstream-"))] | length')
echo "open=$open" >> "$GITHUB_OUTPUT"
[ "$open" != "0" ] && echo "A sync PR is already open; skipping." || true

- name: Attempt merge
id: merge
if: steps.detect.outputs.count != '0' && steps.existing.outputs.open == '0'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "${{ steps.detect.outputs.branch }}"
if git merge --no-edit upstream/main; then
echo "merged=true" >> "$GITHUB_OUTPUT"
else
git merge --abort
echo "merged=false" >> "$GITHUB_OUTPUT"
fi

- uses: actions/setup-go@v6
if: steps.merge.outputs.merged == 'true'
with:
go-version: "1.25"

- name: Quality gates
id: gates
if: steps.merge.outputs.merged == 'true'
continue-on-error: true
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b /usr/local/bin v2.11.4
make build
make test
make vet
make lint

- name: Resolve status
id: status
run: |
if [ "${{ steps.detect.outputs.count }}" = "0" ]; then
echo "status=up-to-date" >> "$GITHUB_OUTPUT"
elif [ "${{ steps.existing.outputs.open }}" != "0" ]; then
echo "status=pr-exists" >> "$GITHUB_OUTPUT"
elif [ "${{ steps.merge.outputs.merged }}" = "true" ] && [ "${{ steps.gates.outcome }}" = "success" ]; then
echo "status=clean-green" >> "$GITHUB_OUTPUT"
else
echo "status=needs-claude" >> "$GITHUB_OUTPUT"
fi

- name: Push branch and open PR
if: steps.status.outputs.status == 'clean-green'
env:
GH_TOKEN: ${{ github.token }}
run: |
# --force: the SHA-keyed branch may linger from an earlier failed run
git push --force origin "${{ steps.detect.outputs.branch }}"
{
echo "Automated upstream sync: merges \`voska/qbo-cli\` main into ours."
echo
echo "Merge was clean and \`make build\`, \`make test\`, \`make vet\`, \`make lint\` all passed in this workflow."
echo
echo "## Upstream commits"
echo
git log --oneline origin/main..upstream/main | sed 's/^/- /'
} > /tmp/pr-body.md
gh pr create \
--title "sync: merge upstream $(date -u +%Y-%m-%d)" \
--body-file /tmp/pr-body.md \
--base main \
--head "${{ steps.detect.outputs.branch }}" \
--reviewer "$REVIEWER"

claude-resolve:
needs: sync
if: needs.sync.outputs.status == 'needs-claude'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write # fetch GitHub OIDC token for Anthropic identity federation
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
# No ambient credentials while Claude runs: "do not push" is enforced
# by mechanism, not just prompt. The push step re-authenticates itself.
persist-credentials: false

- uses: actions/setup-go@v6
with:
go-version: "1.25"

- name: Prepare sync branch
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b /usr/local/bin v2.11.4
git remote add upstream "$UPSTREAM_REPO"
git fetch upstream main
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b "${{ needs.sync.outputs.branch }}"

- name: Claude resolves the merge
uses: anthropics/claude-code-action@v1
with:
# Anthropic workload identity federation — no API key secret needed.
# These are non-sensitive identifiers from the Anthropic Console
# (Organization settings → identity federation).
anthropic_organization_id: 63cc5434-820d-401a-b6c7-9a1e65133921
anthropic_service_account_id: svac_01VsbTypvfaU2JjdxqTALKQG
anthropic_federation_rule_id: fdrl_013FbzWdgBJhvuJutsQhDSXW
claude_args: '--allowedTools "Bash(git:*),Bash(make:*),Bash(go:*),Read,Edit,Write,Grep,Glob"'
prompt: |
You are on branch ${{ needs.sync.outputs.branch }} of a fork of voska/qbo-cli.
The remote `upstream` is already fetched. Merge `upstream/main` into this
branch. A plain `git merge` either conflicts or breaks the quality gates —
your job is to produce a correct, green merge.

Rules for resolving conflicts:
- This fork carries Lean Techniques customizations that MUST be preserved:
`internal/entra/`, `internal/vault/`, the bootstrap.json client-creds
tier in `internal/auth/` and `internal/config/`, and the bouncer-mode
login flow for hosted redirect URIs.
- For everything else, prefer upstream's version unless it breaks the
customizations above.
- After resolving, run `make build`, `make test`, `make vet`, and
`make lint` and fix any failures until all four pass.
- Commit the merge (and any follow-up fixes) with clear messages that list
the files you resolved by hand.
- Do NOT push; the workflow pushes after verifying your work.

- name: Verify Claude's result
id: verify
continue-on-error: true
run: |
[ -z "$(git status --porcelain)" ] # fail on any uncommitted or untracked file
[ "$(git rev-list --count HEAD..upstream/main)" = "0" ] # upstream fully merged
git merge-base --is-ancestor origin/main HEAD # fork history preserved, not reset to upstream
test -d internal/entra && test -d internal/vault # LT customizations still present
make build
make test
make vet
make lint

- name: Push branch and open PR
if: steps.verify.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
run: |
# checkout ran with persist-credentials:false — authenticate this push
# explicitly; --force covers a leftover branch from an earlier run
git push --force \
"https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \
"HEAD:refs/heads/${{ needs.sync.outputs.branch }}"
{
echo "Automated upstream sync: merges \`voska/qbo-cli\` main into ours."
echo
echo "> [!IMPORTANT]"
echo "> The plain merge conflicted or failed the quality gates, so **Claude"
echo "> Code resolved it in CI**. Gates were re-verified deterministically"
echo "> afterward, but review the resolution commits carefully."
echo
echo "## Merged upstream commits"
echo
git log --oneline origin/main..upstream/main | sed 's/^/- /'
} > /tmp/pr-body.md
gh pr create \
--title "sync: merge upstream $(date -u +%Y-%m-%d) (Claude-resolved)" \
--body-file /tmp/pr-body.md \
--base main \
--head "${{ needs.sync.outputs.branch }}" \
--reviewer "$REVIEWER"

- name: File issue on failure
# always(): must fire even when the Claude step itself failed (which
# would otherwise skip this step via the implicit success() guard)
if: always() && steps.verify.outcome != 'success'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh issue create \
--title "Upstream sync needs manual resolution ($(date -u +%Y-%m-%d))" \
--body "The scheduled upstream sync could not be completed automatically: the plain merge of \`upstream/main\` was dirty and Claude's resolution did not verify green. Resolve manually: \`git fetch upstream && git merge upstream/main\`, fix conflicts/tests, and open a PR. Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--assignee "$REVIEWER"
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@ dist/
*.out
.DS_Store
mise.toml

# Beads / Dolt files (added by bd init)
.dolt/
*.db
.beads-credential-key
.beads/proxieddb/

# Claude Code local/runtime state
.claude/settings.local.json
.claude/scheduled_tasks.lock
59 changes: 58 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ CLI for humans and AI agents. Data goes to stdout (parseable), hints/progress to
## Build & Test

```bash
make build # Build to bin/qbo (uses CGO_ENABLED=0 for pure Go)
make build # Build to bin/qbo (needs cgo: macOS Keychain backend is cgo-based)
make test # Run tests with race detector
make lint # golangci-lint
make vet # go vet
Expand Down Expand Up @@ -49,3 +49,60 @@ make fmt # gofmt
## Commits

Conventional Commits (`feat:`, `fix:`, `chore:`) with imperative summary. Keep changesets focused.

<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:970c3bf2 -->
## Beads Issue Tracker

This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands.

### Quick Reference

```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work
```

### Rules

- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
- Run `bd prime` for detailed command reference and session close protocol
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files

**Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns.

## Agent Context Profiles

The managed Beads block is task-tracking guidance, not permission to override repository, user, or orchestrator instructions.

- **Conservative (default)**: Use `bd` for task tracking. Do not run git commits, git pushes, or Dolt remote sync unless explicitly asked. At handoff, report changed files, validation, and suggested next commands.
- **Minimal**: Keep tool instruction files as pointers to `bd prime`; use the same conservative git policy unless active instructions say otherwise.
- **Team-maintainer**: Only when the repository explicitly opts in, agents may close beads, run quality gates, commit, and push as part of session close. A current "do not commit" or "do not push" instruction still wins.

## Session Completion

This protocol applies when ending a Beads implementation workflow. It is subordinate to explicit user, repository, and orchestrator instructions.

1. **File issues for remaining work** - Create beads for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **Handle git/sync by active profile**:
```bash
# Conservative/minimal/default: report status and proposed commands; wait for approval.
git status

# Team-maintainer opt-in only, unless current instructions forbid it:
git pull --rebase
bd dolt push
git push
git status
```
5. **Hand off** - Summarize changes, validation, issue status, and any blocked sync/commit/push step

**Critical rules:**
- Explicit user or orchestrator instructions override this Beads block.
- Do not commit or push without clear authority from the active profile or the current user request.
- If a required sync or push is blocked, stop and report the exact command and error.
<!-- END BEADS INTEGRATION -->

8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ go 1.25.0

require (
github.com/99designs/keyring v1.2.2
github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0
github.com/alecthomas/kong v1.15.0
github.com/muesli/termenv v0.16.0
golang.org/x/oauth2 v0.36.0
golang.org/x/term v0.44.0
)

require (
Expand All @@ -15,11 +17,15 @@ require (
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/dvsekhvalnov/jose2go v1.8.0 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
)
Loading