office: one word for everything — forward board/reset/delete/merge/review/triage, and a help page that lists all of it
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Build or repair a project's panes, in a fixed order.
|
"""Build or repair a project's panes, in a fixed order.
|
||||||
|
|
||||||
bin/board # the project in the current folder
|
office board # the project in the current folder
|
||||||
bin/board --folder ~/src/x # or another one
|
office board --folder ~/src/x # or another one
|
||||||
bin/board --replace # close this project's panes and build again
|
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
|
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
|
workspace holds a shell: it is the pane that never exits, so the session cannot
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def main():
|
|||||||
check_only = "--check" in args
|
check_only = "--check" in args
|
||||||
args = [a for a in args if not a.startswith("--")]
|
args = [a for a in args if not a.startswith("--")]
|
||||||
if len(args) != 1:
|
if len(args) != 1:
|
||||||
h.say("usage: bin/merge <task> [--check]")
|
h.say("usage: office merge <task> [--check]")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
issue = h.find_issue(args[0])
|
issue = h.find_issue(args[0])
|
||||||
|
|||||||
202
bin/office
202
bin/office
@@ -371,70 +371,65 @@ def run_on_remote(host, folder, opts):
|
|||||||
return 0
|
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 <folder> … open a folder as a project
|
||||||
|
office help this page
|
||||||
|
office board … build or repair the panes
|
||||||
|
office reset <folder> … clear panes and worktrees, keep the history
|
||||||
|
office delete <folder> … remove sessions, the Paperclip project, .git, dot files
|
||||||
|
office merge <task> … rebase, check, review, merge one task
|
||||||
|
office review <task> … run ocr over one task's diff
|
||||||
|
office triage <task> … 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 RULE
|
||||||
The folder and the session live on the same machine. If you are typing inside
|
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 <folder>
|
the session, on the machine that owns the folder, it is just: office <folder>
|
||||||
Use --host only when the folder is on a different machine from your shell.
|
Use --host only when the folder is on a different machine from your shell.
|
||||||
|
|
||||||
WHAT IT DOES
|
OPENING A FOLDER
|
||||||
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 <folder>/.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
|
|
||||||
|
|
||||||
COMMANDS
|
|
||||||
|
|
||||||
Each of these is its own command on your PATH, not a subcommand of office:
|
|
||||||
type `board`, not `office board`.
|
|
||||||
|
|
||||||
office <folder> --project "<Name>" adopt a project Paperclip already has
|
office <folder> --project "<Name>" adopt a project Paperclip already has
|
||||||
office <folder> --new "<Name>" create the Paperclip project as well
|
office <folder> --new "<Name>" create the Paperclip project as well
|
||||||
office . adopt the folder you are standing in
|
office . adopt the folder you are standing in
|
||||||
office <folder> --session <name> name the session instead of the folder
|
office <folder> --session <name> name the session instead of the folder
|
||||||
office <folder> --project "<Name>" --host <machine> the folder is on that machine
|
office <folder> … --host <machine> the folder is on that machine
|
||||||
office <folder> --new "<Name>" --dry say what would happen, write nothing
|
office <folder> … --dry say what would happen, write nothing
|
||||||
office <folder> --new "<Name>" (with no repository it asks Y or N)
|
|
||||||
office <folder> --new "<Name>" --commit-all make the repository and commit everything
|
|
||||||
office <folder> --new "<Name>" --anywhere allow it although agents cannot see it
|
|
||||||
|
|
||||||
board create any missing pane in this project's session
|
What it does, in order: reads the folder; runs locally if the named machine is
|
||||||
board --folder <folder> the same, for another project
|
this one; otherwise checks the login and runs the same command there; adopts or
|
||||||
|
creates the Paperclip project; writes what is missing in <folder>/.harness/ and
|
||||||
|
never overwrites; records the project in ~/.config/paperclip-harness/projects.toml;
|
||||||
|
creates the session on that machine's daemon; builds eight panes.
|
||||||
|
|
||||||
|
PAGES: office board [--folder <f>] [--replace] [--reset]
|
||||||
|
board create any pane that is missing, close nothing
|
||||||
board --replace close this project's panes, then build all eight again
|
board --replace close this project's panes, then build all eight again
|
||||||
board --reset close every pane in the session, then build
|
board --reset close every pane in the session, wait, then build
|
||||||
project-reset <folder> clear the panes and the worktrees, keep the history
|
board --folder <folder> do it for another project
|
||||||
project-delete <folder> remove the sessions, the Paperclip project, .git and the dot files
|
|
||||||
(says what would go; add --yes to do it)
|
|
||||||
merge <task> rebase, check, review and merge one task (asks first)
|
|
||||||
review <task> [--post] run ocr over one task's diff, and record the verdict
|
|
||||||
triage <task> [--ask next|failure] ask Mouse-1 one question about a task
|
|
||||||
|
|
||||||
MACHINES (name, or the address ssh reaches)
|
Inside a pane, R rebuilds everything and r re-reads Paperclip. After a reboot
|
||||||
nixos-desktop .13 192.168.20.13 hosts Paperclip, the daemon, the main session
|
TUIOS restores a session with a bare shell in each pane and keeps the pane
|
||||||
sam-4screen-desktop .27 192.168.20.27 your working desktop
|
names, so run board once: it now checks what each pane is really running and
|
||||||
sam-thinkpad .51 192.168.20.51 the laptop
|
replaces any pane that is only a prompt.
|
||||||
|
|
||||||
EVERY MACHINE, FROM THE TERMINAL
|
STARTING OVER
|
||||||
|
office reset <folder> clear the panes and the worktrees; keeps the
|
||||||
|
folder, .harness, the git history and every
|
||||||
|
Paperclip record
|
||||||
|
office reset <folder> --dry-run say what would go
|
||||||
|
office reset <folder> --yes do it
|
||||||
|
office reset <folder> --yes --delete-folder remove the folder too
|
||||||
|
|
||||||
On .13, a folder that is on .13:
|
office delete <folder> remove the TUIOS sessions, every Paperclip
|
||||||
office ~/paseo/projects/test-alpha --project "test-alpha"
|
project that claims the folder, the registry
|
||||||
|
lines, and .git / .gitignore / .harness /
|
||||||
On .27, adopting a folder that lives on .13:
|
.paperclip / .pi. Keeps ordinary files.
|
||||||
office /home/sam/paseo/projects/email_agent --project "Email Agent" --host nixos-desktop
|
office delete <folder> --yes do it
|
||||||
|
office delete <folder> --yes --delete-folder remove the folder as well
|
||||||
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 <session> on this machine
|
|
||||||
tuios attach --host nixos-desktop <session> to a session on another machine
|
|
||||||
|
|
||||||
WORKTREES, AND WHY GIT MATTERS
|
WORKTREES, AND WHY GIT MATTERS
|
||||||
A new project gets a worktree policy: Paperclip makes a branch and a separate
|
A new project gets a worktree policy: Paperclip makes a branch and a separate
|
||||||
@@ -445,51 +440,74 @@ WORKTREES, AND WHY GIT MATTERS
|
|||||||
|
|
||||||
When the folder cannot do that yet, office asks, and never writes history:
|
When the folder cannot do that yet, office asks, and never writes history:
|
||||||
|
|
||||||
Y make the repository, write .gitignore containing .paperclip/, stage
|
Y make the repository, write .gitignore containing .paperclip/, and stop.
|
||||||
everything, and stop. Commit in lazygit, then run this again.
|
Nothing is staged. Ignore first, then stage, then commit, in lazygit.
|
||||||
N create the project in place (local_path): a task edits the folder
|
N create the project in place (local_path): a task edits the folder
|
||||||
directly, there is no diff and nothing to merge.
|
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
|
Y opens a session with lazygit already in the folder, on the machine that owns
|
||||||
will see, and .gitignore is how you keep a file out of its view.
|
it, and tells you how to attach. Ignore with i, stage all with a, commit with c.
|
||||||
--commit-all does the whole thing in one go, for anyone who does not care.
|
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
|
WHERE THINGS RUN
|
||||||
Panes run on the machine that owns the folder. Agents run on the machine that
|
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
|
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.
|
--anywhere is given.
|
||||||
|
|
||||||
ON ANOTHER MACHINE
|
MACHINES (name, or the address ssh reaches)
|
||||||
--host copies this folder of commands to the other machine, at
|
nixos-desktop .13 192.168.20.13 hosts Paperclip, the daemon, the main session
|
||||||
~/.local/share/paperclip-harness/bin/
|
sam-4screen-desktop .27 192.168.20.27 your working desktop
|
||||||
and leaves a short ~/.local/bin/office there, so it can be run from that
|
sam-thinkpad .51 192.168.20.51 the laptop
|
||||||
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 <folder>
|
|
||||||
|
|
||||||
WHEN THE FILES ARE ON ANOTHER MACHINE
|
FROM A TERMINAL, ON ANY MACHINE
|
||||||
The git work has to happen where the files are, because that is where the
|
a folder on .13, typed on .13:
|
||||||
commit, the worktree and the agent are. So when a folder cannot hold a
|
office ~/paseo/projects/test-alpha --project "test-alpha"
|
||||||
worktree yet, office makes a session on THAT machine with lazygit already
|
a folder on .13, typed on .27:
|
||||||
open in the folder, and tells you how to attach to it. No ssh, no cd.
|
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 <session>
|
||||||
|
tuios attach --host nixos-desktop <session>
|
||||||
|
|
||||||
AFTER A REBOOT
|
--host copies these commands to the other machine at
|
||||||
TUIOS restores a session with a fresh shell in every pane and keeps the pane
|
~/.local/share/paperclip-harness/bin/ and leaves ~/.local/bin/office there, so
|
||||||
names. Run board once for that project: it checks what each pane is actually
|
a machine that has never seen this repository still works.
|
||||||
running and replaces any pane that is only a prompt.
|
|
||||||
|
MERGE AND REVIEW
|
||||||
|
office merge <task> 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 <task> ocr over one task's diff; --post records the verdict
|
||||||
|
office triage <task> one four-option Mouse-1 question, posted as a comment
|
||||||
|
|
||||||
KEYS INSIDE THE PANES
|
KEYS INSIDE THE PANES
|
||||||
( ) change tab tab change panel enter open / choose
|
( ) change tab tab change panel enter open / choose
|
||||||
r re-read Paperclip R rebuild all panes ? list the keys
|
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.
|
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
|
MORE
|
||||||
docs/STARTUP.md these commands, the rule, and the three rebuild levels
|
docs/STARTUP.md these commands, the rule, the rebuild levels
|
||||||
docs/WORKSPACES.md what each of the eight panes is for
|
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
|
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.")
|
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():
|
def main():
|
||||||
if os.geteuid() == 0:
|
if os.geteuid() == 0:
|
||||||
h.say("office: run this as your own user, not with sudo.")
|
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.")
|
h.say(" neither of those exists for root, and files it wrote would belong to root.")
|
||||||
return 2
|
return 2
|
||||||
argv = sys.argv[1:]
|
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:
|
if not argv or "-h" in argv or "--help" in argv:
|
||||||
h.say(USAGE.strip())
|
h.say(USAGE.strip())
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Remove every trace of a project, and nothing else.
|
"""Remove every trace of a project, and nothing else.
|
||||||
|
|
||||||
bin/project-delete <folder> # say what would go, change nothing
|
office delete <folder> # say what would go, change nothing
|
||||||
bin/project-delete <folder> --yes # do it
|
office delete <folder> --yes # do it
|
||||||
|
|
||||||
What it removes, in this order:
|
What it removes, in this order:
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Start a project again: close its pages, remove its worktrees, keep its work.
|
"""Start a project again: close its pages, remove its worktrees, keep its work.
|
||||||
|
|
||||||
bin/project-reset <folder> --dry-run
|
office reset <folder> --dry-run
|
||||||
bin/project-reset <folder> --yes
|
office reset <folder> --yes
|
||||||
bin/project-reset <folder> --yes --delete-folder
|
office reset <folder> --yes --delete-folder
|
||||||
|
|
||||||
What it removes, and nothing else:
|
What it removes, and nothing else:
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ def main():
|
|||||||
argv = sys.argv[1:]
|
argv = sys.argv[1:]
|
||||||
args = [a for a in argv if not a.startswith("--")]
|
args = [a for a in argv if not a.startswith("--")]
|
||||||
if not args:
|
if not args:
|
||||||
h.say("usage: bin/project-reset <folder> [--dry-run] [--yes] [--delete-folder]")
|
h.say("usage: office reset <folder> [--dry-run] [--yes] [--delete-folder]")
|
||||||
return 2
|
return 2
|
||||||
folder = os.path.abspath(os.path.expanduser(args[0]))
|
folder = os.path.abspath(os.path.expanduser(args[0]))
|
||||||
dry = "--dry-run" in argv
|
dry = "--dry-run" in argv
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Review one task's diff and print the verdict. Merge nothing.
|
"""Review one task's diff and print the verdict. Merge nothing.
|
||||||
|
|
||||||
bin/review AUD-164 # review and print
|
office review AUD-164 # review and print
|
||||||
bin/review AUD-164 --post # also write the verdict on the task
|
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
|
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
|
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("--")]
|
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||||
post = "--post" in sys.argv
|
post = "--post" in sys.argv
|
||||||
if len(args) != 1:
|
if len(args) != 1:
|
||||||
h.say("usage: bin/review <task> [--post]")
|
h.say("usage: office review <task> [--post]")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
issue = h.find_issue(args[0])
|
issue = h.find_issue(args[0])
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Ask Mouse-1 one four-option question about a task, and record the answer.
|
"""Ask Mouse-1 one four-option question about a task, and record the answer.
|
||||||
|
|
||||||
bin/triage AUD-171 # what does this task need next
|
office triage AUD-171 # what does this task need next
|
||||||
bin/triage AUD-171 --ask redo # a different question set
|
office triage AUD-171 --ask redo # a different question set
|
||||||
|
|
||||||
Mouse-1 answers a typed multiple-choice question about a state you describe. It
|
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
|
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():
|
def main():
|
||||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||||
if not args:
|
if not args:
|
||||||
h.say("usage: bin/triage <task> [--ask next|failure]")
|
h.say("usage: office triage <task> [--ask next|failure]")
|
||||||
return 2
|
return 2
|
||||||
which = "next"
|
which = "next"
|
||||||
if "--ask" in sys.argv:
|
if "--ask" in sys.argv:
|
||||||
|
|||||||
Reference in New Issue
Block a user