From 1340f52c540bf217109b2774ce7e26fb8f527d57 Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Fri, 8 May 2026 19:57:03 +0200 Subject: [PATCH 01/12] ci: add LLM-fallback bridge for fork-sync conflicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the existing snail+claude conflict-resolver pattern into the fork-sync autopilot loop. Until now, rebase failures only filed an issue and waited for a human — the LLM resolver never fired because it triggers on PR labels and fork-sync only created issues. Net result: zero LLM-driven sync resolutions in the last 60 days across 11 forks. This change introduces a new reusable workflow, `fork-conflict-autoresolve-reusable.yml`, that operates directly on the fork branch (rebase ht onto main, force-push) with a deterministic ladder followed by a Snail+Claude fallback: 1. rerere-assisted rebase 2. lock/generated file conflicts -> deterministic resolution 3. Semantic conflicts -> spawn-agent with Opus + drift-category prompt 4. Total failure -> file/update tracking issue Pre-resolve safety tag is pushed before any modification, so every auto-resolution has an immutable rollback point. `fork-sync-reusable.yml` gains an opt-in input `use_llm_autoresolve` (default: false) plus optional secrets `claude_oauth`, `docker_pat`, `kubeconfig`. Existing callers are unaffected — they keep filing issues. Callers that opt in chain into the new resolver job. Background investigation showed two systemic failure modes: 1. HAI_GH_PAT was expired across forks without repo-level overrides (rotated separately, ht-pytorch went 0/47 -> 1/48 immediately) 2. Conflict-resolver never fired because of the bridge gap fixed here Co-Authored-By: Claude Opus 4.7 --- .../fork-conflict-autoresolve-reusable.yml | 274 ++++++++++++++++++ .github/workflows/fork-sync-reusable.yml | 53 +++- 2 files changed, 321 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/fork-conflict-autoresolve-reusable.yml diff --git a/.github/workflows/fork-conflict-autoresolve-reusable.yml b/.github/workflows/fork-conflict-autoresolve-reusable.yml new file mode 100644 index 0000000..c9525fe --- /dev/null +++ b/.github/workflows/fork-conflict-autoresolve-reusable.yml @@ -0,0 +1,274 @@ +name: Reusable Fork Conflict Auto-Resolve + +# Purpose-built for the fork-sync autopilot loop. +# Operates directly on a fork branch (e.g. `ht`) — rebases it onto an +# upstream branch with a deterministic ladder, falling back to a +# Snail+Claude agent for semantic conflicts. Force-pushes the resolved +# fork branch on success. Files a tracking issue on total failure. +# +# This is distinct from core/conflict-resolver.yml which is PR-based. + +on: + workflow_call: + inputs: + upstream_repo: + description: "Full upstream repo (e.g. vllm-project/vllm)" + required: true + type: string + upstream_branch: + description: "Upstream/base branch (default main)" + required: false + default: "main" + type: string + fork_branch: + description: "HT customization branch to rebase (default ht)" + required: false + default: "ht" + type: string + use_llm: + description: "Allow Snail+Claude fallback for semantic conflicts" + required: false + default: true + type: boolean + llm_model: + description: "Model passed to spawn-agent (opus, sonnet, ...)" + required: false + default: "opus" + type: string + secrets: + gh_pat: + required: true + claude_oauth: + required: false + docker_pat: + required: false + kubeconfig: + required: false + outputs: + status: + description: "resolved | resolved_llm | failed" + value: ${{ jobs.deterministic.outputs.status }} + +concurrency: + group: fork-resolve-${{ github.repository }}-${{ inputs.fork_branch }} + cancel-in-progress: false + +jobs: + deterministic: + runs-on: ubuntu-latest + outputs: + status: ${{ steps.resolve.outputs.status }} + conflict_files: ${{ steps.resolve.outputs.conflict_files }} + pre_resolve_sha: ${{ steps.resolve.outputs.pre_resolve_sha }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.fork_branch }} + fetch-depth: 0 + token: ${{ secrets.gh_pat }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git config rerere.enabled true + git config merge.conflictstyle diff3 + + - name: Tag pre-resolve safety point + env: + FORK_BRANCH: ${{ inputs.fork_branch }} + run: | + PRE_SHA=$(git rev-parse HEAD) + TAG="pre-autoresolve-$(date +%Y%m%d-%H%M%S)-${PRE_SHA:0:7}" + git tag "$TAG" "$PRE_SHA" + git push origin "$TAG" + echo "pre_resolve_sha=$PRE_SHA" >> $GITHUB_OUTPUT + echo "Tagged rollback point: $TAG @ $PRE_SHA" + id: tag + + - name: Attempt resolution ladder + id: resolve + env: + FORK_BRANCH: ${{ inputs.fork_branch }} + UPSTREAM_BRANCH: ${{ inputs.upstream_branch }} + run: | + set +e + git fetch origin "$UPSTREAM_BRANCH" + + echo "=== Strategy 1: rebase with rerere ===" + if git rebase "origin/$UPSTREAM_BRANCH"; then + echo "status=resolved" >> $GITHUB_OUTPUT + echo "Clean rebase via rerere" + exit 0 + fi + git rebase --abort 2>/dev/null || true + + echo "=== Strategy 2: lock/generated file conflicts ===" + # Attempt rebase again, capture conflicts + git rebase "origin/$UPSTREAM_BRANCH" 2>/dev/null || true + CONFLICTING=$(git diff --name-only --diff-filter=U) + + if [ -z "$CONFLICTING" ]; then + # Rebase was actually clean on this run (rare race) + echo "status=resolved" >> $GITHUB_OUTPUT + git rebase --continue 2>/dev/null || true + exit 0 + fi + + echo "Conflicts in:" + echo "$CONFLICTING" + + LOCK_RX='\.lock$|lock\.json$|^uv\.lock$|^poetry\.lock$|^Cargo\.lock$|^package-lock\.json$|^yarn\.lock$|^pnpm-lock\.yaml$' + GEN_RX='\.generated\.|\.g\.dart$|\.pb\.go$|_generated\.go$|\.min\.js$|\.min\.css$' + NON_TRIVIAL="" + while IFS= read -r f; do + [ -z "$f" ] && continue + if echo "$f" | grep -qE "$LOCK_RX"; then + echo "Lock file: $f -> keep ours (will regenerate)" + git checkout --ours -- "$f" && git add -- "$f" + elif echo "$f" | grep -qE "$GEN_RX"; then + echo "Generated: $f -> keep theirs" + git checkout --theirs -- "$f" && git add -- "$f" + else + NON_TRIVIAL="$NON_TRIVIAL $f" + fi + done <<< "$CONFLICTING" + + if [ -z "$(echo "$NON_TRIVIAL" | xargs)" ]; then + if git rebase --continue 2>&1; then + echo "status=resolved" >> $GITHUB_OUTPUT + echo "All conflicts were lock/generated; auto-resolved" + exit 0 + fi + fi + + # Real conflicts remain + echo "Non-trivial conflicts in:$NON_TRIVIAL" + git rebase --abort 2>/dev/null || true + + echo "conflict_files<> $GITHUB_OUTPUT + echo "$CONFLICTING" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "status=needs_llm" >> $GITHUB_OUTPUT + + - name: Force-push if deterministically resolved + if: steps.resolve.outputs.status == 'resolved' + env: + FORK_BRANCH: ${{ inputs.fork_branch }} + run: | + git push --force-with-lease origin "$FORK_BRANCH" + echo "Force-pushed deterministically resolved $FORK_BRANCH" + + llm-resolve: + needs: deterministic + if: | + needs.deterministic.outputs.status == 'needs_llm' && + inputs.use_llm == true + uses: heiervang-technologies/core/.github/workflows/spawn-agent.yml@main + with: + prompt: | + You are running headless to auto-resolve a rebase conflict in this HT fork. + + Repo: ${{ github.repository }} + Fork branch: ${{ inputs.fork_branch }} + Upstream branch: ${{ inputs.upstream_branch }} (synced from ${{ inputs.upstream_repo }}) + Pre-resolve SHA tag: see most recent `pre-autoresolve-*` tag for rollback. + + Conflicting files: + ${{ needs.deterministic.outputs.conflict_files }} + + Steps: + 1. `git fetch origin ${{ inputs.upstream_branch }}` + 2. `git checkout ${{ inputs.fork_branch }}` + 3. `git rebase origin/${{ inputs.upstream_branch }}` — handle each conflict by reading the surrounding code on both sides and merging intelligently. + Read `~/ht/forks/FORK_MAINTENANCE_DIARY.md` for known drift categories and resolution patterns specific to this fork family. + 4. After all conflicts resolved, `git rebase --continue` (loop until done). + 5. Run any obvious smoke check (e.g. `python -c "import "` or upstream's lint) if cheap. + 6. **Verify** `git diff origin/${{ inputs.upstream_branch }}..HEAD` is non-empty (catches catastrophic flatten-to-upstream). + 7. `git push --force-with-lease origin ${{ inputs.fork_branch }}` + 8. Report a one-paragraph summary of which drift category each conflict matched and how you resolved it. + + Hard rules: + - Never push to ${{ inputs.upstream_branch }} on origin. + - Never delete tags starting with `pre-autoresolve-` or `ht-backup-`. + - If you cannot resolve confidently, exit with a clear error message — the workflow will file a tracking issue automatically. + summary_title: "Fork Conflict Auto-Resolve" + summary_metadata: '{"fork_branch": "${{ inputs.fork_branch }}", "upstream": "${{ inputs.upstream_repo }}/${{ inputs.upstream_branch }}"}' + execution_mode: kubernetes + model: ${{ inputs.llm_model }} + secrets: + DOCKER_PAT: ${{ secrets.docker_pat }} + GH_PAT: ${{ secrets.gh_pat }} + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.claude_oauth }} + KUBECONFIG: ${{ secrets.kubeconfig }} + + fallback-issue: + needs: [deterministic, llm-resolve] + if: | + always() && + needs.deterministic.outputs.status == 'needs_llm' && + (inputs.use_llm != true || needs.llm-resolve.result != 'success') + runs-on: ubuntu-latest + steps: + - name: File tracking issue + env: + GH_TOKEN: ${{ secrets.gh_pat }} + FORK_BRANCH: ${{ inputs.fork_branch }} + UPSTREAM_BRANCH: ${{ inputs.upstream_branch }} + UPSTREAM_REPO: ${{ inputs.upstream_repo }} + CONFLICTS: ${{ needs.deterministic.outputs.conflict_files }} + LLM_USED: ${{ inputs.use_llm }} + LLM_RESULT: ${{ needs.llm-resolve.result }} + run: | + # Check if an open issue already exists + EXISTING=$(gh issue list --label "sync-conflict" --state open --repo "${{ github.repository }}" --json number --jq ".[0].number // empty") + + STATUS_LINE="Deterministic strategies exhausted." + if [ "$LLM_USED" = "true" ]; then + STATUS_LINE="$STATUS_LINE LLM fallback ran but did not succeed (result: $LLM_RESULT)." + else + STATUS_LINE="$STATUS_LINE LLM fallback was disabled (use_llm=false)." + fi + + BODY=$(cat </dev/null || true + + if [ -n "$EXISTING" ]; then + gh issue comment "$EXISTING" --repo "${{ github.repository }}" --body "$BODY" + echo "Updated existing issue #$EXISTING" + else + gh issue create \ + --repo "${{ github.repository }}" \ + --title "Auto-resolve failed: $FORK_BRANCH on $UPSTREAM_BRANCH ($(date +%Y-%m-%d))" \ + --label "sync-conflict" \ + --body "$BODY" + fi diff --git a/.github/workflows/fork-sync-reusable.yml b/.github/workflows/fork-sync-reusable.yml index 4c5d967..edf627d 100644 --- a/.github/workflows/fork-sync-reusable.yml +++ b/.github/workflows/fork-sync-reusable.yml @@ -27,10 +27,24 @@ on: required: false default: true type: boolean + use_llm_autoresolve: + description: "On rebase conflict, attempt deterministic + Snail+Claude resolution. Requires claude_oauth/docker_pat/kubeconfig secrets." + required: false + default: false + type: boolean secrets: gh_pat: description: "PAT with contents:write, issues:write" required: true + claude_oauth: + description: "Claude Code OAuth token (only required if use_llm_autoresolve=true)" + required: false + docker_pat: + description: "Docker Hub PAT for snail image (only required if use_llm_autoresolve=true)" + required: false + kubeconfig: + description: "Kubeconfig for snail K8s execution (only required if use_llm_autoresolve=true)" + required: false concurrency: group: fork-sync-${{ github.repository }} @@ -98,6 +112,9 @@ jobs: needs: sync-main if: needs.sync-main.outputs.new_commits == 'true' && inputs.skip_rebase != true runs-on: ubuntu-latest + outputs: + status: ${{ steps.rebase.outputs.status }} + conflicts: ${{ steps.rebase.outputs.conflicts }} steps: - uses: actions/checkout@v4 with: @@ -180,8 +197,10 @@ jobs: git push --force-with-lease origin "$FORK_BRANCH" echo "Successfully force-pushed $FORK_BRANCH" - - name: Create conflict issue - if: steps.rebase.outputs.status == 'conflict' + - name: Create conflict issue (legacy fallback) + if: | + steps.rebase.outputs.status == 'conflict' && + inputs.use_llm_autoresolve != true env: GH_TOKEN: ${{ secrets.gh_pat }} FORK_BRANCH: ${{ inputs.fork_branch }} @@ -222,8 +241,25 @@ jobs: --body "$BODY" echo "Created new conflict issue" + auto-resolve: + needs: rebase-ht + if: | + needs.rebase-ht.outputs.status == 'conflict' && + inputs.use_llm_autoresolve == true + uses: heiervang-technologies/.github/.github/workflows/fork-conflict-autoresolve-reusable.yml@main + with: + upstream_repo: ${{ inputs.upstream_repo }} + upstream_branch: ${{ inputs.upstream_branch }} + fork_branch: ${{ inputs.fork_branch }} + use_llm: true + secrets: + gh_pat: ${{ secrets.gh_pat }} + claude_oauth: ${{ secrets.claude_oauth }} + docker_pat: ${{ secrets.docker_pat }} + kubeconfig: ${{ secrets.kubeconfig }} + status-check: - needs: [sync-main, rebase-ht] + needs: [sync-main, rebase-ht, auto-resolve] if: always() runs-on: ubuntu-latest steps: @@ -231,18 +267,23 @@ jobs: env: SYNC_RESULT: ${{ needs.sync-main.result }} REBASE_RESULT: ${{ needs.rebase-ht.result }} + REBASE_STATUS: ${{ needs.rebase-ht.outputs.status }} + AUTORESOLVE_RESULT: ${{ needs.auto-resolve.result }} NEW_COMMITS: ${{ needs.sync-main.outputs.new_commits }} run: | echo "## Fork Sync Summary" echo "Sync main: $SYNC_RESULT" echo "New commits: $NEW_COMMITS" - echo "Rebase ht: $REBASE_RESULT" + echo "Rebase ht: $REBASE_RESULT (status=$REBASE_STATUS)" + echo "Auto-resolve: $AUTORESOLVE_RESULT" if [ "$SYNC_RESULT" != 'success' ]; then echo "::error::Main sync failed — upstream may have diverged" exit 1 fi - if [ "$REBASE_RESULT" = "failure" ]; then - echo "::warning::Rebase failed — check for conflict issue" + if [ "$REBASE_STATUS" = "conflict" ] && [ "$AUTORESOLVE_RESULT" = "success" ]; then + echo "Rebase had conflicts but auto-resolve handled them" + elif [ "$REBASE_RESULT" = "failure" ] || [ "$AUTORESOLVE_RESULT" = "failure" ]; then + echo "::warning::Rebase or auto-resolve failed — check for conflict issue" fi From b881292f688e4dc4ab7db2c8aed2b205049c1d7d Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Fri, 8 May 2026 20:58:15 +0200 Subject: [PATCH 02/12] ci: add fork-sync-conflict skill + drift patterns catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The autopilot bridge in the previous commit gives the agent a trigger and a sandbox, but no decision recipe. This commit adds both. * `skills/fork-sync-conflict/SKILL.md` — the recipe. Triggers when an agent finds itself mid-rebase on an HT fork. Codifies the four-step decision flow (load context → categorize each conflict against the drift catalog → resolve → validate-before-push), the hard safety rules (no push to main, no deletion of pre-autoresolve/ht-backup tags, no force without --with-lease), and the explicit bail-out conditions (>3 rebase --continue iterations, >10 unrelated files, ambiguous directory renames). A flagged tracking issue is more useful than a wrong autoresolve. * `FORK_DRIFT_PATTERNS.md` — the public catalog the skill references. Distilled from the local FORK_MAINTENANCE_DIARY's general knowledge (drift categories, per-fork drift profiles, post-sync hygiene). Operational notes and sync history stay local; only the decision-relevant patterns are published here. * `fork-conflict-autoresolve-reusable.yml` prompt updated to install the skill into ~/.claude/skills before resolution and reference the patterns catalog. The agent now has the same context an experienced human would: which drift types this fork chronically sees, what the default action is per category, and which files have standing rules (e.g. studio/setup.sh in ht-unsloth → always take upstream). Together with the bridge, this completes the self-resolving end-to-end loop for forks that opt into use_llm_autoresolve=true. Co-Authored-By: Claude Opus 4.7 --- .../fork-conflict-autoresolve-reusable.yml | 53 ++++-- FORK_DRIFT_PATTERNS.md | 162 ++++++++++++++++++ skills/fork-sync-conflict/SKILL.md | 125 ++++++++++++++ 3 files changed, 322 insertions(+), 18 deletions(-) create mode 100644 FORK_DRIFT_PATTERNS.md create mode 100644 skills/fork-sync-conflict/SKILL.md diff --git a/.github/workflows/fork-conflict-autoresolve-reusable.yml b/.github/workflows/fork-conflict-autoresolve-reusable.yml index c9525fe..a52b453 100644 --- a/.github/workflows/fork-conflict-autoresolve-reusable.yml +++ b/.github/workflows/fork-conflict-autoresolve-reusable.yml @@ -167,31 +167,48 @@ jobs: uses: heiervang-technologies/core/.github/workflows/spawn-agent.yml@main with: prompt: | - You are running headless to auto-resolve a rebase conflict in this HT fork. + You are running headless to auto-resolve a rebase conflict in an HT fork. Repo: ${{ github.repository }} Fork branch: ${{ inputs.fork_branch }} Upstream branch: ${{ inputs.upstream_branch }} (synced from ${{ inputs.upstream_repo }}) - Pre-resolve SHA tag: see most recent `pre-autoresolve-*` tag for rollback. - Conflicting files: + Conflicting files (from deterministic ladder): ${{ needs.deterministic.outputs.conflict_files }} - Steps: - 1. `git fetch origin ${{ inputs.upstream_branch }}` - 2. `git checkout ${{ inputs.fork_branch }}` - 3. `git rebase origin/${{ inputs.upstream_branch }}` — handle each conflict by reading the surrounding code on both sides and merging intelligently. - Read `~/ht/forks/FORK_MAINTENANCE_DIARY.md` for known drift categories and resolution patterns specific to this fork family. - 4. After all conflicts resolved, `git rebase --continue` (loop until done). - 5. Run any obvious smoke check (e.g. `python -c "import "` or upstream's lint) if cheap. - 6. **Verify** `git diff origin/${{ inputs.upstream_branch }}..HEAD` is non-empty (catches catastrophic flatten-to-upstream). - 7. `git push --force-with-lease origin ${{ inputs.fork_branch }}` - 8. Report a one-paragraph summary of which drift category each conflict matched and how you resolved it. - - Hard rules: - - Never push to ${{ inputs.upstream_branch }} on origin. - - Never delete tags starting with `pre-autoresolve-` or `ht-backup-`. - - If you cannot resolve confidently, exit with a clear error message — the workflow will file a tracking issue automatically. + ## Setup the fork-sync-conflict skill + + Before resolving, install the skill so you have the recipe loaded: + + ```bash + mkdir -p ~/.claude/skills + gh api repos/heiervang-technologies/.github/contents/skills/fork-sync-conflict/SKILL.md \ + --jq '.content' | base64 -d > ~/.claude/skills/fork-sync-conflict.md + # also pull the patterns catalog the skill references + gh api repos/heiervang-technologies/.github/contents/FORK_DRIFT_PATTERNS.md \ + --jq '.content' | base64 -d > /tmp/FORK_DRIFT_PATTERNS.md + ``` + + Then read both files in full before touching any code. They encode the + decision flow, the nine drift categories with default actions, and the + hard safety rules (never push main, never delete safety tags, validate + before push, etc.). + + ## Execute the resolution + + Follow the SKILL.md decision flow exactly. The deterministic ladder has + already exhausted lock-file and generated-file fast paths, so what + remains are semantic conflicts requiring judgment. The most recent + `pre-autoresolve-*` tag on origin is your rollback point — do not delete + it. + + On success: force-push fork_branch with `--force-with-lease` and + comment on any open `sync-conflict` issue with the structured summary + from the skill's Observability section. + + On bail: exit non-zero with a one-paragraph reason. The workflow will + file/update a tracking issue. A flagged issue with your analysis is + more useful than a wrong autoresolve. summary_title: "Fork Conflict Auto-Resolve" summary_metadata: '{"fork_branch": "${{ inputs.fork_branch }}", "upstream": "${{ inputs.upstream_repo }}/${{ inputs.upstream_branch }}"}' execution_mode: kubernetes diff --git a/FORK_DRIFT_PATTERNS.md b/FORK_DRIFT_PATTERNS.md new file mode 100644 index 0000000..d2ce835 --- /dev/null +++ b/FORK_DRIFT_PATTERNS.md @@ -0,0 +1,162 @@ +# Fork Drift Patterns + +Catalog of recurring drift shapes seen while maintaining HT forks via periodic rebase syncs. This document is consumed by the `fork-sync-conflict` skill (under `skills/fork-sync-conflict/SKILL.md`) to drive autonomous conflict resolution. + +This is the **public/internal** version — operational secrets, sync history, and agent identities live in the local `FORK_MAINTENANCE_DIARY.md` only. + +## Drift Categories + +### 1. File Restructuring Drift + +**What**: Upstream reorganizes files or directories that HT has modified. + +**Examples**: +- `ht-unsloth studio/setup.sh`: upstream repeatedly restructures venv setup logic. **Standing rule: always take upstream** (`git checkout --theirs studio/setup.sh`). +- `ht-ACE-Step api_server.py`: upstream modularized a monolithic file into `acestep/api/` submodules; HT had int8 quantization changes in the same file. + +**Resolution**: take upstream's structure, port HT's change into the new layout. For known files with standing rules, follow them. + +**Frequency**: high for actively developed upstreams. + +--- + +### 2. Registry / Config Expansion Drift + +**What**: Upstream adds new entries to registries, model configs, or lookup tables in the same region where HT added entries. + +**Examples**: +- `ht-vllm registry.py`: HT added Qwen2.5-Omni model entries; upstream later added ColQwen3 entries in the same registry block. +- `ht-vllm-omni pyproject.toml`: dependency version bumps and new entries near HT's additions. + +**Resolution**: keep both sets of entries. Check if upstream now includes any of HT's additions (drop duplicates). Verify no conflicting version constraints. + +**Frequency**: medium. Registries grow monotonically so conflicts recur as long as HT carries its own entries. + +--- + +### 3. API Surface / Signature Drift + +**What**: Upstream changes function signatures, class interfaces, or API contracts that HT code depends on or extends. + +**Examples**: +- `ht-vllm-omni serving_speech.py / api_server.py`: upstream refactored serving endpoints; HT's speaker embedding and streaming features needed porting to new signatures. +- `ht-llama.cpp server-common.h / server-context.cpp`: upstream restructured server internals; HT's remap-developer-role feature needed adapting. + +**Resolution**: understand the new API shape, then port HT's feature logic into it. Most labor-intensive drift type — often requires reading upstream's PR to understand intent. + +**Frequency**: medium-low per repo, but high impact when it happens. + +--- + +### 4. CI / Build System Drift + +**What**: Upstream changes CI workflows, build configs, or tooling that HT has also modified. + +**Examples**: +- `ht-llama.cpp` GitHub Actions workflows: upstream reorganized them; HT had modifications in the same files. +- `ht-llama.cpp CONTRIBUTING.md`: documentation conflicts from parallel edits. + +**Resolution**: usually take upstream's version for CI unless HT has specific build requirements. Verify HT-specific CI steps (if any) are preserved. The `fork-sync.yml` and `conflict-resolver.yml` workflows themselves are HT-owned and should always be `--ours`. + +**Frequency**: low-medium. Spiky around upstream release cycles. + +--- + +### 5. UI / Frontend Drift + +**What**: Upstream updates frontend components that HT has customized. + +**Examples**: +- `ht-llama.cpp` Svelte UI components: HT added a cancel-button feature; upstream restructured the chat UI components (10+ file conflicts in one sync). + +**Resolution**: identify HT's feature additions (new components, new props) and graft them onto upstream's updated component tree. Test that features still render correctly. + +**Frequency**: low for most forks. High for repos with active frontend development. + +--- + +### 6. Upstream Convergence Drift + +**What**: Upstream independently implements functionality that overlaps with HT's changes — not because HT upstreamed it, but because both sides needed the same thing. Alternatively, contributions go upstream via a personal fork (`marksverdhei`), and when merged, the HT fork's original commits conflict with the (possibly modified) upstream version. + +**Key distinction**: HT org forks are not for upstreaming. Features meant for upstream go to the personal fork (`marksverdhei`), from which branches can be cherry-picked or copied. The `ht` branch is intended to carry HT-specific changes indefinitely, leaving upstream in peace. + +**Examples**: +- `ht-vllm-omni` speaker embedding PR (upstream #1227): contributed via personal fork, accepted upstream with co-author modifications. Upstream version is a superset. Two HT commits (embedding passthrough + voices API) needed manual skipping during rebase since git couldn't detect they implemented the same feature. + +**Resolution**: when upstream gains functionality that overlaps with HT commits, identify which HT commits are now redundant and `git rebase --skip` them when they become empty. The upstream version should be preferred. + +**Frequency**: medium. Occurs when personal fork contributions are accepted, or when upstream naturally converges on the same features. + +--- + +### 7. Duplicate Implementation Drift + +**What**: Both HT and upstream independently implement the same feature with different function names, validation logic, or code structure. After rebase, both implementations coexist silently. + +**Examples**: +- `ht-vllm-omni` streaming audio: HT implemented `_stream_progressive_audio` and `_make_wav_header`; upstream independently added `_generate_audio_chunks` and `_create_wav_header`. Both exist after rebase, doubling the surface area. +- `ht-vllm-omni` `stream` field: HT added `stream: bool` to protocol; upstream later added the same field with richer pydantic validation. If both survive rebase, pydantic silently uses the last definition. + +**Resolution**: after every sync, diff `ht` against `upstream/main` and audit each modified file for duplicate helpers, redundant validation, and dead code paths. When upstream adds equivalent functionality, remove HT's version and adopt upstream's pattern. If HT's version is genuinely better, contribute it via the personal fork — but the HT org fork should not carry both implementations long-term. + +**Frequency**: medium-high for forks where HT and upstream are working on the same features concurrently. + +--- + +### 8. Metadata / Attribution Drift + +**What**: Small metadata changes (copyright headers, author fields, license notices) accumulate in the fork and create unnecessary diff noise or legal ambiguity. + +**Examples**: +- `ht-vllm-omni cuda_graph_decoder_wrapper.py`: copyright header changed from "The Alibaba Qwen team" to "Heiervang Technologies" on a file HT only slightly modified. Legally questionable and creates conflict noise. + +**Resolution**: never modify copyright/attribution on upstream files unless HT is the sole author. If contributing via personal fork, strip such changes first. During rebase, take upstream's metadata. + +**Frequency**: low but persistent. Often introduced accidentally during development. + +--- + +### 9. History Rewrite Drift + +**What**: Upstream rebases or force-pushes their main branch, causing the HT mirror to diverge. + +**Examples**: +- `ht-codex`: upstream rebased history — 42 commits on HT's main not in upstream, 412 new upstream commits. Required `git reset --hard upstream/main && git push --force` on `main` (not `ht`). + +**Resolution**: force-reset `main` to `upstream/main`. Then rebase `ht` onto new `main`. **This is rare but disruptive — only proceed if a backup tag exists for rollback.** + +**Frequency**: rare. Only seen with newer / less-stable upstreams. + +--- + +## Per-Fork Drift Profile + +| Fork | Primary Drift Types | Chronic Conflicts | Notes | +|------|--------------------|--------------------|-------| +| ht-ACE-Step-1.5 | File restructuring | `api_server.py` (resolved, now modular) | Upstream actively restructuring | +| ht-codex | History rewrite | None currently | Young upstream, expect instability | +| ht-llama.cpp | API surface, CI, UI | Server internals during major releases | Very active upstream, high churn | +| ht-LlamaFactory | Minimal | None observed | Low HT diff | +| ht-pytorch | Minimal | None | Pure additive HT delta (docs + CI) | +| ht-unsloth | File restructuring | `studio/setup.sh` (every sync) | **Standing rule: take upstream** | +| ht-vibe | None observed | None | Upstream less active | +| ht-vllm | Registry expansion | Model registry blocks | HT entries are permanent fork additions | +| ht-vllm-omni | API surface, registry, convergence, duplicate impl | Serving endpoints, streaming audio | Most complex HT diff. Upstream convergence via personal fork contributions. | +| ht-voxcii | None observed | None | Low HT diff | + +## Post-Sync Hygiene Checklist + +After each sync, especially for high-drift forks (vllm-omni, llama.cpp): + +1. **Diff audit**: `git diff origin/main..origin/ht` — review every file for duplicate validation, redundant helpers, dead code. +2. **Convergence check**: if upstream gained functionality overlapping with HT commits, verify redundant HT commits were dropped during rebase. +3. **Copyright scan**: verify no upstream copyright headers were accidentally modified. +4. **Protocol field check**: for forks with shared protocol definitions, verify no duplicate field definitions survived rebase. +5. **Prefer upstream patterns**: when upstream added the same feature differently, adopt their approach and remove HT's version. + +## See also + +- `FORK_CHECKLIST.md` — fork setup checklist (one-time, per new fork). +- `FORK_MANAGEMENT.md` — broader fork management guide. +- `skills/fork-sync-conflict/SKILL.md` — the autonomous resolution recipe that consumes this document. diff --git a/skills/fork-sync-conflict/SKILL.md b/skills/fork-sync-conflict/SKILL.md new file mode 100644 index 0000000..ede7c2e --- /dev/null +++ b/skills/fork-sync-conflict/SKILL.md @@ -0,0 +1,125 @@ +--- +name: fork-sync-conflict +description: Use when resolving a rebase conflict on an HT fork's `ht` branch onto upstream main/master. Triggers from fork-sync-reusable workflow rebase failures, or when manually rebasing an `ht-*/` fork. Encodes the decision recipe and safety rules so an autonomous agent can finish the rebase end-to-end without escalating. +--- + +# Fork-Sync Conflict Resolution + +You are mid-rebase on an HT fork. The mechanical sync (fast-forward `main` from upstream) succeeded but `git rebase main` from `ht` hit a conflict. This skill is the recipe for landing the resolution safely without human escalation. + +## The HT fork model (what you're operating inside) + +- `main` (or `master` in some forks) is a **pure fast-forward mirror** of upstream — never directly committed to. +- `ht` is the customization branch — HT-specific changes rebased on top of `main`. +- A sync = fast-forward `main` + rebase `ht` onto new `main` + force-push `ht`. +- When upstream changes the same files HT changed, the rebase conflicts. **That's where you come in.** + +## Decision flow + +### Step 1 — load context before touching anything + +```bash +git fetch origin && git fetch upstream 2>/dev/null || git fetch +git status # how many files conflict? +git diff --name-only --diff-filter=U # which files? +git log --oneline origin/main..origin/ht | head -20 # what HT carries +``` + +Pull the drift-pattern catalog (categorized recipes per common conflict shape): + +```bash +gh api repos/heiervang-technologies/.github/contents/FORK_DRIFT_PATTERNS.md \ + --jq '.content' | base64 -d > /tmp/drift_patterns.md +``` + +Find this fork's row in `## Per-Fork Drift Profile` — it tells you which drift types are common here. + +### Step 2 — categorize each conflict + +For every conflicting file, match it to one of the nine drift categories in `/tmp/drift_patterns.md`: + +| # | Category | Default action | +|---|----------|----------------| +| 1 | File restructuring (upstream reorganized HT-modified file) | Take upstream's structure, port HT's change into it. Some files have standing rules (e.g. `studio/setup.sh` → always `--theirs`) — check the diary. | +| 2 | Registry / config expansion | Keep both sets of entries; drop dupes if upstream now includes any of HT's. | +| 3 | API surface / signature change | Read upstream's PR; port HT's feature into the new API. Most labor-intensive. | +| 4 | CI / build system | Usually take HT (`--ours`); HT's CI is intentional. | +| 5 | UI / frontend | Re-apply HT's modification on top of upstream's restructured component. | +| 6 | Convergence (upstream absorbed equivalent of HT) | The HT commit is now redundant — `git rebase --skip` if the commit becomes empty after applying upstream. | +| 7 | Duplicate implementation | Adopt upstream's version, remove HT's. If HT's is materially better, leave a note (don't carry both). | +| 8 | Metadata / attribution | Keep upstream's copyright headers. HT should not have modified them. | +| 9 | History rewrite | Only proceed if a `pre-autoresolve-*` or `ht-backup-*` tag exists for rollback. | + +### Step 3 — resolve, file by file + +```bash +# For each conflict, edit the file, then: +git add + +# When all are resolved: +git rebase --continue +``` + +Loop. Some commits may become empty after upstream absorption — `git rebase --skip` is correct for those (Drift Category 6). + +### Step 4 — validate before push (HARD GATE) + +All four must pass. If any fail, **stop and exit with an error** — don't push. + +```bash +# 1. ht still has HT-specific commits (catastrophic flatten check) +test "$(git rev-list --count origin/main..HEAD)" -gt 0 || { echo "FATAL: ht flattened to main"; exit 1; } + +# 2. Pre-autoresolve safety tag exists (rollback point) +git tag -l 'pre-autoresolve-*' | tail -1 | grep -q . || { echo "FATAL: no rollback tag"; exit 1; } + +# 3. No leftover conflict markers +! grep -rE '^<<<<<<< |^>>>>>>> |^======= ' . --include='*.py' --include='*.ts' --include='*.tsx' --include='*.rs' --include='*.go' --include='*.c' --include='*.cpp' --include='*.h' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.toml' 2>/dev/null + +# 4. Cheap smoke check — package importable / lint clean (best-effort) +[ -f pyproject.toml ] && python -c "import $(basename "$(pwd)" | sed 's/^ht-//; s/[.-]/_/g')" 2>/dev/null +[ -f Cargo.toml ] && cargo check --quiet 2>&1 | tail -5 +``` + +### Step 5 — push + +```bash +git push --force-with-lease origin ht +``` + +Then comment on the open `sync-conflict` issue (if any) with a one-paragraph summary: which drift categories applied, what you did per file, and the new `ht` SHA. Close the issue. + +## Hard safety rules — never violate + +1. **Never push to `main` or `master` on origin.** The FF-only sync owns that. +2. **Never delete tags** matching `pre-autoresolve-*` or `ht-backup-*`. They're rollback points. +3. **Never use `--force` without `--with-lease`.** +4. **Never auto-close a `sync-conflict` issue** unless validation Step 4 passed end-to-end. +5. **Never modify upstream copyright headers** during conflict resolution. +6. **If you can't resolve confidently, exit non-zero with a clear summary.** The fallback workflow files / updates the tracking issue. Forced wrong answers are worse than a flagged issue. + +## When to bail out + +Exit early and let the issue-fallback handle it if any of these hold: + +- After 3 attempts at `rebase --continue`, the same conflicts keep returning (you're not making progress) +- Conflicts span >10 files in unrelated subsystems (likely a major upstream refactor — needs human judgment) +- Upstream did a directory rename you can't unambiguously map (`git log --diff-filter=R --follow` to confirm) +- You're tempted to delete or modify HT commits whose intent you can't confidently identify from the commit message + diff + +A flagged issue with your analysis is more useful than a wrong autoresolve. + +## Observability + +When you finish (success or bail), produce a short structured summary: + +``` +fork: +new-ht-sha: +categories: +files-resolved: +commits-skipped: +validation: pass | fail () +``` + +This gets included in the issue comment / workflow summary. From 702af6e2bd52e00d051976845f0016d6703e15e1 Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Sat, 9 May 2026 10:33:06 +0200 Subject: [PATCH 03/12] ci: temporary security watch in fork-sync conflict resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until Phase 5 (security-scan job + irondome private findings) lands, the autopilot agent carries a best-effort backstop while it's already reading the upstream diff to resolve conflicts. Codified in both the workflow prompt and the skill so the directive survives prompt drift. The agent looks for supply-chain-attack-shaped patterns in upstream commits (obfuscated payloads, postinstall script fetches, new outbound network calls, typosquat renames, silently removed validation, install- time privilege escalation). On a hit: * exploit specifics stay in the private workflow run logs — never in the public-visible issue body * the `sync-conflict` issue gets a `## Security watch` section with only severity guess + commit SHA(s) + one-sentence category + workflow run URL * @marksverdhei is mentioned so GitHub's notification email fires — this is the temporary delivery channel until Phase 5 replaces it with Proton-to-Proton mail * critical-severity high-confidence findings exit non-zero AFTER filing the issue so the rebased ht is not force-pushed This is a backstop, not a scanner. The skill explicitly says "flag, don't speculate" so the agent doesn't chase ghosts. Co-Authored-By: Claude Opus 4.7 --- .../fork-conflict-autoresolve-reusable.yml | 37 +++++++++++++++++++ skills/fork-sync-conflict/SKILL.md | 24 ++++++++++++ 2 files changed, 61 insertions(+) diff --git a/.github/workflows/fork-conflict-autoresolve-reusable.yml b/.github/workflows/fork-conflict-autoresolve-reusable.yml index a52b453..53e4b0c 100644 --- a/.github/workflows/fork-conflict-autoresolve-reusable.yml +++ b/.github/workflows/fork-conflict-autoresolve-reusable.yml @@ -209,6 +209,43 @@ jobs: On bail: exit non-zero with a one-paragraph reason. The workflow will file/update a tracking issue. A flagged issue with your analysis is more useful than a wrong autoresolve. + + ## TEMPORARY: security watch (until Phase 5 + irondome land) + + While reading the upstream diff to resolve conflicts, also keep an eye + out for anything that smells like a supply-chain attack or new CVE in + the upstream commits being absorbed: + + - obfuscated payloads (base64 / hex blobs in normally-text files) + - postinstall scripts in package manifests that fetch external code + - sudden new outbound network calls in build / install / setup scripts + - typosquat-shaped dependency renames + - silent removal of validation / auth / signature checks + - unexpected privilege escalation (sudo / setuid / chmod 777 in install) + + If you spot anything suspicious — even "probably nothing": + + 1. **Do NOT include exploit details** (PoC, vulnerable code path, + reproduction steps) in any public-visible artifact. Keep specifics + in the workflow run logs only — the run is private to the org. + 2. When you file or update the `sync-conflict` issue, add a + `## Security watch` section with ONLY: + - severity guess (`low` / `medium` / `high` / `critical`) + - the upstream commit SHA(s) involved + - one sentence describing the *category* of concern (no specifics) + - the workflow run URL + 3. Mention `@marksverdhei` in the issue body so GitHub emails the owner. + This is the temporary notification channel until Phase 5 (security + scan + irondome private findings + Proton-to-Proton mail) is built. + 4. If severity is `critical` and you have *high* confidence, exit + non-zero AFTER filing the issue. The workflow will then NOT + force-push the rebased ht. The conflict-resolution work itself + is still recorded in the workflow logs for review. + + This is a best-effort backstop only — not a real security scanner. + Phase 5 will replace it with a dedicated `security-scan` job, the + full deterministic→pattern→LLM ladder, and findings filed in + `heiervang-technologies/irondome`. summary_title: "Fork Conflict Auto-Resolve" summary_metadata: '{"fork_branch": "${{ inputs.fork_branch }}", "upstream": "${{ inputs.upstream_repo }}/${{ inputs.upstream_branch }}"}' execution_mode: kubernetes diff --git a/skills/fork-sync-conflict/SKILL.md b/skills/fork-sync-conflict/SKILL.md index ede7c2e..ea1ef97 100644 --- a/skills/fork-sync-conflict/SKILL.md +++ b/skills/fork-sync-conflict/SKILL.md @@ -109,6 +109,30 @@ Exit early and let the issue-fallback handle it if any of these hold: A flagged issue with your analysis is more useful than a wrong autoresolve. +## Temporary security watch (while reading the upstream diff) + +Until the dedicated security-scan job ships (Phase 5, see `FORK_AUTOMATION_PLAN.md`), this skill carries a best-effort backstop. While you're reading conflict diffs to resolve them, also notice: + +- obfuscated payloads (base64 / hex blobs in normally-text files) +- postinstall / preinstall scripts in package manifests fetching external code +- sudden new outbound network calls in build / install / setup scripts +- typosquat-shaped dependency renames +- silent removal of validation / auth / signature checks +- unexpected privilege escalation in install paths + +If something looks suspicious — even faintly — handle it like this: + +1. **NEVER put exploit specifics in any public-visible artifact.** No PoC, no vulnerable code paths, no repro steps in the issue body. Keep specifics in the workflow run logs (private to the org). +2. Append a `## Security watch` section to the `sync-conflict` issue you create or update, containing ONLY: + - severity guess: `low` | `medium` | `high` | `critical` + - upstream commit SHA(s) involved + - one sentence on the *category* of concern (e.g. "obfuscated payload in postinstall script") — no specifics + - workflow run URL +3. Mention `@marksverdhei` in the issue body so GitHub emails the owner. This is the temporary notification channel until Phase 5 (irondome + Proton-to-Proton mail) replaces it. +4. If severity is `critical` AND your confidence is high, exit non-zero AFTER filing the issue so the rebased `ht` is not force-pushed. Default `pre-autoresolve-*` tag is the rollback point. + +This is a backstop, not a scanner. Don't chase ghosts; flag, don't speculate. + ## Observability When you finish (success or bail), produce a short structured summary: From 0fbd1a389248b4bb5e729975f698667614f0a761 Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Sat, 9 May 2026 11:13:50 +0200 Subject: [PATCH 04/12] =?UTF-8?q?docs(drift-patterns):=20add=20Cat=2010=20?= =?UTF-8?q?=E2=80=94=20Deployment=20Governance=20Drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authored by ht-vllm-omni-dev after the 2026-05-09 1cd52104-main incident. Catalogs the unsanctioned-image-channel failure mode where operators manually skopeo-copy upstream images into a fork's registry namespace, hand-tagging with branch suffixes that the fork's own docker-publish workflow does not produce. Categorically distinct from Cat 4 (CI/build): no fork-side workflow misconfiguration is involved. The pressure is downstream — when ht falls behind upstream, operators reach for upstream-pinned images to unblock prod, and the temporary pin becomes permanent. Resolving the parked rebase is the actual fix; the tag-suffix admission rule at the registry is the belt-and-suspenders. Cross-references Phase 5 security scan in the resolution pattern — manually-skopeo'd images bypass that future scanner too, so the registry gate is the chokepoint either way. Co-Authored-By: ht-vllm-omni-dev (HT agent) Co-Authored-By: Claude Opus 4.7 --- FORK_DRIFT_PATTERNS.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/FORK_DRIFT_PATTERNS.md b/FORK_DRIFT_PATTERNS.md index d2ce835..20cc711 100644 --- a/FORK_DRIFT_PATTERNS.md +++ b/FORK_DRIFT_PATTERNS.md @@ -130,6 +130,25 @@ This is the **public/internal** version — operational secrets, sync history, a --- +### 10. Deployment Governance Drift / Unsanctioned Image Channel + +**What**: An image reaches prod via a path that bypasses the fork's CI pipeline. An operator manually `skopeo`-copies an upstream image into the fork's registry namespace, sometimes hand-tagging it with branch suffixes (`-main`, `-ht`, `-voices`) that the fork's own `docker-publish.yml` does not produce. The image then looks plausibly home-grown but lacks any HT-specific patches. + +**Examples**: +- `ht-vllm-omni 1cd52104-main` (2026-05-09 incident): operator pinned this tag in `cloud/k8s/ai/vllm-omni-tts.yaml` while `ht` had a known compat issue with current vllm. The YAML comment documented the reason. After `ht` caught up (commit `739c1e66` bumped the vllm base), the comment became stale but the pin was not updated. Crashed every `/v1/audio/speech` because upstream `1cd52104` predated HT's speaker/voice alias fix (cherry-pick of upstream PR#2424 that `ht` carried as `7ae20e10`). Producer was manual `skopeo`, not any workflow. + +**Resolution pattern**: +- **Tag-suffix gate at the registry side**: deployment-time admission rule that refuses any image whose tag does not end in the fork-owned suffix (`-ht` for HT forks). Belongs in `cloud/`, not the fork. +- When operators reach for upstream-pinned images, treat it as a signal that the fork has fallen behind — resolve the parked rebase rather than let the temporary pin become permanent. If a YAML comment claims the fork is incompatible, verify against current fork `HEAD` before trusting it. +- Fork's `docker-publish.yml` should set explicit `type=ref,event=branch` + `type=sha,suffix={{branch}}` so its own production tags carry a branch suffix and can be recognized by the registry-side rule. +- This category is also relevant for [Phase 5 security scanning](FORK_AUTOMATION_PLAN.md#phase-5-upstream-security-scan-future--multi-step-extension) — manually-skopeo'd images bypass any future security-scan job too. The registry gate is the chokepoint. + +**Frequency**: rare in absolute terms (operator decision under pressure), but the impact is full-prod-down for the affected service and the diagnostic chain is non-obvious from fork-side alone — requires correlating registry tags with deployment manifest with operator history. High debugging cost. + +**Detection**: audit each fork's deployment manifest against the set of tags producible by that fork's own `docker-publish.yml`. Any tag in prod not in the producible set is a candidate. + +--- + ## Per-Fork Drift Profile | Fork | Primary Drift Types | Chronic Conflicts | Notes | From b6fd92f0eeb0174728afe859e9e35e5ad6098dfa Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Sat, 9 May 2026 14:09:50 +0200 Subject: [PATCH 05/12] docs(drift-patterns): link Cat 7 to ht-vllm-omni#39 prod incident The 2026-05-09 vllm-omni-tts incident is concrete evidence of the cost of letting Cat 7 (Duplicate Implementation Drift) survive rebase. Cross- linked from the streaming-audio example to ht-vllm-omni#39 so future maintainers see the abstract risk paired with the actual prod outage: 3+ hours of broken /v1/audio/speech, engine death after 1-2 requests, rollback to speaker-alias-v1 required. Code2Wav byte-identical between working era (d6fb9083) and current ht HEAD pinpointed d5692616 (progressive WAV streaming, Apr 22) as the introduction point. Also added a process note: when a Cat 7 duplicate causes a prod incident, capture diagnostic context BEFORE the rollback erases the evidence trail. Otherwise the eventual rebase re-introduces the bug. Co-Authored-By: ht-vllm-omni-dev (HT agent) Co-Authored-By: Claude Opus 4.7 --- FORK_DRIFT_PATTERNS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FORK_DRIFT_PATTERNS.md b/FORK_DRIFT_PATTERNS.md index 20cc711..9f879e9 100644 --- a/FORK_DRIFT_PATTERNS.md +++ b/FORK_DRIFT_PATTERNS.md @@ -95,11 +95,13 @@ This is the **public/internal** version — operational secrets, sync history, a **What**: Both HT and upstream independently implement the same feature with different function names, validation logic, or code structure. After rebase, both implementations coexist silently. **Examples**: -- `ht-vllm-omni` streaming audio: HT implemented `_stream_progressive_audio` and `_make_wav_header`; upstream independently added `_generate_audio_chunks` and `_create_wav_header`. Both exist after rebase, doubling the surface area. +- `ht-vllm-omni` streaming audio: HT implemented `_stream_progressive_audio` and `_make_wav_header`; upstream independently added `_generate_audio_chunks` and `_create_wav_header`. Both exist after rebase, doubling the surface area. **Concrete cost** (2026-05-09 prod incident): caused engine death after ~1-2 requests in production; required a 3+ hour incident response and a rollback to `speaker-alias-v1`. Tracked in [ht-vllm-omni#39](https://github.com/heiervang-technologies/ht-vllm-omni/issues/39) as a must-fix-in-rebase. Code2Wav file was byte-identical between the working era (`d6fb9083`) and current `ht` HEAD, confirming the latent bug was introduced by HT's `d5692616` (progressive WAV streaming, 2026-04-22) and survived undetected because no prior production load reached the trigger threshold. - `ht-vllm-omni` `stream` field: HT added `stream: bool` to protocol; upstream later added the same field with richer pydantic validation. If both survive rebase, pydantic silently uses the last definition. **Resolution**: after every sync, diff `ht` against `upstream/main` and audit each modified file for duplicate helpers, redundant validation, and dead code paths. When upstream adds equivalent functionality, remove HT's version and adopt upstream's pattern. If HT's version is genuinely better, contribute it via the personal fork — but the HT org fork should not carry both implementations long-term. +**Process note from the 2026-05-09 incident**: when a Cat 7 duplicate causes a prod incident, file an issue capturing diagnostic context (suspected commit, byte-level evidence, why prior production didn't surface it) BEFORE the rollback erases the evidence trail. Otherwise the eventual rebase will just re-introduce the bug. + **Frequency**: medium-high for forks where HT and upstream are working on the same features concurrently. --- From fd0cfc745236fe5a9ca5b01be76a9d257e05dd78 Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Tue, 12 May 2026 16:20:12 +0200 Subject: [PATCH 06/12] ci: disable rerere in autopilot + document the near-miss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caught a real near-miss on ht-llama.cpp 2026-05-12: rerere had cached resolutions from exploratory merges on May 3rd, and silently auto- resolved a fresh merge of `tools/server/server-models.cpp` using the May 3rd resolution. The cached resolution predated the LoRA dedupe + auto-discovery work that landed later; result was a working tree missing the LoRA dedupe block, the new common_lora_adapter_info fields, and the gguf_is_lora_adapter signature changes — exactly the "accidentally revert features" failure mode the owner warned about. No conflict markers, no warning. Caught only by spot-check against origin/ht. Root cause: rerere keys resolutions on conflict hunk content, not on surrounding structural context. On a long-lived fork where the surrounding code has changed between conflict-encounters, cached resolutions outlive the structural context they were made in. Mitigation: * fork-conflict-autoresolve-reusable.yml: default rerere.enabled=false with an inline comment pointing at the incident. * skills/fork-sync-conflict/SKILL.md: new "rerere caveat" section before the safety rules, with the standing rule (default disabled, opt-in only for tight-loop cases like studio/setup.sh in ht-unsloth, and even then verify cache freshness or nuke it). Co-Authored-By: ht-llama.cpp-dev (HT agent) Co-Authored-By: Claude Opus 4.7 --- .../fork-conflict-autoresolve-reusable.yml | 16 +++++++++++++--- skills/fork-sync-conflict/SKILL.md | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/workflows/fork-conflict-autoresolve-reusable.yml b/.github/workflows/fork-conflict-autoresolve-reusable.yml index 53e4b0c..b9a1334 100644 --- a/.github/workflows/fork-conflict-autoresolve-reusable.yml +++ b/.github/workflows/fork-conflict-autoresolve-reusable.yml @@ -71,7 +71,15 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git config rerere.enabled true + # rerere is intentionally disabled — see SKILL.md "rerere caveat". + # Stale rerere cache from earlier exploratory merges can silently + # auto-resolve a conflict using a resolution made in a different + # structural context, dropping later HT commits. Caught as a + # near-miss on ht-llama.cpp 2026-05-12 when cached May 3rd + # resolutions silently dropped the LoRA dedupe block in + # server-models.cpp. Disabled by default; opt in only if you've + # verified the cache is fresh. + git config rerere.enabled false git config merge.conflictstyle diff3 - name: Tag pre-resolve safety point @@ -95,10 +103,12 @@ jobs: set +e git fetch origin "$UPSTREAM_BRANCH" - echo "=== Strategy 1: rebase with rerere ===" + echo "=== Strategy 1: clean rebase attempt ===" + # No rerere — see SKILL.md "rerere caveat". This is a clean-slate + # rebase; it only succeeds if there are no real conflicts. if git rebase "origin/$UPSTREAM_BRANCH"; then echo "status=resolved" >> $GITHUB_OUTPUT - echo "Clean rebase via rerere" + echo "Clean rebase, no conflicts" exit 0 fi git rebase --abort 2>/dev/null || true diff --git a/skills/fork-sync-conflict/SKILL.md b/skills/fork-sync-conflict/SKILL.md index ea1ef97..cf14f24 100644 --- a/skills/fork-sync-conflict/SKILL.md +++ b/skills/fork-sync-conflict/SKILL.md @@ -89,6 +89,21 @@ git push --force-with-lease origin ht Then comment on the open `sync-conflict` issue (if any) with a one-paragraph summary: which drift categories applied, what you did per file, and the new `ht` SHA. Close the issue. +## rerere caveat (read this before you touch a conflict) + +`git rerere` ("reuse recorded resolution") looks tempting for HT forks because the same conflicts recur every sync (Drift Cat 1 / 2). **It is unsafe to trust on long-lived divergences and you should default to disabling it.** + +Why: rerere caches a resolution keyed by the conflict hunk's content, not by the surrounding structural context. When the surrounding code has changed between conflict-encounters (which it always does on a long-lived fork), rerere can apply a *stale* resolution that drops later commits' changes silently. No conflict markers, no warning — just a working tree that looks plausibly correct but is missing real HT work. + +**Near-miss (ht-llama.cpp, 2026-05-12)**: rerere had cached resolutions from exploratory merges on May 3rd. A fresh merge of `origin/ht` (which had since gained LoRA dedupe + auto-discovery in `tools/server/server-models.cpp`) silently applied the May 3rd resolution to that file. The entire LoRA dedupe block, new `common_lora_adapter_info` fields, and the `gguf_is_lora_adapter` signature changes were dropped. Caught only by spot-checking against `origin/ht`. + +**Standing rule**: +- Default: `git config rerere.enabled false` before any resolution work on an HT fork. +- If you opt in for a tight-loop standing-rule case (e.g. `studio/setup.sh` in ht-unsloth, where the resolution genuinely is "always take upstream"): first verify the rr-cache is fresh and matches the current structural context. Otherwise nuke it: `mv .git/rr-cache .git/rr-cache.bak.$(date +%Y%m%d)`. +- Never enable rerere in CI automation that runs across many forks with shared cache — there's no shared cache mechanism that's safe across forks anyway, but defense-in-depth. + +The autopilot workflow (`fork-conflict-autoresolve-reusable.yml`) sets `rerere.enabled false` by default. Don't override. + ## Hard safety rules — never violate 1. **Never push to `main` or `master` on origin.** The FF-only sync owns that. From 0caa748f84ee724d33a10051cfb5a67f2c5a6852 Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Wed, 13 May 2026 21:17:26 +0200 Subject: [PATCH 07/12] =?UTF-8?q?docs(drift-patterns):=20add=20Cat=2011=20?= =?UTF-8?q?=E2=80=94=20Silent=20Merge=20Anomalies=20(Displacement=20+=20Su?= =?UTF-8?q?rvival=20duals)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both failure modes surfaced together during the 2026-05-12 ht-vllm-omni streaming rebase. Both invisible to `git status` and to plain diff review; both shipped silently without active detection. * **11a Silent Displacement**: a both-touched file auto-merges with upstream's version winning entirely. HT delta vanishes, no conflict marker. Live example: `openai_speech_client.py` — HT `payload["voice"]` rename eclipsed by upstream's directory-rename rewrite. Benign Cat 6 convergence in this case, but path-equality misses it — the rename-aware both-touched list (`git log --diff-filter=AMR --name-only`) is required. * **11b Silent Survival**: a merge commit documents "Drops ``" but `` modified an HT-only file. The merge discards the COMMIT lineage but cannot revert the file CONTENT without an upstream alternative to triangulate against. Live example: `docker/Dockerfile.slim` — HT `739c1e66` documented as dropped, but Dockerfile.slim is HT-only, so the `v0.19.1` base survived. Caught downstream by snoop-kube, fixed in follow-up commit `d1ad5ee1`. Same detection surface (three-way delta verification), opposite failure direction. One recipe with two case studies, framed as duals. Hygiene checklist updated: * Step 6: silent-merge-anomaly check (triangulation + survival pass). * Step 7: native-build gate for forks with non-script targets — drift-zero must include a fresh `cmake`/`cargo`/`npm run build`, not just `tsc` clean. Caught on ht-llama.cpp 2026-05-12 when a single missing close-brace in `server-models.cpp` produced a 200+-error cmake cascade the webui typecheck never surfaced. Per-fork drift profile updated: ht-vllm-omni and ht-llama.cpp now marked as having seen silent merge anomalies; ht-llama.cpp note references the SKILL.md rerere caveat near-miss. Source: /home/me/ht/forks/parked-work/vllmomni-discoveries-11-12-onepager.md (drafted by ht-vllm-omni-dev after the 2026-05-12 rebase). Co-Authored-By: ht-vllm-omni-dev (HT agent) Co-Authored-By: Claude Opus 4.7 --- FORK_DRIFT_PATTERNS.md | 60 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/FORK_DRIFT_PATTERNS.md b/FORK_DRIFT_PATTERNS.md index 9f879e9..186c96e 100644 --- a/FORK_DRIFT_PATTERNS.md +++ b/FORK_DRIFT_PATTERNS.md @@ -151,19 +151,73 @@ This is the **public/internal** version — operational secrets, sync history, a --- +### 11. Silent Merge Anomalies (Displacement + Survival Duals) + +**What**: Two failure modes that share a detection surface but invert their failure direction. Both are invisible to `git status` and to a plain `git diff` review — the merge completes "cleanly", no conflict marker, and the resulting tree looks plausible. + +**11a — Silent Displacement (HT content discarded by upstream)**: +A both-touched file auto-merges with upstream's version winning entirely. HT's delta vanishes. Failure shape: HT improvement / correctness change silently dropped. + +**11b — Silent Survival (dropped-commit content persists)**: +A merge commit message documents "Drops ``: ``", but `` modified a file that has no upstream side. The merge can discard the COMMIT lineage but cannot revert the file CONTENT — there is no upstream alternative to triangulate against. Failure shape: a commit listed as dropped still ships its effect. + +**Detection — three-way delta triangulation** (covers both): + +```bash +MB=$(git merge-base upstream/main origin/ht) +# Rename-aware both-touched list (path-equality misses upstream renames): +ht_files=$(git log --diff-filter=AMR --name-only $MB..origin/ht | sort -u) +up_files=$(git log --diff-filter=AMR --name-only $MB..upstream/main | sort -u) +both=$(comm -12 <(echo "$ht_files") <(echo "$up_files")) + +for f in $both; do + ht=$(git diff $MB..origin/ht -- "$f" | wc -l) + up=$(git diff $MB..upstream/main -- "$f" | wc -l) + mvU=$(git diff upstream/main -- "$f" | wc -l) # after merge, vs upstream + printf '%-70s ht=%-5d up=%-5d mvU=%-5d\n' "$f" $ht $up $mvU +done +``` + +**Signature**: any line with `ht` large AND `mvU` ≈ 0 is silent displacement — the merged file is identical to upstream, HT delta gone. The triangulation cannot distinguish intentional (Cat 6 convergence) from accidental — that classification is manual. + +**Survival check** (separate pass, for each "Drops ``" line in the merge message): + +```bash +DROPPED= +for f in $(git show --name-only --pretty=format: $DROPPED); do + if git ls-tree upstream/main -- "$f" | grep -q .; then + echo "$f: upstream-touched — covered by displacement triangulation" + else + echo "$f: HT-only — VERIFY MANUALLY that $DROPPED's content effect is undone" + fi +done +``` + +For each HT-only file, read the dropped commit's diff and confirm each `+`/`-` line is matched (or supplanted) in the merged tree. If not, follow-up commit needed. + +**Examples (both from the 2026-05-12 ht-vllm-omni rebase, found in one pass)**: +- **Displacement (benign Cat 6)**: `examples/online_serving/qwen3_tts/openai_speech_client.py`. HT delta = 13 (`payload["voice"] = args.speaker` rename); upstream delta = 268 (file renamed + rewritten to `.../text_to_speech/qwen3_tts/openai_speech_client.py`). Merged tree = upstream version; the rename carries upstream's logic which already implements HT's intent. Plain path-equality misses this — the rename-aware list is required. +- **Survival (silent failure)**: `docker/Dockerfile.slim`. HT commit `739c1e66` ("build: bump vllm base to v0.19.1") was documented as dropped in the merge message. But Dockerfile.slim is HT-only (introduced by `406f2448`), so the merge couldn't revert `v0.19.1`. File content survived at v0.19.1 despite the lineage drop. Caught by snoop-kube during image build setup. Follow-up commit `d1ad5ee1` ("build: default Dockerfile.slim base to vllm-openai v0.20.0") fixed it. + +**Resolution**: bake the triangulation and survival checks into the post-sync hygiene step (see checklist below). Cost: ~5 minutes per rebase. Catches a class of regressions that diff-review alone does not. + +**Frequency**: per-rebase risk on any fork with non-trivial both-touched files OR explicit drop-lines in merge messages. Higher for forks where upstream is restructuring directories (Cat 1 amplifies displacement risk). + +--- + ## Per-Fork Drift Profile | Fork | Primary Drift Types | Chronic Conflicts | Notes | |------|--------------------|--------------------|-------| | ht-ACE-Step-1.5 | File restructuring | `api_server.py` (resolved, now modular) | Upstream actively restructuring | | ht-codex | History rewrite | None currently | Young upstream, expect instability | -| ht-llama.cpp | API surface, CI, UI | Server internals during major releases | Very active upstream, high churn | +| ht-llama.cpp | API surface, CI, UI, silent merge anomalies | Server internals during major releases | Very active upstream, high churn. Rerere near-miss 2026-05-12 on `server-models.cpp` — see SKILL.md "rerere caveat". | | ht-LlamaFactory | Minimal | None observed | Low HT diff | | ht-pytorch | Minimal | None | Pure additive HT delta (docs + CI) | | ht-unsloth | File restructuring | `studio/setup.sh` (every sync) | **Standing rule: take upstream** | | ht-vibe | None observed | None | Upstream less active | | ht-vllm | Registry expansion | Model registry blocks | HT entries are permanent fork additions | -| ht-vllm-omni | API surface, registry, convergence, duplicate impl | Serving endpoints, streaming audio | Most complex HT diff. Upstream convergence via personal fork contributions. | +| ht-vllm-omni | API surface, registry, convergence, duplicate impl, silent merge anomalies | Serving endpoints, streaming audio | Most complex HT diff. Upstream convergence via personal fork contributions. Both 11a (displacement) and 11b (survival) duals caught in 2026-05-12 rebase. | | ht-voxcii | None observed | None | Low HT diff | ## Post-Sync Hygiene Checklist @@ -175,6 +229,8 @@ After each sync, especially for high-drift forks (vllm-omni, llama.cpp): 3. **Copyright scan**: verify no upstream copyright headers were accidentally modified. 4. **Protocol field check**: for forks with shared protocol definitions, verify no duplicate field definitions survived rebase. 5. **Prefer upstream patterns**: when upstream added the same feature differently, adopt their approach and remove HT's version. +6. **Silent merge anomaly check (Cat 11)**: run the three-way delta triangulation against the rename-aware both-touched set and flag any `mvU≈0` row for manual displacement classification. For every "Drops ``" line in the merge message, run the survival check on HT-only files touched by ``. +7. **Native-build gate (forks with non-script targets)**: for forks that ship native binaries (e.g. ht-llama.cpp's C++ server, Tauri shells), the drift-zero check must include a fresh build (`cmake --build`, `cargo build`, `npm run build`), not just a typecheck of the script layer. A `tsc`-clean tree can still ship a backend that fails to compile — caught on ht-llama.cpp 2026-05-12 when a missing close-brace in `tools/server/server-models.cpp` produced a 200+-error cascade only visible under `cmake`. ## See also From aa4bdb0d920c3125d115924f2a0457e0c5484087 Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Wed, 13 May 2026 21:18:24 +0200 Subject: [PATCH 08/12] docs(skill): add resolution-time anti-patterns section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four process hazards caught in real rebases during the ht-llama.cpp and ht-vllm-omni work. These are agent-side failure modes, not upstream drift shapes — they belong in the resolution recipe alongside the rerere caveat. * **Soft-reset erasure**: `git reset --soft upstream/main` looks like a clever flatten but silently erases every file that exists only in the `master..main` upstream range. Tree-identity preserves HT, not upstream-only paths. Hundreds of files can vanish with zero conflict markers. Correct flatten path is merge-then-checkout-from-merged. * **Endurance-impaired waivers**: `--no-verify`, `eslint-disable`, `// @ts-ignore` after a long session. The checks usually catch silent-feature-loss (strategy-a prop-loosening that hides missing HT caller updates). If you're reaching for a waiver, bail out and file an issue instead of pushing. * **Peer-agent policy contradiction**: when two agents hold contradictory rules, picking one silently propagates the wrong rule. Escalate to the human with both positions stated; do not force local consistency. * **Runbook-hypothesis staleness**: RESUME.md / parked-work notes / issue comments embed hypotheses about the codebase at write-time. After a rebase advances HEAD, those hypotheses can be wrong. Verify named targets exist with the named shape before acting on the runbook. All four were observed across the 2026-05-12 rebase saga. Documenting them here closes the gap between the discoveries and the autopilot's operating instructions. Co-Authored-By: Claude Opus 4.7 --- skills/fork-sync-conflict/SKILL.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/skills/fork-sync-conflict/SKILL.md b/skills/fork-sync-conflict/SKILL.md index cf14f24..c623822 100644 --- a/skills/fork-sync-conflict/SKILL.md +++ b/skills/fork-sync-conflict/SKILL.md @@ -104,6 +104,36 @@ Why: rerere caches a resolution keyed by the conflict hunk's content, not by the The autopilot workflow (`fork-conflict-autoresolve-reusable.yml`) sets `rerere.enabled false` by default. Don't override. +## Resolution-time anti-patterns (process hazards) + +These are not drift shapes in the upstream — they are *your* failure modes as the resolving agent. All four were caught in real rebases. They feel like shortcuts when you're deep in a conflict; they aren't. + +### Soft-reset erasure (looks like a clever flatten, is actually a feature-loss) + +Tempting move: `git reset --soft upstream/main && git commit -m "rebase"` — produces a single HT commit on upstream's tip with the right tree. Looks like a fast flatten. + +What it actually does: the resulting tree contains HT's files but **silently erases every file that exists only in upstream's `master..main` range**. Tree-identity preserves the HT side; it does not preserve upstream-only paths. You will lose hundreds of upstream-only files with zero conflict markers. + +**Rule**: never resolve a divergent rebase via soft-reset. Use `git merge upstream/main` (then resolve conflicts) and *separately* linearize via `git checkout -b ht-linear upstream/main; git checkout ht-merged -- .; git commit` if you want a single commit. The checkout-from-merged step is what preserves upstream-only files. + +### Endurance-impaired waivers (you're tired and reaching for `--no-verify`) + +After a long resolution session, the temptation to disable a failing check rises: `git commit --no-verify`, `eslint-disable`, `// @ts-ignore`, "we can fix this in a follow-up". These checks usually catch the silent-feature-loss class of bugs — strategy (a) prop-loosening on upstream interfaces that hides a missing HT caller-update. + +**Rule**: if you find yourself wanting to bypass a check, that is itself a signal you should bail out and file an issue rather than push. The protocol against waiver-creep is pushback at the call-site, not internal restraint. If the autopilot reaches this point, exit non-zero with a clear summary and let a fresh session pick it up. + +### Peer-agent policy contradiction (two voices, one of them wrong) + +When coordinating with another agent (e.g. a per-repo dev agent), watch for rule contradictions: one agent says "X is required", the other agent's prior message implied "X is fine". Picking one silently propagates the wrong rule. Surface the conflict. + +**Rule**: if two agents (or two prior messages from the same agent) hold contradictory policies, neither agent's local judgment resolves it — escalate to the human (`say` or issue comment) with both positions stated, then wait. Forced consistency without escalation is how silently-wrong rules ship. + +### Runbook-hypothesis staleness (your RESUME.md is older than the code) + +Resume notes, parked-work READMEs, and issue comments embed *hypotheses about the codebase* at the time of writing. After a rebase advances HEAD, those hypotheses can be wrong — the file you were told to fix may have been renamed, the function signature may have changed, the bug may already be fixed. + +**Rule**: before acting on a runbook's named target (file, function, flag), verify it still exists at the named path with the named shape. If not, the runbook is stale — update it before continuing, don't act on the obsolete assumption. A two-line `grep` is cheaper than a wrong fix. + ## Hard safety rules — never violate 1. **Never push to `main` or `master` on origin.** The FF-only sync owns that. From a2f4faca0c3689643d2f65115a66bf1b78d7e0f2 Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Wed, 13 May 2026 21:37:13 +0200 Subject: [PATCH 09/12] docs(drift-patterns): add ht-mergekit row to per-fork profile ht-mergekit was missing from the per-fork drift profile table even though it's a standard HT fork. Adding it with the minimal-drift classification matching ht-LlamaFactory and ht-pytorch. Fork-sync caller installed 2026-05-13 (f37e1cd in ht-mergekit, daily 08:30 UTC). Co-Authored-By: Claude Opus 4.7 --- FORK_DRIFT_PATTERNS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/FORK_DRIFT_PATTERNS.md b/FORK_DRIFT_PATTERNS.md index 186c96e..0a009ea 100644 --- a/FORK_DRIFT_PATTERNS.md +++ b/FORK_DRIFT_PATTERNS.md @@ -213,6 +213,7 @@ For each HT-only file, read the dropped commit's diff and confirm each `+`/`-` l | ht-codex | History rewrite | None currently | Young upstream, expect instability | | ht-llama.cpp | API surface, CI, UI, silent merge anomalies | Server internals during major releases | Very active upstream, high churn. Rerere near-miss 2026-05-12 on `server-models.cpp` — see SKILL.md "rerere caveat". | | ht-LlamaFactory | Minimal | None observed | Low HT diff | +| ht-mergekit | Minimal | None observed | Low HT diff. Active 2026-05-13. | | ht-pytorch | Minimal | None | Pure additive HT delta (docs + CI) | | ht-unsloth | File restructuring | `studio/setup.sh` (every sync) | **Standing rule: take upstream** | | ht-vibe | None observed | None | Upstream less active | From c1c42c94f354fad4649e1246a683aa2f3d6f505e Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Wed, 13 May 2026 21:56:13 +0200 Subject: [PATCH 10/12] docs(skill+patterns): refresh categorize table to 11 cats, sharpen Cat 9 to operator-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Step-2 categorize table in SKILL.md was stale — still said "nine drift categories" and stopped at Cat 9. It now covers all eleven, with explicit default actions for Cats 10 and 11: * Cat 10 (Deployment Governance): out of scope for the rebase autopilot — a registry-side concern. Note it in the sync-conflict issue if observed; don't attempt to resolve during rebase. * Cat 11 (Silent Merge Anomalies): not a per-file conflict — runs as a post-rebase scan via the three-way delta triangulation + survival check. Any mvU≈0 row or HT-only file touched by a dropped commit goes into the validation report. Don't push if either is unresolved. Cat 9 (History Rewrite) was internally contradictory: the resolution prescribed `git push --force main`, but hard safety rule #1 forbids the autopilot from pushing to main. Resolved by labelling Cat 9 as operator territory — the autopilot bails, the issue-fallback files a ticket, a human handles the force-reset and backup-tag verification. Same wording applied in both SKILL.md and FORK_DRIFT_PATTERNS.md so the rule and the example are consistent. Co-Authored-By: Claude Opus 4.7 --- FORK_DRIFT_PATTERNS.md | 2 +- skills/fork-sync-conflict/SKILL.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/FORK_DRIFT_PATTERNS.md b/FORK_DRIFT_PATTERNS.md index 0a009ea..4a4d2c7 100644 --- a/FORK_DRIFT_PATTERNS.md +++ b/FORK_DRIFT_PATTERNS.md @@ -126,7 +126,7 @@ This is the **public/internal** version — operational secrets, sync history, a **Examples**: - `ht-codex`: upstream rebased history — 42 commits on HT's main not in upstream, 412 new upstream commits. Required `git reset --hard upstream/main && git push --force` on `main` (not `ht`). -**Resolution**: force-reset `main` to `upstream/main`. Then rebase `ht` onto new `main`. **This is rare but disruptive — only proceed if a backup tag exists for rollback.** +**Resolution**: force-reset `main` to `upstream/main`, then rebase `ht` onto new `main`. **Operator territory, not autopilot.** Force-pushing `main` violates the autopilot's hard safety rule #1 (no push to `main`/`master`), and verifying that the backup tag is fresh enough to roll back to requires human judgment about how much fork-side work post-dates it. The autopilot must bail on Cat 9 and let the issue-fallback file a tracking ticket. **Frequency**: rare. Only seen with newer / less-stable upstreams. diff --git a/skills/fork-sync-conflict/SKILL.md b/skills/fork-sync-conflict/SKILL.md index c623822..fe641fe 100644 --- a/skills/fork-sync-conflict/SKILL.md +++ b/skills/fork-sync-conflict/SKILL.md @@ -36,7 +36,7 @@ Find this fork's row in `## Per-Fork Drift Profile` — it tells you which drift ### Step 2 — categorize each conflict -For every conflicting file, match it to one of the nine drift categories in `/tmp/drift_patterns.md`: +For every conflicting file, match it to one of the eleven drift categories in `/tmp/drift_patterns.md`: | # | Category | Default action | |---|----------|----------------| @@ -48,7 +48,9 @@ For every conflicting file, match it to one of the nine drift categories in `/tm | 6 | Convergence (upstream absorbed equivalent of HT) | The HT commit is now redundant — `git rebase --skip` if the commit becomes empty after applying upstream. | | 7 | Duplicate implementation | Adopt upstream's version, remove HT's. If HT's is materially better, leave a note (don't carry both). | | 8 | Metadata / attribution | Keep upstream's copyright headers. HT should not have modified them. | -| 9 | History rewrite | Only proceed if a `pre-autoresolve-*` or `ht-backup-*` tag exists for rollback. | +| 9 | History rewrite | **Autopilot bails — operator territory.** Force-resetting `main` violates rule #1 and requires human judgment about backup-tag freshness. Exit non-zero and let the issue-fallback file a ticket. | +| 10 | Deployment governance / unsanctioned image | Out of scope for the rebase autopilot — this is a registry-side concern (manifest pin vs producible-tag set). Note it in the sync-conflict issue if observed; do not attempt to "resolve" it during rebase. | +| 11 | Silent merge anomalies | Not a per-file conflict — runs as a post-rebase scan. Execute the three-way delta triangulation + survival check from Cat 11. Any `mvU≈0` row or HT-only file touched by a dropped commit goes into the validation report. Do not push if either check is unresolved. | ### Step 3 — resolve, file by file From 39b4ba2751c8b0d75039ec2fde85d2d40a76dd16 Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Wed, 13 May 2026 22:18:00 +0200 Subject: [PATCH 11/12] ci(autoresolve): refresh agent prompt to 11 cats + flag public-fork secrets gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two consistency fixes against the workflow that drove the PR #9 catalog landing: * Agent prompt said "the nine drift categories" — now eleven. Also adds explicit guidance for Cats 9, 10, and 11 since they have non-trivial default actions (Cat 9/10 = bail to operator/registry; Cat 11 = run the post-rebase triangulation + survival check, do not push if either is unresolved). Mentions the resolution-time anti-patterns so the agent loads them before touching code. * Inline NOTE in the secrets block: HAI_DOCKER_PAT and KUBECONFIG are scoped private-only at the org level, so docker_pat and kubeconfig arrive empty on PUBLIC forks (ht-llama.cpp). The K8s spawn-agent job then fails to pull / authenticate. To extend LLM autoresolve to public forks, widen those org-secret visibilities. Filing the fix as a follow-up; the comment is a defensive doc-in-code so operators reading the workflow notice the gap. Both are doc-only — no behavioural changes. Co-Authored-By: Claude Opus 4.7 --- .../fork-conflict-autoresolve-reusable.yml | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/fork-conflict-autoresolve-reusable.yml b/.github/workflows/fork-conflict-autoresolve-reusable.yml index b9a1334..c775606 100644 --- a/.github/workflows/fork-conflict-autoresolve-reusable.yml +++ b/.github/workflows/fork-conflict-autoresolve-reusable.yml @@ -200,9 +200,15 @@ jobs: ``` Then read both files in full before touching any code. They encode the - decision flow, the nine drift categories with default actions, and the - hard safety rules (never push main, never delete safety tags, validate - before push, etc.). + decision flow, the eleven drift categories with default actions, the + resolution-time anti-patterns (soft-reset erasure, endurance waivers, + peer-agent contradictions, runbook staleness), and the hard safety + rules (never push main, never delete safety tags, validate before + push, etc.). For Cat 9 (history rewrite) and Cat 10 (deployment + governance) the default action is bail — these are operator/registry + territory, not autopilot work. For Cat 11 (silent merge anomalies), + run the three-way delta triangulation + survival check as a + post-rebase scan; do not push if either check is unresolved. ## Execute the resolution @@ -261,6 +267,14 @@ jobs: execution_mode: kubernetes model: ${{ inputs.llm_model }} secrets: + # NOTE (2026-05-13): the org-level secrets HAI_DOCKER_PAT and KUBECONFIG + # currently have "selected repositories" / private-only visibility at + # heiervang-technologies, so docker_pat and kubeconfig will arrive empty + # on PUBLIC forks (e.g. ht-llama.cpp). The K8s spawn-agent job will then + # fail to pull the snail image / authenticate against the cluster. To + # extend LLM autoresolve to public forks, widen those org-secret + # visibilities to include the public repos (or move them to repo-level). + # Until then, the LLM fallback only fires on private forks. DOCKER_PAT: ${{ secrets.docker_pat }} GH_PAT: ${{ secrets.gh_pat }} CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.claude_oauth }} From c8e673e78b7b07dd1b3adb5376b60f7ebb91f878 Mon Sep 17 00:00:00 2001 From: marksverdhei Date: Thu, 14 May 2026 17:22:56 +0200 Subject: [PATCH 12/12] ci(fork-sync): gate rebase-ht on ht_behind, not on new_commits this run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug reproduced 2026-05-14 on ht-llama.cpp: 1. Stale sync-conflict issue (#12 from 2026-03-11) was still open after a 2-month PAT outage masked the cron. Each cron run hit the "Found open conflict issue. Skipping rebase to prevent log spam" branch INSIDE rebase-ht's run step. That branch correctly avoided spam but did not bubble its skip up to the job's gating condition. 2. Operator closed the stale issue and re-dispatched. 3. sync-main found master already current (FF'd on the previous run that had skipped the rebase). new_commits=false. rebase-ht job's gating condition `new_commits == 'true'` therefore caused the job to be skipped at the job level — before the now-empty issue check could fire. ht stayed 31 commits behind master with no path forward short of waiting for upstream to push something new. The real predicate is "ht is behind upstream", not "main moved this run". sync-main now emits an `ht_behind` output computed unconditionally (works on both the FF-and-push path and the already-current path). rebase-ht's condition becomes: needs.sync-main.outputs.ht_behind != '0' && needs.sync-main.outputs.ht_behind != '' && inputs.skip_rebase != true Empty-string guard handles the rare case where the ht_behind step errored and emitted nothing (defensive — should not happen now that the rev-list has its own `|| echo 0` fallback). new_commits is still emitted for status-check's summary log; the output is unchanged for downstream consumers. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/fork-sync-reusable.yml | 44 ++++++++++++++++-------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/.github/workflows/fork-sync-reusable.yml b/.github/workflows/fork-sync-reusable.yml index edf627d..0b01483 100644 --- a/.github/workflows/fork-sync-reusable.yml +++ b/.github/workflows/fork-sync-reusable.yml @@ -58,6 +58,7 @@ jobs: upstream_sha: ${{ steps.sync.outputs.upstream_sha }} main_sha_before: ${{ steps.sync.outputs.main_sha_before }} main_sha_after: ${{ steps.sync.outputs.main_sha_after }} + ht_behind: ${{ steps.sync.outputs.ht_behind }} steps: - uses: actions/checkout@v4 with: @@ -75,6 +76,7 @@ jobs: env: UPSTREAM_REPO: ${{ inputs.upstream_repo }} UPSTREAM_BRANCH: ${{ inputs.upstream_branch }} + FORK_BRANCH: ${{ inputs.fork_branch }} run: | MAIN_SHA_BEFORE=$(git rev-parse HEAD) echo "main_sha_before=$MAIN_SHA_BEFORE" >> $GITHUB_OUTPUT @@ -90,27 +92,41 @@ jobs: echo "$UPSTREAM_BRANCH is already up to date with upstream" echo "new_commits=false" >> $GITHUB_OUTPUT echo "main_sha_after=$MAIN_SHA_BEFORE" >> $GITHUB_OUTPUT - exit 0 - fi + else + # Fast-forward only — if this fails, local branch has diverged (bad state) + if ! git merge --ff-only "upstream/$UPSTREAM_BRANCH"; then + echo "::error::$UPSTREAM_BRANCH has diverged from upstream — manual intervention required" + exit 1 + fi - # Fast-forward only — if this fails, local branch has diverged (bad state) - if ! git merge --ff-only "upstream/$UPSTREAM_BRANCH"; then - echo "::error::$UPSTREAM_BRANCH has diverged from upstream — manual intervention required" - exit 1 - fi + git push origin "$UPSTREAM_BRANCH" - git push origin "$UPSTREAM_BRANCH" + MAIN_SHA_AFTER=$(git rev-parse HEAD) + echo "main_sha_after=$MAIN_SHA_AFTER" >> $GITHUB_OUTPUT + echo "new_commits=true" >> $GITHUB_OUTPUT - MAIN_SHA_AFTER=$(git rev-parse HEAD) - echo "main_sha_after=$MAIN_SHA_AFTER" >> $GITHUB_OUTPUT - echo "new_commits=true" >> $GITHUB_OUTPUT + COMMIT_COUNT=$(git rev-list --count "$MAIN_SHA_BEFORE".."$MAIN_SHA_AFTER") + echo "Synced $COMMIT_COUNT new commits from upstream" + fi - COMMIT_COUNT=$(git rev-list --count "$MAIN_SHA_BEFORE".."$MAIN_SHA_AFTER") - echo "Synced $COMMIT_COUNT new commits from upstream" + # Compute ht_behind regardless of whether sync-main moved this run. + # rebase-ht is gated on this value, not on new_commits, so that a stale + # sync-conflict issue closed after a previous skipped run can trigger + # the rebase on next dispatch even if upstream has not advanced since. + # See: https://github.com/heiervang-technologies/.github/issues -- the + # bug was reproduced 2026-05-14 on ht-llama.cpp. + git fetch origin "$FORK_BRANCH" + HT_BEHIND=$(git rev-list --count "origin/$FORK_BRANCH..upstream/$UPSTREAM_BRANCH" 2>/dev/null || echo 0) + echo "ht_behind=$HT_BEHIND" >> $GITHUB_OUTPUT + echo "ht is $HT_BEHIND commits behind upstream/$UPSTREAM_BRANCH" rebase-ht: needs: sync-main - if: needs.sync-main.outputs.new_commits == 'true' && inputs.skip_rebase != true + # Run rebase whenever ht is behind upstream — not just when main moved THIS run. + # If a previous run skipped rebase due to an open sync-conflict issue, closing + # that issue and re-dispatching should trigger the rebase even if upstream has + # not advanced since. Gating on new_commits alone leaves the fork stuck behind. + if: needs.sync-main.outputs.ht_behind != '0' && needs.sync-main.outputs.ht_behind != '' && inputs.skip_rebase != true runs-on: ubuntu-latest outputs: status: ${{ steps.rebase.outputs.status }}