openlanes 3.0.0 → 3.0.1
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 +4 -4
- data/agents/c3po-operator.md +58 -0
- data/lib/openlanes/adapters/base.rb +7 -0
- data/lib/openlanes/adapters/claude.rb +21 -2
- data/lib/openlanes/adapters.rb +19 -2
- data/lib/openlanes/cli/run/run.rb +47 -15
- data/lib/openlanes/cli/start/start.rb +1 -14
- data/lib/openlanes/cli/web/web.rb +11 -1
- data/lib/openlanes/engine/dispatcher.rb +62 -7
- data/lib/openlanes/engine/narrator.rb +107 -0
- data/lib/openlanes/engine/progress_log.rb +50 -9
- data/lib/openlanes/execution/prompt.rb +13 -0
- data/lib/openlanes/lifecycle/creator.rb +24 -0
- data/lib/openlanes/live/chat.rb +616 -0
- data/lib/openlanes/live/commands.rb +4 -1
- data/lib/openlanes/live/copilot/brief.rb +128 -0
- data/lib/openlanes/live/copilot/cards.rb +226 -0
- data/lib/openlanes/live/copilot/operator.rb +259 -0
- data/lib/openlanes/live/copilot.rb +256 -0
- data/lib/openlanes/live/keys.rb +58 -0
- data/lib/openlanes/live/memory_store.rb +24 -0
- data/lib/openlanes/live/memory_subscriber.rb +40 -0
- data/lib/openlanes/live/session.rb +7 -1
- data/lib/openlanes/live/stations.rb +9 -5
- data/lib/openlanes/live/store.rb +18 -0
- data/lib/openlanes/live/subscriber.rb +73 -0
- data/lib/openlanes/live.rb +5 -0
- data/lib/openlanes/presentation/reporter.rb +2 -2
- data/lib/openlanes/support/ui.rb +75 -1
- data/lib/openlanes/vcs/publisher.rb +62 -0
- data/lib/openlanes/vcs/worktree.rb +44 -9
- data/lib/openlanes/version.rb +1 -1
- data/lib/openlanes/web/app.rb +100 -1
- data/lib/openlanes/web/context.rb +20 -2
- data/lib/openlanes/web/demo.rb +10 -0
- data/lib/openlanes/web/events.rb +51 -2
- data/lib/openlanes/web/launcher.rb +1 -1
- data/lib/openlanes/web/server.rb +2 -0
- data/lib/openlanes/web/stores.rb +11 -0
- data/lib/openlanes/web.rb +13 -0
- data/web/dist/assets/favicon-BFJEKKAE.png +0 -0
- data/web/dist/assets/{index-BdR34cXC.js → index-6MZkmh3k.js} +2 -2
- data/web/dist/assets/{index-uM88Njdj.js → index-B5CKJPm-.js} +2 -2
- data/web/dist/assets/{index-DB4QwuYn.js → index-BF6QYroq.js} +2 -2
- data/web/dist/assets/{index-Bp7W-ZTq.js → index-BFPI-zXI.js} +2 -2
- data/web/dist/assets/{index-CI74IkWP.js → index-BYJCWlk0.js} +49 -49
- data/web/dist/assets/{index-popk9wbW.js → index-C2jwoWVV.js} +2 -2
- data/web/dist/assets/{index-CYxcbpRM.js → index-C3zc4gFe.js} +2 -2
- data/web/dist/assets/index-CjInBi5X.css +1 -0
- data/web/dist/assets/{index-2xPC-eHt.js → index-CnJfB0l2.js} +2 -2
- data/web/dist/assets/{index-P3vSkiU-.js → index-CnvkZu5Y.js} +2 -2
- data/web/dist/assets/{index-OR-odsl9.js → index-DH0gDlee.js} +2 -2
- data/web/dist/assets/{index-D0rmJN3h.js → index-DXgeajd6.js} +2 -2
- data/web/dist/assets/{index-C0VmkrO-.js → index-DaSjQ-H4.js} +2 -2
- data/web/dist/assets/{index-B7-O9ujQ.js → index-DbueqOK5.js} +2 -2
- data/web/dist/assets/{index-DDI7zoAS.js → index-Dbxvk-EC.js} +2 -2
- data/web/dist/assets/{index-8dNhRmiO.js → index-DtA46i32.js} +2 -2
- data/web/dist/assets/{index-DGLUuEXi.js → index-IqD8mBos.js} +2 -2
- data/web/dist/assets/{index-Byyxbh_z.js → index-bpsFIpXE.js} +2 -2
- data/web/dist/assets/{index-mRV4XhJZ.js → index-pvJi-JJO.js} +2 -2
- data/web/dist/assets/logo-DXDCUiD8.png +0 -0
- data/web/dist/index.html +3 -3
- metadata +31 -23
- data/web/dist/assets/favicon-C8pAuqqr.png +0 -0
- data/web/dist/assets/index-a4EHR-u4.css +0 -1
- data/web/dist/assets/logo-on-dark-DI9gCdN4.png +0 -0
- data/web/dist/assets/logo-on-light-BciwqxcJ.png +0 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
3
6
|
module OpenLanes
|
|
4
7
|
module Engine
|
|
5
8
|
# One line of `run --log FILE`, laid out as fixed-width columns.
|
|
@@ -19,8 +22,7 @@ module OpenLanes
|
|
|
19
22
|
# @example
|
|
20
23
|
# ProgressLog.render("editing spec.md", plan: "003.00", status: "⭐️ Planned",
|
|
21
24
|
# agent: "yoda-writer", seconds: 42, round: "01", pid: 91_234)
|
|
22
|
-
|
|
23
|
-
# #=> "[16:22:14 | 003.00 | ⭐️ Planned | yoda-writer | 01 | 91234 | 42s] editing spec.md"
|
|
25
|
+
# #=> "[16:22:14 | 003.00 | ⭐️ Planned | yoda-writer | 01 | 91234 | 42s] editing spec.md"
|
|
24
26
|
module ProgressLog
|
|
25
27
|
# Wall clock only. A log line answers "when", and the date is the file's.
|
|
26
28
|
TIME_FORMAT = "%H:%M:%S"
|
|
@@ -31,12 +33,17 @@ module OpenLanes
|
|
|
31
33
|
# "003.00".
|
|
32
34
|
PLAN_WIDTH = 6
|
|
33
35
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
# Cells given to the emoji, and the gap after it. Many terminal fonts draw
|
|
37
|
+
# a colour emoji wider than its two cells, and with one space it overlapped
|
|
38
|
+
# the label's first letter ("🔴Changes").
|
|
39
|
+
EMOJI_WIDTH = 2
|
|
40
|
+
EMOJI_GAP = " "
|
|
41
|
+
|
|
42
|
+
# Derived from the widest state as {.status_cell} renders it, so a state
|
|
43
|
+
# added to {STATUSES} with a longer label cannot silently knock the columns
|
|
44
|
+
# after it out of line. Untrimmed: cut to fit, "Changes Requested" and
|
|
45
|
+
# "Ready for Planning" read as "Changes Reque" and "Ready for Pla".
|
|
46
|
+
STATUS_WIDTH = Plans::STATUSES.map { |status| EMOJI_WIDTH + EMOJI_GAP.length + UI.display_width(status.label) }.max
|
|
40
47
|
|
|
41
48
|
# The longest specialist name in `agents/` is `palpatine-planner` at 17,
|
|
42
49
|
# and the names are hyphenated words rather than a bounded vocabulary, so
|
|
@@ -54,6 +61,10 @@ module OpenLanes
|
|
|
54
61
|
# is still waiting on.
|
|
55
62
|
SECONDS_WIDTH = 6
|
|
56
63
|
|
|
64
|
+
# The keywords {.render} takes besides the message. {UI.log} hands its
|
|
65
|
+
# taps more than these, and the file only these.
|
|
66
|
+
FIELDS = %i[plan status agent seconds round pid at].freeze
|
|
67
|
+
|
|
57
68
|
# Between columns, inside the brackets.
|
|
58
69
|
SEPARATOR = " | "
|
|
59
70
|
|
|
@@ -67,7 +78,25 @@ module OpenLanes
|
|
|
67
78
|
# @see COLUMN_WIDTHS
|
|
68
79
|
LINE_WIDTH = COLUMN_WIDTHS.sum + (SEPARATOR.length * (COLUMN_WIDTHS.size - 1)) + 2
|
|
69
80
|
|
|
81
|
+
# Hex digits of the checkout's path in a default log's name.
|
|
82
|
+
CHECKOUT_DIGITS = 6
|
|
83
|
+
|
|
70
84
|
class << self
|
|
85
|
+
# Where a run's log goes when `--log` names nowhere: the temp
|
|
86
|
+
# directory, under the checkout's directory name and a short digest
|
|
87
|
+
# of its full path. The name alone collided: `~/a/minesweeper` and
|
|
88
|
+
# `~/b/minesweeper` both appended to `openlanes-minesweeper.log`, and
|
|
89
|
+
# two runs' lines read as one.
|
|
90
|
+
#
|
|
91
|
+
# @param root [String] the checkout
|
|
92
|
+
# @param kind [String, nil] which process logs, e.g. "web"
|
|
93
|
+
# @return [String] e.g. "/tmp/openlanes-minesweeper-3f9a1c.log"
|
|
94
|
+
def default_path(root, kind: nil)
|
|
95
|
+
root = File.expand_path(root)
|
|
96
|
+
digest = Digest::SHA256.hexdigest(root)[0, CHECKOUT_DIGITS]
|
|
97
|
+
File.join(Dir.tmpdir, "#{["openlanes", kind, File.basename(root), digest].compact.join("-")}.log")
|
|
98
|
+
end
|
|
99
|
+
|
|
71
100
|
# Render one log line: bracketed fixed-width columns, then the message.
|
|
72
101
|
#
|
|
73
102
|
# No colour. The line goes to a file, where an escape sequence is neither
|
|
@@ -87,7 +116,7 @@ module OpenLanes
|
|
|
87
116
|
columns = [
|
|
88
117
|
left(at.strftime(TIME_FORMAT), TIME_WIDTH),
|
|
89
118
|
left(plan, PLAN_WIDTH),
|
|
90
|
-
left(status, STATUS_WIDTH),
|
|
119
|
+
left(status_cell(status), STATUS_WIDTH),
|
|
91
120
|
left(agent, AGENT_WIDTH),
|
|
92
121
|
right(round, ROUND_WIDTH),
|
|
93
122
|
right(pid, PID_WIDTH),
|
|
@@ -99,6 +128,18 @@ module OpenLanes
|
|
|
99
128
|
text.empty? ? prefix : "#{prefix} #{text}"
|
|
100
129
|
end
|
|
101
130
|
|
|
131
|
+
# The emoji fitted to {EMOJI_WIDTH} and followed by {EMOJI_GAP}, then the
|
|
132
|
+
# label. A value with no label (or none at all) passes through.
|
|
133
|
+
#
|
|
134
|
+
# @param status [String, nil] emoji and label, e.g. "⭐️ Planned"
|
|
135
|
+
# @return [String, nil]
|
|
136
|
+
def status_cell(status)
|
|
137
|
+
emoji, label = status.to_s.split(" ", 2)
|
|
138
|
+
return status if label.nil?
|
|
139
|
+
|
|
140
|
+
"#{UI.fit(emoji, EMOJI_WIDTH)}#{EMOJI_GAP}#{label}"
|
|
141
|
+
end
|
|
142
|
+
|
|
102
143
|
# @param seconds [Numeric, nil]
|
|
103
144
|
# @return [String] whole seconds with a trailing "s", or blank for nil
|
|
104
145
|
def duration(seconds) = seconds.nil? ? "" : "#{seconds.round}s"
|
|
@@ -65,6 +65,7 @@ module OpenLanes
|
|
|
65
65
|
Repository root: #{root}
|
|
66
66
|
|
|
67
67
|
#{"The folder's name is not currently justified: #{subject.violation}" if subject.violation}
|
|
68
|
+
#{snapshot_note}
|
|
68
69
|
#{operator_instructions}#{requirements_section}#{completion_section}#{suggestions_section}#{ledger_section(agent, subject, round:, successor:)}#{budget_section}#{time_budget_section(agent)}#{control_section(control, agent, subject)}#{questions_section}#{mailbox_section(agent, subject, partners)}
|
|
69
70
|
## Boundary — enforced, not requested
|
|
70
71
|
|
|
@@ -122,6 +123,18 @@ module OpenLanes
|
|
|
122
123
|
|
|
123
124
|
attr_reader :agent, :subject, :root, :profile, :round, :successor, :partners, :control
|
|
124
125
|
|
|
126
|
+
# In a worktree the repository root holds its own `.plans`, a snapshot
|
|
127
|
+
# the publisher refreshes for the pull request. An agent that edited or
|
|
128
|
+
# signed it would be writing to a copy the next publish throws away.
|
|
129
|
+
#
|
|
130
|
+
# @return [String, nil]
|
|
131
|
+
def snapshot_note
|
|
132
|
+
return nil if File.expand_path(subject.feature.path).start_with?("#{File.expand_path(root)}/")
|
|
133
|
+
|
|
134
|
+
"The plan lives only at the plan folder above. Any `.plans` under the repository root is a\n" \
|
|
135
|
+
"snapshot the harness overwrites when it publishes: never read the plan from it or write to it."
|
|
136
|
+
end
|
|
137
|
+
|
|
125
138
|
# Only a CLI that actually refuses the commands may be described as
|
|
126
139
|
# refusing them.
|
|
127
140
|
#
|
|
@@ -61,6 +61,30 @@ module OpenLanes
|
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# How many words of an idea name its plan.
|
|
65
|
+
TOPIC_WORDS = 8
|
|
66
|
+
|
|
67
|
+
# The words a free-form idea's plan is named after: a frontmatter
|
|
68
|
+
# `title:`, else its first line, a heading's `#` marks dropped.
|
|
69
|
+
#
|
|
70
|
+
# Taking the first words of the raw text named the minesweeper trial's
|
|
71
|
+
# plan `minesweeper-minesweeper-is-played-on-a-grid`: the idea opened
|
|
72
|
+
# with `# Minesweeper` and then a paragraph starting with the same
|
|
73
|
+
# word, and the heading's `#` was dropped while its word was kept.
|
|
74
|
+
#
|
|
75
|
+
# @param idea [String] Markdown, as the page's idea dialog sends it
|
|
76
|
+
# @return [Array<String>] at most {TOPIC_WORDS} words, possibly none
|
|
77
|
+
def self.topic(idea)
|
|
78
|
+
meta, body = begin
|
|
79
|
+
Frontmatter.split(idea.to_s)
|
|
80
|
+
rescue Psych::Exception
|
|
81
|
+
[{}, idea.to_s.sub(Frontmatter::PATTERN, '\\2')]
|
|
82
|
+
end
|
|
83
|
+
title = meta["title"].to_s.strip
|
|
84
|
+
line = title.empty? ? body.lines.map(&:strip).find { !it.empty? }.to_s : title
|
|
85
|
+
line.sub(/\A\#{1,6}\s+/, "").split.first(TOPIC_WORDS)
|
|
86
|
+
end
|
|
87
|
+
|
|
64
88
|
# Turn free text into the kebab tail of a folder name.
|
|
65
89
|
#
|
|
66
90
|
# @param words [Array<String>, String]
|