lux-hammer 0.3.24 → 0.3.26
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/.version +1 -1
- data/lib/hammer/parser.rb +11 -0
- data/recipes/lib/llm/launch.rb +76 -0
- data/recipes/llm.rb +53 -287
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acc942b0818d0cc60a8cbe9a55fbf092665d6efcb46df209b2a8e9721aa1e190
|
|
4
|
+
data.tar.gz: 94baed7a0025611360d0315d084fd7432a1eb68d84302bfeaf0960ee0326aee7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 486bb304e25a56faada26757651998d75e589fabca7402f499dc9cdd3228a90b30cc3fc7cda765d2cd34fc90eecc906208681c031e38628e0087f59eb5250af7
|
|
7
|
+
data.tar.gz: 87115686cf4bbdd6a47ed9a25e048cf811fe1c8071966e5ee3143b7fcafc286ff1480d00c5ba494e407fa9032b8e1b6dda43e8623c011ae9ffdbdf70e8a33612
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.26
|
data/lib/hammer/parser.rb
CHANGED
|
@@ -50,6 +50,17 @@ class Hammer
|
|
|
50
50
|
next
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# Bundled boolean short flags: `-cf` is `-c -f`. Only when every letter
|
|
54
|
+
# is a boolean switch, so `-pVALUE` below keeps its meaning.
|
|
55
|
+
if token =~ /\A-[^-]{2,}\z/
|
|
56
|
+
bundle = token[1..-1].chars.map { |ch| @by_switch["-#{ch}"] }
|
|
57
|
+
if bundle.all? { |o| o&.boolean? }
|
|
58
|
+
bundle.each { |o| values[o.name] = true }
|
|
59
|
+
i += 1
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
53
64
|
# Glued short flag with value: `-pVALUE` (non-boolean short opts only).
|
|
54
65
|
if token.start_with?('-') && !token.start_with?('--') && token.length > 2
|
|
55
66
|
if (opt = @by_switch[token[0, 2]]) && !opt.boolean?
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
# Builds the command `llm wrap:run` hands to LlmWrap: picks the agent CLI by
|
|
7
|
+
# prefix and maps the generic -c / -f / -a switches to what that CLI takes.
|
|
8
|
+
module LlmLaunch
|
|
9
|
+
Launch = Struct.new(:argv, :env, :files)
|
|
10
|
+
|
|
11
|
+
# :agents gets the combined AGENTS.md text and the file paths, and answers
|
|
12
|
+
# with the argv to add and/or env to set - opencode has no flag for
|
|
13
|
+
# instructions, only inline config, and that one takes paths.
|
|
14
|
+
TOOLS ||= {
|
|
15
|
+
'claude' => { continue: %w[-c], full: %w[--dangerously-skip-permissions],
|
|
16
|
+
agents: ->(text, _files) { { args: ['--append-system-prompt', text] } } },
|
|
17
|
+
'codex' => { continue: %w[resume --last], full: %w[--dangerously-bypass-approvals-and-sandbox],
|
|
18
|
+
agents: ->(text, _files) { { args: ['-c', "developer_instructions=#{text}"] } } },
|
|
19
|
+
'grok' => { continue: %w[-c], full: %w[--always-approve],
|
|
20
|
+
agents: ->(text, _files) { { args: ['--rules', text] } } },
|
|
21
|
+
'opencode' => { continue: %w[-c], full: %w[--auto],
|
|
22
|
+
agents: ->(_text, files) { { env: { 'OPENCODE_CONFIG_CONTENT' => { instructions: files }.to_json } } } },
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# Read AGENTS.md from the git root down on their own; -a only adds the
|
|
26
|
+
# files above it for these. Claude reads CLAUDE.md, so it gets the chain.
|
|
27
|
+
NATIVE_AGENTS ||= %w[codex grok opencode].freeze
|
|
28
|
+
|
|
29
|
+
# First tool whose name starts with the prefix, in TOOLS order: `c` is
|
|
30
|
+
# claude, `co` codex. No prefix means claude.
|
|
31
|
+
def self.tool(prefix)
|
|
32
|
+
return 'claude' if prefix.nil? || prefix.empty?
|
|
33
|
+
TOOLS.keys.find { |name| name.start_with?(prefix) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.build(tool, extra = [], continue: false, full: false, agents: false, cwd: Dir.pwd, home: Dir.home)
|
|
37
|
+
spec = TOOLS.fetch(tool)
|
|
38
|
+
argv = [tool]
|
|
39
|
+
argv.concat(spec[:continue]) if continue
|
|
40
|
+
argv.concat(spec[:full]) if full
|
|
41
|
+
|
|
42
|
+
files = agents ? agents_files(tool, cwd: cwd, home: home) : []
|
|
43
|
+
env = {}
|
|
44
|
+
if files.any?
|
|
45
|
+
added = spec[:agents].call(agents_text(files), files.map(&:to_s))
|
|
46
|
+
argv.concat(added[:args] || [])
|
|
47
|
+
env.update(added[:env] || {})
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
argv.concat(extra)
|
|
51
|
+
Launch.new(argv, env, files)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# AGENTS.md from home down to cwd, outermost first. For NATIVE_AGENTS tools
|
|
55
|
+
# the walk stops above the git root - or above cwd outside git, since that is
|
|
56
|
+
# what they take as the project then.
|
|
57
|
+
def self.agents_files(tool, cwd: Dir.pwd, home: Dir.home)
|
|
58
|
+
stop = nil
|
|
59
|
+
if NATIVE_AGENTS.include?(tool)
|
|
60
|
+
stop = IO.popen(['git', '-C', cwd, 'rev-parse', '--show-toplevel'], err: File::NULL, &:read).to_s.strip
|
|
61
|
+
stop = cwd if stop.empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Pathname(cwd).ascend
|
|
65
|
+
.select { |dir| dir.to_s.start_with?(home) }
|
|
66
|
+
.reject { |dir| stop && dir.to_s.start_with?(stop) }
|
|
67
|
+
.reverse
|
|
68
|
+
.map { |dir| dir + 'AGENTS.md' }
|
|
69
|
+
.select(&:file?)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.agents_text(files)
|
|
73
|
+
body = files.map { |f| "## #{f}\n\n#{f.read.strip}\n" }.join("\n")
|
|
74
|
+
"Project instructions (AGENTS.md), broad to specific:\n\n#{body}"
|
|
75
|
+
end
|
|
76
|
+
end
|
data/recipes/llm.rb
CHANGED
|
@@ -9,11 +9,11 @@ desc <<~TXT
|
|
|
9
9
|
memory persistent memory store (backs the Claude Code memory plugin)
|
|
10
10
|
plan apply a /plan bundle - sha1 checked, drift aware
|
|
11
11
|
prompt token-prefix prompt expander (UserPromptSubmit hook + CLI)
|
|
12
|
-
todo per-project task queue for agents (./LLM_TODO.local.md)
|
|
13
12
|
|
|
14
13
|
Commands:
|
|
15
14
|
usage subscription limits and per-model token usage
|
|
16
15
|
wrap run a command with your last two prompts pinned to the screen
|
|
16
|
+
wrap:run start claude / codex / grok / opencode in wrap, same -c -f -a switches for all
|
|
17
17
|
TXT
|
|
18
18
|
|
|
19
19
|
require 'fileutils'
|
|
@@ -186,6 +186,58 @@ task :wrap do
|
|
|
186
186
|
end
|
|
187
187
|
end
|
|
188
188
|
|
|
189
|
+
namespace :wrap do
|
|
190
|
+
task :run do
|
|
191
|
+
desc <<~D
|
|
192
|
+
Start an agent CLI inside `llm wrap`, with the same switches whichever one it is.
|
|
193
|
+
|
|
194
|
+
TOOL is a prefix of claude, codex, grok or opencode - first match wins, so `c` is
|
|
195
|
+
claude and `co` is codex. No tool means claude. The switches map to what each
|
|
196
|
+
CLI actually takes:
|
|
197
|
+
|
|
198
|
+
-c continue the last session here claude/grok/opencode -c, codex resume --last
|
|
199
|
+
-f full permissions, no prompts --dangerously-skip-permissions, --dangerously-bypass-
|
|
200
|
+
approvals-and-sandbox, --always-approve, --auto
|
|
201
|
+
-a AGENTS.md from ~ down to cwd claude --append-system-prompt, codex -c
|
|
202
|
+
developer_instructions, grok --rules, opencode
|
|
203
|
+
inline config
|
|
204
|
+
|
|
205
|
+
codex, grok and opencode read AGENTS.md from the git root down by themselves, so
|
|
206
|
+
-a only adds the files above the repo for them; claude gets the whole chain.
|
|
207
|
+
Anything after `--` is handed to the tool untouched. `~/bin/ai` is this command.
|
|
208
|
+
D
|
|
209
|
+
example 'wrap:run'
|
|
210
|
+
example 'wrap:run g -cf'
|
|
211
|
+
example 'wrap:run cod -ca'
|
|
212
|
+
example 'wrap:run c -- --model opus'
|
|
213
|
+
|
|
214
|
+
opt :continue, type: :boolean, desc: 'continue the last session in this folder'
|
|
215
|
+
opt :full, type: :boolean, desc: 'full permissions, no approval prompts'
|
|
216
|
+
opt :agents, type: :boolean, desc: 'inject AGENTS.md found from ~ down to cwd'
|
|
217
|
+
|
|
218
|
+
proc do |opts|
|
|
219
|
+
require File.join(_llm_root, 'lib/llm/wrap')
|
|
220
|
+
require File.join(_llm_root, 'lib/llm/launch')
|
|
221
|
+
|
|
222
|
+
args = Array(opts[:args])
|
|
223
|
+
prefix = args.first unless args.first.nil? || args.first.start_with?('-')
|
|
224
|
+
tool = LlmLaunch.tool(prefix) ||
|
|
225
|
+
error("unknown tool #{prefix.inspect} - one of #{LlmLaunch::TOOLS.keys.join(', ')}")
|
|
226
|
+
extra = prefix ? args.drop(1) : args
|
|
227
|
+
|
|
228
|
+
launch = LlmLaunch.build(tool, extra, continue: opts[:continue], full: opts[:full], agents: opts[:agents])
|
|
229
|
+
ENV.update(launch.env)
|
|
230
|
+
say.gray "agents: #{launch.files.map { |f| f.to_s.sub(Dir.home, '~') }.join(', ')}" if launch.files.any?
|
|
231
|
+
|
|
232
|
+
flags = %i[continue full agents].select { |k| opts[k] }.map { |k| "--#{k}" }
|
|
233
|
+
origin = ['llm', 'wrap:run', tool, *flags, '--', *extra]
|
|
234
|
+
# Piped stdout is block-buffered and exec drops the buffer with the banner in it.
|
|
235
|
+
$stdout.flush
|
|
236
|
+
exit LlmWrap.run(launch.argv, origin: origin)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
189
241
|
namespace :memory do
|
|
190
242
|
# Helpers are defined inside the namespace block (class_eval'd on the
|
|
191
243
|
# namespace's anonymous Hammer subclass) so the task procs reach them.
|
|
@@ -333,292 +385,6 @@ namespace :memory do
|
|
|
333
385
|
end
|
|
334
386
|
end
|
|
335
387
|
|
|
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:
|
|
341
|
-
`* ` bullets delimit tasks (multi-line ok),
|
|
342
|
-
no bullets at all = one task per line
|
|
343
|
-
llm todo:list tasks by id, counts, format warnings
|
|
344
|
-
llm todo:pop + llm todo:done take / finish a single task by hand
|
|
345
|
-
|
|
346
|
-
Edit the file freely: only the `# todo`, `# doing` and `# done` sections are
|
|
347
|
-
managed. `# plan`, `# idea` and any prose stay untouched. A task starts with
|
|
348
|
-
`*` at the line start and runs until the next `*` or header; when finished it
|
|
349
|
-
moves to `# done` verbatim.
|
|
350
|
-
|
|
351
|
-
LLM usage - one of these lines is the whole prompt:
|
|
352
|
-
"Run `llm todo go` and follow its output." work through every task
|
|
353
|
-
"Run `llm todo pop`, do the task, run `llm todo done`." do a single task
|
|
354
|
-
|
|
355
|
-
Task text is printed raw on stdout. `todo go` repeats the protocol after each
|
|
356
|
-
task and says when the queue is empty; `todo pop` re-prints the task in
|
|
357
|
-
progress, so an interrupted agent resumes instead of skipping ahead.
|
|
358
|
-
TXT
|
|
359
|
-
|
|
360
|
-
task :todo do
|
|
361
|
-
desc <<~D
|
|
362
|
-
How the todo queue works, for humans and LLMs.
|
|
363
|
-
|
|
364
|
-
#{TODO_GUIDE}
|
|
365
|
-
D
|
|
366
|
-
example 'todo'
|
|
367
|
-
|
|
368
|
-
proc do
|
|
369
|
-
say TODO_GUIDE
|
|
370
|
-
say ''
|
|
371
|
-
self.class.print_help 'todo:'
|
|
372
|
-
end
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
namespace :todo do
|
|
376
|
-
# Same story as :memory - helpers must live inside the namespace block.
|
|
377
|
-
private
|
|
378
|
-
|
|
379
|
-
TODO_FILE ||= 'LLM_TODO.local.md'
|
|
380
|
-
TODO_SECTIONS ||= { todo: 'todo', doing: 'doing', done: 'done' }.freeze
|
|
381
|
-
|
|
382
|
-
def todo_path
|
|
383
|
-
File.join(Dir.pwd, TODO_FILE)
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
# Exact title match on purpose - "# done ideas" must stay an unmanaged section.
|
|
387
|
-
def todo_section_key(title)
|
|
388
|
-
case title.strip.downcase
|
|
389
|
-
when 'todo', 'pending' then :todo
|
|
390
|
-
when 'doing', 'in progress', 'in-progress' then :doing
|
|
391
|
-
when 'done', 'completed' then :done
|
|
392
|
-
end
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
# The tool manages only the `# todo`, `# doing` and `# done` sections (any
|
|
396
|
-
# heading level). Inside them a task starts at a `*` or `-` bullet in the
|
|
397
|
-
# first column and runs until the next bullet or header, so tasks can span
|
|
398
|
-
# multiple lines and move between sections verbatim. Every other section
|
|
399
|
-
# (`# plan`, `# idea`, ...) and any preamble is kept as-is and never touched.
|
|
400
|
-
# Returns [tasks, nodes]; nodes reproduce the document layout for todo_save.
|
|
401
|
-
def todo_parse
|
|
402
|
-
tasks = { todo: [], doing: [], done: [] }
|
|
403
|
-
nodes = []
|
|
404
|
-
return [tasks, nodes] unless File.file?(todo_path)
|
|
405
|
-
|
|
406
|
-
section = nil # managed section key, nil while inside a raw chunk
|
|
407
|
-
current = nil # lines of the task being collected
|
|
408
|
-
|
|
409
|
-
flush_task = lambda do
|
|
410
|
-
tasks[section] << current.join("\n").rstrip if current
|
|
411
|
-
current = nil
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
File.foreach(todo_path) do |raw|
|
|
415
|
-
line = raw.chomp
|
|
416
|
-
if (m = line.match(/^#+\s*(\S.*?)\s*$/))
|
|
417
|
-
flush_task.call
|
|
418
|
-
section = todo_section_key(m[1])
|
|
419
|
-
if section
|
|
420
|
-
nodes << [:section, section] unless nodes.include?([:section, section])
|
|
421
|
-
else
|
|
422
|
-
nodes << [:raw, [line]]
|
|
423
|
-
end
|
|
424
|
-
elsif section
|
|
425
|
-
if line =~ /^[-*]\s+(.*)$/
|
|
426
|
-
flush_task.call
|
|
427
|
-
current = [$1.strip]
|
|
428
|
-
elsif current
|
|
429
|
-
current << line.rstrip
|
|
430
|
-
end
|
|
431
|
-
else
|
|
432
|
-
nodes << [:raw, []] unless nodes.last && nodes.last[0] == :raw
|
|
433
|
-
nodes.last[1] << line.rstrip
|
|
434
|
-
end
|
|
435
|
-
end
|
|
436
|
-
flush_task.call
|
|
437
|
-
[tasks, nodes]
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
# Rewrite: managed sections in canonical form, raw chunks verbatim, original
|
|
441
|
-
# order kept. Managed sections missing from the file are appended at the end.
|
|
442
|
-
def todo_save(tasks, nodes)
|
|
443
|
-
order = nodes + (TODO_SECTIONS.keys - nodes.map { |t, v| v if t == :section }).map { |k| [:section, k] }
|
|
444
|
-
|
|
445
|
-
File.open(todo_path, 'w') do |io|
|
|
446
|
-
order.each_with_index do |(type, value), i|
|
|
447
|
-
io.puts unless i.zero?
|
|
448
|
-
if type == :section
|
|
449
|
-
io.puts "# #{TODO_SECTIONS[value]}"
|
|
450
|
-
tasks[value].each do |text|
|
|
451
|
-
io.puts
|
|
452
|
-
io.puts "* #{text}"
|
|
453
|
-
end
|
|
454
|
-
else
|
|
455
|
-
lines = value.dup
|
|
456
|
-
lines.pop while lines.any? && lines.last.empty?
|
|
457
|
-
lines.each { |l| io.puts l }
|
|
458
|
-
end
|
|
459
|
-
end
|
|
460
|
-
end
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
# One-line label for confirmations - multi-line tasks show their first line.
|
|
464
|
-
def todo_label(text)
|
|
465
|
-
first, rest = text.split("\n", 2)
|
|
466
|
-
rest ? "#{first} ..." : first
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
# Format sanity check for hand-edited files. Returns warning strings.
|
|
470
|
-
def todo_lint(tasks)
|
|
471
|
-
warns = []
|
|
472
|
-
seen = Hash.new(0)
|
|
473
|
-
section = :preamble
|
|
474
|
-
File.foreach(todo_path) do |raw|
|
|
475
|
-
line = raw.chomp
|
|
476
|
-
if (m = line.match(/^#+\s*(\S.*?)\s*$/))
|
|
477
|
-
key = todo_section_key(m[1])
|
|
478
|
-
section = key || :other
|
|
479
|
-
seen[key] += 1 if key
|
|
480
|
-
elsif section == :preamble && line =~ /^[-*]\s/
|
|
481
|
-
warns << "ignored bullet before any section header: #{line}"
|
|
482
|
-
end
|
|
483
|
-
end
|
|
484
|
-
TODO_SECTIONS.each_key do |key|
|
|
485
|
-
warns << "duplicate '# #{key}' sections - their tasks are merged" if seen[key] > 1
|
|
486
|
-
warns << "missing '# #{key}' section - will be added on next write" if seen[key].zero?
|
|
487
|
-
end
|
|
488
|
-
warns << "#{tasks[:doing].length} tasks in doing - expected at most 1" if tasks[:doing].length > 1
|
|
489
|
-
warns
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
task :add do
|
|
493
|
-
desc <<~DESC
|
|
494
|
-
Add task(s) to ./#{TODO_FILE} (created on first add).
|
|
495
|
-
|
|
496
|
-
Text comes from the argument (one task), or from stdin. Piped input
|
|
497
|
-
that has `*` / `-` bullets is a list: a bullet begins a task and the
|
|
498
|
-
lines under it belong to it, so multi-line tasks and whole lists can be
|
|
499
|
-
piped in at once. Input with no bullets at all is one task per line.
|
|
500
|
-
DESC
|
|
501
|
-
example 'todo add "migrate the user model to Sequel"'
|
|
502
|
-
example 'cat tasks.md | llm todo add'
|
|
503
|
-
opt :text, desc: 'task text (or pipe tasks on stdin)'
|
|
504
|
-
|
|
505
|
-
proc do |opts|
|
|
506
|
-
# unquoted multi-word input lands in :text + :args - join it back
|
|
507
|
-
arg = [opts[:text], *opts[:args]].compact.join(' ').strip
|
|
508
|
-
if arg.empty?
|
|
509
|
-
lines = opts[:stdin].to_s.split("\n")
|
|
510
|
-
bulleted = lines.any? { |l| l =~ /^[-*]\s+/ }
|
|
511
|
-
new_tasks = []
|
|
512
|
-
lines.each do |line|
|
|
513
|
-
if line =~ /^[-*]\s+(.*)$/
|
|
514
|
-
new_tasks << [$1.strip]
|
|
515
|
-
elsif bulleted
|
|
516
|
-
new_tasks.last << line.rstrip if new_tasks.any?
|
|
517
|
-
elsif line.strip != ''
|
|
518
|
-
new_tasks << [line.rstrip]
|
|
519
|
-
end
|
|
520
|
-
end
|
|
521
|
-
new_tasks = new_tasks.map { |ls| ls.join("\n").rstrip }.reject(&:empty?)
|
|
522
|
-
else
|
|
523
|
-
new_tasks = [arg]
|
|
524
|
-
end
|
|
525
|
-
error 'usage: llm todo add <text> (or pipe tasks on stdin)' if new_tasks.empty?
|
|
526
|
-
|
|
527
|
-
tasks, nodes = todo_parse
|
|
528
|
-
tasks[:todo].concat new_tasks
|
|
529
|
-
todo_save tasks, nodes
|
|
530
|
-
new_tasks.each { |t| say "added: #{todo_label(t)}", :green }
|
|
531
|
-
end
|
|
532
|
-
end
|
|
533
|
-
|
|
534
|
-
task :pop do
|
|
535
|
-
desc <<~DESC
|
|
536
|
-
Print the current task and stop there - one task, no loop.
|
|
537
|
-
|
|
538
|
-
Moves the first todo task to doing if nothing is doing yet. Prints only
|
|
539
|
-
the raw task text (agent-friendly stdout). Re-running without `todo done`
|
|
540
|
-
prints the same task again, so an interrupted agent resumes instead of
|
|
541
|
-
starting the next one. Exits 1 when the list is empty.
|
|
542
|
-
DESC
|
|
543
|
-
example 'todo pop'
|
|
544
|
-
|
|
545
|
-
proc do
|
|
546
|
-
tasks, nodes = todo_parse
|
|
547
|
-
if tasks[:doing].empty?
|
|
548
|
-
error 'todo list is empty' if tasks[:todo].empty?
|
|
549
|
-
tasks[:doing] << tasks[:todo].shift
|
|
550
|
-
todo_save tasks, nodes
|
|
551
|
-
end
|
|
552
|
-
puts tasks[:doing].first
|
|
553
|
-
end
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
task :go do
|
|
557
|
-
desc <<~DESC
|
|
558
|
-
Agent entry point: work through the whole todo list.
|
|
559
|
-
|
|
560
|
-
Prints the current task (pop) followed by the loop protocol, so telling
|
|
561
|
-
any agent "run llm todo go" is enough to drain the queue. When the list
|
|
562
|
-
is empty it says so and tells the agent to stop and summarize.
|
|
563
|
-
DESC
|
|
564
|
-
example 'todo go'
|
|
565
|
-
|
|
566
|
-
proc do
|
|
567
|
-
tasks, nodes = todo_parse
|
|
568
|
-
if tasks[:doing].empty? && tasks[:todo].empty?
|
|
569
|
-
say 'All tasks done - stop the loop and summarize what was done.'
|
|
570
|
-
next
|
|
571
|
-
end
|
|
572
|
-
if tasks[:doing].empty?
|
|
573
|
-
tasks[:doing] << tasks[:todo].shift
|
|
574
|
-
todo_save tasks, nodes
|
|
575
|
-
end
|
|
576
|
-
say "TASK: #{tasks[:doing].first}"
|
|
577
|
-
say ''
|
|
578
|
-
say 'Do this task fully and verify it. Then run `llm todo done`, and `llm todo go` for the next one.'
|
|
579
|
-
end
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
task :done do
|
|
583
|
-
desc 'Mark the in-progress task as done'
|
|
584
|
-
example 'todo done'
|
|
585
|
-
|
|
586
|
-
proc do
|
|
587
|
-
tasks, nodes = todo_parse
|
|
588
|
-
error 'no task in progress (run: llm todo pop)' if tasks[:doing].empty?
|
|
589
|
-
text = tasks[:doing].shift
|
|
590
|
-
tasks[:done] << text
|
|
591
|
-
todo_save tasks, nodes
|
|
592
|
-
say "done: #{todo_label(text)}", :green
|
|
593
|
-
end
|
|
594
|
-
end
|
|
595
|
-
|
|
596
|
-
task :list do
|
|
597
|
-
alt :inspect
|
|
598
|
-
desc 'Inspect the todo file: tasks by id, counts, format validity warnings'
|
|
599
|
-
example 'todo list'
|
|
600
|
-
|
|
601
|
-
proc do
|
|
602
|
-
error "no #{TODO_FILE} in #{Dir.pwd} (run: llm todo add <text>)" unless File.file?(todo_path)
|
|
603
|
-
tasks, = todo_parse
|
|
604
|
-
id = 0
|
|
605
|
-
TODO_SECTIONS.each do |key, title|
|
|
606
|
-
say "#{title}:", :cyan
|
|
607
|
-
color = { todo: nil, doing: :yellow, done: :gray }[key]
|
|
608
|
-
tasks[key].each do |text|
|
|
609
|
-
first, *rest = text.split("\n")
|
|
610
|
-
say "#{(id += 1).to_s.rjust(3)}. #{first}", color
|
|
611
|
-
rest.each { |l| say " #{l}", color }
|
|
612
|
-
end
|
|
613
|
-
say ' (none)', :gray if tasks[key].empty?
|
|
614
|
-
end
|
|
615
|
-
say ''
|
|
616
|
-
say "#{tasks[:todo].length} todo, #{tasks[:doing].length} doing, #{tasks[:done].length} done"
|
|
617
|
-
todo_lint(tasks).each { |w| say "warning: #{w}", :yellow }
|
|
618
|
-
end
|
|
619
|
-
end
|
|
620
|
-
end
|
|
621
|
-
|
|
622
388
|
namespace :prompt do
|
|
623
389
|
TOKEN_PATTERN ||= /[a-z0-9_-]+/.freeze
|
|
624
390
|
TOKEN_LINE_RE ||= /\A(?:\s*:[a-z0-9_-]+)+\s*\z/.freeze
|
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.
|
|
4
|
+
version: 0.3.26
|
|
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-
|
|
11
|
+
date: 2026-08-27 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/launch.rb"
|
|
58
59
|
- "./recipes/lib/llm/plan.rb"
|
|
59
60
|
- "./recipes/lib/llm/usage.rb"
|
|
60
61
|
- "./recipes/lib/llm/wrap.rb"
|