office: open a lazygit session on the machine that owns the folder, and stop overwriting a working wrapper

This commit is contained in:
Sam
2026-11-13 10:54:10 +11:00
parent fd7183778a
commit c53f37dad9
4 changed files with 391 additions and 5 deletions

View File

@@ -1663,3 +1663,105 @@ And for the record: the `office` commands no longer need any of this. They read
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-12-22-47-55|Rich discovery: home 2026-11-12 22:47:55]]
- context: [[packs/paperclip-harness/20-context/home|home]]
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|home repository structure and configuration patterns]]
---
## Update (2026-11-12 23:34:09)
## Rich discovery update (2026-11-12 23:34:09)
## The problem was `sudo`, not the command
```
/home/sam/.local/bin/office: line 3: /root/.local/share/paperclip-harness/bin/office: No such file or directory
```
`sudo` sets `HOME=/root`, so the wrapper's `$HOME/.local/share/...` pointed at a directory that does not exist. The command underneath was fine.
**`office` never needed `sudo`, and must not have it.** It is a user command in three ways:
| it needs | why root cannot have it |
|---|---|
| your Paperclip key | it is in **your** `~/.config/environment.d`, not root's |
| **your** TUIOS daemon | the daemon socket lives in your runtime directory; root talks to a different one |
| files you will edit | anything written as root would be owned by root in your home |
So: no `sudo`.
## Two things I changed so this cannot bite again
**1. The wrapper now refuses root, clearly.**
```sh
if [ "$(id -u)" = "0" ]; then
echo "office: run this as your own user, not with sudo." >&2
exit 2
fi
```
**2. The wrapper no longer uses `$HOME`.** The installation writes the real path out in full, so `sudo` cannot make it look for a copy that is not there:
```sh
exec "/home/sam/.local/share/paperclip-harness/bin/office" "$@"
```
The same guard is inside `office` itself, so a copy invoked directly as root refuses the same way rather than half-working. Both are on `.13`, `.27` and `.51`.
## Your command, corrected
```sh
office ~/TEST-BETA --new "TEST-BETA" --host nixos-desktop --session TEST-BETA
```
I dry-ran exactly that from `.27` and it resolves:
```
worktrees: no — this is not a repository
would write /home/sam/TEST-BETA/.harness/harness.toml
would create or reuse session 'TEST-BETA' and build eight panes there
switch to it: tuios attach --host nixos-desktop TEST-BETA
```
`--session TEST-BETA` is fine — no clash with `security`.
Two things will happen when you run it for real:
1. It will find that `/home/sam/TEST-BETA` is **not a git repository** and ask **Y or N**, because that decides whether each task gets its own branch and worktree. Y makes the repository, writes `.gitignore` with `.paperclip/`, stages everything, and stops so you can commit in lazygit. N creates the project in place with no diff and no merge.
2. If you would rather not be asked, `--git-init` means Y, and `--commit-all` means Y and commit in one step.
```sh
office ~/TEST-BETA --new "TEST-BETA" --host nixos-desktop --session TEST-BETA --git-init
```
## After it is created
```sh
tuios attach --host nixos-desktop TEST-BETA
```
Then `Project>project` → `(` for the **git** tab to see the state, `g` for lazygit in the folder, `t` for the other tools. If you answered Y, the top of every pane will carry the warning line until you commit — that line is what tells you the worktrees are not real yet.
Committed: `paperclip-harness` `fd71837`, pushed, and copied to `.27` and `.51`.
## Source paths observed
- `home/sam/.local/bin/office`
- `root/.local/share/paperclip-harness/bin/office`
- `home/sam/paseo/projects/paperclip-harness/bin/office`
- `home/sam/.local/share/paperclip-harness/bin/office`
- `home/sam/TEST-BETA/.harness/harness.toml`
- `HOME/.local/share/`
- `home/sam/TEST-BETA`
## Evidence
- Confidence: 84%
- Reason: Update component/repository context with detailed discovered structure and configuration
## Related learned memories <!-- same-turn-2026-11-12 23:34:09 -->
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-would-2026-11-12-23-34-09|Rich discovery: would 2026-11-12 23:34:09]]
- context: [[packs/paperclip-harness/20-context/home|would]]
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|would repository structure and configuration patterns]]

View File

@@ -7012,3 +7012,105 @@ All committed and pushed: `paperclip-harness` `32fb356`, `nixos-4screen` `587db3
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-local-2026-11-12-23-03-35|Rich discovery: .local 2026-11-12 23:03:35]]
- context: [[packs/paperclip-harness/20-context/paperclip-harness|.local]]
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|.local repository structure and configuration patterns]]
---
## Update (2026-11-12 23:34:09)
## Structural/configuration observations (2026-11-12 23:34:09)
## The problem was `sudo`, not the command
```
/home/sam/.local/bin/office: line 3: /root/.local/share/paperclip-harness/bin/office: No such file or directory
```
`sudo` sets `HOME=/root`, so the wrapper's `$HOME/.local/share/...` pointed at a directory that does not exist. The command underneath was fine.
**`office` never needed `sudo`, and must not have it.** It is a user command in three ways:
| it needs | why root cannot have it |
|---|---|
| your Paperclip key | it is in **your** `~/.config/environment.d`, not root's |
| **your** TUIOS daemon | the daemon socket lives in your runtime directory; root talks to a different one |
| files you will edit | anything written as root would be owned by root in your home |
So: no `sudo`.
## Two things I changed so this cannot bite again
**1. The wrapper now refuses root, clearly.**
```sh
if [ "$(id -u)" = "0" ]; then
echo "office: run this as your own user, not with sudo." >&2
exit 2
fi
```
**2. The wrapper no longer uses `$HOME`.** The installation writes the real path out in full, so `sudo` cannot make it look for a copy that is not there:
```sh
exec "/home/sam/.local/share/paperclip-harness/bin/office" "$@"
```
The same guard is inside `office` itself, so a copy invoked directly as root refuses the same way rather than half-working. Both are on `.13`, `.27` and `.51`.
## Your command, corrected
```sh
office ~/TEST-BETA --new "TEST-BETA" --host nixos-desktop --session TEST-BETA
```
I dry-ran exactly that from `.27` and it resolves:
```
worktrees: no — this is not a repository
would write /home/sam/TEST-BETA/.harness/harness.toml
would create or reuse session 'TEST-BETA' and build eight panes there
switch to it: tuios attach --host nixos-desktop TEST-BETA
```
`--session TEST-BETA` is fine — no clash with `security`.
Two things will happen when you run it for real:
1. It will find that `/home/sam/TEST-BETA` is **not a git repository** and ask **Y or N**, because that decides whether each task gets its own branch and worktree. Y makes the repository, writes `.gitignore` with `.paperclip/`, stages everything, and stops so you can commit in lazygit. N creates the project in place with no diff and no merge.
2. If you would rather not be asked, `--git-init` means Y, and `--commit-all` means Y and commit in one step.
```sh
office ~/TEST-BETA --new "TEST-BETA" --host nixos-desktop --session TEST-BETA --git-init
```
## After it is created
```sh
tuios attach --host nixos-desktop TEST-BETA
```
Then `Project>project` → `(` for the **git** tab to see the state, `g` for lazygit in the folder, `t` for the other tools. If you answered Y, the top of every pane will carry the warning line until you commit — that line is what tells you the worktrees are not real yet.
Committed: `paperclip-harness` `fd71837`, pushed, and copied to `.27` and `.51`.
## Source paths observed
- `home/sam/.local/bin/office`
- `root/.local/share/paperclip-harness/bin/office`
- `home/sam/paseo/projects/paperclip-harness/bin/office`
- `home/sam/.local/share/paperclip-harness/bin/office`
- `home/sam/TEST-BETA/.harness/harness.toml`
- `HOME/.local/share/`
- `home/sam/TEST-BETA`
## Evidence
- Confidence: 82%
- Reason: Captured factual repo structure/configuration patterns from rich discovery
## Related learned memories <!-- same-turn-2026-11-12 23:34:09 -->
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-would-2026-11-12-23-34-09|Rich discovery: would 2026-11-12 23:34:09]]
- context: [[packs/paperclip-harness/20-context/home|would]]
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|would repository structure and configuration patterns]]

View File

@@ -0,0 +1,121 @@
---
type: session
id: session.paperclip-harness.rich-discovery-would-2026-11-12-23-34-09
title: Rich discovery: would 2026-11-12 23:34:09
status: active
source_of_truth: false
freshness: current
last_reviewed: 2026-11-12
tags:
- pack/paperclip-harness
- agent-memory/session
- autolearn
- rich-discovery
- repo/would
---
# Rich discovery: would 2026-11-12 23:34:09
Sanitized rich discovery snapshot captured automatically from the completed turn.
## Source paths observed
- `home/sam/.local/bin/office`
- `root/.local/share/paperclip-harness/bin/office`
- `home/sam/paseo/projects/paperclip-harness/bin/office`
- `home/sam/.local/share/paperclip-harness/bin/office`
- `home/sam/TEST-BETA/.harness/harness.toml`
- `HOME/.local/share/`
- `home/sam/TEST-BETA`
## Final answer snapshot
## The problem was `sudo`, not the command
```
/home/sam/.local/bin/office: line 3: /root/.local/share/paperclip-harness/bin/office: No such file or directory
```
`sudo` sets `HOME=/root`, so the wrapper's `$HOME/.local/share/...` pointed at a directory that does not exist. The command underneath was fine.
**`office` never needed `sudo`, and must not have it.** It is a user command in three ways:
| it needs | why root cannot have it |
|---|---|
| your Paperclip key | it is in **your** `~/.config/environment.d`, not root's |
| **your** TUIOS daemon | the daemon socket lives in your runtime directory; root talks to a different one |
| files you will edit | anything written as root would be owned by root in your home |
So: no `sudo`.
## Two things I changed so this cannot bite again
**1. The wrapper now refuses root, clearly.**
```sh
if [ "$(id -u)" = "0" ]; then
echo "office: run this as your own user, not with sudo." >&2
exit 2
fi
```
**2. The wrapper no longer uses `$HOME`.** The installation writes the real path out in full, so `sudo` cannot make it look for a copy that is not there:
```sh
exec "/home/sam/.local/share/paperclip-harness/bin/office" "$@"
```
The same guard is inside `office` itself, so a copy invoked directly as root refuses the same way rather than half-working. Both are on `.13`, `.27` and `.51`.
## Your command, corrected
```sh
office ~/TEST-BETA --new "TEST-BETA" --host nixos-desktop --session TEST-BETA
```
I dry-ran exactly that from `.27` and it resolves:
```
worktrees: no — this is not a repository
would write /home/sam/TEST-BETA/.harness/harness.toml
would create or reuse session 'TEST-BETA' and build eight panes there
switch to it: tuios attach --host nixos-desktop TEST-BETA
```
`--session TEST-BETA` is fine — no clash with `security`.
Two things will happen when you run it for real:
1. It will find that `/home/sam/TEST-BETA` is **not a git repository** and ask **Y or N**, because that decides whether each task gets its own branch and worktree. Y makes the repository, writes `.gitignore` with `.paperclip/`, stages everything, and stops so you can commit in lazygit. N creates the project in place with no diff and no merge.
2. If you would rather not be asked, `--git-init` means Y, and `--commit-all` means Y and commit in one step.
```sh
office ~/TEST-BETA --new "TEST-BETA" --host nixos-desktop --session TEST-BETA --git-init
```
## After it is created
```sh
tuios attach --host nixos-desktop TEST-BETA
```
Then `Project>project` → `(` for the **git** tab to see the state, `g` for lazygit in the folder, `t` for the other tools. If you answered Y, the top of every pane will carry the warning line until you commit — that line is what tells you the worktrees are not real yet.
Committed: `paperclip-harness` `fd71837`, pushed, and copied to `.27` and `.51`.
## Evidence
- Confidence: 86%
- Reason: Preserve full sanitized discovery detail from a rich investigation turn
## Related
- [[packs/paperclip-harness/00-system/pi-agent/memory-manifest|Memory Manifest]]
## Related learned memories <!-- same-turn-2026-11-12 23:34:09 -->
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-would-2026-11-12-23-34-09|Rich discovery: would 2026-11-12 23:34:09]]
- context: [[packs/paperclip-harness/20-context/home|would]]
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|would repository structure and configuration patterns]]

View File

@@ -47,14 +47,16 @@ def parse(argv):
opts = {"folder": ".", "folder_given": False, "project": "", "session": "",
"dry": False, "host": "", "no_switch": False, "new": "", "remote": "",
"git_init": "--git-init" in argv, "anywhere": "--anywhere" in argv,
"commit_all": "--commit-all" in argv, "git_init_commits": False}
"commit_all": "--commit-all" in argv, "git_init_commits": False,
"client_host": ""}
positional = []
i = 0
while i < len(argv):
a = argv[i]
if a in ("--project", "--session", "--host", "--new", "--remote"):
if a in ("--project", "--session", "--host", "--new", "--remote", "--client-host"):
if i + 1 < len(argv):
opts[a[2:]] = argv[i + 1]
# Keys are Python names, so a flag with a hyphen becomes an underscore.
opts[a[2:].replace("-", "_")] = argv[i + 1]
i += 2
continue
i += 1
@@ -146,6 +148,45 @@ def ensure_session(session, folder):
h.say("session: %s (created; switch to it with TUIOS's session switcher)" % session)
def open_git_session(folder, session, opts):
"""A session with lazygit already open in the folder, on the machine that has it.
The commit has to happen where the files are, and that machine is often not
the one being typed at. A session on that machine is how the person gets
there from TUIOS, which is the whole point of this program: one attach, no
ssh, no cd.
"""
helper = session + "-git"
created = ensure_session_quiet(helper, folder)
# The pane a tool needs a PATH of its own: a pane started this way does not
# inherit an interactive shell's.
prelude = ('export PATH="$HOME/.local/bin:$HOME/.nix-profile/bin:'
'/run/current-system/sw/bin:/etc/profiles/per-user/$USER/bin:$PATH"; ')
tuios.open_pane("git", "Git", folder, prelude + "exec lazygit", helper)
h.say("")
h.say(" A session called %r now holds lazygit in that folder%s." % (helper, "" if created else " (it was already there)"))
h.say(" Attach to it, stage and commit, then come back and repeat the command:")
h.say("")
client = opts.get("client_host") or ""
h.say(" tuios attach%s %s" % (" --host " + client if client else "", helper))
h.say("")
h.say(" When you are done with it: tuios kill-session %s" % helper)
return helper
def ensure_session_quiet(session, folder):
"""ensure_session, without the line about switching: the caller explains."""
if session in tuios.sessions():
return False
params = {"name": session, "cwd": folder, "window": False}
out = subprocess.run(["tuios", "call", "new-session", json.dumps(params)],
capture_output=True, text=True, timeout=60)
if out.returncode != 0 and "session_exists" not in (out.stderr or out.stdout):
raise h.Refused(3, "could not create session %r: %s"
% (session, (out.stderr or out.stdout).strip()[:300]))
return True
def scaffold(folder, name):
"""Write the files this project keeps, and only the ones that are missing.
@@ -237,6 +278,15 @@ def install_wrapper(addr, remote_bin):
capture_output=True, text=True, timeout=60).stdout.strip()
if not home:
return
# Never replace a wrapper that already works. Pushing from another machine
# would otherwise rewrite the owner's own `office` command to point at the
# copy that machine sent, which is how the canonical machine ended up
# running someone else's older copy.
existing = subprocess.run(
["ssh", "-o", "BatchMode=yes", addr, 'cat "$HOME/.local/bin/office" 2>/dev/null'],
capture_output=True, text=True, timeout=60).stdout
if "paperclip-harness/bin/office" in existing:
return
script = ("#!/bin/sh\n"
"# Written by office on another machine. Runs the copy it keeps here.\n"
"#\n"
@@ -291,6 +341,9 @@ def run_on_remote(host, folder, opts):
for flag in ("--git-init", "--commit-all", "--anywhere"):
if opts.get(flag[2:].replace("-", "_")):
inner.append(flag)
# So the instructions printed on the other machine say how to reach it from
# where the person is sitting.
inner += ["--client-host", host]
if opts["dry"]:
inner.append("--dry")
# "$HOME" must stay unquoted so the other machine's shell expands it; every
@@ -410,6 +463,12 @@ ON ANOTHER MACHINE
is not on PATH yet, so there you call it by its full path:
~/.local/share/paperclip-harness/bin/office <folder>
WHEN THE FILES ARE ON ANOTHER MACHINE
The git work has to happen where the files are, because that is where the
commit, the worktree and the agent are. So when a folder cannot hold a
worktree yet, office makes a session on THAT machine with lazygit already
open in the folder, and tells you how to attach to it. No ssh, no cd.
AFTER A REBOOT
TUIOS restores a session with a fresh shell in every pane and keeps the pane
names. Run board once for that project: it checks what each pane is actually
@@ -482,7 +541,7 @@ def repeat_command(folder, opts):
return " ".join(parts)
def prepare_git(folder, opts):
def prepare_git(folder, opts, session):
"""Get the folder as far as it can go toward a worktree per task.
A worktree needs a repository and at least one commit. Neither is made
@@ -568,6 +627,8 @@ def prepare_git(folder, opts):
h.say(" space stage or unstage e edit a file, .gitignore included")
h.say(" c commit ? every key")
h.say("")
open_git_session(folder, session, opts)
h.say("")
h.say(" Then repeat this command to create the project with worktrees:")
h.say("")
h.say(" %s" % repeat_command(folder, opts))
@@ -643,7 +704,7 @@ def main():
# Only now: a folder on another machine has already been handed over, and
# the machine that owns it does these two checks itself.
guard_machine(folder, opts)
ready = prepare_git(folder, opts)
ready = prepare_git(folder, opts, session)
if opts["dry"]:
if opts["new"]: