agentilda 2.0.3 → 2.0.4
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/lib/agentilda/agents/agent.rb +79 -0
- data/lib/agentilda/agents/registry.rb +121 -0
- data/lib/agentilda/agents/roster.rb +92 -0
- data/lib/agentilda/agents.rb +11 -113
- data/lib/agentilda/cli/agents/subcommands/describe.rb +3 -3
- data/lib/agentilda/cli/agents/subcommands/list.rb +1 -1
- data/lib/agentilda/cli/base.rb +3 -3
- data/lib/agentilda/cli/create/create.rb +17 -18
- data/lib/agentilda/cli/docs/docs.rb +1 -1
- data/lib/agentilda/cli/index/index.rb +1 -1
- data/lib/agentilda/cli/linear/subcommands/import.rb +1 -1
- data/lib/agentilda/cli/list_plans/list_plans.rb +1 -1
- data/lib/agentilda/cli/mail/mail.rb +4 -4
- data/lib/agentilda/cli/mail/subcommands/send.rb +1 -1
- data/lib/agentilda/cli/resync/subcommands/dirs.rb +2 -2
- data/lib/agentilda/cli/resync/subcommands/prs.rb +3 -3
- data/lib/agentilda/cli/run/run.rb +40 -40
- data/lib/agentilda/cli/states/states.rb +1 -1
- data/lib/agentilda/cli/unblock/unblock.rb +17 -17
- data/lib/agentilda/cli/worktree/worktree.rb +4 -4
- data/lib/agentilda/cli.rb +1 -1
- data/lib/agentilda/engine/board.rb +93 -0
- data/lib/agentilda/engine/dispatcher.rb +646 -0
- data/lib/agentilda/engine/progress_log.rb +125 -0
- data/lib/agentilda/engine/runner.rb +141 -0
- data/lib/agentilda/engine/state_file.rb +176 -0
- data/lib/agentilda/engine/tally.rb +177 -0
- data/lib/agentilda/engine.rb +19 -0
- data/lib/agentilda/execution/child.rb +94 -0
- data/lib/agentilda/execution/clock.rb +116 -0
- data/lib/agentilda/execution/control.rb +180 -0
- data/lib/agentilda/execution/executor.rb +699 -0
- data/lib/agentilda/execution/transcript.rb +468 -0
- data/lib/agentilda/execution.rb +18 -0
- data/lib/agentilda/lifecycle/adoption.rb +197 -0
- data/lib/agentilda/lifecycle/brief.rb +285 -0
- data/lib/agentilda/lifecycle/creator.rb +125 -0
- data/lib/agentilda/lifecycle/resync.rb +428 -0
- data/lib/agentilda/lifecycle/unblocker.rb +223 -0
- data/lib/agentilda/lifecycle.rb +28 -0
- data/lib/agentilda/linear/api.rb +1 -1
- data/lib/agentilda/linear/attribution.rb +7 -7
- data/lib/agentilda/linear/import.rb +18 -18
- data/lib/agentilda/linear/mapping.rb +6 -6
- data/lib/agentilda/linear/push.rb +5 -5
- data/lib/agentilda/linear/survey.rb +7 -7
- data/lib/agentilda/linear/unit.rb +5 -5
- data/lib/agentilda/plans/dev_work.rb +56 -0
- data/lib/agentilda/plans/feature.rb +131 -0
- data/lib/agentilda/plans/index.rb +208 -0
- data/lib/agentilda/plans/ledger.rb +235 -0
- data/lib/agentilda/plans/mailbox.rb +143 -0
- data/lib/agentilda/plans/ordinal.rb +92 -0
- data/lib/agentilda/plans/pull_request.rb +29 -0
- data/lib/agentilda/plans/pull_requests.rb +229 -0
- data/lib/agentilda/plans/state_machine.rb +383 -0
- data/lib/agentilda/plans/status.rb +365 -0
- data/lib/agentilda/plans/subject.rb +131 -0
- data/lib/agentilda/plans/tree.rb +79 -0
- data/lib/agentilda/plans.rb +39 -0
- data/lib/agentilda/presentation/console.rb +159 -0
- data/lib/agentilda/presentation/dashboard.rb +221 -0
- data/lib/agentilda/presentation/diagram.rb +149 -0
- data/lib/agentilda/presentation/documentation.rb +439 -0
- data/lib/agentilda/presentation/keyboard.rb +158 -0
- data/lib/agentilda/presentation/reporter.rb +181 -0
- data/lib/agentilda/presentation/screen/ratatui/bar.rb +59 -0
- data/lib/agentilda/presentation/screen/ratatui/key_translator.rb +31 -0
- data/lib/agentilda/presentation/screen/ratatui.rb +320 -0
- data/lib/agentilda/presentation/viewer.rb +62 -0
- data/lib/agentilda/presentation.rb +23 -0
- data/lib/agentilda/{ui.rb → support/ui.rb} +5 -5
- data/lib/agentilda/vcs/github.rb +170 -0
- data/lib/agentilda/vcs/publisher.rb +177 -0
- data/lib/agentilda/vcs/worktree.rb +209 -0
- data/lib/agentilda/vcs.rb +19 -0
- data/lib/agentilda/version.rb +1 -1
- data/lib/agentilda.rb +6 -5
- metadata +56 -49
- data/lib/agentilda/adoption.rb +0 -197
- data/lib/agentilda/agent.rb +0 -77
- data/lib/agentilda/board.rb +0 -91
- data/lib/agentilda/brief.rb +0 -283
- data/lib/agentilda/child.rb +0 -92
- data/lib/agentilda/clock.rb +0 -114
- data/lib/agentilda/console.rb +0 -157
- data/lib/agentilda/control.rb +0 -178
- data/lib/agentilda/creator.rb +0 -125
- data/lib/agentilda/dashboard.rb +0 -219
- data/lib/agentilda/dev_work.rb +0 -56
- data/lib/agentilda/diagram.rb +0 -149
- data/lib/agentilda/dispatcher.rb +0 -646
- data/lib/agentilda/documentation.rb +0 -437
- data/lib/agentilda/executor.rb +0 -697
- data/lib/agentilda/feature.rb +0 -132
- data/lib/agentilda/github.rb +0 -168
- data/lib/agentilda/index.rb +0 -206
- data/lib/agentilda/keyboard.rb +0 -156
- data/lib/agentilda/ledger.rb +0 -233
- data/lib/agentilda/mailbox.rb +0 -141
- data/lib/agentilda/ordinal.rb +0 -90
- data/lib/agentilda/progress_log.rb +0 -126
- data/lib/agentilda/publisher.rb +0 -175
- data/lib/agentilda/pull_request.rb +0 -27
- data/lib/agentilda/pull_requests.rb +0 -230
- data/lib/agentilda/reporter.rb +0 -181
- data/lib/agentilda/resync.rb +0 -426
- data/lib/agentilda/roster.rb +0 -93
- data/lib/agentilda/runner.rb +0 -139
- data/lib/agentilda/screen/ratatui/bar.rb +0 -57
- data/lib/agentilda/screen/ratatui/key_translator.rb +0 -29
- data/lib/agentilda/screen/ratatui.rb +0 -318
- data/lib/agentilda/state_file.rb +0 -174
- data/lib/agentilda/state_machine.rb +0 -383
- data/lib/agentilda/status.rb +0 -363
- data/lib/agentilda/subject.rb +0 -133
- data/lib/agentilda/tally.rb +0 -175
- data/lib/agentilda/transcript.rb +0 -466
- data/lib/agentilda/tree.rb +0 -81
- data/lib/agentilda/unblocker.rb +0 -221
- data/lib/agentilda/viewer.rb +0 -60
- data/lib/agentilda/worktree.rb +0 -207
- /data/lib/agentilda/{config.rb → support/config.rb} +0 -0
- /data/lib/agentilda/{frontmatter.rb → support/frontmatter.rb} +0 -0
- /data/lib/agentilda/{markdown.rb → support/markdown.rb} +0 -0
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "aasm"
|
|
4
|
-
|
|
5
|
-
require "agentilda/status"
|
|
6
|
-
|
|
7
|
-
module Agentilda
|
|
8
|
-
# The state machine for one plan folder.
|
|
9
|
-
#
|
|
10
|
-
# The whole topology is the `aasm` block below, and it is nowhere else. To
|
|
11
|
-
# change what may follow what, edit an event's `from:` list. To add a state,
|
|
12
|
-
# add it to {STATUSES} and give it an event. There is no second table to keep
|
|
13
|
-
# in step: {inbound}, {outbound}, {edge?} and `terminal?` are all read back
|
|
14
|
-
# off these declarations.
|
|
15
|
-
#
|
|
16
|
-
# Two rules hold the rest together:
|
|
17
|
-
#
|
|
18
|
-
# 1. **The guard for entering a state is that state's own invariant.** There
|
|
19
|
-
# is one guard method, {#justified?}, and it reads the destination off the
|
|
20
|
-
# transition in flight — so a destination is named once, in `to:`.
|
|
21
|
-
# 2. **The state lives in the folder name.** There is no column and no
|
|
22
|
-
# database. A successful transition renames the directory, which is why
|
|
23
|
-
# firing an event is a real side effect and asking a question is not.
|
|
24
|
-
class StateMachine
|
|
25
|
-
include AASM
|
|
26
|
-
|
|
27
|
-
# Raised when a transition is refused, carrying the reason rather than
|
|
28
|
-
# AASM's "failed callback #17".
|
|
29
|
-
class Refused < Error; end
|
|
30
|
-
|
|
31
|
-
# The forward path a bare {#promote!} walks. Everything off this spine —
|
|
32
|
-
# blocking, deferring, discarding — has to be named explicitly, which is
|
|
33
|
-
# the entire reason for having a machine rather than a rename.
|
|
34
|
-
#
|
|
35
|
-
# 🔴 rejoins at 🎨 rather than continuing: fixing review comments puts the
|
|
36
|
-
# work back through both halves of building, it does not skip the reviewer
|
|
37
|
-
# and it does not assume the half nobody complained about still holds.
|
|
38
|
-
SPINE = {
|
|
39
|
-
retroactive: :planned,
|
|
40
|
-
new: :researched,
|
|
41
|
-
researched: :ready_for_planning,
|
|
42
|
-
ready_for_planning: :planned,
|
|
43
|
-
planned: :building,
|
|
44
|
-
building: :building_ui,
|
|
45
|
-
building_ui: :ready_for_review,
|
|
46
|
-
ready_for_review: :in_review,
|
|
47
|
-
in_review: :approved,
|
|
48
|
-
approved: :deployed,
|
|
49
|
-
rejected: :building_ui,
|
|
50
|
-
rolled_back: :ready_for_review,
|
|
51
|
-
shit: :planned
|
|
52
|
-
}.freeze
|
|
53
|
-
|
|
54
|
-
# Preference order when several states fit a folder's contents at once.
|
|
55
|
-
# Read top to bottom: the loudest fact wins. A discard outranks everything,
|
|
56
|
-
# a block outranks progress, and `retroactive` is last because it is the
|
|
57
|
-
# absence of documents rather than the presence of any.
|
|
58
|
-
PREFERENCE = %i[
|
|
59
|
-
discarded rolled_back shit deferred blocked product_blocked
|
|
60
|
-
deployed approved rejected in_review ready_for_review building_ui building planned ready_for_planning
|
|
61
|
-
researched new
|
|
62
|
-
retroactive
|
|
63
|
-
].freeze
|
|
64
|
-
|
|
65
|
-
# States whose members a folder's *contents* cannot tell apart. Within a
|
|
66
|
-
# family the current name always wins, because re-deriving it would be a
|
|
67
|
-
# guess dressed up as a correction.
|
|
68
|
-
#
|
|
69
|
-
# ⭕️ and 🅱️ both mean "a human must decide before this moves"; *which*
|
|
70
|
-
# human is recorded nowhere but the emoji.
|
|
71
|
-
#
|
|
72
|
-
# ⭐️ 🟡 🎨 look identical on disk: a `spec.md` and a `plan.md` with work
|
|
73
|
-
# units in it. Whether nobody has started, the back end is under way, or
|
|
74
|
-
# the interface is, is recorded only by the agent that renamed the folder
|
|
75
|
-
# when it started. 🟢 👀 🔴 add an open pull request and are just as
|
|
76
|
-
# mute about whether a reviewer has seen it. So `resync` never moves
|
|
77
|
-
# between any of them; they advance by events alone.
|
|
78
|
-
#
|
|
79
|
-
# Order matters: the members run weakest claim first, and a folder
|
|
80
|
-
# arriving from outside lands on the weakest member its contents fit.
|
|
81
|
-
# Contents that fit the family justify only its floor, never its ceiling.
|
|
82
|
-
FAMILIES = [
|
|
83
|
-
%i[blocked product_blocked],
|
|
84
|
-
%i[planned building building_ui ready_for_review in_review rejected]
|
|
85
|
-
].freeze
|
|
86
|
-
|
|
87
|
-
# States the agent loop leaves alone: work that is finished (✅ 😎), work
|
|
88
|
-
# that was dropped (❌), and work waiting on a human (⭕️ 🅱️ ☢️).
|
|
89
|
-
# Everything else is fair game for a specialist.
|
|
90
|
-
#
|
|
91
|
-
# ✅ Approved is here deliberately. `hansolo-reviewer` advancing a plan to
|
|
92
|
-
# it is the end of the loop, not a step in it: nothing merges, and no agent
|
|
93
|
-
# handles `approved`. Merging is the one act in this lifecycle that changes
|
|
94
|
-
# a branch everyone else builds on, and an autonomous loop that does it
|
|
95
|
-
# unattended has no way to be wrong quietly.
|
|
96
|
-
#
|
|
97
|
-
# Moving `approved` out of this list is therefore a decision about blast
|
|
98
|
-
# radius rather than about topology. If it ever moves, something has to own
|
|
99
|
-
# `approved -> deployed`, and today nothing does.
|
|
100
|
-
SETTLED = %i[approved deployed discarded blocked product_blocked deferred].freeze
|
|
101
|
-
|
|
102
|
-
# Every state an agent that may sign `Blocked` works in. A block the
|
|
103
|
-
# machine refuses is reported as a failed attempt and parks nothing, so a
|
|
104
|
-
# state missing here turns an honest "I need a decision" into a failure.
|
|
105
|
-
# The agents spec checks every prompt that describes blocking against it.
|
|
106
|
-
#
|
|
107
|
-
# Review is absent: a reviewer rules, it does not ask.
|
|
108
|
-
BLOCKABLE = %i[new researched retroactive ready_for_planning planned building building_ui rejected].freeze
|
|
109
|
-
|
|
110
|
-
# Rerouting a transition below makes the hand-drawn
|
|
111
|
-
# `docs/img/plan-spec-build.png` stale — `just docs` will show you, because
|
|
112
|
-
# the mermaid source in the generated document is derived from this block.
|
|
113
|
-
aasm do
|
|
114
|
-
# The vocabulary is defined once, in {STATUSES}. This machine may not
|
|
115
|
-
# quietly know a different set.
|
|
116
|
-
Agentilda::STATUSES.each { |status| state status.key }
|
|
117
|
-
|
|
118
|
-
event :specify, guard: :justified? do
|
|
119
|
-
transitions from: %i[retroactive blocked product_blocked deferred], to: :new
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
event :research, guard: :justified? do
|
|
123
|
-
transitions from: %i[new retroactive blocked product_blocked deferred], to: :researched
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# The specification is done and the planner has not started. Entered
|
|
127
|
-
# by the harness when `yoda-writer` completes; `resync` reaches it on
|
|
128
|
-
# a blank `plan.md`. Without it ⭐️ Planned required a file only the
|
|
129
|
-
# agent handling ⭐️ Planned writes, and nothing could enter it.
|
|
130
|
-
event :ready_to_plan, guard: :justified? do
|
|
131
|
-
transitions from: %i[researched new retroactive shit blocked product_blocked deferred],
|
|
132
|
-
to: :ready_for_planning
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# `new` stays in this list. The spine routes a plan through research, and
|
|
136
|
-
# that is what the agent loop follows — but a specification somebody has
|
|
137
|
-
# already researched by hand should not have to pretend otherwise to get
|
|
138
|
-
# planned. What research buys is not enforced here; it is enforced by
|
|
139
|
-
# `yoda-writer` handling `researched` and nothing else.
|
|
140
|
-
event :plan, guard: :justified? do
|
|
141
|
-
transitions from: %i[ready_for_planning researched new retroactive shit blocked product_blocked deferred],
|
|
142
|
-
to: :planned
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
event :build, guard: :justified? do
|
|
146
|
-
transitions from: %i[planned shit approved rolled_back retroactive blocked product_blocked deferred],
|
|
147
|
-
to: :building
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Building hands off rather than finishing. A plan with no interface work
|
|
151
|
-
# still passes through Building UI: `rey-frontend` says there is nothing
|
|
152
|
-
# to build and moves it on, which costs one cheap round and keeps the
|
|
153
|
-
# spine one shape instead of two. `rejected` re-enters here rather than
|
|
154
|
-
# going straight back to review, because a change request reopens the
|
|
155
|
-
# implementation and neither half can assume the other still holds.
|
|
156
|
-
event :build_ui, guard: :justified? do
|
|
157
|
-
transitions from: %i[building rejected], to: :building_ui
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
# The pair lands one pull request, and whichever half finishes last
|
|
161
|
-
# carries the plan to review from wherever the folder stands.
|
|
162
|
-
event :submit, guard: :justified? do
|
|
163
|
-
transitions from: %i[building building_ui rejected rolled_back], to: :ready_for_review
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
event :review, guard: :justified? do
|
|
167
|
-
transitions from: %i[ready_for_review], to: :in_review
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
event :approve, guard: :justified? do
|
|
171
|
-
transitions from: %i[in_review retroactive], to: :approved
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
event :request_changes, guard: :justified? do
|
|
175
|
-
transitions from: %i[in_review], to: :rejected
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
event :slop, guard: :justified? do
|
|
179
|
-
transitions from: %i[in_review], to: :shit
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
event :deploy, guard: :justified? do
|
|
183
|
-
transitions from: %i[approved], to: :deployed
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# 🚀 is not the end of the line. A release that breaks in production comes
|
|
187
|
-
# back through ⏪, which needs its own proof on disk: by the time work is
|
|
188
|
-
# deployed every pull request is merged, so 🟢's "at least one open pull
|
|
189
|
-
# request" guard would refuse a direct return, permanently.
|
|
190
|
-
event :rollback, guard: :justified? do
|
|
191
|
-
transitions from: %i[deployed], to: :rolled_back
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
event :block, guard: :justified? do
|
|
195
|
-
transitions from: BLOCKABLE, to: :blocked
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
event :block_on_product, guard: :justified? do
|
|
199
|
-
transitions from: BLOCKABLE, to: :product_blocked
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
event :defer, guard: :justified? do
|
|
203
|
-
transitions from: BLOCKABLE + %i[blocked product_blocked], to: :deferred
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Anything, from anywhere, may be dropped for good.
|
|
207
|
-
event :discard, guard: :justified? do
|
|
208
|
-
transitions from: Agentilda::STATUSES.map(&:key) - %i[discarded], to: :discarded
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# A transition is not a bookkeeping entry: it is the folder moving.
|
|
212
|
-
after_all_transitions :rename!
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
class << self
|
|
216
|
-
# Destination => the event that reaches it. Every event above has exactly
|
|
217
|
-
# one `to:`, which is what lets `--to <state>` name a destination rather
|
|
218
|
-
# than making callers learn the verbs.
|
|
219
|
-
#
|
|
220
|
-
# @return [Hash{Symbol => Symbol}]
|
|
221
|
-
def event_for
|
|
222
|
-
@event_for ||= aasm.events.each_with_object({}) { |event, map|
|
|
223
|
-
event.transitions.each { |t| map[t.to] ||= event.name }
|
|
224
|
-
}.freeze
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# Destination => the states that may legitimately reach it.
|
|
228
|
-
#
|
|
229
|
-
# `retroactive` is absent: it is a birth state, produced by
|
|
230
|
-
# `create --after` for work that shipped undocumented.
|
|
231
|
-
#
|
|
232
|
-
# @return [Hash{Symbol => Array<Symbol>}]
|
|
233
|
-
def inbound
|
|
234
|
-
@inbound ||= aasm.events.each_with_object({}) { |event, map|
|
|
235
|
-
event.transitions.each { |t| (map[t.to] ||= []).concat(Array(t.from)) }
|
|
236
|
-
}.transform_values { |froms| froms.uniq.freeze }.freeze
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Source => the states it may reach.
|
|
240
|
-
#
|
|
241
|
-
# @return [Hash{Symbol => Array<Symbol>}]
|
|
242
|
-
def outbound_map
|
|
243
|
-
@outbound_map ||= inbound.each_with_object({}) { |(to, froms), map|
|
|
244
|
-
froms.each { |from| (map[from] ||= []) << to }
|
|
245
|
-
}.transform_values(&:freeze).freeze
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# @param key [Symbol]
|
|
249
|
-
# @return [Array<Symbol>]
|
|
250
|
-
def outbound(key) = outbound_map.fetch(key, [])
|
|
251
|
-
|
|
252
|
-
# @param from [Symbol]
|
|
253
|
-
# @param to [Symbol]
|
|
254
|
-
# @return [Boolean] whether the topology permits this edge at all
|
|
255
|
-
def edge?(from, to) = outbound(from).include?(to)
|
|
256
|
-
|
|
257
|
-
# @param key [Symbol]
|
|
258
|
-
# @return [Array<Symbol>] the family +key+ belongs to, empty when it has none
|
|
259
|
-
def family_of(key) = FAMILIES.find { |family| family.include?(key) } || []
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
# @param subject [#status, #file?, #read, #pull_requests, #rename_to]
|
|
263
|
-
def initialize(subject)
|
|
264
|
-
@subject = subject
|
|
265
|
-
@key = subject.status.key
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# @return [Object] the plan folder this machine speaks for
|
|
269
|
-
attr_reader :subject
|
|
270
|
-
|
|
271
|
-
# @return [Symbol] the state right now
|
|
272
|
-
attr_reader :key
|
|
273
|
-
|
|
274
|
-
# @return [Agentilda::Status]
|
|
275
|
-
def status = STATUS_BY_KEY.fetch(key)
|
|
276
|
-
|
|
277
|
-
# AASM keeps state in an ORM column; we keep it in a directory name. These
|
|
278
|
-
# three methods are the whole of that adaptation.
|
|
279
|
-
#
|
|
280
|
-
# @return [Symbol]
|
|
281
|
-
def aasm_read_state(_name = :default) = @key
|
|
282
|
-
|
|
283
|
-
# @return [Boolean]
|
|
284
|
-
def aasm_write_state(new_state, name = :default) = aasm_write_state_without_persistence(new_state, name)
|
|
285
|
-
|
|
286
|
-
# @return [Boolean]
|
|
287
|
-
def aasm_write_state_without_persistence(new_state, _name = :default)
|
|
288
|
-
@key = new_state
|
|
289
|
-
true
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
# States reachable right now, guards applied.
|
|
293
|
-
#
|
|
294
|
-
# @return [Array<Symbol>]
|
|
295
|
-
def allowed = aasm.states(permitted: true).map(&:name)
|
|
296
|
-
|
|
297
|
-
# @param to [Symbol]
|
|
298
|
-
# @return [Boolean] whether that move is permitted right now
|
|
299
|
-
def may?(to) = allowed.include?(to)
|
|
300
|
-
|
|
301
|
-
# The next state along the spine, whether or not its guard passes.
|
|
302
|
-
#
|
|
303
|
-
# @return [Symbol, nil]
|
|
304
|
-
def spine_next = SPINE[key]
|
|
305
|
-
|
|
306
|
-
# Move the folder. With no argument it walks one step along the {SPINE}.
|
|
307
|
-
#
|
|
308
|
-
# A refusal is information — it means the phase has not actually happened
|
|
309
|
-
# yet — so it arrives as {Refused} carrying the reason, never as `false`.
|
|
310
|
-
#
|
|
311
|
-
# @param to [Symbol] destination, defaulting to the next spine state
|
|
312
|
-
# @return [Agentilda::Status] the state now occupied
|
|
313
|
-
# @raise [Agentilda::StateMachine::Refused]
|
|
314
|
-
def promote!(to = spine_next)
|
|
315
|
-
raise Refused, "#{describe(key)} is terminal — nothing follows it" if to.nil?
|
|
316
|
-
raise Refused, "nothing can reach #{describe(to)}" unless self.class.event_for.key?(to)
|
|
317
|
-
raise Refused, refusal(to) unless may?(to)
|
|
318
|
-
|
|
319
|
-
aasm.fire!(self.class.event_for.fetch(to))
|
|
320
|
-
status
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
# The state a folder's contents justify — the answer to "well, what should
|
|
324
|
-
# it be, then?". This is what `resync dirs` renames toward.
|
|
325
|
-
#
|
|
326
|
-
# Invariants are *minimum* requirements, not exact matches: a ⚪️ folder
|
|
327
|
-
# that has since grown a `plan.md` still satisfies ⚪️, and is nonetheless
|
|
328
|
-
# ⭐️ now. So the furthest-justified state wins, and the caller compares it
|
|
329
|
-
# against the current one to decide whether anything should move.
|
|
330
|
-
#
|
|
331
|
-
# The exception is a {FAMILIES family}, whose members contents cannot tell
|
|
332
|
-
# apart. Two rules cover it:
|
|
333
|
-
#
|
|
334
|
-
# - **Already inside one** — the current name wins. Re-deriving it would be
|
|
335
|
-
# a guess dressed up as a correction, and every ⭕️ would become 🅱️.
|
|
336
|
-
# - **Arriving from outside** — the *weakest* member the contents fit
|
|
337
|
-
# wins, not the furthest. A 📋 folder whose `plan.md` grew work units
|
|
338
|
-
# lands on ⭐️, not 🟡, because nobody has started. A ✅ folder found
|
|
339
|
-
# with an open pull request is demonstrably back in the PR phase, and
|
|
340
|
-
# ⭐️ refuses an open pull request, so it lands on 🟡 rather than
|
|
341
|
-
# claiming 🔴.
|
|
342
|
-
#
|
|
343
|
-
# @return [Agentilda::Status, nil] nil when nothing fits at all
|
|
344
|
-
def best_fit
|
|
345
|
-
fitting = STATUSES.select { |s| s.satisfied_by?(subject) }
|
|
346
|
-
return nil if fitting.empty?
|
|
347
|
-
|
|
348
|
-
best = PREFERENCE.filter_map { |k| fitting.find { |s| s.key == k } }.first || fitting.first
|
|
349
|
-
return status if self.class.family_of(key).include?(best.key)
|
|
350
|
-
|
|
351
|
-
family = self.class.family_of(best.key)
|
|
352
|
-
return best if family.empty?
|
|
353
|
-
|
|
354
|
-
floor = family.find { |k| fitting.any? { |s| s.key == k } }
|
|
355
|
-
STATUS_BY_KEY.fetch(floor)
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
private
|
|
359
|
-
|
|
360
|
-
# The one guard. Entering a state is permitted exactly when that state's
|
|
361
|
-
# invariant holds for this folder's contents.
|
|
362
|
-
#
|
|
363
|
-
# @return [Boolean]
|
|
364
|
-
def justified? = STATUS_BY_KEY.fetch(aasm.to_state).satisfied_by?(subject)
|
|
365
|
-
|
|
366
|
-
# @param to [Symbol]
|
|
367
|
-
# @return [String]
|
|
368
|
-
def refusal(to)
|
|
369
|
-
return "#{describe(key)} cannot become #{describe(to)}" unless self.class.edge?(key, to)
|
|
370
|
-
|
|
371
|
-
STATUS_BY_KEY.fetch(to).violation(subject)
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
# @param key [Symbol]
|
|
375
|
-
# @return [String]
|
|
376
|
-
def describe(key) = STATUS_BY_KEY.fetch(key).to_s
|
|
377
|
-
|
|
378
|
-
# The folder moves. {Subject} owns the path, so it owns the move.
|
|
379
|
-
#
|
|
380
|
-
# @return [void]
|
|
381
|
-
def rename! = subject.rename_to(STATUS_BY_KEY.fetch(aasm.to_state))
|
|
382
|
-
end
|
|
383
|
-
end
|