diff --git a/.github/workflows/fork-conflict-autoresolve-reusable.yml b/.github/workflows/fork-conflict-autoresolve-reusable.yml new file mode 100644 index 0000000..c775606 --- /dev/null +++ b/.github/workflows/fork-conflict-autoresolve-reusable.yml @@ -0,0 +1,352 @@ +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" + # 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 + 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: 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, no conflicts" + 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 an HT fork. + + Repo: ${{ github.repository }} + Fork branch: ${{ inputs.fork_branch }} + Upstream branch: ${{ inputs.upstream_branch }} (synced from ${{ inputs.upstream_repo }}) + + Conflicting files (from deterministic ladder): + ${{ needs.deterministic.outputs.conflict_files }} + + ## 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 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 + + 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. + + ## 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 + 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 }} + 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..0b01483 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 }} @@ -44,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: @@ -61,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 @@ -76,28 +92,45 @@ 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 }} + conflicts: ${{ steps.rebase.outputs.conflicts }} steps: - uses: actions/checkout@v4 with: @@ -180,8 +213,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 +257,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 +283,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 diff --git a/FORK_DRIFT_PATTERNS.md b/FORK_DRIFT_PATTERNS.md new file mode 100644 index 0000000..4a4d2c7 --- /dev/null +++ b/FORK_DRIFT_PATTERNS.md @@ -0,0 +1,240 @@ +# 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. **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. + +--- + +### 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`. **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. + +--- + +### 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. + +--- + +### 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, 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 | +| ht-vllm | Registry expansion | Model registry blocks | HT entries are permanent fork additions | +| 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 + +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. +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 + +- `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..fe641fe --- /dev/null +++ b/skills/fork-sync-conflict/SKILL.md @@ -0,0 +1,196 @@ +--- +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 eleven 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 | **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 + +```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. + +## 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. + +## 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. +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. + +## 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: + +``` +fork: +new-ht-sha: +categories: +files-resolved: +commits-skipped: +validation: pass | fail () +``` + +This gets included in the issue comment / workflow summary.