proctolog 0.2.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: efc3995bd997f8129593d45bbe4de75341a7b27f43401cb329566ac552baffb5
4
- data.tar.gz: 47456fcf6f060778f085e96a41d015e8d2b14c6d3e450178f8122b7bd6295bf6
3
+ metadata.gz: 7e70671fa7a287f7273aa19d34e110411abdaf6eaa53af119c538e6f9f558b83
4
+ data.tar.gz: 715e723cad15056e1c5e878e21f89895e10a8a970550a6eb84183e9ccfc2c84a
5
5
  SHA512:
6
- metadata.gz: f0d7b8c993fe32e7a08c4212de1a30f287c10e0b1ce33a4d58a85c557f172f55a82196098ec6a8b5d894b2c003733809599e1db279a85a67ade37e680de2dae3
7
- data.tar.gz: 3b38800d1b7bd1ef1aeb3dc364a87510ab3bf55837c8f3827b5ccc509c870aef742b5a78bbefbe3d2a6acb92cf131e7fde0b6d317e39686dbcef19dc2422dcf2
6
+ metadata.gz: ab6e65d6dffbb0cf8cfa2b1dd228879e3aae55f32f5118a77873c5e69fab28e840dfce9240cf44d88efc30e59a7cb4b8e271d800e987126e6c3514d60a718109
7
+ data.tar.gz: 38aa55282ff812f1eb8ff1fe22b6dbff1493f0debfe592c6b57f6e21a0cd6a2b7343ad86d45363202cfa3318070f9116a71039bffec371866a60c2cc8d1680f4
data/CHANGELOG.md CHANGED
@@ -6,6 +6,40 @@ adds, what it changes and what it fixes. Versions are semantic
6
6
  something of you says what under **You do**. Unreleased is what main has that
7
7
  no gem has yet.
8
8
 
9
+ ## 0.3.0
10
+
11
+ ### Added
12
+
13
+ - `proctolog setup` is the one command to remember: it takes you from
14
+ wherever you are to an open desk, a step at a time. It links PRoctolog's
15
+ command, completion and skills, then goes through doctor's checks in the
16
+ order you meet them, each saying why it matters, checked again when you
17
+ press Enter, and stamped done or left for later. Outside a checkout, it
18
+ asks for one. Its sharing step says what Tailscale and ngrok each give, and
19
+ where to get them. Of anyone else's, it installs nothing
20
+ ([docs/manual/setup.md](docs/manual/setup.md)). A bare `proctolog`, and
21
+ `serve` outside a checkout, point to it.
22
+ - `proctolog update` says what's new since the version you had, from this
23
+ changelog: each change by its first sentence, under Added, Changed and
24
+ Fixed, and in full what you must do. It starts with the update after this
25
+ one, as the version you leave is the one that runs it.
26
+
27
+ ### Changed
28
+
29
+ - `proctolog install` is another name for `setup`: it links as before, then
30
+ takes you through the rest. After an update it only links, and says what's
31
+ new.
32
+ - `doctor` says how to install Chrome when it is missing.
33
+
34
+ ### Fixed
35
+
36
+ - `proctolog update` said it pulled a checkout forward, and to restart the
37
+ desk, when origin had nothing newer. It says the checkout is the latest now,
38
+ and changes nothing.
39
+ - On macOS 27 the desk took every port on this Mac for answering: Try it
40
+ showed the app up and never started it, `doctor` said port 4040 was taken,
41
+ and `proctolog share start` shared a desk that was not there.
42
+
9
43
  ## 0.2.0
10
44
 
11
45
  ### Added
data/README.md CHANGED
@@ -12,16 +12,16 @@ it works.
12
12
 
13
13
  ## The short version
14
14
 
15
- Install it with [rv](https://rv.dev), then run it from a checkout of the repo
16
- whose PRs you review:
15
+ With [rv](https://rv.dev), from a checkout of the repo whose PRs you review:
17
16
 
18
17
  ```sh
19
- rvx proctolog install
20
18
  cd path/to/your-repo
21
- proctolog doctor # fix what it marks !!
22
- proctolog serve # then open http://proctolog.localhost:4040
19
+ rvx proctolog setup # a step at a time, to http://proctolog.localhost:4040
23
20
  ```
24
21
 
22
+ `setup` is the one command to remember: it links PRoctolog's own pieces, says
23
+ what to do about the rest, and opens the desk. `install` is another name for it.
24
+
25
25
  Pick a PR, a stack, or a few ticked ones, and press `?` once you're in. When
26
26
  your agent opens PRs, tell it to "write these PRs for PRoctolog", so they come
27
27
  with decisions to review. Everything below is the long version.
@@ -41,11 +41,10 @@ brew install diffle imagemagick ffmpeg
41
41
  With rv, whatever Ruby your shell or your repo runs:
42
42
 
43
43
  ```sh
44
- rvx proctolog install
45
- proctolog doctor
44
+ rvx proctolog setup
46
45
  ```
47
46
 
48
- rv gives PRoctolog a Ruby 4 and gems of its own, and `install` puts a
47
+ rv gives PRoctolog a Ruby 4 and gems of its own, and `setup` puts a
49
48
  `proctolog` in `~/.local/bin` that runs it through `rvx`. The apps you review
50
49
  keep their own Ruby: what the desk starts for them never sees PRoctolog's.
51
50
  rv installs from gem.coop, which mirrors RubyGems.
@@ -54,20 +53,24 @@ With Ruby 4.0 already in your shell:
54
53
 
55
54
  ```sh
56
55
  gem install proctolog
57
- proctolog install
58
- proctolog doctor
56
+ proctolog setup
59
57
  ```
60
58
 
61
- Either way `install` also links the zsh completion into `~/.zsh/completions`
62
- and the agent skills (below) into `~/.claude/skills` and `~/.agents/skills`. `doctor`
63
- says what's missing, a line each, with the fix: `!!` for what the desk can't do
59
+ Either way `setup` first links the zsh completion into `~/.zsh/completions`
60
+ and the agent skills (below) into `~/.claude/skills` and `~/.agents/skills`.
61
+ Then it takes the rest a step at a time: gh, the checkout, a first burst, what
62
+ turns features on, sharing the desk with your phone. Each step says why it
63
+ matters, checks again once you've acted, and is stamped done or left for
64
+ later; the last one opens the desk ([setting up](docs/manual/setup.md)). Run it
65
+ again whenever: what's done is stamped and passed. `doctor` asks the same
66
+ things all at once, a line each with the fix: `!!` for what the desk can't do
64
67
  without, `--` for what only turns a feature off.
65
68
 
66
69
  When a newer PRoctolog is out, the desk says so, and `proctolog update` takes
67
70
  it ([versions and updates](docs/manual/updates.md)).
68
71
 
69
72
  Working on PRoctolog itself? Run it from a checkout instead: clone it, then
70
- `bundle install && ruby bin/install`, which also puts a `proctolog` that runs
73
+ `bundle install && bin/proctolog setup`, which also puts a `proctolog` that runs
71
74
  the checkout in `~/.local/bin`. Keep the checkout where it is, since
72
75
  everything links back to it. The desk tells a checkout of a newer release
73
76
  too, and `proctolog update` pulls it forward.
@@ -104,7 +107,7 @@ Your review is saved as you go, on your Mac under `~/.local/share/proctolog/`.
104
107
 
105
108
  PRoctolog finds decisions under a `## Decisions for the owner` heading, one
106
109
  checkbox line each. The `proctolog-pr-writing` skill teaches your agent the
107
- format, and `proctolog install` already put it where the agent looks. Tell it to
110
+ format, and `proctolog setup` already put it where the agent looks. Tell it to
108
111
  "write these PRs for PRoctolog", or "rewrite this PR's description for
109
112
  PRoctolog" for one that's open. Check a description before publishing it:
110
113
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Proctolog
4
4
  class ApplicationController < ActionController::Base
5
- rescue_from(Github::Unreachable) { render plain: "GitHub did not answer.\n\n#{it.message}", status: :service_unavailable }
5
+ rescue_from(Github::Unreachable) { render plain: "GitHub did not answer.\n\n#{it.message}\n\nIf gh is not set up on this Mac yet, proctolog setup takes you through it.", status: :service_unavailable }
6
6
  rescue_from(ActionController::InvalidAuthenticityToken) do
7
7
  redirect_back_or_to root_path, alert: "The desk did not know this page any more, so nothing was saved. Do it once more."
8
8
  end
@@ -11,6 +11,7 @@ module Proctolog
11
11
  MARKS = {fine: "ok", missing: "!!", off: "--"}.freeze
12
12
 
13
13
  Finding = Data.define(:state, :line) do
14
+ def fine? = state == :fine
14
15
  def missing? = state == :missing
15
16
  def to_s = "#{MARKS.fetch(state)} #{line}"
16
17
  end
@@ -23,7 +24,16 @@ module Proctolog
23
24
  @project, @home, @path = project, Pathname(home), path.split(File::PATH_SEPARATOR)
24
25
  end
25
26
 
26
- def findings = @findings ||= (NEEDS + NICETIES).filter_map { send(it) }
27
+ def findings = (NEEDS + NICETIES).filter_map { self[it] }
28
+
29
+ # One check's finding, by its name, asked once: setup asks a step's
30
+ # checks alone. Nil for one not asked here, as the repository is not
31
+ # without gh.
32
+ def [](check)
33
+ raise ArgumentError, "Doctor checks no #{check}" unless (NEEDS + NICETIES).include?(check)
34
+
35
+ (@checked ||= {}).fetch(check) { @checked[check] = send(check) }
36
+ end
27
37
 
28
38
  def fine? = findings.none?(&:missing?)
29
39
 
@@ -72,7 +82,7 @@ module Proctolog
72
82
  def skills
73
83
  folders = %w[.claude/skills .agents/skills].select { |folder| SKILLS.keys.all? { @home.join(folder, it).exist? } }
74
84
  missing = SKILLS.reject { |skill, _| %w[.claude/skills .agents/skills].any? { @home.join(it, skill).exist? } }.values
75
- if missing.any? then off("The #{missing.to_sentence} #{"skill".pluralize(missing.size)} #{missing.one? ? "is" : "are"} not linked: proctolog install")
85
+ if missing.any? then off("The #{missing.to_sentence} #{"skill".pluralize(missing.size)} #{missing.one? ? "is" : "are"} not linked: proctolog setup")
76
86
  else fine("The #{SKILLS.values.to_sentence} skills, in ~/#{folders.join(" and ~/")}")
77
87
  end
78
88
  end
@@ -82,7 +92,7 @@ module Proctolog
82
92
 
83
93
  def chrome
84
94
  require "ferrum"
85
- Ferrum::Browser::Options::Chrome.options.detect_path ? fine("Chrome, for browser tests and scenarios") : off("No Chrome: browser tests skip and scenarios cannot open")
95
+ Ferrum::Browser::Options::Chrome.options.detect_path ? fine("Chrome, for browser tests and scenarios") : off("No Chrome, so browser tests skip and scenarios cannot open: brew install --cask google-chrome")
86
96
  end
87
97
 
88
98
  def diffle = command?("diffle") ? fine("Diffle, for d on a pull request") : off("No Diffle, so d opens no code: brew install diffle")
@@ -112,7 +122,7 @@ module Proctolog
112
122
  off("Nothing shares the desk beyond this Mac. #{unready.map { "#{it.unready}." }.join(" ")}")
113
123
  end
114
124
 
115
- def port = answers?(4040) ? off("Port 4040 answers already: a desk may be open there, or serve --port 4041") : fine("Port 4040 is free")
125
+ def port = Localhost.answers?(4040, within: 1) ? off("Port 4040 answers already: a desk may be open there, or serve --port 4041") : fine("Port 4040 is free")
116
126
 
117
127
  def fine(line) = Finding.new(:fine, line)
118
128
  def missing(line) = Finding.new(:missing, line)
@@ -124,12 +134,5 @@ module Proctolog
124
134
  def runnable?(program) = program.include?("/") ? @project.root.join(program).executable? : command?(program)
125
135
 
126
136
  def succeeds?(*command) = Open3.capture2e({"PATH" => @path.join(File::PATH_SEPARATOR)}, *command).last.success?
127
-
128
- def answers?(port)
129
- Socket.tcp("localhost", port, connect_timeout: 1, &:close)
130
- true
131
- rescue SystemCallError
132
- false
133
- end
134
137
  end
135
138
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module Proctolog
6
+ # This Mac, as a program on it is reached: by a port something answers on.
7
+ module Localhost
8
+ # TCPSocket, not Socket.tcp: on macOS 27, Socket.tcp with a connect
9
+ # timeout takes a port nothing listens on for one that answers.
10
+ def self.answers?(port, within: 0.5)
11
+ TCPSocket.new("localhost", port, connect_timeout: within).close
12
+ true
13
+ rescue SystemCallError, IO::TimeoutError
14
+ false
15
+ end
16
+ end
17
+ end
@@ -35,7 +35,7 @@ module Proctolog
35
35
  end
36
36
 
37
37
  # Every port the app listens on answers.
38
- def up? = app.listens(port).all? { listening?(it) }
38
+ def up? = app.listens(port).all? { Localhost.answers?(it) }
39
39
 
40
40
  def state
41
41
  return :up if up?
@@ -179,12 +179,6 @@ module Proctolog
179
179
 
180
180
  def logs = Project.current.cache.join("servers")
181
181
 
182
- def listening?(port)
183
- Socket.tcp("localhost", port, connect_timeout: 0.5) { true }
184
- rescue SystemCallError
185
- false
186
- end
187
-
188
182
  def git(*) = Open3.capture2e("git", "-C", checkout.to_s, *)
189
183
 
190
184
  def tip(name)
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proctolog
4
+ # From nothing to an open desk, a step at a time: PRoctolog's own links,
5
+ # then doctor's checks in the order a newcomer meets them, each step saying
6
+ # why it matters. It explains, links to what to install and checks again
7
+ # once the owner says a step is done; of anyone else's, it installs
8
+ # nothing. `proctolog setup` walks it, and `install` is another name for it.
9
+ #
10
+ # A step is done when every one of its checks is fine. A nicety that is
11
+ # off never holds it up: the owner may leave it for later. A need that is
12
+ # missing does, unless the step says what does its work until then, as the
13
+ # desk's first page starts a burst.
14
+ class Setup
15
+ # first names what the setup does itself before checking the step; asks,
16
+ # what the owner may type in place of Enter: the step's folder.
17
+ Step = Data.define(:title, :why, :checks, :advice, :until_then, :first, :asks) do
18
+ def initialize(title:, why:, checks:, advice: [], until_then: nil, first: nil, asks: nil) = super
19
+
20
+ def findings(doctor) = checks.filter_map { doctor[it] }
21
+
22
+ # Whether the owner may go on without it: nothing it needs is missing,
23
+ # or something does that work meanwhile.
24
+ def leavable?(findings) = until_then.present? || findings.none?(&:missing?)
25
+
26
+ def question(findings) = [asks, "Enter checks again", ("l leaves it for later" if leavable?(findings)), "q stops"].compact.join(", ")
27
+ end
28
+
29
+ class << self
30
+ def steps = @steps ||= []
31
+
32
+ private
33
+
34
+ def step(title, **) = steps << Step.new(title:, **)
35
+ end
36
+
37
+ step "PRoctolog on this Mac", checks: %i[ruby launcher skills release], first: :link,
38
+ why: "From wherever this PRoctolog lives, setup links the proctolog command, zsh's completion, and the skills your agents write pull requests and carry out your review with.",
39
+ until_then: "This setup goes on as it is; a new terminal finds proctolog once your PATH has it."
40
+
41
+ step "GitHub, through gh", checks: %i[gh],
42
+ why: "The desk reads pull requests, their stacks and their checks with your own gh, and writes to GitHub only when you say so."
43
+
44
+ step "The checkout you review", checks: %i[checkout repository], asks: "type its folder",
45
+ why: "The desk opens the pull requests of one checkout, from its GitHub origin, which your gh must be able to read."
46
+
47
+ step "A first burst", checks: %i[bursts],
48
+ why: "A burst is a body of pull requests you review together, a part at a time; the desk keeps your marks by it.",
49
+ until_then: "Review on the desk's first page starts one."
50
+
51
+ step "What turns features on", checks: %i[chrome diffle imagemagick ffmpeg app],
52
+ why: "Each of these turns one feature on. The desk opens without any of them.",
53
+ until_then: "Each feature waits for its own."
54
+
55
+ step "Your phone, through a share", checks: %i[share],
56
+ why: "Shared, the desk is yours from your phone or a computer elsewhere, while this Mac keeps it and an agent works on.",
57
+ advice: [
58
+ "Tailscale keeps the desk off the internet: only the devices signed in to your tailnet reach it, your phone among them once it has the Tailscale app. Install it from https://tailscale.com/download/mac.",
59
+ "ngrok puts the desk on the internet, behind its sign-in with Google, for your email alone; its free plan carries 1 GB a month. Install it from https://ngrok.com/download, then copy your authtoken from https://dashboard.ngrok.com/get-started/your-authtoken and run ngrok config add-authtoken TOKEN.",
60
+ "Either one is enough. Once the desk is open, proctolog share shares it."
61
+ ]
62
+
63
+ INDENT = " " * 8
64
+ WIDTH = 78
65
+
66
+ # The folder setup takes the checkout from: where it started, or the one
67
+ # the owner typed at its step.
68
+ attr_reader :directory
69
+
70
+ # installation links PRoctolog's own files. doctor gives a fresh Doctor
71
+ # for a folder each time it is called, so that a step checked again sees
72
+ # what the owner just did.
73
+ def initialize(installation:, directory: Dir.pwd, input: $stdin, output: $stdout, &doctor)
74
+ @installation, @directory, @input, @output, @doctor = installation, Pathname(directory), input, output, doctor
75
+ end
76
+
77
+ def steps = self.class.steps
78
+
79
+ # Asks at each step that is not done yet, and checks it again on Enter.
80
+ # :serve when the owner asked for the desk to open now, :finished when
81
+ # it is left for later or one answers already, :stopped when they
82
+ # stopped on the way.
83
+ def walk
84
+ say "PRoctolog, a step at a time: each says why it matters, and is stamped DONE", "once it is met. Of anyone else's, nothing is installed for you: a step says", "what to do, and Enter checks it again once you have."
85
+ steps.each.with_index(1) { |step, number| taken(step, number) or return :stopped }
86
+ desk
87
+ end
88
+
89
+ # Every step as it stands, asking nothing: for an agent, or a pipe.
90
+ # True when the desk can open: no step waits on a need.
91
+ def show
92
+ shown = steps.each.with_index(1).map { |step, number| shown(step, number) }
93
+ the_desk
94
+ lines [doctor[:port]]
95
+ say(*wrapped("Run proctolog setup in a terminal to take it a step at a time, or proctolog serve to open the desk."))
96
+ shown.all?
97
+ end
98
+
99
+ private
100
+
101
+ def doctor = @doctor.call(directory)
102
+
103
+ def taken(step, number)
104
+ begun(step, number)
105
+ advised = false
106
+ loop do
107
+ findings = step.findings(doctor)
108
+ lines findings
109
+ return stamp("DONE") if findings.all?(&:fine?)
110
+
111
+ advise(step) unless advised
112
+ advised = true
113
+ answer = ask(step.question(findings))
114
+ case answer&.downcase
115
+ in "q" | nil then return stopped(number)
116
+ in "l" then (return stamp("LATER", step.until_then) if step.leavable?(findings))
117
+ in "" then next
118
+ else @directory = Pathname(File.expand_path(answer, directory)) if step.asks
119
+ end
120
+ end
121
+ end
122
+
123
+ def shown(step, number)
124
+ begun(step, number)
125
+ findings = step.findings(doctor)
126
+ lines findings
127
+ return stamp("DONE") if findings.all?(&:fine?)
128
+
129
+ advise(step)
130
+ stamp("TO DO", step.until_then)
131
+ step.leavable?(findings)
132
+ end
133
+
134
+ def begun(step, number)
135
+ heading(step.title, number, why: step.why)
136
+ send(step.first) if step.first
137
+ end
138
+
139
+ # PRoctolog's own links, made fresh before its first step is checked.
140
+ # One in the way that is someone else's stays, and is said.
141
+ def link
142
+ @installation.install.each { say "#{INDENT}#{it.gsub(Dir.home, "~")}" }
143
+ rescue Installation::Refused => e
144
+ say(*wrapped("!! #{e.message}"))
145
+ end
146
+
147
+ # The last step: the desk itself, opened by the owner's Enter.
148
+ def desk
149
+ the_desk
150
+ port = doctor[:port]
151
+ lines [port]
152
+ return :finished unless port.fine?
153
+
154
+ (ask("Enter opens the desk, q leaves it for proctolog serve") == "") ? :serve : :finished
155
+ end
156
+
157
+ def the_desk = heading("The desk", steps.size + 1, why: "It opens on this Mac at #{Desk.address(4040)}; after the first visit, typing procto in the address bar finds it.")
158
+
159
+ def stopped(number)
160
+ say "", "Stopped at step #{number}. proctolog setup starts again from the first, and stamps each step that is done."
161
+ false
162
+ end
163
+
164
+ def heading(title, number, why: nil)
165
+ say "", format("%-8s%s", "#{number} of #{steps.size + 1}", title)
166
+ say(*wrapped(why)) if why
167
+ end
168
+
169
+ def lines(findings) = findings.each { say "#{INDENT}#{it}" }
170
+
171
+ def advise(step) = step.advice.each { say(*wrapped(it)) }
172
+
173
+ def stamp(word, after = nil)
174
+ say "#{INDENT}[ #{word} ]#{" #{after}" if after}"
175
+ true
176
+ end
177
+
178
+ def ask(question)
179
+ @output.print "#{INDENT}#{question.upcase_first} > "
180
+ @output.flush
181
+ @input.gets&.strip
182
+ end
183
+
184
+ # A long line broken at spaces under the step's title; a word too long
185
+ # for the line, an address, stays whole.
186
+ def wrapped(text) = text.scan(/\S.{0,#{WIDTH - INDENT.size - 1}}(?=\s|\z)|\S+/).map { INDENT + it }
187
+
188
+ def say(*lines) = @output.puts(*lines)
189
+ end
190
+ end
data/bin/proctolog CHANGED
@@ -157,7 +157,7 @@ module Proctolog
157
157
  option :allow, type: :string, repeatable: true, desc: "An email that may sign in through ngrok, in place of your Git email"
158
158
  def start
159
159
  boot_desk!
160
- abort "No desk answers on :#{port}. Start one with the share: proctolog serve --share#{port_option}" unless answers?
160
+ abort "No desk answers on :#{port}. Start one with the share: proctolog serve --share#{port_option}" unless Localhost.answers?(port, within: 1)
161
161
  puts Share.open(port, via: options[:via], allow: options[:allow])
162
162
  puts "Stop it with: proctolog share stop#{port_option}"
163
163
  rescue Share::Refused => e
@@ -188,13 +188,6 @@ module Proctolog
188
188
  def port = options[:port]
189
189
 
190
190
  def port_option = (" --port #{port}" unless port == 4040)
191
-
192
- def answers?
193
- Socket.tcp("localhost", port, connect_timeout: 1, &:close)
194
- true
195
- rescue SystemCallError
196
- false
197
- end
198
191
  end
199
192
  end
200
193
 
@@ -205,6 +198,14 @@ module Proctolog
205
198
 
206
199
  class_option :project, type: :string, aliases: "-C", desc: "The Git checkout whose bursts to open"
207
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
+
208
209
  desc "burst SUBCOMMAND", "Start a burst of your own, or list the bursts there are"
209
210
  subcommand "burst", BurstCommands
210
211
 
@@ -233,7 +234,7 @@ module Proctolog
233
234
  abort "Port #{options[:port]} is taken. If that is the desk, it is open at #{Desk.address(options[:port])}; " \
234
235
  "for a second one: proctolog serve --port #{options[:port] + 1}"
235
236
  rescue Project::Missing => e
236
- abort e.message
237
+ abort "#{e.message} New here? proctolog setup takes you to the desk, a step at a time."
237
238
  end
238
239
 
239
240
  desc "route [BURST]", "Print the order to take a burst in"
@@ -318,25 +319,20 @@ module Proctolog
318
319
  map %w[-v --version] => :version
319
320
  def version = puts("PRoctolog #{VERSION}")
320
321
 
321
- 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"
322
323
  long_desc <<~TEXT
323
- Links zsh's completion into ~/.zsh/completions and the agent skills into ~/.claude/skills and
324
- ~/.agents/skills, from wherever this PRoctolog lives: the gem, or the checkout, which also gets its
325
- launcher in ~/.local/bin. Leaves anything of the same name that is not PRoctolog's as it is.
326
- `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.
327
327
  TEXT
328
- def install
329
- puts Installation.here.install
330
- rescue Installation::Refused => e
331
- abort e.message
332
- end
328
+ def install = setup
333
329
 
334
330
  desc "update", "Install the latest PRoctolog, and link to it"
335
331
  long_desc <<~TEXT
336
332
  Asks RubyGems for the latest PRoctolog and, when it is newer than this one, installs it the way this one
337
333
  was: with `rv tool install` for one rvx runs, with `gem install` otherwise. A checkout is pulled
338
334
  forward instead, when it is clean and on origin's own branch, and its bundle installed. Then runs the
339
- 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.
340
336
  TEXT
341
337
  def update
342
338
  installation = Installation.here
@@ -367,7 +363,7 @@ module Proctolog
367
363
  system(environment, "rv", "tool", "install", "proctolog@#{latest}", "--gem-server", "#{Release.server}/", exception: true)
368
364
  puts taken(latest)
369
365
  # rvx runs the newest it holds, which is now this one; rv 0.7 runs no proctolog@VERSION.
370
- exec(environment, "rvx", "--no-install", "proctolog", "install")
366
+ exec(environment.merge(Installation.updating_from(VERSION)), "rvx", "--no-install", "proctolog", "setup")
371
367
  end
372
368
  end
373
369
 
@@ -376,17 +372,20 @@ module Proctolog
376
372
  system(RbConfig.ruby, File.join(RbConfig::CONFIG["bindir"], "gem"), "install", "proctolog", "--version", latest.version, "--clear-sources", "--source", Release.server, exception: true)
377
373
  puts taken(latest)
378
374
  # RubyGems' launcher takes the version to run as its first word.
379
- 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")
380
376
  end
381
377
 
382
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.
383
380
  def take_with_git(installation)
384
- 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
+
385
384
  Bundler.with_unbundled_env do
386
385
  system(RbConfig.ruby, Gem.bin_path("bundler", "bundle"), "install", "--quiet", chdir: installation.root.to_s, exception: true)
387
386
  end
388
387
  puts "Pulled #{installation.root} forward. Restart the desk to run it."
389
- 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)
390
389
  end
391
390
 
392
391
  def taken(latest) = "Installed PRoctolog #{latest}. Restart the desk to run it: one open now still runs #{Release.running}."
@@ -402,13 +401,55 @@ module Proctolog
402
401
  Exits 0 when every need is met, 1 when one is not.
403
402
  TEXT
404
403
  def doctor
405
- project = begin
406
- 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)
407
450
  rescue Project::Missing
408
- nil
451
+ Project.current = nil
409
452
  end
410
- Desk.initialize! unless Desk.initialized?
411
- Doctor.new(project).tap { puts it }.then { exit(it.fine? ? 0 : 1) }
412
453
  end
413
454
  end
414
455
  end
@@ -58,8 +58,9 @@ _proctolog() {
58
58
  'check:Check a description as the desk reads it, of a pull request or a file'
59
59
  'themes:Print the themes a decision may name, with what each means'
60
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'
61
62
  'version:Print which PRoctolog this is'
62
- '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'
63
64
  'update:Install the latest PRoctolog, and link to it'
64
65
  'tree:Print a tree of all available commands'
65
66
  'help:Describe the commands, or one of them'
@@ -157,7 +158,7 @@ _proctolog() {
157
158
  unopened=(${(f)"$(_proctolog_unopened "$root")"})
158
159
  _arguments $project '(-f --file)'{-f,--file}'[A local Markdown file to check before it is published]:file:_files' \
159
160
  "1:pull request:{_describe -t pull-requests 'open pull request' pull_requests; _describe -t unopened 'not opened yet' unopened}" ;;
160
- themes|doctor|version|install|update) _arguments $project ;;
161
+ themes|doctor|setup|version|install|update) _arguments $project ;;
161
162
  help) _describe -t commands 'command' commands ;;
162
163
  esac
163
164
  ;;
@@ -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
 
@@ -0,0 +1,59 @@
1
+ # Setting up, a step at a time
2
+
3
+ `proctolog setup` is the one command to remember. Run it the first time, with
4
+ `rvx proctolog setup` or after `gem install proctolog`, and again whenever
5
+ you're unsure something is in place. It takes you from wherever you are to an
6
+ open desk, one step at a time. `proctolog install` is another name for it, so
7
+ whichever you remember works.
8
+
9
+ 1. **PRoctolog on this Mac**: setup links the `proctolog` command, zsh's
10
+ completion and the agent skills to this PRoctolog, then checks Ruby, the
11
+ command on your PATH, and whether a newer PRoctolog is out.
12
+ 2. **GitHub, through gh**: installed and signed in.
13
+ 3. **The checkout you review**: a Git checkout whose origin is on GitHub,
14
+ and which your `gh` can read. Outside one, type its folder here.
15
+ 4. **A first burst**: the pull requests you review together.
16
+ 5. **What turns features on**: Chrome, Diffle, ImageMagick, ffmpeg and the
17
+ checkout's `.proctolog/app.yml`.
18
+ 6. **Your phone, through a share**: Tailscale or ngrok, what each gives, and
19
+ where to get them ([sharing](sharing.md)).
20
+ 7. **The desk**: Enter opens it with `proctolog serve`.
21
+
22
+ Each step says why it matters and shows doctor's lines for it, each with the
23
+ fix. Do what it says in another terminal, then press Enter there: the step
24
+ checks itself again. A step that is met is stamped `[ DONE ]` and the next
25
+ one begins, so a second run passes over what you already have, stamping each
26
+ step as it goes. The links in the first step are PRoctolog's own, made again
27
+ each time; anything of the same name that is someone else's stays, and is
28
+ said. Nothing else is installed for you: a step explains, links and checks.
29
+
30
+ Some steps can wait. Type `l` and Enter to leave one for later, and it is
31
+ stamped `[ LATER ]` with what does its work meanwhile:
32
+
33
+ - A step whose lines are only `--` is a nicety: the desk opens without it.
34
+ - The first step, when the command is linked but your PATH doesn't have it
35
+ yet: setup goes on as it is, and a new terminal finds `proctolog` once
36
+ `~/.zshrc` puts it there.
37
+ - A first burst: **Review** on the desk's first page starts one, so the step
38
+ needn't hold you up.
39
+ - The features step: each feature waits for its own program, and the rest of
40
+ the desk works.
41
+
42
+ gh and the checkout can't wait, as the desk opens nothing without them;
43
+ there, `l` only checks again. `q` stops anywhere, and `proctolog setup`
44
+ starts again from the first step.
45
+
46
+ If a desk already answers on port 4040, the last step says so instead of
47
+ opening a second one.
48
+
49
+ ## After an update
50
+
51
+ `proctolog update` runs the new version's `setup`, which then only links it
52
+ and says what's new: you asked for an update, not to go through setup again.
53
+
54
+ ## For an agent
55
+
56
+ Outside a terminal, in a pipe or an agent's shell, `setup` asks nothing. It
57
+ links, prints every step as it stands, stamping the unfinished ones
58
+ `[ TO DO ]`, and exits 0 when the desk can open, 1 when a step still waits on
59
+ something the desk needs.
@@ -34,7 +34,7 @@ in. It goes through whichever of these this Mac has:
34
34
  and run `ngrok config add-authtoken TOKEN`. On the free plan, ngrok shows a
35
35
  warning page of its own before the desk, and carries 1 GB a month.
36
36
 
37
- Neither is installed for you. `proctolog doctor` and `proctolog share status`
37
+ Neither is installed for you. `proctolog doctor`, `proctolog setup` and `proctolog share status`
38
38
  say what each one still needs.
39
39
 
40
40
  ### Turning it on
@@ -39,10 +39,17 @@ proctolog update
39
39
 
40
40
  It asks RubyGems, installs the newer version the way yours was installed
41
41
  (`rv tool install` for a PRoctolog that `rvx` runs, `gem install` otherwise),
42
- and runs that version's `proctolog install`, so zsh's completion and the agent
43
- skills link to it. A checkout of PRoctolog is pulled forward instead, and its
44
- bundle installed. A desk that's open keeps running the old one: stop it and run
45
- `proctolog serve` again.
42
+ and runs that version's `proctolog setup`, which after an update only links
43
+ zsh's completion and the agent skills to it, and walks no steps. A checkout of PRoctolog is pulled forward instead, and its
44
+ bundle installed. When there's nothing newer, it says so and changes nothing.
45
+ A desk that's open keeps running the old one: stop it and run `proctolog
46
+ serve` again.
47
+
48
+ Then it says what's new since the version you had, from the changelog: each
49
+ change by its first sentence, under Added, Changed and Fixed, and in full
50
+ what you must do. Past a dozen changes it counts the rest, and it ends with
51
+ where the whole changelog is. An update from 0.2.0 or earlier says none of
52
+ this, as the version you leave is the one that runs the update.
46
53
 
47
54
  The old version stays installed. `gem cleanup proctolog` removes the ones
48
55
  you no longer run; with rv, `rv tool list` shows them and
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "io/console"
4
+
5
+ module Proctolog
6
+ class Changelog
7
+ # What an update brings, as the console says it once the new version is
8
+ # in: each change by the first sentence of its line, under its heading,
9
+ # and what to do in full. Plain text, as a terminal reads no markdown.
10
+ class News
11
+ # Past this many changes, the rest are counted rather than shown.
12
+ SHOWN = 12
13
+
14
+ # As wide as the terminal it is said in, within reason.
15
+ def self.columns = (IO.console&.winsize&.last || 80).clamp(40, 100)
16
+
17
+ def initialize(since:, sections:, path:)
18
+ @since, @sections, @path = since, sections, path
19
+ end
20
+
21
+ def to_s(width: self.class.columns)
22
+ shown = changes.first(SHOWN)
23
+ more = changes.size - shown.size
24
+ told = @sections.flat_map { told(it, shown:, width:) }
25
+ ["What's new since PRoctolog #{@since}:", *told, "", more.positive? ? "#{more} more, and all of it: #{@path}" : "All of it: #{@path}"].join("\n")
26
+ end
27
+
28
+ private
29
+
30
+ # Every change, in the order written: its section, its heading and its line.
31
+ def changes = @sections.flat_map { |section| section.changes.flat_map { |heading, lines| lines.map { [section, heading, it] } } }
32
+
33
+ # A section's changes that are shown, and what it asks, which always is.
34
+ def told(section, shown:, width:)
35
+ headed = section.changes.flat_map do |heading, lines|
36
+ kept = lines.select { shown.include?([section, heading, it]) }
37
+ kept.any? ? [" #{heading}", *kept.map { " - #{headline(it, room: width - 6)}" }] : []
38
+ end
39
+ asked = section.you_do&.then { laid("You do#{plain(it)}", width:) } || []
40
+ (headed.any? || asked.any?) ? ["", section.name, *headed, *asked] : []
41
+ end
42
+
43
+ # The first sentence of a line. One longer than the room ends at its last
44
+ # clause that fits, when that keeps half the room, or else at a word.
45
+ def headline(line, room:)
46
+ sentence = plain(line).then { it[/\A.+?[.!?](?=\s|\z)/] || it }
47
+ return sentence if sentence.size <= room
48
+
49
+ cut = sentence[0, room]
50
+ clause = cut[/\A.*(?=[,;:]\s)/]
51
+ "#{(clause && clause.size >= room / 2) ? clause : cut[/\A.*(?=\s)/]}…"
52
+ end
53
+
54
+ # In lines no wider than the console, the first two spaces in, the rest four.
55
+ def laid(text, width:)
56
+ text.split.each_with_object([+" "]) do |word, lines|
57
+ lines << +" " if lines.last.size + word.size >= width && !lines.last.strip.empty?
58
+ lines.last << (lines.last.strip.empty? ? word : " #{word}")
59
+ end
60
+ end
61
+
62
+ # Without markdown's marks: **bold**, <links> and [text](links) keep
63
+ # their words. Backticks stay, as the console writes commands in them.
64
+ def plain(text) = text.gsub(/\*\*(.+?)\*\*/, '\1').gsub(/<(\S+?)>/, '\1').gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
65
+ end
66
+ end
67
+ end
@@ -1,23 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "changelog/news"
4
+
3
5
  module Proctolog
4
- # CHANGELOG.md as a release reads it: a section to each version, newest
5
- # first, under ## Unreleased what main has that no gem has yet. Plain Ruby,
6
- # as bin/release and the Release workflow read it before any bundle.
6
+ # CHANGELOG.md as a release and an update read it: a section to each
7
+ # version, newest first, under ## Unreleased what main has that no gem has
8
+ # yet. Plain Ruby, as bin/release, the Release workflow and bin/install
9
+ # read it before any bundle.
7
10
  class Changelog
8
11
  PATH = File.expand_path("../../CHANGELOG.md", __dir__)
9
12
 
10
- # One ## section, its lines under ### Added, Changed and Fixed.
13
+ # One ## section, its lines under ### Added, Changed and Fixed, and what
14
+ # to do after **You do**.
11
15
  Section = Data.define(:name, :text) do
12
16
  def adds? = text.match?(/^### Added$/)
13
17
  def lines? = text.match?(/^- /)
14
18
  def notes = text.strip
19
+
20
+ def unreleased? = name == "Unreleased"
21
+ def version = (Gem::Version.new(name) if Gem::Version.correct?(name))
22
+
23
+ # Its lines by heading, in the order written, each on one line.
24
+ def changes
25
+ text.scan(/^### ([^\n]+)\n(.*?)(?=^### |^\*\*You do\*\*|\z)/m).to_h do |heading, lines|
26
+ [heading, lines.split(/^- /).drop(1).map { it.split.join(" ") }]
27
+ end
28
+ end
29
+
30
+ # What it asks, on one line, as it goes on after **You do**: ": nothing",
31
+ # or ", coming from a checkout: …".
32
+ def you_do = text[/^\*\*You do\*\*(.*)/m, 1]&.split&.join(" ")
15
33
  end
16
34
 
17
35
  # It writes ⌘ and ↩, so it is read as UTF-8 whatever the locale.
18
- def self.read(path = PATH) = new(File.read(path, encoding: "UTF-8"))
36
+ def self.read(path = PATH) = new(File.read(path, encoding: "UTF-8"), path:)
19
37
 
20
- def initialize(text)
38
+ def initialize(text, path: PATH)
39
+ @path = path
21
40
  @sections = text.split(/^## /).drop(1).map do
22
41
  name, body = it.split("\n", 2)
23
42
  Section.new(name: name.strip, text: body.to_s)
@@ -28,6 +47,15 @@ module Proctolog
28
47
 
29
48
  def unreleased = self["Unreleased"]
30
49
 
50
+ # What an update from this version brings: every section newer than it,
51
+ # Unreleased too in a checkout. Nil when it brings nothing written here.
52
+ def since(version)
53
+ return unless Gem::Version.correct?(version)
54
+
55
+ newer = @sections.select { it.unreleased? || it.version&.>(Gem::Version.new(version)) }
56
+ News.new(since: version, sections: newer, path: @path) if newer.any?
57
+ end
58
+
31
59
  # Adding something makes a minor (docs/manual/updates.md): the minor this
32
60
  # version owes when it adds something but is only a patch on the one
33
61
  # before it.
@@ -42,9 +70,6 @@ module Proctolog
42
70
  # too.
43
71
  def adds?(version) = [self[version], unreleased].compact.any?(&:adds?)
44
72
 
45
- def released_before(version)
46
- older = @sections.drop_while { it.name != version }.drop(1)
47
- older.map(&:name).find { Gem::Version.correct?(it) }&.then { Gem::Version.new(it) }
48
- end
73
+ def released_before(version) = @sections.drop_while { it.name != version }.drop(1).find(&:version)&.version
49
74
  end
50
75
  end
@@ -4,6 +4,7 @@ require "fileutils"
4
4
  require "open3"
5
5
  require "rbconfig"
6
6
  require "shellwords"
7
+ require_relative "changelog"
7
8
 
8
9
  module Proctolog
9
10
  # Where this PRoctolog lives, and what it puts on this Mac for zsh and the
@@ -22,8 +23,20 @@ module Proctolog
22
23
 
23
24
  SKILL_FOLDERS = %w[.claude/skills .agents/skills].freeze
24
25
 
26
+ # What a pull did: the commit the checkout was at, and the one it is at.
27
+ Pull = Data.define(:from, :to) do
28
+ def moved? = from != to
29
+ end
30
+
31
+ # `proctolog update` hands the new version's install the version it
32
+ # leaves, in the environment, for that install to say what is new since.
33
+ UPDATED_FROM = "PROCTOLOG_UPDATED_FROM"
34
+
25
35
  def self.here = new(File.expand_path("../..", __dir__))
26
36
 
37
+ # The environment update starts the new version's install in.
38
+ def self.updating_from(version) = {UPDATED_FROM => version.to_s}
39
+
27
40
  attr_reader :root
28
41
 
29
42
  def initialize(root, home: Dir.home, env: ENV)
@@ -47,9 +60,15 @@ module Proctolog
47
60
  branch, main = git("branch", "--show-current"), origin_branch
48
61
  raise Refused, "#{root} is on #{branch}, not #{main}: switch to #{main}, or pull #{branch} yourself." unless branch == main
49
62
 
63
+ from = git("rev-parse", "HEAD")
50
64
  git("pull", "--ff-only")
65
+ Pull.new(from:, to: git("rev-parse", "HEAD"))
51
66
  end
52
67
 
68
+ # What is new in this PRoctolog since the one an update left, or nil when
69
+ # no update brought it here.
70
+ def news = @env[UPDATED_FROM]&.then { Changelog.read(root.join("CHANGELOG.md")).since(it) }
71
+
53
72
  def launcher = @home.join(".local/bin/proctolog")
54
73
 
55
74
  def completion = @home.join(".zsh/completions/_proctolog")
@@ -3,5 +3,5 @@
3
3
  module Proctolog
4
4
  # Semantic: a patch fixes, a minor adds, a major asks something new of the
5
5
  # owner (a command gone, a file moved). docs/manual/updates.md says more.
6
- VERSION = "0.2.0"
6
+ VERSION = "0.3.0"
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: proctolog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gill Abarbanel
@@ -193,6 +193,7 @@ files:
193
193
  - app/models/proctolog/handover/worklist.rb
194
194
  - app/models/proctolog/housekeeping.rb
195
195
  - app/models/proctolog/kit.rb
196
+ - app/models/proctolog/localhost.rb
196
197
  - app/models/proctolog/marks.rb
197
198
  - app/models/proctolog/membership.rb
198
199
  - app/models/proctolog/mentions.rb
@@ -217,6 +218,7 @@ files:
217
218
  - app/models/proctolog/rules.rb
218
219
  - app/models/proctolog/scenario.rb
219
220
  - app/models/proctolog/server.rb
221
+ - app/models/proctolog/setup.rb
220
222
  - app/models/proctolog/share.rb
221
223
  - app/models/proctolog/share/ngrok.rb
222
224
  - app/models/proctolog/share/settings.rb
@@ -306,10 +308,12 @@ files:
306
308
  - docs/manual/handover.md
307
309
  - docs/manual/pr-format.md
308
310
  - docs/manual/reading.md
311
+ - docs/manual/setup.md
309
312
  - docs/manual/sharing.md
310
313
  - docs/manual/unopened.md
311
314
  - docs/manual/updates.md
312
315
  - lib/proctolog/changelog.rb
316
+ - lib/proctolog/changelog/news.rb
313
317
  - lib/proctolog/installation.rb
314
318
  - lib/proctolog/version.rb
315
319
  - public/asking.js