samagotchi 0.149.0 → 0.151.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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -1
  3. data/README.md +4 -2
  4. data/docs/architecture.md +4 -4
  5. data/docs/bundle-authoring.md +1 -1
  6. data/docs/cli.md +59 -24
  7. data/docs/configuration.md +6 -6
  8. data/docs/internals/background-tasks.md +1 -1
  9. data/docs/internals/block-renderers.md +96 -0
  10. data/docs/internals/bridge.md +53 -9
  11. data/docs/internals/bundles.md +43 -14
  12. data/docs/internals/objectives.md +7 -2
  13. data/docs/internals/web-frontend.md +2 -2
  14. data/docs/internals/web-push.md +2 -2
  15. data/docs/internals/working-set.md +5 -2
  16. data/docs/memory.md +48 -10
  17. data/docs/plugins.md +49 -3
  18. data/docs/sessions.md +5 -4
  19. data/docs/testing.md +5 -0
  20. data/lib/samagotchi/atomic_file.rb +3 -0
  21. data/lib/samagotchi/block_renderers.rb +115 -0
  22. data/lib/samagotchi/bootstrap_command.rb +5 -2
  23. data/lib/samagotchi/bridge/card_places.rb +93 -0
  24. data/lib/samagotchi/bridge/card_store.rb +224 -82
  25. data/lib/samagotchi/bridge.rb +12 -4
  26. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  27. data/lib/samagotchi/bundle_command.rb +91 -68
  28. data/lib/samagotchi/bundler_env.rb +24 -0
  29. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  30. data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
  31. data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
  32. data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
  33. data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
  34. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
  35. data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
  36. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  37. data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
  38. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  39. data/lib/samagotchi/chi_home.rb +4 -2
  40. data/lib/samagotchi/chi_shim.rb +2 -1
  41. data/lib/samagotchi/child_env.rb +10 -3
  42. data/lib/samagotchi/engine.rb +19 -1
  43. data/lib/samagotchi/git_env.rb +43 -0
  44. data/lib/samagotchi/guardrails/context.rb +2 -0
  45. data/lib/samagotchi/guardrails/targets.rb +4 -1
  46. data/lib/samagotchi/memory_bundle/asset_installer.rb +23 -3
  47. data/lib/samagotchi/memory_bundle/builder.rb +5 -3
  48. data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
  49. data/lib/samagotchi/memory_bundle/installed_bundle.rb +70 -6
  50. data/lib/samagotchi/memory_bundle/installed_copy.rb +1 -0
  51. data/lib/samagotchi/memory_bundle/installer.rb +162 -52
  52. data/lib/samagotchi/memory_bundle/listing.rb +4 -3
  53. data/lib/samagotchi/memory_bundle/manifest.rb +23 -2
  54. data/lib/samagotchi/memory_bundle/merger.rb +209 -8
  55. data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
  56. data/lib/samagotchi/memory_bundle/profile.rb +7 -3
  57. data/lib/samagotchi/memory_bundle/provenance.rb +182 -22
  58. data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
  59. data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
  60. data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
  61. data/lib/samagotchi/memory_bundle/source.rb +3 -3
  62. data/lib/samagotchi/memory_bundle/status.rb +21 -3
  63. data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
  64. data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
  65. data/lib/samagotchi/memory_bundle/trash.rb +58 -2
  66. data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
  67. data/lib/samagotchi/objectives_check.rb +37 -7
  68. data/lib/samagotchi/project_env.rb +3 -2
  69. data/lib/samagotchi/self_report.rb +2 -0
  70. data/lib/samagotchi/session_chain.rb +2 -7
  71. data/lib/samagotchi/session_commands.rb +15 -9
  72. data/lib/samagotchi/session_manager.rb +10 -7
  73. data/lib/samagotchi/steer.rb +17 -0
  74. data/lib/samagotchi/terminal_ui.rb +4 -4
  75. data/lib/samagotchi/testing/boundary.rb +1 -0
  76. data/lib/samagotchi/testing/minitest.rb +3 -0
  77. data/lib/samagotchi/testing/rspec.rb +3 -0
  78. data/lib/samagotchi/testing/sandbox.rb +3 -1
  79. data/lib/samagotchi/tools/delegate.rb +2 -1
  80. data/lib/samagotchi/tools/task_runtime.rb +13 -10
  81. data/lib/samagotchi/turn_checkpoint.rb +11 -5
  82. data/lib/samagotchi/turn_flow.rb +2 -1
  83. data/lib/samagotchi/update_command.rb +76 -28
  84. data/lib/samagotchi/update_hint.rb +4 -1
  85. data/lib/samagotchi/version.rb +1 -1
  86. data/lib/samagotchi/web/app.rb +22 -12
  87. data/lib/samagotchi/web/auto_update.rb +8 -8
  88. data/lib/samagotchi/web/auto_update_record.rb +6 -4
  89. data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
  90. data/lib/samagotchi/web/markdown_renderer.rb +39 -6
  91. data/lib/samagotchi/web/message_display.rb +3 -3
  92. data/lib/samagotchi/web/public/app.js +7 -3
  93. data/lib/samagotchi/web/public/block_frame.js +202 -0
  94. data/lib/samagotchi/web/public/block_host.js +502 -0
  95. data/lib/samagotchi/web/public/block_open.js +13 -0
  96. data/lib/samagotchi/web/public/block_renderers.css +20 -0
  97. data/lib/samagotchi/web/public/block_source.js +37 -0
  98. data/lib/samagotchi/web/public/bubbles.js +11 -5
  99. data/lib/samagotchi/web/public/card.js +17 -10
  100. data/lib/samagotchi/web/public/cards_place.js +10 -5
  101. data/lib/samagotchi/web/public/claim_link.js +4 -2
  102. data/lib/samagotchi/web/public/details_motion.js +2 -1
  103. data/lib/samagotchi/web/public/history_reveal.js +4 -1
  104. data/lib/samagotchi/web/public/index.html +20 -14
  105. data/lib/samagotchi/web/public/markdown_place.js +2 -3
  106. data/lib/samagotchi/web/public/stage_model.js +5 -6
  107. data/lib/samagotchi/web/public/stage_view.js +8 -7
  108. data/lib/samagotchi/web/public/text_roots.js +6 -0
  109. data/lib/samagotchi/web/public/turn_block.js +34 -19
  110. data/lib/samagotchi/web/public/turn_html.js +4 -1
  111. data/lib/samagotchi/web/public/turn_model.js +1 -1
  112. data/lib/samagotchi/web/public/update_sheet.js +8 -3
  113. data/lib/samagotchi/web/public/working_set_view.js +10 -7
  114. data/lib/samagotchi/web/push/notifier.rb +7 -9
  115. data/lib/samagotchi/web/session_hub.rb +2 -1
  116. data/lib/samagotchi/web/session_show.rb +1 -1
  117. data/lib/samagotchi/web/update_ops.rb +2 -1
  118. data/lib/samagotchi/worker.rb +4 -2
  119. metadata +17 -1
@@ -15,17 +15,19 @@ module Samagotchi
15
15
  install <source> [--scope system|project] [--force]
16
16
  Install a memory bundle from a directory, zip, tar archive, or git URL.
17
17
  Source is the path/URL to the bundle (dir/zip/tar.gz/git).
18
- --force overwrites existing entries; default skips them.
18
+ --force overwrites existing entries (one that differs is copied to the trash first); default skips them.
19
19
 
20
20
  upgrade <source> [--scope system|project] [--force] [--dry-run] [--agent|--no-agent]
21
- Upgrade a bundle (3-way merge: auto-merge if not edited, conflict otherwise).
21
+ Upgrade a bundle (3-way: a file you didn't edit updates; one you edited takes a text
22
+ merge, the old file in the bundle trash; a real conflict leaves it as it is).
22
23
  --dry-run shows what would change. --agent launches interactive session on conflict.
23
24
 
24
25
  uninstall <bundle> [--scope system|project] [--force]
25
- Remove a bundle and its index entries (--force if locally edited).
26
+ Remove a bundle and its index entries (--force moves a locally edited file to the trash).
26
27
 
27
28
  status [<bundle>]
28
- Show provenance + modification status for bundles.
29
+ Show provenance and each file's status for bundles. issues= counts what
30
+ needs you: a file that stays behind, is missing or can't be read, not one you edited.
29
31
 
30
32
  diff <bundle> [file]
31
33
  Show diffs between base/current/incoming for a bundle.
@@ -66,7 +68,7 @@ module Samagotchi
66
68
  install <source> [--scope system|project] [--force]
67
69
  Install a memory bundle from a directory, zip, tar archive, or git URL,
68
70
  or a bundle shipped with chi by name (see: chi bundle list).
69
- --force overwrites existing entries; default skips them.
71
+ --force overwrites existing entries (one that differs is copied to the trash first); default skips them.
70
72
  install --dev PATH
71
73
  Install the bundle in the local directory PATH as a link to it: its plugin, hooks, rules and scripts
72
74
  are PATH's own files (an edit applies at the next worker or chi web restart, with no re-install) and
@@ -231,6 +233,9 @@ module Samagotchi
231
233
  run_installer(installer, failed: "Upgrade") do |manifest|
232
234
  print_hooks_and_plugin(manifest, plugin: !dry_run)
233
235
  next dry_run_done if dry_run
236
+
237
+ # Its summary said so, to someone reading.
238
+ Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).clear_merge_notes([], files: installer.merged_files)
234
239
  next upgrade_conflicts(installer, bundle_name, expanded_source, agent) if installer.conflicts.any? && !force
235
240
 
236
241
  print_provenance(bundle_name, manifest)
@@ -321,42 +326,64 @@ module Samagotchi
321
326
  # (scripts already read it so): not CLI::Exit::USAGE by meaning.
322
327
  CONFLICTS_KEPT = 2
323
328
 
324
- # @return [Integer] 0 resolved, CONFLICTS_KEPT kept the edits
329
+ # @return [Integer] 0 all resolved, CONFLICTS_KEPT kept some edits
325
330
  def upgrade_conflicts(installer, bundle_name, expanded_source, agent)
326
- @stdout.puts "\n#{installer.conflicts.size} conflict(s) need resolution."
327
- installer.conflicts.each { |k, _| @stdout.puts " conflict: #{k}" }
331
+ standing = Samagotchi::MemoryBundle::FileStanding
332
+ @stdout.puts "\n#{standing.stay_behind(installer.behind.size)}: #{installer.behind.map(&:file).join(", ")}"
328
333
  launch = if agent.nil? && @stdin.tty?
329
- @stdout.print "Conflicts detected — launch interactive agent to resolve? [y/N] "
334
+ @stdout.print "Launch an interactive agent to merge them? [y/N] "
330
335
  ans = begin; @stdin.gets; rescue StandardError => _e; nil; end
331
336
  ans && ans.strip.downcase.start_with?("y")
332
337
  elsif agent.nil?
333
- @stdout.puts "Non-interactive terminal: kept your edits in the file(s) above; the rest is upgraded. Re-run with --force to take the bundle's version, or --agent in a TTY to merge."
338
+ @stdout.puts "Non-interactive terminal: the rest is upgraded. #{standing::FORCE_TAKES}; --agent in a TTY merges."
334
339
  return CONFLICTS_KEPT
335
340
  else
336
341
  agent
337
342
  end
338
343
  unless launch
339
- @stdout.puts "Kept your edits in the file(s) above; the rest is upgraded. chi bundle diff #{bundle_name} FILE shows the base; re-run with --force to take the bundle's version."
344
+ @stdout.puts "The rest is upgraded. #{standing.diff_hint(bundle_name, "FILE")} shows the base; #{standing::FORCE_TAKES}."
340
345
  return CONFLICTS_KEPT
341
346
  end
342
347
 
343
348
  prompt = build_conflict_prompt(bundle_name, installer.conflicts, expanded_source)
344
- @stdout.puts "Launching interactive session for conflict resolution… (/exit when done)"
349
+ before = installer.conflicts.to_h { |key, info| [key, Samagotchi::MemoryBundle::Merger.digest(info[:current])] }
350
+ @stdout.puts "Launching an interactive session to merge them… (/exit when done)"
345
351
  require "samagotchi/terminal_ui"
346
352
  Samagotchi::TerminalUI.new(prompt: prompt).run
347
- # The installer already recorded the upgrade (conflicted files kept
348
- # their old base); the resolved files now start from the bundle's
349
- # version.
350
- Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).resolve_conflicts(installer.conflicts)
351
- @stdout.puts "Provenance updated after interactive resolution."
352
- @stdout.puts "Upgrade resolved interactively."
353
- 0
353
+ settle_conflicts(installer.conflicts, before, bundle_name)
354
354
  ensure
355
355
  # An owned source (git/zip/tar) was kept for the incoming files above;
356
356
  # the agent step is done with them now.
357
357
  installer.cleanup_source!
358
358
  end
359
359
 
360
+ # After the agent step. The installer already recorded the upgrade
361
+ # (conflicted files kept their old base); a file the step changed, with
362
+ # no conflict markers left, now starts from the bundle's version. Any
363
+ # other stays a conflict: moving its base would turn a visible miss into
364
+ # a kept edit the next upgrade says nothing about.
365
+ # @param before [Hash{String => String, nil}] each file's digest before the step
366
+ # @return [Integer] 0 all resolved, CONFLICTS_KEPT some still behind
367
+ def settle_conflicts(conflicts, before, bundle_name)
368
+ merger = Samagotchi::MemoryBundle::Merger
369
+ untouched, changed = conflicts.partition { |key, info| merger.digest(info[:current]) == before[key] }
370
+ marked, clean = changed.partition { |_, info| merger.markers?(info[:current]) }
371
+ settled = Samagotchi::MemoryBundle::Provenance.new(name: bundle_name).resolve_conflicts(clean.to_h)
372
+ gone = clean.map(&:first) - settled
373
+ standing = Samagotchi::MemoryBundle::FileStanding
374
+ untouched.each { |key, _| @stdout.puts standing.still_behind(:untouched, key) }
375
+ marked.each { |key, _| @stdout.puts standing.still_behind(:markers, key) }
376
+ gone.each { |key| @stdout.puts standing.still_behind(:incoming_gone, key) }
377
+ if untouched.empty? && marked.empty? && gone.empty?
378
+ @stdout.puts "Provenance updated after interactive resolution."
379
+ @stdout.puts "Upgrade resolved interactively."
380
+ return 0
381
+ end
382
+
383
+ @stdout.puts "Resolved: #{settled.join(", ")}" unless settled.empty?
384
+ CONFLICTS_KEPT
385
+ end
386
+
360
387
  def uninstall(rest)
361
388
  parsed = parse_flags(UNINSTALL_FLAGS, rest, "uninstall", help: UNINSTALL_HELP)
362
389
  return parsed if parsed.is_a?(Integer)
@@ -377,6 +404,7 @@ module Samagotchi
377
404
  uninstaller.run
378
405
  @stdout.puts "Uninstalled bundle '#{bundle_name}'"
379
406
  puts_trashed(uninstaller.trashed_files, uninstaller.trash_dir)
407
+ @stdout.puts uninstaller.trash_copy_line if uninstaller.trash_copy_line
380
408
  @stdout.puts "Removed: #{uninstaller.removed_files.join(", ")}" unless uninstaller.removed_files.empty?
381
409
  hook_removed = uninstaller.removed_files.count { |f| f.start_with?("hooks/") }
382
410
  @stdout.puts "Hooks removed: #{hook_removed}" if hook_removed > 0
@@ -402,6 +430,7 @@ module Samagotchi
402
430
  result.skipped.each { |member, why| lines << "skipped #{member}: #{why}" }
403
431
  result.failed.each { |member, why| lines << "failed #{member}: #{why}" }
404
432
  lines.concat(result.kept_own_lines)
433
+ lines.concat(result.trash_copies)
405
434
  lines << "nothing new to install" if lines.empty?
406
435
  lines.each { |line| @stdout.puts " #{line}" }
407
436
  @stdout.puts "(dry-run: no changes written)" if dry_run
@@ -453,16 +482,21 @@ module Samagotchi
453
482
  else
454
483
  @stdout.puts "Target: #{st.target_dir}"
455
484
  end
485
+ standing = Samagotchi::MemoryBundle::FileStanding
456
486
  st.files.each do |k, info|
457
487
  next @stdout.puts(" #{k}: unchecked") if info[:unchecked]
458
488
 
459
489
  mods = []
460
- mods << "conflict (kept your edits over v#{st.provenance.version}: chi bundle diff #{name} #{k})" if info[:conflict]
461
490
  mods << "modified" if info[:modified]
462
491
  mods << "missing" if info[:missing]
492
+ mods << "unreadable" if info[:unreadable]
463
493
  mods << "no-index" unless info[:index_present] || info[:overlay]
464
- label = mods.empty? ? "ok" : mods.join(",")
494
+ label = mods.join(",")
495
+ label = "ok" if mods.empty? && !info[:behind]
465
496
  label += " (model overlay)" if info[:overlay]
497
+ behind = info[:behind] && "#{info[:behind].state} (#{standing.diff_hint(name, k)})"
498
+ label = [label, behind].reject { |part| part.nil? || part.empty? }.join("; ")
499
+ label += "; #{info[:merge_note].line}#{" (old copy in #{info[:merge_note].trash})" if info[:merge_note].trash}" if info[:merge_note]
466
500
  @stdout.puts " #{k}: #{label}"
467
501
  end
468
502
  # Hooks
@@ -499,7 +533,7 @@ module Samagotchi
499
533
  next @stdout.puts(" #{bname} (manifest.json unreadable)") if record.error?
500
534
 
501
535
  st = Samagotchi::MemoryBundle::Status.bundle_status(bname)
502
- mods = st.files.values.count { |v| v[:modified] || v[:missing] }
536
+ mods = st.files.values.count { |v| v[:behind] || v[:missing] || v[:unreadable] }
503
537
  hooks_count = st.provenance.hooks.size
504
538
  hook_info = hooks_count > 0 ? " hooks=#{hooks_count}" : ""
505
539
  plugin = st.plugin
@@ -543,77 +577,62 @@ module Samagotchi
543
577
  rescue ArgumentError => e
544
578
  @stderr.puts "Bundle '#{bname}': #{e.message}"; return 1
545
579
  end
546
- files = record.files
547
580
  hooks = record.hooks
548
581
  plugin_path = prov.plugin_path(record)
549
582
  plugin_file = plugin_path && File.basename(plugin_path)
550
- show_plugin = lambda do
551
- @stdout.puts "=== plugin/#{plugin_file} ==="
552
- @stdout.puts "--- base (provenance) ---"
553
- base = prov.plugin_base_path(plugin_file)
554
- @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
555
- @stdout.puts "--- current (on-disk) ---"
556
- @stdout.puts File.exist?(plugin_path) ? File.read(plugin_path) : "(missing)"
557
- @stdout.puts "--- metadata: sha256=#{record.plugin.sha256}#{" requires_chi=#{record.requires_chi}" if record.requires_chi}"
558
- end
559
583
  if file_arg
560
584
  # Try file first, then hook
561
585
  if record.owns?(file_arg)
562
- base = prov.base_path(file_arg)
563
- cur = File.join(target_dir, file_arg)
564
- @stdout.puts "=== #{file_arg} ==="
565
- @stdout.puts "--- base (provenance) ---"
566
- @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
567
- @stdout.puts "--- current (on-disk) ---"
568
- @stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
586
+ diff_memory(prov, target_dir, file_arg)
569
587
  elsif hooks.key?(file_arg)
570
- base = prov.base_path(file_arg)
571
- cur = File.join(prov.hooks_dir, file_arg)
572
- @stdout.puts "=== hooks/#{file_arg} ==="
573
- @stdout.puts "--- base (provenance) ---"
574
- @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
575
- @stdout.puts "--- current (on-disk) ---"
576
- @stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
577
- # Show hook metadata
578
- @stdout.puts hook_metadata_line(hooks[file_arg])
588
+ diff_hook(prov, file_arg, hooks[file_arg])
579
589
  elsif plugin_file && [plugin_file, "plugin/#{plugin_file}"].include?(file_arg)
580
- show_plugin.call
590
+ diff_plugin(prov, record, plugin_path)
581
591
  else
582
592
  @stdout.puts "=== #{file_arg} ==="
583
593
  @stdout.puts "(not found in bundle)"
584
594
  end
585
595
  @stdout.puts ""
586
596
  else
587
- files.each_key do |k|
588
- base = prov.base_path(k)
589
- cur = File.join(target_dir, k)
590
- @stdout.puts "=== #{k} ==="
591
- @stdout.puts "--- base (provenance) ---"
592
- @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
593
- @stdout.puts "--- current (on-disk) ---"
594
- @stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
597
+ record.files.each_key do |key|
598
+ diff_memory(prov, target_dir, key)
595
599
  @stdout.puts ""
596
600
  end
597
- # Hooks diff
598
- hooks.each do |k, hook|
599
- base = prov.base_path(k)
600
- cur = File.join(prov.hooks_dir, k)
601
- @stdout.puts "=== hooks/#{k} ==="
602
- @stdout.puts "--- base (provenance) ---"
603
- @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
604
- @stdout.puts "--- current (on-disk) ---"
605
- @stdout.puts File.exist?(cur) ? File.read(cur) : "(missing)"
606
- @stdout.puts hook_metadata_line(hook)
601
+ hooks.each do |key, hook|
602
+ diff_hook(prov, key, hook)
607
603
  @stdout.puts ""
608
604
  end
609
605
  if plugin_file
610
- show_plugin.call
606
+ diff_plugin(prov, record, plugin_path)
611
607
  @stdout.puts ""
612
608
  end
613
609
  end
614
610
  0
615
611
  end
616
612
 
613
+ def diff_memory(prov, target_dir, key)
614
+ print_base_and_current(key, prov.base_path(key), File.join(target_dir, key))
615
+ end
616
+
617
+ def diff_hook(prov, key, hook)
618
+ print_base_and_current("hooks/#{key}", prov.base_path(key), File.join(prov.hooks_dir, key))
619
+ @stdout.puts hook_metadata_line(hook)
620
+ end
621
+
622
+ def diff_plugin(prov, record, plugin_path)
623
+ file = File.basename(plugin_path)
624
+ print_base_and_current("plugin/#{file}", prov.plugin_base_path(file), plugin_path)
625
+ @stdout.puts "--- metadata: sha256=#{record.plugin.sha256}#{" requires_chi=#{record.requires_chi}" if record.requires_chi}"
626
+ end
627
+
628
+ def print_base_and_current(heading, base, current)
629
+ @stdout.puts "=== #{heading} ==="
630
+ @stdout.puts "--- base (provenance) ---"
631
+ @stdout.puts File.exist?(base) ? File.read(base) : "(no base)"
632
+ @stdout.puts "--- current (on-disk) ---"
633
+ @stdout.puts File.exist?(current) ? File.read(current) : "(missing)"
634
+ end
635
+
617
636
  def list
618
637
  listing = Samagotchi::MemoryBundle::Listing
619
638
  shipped = listing.shipped
@@ -634,6 +653,10 @@ module Samagotchi
634
653
  line = " #{b.name.ljust(name_w)} #{"v#{b.version || "?"}".ljust(ver_w)} scope=#{b.scope || "?"} " \
635
654
  "#{b.includes ? profile_members(b.includes, installed) : "files=#{b.files}"} installed=#{b.installed_at || "?"}"
636
655
  line += " (shipped v#{b.upgrade.version}: chi bundle upgrade #{b.upgrade.source})" if b.upgrade
656
+ if b.behind&.any?
657
+ count = Samagotchi::MemoryBundle::FileStanding.behind_count(b.behind.size, unsure: b.behind.any?(&:unsure))
658
+ line += " (#{count}: chi bundle status #{b.name})"
659
+ end
637
660
  @stdout.puts line
638
661
  end
639
662
  @stdout.puts " (or all at once: chi update)" if installed.any?(&:upgrade)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samagotchi
4
+ # Bundler's variables, in one place. Every chi spawn that must not carry a
5
+ # `bundle exec` around it takes its names from here.
6
+ module BundlerEnv
7
+ # All Bundler's own and its settings (BUNDLE_PATH, BUNDLE_WITHOUT, ...), for
8
+ # what must take none: a pattern's start (ChiShim, ProjectEnv::RESTORE_NEVER).
9
+ PREFIX = "BUNDLE"
10
+ # What `bundle exec` sets for a child (Bundler::SharedHelpers#set_bundle_variables;
11
+ # bundler_env_spec asks it). Without them, and without Bundler's RUBYOPT, a
12
+ # child ruby is free of the bundle; BUNDLER_SETUP on its own isn't: RubyGems
13
+ # requires what it names at boot, and a Gemfile it then finds writes its lock
14
+ # to BUNDLE_LOCKFILE, the bundle's. A project's own settings (BUNDLE_PATH and
15
+ # the like) aren't here: its `bundle` keeps them. Nor is BUNDLER_ORIG_*, which
16
+ # also reaches a tool: a project's `bundle exec` there reads it, for
17
+ # Bundler.original_env and with_unbundled_env, whose children get the PATH and
18
+ # GEM_* chi's own `bundle exec` started from.
19
+ VARS = %w[BUNDLE_BIN_PATH BUNDLE_GEMFILE BUNDLE_LOCKFILE BUNDLER_SETUP BUNDLER_VERSION].freeze
20
+ # With what else `bundle exec` steers a child ruby by: what a command that
21
+ # isn't the bundle's runs without (TaskRuntime, ProjectEnv, ChiHome.probe).
22
+ CHILD_RUBY = (%w[RUBYOPT RUBYLIB] + VARS).freeze
23
+ end
24
+ end
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: dev
3
- version: 0.7.5
3
+ version: 0.7.6
4
4
  scope: system
5
5
  description: "Developer tools"
6
6
  trust_level: reviewed
7
- requires_chi: ">= 0.149.0"
7
+ requires_chi: ">= 0.150.0"
8
8
  includes:
9
9
  - known-names
10
10
  - mcp
@@ -1,17 +1,17 @@
1
1
  ---
2
2
  name: github-pr
3
- version: 0.6.2
3
+ version: 0.6.3
4
4
  scope: system
5
5
  description: Proposes the branch's open GitHub pull request to a session, attached once you mention it (#N, the branch or its URL), and attaches any PR URL with chi context add or the web's "+ URL"; the agent gets a note when reviews, comments, checks or its state change; in the web, a path:line in an answer links to that line of the PR; the auto_attach setting can attach the PR at once instead, or do nothing
6
6
  trust_level: reviewed
7
- requires_chi: ">= 0.140.0"
7
+ requires_chi: ">= 0.150.0"
8
8
  needs:
9
9
  - command: gh
10
10
  why: reads the pull request
11
11
  hint: brew install gh, then gh auth login
12
12
  plugin:
13
13
  file: plugin.rb
14
- sha256: sha256:114e999c49d4d427b7570e3f4bec8db5565ee0dd1a99f2f5ec3000748a1aec3a
14
+ sha256: sha256:82f8c76d38f314c3503eef3a5de0f8c0b20d0bb5bf4a3daebca43b60d00f8c7a
15
15
  scripts:
16
16
  pr_context.rb: sha256:a883d2780e89c68c0d7b6d5e0fcb22c273f17bb167e79064e0e99db6bd2ee6f0
17
17
  context_providers:
@@ -5,6 +5,7 @@ require "json"
5
5
  require "open3"
6
6
  require "time"
7
7
  require "tmpdir"
8
+ require "samagotchi/git_env"
8
9
 
9
10
  # github-pr: a session on a branch with an open GitHub PR gets the PR
10
11
  # proposed as a candidate (docs/context.md, Candidates) when its worker
@@ -334,7 +335,7 @@ class Plugin
334
335
 
335
336
  # stdout of a command that succeeded within COMMAND_SECONDS, else nil.
336
337
  def run(ctx, *command, chdir: ctx.cwd)
337
- Open3.popen2(*command, chdir: chdir, err: File::NULL, pgroup: true) do |stdin, stdout, wait|
338
+ Open3.popen2(Samagotchi::GitEnv::UNSET, *command, chdir: chdir, err: File::NULL, pgroup: true) do |stdin, stdout, wait|
338
339
  stdin.close
339
340
  reader = Thread.new { stdout.read }
340
341
  unless wait.join(COMMAND_SECONDS)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "samagotchi/git_env"
4
+
3
5
  # A before_tool_call guard against near-misses of protected identifiers in
4
6
  # a tool call's command or paths: the user's home folder name, login, git
5
7
  # name and email, the repo name, and any names from config. A local model
@@ -187,7 +189,7 @@ class KnownNames
187
189
  end
188
190
 
189
191
  def git_config(root, key)
190
- IO.popen(["git", "-C", root, "config", "--get", key], err: File::NULL, &:read).to_s.strip
192
+ IO.popen(Samagotchi::GitEnv::UNSET, ["git", "-C", root, "config", "--get", key], err: File::NULL, &:read).to_s.strip
191
193
  rescue StandardError
192
194
  ""
193
195
  end
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: known-names
3
- version: 0.1.8
3
+ version: 0.1.9
4
+ requires_chi: ">= 0.150.0"
4
5
  scope: system
5
6
  description: Catches a near-miss of a protected name (home folder, login, git name, repo name, names from config) in a tool call and rejects it with the right spelling; mode ask / correct optional
6
7
  trust_level: reviewed
@@ -8,7 +9,7 @@ files:
8
9
  known_names.md: sha256:d5348b95b6c655017b541910b36e384220da280818160201f6ec9a50e1e4287c
9
10
  hooks:
10
11
  known_names.rb:
11
- sha256: sha256:a851637e9292f825dc5b8877cd060184de976aa4297b114dab412a89fa3cf091
12
+ sha256: sha256:d7eed0dd0f418d8099a147ed124c6e6062b8c752035dd6b5aa5e926e29941c8b
12
13
  event: before_tool_call
13
14
  on_error: log
14
15
  priority: 50
@@ -2,6 +2,7 @@
2
2
 
3
3
  # Loaded through module_eval: it requires what it uses.
4
4
  require "open3"
5
+ require "samagotchi/git_env"
5
6
  require "samagotchi/ref"
6
7
 
7
8
  # An after_turn (and question_asked) hook that links the source refs the
@@ -621,8 +622,7 @@ class SourceLinks
621
622
  end
622
623
 
623
624
  def read_remote(dir, name)
624
- out, status = Open3.capture2({ "GIT_DIR" => nil, "GIT_WORK_TREE" => nil },
625
- "git", "-C", dir, "remote", "get-url", name, err: File::NULL)
625
+ out, status = Open3.capture2(Samagotchi::GitEnv::UNSET, "git", "-C", dir, "remote", "get-url", name, err: File::NULL)
626
626
  unless status.success?
627
627
  Samagotchi::Log.debug(:hooks, "source_links_no_remote", remote: name, exit: status.exitstatus)
628
628
  return nil
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: source-links
3
- version: 0.6.2
3
+ version: 0.6.3
4
4
  scope: system
5
5
  description: Links source refs (JIRA tickets, GitHub issues, …) in the model's answer — inline links in the web, refs in a waiting question's reply, and a one-line note after the turn (note false turns it off); sources are configured patterns
6
6
  trust_level: reviewed
7
- requires_chi: ">= 0.98.0"
7
+ requires_chi: ">= 0.150.0"
8
8
  files:
9
9
  source_links.md: sha256:ace01a0f7c308a0ae54ece714c31c0e6d7a49c372d98111030e6d268320bed3a
10
10
  hooks:
11
11
  source_links.rb:
12
- sha256: sha256:7e3fafb843f1a9006b1a42f4ec647b41a358d814515cd0be73b60eb08b898992
12
+ sha256: sha256:21d7a9f7c381c653b126b625feed0e0b6422eaa84c8dfe716b6c5b41644a33b3
13
13
  events: [after_turn, question_asked]
14
14
  on_error: log
15
15
  priority: 90
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.149.0
3
+ version: 0.151.0
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
7
7
  identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
8
- self_map.md: sha256:c9ece2d56c63fb0f9842963cb0b123e89abadec888645d7a765a2578ceefee3a
8
+ self_map.md: sha256:e695f78f858c29ccfef2ed6f9d6dd86d0ec067ed704a5a1956c01e53617948a1
9
9
  config_modification_protocol.md: sha256:8896d1d364ce835040f69b504518d43dbdf959b93fdd9b10f87a8de61cae84a8
10
- memory_guide.md: sha256:64c3fa0703a3bdd8ae33b9e7cb341caa88a205c289d767f4877b38dfa6629fda
10
+ memory_guide.md: sha256:bfe9f13e8a6c929056fe531c4c74c7e05d0f17f025191c1a111c8e7dfd224fd6
11
11
  delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
@@ -114,9 +114,9 @@ Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and
114
114
  | Command | Purpose |
115
115
  |---------|---------|
116
116
  | `install <source> [--scope system\|project] [--force]` | Install from dir/zip/tar.gz/git URL. `--force` overwrites existing entries, otherwise skips them; a skipped file stays the user's (not recorded, never upgraded or removed by the bundle). Writes provenance to `.bundles/<name>/` (base snapshots + `manifest.json`) and updates `index.md`. Warns on double-brace placeholders and checksum mismatches (strict mode). |
117
- | `upgrade <source> [--force] [--dry-run] [--agent]` | 3-way merge upgrade (base vs current vs incoming) per `Merger.classify`: `install` (new file), `noop` (current==incoming), `fast_forward` (current==base, not edited → auto-update), `keep` (incoming==base → preserve local edits), `conflict` (both edited → warn, needs `--force` or interactive `memory_write` resolution). Pruned files (removed from new bundle) are kept if locally edited, otherwise warned. |
117
+ | `upgrade <source> [--force] [--dry-run] [--agent]` | 3-way merge upgrade (base vs current vs incoming) per `Merger::Upgrade#outcome` (`Merger.classify`, then a text merge where both sides changed): `install` (new file), `noop` (current==incoming), `fast_forward` (current==base, not edited → auto-update), `keep` (incoming==base → preserve local edits), `merged` (both edited, a text merge with `git` comes out clean → the file takes it, the old one goes to the bundle trash, and entries both sides added under a `Changelog` heading are kept), `conflict` (both edited, a real conflict, or no `git` or base → the file stays untouched and marked, reported as `X stays behind 0.8.0: …` with why and what it missed, needs a retry, `--force`, which copies it to the trash first, or interactive `memory_write` resolution). A marked file is retried at the same version: the system bundle's at each chi start and on `chi update`, another shipped bundle's on `chi update`, any other bundle's on `chi bundle upgrade`. A file marked by an older chi merges normally when its recorded base is a version chi shipped (an installed samagotchi gem still has it); otherwise it merges only if the bundle takes out or rewords no line it still holds, and if it does, it stays behind until resolved by hand, with `--agent` or with `--force`, and the retry can't fix that. Pruned files (removed from new bundle) are kept if locally edited, otherwise warned. |
118
118
  | `uninstall <bundle> [--force]` | Moves the bundle's memory files to `.bundles/.trash/<bundle>-<time>/` (refuses locally edited ones unless `--force`; keeps a file another installed bundle also lists), removes their `index.md` lines, deletes the provenance dir. |
119
- | `status [<bundle>]` | Provenance + per-file `ok|modified|missing|no-index` vs stored checksum and base snapshot; a model overlay (`<name>.<key>.md`) has no index line and reads `ok (model overlay)`. |
119
+ | `status [<bundle>]` | Provenance + per-file `ok|modified|missing|unreadable|no-index` vs stored checksum and base snapshot, and what stays behind or merged at a chi start; `issues=` counts files behind, missing or unreadable, not edits; a model overlay (`<name>.<key>.md`) has no index line and reads `ok (model overlay)`. |
120
120
  | `diff <bundle> [file]` | Prints `base` (provenance snapshot) vs `current` (on-disk) for each file. |
121
121
  | `list` | Lists installed bundles (`name v<version> scope files installed_at`, plus the shipped version when newer) and the bundles shipped with chi that are not installed (`install <name>` installs one), grouped under their profile; an installed profile shows `includes=` and `left out=`. |
122
122
  | `build [--scope system\|project] [--name NAME] [--version VER] [--description DESC] [--out PATH] [FILES...]` | **Inverse of install** — builds a shareable bundle from local memories and installed hooks. Infers `zip` vs `dir`/`tar.gz` from `--out` extension; default `chi_system_memories.zip` (system) or `chi_<repo>_memories.zip` (project, named after the project root) v`1.0.0` in `Dir.pwd`. `FILES...` is an optional allowlist of memory basenames (`identity` or `identity.md`); if omitted, all `*.md` except `index.md`/hidden/non-md and the memories other installed bundles own (each named in a `Left out` line) are included. Installed hooks are copied to `hooks/` with their manifest metadata. Computes `sha256:` checksums and writes `manifest.yml` via `Manifest.write`. No provenance write. |
@@ -138,7 +138,7 @@ chi bundle install https://github.com/org/bundle.git#v1.2.0 --scope system --for
138
138
 
139
139
  # check what would change and upgrade
140
140
  chi bundle upgrade ./my-bundle --dry-run
141
- chi bundle upgrade ./my-bundle # auto-merges, warns on conflicts
141
+ chi bundle upgrade ./my-bundle # merges your edits, warns on a real conflict
142
142
 
143
143
  # share again after editing
144
144
  chi bundle build --scope system --out updated.zip
@@ -155,9 +155,9 @@ chi bundle build --scope system --out updated.zip
155
155
  2. **Prefer project scope** for repo decisions; `system` for cross-project identity/preferences.
156
156
  3. **One concept per file:** small files merge and share better than monoliths.
157
157
  4. **Use bundles for sharing:** `build` → zip → share → `install`. Do not copy raw `~/.config` paths in docs — give `chi bundle install <url>` instructions.
158
- 5. **Respect local edits:** installs skip existing files by default; use `--force` only when the user explicitly wants overwrite. Upgrades preserve edits (`keep`) or report `conflict` — guide the user to resolve via `memory_read`/`memory_write` or `chi bundle diff <bundle>` + `--force`.
158
+ 5. **Respect local edits:** installs skip existing files by default; use `--force` only when the user explicitly wants overwrite. Upgrades preserve edits (`keep`, `merged`) or report `conflict`, leaving the file behind — guide the user to resolve via `memory_read`/`memory_write` or `chi bundle diff <bundle>` + `--force`.
159
159
  6. **Keep secrets out:** never write tokens/keys to memories — they are plain files and go into bundles.
160
160
 
161
161
  ## Current system bundle
162
162
 
163
- `samagotchi-system` (`lib/samagotchi/bundles/system/manifest.yml`) ships `identity.md` + `self_map.md` + `config_modification_protocol.md` + `delegated.md` + this guide itself. It is auto-installed/upgraded on first `Engine` creation (`SystemBundle.ensure!`) — no manual install needed. Its files are managed by chi: a newer chi upgrades them (a 3-way merge keeps local edits and reports conflicts), so put your own preferences in separate memories rather than editing these.
163
+ `samagotchi-system` (`lib/samagotchi/bundles/system/manifest.yml`) ships `identity.md` + `self_map.md` + `config_modification_protocol.md` + `delegated.md` + this guide itself. It is auto-installed/upgraded on first `Engine` creation (`SystemBundle.ensure!`) — no manual install needed. Its files are managed by chi: a newer chi upgrades them (a text merge keeps local edits, and a real conflict leaves the file behind), so put your own preferences in separate memories rather than editing these.
@@ -83,7 +83,7 @@
83
83
  `file:line`. Read the source `chi self` reports, not some other checkout on disk.
84
84
  - Installed gem files and bundled memories (`identity`, `memory_guide`,
85
85
  `config_modification_protocol`, `self_map`, `delegated`) are managed: don't edit them
86
- (local edits conflict on upgrade).
86
+ (an upgrade merges local edits when it can, and a real conflict leaves the file behind).
87
87
  Put notes in my own memories. Guidance for some models only is a model note
88
88
  (`model_notes_<name>`, first line `models: <glob>`; memory_guide); a model-only
89
89
  addition to one memory is an overlay on it (`current_model_only: true`), and it
@@ -6,6 +6,7 @@ require "open3"
6
6
  require "rbconfig"
7
7
  require "time"
8
8
  require_relative "atomic_file"
9
+ require_relative "bundler_env"
9
10
  require_relative "installed_gem"
10
11
  require_relative "log"
11
12
  require_relative "paths"
@@ -33,8 +34,9 @@ module Samagotchi
33
34
  # Where `mise` usually is when PATH doesn't have it (launchd's bare PATH).
34
35
  MISE_DIRS = ["/opt/homebrew/bin", "/usr/local/bin", File.join(Dir.home, ".local", "bin")].freeze
35
36
  # What a probe of another Ruby must not inherit: they would move it off
36
- # its own gems.
37
- PROBE_UNSET = %w[GEM_HOME GEM_PATH RUBYOPT RUBYLIB BUNDLE_GEMFILE BUNDLE_BIN_PATH].freeze
37
+ # its own gems. BUNDLER_SETUP alone does: RubyGems requires what it names at
38
+ # boot, and a bundle with a path then replaces GEM_HOME and GEM_PATH.
39
+ PROBE_UNSET = (%w[GEM_HOME GEM_PATH] + BundlerEnv::CHILD_RUBY).freeze
38
40
  PROBE = "require 'json'; print JSON.generate('ruby' => RbConfig.ruby, 'gem_home' => Gem.dir, " \
39
41
  "'gem_path' => Gem.path.join(File::PATH_SEPARATOR))"
40
42
  GEMSPEC = /\Asamagotchi-(#{Gem::Version::VERSION_PATTERN})\.gemspec\z/
@@ -3,6 +3,7 @@
3
3
  require "fileutils"
4
4
  require "shellwords"
5
5
  require_relative "atomic_file"
6
+ require_relative "bundler_env"
6
7
  require_relative "chi_home"
7
8
  require_relative "log"
8
9
  require_relative "paths"
@@ -25,7 +26,7 @@ module Samagotchi
25
26
  # ruby: unset before the exec.
26
27
  UNSET = "unset RUBYOPT RUBYLIB RUBYGEMS_GEMDEPS GEMRC LD_PRELOAD LD_LIBRARY_PATH DYLD_INSERT_LIBRARIES " \
27
28
  "DYLD_LIBRARY_PATH\n" \
28
- "for v in $(/usr/bin/env | /usr/bin/sed -n 's/^\\(BUNDLE[A-Za-z0-9_]*\\)=.*/\\1/p'); do unset \"$v\"; done\n"
29
+ "for v in $(/usr/bin/env | /usr/bin/sed -n 's/^\\(#{BundlerEnv::PREFIX}[A-Za-z0-9_]*\\)=.*/\\1/p'); do unset \"$v\"; done\n"
29
30
 
30
31
  module_function
31
32
 
@@ -20,16 +20,23 @@ module Samagotchi
20
20
  # (Tools::Builtins.parent_env): that session's, not a child chi's.
21
21
  SESSION_MARKERS = %w[SAMAGOTCHI_PARENT_SESSION SAMAGOTCHI_SESSION_MODEL SAMAGOTCHI_SESSION_THINKING
22
22
  SAMAGOTCHI_SESSION_REASONING_HISTORY].freeze
23
+ # "1" in the chi update an automatic run starts (Web::UpdateOps): no person
24
+ # reads its rows. An env marker, so it reaches the chi a hand-over or the home
25
+ # execs, and an older chi that doesn't know it simply ignores it.
26
+ UNATTENDED_UPDATE = "SAMAGOTCHI_UPDATE_UNATTENDED"
23
27
 
24
28
  module_function
25
29
 
26
30
  # The keys of +env+ a child chi must not inherit: the guardrails
27
31
  # settings, the session markers, a smoke's fake "newest installed"
28
- # (InstalledVersions::ENV_KEY, for the spawner's notices only) and the
29
- # worker's project env marker and record (a child worker applies its own).
32
+ # (InstalledVersions::ENV_KEY, for the spawner's notices only), the
33
+ # worker's project env marker and record (a child worker applies its own) and
34
+ # an automatic update's marker (a session someone starts from one isn't it; the
35
+ # update's own hand-over execs, which this doesn't touch).
30
36
  # @return [Array<String>]
31
37
  def spawner_keys(env = ENV)
32
- env.each_key.select { |key| key.start_with?(GUARDRAILS_PREFIX) } + SESSION_MARKERS + [InstalledVersions::ENV_KEY, ProjectEnv::MARKER_ENV, ProjectEnv::ORIG_ENV]
38
+ env.each_key.select { |key| key.start_with?(GUARDRAILS_PREFIX) } + SESSION_MARKERS +
39
+ [InstalledVersions::ENV_KEY, ProjectEnv::MARKER_ENV, ProjectEnv::ORIG_ENV, UNATTENDED_UPDATE]
33
40
  end
34
41
 
35
42
  # #spawner_keys mapped to nil: Process.spawn unsets a key mapped to nil.
@@ -1107,6 +1107,8 @@ module Samagotchi
1107
1107
  # @raise [ArgumentError] a card without a title, or a bad action
1108
1108
  def show_card(source:, title:, body: "", actions: [], level: :info, id: nil)
1109
1109
  card = Plugin::Card.build(source: source, title: title, body: body, actions: actions, level: level, id: id)
1110
+ command = Thread.current[running_command_key]
1111
+ card[:command] = command if command
1110
1112
  @event_routes.route(card, **EventRoutes::CARD_MARKS)
1111
1113
  card[:id]
1112
1114
  end
@@ -1126,6 +1128,20 @@ module Samagotchi
1126
1128
  Thread.current[key] = outer
1127
1129
  end
1128
1130
 
1131
+ # Run a session command's dispatch: a card shown on this thread says
1132
+ # it's that command's (:card's command:), for a reload that no longer has
1133
+ # the command's line above it (Bridge::CardStore). A thread of its own
1134
+ # isn't the command, nor a card shown after it.
1135
+ # @param name [String] the command, "/short"
1136
+ # @return the block's value
1137
+ def running_command(name)
1138
+ outer = Thread.current[running_command_key]
1139
+ Thread.current[running_command_key] = name
1140
+ yield
1141
+ ensure
1142
+ Thread.current[running_command_key] = outer
1143
+ end
1144
+
1129
1145
  # Whether an anytime command (/btw) still runs on its thread.
1130
1146
  def anytime_running?
1131
1147
  @lifecycle_mutex.synchronize { @anytime_threads.any?(&:alive?) }
@@ -2266,7 +2282,7 @@ module Samagotchi
2266
2282
 
2267
2283
  messages = @session&.messages || []
2268
2284
  covered = state[:covered].to_i
2269
- turns_since = Array(messages).drop(covered).count { |m| Steer.turn_prompt?(m) }
2285
+ turns_since = Array(messages).drop(covered).count { |m| Steer.turn_opening?(m) }
2270
2286
  { text: state[:text], covered: covered, turns_since: turns_since, created_at: state[:created_at] }
2271
2287
  end
2272
2288
 
@@ -2974,6 +2990,8 @@ module Samagotchi
2974
2990
 
2975
2991
  private
2976
2992
 
2993
+ def running_command_key = :"samagotchi_command_#{object_id}"
2994
+
2977
2995
  # The one named seam for replacing a session's conversation (the
2978
2996
  # array is replaced, never mutated in place). No lock: a reader gets
2979
2997
  # the old array or the new one, and both are whole.