agentilda 1.0.3 → 2.0.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/agentilda.gemspec +8 -6
  3. data/agents/hansolo-reviewer.md +42 -13
  4. data/agents/lando-broker.md +40 -46
  5. data/agents/leah-researcher.md +36 -58
  6. data/agents/luke-backend.md +49 -61
  7. data/agents/palpatine-planner.md +39 -21
  8. data/agents/rey-frontend.md +44 -86
  9. data/agents/yoda-writer.md +38 -36
  10. data/bin/setup +26 -0
  11. data/exe/agentilda +13 -92
  12. data/exe/tilda-rs +0 -0
  13. data/lib/agentilda/adoption.rb +7 -2
  14. data/lib/agentilda/agent.rb +40 -99
  15. data/lib/agentilda/agents.rb +122 -0
  16. data/lib/agentilda/board.rb +91 -0
  17. data/lib/agentilda/brief.rb +54 -5
  18. data/lib/agentilda/child.rb +78 -0
  19. data/lib/agentilda/cli/agents/subcommands/describe.rb +8 -4
  20. data/lib/agentilda/cli/base.rb +22 -6
  21. data/lib/agentilda/cli/create/create.rb +46 -19
  22. data/lib/agentilda/cli/docs/docs.rb +6 -2
  23. data/lib/agentilda/cli/index/index.rb +6 -4
  24. data/lib/agentilda/cli/linear/linear.rb +3 -2
  25. data/lib/agentilda/cli/linear/subcommands/import.rb +26 -15
  26. data/lib/agentilda/cli/mail/mail.rb +31 -0
  27. data/lib/agentilda/cli/mail/subcommands/read.rb +43 -0
  28. data/lib/agentilda/cli/mail/subcommands/send.rb +43 -0
  29. data/lib/agentilda/cli/resync/subcommands/dirs.rb +6 -4
  30. data/lib/agentilda/cli/resync/subcommands/prs.rb +21 -13
  31. data/lib/agentilda/cli/run/run.rb +207 -99
  32. data/lib/agentilda/cli/unblock/unblock.rb +24 -13
  33. data/lib/agentilda/cli/worktree/worktree.rb +85 -0
  34. data/lib/agentilda/cli.rb +43 -0
  35. data/lib/agentilda/clock.rb +114 -0
  36. data/lib/agentilda/console.rb +157 -0
  37. data/lib/agentilda/control.rb +63 -0
  38. data/lib/agentilda/creator.rb +7 -2
  39. data/lib/agentilda/dashboard.rb +219 -0
  40. data/lib/agentilda/dev_work.rb +2 -0
  41. data/lib/agentilda/diagram.rb +12 -7
  42. data/lib/agentilda/dispatcher.rb +646 -0
  43. data/lib/agentilda/documentation.rb +14 -6
  44. data/lib/agentilda/executor.rb +362 -204
  45. data/lib/agentilda/feature.rb +14 -135
  46. data/lib/agentilda/frontmatter.rb +4 -2
  47. data/lib/agentilda/github.rb +19 -11
  48. data/lib/agentilda/index.rb +11 -11
  49. data/lib/agentilda/keyboard.rb +76 -8
  50. data/lib/agentilda/launcher.rb +69 -0
  51. data/lib/agentilda/ledger.rb +233 -0
  52. data/lib/agentilda/linear/api.rb +14 -10
  53. data/lib/agentilda/linear/attribution.rb +5 -5
  54. data/lib/agentilda/linear/import.rb +30 -14
  55. data/lib/agentilda/linear/issue.rb +6 -3
  56. data/lib/agentilda/linear/mapping.rb +18 -17
  57. data/lib/agentilda/linear/push.rb +19 -12
  58. data/lib/agentilda/linear/survey.rb +3 -2
  59. data/lib/agentilda/linear/unit.rb +12 -12
  60. data/lib/agentilda/linear.rb +5 -6
  61. data/lib/agentilda/mailbox.rb +141 -0
  62. data/lib/agentilda/markdown.rb +1 -1
  63. data/lib/agentilda/progress_log.rb +5 -1
  64. data/lib/agentilda/publisher.rb +6 -3
  65. data/lib/agentilda/pull_request.rb +0 -186
  66. data/lib/agentilda/pull_requests.rb +230 -0
  67. data/lib/agentilda/reporter.rb +9 -3
  68. data/lib/agentilda/resync.rb +120 -52
  69. data/lib/agentilda/roster.rb +31 -48
  70. data/lib/agentilda/runner.rb +72 -389
  71. data/lib/agentilda/screen/ratatui/bar.rb +57 -0
  72. data/lib/agentilda/screen/ratatui/key_translator.rb +29 -0
  73. data/lib/agentilda/screen/ratatui.rb +318 -0
  74. data/lib/agentilda/state_file.rb +174 -0
  75. data/lib/agentilda/state_machine.rb +60 -32
  76. data/lib/agentilda/status.rb +120 -37
  77. data/lib/agentilda/subject.rb +133 -0
  78. data/lib/agentilda/tally.rb +17 -11
  79. data/lib/agentilda/transcript.rb +47 -16
  80. data/lib/agentilda/tree.rb +10 -6
  81. data/lib/agentilda/ui.rb +204 -190
  82. data/lib/agentilda/unblocker.rb +25 -11
  83. data/lib/agentilda/version.rb +1 -1
  84. data/lib/agentilda/worktree.rb +14 -18
  85. data/lib/agentilda.rb +57 -70
  86. metadata +61 -15
  87. data/Gemfile +0 -26
  88. data/Gemfile.lock +0 -261
  89. data/bin/create-plan-folder +0 -125
  90. data/lib/dry/cli/banner.rb +0 -293
@@ -8,18 +8,26 @@ module Agentilda
8
8
  class Import < Team
9
9
  desc "Create Linear issues from the plans: one per folder, one child per work unit"
10
10
 
11
- option :project, aliases: ["-p", "--project-url", "--project-id"],
12
- desc: "The project to file everything under: its URL, its name, or its id"
13
- option :commit, type: :boolean, default: false,
14
- desc: "Actually create and update in Linear (default: dry run)"
15
- option :format, default: "text", values: %w[text json],
16
- desc: "json emits the exact arguments the Linear MCP tools take"
17
- option :since, aliases: ["-s"],
18
- desc: "Skip plans numbered below this, e.g. 010.00"
11
+ option :project,
12
+ aliases: ["-p", "--project-url", "--project-id"],
13
+ desc: "The project to file everything under: its URL, its name, or its id"
14
+ option :commit,
15
+ type: :boolean,
16
+ default: false,
17
+ desc: "Actually create and update in Linear (default: dry run)"
18
+ option :format,
19
+ default: "text",
20
+ values: %w[text json],
21
+ desc: "json emits the exact arguments the Linear MCP tools take"
22
+ option :since,
23
+ aliases: ["-s"],
24
+ desc: "Skip plans numbered below this, e.g. 010.00"
19
25
  option :status,
20
26
  desc: "Only plans in these states: a comma-separated list of status keys"
21
- option :force, type: :boolean, default: false,
22
- desc: "Update everything, whether the plan has changed or not"
27
+ option :force,
28
+ type: :boolean,
29
+ default: false,
30
+ desc: "Update everything, whether the plan has changed or not"
23
31
 
24
32
  example [
25
33
  "TAX -p 'US Tax Law: Self Contained Ruby Gem' # show what would be created",
@@ -61,7 +69,7 @@ module Agentilda
61
69
  def resolve_project(team, tree, options)
62
70
  reference = options[:project].to_s
63
71
  looks_up = reference.match?(%r{\Ahttps?://}) || reference.match?(/\A[0-9a-f-]{32,}\z/)
64
- return {"id" => nil, "name" => reference, "url" => nil} if !looks_up && offline?
72
+ return { "id" => nil, "name" => reference, "url" => nil } if !looks_up && offline?
65
73
 
66
74
  _api, survey = survey_for(team, tree)
67
75
  survey.project(reference)
@@ -84,8 +92,11 @@ module Agentilda
84
92
 
85
93
  # @return [Agentilda::Linear::Import]
86
94
  def build(tree, team, project, options)
87
- Agentilda::Linear::Import.new(tree:, team: Agentilda::Linear.key!(team), project:,
88
- since: options[:since], statuses: statuses(options),
95
+ Agentilda::Linear::Import.new(tree:,
96
+ team: Agentilda::Linear.key!(team),
97
+ project:,
98
+ since: options[:since],
99
+ statuses: statuses(options),
89
100
  force: options.fetch(:force, false))
90
101
  end
91
102
 
@@ -116,13 +127,13 @@ module Agentilda
116
127
  # @return [String]
117
128
  def row(action)
118
129
  [action.op, action.kind, action.ordinal, action.unit || "-",
119
- action.identifier || "-", action.title].join("\t")
130
+ action.identifier || "-", action.title].join("\t")
120
131
  end
121
132
 
122
133
  # @param action [Agentilda::Linear::Action]
123
134
  # @return [String]
124
135
  def line(action)
125
- verb = paint(action.op.to_s.ljust(6), (action.op == :create) ? :green : :yellow)
136
+ verb = paint(action.op.to_s.ljust(6), action.op == :create ? :green : :yellow)
126
137
  indent = action.child? ? " " : ""
127
138
  "#{verb} #{indent}#{action.title} #{paint("(#{action.reason})", :bright_black)}"
128
139
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentilda
4
+ module CLI
5
+ # `agentilda mail` — the two halves of a pair talking through the plan
6
+ # folder. See {Agentilda::Mailbox} for why a file rather than a session.
7
+ module Mail
8
+ # Both subcommands name a plan and need its folder.
9
+ module Locating
10
+ private
11
+
12
+ # An unknown number is refused rather than given an empty mailbox:
13
+ # an agent polling the wrong plan for a whole round would read
14
+ # silence as "my partner has nothing to say".
15
+ #
16
+ # @param options [Hash]
17
+ # @return [Agentilda::Mailbox]
18
+ def mailbox_for(options)
19
+ tree = tree_for(options)
20
+ ordinal = Ordinal.parse(options[:plan].to_s)
21
+ subject = ordinal && tree.find(ordinal)
22
+ unless subject
23
+ refuse("No plan #{options[:plan]} in #{tree.dir}.\n\nKnown: #{tree.ordinals.join(", ")}", 66)
24
+ end
25
+
26
+ Mailbox.new(dir: subject.feature.path)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentilda
4
+ module CLI
5
+ module Mail
6
+ # `agentilda mail read` — what is waiting for one agent.
7
+ #
8
+ # Nothing waiting is the ordinary result of a poll, so it is not an
9
+ # error and not even STDOUT: the messages are the deliverable, and an
10
+ # empty one is empty.
11
+ class Read < Base
12
+ include Locating
13
+
14
+ desc "Print the messages waiting for an agent in a plan's mailbox"
15
+
16
+ option :plan, required: true, desc: "Which plan: NNN or NNN.MM"
17
+ option :for, required: true, desc: "The agent reading"
18
+ option :after, default: "0", desc: "Only messages numbered above this, so a poll shows what is new"
19
+
20
+ example [
21
+ "--plan 003 --for rey-frontend # everything addressed to rey",
22
+ "--plan 003 --for rey-frontend --after 4 # only what arrived since #4"
23
+ ]
24
+
25
+ # @param options [Hash]
26
+ # @return [void]
27
+ def call(**options)
28
+ quiet?(options)
29
+ mailbox = mailbox_for(options)
30
+ after = options[:after].to_i
31
+ waiting = mailbox.for(options[:for], after:)
32
+
33
+ if waiting.empty?
34
+ UI.line("Nothing for #{options[:for]}#{" after ##{after}" if after.positive?}") unless quiet?(options)
35
+ return
36
+ end
37
+
38
+ puts waiting.join("\n")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentilda
4
+ module CLI
5
+ module Mail
6
+ # `agentilda mail send` — leave a message for the other half.
7
+ class Send < Base
8
+ include Locating
9
+
10
+ desc "Append a message to a plan's mailbox"
11
+
12
+ argument :body, type: :string, required: true, desc: "The message, quoted"
13
+
14
+ option :plan, required: true, desc: "Which plan: NNN or NNN.MM"
15
+ option :from, required: true, desc: "The agent writing"
16
+ option :to, required: true, desc: "The agent it is for"
17
+
18
+ example [
19
+ %(--plan 003 --from luke-backend --to rey-frontend "GET /returns/:id is up; errors come back as {error: {code, message}}")
20
+ ]
21
+
22
+ # @param body [String]
23
+ # @param options [Hash]
24
+ # @return [void]
25
+ def call(body:, **options)
26
+ quiet?(options)
27
+ mailbox = mailbox_for(options)
28
+ message = begin
29
+ mailbox.append(from: options[:from], to: options[:to], body:)
30
+ rescue Agentilda::Error => e
31
+ refuse(e.message, 64)
32
+ end
33
+
34
+ return if quiet?(options)
35
+
36
+ # One line, not a box: an agent runs this between steps, and a
37
+ # four-line frame around "#7 → rey-frontend" is noise it reads.
38
+ UI.line("##{message.number} → #{message.to}, in #{File.basename(mailbox.dir)}/#{Mailbox::FILENAME}")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -8,8 +8,10 @@ module Agentilda
8
8
  class Dirs < Base
9
9
  desc "Rename plan folders so the name matches the contents and the NNN.MM form"
10
10
 
11
- option :commit, type: :boolean, default: false,
12
- desc: "Actually rename the folders (default: dry run)"
11
+ option :commit,
12
+ type: :boolean,
13
+ default: false,
14
+ desc: "Actually rename the folders (default: dry run)"
13
15
 
14
16
  example [
15
17
  " # show what would be renamed",
@@ -20,10 +22,10 @@ module Agentilda
20
22
  # @return [void]
21
23
  def call(**options)
22
24
  changes = Agentilda::Resync::Dirs.new(tree: tree_for(options))
23
- .call(commit: commit?(options))
25
+ .call(commit: commit?(options))
24
26
 
25
27
  if changes.empty?
26
- success("Every folder is already named NNN.MM-<emoji>-<slug> and the emoji matches.") unless quiet?(options)
28
+ success("Every folder is already named NNN.MM-<emoji> → <slug> and the emoji matches.") unless quiet?(options)
27
29
  return
28
30
  end
29
31
 
@@ -8,12 +8,18 @@ module Agentilda
8
8
  class Prs < Base
9
9
  desc "Add missing [NNN.MM] prefixes to pull request titles"
10
10
 
11
- option :commit, type: :boolean, default: false,
12
- desc: "Actually retitle the pull requests (default: dry run)"
13
- option :state, default: "all", values: %w[open closed merged all],
14
- desc: "Which pull requests to consider"
15
- option :adopt, type: :boolean, default: true,
16
- desc: "Mint a retroactive plan folder for every pull request that resolves to none. --no-adopt flags them for a human instead"
11
+ option :commit,
12
+ type: :boolean,
13
+ default: false,
14
+ desc: "Actually retitle the pull requests (default: dry run)"
15
+ option :state,
16
+ default: "all",
17
+ values: %w[open closed merged all],
18
+ desc: "Which pull requests to consider"
19
+ option :adopt,
20
+ type: :boolean,
21
+ default: true,
22
+ desc: "Mint a retroactive plan folder for every pull request that resolves to none. --no-adopt flags them for a human instead"
17
23
 
18
24
  example [
19
25
  " # show what would be retitled, and what would be adopted",
@@ -27,9 +33,11 @@ module Agentilda
27
33
  def call(**options)
28
34
  github = GitHub.new
29
35
  tree = tree_for(options)
30
- changes = Agentilda::Resync::Prs.new(tree:, github:,
31
- adopt: options.fetch(:adopt, true), root: File.dirname(tree.dir))
32
- .call(commit: commit?(options))
36
+ changes = Agentilda::Resync::Prs.new(tree:,
37
+ github:,
38
+ adopt: options.fetch(:adopt, true),
39
+ root: File.dirname(tree.dir))
40
+ .call(commit: commit?(options))
33
41
 
34
42
  if changes.empty?
35
43
  success("Every pull request title already carries a prefix.") unless quiet?(options)
@@ -58,10 +66,10 @@ module Agentilda
58
66
 
59
67
  applicable.each do |c|
60
68
  note = if c.adopted?
61
- paint(" (new plan)", :magenta)
62
- elsif c.assumed?
63
- paint(" (assumed)", :yellow)
64
- end
69
+ paint(" (new plan)", :magenta)
70
+ elsif c.assumed?
71
+ paint(" (assumed)", :yellow)
72
+ end
65
73
  say("##{c.number} #{c.new_title}#{note}")
66
74
  end
67
75