From df9544a9134fd8761cff6f524908f4ef9d4663cf Mon Sep 17 00:00:00 2001 From: Robert Doggett Date: Tue, 30 Dec 2025 06:41:57 -0800 Subject: [PATCH 1/3] Update 01-quick-start.md with tab usage guidelines --- docs/01-quick-start.md | 12 ++++++++++++ docs/02-troubleshooting.md | 12 ++++++++++++ docs/03-scripts.md | 3 +++ handoff.md | 7 +++++++ scripts/applyPatch | 13 +++++++++++++ scripts/fix-diff-counts.sh | 6 ++++++ 6 files changed, 53 insertions(+) diff --git a/docs/01-quick-start.md b/docs/01-quick-start.md index 38834b1..e8962e5 100644 --- a/docs/01-quick-start.md +++ b/docs/01-quick-start.md @@ -54,6 +54,18 @@ Assistant first peek request (template) - Batch all needed peeks into one block so the operator runs a single command. - Never guess bytes—peek first; then patch with hunks in descending order per file. +### Important: `nl -ba` Inserts TAB Delimiters +**🚨 Do NOT Use Tabs in Code or Patches! 🚨** +- The `nl -ba` command **inserts a literal TAB character** between the line number and the actual source text. +- This TAB is a display delimiter only and is **not part of the file bytes**. + - They can, however, be relied upon to determine the beginning of the raw text. +- **Tabs can cause complications**: + - They usually go undetected by the operator, and can result in mismatched search strings. + - Their presence in source code introduces inconsistencies across different environments or editors. +#### Guidelines to Follow: +- **Always filter tabs**: Ensure any patches or files are validated to exclude TAB characters. +- **Avoid introducing tabs into patches**: Never copy this delimiter into any patches, as it will create unintended issues in your codebase. + Share Your Project repo (usually once per session) - Right before pasting your repo snapshot is a good time to confirm a test patch, or share handoff instructions, or a goal. - Once you've shared your files, you're ready to begin. diff --git a/docs/02-troubleshooting.md b/docs/02-troubleshooting.md index 9d8ef88..a2d4459 100644 --- a/docs/02-troubleshooting.md +++ b/docs/02-troubleshooting.md @@ -29,6 +29,16 @@ Hunk length/count mismatches - Explanation: Expected and normal. Counts are recomputed automatically from the body. - Action: None, unless patch still fails; then re-peek and retry as above. +Tabs in peeks vs tabs in patches (IMPORTANT) +- `nl -ba` prints line numbers and inserts a literal TAB character between the number and the real file text. +- So in a peek, lines look like they contain tabs even when the underlying file contains none. +- Treat that TAB as a UI delimiter only: + - It is not part of the file bytes. + - It must never be introduced into patches. +- In this workflow, patches should not contain TAB characters. + - If a model emits tabs (often by accident, copying the peek formatting), `git apply` can fail or the repo can gain unwanted tabs. + - `applyPatch` rejects TABs by default. To override (rare), set: `APPLY_PATCH_ALLOW_TABS=1`. + No-op / “ghost” hunks - Symptom: - git apply reports a “corrupt patch” even though the @@ header looks OK, or @@ -45,6 +55,8 @@ No-op / “ghost” hunks - Cause: the assistant (or a tool) emitted a hunk that doesn’t add or remove any lines, or that only twiddles whitespace in a way git doesn’t encode. - Fix: - Regenerate the patch and ensure every hunk contains at least one `+` or `-` line. + - Note: `applyPatch` runs `fix-diff-counts.sh`, which drops ghost hunks automatically. If you still see ghost-hunk symptoms, + your diff was likely malformed in some other way (re-peek and regenerate). - Avoid whitespace-only edits (especially on blank-looking lines) unless you’re explicitly doing a formatting pass. - If the only difference between `-` and `+` in a hunk is invisible whitespace, drop those edits unless you *intend* to reformat. - As a rule: if a hunk has no `+` or `-` lines, delete that hunk and try again; applyPatch cannot “repair” ghost hunks. diff --git a/docs/03-scripts.md b/docs/03-scripts.md index 4a28615..65380db 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -27,6 +27,9 @@ These helpers keep the loop fast and consistent. They are small, transparent, an - applyPatch path/to/patch.diff # read from file - Behavior: - Recomputes hunk lengths with fix-diff-counts.sh and ensures a trailing newline. + - Drops "ghost hunks" (hunks with no `+`/`-` lines) via fix-diff-counts.sh. + - Rejects TAB characters in patches by default (to avoid accidentally copying `nl -ba` delimiters into real files). + - Override (rare): set `APPLY_PATCH_ALLOW_TABS=1`. - Runs git apply --check first, then applies if clean. - Streams stdout+stderr to terminal and copies the same output to the clipboard via toClip (tee). - If the chat UI mangles fenced blocks inside patches, ask the assistant for a here-doc command to write the file(s) diff --git a/handoff.md b/handoff.md index 3f2d685..756693b 100644 --- a/handoff.md +++ b/handoff.md @@ -34,6 +34,13 @@ You must follow these patterns. - Base your diff only on what you just saw in that peek, not on memory. +### Important: `nl -ba` inserts TAB delimiters in peeks + +- Peeks use `nl -ba`, which inserts a literal TAB character between the line number and the real file content. +- That TAB is a display delimiter only. It is not part of the file bytes. +- Do not introduce tabs into patches by copying peek formatting. +- In this workflow, tabs in patches are treated as errors. + ### 2. Use the patch checklist after you generate a diff After you have written a diff, mechanically walk the checklist from 'docs/02-troubleshooting.md': diff --git a/scripts/applyPatch b/scripts/applyPatch index 8d429ab..edda2c5 100755 --- a/scripts/applyPatch +++ b/scripts/applyPatch @@ -47,6 +47,19 @@ fi echo "applyPatch: fixing hunk counts…" "$fixer" "$tmp_in" > "$tmp_fixed" +if [[ "${APPLY_PATCH_ALLOW_TABS:-0}" != "1" ]]; then + tab_hits="$(LC_ALL=C grep -n $'\t' "$tmp_fixed" | head -n 5 || true)" + if [[ -n "$tab_hits" ]]; then + echo "applyPatch: ERROR: patch contains TAB characters. This workflow treats that as a mistake." + echo "applyPatch: Rationale: peeks use 'nl -ba', which inserts a TAB delimiter after the line number." + echo "applyPatch: Do not copy that delimiter into patches; source files should not gain tabs accidentally." + echo "applyPatch: First TAB occurrences (line:content):" + printf "%s\n" "$tab_hits" + echo "applyPatch: If you truly need tabs in a patch, re-run with APPLY_PATCH_ALLOW_TABS=1." + exit 1 + fi +fi + echo "applyPatch: git apply --check …" git apply --check -v "$tmp_fixed" echo "applyPatch: check OK." diff --git a/scripts/fix-diff-counts.sh b/scripts/fix-diff-counts.sh index 3c7bb0d..a0c1b12 100755 --- a/scripts/fix-diff-counts.sh +++ b/scripts/fix-diff-counts.sh @@ -9,6 +9,12 @@ set -euo pipefail awk ' function flush_hunk( oldLen,newLen,i) { if (!in_hunk) return; oldLen = c + d newLen = c + a + # Drop "ghost hunks" (no additions/deletions). These can be generated accidentally and + # may cause git apply to fail with "corrupt patch", even though they encode no change. + if ((a + d) == 0) { + in_hunk=0; nb=0; c=0; d=0; a=0; tail="" + return + } # Re-emit corrected header, preserving any tail after @@ printf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLen, newStart, newLen, tail) for (i=1; i<=nb; i++) print body[i] From 9528ad40b24700fb2f5e73bdc1042bf01bc30f1a Mon Sep 17 00:00:00 2001 From: Robert Doggett Date: Wed, 31 Dec 2025 05:42:27 -0800 Subject: [PATCH 2/3] aHa moment: don't allow nl to add Tab chars --- docs/00-overview.md | 2 +- docs/01-quick-start.md | 105 +++++++++++++--------------------- docs/02-troubleshooting.md | 113 +++++++++++++++---------------------- docs/03-scripts.md | 1 + docs/04-philosophy.md | 10 ++-- handoff.md | 86 ++++------------------------ 6 files changed, 103 insertions(+), 214 deletions(-) diff --git a/docs/00-overview.md b/docs/00-overview.md index db9eb2f..fd200c5 100644 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -39,7 +39,7 @@ Assistant template (one paragraph) - “Please run sharefiles in this repo and paste the snapshot. Then run sharefiles in your project repo and paste that. I’ll request a generous peek (±30–200 lines or full sections) of the exact lines I’ll edit, then return a unified diff with hunks listed in descending line order per file. I’ll keep changes scoped to the peek, avoid surprises, and iterate until green.” Glossary (at a glance) -- Peek: read-only, numbered slice of files (nl -ba | sed -n 'START,ENDp'). +- Peek: read-only, numbered slice of files (nl -ba -s'|' | sed -n 'START,ENDp'). - Generous peek: full section/function or ±100–200 lines to avoid re-peeks. - Patch: unified diff; one fenced block; ends with a newline. - Descending diffs: list hunks bottom-to-top per file to reduce offset drift. diff --git a/docs/01-quick-start.md b/docs/01-quick-start.md index e8962e5..1ad80e9 100644 --- a/docs/01-quick-start.md +++ b/docs/01-quick-start.md @@ -1,116 +1,87 @@ # Quick Start -- Peek → Patch → Apply (clipboard-first) - Why this -- GPT can't clone/push to your repo, and forgets contents of a zip archive quickly. A plain-text repo dump can persist. -- We keep a tight loop: Share repo, then Peek → Patch → Apply → Test → Repeat. -- Chat UIs slow down as context grows. Starting a fresh instance and getting back to speed quickly is essential. - +- GPT can't clone/push to your repo, and it often forgets contents of a zip archive quickly. A plain-text repo dump can persist. +- We keep a tight workflow: Share repo, then Peek → Patch → Apply → Test → Repeat. +- Chat UIs can slow down as context grows, making it essential to start fresh when lag occurs. Privacy Warning - Don't share private or unshareable repos without permission. Repeat that out loud and pause. This workflow works by pasting the contents of all text files in a repo into GPT or similar chat. That may not be ok for your situation, especially in a professional setting. Think before using. Don't neglect to think about embedded credentials or other secrets. - Tools (scripts/) - Put these in your $PATH, copy them to your $HOME/bin, or otherwise make them accessible. - sharefiles -- copies repo list and all (text) tracked files to clipboard as fenced code blocks. Paste once per session. - applyPatch -- reads a diff from clipboard (or file), auto-fixes hunk counts, ensures final newline, then git apply. - fix-diff-counts.sh -- recomputes @@ hunk lengths from the body of the changes made. - toClip / fromClip - Agnostic clipboard adapters to copy to or paste clipboard contents to stdout. - Contract (safety + cadence) - One action per step; everything clipboard-driven. -- One patch per step: at any given step the assistant should provide at most one combined diff for you to apply, even if it touches multiple files. +- One patch per step: at any given step, the assistant should provide at most one combined diff for you to apply, even if it touches multiple files. - The assistant only requests obvious, low-risk commands; the human operator should refuse anything unclear at a glance. - Always paste code/patches/logs as fenced code blocks that end with a newline. - The assistant batches multiple peeks into one block so the human runs one command per step. - Project-specific aliases like 'test', 'build', and 'lint' (that send output through toClip) are a good alternative to hard-coded scripts. - -Share This repo (once per session) +### Note: `nl` delimiter (TAB by default) vs file bytes +- By default, `nl -ba` uses a TAB delimiter between the line number and the real file content. +- Prefer `nl -ba -s'|'` in peek commands so the delimiter is a visible pipe character instead. +- In patches: avoid introducing TABs by accidentally copying peek formatting (applyPatch rejects TABs by default). +### Share This repo (once per session) - From this repo run "sharefiles" and paste the clipboard into a fresh GPT session to get it up to speed quickly. -```bash +~~~bash scripts/sharefiles -``` +~~~ Assistant's first reply (template) - From the repo you want to work on, run this and paste the clipboard output here to share all the files at once: -```bash +~~~bash sharefiles -``` +~~~ - After I see the snapshot, I'll propose one tiny, low-risk first change, request a generous peek of the exact lines I'll touch, and return a unified diff you can apply with applyPatch. Assistant first peek request (template) - One command, generous windows, batched into a single clipboard block. -```bash +~~~bash { echo "=== Sources/Feature/FileA.swift (1-200) ===" - nl -ba Sources/Feature/FileA.swift | sed -n '1,200p' + nl -ba -s'|' Sources/Feature/FileA.swift | sed -n '1,200p' echo echo "=== Sources/Feature/ModuleB.swift (120-280) ===" - nl -ba Sources/Feature/ModuleB.swift | sed -n '120,280p' + nl -ba -s'|' Sources/Feature/ModuleB.swift | sed -n '120,280p' echo echo "=== Tests/ModuleBTests.swift (1-180) ===" - nl -ba Tests/ModuleBTests.swift | sed -n '1,180p' + nl -ba -s'|' Tests/ModuleBTests.swift | sed -n '1,180p' } | toClip -``` +~~~ - Adjust ranges to cover full functions/sections or ±100–200 lines when unsure. - Batch all needed peeks into one block so the operator runs a single command. -- Never guess bytes—peek first; then patch with hunks in descending order per file. - -### Important: `nl -ba` Inserts TAB Delimiters -**🚨 Do NOT Use Tabs in Code or Patches! 🚨** -- The `nl -ba` command **inserts a literal TAB character** between the line number and the actual source text. -- This TAB is a display delimiter only and is **not part of the file bytes**. - - They can, however, be relied upon to determine the beginning of the raw text. -- **Tabs can cause complications**: - - They usually go undetected by the operator, and can result in mismatched search strings. - - Their presence in source code introduces inconsistencies across different environments or editors. -#### Guidelines to Follow: -- **Always filter tabs**: Ensure any patches or files are validated to exclude TAB characters. -- **Avoid introducing tabs into patches**: Never copy this delimiter into any patches, as it will create unintended issues in your codebase. - -Share Your Project repo (usually once per session) -- Right before pasting your repo snapshot is a good time to confirm a test patch, or share handoff instructions, or a goal. -- Once you've shared your files, you're ready to begin. -- Collaborate as makes sense. Partnership or pair programming techniques work well. -- Expect assistant to make errors and generate compile/test errors just like any other partner might. -- Scripts are designed to copy errors/results to clipboard, ready to paste into assistant to repair/retry. +- Never guess bytes—peek first; then patch with hunks in descending line order per file. -The loop +### Tabs: mostly a non-issue if you use `nl -ba -s'|'` (still avoid in patches) +- Default `nl -ba` uses a TAB between the line number and file content; `-s'|'` replaces it with a visible `|`. +- So: prefer `nl -ba -s'|'` in peeks, and avoid introducing TABs into patches by copying peek formatting. +- `applyPatch` rejects TABs by default as a guardrail; override only if you explicitly intend tabs. +### The loop 1) Assistant requests peek of places it will be patching. Peeks are directly executable shell command blocks. -- Unfortunately assistants sometimes skip this step and go right to creating the patch. That patch will probably fail. -- It's better to reject patches without a peek first, and create a cadence which usually works with fewer stumbles. -- It's fun and goes quickly when this works well. +- It's better to reject patches without a peek first and create a cadence which usually works with fewer stumbles. 2) Paste peek request into shell, review for sanity/safety, then hit Return to load the clipboard. Paste directly to assistant verbatim. -- What peeks do: - - nl -ba adds visible line numbers (including blanks) without changing bytes. - - sed -n 'START,ENDp' prints only that range. - - Both are read-only and safe. - - toClip copies standard input to the clipboard. - - Window size guidance: - - Err on wider windows to avoid re-peeking later. - - Small edit: ±30–80 lines around the target. - - Medium/uncertain: ±100–200 lines or the whole function/section. - - Multiple ranges are OK; batch into one fenced block so the operator runs a single command. - - Never guess bytes—peek, then patch. - - Typical bundle requested by the assistant: -```bash +- Typical bundle requested by the assistant: +~~~bash { echo "=== Sources/Widget.swift (1-140) ===" - nl -ba Sources/Widget.swift | sed -n '1,140p' + nl -ba -s'|' Sources/Widget.swift | sed -n '1,140p' echo echo "=== Sources/Utils/Tools.swift (60-120) ===" - nl -ba Sources/Utils/Tools.swift | sed -n '60,120p' + nl -ba -s'|' Sources/Utils/Tools.swift | sed -n '60,120p' echo echo "=== Tests/WidgetTests.swift (1-200) ===" - nl -ba Tests/WidgetTests.swift | sed -n '1,200p' + nl -ba -s'|' Tests/WidgetTests.swift | sed -n '1,200p' } | toClip -``` +~~~ 3) Assistant prepares and returns a fenced code block with its patch. - - Multi-file patches are OK only if we just peeked all touched files in one request. Otherwise, send a single-file patch and iterate. +- Multi-file patches are OK only if we just peeked all touched files in one request. Otherwise, send a single-file patch and iterate. - Hunks must be listed in descending line order per file (bottom-to-top) to minimize offset churn during apply. - New files use --- /dev/null and +++ b/path; include the full file body in one hunk. 4) Copy the returned patch, and apply it directly from the clipboard (applyPatch reads from the clipboard by default): -```bash +~~~bash applyPatch -``` +~~~ 5) Build/test and send short logs: - As early as step two, errors can occur. When they do, just paste the error to the assistant and the loop restarts. - Just getting to the point where there are no compile errors and testing can be done can be challenging. @@ -148,7 +119,7 @@ Unified diff in brief (what Assistant will create and paste back) - Lines: space = context, - = deletion, + = addition (LF endings) Replace one line (example) -```diff +~~~diff --- a/Sources/Foo.swift +++ b/Sources/Foo.swift @@ -42,3 +42,3 @@ @@ -156,16 +127,16 @@ Replace one line (example) - let y = oldCall(x) + let y = newCall(x) return y -``` +~~~ Insert one line (example, characteristically with a bad count) -```diff +~~~diff --- a/Sources/Foo.swift +++ b/Sources/Foo.swift @@ -99,2 +99,3 @@ vm.refresh() + vm.bootstrapIfNeeded() -``` +~~~ Patch expectations - Pure unified diff in one fenced block: diff --git a/docs/02-troubleshooting.md b/docs/02-troubleshooting.md index a2d4459..ba41e1e 100644 --- a/docs/02-troubleshooting.md +++ b/docs/02-troubleshooting.md @@ -1,139 +1,118 @@ # Troubleshooting -- common bumps and quick fixes - This workflow is resilient, but a few issues come up regularly. Here's how to fix them fast. - -Stop on error +## Stop on error - When anything fails (apply/build/test), stop. Re-peek a small, numbered slice of the exact target, then retry with a rebased patch. - -Patch won't apply ("patch does not apply" / "while searching for") +## Patch won't apply ("patch does not apply" / "while searching for") - Cause: the diff was based on older bytes than your working copy (drift), or whitespace/formatting altered lines. - Fix: 1) Re-peek the exact file/range the hunk targets (the assistant will ask for a narrow slice). 2) The assistant rebases and sends a fresh diff. 3) Apply again. - -Why descending diffs help +## Why descending diffs help - List patch hunks in descending line order per file (bottom-to-top). - This reduces mid-apply offset drift when earlier hunks change line numbers above later ones. - It increases apply reliability without changing patch content or scope. - If a patch still fails, re-peek a wider window and rebase precisely. - -"corrupt patch at line N" +## "corrupt patch at line N" - Cause: malformed diff (missing fences, missing final newline, or mangled @@ headers). - Fix: - Ensure you pasted a single fenced code block with unified diff only, ending in a newline. - - Re-request the patch; applyPatch auto-fixes hunk counts but can't fix a broken structure. - -Hunk length/count mismatches -- Symptom: applyPatch logs "fixDiffCounts: corrected hunk ...". + - Re-request the patch; `applyPatch` auto-fixes hunk counts but can't fix a broken structure. +## Hunk length/count mismatches +- Symptom: `applyPatch` logs "fixDiffCounts: corrected hunk ...". - Explanation: Expected and normal. Counts are recomputed automatically from the body. -- Action: None, unless patch still fails; then re-peek and retry as above. - -Tabs in peeks vs tabs in patches (IMPORTANT) -- `nl -ba` prints line numbers and inserts a literal TAB character between the number and the real file text. -- So in a peek, lines look like they contain tabs even when the underlying file contains none. -- Treat that TAB as a UI delimiter only: - - It is not part of the file bytes. - - It must never be introduced into patches. +- Action: None, unless the patch still fails; then re-peek and retry as above. +## Tabs in peeks vs tabs in patches (IMPORTANT) +- Don’t use `nl -ba`, as it inserts a TAB delimiter. Instead, use `nl -ba -s'|'` in peeks. +- This way, the delimiter is visible and avoids the issues caused by TABs. +- If you do see TABs in a peek, treat them as UI delimiters only: + - They are not part of the file bytes. + - They must not be introduced into patches. - In this workflow, patches should not contain TAB characters. - - If a model emits tabs (often by accident, copying the peek formatting), `git apply` can fail or the repo can gain unwanted tabs. + - If a model emits tabs (often by accident), `git apply` can fail or the repository may gain unwanted tabs. - `applyPatch` rejects TABs by default. To override (rare), set: `APPLY_PATCH_ALLOW_TABS=1`. - -No-op / “ghost” hunks +## No-op / “ghost” hunks - Symptom: - git apply reports a “corrupt patch” even though the @@ header looks OK, or - a hunk shows only context lines (no + or - lines), so it appears to “touch” a region without actually changing it. - You see something like: - @@ -305,10 +328,11 @@ // Optional raw recording file for this listen... var recordingFile: AVAudioFile? if record { @@ -320,13 +344,14 @@ - - There are **no** lines starting with `+` or `-` between those `@@` headers. +There are **no** lines starting with `+` or `-` between those `@@` headers. - Cause: the assistant (or a tool) emitted a hunk that doesn’t add or remove any lines, or that only twiddles whitespace in a way git doesn’t encode. - Fix: - Regenerate the patch and ensure every hunk contains at least one `+` or `-` line. - - Note: `applyPatch` runs `fix-diff-counts.sh`, which drops ghost hunks automatically. If you still see ghost-hunk symptoms, - your diff was likely malformed in some other way (re-peek and regenerate). + - Note: `applyPatch` runs `fix-diff-counts.sh`, which drops ghost hunks automatically. If you still see ghost-hunk symptoms, your diff was likely malformed in some other way (re-peek and regenerate). - Avoid whitespace-only edits (especially on blank-looking lines) unless you’re explicitly doing a formatting pass. - If the only difference between `-` and `+` in a hunk is invisible whitespace, drop those edits unless you *intend* to reformat. - - As a rule: if a hunk has no `+` or `-` lines, delete that hunk and try again; applyPatch cannot “repair” ghost hunks. + - As a rule: if a hunk has no `+` or `-` lines, delete that hunk and try again; `applyPatch` cannot “repair” ghost hunks. - Quick human diagnostic: - Scan for `@@` lines in the patch. - - If you ever see two `@@` headers with nothing but `" "` (context) lines between them, that hunk is a ghost and should be removed / regenerated. - -Minimum context per hunk + - If you ever see two `@@` headers with nothing but `" "` (context) lines between them, that hunk is a ghost and should be removed or regenerated. +## Minimum context per hunk - Symptom: - - applyPatch reports a failure, even though the changed lines look correct. + - `applyPatch` reports a failure, even though the changed lines look correct. - Hunks appear to start or end exactly on the changed lines with no surrounding context. - Cause: some tools (and humans) emit hunks with no unchanged prefix/suffix lines; this makes matching fragile once code shifts even a little. - Fix: - Ensure each hunk includes at least one unchanged context line before and after the modified region. - - When in doubt, widen the context window (one or two extra unchanged lines above and below) so applyPatch has a stable anchor. - -Triple-backtick blocks inside patches (nested fences) + - When in doubt, widen the context window (one or two extra unchanged lines above and below) so `applyPatch` has a stable anchor. +## Triple-backtick blocks inside patches (nested fences) - Symptom: Chat UI or clipboard mangles a patch that contains fenced code blocks (like ```bash). - Fix options: - The assistant uses only unified diff fences for patches; internal example fences are allowed and should be preserved. - If your UI still mangles them, ask for a "full-file replacement" instead of a diff for that file, or request a here-doc shell command to write the file bytes locally and then git diff it yourself. - -Full-file replacement instead of diff +## Full-file replacement instead of diff - When to use: the target file is small and formatting keeps breaking the diff in chat. - How: 1) The assistant provides the entire file contents in a single fenced block (no diff). 2) You overwrite the file locally (here-doc or editor). 3) Run git diff -- path/to/file to confirm the change and proceed. - -Deleting a file via diff is NOT recommended. It's blabbly and often fails. +## Deleting a file via diff is NOT recommended. It's blabbly and often fails. - Unified diff representation: - Header: --- a/path and +++ /dev/null - Body: all lines removed - Equivalent CLI: - git rm -- path/to/file - -Creating a file via diff +## Creating a file via diff - Unified diff representation: - Header: --- /dev/null and +++ b/path/to/file - Hunk: @@ -0,0 +N,N @@ followed by the full file body - Equivalent CLI: - Ensure parent directory exists (mkdir -p ...), write the file, then git add path/to/file - -Line endings and final newline +## Line endings and final newline - Requirement: LF endings and a final trailing newline. -- Fix: applyPatch ensures a final newline; if a tool converts CRLF->LF or vice-versa, re-peek to re-sync before next patch. - -Clipboard issues (non-macOS) +- Fix: `applyPatch` ensures a final newline; if a tool converts CRLF->LF or vice-versa, re-peek to re-sync before the next patch. +## Clipboard issues (non-macOS) - Only macOS has been tested. Linux/Windows should work, and the assistant can provide adapted scripts as needed. - -Out-of-band local edits +## Out-of-band local edits - If you change files locally between peeks, the assistant's next diff may not match. - Fix: say what changed or re-share a fresh peek of the affected ranges; the assistant will rebase the patch. - -"I only do one thing at a time" +## "I only do one thing at a time" - That's by design. The assistant batches requests so you run exactly one command per step (peek, apply, build/test). - If a step looks risky or unclear, say so; the assistant will simplify or explain. If the assistant asks for several separate peeks, remind them that this makes busywork for you and ask commands to always be grouped. - -When in doubt: re-peek wider (with context) +## When in doubt: re-peek wider (with context) - The fastest way to unstick is to re-peek a generously sized, numbered slice around the target lines. - Assistant should err on the side of more context to avoid a second re-peek: - For a small edit: request ±30–80 lines around the target. - For a medium edit or uncertain location: request the whole function/section or ±100–200 lines. - Never guess bytes. Re-peek, then rebase the patch precisely. - -Example re-peek bundle (one command, generous windows) - - { - echo "=== Sources/Feature/FileA.swift (1-220) ===" - nl -ba Sources/Feature/FileA.swift | sed -n '1,220p' - echo - echo "=== Sources/Feature/ModuleB.swift (100-320) ===" - nl -ba Sources/Feature/ModuleB.swift | sed -n '100,320p' - echo - echo "=== Tests/ModuleBTests.swift (1-200) ===" - nl -ba Tests/ModuleBTests.swift | sed -n '1,200p' - } | toClip +## Example re-peek bundle (one command, generous windows) +~~~bash +{ + echo "=== Sources/Feature/FileA.swift (1-220) ===" + nl -ba -s'|' Sources/Feature/FileA.swift | sed -n '1,220p' + echo + echo "=== Sources/Feature/ModuleB.swift (100-320) ===" + nl -ba -s'|' Sources/Feature/ModuleB.swift | sed -n '100,320p' + echo + echo "=== Tests/ModuleBTests.swift (1-200) ===" + nl -ba -s'|' Tests/ModuleBTests.swift | sed -n '1,200p' +} | toClip +~~~ UI mangled my fenced blocks (patches with embedded ```bash, etc.) - Symptom: The chat UI or clipboard strips/rewraps inner fences, or drops the final newline, breaking a unified diff. diff --git a/docs/03-scripts.md b/docs/03-scripts.md index 65380db..ead211f 100644 --- a/docs/03-scripts.md +++ b/docs/03-scripts.md @@ -31,6 +31,7 @@ These helpers keep the loop fast and consistent. They are small, transparent, an - Rejects TAB characters in patches by default (to avoid accidentally copying `nl -ba` delimiters into real files). - Override (rare): set `APPLY_PATCH_ALLOW_TABS=1`. - Runs git apply --check first, then applies if clean. + - Prefers `nl -ba -s'|'` for peeks to avoid inserting TAB characters. - Streams stdout+stderr to terminal and copies the same output to the clipboard via toClip (tee). - If the chat UI mangles fenced blocks inside patches, ask the assistant for a here-doc command to write the file(s) locally, then review with: git diff -- path/to/file diff --git a/docs/04-philosophy.md b/docs/04-philosophy.md index ce00c8a..07fab92 100644 --- a/docs/04-philosophy.md +++ b/docs/04-philosophy.md @@ -34,7 +34,7 @@ Guiding principles (non-negotiable) - One action per step. No surprises. If anything is unclear, ask. - All changes must be intentional and reviewable. Small, explicit diffs only. 2) Peek before patch - - Always request a precise, numbered peek (nl -ba + sed) of the exact lines you will edit—right before drafting a diff. + - Always request a precise, numbered peek (nl -ba -s'|' + sed) of the exact lines you will edit—right before drafting a diff. - Do not guess file contents or rely on memory. Drift happens; peeking prevents failure. 3) Strict descending diffs - Patch hunks must apply bottom-to-top in a file. This minimizes offset churn mid-apply. @@ -208,7 +208,7 @@ Design patterns we favor Example: a11y and DI seam done right (pseudo) Note: Examples are illustrative and stack-agnostic. Adapt patterns to your language/tooling. -```swift +~~~swift struct ChorusLabView: View { let voicesProvider: any SystemVoicesProvider let engineFactory: () -> RealVoiceIO @@ -220,14 +220,14 @@ struct ChorusLabView: View { .accessibilityIdentifier("vk.voicesList") } } -``` +~~~ Key notes: - Inject both the provider and factory; default them in init for production. - Keep identifiers invisible to users; they’re for tests and clarity. - Add doc comments to the init describing the seams. Example: tests as usage docs (pseudo) -```swift +~~~swift final class AccessibilitySmokeTests: XCTestCase { @MainActor func testKeyControls() throws { @@ -241,7 +241,7 @@ final class AccessibilitySmokeTests: XCTestCase { // ... } } -``` +~~~ Key notes: - Tests name what they prove; assertions read like documentation. - Fakes and identifiers make this deterministic and readable. diff --git a/handoff.md b/handoff.md index 756693b..b4619b7 100644 --- a/handoff.md +++ b/handoff.md @@ -1,50 +1,34 @@ -# Handoff for GPT assistants (and humans) - -This repo defines a clipboard-first workflow for pair-programming with GPT on real codebases. - -The operator is always a human with a shell and git. -The assistant is a fresh instance of GPT (or similar) that reads peeks, proposes patches, and iterates. - -This handoff is for you, the assistant, so you start aligned with how this workflow actually works in practice. - + # Handoff for GPT assistants (and humans) + This repo defines a clipboard-first workflow for pair-programming with GPT on real codebases. + The operator is always a human with a shell and git. + The assistant is a fresh instance of GPT (or similar) that reads peeks, proposes patches, and iterates. + This handoff is for you, the assistant, so you start aligned with how this workflow actually works in practice. --- - ## Ground rules - - You never run commands directly. You only suggest short, safe, copy-pasteable shell snippets. - The operator: - Runs 'scripts/sharefiles' in this repo once per chat to share these docs and scripts. - Runs 'sharefiles' (symlink/alias) in the target project repo once per chat to share that codebase. - Uses 'applyPatch' to apply diffs you create. - Uses their own test/build aliases (for example 'build', 'test'). - You must follow these patterns. - ### 1. Peek before patch - - Always request a numbered, read-only peek of every file or range you will touch: - ~~~bash { echo "=== path/to/File.swift (START-END) ===" - nl -ba path/to/File.swift | sed -n 'START,ENDp' + nl -ba -s'|' path/to/File.swift | sed -n 'START,ENDp' # more files/ranges here... } | toClip ~~~ - - Base your diff only on what you just saw in that peek, not on memory. - -### Important: `nl -ba` inserts TAB delimiters in peeks - -- Peeks use `nl -ba`, which inserts a literal TAB character between the line number and the real file content. -- That TAB is a display delimiter only. It is not part of the file bytes. -- Do not introduce tabs into patches by copying peek formatting. -- In this workflow, tabs in patches are treated as errors. - +- **Note:** Prefer `nl -ba -s'|'` for peeks to avoid inserting TAB characters into patches, which can cause apply issues. +### Note: prefer `nl -ba -s'|'` for peeks +- By default, `nl` uses a TAB delimiter between the line number and the file content. +- This workflow prefers `-s'|'` so the delimiter is a visible pipe character instead. +- Avoid copying any peek delimiter into patches (TABs or pipes); patches should match the file bytes. ### 2. Use the patch checklist after you generate a diff - After you have written a diff, mechanically walk the checklist from 'docs/02-troubleshooting.md': - - Every '@@' hunk has: - At least one context line before and after. - At least one '+' or '-' line (no ghost hunks). @@ -52,123 +36,77 @@ After you have written a diff, mechanically walk the checklist from 'docs/02-tro - Hunks are in descending order per file (bottom-to-top). - No tab characters (those come only from 'nl', never from the real file). - The whole diff is a single fenced block and ends with a newline. - If a hunk fails any of those, fix it before you send. - ### 3. One action per step - - One command: peek. - One command: applyPatch. - One command: run tests (via the operator's alias). - Do not mix peeks, patches, and tests in one big shell block. - ### 4. Respect local build and test aliases - The operator prefers project-local aliases instead of hard-coded scripts. For example, for an Xcode project they may use something like: - ~~~bash build='(xcodebuild -scheme VoiceLogin -destination '\''platform=iOS Simulator,name=iPhone SE (3rd generation)'\'' test) 2>&1 | tee >(tail -n 100 | toClip)' test="$build" ~~~ - Note that the 'tail -n 100' step should be adjusted to suit the assistant and avoid spam. - You should: - - Notice repeated actions and help the operator design or adjust aliases for their project and platform. For example, a SwiftLint helper: - ~~~bash lint=$'{\n echo "=== swiftlint (current) ==="\n swiftlint lint --quiet\n} 2>&1 | toClip' ~~~ - - Not assume any particular scheme, destination, or tool. Always ask what they already use (for example: "What is your 'test' alias right now?"). - --- - ## Recommended flow for a fresh assistant - ### 1. Ask for this repo snapshot (if not already provided) - Ask the operator to run: - ~~~text From the 'gpt-pair-workflow' repo, please run: - scripts/sharefiles - and paste the clipboard here. Then from the project repo you want to work on, run: - sharefiles - and paste that snapshot too. ~~~ - This gives you: - - Repo meta. - The helper scripts in 'scripts/'. - The docs under 'docs/'. - ### 2. Confirm current goal and local aliases - Ask things like: - - "What is your current goal in this project?" - "How do you usually run tests here? Do you have a 'test' alias or a script?" - Assume: - - You are working from the project root (the operator has said this explicitly). - 'test' and 'build' (or similar) are the right commands to run. - ### 3. First change: tiny and low risk - - Propose one very small, concrete change (for example, remove a single SwiftLint error, add a comment, or adjust one function). - Request a generous peek only for what you will touch. - Return a small diff, then ask the operator to run 'applyPatch' and their 'test' alias. - ### 4. Iterate - After each patch: - - The operator runs tests ('test') and lints (for example 'lint'), then pastes the result. - You fix only what is broken or what they ask you to improve next. - Keep each patch small and justifiable in a sentence or two. - ### 5. When latency or confusion grows - Ask the operator if they would like a handoff: - - Summarize: - Current goal. - What is done. - What is failing. - The obvious next step. - They can paste that handoff into a fresh instance of you. - --- - ## Notes about this repo specifically - 'scripts/' contains: - - 'sharefiles', 'applyPatch', 'fix-diff-counts.sh', 'toClip', 'fromClip'. - Treat these as primitives: - - 'sharefiles' to share repo contents. - 'applyPatch' to apply diffs from the clipboard. - 'toClip' and 'fromClip' as clipboard adapters. - You should help the operator design per-project aliases or custom scripts to build and test their code, always funneling output through 'toClip' so it is easy to paste back into chat. - Docs: - - 'docs/00-overview.md' — high-level concept and cadence. - 'docs/01-quick-start.md' — step-by-step loop. - 'docs/02-troubleshooting.md' — patch/apply issues, plus the patch checklist. - 'docs/03-scripts.md' — script reference. - 'docs/04-philosophy.md' — expectations for style, tests, and assistant behavior. - -If you are ever unsure, ask one clarifying question and propose one best-guess option rather than many. The goal is to keep the operator's effort low: one peek, one patch, one test run. Small, boringly reliable steps. \ No newline at end of file +If you are ever unsure, ask one clarifying question and propose one best-guess option rather than many. The goal is to keep the operator's effort low: one peek, one patch, one test run. Small, boringly reliable steps. From 919ce489478edf63fbcd13c950ab00095b6ac1d7 Mon Sep 17 00:00:00 2001 From: Robert Doggett Date: Thu, 1 Jan 2026 01:11:58 -0800 Subject: [PATCH 3/3] Enhance troubleshooting documentation and scripts --- README.md | 2 + docs/02-troubleshooting.md | 69 +++++++++++++++++++++++++++++++-- handoff.md | 5 +++ scripts/fix-diff-counts.sh | 78 -------------------------------------- 4 files changed, 73 insertions(+), 81 deletions(-) delete mode 100755 scripts/fix-diff-counts.sh diff --git a/README.md b/README.md index ebf1a1d..f8b23f0 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Operator cheat sheet (3 steps) Notes - Peeks are read-only; patches are explicit. Prefer generous peeks to avoid re-peeks. - Hunks in diffs should be listed bottom-to-top per file (descending order). +- Avoid anchorless hunks: every `@@` hunk must include real, unchanged context lines above and below. +- Prefer small hunks: one clear change per hunk beats a huge “miles of context” hunk. Requirements - git, bash; macOS, Linux, or Windows terminal. Basic command-line literacy. diff --git a/docs/02-troubleshooting.md b/docs/02-troubleshooting.md index ba41e1e..0c02ff0 100644 --- a/docs/02-troubleshooting.md +++ b/docs/02-troubleshooting.md @@ -60,8 +60,8 @@ There are **no** lines starting with `+` or `-` between those `@@` headers. - Fix: - Ensure each hunk includes at least one unchanged context line before and after the modified region. - When in doubt, widen the context window (one or two extra unchanged lines above and below) so `applyPatch` has a stable anchor. -## Triple-backtick blocks inside patches (nested fences) -- Symptom: Chat UI or clipboard mangles a patch that contains fenced code blocks (like ```bash). +## Nested fences inside patches (avoid them in docs) +- Symptom: Chat UI or clipboard mangles a patch that contains fenced code blocks (for example, a markdown block inside a diff). - Fix options: - The assistant uses only unified diff fences for patches; internal example fences are allowed and should be preserved. - If your UI still mangles them, ask for a "full-file replacement" instead of a diff for that file, or request a here-doc shell command to write the file bytes locally and then git diff it yourself. @@ -114,7 +114,7 @@ There are **no** lines starting with `+` or `-` between those `@@` headers. } | toClip ~~~ -UI mangled my fenced blocks (patches with embedded ```bash, etc.) +UI mangled my fenced blocks (patches with embedded ~~~bash, etc.) - Symptom: The chat UI or clipboard strips/rewraps inner fences, or drops the final newline, breaking a unified diff. - Quick fixes: - Ask the assistant for a here-doc command to write the file locally, for example: @@ -129,6 +129,67 @@ Avoid pasting sensitive or very large files - Review code for embedded credentials or other secrets before running `sharefiles`. - If a file is too large or sensitive, skip sharing it and describe it instead, or share only the minimal numbered slices needed for the change. +## Avoid anchorless hunks (a common AI failure mode) +- Problem: assistants sometimes emit hunks with no stable context (“anchorless hunks”). These often fail with: + - `error: corrupt patch at line N`, or + - `error: while searching for: ...` +- Rule: every `@@` hunk must include **real, unchanged context lines** before and after the edited lines. +- Bad (anchorless) hunk example (do not send): +~~~diff +@@ -240,0 +241,3 @@ ++new line ++another new line ++third new line +~~~ +- Better: include unchanged lines above and below so `git apply` can match: +~~~diff +@@ -238,5 +238,6 @@ + func foo() { + bar() ++ baz() + qux() + } +~~~ + +## Prefer small hunks (easier to debug, higher apply rate) +- Huge hunks with “miles of context” are fragile and hard to troubleshoot when they fail. +- Prefer multiple small hunks, each with one clear intent, and a few stable context lines. +- If a patch fails: + - stop + - re-peek the exact region + - regenerate a small, well-anchored hunk from the peek + +## Never invent anchors (patch context must match the peek) +- A common AI failure mode is to “patch what the file probably says” instead of what the peek shows. +- Rule: every context line in a diff must be copied exactly from the most recent peek. +- If you can’t find a good anchor in the peek, request a re-peek with a wider window. + +## Suffix anchors for insertions (especially important) +- Insertions need stable context *before* and *after* the inserted block. +- Avoid using a blank line as your only suffix anchor; anchor to a real line (often the next header). + +Mini gallery (good vs bad) + +- Bad: anchorless insertion (fragile; likely to fail) +~~~diff +@@ -240,0 +241,3 @@ ++new line ++another new line ++third new line +~~~ + +- Good: insertion with a real suffix anchor line +~~~diff +@@ -38,7 +38,10 @@ + If a hunk fails any of those, fix it before you send. + ++Practical enforcement (required): ++- Every patch response must include: `Checklist: PASS (peeked; anchored; no tabs; docs: ~~~ only)` + + ### 3. One action per step + - One command: peek. +~~~ + ## Patch checklist for assistants (please actually use this) Before you send a patch, walk this checklist after emitting the patch bytes. We've tried this many times @@ -143,10 +204,12 @@ Failing to follow it becomes obvious quickly, because most patches will fail to 1. **Fresh peek** - [ ] Did I request `nl -ba … | sed -n 'start,endp'` for every file and range I am touching? - [ ] Is the patch based on exactly what I just saw in that peek (not on memory from earlier)? + - [ ] Did I avoid inventing/paraphrasing anchor text that "seems like it should be there"? 2. **Anchors (per hunk)** - [ ] Does each `@@` hunk include at least one unchanged context line **before and after** the edited lines? - [ ] Do those context lines match the peeked file byte-for-byte (just with a leading space in the diff)? + - [ ] For insertions: do I have a real suffix anchor line after the inserted block (not a blank line)? 3. **No ghost hunks** - [ ] Does every hunk have at least one `+` or `-` line? diff --git a/handoff.md b/handoff.md index b4619b7..8cdfb83 100644 --- a/handoff.md +++ b/handoff.md @@ -37,6 +37,11 @@ After you have written a diff, mechanically walk the checklist from 'docs/02-tro - No tab characters (those come only from 'nl', never from the real file). - The whole diff is a single fenced block and ends with a newline. If a hunk fails any of those, fix it before you send. + +Practical enforcement (required): +- Every patch response must include: `Checklist: PASS (peeked; anchored; no tabs; docs: ~~~ only)` +- If that line is missing, the operator should not apply the patch; request a re-peek instead. + ### 3. One action per step - One command: peek. - One command: applyPatch. diff --git a/scripts/fix-diff-counts.sh b/scripts/fix-diff-counts.sh deleted file mode 100755 index a0c1b12..0000000 --- a/scripts/fix-diff-counts.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -# Recompute unified diff hunk counts (oldLen/newLen) and ensure a trailing newline. -# Usage: -# fix-diff-counts.sh < patch.diff > patch.fixed.diff -# fix-diff-counts.sh patch.diff > patch.fixed.diff - -set -euo pipefail - -awk ' -function flush_hunk( oldLen,newLen,i) { if (!in_hunk) return; oldLen = c + d - newLen = c + a - # Drop "ghost hunks" (no additions/deletions). These can be generated accidentally and - # may cause git apply to fail with "corrupt patch", even though they encode no change. - if ((a + d) == 0) { - in_hunk=0; nb=0; c=0; d=0; a=0; tail="" - return - } - # Re-emit corrected header, preserving any tail after @@ - printf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLen, newStart, newLen, tail) - for (i=1; i<=nb; i++) print body[i] - # reset - in_hunk=0; nb=0; c=0; d=0; a=0; tail="" -} - -BEGIN { in_hunk=0; nb=0; c=0; d=0; a=0; tail="" } - -{ - line = $0 - - if (substr(line,1,2)=="@@") { - # Starting a new hunk: flush the previous one first - flush_hunk() - # Extract numbers in order: oldStart[,oldLen] newStart[,newLen] - s = line - i = 0 - while (match(s, /[0-9]+/)) { - i++ - nums[i] = substr(s, RSTART, RLENGTH) - s = substr(s, RSTART + RLENGTH) - } - oldStart = (i>=1 ? nums[1]+0 : 0) - newStart = (i>=3 ? nums[3]+0 : (i>=2 ? nums[2]+0 : 0)) - p1 = index(line, "@@"); s2 = substr(line, p1+2); p2 = index(s2, "@@"); tail = (p2>0 ? substr(s2, p2+2) : "") - in_hunk=1; nb=0; c=0; d=0; a=0 - next - } - - # End of current hunk when a new header/file header begins - if (in_hunk && (substr(line,1,2)=="@@" || substr(line,1,4)=="diff" || substr(line,1,3)=="---" || substr(line,1,3)=="+++")) { - flush_hunk() - print line - next - } - - if (in_hunk) { - # Count by prefix; do not count the special backslash line - first = substr(line,1,1) - if (first == " ") c++ - else if (first == "-") d++ - else if (first == "+") a++ - else if (first == "\\") { ; } - # Buffer body lines verbatim - body[++nb] = line - next - } - - # Pass-through for non-hunk lines - print line -} - -END { - flush_hunk() - # Ensure the output ends with a single newline: - # awk print/printf end lines with LF, so no extra action needed. - # If the input lacked a final newline inside a hunk, our reprint adds it. -} -' "${1:-/dev/stdin}" -