proctolog 0.1.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 +7 -0
- data/CHANGELOG.md +23 -0
- data/README.md +178 -0
- data/app/controllers/proctolog/acceptances_controller.rb +29 -0
- data/app/controllers/proctolog/application_controller.rb +32 -0
- data/app/controllers/proctolog/bursts_controller.rb +34 -0
- data/app/controllers/proctolog/choices_controller.rb +30 -0
- data/app/controllers/proctolog/code_reviews_controller.rb +26 -0
- data/app/controllers/proctolog/decisions_controller.rb +33 -0
- data/app/controllers/proctolog/entries_controller.rb +14 -0
- data/app/controllers/proctolog/evidence_controller.rb +23 -0
- data/app/controllers/proctolog/freshness_controller.rb +27 -0
- data/app/controllers/proctolog/good_to_go_controller.rb +29 -0
- data/app/controllers/proctolog/memberships_controller.rb +20 -0
- data/app/controllers/proctolog/mentions_controller.rb +16 -0
- data/app/controllers/proctolog/moments_controller.rb +26 -0
- data/app/controllers/proctolog/note_params.rb +24 -0
- data/app/controllers/proctolog/notes_controller.rb +56 -0
- data/app/controllers/proctolog/open_pull_requests_controller.rb +59 -0
- data/app/controllers/proctolog/pictures_controller.rb +27 -0
- data/app/controllers/proctolog/prompts_controller.rb +28 -0
- data/app/controllers/proctolog/pull_request_scoped.rb +21 -0
- data/app/controllers/proctolog/pull_requests_controller.rb +34 -0
- data/app/controllers/proctolog/put_away_controller.rb +19 -0
- data/app/controllers/proctolog/recordings_controller.rb +28 -0
- data/app/controllers/proctolog/rendered_pieces_controller.rb +11 -0
- data/app/controllers/proctolog/review_data_controller.rb +12 -0
- data/app/controllers/proctolog/rules_controller.rb +20 -0
- data/app/controllers/proctolog/scenario_notes_controller.rb +26 -0
- data/app/controllers/proctolog/scenario_scoped.rb +21 -0
- data/app/controllers/proctolog/scenarios_controller.rb +67 -0
- data/app/controllers/proctolog/sitting_servers_controller.rb +30 -0
- data/app/controllers/proctolog/waiting_controller.rb +14 -0
- data/app/controllers/proctolog/with_decisions.rb +28 -0
- data/app/helpers/proctolog/bursts_helper.rb +52 -0
- data/app/helpers/proctolog/decisions_helper.rb +102 -0
- data/app/helpers/proctolog/desk_helper.rb +61 -0
- data/app/helpers/proctolog/mentions_helper.rb +27 -0
- data/app/helpers/proctolog/open_pull_requests_helper.rb +31 -0
- data/app/helpers/proctolog/prompts_helper.rb +21 -0
- data/app/helpers/proctolog/pull_requests_helper.rb +67 -0
- data/app/helpers/proctolog/recordings_helper.rb +30 -0
- data/app/helpers/proctolog/rounds_helper.rb +27 -0
- data/app/helpers/proctolog/scenarios_helper.rb +123 -0
- data/app/helpers/proctolog/standings_helper.rb +80 -0
- data/app/jobs/proctolog/application_job.rb +8 -0
- data/app/jobs/proctolog/ask_for_release_job.rb +9 -0
- data/app/jobs/proctolog/fetch_pictures_ahead_job.rb +36 -0
- data/app/jobs/proctolog/prune_fresh_profiles_job.rb +9 -0
- data/app/jobs/proctolog/read_mentions_ahead_job.rb +12 -0
- data/app/models/proctolog/acceptance.rb +47 -0
- data/app/models/proctolog/app.rb +92 -0
- data/app/models/proctolog/browser.rb +42 -0
- data/app/models/proctolog/burst.rb +268 -0
- data/app/models/proctolog/burst_notices.rb +143 -0
- data/app/models/proctolog/chapters.rb +42 -0
- data/app/models/proctolog/check.rb +122 -0
- data/app/models/proctolog/choices.rb +112 -0
- data/app/models/proctolog/code_review.rb +251 -0
- data/app/models/proctolog/decision_record.rb +12 -0
- data/app/models/proctolog/decisions.rb +199 -0
- data/app/models/proctolog/description.rb +165 -0
- data/app/models/proctolog/difference.rb +138 -0
- data/app/models/proctolog/doctor.rb +127 -0
- data/app/models/proctolog/evidence.rb +29 -0
- data/app/models/proctolog/failure.rb +7 -0
- data/app/models/proctolog/focus.rb +48 -0
- data/app/models/proctolog/fresh_sitting.rb +35 -0
- data/app/models/proctolog/github.rb +353 -0
- data/app/models/proctolog/handover/brief.rb +138 -0
- data/app/models/proctolog/handover/layers.rb +128 -0
- data/app/models/proctolog/handover/places.rb +98 -0
- data/app/models/proctolog/handover/taken.rb +41 -0
- data/app/models/proctolog/handover/worklist.rb +35 -0
- data/app/models/proctolog/handover.rb +123 -0
- data/app/models/proctolog/housekeeping.rb +23 -0
- data/app/models/proctolog/kit.rb +36 -0
- data/app/models/proctolog/marks.rb +130 -0
- data/app/models/proctolog/membership.rb +91 -0
- data/app/models/proctolog/mentions.rb +117 -0
- data/app/models/proctolog/note.rb +94 -0
- data/app/models/proctolog/outside.rb +45 -0
- data/app/models/proctolog/overlay/brief.rb +52 -0
- data/app/models/proctolog/overlay/written.rb +33 -0
- data/app/models/proctolog/overlay.rb +87 -0
- data/app/models/proctolog/owner.rb +16 -0
- data/app/models/proctolog/pick.rb +47 -0
- data/app/models/proctolog/picture.rb +138 -0
- data/app/models/proctolog/pieces_rendering.rb +40 -0
- data/app/models/proctolog/project.rb +118 -0
- data/app/models/proctolog/pull_request.rb +89 -0
- data/app/models/proctolog/recording.rb +194 -0
- data/app/models/proctolog/release.rb +59 -0
- data/app/models/proctolog/rendering.rb +303 -0
- data/app/models/proctolog/reports.rb +66 -0
- data/app/models/proctolog/response_files.rb +55 -0
- data/app/models/proctolog/rounds.rb +305 -0
- data/app/models/proctolog/route.rb +151 -0
- data/app/models/proctolog/rules.rb +121 -0
- data/app/models/proctolog/scenario.rb +126 -0
- data/app/models/proctolog/server.rb +273 -0
- data/app/models/proctolog/since.rb +108 -0
- data/app/models/proctolog/size.rb +30 -0
- data/app/models/proctolog/standing.rb +132 -0
- data/app/models/proctolog/theme.rb +40 -0
- data/app/models/proctolog/unopened/numbers.rb +48 -0
- data/app/models/proctolog/unopened/opening.rb +88 -0
- data/app/models/proctolog/unopened.rb +203 -0
- data/app/models/proctolog/visit.rb +26 -0
- data/app/views/layouts/proctolog/application.html.erb +58 -0
- data/app/views/proctolog/acceptances/new.html.erb +34 -0
- data/app/views/proctolog/bursts/_line.html.erb +28 -0
- data/app/views/proctolog/bursts/_merged.html.erb +23 -0
- data/app/views/proctolog/bursts/_moved.html.erb +16 -0
- data/app/views/proctolog/bursts/_notices.html.erb +39 -0
- data/app/views/proctolog/bursts/_put_away.html.erb +11 -0
- data/app/views/proctolog/bursts/_rules.html.erb +27 -0
- data/app/views/proctolog/bursts/_side.html.erb +49 -0
- data/app/views/proctolog/bursts/_side_stop.html.erb +10 -0
- data/app/views/proctolog/bursts/show.html.erb +54 -0
- data/app/views/proctolog/code_reviews/show.html.erb +33 -0
- data/app/views/proctolog/decisions/_alternative.html.erb +5 -0
- data/app/views/proctolog/decisions/_beneath.html.erb +21 -0
- data/app/views/proctolog/decisions/_box.html.erb +27 -0
- data/app/views/proctolog/decisions/_choices.html.erb +9 -0
- data/app/views/proctolog/decisions/_draft_tools.html.erb +47 -0
- data/app/views/proctolog/decisions/_part.html.erb +61 -0
- data/app/views/proctolog/decisions/_round_data.html.erb +1 -0
- data/app/views/proctolog/decisions/_theme.html.erb +13 -0
- data/app/views/proctolog/decisions/index.html.erb +75 -0
- data/app/views/proctolog/notes/_card.html.erb +19 -0
- data/app/views/proctolog/notes/_template.html.erb +41 -0
- data/app/views/proctolog/open_pull_requests/_side.html.erb +30 -0
- data/app/views/proctolog/open_pull_requests/index.html.erb +106 -0
- data/app/views/proctolog/prompts/show.html.erb +41 -0
- data/app/views/proctolog/pull_requests/_connections.html.erb +65 -0
- data/app/views/proctolog/pull_requests/_finish.html.erb +42 -0
- data/app/views/proctolog/pull_requests/_margin.html.erb +33 -0
- data/app/views/proctolog/pull_requests/_note.html.erb +4 -0
- data/app/views/proctolog/pull_requests/_outline.html.erb +26 -0
- data/app/views/proctolog/pull_requests/_paragraphs.html.erb +9 -0
- data/app/views/proctolog/pull_requests/_scenarios.html.erb +16 -0
- data/app/views/proctolog/pull_requests/_since.html.erb +42 -0
- data/app/views/proctolog/pull_requests/_standing.html.erb +51 -0
- data/app/views/proctolog/pull_requests/_status.html.erb +5 -0
- data/app/views/proctolog/pull_requests/_try.html.erb +122 -0
- data/app/views/proctolog/pull_requests/show.html.erb +145 -0
- data/app/views/proctolog/recordings/_dialog.html.erb +26 -0
- data/app/views/proctolog/recordings/_list.html.erb +8 -0
- data/app/views/proctolog/recordings/_note.html.erb +22 -0
- data/app/views/proctolog/recordings/index.html.erb +33 -0
- data/app/views/proctolog/recordings/show.html.erb +118 -0
- data/app/views/proctolog/scenarios/_dialog.html.erb +22 -0
- data/app/views/proctolog/scenarios/_note.html.erb +18 -0
- data/app/views/proctolog/scenarios/_open.html.erb +9 -0
- data/app/views/proctolog/scenarios/_scenario.html.erb +48 -0
- data/app/views/proctolog/scenarios/_server.html.erb +20 -0
- data/app/views/proctolog/scenarios/index.html.erb +62 -0
- data/bin/proctolog +330 -0
- data/completions/_proctolog +143 -0
- data/config/application.rb +94 -0
- data/config/database.yml +16 -0
- data/config/environment.rb +6 -0
- data/config/initializers/grouse.rb +113 -0
- data/config/initializers/grouse_notes.rb +9 -0
- data/config/keys.yml +6 -0
- data/config/routes.rb +71 -0
- data/config/storage.yml +4 -0
- data/db/migrate/20261019213542_create_grouse_tables.grouse.rb +128 -0
- data/db/migrate/20261019213550_create_active_storage_tables.active_storage.rb +58 -0
- data/docs/manual/app.md +157 -0
- data/docs/manual/bursts.md +129 -0
- data/docs/manual/deciding.md +89 -0
- data/docs/manual/desk.png +0 -0
- data/docs/manual/films.md +20 -0
- data/docs/manual/handover.md +93 -0
- data/docs/manual/pr-format.md +75 -0
- data/docs/manual/reading.md +233 -0
- data/docs/manual/unopened.md +80 -0
- data/docs/manual/updates.md +55 -0
- data/lib/proctolog/installation.rb +125 -0
- data/lib/proctolog/version.rb +7 -0
- data/public/asking.js +50 -0
- data/public/code-review.js +40 -0
- data/public/cursor.js +292 -0
- data/public/decision-screenshots.js +97 -0
- data/public/decisions.css +949 -0
- data/public/decisions.js +1289 -0
- data/public/desk.css +582 -0
- data/public/desk.js +126 -0
- data/public/favicon.png +0 -0
- data/public/keys.js +228 -0
- data/public/mentions.js +389 -0
- data/public/messages.js +103 -0
- data/public/notes.js +731 -0
- data/public/pick.css +99 -0
- data/public/plaque.png +0 -0
- data/public/pr-number.js +11 -0
- data/public/prompt.css +46 -0
- data/public/prompt.js +16 -0
- data/public/pull_request.css +805 -0
- data/public/recording.css +379 -0
- data/public/recording.js +394 -0
- data/public/release.js +25 -0
- data/public/respond.css +260 -0
- data/public/respond.js +95 -0
- data/public/review-drafts.js +949 -0
- data/public/review-rounds.js +1447 -0
- data/public/scenarios.css +253 -0
- data/public/scenarios.js +36 -0
- data/public/schemas/proctolog-response-v1.json +101 -0
- data/public/side.js +133 -0
- data/public/standing.css +478 -0
- data/public/terminal-apl.svg +6 -0
- data/public/terminal-cuneiform.svg +3 -0
- data/public/terminal-grain-dots.svg +6 -0
- data/public/terminal-grain-weave.svg +6 -0
- data/public/terminal-grain-wedges.svg +6 -0
- data/public/terminal-shades.svg +3 -0
- data/public/terminal.css +4426 -0
- data/public/toast.js +26 -0
- data/public/try.js +124 -0
- data/public/viewer.js +808 -0
- data/public/words.js +81 -0
- data/skills/proctolog-handover/SKILL.md +77 -0
- data/skills/proctolog-pr-writing/SKILL.md +228 -0
- data/skills/proctolog-pr-writing/references/example-description.md +33 -0
- metadata +366 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
return unless Proctolog::Desk.grouse?
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
# Grouse: the overlay the owner writes notes on the desk with, read in its inbox at /grouse
|
|
6
|
+
# and by coding agents through its export API (AGENTS.md, "Review notes"). Every setting
|
|
7
|
+
# left commented out is at its default.
|
|
8
|
+
#
|
|
9
|
+
# Grouse calls these lambdas from its own controllers, which answer before any callback of
|
|
10
|
+
# the desk's runs, so each finds what it needs from the request itself.
|
|
11
|
+
|
|
12
|
+
key = ->(name) do
|
|
13
|
+
next unless Rails.configuration.x.reads_keys
|
|
14
|
+
|
|
15
|
+
reference = YAML.load_file(Rails.root.join("config/keys.yml")).fetch(name)
|
|
16
|
+
read, done = Open3.capture2("op", "read", "--no-newline", reference)
|
|
17
|
+
read.presence if done.success?
|
|
18
|
+
rescue Errno::ENOENT
|
|
19
|
+
Rails.logger.warn("[proctolog] 1Password's op is not installed, so Grouse has no #{name} key.")
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Grouse.configure do |c|
|
|
24
|
+
# The desk serves one person, on this Mac, and listens on localhost alone: a request from
|
|
25
|
+
# this machine is the owner. Its tests open Grouse only where they test it.
|
|
26
|
+
c.enabled_for = ->(request) { request.local? } unless Rails.env.test?
|
|
27
|
+
|
|
28
|
+
# The desk has no sign-in. Every note is the owner's, and nobody else is signed in.
|
|
29
|
+
c.current_author = ->(request) { Proctolog::Owner.current }
|
|
30
|
+
# c.author_label = ->(person) { person.name } # default: name, else username, else "User 12"
|
|
31
|
+
# c.app_user_label = nil # nil: author_label labels both
|
|
32
|
+
|
|
33
|
+
# Grouse's controllers inherit this. Keep ActionController::Base unless every callback of
|
|
34
|
+
# the class you name is safe to run on Grouse's endpoints.
|
|
35
|
+
# c.parent_controller = "ActionController::Base"
|
|
36
|
+
|
|
37
|
+
# Where things live, and for how long.
|
|
38
|
+
# c.base_url = nil # e.g. "https://app.example.com": inbox links in PostHog events and hooks, which run with no request
|
|
39
|
+
# c.queue_name = :default
|
|
40
|
+
# c.rate_limits.notes = {to: 30, within: 1.minute} # notes per reviewer
|
|
41
|
+
# c.rate_limits.takes = {to: 120, within: 1.minute} # per reviewer: the updates while a voice note records
|
|
42
|
+
# c.rate_limits.screenshots = {to: 30, within: 1.minute} # screenshot uploads per reviewer, one a note
|
|
43
|
+
# c.rate_limits.store = nil # nil: Grouse's own memory store
|
|
44
|
+
# c.inbox.poll_interval = 20.seconds # how often an open inbox asks whether it changed; nil: never
|
|
45
|
+
# c.storage_service = nil # where recordings and screenshots are kept, a service in config/storage.yml; nil: config.active_storage.service
|
|
46
|
+
# c.retention.media = 90.days # reserved for pruning: nothing prunes yet
|
|
47
|
+
# c.retention.resolved_notes = 90.days # reserved for pruning: nothing prunes yet
|
|
48
|
+
|
|
49
|
+
# What a page says about itself.
|
|
50
|
+
# c.release = nil # e.g. -> { ENV["GIT_REVISION"] }, or a String
|
|
51
|
+
# c.context_providers = [] # each ->(request) { {"native_shell" => true} }, 4 KB in all
|
|
52
|
+
# c.privacy.mask_selectors = [] # on top of .ph-mask and [data-grouse-mask]
|
|
53
|
+
# c.privacy.block_selectors = [] # on top of .ph-no-capture and [data-grouse-block]
|
|
54
|
+
# c.privacy.redact = %i[tokens emails numbers] # drop a name to keep that kind of text in the outline
|
|
55
|
+
# c.capture_network_errors = false # true wraps fetch and XHR to keep same-origin requests answered >= 400 or not at all:
|
|
56
|
+
# # method, path, status; never a body, a header, a query, another origin's or Grouse's own
|
|
57
|
+
c.screenshots = :opt_in # :opt_in offers a camera per note, :always starts it on, :off takes none and refuses uploads;
|
|
58
|
+
# # screenshots are kept by Active Storage, as recordings are
|
|
59
|
+
|
|
60
|
+
# The pill.
|
|
61
|
+
c.widget.position = "bottom-right" # clear of the route pane at the desk's left
|
|
62
|
+
# c.widget.z_index = 58
|
|
63
|
+
# c.widget.shortcut = "Alt+Shift+KeyN" # a KeyboardEvent.code form; nil turns it off
|
|
64
|
+
# c.widget.gesture = "toggle" # or "hold": the record button is held while speaking
|
|
65
|
+
|
|
66
|
+
# Voice notes need Active Storage (bin/rails active_storage:install). Without it they are
|
|
67
|
+
# simply not offered, and notes are typed-only.
|
|
68
|
+
# c.voice.enabled = true # false: typed-only, even with Active Storage
|
|
69
|
+
# c.voice.salvage_after = 3.minutes # a voice draft no page has touched for this long is delivered as it stands
|
|
70
|
+
|
|
71
|
+
# PostHog. Grouse reads your posthog-js and never boots its own. With posthog-rails
|
|
72
|
+
# installed, its PostHog.client is used for server events.
|
|
73
|
+
# c.posthog.client = nil # e.g. PostHog::Client.new(api_key: ENV["POSTHOG_KEY"], host: "https://us.i.posthog.com")
|
|
74
|
+
# c.posthog.ready_event = nil # e.g. "posthog:loaded", if your snippet's `loaded` callback dispatches it
|
|
75
|
+
# c.posthog.consent_meta = nil # e.g. "posthog-consent": a <meta name> whose content says whether analytics were accepted
|
|
76
|
+
# c.posthog.client_events = false # true sends grouse_note_started into your PostHog project when a reviewer starts a note
|
|
77
|
+
# c.posthog.replay_hosts = %w[us.posthog.com eu.posthog.com] # the PostHog UI hosts a replay link may point at
|
|
78
|
+
# c.posthog.distinct_id_for = nil # e.g. ->(author) { author.id.to_s }
|
|
79
|
+
# c.posthog.include_text = false # true sends the note's words (first 2,000 characters) with grouse_note_created
|
|
80
|
+
# c.posthog.llm_analytics = false # true sends one $ai_generation per structuring request (PostHog LLM analytics, billed on its own); the prompt and the answer only with include_text
|
|
81
|
+
|
|
82
|
+
# Transcription, structuring and hand-off. Each is optional and ends in a visible state.
|
|
83
|
+
# c.on_delivered = [] # each ->(note) { }, run in a job after the request; keep them short
|
|
84
|
+
# c.on_ready = {} # name => sink, each run in a job once a note's words and structure are in; none: nothing is handed on
|
|
85
|
+
# c.on_ready[:linear] = ReviewNotes::LinearExport # a callable: gets a Grouse::ReadyNote, answers {id:, url:}, or raises Grouse::Sinks::Unavailable, "linear_503" to be asked again
|
|
86
|
+
# The owner's keys, read from 1Password by config/keys.yml's references where the desk
|
|
87
|
+
# reads keys at all (config.x.reads_keys). Without one, a voice note arrives as audio only
|
|
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.
|
|
90
|
+
c.transcriber = Grouse::Transcribers::ElevenLabs.new(api_key: key.call("elevenlabs"))
|
|
91
|
+
c.structurer = Grouse::Structurers::Anthropic.new(api_key: key.call("anthropic"))
|
|
92
|
+
# c.transcriber = nil # turns a recording into words; with none, voice notes arrive as audio only
|
|
93
|
+
# Grouse::Transcribers::ElevenLabs.new(api_key: ENV["ELEVENLABS_API_KEY"], language: nil, keyterms: %w[YourProduct])
|
|
94
|
+
# Grouse::Transcribers::OpenAI.new(api_key: ENV["OPENAI_API_KEY"], model: "gpt-transcribe", language: "he")
|
|
95
|
+
# ->(io, filename:, content_type:) { "the words" } # or your own: a String, or a Grouse::Transcript
|
|
96
|
+
# c.structurer = nil # writes each note up as an issue (title, kind, steps); with none, notes stay as written
|
|
97
|
+
# Grouse::Structurers::Anthropic.new(api_key: Rails.application.credentials.dig(:anthropic, :api_key))
|
|
98
|
+
# Grouse::Structurers::OpenAI.new(api_key: ENV["OPENAI_API_KEY"], model: "gpt-5-mini") # base_url: any server that speaks its chat API
|
|
99
|
+
# ->(note, prompt) { YourGateway.json(system: prompt.system, user: prompt.user, schema: prompt.schema) } # a Hash in that schema's shape
|
|
100
|
+
|
|
101
|
+
# Coding agents read notes with a token each: {label => token}, tokens of 32+ characters.
|
|
102
|
+
# No token means no API.
|
|
103
|
+
# The agents' token, made the first time the desk boots and kept beside its secret, out
|
|
104
|
+
# of Git (AGENTS.md says where). Tests have no API.
|
|
105
|
+
c.api_tokens = -> { {"agent" => Proctolog::Desk.kept("grouse_token") { SecureRandom.hex(32) }} } unless Rails.env.test?
|
|
106
|
+
|
|
107
|
+
# Review sessions (docs/plan/06): a review an outside tool prepares, from a runner with
|
|
108
|
+
# Grouse::Sessions.open or with an API token over POST /api/sessions, shows the widget to whoever
|
|
109
|
+
# carries its cookie and collects their notes. They open only in these environments:
|
|
110
|
+
# c.sessions.environments = %w[development]
|
|
111
|
+
# The hosts a session's notes may be pushed back to, at its return_url; [] turns pushing off.
|
|
112
|
+
# c.sessions.return_hosts = %w[localhost 127.0.0.1 ::1]
|
|
113
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The desk makes its database, and brings it up to the latest migration,
|
|
4
|
+
# whenever it boots: nobody runs db:migrate on a tool they only start, and a
|
|
5
|
+
# pull that brings a migration then needs no step of the owner's.
|
|
6
|
+
Rails.application.config.after_initialize do
|
|
7
|
+
ActiveRecord::Migration.verbose = false
|
|
8
|
+
ActiveRecord::Base.connection_pool.migration_context.migrate
|
|
9
|
+
end
|
data/config/keys.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# The owner's vendor keys for Grouse, as 1Password references, never the keys
|
|
2
|
+
# themselves: the desk reads each with `op read` when it serves
|
|
3
|
+
# (config/initializers/grouse.rb). A key 1Password cannot give leaves voice
|
|
4
|
+
# notes audio only, or notes unwritten up.
|
|
5
|
+
elevenlabs: "op://Private/eqn4mdg3vkzv5gkgrxuuqchiqa/credential"
|
|
6
|
+
anthropic: "op://Private/Anthropic API Key/credential"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Proctolog::Desk.routes.draw do
|
|
4
|
+
# The owner's notes on the desk itself: the overlay's endpoints, its inbox and the export API.
|
|
5
|
+
mount Grouse::Engine, at: "/grouse" if Proctolog::Desk.grouse?
|
|
6
|
+
|
|
7
|
+
scope module: :proctolog do
|
|
8
|
+
root "bursts#index"
|
|
9
|
+
|
|
10
|
+
# The repository's open pull requests, to pick a burst from.
|
|
11
|
+
resources :open_pull_requests, only: :index, path: "pull_requests" do
|
|
12
|
+
post :alone, on: :member
|
|
13
|
+
post :chain, on: :member
|
|
14
|
+
end
|
|
15
|
+
post "stacks/:id", to: "open_pull_requests#stack", as: :review_stack, constraints: {id: /\d+/}
|
|
16
|
+
|
|
17
|
+
# The pull requests, of every state, that a # typed in any box may mean.
|
|
18
|
+
resources :mentions, only: :index
|
|
19
|
+
|
|
20
|
+
resources :bursts, only: %i[show create] do
|
|
21
|
+
resource :put_away, only: %i[create destroy], controller: :put_away
|
|
22
|
+
resources :pull_requests, only: :show, param: :number do
|
|
23
|
+
resources :notes, only: %i[create update]
|
|
24
|
+
resource :good_to_go, only: %i[create destroy], controller: :good_to_go
|
|
25
|
+
resource :waiting, only: :update, controller: :waiting
|
|
26
|
+
resource :code_review, only: %i[show create destroy]
|
|
27
|
+
resources :decisions, only: :update, param: :position
|
|
28
|
+
resource :acceptance, only: %i[new create]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
resources :decisions, only: :index
|
|
32
|
+
resource :prompt, only: %i[show create] do
|
|
33
|
+
post :preview
|
|
34
|
+
end
|
|
35
|
+
resource :review_data, only: :show, controller: :review_data
|
|
36
|
+
resource :rendered_pieces, only: :create
|
|
37
|
+
resource :evidence, only: :show, controller: :evidence do
|
|
38
|
+
get :compare
|
|
39
|
+
end
|
|
40
|
+
resource :choices, only: %i[show update]
|
|
41
|
+
resources :rules, only: %i[update destroy], id: /[^\/]+/
|
|
42
|
+
resource :freshness, only: :show, controller: :freshness
|
|
43
|
+
resource :membership, only: :update do
|
|
44
|
+
post :handed_over
|
|
45
|
+
end
|
|
46
|
+
resources :scenarios, only: :index do
|
|
47
|
+
post :open, on: :member
|
|
48
|
+
post :saw, on: :member
|
|
49
|
+
post :pull, on: :member
|
|
50
|
+
resources :notes, only: %i[create update], controller: :scenario_notes
|
|
51
|
+
end
|
|
52
|
+
resource :sitting_server, only: %i[show create destroy]
|
|
53
|
+
resources :recordings, only: :index
|
|
54
|
+
get "recordings/*path", to: "recordings#show", as: :recording, format: false
|
|
55
|
+
get "stills/:index/*path", to: "recordings#still", as: :recording_still, format: false, constraints: {index: /\d+/}
|
|
56
|
+
resources :moments, only: %i[create update]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Bursts were called reviews: a tab opened then still reads and saves.
|
|
60
|
+
# 307 keeps a PUT a PUT, with its body, and is not remembered by the browser.
|
|
61
|
+
match "reviews(/*rest)", via: :all, format: false, to: redirect(status: 307) { |_, request| request.fullpath.sub(%r{\A/reviews}, "/bursts") }
|
|
62
|
+
|
|
63
|
+
# Rack::Files answers Range requests, which a browser needs to seek in a video.
|
|
64
|
+
mount Rack::Files.new(Proctolog::Recording.directory.to_s, {"cache-control" => "no-cache"}), at: "films"
|
|
65
|
+
|
|
66
|
+
resources :entries, only: :show, param: :token
|
|
67
|
+
get "pictures/:asset", to: "pictures#show", as: :picture, constraints: {asset: Proctolog::Picture::ASSET}
|
|
68
|
+
get "pictures/:before/against/:after", to: "pictures#compare", as: :difference, format: false,
|
|
69
|
+
constraints: {before: Proctolog::Picture::FILE, after: Proctolog::Picture::FILE}
|
|
70
|
+
end
|
|
71
|
+
end
|
data/config/storage.yml
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# This migration comes from grouse (originally 20261014000000)
|
|
2
|
+
# Portions adapted from rails-markup (https://github.com/nauman/rails-markup) at ad756b8, Copyright (c) 2026 InventList, MIT licence. See NOTICE.
|
|
3
|
+
|
|
4
|
+
# Grouse's two tables in one migration, so a host never needs a second one to turn on
|
|
5
|
+
# voice (M2) or review sessions (M6a) (D11, D16). It is copied into the host's db/migrate,
|
|
6
|
+
# so it decides two things when it runs there, not when the gem was built: jsonb on
|
|
7
|
+
# PostgreSQL (json on SQLite), and the host's primary-key type, the way Active Storage's
|
|
8
|
+
# own first migration does. Written against 8.0 because the gemspec admits 8.0 hosts.
|
|
9
|
+
class CreateGrouseTables < ActiveRecord::Migration[8.0]
|
|
10
|
+
def change
|
|
11
|
+
refuse_unsupported_adapter!
|
|
12
|
+
primary_key_type, foreign_key_type = primary_and_foreign_key_types
|
|
13
|
+
|
|
14
|
+
create_table :grouse_sessions, id: primary_key_type do |t|
|
|
15
|
+
t.string :public_id, null: false
|
|
16
|
+
t.string :source, null: false # who opened it, e.g. "proctolog"
|
|
17
|
+
t.string :label, null: false # what the overlay shows
|
|
18
|
+
t.string :reviewer_label, null: false # the author of the session's notes
|
|
19
|
+
t.string :external_ref # the opener's own key for it
|
|
20
|
+
t.column :metadata, json_type, null: false, default: {}
|
|
21
|
+
t.text :return_url # allow-listed again before any push (M6a)
|
|
22
|
+
t.string :entry_token_digest # the one-time enter link (M6a), stored as a digest only
|
|
23
|
+
t.datetime :entered_at # set when that link is spent
|
|
24
|
+
t.datetime :expires_at, null: false
|
|
25
|
+
t.datetime :closed_at
|
|
26
|
+
t.timestamps
|
|
27
|
+
|
|
28
|
+
t.index :public_id, unique: true, name: "index_grouse_sessions_on_public_id"
|
|
29
|
+
t.index :entry_token_digest, unique: true, name: "index_grouse_sessions_on_entry_token"
|
|
30
|
+
t.index %i[source external_ref], name: "index_grouse_sessions_on_source_ref"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
create_table :grouse_notes, id: primary_key_type do |t|
|
|
34
|
+
t.string :public_id, null: false
|
|
35
|
+
t.string :client_uuid, null: false
|
|
36
|
+
|
|
37
|
+
# Who. No foreign key to the host's users: they are not ours (03 §7).
|
|
38
|
+
t.references :author, polymorphic: true, type: foreign_key_type, index: false
|
|
39
|
+
t.string :author_label
|
|
40
|
+
t.string :app_user_gid
|
|
41
|
+
t.string :app_user_label
|
|
42
|
+
# Named: a host may have its own `sessions` table (sartoria does).
|
|
43
|
+
t.references :session, type: foreign_key_type, index: {name: "index_grouse_notes_on_session_id"},
|
|
44
|
+
foreign_key: {to_table: :grouse_sessions, on_delete: :nullify, name: "fk_grouse_notes_session"}
|
|
45
|
+
|
|
46
|
+
# Capture: a draft until exactly one ending claims it (invariant 2).
|
|
47
|
+
t.string :capture, null: false, default: "draft"
|
|
48
|
+
t.string :delivered_via # sent | closed | salvaged (D29)
|
|
49
|
+
t.datetime :delivered_at
|
|
50
|
+
t.string :channel, null: false, default: "typed"
|
|
51
|
+
t.datetime :started_at # when the reviewer began; the context froze then
|
|
52
|
+
t.integer :take_seq, null: false, default: 0 # M2: a PATCH carrying an older take changes nothing
|
|
53
|
+
|
|
54
|
+
# Words, and the reviewer's own labels for them (D22).
|
|
55
|
+
t.text :body
|
|
56
|
+
t.string :intent
|
|
57
|
+
t.string :severity
|
|
58
|
+
|
|
59
|
+
# Annotations (M2, M4). Each ends in a state the inbox shows (invariant 1).
|
|
60
|
+
t.text :transcript
|
|
61
|
+
t.string :transcript_provider
|
|
62
|
+
t.string :transcript_language
|
|
63
|
+
t.string :transcription_state, null: false, default: "none"
|
|
64
|
+
t.string :transcription_error
|
|
65
|
+
t.integer :audio_ms
|
|
66
|
+
t.column :structured, json_type, null: false, default: {}
|
|
67
|
+
t.string :structure_state, null: false, default: "none"
|
|
68
|
+
t.string :structure_error
|
|
69
|
+
|
|
70
|
+
# Where: copies lifted from the frozen context by Contextual, for filtering and lists.
|
|
71
|
+
t.text :page_url
|
|
72
|
+
t.string :route # the URI pattern, e.g. /orders/:id(.:format)
|
|
73
|
+
t.string :controller_action # orders#show (CC-14)
|
|
74
|
+
t.string :request_id
|
|
75
|
+
t.string :release
|
|
76
|
+
t.string :environment
|
|
77
|
+
t.column :context, json_type, null: false, default: {}
|
|
78
|
+
t.column :anchor, json_type, null: false, default: {} # M3; {} means unanchored
|
|
79
|
+
|
|
80
|
+
# PostHog: why a replay link exists, or why it does not (D29).
|
|
81
|
+
t.string :posthog_distinct_id
|
|
82
|
+
t.string :posthog_session_id
|
|
83
|
+
t.string :replay_status, null: false, default: "error"
|
|
84
|
+
t.string :replay_detail
|
|
85
|
+
t.text :replay_url
|
|
86
|
+
|
|
87
|
+
# Triage (D14).
|
|
88
|
+
t.string :status, null: false, default: "pending"
|
|
89
|
+
t.integer :revision, null: false, default: 0
|
|
90
|
+
t.column :thread, json_type, null: false, default: []
|
|
91
|
+
t.datetime :resolved_at
|
|
92
|
+
t.string :resolved_by_label
|
|
93
|
+
t.string :resolution_commit
|
|
94
|
+
|
|
95
|
+
# What happened after delivery: the PostHog event and the hooks (M1), sinks (M4).
|
|
96
|
+
t.column :external_refs, json_type, null: false, default: {}
|
|
97
|
+
|
|
98
|
+
t.timestamps
|
|
99
|
+
|
|
100
|
+
t.index :public_id, unique: true, name: "index_grouse_notes_on_public_id"
|
|
101
|
+
t.index :client_uuid, unique: true, name: "index_grouse_notes_on_client_uuid"
|
|
102
|
+
t.index %i[status created_at], name: "index_grouse_notes_for_inbox"
|
|
103
|
+
t.index %i[capture updated_at], name: "index_grouse_notes_for_sweep"
|
|
104
|
+
t.index :posthog_session_id, name: "index_grouse_notes_on_posthog_session"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Active Storage's own idiom (activestorage-8.1.4 create_active_storage_tables.rb:49-55). A
|
|
111
|
+
# UUID-keyed host gets UUID keys here too, which M2 needs: active_storage_attachments.record_id
|
|
112
|
+
# follows the same setting.
|
|
113
|
+
def primary_and_foreign_key_types
|
|
114
|
+
config = Rails.configuration.generators
|
|
115
|
+
setting = config.options[config.orm][:primary_key_type]
|
|
116
|
+
[setting || :primary_key, setting || :bigint]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def json_type = postgresql? ? :jsonb : :json
|
|
120
|
+
def postgresql? = connection.adapter_name.match?(/postg/i)
|
|
121
|
+
|
|
122
|
+
# MySQL refuses literal defaults on JSON columns, and nothing here is tested on it. A plain
|
|
123
|
+
# refusal beats a cryptic failure halfway through a host's deploy.
|
|
124
|
+
def refuse_unsupported_adapter!
|
|
125
|
+
return if postgresql? || connection.adapter_name.match?(/sqlite/i)
|
|
126
|
+
raise NotImplementedError, "Grouse's tables need PostgreSQL or SQLite: their JSON columns carry defaults"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# This migration comes from active_storage (originally 20170806125915)
|
|
2
|
+
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
|
|
3
|
+
def change
|
|
4
|
+
# Use Active Record's configured type for primary and foreign keys
|
|
5
|
+
primary_key_type, foreign_key_type = primary_and_foreign_key_types
|
|
6
|
+
|
|
7
|
+
create_table :active_storage_blobs, id: primary_key_type do |t|
|
|
8
|
+
t.string :key, null: false
|
|
9
|
+
t.string :filename, null: false
|
|
10
|
+
t.string :content_type
|
|
11
|
+
t.text :metadata
|
|
12
|
+
t.string :service_name, null: false
|
|
13
|
+
t.bigint :byte_size, null: false
|
|
14
|
+
t.string :checksum
|
|
15
|
+
|
|
16
|
+
if connection.supports_datetime_with_precision?
|
|
17
|
+
t.datetime :created_at, precision: 6, null: false
|
|
18
|
+
else
|
|
19
|
+
t.datetime :created_at, null: false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
t.index [:key], unique: true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
create_table :active_storage_attachments, id: primary_key_type do |t|
|
|
26
|
+
t.string :name, null: false
|
|
27
|
+
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
|
|
28
|
+
t.references :blob, null: false, type: foreign_key_type
|
|
29
|
+
|
|
30
|
+
if connection.supports_datetime_with_precision?
|
|
31
|
+
t.datetime :created_at, precision: 6, null: false
|
|
32
|
+
else
|
|
33
|
+
t.datetime :created_at, null: false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
t.index [:record_type, :record_id, :name, :blob_id], name: :index_active_storage_attachments_uniqueness, unique: true
|
|
37
|
+
t.foreign_key :active_storage_blobs, column: :blob_id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
create_table :active_storage_variant_records, id: primary_key_type do |t|
|
|
41
|
+
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
|
|
42
|
+
t.string :variation_digest, null: false
|
|
43
|
+
|
|
44
|
+
t.index [:blob_id, :variation_digest], name: :index_active_storage_variant_records_uniqueness, unique: true
|
|
45
|
+
t.foreign_key :active_storage_blobs, column: :blob_id
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def primary_and_foreign_key_types
|
|
52
|
+
config = Rails.configuration.generators
|
|
53
|
+
setting = config.options[config.orm][:primary_key_type]
|
|
54
|
+
primary_key_type = setting || :primary_key
|
|
55
|
+
foreign_key_type = setting || :bigint
|
|
56
|
+
[primary_key_type, foreign_key_type]
|
|
57
|
+
end
|
|
58
|
+
end
|
data/docs/manual/app.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# How the desk runs your app
|
|
2
|
+
|
|
3
|
+
A scenario button starts the app, puts it in a state, signs the right member in
|
|
4
|
+
and opens a browser on the right page. The desk knows nothing about your app to
|
|
5
|
+
do that. It reads `.proctolog/app.yml` in the checkout under review, a handful
|
|
6
|
+
of commands and paths in your app's own words. With no `app.yml`, scenarios
|
|
7
|
+
still list their steps, and you mark what you saw by hand.
|
|
8
|
+
|
|
9
|
+
A pull request's scenarios come from its description: each bold heading of its
|
|
10
|
+
"How to test locally" with numbered steps under it is one, opening the home
|
|
11
|
+
page as whoever is signed in. A burst's `scenarios/<burst>.yml` names the state
|
|
12
|
+
and the page a pull request's scenarios start from, as `recipe:` and `opens:`;
|
|
13
|
+
a pull request it names takes its scenarios from the file alone.
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
# Makes the app ready to serve, run before `serve` on every Start: so a
|
|
17
|
+
# database that is not there yet is made, and one that is costs a moment.
|
|
18
|
+
prepare: bin/scratch_dev %{slug} bin/rails db:prepare
|
|
19
|
+
|
|
20
|
+
# Runs until stopped, in the sitting's checkout. The desk calls the app up once
|
|
21
|
+
# every port in `listens` answers, counted up from %{port}.
|
|
22
|
+
serve: bin/scratch_dev %{slug} env PORT=%{port} bin/dev
|
|
23
|
+
listens: [0, 36]
|
|
24
|
+
|
|
25
|
+
# Makes a named state. %{args} becomes one key=value word per argument.
|
|
26
|
+
state: bin/scratch_dev %{slug} bin/dev-state %{name} %{args}
|
|
27
|
+
|
|
28
|
+
# Paths the browser goes through to be signed in or out, then on to %{to}.
|
|
29
|
+
sign_in: /dev/sessions/as?email=%{email}&to=%{to}
|
|
30
|
+
sign_out: /dev/sessions/out?to=%{to}
|
|
31
|
+
|
|
32
|
+
# Cookies a clean browser starts with, as name=value.
|
|
33
|
+
cookies: ["cookie_consent=1:declined"]
|
|
34
|
+
|
|
35
|
+
# Grouse's overlay, labelled with the scenario (below): `overlay` opens a review
|
|
36
|
+
# session from %{brief}, one JSON word, and `notes` prints one's notes.
|
|
37
|
+
overlay: env GROUSE_SESSION=%{brief} bin/rails grouse:sessions:open
|
|
38
|
+
notes: env GROUSE_SESSION_ID=%{session} bin/rails grouse:sessions:notes
|
|
39
|
+
|
|
40
|
+
# Window sizes beyond the desk's own desktop (1440×900) and phone (390×844).
|
|
41
|
+
sizes:
|
|
42
|
+
app: {width: 390, height: 844, user_agent: "… Hotwire Native iOS; Turbo Native iOS; …"}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Every key is optional; a scenario that needs a missing one says which.
|
|
46
|
+
|
|
47
|
+
## Where it runs
|
|
48
|
+
|
|
49
|
+
With nothing more said, a burst's scenarios run the checkout you review, as
|
|
50
|
+
it is, on port 3100 and a database named after the burst. A burst file can say
|
|
51
|
+
otherwise with a `sitting:`, for a checkout kept for reviewing:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
sitting:
|
|
55
|
+
checkout: ~/code/review-checkout # moved onto the branch when it has no changes of its own
|
|
56
|
+
branch: review/everything-together
|
|
57
|
+
port: 3300
|
|
58
|
+
slug: sitting
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The desk starts the app and makes its states only in a checkout that keeps a
|
|
62
|
+
`.proctolog/app.yml` of its own. A checkout older than that file is older than
|
|
63
|
+
the commands in it, which may not do there what they say: a `prepare` that
|
|
64
|
+
counts on the app reading `%{slug}` would make and fill the app's own
|
|
65
|
+
development database instead. Try it says so beside the pull request, and
|
|
66
|
+
Start refuses.
|
|
67
|
+
|
|
68
|
+
## What the desk expects back
|
|
69
|
+
|
|
70
|
+
`state`'s last line of output is one JSON object. `email`, when present, is the
|
|
71
|
+
member the desk signs in through `sign_in`. Every other key whose value is a
|
|
72
|
+
path is a place a scenario can open by name (`opens: lands_on`). Anything
|
|
73
|
+
earlier in the output is ignored, and a command that fails shows its last lines.
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{"email": "clara.vogel@example.com", "lands_on": "/onboarding/pact"}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`sign_in` and `sign_out` are paths on the app, opened by GET in a browser with
|
|
80
|
+
nothing else to go on. They answer by redirecting to `to`, a path. They only
|
|
81
|
+
need to exist where the app runs in development.
|
|
82
|
+
|
|
83
|
+
## Grouse's overlay
|
|
84
|
+
|
|
85
|
+
An app with [Grouse](https://github.com/LemuelCushing/grouse-rails) mounted
|
|
86
|
+
can name `overlay` and `notes`. Then a scenario's button opens Grouse's overlay
|
|
87
|
+
in the app too, labelled with the scenario. The panel opens by itself on the
|
|
88
|
+
first page, with a strip at its top: `proctolog · ` and the label, until when
|
|
89
|
+
the session lasts, what to expect, the steps, End session, and Back to the desk,
|
|
90
|
+
which opens the scenario's page on the desk in a new tab. Whatever you say or
|
|
91
|
+
type there is a note on the scenario once you press **Pull notes** beside it.
|
|
92
|
+
|
|
93
|
+
`overlay` runs after the state is made, and is given `%{brief}`, one JSON word:
|
|
94
|
+
|
|
95
|
+
| | |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `source` | `proctolog` |
|
|
98
|
+
| `label` | `grouse-rails #7 · Send a note`, or `sartoria · Step 2 · Sign in` for a step of the sitting |
|
|
99
|
+
| `reviewer_label` | who narrates, and each note's author: `Gill (PRoctolog)` |
|
|
100
|
+
| `external_ref` | `proctolog:<burst>/<scenario>@<head>` |
|
|
101
|
+
| `metadata` | `expect`, `steps` (one line each), `desk_url`, and the burst, pull request, scenario, recipe and head |
|
|
102
|
+
|
|
103
|
+
The member's email is never in the label, what to expect or the steps. Its
|
|
104
|
+
last line of output is `{"public_id": …, "cookie": "grouse_session=…; path=/; …"}`.
|
|
105
|
+
The cookie goes to the browser as it was printed, after the app's own, whichever
|
|
106
|
+
browser the scenario opens in: cookies on `localhost` are shared by every
|
|
107
|
+
port. A refusal, `{"problem": …}`, or a command that fails, never stops the
|
|
108
|
+
entering: the member is signed in all the same, and the desk says why the
|
|
109
|
+
overlay is missing.
|
|
110
|
+
|
|
111
|
+
Each press opens a session of its own, kept beside its scenario in the marks
|
|
112
|
+
file (`overlays:`, by scenario), with the notes pinned from it. **Pull notes**
|
|
113
|
+
runs `notes` for each with `%{session}`, its `public_id`, and expects
|
|
114
|
+
`{"session": {…}, "notes": […]}` last. Every note it has not pinned yet becomes
|
|
115
|
+
a note on the scenario, once: a fix or a change as a change, a question as a
|
|
116
|
+
question, anything else as a remark, in the owner's words (typed, then spoken),
|
|
117
|
+
then a line with Grouse's link to it, its route and the first templates that
|
|
118
|
+
drew it. A voice note still being written out waits for the next pull. Pinned,
|
|
119
|
+
they go with the next handover like any other note. `notes` boots the app, so
|
|
120
|
+
it runs only when you press.
|
|
121
|
+
|
|
122
|
+
An app without `overlay` opens as it always did.
|
|
123
|
+
|
|
124
|
+
## Placeholders
|
|
125
|
+
|
|
126
|
+
| | |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `%{slug}`, `%{port}` | the sitting's (above) |
|
|
129
|
+
| `%{name}`, `%{args}` | the scenario's `recipe:` and its arguments |
|
|
130
|
+
| `%{email}`, `%{to}` | filled in URL-encoded |
|
|
131
|
+
| `%{brief}`, `%{session}` | the overlay's brief, as one JSON word, and a session's `public_id` |
|
|
132
|
+
|
|
133
|
+
Commands are split into words before they are filled in, so a value with a
|
|
134
|
+
space stays one word. They run in the sitting's checkout, outside the desk's
|
|
135
|
+
bundle.
|
|
136
|
+
|
|
137
|
+
## Settings
|
|
138
|
+
|
|
139
|
+
Beside `app.yml`, `.proctolog/project.yml` says what is true of the repository
|
|
140
|
+
in every checkout of it, so it is kept in Git too:
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
# Folders of decision records, from the root. A handover asks for a rule you
|
|
144
|
+
# made to be written in CLAUDE.md or a record in one of them.
|
|
145
|
+
decision_records: [docs/adr, docs/solutions/architecture-decisions]
|
|
146
|
+
|
|
147
|
+
# The themes a decision may name, each with what it means, in the order the
|
|
148
|
+
# desk groups them. Without them, the desk's own stand: proctolog themes.
|
|
149
|
+
themes:
|
|
150
|
+
copy: What the words on a screen say, and how
|
|
151
|
+
money: What anything costs
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`.proctolog/local.yml`, kept out of Git, says what is true of one machine:
|
|
155
|
+
`capture_kit`, the folder of the capture kit films come from. A key it names
|
|
156
|
+
wins over the same key in `project.yml`, whole: its `themes` replace the
|
|
157
|
+
project's rather than add to them.
|