Skip to content

design: PRs through a std/github tool, not gh or /pr - #300

Merged
nishu-builder merged 4 commits into
mainfrom
claude/upbeat-lovelace-1xy4qn
Oct 5, 2026
Merged

nishu-builder merged 4 commits into
mainfrom
claude/upbeat-lovelace-1xy4qn

Conversation

@nishu-builder

@nishu-builder nishu-builder commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

First of three PRs that replace the TUI's /pr with agent-side publishing: #300 (this, the design) → #301 (std/github) → #302 (removes /pr). This replaces stack #261, which is closed.

The PRs section of design/agent-github.md, rewritten:

  • A PR is a pushed branch plus metadata. publish_source already pushes the branch from the server's store. std/github, a std tool, sends the rest to the GitHub API. Neither checks anything out.
  • at. Runs are memoized by their arguments, so each call carries an at. A repeated call returns the stored result; only a run that died mid-flight sends a write twice.
  • Token. The github-token secret, granted by a second reader: line naming std/github. Its scope is the boundary.
  • The per-layer recipe for a squashed stack: find or open each layer's PR on the branch below, then link them.
  • Removing /pr, including its conflict checks, which nothing replaces.

design/stacks.md points its publishing steps at the recipe.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CZGkTta5NYxhXcLPZwH3q1

Rewrite agent-github.md's PRs section. A PR reaches GitHub in two halves
and neither checks anything out: publish_source pushes the commit from
the server's bare store, and everything else (opening a PR, retargeting
its base, linking a stack) is metadata, sent to the GitHub API by a new
inline llm-step tool, github(method, path, body?).

- Inline, not a std/github worker running gh: a worker result is
  memoized by its ArgTree, the github-token secret's reader already
  names std/llm-step, and gh adds nothing the API lacks. gh stack link
  with PR numbers is POST /repos/{owner}/{repo}/stacks.
- Writes (anything but GET) are pinned in a tool.start before sending.
  Only the attempt that appended the pin sends; one that finds the call
  started and unfinished completes it as uncertain, never resends.
- The tool reaches api.github.com only and follows no redirects. Paths
  are unrestricted, so the token's scope is the boundary.
- The per-layer PR recipe for a squashed stack, publish_source refusing
  a commit with a .caos entry, and what removing /pr takes away.

The "Retrying GitHub writes" subsection is folded into "Writes", and
"Merges" becomes its own section. stacks.md's Publishing gains a third
step pointing at "PRs for a stack" in place of the TODO, and its
"Not built" section goes: linking is now designed, and the reasoning
about the rest of gh-stack moved into agent-github.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZGkTta5NYxhXcLPZwH3q1
Comment thread design/agent-github.md Outdated
Comment thread design/agent-github.md Outdated
Comment thread design/agent-github.md Outdated
Comment thread design/agent-github.md Outdated
Comment thread design/agent-github.md Outdated
Comment thread design/agent-github.md Outdated
Comment thread design/agent-github.md Outdated
Review on #300: the GitHub caller should follow the general pattern, a
std tool run by path, not a built-in inline in llm-step.

- std/github: run with run_tool(path="caos-std/github"), described by
  tool_help. Arguments method, path, body and at. The token reaches it
  through a second reader: line naming std/github.
- at: a run is memoized by its ArgTree, so each call carries a value of
  its own (salt is reserved for the interpreter). Repeating every
  argument returns the stored result; a write is therefore sent once
  per stored result, at least once if its run dies mid-flight.
- Not gh: one line, its stack commands need local branches, so a
  repository would be materialized for every call.
- No publication check. Resolving a conflict clears its ledger entry and
  saving removes the emptied ledger, so a ledger survives only while a
  conflict is unresolved, which is the same mistake as publishing code
  that does not build.

The Writes and Publication checks sections go; the tool.start pin was
the inline design's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZGkTta5NYxhXcLPZwH3q1
nishu-builder pushed a commit that referenced this pull request Oct 5, 2026
Review on #300: follow the general pattern. std/github is a tool like
any other, run with run_tool(path="caos-std/github") and described by
tool_help; llm-step's inline github tool and its tool.start pin go.

- worker.go, on std/go (whose image carries the CA bundle): one call to
  api.github.com per run. Arguments method, path, body and at. The
  result is the status line and the body, cut at 100 kB on a character
  boundary; a response of any status is a result, and only a request
  with no response fails the run (for a write, saying it may still have
  arrived). Redirects are not followed and the host is fixed, so the
  token goes nowhere else. Arguments are checked before anything is
  sent: the method, an API path (a full api.github.com URL is accepted),
  no body on a GET, a body that is JSON.
- at is required: the tool has no @in, so its ArgTree is its arguments,
  and a call repeating all of them returns the stored result (salt is
  reserved for the interpreter).
- The token is /secret/github-token, granted by a second reader: line
  naming std/github.

Also reverted from the previous commit: publish_source's .caos refusal,
the SPEC.md registry entry, and agent-publish.md's note. publish_source's
help points at caos-std/github; the agent guide opens PRs and links a
stack with it, and no longer suggests /pr.

Checked locally against a stand-in server, the worker built from this
file with the API base, /cas/args and /secret paths read from the
environment, and a fake caos: a POST sent once with its body verbatim
and Accept, API version, Bearer and JSON content-type headers; a GET with
no token sends no Authorization and a 401 gets the no-token hint; a 302
is reported and not followed; a 120 kB body is cut at 99,999 bytes,
valid UTF-8; a bodiless PUT sends Content-Length 0; bad method, missing
at, a GET body, a relative or spaced path and non-JSON bodies are
refused before sending; a dead server fails the run, with the may-have-
arrived advice for a write. go build, go vet and gofmt are clean, and
llm-step's 46 tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZGkTta5NYxhXcLPZwH3q1
nishu-builder pushed a commit that referenced this pull request Oct 5, 2026
Follows the review on #300, now merged in: the GitHub caller is the std
tool caos-std/github, and publication has no conflict check. SPEC.md,
chat.md and TUI.md name the tool, and SPEC.md and TUI.md no longer say
a .caos entry blocks publication: resolving a conflict clears its entry
and saving removes the emptied ledger, so one left in a published commit
is an unresolved conflict, like code that does not build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZGkTta5NYxhXcLPZwH3q1
@nishu-builder nishu-builder changed the title design: PRs through a github tool in llm-step, not gh or /pr design: PRs through a std/github tool, not gh or /pr Oct 5, 2026
claude added 2 commits October 5, 2026 06:48
The PRs section stated that nothing is checked out four times, described
at in a table, a section and the stack recipe, said twice that a failed
write may have arrived, and restated the tool's help in an argument
table and stacks.md's republish note. Each is now said once: half the
length, same design.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZGkTta5NYxhXcLPZwH3q1
The secret file example under Importing now carries both reader lines,
llm-step's and std/github's, and the PRs section says plainly that the
tool uses the same github-token secret imports do, so no new secret is
made: its file gains one reader line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZGkTta5NYxhXcLPZwH3q1
@nishu-builder
nishu-builder added this pull request to stack #306 October 5, 2026 07:30
@nishu-builder
nishu-builder merged commit fccb0ac into main Oct 5, 2026
2 checks passed
nishu-builder added a commit that referenced this pull request Oct 5, 2026
* llm-step: a github tool for PRs, and publish_source refuses .caos

github(method, path, body?) makes one call to api.github.com, inline in
the step, with the github-token secret llm-step is already granted for
imports (design/agent-github.md, "PRs"). With publish_source pushing the
commits, it is everything else a PR needs: find a branch's open PR, open
one, change its base, link a stack. Nothing is checked out.

- A GET runs, and a retry runs it again. Any other method is a write,
  pinned in a tool.start (github.json: the exact request) before it is
  sent. Only the attempt whose tool.start was appended sends it; an
  attempt that finds the call started and unfinished completes it as
  not confirmed and never sends it again. The pin carries a per-attempt
  nonce, since two attempts minting the same commit would each see
  their own append land and both send.
- The body is pinned as the text sent, not as JSON: canonical JSON has
  no negative or fractional numbers, and a body a model writes must not
  be able to fail the pin.
- Only api.github.com is reached, and redirects are not followed, so
  the token goes nowhere else. Paths are unrestricted; the token's
  scope is the boundary.

publish_source now refuses a commit whose root tree has a .caos entry,
such as an uncleared .caos/conflicts ledger. It reads the commit and
its root tree, nothing else. /pr made this check; a squash carries a
ledger into its commit, so a squashed layer needs it too.

The agent guide opens PRs with the github tool, including the per-layer
PRs and stack link for a squashed stack, and no longer suggests /pr,
which the next change removes. SPEC.md lists the tool among the
registered built-ins.

Tests, run with cargo over llm-step and its spliced crates: request
checks, rendering and truncation; a write pinned then sent once, with a
lost acknowledgement on the pin; a write another attempt pinned, before
this one or racing it, never sent again; a GET that is not pinned and
does not follow a 302; publish_source's .caos check. A local HTTP
server stands in for api.github.com.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZGkTta5NYxhXcLPZwH3q1

* std/github: the GitHub caller as a std tool, not built into llm-step

Review on #300: follow the general pattern. std/github is a tool like
any other, run with run_tool(path="caos-std/github") and described by
tool_help; llm-step's inline github tool and its tool.start pin go.

- worker.go, on std/go (whose image carries the CA bundle): one call to
  api.github.com per run. Arguments method, path, body and at. The
  result is the status line and the body, cut at 100 kB on a character
  boundary; a response of any status is a result, and only a request
  with no response fails the run (for a write, saying it may still have
  arrived). Redirects are not followed and the host is fixed, so the
  token goes nowhere else. Arguments are checked before anything is
  sent: the method, an API path (a full api.github.com URL is accepted),
  no body on a GET, a body that is JSON.
- at is required: the tool has no @in, so its ArgTree is its arguments,
  and a call repeating all of them returns the stored result (salt is
  reserved for the interpreter).
- The token is /secret/github-token, granted by a second reader: line
  naming std/github.

Also reverted from the previous commit: publish_source's .caos refusal,
the SPEC.md registry entry, and agent-publish.md's note. publish_source's
help points at caos-std/github; the agent guide opens PRs and links a
stack with it, and no longer suggests /pr.

Checked locally against a stand-in server, the worker built from this
file with the API base, /cas/args and /secret paths read from the
environment, and a fake caos: a POST sent once with its body verbatim
and Accept, API version, Bearer and JSON content-type headers; a GET with
no token sends no Authorization and a 401 gets the no-token hint; a 302
is reported and not followed; a 120 kB body is cut at 99,999 bytes,
valid UTF-8; a bodiless PUT sends Content-Length 0; bad method, missing
at, a GET body, a relative or spaced path and non-JSON bodies are
refused before sending; a dead server fails the run, with the may-have-
arrived advice for a write. go build, go vet and gofmt are clean, and
llm-step's 46 tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZGkTta5NYxhXcLPZwH3q1

* std/github: take the call id from the harness instead of `at`

std/github's results are kept by their arguments, so each call needs a value
of its own among them. That was `at`, a parameter the model filled in; it
read as a timestamp, and models invented dates for it. A reused value also
silently returned an earlier call's result.

A new help tag, `@call`, has llm-step bind the tool call's id as `call`.
The id is unique to the call and the same when a call is recovered, so a
retry still gets the stored result and a new call gets a fresh run. The
model no longer sees the parameter. `call` is reserved, like `in` and `wc`.
A caller outside llm-step passes --call itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
nishu-builder added a commit that referenced this pull request Oct 5, 2026
* caos-cli: remove /pr, /publish-branch and the client publisher

The agent publishes now: publish_source pushes the commit from the
server's store and the github tool opens or updates the PR
(design/agent-github.md, "PRs"). The client's own path to the same end
goes, and with it the client's need for gh, a host git push, and a
local copy of the history it publishes.

Removed:
- TUI: the /pr and /publish-branch commands, the preview modal and its
  key and mouse handling, the publishing state and its status, sidebar
  and footer rendering, and the PublicationPlanned, Published and
  PublicationBaseImported messages (bin/tui/publication.rs).
- Library: src/publication.rs (gh pr create/edit), the publication
  half of source_trees.rs (preview, provenance inference, base import,
  default_branch, branch_snapshot), the PR half of host_git.rs, and in
  lib.rs the client push (publish_source_tree_branch and friends),
  reject_publish_caos, publication_diagnostic, PublishedBranch, and
  append_system_notice, which only the publisher called.
- Their tests.

Kept: publication records stay readable. The agent's publish_source
still writes them, and conversation_load still summarizes them; a new
test appends one the way llm-step does and loads it back. Two escape
key tests lose their publishing cases and keep the running ones.

Docs: chat.md's "Publishing with /pr" becomes a short "Publishing"
pointing at the agent's tools; TUI.md, SPEC.md ("Publication"),
README.md and agent-harness.md drop the commands.

cargo test -p caos-cli: 39/39 lib, 148/150 bin. The two failures,
plain_commit_fork_failure_never_becomes_a_markerless_conversation and
checkout_import_completes_partial_history_and_keeps_local_edits, fail
identically without this change on this machine's git 2.43 (a promisor
lazy fetch, and a git error message). cargo clippy -p caos-cli
--all-targets -- -D warnings and cargo fmt --all --check are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZGkTta5NYxhXcLPZwH3q1

* docs: caos-std/github opens PRs, and nothing refuses .caos at publish

Follows the review on #300, now merged in: the GitHub caller is the std
tool caos-std/github, and publication has no conflict check. SPEC.md,
chat.md and TUI.md name the tool, and SPEC.md and TUI.md no longer say
a .caos entry blocks publication: resolving a conflict clears its entry
and saving removes the emptied ledger, so one left in a published commit
is an unresolved conflict, like code that does not build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZGkTta5NYxhXcLPZwH3q1

---------

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