Skip to content

feat(tools/github/rest-push.ts): REST git-data API helper for git-push bypass (B-0615)#4147

Closed
AceHack wants to merge 2 commits into
mainfrom
otto-cli/tools-github-rest-push-helper-2026-05-18-1349z
Closed

feat(tools/github/rest-push.ts): REST git-data API helper for git-push bypass (B-0615)#4147
AceHack wants to merge 2 commits into
mainfrom
otto-cli/tools-github-rest-push-helper-2026-05-18-1349z

Conversation

@AceHack
Copy link
Copy Markdown
Member

@AceHack AceHack commented May 18, 2026

What

A reusable bun tools/github/rest-push.ts helper that lands single- or multi-file changes via GitHub REST git-data API instead of git push. Bypasses the B-0615 push-hang failure mode (silent exit 0, no remote update).

Usage

bun tools/github/rest-push.ts \
  --file path/to/file.md \
  --file path/to/other.ts \
  --branch otto/my-branch-2026-05-18 \
  --message "feat: my change\n\nLong description..."

Output (JSON): { branch, sha, url }.

Self-eating dog food

The commit on this branch (6ae8326) was created by running the script ON ITSELF — the script committed its own .ts file to its own branch via the REST git-data API.

Why this matters now

PRs #4145 + #4146 (this session) both landed via this same JSON-inline pattern after git push silently exited 0 with no remote update. Inline JSON construction is fragile (escaping, base64, multi-step) and bypasses TypeScript's type system. Making it a real script with parseable args + type definitions makes the workflow reusable for:

Composes with

  • PR #4145 (rule documenting timeout --kill-after + REST bypass discipline)
  • PR #4146 (background-loop prompt updates referencing this pattern; should be amended to call this script directly)
  • B-0615 (the open bug the script works around)

Limitations / future work

  • Only handles file creation/modification, not deletion. Deletions require setting sha: null in the tree entry; can be added if needed.
  • Doesn't update existing branch refs (POST /refs fails if ref exists). Force-update workflows would need PATCH /refs/heads/<branch>; can be added.
  • No retry logic for transient REST 5xx. The script fails fast on any REST error.

These are intentional MVP simplifications; can be extended as the bypass pattern matures.

Co-Authored-By: Claude noreply@anthropic.com

…h bypass under saturation (B-0615)

When git push silently fails under multi-agent saturation, this script
lands single- or multi-file changes via the GitHub REST git-data API
(POST .../git/blobs, /trees, /commits, /refs). The REST endpoints are
served by different infrastructure than the git-push transport, so they
remain responsive while push is hung.

Empirical anchor: PRs #4145 + #4146 (2026-05-18) both landed via this
inline-JSON pattern after git push silently exited 0 with no remote
update. This helper makes the workflow reusable — spawned-claude
sessions in claude-loop-tick.ts can invoke:

  bun tools/github/rest-push.ts \
    --file <path> [--file <path> ...] \
    --branch <ref> \
    --message <msg>

Instead of constructing the JSON inline each time.

Self-eating dog food: THIS commit was created by running the script on
itself — the script committed its own .ts file to its own branch via
the REST git-data API.

Composes with:
- PR #4145 (rule documenting timeout --kill-after + REST bypass)
- PR #4146 (claude-loop-tick prompt updates that reference this
  pattern)
- B-0615 (the open bug the script works around)

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 18, 2026 13:49
@AceHack AceHack enabled auto-merge (squash) May 18, 2026 13:49
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new tools/github/rest-push.ts Bun/TypeScript helper to create a commit + branch ref via GitHub’s REST git-data API (blobs/trees/commits/refs) as a workaround when git push hangs or silently fails under saturation (B-0615). This fits into the existing tools/github/* suite of gh-driven automation scripts.

Changes:

  • Introduces rest-push.ts CLI that commits one or more local files to a new branch via GitHub REST git-data API.
  • Implements basic arg parsing (--file repeatable, --branch, --message, optional --base/--owner/--repo) and emits { branch, sha, url } JSON.
Comments suppressed due to low confidence (1)

tools/github/rest-push.ts:151

  • Unconditional main(); means importing this module will immediately execute network/file side effects. Prefer if (import.meta.main) { process.exit(main(process.argv.slice(2))); } to match other tools scripts and keep it safe to import (e.g., for tests or reuse).
main();

Comment thread tools/github/rest-push.ts
Comment on lines +61 to +69
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const next = argv[i + 1];
if (a === "--file" && next) { args.files.push(next); i++; }
else if (a === "--branch" && next) { args.branch = next; i++; }
else if (a === "--message" && next) { args.message = next; i++; }
else if (a === "--base" && next) { args.base = next; i++; }
else if (a === "--owner" && next) { args.owner = next; i++; }
else if (a === "--repo" && next) { args.repo = next; i++; }
Comment thread tools/github/rest-push.ts
Comment on lines +88 to +89
}
const result = spawnSync("gh", cmdArgs, { encoding: "utf8", input: stdin, maxBuffer: 16 * 1024 * 1024 });
Comment thread tools/github/rest-push.ts Outdated
Comment on lines +104 to +106
function main(): void {
const args = parseArgs(process.argv.slice(2));
const { owner, repo, base, branch, message, files } = args;
Comment thread tools/github/rest-push.ts
Comment on lines +13 to +15
// Usage:
// bun tools/github/rest-push.ts --file <path> --branch <ref> --message <msg> [--base main] [--owner X] [--repo Y]
//
Comment thread tools/github/rest-push.ts
Comment on lines +52 to +79
function parseArgs(argv: string[]): Args {
const args: Args = {
files: [],
branch: "",
message: "",
base: "main",
owner: "Lucent-Financial-Group",
repo: "Zeta",
};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const next = argv[i + 1];
if (a === "--file" && next) { args.files.push(next); i++; }
else if (a === "--branch" && next) { args.branch = next; i++; }
else if (a === "--message" && next) { args.message = next; i++; }
else if (a === "--base" && next) { args.base = next; i++; }
else if (a === "--owner" && next) { args.owner = next; i++; }
else if (a === "--repo" && next) { args.repo = next; i++; }
else if (a === "--help" || a === "-h") {
process.stdout.write(`Usage: bun tools/github/rest-push.ts --file <path> --branch <ref> --message <msg> [--base main]\n --file can be repeated to land multiple files in one commit.\n`);
process.exit(0);
}
else { process.stderr.write(`unknown arg: ${a}\n`); process.exit(2); }
}
if (args.files.length === 0) { process.stderr.write("--file required (at least one)\n"); process.exit(2); }
if (!args.branch) { process.stderr.write("--branch required\n"); process.exit(2); }
if (!args.message) { process.stderr.write("--message required\n"); process.exit(2); }
return args;
…anches

Extends the rest-push.ts helper from CREATE-ONLY to also support UPDATE
mode for adding commits to existing branches via PATCH /git/refs/heads/<branch>.

Usage:
  bun tools/github/rest-push.ts --update --file <path> --branch <existing-ref> --message <msg>

Difference from create mode:
- Parent commit is HEAD of <branch> (not main)
- Uses PATCH refs (fast-forward only) instead of POST refs
- Fails cleanly if branch doesn't exist
- Fails cleanly if HEAD has diverged (no --force; protects against
  accidentally rewriting peer commits during the parent-read-vs-PATCH window)

Motivation: when shipping fixes to reviewer findings on existing open PRs,
the original rest-push.ts could only create NEW branches. Reviewers
expect fixes pushed to the SAME branch (so threads can auto-outdate +
auto-merge re-evaluates with the new HEAD). The --update mode supports
this workflow when git push is hanging.

Self-eating dog food (recursive): THIS commit was created using the
new --update mode on PR #4147's own branch. The script extends itself
on its own PR via the very mode it adds. Proves the implementation
works.

Co-Authored-By: Claude <noreply@anthropic.com>
@AceHack
Copy link
Copy Markdown
Member Author

AceHack commented May 18, 2026

Verify-before-fix on the 5 unresolved Copilot threads (2026-05-18T16:14Z autonomous-loop tick from Otto-CLI, under 28-Otto + 3-Lior saturation; non-git-mutating verification per blocked-green-ci-investigate-threads.mdBLOCKED with SUCCESS rollup, all threads needed verification against tools/github/rest-push.ts@e5e4c0d).

Rollup is green; the 5 unresolved Copilot threads are the only block.

# Copilot finding Verification Disposition
1 parseArgs treats next token as value when it's another flag (--file --branch foo) TRUErest-push.ts:64-72 checks && next only; no next.startsWith("--") guard. Worked example: --file --branch foo pushes --branch into args.files, loops with i=2 a="foo" → exit 2 with misleading "unknown arg: foo". Matches the empirical pattern Codex catches: empty/wrong-input-after-flag bugs. Fix-needed — fail-closed if (!next || next.startsWith("--")) { error } before consuming.
2 Missing sonarjs/no-os-command-from-path suppression on spawnSync("gh", ...) TRUErest-push.ts:102 has no suppression. Convention established in 5 spots in refresh-worldview.ts (lines 125/148/281/321/334) and 1 spot in poll-pr-gate.ts:286. Same gap in sibling tools/github/rest-ship.ts at lines 127 + 143 — bundle the fix. Fix-needed — add // eslint-disable-next-line sonarjs/no-os-command-from-path -- gh is a trusted binary above each spawnSync("gh", ...).
3 Should export main(argv) entrypoint and return exit code, then process.exit(...) Copilot marked outdated, but pattern NOT actually followed — current rest-push.ts:117 is function main(): void (no argv param, no return), called as main() at line 191 (no process.exit). Verify-before-resolve: either Copilot's outdated marker reflects a prior commit that DID violate-then-fix-differently, OR the convention drifted. Substrate-honest move — bundle a small refactor with #1 + #2 fixes (export, accept argv, return exit code) rather than resolve-noop. Fix-recommended (small).
4 Help text says --branch <ref> but code prefixes refs/heads/ TRUErest-push.ts:178 writes refs/heads/${branch}, line 170 writes git/refs/heads/${branch}. User passing --branch refs/heads/foo → double-prefix refs/heads/refs/heads/foo. Fix-needed — either strip leading refs/heads/ from input, OR update help text to say --branch <short-name> (short-name is the natural convention; just clarify).
5 No bun:test coverage for non-trivial parsing logic TRUE — no tools/github/rest-push.test.ts. Convention established by tools/github/poll-pr-gate.test.ts. Fix-recommended — at minimum test parseArgs ({happy-path, --update, empty files, flag-as-value-after-flag for finding #1}).

Net: 4 findings need substantive fix (#1, #2, #4, plus convention-recommended #3 + #5). #2 also applies to sibling rest-ship.ts — bundle them.

Path forward (re-land via cherry-pick per stale-armed-PR resolution patterns, since current branch is armed >2h with all-real findings):

  1. Fix deps: Bump FsUnit.xUnit from 7.1.0 to 7.1.1 #1 + Round 26 — rename tail, §18 memory clarification, three dispatches #2 + Round 28 — FsCheck LawRunner (Option B) + round-29 CI anchor #4 in rest-push.ts (3 small edits)
  2. Add same Round 26 — rename tail, §18 memory clarification, three dispatches #2 suppression to rest-ship.ts:127 + rest-ship.ts:143 (2 edits)
  3. Optional: add tools/github/rest-push.test.ts covering parseArgs flag-as-value
  4. Push to this branch (otto/b0615-rest-push-helper-2026-05-18 or successor) — gh pr merge --auto --squash is already armed.

Downstream impact: #4149 (codex-loop-tick B-0615 push-hang awareness) depends on rest-push.ts being on main per Codex P1 finding from earlier ticks (memo) — landing #4147 self-heals that thread.

No worktree-mutating action this tick (saturation: 28 claude-code + 3 Lior procs, dotgit-saturation tier risk for new-worktree creation per B-0615 + B-0530).

@AceHack
Copy link
Copy Markdown
Member Author

AceHack commented May 18, 2026

Supersession update — #4163 merged 2 min ago and added the same file (2026-05-18T16:31Z autonomous-loop follow-up from Otto-CLI; supersedes my prior verify-before-fix at 16:14Z).

Refreshed state:

tools/github/rest-push.ts is now on origin/main via #4163. The 5 verified Copilot findings I documented earlier are likely either (a) already resolved in #4163's version of the file, or (b) still present and the next slice of work after #4163 lands.

Substrate-honest resolution path (per blocked-green-ci-investigate-threads.md"Close as redundant" stale-armed-PR pattern):

The PR's substrate already exists on main via a different PR (byte-identical file paths, or content shipped via newer PR). Close with substrate-honest comment + cross-link to the merged equivalent. Preserves alternate-content version in branch history per lost-files-surface.

Recommended next-Otto action:

  1. Diff #4147 head (e5e4c0d per my prior verify) against origin/main tools/github/rest-push.ts (post-feat(rest-push): --delete + --rename extension mechanizes ID-renumber pattern (B-0650) #4163) — see if feat(tools/github/rest-push.ts): REST git-data API helper for git-push bypass (B-0615) #4147 carries anything not already on main
  2. If feat(tools/github/rest-push.ts): REST git-data API helper for git-push bypass (B-0615) #4147 ≈ subset-of-main: close as redundant + cross-link feat(rest-push): --delete + --rename extension mechanizes ID-renumber pattern (B-0650) #4163
  3. If feat(tools/github/rest-push.ts): REST git-data API helper for git-push bypass (B-0615) #4147 carries genuinely-new content: rebase on main (high conflict risk since file is now on main) OR cherry-pick the delta onto a fresh branch
  4. Address the 5 Copilot findings I verified earlier against origin/main's version of the file (not against #4147 head) — they may be already-fixed OR carried-forward into feat(rest-push): --delete + --rename extension mechanizes ID-renumber pattern (B-0650) #4163's content
  5. Bus envelope 177f2bf8 work-assignment payload (advertising the fix-list) is now stale; my prior shadow-catch 61ebae6f for the alignment-cluster is unaffected and still active.

Downstream impact on #4149 (codex-loop-tick B-0615 push-hang awareness): the Codex P1 "rest-push.ts not in tree" finding self-heals via #4163 (different PR than expected, but same effect — tools/github/rest-push.ts is now on main).

Saturation context: 28 claude-code + 3 Lior peer-procs unchanged this session; this finding was surfaced during an autonomous-loop tick refresh at minute 13 of the session, not via active investigation — substrate-honest catch from refresh-before-decide invariant.

@AceHack
Copy link
Copy Markdown
Member Author

AceHack commented May 18, 2026

Sharper finding — #4163's merged version ships ALL 5 Copilot bugs to main (2026-05-18T16:37Z autonomous-loop pre-empt #5 from Otto-CLI; supersedes my 16:31Z supersession-update which assumed #4163 might have addressed them).

Verified tools/github/rest-push.ts on origin/main (blob f175b667b70092d3a371559e129599ef49265c10, 246 LOC) via gh api repos/.../contents/... (local git ls-tree hung at 8s — dotgit-saturation tier). All 5 Copilot findings from my 16:14Z verify-before-fix are STILL PRESENT in the merged file:

# Finding origin/main evidence Status on main
1 parseArgs flag-as-value bug main:83-86const next = argv[i + 1]; if (a === "--file" && next) { args.files.push(next); i++; } — no next.startsWith("--") guard. Now applies to both --file AND --delete (#4163 added --delete). Shipped to main — needs fix-forward PR
2 Missing sonarjs/no-os-command-from-path suppression grep sonarjs /tmp/main-rest-push.ts → empty. spawnSync(gh, ...) unchanged on main. Shipped to main
3 main(argv) convention main:150 function main(): void {, main:246 main(); — not exported, no argv param, no return, no process.exit. Shipped to main
4 refs/heads/ double-prefix main:225 repos/.../git/refs/heads/${branch}, main:233 ref: refs/heads/${branch}. Shipped to main
5 Missing tests gh api repos/.../contents/tools/github/rest-push.test.ts → 404. Shipped to main

Substrate-honest re-resolution path (refined from 16:31Z):

  1. Close feat(tools/github/rest-push.ts): REST git-data API helper for git-push bypass (B-0615) #4147 as redundant (cross-link to feat(rest-push): --delete + --rename extension mechanizes ID-renumber pattern (B-0650) #4163) per blocked-green-ci-investigate-threads.mddirty state confirms file-collision
  2. File a NEW small fix-forward PR against main addressing finding deps: Bump FsUnit.xUnit from 7.1.0 to 7.1.1 #1 (real bug, real impact) + Round 28 — FsCheck LawRunner (Option B) + round-29 CI anchor #4 (real bug, real impact) — these will manifest as user-facing footguns
  3. Bundle finding Round 26 — rename tail, §18 memory clarification, three dispatches #2 fix in same PR (3-spot ESLint suppression across rest-push.ts:102 + rest-ship.ts:127 + rest-ship.ts:143) — convention compliance
  4. Optional same-PR: add tools/github/rest-push.test.ts with the flag-as-value test case (mechanizes the catch for finding deps: Bump FsUnit.xUnit from 7.1.0 to 7.1.1 #1)
  5. Optional separate PR: refactor to main(argv) convention if maintainer wants the discipline enforced (lower urgency)
  6. B-0553 substrate-drift auditor could catch this class automatically: "PR closes-as-redundant when SAME-FILE merged via different PR, BUT unresolved review threads on the closed-PR still apply to main's version" is a mechanizable pattern.

Discovered via refresh-before-decide invariant during autonomous-loop tick — local git ls-tree origin/main hung at 8s (dotgit-saturation tier hit, confirming the broader saturation observation pattern from earlier sessions today). gh api contents route bypasses the local-git-saturation entirely.

@AceHack
Copy link
Copy Markdown
Member Author

AceHack commented May 18, 2026

Closing as redundant — substrate from this PR (rest-push.ts initial add) has been superseded by PR #4163 (B-0650) which shipped rest-push.ts WITH the --delete + --rename extension. Main now has the full version. Per blocked-green-ci-investigate-threads.md stale-armed-PR Pattern 'Close as redundant'.

@AceHack AceHack closed this May 18, 2026
auto-merge was automatically disabled May 18, 2026 17:02

Pull request was closed

AceHack added a commit that referenced this pull request May 18, 2026
…pline to Vera's spawned prompt (#4149)

Cross-agent consistency with claude-loop-tick (PR #4146): Vera's
spawned codex sessions now know about (1) timeout --kill-after for
git network ops, (2) the REST git-data API bypass via
bun tools/github/rest-push.ts (PR #4147), and (3) refresh-worldview
should be timeout-wrapped.

Three new sentences appended to the existing refresh-worldview prompt
block (lines 203-209):

- Wraps refresh-worldview invocation in timeout --kill-after
- Generic wrap-all-git-network-ops discipline per the rule landed in
  PR #4145
- Push-hang workaround: prefer bun tools/github/rest-push.ts
  (PR #4147) over git push when push hangs

No changes to Vera's loop-tick.ts structure itself (no produced_pr
tracking refactor) — Vera already has 15min interval (low burn rate
vs claude's 60s), so backoff is lower priority. Push-hang awareness
is the high-leverage cross-cutting fix.

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants