latticerb 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/LICENSE +176 -0
- data/README.md +121 -0
- data/exe/lattice +7 -0
- data/lib/lattice/agent.rb +153 -0
- data/lib/lattice/cli.rb +469 -0
- data/lib/lattice/constraints.rb +357 -0
- data/lib/lattice/formatters.rb +126 -0
- data/lib/lattice/ipog.rb +222 -0
- data/lib/lattice/mersenne_twister.rb +102 -0
- data/lib/lattice/model.rb +83 -0
- data/lib/lattice/parser.rb +387 -0
- data/lib/lattice/resources/claude/lattice-workflow/SKILL.md +142 -0
- data/lib/lattice/resources/claude/lattice-workflow/references/modeling-rules.md +52 -0
- data/lib/lattice/resources/claude/lattice-workflow/references/worked-examples.md +59 -0
- data/lib/lattice/resources/codex/lattice-workflow/SKILL.md +142 -0
- data/lib/lattice/resources/codex/lattice-workflow/agents/openai.yaml +7 -0
- data/lib/lattice/resources/codex/lattice-workflow/references/modeling-rules.md +52 -0
- data/lib/lattice/resources/codex/lattice-workflow/references/worked-examples.md +59 -0
- data/lib/lattice/resources/generic/lattice-workflow/SKILL.md +150 -0
- data/lib/lattice/resources/generic/lattice-workflow/references/modeling-rules.md +52 -0
- data/lib/lattice/resources/generic/lattice-workflow/references/worked-examples.md +59 -0
- data/lib/lattice/version.rb +5 -0
- data/lib/lattice.rb +87 -0
- data/lib/latticerb.rb +4 -0
- metadata +86 -0
data/lib/lattice/cli.rb
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
# Load the full library API so `require "lattice/cli"` alone is enough.
|
|
7
|
+
require_relative "../lattice"
|
|
8
|
+
require_relative "agent"
|
|
9
|
+
require_relative "constraints"
|
|
10
|
+
require_relative "formatters"
|
|
11
|
+
require_relative "ipog"
|
|
12
|
+
require_relative "parser"
|
|
13
|
+
|
|
14
|
+
module Lattice
|
|
15
|
+
# Command-line interface mirroring the Python `lattice` CLI: same
|
|
16
|
+
# subcommands, flags, defaults, output, and exit codes.
|
|
17
|
+
module CLI
|
|
18
|
+
DEFAULT_AGENT_FORMAT = "text"
|
|
19
|
+
SCHEMA_HELP =
|
|
20
|
+
"Schema features: constraints include invalid_pair, higher_order, " \
|
|
21
|
+
"forward_dep, bidirectional, conditional, and forced; parameters may use " \
|
|
22
|
+
"weights. Run `lattice agent instructions` for the full schema workflow."
|
|
23
|
+
|
|
24
|
+
extend self
|
|
25
|
+
|
|
26
|
+
def main(argv = ARGV)
|
|
27
|
+
command = argv.first
|
|
28
|
+
case command
|
|
29
|
+
when "generate" then run_generate(argv.drop(1))
|
|
30
|
+
when "validate" then run_validate(argv.drop(1))
|
|
31
|
+
when "agent" then run_agent(argv.drop(1))
|
|
32
|
+
when "--help", "-h", nil
|
|
33
|
+
print_usage(command.nil? ? $stderr : $stdout)
|
|
34
|
+
command.nil? ? 2 : 0
|
|
35
|
+
else
|
|
36
|
+
warn "lattice: unknown command `#{command}`"
|
|
37
|
+
print_usage($stderr)
|
|
38
|
+
2
|
|
39
|
+
end
|
|
40
|
+
rescue OptionParser::ParseError => e
|
|
41
|
+
warn "lattice: #{e.message}"
|
|
42
|
+
2
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def print_usage(io)
|
|
46
|
+
io.puts <<~USAGE
|
|
47
|
+
usage: lattice {generate,validate,agent} ...
|
|
48
|
+
|
|
49
|
+
commands:
|
|
50
|
+
generate Generate deterministic scenarios from a schema.
|
|
51
|
+
validate Validate a schema without generating scenarios.
|
|
52
|
+
agent Install and describe coding-agent integrations.
|
|
53
|
+
|
|
54
|
+
#{SCHEMA_HELP}
|
|
55
|
+
USAGE
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def run_generate(argv)
|
|
59
|
+
options = {
|
|
60
|
+
format: "json",
|
|
61
|
+
seed: 42,
|
|
62
|
+
pool_size: 50,
|
|
63
|
+
progress: false,
|
|
64
|
+
progress_every: 10
|
|
65
|
+
}
|
|
66
|
+
parser = OptionParser.new do |opts|
|
|
67
|
+
opts.on("--strength STRENGTH", Integer)
|
|
68
|
+
opts.on("--format FORMAT", %w[table json csv summary])
|
|
69
|
+
opts.on("--seed SEED", Integer)
|
|
70
|
+
opts.on("--pool-size SIZE", Integer) { |value| options[:pool_size] = value }
|
|
71
|
+
opts.on("--max-rows ROWS", Integer) { |value| options[:max_rows] = value }
|
|
72
|
+
opts.on("--stop-after-coverage PCT", Float) { |value| options[:stop_after_coverage] = value }
|
|
73
|
+
opts.on("--progress")
|
|
74
|
+
opts.on("--progress-every N", Integer) { |value| options[:progress_every] = value }
|
|
75
|
+
end
|
|
76
|
+
positional = parser.parse(argv, into: options)
|
|
77
|
+
model_path = positional.first
|
|
78
|
+
|
|
79
|
+
model = load_and_check_model(model_path, options)
|
|
80
|
+
return model if model.is_a?(Integer)
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
progress_callback = options[:progress] ? build_progress_callback(options[:progress_every]) : nil
|
|
84
|
+
result = IPOG.generate_covering_array(
|
|
85
|
+
model,
|
|
86
|
+
strength: model.strength,
|
|
87
|
+
seed: options[:seed],
|
|
88
|
+
pool_size: options[:pool_size],
|
|
89
|
+
progress: progress_callback,
|
|
90
|
+
max_rows: options[:max_rows],
|
|
91
|
+
stop_after_coverage: options[:stop_after_coverage]
|
|
92
|
+
)
|
|
93
|
+
rescue ArgumentError => e
|
|
94
|
+
return emit_error(options, "generation_error", [e.message], exit_code: 1)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
puts Formatters.render_output(model, result, output_format: options[:format], seed: options[:seed])
|
|
98
|
+
0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def run_validate(argv)
|
|
102
|
+
options = { format: "json" }
|
|
103
|
+
parser = OptionParser.new do |opts|
|
|
104
|
+
opts.on("--strength STRENGTH", Integer)
|
|
105
|
+
opts.on("--format FORMAT", %w[json text])
|
|
106
|
+
end
|
|
107
|
+
positional = parser.parse(argv, into: options)
|
|
108
|
+
model_path = positional.first
|
|
109
|
+
|
|
110
|
+
model = load_and_check_model(model_path, options)
|
|
111
|
+
return model if model.is_a?(Integer)
|
|
112
|
+
|
|
113
|
+
print_validation_success(options, model)
|
|
114
|
+
0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def load_and_check_model(model_path, options)
|
|
118
|
+
model = Parser.load_model(model_path, strength_override: options[:strength])
|
|
119
|
+
engine = ConstraintEngine.new(model)
|
|
120
|
+
raise ValidationError, ["Model has no valid complete assignments."] unless engine.can_complete?({})
|
|
121
|
+
|
|
122
|
+
model
|
|
123
|
+
rescue ValidationError => e
|
|
124
|
+
emit_error(options, "validation_error", e.errors, exit_code: 1)
|
|
125
|
+
rescue ConstraintError => e
|
|
126
|
+
emit_error(options, "constraint_error", [e.message], exit_code: 1)
|
|
127
|
+
rescue ModelIOError => e
|
|
128
|
+
emit_error(options, "io_error", [e.message], exit_code: 2)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def build_progress_callback(every)
|
|
132
|
+
raise ArgumentError, "--progress-every must be at least 1" if every < 1
|
|
133
|
+
|
|
134
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
135
|
+
|
|
136
|
+
lambda do |progress|
|
|
137
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
138
|
+
case progress.event
|
|
139
|
+
when "enumerating"
|
|
140
|
+
eta = format_eta(elapsed, progress.covered, progress.total)
|
|
141
|
+
$stderr.puts(
|
|
142
|
+
"lattice progress: enumerating interactions " \
|
|
143
|
+
"checked=#{progress.covered}/#{progress.total} " \
|
|
144
|
+
"progress=#{format('%.1f', progress.cumulative_pct)}% " \
|
|
145
|
+
"elapsed=#{format_seconds(elapsed)} " \
|
|
146
|
+
"eta=#{eta}"
|
|
147
|
+
)
|
|
148
|
+
when "enumerated"
|
|
149
|
+
$stderr.puts(
|
|
150
|
+
"lattice progress: enumerated #{progress.total} valid interactions; starting generation"
|
|
151
|
+
)
|
|
152
|
+
else
|
|
153
|
+
return if progress.event == "scenario" && (progress.scenario % every) != 0 && progress.uncovered != 0
|
|
154
|
+
|
|
155
|
+
eta = format_eta(elapsed, progress.covered, progress.total)
|
|
156
|
+
label = progress.event == "stopped" ? "stopped " : ""
|
|
157
|
+
$stderr.puts(
|
|
158
|
+
"lattice progress: #{label}scenario=#{progress.scenario} " \
|
|
159
|
+
"covered=#{progress.covered}/#{progress.total} " \
|
|
160
|
+
"coverage=#{format('%.1f', progress.cumulative_pct)}% " \
|
|
161
|
+
"uncovered=#{progress.uncovered} " \
|
|
162
|
+
"elapsed=#{format_seconds(elapsed)} " \
|
|
163
|
+
"eta=#{eta}"
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def format_eta(elapsed, covered, total)
|
|
170
|
+
return "unknown" if total <= 0 || covered <= 0
|
|
171
|
+
return "0s" if covered >= total
|
|
172
|
+
|
|
173
|
+
format_seconds(elapsed * ((total - covered).to_f / covered))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def format_seconds(seconds)
|
|
177
|
+
seconds = [0, seconds.round].max
|
|
178
|
+
minutes, sec = seconds.divmod(60)
|
|
179
|
+
hours, minutes = minutes.divmod(60)
|
|
180
|
+
return format("%dh%02dm%02ds", hours, minutes, sec) if hours.positive?
|
|
181
|
+
return format("%dm%02ds", minutes, sec) if minutes.positive?
|
|
182
|
+
|
|
183
|
+
"#{sec}s"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def emit_error(options, code, errors, exit_code:)
|
|
187
|
+
if options[:format] == "json"
|
|
188
|
+
puts JSON.pretty_generate({ "status" => "error", "code" => code, "errors" => errors }, ascii_only: true)
|
|
189
|
+
else
|
|
190
|
+
errors.each { |error| warn error }
|
|
191
|
+
end
|
|
192
|
+
exit_code
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def print_validation_success(options, model)
|
|
196
|
+
if options[:format] == "json"
|
|
197
|
+
puts JSON.pretty_generate(
|
|
198
|
+
{
|
|
199
|
+
"status" => "ok",
|
|
200
|
+
"valid" => true,
|
|
201
|
+
"schema" => {
|
|
202
|
+
"name" => model.model_name,
|
|
203
|
+
"parameter_count" => model.parameters.length,
|
|
204
|
+
"constraint_count" => model.constraints.length,
|
|
205
|
+
"strength" => model.strength
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
ascii_only: true
|
|
209
|
+
)
|
|
210
|
+
return
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
puts [
|
|
214
|
+
"Schema is valid.",
|
|
215
|
+
"name: #{model.model_name}",
|
|
216
|
+
"parameters: #{model.parameters.length}",
|
|
217
|
+
"constraints: #{model.constraints.length}",
|
|
218
|
+
"strength: #{model.strength}"
|
|
219
|
+
].join("\n")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def run_agent(argv)
|
|
223
|
+
command = argv.first
|
|
224
|
+
rest = argv.drop(1)
|
|
225
|
+
case command
|
|
226
|
+
when "bootstrap" then run_agent_bootstrap(rest)
|
|
227
|
+
when "install-codex-skill" then run_agent_install(rest, :codex)
|
|
228
|
+
when "install-skill" then run_agent_install_generic(rest)
|
|
229
|
+
when "install-claude-skill" then run_agent_install_claude(rest)
|
|
230
|
+
when "memory" then run_agent_memory(rest)
|
|
231
|
+
when "instructions" then run_agent_instructions(rest)
|
|
232
|
+
when "doctor" then run_agent_doctor(rest)
|
|
233
|
+
else
|
|
234
|
+
warn "Unknown agent command `#{command}`."
|
|
235
|
+
2
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def run_agent_bootstrap(argv)
|
|
240
|
+
options = { format: DEFAULT_AGENT_FORMAT }
|
|
241
|
+
parser = OptionParser.new do |opts|
|
|
242
|
+
opts.on("--target TARGET")
|
|
243
|
+
opts.on("--claude-target TARGET") { |value| options[:claude_target] = value }
|
|
244
|
+
opts.on("--force-codex") { options[:force_codex] = true }
|
|
245
|
+
opts.on("--skip-codex") { options[:skip_codex] = true }
|
|
246
|
+
opts.on("--skip-claude") { options[:skip_claude] = true }
|
|
247
|
+
opts.on("--force-claude") { options[:force_claude] = true }
|
|
248
|
+
opts.on("--format FORMAT", %w[text json])
|
|
249
|
+
end
|
|
250
|
+
parser.parse(argv, into: options)
|
|
251
|
+
|
|
252
|
+
codex_detected = Agent.codex_available?
|
|
253
|
+
claude_available = Agent.claude_code_available?
|
|
254
|
+
codex_skill_path = options[:target] ? File.expand_path(options[:target]) : Agent.default_codex_skill_path
|
|
255
|
+
claude_skill_path = options[:claude_target] ? File.expand_path(options[:claude_target]) : Agent.default_claude_skill_path
|
|
256
|
+
|
|
257
|
+
codex_setup, codex_skipped_reason = maybe_install_agent_skill(
|
|
258
|
+
detected: codex_detected,
|
|
259
|
+
force: options[:force_codex],
|
|
260
|
+
skip: options[:skip_codex],
|
|
261
|
+
skip_flag: "--skip-codex",
|
|
262
|
+
unavailable_reason: "Codex was not detected"
|
|
263
|
+
) { Agent.install_codex_skill(options[:target]) }
|
|
264
|
+
claude_setup, claude_skipped_reason = maybe_install_agent_skill(
|
|
265
|
+
detected: claude_available,
|
|
266
|
+
force: options[:force_claude],
|
|
267
|
+
skip: options[:skip_claude],
|
|
268
|
+
skip_flag: "--skip-claude",
|
|
269
|
+
unavailable_reason: "Claude Code was not detected"
|
|
270
|
+
) { Agent.install_claude_skill(options[:claude_target], force: options[:force_claude]) }
|
|
271
|
+
|
|
272
|
+
if options[:format] == "json"
|
|
273
|
+
payload = {
|
|
274
|
+
"status" => "ok",
|
|
275
|
+
"codex" => agent_surface_payload(
|
|
276
|
+
detected: codex_detected,
|
|
277
|
+
setup: codex_setup,
|
|
278
|
+
skill_path: codex_skill_path,
|
|
279
|
+
skipped_reason: codex_skipped_reason
|
|
280
|
+
),
|
|
281
|
+
"claude" => agent_surface_payload(
|
|
282
|
+
detected: claude_available,
|
|
283
|
+
setup: claude_setup,
|
|
284
|
+
skill_path: claude_skill_path,
|
|
285
|
+
skipped_reason: claude_skipped_reason
|
|
286
|
+
),
|
|
287
|
+
"memory" => Agent::MEMORY_SNIPPET,
|
|
288
|
+
"default_prompt" => Agent::DEFAULT_PROMPT,
|
|
289
|
+
"instructions_command" => "lattice agent instructions",
|
|
290
|
+
"generic_skill_command" => "lattice agent install-skill <target>"
|
|
291
|
+
}
|
|
292
|
+
puts JSON.pretty_generate(payload, ascii_only: true)
|
|
293
|
+
return 0
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
print_agent_install_status("Codex", codex_setup, codex_skipped_reason)
|
|
297
|
+
print_agent_install_status("Claude Code", claude_setup, claude_skipped_reason)
|
|
298
|
+
puts ""
|
|
299
|
+
puts "Agent memory:"
|
|
300
|
+
puts Agent::MEMORY_SNIPPET
|
|
301
|
+
puts ""
|
|
302
|
+
puts "Default prompt:"
|
|
303
|
+
puts Agent::DEFAULT_PROMPT
|
|
304
|
+
puts ""
|
|
305
|
+
puts "Any other harness:"
|
|
306
|
+
puts "Run `lattice agent instructions`, or `lattice agent install-skill <target>` if the harness accepts a SKILL.md directory."
|
|
307
|
+
0
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def run_agent_install(argv, _surface)
|
|
311
|
+
options = { format: DEFAULT_AGENT_FORMAT }
|
|
312
|
+
parser = OptionParser.new do |opts|
|
|
313
|
+
opts.on("--target TARGET")
|
|
314
|
+
opts.on("--format FORMAT", %w[text json])
|
|
315
|
+
end
|
|
316
|
+
parser.parse(argv, into: options)
|
|
317
|
+
setup = Agent.install_codex_skill(options[:target])
|
|
318
|
+
emit_agent_setup(setup, "Codex", options[:format])
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def run_agent_install_generic(argv)
|
|
322
|
+
options = { format: DEFAULT_AGENT_FORMAT }
|
|
323
|
+
parser = OptionParser.new do |opts|
|
|
324
|
+
opts.on("--format FORMAT", %w[text json])
|
|
325
|
+
end
|
|
326
|
+
positional = parser.parse(argv, into: options)
|
|
327
|
+
target = positional.first
|
|
328
|
+
if target.nil?
|
|
329
|
+
warn "lattice: `agent install-skill` requires a target directory."
|
|
330
|
+
return 2
|
|
331
|
+
end
|
|
332
|
+
setup = Agent.install_generic_skill(target)
|
|
333
|
+
emit_agent_setup(setup, "generic", options[:format])
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def run_agent_install_claude(argv)
|
|
337
|
+
options = { format: DEFAULT_AGENT_FORMAT }
|
|
338
|
+
parser = OptionParser.new do |opts|
|
|
339
|
+
opts.on("--target TARGET")
|
|
340
|
+
opts.on("--force")
|
|
341
|
+
opts.on("--format FORMAT", %w[text json])
|
|
342
|
+
end
|
|
343
|
+
parser.parse(argv, into: options)
|
|
344
|
+
|
|
345
|
+
begin
|
|
346
|
+
setup = Agent.install_claude_skill(options[:target], force: options[:force] || false)
|
|
347
|
+
rescue RuntimeError => e
|
|
348
|
+
if options[:format] == "json"
|
|
349
|
+
puts JSON.pretty_generate({ "status" => "unavailable", "error" => e.message }, ascii_only: true)
|
|
350
|
+
else
|
|
351
|
+
warn e.message
|
|
352
|
+
end
|
|
353
|
+
return 1
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
emit_agent_setup(setup, "Claude Code", options[:format], extra: { "available" => Agent.claude_code_available? })
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def run_agent_memory(argv)
|
|
360
|
+
options = { format: DEFAULT_AGENT_FORMAT }
|
|
361
|
+
OptionParser.new { |opts| opts.on("--format FORMAT", %w[text json]) }.parse(argv, into: options)
|
|
362
|
+
if options[:format] == "json"
|
|
363
|
+
puts JSON.pretty_generate({ "memory" => Agent::MEMORY_SNIPPET }, ascii_only: true)
|
|
364
|
+
else
|
|
365
|
+
puts Agent::MEMORY_SNIPPET
|
|
366
|
+
end
|
|
367
|
+
0
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def run_agent_instructions(argv)
|
|
371
|
+
options = { format: DEFAULT_AGENT_FORMAT }
|
|
372
|
+
OptionParser.new { |opts| opts.on("--format FORMAT", %w[text json]) }.parse(argv, into: options)
|
|
373
|
+
if options[:format] == "json"
|
|
374
|
+
puts JSON.pretty_generate(
|
|
375
|
+
{
|
|
376
|
+
"instructions" => Agent::GENERIC_AGENT_INSTRUCTIONS,
|
|
377
|
+
"memory" => Agent::MEMORY_SNIPPET,
|
|
378
|
+
"default_prompt" => Agent::DEFAULT_PROMPT
|
|
379
|
+
},
|
|
380
|
+
ascii_only: true
|
|
381
|
+
)
|
|
382
|
+
else
|
|
383
|
+
puts Agent::GENERIC_AGENT_INSTRUCTIONS
|
|
384
|
+
end
|
|
385
|
+
0
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def run_agent_doctor(argv)
|
|
389
|
+
options = { format: DEFAULT_AGENT_FORMAT }
|
|
390
|
+
parser = OptionParser.new do |opts|
|
|
391
|
+
opts.on("--target TARGET")
|
|
392
|
+
opts.on("--claude-target TARGET") { |value| options[:claude_target] = value }
|
|
393
|
+
opts.on("--format FORMAT", %w[text json])
|
|
394
|
+
end
|
|
395
|
+
parser.parse(argv, into: options)
|
|
396
|
+
|
|
397
|
+
codex_path = options[:target] || Agent.default_codex_skill_path
|
|
398
|
+
claude_path = options[:claude_target] || Agent.default_claude_skill_path
|
|
399
|
+
codex_detected = Agent.codex_available?
|
|
400
|
+
codex_installed = Agent.codex_skill_installed?(options[:target])
|
|
401
|
+
claude_available = Agent.claude_code_available?
|
|
402
|
+
claude_installed = Agent.claude_skill_installed?(options[:claude_target])
|
|
403
|
+
known_harness_detected = codex_detected || claude_available
|
|
404
|
+
ok = (!codex_detected || codex_installed) && (!claude_available || claude_installed)
|
|
405
|
+
|
|
406
|
+
if options[:format] == "json"
|
|
407
|
+
puts JSON.pretty_generate(
|
|
408
|
+
{
|
|
409
|
+
"status" => ok ? "ok" : "missing",
|
|
410
|
+
"generic_instructions_available" => true,
|
|
411
|
+
"known_harness_detected" => known_harness_detected,
|
|
412
|
+
"codex_available" => codex_detected,
|
|
413
|
+
"codex_skill_installed" => codex_installed,
|
|
414
|
+
"codex_skill_path" => codex_path.to_s,
|
|
415
|
+
"claude_code_available" => claude_available,
|
|
416
|
+
"claude_skill_installed" => claude_installed,
|
|
417
|
+
"claude_skill_path" => claude_path.to_s
|
|
418
|
+
},
|
|
419
|
+
ascii_only: true
|
|
420
|
+
)
|
|
421
|
+
else
|
|
422
|
+
puts "Lattice agent setup"
|
|
423
|
+
puts "cli: ok"
|
|
424
|
+
puts "generic instructions: available (`lattice agent instructions`)"
|
|
425
|
+
puts "codex: #{codex_detected ? 'available' : 'not detected'}"
|
|
426
|
+
puts "codex skill: #{codex_installed ? 'installed' : 'missing'}"
|
|
427
|
+
puts "codex path: #{codex_path}"
|
|
428
|
+
puts "claude code: #{claude_available ? 'available' : 'not detected'}"
|
|
429
|
+
puts "claude skill: #{claude_installed ? 'installed' : 'missing'}"
|
|
430
|
+
puts "claude path: #{claude_path}"
|
|
431
|
+
end
|
|
432
|
+
ok ? 0 : 1
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def maybe_install_agent_skill(detected:, force:, skip:, skip_flag:, unavailable_reason:)
|
|
436
|
+
return [nil, "skipped by #{skip_flag}"] if skip
|
|
437
|
+
return [yield, nil] if detected || force
|
|
438
|
+
|
|
439
|
+
[nil, unavailable_reason]
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def agent_surface_payload(detected:, setup:, skill_path:, skipped_reason:)
|
|
443
|
+
{
|
|
444
|
+
"available" => detected,
|
|
445
|
+
"installed" => !setup.nil?,
|
|
446
|
+
"skill_path" => (setup ? setup.skill_path : skill_path).to_s,
|
|
447
|
+
"skipped_reason" => skipped_reason
|
|
448
|
+
}
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def emit_agent_setup(setup, label, output_format, extra: nil)
|
|
452
|
+
if output_format == "json"
|
|
453
|
+
puts JSON.pretty_generate({ "status" => "ok" }.merge(extra || {}).merge(setup.to_json_payload), ascii_only: true)
|
|
454
|
+
return 0
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
puts "Installed `#{setup.skill_name}` #{label} skill at #{setup.skill_path}"
|
|
458
|
+
0
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def print_agent_install_status(label, setup, skipped_reason)
|
|
462
|
+
if setup
|
|
463
|
+
puts "Installed `#{setup.skill_name}` #{label} skill at #{setup.skill_path}"
|
|
464
|
+
elsif skipped_reason
|
|
465
|
+
puts "#{label} skill: not installed (#{skipped_reason})"
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|