Skip to content

feat: headless mode + bundled PR creation for CI - #6

Merged
gyulsbox merged 6 commits into
mainfrom
feat/headless-create-pr
May 22, 2026
Merged

feat: headless mode + bundled PR creation for CI#6
gyulsbox merged 6 commits into
mainfrom
feat/headless-create-pr

Conversation

@gyulsbox

@gyulsbox gyulsbox commented May 22, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the v0.5 deliverable from the PRD: promote scan and promote <id> can now run fully headless and open a single bundled PR with the applied memory changes — unlocking CI / GitHub Actions usage.

New flags

promote scan:

  • --no-interactive — disables all prompts (auto-enabled in CI or non-TTY)
  • --min-confidence <n> — confidence floor for auto-apply
  • --create-pr — open one bundled PR after applying
  • --base-branch <name> — override PR base
  • --allow-foreign-scan — decouple scanned repo from PR target (testing / upstream-tracking)

promote <id>:

  • --create-pr — open a PR for a single applied candidate
  • --base-branch <name>

How PR creation works

  • gh CLI primary, octokit fallback. Works on both GitHub-hosted runners (gh pre-installed) and self-hosted (falls through to REST API with GITHUB_TOKEN).
  • PR target = local origin, always. The scanned repo provides evidence; the PR lands in the user's local repo. Decoupling these enables the testing / mirror workflow via --allow-foreign-scan.
  • PULL_REQUEST_TEMPLATE.md aware. If the local repo has a template (.github/, root, or docs/), the draftingModel fills its section bodies while preserving checkboxes (- [ ]) verbatim and leaving unknown sections (Screenshots, etc.) untouched. A ## Memory promotion details section with raw evidence is always appended for verifiability.

Path change

Digest moves from .promote/digests/ to docs/promote/digests/ so it can be committed alongside the memory PR. This repo's .gitignore still blocks docs/promote/digests/ so we don't pollute the tool's own history while dogfooding.

Files touched

  • src/index.ts — flag registration + help text
  • src/cli/commands/scan.ts — headless detection, prompt-site gating, bundled PR flow
  • src/cli/commands/promote.tsapplyPromotion returns ApplyPromotionResult, single-candidate PR flow
  • src/cli/commands/init.ts — Next steps updated for headless / CI use
  • src/pr/branch.ts — deterministic branch names
  • src/pr/template.ts — PR body builder, template detection
  • src/pr/llm-fill.ts — LLM-driven template section filling
  • src/pr/create.ts — gh-first / octokit-fallback PR creator
  • examples/github-actions/weekly-digest.yml + README.md — ready-to-copy workflow

Tests

35/35 pass. New tests cover branch-name idempotency, PR-body generation across template-prefilled / standalone variants, and template-discovery across the 6 standard locations.

Test plan

  • pnpm typecheck clean
  • pnpm test passes
  • pnpm build clean
  • Manual dogfood: promote scan --no-interactive --create-pr --allow-foreign-scan against an external repo

Adds --no-interactive, --min-confidence, --create-pr, --base-branch,
--allow-foreign-scan to `promote scan`, and --create-pr/--base-branch to
`promote <id>`.

PR creation prefers gh CLI with octokit fallback (works on both
GitHub-hosted and self-hosted runners). When the repo has a
PULL_REQUEST_TEMPLATE.md, the LLM (draftingModel) fills its section
bodies while preserving checkboxes and unknown sections; the full
evidence breakdown is always appended as a "Memory promotion details"
section. PR target is always the local origin, never the scanned repo.

Digest path moves from .promote/digests/ to docs/promote/digests/ so it
can be committed alongside the memory PR for traceability.

Ships examples/github-actions/weekly-digest.yml + installation guide and
updates `promote init`'s Next steps to mention the new flows. 35 tests
pass (new ones cover branch-name idempotency and PR-body generation
across template-prefilled / standalone variants).
@gyulsbox
gyulsbox force-pushed the feat/headless-create-pr branch from 62e1fd6 to 06820e6 Compare May 22, 2026 16:32
gyulsbox added 5 commits May 23, 2026 01:50
  - detectDefaultBranch no longer falls back to current branch (was creating
    PRs against the previous promote/... or feature branch). Tries
    refs/remotes/origin/HEAD first, then 'gh repo view', defaults to 'main'.
  - gitStage uses update-ref + symbolic-ref + reset --mixed to create the
    promote branch from origin/<base> without disturbing the working tree,
    so applyPromotion's already-written files become a clean diff vs main.
  - gh path no longer passes --label to 'gh pr create' (hard-fails on
    missing labels). Labels are created best-effort with 'gh label create
    --force' then attached via 'gh pr edit --add-label', matching the
    Octokit path's best-effort behavior.
…fidence

Four follow-ups from dogfooding the headless cross-repo flow on trpc/trpc:

1. resolveTargetFile now ignores the LLM's suggestedFile unless it matches
   the configured layout. For `agents` the suggestion must be a known
   instruction file (AGENTS.md, CLAUDE.md, GEMINI.md, …); for
   path_scoped_rule it must sit under config.memoryTargets.pathScoped.preferredDir.
   Anything else falls back to the configured destination. Foreign scans
   were leaking scanned-repo paths (e.g. packages/server/src/.../*.ts) into
   the PR target repo; this guard prevents that.

2. isValidFilePath rejects glob characters (**, *, ?), path traversal
   (../), and absolute paths. Closes the "literal `**` directory in the
   committed path" bug.

3. run() now surfaces the child process's stderr/stdout in the thrown
   error message instead of just "Command failed". gh label/edit calls
   are wrapped per-step with explicit stderr warnings — previously both
   were silently swallowed, which hid label-creation failures on real
   runs.

4. confidence values are formatted via toFixed(2) in PR body and LLM
   facts. Eliminates artifacts like "confidence 0.9500000000000001"
   coming from the human-signal +0.05 bump.
… + tests

Addresses PR-review follow-ups (analysis items 1-6).

### 1+4. Atomic flow via prepareBranchForPr / finalizePr / rollbackBranch

Splits createPullRequest into composable phases:
- prepareBranchForPr: switches HEAD to a new branch cut from baseBranch using
  update-ref + symbolic-ref + reset --mixed, preserving the working tree.
- finalizePr: stages files, commits, pushes, opens PR (no branch switching).
- rollbackBranch: discards file mods, switches back, deletes the branch.
- restoreOriginalBranch: clean checkout back after PR success.

scan.ts headless --create-pr and promote.ts --create-pr now:
  prepareBranchForPr → applyPromotion → finalizePr
  → on success: updateCandidateStatus (atomic) + restoreOriginalBranch
  → on failure: rollbackBranch (working tree restored, branch deleted, DB untouched)

DB status flips are now gated on PR-creation success. If gh/octokit hiccups
mid-flow, the candidate stays as 'candidate' and the next scan can retry.

The legacy createPullRequest wrapper (used by interactive --create-pr) keeps
its apply-then-PR shape and intentionally does NOT rollback files on failure
— in that flow, file mods were user-confirmed and rolling back would destroy
their work.

### 2. updateCandidateStatus moved AFTER PR success (atomic gate)

Headless: applies all eligible → finalizePr → only then loop updateCandidateStatus.
Single-PR (promote <id> --create-pr): apply → finalizePr → updateCandidateStatus.

Interactive --create-pr keeps immediate status updates (each apply is explicit
user confirmation; PR is best-effort bonus).

### 3. needs_human_decision skips now surface in headless output

Previously skipped silently. Now: "N candidate(s) flagged needs_human_decision
— review locally with 'promote review'."

### 5. Unit tests for resolveTargetFile + formatConfidence

src/cli/commands/promote.test.ts: 13 cases covering known-agent acceptance,
foreign-path rejection, glob/traversal/absolute rejection, path_scoped_rule
config-derivation, adr numbering, test stub path.

src/pr/template.test.ts: 3 cases for formatConfidence (float precision,
trailing zeros, boundary rounding).

51 tests total, all green.

### 6. Workflow cache key simplification

examples/github-actions/weekly-digest.yml: key was promote-{repo}-{run_id}
which made every run a unique cache entry — restore-keys fallback worked
but the cache was never re-hit and entries piled up. Single repo-keyed
entry now updates monotonically across runs.
@gyulsbox
gyulsbox merged commit 23e6247 into main May 22, 2026
2 checks passed
@gyulsbox
gyulsbox deleted the feat/headless-create-pr branch May 23, 2026 13:10
gyulsbox added a commit that referenced this pull request May 23, 2026
feat: headless mode + bundled PR creation for CI
gyulsbox added a commit that referenced this pull request May 23, 2026
feat: headless mode + bundled PR creation for CI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant