lux-hammer 0.3.21 → 0.3.23

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.
data/recipes/llm.rb CHANGED
@@ -7,7 +7,9 @@ desc <<~TXT
7
7
 
8
8
  Namespaces:
9
9
  memory persistent memory store (backs the Claude Code memory plugin)
10
+ plan apply a /plan bundle - sha1 checked, drift aware
10
11
  prompt token-prefix prompt expander (UserPromptSubmit hook + CLI)
12
+ todo per-project task queue for agents (./LLM_TODO.local.md)
11
13
 
12
14
  Commands:
13
15
  usage subscription limits and per-model token usage
@@ -23,6 +25,7 @@ require 'set'
23
25
  # checkout still load matching lib/, not an older installed gem copy).
24
26
  _llm_root = File.dirname(File.realpath(__FILE__))
25
27
  require File.join(_llm_root, 'lib/llm/usage')
28
+ require File.join(_llm_root, 'lib/llm/plan')
26
29
 
27
30
  STORE ||= ENV['CLAUDE_MEMORY_STORE'] || File.expand_path('~/dev/ai/memory')
28
31
  VALID_TYPES ||= %w[user feedback project reference].freeze
@@ -40,10 +43,10 @@ task :usage do
40
43
  `~/.cache/llm/claude-limits.json`, falling back to the OAuth usage API. `month` shows
41
44
  Claude extra-usage credits (API only, and only when you've enabled them).
42
45
  D
43
- example 'llm usage'
44
- example 'llm usage month'
45
- example 'llm usage --json'
46
- example 'llm usage --provider grok'
46
+ example 'usage'
47
+ example 'usage month'
48
+ example 'usage --json'
49
+ example 'usage --provider grok'
47
50
 
48
51
  # :period is declared first on purpose — the parser fills un-set scalar opts
49
52
  # from positionals in declaration order, so `llm usage month` must reach
@@ -98,6 +101,17 @@ task :wrap do
98
101
  inserts for you (history recall, autocomplete, menu picks) will not show up, and
99
102
  nothing is captured while the program has echo off in canonical mode (sudo, ssh).
100
103
 
104
+ A line starting with ! is taken over: nothing reaches the wrapped program, the line
105
+ runs in $SHELL here, and the bar grows to show where it ran, what ran and what it
106
+ printed. That is a shell round trip instead of a tool call, a permission check and a
107
+ model turn - and it costs no context either, since the agent is never told.
108
+
109
+ Output stays up until you type anything at all, and a bare ! clears it too. Long
110
+ output is cut to the rows there are, and a command still running after 10s is killed,
111
+ since the wrapper is stopped while one runs. `cd` is handled here so that it sticks,
112
+ and moves nothing but the wrapper. Type !! to send a literal ! to the program instead,
113
+ which is how you reach Claude Code's own bash mode; a leading space does the same.
114
+
101
115
  Keys are decoded from plain bytes, the kitty keyboard protocol and xterm
102
116
  modifyOtherKeys, since a program can ask the terminal for any of the three (Claude
103
117
  Code turns on the last two). Mouse reports, focus events, arrows and Ctrl-<key> are
@@ -108,27 +122,59 @@ task :wrap do
108
122
 
109
123
  Put `--` before the wrapped command when it has flags of its own, otherwise they are
110
124
  parsed as flags to `llm wrap`.
125
+
126
+ With no command at all, the commands in ~/.config/hammer/llm-wrap.txt are offered in
127
+ an arrow-key picker instead. That file is plain text, one command per line. A line
128
+ starting with # is dropped, and a line with no letter in it stays on screen but
129
+ cannot be picked - so blanks and ---- rules group the list without being in the way.
130
+ `--config` opens it in $EDITOR, writing a starting set the first time.
111
131
  D
112
- example 'llm wrap claude'
113
- example 'llm wrap -- claude --resume'
114
- example 'llm wrap --lines 5 -- bash -l'
132
+ example 'wrap'
133
+ example 'wrap --config'
134
+ example 'wrap claude'
135
+ example 'wrap -- claude --resume'
136
+ example 'wrap --lines 5 -- bash -l'
115
137
 
116
138
  # `positional: false` is load-bearing: the parser fills un-set scalar opts
117
139
  # from positionals in declaration order, so without it `llm wrap claude`
118
- # would hand "claude" to --lines and die on the integer cast.
140
+ # would hand "claude" to --lines and die on the integer cast. `--config` is
141
+ # boolean, which the parser skips over, so it needs no such guard.
119
142
  opt :lines, type: :integer, default: 3, positional: false,
120
143
  desc: 'how many recent prompts to pin', placeholder: 'N'
144
+ opt :config, type: :boolean, desc: 'open the command list in $EDITOR'
121
145
 
122
146
  proc do |opts|
123
- cmd = Array(opts[:args])
124
- error 'usage: llm wrap [--lines N] [--] <command> [args...]' if cmd.empty?
125
-
126
147
  # Required lazily - llm.rb also runs as a per-prompt hook (`llm prompt
127
148
  # hook`), which should not pay to load pty/io-console. `_llm_root` is a
128
149
  # local from the file scope; the block closes over it (instance_exec
129
150
  # rebinds self, not locals).
130
151
  require File.join(_llm_root, 'lib/llm/wrap')
131
152
 
153
+ if opts[:config]
154
+ editor = ENV['EDITOR'] || ENV['VISUAL']
155
+ error '$EDITOR not set' unless editor
156
+ say.green "created #{LlmWrap::Config.path}" if LlmWrap::Config.seed
157
+ exit system(editor, LlmWrap::Config.path) ? 0 : 1
158
+ end
159
+
160
+ cmd = Array(opts[:args])
161
+
162
+ # Nothing asked for: offer the list rather than a usage error. The picker
163
+ # shows each line as it was written - re-joining a split argv would quote
164
+ # it back differently from what is in the file.
165
+ if cmd.empty?
166
+ LlmWrap::Config.seed
167
+ choices = LlmWrap::Config.lines
168
+ unless choices.any? { |line| LlmWrap::Config.runnable?(line) }
169
+ error "no commands in #{LlmWrap::Config.path} - add one with: llm wrap --config"
170
+ end
171
+
172
+ idx = choose('wrap which command?', choices, skip: ->(line) { !LlmWrap::Config.runnable?(line) })
173
+ next say.gray('cancelled') unless idx
174
+
175
+ cmd = LlmWrap::Config.argv(choices[idx])
176
+ end
177
+
132
178
  # The wrapper renames itself after the program it runs, so that a pane
133
179
  # watcher can still see which agent is in there - which leaves nothing
134
180
  # behind that says how the pane was started. This is that: the invocation
@@ -179,7 +225,7 @@ namespace :memory do
179
225
 
180
226
  task :list do
181
227
  desc 'List stored memories with type and one-line description'
182
- example 'llm memory list'
228
+ example 'memory list'
183
229
 
184
230
  proc do
185
231
  files = Dir[File.join(STORE, '*.md')].sort
@@ -199,7 +245,7 @@ namespace :memory do
199
245
 
200
246
  task :read do
201
247
  desc 'Print the full content of a memory (frontmatter + body)'
202
- example 'llm memory read user-role'
248
+ example 'memory read user-role'
203
249
 
204
250
  proc do |opts|
205
251
  name = opts[:args].first
@@ -251,7 +297,7 @@ namespace :memory do
251
297
 
252
298
  task :delete do
253
299
  desc 'Delete a memory by name'
254
- example 'llm memory delete old-fact'
300
+ example 'memory delete old-fact'
255
301
 
256
302
  proc do |opts|
257
303
  name = opts[:args].first
@@ -265,7 +311,7 @@ namespace :memory do
265
311
 
266
312
  task :search do
267
313
  desc 'Search memory bodies for a query string (case-insensitive)'
268
- example 'llm memory search react'
314
+ example 'memory search react'
269
315
 
270
316
  proc do |opts|
271
317
  query = opts[:args].first
@@ -287,6 +333,287 @@ namespace :memory do
287
333
  end
288
334
  end
289
335
 
336
+ TODO_GUIDE ||= <<~TXT
337
+ Per-project task queue in ./LLM_TODO.local.md - workable by humans and any agent.
338
+
339
+ Human usage:
340
+ llm todo:add "fix login redirect" queue a task (pipe stdin for bulk / multi-line)
341
+ llm todo:list tasks by id, counts, format warnings
342
+ llm todo:pop + llm todo:done take / finish a single task by hand
343
+
344
+ Edit the file freely: only the `# todo`, `# doing` and `# done` sections are
345
+ managed. `# plan`, `# idea` and any prose stay untouched. A task starts with
346
+ `*` at the line start and runs until the next `*` or header; when finished it
347
+ moves to `# done` verbatim.
348
+
349
+ LLM usage - one of these lines is the whole prompt:
350
+ "Run `llm todo go` and follow its output." work through every task
351
+ "Run `llm todo pop`, do the task, run `llm todo done`." do a single task
352
+
353
+ Task text is printed raw on stdout. `todo go` repeats the protocol after each
354
+ task and says when the queue is empty; `todo pop` re-prints the task in
355
+ progress, so an interrupted agent resumes instead of skipping ahead.
356
+ TXT
357
+
358
+ task :todo do
359
+ desc <<~D
360
+ How the todo queue works, for humans and LLMs.
361
+
362
+ #{TODO_GUIDE}
363
+ D
364
+ example 'todo'
365
+
366
+ proc do
367
+ say TODO_GUIDE
368
+ say ''
369
+ self.class.print_help 'todo:'
370
+ end
371
+ end
372
+
373
+ namespace :todo do
374
+ # Same story as :memory - helpers must live inside the namespace block.
375
+ private
376
+
377
+ TODO_FILE ||= 'LLM_TODO.local.md'
378
+ TODO_SECTIONS ||= { todo: 'todo', doing: 'doing', done: 'done' }.freeze
379
+
380
+ def todo_path
381
+ File.join(Dir.pwd, TODO_FILE)
382
+ end
383
+
384
+ # Exact title match on purpose - "# done ideas" must stay an unmanaged section.
385
+ def todo_section_key(title)
386
+ case title.strip.downcase
387
+ when 'todo', 'pending' then :todo
388
+ when 'doing', 'in progress', 'in-progress' then :doing
389
+ when 'done', 'completed' then :done
390
+ end
391
+ end
392
+
393
+ # The tool manages only the `# todo`, `# doing` and `# done` sections (any
394
+ # heading level). Inside them a task starts at a `*` or `-` bullet in the
395
+ # first column and runs until the next bullet or header, so tasks can span
396
+ # multiple lines and move between sections verbatim. Every other section
397
+ # (`# plan`, `# idea`, ...) and any preamble is kept as-is and never touched.
398
+ # Returns [tasks, nodes]; nodes reproduce the document layout for todo_save.
399
+ def todo_parse
400
+ tasks = { todo: [], doing: [], done: [] }
401
+ nodes = []
402
+ return [tasks, nodes] unless File.file?(todo_path)
403
+
404
+ section = nil # managed section key, nil while inside a raw chunk
405
+ current = nil # lines of the task being collected
406
+
407
+ flush_task = lambda do
408
+ tasks[section] << current.join("\n").rstrip if current
409
+ current = nil
410
+ end
411
+
412
+ File.foreach(todo_path) do |raw|
413
+ line = raw.chomp
414
+ if (m = line.match(/^#+\s*(\S.*?)\s*$/))
415
+ flush_task.call
416
+ section = todo_section_key(m[1])
417
+ if section
418
+ nodes << [:section, section] unless nodes.include?([:section, section])
419
+ else
420
+ nodes << [:raw, [line]]
421
+ end
422
+ elsif section
423
+ if line =~ /^[-*]\s+(.*)$/
424
+ flush_task.call
425
+ current = [$1.strip]
426
+ elsif current
427
+ current << line.rstrip
428
+ end
429
+ else
430
+ nodes << [:raw, []] unless nodes.last && nodes.last[0] == :raw
431
+ nodes.last[1] << line.rstrip
432
+ end
433
+ end
434
+ flush_task.call
435
+ [tasks, nodes]
436
+ end
437
+
438
+ # Rewrite: managed sections in canonical form, raw chunks verbatim, original
439
+ # order kept. Managed sections missing from the file are appended at the end.
440
+ def todo_save(tasks, nodes)
441
+ order = nodes + (TODO_SECTIONS.keys - nodes.map { |t, v| v if t == :section }).map { |k| [:section, k] }
442
+
443
+ File.open(todo_path, 'w') do |io|
444
+ order.each_with_index do |(type, value), i|
445
+ io.puts unless i.zero?
446
+ if type == :section
447
+ io.puts "# #{TODO_SECTIONS[value]}"
448
+ tasks[value].each do |text|
449
+ io.puts
450
+ io.puts "* #{text}"
451
+ end
452
+ else
453
+ lines = value.dup
454
+ lines.pop while lines.any? && lines.last.empty?
455
+ lines.each { |l| io.puts l }
456
+ end
457
+ end
458
+ end
459
+ end
460
+
461
+ # One-line label for confirmations - multi-line tasks show their first line.
462
+ def todo_label(text)
463
+ first, rest = text.split("\n", 2)
464
+ rest ? "#{first} ..." : first
465
+ end
466
+
467
+ # Format sanity check for hand-edited files. Returns warning strings.
468
+ def todo_lint(tasks)
469
+ warns = []
470
+ seen = Hash.new(0)
471
+ section = :preamble
472
+ File.foreach(todo_path) do |raw|
473
+ line = raw.chomp
474
+ if (m = line.match(/^#+\s*(\S.*?)\s*$/))
475
+ key = todo_section_key(m[1])
476
+ section = key || :other
477
+ seen[key] += 1 if key
478
+ elsif section == :preamble && line =~ /^[-*]\s/
479
+ warns << "ignored bullet before any section header: #{line}"
480
+ end
481
+ end
482
+ TODO_SECTIONS.each_key do |key|
483
+ warns << "duplicate '# #{key}' sections - their tasks are merged" if seen[key] > 1
484
+ warns << "missing '# #{key}' section - will be added on next write" if seen[key].zero?
485
+ end
486
+ warns << "#{tasks[:doing].length} tasks in doing - expected at most 1" if tasks[:doing].length > 1
487
+ warns
488
+ end
489
+
490
+ task :add do
491
+ desc <<~DESC
492
+ Add task(s) to ./#{TODO_FILE} (created on first add).
493
+
494
+ Text comes from the argument (one task), or from stdin - there a `*` at
495
+ the start of a line begins a new task and following lines belong to it,
496
+ so multi-line tasks and whole lists can be piped in at once.
497
+ DESC
498
+ example 'todo add "migrate the user model to Sequel"'
499
+ example 'cat tasks.md | llm todo add'
500
+ opt :text, desc: 'task text (or pipe tasks on stdin)'
501
+
502
+ proc do |opts|
503
+ # unquoted multi-word input lands in :text + :args - join it back
504
+ arg = [opts[:text], *opts[:args]].compact.join(' ').strip
505
+ if arg.empty?
506
+ new_tasks = []
507
+ opts[:stdin].to_s.split("\n").each do |line|
508
+ if line =~ /^[-*]\s+(.*)$/
509
+ new_tasks << [$1.strip]
510
+ elsif new_tasks.any?
511
+ new_tasks.last << line.rstrip
512
+ elsif line.strip != ''
513
+ new_tasks << [line.rstrip]
514
+ end
515
+ end
516
+ new_tasks = new_tasks.map { |lines| lines.join("\n").rstrip }.reject(&:empty?)
517
+ else
518
+ new_tasks = [arg]
519
+ end
520
+ error 'usage: llm todo add <text> (or pipe tasks on stdin)' if new_tasks.empty?
521
+
522
+ tasks, nodes = todo_parse
523
+ tasks[:todo].concat new_tasks
524
+ todo_save tasks, nodes
525
+ new_tasks.each { |t| say "added: #{todo_label(t)}", :green }
526
+ end
527
+ end
528
+
529
+ task :pop do
530
+ desc <<~DESC
531
+ Print the current task and stop there - one task, no loop.
532
+
533
+ Moves the first todo task to doing if nothing is doing yet. Prints only
534
+ the raw task text (agent-friendly stdout). Re-running without `todo done`
535
+ prints the same task again, so an interrupted agent resumes instead of
536
+ starting the next one. Exits 1 when the list is empty.
537
+ DESC
538
+ example 'todo pop'
539
+
540
+ proc do
541
+ tasks, nodes = todo_parse
542
+ if tasks[:doing].empty?
543
+ error 'todo list is empty' if tasks[:todo].empty?
544
+ tasks[:doing] << tasks[:todo].shift
545
+ todo_save tasks, nodes
546
+ end
547
+ puts tasks[:doing].first
548
+ end
549
+ end
550
+
551
+ task :go do
552
+ desc <<~DESC
553
+ Agent entry point: work through the whole todo list.
554
+
555
+ Prints the current task (pop) followed by the loop protocol, so telling
556
+ any agent "run llm todo go" is enough to drain the queue. When the list
557
+ is empty it says so and tells the agent to stop and summarize.
558
+ DESC
559
+ example 'todo go'
560
+
561
+ proc do
562
+ tasks, nodes = todo_parse
563
+ if tasks[:doing].empty? && tasks[:todo].empty?
564
+ say 'All tasks done - stop the loop and summarize what was done.'
565
+ next
566
+ end
567
+ if tasks[:doing].empty?
568
+ tasks[:doing] << tasks[:todo].shift
569
+ todo_save tasks, nodes
570
+ end
571
+ say "TASK: #{tasks[:doing].first}"
572
+ say ''
573
+ say 'Do this task fully and verify it. Then run `llm todo done`, and `llm todo go` for the next one.'
574
+ end
575
+ end
576
+
577
+ task :done do
578
+ desc 'Mark the in-progress task as done'
579
+ example 'todo done'
580
+
581
+ proc do
582
+ tasks, nodes = todo_parse
583
+ error 'no task in progress (run: llm todo pop)' if tasks[:doing].empty?
584
+ text = tasks[:doing].shift
585
+ tasks[:done] << text
586
+ todo_save tasks, nodes
587
+ say "done: #{todo_label(text)}", :green
588
+ end
589
+ end
590
+
591
+ task :list do
592
+ alt :inspect
593
+ desc 'Inspect the todo file: tasks by id, counts, format validity warnings'
594
+ example 'todo list'
595
+
596
+ proc do
597
+ error "no #{TODO_FILE} in #{Dir.pwd} (run: llm todo add <text>)" unless File.file?(todo_path)
598
+ tasks, = todo_parse
599
+ id = 0
600
+ TODO_SECTIONS.each do |key, title|
601
+ say "#{title}:", :cyan
602
+ color = { todo: nil, doing: :yellow, done: :gray }[key]
603
+ tasks[key].each do |text|
604
+ first, *rest = text.split("\n")
605
+ say "#{(id += 1).to_s.rjust(3)}. #{first}", color
606
+ rest.each { |l| say " #{l}", color }
607
+ end
608
+ say ' (none)', :gray if tasks[key].empty?
609
+ end
610
+ say ''
611
+ say "#{tasks[:todo].length} todo, #{tasks[:doing].length} doing, #{tasks[:done].length} done"
612
+ todo_lint(tasks).each { |w| say "warning: #{w}", :yellow }
613
+ end
614
+ end
615
+ end
616
+
290
617
  namespace :prompt do
291
618
  TOKEN_PATTERN ||= /[a-z0-9_-]+/.freeze
292
619
  TOKEN_LINE_RE ||= /\A(?:\s*:[a-z0-9_-]+)+\s*\z/.freeze
@@ -524,8 +851,8 @@ namespace :prompt do
524
851
 
525
852
  task :expand do
526
853
  desc 'Expand prompt token(s) and print the resulting context'
527
- example 'llm prompt:expand :foo :bar'
528
- example 'llm prompt:expand foo:'
854
+ example 'prompt:expand :foo :bar'
855
+ example 'prompt:expand foo:'
529
856
 
530
857
  proc do |opts|
531
858
  input = opts[:args].join(' ')
@@ -561,3 +888,104 @@ namespace :prompt do
561
888
  end
562
889
  end
563
890
  end
891
+
892
+ # The namespace's sibling task: `llm plan` explains the thing, `llm plan:*`
893
+ # does it. The command list underneath is hammer's own, not a copy.
894
+ task :plan do
895
+ desc 'How the /plan bundle apply works: logic, bundle format, exit codes'
896
+ example 'plan'
897
+
898
+ proc do
899
+ say LlmPlan.readme
900
+ say ''
901
+ self.class.print_help 'plan:'
902
+ end
903
+ end
904
+
905
+ namespace :plan do
906
+ # Helpers live inside the namespace block for the same reason as :memory -
907
+ # a top-level `helpers do` lands on the root class, which namespaces do not
908
+ # inherit from.
909
+ private
910
+
911
+ def load_bundle(opts)
912
+ path = opts[:args].first
913
+ error 'usage: llm plan:<apply|check|verify|revert> ./tmp/plan-[SLUG].json' unless path
914
+ LlmPlan::Bundle.new(path)
915
+ rescue LlmPlan::Error => e
916
+ error e.message
917
+ end
918
+
919
+ # Hammer owns colour (and turns it off for a non-tty), so hand its painter
920
+ # to the report rather than teaching the report about ANSI.
921
+ def render(outcome)
922
+ report = LlmPlan::Report.new(outcome, paint: Hammer::Shell.method(:paint))
923
+ report.lines.each { |text, color| say text, color }
924
+ end
925
+
926
+ def run_verify(runner)
927
+ result = runner.verify { |cmd| say " > #{cmd}", :cyan }
928
+ result.ok ? say(' verify ok', :green) : say(" FAIL #{result.failed_command}", :red)
929
+ result
930
+ end
931
+
932
+ task :apply do
933
+ # Composed from plan.md rather than written out again - see `llm plan`.
934
+ desc ['Apply a /plan bundle: sha1-checked edits now, drift handed back to you.',
935
+ "The bundle, normally ./tmp/plan-[SLUG].json:\n\n#{LlmPlan.section('The bundle')}",
936
+ LlmPlan.section('Drift'),
937
+ LlmPlan.section('Exit codes')].join("\n\n")
938
+ example 'plan:apply ./tmp/plan-note-anchor.json'
939
+
940
+ proc do |opts|
941
+ bundle = load_bundle(opts)
942
+ runner = LlmPlan::Runner.new(bundle)
943
+ outcome = runner.apply
944
+
945
+ render outcome
946
+ outcome.verify = run_verify(runner) if outcome.clean?
947
+
948
+ exit outcome.exit_code
949
+ end
950
+ end
951
+
952
+ task :check do
953
+ desc ['Dry run: the summary to read before approving a plan. Writes nothing.',
954
+ LlmPlan.section('Summary')].join("\n\n")
955
+ example 'plan:check ./tmp/plan-note-anchor.json'
956
+ example 'plan:check --md ./tmp/plan-note-anchor.json'
957
+
958
+ opt :md, type: :boolean, desc: 'emit the summary as markdown, to paste into a reply'
959
+
960
+ proc do |opts|
961
+ outcome = LlmPlan::Runner.new(load_bundle(opts)).apply(check_only: true)
962
+ opts[:md] ? puts(LlmPlan::MarkdownReport.new(outcome).to_s) : render(outcome)
963
+ exit outcome.exit_code
964
+ end
965
+ end
966
+
967
+ task :verify do
968
+ desc <<~D
969
+ Run only the bundle's verify commands.
970
+
971
+ For after you have closed a drift by hand, or fixed what a failing verify
972
+ caught. Exit 0 green, 20 failed.
973
+ D
974
+ example 'plan:verify ./tmp/plan-note-anchor.json'
975
+
976
+ proc do |opts|
977
+ exit run_verify(LlmPlan::Runner.new(load_bundle(opts))).ok ? 0 : 20
978
+ end
979
+ end
980
+
981
+ task :revert do
982
+ desc 'Undo an applied bundle from <slug>.bak: restore changed and deleted files, remove created ones.'
983
+ example 'plan:revert ./tmp/plan-note-anchor.json'
984
+
985
+ proc do |opts|
986
+ LlmPlan::Runner.new(load_bundle(opts)).revert.each { |path| say " restored #{path}", :green }
987
+ rescue LlmPlan::Error => e
988
+ error e.message
989
+ end
990
+ end
991
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lux-hammer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.21
4
+ version: 0.3.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dino Reic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-08 00:00:00.000000000 Z
11
+ date: 2026-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -55,6 +55,7 @@ files:
55
55
  - "./lib/lux-hammer.rb"
56
56
  - "./recipes/deploy.rb"
57
57
  - "./recipes/git-helper.rb"
58
+ - "./recipes/lib/llm/plan.rb"
58
59
  - "./recipes/lib/llm/usage.rb"
59
60
  - "./recipes/lib/llm/wrap.rb"
60
61
  - "./recipes/llm.rb"