Skip to content

feat(fork): upstream sync job, fork-delta check, and agent guards #6

Description

@nohat

Problem

The fork has no automatic upstream sync and no check that a clean merge kept the fork's behavior. Syncs are by hand, and a merge that applies cleanly can silently drop a fork feature. The decisions are in docs/fork/maintenance.md (branch docs/fork-defect-resolution).

Proposed work, smallest first

  1. Guard hooks: a user-level PreToolUse hook denying gh repo delete, gh repo fork --fork-name, git push --force, and -X ours / -X theirs merges. A pre-push check (in .vite-hooks, since core.hooksPath points there) refusing main and force-pushes to fork/prod; refuse commits on main.
  2. Fork patch list + delta check: docs/fork/patches.tsv (name, branch, guard command) and scripts/fork/delta-check, which compares git diff --numstat main fork/prod before and after a trial merge and flags fork files that lost lines.
  3. t3.upstream-sync launchd job: fetch upstream, fast-forward main only, trial-merge into fork/prod in a scratch worktree, run the guard tests and delta check, and file an issue (ids only; the repo is public) on conflict or loss. It never lands anything.
  4. Fork build versioning: fork-suffixed version from the environment, no update feed, bundle id patched on a scratch copy, tag fork/<date>-<sha>.
  5. Move POSTURE_REF in ~/.claude/hooks/t3code-fork-posture.sh to fork/prod once docs/fork/ lands there; check whether Codex has a start-of-session hook equivalent.

Done already

rerere is enabled globally, upstream has no_push, and a SessionStart hook injects the posture. Tested with claude -p. Not tested: a session started by T3 itself.

Related: #5.

Activity

  1. nohat commented on Oct 3, 2026

    @nohat
    OwnerAuthor

    Triage 2026-10-03T23:46Z: intended (fork maintenance, not a defect). Grounded in docs/fork/maintenance.md.

    Already landed: upstream push URL is no_push, rerere is on globally, and the posture hook reads fork/prod (scripts/fork/agent-config/t3code-fork-posture.sh).

    Remaining: PreToolUse deny hooks, the pre-push main/force-push check in .vite-hooks, docs/fork/patches.tsv + delta-check, the t3.upstream-sync launchd job, and fork build versioning. No classification change needed.

  2. nohat commented on Oct 5, 2026

    @nohat
    OwnerAuthor

    Status and finalized plan, 2026-10-05. Everything below was checked against the machine and the code by effect, not by reading the issue.

    Status of the five items

    # Item State
    1 Guard hooks Not built. ~/.claude/settings.json has only the SessionStart entry and no deny list. .vite-hooks/ tracks only pre-commit (vp staged); the pre-push shim in .vite-hooks/_ exists, so a tracked .vite-hooks/pre-push will run.
    2 patches.tsv + delta-check Not built. Nothing under scripts/fork/ or docs/fork/.
    3 Sync job Not built. No local.t3code.*sync* launchd job (main-sync is scaffold's). Drift is the exhibit: main is 185 commits behind upstream/main, and a trial merge conflicts in 69 files (#36).
    4 Fork build versioning In flight, uncommitted in the primary checkout (fork-versions.ts and test, docs/fork/versioning.md, edits to fork-deploy.ts, build-desktop-artifact.ts, app.config.ts, both vite configs, ServerEnvironment.ts). Design changed from this issue: a fork semver line from 1.0.0 with a decision ledger, not a -nohat.<sha> suffix. Known defect: upstream's new apps/mobile/fingerprint.config.js matches /^ {2}version: "(\d+)\./m and throws on the process.env... form in app.config.ts.
    5 Move POSTURE_REF Done. The installed hook equals scripts/fork/agent-config/t3code-fork-posture.sh and reads fork/prod. The "not tested from a T3-started session" gap is closed: this session was started by T3 and received the hook output. Codex has a session-start hook (hooks feature is stable in codex-cli 0.160.0; session_start and pre_tool_use events exist); not wired, no exhibit yet.

    Also verified done: upstream push URL is no_push; rerere.enabled and rerere.autoupdate are on.

    What changed the plan

    Upstream's V2 orchestrator rewrite (#36) makes the first sync a one-off port, not a job run. It also shows the premise of the delta check: git auto-merges many fork files that now sit on deleted code, so "no conflict" would have meant nothing. Two consequences for design:

    • delta-check must follow renames (git diff -M --numstat) and compare against the new base (upstream/main vs the merge result), not main before the sync.
    • Guard tests in patches.tsv are the primary signal; delta-check is only the net for features without one.

    Sync job design (final)

    scripts/fork/upstream-sync.ts, run by local.t3code.upstream-sync (daily; launchd runs it on wake if the Mac slept). One run:

    1. Lock. git fetch upstream --prune. Fast-forward main with git fetch upstream main:main (refuses non-ff), push to origin. Stop quietly if there are no new commits.
    2. Trial-merge upstream/main into the current fork/prod tip in a scratch worktree under the deploy root, rerere on.
    3. Conflict: abort, record counts and file paths mapped to patches.tsv entries, and create or update one issue labeled upstream-sync (ids, paths, counts only; the repo is public). Notify once per upstream sha.
    4. Clean: run each patches.tsv guard, then delta-check, then targeted typecheck for touched packages. If all pass, push sync/upstream-<date> at the merge commit and send a one-line digest. If a guard fails or a fork file lost lines, file the issue instead.
    5. It never changes fork/prod. Landing is fast-forward fork/prod to the sync branch, done by the orchestrating session and then fork-deploy (merged is not deployed). Auto-landing after N green days is a later option, not part of the build.
    6. Silence means no news or a clean candidate with nothing to say; a buzz only for conflict, loss, or a failing guard.

    Decisions recorded as assumptions (change by saying so): daily cadence; sync/upstream-<date> branches pushed to the fork; no auto-land; the visual-diff step in README.md waits until the design-system gate exists.

    Priority of what remains

    1. Guard hooks (item 1). Hours of work, independent of everything, and it has real exhibits: the fork deleted by gh repo fork --fork-name (2026-05-24) and the emptied main branches (2026-08-28). Claude PreToolUse deny rules for gh repo delete, gh repo fork --fork-name, git push --force|-f, and -X ours|theirs; tracked .vite-hooks/pre-push refusing main and non-ff pushes to fork/prod; pre-commit refusing commits on main. A Codex pre_tool_use mirror only when Codex runs fork work.
    2. Commit the versioning work (item 4), after fixing the fingerprint regex. It is already written; leaving it dirty blocks any merge in the primary checkout and it conflicts with upstream on app.config.ts and ServerEnvironment.ts, so it should land before feat(fork): catch up to upstream's V2 orchestrator (185 commits, 69 conflicts) #36. Also set T3CODE_MOBILE_UPDATES_ENABLED=0 for fork mobile builds (cheap insurance against upstream's OTA channel; not verified).
    3. patches.tsv + delta-check (item 2). Needed to accept feat(fork): catch up to upstream's V2 orchestrator (185 commits, 69 conflicts) #36 (the merge's acceptance test) and for the job. Seed from the accepted branches, using the guard test each already has; the V2 port rewrites the failing ones.
    4. feat(fork): catch up to upstream's V2 orchestrator (185 commits, 69 conflicts) #36, the V2 catch-up. Manual, dispatched, gated by items 2 and 3 above. Includes making fork-deploy V2-aware before the first V2 deploy (its running-turn count reads the frozen state.sqlite).
    5. upstream-sync script, then the launchd job (item 3). Build the script's trial-merge mode first; it doubles as the pre-flight for feat(fork): catch up to upstream's V2 orchestrator (185 commits, 69 conflicts) #36. Turn on the daily job only after feat(fork): catch up to upstream's V2 orchestrator (185 commits, 69 conflicts) #36 lands, otherwise it reports the same 69 conflicts every morning.
    6. Codex session-start hook (item 5 remainder). No exhibit. Do not build.

    Items 1 to 3 are independent and can run in parallel. 4 waits on 2 and 3; 5 waits on 4.

  3. nohat commented on Oct 5, 2026

    @nohat
    OwnerAuthor

    Ratified 2026-10-05 by the owner ("let's ratify it"), with one addition. The full design is now docs/fork/upstream-sync.md (branch docs/upstream-sync-v2, commit c8b2d29, to be merged to fork/prod with the V2 report).

    Addition: weekly cadence review. Once a week, or whenever asked (node scripts/fork/upstream-sync.ts cadence), the script gathers deterministic facts (upstream commits and files per day over 14 and 28 days, the sync run ledger, how long a clean merge survived before first conflicting) and runs one agent turn that writes the analysis and returns a new intervalHours with a reason. The agent adjusts the frequency on its own; the script enforces the bounds (6 to 168 hours, at most 2x per review) and records the analysis in upstream-sync-decisions.md in the deploy root. launchd wakes hourly and the script exits unless due, so the cadence is one number in upstream-sync.json, not a plist reload. An unchanged cadence is a ledger entry; a change is a one-line notification.

    Build order is unchanged (guards, versioning commit, patches.tsv + delta-check, #36, then the script and job). The cadence review ships with the job.

  4. nohat commented on Oct 6, 2026

    @nohat
    OwnerAuthor

    Resumed implementation here at the owner's request (2026-10-05).

    On fork/prod (b0ad7ce):

    • Agent and Git guards committed and installed. Claude PreToolUse rejects the destructive commands named in this issue. Git refuses main commits/pushes and non-fast-forward updates/deletion of fork/prod. A stable common-Git-directory dispatcher covers upstream branches lacking fork scripts; Vite's formatter still runs. The only main-push exception is an exact, fast-forward upstream/main mirror by the sync job.
    • Finished the in-flight release versioning, preserving the mobile version literal required by upstream's fingerprint reader and disabling upstream OTA on fork mobile builds. Fixed partial-release marker placement and refused falsely stamping legacy cached artifacts.
    • Patch registry and delta-check gates merged from feat/sync-gates.

    On sync/upstream-20261005 (09f1496):

    • Integrated the above into the existing V2 candidate, preserving its completed ports.
    • Built upstream-sync.ts: scratch trials, guards, rename-aware delta check with explicit allowances, touched-package typechecks, reusable candidate branches, metadata-only issue updates, and once-per-upstream notification state.
    • Weekly cadence review gathers deterministic 14/28-day volume, run/landing history, and clean-merge survival; the script enforces 6–168 hours and at most a 2x change. The hourly launchd plist is prepared and passes plutil. Machine config remains enabled=false; no job was loaded.
    • Fixed V2 port dependency wiring exposed by server typecheck (optional MCP model hints supplied at production composition, drain fixture supplied with its service).

    Evidence: 19 existing V2 feature guards passed; 8 installed-hook regression tests passed; 37 selected release/sync checks passed (62 unrelated cases skipped); 12 affected MCP tests passed. Typechecks passed for server, web, mobile, client-runtime, contracts, and scripts. Final delta check: 33 explicitly allowed losses, zero unapproved losses. Fresh review found no remaining material blocker in the changed guards, versioning, sync runner, or MCP wiring.

    Real local trial from current fork/prod against upstream 194c73f found 31 unresolved conflicts mapped to 16 features (rerere reduced the original 69); its scratch merge was aborted and removed. The trial made no pushes or issue writes. No production data was opened or changed.

    Remaining: V2 client/build/cutover/deploy gates in #36, then enable/bootstrap the prepared sync job. Codex has the Git guards; its provider-specific pre_tool_use mirror is still pending. Existing unrelated dirty iOS/papercut edits were preserved. Issue stays open; nothing here claims V2 is deployed.

  5. nohat commented on Oct 6, 2026

    @nohat
    OwnerAuthor

    Merged and built at David's request.

    • Fast-forwarded fork/prod to 09f14960c1 and pushed it. Existing local iOS/doc edits were preserved and restored.
    • Built fork version 1.0.0, macOS ARM64, including matching V2 server and web bundles. The contracts build gate passed after installing the new lockfile dependencies in the dedicated build worktree.
    • Verified packaged macOS metadata and package version are 1.0.0, and the bundled server chunks contain the V2 database implementation and fork version. Completed release staged under releases/09f14960c1; ZIP retained locally.
    • DMG creation failed in host hdiutil with No child processes/invalid plist output. ZIP packaging of the same already-built bundles succeeded. Build ran under the existing governor's 40% CPU cap, with ordinary scheduling priority to avoid background starvation.

    No production deployment or live data migration was performed. current remains on 7345a87552; the upstream-sync schedule remains disabled. Integrated client/cutover verification and a matching native mobile build are still pending. This update does not close the tracking issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions