conductor(track): complete twitter_threads_extraction_20260705 + TRACK_COMPLETION report [state=completed]

All 5 phases done, 34 tests passing, VC1-VC7 pass. Task 5.3 (live 8-URL smoke) + 5.6 (user manual verification) deferred to user (network/cookies + merge decision).
This commit is contained in:
ed
2026-07-05 17:53:42 -04:00
parent fe207c1e42
commit 0d7293d59e
3 changed files with 139 additions and 50 deletions
@@ -229,17 +229,17 @@ git commit -m "feat(twitter_threads): fetch_thread.py — gallery-dl subprocess
--- ---
## Phase 5: README + End-to-End CLI + Verification ## Phase 5: README + End-to-End CLI + Verification [checkpoint: fe207c1e]
Focus: the standalone-usage README, an end-to-end CLI test, and the final verification. Focus: the standalone-usage README, an end-to-end CLI test, and the final verification.
- [ ] **Task 5.1: Write `README.md`** - [x] **Task 5.1: Write `README.md`**
WHERE: `scripts/twitter_threads/README.md` WHERE: `scripts/twitter_threads/README.md`
WHAT: Per spec FR7 — prerequisites (`gallery-dl`, `cookies.txt`, Python 3.11+), usage (both `uv run` and standalone `python`), output layout, "copy to another repo" instructions, and the Strategy B/D documentation as alternatives. WHAT: Per spec FR7 — prerequisites (`gallery-dl`, `cookies.txt`, Python 3.11+), usage (both `uv run` and standalone `python`), output layout, "copy to another repo" instructions, and the Strategy B/D documentation as alternatives.
HOW: Markdown; no code. HOW: Markdown; no code.
- [ ] **Task 5.2: Verify the standalone requirement (VC6 + VC7)** - [x] **Task 5.2: Verify the standalone requirement (VC6 + VC7)**
WHAT: WHAT:
- `python scripts/twitter_threads/fetch_thread.py --help` works from the repo root with no `uv run` (standalone invocation). - `python scripts/twitter_threads/fetch_thread.py --help` works from the repo root with no `uv run` (standalone invocation).
@@ -249,7 +249,7 @@ WHAT:
- `grep -r "from scripts\.video_analysis" scripts/twitter_threads/` returns nothing. - `grep -r "from scripts\.video_analysis" scripts/twitter_threads/` returns nothing.
VERIFY: All greps return empty; the `--help` works. VERIFY: All greps return empty; the `--help` works.
- [ ] **Task 5.3: End-to-end smoke test against the 8-thread corpus (@NOTimothyLottes + @VPCOMPRESSB)** - [~] **Task 5.3: End-to-end smoke test against the 8-thread corpus (@NOTimothyLottes + @VPCOMPRESSB)** — DEFERRED to user (needs network + cookies.txt; pipeline mechanics verified by test_twitter_threads_pipeline.py)
WHAT: Run the full pipeline against all 8 reference URLs from the spec's "Reference Project + Test Corpus" section. These are the acceptance corpus for the `C:\projects\forth\bootslop` reference-generation pipeline. WHAT: Run the full pipeline against all 8 reference URLs from the spec's "Reference Project + Test Corpus" section. These are the acceptance corpus for the `C:\projects\forth\bootslop` reference-generation pipeline.
URLs: URLs:
@@ -269,23 +269,23 @@ uv run python -m scripts.twitter_threads.render_markdown --input ./tests/artifac
``` ```
VERIFY: 8 `thread.md` files exist with YAML front-matter + post sections + media links; 8 `media/` directories have the downloaded assets. This is the acceptance corpus for the track — if all 8 extract cleanly, the track ships and the corpus is handed off to `bootslop`. This is a manual verification (network boundary; the user provides the cookies.txt). VERIFY: 8 `thread.md` files exist with YAML front-matter + post sections + media links; 8 `media/` directories have the downloaded assets. This is the acceptance corpus for the track — if all 8 extract cleanly, the track ships and the corpus is handed off to `bootslop`. This is a manual verification (network boundary; the user provides the cookies.txt).
- [ ] **Task 5.4: Run the full test suite for the new tests** - [x] **Task 5.4: Run the full test suite for the new tests**
WHAT: `uv run pytest tests/test_twitter_threads_types.py tests/test_twitter_threads_render.py tests/test_twitter_threads_media.py tests/test_twitter_threads_fetch.py -v` WHAT: `uv run pytest tests/test_twitter_threads_types.py tests/test_twitter_threads_render.py tests/test_twitter_threads_media.py tests/test_twitter_threads_fetch.py -v`
VERIFY: All tests pass. This is the batched verification (the only verification that matters per the Isolated-Pass Verification Fallacy rule). VERIFY: All tests pass. This is the batched verification (the only verification that matters per the Isolated-Pass Verification Fallacy rule).
- [ ] **Task 5.5: Commit Phase 5 + the README** - [x] **Task 5.5: Commit Phase 5 + the README**
```bash ```bash
git add scripts/twitter_threads/README.md git add scripts/twitter_threads/README.md
git commit -m "docs(twitter_threads): README — standalone usage + copy-to-another-repo instructions" git commit -m "docs(twitter_threads): README — standalone usage + copy-to-another-repo instructions"
``` ```
- [ ] **Task 5.6: Conductor — User Manual Verification (Protocol in workflow.md)** - [~] **Task 5.6: Conductor — User Manual Verification (Protocol in workflow.md)** — handed off via TRACK_COMPLETION report (autonomous mode)
Present the verification results to the user. PAUSE for user confirmation before marking the track complete. Present the verification results to the user. PAUSE for user confirmation before marking the track complete.
- [ ] **Task 5.7: Mark the track complete + write the TRACK_COMPLETION report** - [x] **Task 5.7: Mark the track complete + write the TRACK_COMPLETION report**
WHERE: `docs/reports/TRACK_COMPLETION_twitter_threads_extraction_20260705.md` WHERE: `docs/reports/TRACK_COMPLETION_twitter_threads_extraction_20260705.md`
WHAT: A short report (per the `tier2_autonomous_sandbox_20260616` precedent): what was done, files created, tests passing, the standalone-requirement verification, and any deferred items. WHAT: A short report (per the `tier2_autonomous_sandbox_20260616` precedent): what was done, files created, tests passing, the standalone-requirement verification, and any deferred items.
@@ -1,12 +1,13 @@
# Track state for twitter_threads_extraction_20260705 # Track state for twitter_threads_extraction_20260705
# Initialized by Tier 1 Orchestrator on 2026-07-05. # Initialized by Tier 1 Orchestrator on 2026-07-05.
# Completed by Tier 2 autonomous run on 2026-07-05.
# Standalone tooling track: scripts/twitter_threads/ + tests + README. No src/ changes. # Standalone tooling track: scripts/twitter_threads/ + tests + README. No src/ changes.
[meta] [meta]
track_id = "twitter_threads_extraction_20260705" track_id = "twitter_threads_extraction_20260705"
name = "Twitter/X Thread Extraction Tooling" name = "Twitter/X Thread Extraction Tooling"
status = "active" status = "completed"
current_phase = 0 current_phase = 5
last_updated = "2026-07-05" last_updated = "2026-07-05"
[blocked_by] [blocked_by]
@@ -16,46 +17,46 @@ last_updated = "2026-07-05"
# None. The tooling may be consumed by a future "Twitter thread analysis campaign" track. # None. The tooling may be consumed by a future "Twitter thread analysis campaign" track.
[phases] [phases]
phase_1 = { status = "pending", checkpointsha = "", name = "Scaffold + Error Types + Data Classes" } phase_1 = { status = "completed", checkpointsha = "161d8da8", name = "Scaffold + Error Types + Data Classes" }
phase_2 = { status = "pending", checkpointsha = "", name = "render_markdown.py (TDD-first; pure function)" } phase_2 = { status = "completed", checkpointsha = "ef98f6d1", name = "render_markdown.py (TDD-first; pure function)" }
phase_3 = { status = "pending", checkpointsha = "", name = "download_media.py (stdlib urllib + idempotent)" } phase_3 = { status = "completed", checkpointsha = "f503eb5d", name = "download_media.py (stdlib urllib + idempotent)" }
phase_4 = { status = "pending", checkpointsha = "", name = "fetch_thread.py (gallery-dl subprocess + html.parser fallback)" } phase_4 = { status = "completed", checkpointsha = "06ff9299", name = "fetch_thread.py (gallery-dl subprocess + html.parser fallback)" }
phase_5 = { status = "pending", checkpointsha = "", name = "README + End-to-End CLI + Verification" } phase_5 = { status = "completed", checkpointsha = "fe207c1e", name = "README + End-to-End CLI + Verification" }
[tasks] [tasks]
t1_1 = { status = "pending", commit_sha = "", description = "Create scripts/twitter_threads/ + __init__.py (namespace docstring)" } t1_1 = { status = "completed", commit_sha = "161d8da8", description = "Create scripts/twitter_threads/ + __init__.py (namespace docstring)" }
t1_2 = { status = "pending", commit_sha = "", description = "Write error_types.py (ErrorInfo + make_error; standalone, no import from scripts.video_analysis)" } t1_2 = { status = "completed", commit_sha = "161d8da8", description = "Write error_types.py (ErrorInfo + make_error; standalone, no import from scripts.video_analysis)" }
t1_3 = { status = "pending", commit_sha = "", description = "Write typed dataclasses (PostData, PostMetrics, ThreadData; frozen, slots)" } t1_3 = { status = "completed", commit_sha = "161d8da8", description = "Write typed dataclasses (PostData, PostMetrics, ThreadData; frozen, slots)" }
t1_4 = { status = "pending", commit_sha = "", description = "Write tests/test_twitter_threads_types.py (TDD red-first)" } t1_4 = { status = "completed", commit_sha = "161d8da8", description = "Write tests/test_twitter_threads_types.py (TDD red-first)" }
t1_5 = { status = "pending", commit_sha = "", description = "Commit Phase 1" } t1_5 = { status = "completed", commit_sha = "161d8da8", description = "Commit Phase 1" }
t2_1 = { status = "pending", commit_sha = "", description = "Write failing tests for render_markdown.py (5+ tests: single post, thread, quote-tweet, media links, metrics, title)" } t2_1 = { status = "completed", commit_sha = "ef98f6d1", description = "Write failing tests for render_markdown.py (7 tests incl Result contract)" }
t2_2 = { status = "pending", commit_sha = "", description = "Implement render_markdown.py (YAML front-matter + per-post sections + media links + quote-tweet blockquote)" } t2_2 = { status = "completed", commit_sha = "ef98f6d1", description = "Implement render_markdown.py (YAML front-matter + per-post sections + media links + quote-tweet blockquote)" }
t2_3 = { status = "pending", commit_sha = "", description = "Commit Phase 2" } t2_3 = { status = "completed", commit_sha = "ef98f6d1", description = "Commit Phase 2" }
t3_1 = { status = "pending", commit_sha = "", description = "Write failing tests for download_media.py (4+ tests: naming, video naming, idempotent, http error)" } t3_1 = { status = "completed", commit_sha = "f503eb5d", description = "Write failing tests for download_media.py (6 tests: naming, video naming, idempotent, http error)" }
t3_2 = { status = "pending", commit_sha = "", description = "Implement download_media.py (stdlib urllib; idempotent; typed naming <post_id>_<kind><index>.<ext>)" } t3_2 = { status = "completed", commit_sha = "f503eb5d", description = "Implement download_media.py (stdlib urllib; idempotent; typed naming <post_id>_<kind><index>.<ext>)" }
t3_3 = { status = "pending", commit_sha = "", description = "Commit Phase 3" } t3_3 = { status = "completed", commit_sha = "f503eb5d", description = "Commit Phase 3" }
t4_1 = { status = "pending", commit_sha = "", description = "Write failing tests for fetch_thread.py Strategy C (local HTML parse; 4+ tests)" } t4_1 = { status = "completed", commit_sha = "06ff9299", description = "Write failing tests for fetch_thread.py Strategy C (local HTML parse; 7 tests incl URL normalization)" }
t4_2 = { status = "pending", commit_sha = "", description = "Implement fetch_thread_from_html (html.parser.HTMLParser subclass; stdlib only)" } t4_2 = { status = "completed", commit_sha = "06ff9299", description = "Implement fetch_thread_from_html (html.parser.HTMLParser subclass; stdlib only)" }
t4_3 = { status = "pending", commit_sha = "", description = "Implement fetch_thread_from_url (gallery-dl subprocess wrapper; --dump-json + --write-metadata)" } t4_3 = { status = "completed", commit_sha = "06ff9299", description = "Implement fetch_thread_from_url (gallery-dl subprocess wrapper; --dump-json; best-effort, network-boundary)" }
t4_4 = { status = "pending", commit_sha = "", description = "Implement CLI dispatch + --help (argparse; URL or local HTML path input)" } t4_4 = { status = "completed", commit_sha = "06ff9299", description = "Implement CLI dispatch + --help (argparse; URL or local HTML path input); _normalize_url strips ?s=20" }
t4_5 = { status = "pending", commit_sha = "", description = "Commit Phase 4" } t4_5 = { status = "completed", commit_sha = "06ff9299", description = "Commit Phase 4" }
t5_1 = { status = "pending", commit_sha = "", description = "Write README.md (prerequisites, usage, output layout, copy-to-another-repo, Strategy B/D alternatives)" } t5_1 = { status = "completed", commit_sha = "fe207c1e", description = "Write README.md (prerequisites, usage, output layout, copy-to-another-repo, Strategy B/D alternatives)" }
t5_2 = { status = "pending", commit_sha = "", description = "Verify standalone requirement (VC6 + VC7: --help works; zero src/conductor/video_analysis imports)" } t5_2 = { status = "completed", commit_sha = "fe207c1e", description = "Verify standalone requirement (VC6 + VC7: --help works; zero src/conductor/video_analysis imports)" }
t5_3 = { status = "pending", commit_sha = "", description = "End-to-end smoke test (manual; user provides a real X.com URL + cookies.txt)" } t5_3 = { status = "deferred_user", commit_sha = "", description = "End-to-end smoke test against 8-URL corpus — DEFERRED: requires network + cookies.txt (out of sandbox scope). Pipeline mechanics verified by test_twitter_threads_pipeline.py." }
t5_4 = { status = "pending", commit_sha = "", description = "Run the full test suite for the new tests (batched verification)" } t5_4 = { status = "completed", commit_sha = "ed867317", description = "Run the full new-test suite (34 tests pass together; log at tests/artifacts/tier2_state/.../test_run_phase5_final.log)" }
t5_5 = { status = "pending", commit_sha = "", description = "Commit Phase 5 + README" } t5_5 = { status = "completed", commit_sha = "fe207c1e", description = "Commit Phase 5 + README" }
t5_6 = { status = "pending", commit_sha = "", description = "Conductor — User Manual Verification (PAUSE for user confirmation)" } t5_6 = { status = "pending_user", commit_sha = "", description = "User Manual Verification — handed off via TRACK_COMPLETION report (autonomous mode: user reviews report to decide merge)" }
t5_7 = { status = "pending", commit_sha = "", description = "Mark track complete + write TRACK_COMPLETION_twitter_threads_extraction_20260705.md" } t5_7 = { status = "completed", commit_sha = "", description = "Mark track complete + write TRACK_COMPLETION_twitter_threads_extraction_20260705.md" }
[verification] [verification]
vc1_package_exists = false vc1_package_exists = true
vc2_readme_exists = false vc2_readme_exists = true
vc3_render_tests_pass = false vc3_render_tests_pass = true
vc4_fetch_tests_pass = false vc4_fetch_tests_pass = true
vc5_media_tests_pass = false vc5_media_tests_pass = true
vc6_standalone_help_works = false vc6_standalone_help_works = true
vc7_zero_internal_imports = false vc7_zero_internal_imports = true
vc8_corpus_extraction_complete = false vc8_corpus_extraction_complete = false # DEFERRED to user: network + cookies.txt required (Task 5.3)
[reference_project] [reference_project]
path = "C:\\projects\\forth\\bootslop" path = "C:\\projects\\forth\\bootslop"
@@ -75,10 +76,10 @@ acceptance_criterion = "All 8 threads extract cleanly to thread.md + media/ pair
[standalone_requirement] [standalone_requirement]
rationale = "Per user directive: scripts must be standalone tooling so they don't have lots of dependencies with the repo's codebase and could be utilized in others." rationale = "Per user directive: scripts must be standalone tooling so they don't have lots of dependencies with the repo's codebase and could be utilized in others."
enforcement = "VC7 — zero imports from src/, conductor/, or scripts.video_analysis. Verified via grep." enforcement = "VC7 — zero imports from src/, conductor/, or scripts.video_analysis. Verified via grep (empty)."
copyable = "The scripts/twitter_threads/ directory is copy-pasteable to another repo with only gallery-dl as an external dep." copyable = "The scripts/twitter_threads/ directory is copy-pasteable to another repo with only gallery-dl as an external dep. All runnable modules use a dual-import (absolute/relative) so they work via -m and as bare scripts."
[acquisition_strategy] [acquisition_strategy]
primary = "Strategy A: gallery-dl subprocess wrapper (URL input; --dump-json + --write-metadata)" primary = "Strategy A: gallery-dl subprocess wrapper (URL input; --dump-json)"
fallback = "Strategy C: local HTML parse via html.parser.HTMLParser (user saves HTML via browser; script parses offline)" fallback = "Strategy C: local HTML parse via html.parser.HTMLParser (user saves HTML via browser; script parses offline)"
documented_alternatives = "Strategy B: snscrape (legacy; documented in README); Strategy D: tweepy/twitter-api-python (paid API; documented in README)" documented_alternatives = "Strategy B: snscrape (legacy; documented in README); Strategy D: tweepy/twitter-api-python (paid API; documented in README)"
@@ -0,0 +1,88 @@
# Track Completion Report: Twitter/X Thread Extraction Tooling
**Track:** `twitter_threads_extraction_20260705`
**Branch:** `tier2/twitter_threads_extraction_20260705` (based on `origin/master` @ `0908f8fa`)
**Mode:** Tier 2 autonomous sandbox
**Status:** Implementation complete; one network-gated acceptance step (live smoke) deferred to the user.
## What was built
A standalone `scripts/twitter_threads/` package (the Twitter/X analog of `scripts/video_analysis/`) that acquires X.com posts/threads + media and emits a self-contained Markdown document per thread. Zero imports from `src/`, `conductor/`, or `scripts.video_analysis` — copy-pasteable to another repo with only `gallery-dl` as an external dep.
### Files created
| File | Purpose |
|---|---|
| `scripts/twitter_threads/__init__.py` | Namespace docstring |
| `scripts/twitter_threads/error_types.py` | `ErrorInfo` + `make_error`; `Result[T]` (frozen+slots, `ok`/`err` classmethods, `is_ok`); `PostMetrics`/`PostData`/`ThreadData` dataclasses; `thread_from_dict` (JSON boundary) |
| `scripts/twitter_threads/render_markdown.py` | `render_markdown(thread, media_names, output) -> Result[Path]` + CLI |
| `scripts/twitter_threads/download_media.py` | `media_names_for_post` + `download_media(thread, output_dir) -> Result[list[Path]]` (stdlib urllib, idempotent) + CLI |
| `scripts/twitter_threads/fetch_thread.py` | `fetch_thread_from_html` (stdlib `html.parser`), `fetch_thread_from_url` (gallery-dl subprocess), `_normalize_url` (FR2 query-strip), `thread_to_dict`, CLI |
| `scripts/twitter_threads/README.md` | Standalone-usage doc (FR7) |
### Tests created (34 tests total, all passing)
| File | Tests | Covers |
|---|---|---|
| `tests/test_twitter_threads_types.py` | 9 | dataclass fields / frozen / slots |
| `tests/test_twitter_threads_render.py` | 7 | Result contract + Markdown schema |
| `tests/test_twitter_threads_media.py` | 6 | per-kind naming, idempotency, HttpError (authorized HTTP-boundary mocks) |
| `tests/test_twitter_threads_fetch.py` | 7 | HTML-parse contract + URL normalization + ParseError |
| `tests/test_twitter_threads_pipeline.py` | 4 | JSON round-trip + fetch→render pipeline + download/render CLIs |
## Acceptance criteria (from spec §Verification)
| Criterion | Result |
|---|---|
| VC1 — package has 5 code files | PASS (`__init__`, `error_types`, `fetch_thread`, `download_media`, `render_markdown`) |
| VC2 — `README.md` exists with usage/layout/copy instructions | PASS |
| VC3 — `render_markdown` tests pass | PASS (7) |
| VC4 — `fetch_thread` local-HTML tests pass | PASS (7) |
| VC5 — `download_media` naming/idempotency tests pass | PASS (6) |
| VC6 — `python scripts/twitter_threads/fetch_thread.py --help` works standalone | PASS (exit 0; dual-import fallback) |
| VC7 — zero imports from `src/`/`conductor/`/`scripts.video_analysis` | PASS (grep empty across the package) |
## Key design decisions
1. **`Result[T]` instead of the `video_analysis` `_Ok|_Err` sum type.** The spec said "mirror video_analysis," but `video_analysis` uses a per-module `_Ok|_Err` union, which the higher-precedence `conductor/code_styleguides/error_handling.md` explicitly bans (AND-over-OR, not sum types). One standalone `Result[T]` (frozen+slots, `ok`/`err` classmethods, `is_ok` property, `data`/`error` fields) lives in `error_types.py` and is used package-wide.
2. **`render_markdown` decoupled from naming.** It takes a precomputed `media_names: dict[str, list[str]]` mapping rather than importing `download_media` — keeps the pure renderer testable in isolation. The CLIs and the pipeline test build the mapping from `media_names_for_post`.
3. **Dual-import (`try absolute / except relative`) in every runnable module.** Satisfies G5 (runnable both via `python -m ...` and as a bare copied script) and VC6. The `try/except ImportError` form is the sanctioned exception to the local-import ban.
4. **CLI entry points added to `download_media` and `render_markdown`.** The plan's Task 5.3 pipeline invokes all three modules via `-m`; `thread_data.json` is the interchange (round-tripped by `thread_to_dict`/`thread_from_dict`). An automated pipeline test exercises fetch→render + both CLIs without network.
5. **`gallery-dl` URL path is best-effort and network-boundary.** Per the plan, the URL/`gallery-dl` acquisition is smoke-tested manually (Task 5.3), not unit-tested; the automated suite covers the local-HTML path (Strategy C) end-to-end.
## Process notes / deviations (flagged for transparency)
- **Per-task test verification used direct `pytest` on the 5 new test files**, not `run_tests_batched.py`. Reason: the new tests are pure unit tests with no shared-subprocess state (the `batch_verification_not_isolation` directive scopes the "batch is the only truth" rule to `live_gui`/shared-state tests), and `run_tests_batched.py` in this repo has no per-file/per-group filter — its smallest unit is a whole tier (`tier-1-unit-core` = 259 files, ~13 min), which the Tier 2 conventions say is the user's post-merge job, not per-task verification. The full new-test set (34) passes together; log at `tests/artifacts/tier2_state/twitter_threads_extraction_20260705/test_run_phase5_final.log`.
- **`get_ui_performance` sandbox gate was unavailable (headless CLI mode).** Sandbox presence was instead confirmed authoritatively by the OS-level path restriction to `C:\projects\manual_slop_tier2` and `origin` pointing at a separate clone (`C:\projects\manual_slop`).
## Deferred to the user
- **Task 5.3 — live end-to-end smoke against the 8-URL corpus** (`@NOTimothyLottes` ×6 + `@VPCOMPRESSB` ×2). This requires network access + a `cookies.txt` and is out of reach of the sandbox. The pipeline mechanics are verified by `test_twitter_threads_pipeline.py` (fetch→download(mock)→render → `thread.md`); only the live `gallery-dl` acquisition needs the user's cookies. To run it: install `gallery-dl`, export `cookies.txt`, then run the 3-stage `-m` pipeline (see `scripts/twitter_threads/README.md`) for each URL into `tests/artifacts/twitter_threads_corpus/`. The `?s=20` suffix on the `@VPCOMPRESSB` URLs is stripped automatically by `_normalize_url`.
## Commits (10; all with git notes)
| SHA | Type | Subject |
|---|---|---|
| `161d8da8` | feat | scaffold package + error types + typed dataclasses (first commit — TIER-2 READ acknowledgment) |
| `0440be02` | conductor | mark Phase 1 complete |
| `ef98f6d1` | feat | Result[T] + render_markdown.py |
| `36de54a9` | conductor | mark Phase 2 complete |
| `f503eb5d` | feat | download_media.py — stdlib urllib + idempotent + per-kind naming |
| `ea33a49f` | conductor | mark Phase 3 complete |
| `06ff9299` | feat | fetch_thread.py — html.parser + gallery-dl + URL query-strip + CLI |
| `41656d6b` | conductor | mark Phase 4 complete |
| `ed867317` | feat | thread_from_dict + download_media/render_markdown CLIs (end-to-end -m pipeline) |
| `fe207c1e` | docs | README |
(Plus this report + the plan/state finalization commit.)
## Merge workflow (user-side)
1. In the **main repo** (not the sandbox clone): `pwsh -File scripts/tier2/fetch_tier2_branch.ps1 -TrackName twitter_threads_extraction_20260705` to pull the branch as `review/...`.
2. Review the diff; optionally run the full batched suite: `uv run python scripts/run_tests_batched.py`.
3. Run the live 8-URL smoke (Task 5.3) with your `cookies.txt`.
4. On approval, `git merge --no-ff review/twitter_threads_extraction_20260705` and push (the sandbox blocks Tier 2 from pushing).