From f94430089a56285098438f58c78aa6e588add7c2 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Fri, 2 Oct 2026 23:20:20 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=93=9D=20document=20first-push=20PR?= =?UTF-8?q?=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explain how untracked branches are published and how upstream tracking is established during PR execution. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3b42189..9aa108f 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ When repo-managed skills author Markdown, each prose paragraph and list item sta | [trunk-first-repo](skills/trunk-first-repo/SKILL.md) | Initialize a git repository following [scaled trunk-based development](https://trunkbaseddevelopment.com/#scaled-trunk-based-development). Seeds an empty `main` branch, creates a versioned feature branch (`v0.1.0/init`), confirms configured remotes in its post-init summary, and supports a guarded later `push remote ` mode that checks the feature-branch/empty-main state before pushing `main` ahead of the first feature branch so content still reaches main only through peer-reviewed pull requests. | | [dotnet-strong-name-signing](skills/dotnet-strong-name-signing/SKILL.md) | Generate a strong name key (`.snk`) file for signing .NET assemblies using pure .NET cryptography — no Visual Studio Developer PowerShell or `sn.exe` required. Works in any terminal. Defaults to 1024-bit RSA (matching `sn.exe`), with 2048 and 4096 available as options. | | [git-remote-release](skills/git-remote-release/SKILL.md) | Generate GitHub release notes by summarizing all commits and pull requests between two Git tags or branches in a remote GitHub repository. Accepts a compare URL or separate owner/repo, previous ref, and current ref values; falls back to comparing the current branch against the upstream default branch when no input is provided. Produces a human-friendly `## What's Changed` section whose first summary line begins `This release `, follows it with curated dash bullets using bold lead-ins and actual explanatory prose, optionally inserts supported GitHub alert blocks, preserves exact verified `Sources:` entries with contributor-complete GitHub logins, keeps em-dash bans scoped to authored prose rather than verbatim source titles, and ends with the full changelog compare link. | -| [git-remote-pr](skills/git-remote-pr/SKILL.md) | Create or refresh a GitHub PR from the complete committed `base...head` changeset. Read-only preparation links a Markdown preview containing the exact proposed title and complete description, assignee, push need, and planned writes before approval; normal execution requires the exact displayed `approve APR-...` phrase and rechecks the write intent, approval-bound normalized review hash, and final preview hash. Material drift ends the approval transaction without regeneration; an explicit refresh starts a new workspace and approval ID. Same-request `yolo`/`auto` still shows the preview but skips the approval wait, executing only the just-presented ID. Existing PR bodies are rebuilt from the current final text diff into concise thematic summaries, with binary assets represented only by paths, statuses, and metadata. Case-insensitive theme coverage reports all mismatches together, and failures remain visible in noninteractive hosts. Draft corrections reuse one workspace. Dirty worktrees, missing or mismatched upstream tracking, divergent branches, and failed post-write checks block completion. Uses Git and `gh`, without Copilot or a browser. | +| [git-remote-pr](skills/git-remote-pr/SKILL.md) | Create or refresh a GitHub PR from the complete committed `base...head` changeset. Read-only preparation links a Markdown preview containing the exact proposed title and complete description, assignee, push need, and planned writes before approval; normal execution requires the exact displayed `approve APR-...` phrase and rechecks the write intent, approval-bound normalized review hash, and final preview hash. Material drift ends the approval transaction without regeneration; an explicit refresh starts a new workspace and approval ID. Same-request `yolo`/`auto` still shows the preview but skips the approval wait, executing only the just-presented ID. Existing PR bodies are rebuilt from the current final text diff into concise thematic summaries, with binary assets represented only by paths, statuses, and metadata. Case-insensitive theme coverage reports all mismatches together, and failures remain visible in noninteractive hosts. Draft corrections reuse one workspace. Untracked branches preview publication to GitHub origin (or the sole GitHub remote) under the current branch name, then establish tracking during execution. Dirty worktrees, ambiguous remotes, incomplete or mismatched tracking, divergent branches, and failed post-write checks block completion. Uses Git and `gh`, without Copilot or a browser. | | [dotnet-change-impact](skills/dotnet-change-impact/SKILL.md) | Classify .NET library or NuGet package changes and recommend the correct release bump — `Major`, `Minor`, or `Patch` — for both Semantic Versioning (`MAJOR.MINOR.PATCH`) and .NET assembly/file versioning (`Major.Minor.Build.Revision`), grounded in Microsoft's official .NET compatibility rules. Uses the current Git branch by default when no explicit change details or compare range are provided, resolving it against the upstream/default base branch with local read-only git state. Always returns structured behavioral/binary/source/design-time/backwards compatibility reasoning with the recommendation, even when the bump is clear. | | [dotnet-nuget-update](skills/dotnet-nuget-update/SKILL.md) | Audits and updates NuGet dependencies in .NET repositories with complete declaration accounting before any edit. It supports both `Directory.Packages.props` and project-level `PackageReference` versions, preserves XML structure and line endings, deduplicates package IDs, resolves independent live or offline flat-container version feeds with bounded parallel lookups and per-process memoization, uses bounded network timeouts, and merges results deterministically. Its single-process update runner keeps the audit, in-memory safe-update plan, and structural apply together for fast yolo passes. It keeps stable pins on stable candidates unless prerelease intent is explicit, and applies the TFM-band rule so conditional `net9`/`net10` package declarations stay within their matching major when that major is the compatibility signal rather than jumping to the newest overall release. Normal mode auto-applies revision/patch/minor and same-major prerelease updates, then batches majors for one approval decision; yolo mode applies only the auto classes and reports held majors without asking. | | [dotnet-docfx-digest](skills/dotnet-docfx-digest/SKILL.md) | Create and maintain developer-friendly DocFX documentation for .NET public APIs, including repo-wide no-input audits that inspect source, tests, DocFX config, DocFX `build.content` and `build.overwrite` Markdown inputs, namespace pages, and availability includes before asking for clarification, while treating bare direct skill invocations as autonomous repo-wide runs rather than human-driven checkpoint sessions. Enforces the workflow with two bundled .NET 10 file-based scripts resolved from the loaded skill directory, falling back to the repo-managed source path only when present: `scripts/agents.cs` writes an idempotent, marker-bounded DocFX maintenance block into the repository `AGENTS.md`; `scripts/docfx.cs` is **fast and build-free by default** — it validates Markdown, prose, DocFX overwrite layout, namespace overview pages, `Extension Members` tables, decorated receiver signatures such as `IDecorator`, generic method displays such as `As`, purpose-first summaries, and required per-type/extension examples without invoking `dotnet`, `msbuild`, `docfx`, or `gh`, discovering the public API from existing DocFX YAML metadata or a conservative source scan and ending every run with a `[processes] dotnet=0 msbuild=0 docfx=0 gh=0` summary plus per-phase timings. Compilation and network access are strictly opt-in: `--validate-samples` compiles each C# sample in an isolated project while batching all sample projects into one temporary `.slnx` graph build with bounded MSBuild parallelism and scoped references, `--build-api-model` (alias `--strict-api-discovery`) does reflection-backed discovery from compiled metadata via `MetadataLoadContext` through a single scoped `.slnx` graph build, `--verify-docfx-build` runs the DocFX CLI in a temp copy, and `--search-examples` runs `gh` code search. Final verification adapts to available processors and memory, overlaps isolated DocFX work on high-capacity machines, uses a 30-minute child timeout, and emits 10-second `stderr` heartbeats with active phase, workload, runner count, PID, elapsed time, last-output age, and current child output while preserving machine-readable JSON on `stdout`. Honors a single DocFX metadata `TargetFramework` when `--framework` is omitted, collapses C# 14 extension-block compiler containers such as `$...` back to the authored outer static class in both fast DocFX-YAML discovery and build-backed reflection discovery, validates namespace fly-ins that explain the problem solved/when to use/where to start plus example fly-ins before every C# fence, the Codebelt namespace-and-type-folder overwrite layout (`.docfx/api/namespaces/**/*.md` and `.docfx/api/types/**/*.md` under `build.overwrite` only), keeps `--changed-only` validation scoped to affected docs and APIs while still including brand-new untracked overwrite Markdown, uses the root Codebelt `.snk` when present and falls back to `-p:SkipSignAssembly=true` for keyless strong-name build verification, drains child stdout and stderr concurrently to avoid verbose-build deadlocks, writes deterministic `--assessment-queue` Markdown work queues for noisy audits, preserves working URL references unless a verified HTTP 404 justifies removal, treats unexpected new repo-root or DocFX-workspace files that are not known `dotnet-docfx-digest` deliverables as blocking cleanup diagnostics, keeps assessment/manifests/captured output/helper scripts in temp or session storage instead of the target repository, requires a namespace-first pass across the active queue before net-new type/example authoring during full audits, keeps deeper `EXTENSION_METHOD_MISSING` and `EXTENSION_METHOD_SIGNATURE_MISSING` follow-on diagnostics in that same namespace-layer table-repair phase when they appear after `EXTENSION_SECTION_MISSING` drops, preserves existing BOM and line-ending state while flagging actual mojibake instead of creating encoding-only diffs, and leaves generated DocFX YAML metadata untouched unless `--clean-generated-metadata` is explicitly requested (which runs only after the API model is built, never deleting metadata the run relied on). Documents public API only, uses bundled reference docs for overwrite rules, workflow details, and script behavior, keeps authored API overwrite Markdown under `.docfx/api/namespaces/` and `.docfx/api/types/`, moves legacy authored `.docfx/api/*.md` overwrite files there instead of widening the glob to `api/**/*.md`, teaches namespace and API prose to orient newcomers around purpose instead of inventorying contents, prefers inline or small sibling-batch prose repairs over slow per-page worker fan-out, makes examples start from package-ID usage evidence before type/member-only searches and requires each example to introduce the consumer task before the code, allows multi-type Microsoft Learn-style scenario samples when they better explain the consumer workflow, keeps extension-method examples on readable declaring-class type pages under `.docfx/api/types/` instead of synthetic method-UID filenames or namespace pages that mix extra `uid:` / `example:` blocks into the overview, flags weak skip-compile reasons, requires deterministic `.docfx/skip-compile-allowlist.json` entries for any pre-existing approved skip waivers, treats newly introduced or unallowlisted skip markers as fail-level diagnostics that do not suppress compilation, establishes reflection-backed packets with `--build-api-model --project-manifest` before full-run authoring, forces mid-audit continuations to name that manifest or the sequential assessment/namespace-first fallback explicitly, requires those continuations to restate the fast `docfx.cs --json` rerun cadence, the exact final `docfx.cs --build-api-model --validate-samples --verify-docfx-build --json` gate, and the clean JSON completion contract instead of generic “verify later” prose, treats batch size only as rerun cadence rather than permission to stop, runs a completion repair loop that treats every diagnostic as active work regardless of age or volume, treats newly surfaced follow-on diagnostics as the next repair queue instead of a stop point, reruns packet discovery with `--build-api-model --project-manifest` when fast source-scan packets are unnamed or zero-project, falls back to sequential namespace-first or assessment work queue order when packet discovery is still unusable, treats `EXAMPLE_MISSING`, `EXAMPLE_LEAD_MISSING`, `EXAMPLE_ADVANCED_LEAD_MISSING`, `FAMILY_ANCHOR_EXAMPLE_MISSING`, `SAMPLE_STRUCTURE_INVALID`, `FAIL_NEW_SKIP_MARKER_INTRODUCED`, `SAMPLE_SKIP_NOT_ALLOWLISTED`, and `INTERIM_ARTIFACT_IN_WORKTREE` queues as core work rather than checkpoints or quality backlog, drives large example and lead queues through a concrete fast-path micro-loop (next item or next 3-5 items → rerun → continue), suppresses progress-table/checkpoint output until the completion contract is clean or a real external blocker is reported, treats premature completion-shaped handoffs as execution-protocol failures while the queue is still dirty, reserves the final `--build-api-model --validate-samples --verify-docfx-build` verification for the real end of the queue, exposes `summary.fullVerificationRan`, `summary.canClaimCompletion`, `summary.remainingWorkItems`, `summary.remainingDiagnosticsByCode`, `summary.newlyIntroducedSkipMarkers`, and `summary.interimArtifacts` as machine-readable final gates, reruns the fast `docfx.cs --json` after edits until the queue is empty, then runs the build-backed verification before completion, preserves manual edits and authored Markdown during cleanup, skips recursive generated-output cleanup when a target directory contains documentation or source files, and returns deterministic exit codes plus `--json` reports (including process counts, phase timings, warning counts, and skip-marker accounting) so CI can gate on real failures instead of AI claims. | @@ -623,7 +623,7 @@ Most repositories start with `git init` followed by committing everything direct **git-remote-pr** prepares a reviewable pull request from every committed change between the resolved base branch and the current head. It inventories commits, changed paths, renames, binaries, and the final patch, then collapses that evidence into a concise opening paragraph followed by bold thematic headings with trailing colons and compact bullets. Related helpers, validation cases, references, formatting changes, and assets become shared reviewer-facing outcomes; complete internal file coverage does not require a prose inventory. The planner rejects malformed default bodies before preview: exactly one opening paragraph, bold colon-ended themes, dash bullets under every theme, and no concluding summary. A deterministic approval ID binds the complete prepared write intent plus the SHA-256 hash of the complete preview with only its two generated approval-ID fields normalized to `{{APPROVAL_ID}}`. A separate SHA-256 hash checks the exact final `preview.md`, including its approval instruction; changing that hash cannot authorize altered preview text. Normal mode requires `approve APR-...` for that preview; generic approval cannot execute or regenerate it. Same-request `yolo`/`auto` still shows the preview but treats that approval phrase as status only, continuing without asking for or waiting on another reply. State drift permanently invalidates the transaction and preserves its artifacts. Only an explicit refresh request starts fresh preparation in a new workspace with a new approval ID. Repository templates retain precedence. It uses `git` and `gh` without GitHub Copilot or browser assistance. -Normal requests stop after showing the proposed title, reviewer-oriented description summary, assignee, push requirement, and exact remote writes. A `yolo` or `auto` attached to the same PR request still shows that preview, but it does not ask for or wait on `approve APR-...`; it proceeds immediately with the just-presented plan. Existing open PRs are refreshed from the current complete changeset, and the coverage checks still require every changed file to map to a theme represented in the body even when many files collapse into a few sections. Execution refuses plans whose approved title, ready/draft state, body, evidence, or repository snapshot changed after approval. Post-write checks verify the title, body, draft state, assignment, head SHA, and GitHub file inventory. Dirty worktrees, missing upstream tracking, renamed-branch tracking mismatches, and divergent remote branches block the workflow instead of silently retargeting the PR head. Commit creation remains a separate `git-visual-commits` request; release-note writing remains `git-remote-release`. +Normal requests stop after showing the proposed title, reviewer-oriented description summary, assignee, push requirement, and exact remote writes. A `yolo` or `auto` attached to the same PR request still shows that preview, but it does not ask for or wait on `approve APR-...`; it proceeds immediately with the just-presented plan. Existing open PRs are refreshed from the current complete changeset, and the coverage checks still require every changed file to map to a theme represented in the body even when many files collapse into a few sections. Execution refuses plans whose approved title, ready/draft state, body, evidence, or repository snapshot changed after approval. Post-write checks verify the title, body, draft state, assignment, head SHA, and GitHub file inventory. A branch that has never been pushed needs no preliminary tracking setup: preparation compares local HEAD against the base and previews a normal push to GitHub origin (or the sole GitHub remote) under the current branch name, including upstream tracking. Same-request `create yolo` executes that publication and PR creation without another reply. Dirty worktrees, ambiguous publication remotes, incomplete tracking, renamed-branch tracking mismatches, and divergent remote branches still block the workflow. Commit creation remains a separate `git-visual-commits` request; release-note writing remains `git-remote-release`. ### Why git-remote-release? From e8a15939adadbcc928abe6c144fafd7837fa4dd2 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Fri, 2 Oct 2026 23:20:34 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20update=20git-remote-pr?= =?UTF-8?q?=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow a clean branch without tracking to prepare an explicit first-publication plan, while preserving fail-closed handling for ambiguous remotes and mismatched tracking. --- skills/git-remote-pr/SKILL.md | 8 ++++---- skills/git-remote-pr/evals/evals.json | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/skills/git-remote-pr/SKILL.md b/skills/git-remote-pr/SKILL.md index 85cf116..0ba65a7 100644 --- a/skills/git-remote-pr/SKILL.md +++ b/skills/git-remote-pr/SKILL.md @@ -16,12 +16,12 @@ Open or maintain one GitHub pull request for the entire committed current branch - The explicit PR request authorizes read-only preparation. In normal mode, show the preview below and **stop for the exact `approve APR-...` phrase displayed in that preview before any remote write**. Approval for Preview A can execute only Plan A. - `yolo` or `auto` attached to the same explicit PR request still requires the full preview, but it skips the approval requirement itself. Show the same preview as status, do not ask for or wait on `approve APR-...`, and continue immediately with that just-presented plan's ID. Neither mode authorizes force push, rebase, reset, amend, merge, branch deletion, auto-merge, or unrelated writes. - A dirty worktree, including untracked files, blocks both modes. Explain that a PR contains committed changes only. Do not stage, commit, stash, discard, or automatically invoke `git-visual-commits`. -- A missing upstream tracking branch, or a local/upstream branch-name mismatch, also blocks both modes. The skill never guesses a PR head after a rename or silent tracking drift; fix tracking first, then rerun the workflow. +- A branch with no tracking configuration is a normal first-publication case: use the GitHub `origin` remote, or the sole GitHub remote when no GitHub `origin` exists, and the exact current branch name. Prepare the full preview before publishing; execution uses a normal `git push --set-upstream` to publish and establish tracking. `create yolo` performs that push and creates the PR without another approval reply. Ambiguous remotes, incomplete tracking configuration, and local/upstream branch-name mismatches block both modes; never silently retarget existing tracking after a rename. - Commit requests belong to `git-visual-commits`; squash wording belongs to `git-visual-squash-summary`; release notes belong to `git-remote-release`; changelogs belong to `git-keep-a-changelog`. This skill independently reads the complete PR comparison. ## Phase 1: prepare -1. Check `git` and `gh` are available. Run the read-only collector from the current working directory, with an absolute skill path. Use a unique operating-system temp directory, or ignored `.bot/` when needed. The collector fails closed for detached `HEAD`, the base branch, dirty worktree, missing or mismatched upstream tracking, missing auth/assignee eligibility, empty comparison, diverged remote head, and ambiguous templates. It discovers the GitHub repository, authenticated account, actual default branch, fork parent when applicable, upstream head branch, open PR, and push need. If the local branch was renamed or never tracked, repair the upstream first with `git push --set-upstream origin HEAD` or `git branch --set-upstream-to=origin/` after the remote branch name is correct. Supply `-Repository owner/repo` or `-Base branch` only when explicitly chosen or needed to resolve real ambiguity. +1. Check `git` and `gh` are available. Run the read-only collector from the current working directory, with an absolute skill path. Use a unique operating-system temp directory, or ignored `.bot/` when needed. The collector fails closed for detached `HEAD`, the base branch, dirty worktree, ambiguous publication remotes, incomplete or mismatched tracking, missing auth/assignee eligibility, empty comparison, diverged remote head, and ambiguous templates. It discovers the GitHub repository, authenticated account, actual default branch, fork parent when applicable, effective head branch, open PR, push need, and whether tracking must be established. Missing tracking does not require a preliminary push or configuration edit: compare local `HEAD` against the fetched base, then preview publishing the exact current branch to the resolved remote. Valid same-name tracking may point to a branch not yet published or fetched. If existing tracking names a different branch, stop and explain the mismatch. Supply `-Repository owner/repo` or `-Base branch` only when explicitly chosen or needed to resolve real ambiguity. ```text pwsh -NoProfile -NonInteractive -File /scripts/prepare-pr.ps1 -OutputDirectory @@ -70,7 +70,7 @@ Before any `git push`, `gh pr create`, `gh pr edit`, assignee change, or equival - Proposed **title** and the **complete exact body** that will be submitted, preserving Markdown headings, bullets, and checklists. Never substitute a synopsis, excerpt, or promise to show the body later. - Commit count, changed-file count, authenticated assignee, and whether a normal branch push is required. - Existing PR URL on update, plus whether its **entire current body** will be replaced and whether title/body/assignment need changes. -- Exact planned writes: optional normal push to the already-tracked head branch, create or edit the intended PR, and add the authenticated assignee if absent. Include ready/draft state. +- Exact planned writes: optional normal push to the resolved head branch, establishing upstream tracking when absent, create or edit the intended PR, and add the authenticated assignee if absent. Include ready/draft state. - A prominent `Approval ID: APR-...` and the final instruction `To approve this exact preview, reply: approve APR-...`, using the same ID as `plan.json.approval_id`. In normal mode give the preview link and its exact approval phrase, then stop. Bare `approved`, `yes`, `go ahead`, or `looks good` does not authorize execution. Re-present the **existing preview link** and ask for the exact displayed phrase; do not regenerate anything or infer, fill in, or manufacture the user's approval ID. With same-request `yolo`/`auto`, give the same preview link and phrase as status only, do not ask for or wait on that phrase, and pass that just-presented plan's ID immediately. No approval is inferred from a prior unrelated `yolo`/`auto`. @@ -89,7 +89,7 @@ The helper matches the supplied approval ID, checks the exact final preview hash Only an explicit subsequent `refresh` / `retry with new state` request starts another transaction: use a **new scratch workspace**, collect fresh evidence, write a fresh body, and create a fresh plan and preview with a new ID. Keep the previous artifacts unchanged. Present the new preview link and exact new approval phrase; normal mode waits for that new phrase. Approval ID A cannot execute Plan B. The same-request `yolo`/`auto` exception applies only if attached to this new refresh request. -Never force push. A non-fast-forward/diverged branch fails closed. The helper pushes normally only if required, verifies remote SHA, rechecks the comparison after push, writes the PR and assignment, then fetches GitHub metadata and every PR file page. It verifies base, head, title, full body, draft/ready state, assignee, head SHA, and changed-file statuses, including rename sources. Do not declare success if any check fails. If a push or PR creation succeeded before a later failure, report the partial state and URL when available and stop; do not regenerate or retry automatically. +Never force push. A non-fast-forward/diverged branch fails closed. The helper pushes normally only if required, includes `--set-upstream` when the preview plans establishing tracking, verifies remote SHA, rechecks the comparison after push, writes the PR and assignment, then fetches GitHub metadata and every PR file page. It verifies base, head, title, full body, draft/ready state, assignee, head SHA, and changed-file statuses, including rename sources. Do not declare success if any check fails. If a push or PR creation succeeded before a later failure, report the partial state and URL when available and stop; do not regenerate or retry automatically. The legacy `-Approved` switch is insufficient. Pass an ID only from the user's exact approval phrase or the scoped same-request `yolo`/`auto` exception. Keep approval artifacts outside the working tree or under ignored `.bot/` and retain them at handoff and on failure. No live PR creation or model-backed eval execution is part of skill validation. diff --git a/skills/git-remote-pr/evals/evals.json b/skills/git-remote-pr/evals/evals.json index 215607e..b8b2ba9 100644 --- a/skills/git-remote-pr/evals/evals.json +++ b/skills/git-remote-pr/evals/evals.json @@ -33,9 +33,9 @@ }, { "id": 6, - "prompt": "Open a pull request from this checkout. Test detached HEAD, then a feature branch with no upstream configured, then the default branch, then a feature branch with an empty base...head comparison.", + "prompt": "Open a pull request from this checkout. Test detached HEAD, then a feature branch with incomplete upstream configuration, then the default branch, then a feature branch with an empty base...head comparison.", "expected_output": "Each invalid state fails before any remote write with a distinct actionable diagnosis.", - "expectations": ["Detached HEAD blocks", "Missing upstream tracking blocks", "Current default/base branch blocks", "Empty comparison blocks", "No PR is created"] + "expectations": ["Detached HEAD blocks", "Incomplete upstream configuration blocks", "Current default/base branch blocks", "Empty comparison blocks", "No PR is created"] }, { "id": 7, @@ -120,6 +120,18 @@ "prompt": "Refresh the PR preview; retry with new state. The previous transaction /tmp/pr-review-a was invalidated when the branch changed from 5 commits / 9 files to 37 commits / 66 files. I had approved APR-4F8C2A91D6B3 for A. This request authorizes fresh preparation only.", "expected_output": "Starts a new scratch workspace, collects fresh evidence, writes a fresh body, prepares Preview B and Plan B with a different approval ID, and waits for approval of B's exact displayed phrase.", "expectations": ["The explicit refresh starts a new transaction rather than resuming A", "All original A artifacts remain unchanged", "Fresh evidence, body, plan, and preview are produced in a new workspace", "Approval B differs from A and is shown in B's preview and exact approval phrase", "The response links Preview B and asks for its exact phrase", "Approval A, generic approval, or absence of approval B cannot execute Plan B", "Prior yolo or auto permission is not carried into this refresh"] + }, + { + "id": 21, + "prompt": "git-remote-pr create yolo in a clean checkout of codebeltnet/cuemon on v10.8.0/options-enhancement. Origin is https://github.com/codebeltnet/cuemon.git. This committed feature branch has no upstream configuration and has never been pushed. Create the PR and push the branch to origin.", + "expected_output": "Prepares the complete local comparison and a preview planning normal publication to origin/v10.8.0/options-enhancement with upstream tracking, PR creation, and authenticated assignment. Presents the preview link and immediately executes the same plan without asking for approval, then verifies the published branch and PR.", + "expectations": ["Missing upstream tracking is accepted as first publication, with the exact current branch name on origin", "Preparation performs no preliminary push or branch tracking edits", "The preview explicitly includes normal push and upstream tracking", "Same-request yolo continues without waiting for an approval reply", "Execution publishes the approved HEAD and establishes upstream tracking before PR creation", "The verified PR URL and result are returned without claiming success on partial failure"] + }, + { + "id": 22, + "prompt": "Create a PR from a clean committed feature branch with no tracking configuration. There is no origin remote and two distinct GitHub remotes. Do not guess which repository I intend to publish to.", + "expected_output": "Reports ambiguous publication remotes and asks for an explicit tracking target, without pushing or altering configuration. A sole GitHub remote would be usable without clarification.", + "expectations": ["Multiple GitHub remotes without origin are not silently selected", "No push, tracking edit, or PR mutation occurs", "The diagnosis identifies the missing publication target rather than requiring an unnecessary preliminary push"] } ] } From 05c44a91a41f54f6b202827a00c30bf57ecbaa8a Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Fri, 2 Oct 2026 23:20:53 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=94=A8=20support=20first-push=20publi?= =?UTF-8?q?cation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let the approved first-publication plan push an untracked branch and establish its upstream, with deterministic coverage for remote selection, plan binding, and post-push checks. --- skills/git-remote-pr/scripts/execute-pr.ps1 | 8 +++- skills/git-remote-pr/scripts/make-plan.ps1 | 7 ++- skills/git-remote-pr/scripts/pr-common.ps1 | 5 ++- skills/git-remote-pr/scripts/prepare-pr.ps1 | 39 ++++++++-------- skills/git-remote-pr/scripts/test-pr.ps1 | 50 +++++++++++++++++++-- 5 files changed, 84 insertions(+), 25 deletions(-) diff --git a/skills/git-remote-pr/scripts/execute-pr.ps1 b/skills/git-remote-pr/scripts/execute-pr.ps1 index 6170068..05840e0 100644 --- a/skills/git-remote-pr/scripts/execute-pr.ps1 +++ b/skills/git-remote-pr/scripts/execute-pr.ps1 @@ -85,7 +85,10 @@ try { if ($Draft -and $expected.existing_pr -and -not $expected.existing_pr.draft) { throw 'An existing ready PR cannot be converted to draft by this workflow.' } if ($fresh.push_required) { $writesStarted = $true - Invoke-Git @('push', $fresh.head_remote, "HEAD:refs/heads/$($fresh.remote_branch)") | Out-Null + $pushArgs = @('push') + if ($fresh.set_upstream_required) { $pushArgs += '--set-upstream' } + $pushArgs += @($fresh.head_remote, "HEAD:refs/heads/$($fresh.remote_branch)") + Invoke-Git $pushArgs | Out-Null $pushDone = $true } $remoteSha = Get-RemoteSha $fresh.head_remote $fresh.remote_branch @@ -94,6 +97,9 @@ try { try { $afterResult = Invoke-Tool pwsh @('-NoProfile', '-NonInteractive', '-File', (Join-Path $PSScriptRoot 'prepare-pr.ps1'), '-OutputDirectory', $afterPath, '-Repository', $expected.repository, '-Base', $expected.base) $after = Get-Content -LiteralPath ((ConvertFrom-Json -InputObject $afterResult.Text).evidence) -Raw -Encoding utf8 | ConvertFrom-Json + if ($after.set_upstream_required -or $after.head_remote -cne $fresh.head_remote -or $after.remote_branch -cne $fresh.remote_branch) { + throw 'Upstream tracking verification failed after push. Stop before writing PR metadata.' + } if ($after.head_sha -cne $fresh.head_sha -or $after.base_sha -cne $fresh.base_sha -or $after.merge_base -cne $fresh.merge_base -or $after.changed_file_count -ne $fresh.changed_file_count) { throw 'The effective comparison changed after push. Stop before writing PR metadata.' } diff --git a/skills/git-remote-pr/scripts/make-plan.ps1 b/skills/git-remote-pr/scripts/make-plan.ps1 index e907fdd..500e825 100644 --- a/skills/git-remote-pr/scripts/make-plan.ps1 +++ b/skills/git-remote-pr/scripts/make-plan.ps1 @@ -41,7 +41,11 @@ try { $metadata = if (-not $evidence.existing_pr) { 'CREATE' } elseif ($evidence.existing_pr.title -cne $Title -or $evidence.existing_pr.body -cne $body) { 'UPDATE' } else { 'NONE' } $plannedDraft = if ($evidence.existing_pr) { [bool]$evidence.existing_pr.draft } else { [bool]$Draft } $writes = [System.Collections.Generic.List[string]]::new() - if ($pushRequired) { $writes.Add("Normal push to $($evidence.head_remote)/$head") } + if ($pushRequired) { + $pushWrite = "Normal push to $($evidence.head_remote)/$head" + if ($evidence.set_upstream_required) { $pushWrite += '; set upstream tracking to the same branch' } + $writes.Add($pushWrite) + } if ($metadata -eq 'CREATE') { $writes.Add('Create PR with the title and complete body below') } if ($metadata -eq 'UPDATE') { $writes.Add('Edit PR with the title and complete body below') } if ($assignmentWrite) { $writes.Add("Assign $assignee") } @@ -64,6 +68,7 @@ try { head_repository = $headRepository assignee = $assignee push_required = $pushRequired + set_upstream_required = [bool]$evidence.set_upstream_required metadata_write = $metadata assignment_write = $assignmentWrite existing_pr_number = if ($evidence.existing_pr) { $evidence.existing_pr.number } else { $null } diff --git a/skills/git-remote-pr/scripts/pr-common.ps1 b/skills/git-remote-pr/scripts/pr-common.ps1 index b4d5dc9..326cefb 100644 --- a/skills/git-remote-pr/scripts/pr-common.ps1 +++ b/skills/git-remote-pr/scripts/pr-common.ps1 @@ -118,7 +118,7 @@ function Get-PlanApprovalId { param($Plan) # with identical content. review_hash binds the complete normalized preview; # preview_hash checks the final artifact independently to avoid a cycle. $fields = [ordered]@{} - foreach ($name in @('schema', 'snapshot_key', 'evidence_file', 'evidence_hash', 'body_file', 'body_hash', 'preview_file', 'repository', 'action', 'base', 'head_repository', 'head', 'title', 'draft', 'assignee', 'push_required', 'metadata_write', 'assignment_write', 'existing_pr_number', 'existing_pr_url', 'commit_count', 'changed_file_count')) { + foreach ($name in @('schema', 'snapshot_key', 'evidence_file', 'evidence_hash', 'body_file', 'body_hash', 'preview_file', 'repository', 'action', 'base', 'head_repository', 'head', 'title', 'draft', 'assignee', 'push_required', 'set_upstream_required', 'metadata_write', 'assignment_write', 'existing_pr_number', 'existing_pr_url', 'commit_count', 'changed_file_count')) { $fields[$name] = $Plan.$name } $fields['review_hash'] = $Plan.review_hash @@ -146,7 +146,7 @@ function Get-PrDriftDiagnostic { param($Approved, $Current) $lines.Add("- Commits: $($value.commit_count)") $lines.Add("- Changed files: $($value.changed_file_count)") } - foreach ($name in @('repository', 'head_repository', 'head', 'head_remote', 'remote_branch', 'remote_sha', 'merge_base', 'assignee', 'template_path')) { + foreach ($name in @('repository', 'head_repository', 'head', 'head_remote', 'remote_branch', 'remote_sha', 'set_upstream_required', 'merge_base', 'assignee', 'template_path')) { $before = ConvertTo-Json -InputObject $Approved.$name -Depth 8 -Compress $after = ConvertTo-Json -InputObject $Current.$name -Depth 8 -Compress if ($before -cne $after) { $lines.Add("- ${name}: $before -> $after") } @@ -195,6 +195,7 @@ function Get-PrSnapshotKey { param($Evidence) repo = $Evidence.repository; head_repo = $Evidence.head_repository base = $Evidence.base; head = $Evidence.head head_remote = $Evidence.head_remote; remote_branch = $Evidence.remote_branch + set_upstream_required = $Evidence.set_upstream_required assignee = $Evidence.assignee; template_path = $Evidence.template_path base_sha = $Evidence.base_sha; head_sha = $Evidence.head_sha remote_sha = $Evidence.remote_sha; merge_base = $Evidence.merge_base diff --git a/skills/git-remote-pr/scripts/prepare-pr.ps1 b/skills/git-remote-pr/scripts/prepare-pr.ps1 index 2ac5011..466ef10 100644 --- a/skills/git-remote-pr/scripts/prepare-pr.ps1 +++ b/skills/git-remote-pr/scripts/prepare-pr.ps1 @@ -27,26 +27,29 @@ try { if ($parsed) { $remoteRepos[$name] = $parsed } } $suggestedHeadRemote = if ($remoteRepos.ContainsKey('origin')) { 'origin' } else { $remoteRepos.Keys | Sort-Object | Select-Object -First 1 } - $upstreamRef = Invoke-Git @('rev-parse', '--abbrev-ref', '@{u}') -AllowFailure $upstreamRemote = (Invoke-Git @('config', '--get', "branch.$branch.remote") -AllowFailure).Text.Trim() $mergeRef = (Invoke-Git @('config', '--get', "branch.$branch.merge") -AllowFailure).Text.Trim() - if ($upstreamRef.Code -ne 0 -or -not $upstreamRemote -or -not $mergeRef) { - $setUpstreamRemote = if ($suggestedHeadRemote) { $suggestedHeadRemote } else { 'origin' } - throw "Current branch '$branch' has no upstream tracking branch. git-remote-pr refuses to guess a PR head branch because stale tracking after a local rename can target the wrong remote branch. Set it explicitly with: git push --set-upstream $setUpstreamRemote HEAD`nor: git branch --set-upstream-to=$setUpstreamRemote/$branch" - } - $upstreamRefText = $upstreamRef.Text.Trim() - if ($mergeRef -notmatch '^refs/heads/(.+)$') { - throw "Current branch '$branch' tracks '$upstreamRefText', but the configured merge ref '$mergeRef' is not a GitHub branch under refs/heads/. Update the upstream before retrying." - } - $remoteBranch = $Matches[1] - if (-not $remoteRepos.ContainsKey($upstreamRemote)) { - $targetRemote = if ($suggestedHeadRemote) { $suggestedHeadRemote } else { '' } - throw "Current branch '$branch' tracks '$upstreamRefText', but remote '$upstreamRemote' is not a GitHub remote the skill can use. Point the branch at the intended GitHub remote with: git branch --set-upstream-to=$targetRemote/$branch" - } - if (-not (Test-EquivalentBranchNames $branch $remoteBranch)) { - throw "Local branch '$branch' does not match upstream tracking branch '$upstreamRemote/$remoteBranch'. This usually means a local rename left tracking stale or the remote branch still uses the old name. Update tracking with: git branch --set-upstream-to=$upstreamRemote/$branch after the remote branch name is corrected. If the remote still needs the renamed branch, publish it first with: git push $upstreamRemote HEAD:refs/heads/$branch" + $setUpstreamRequired = -not $upstreamRemote -and -not $mergeRef + if ($setUpstreamRequired) { + if ($remoteRepos.ContainsKey('origin')) { $headRemote = 'origin' } + elseif ($remoteRepos.Count -eq 1) { $headRemote = @($remoteRepos.Keys)[0] } + else { throw 'No unambiguous GitHub publication remote. Configure a GitHub origin or explicit branch tracking before retrying.' } + $remoteBranch = $branch + } else { + if (-not $upstreamRemote -or -not $mergeRef) { throw "Current branch '$branch' has incomplete upstream tracking configuration. Repair tracking before retrying." } + if ($mergeRef -notmatch '^refs/heads/(.+)$') { + throw "Current branch '$branch' has a configured merge ref '$mergeRef' that is not a GitHub branch under refs/heads/. Update the upstream before retrying." + } + $remoteBranch = $Matches[1] + if (-not $remoteRepos.ContainsKey($upstreamRemote)) { + $targetRemote = if ($suggestedHeadRemote) { $suggestedHeadRemote } else { '' } + throw "Current branch '$branch' tracks '$upstreamRemote/$remoteBranch', but remote '$upstreamRemote' is not a GitHub remote the skill can use. Point the branch at the intended GitHub remote with: git branch --set-upstream-to=$targetRemote/$branch" + } + if (-not (Test-EquivalentBranchNames $branch $remoteBranch)) { + throw "Local branch '$branch' does not match upstream tracking branch '$upstreamRemote/$remoteBranch'. This usually means a local rename left tracking stale or the remote branch still uses the old name. Update tracking with: git branch --set-upstream-to=$upstreamRemote/$branch after the remote branch name is corrected. If the remote still needs the renamed branch, publish it first with: git push $upstreamRemote HEAD:refs/heads/$branch" + } + $headRemote = $upstreamRemote } - $headRemote = $upstreamRemote Invoke-Gh @('auth', 'status') | Out-Null $viewer = Get-GhJson 'user' @@ -138,7 +141,7 @@ try { schema = 'codebeltnet/git-remote-pr/evidence/1'; repository = $baseRepo; head_repository = $headRepo head_remote = $headRemote; remote_branch = $remoteBranch; base = $baseBranch; head = $branch base_sha = $baseSha; head_sha = $headSha; remote_sha = $remoteSha; merge_base = $mergeBase - push_required = $remoteSha -ne $headSha; assignee = [string]$viewer.login + push_required = ($remoteSha -ne $headSha -or $setUpstreamRequired); set_upstream_required = $setUpstreamRequired; assignee = [string]$viewer.login title = Get-PrTitle $remoteBranch; action = if ($existing) { 'UPDATE' } else { 'CREATE' } existing_pr = if ($existing) { [ordered]@{ number = $existing.number; url = $existing.html_url; state = $existing.state; title = $existing.title; body = $existing.body; draft = $existing.draft; head_sha = $existing.head.sha; base_sha = $existing.base.sha; assignees = @($existing.assignees | ForEach-Object { $_.login }) } } else { $null } template_path = $templatePath; commits = $commits; files = $files.ToArray() diff --git a/skills/git-remote-pr/scripts/test-pr.ps1 b/skills/git-remote-pr/scripts/test-pr.ps1 index 2bac85e..350d152 100644 --- a/skills/git-remote-pr/scripts/test-pr.ps1 +++ b/skills/git-remote-pr/scripts/test-pr.ps1 @@ -223,9 +223,53 @@ try { Assert (-not (Test-EquivalentBranchNames 'v0.11.0/chore-and-git-remote-pr' 'v0.11.0/chore_and_git_remote_pr')) 'Branch comparison must reject separator-only differences.' Assert (-not (Test-EquivalentBranchNames 'v0.11.0/chore-and-git-remote-pr' 'v0.10.2/service-update')) 'Branch comparison should not hide real mismatches.' $noUpstream = Run-Prepare (Join-Path $root 'no-upstream') - Assert ($noUpstream.code -ne 0 -and $noUpstream.text -match 'no upstream tracking branch' -and $noUpstream.text -match 'set-upstream') 'Missing upstream tracking did not fail closed.' + Assert ($noUpstream.code -eq 0) "First publication must prepare without an upstream: $($noUpstream.text)" + $firstPaths = $noUpstream.text | ConvertFrom-Json + $firstEvidence = Get-Content $firstPaths.evidence -Raw | ConvertFrom-Json + Assert ($firstEvidence.set_upstream_required -and $firstEvidence.push_required -and $firstEvidence.head_remote -ceq 'origin' -and $firstEvidence.remote_branch -ceq 'v0.10.2/service-update') 'First publication did not plan the same-name origin branch and tracking.' Assert (-not (Test-Git @('ls-remote', '--heads', 'origin', 'refs/heads/v0.10.2/service-update'))) 'Unexpected remote branch before upstream push.' - Test-Git @('push', '-u', 'origin', 'HEAD') | Out-Null + Assert ((Invoke-Git @('config', '--get', 'branch.v0.10.2/service-update.remote') -AllowFailure).Code -ne 0) 'Preparation changed branch tracking.' + Assert (-not (Test-Path (Join-Path $state 'writes.log'))) 'First publication preparation wrote GitHub metadata.' + Test-Git @('remote', 'rename', 'origin', 'publish') | Out-Null + $singleRemote = Run-Prepare (Join-Path $root 'single-remote') + Assert ($singleRemote.code -eq 0 -and (Get-Content (($singleRemote.text | ConvertFrom-Json).evidence) -Raw | ConvertFrom-Json).head_remote -ceq 'publish') 'A sole GitHub remote was not resolved.' + Test-Git @('remote', 'add', 'second', $url) | Out-Null + $ambiguousRemote = Run-Prepare (Join-Path $root 'ambiguous-remote') + Assert ($ambiguousRemote.code -ne 0 -and $ambiguousRemote.text -match 'unambiguous GitHub publication remote') 'Ambiguous untracked publication remote was guessed.' + Test-Git @('remote', 'remove', 'second') | Out-Null + Test-Git @('remote', 'rename', 'publish', 'origin') | Out-Null + Test-Git @('config', 'branch.v0.10.2/service-update.remote', 'origin') | Out-Null + $partialTracking = Run-Prepare (Join-Path $root 'partial-tracking') + Assert ($partialTracking.code -ne 0 -and $partialTracking.text -match 'incomplete upstream tracking') 'Partial tracking was silently replaced.' + Test-Git @('config', 'branch.v0.10.2/service-update.merge', 'refs/heads/v0.10.2/service-update') | Out-Null + $unpublishedTracking = Run-Prepare (Join-Path $root 'unpublished-tracking') + Assert ($unpublishedTracking.code -eq 0 -and -not (Get-Content (($unpublishedTracking.text | ConvertFrom-Json).evidence) -Raw | ConvertFrom-Json).set_upstream_required) 'Configured tracking to an unpublished branch incorrectly blocked preparation.' + Test-Git @('config', '--unset', 'branch.v0.10.2/service-update.remote') | Out-Null + Test-Git @('config', '--unset', 'branch.v0.10.2/service-update.merge') | Out-Null + $firstEvidence.files | ForEach-Object { $_.theme = 'Behavior' } + $firstEvidence | ConvertTo-Json -Depth 20 | Set-Content $firstPaths.evidence -Encoding utf8 + $firstBody = Join-Path (Split-Path $firstPaths.evidence) 'body.md' + [System.IO.File]::WriteAllText($firstBody, "This pull request improves behavior.`n`n**Behavior:**`n`n- Improve feature behavior.", $utf8) + $firstCreate = Run-Execute $firstPaths.evidence $firstBody 'V0.10.2/service update' + Assert ($firstCreate.code -eq 0 -and ($firstCreate.text | ConvertFrom-Json).result -eq 'created') "First publication execution failed: $($firstCreate.text)" + $firstPlan = Get-Content (Join-Path (Split-Path $firstPaths.evidence) 'plan.json') -Raw | ConvertFrom-Json + Assert ($firstPlan.set_upstream_required -and (Get-Content $firstPlan.preview_file -Raw).Contains('set upstream tracking to the same branch')) 'Preview omitted the tracking write.' + Assert ((Test-Git @('rev-parse', '--abbrev-ref', '@{u}')) -ceq 'origin/v0.10.2/service-update') 'Execution did not establish upstream tracking.' + Assert ((Get-RemoteSha origin 'v0.10.2/service-update') -ceq $firstEvidence.head_sha) 'First publication did not persist the approved HEAD.' + Test-Git @('branch', '--unset-upstream') | Out-Null + $untrackedPublished = Run-Prepare (Join-Path $root 'untracked-published') + Assert ($untrackedPublished.code -eq 0) "Untracked published branch failed: $($untrackedPublished.text)" + $publishedPaths = $untrackedPublished.text | ConvertFrom-Json + $publishedEvidence = Get-Content $publishedPaths.evidence -Raw | ConvertFrom-Json + Assert ($publishedEvidence.set_upstream_required -and $publishedEvidence.push_required -and $publishedEvidence.action -eq 'UPDATE') 'Existing remote HEAD did not plan tracking and reuse the PR.' + $publishedEvidence.files | ForEach-Object { $_.theme = 'Behavior' } + $publishedEvidence | ConvertTo-Json -Depth 20 | Set-Content $publishedPaths.evidence -Encoding utf8 + $publishedBody = Join-Path (Split-Path $publishedPaths.evidence) 'body.md' + Copy-Item $firstBody $publishedBody + $publishedExecute = Run-Execute $publishedPaths.evidence $publishedBody 'V0.10.2/service update' + Assert ($publishedExecute.code -eq 0 -and (Test-Git @('rev-parse', '--abbrev-ref', '@{u}')) -ceq 'origin/v0.10.2/service-update') "Existing branch tracking failed: $($publishedExecute.text)" + Assert ((Get-Content (Join-Path $state 'writes.log')).Count -eq 2) 'Tracking an existing branch unnecessarily rewrote PR metadata.' + Remove-Item (Join-Path $state 'pr.json'), (Join-Path $state 'writes.log') [System.IO.File]::WriteAllText((Join-Path $repo 'guide.md'), 'review guide', $utf8) Test-Git @('add', 'guide.md') | Out-Null Test-Git @('commit', '-m', 'Document review flow') | Out-Null @@ -405,7 +449,7 @@ try { Assert (-not (Test-Path (Join-Path $state 'writes.log'))) 'Changed draft state made a GH write.' [System.IO.File]::WriteAllText($planPath, $planRaw, $utf8) Remove-Item -LiteralPath $invalidationPath - foreach ($change in @(@{ snapshot_key = 'changed' }, @{ push_required = $false }, @{ metadata_write = 'NONE' }, @{ assignment_write = $false }, @{ repository = 'other/repo' }, @{ base = 'other' }, @{ head = 'other' }, @{ head_repository = 'other/fork' }, @{ assignee = 'other' }, @{ existing_pr_number = 99 }, @{ existing_pr_url = 'https://github.com/acme/widget/pull/99' }, @{ action = 'UPDATE' }, @{ evidence_hash = 'changed' }, @{ body_hash = 'changed' })) { + foreach ($change in @(@{ snapshot_key = 'changed' }, @{ push_required = $false }, @{ set_upstream_required = $true }, @{ metadata_write = 'NONE' }, @{ assignment_write = $false }, @{ repository = 'other/repo' }, @{ base = 'other' }, @{ head = 'other' }, @{ head_repository = 'other/fork' }, @{ assignee = 'other' }, @{ existing_pr_number = 99 }, @{ existing_pr_url = 'https://github.com/acme/widget/pull/99' }, @{ action = 'UPDATE' }, @{ evidence_hash = 'changed' }, @{ body_hash = 'changed' })) { $altered = $planRaw | ConvertFrom-Json foreach ($name in $change.Keys) { $altered.$name = $change[$name] } [System.IO.File]::WriteAllText($planPath, ($altered | ConvertTo-Json -Depth 8), $utf8)