letsdo 0.1.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.
@@ -1,90 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'session/keys'
4
+ require_relative 'session/view'
5
+
3
6
  module Letsdo
4
7
  module Tui
5
- # The TUI controller: owns the terminal lifecycle, the background input
6
- # thread and the repaint loop around an orchestrator run.
7
- #
8
- # Threading model (the orchestrator loop stays on the calling thread,
9
- # exactly as in plain mode — Letsdo::PiRunner's signal design keeps
10
- # working unchanged):
11
- # main thread — the injected block (Letsdo::AgentLoop#run with its
12
- # own signal traps): provider queries, agent runs,
13
- # streamer writes into the log, metrics events;
14
- # input thread — the SOLE repainter: polls keys (tty-reader),
15
- # drains the log snapshot, tracks the metrics
16
- # snapshot and repaints on keys, data, 1s timer
17
- # ticks and SIGWINCH. Never touches the loop.
18
- #
19
- # Interactive actions (footer lists them):
20
- # ↑ / ↓ / PgUp / PgDn / Home / End — scroll; the view auto-follows
21
- # the newest line while at the bottom (tail -f semantics);
22
- # p — pause/resume: display freeze (PAUSED in the header, the log
23
- # keeps buffering, the view stays); keys still repaint;
24
- # r — immediate backlog re-query for the 'left' metric;
25
- # q / Ctrl-C — quit: exactly the signal-stop unwinding — Letsdo::Stopped
26
- # raised into the main thread terminates the pi child (PiRunner
27
- # rescue), stops the loop (AgentLoop rescue) and restores the
28
- # terminal from the ensure block; exit code 0.
29
- #
30
- # Signals: SIGWINCH sets a flag → the input thread re-queries the size
31
- # and repaints (no corruption). SIGINT/SIGTERM keep working through the
32
- # loop's own traps (main thread).
8
+ # The TUI controller: terminal lifecycle, background input thread, and
9
+ # the repaint loop around an orchestrator run.
33
10
  class Session
34
- # 1s session-timer repaint interval.
11
+ include SessionKeys
12
+ include SessionView
13
+
35
14
  REPAINT_INTERVAL = 1.0
36
- # Poll sleep when the display is frozen or nothing happened (avoids a
37
- # busy loop while still repainting at the timer interval).
38
15
  IDLE_SLEEP = 0.01
39
- # How long teardown waits for the input thread to finish its poll.
40
16
  INPUT_JOIN_TIMEOUT = 2.0
41
17
 
42
- # @param name [String] agent name (header identity)
43
- # @param handle [String] assignee handle (header identity)
44
- # @param log [LogBuffer] the combined log the streamer writes into
45
- # @param metrics [Metrics] the header metrics facade
46
- # @param terminal [Terminal] the terminal wrapper
47
- # @param input [Input] the key reader
48
- # @param refresh [Proc, nil] callable → open task count (Integer) or
49
- # nil (backlog unreadable); 'r' uses it for an immediate
50
- # re-query
51
- # @param wait_seconds [Numeric] retry interval for the waiting state
52
- # @param clock [Proc] monotonic clock for tick timing (injectable)
53
- def initialize(name:, handle:, log:, metrics:, terminal:, input:,
54
- refresh: nil, wait_seconds: 10.0, clock: nil)
55
- @name = name
56
- @handle = handle
57
- @log = log
58
- @metrics = metrics
59
- @terminal = terminal
60
- @input = input
61
- @refresh = refresh
62
- @wait_seconds = wait_seconds
63
- @clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
64
- @offset = 0
65
- @follow = true
66
- @paused = false
67
- @stop = false
68
- @winch = false
69
- @body_height = 1
70
- @seen_version = 0
71
- @input_thread = nil
18
+ def initialize(**opts)
19
+ assign_identity(opts)
20
+ assign_io(opts)
21
+ assign_controls(opts)
22
+ reset_view_state
72
23
  end
73
24
 
74
- # Runs the TUI around the given orchestrator work.
75
- #
76
- # The block runs on the calling thread. On quit (q/Ctrl-C) or a stop
77
- # signal Letsdo::Stopped unwinds it; the terminal is restored from
78
- # the ensure block on every exit path.
79
- #
80
- # @yield the orchestrator run (e.g. Letsdo::AgentLoop#run)
81
- # @return [Integer] the block's result, or 0 when stopped
82
25
  def run(&work)
83
26
  install_winch_handler
84
27
  @terminal.enter
85
- start_input_thread
86
- result = work.call
87
- result
28
+ run_work(&work)
88
29
  rescue Letsdo::Stopped
89
30
  0
90
31
  ensure
@@ -93,124 +34,50 @@ module Letsdo
93
34
  restore_winch_handler
94
35
  end
95
36
 
96
- private
97
-
98
- # The input thread body: the only place that writes to the terminal.
99
- def input_loop
37
+ def run_work(&work)
100
38
  with_raw_input do
101
- repaint
102
- @last_repaint = @clock.call
103
- loop do
104
- break if @stop
105
-
106
- key = @input.next_key
107
- handled = key ? handle_key(key) : false
108
- winch = take_winch
109
- handled = true if winch
110
- now = @clock.call
111
- tick = now - @last_repaint >= REPAINT_INTERVAL
112
- new_data = @log.version != @seen_version
113
- if handled || (!@paused && (tick || new_data))
114
- repaint
115
- @last_repaint = @clock.call
116
- else
117
- sleep(IDLE_SLEEP)
118
- end
119
- end
39
+ start_input_thread
40
+ work.call
120
41
  end
121
- rescue StandardError
122
- # The input thread must never die loudly into the middle of the
123
- # screen, and a quiet exit still leaves the loop running (teardown
124
- # joins it). Terminal state is restored by the ensure blocks.
125
- nil
126
42
  end
127
43
 
128
- def handle_key(key)
129
- case key
130
- when :up, :page_up
131
- @offset -= key == :page_up ? page_step : 1
132
- @follow = false
133
- true
134
- when :down, :page_down
135
- @offset += key == :page_down ? page_step : 1
136
- true # repaint decides re-sticking at the bottom
137
- when :home
138
- @offset = 0
139
- @follow = false
140
- true
141
- when :end
142
- @follow = true
143
- true
144
- when :p
145
- @paused = !@paused
146
- true
147
- when :r
148
- refresh
149
- true
150
- when :q, :ctrl_c
151
- quit
152
- false # leaving the session; no repaint needed
153
- else
154
- false
155
- end
156
- end
157
-
158
- # Immediate backlog re-query: the provider is re-run on this thread
159
- # (a subprocess call, thread-safe) and the count reaches the header
160
- # snapshot.
161
- def refresh
162
- return unless @refresh
163
-
164
- @metrics.provider_result(@refresh.call)
165
- end
166
-
167
- # Quit = the same unwinding as a stop signal: Letsdo::Stopped raised
168
- # into the main thread (inside the orchestrator work) terminates the
169
- # pi child and stops the loop; the ensure block restores the
170
- # terminal. No-op after teardown already started.
171
- def quit
172
- return if @stop
44
+ private
173
45
 
174
- Thread.main.raise(Letsdo::Stopped)
46
+ def assign_identity(opts)
47
+ @name = opts.fetch(:name)
48
+ @handle = opts.fetch(:handle)
49
+ @log = opts.fetch(:log)
50
+ @metrics = opts.fetch(:metrics)
175
51
  end
176
52
 
177
- # Repaints the whole frame from the current state.
178
- def repaint
179
- lines, version = @log.lines
180
- @seen_version = version
181
- height, width = @terminal.size
182
- @body_height = Renderer.body_height_for(height)
183
- max_offset = Renderer.max_offset(lines, @body_height)
184
- if @paused
185
- @offset = [[@offset, max_offset].min, 0].max
186
- elsif @follow
187
- @offset = max_offset
188
- else
189
- @offset = [[@offset, max_offset].min, 0].max
190
- @follow = true if @offset == max_offset
191
- end
192
- frame = Renderer.render(
193
- metrics: @metrics.snapshot, lines: lines, width: width, height: height,
194
- offset: @offset, follow: @follow, paused: @paused,
195
- wait_seconds: @wait_seconds
196
- )
197
- @terminal.render(frame)
53
+ def assign_io(opts)
54
+ @terminal = opts.fetch(:terminal)
55
+ @input = opts.fetch(:input)
56
+ @refresh = opts[:refresh]
57
+ @wait_seconds = opts.fetch(:wait_seconds, 10.0)
58
+ @clock = opts[:clock] || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
198
59
  end
199
60
 
200
- # Page height for PgUp/PgDn.
201
- def page_step
202
- @body_height.positive? ? @body_height : 1
61
+ def assign_controls(opts)
62
+ @pause_gate = opts[:pause_gate]
63
+ @runner = opts[:runner]
203
64
  end
204
65
 
205
- def take_winch
206
- flag = @winch
66
+ def reset_view_state
67
+ @offset = 0
68
+ @follow = true
69
+ @paused = false
70
+ @stop = false
207
71
  @winch = false
208
- flag
72
+ @body_height = 1
73
+ @seen_version = 0
74
+ @input_thread = nil
209
75
  end
210
76
 
211
- # Raw keyboard mode for the whole input session (the tty-reader
212
- # per-read raw wraps would leave the terminal echoing between polls);
213
- # no-op for non-tty inputs (tests use a pipe).
77
+ # Enters raw mode on the keyboard for the duration of the block. Held
78
+ # by the main thread (not the killable input thread) so the terminal
79
+ # state is restored on every quit path — a killed input thread would
80
+ # leave the shared tty in raw mode and break the calling shell (TASK-83).
214
81
  def with_raw_input(&block)
215
82
  if @input.stdin.tty? && @input.stdin.respond_to?(:raw)
216
83
  @input.stdin.raw(&block)
@@ -220,13 +87,13 @@ module Letsdo
220
87
  end
221
88
 
222
89
  def install_winch_handler
223
- Signal.trap("SIGWINCH") { @winch = true }
90
+ Signal.trap('SIGWINCH') { @winch = true }
224
91
  rescue ArgumentError
225
- nil # no SIGWINCH on this platform — no resize repaints
92
+ nil
226
93
  end
227
94
 
228
95
  def restore_winch_handler
229
- Signal.trap("SIGWINCH", "DEFAULT")
96
+ Signal.trap('SIGWINCH', 'DEFAULT')
230
97
  rescue ArgumentError
231
98
  nil
232
99
  end
@@ -250,4 +117,4 @@ module Letsdo
250
117
  end
251
118
  end
252
119
  end
253
- end
120
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tty-cursor"
4
-
5
3
  module Letsdo
6
4
  module Tui
7
5
  # Thin wrapper over the terminal: alternate screen, cursor handling and
@@ -24,28 +22,40 @@ module Letsdo
24
22
  # @param size_provider [Proc] callable → [height, width]; defaults to
25
23
  # TTY::Screen.size; injected in tests for deterministic size
26
24
  def initialize(stream:, size_provider: nil)
27
- require "tty-screen" unless defined?(TTY::Screen)
25
+ require 'tty-screen' unless defined?(TTY::Screen)
28
26
 
29
27
  @stream = stream
30
28
  @size_provider = size_provider ||
31
- -> { size = TTY::Screen.size; [size[0] || 24, size[1] || 80] }
29
+ lambda {
30
+ size = TTY::Screen.size
31
+ [size[0] || 24, size[1] || 80]
32
+ }
32
33
  end
33
34
 
34
35
  # Enters the alternate screen and hides the cursor.
35
36
  def enter
36
- write(ENTER_ALT_SCREEN + TTY::Cursor.hide)
37
+ write(ENTER_ALT_SCREEN + cursor.hide)
37
38
  end
38
39
 
39
40
  # Shows the cursor and leaves the alternate screen.
40
41
  def leave
41
- write(TTY::Cursor.show + LEAVE_ALT_SCREEN)
42
+ write(cursor.show + LEAVE_ALT_SCREEN)
42
43
  end
43
44
 
44
45
  # Repaints the whole frame: cursor to the top left, then the frame.
45
46
  #
47
+ # Row separators are written as CRLF (\r\n), never bare LF. The TUI
48
+ # input thread holds stdin in io-console raw mode for the whole
49
+ # session, and raw mode clears OPOST on the shared tty — without it a
50
+ # bare \n no longer implies a carriage return, so every row after the
51
+ # first would start at the previous row's end column and the frame
52
+ # would wrap/scroll into the "blank activity pane" scramble (TASK-82).
53
+ # Explicit CRLF renders correctly both in raw mode and on terminals
54
+ # whose ONLCR already expands \n (a doubled CR is harmless).
55
+ #
46
56
  # @param frame [String] the rendered screen (see Letsdo::Tui::Renderer)
47
57
  def render(frame)
48
- write(HOME + frame)
58
+ write(HOME + frame.gsub("\n", "\r\n"))
49
59
  end
50
60
 
51
61
  # The current terminal size.
@@ -58,10 +68,18 @@ module Letsdo
58
68
 
59
69
  private
60
70
 
71
+ # Loaded lazily (like tty-screen/tty-reader) so requiring letsdo on a
72
+ # clean Ruby loads no tty-* gems — only the interactive TTY path needs
73
+ # them (TASK-78).
74
+ def cursor
75
+ require 'tty-cursor' unless defined?(TTY::Cursor)
76
+ TTY::Cursor
77
+ end
78
+
61
79
  def write(text)
62
80
  @stream.write(text)
63
81
  @stream.flush
64
82
  end
65
83
  end
66
84
  end
67
- end
85
+ end
data/lib/letsdo/tui.rb CHANGED
@@ -16,9 +16,9 @@
16
16
  # Letsdo::Tui::Input - tty-reader key decoding (injectable)
17
17
  # Letsdo::Tui::Session - controller: terminal lifecycle + input thread
18
18
 
19
- require_relative "tui/log_buffer"
20
- require_relative "tui/metrics"
21
- require_relative "tui/renderer"
22
- require_relative "tui/terminal"
23
- require_relative "tui/input"
24
- require_relative "tui/session"
19
+ require_relative 'tui/log_buffer'
20
+ require_relative 'tui/metrics'
21
+ require_relative 'tui/renderer'
22
+ require_relative 'tui/terminal'
23
+ require_relative 'tui/input'
24
+ require_relative 'tui/session'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Letsdo
4
- VERSION = "0.1.0"
5
- end
4
+ VERSION = '0.3.0'
5
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiddle/import'
4
+
5
+ module Letsdo
6
+ # OS file-change watcher for the backlog folder.
7
+ #
8
+ # On Linux it subscribes to inotify (via Fiddle, no external gem) and wakes
9
+ # as soon as a backlog entry changes; everywhere else it degrades to the
10
+ # same periodic polling a plain sleeper would do. Either way the wait is
11
+ # interruptible — a signal trap raising Letsdo::Stopped breaks the
12
+ # underlying IO.select — and an internal self-pipe lets #wake interrupt a
13
+ # blocked #wait from another thread.
14
+ class Watcher
15
+ # inotify event masks we subscribe to (Linux <sys/inotify.h>).
16
+ IN_ACCESS = 0x00000001
17
+ IN_MODIFY = 0x00000002
18
+ IN_ATTRIB = 0x00000004
19
+ IN_CLOSE_WRITE = 0x00000008
20
+ IN_CLOSE_NOWRITE = 0x00000010
21
+ IN_OPEN = 0x00000020
22
+ IN_MOVED_FROM = 0x00000040
23
+ IN_MOVED_TO = 0x00000080
24
+ IN_CREATE = 0x00000100
25
+ IN_DELETE = 0x00000200
26
+ IN_DELETE_SELF = 0x00000400
27
+ IN_MOVE_SELF = 0x00000800
28
+
29
+ RELEVANT_MASK = IN_CREATE | IN_MODIFY | IN_CLOSE_WRITE | IN_DELETE |
30
+ IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE_SELF | IN_MOVE_SELF
31
+
32
+ EXCLUDED_DIR = '.locks'
33
+
34
+ # @param path [String] the watched folder (its direct entries)
35
+ # @param poll_seconds [Float] fallback poll interval / select timeout
36
+ def initialize(path:, poll_seconds: 10.0)
37
+ @poll_seconds = poll_seconds
38
+ @wake_r, @wake_w = IO.pipe
39
+ @backend = build_backend(path)
40
+ end
41
+
42
+ # Blocks until a relevant backlog change, an explicit #wake, or the poll
43
+ # timeout. Irrelevant events (e.g. .locks churn) keep the wait going for
44
+ # the remainder of the timeout.
45
+ #
46
+ # @return [Symbol] :change, :wake, or :timeout
47
+ def wait(timeout = @poll_seconds)
48
+ deadline = monotonic + timeout
49
+ loop do
50
+ result = wait_once(deadline)
51
+ return result if result
52
+ end
53
+ end
54
+
55
+ # Interrupts a blocked #wait from another thread.
56
+ def wake
57
+ @wake_w.write_nonblock('.')
58
+ rescue IO::WaitWritable, Errno::EPIPE
59
+ nil
60
+ end
61
+
62
+ def close
63
+ @backend&.close
64
+ close_io(@wake_w)
65
+ close_io(@wake_r)
66
+ end
67
+
68
+ private
69
+
70
+ def wait_once(deadline)
71
+ remaining = deadline - monotonic
72
+ return :timeout if remaining <= 0
73
+
74
+ ready = IO.select(read_fds, nil, nil, remaining)
75
+ return :timeout if ready.nil?
76
+ return :wake if wake_ready?(ready)
77
+
78
+ @backend.relevant_event? ? :change : nil
79
+ end
80
+
81
+ def read_fds
82
+ fds = [@wake_r]
83
+ fds << @backend.io if @backend
84
+ fds
85
+ end
86
+
87
+ def wake_ready?(ready)
88
+ return false unless ready.first.include?(@wake_r)
89
+
90
+ drain_wake
91
+ true
92
+ end
93
+
94
+ def drain_wake
95
+ @wake_r.read_nonblock(4096)
96
+ rescue IO::WaitReadable, EOFError
97
+ nil
98
+ end
99
+
100
+ def build_backend(path)
101
+ InotifyBackend.new(path, RELEVANT_MASK)
102
+ rescue InotifyBackend::Unavailable, Fiddle::DLError
103
+ nil
104
+ end
105
+
106
+ def monotonic
107
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
108
+ end
109
+
110
+ def close_io(io)
111
+ io.close unless io.nil? || io.closed?
112
+ rescue IOError
113
+ nil
114
+ end
115
+
116
+ # Linux inotify backend. Construction raises InotifyBackend::Unavailable
117
+ # when inotify is missing or the watch cannot be set up, so the watcher
118
+ # falls back to polling through its self-pipe timeout.
119
+ class InotifyBackend
120
+ # libc inotify bindings via Fiddle (no external gem).
121
+ module Lib
122
+ extend Fiddle::Importer
123
+ dlload Fiddle.dlopen(nil)
124
+ extern 'int inotify_init1(int)'
125
+ extern 'int inotify_add_watch(int, const char*, uint32_t)'
126
+ end
127
+
128
+ # Signals that inotify is unavailable and the watcher should fall back.
129
+ class Unavailable < StandardError; end
130
+
131
+ IN_CLOEXEC = 0x00080000
132
+ EVENT_HEADER_SIZE = 16
133
+
134
+ attr_reader :io
135
+
136
+ def initialize(path, mask)
137
+ fd = Lib.inotify_init1(IN_CLOEXEC)
138
+ guard(fd)
139
+ @io = IO.new(fd, 'r', autoclose: true)
140
+ guard(Lib.inotify_add_watch(fd, path, mask))
141
+ rescue Unavailable, Fiddle::DLError
142
+ @io&.close
143
+ raise Unavailable
144
+ end
145
+
146
+ # Drains pending events and reports whether any relevant (non-.locks)
147
+ # entry changed.
148
+ def relevant_event?
149
+ buffer = @io.read_nonblock(8192)
150
+ parse_relevant(buffer)
151
+ rescue IO::WaitReadable, EOFError
152
+ false
153
+ end
154
+
155
+ def close
156
+ @io&.close
157
+ rescue IOError
158
+ nil
159
+ end
160
+
161
+ private
162
+
163
+ def guard(value)
164
+ raise Unavailable if value.negative?
165
+ end
166
+
167
+ def parse_relevant(buffer)
168
+ offset = 0
169
+ relevant = false
170
+ while offset + EVENT_HEADER_SIZE <= buffer.bytesize
171
+ _wd, mask, _cookie, len = buffer.byteslice(offset, EVENT_HEADER_SIZE).unpack('L4')
172
+ name = buffer.byteslice(offset + EVENT_HEADER_SIZE, len).to_s.sub(/\0.*/m, '')
173
+ relevant ||= relevant_mask?(mask) && !excluded?(name)
174
+ offset += EVENT_HEADER_SIZE + len
175
+ end
176
+ relevant
177
+ end
178
+
179
+ def relevant_mask?(mask)
180
+ (mask & Watcher::RELEVANT_MASK) != 0
181
+ end
182
+
183
+ def excluded?(name)
184
+ name == Watcher::EXCLUDED_DIR || name.start_with?("#{Watcher::EXCLUDED_DIR}/")
185
+ end
186
+ end
187
+ end
188
+ end
data/lib/letsdo.rb CHANGED
@@ -3,28 +3,35 @@
3
3
  # The Letsdo package — a local agent worker for Backlog.md/markdown tasks.
4
4
  #
5
5
  # OOP structure:
6
- # Letsdo::Errors - error hierarchy (UnknownAgentError and others)
6
+ # Letsdo::Errors - error hierarchy (Letsdo::Error and others)
7
7
  # Letsdo::PromptStore - access to agents/*.md prompts
8
+ # Letsdo::DefaultPrompt - the built-in default prompt (fallback run, --init template)
8
9
  # Letsdo::OutputStreamer - where and how agent text and service lines are printed
9
10
  # Letsdo::PiRunner - running pi --mode json and parsing the event stream
10
- # Letsdo::Agent - a single agent run: prompt from agents/ + pi
11
+ # Letsdo::Agent - a single agent run: prompt from agents/ or default + pi
12
+ # Letsdo::Capture - interruption-safe child stdout/stderr capture
11
13
  # Letsdo::BacklogTasks - open tasks from the backlog CLI (the task provider)
12
14
  # Letsdo::Loop - generic orchestrator: tasks → runs → waiting
13
15
  # Letsdo::AgentLoop - letsdo wiring: provider + agent + signals + messages
16
+ # Letsdo::Control - agent-control primitives (PauseGate)
14
17
  # Letsdo::Tui - the interactive TUI (LogBuffer, Metrics,
15
18
  # Renderer, Terminal, Input, Session)
16
19
  # Letsdo::CLI - command-line arguments, usage, exit code
17
20
  #
18
21
  # Entry point — bin/letsdo (a thin wrapper over Letsdo::CLI).
19
22
 
20
- require_relative "letsdo/version"
21
- require_relative "letsdo/errors"
22
- require_relative "letsdo/prompt_store"
23
- require_relative "letsdo/output_streamer"
24
- require_relative "letsdo/pi_runner"
25
- require_relative "letsdo/agent"
26
- require_relative "letsdo/backlog_tasks"
27
- require_relative "letsdo/loop"
28
- require_relative "letsdo/agent_loop"
29
- require_relative "letsdo/tui"
30
- require_relative "letsdo/cli"
23
+ require_relative 'letsdo/version'
24
+ require_relative 'letsdo/errors'
25
+ require_relative 'letsdo/prompt_store'
26
+ require_relative 'letsdo/default_prompt'
27
+ require_relative 'letsdo/output_streamer'
28
+ require_relative 'letsdo/pi_runner'
29
+ require_relative 'letsdo/agent'
30
+ require_relative 'letsdo/capture'
31
+ require_relative 'letsdo/backlog_tasks'
32
+ require_relative 'letsdo/loop'
33
+ require_relative 'letsdo/watcher'
34
+ require_relative 'letsdo/agent_loop'
35
+ require_relative 'letsdo/control'
36
+ require_relative 'letsdo/tui'
37
+ require_relative 'letsdo/cli'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: letsdo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergei O. Udalov
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '2.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 1.77.0
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 1.77.0
68
82
  description: 'letsdo — a local agent worker for Backlog.md/markdown tasks. OOP structure:
69
83
  Letsdo::PromptStore (agents/), Letsdo::OutputStreamer and Letsdo::PiRunner (pi --mode
70
84
  json), Letsdo::Agent (one run), Letsdo::Loop (orchestrator loop), Letsdo::CLI. Minitest
@@ -82,12 +96,21 @@ files:
82
96
  - lib/letsdo.rb
83
97
  - lib/letsdo/agent.rb
84
98
  - lib/letsdo/agent_loop.rb
99
+ - lib/letsdo/agent_loop/tasks.rb
85
100
  - lib/letsdo/backlog_tasks.rb
101
+ - lib/letsdo/capture.rb
86
102
  - lib/letsdo/cli.rb
103
+ - lib/letsdo/cli/init.rb
104
+ - lib/letsdo/cli/launch.rb
105
+ - lib/letsdo/control.rb
106
+ - lib/letsdo/default_prompt.rb
87
107
  - lib/letsdo/errors.rb
88
108
  - lib/letsdo/loop.rb
89
109
  - lib/letsdo/output_streamer.rb
110
+ - lib/letsdo/output_streamer/arg_summary.rb
90
111
  - lib/letsdo/pi_runner.rb
112
+ - lib/letsdo/pi_runner/events.rb
113
+ - lib/letsdo/pi_runner/process.rb
91
114
  - lib/letsdo/prompt_store.rb
92
115
  - lib/letsdo/tui.rb
93
116
  - lib/letsdo/tui/input.rb
@@ -95,12 +118,20 @@ files:
95
118
  - lib/letsdo/tui/metrics.rb
96
119
  - lib/letsdo/tui/renderer.rb
97
120
  - lib/letsdo/tui/session.rb
121
+ - lib/letsdo/tui/session/keys.rb
122
+ - lib/letsdo/tui/session/view.rb
98
123
  - lib/letsdo/tui/terminal.rb
99
124
  - lib/letsdo/version.rb
125
+ - lib/letsdo/watcher.rb
126
+ homepage: https://github.com/sergio-fry/letsdo
100
127
  licenses:
101
128
  - MIT
102
129
  metadata:
130
+ homepage_uri: https://github.com/sergio-fry/letsdo
131
+ source_code_uri: https://github.com/sergio-fry/letsdo
132
+ changelog_uri: https://github.com/sergio-fry/letsdo/blob/main/CHANGELOG.md
103
133
  rubygems_mfa_required: 'true'
134
+ allowed_push_host: https://rubygems.org
104
135
  rdoc_options: []
105
136
  require_paths:
106
137
  - lib