proctolog 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -3
  3. data/README.md +32 -18
  4. data/app/controllers/proctolog/application_controller.rb +2 -2
  5. data/app/controllers/proctolog/bursts_controller.rb +1 -1
  6. data/app/controllers/proctolog/choices_controller.rb +3 -1
  7. data/app/controllers/proctolog/decisions_controller.rb +1 -1
  8. data/app/controllers/proctolog/notes_controller.rb +2 -2
  9. data/app/controllers/proctolog/prompts_controller.rb +1 -1
  10. data/app/controllers/proctolog/pull_requests_controller.rb +2 -4
  11. data/app/controllers/proctolog/review_data_controller.rb +3 -1
  12. data/app/controllers/proctolog/shares_controller.rb +27 -0
  13. data/app/helpers/proctolog/decisions_helper.rb +6 -2
  14. data/app/helpers/proctolog/desk_helper.rb +48 -8
  15. data/app/helpers/proctolog/rounds_helper.rb +4 -2
  16. data/app/helpers/proctolog/shares_helper.rb +11 -0
  17. data/app/helpers/proctolog/standings_helper.rb +5 -4
  18. data/app/models/proctolog/burst.rb +17 -8
  19. data/app/models/proctolog/choices.rb +8 -0
  20. data/app/models/proctolog/doctor.rb +23 -12
  21. data/app/models/proctolog/github.rb +1 -1
  22. data/app/models/proctolog/localhost.rb +17 -0
  23. data/app/models/proctolog/marks.rb +1 -1
  24. data/app/models/proctolog/note.rb +4 -0
  25. data/app/models/proctolog/owner.rb +3 -0
  26. data/app/models/proctolog/pick.rb +12 -0
  27. data/app/models/proctolog/rounds.rb +37 -0
  28. data/app/models/proctolog/server.rb +1 -7
  29. data/app/models/proctolog/setup.rb +190 -0
  30. data/app/models/proctolog/share/ngrok.rb +120 -0
  31. data/app/models/proctolog/share/settings.rb +23 -0
  32. data/app/models/proctolog/share/tailscale.rb +103 -0
  33. data/app/models/proctolog/share.rb +101 -0
  34. data/app/models/proctolog/standing.rb +24 -7
  35. data/app/views/proctolog/bursts/_line.html.erb +2 -2
  36. data/app/views/proctolog/bursts/_side.html.erb +6 -5
  37. data/app/views/proctolog/bursts/_side_stop.html.erb +4 -4
  38. data/app/views/proctolog/notes/_card.html.erb +7 -3
  39. data/app/views/proctolog/open_pull_requests/_side.html.erb +4 -3
  40. data/app/views/proctolog/open_pull_requests/_start.html.erb +8 -0
  41. data/app/views/proctolog/open_pull_requests/index.html.erb +4 -5
  42. data/app/views/proctolog/open_pull_requests/start/_first_time.html.erb +3 -0
  43. data/app/views/proctolog/open_pull_requests/start/_nothing_open.html.erb +3 -0
  44. data/app/views/proctolog/open_pull_requests/start/_nothing_written.html.erb +3 -0
  45. data/app/views/proctolog/shares/_line.html.erb +5 -0
  46. data/app/views/proctolog/shares/show.html.erb +48 -0
  47. data/app/views/proctolog/shares/ways/_ngrok.html.erb +6 -0
  48. data/app/views/proctolog/shares/ways/_tailscale.html.erb +2 -0
  49. data/bin/proctolog +154 -27
  50. data/completions/_proctolog +31 -6
  51. data/config/application.rb +23 -4
  52. data/config/initializers/grouse.rb +1 -1
  53. data/config/routes.rb +3 -0
  54. data/docs/manual/bursts.md +7 -2
  55. data/docs/manual/deciding.md +4 -1
  56. data/docs/manual/handover.md +6 -0
  57. data/docs/manual/pr-format.md +1 -1
  58. data/docs/manual/reading.md +54 -27
  59. data/docs/manual/setup.md +59 -0
  60. data/docs/manual/sharing.md +77 -0
  61. data/docs/manual/updates.md +11 -4
  62. data/lib/proctolog/changelog/news.rb +67 -0
  63. data/lib/proctolog/changelog.rb +75 -0
  64. data/lib/proctolog/installation.rb +19 -0
  65. data/lib/proctolog/version.rb +1 -1
  66. data/public/asking.js +31 -4
  67. data/public/decisions.js +4 -4
  68. data/public/desk.js +10 -6
  69. data/public/keys.js +6 -0
  70. data/public/notes.js +164 -58
  71. data/public/pick.css +21 -0
  72. data/public/respond.css +6 -0
  73. data/public/respond.js +51 -7
  74. data/public/review-drafts.js +47 -9
  75. data/public/review-rounds.js +27 -3
  76. data/public/share.css +58 -0
  77. data/public/side.js +11 -20
  78. data/public/terminal.css +295 -34
  79. data/public/viewer.js +700 -173
  80. metadata +22 -1
data/bin/proctolog CHANGED
@@ -17,6 +17,7 @@ end
17
17
  require_relative "../config/application"
18
18
  require_relative "../app/models/proctolog/project"
19
19
  require "rbconfig"
20
+ require "socket"
20
21
  require "thor"
21
22
 
22
23
  module Proctolog
@@ -26,9 +27,15 @@ module Proctolog
26
27
 
27
28
  # A command prints what it was asked for and nothing else, and asks
28
29
  # 1Password for no keys. The desk's log, with what Grouse says at boot it
29
- # is missing, and the keys are for `serve`, which says so in the block.
30
- def boot!
30
+ # is missing, and the keys are for the owner's `serve`, which says so in
31
+ # the block. A trial desk serves without them.
32
+ def boot!(&)
31
33
  Project.current = Project.discover(options[:project] || Dir.pwd)
34
+ boot_desk!(&)
35
+ end
36
+
37
+ # The desk alone, for a command of this Mac's rather than a checkout's.
38
+ def boot_desk!
32
39
  Desk.config.log_level = :error
33
40
  Desk.config.x.reads_keys = false
34
41
  yield Desk.config if block_given?
@@ -128,6 +135,62 @@ module Proctolog
128
135
  def tree = super
129
136
  end
130
137
 
138
+ class ShareCommands < Thor
139
+ include Booting
140
+
141
+ def self.exit_on_failure? = true
142
+
143
+ class_option :port, type: :numeric, default: 4040, aliases: "-p", desc: "The port of the desk"
144
+ default_command :start
145
+
146
+ desc "start", "Share the desk through Tailscale or ngrok, and print its address"
147
+ long_desc <<~TEXT
148
+ Lets you reach the desk on this Mac from your phone, or a computer elsewhere. Through Tailscale, only
149
+ the devices signed in to your tailnet reach it, at this Mac's name there, and nothing goes on the
150
+ internet. Through ngrok, it is on the internet behind ngrok's sign-in with Google, which lets in only
151
+ the addresses --allow names, your Git email when none is named. Without --via, it shares the way named
152
+ last, or the first one ready, Tailscale first. The share stays open until `proctolog share stop`, the
153
+ desk running or not; one already open is only printed. `proctolog share status` says what each way
154
+ needs first.
155
+ TEXT
156
+ option :via, type: :string, enum: %w[tailscale ngrok], desc: "The way to share it, remembered for the next time"
157
+ option :allow, type: :string, repeatable: true, desc: "An email that may sign in through ngrok, in place of your Git email"
158
+ def start
159
+ boot_desk!
160
+ abort "No desk answers on :#{port}. Start one with the share: proctolog serve --share#{port_option}" unless Localhost.answers?(port, within: 1)
161
+ puts Share.open(port, via: options[:via], allow: options[:allow])
162
+ puts "Stop it with: proctolog share stop#{port_option}"
163
+ rescue Share::Refused => e
164
+ abort e.message
165
+ end
166
+
167
+ desc "stop", "Stop sharing the desk: it is only on this Mac again"
168
+ def stop
169
+ boot_desk!
170
+ share = Share.at(port)
171
+ share&.close
172
+ puts share ? "The desk on :#{port} is only on this Mac again." : "The desk on :#{port} was not shared."
173
+ rescue Share::Refused => e
174
+ abort e.message
175
+ end
176
+
177
+ desc "status", "Say whether the desk is shared, and what Tailscale and ngrok each need to share it"
178
+ def status
179
+ boot_desk!
180
+ puts Share.at(port) || "The desk on :#{port} is only on this Mac, at #{Desk.address(port)}."
181
+ Share.ways.each { puts it.ready? ? "ok #{it.name}: ready, and #{it.who}" : "-- #{it.unready}" }
182
+ end
183
+
184
+ desc "tree", "Print a tree of all available commands", hide: true
185
+ def tree = super
186
+
187
+ no_commands do
188
+ def port = options[:port]
189
+
190
+ def port_option = (" --port #{port}" unless port == 4040)
191
+ end
192
+ end
193
+
131
194
  class CLI < Thor
132
195
  include Booting
133
196
 
@@ -135,28 +198,43 @@ module Proctolog
135
198
 
136
199
  class_option :project, type: :string, aliases: "-C", desc: "The Git checkout whose bursts to open"
137
200
 
201
+ no_commands do
202
+ # Bare, proctolog shows its commands under the one to start with.
203
+ def help(command = nil, subcommand = false)
204
+ puts "New here? proctolog setup takes you to the desk, a step at a time.", "" unless command
205
+ super
206
+ end
207
+ end
208
+
138
209
  desc "burst SUBCOMMAND", "Start a burst of your own, or list the bursts there are"
139
210
  subcommand "burst", BurstCommands
140
211
 
141
212
  desc "unopened SUBCOMMAND", "List the pull requests nobody has opened yet, or say where one is written"
142
213
  subcommand "unopened", UnopenedCommands
143
214
 
144
- desc "serve", "Open the desk on http://localhost:4040"
215
+ desc "share SUBCOMMAND", "Share the desk with your phone or another computer, through Tailscale or ngrok"
216
+ subcommand "share", ShareCommands
217
+
218
+ desc "serve", "Open the desk on http://proctolog.localhost:4040"
145
219
  option :port, type: :numeric, default: 4040, aliases: "-p", desc: "The port to listen on"
220
+ option :share, type: :boolean, default: false, desc: "Share it too, as `proctolog share` does"
146
221
  def serve
147
222
  boot! do |config|
148
223
  config.log_level = :warn
149
- config.x.reads_keys = true
224
+ config.x.reads_keys = !Desk.trial?
225
+ config.x.port = options[:port]
150
226
  end
151
227
  require "rackup"
152
228
  Release.newer&.then { puts it.news }
153
229
  Housekeeping.start
154
- Rackup::Handler.get("puma").run(Desk, Host: "localhost", Port: options[:port])
230
+ Rackup::Handler.get("puma").run(Desk, Host: "localhost", Port: options[:port]) do |launcher|
231
+ launcher.events.after_booted { opened }
232
+ end
155
233
  rescue Errno::EADDRINUSE
156
- abort "Port #{options[:port]} is taken. If that is the desk, it is open at http://localhost:#{options[:port]}; " \
234
+ abort "Port #{options[:port]} is taken. If that is the desk, it is open at #{Desk.address(options[:port])}; " \
157
235
  "for a second one: proctolog serve --port #{options[:port] + 1}"
158
236
  rescue Project::Missing => e
159
- abort e.message
237
+ abort "#{e.message} New here? proctolog setup takes you to the desk, a step at a time."
160
238
  end
161
239
 
162
240
  desc "route [BURST]", "Print the order to take a burst in"
@@ -241,25 +319,20 @@ module Proctolog
241
319
  map %w[-v --version] => :version
242
320
  def version = puts("PRoctolog #{VERSION}")
243
321
 
244
- desc "install", "Link zsh's completion and the agent skills to this PRoctolog"
322
+ desc "install", "Another name for setup: link this PRoctolog, then set it up a step at a time"
245
323
  long_desc <<~TEXT
246
- Links zsh's completion into ~/.zsh/completions and the agent skills into ~/.claude/skills and
247
- ~/.agents/skills, from wherever this PRoctolog lives: the gem, or the checkout, which also gets its
248
- launcher in ~/.local/bin. Leaves anything of the same name that is not PRoctolog's as it is.
249
- `update` runs it for the version it installs.
324
+ Does what `proctolog setup` does, whichever of the two you remember: links the proctolog command, zsh's
325
+ completion and the agent skills to this PRoctolog, then takes you through the rest a step at a time.
326
+ `proctolog help setup` says how.
250
327
  TEXT
251
- def install
252
- puts Installation.here.install
253
- rescue Installation::Refused => e
254
- abort e.message
255
- end
328
+ def install = setup
256
329
 
257
330
  desc "update", "Install the latest PRoctolog, and link to it"
258
331
  long_desc <<~TEXT
259
332
  Asks RubyGems for the latest PRoctolog and, when it is newer than this one, installs it the way this one
260
333
  was: with `rv tool install` for one rvx runs, with `gem install` otherwise. A checkout is pulled
261
334
  forward instead, when it is clean and on origin's own branch, and its bundle installed. Then runs the
262
- new one's `install`. A desk open now runs on as it was: restart it to run the new one.
335
+ new one's `setup`, which links it. A desk open now runs on as it was: restart it to run the new one.
263
336
  TEXT
264
337
  def update
265
338
  installation = Installation.here
@@ -275,13 +348,22 @@ module Proctolog
275
348
  end
276
349
 
277
350
  no_commands do
351
+ # Once the desk listens: where it is, and where else, when it is shared.
352
+ # A share that cannot open leaves the desk open on this Mac.
353
+ def opened
354
+ puts "The desk is open at #{Desk.address}."
355
+ puts(options[:share] ? Share.open(Desk.port) : Share.at(Desk.port) || return)
356
+ rescue Share::Refused => e
357
+ warn "The desk is not shared. #{e.message}"
358
+ end
359
+
278
360
  # rv runs as the shell would, not on the Ruby and gems it gave this one.
279
361
  def take_with_rv(latest)
280
362
  Outside.run do |environment|
281
363
  system(environment, "rv", "tool", "install", "proctolog@#{latest}", "--gem-server", "#{Release.server}/", exception: true)
282
364
  puts taken(latest)
283
365
  # rvx runs the newest it holds, which is now this one; rv 0.7 runs no proctolog@VERSION.
284
- exec(environment, "rvx", "--no-install", "proctolog", "install")
366
+ exec(environment.merge(Installation.updating_from(VERSION)), "rvx", "--no-install", "proctolog", "setup")
285
367
  end
286
368
  end
287
369
 
@@ -290,17 +372,20 @@ module Proctolog
290
372
  system(RbConfig.ruby, File.join(RbConfig::CONFIG["bindir"], "gem"), "install", "proctolog", "--version", latest.version, "--clear-sources", "--source", Release.server, exception: true)
291
373
  puts taken(latest)
292
374
  # RubyGems' launcher takes the version to run as its first word.
293
- exec(RbConfig.ruby, File.join(Gem.bindir, "proctolog"), "_#{latest}_", "install")
375
+ exec(Installation.updating_from(VERSION), RbConfig.ruby, File.join(Gem.bindir, "proctolog"), "_#{latest}_", "setup")
294
376
  end
295
377
 
296
378
  # The checkout's own bundle, on this Ruby, as bin/proctolog runs it.
379
+ # Nothing to take when origin has nothing newer, as for a gem.
297
380
  def take_with_git(installation)
298
- puts installation.pull
381
+ pull = installation.pull
382
+ return puts("PRoctolog #{VERSION} in #{installation.root} is the latest: origin has nothing newer.") unless pull.moved?
383
+
299
384
  Bundler.with_unbundled_env do
300
385
  system(RbConfig.ruby, Gem.bin_path("bundler", "bundle"), "install", "--quiet", chdir: installation.root.to_s, exception: true)
301
386
  end
302
387
  puts "Pulled #{installation.root} forward. Restart the desk to run it."
303
- exec(RbConfig.ruby, installation.root.join("bin/install").to_s)
388
+ exec(Installation.updating_from(VERSION), RbConfig.ruby, installation.root.join("bin/install").to_s)
304
389
  end
305
390
 
306
391
  def taken(latest) = "Installed PRoctolog #{latest}. Restart the desk to run it: one open now still runs #{Release.running}."
@@ -316,13 +401,55 @@ module Proctolog
316
401
  Exits 0 when every need is met, 1 when one is not.
317
402
  TEXT
318
403
  def doctor
319
- project = begin
320
- Project.current = Project.discover(options[:project] || Dir.pwd)
404
+ project = discovered
405
+ boot_desk!
406
+ Doctor.new(project).tap { puts it }.then { exit(it.fine? ? 0 : 1) }
407
+ end
408
+
409
+ desc "setup", "Set PRoctolog up, a step at a time, from wherever you are to an open desk"
410
+ long_desc <<~TEXT
411
+ First links the proctolog command, zsh's completion and the agent skills to this PRoctolog: from the gem,
412
+ or the checkout, which also gets its launcher in ~/.local/bin. Anything of the same name that is not
413
+ PRoctolog's stays as it is. Then takes doctor's checks in the order you meet them: gh signed in, the
414
+ checkout you review and its GitHub repository, a first burst, what turns features on, sharing the desk
415
+ with your phone, and the desk itself. Each step says why it matters and what to do; Enter checks it again
416
+ once you have, and it is stamped DONE when it is met, or LATER when you leave it. Outside a checkout, its
417
+ step asks for the folder of one. Of anyone else's, nothing is installed for you. The last step opens the
418
+ desk with `proctolog serve`. `install` is another name for it, and `update` runs it for the version it
419
+ installs, which then only links and says what is new.
420
+
421
+ Outside a terminal it asks nothing: it prints each step as it stands, and exits 0 when the desk can
422
+ open, 1 when a step still waits on something the desk needs.
423
+ TEXT
424
+ def setup
425
+ installation = Installation.here
426
+ # An update hands over to the version it installed, to link it and say
427
+ # what is new: the owner asked for an update, not for a setup.
428
+ return puts(installation.install, "", installation.news) if installation.news
429
+
430
+ discovered
431
+ boot_desk!
432
+ setup = Setup.new(installation:, directory: options[:project] || Dir.pwd) { Doctor.new(discovered(it)) }
433
+ exit(setup.show ? 0 : 1) unless $stdin.tty?
434
+
435
+ case setup.walk
436
+ # The command that runs now, as a new terminal may not find proctolog yet.
437
+ in :serve then exec(RbConfig.ruby, $PROGRAM_NAME, "serve", *(["--project", Project.current.root.to_s] if Project.current))
438
+ in :finished then exit 0
439
+ in :stopped then exit 1
440
+ end
441
+ rescue Installation::Refused => e
442
+ abort e.message
443
+ end
444
+
445
+ no_commands do
446
+ # The checkout --project names, or the one this runs in, found again
447
+ # each time: one a setup step made is there the next time it checks.
448
+ def discovered(directory = options[:project] || Dir.pwd)
449
+ Project.current = Project.discover(directory)
321
450
  rescue Project::Missing
322
- nil
451
+ Project.current = nil
323
452
  end
324
- Desk.initialize! unless Desk.initialized?
325
- Doctor.new(project).tap { puts it }.then { exit(it.fine? ? 0 : 1) }
326
453
  end
327
454
  end
328
455
  end
@@ -5,7 +5,7 @@
5
5
  # where burst files were kept before; for check and burst new, the open pull
6
6
  # requests of the desk's last reading, with their titles, and the branches
7
7
  # whose pull requests nobody has opened yet; for unopened folder, every
8
- # branch of the checkout.
8
+ # branch of the checkout; for share, its commands and the ways to share.
9
9
 
10
10
  # "owner/name" of the checkout's GitHub origin.
11
11
  _proctolog_repository() {
@@ -47,9 +47,10 @@ _proctolog_parts() {
47
47
 
48
48
  _proctolog() {
49
49
  local root own named
50
- local -a commands burst_commands unopened_commands bursts pull_requests unopened branches parts project files
50
+ local -a commands burst_commands unopened_commands share_commands bursts pull_requests unopened branches parts project port files
51
51
  commands=(
52
- 'serve:Open the desk on http://localhost:4040'
52
+ 'serve:Open the desk on http://proctolog.localhost:4040'
53
+ 'share:Share the desk with your phone or another computer, through Tailscale or ngrok'
53
54
  'route:Print the order to take a burst in'
54
55
  'handover:Hand what the owner decided so far to the agent that runs it'
55
56
  'burst:Start a burst of your own, or list the bursts there are'
@@ -57,8 +58,9 @@ _proctolog() {
57
58
  'check:Check a description as the desk reads it, of a pull request or a file'
58
59
  'themes:Print the themes a decision may name, with what each means'
59
60
  'doctor:Say what this Mac and this checkout lack, a line each with the fix'
61
+ 'setup:Set PRoctolog up, a step at a time, from wherever you are to an open desk'
60
62
  'version:Print which PRoctolog this is'
61
- 'install:Link zsh'"'"'s completion and the agent skills to this PRoctolog'
63
+ 'install:Another name for setup: link this PRoctolog, then set it up a step at a time'
62
64
  'update:Install the latest PRoctolog, and link to it'
63
65
  'tree:Print a tree of all available commands'
64
66
  'help:Describe the commands, or one of them'
@@ -73,7 +75,14 @@ _proctolog() {
73
75
  'folder:Print the folder a pull request nobody has opened yet is written in'
74
76
  'help:Describe the unopened commands, or one of them'
75
77
  )
78
+ share_commands=(
79
+ 'start:Share the desk through Tailscale or ngrok, and print its address'
80
+ 'stop:Stop sharing the desk: it is only on this Mac again'
81
+ 'status:Say whether the desk is shared, and what Tailscale and ngrok each need'
82
+ 'help:Describe the share commands, or one of them'
83
+ )
76
84
  project=('(-C --project)'{-C,--project}'[Git checkout]:directory:_directories')
85
+ port=('(-p --port)'{-p,--port}'[The port of the desk]:port:')
77
86
  root=$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null)
78
87
  own=${PROCTOLOG_DATA:-$HOME/.local/share/proctolog}/$(_proctolog_repository "$root")/bursts
79
88
  bursts=($own/*.yml(N:t:r) $root/.proctolog/{bursts,reviews}/*.yml(N:t:r))
@@ -85,7 +94,23 @@ _proctolog() {
85
94
  command) _describe -t commands 'command' commands ;;
86
95
  argument)
87
96
  case $words[1] in
88
- serve) _arguments '(-p --port)'{-p,--port}'[The port to listen on]:port:' $project ;;
97
+ serve) _arguments $port '--share[Share it too, as proctolog share does]' $project ;;
98
+ share)
99
+ # Without a subcommand, share is start: its options come at once.
100
+ if (( CURRENT == 2 )) && [[ $words[2] != -* ]]; then
101
+ _describe -t commands 'share command' share_commands
102
+ else
103
+ case $words[2] in
104
+ start|-*)
105
+ _arguments $port \
106
+ '--via[The way to share it, remembered for the next time]:way:(tailscale ngrok)' \
107
+ '*--allow[An email that may sign in through ngrok]:email:' \
108
+ '1:: :' ;;
109
+ stop|status) _arguments $port '1:: :' ;;
110
+ help) _describe -t commands 'share command' share_commands ;;
111
+ esac
112
+ fi
113
+ ;;
89
114
  route) _arguments $project "1:burst:(${bursts})" ;;
90
115
  handover)
91
116
  named=${${words[2,-1]:#-*}[1]}
@@ -133,7 +158,7 @@ _proctolog() {
133
158
  unopened=(${(f)"$(_proctolog_unopened "$root")"})
134
159
  _arguments $project '(-f --file)'{-f,--file}'[A local Markdown file to check before it is published]:file:_files' \
135
160
  "1:pull request:{_describe -t pull-requests 'open pull request' pull_requests; _describe -t unopened 'not opened yet' unopened}" ;;
136
- themes|doctor|version|install|update) _arguments $project ;;
161
+ themes|doctor|setup|version|install|update) _arguments $project ;;
137
162
  help) _describe -t commands 'command' commands ;;
138
163
  esac
139
164
  ;;
@@ -53,6 +53,18 @@ module Proctolog
53
53
 
54
54
  def self.scratch = @scratch ||= Pathname(Dir.mktmpdir("proctolog")).tap { |made| at_exit { made.rmtree } }
55
55
 
56
+ def self.port = config.x.port
57
+
58
+ # Where the desk is on this Mac: a name of its own, which a browser's
59
+ # history completes from "procto" where localhost:4040 is one of many.
60
+ def self.address(port = self.port) = "http://proctolog.localhost:#{port}"
61
+
62
+ # A desk on marks of its own (PROCTOLOG_MARKS): a test's, or one an agent
63
+ # starts to try a change, often while the owner is away. It writes nothing
64
+ # to GitHub, and asks 1Password for no keys, which would wait on the owner
65
+ # to unlock it.
66
+ def self.trial? = ENV.key?("PROCTOLOG_MARKS")
67
+
56
68
  config.root = File.expand_path("..", __dir__)
57
69
  config.load_defaults 8.1
58
70
  config.eager_load = false
@@ -61,8 +73,14 @@ module Proctolog
61
73
  # Kept outside the installed app so an update does not sign out an open desk.
62
74
  config.secret_key_base = kept("secret_key_base") { SecureRandom.hex(64) }
63
75
  # Whatever the environment: a page of another site that points a name of
64
- # its own at this Mac is turned away.
65
- config.hosts = ["localhost", IPAddr.new("127.0.0.0/8"), IPAddr.new("::1")]
76
+ # its own at this Mac is turned away. Names under localhost are this
77
+ # Mac's alone, as browsers send them nowhere else: proctolog.localhost is
78
+ # the desk's own. While the desk is shared, it answers to the share's
79
+ # name too (Share).
80
+ config.hosts = [".localhost", IPAddr.new("127.0.0.0/8"), IPAddr.new("::1")]
81
+ config.host_authorization = {exclude: ->(request) { Proctolog::Share.carries?(request) }}
82
+ # The port the desk serves on, which `serve` names.
83
+ config.x.port = 4040
66
84
  config.logger = ActiveSupport::Logger.new($stdout)
67
85
  # Grouse says at boot what is not set up yet, which its tests need not hear.
68
86
  config.log_level = Rails.env.test? ? :error : :warn
@@ -80,8 +98,9 @@ module Proctolog
80
98
  config.active_job.queue_adapter = Rails.env.test? ? :test : :async
81
99
  # Whether Grouse is given the owner's keys from 1Password (config/keys.yml):
82
100
  # when the desk serves, and for Rails' own commands such as grouse:doctor.
83
- # A `proctolog` command that only prints asks 1Password for nothing.
84
- config.x.reads_keys = Rails.env.development?
101
+ # A `proctolog` command that only prints asks 1Password for nothing, and
102
+ # nor does a trial desk.
103
+ config.x.reads_keys = Rails.env.development? && !trial?
85
104
  config.active_storage.service = :local
86
105
  config.active_storage.variant_processor = :disabled
87
106
  # The migrations are the record; the desk brings its database up to them
@@ -86,7 +86,7 @@ Grouse.configure do |c|
86
86
  # The owner's keys, read from 1Password by config/keys.yml's references where the desk
87
87
  # reads keys at all (config.x.reads_keys). Without one, a voice note arrives as audio only
88
88
  # and a note is not written up; `op` says on the terminal why it could not read it. Tests
89
- # read none, and send nothing to a vendor.
89
+ # and trial desks (PROCTOLOG_MARKS) read none, and send nothing to a vendor.
90
90
  c.transcriber = Grouse::Transcribers::ElevenLabs.new(api_key: key.call("elevenlabs"))
91
91
  c.structurer = Grouse::Structurers::Anthropic.new(api_key: key.call("anthropic"))
92
92
  # c.transcriber = nil # turns a recording into words; with none, voice notes arrive as audio only
data/config/routes.rb CHANGED
@@ -17,6 +17,9 @@ Proctolog::Desk.routes.draw do
17
17
  # The pull requests, of every state, that a # typed in any box may mean.
18
18
  resources :mentions, only: :index
19
19
 
20
+ # The desk shared with the owner's other devices, through Tailscale or ngrok.
21
+ resource :share, only: %i[show create destroy]
22
+
20
23
  resources :bursts, only: %i[show create] do
21
24
  resource :put_away, only: %i[create destroy], controller: :put_away
22
25
  resources :pull_requests, only: :show, param: :number do
@@ -7,8 +7,8 @@ the one waiting for you; `proctolog route` and `proctolog handover` take it too
7
7
  when no burst is named. With no burst yet, it opens on the repository's open
8
8
  pull requests, at `/pull_requests`. "bursts & PRs" in the pane of every page
9
9
  goes there: every burst at its top, the newest first, then the pull requests.
10
- Its own pane says no burst is open, and lists your bursts with how far each
11
- one's decisions are. A burst put away says so beside its name in its pane.
10
+ Its own pane says no burst is open, and lists your bursts with how many of
11
+ each one's decisions wait: a bead of each grade, and how many of it. A burst put away says so beside its name in its pane.
12
12
  GitHub's stacks come first, each whole from the bottom up, then the pull requests in no
13
13
  stack: yours first, then those written for PRoctolog (they have decisions),
14
14
  then the newest. Each line says whose it is, what it sits on when that is not
@@ -27,6 +27,11 @@ Under "Not opened" come the branches with a description written for them that
27
27
  nobody has opened yet, each with "Review alone", and "Review the chain" for one
28
28
  that sits on another's branch ([how](unopened.md)).
29
29
 
30
+ "Start here" heads the page until you have a burst of your own, and whenever
31
+ there is nothing to decide: it says how many pull requests are written for
32
+ PRoctolog and how to open one, that none is yet and how to have them
33
+ rewritten, or that nothing is open and how to have some written.
34
+
30
35
  ## When a burst is done
31
36
 
32
37
  A pull request that merges leaves the route, and goes under "Merged" at its
@@ -17,7 +17,10 @@ question's text, “Ask Claude now” opens a new Claude Code session in the
17
17
  Claude app, in the checkout under review, with a prompt typed in: the decision
18
18
  quoted with its pull request, branch and head, the pictures it rests on where
19
19
  the desk keeps them on this Mac, and your question last, where you can go on
20
- typing. It asks the session to answer and change nothing. Nothing is sent
20
+ typing. It asks the session to answer and change nothing. A pull request
21
+ nobody has opened yet is named by its branch, its base and the file its
22
+ description is kept in, and the session is told to read it with git, since
23
+ GitHub has nothing of it. Nothing is sent
21
24
  until you send it there. “Copy prompt” copies the same prompt, for a session
22
25
  already open or another agent. With decisions selected, the prompt quotes each
23
26
  of them. Asking leaves the question here a draft and the decision open: once
@@ -56,6 +56,12 @@ to response, and Esc goes back to where `v` was pressed. Wherever you are in a
56
56
  response, `a` confirms it, `q` replies or asks, `c` requests a correction, and
57
57
  `l` defers. Ctrl/⌘ + Enter saves the reply; `e` exports the next pass.
58
58
 
59
+ A note of yours that an agent answers follows its answer in the marks, where
60
+ the page's Notes and an agent reading the files both see it: addressed once
61
+ the answer is in, confirmed once you confirm it here, open again once you
62
+ answer back. Its card says to judge it under Returned work and offers only
63
+ Withdraw. The pane's `↩` follows as you go, without the page loading again.
64
+
59
65
  What the agent wrote is drawn as GitHub draws markdown. Under Evidence, the
60
66
  pictures it names are shown, read from where they are on this Mac: two named
61
67
  alike but for `before` and `after` stand side by side as one, and Space looks
@@ -21,7 +21,7 @@ which only renders and keeps nothing.
21
21
  ## PR format
22
22
 
23
23
  The skill in `skills/proctolog-pr-writing/` teaches an agent this format, with a
24
- whole example in its `references/example-description.md`. `proctolog install`
24
+ whole example in its `references/example-description.md`. `proctolog setup`
25
25
  links it into `~/.claude/skills/` and `~/.agents/skills/`. Keep the project's
26
26
  template (Summary, Changes, Verification) and add:
27
27
 
@@ -42,18 +42,23 @@ server goes on in the background until it is stopped. When it has stopped
42
42
  without the desk, the end of its log shows while the pointer is on that line.
43
43
  `t` goes to Try it. In a narrower window it waits at the foot of the page.
44
44
 
45
- On the Route, `j` / `k` select a PR and Enter opens it. Each line shows how many
46
- of its decisions you have reviewed, and whose it is when it is not yours; the
47
- line the cursor is on opens out with the rest: its layer, what is open, its
48
- pictures and words, and when it last changed. `N` opens the next PR with
49
- decisions left. Clicking a title opens that PR. Clicking elsewhere on a row,
45
+ On the Route, `j` / `k` select a PR and Enter opens it. Each line ends with its
46
+ decisions as beads, a bead to each, as deciding draws them: a full-size bead
47
+ for a blocking decision, a small one for a non-blocking, solid once reviewed,
48
+ hollow while it waits. Before them it says **back to you** once the agent's
49
+ answers have come back for you to confirm, or answer, or once it addressed one
50
+ of your notes; otherwise whose it is when it is not yours: with the agent,
51
+ settled or waiting. The line the cursor is on opens out with the rest: its
52
+ layer, what is open (“4 returned, to confirm”), its pictures and words, and
53
+ when it last changed. `N` opens the next PR that asks for you: one with a
54
+ blocking decision left, or something back from the agent. Clicking a title opens that PR. Clicking elsewhere on a row,
50
55
  or tabbing to one of its links, selects it and shows its actions. In a narrow
51
56
  window those actions wrap under the selected row. On
52
57
  a PR's page, `j` / `k` or the up/down arrows move through headings, description
53
58
  blocks, pictures, decisions and returned work, as below. `[` / `]` jump between
54
59
  sections; Enter or Right opens a section, Left closes it. Home / End reach the
55
60
  first / last item. Tab / Shift+Tab reach links, buttons and fields. `J` / `K` open the
56
- next / previous PR, `N` the next one with decisions to review, and `h` returns
61
+ next / previous PR, `N` the next one that asks for you, and `h` returns
57
62
  to the Route. `G`, on the Route or a PR's page, opens the PR on GitHub in a tab
58
63
  of its own.
59
64
 
@@ -93,12 +98,14 @@ part stands apart in the pane. A handover of one part says the rest of the
93
98
  burst is outside it, and names the chains that sit on it, so a change to it is
94
99
  carried up through them.
95
100
 
96
- A pane at the left of every page lists the route, each PR with its count
97
- (`x` all reviewed, `~` some, no mark for none yet, `-` when it has none), and
98
- marks the one you are on. A `*` before a title means GitHub moved on since the
99
- desk read it. Hold the pointer on a row and a card beside the pane gives its
100
- title in full, says each of its marks in words, and links to it on GitHub
101
- (and to “read again” when it moved).
101
+ A pane at the left of every page lists the route, and marks the one you are
102
+ on. Each PR ends with the same beads as its line on the Route, smaller, and
103
+ they follow your choices as you make them. A `↩` before it means the agent's
104
+ answers came back on it for you. A `*` before a title means GitHub moved on
105
+ since the desk read it. Hold the pointer on a row and a card beside the pane
106
+ gives its title in full, says each of its marks in words (“back to you: 4
107
+ returned, to confirm”, “1 of 4 blocking decisions reviewed”), and links to it
108
+ on GitHub (and to “read again” when it moved).
102
109
 
103
110
  The desk has no colour. What you are on and what waits for you is white and
104
111
  bold; what is settled is grey. A bead says the same of anything that is
@@ -120,23 +127,43 @@ beside its box with its words: ⌘ Enter saves it written over, in the same
120
127
  place, and Withdraw takes it back off the picture. Escape puts it away while
121
128
  nothing in it is changed. Escape, or the ✕ at the top
122
129
  right, closes the viewer and nothing else: you are back on the block, or the
123
- decision, you opened it from. Once it closes, the page comes back at the last
124
- note written; while deciding, it stays on the decision, and the note shows the
125
- next time the page loads.
130
+ decision, you opened it from, and the page draws the notes written there
131
+ where it stands. Once the viewer shows only part of the picture, a map in its
132
+ corner shows the whole, with the part on the stage framed; a press on the map
133
+ goes there.
134
+
135
+ A note on a picture of the page itself, a click or a drag on it, is written
136
+ under the picture and saved where it stands too: the page does not load
137
+ again or move, zoomed in or not, and draws the note's box and its card.
126
138
 
127
139
  Two pictures named `NAME-before` and `NAME-after` are one picture of a change:
128
140
  the description shows them side by side under one name, and a decision links
129
- them as `#screenshot-NAME`. In the viewer, `s` shows them side by side, `w`
130
- swipes (the after picture over the before, up to a divider that a drag or the
131
- arrows move), `b` blinks from one to the other at each press, `o` lays the
132
- after picture half seen through over the before, and `d` lights only what
133
- differs. `n` goes from one changed region to the next, each outlined on the
134
- pictures. The caption says “Comparing…” until the regions are found, then how
135
- many there are, “Identical”, “Different sizes: compared from the top left”, or
136
- “Regions need ImageMagick” on a Mac without it. A mark goes on the half it is
137
- made on side by side, and on the one showing in blink; swipe, onion skin and
138
- difference take none. Comparisons are kept in the cache, by the pictures'
139
- pixels.
141
+ them as `#screenshot-NAME`. In the viewer a quiet panel beside them lists the
142
+ five ways to look at them, each a button with its key; the one you are in
143
+ says what it shows. `s` side by side, `w` swipe (the after picture over the
144
+ before, up to a divider that a drag or the arrows move), `b` blink (one, then
145
+ the other), `o` onion skin (the after picture half seen through over the
146
+ before) and `d` difference (only what differs is lit). A view's key again, or
147
+ a press on its button, goes back to side by side. Blink is the exception: `b`
148
+ again shows the other half, when you want it, and Shift B sets it blinking by
149
+ itself, or stops it.
150
+
151
+ Under the views the panel counts the changed regions. They are outlined on
152
+ the pictures in black dashed with white, at the same place on each half; a
153
+ note's box is a heavier solid frame, so the two are told apart at a glance.
154
+ `n` and `p` focus on the next region and the one before, as does a press on
155
+ its number: it is tagged with its number and its half. Focusing keeps your
156
+ zoom: seen whole, the region is only outlined; zoomed in, the stage goes near
157
+ enough to see each region whole. `z` goes near to the one in focus, and `0`
158
+ back to the whole. The panel shows the region focused on as the before has it
159
+ and as the after does; Enter writes a note on it in the after, and a press on
160
+ either writes one there, in any view. Until the regions are found the panel
161
+ says “Comparing…”, then how many there are, “Nothing: the same pixels.”,
162
+ “Sizes differ: laid from the top left.”, or that finding them needs
163
+ ImageMagick. A click or a drag marks the half it is made on side by side, and
164
+ the one showing in blink, which holds still while you write; swipe, onion skin
165
+ and difference take a note only on a region the panel shows. Comparisons are
166
+ kept in the cache, by the pictures' pixels.
140
167
 
141
168
  A run of pictures that no decision or other line of the description links
142
169
  folds where it stood, with its captions, into one “More pictures (N)”, opened
@@ -162,7 +189,7 @@ discarding it, then Escape returns to the section heading. `u` undoes the last
162
189
  choice, wherever you are on the page. Once every blocking decision is
163
190
  reviewed, the page shows where the decisions end: what is reviewed and how many
164
191
  non-blocking still wait; what to do next, where `g` marks the PR good to go,
165
- `N` opens the next PR with decisions to review, `e` hands over and
192
+ `N` opens the next PR that asks for you, `e` hands over and
166
193
  `h` goes back to the Route; and every decision on a line under its grade, with
167
194
  what you chose on it or what holds it. Each grade says what it asks of you: a
168
195
  blocking decision needs your answer before the work goes on, and a non-blocking