Prompt file imported from zoolutions/dash-proxy (
.claude/commands/lfg.md). Fill in{{arguments}}before use. Copyright stays with the author.
LFG - Full Autonomous Workflow
Execute a complete engineering workflow with verification at each phase, respecting this repo's fork branch model.
Phase 0: Branch Setup
BEFORE any other work, prepare the git branch. main changes only through merged PRs. NEVER commit to it.
- Check the current branch:
git branch --show-current - If NOT on
main, switch:git checkout main - Sync with origin (do not assume your local
mainis current):git pull --ff-only origin main - Create feature branch off
main:git checkout -b feature/{description}(orfix/{description},issue-{number}-{brief-description}) - The branch merges forward into
mainat PR time — never rebase it once pushed. See.claude/rules/git-workflow.mdand.claude/rules/upstream-sync.md.
Phase 1: Understand
Step 1: Gather Requirements
If {{arguments}} is a GitHub issue number or URL:
gh issue view <number> --json title,body,labels,assignees,comments
If {{arguments}} is a description, use it directly.
Step 2: Define Acceptance Criteria
MANDATORY: Write explicit acceptance criteria:
- GIVEN [context/setup]
- WHEN [action taken]
- THEN [expected outcome]
You MUST NOT proceed until you can articulate these clearly.
Step 3: Comprehension Gate
Before proceeding, you must:
- State the problem/feature in one sentence
- Explain WHY this is needed (business context — check
ROADMAP.mdfor an existing anchor before inventing one) - List what will change from the user's perspective (CLI flag? RPC arg? deploy behavior?)
- Identify edge cases not explicitly mentioned
- Explain the data flow or code path involved, in terms of this repo's layers:
cmd/kamal-proxy→internal/cmd(cobra CLI + RPC client) → unix socket RPC →internal/server(Router, Service, LoadBalancer, cert managers)
If you cannot complete ALL five items, investigate further.
Step 4: Create Task List
Create a TaskCreate todo list with specific implementation steps.
Phase 2: Explore
- Find related files (Glob/Grep or Explore agent,
model: haiku) - Read existing patterns in similar features — e.g. how
ServiceOptions(internal/server/service.go:82) orTargetOptions(internal/server/target.go:65) added a prior knob - Understand dependencies and integration points across the three layers
- Check existing test coverage (
*_test.gonext to the file you're touching) - If touching RPC surface, review
internal/server/commands.go(RPC name registration + arg structs) and the ~9 client call sites ininternal/cmd/ - If touching persisted config, check
Service.MarshalJSON/UnmarshalJSON(internal/server/service.go:273/294) for round-trip/default-safety against old state files - Check
ROADMAP.mdfor a code anchor already scoped for this work
Phase 3: Plan
- List files to modify with specific changes
- List new files to create with purpose
- Identify flag/RPC changes needed:
internal/cmd/{deploy,run}.goflags →server.DeployArgs/server.GlobalConfig→ RPC arg structs ininternal/server/commands.go - Plan test coverage (TDD: tests FIRST) — table-driven
go test,testify/assert, follow patterns in existing*_test.go - Update task list with implementation steps
- Consider backwards compatibility: old state files (JSON-persisted
ServiceOptions/TargetOptions) must still load; never rename module/binary/RPC/socket away fromkamal-proxy
Phase 4: Implement (TDD)
The deviation log (keep it from the first edit)
The plan is the map; the codebase is the territory. The moment reality forces a choice the plan or issue didn't settle, log it in implementation-notes.md at the repo root — one line, at the moment it happens, not reconstructed later:
- Deviations — the plan said X, you did Y, because Z
- Discoveries — facts about the codebase the plan didn't know (an options struct that doesn't round-trip, an upstream-owned file in the path, a cert-manager collision with the other fork branch)
- Judgment calls — choices the user might have made differently (flag naming, defaults, scope cuts)
Pick the conservative option and keep going. The log is how the user audits your judgment afterwards. Never commit the file: its contents move into the PR body (Phase 7), then the file is deleted.
For each logical unit:
4.1: Write Failing Test First
Create a test that demonstrates the expected behavior. Run it to confirm it FAILS:
go test ./internal/server/... -run TestYourNewBehavior -v
4.2: Implement Minimum Code
Write the MINIMUM code to make the test pass. Follow project patterns:
| Never Do | Always Do |
|---|---|
Rename module/binary/RPC service/socket away from kamal-proxy |
Keep it load-bearing-identical (Dockerfile, kamal gem exec calls, RPC registration all depend on it) |
| Hand-roll RPC dialing | Reuse the net/rpc client pattern in internal/cmd/util.go |
Add a knob only to ServiceOptions and forget the flag |
Wire flag (internal/cmd/deploy.go or run.go) → arg struct (commands.go) → ServiceOptions/TargetOptions/DeploymentOptions |
| Skip JSON round-trip for new persisted fields | Update MarshalJSON/UnmarshalJSON and default old state files safely |
| Ignore the streaming/SSE bypass when touching response middleware | Check response_buffer_middleware.go:86 bypass logic first |
Edit Dockerfile, Makefile, or bin/release casually |
Release plumbing is load-bearing: bin/release's tag grammar and docker-publish.yml's tag filter must stay in step |
4.3: Refactor
Once green, refactor while keeping tests passing.
4.4: Validate
gofmt -l internal/ cmd/ # must print nothing — CI enforces formatting
go vet ./...
make lint (golangci-lint) is a real local gate — install the version ci.yml pins with go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3. gofmt -l alone does not catch what staticcheck does.
4.5: Repeat
Move to next logical unit. Mark task items complete.
Phase 5: Deep Root Cause Analysis (Bug Fixes Only)
If this is a bug fix, apply deep investigation before implementing:
Trace the Request Lifecycle
For the request/connection causing the issue:
- Where did it enter —
internal/server/server.golistener, thenRouter, thenService, thenLoadBalancer.StartRequest(load_balancer.go:174), thenTarget.createProxyHandler(target.go:293)? - What timeout/deadline applies at that point (
ResponseHeaderTimeoutattarget.go:302, health check intervals, cert renewal windows)? - What ASSUMPTIONS does the code make at the failure point?
- Which assumption was violated, and WHY?
Use Git History
git log --oneline -20 <file>
git blame <file>
- When was the code written — upstream, or one of the fork's cert branches (
san-certificate-batching,wildcard-certs)? - Has a later
mainmerge changed an assumption the fork code relied on? Check.claude/rules/upstream-sync.md's conflict playbook for this file.
Map All Callers
Don't just look at the method that failed:
- Use Grep to find all call sites (RPC client in
internal/cmd/, RPC server ininternal/server/commands.go, direct calls withininternal/server/) - Different contexts (CLI deploy path vs RPC server vs test harness in
internal/server/testing.go)? - Does the error only happen in ONE context? Why?
Five Whys
Keep asking WHY until you reach a meaningful fix point:
- Error: X happened -> Why?
- Because Y -> Why was Y in that state?
- Because Z -> Why wasn't Z prevented?
- Because no check existed -> Why not?
- THIS is where the fix belongs
Fix Location Principle
The best fix is usually NOT where the error is raised:
- Nil target in load balancer -> fix in
Servicethat should never register a nil target - Certificate not found -> fix in the manager that should ensure provisioning before serving
- Race condition -> fix at the state-file / mutex boundary, not with a retry loop
- RPC arg mismatch -> fix the arg struct/version contract in
commands.go, not the symptom at the call site
Ask: "Where is the EARLIEST point I could prevent this error?" Fix there.
Unacceptable Superficial Fixes -- DO NOT DO THESE
if err != nil { return nil }swallowing an error without understanding why it occurs- Ignoring an error return (
_ = fn()) to silence a failure path - Nil-checking a pointer defensively without understanding why it could be nil
- Wrapping goroutines in blanket
recover()to hide panics - Increasing a timeout to mask a deadlock/race instead of fixing it
These HIDE bugs. The root cause continues causing issues elsewhere.
Phase 6: Verify
ALL of these must pass before committing:
gofmt -l internal/ cmd/ # Formatting — must be empty
make test # go test ./...
go vet ./...
If you have golangci-lint installed locally, also run make lint — but its absence is not a blocker; CI runs it on main.
Solution Verification
Re-read the original requirements and verify:
- "If I were the requester, would I consider this fully resolved?"
- "Have I addressed the ROOT CAUSE, not just the symptom?"
- "Do my tests prove the issue is ACTUALLY fixed, not just suppressed?"
- "Does this maintain backwards compatibility with existing state files and the
kamalgem's RPC/CLI expectations?"
Phase 6.5: Fable validation
Spawn the fable-validator agent (it is pinned to Fable) with the issue, the acceptance criteria from Phase 1 and the base branch. On BLOCK, fix every blocker (back to Phase 4 for code, with a failing test first), re-verify, and run the validator again. On PASS WITH NOTES, fix the risks you agree with (if those fixes change the diff, re-verify and run the validator again) and list the rest in the pull request under "Accepted risks". Put the validator's one-line verdict and its "Not verified" list in the pull request body. Do not open the pull request before a PASS or PASS WITH NOTES.
Phase 7: Commit & PR
Commit
git add <specific_files>
git commit -m "$(cat <<'EOF'
feat(scope): brief description
## Summary
[What changed and why]
## Test Coverage
- TestX: validates requirement X
- TestY: validates edge case Y
## Verification
- [x] gofmt -l internal/ cmd/ clean
- [x] make test passes
EOF
)"
Scope = the package/feature area, e.g. san-cert, wildcard-certs, router, rpc. See .claude/rules/git-workflow.md for commit conventions.
Push & PR
PRs target main — nothing is pushed to main directly.
git push -u origin $(git branch --show-current)
gh pr create --base main --title "feat(scope): brief description" --body "$(cat <<'EOF'
## Summary
- Key change 1 touching `internal/server/foo.go`
- Key change 2
Closes #<issue_number>
## Fable validation
<the validator's one-line verdict, and its "Not verified" list>
## Accepted risks
<risks the validator raised that were not fixed, and why; or "None">
## Test plan
- [ ] Scenario 1
- [ ] Scenario 2
EOF
)"
Markdown inside the quoted heredoc is literal — do not escape. The single-quoted <<'EOF' delimiter disables shell expansion on the body, so:
- Write backticks as backticks:
`foo`. Do NOT write\foo``; that writes a literal backslash-backtick and breaks the code span. - Write dollar signs as-is:
$HOME. No escaping needed. - Write backslashes as-is:
\nstays\n.
The body is copied verbatim into the PR / commit message. If you would not type a backslash in a GitHub comment, do not type one in the heredoc.
If the body is long or contains many backticks / tables, prefer writing it to a temp file and passing --body-file:
cat > /tmp/pr-body.md << 'EOF'
## Summary
...any markdown...
EOF
gh pr create --base main --title "..." --body-file /tmp/pr-body.md
rm /tmp/pr-body.md
The --body-file path avoids the double-layer of shell interpretation entirely and makes long PR bodies easier to read in the terminal buffer.
The PR body MUST end with a ## Deviations & judgment calls section copied from
implementation-notes.md (then delete the file). If the plan held completely,
write "None — the plan held." This section is read FIRST in review — it is the
audit trail for every decision the plan didn't make.
Release (only if this workflow ends in a release)
Not part of the default flow — only after a PR is merged to main and a release is explicitly requested. Full runbook: .claude/rules/git-workflow.md.
git checkout main && git pull --ff-only origin main
bin/release # works out the next vX.Y.Z.N, tests, tags, pushes, publishes the release
# CI publishes ghcr.io/zoolutions/dash-proxy:v1.0.0.0 (+ :latest)
docker buildx imagetools inspect ghcr.io/zoolutions/dash-proxy:v1.0.0.0 # verify amd64+arm64
The image has no version command — the tag IS the version. Release the proxy before the dash gem; the gem's MINIMUM_VERSION must name an already-published tag.
Phase 8: Comprehension Close-Out
The tests prove the CODE is right; this phase keeps the USER's mental model right. After the PR is up, end your final message with:
- The decisions, not the diff — the 3–5 non-obvious choices in this change someone must understand to maintain it. Lead with anything from the deviation log; the user has never seen those.
- Three merge-gate questions the user should be able to answer before merging. If any answer isn't obvious to them, offer a walkthrough — an unanswerable question is comprehension debt, and merging anyway is how it compounds.
Verification Checklist
- All acceptance criteria met
- Tests written BEFORE implementation
-
gofmt -l internal/ cmd/clean -
make testpasses -
go vet ./...clean -
fable-validatorreturned PASS or PASS WITH NOTES; its verdict and "Not verified" list are in the PR body - Backwards compatibility maintained (state files, RPC contract,
kamal-proxynaming untouched) - Branch rooted off
main, PR opened againstmain - PR created with description
- PR body ends with
## Deviations & judgment calls(from implementation-notes.md, since deleted) - Comprehension close-out delivered (decisions + three merge-gate questions)
Handoff
When complete:
- All phases executed
- Verification passed
- PR created against
mainand linked
Now, execute this workflow for the provided issue or feature.
