diff --git a/bin/board b/bin/board index c3087c0..887ef14 100755 --- a/bin/board +++ b/bin/board @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """Build or repair a project's panes, in a fixed order. - bin/board # the project in the current folder - bin/board --folder ~/src/x # or another one - bin/board --replace # close this project's panes and build again + office board # the project in the current folder + office board --folder ~/src/x # or another one + office board --replace # close this project's panes and build again Every page is a workspace with one pane, named after the page. The first workspace holds a shell: it is the pane that never exits, so the session cannot diff --git a/bin/merge b/bin/merge index 508a8d3..9907a62 100755 --- a/bin/merge +++ b/bin/merge @@ -33,7 +33,7 @@ def main(): check_only = "--check" in args args = [a for a in args if not a.startswith("--")] if len(args) != 1: - h.say("usage: bin/merge [--check]") + h.say("usage: office merge [--check]") return 2 issue = h.find_issue(args[0]) diff --git a/bin/office b/bin/office index c2f24e4..a377ba6 100755 --- a/bin/office +++ b/bin/office @@ -371,70 +371,65 @@ def run_on_remote(host, folder, opts): return 0 -USAGE = """office — open a folder as a project, and say what else can be run +USAGE = """office — everything for one project, under one word + + office … open a folder as a project + office help this page + office board … build or repair the panes + office reset … clear panes and worktrees, keep the history + office delete … remove sessions, the Paperclip project, .git, dot files + office merge … rebase, check, review, merge one task + office review … run ocr over one task's diff + office triage … ask Mouse-1 one question about a task + + Every one of these is also on your PATH under its own name (board, merge, + review, triage, project-reset, project-delete). Both spellings do the same + thing; office is the one to remember. THE RULE The folder and the session live on the same machine. If you are typing inside the session, on the machine that owns the folder, it is just: office Use --host only when the folder is on a different machine from your shell. -WHAT IT DOES - 1. reads the folder - 2. if a machine is named, checks whether that is this machine, and runs locally if so - 3. if it really is another machine, checks the login and runs the same command there - 4. adopts the Paperclip project, or creates it with --new - 5. writes what is missing in /.harness/ and never overwrites - 6. records the project in ~/.config/paperclip-harness/projects.toml - 7. creates the session on that machine's daemon, then builds eight panes +OPENING A FOLDER + office --project "" adopt a project Paperclip already has + office --new "" create the Paperclip project as well + office . adopt the folder you are standing in + office --session name the session instead of the folder + office … --host the folder is on that machine + office … --dry say what would happen, write nothing -COMMANDS + What it does, in order: reads the folder; runs locally if the named machine is + this one; otherwise checks the login and runs the same command there; adopts or + creates the Paperclip project; writes what is missing in /.harness/ and + never overwrites; records the project in ~/.config/paperclip-harness/projects.toml; + creates the session on that machine's daemon; builds eight panes. - Each of these is its own command on your PATH, not a subcommand of office: - type `board`, not `office board`. +PAGES: office board [--folder ] [--replace] [--reset] + board create any pane that is missing, close nothing + board --replace close this project's panes, then build all eight again + board --reset close every pane in the session, wait, then build + board --folder do it for another project - office --project "" adopt a project Paperclip already has - office --new "" create the Paperclip project as well - office . adopt the folder you are standing in - office --session name the session instead of the folder - office --project "" --host the folder is on that machine - office --new "" --dry say what would happen, write nothing - office --new "" (with no repository it asks Y or N) - office --new "" --commit-all make the repository and commit everything - office --new "" --anywhere allow it although agents cannot see it + Inside a pane, R rebuilds everything and r re-reads Paperclip. After a reboot + TUIOS restores a session with a bare shell in each pane and keeps the pane + names, so run board once: it now checks what each pane is really running and + replaces any pane that is only a prompt. - board create any missing pane in this project's session - board --folder the same, for another project - board --replace close this project's panes, then build all eight again - board --reset close every pane in the session, then build - project-reset clear the panes and the worktrees, keep the history - project-delete remove the sessions, the Paperclip project, .git and the dot files - (says what would go; add --yes to do it) - merge rebase, check, review and merge one task (asks first) - review [--post] run ocr over one task's diff, and record the verdict - triage [--ask next|failure] ask Mouse-1 one question about a task +STARTING OVER + office reset clear the panes and the worktrees; keeps the + folder, .harness, the git history and every + Paperclip record + office reset --dry-run say what would go + office reset --yes do it + office reset --yes --delete-folder remove the folder too -MACHINES (name, or the address ssh reaches) - nixos-desktop .13 192.168.20.13 hosts Paperclip, the daemon, the main session - sam-4screen-desktop .27 192.168.20.27 your working desktop - sam-thinkpad .51 192.168.20.51 the laptop - -EVERY MACHINE, FROM THE TERMINAL - - On .13, a folder that is on .13: - office ~/paseo/projects/test-alpha --project "test-alpha" - - On .27, adopting a folder that lives on .13: - office /home/sam/paseo/projects/email_agent --project "Email Agent" --host nixos-desktop - - On .27, a folder that is on .27 (stays local, no --host): - office ~/src/thing --new "thing" - - On .51, the same folder on .51: - office ~/src/thing --project "thing" --host sam-thinkpad - - Then attach. Nothing may move your client from inside a pane, so the last step is yours: - tuios attach on this machine - tuios attach --host nixos-desktop to a session on another machine + office delete remove the TUIOS sessions, every Paperclip + project that claims the folder, the registry + lines, and .git / .gitignore / .harness / + .paperclip / .pi. Keeps ordinary files. + office delete --yes do it + office delete --yes --delete-folder remove the folder as well WORKTREES, AND WHY GIT MATTERS A new project gets a worktree policy: Paperclip makes a branch and a separate @@ -445,52 +440,75 @@ WORKTREES, AND WHY GIT MATTERS When the folder cannot do that yet, office asks, and never writes history: - Y make the repository, write .gitignore containing .paperclip/, stage - everything, and stop. Commit in lazygit, then run this again. + Y make the repository, write .gitignore containing .paperclip/, and stop. + Nothing is staged. Ignore first, then stage, then commit, in lazygit. N create the project in place (local_path): a task edits the folder directly, there is no diff and nothing to merge. - Y leaves you in lazygit's hands on purpose: what you commit is what the agent - will see, and .gitignore is how you keep a file out of its view. - --commit-all does the whole thing in one go, for anyone who does not care. + Y opens a session with lazygit already in the folder, on the machine that owns + it, and tells you how to attach. Ignore with i, stage all with a, commit with c. + Then run the same office command again and it creates the project. - The base branch is read from the folder, not assumed to be main. + .paperclip/ is written into .gitignore every time and is not optional: it is + where Paperclip puts the worktrees, inside the project folder. + Ignoring a file does NOT untrack one that is already committed: office warns + and gives you the git rm --cached line when it finds that. WHERE THINGS RUN Panes run on the machine that owns the folder. Agents run on the machine that serves Paperclip, because Paperclip starts them itself. If those differ, the - panes work and the agents cannot see the files, so --new refuses unless + panes work and the agents cannot see the files, so office --new refuses unless --anywhere is given. -ON ANOTHER MACHINE - --host copies this folder of commands to the other machine, at - ~/.local/share/paperclip-harness/bin/ - and leaves a short ~/.local/bin/office there, so it can be run from that - machine too. Nothing has to be installed by hand. On .27 and .51 ~/.local/bin - is not on PATH yet, so there you call it by its full path: - ~/.local/share/paperclip-harness/bin/office +MACHINES (name, or the address ssh reaches) + nixos-desktop .13 192.168.20.13 hosts Paperclip, the daemon, the main session + sam-4screen-desktop .27 192.168.20.27 your working desktop + sam-thinkpad .51 192.168.20.51 the laptop -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. +FROM A TERMINAL, ON ANY MACHINE + a folder on .13, typed on .13: + office ~/paseo/projects/test-alpha --project "test-alpha" + a folder on .13, typed on .27: + office /home/sam/TEST-BETA --new "TEST-BETA" --host nixos-desktop + a folder on .27, typed on .27: + office ~/src/thing --new "thing" + then attach; nothing may move your client from inside a pane: + tuios attach + tuios attach --host nixos-desktop -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 - running and replaces any pane that is only a prompt. + --host copies these commands to the other machine at + ~/.local/share/paperclip-harness/bin/ and leaves ~/.local/bin/office there, so + a machine that has never seen this repository still works. + +MERGE AND REVIEW + office merge rebase onto the base, run the project's check, run ocr, + merge, re-run the check, roll back if it fails. It asks + for confirmation before touching anything. + office review ocr over one task's diff; --post records the verdict + office triage one four-option Mouse-1 question, posted as a comment KEYS INSIDE THE PANES ( ) change tab tab change panel enter open / choose r re-read Paperclip R rebuild all panes ? list the keys - t open lazygit in the project folder (stage, ignore, commit) + t tools (lazygit, yazi, nvim, hunk, fzf, ocr, shell) + g lazygit, straight away, in the project folder h j k l z n and digits belong to TUIOS and never reach the program. +THE PROJECT PANE + overview · git · facts · costs · settings · projects + The git tab reads the folder and Paperclip together: repository, branch, + commits, remote, what is staged, what .gitignore holds, the worktree policy, + and Paperclip's own scan of every task worktree (dirty, ahead, behind, merged). + +WHILE A FOLDER CANNOT HOLD A WORKTREE + Every pane carries one line at the top, naming what is wrong and where to look. + It is read when the pane starts; fix the folder and press R to clear it. + MORE - docs/STARTUP.md these commands, the rule, and the three rebuild levels - docs/WORKSPACES.md what each of the eight panes is for - docs/CLEAN-TESTS.md the two end-to-end tests and what they measured + docs/STARTUP.md these commands, the rule, the rebuild levels + docs/WORKSPACES.md what each of the eight panes is for + docs/RUNS-AND-MERGE.md how a task becomes merged work + docs/CLEAN-TESTS.md the two end-to-end tests and what they measured """ @@ -680,6 +698,38 @@ def note_ignore(folder, opts): h.say(" take Paperclip's task worktrees with it. Add it before you commit.") +# The commands that have their own script next to this one. `office board` and +# `board` must do the same thing: a person should only have to remember office. +SIBLINGS = { + "board": "board", + "pages": "board", + "build": "board", + "reset": "project-reset", + "project-reset": "project-reset", + "delete": "project-delete", + "project-delete": "project-delete", + "forget": "project-delete", + "merge": "merge", + "review": "review", + "triage": "triage", +} + + +def forward(argv): + """Run a sibling command with the rest of the arguments, or return None.""" + if not argv: + return None + name = SIBLINGS.get(argv[0]) + if name is None: + return None + script = os.path.join(HERE, name) + if not os.path.isfile(script): + raise h.Refused(2, "no %s next to office; the repository looks incomplete" % name) + # exec, not a subprocess: this program is a router for this one call and has + # nothing left to do, so the sibling should be the process the person sees. + os.execv(sys.executable, [sys.executable, script] + argv[1:]) + + def main(): if os.geteuid() == 0: h.say("office: run this as your own user, not with sudo.") @@ -687,6 +737,10 @@ def main(): h.say(" neither of those exists for root, and files it wrote would belong to root.") return 2 argv = sys.argv[1:] + if argv and argv[0] in ("help", "-h", "--help"): + h.say(USAGE.strip()) + return 0 + forward(argv) if not argv or "-h" in argv or "--help" in argv: h.say(USAGE.strip()) return 0 diff --git a/bin/project-delete b/bin/project-delete index a4c7846..056a35f 100755 --- a/bin/project-delete +++ b/bin/project-delete @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Remove every trace of a project, and nothing else. - bin/project-delete # say what would go, change nothing - bin/project-delete --yes # do it + office delete # say what would go, change nothing + office delete --yes # do it What it removes, in this order: diff --git a/bin/project-reset b/bin/project-reset index b623090..4b2e20e 100755 --- a/bin/project-reset +++ b/bin/project-reset @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """Start a project again: close its pages, remove its worktrees, keep its work. - bin/project-reset --dry-run - bin/project-reset --yes - bin/project-reset --yes --delete-folder + office reset --dry-run + office reset --yes + office reset --yes --delete-folder What it removes, and nothing else: @@ -36,7 +36,7 @@ def main(): argv = sys.argv[1:] args = [a for a in argv if not a.startswith("--")] if not args: - h.say("usage: bin/project-reset [--dry-run] [--yes] [--delete-folder]") + h.say("usage: office reset [--dry-run] [--yes] [--delete-folder]") return 2 folder = os.path.abspath(os.path.expanduser(args[0])) dry = "--dry-run" in argv diff --git a/bin/review b/bin/review index c033929..cbf22a5 100755 --- a/bin/review +++ b/bin/review @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Review one task's diff and print the verdict. Merge nothing. - bin/review AUD-164 # review and print - bin/review AUD-164 --post # also write the verdict on the task + office review AUD-164 # review and print + office review AUD-164 --post # also write the verdict on the task This is the same review `bin/merge` runs, on its own, so a person can look before the merge is attempted. Exit 0 means nothing blocking was found; exit 5 @@ -20,7 +20,7 @@ def main(): args = [a for a in sys.argv[1:] if not a.startswith("--")] post = "--post" in sys.argv if len(args) != 1: - h.say("usage: bin/review [--post]") + h.say("usage: office review [--post]") return 2 issue = h.find_issue(args[0]) diff --git a/bin/triage b/bin/triage index 2544ec7..132a81a 100755 --- a/bin/triage +++ b/bin/triage @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Ask Mouse-1 one four-option question about a task, and record the answer. - bin/triage AUD-171 # what does this task need next - bin/triage AUD-171 --ask redo # a different question set + office triage AUD-171 # what does this task need next + office triage AUD-171 --ask redo # a different question set Mouse-1 answers a typed multiple-choice question about a state you describe. It does not write code, run tools, or decide whether work is finished. It labels a @@ -85,7 +85,7 @@ def ask(state, question): def main(): args = [a for a in sys.argv[1:] if not a.startswith("--")] if not args: - h.say("usage: bin/triage [--ask next|failure]") + h.say("usage: office triage [--ask next|failure]") return 2 which = "next" if "--ask" in sys.argv: