chrono_forge 0.10.0 → 0.11.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.
@@ -107,20 +107,28 @@ module ChronoForge
107
107
  validate_step_name_segment!(name || method)
108
108
  step_name = "durably_repeat$#{name || method}"
109
109
 
110
- # Get or create the main coordination log for this periodic task
110
+ # Get or create the main coordination log for this periodic task. A fresh
111
+ # coordinator records its first pass in the INSERT (attempts: 1,
112
+ # last_executed_at); only later passes bump via UPDATE.
111
113
  coordination_log = find_or_create_execution_log!(step_name) do |log|
112
- log.started_at = Time.current
114
+ now = Time.current
115
+ log.started_at = now
116
+ log.last_executed_at = now
117
+ log.attempts = 1
113
118
  log.metadata = {last_execution_at: nil}
114
119
  end
115
120
 
116
121
  # Return if already completed
117
122
  return if coordination_log.completed?
118
123
 
119
- # Update coordination log attempt tracking
120
- coordination_log.update!(
121
- attempts: coordination_log.attempts + 1,
122
- last_executed_at: Time.current
123
- )
124
+ # Update coordination log attempt tracking (first pass already recorded
125
+ # above on create).
126
+ unless coordination_log.previously_new_record?
127
+ coordination_log.update!(
128
+ attempts: coordination_log.attempts + 1,
129
+ last_executed_at: Time.current
130
+ )
131
+ end
124
132
 
125
133
  # Check if we should stop repeating
126
134
  condition_met = if till.is_a?(Symbol)
@@ -252,9 +260,14 @@ module ChronoForge
252
260
  def execute_or_schedule_repetition(method, coordination_log, next_execution_at, every, policy, timeout, on_error)
253
261
  step_name = "#{coordination_log.step_name}$#{next_execution_at.to_i}"
254
262
 
255
- # Create execution log for this specific repetition
263
+ # Create execution log for this specific repetition. A fresh repetition
264
+ # records its first attempt in the INSERT itself (attempts: 1,
265
+ # last_executed_at), so there is no separate pre-execution UPDATE.
256
266
  repetition_log = find_or_create_execution_log!(step_name) do |log|
257
- log.started_at = Time.current
267
+ now = Time.current
268
+ log.started_at = now
269
+ log.last_executed_at = now
270
+ log.attempts = 1
258
271
  log.metadata = {
259
272
  scheduled_for: next_execution_at,
260
273
  timeout_at: next_execution_at + timeout,
@@ -265,11 +278,14 @@ module ChronoForge
265
278
  # Return if this repetition is already completed
266
279
  return if repetition_log.completed?
267
280
 
268
- # Update execution log with attempt
269
- repetition_log.update!(
270
- attempts: repetition_log.attempts + 1,
271
- last_executed_at: Time.current
272
- )
281
+ # Existing logs (a resume of a scheduled-for-later repetition) still need
282
+ # the attempt bump; a freshly-created one recorded its first above.
283
+ unless repetition_log.previously_new_record?
284
+ repetition_log.update!(
285
+ attempts: repetition_log.attempts + 1,
286
+ last_executed_at: Time.current
287
+ )
288
+ end
273
289
 
274
290
  # Check if it's time to execute this repetition
275
291
  if next_execution_at <= Time.current
@@ -5,7 +5,8 @@ module ChronoForge
5
5
  # Join one or more named branches. Separate from dispatch so branches run
6
6
  # concurrently. Does one immediate check; if not done, hands off to the
7
7
  # lightweight BranchMergeJob and halts (the heavy parent is not replayed
8
- # per poll). Cadence clamps between min/max, scaled by pending.
8
+ # per poll). Poll cadence is driven by estimated time-to-drain, clamped
9
+ # between min/max (see BranchMergeJob#reschedule_delay).
9
10
  def merge_branches(*names, min_interval: 5.seconds, max_interval: 5.minutes)
10
11
  names.each do |nm|
11
12
  validate_step_name_segment!(nm) # rejects "$"
@@ -16,9 +17,9 @@ module ChronoForge
16
17
  end
17
18
 
18
19
  # Validate cadence here, in the parent, so a misconfiguration fails at the
19
- # call site instead of deep inside the poller — where (pending * FACTOR)
20
- # .clamp(min, max) would raise ArgumentError, a non-transient error that
21
- # dead-letters BranchMergeJob and orphans the parent.
20
+ # call site instead of deep inside the poller — where the clamp to
21
+ # [min_interval, max_interval] would raise ArgumentError, a non-transient
22
+ # error that dead-letters BranchMergeJob and orphans the parent.
22
23
  if min_interval > max_interval
23
24
  raise ArgumentError,
24
25
  "min_interval (#{min_interval}) must be <= max_interval (#{max_interval})"
@@ -50,34 +50,28 @@ module ChronoForge
50
50
  def complete_workflow!
51
51
  enforce_branch_joins!
52
52
 
53
- # Create an execution log for workflow completion
54
- execution_log = find_or_create_execution_log!("$workflow_completion$") do |log|
55
- log.started_at = Time.current
56
- end
57
-
53
+ # Completion is two writes with no external side effect between them: the
54
+ # workflow → :completed transition and the (born-completed) marker. Batch
55
+ # them in one transaction so a trivial child pays a single commit here,
56
+ # and write the marker in its terminal state in a single INSERT.
57
+ execution_log = nil
58
58
  begin
59
- workflow.completed_at = Time.current
60
- workflow.completed!
61
-
62
- # Mark execution log as completed. Attempt tracking and the terminal
63
- # state are written together: completion is not retried on an
64
- # attempt-count basis, so there is no need for a separate pre-write.
65
- execution_log.update!(
66
- attempts: execution_log.attempts + 1,
67
- last_executed_at: Time.current,
68
- state: :completed,
69
- completed_at: Time.current
70
- )
59
+ ActiveRecord::Base.transaction do
60
+ workflow.completed_at = Time.current
61
+ workflow.completed!
62
+ execution_log = create_completed_execution_log!("$workflow_completion$")
63
+ end
71
64
 
72
65
  # Return the execution log for tracking
73
66
  execution_log
74
67
  rescue => e
75
- # Log any errors
76
- execution_log.update!(
77
- state: :failed,
78
- error_message: e.message,
79
- error_class: e.class.name
80
- )
68
+ # The transaction rolled back (so the marker may be gone too). Re-find
69
+ # or recreate it and record the failure for observability, then re-raise.
70
+ # The workflow stays not-completed, so a resume retries completion.
71
+ log = find_or_create_execution_log!("$workflow_completion$") do |l|
72
+ l.started_at = Time.current
73
+ end
74
+ log.update!(state: :failed, error_message: e.message, error_class: e.class.name)
81
75
  raise
82
76
  end
83
77
  end
@@ -153,36 +147,30 @@ module ChronoForge
153
147
  # - Safe to call multiple times with same error_log
154
148
  #
155
149
  def fail_workflow!(error_log)
156
- # Create an execution log for workflow failure
157
- execution_log = find_or_create_execution_log!("$workflow_failure$#{error_log.id}") do |log|
158
- log.started_at = Time.current
159
- log.metadata = {
160
- error_log_id: error_log.id
161
- }
162
- end
150
+ step_name = "$workflow_failure$#{error_log.id}"
163
151
 
152
+ # Mirror complete_workflow!: the workflow → :failed transition and the
153
+ # (born-completed) failure marker are batched in one transaction, and the
154
+ # marker is written in its terminal state in a single INSERT.
155
+ execution_log = nil
164
156
  begin
165
- workflow.failed!
166
-
167
- # Mark execution log as completed. Attempt tracking and the terminal
168
- # state are written together (failure handling is not retried on an
169
- # attempt-count basis).
170
- execution_log.update!(
171
- attempts: execution_log.attempts + 1,
172
- last_executed_at: Time.current,
173
- state: :completed,
174
- completed_at: Time.current
175
- )
157
+ ActiveRecord::Base.transaction do
158
+ workflow.failed!
159
+ execution_log = create_completed_execution_log!(step_name) do |log|
160
+ log.metadata = {error_log_id: error_log.id}
161
+ end
162
+ end
176
163
 
177
164
  # Return the execution log for tracking
178
165
  execution_log
179
166
  rescue => e
180
- # Log any errors
181
- execution_log.update!(
182
- state: :failed,
183
- error_message: e.message,
184
- error_class: e.class.name
185
- )
167
+ # The transaction rolled back; re-find/recreate the marker and record
168
+ # the failure for observability, then re-raise.
169
+ log = find_or_create_execution_log!(step_name) do |l|
170
+ l.started_at = Time.current
171
+ l.metadata = {error_log_id: error_log.id}
172
+ end
173
+ log.update!(state: :failed, error_message: e.message, error_class: e.class.name)
186
174
  raise
187
175
  end
188
176
  end
@@ -210,15 +210,6 @@ module ChronoForge
210
210
  workflow.kwargs = kwargs
211
211
  workflow.started_at = Time.current
212
212
  end
213
-
214
- # Branch children are pre-inserted by their parent (dispatch_children's
215
- # insert_all), so the creation block above never runs for them and their
216
- # started_at stays nil. Stamp it the first time the child actually executes
217
- # so started_at reliably means "has been picked up and run" — the
218
- # BranchMergeJob rekick poller treats a nil started_at as a never-executed
219
- # (dropped) child, and must not mistake a child that ran and is now parked
220
- # on a wait (also :idle) for one that was never picked up.
221
- @workflow.update_column(:started_at, Time.current) if @workflow.started_at.nil?
222
213
  end
223
214
 
224
215
  def setup_context!
@@ -259,6 +250,40 @@ module ChronoForge
259
250
  ExecutionLog.create_or_find_by!(workflow: @workflow, step_name: step_name, &)
260
251
  end
261
252
 
253
+ # Record a step that completes synchronously within this pass as a single
254
+ # INSERT already in its terminal :completed state — there is no
255
+ # started→completed UPDATE chasing the INSERT (one statement, not two). Use
256
+ # this for steps with no deferral point between create and completion
257
+ # (workflow completion/failure markers and similar); deferring steps (waits,
258
+ # branch coordination) must stay :started across resumes and use
259
+ # find_or_create_execution_log! instead.
260
+ #
261
+ # Idempotent: an already-existing row (a resume after a partial pass, or a
262
+ # create race) is flipped to :completed instead of duplicated. The block runs
263
+ # only on create, letting callers attach metadata to the new row.
264
+ def create_completed_execution_log!(step_name)
265
+ now = Time.current
266
+ log = find_or_create_execution_log!(step_name) do |l|
267
+ l.attempts = 1
268
+ l.started_at = now
269
+ l.last_executed_at = now
270
+ l.completed_at = now
271
+ l.state = :completed
272
+ yield l if block_given?
273
+ end
274
+
275
+ unless log.completed?
276
+ log.update!(
277
+ attempts: log.attempts + 1,
278
+ last_executed_at: now,
279
+ completed_at: now,
280
+ state: :completed
281
+ )
282
+ end
283
+
284
+ log
285
+ end
286
+
262
287
  # One bulk read of this workflow's completed steps, mapping step_name to its
263
288
  # metadata, memoized for the duration of a single replay pass.
264
289
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChronoForge
4
- VERSION = "0.10.0"
4
+ VERSION = "0.11.0"
5
5
  end
data/lib/chrono_forge.rb CHANGED
@@ -13,4 +13,12 @@ module ChronoForge
13
13
  class Error < StandardError; end
14
14
 
15
15
  def self.ApplicationRecord = defined?(::ApplicationRecord) ? ::ApplicationRecord : ActiveRecord::Base
16
+
17
+ # Engine configuration (see ChronoForge::Configuration).
18
+ # ChronoForge.configure { |c| c.branch_merge_queue = :chrono_forge_pollers }
19
+ def self.config = @config ||= Configuration.new
20
+
21
+ def self.configure = yield(config)
22
+
23
+ def self.reset_configuration! = @config = Configuration.new
16
24
  end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Release flow (chrono_forge monorepo: core + dashboard)
4
+ # ------------------------------------------------------
5
+ # Publishing happens from a laptop. CI does NOT push to any registry — it only
6
+ # cuts the GitHub Release (notes + the built gem) when the tag lands.
7
+ #
8
+ # 1. rake release:core:prepare # auto-computes next version (git-cliff)
9
+ # rake release:core:prepare[0.11.0] # ...or pass one explicitly
10
+ # 2. git diff # review the bump + changelog (nothing committed yet)
11
+ # 3. rake release:core:publish # commit, build + push gem, then tag + push → CI cuts the Release
12
+ #
13
+ # Same tasks under release:dashboard:*. Release `core` BEFORE `dashboard` —
14
+ # the dashboard depends on core, so bump its `chrono_forge` floor first if needed.
15
+ #
16
+ # prepare leaves the bump + changelog UNCOMMITTED so you can review the diff
17
+ # first; publish commits them, then publishes. publish is idempotent + resumable:
18
+ # it skips a gem already live and only tags if the tag is missing, so a partial
19
+ # failure can just be re-run.
20
+
21
+ RELEASE_CLIFF_CONFIG = "cliff.toml"
22
+
23
+ # Per-gem config. tag_pattern + scope mirror what each CHANGELOG should reflect:
24
+ # core = everything except the dashboard subtree, dashboard = only that subtree.
25
+ RELEASE_GEMS = {
26
+ "core" => {
27
+ name: "chrono_forge",
28
+ version_file: "lib/chrono_forge/version.rb",
29
+ changelog: "CHANGELOG.md",
30
+ gemspec: "chrono_forge.gemspec",
31
+ build_dir: ".",
32
+ tag_prefix: "v",
33
+ tag_pattern: "^v[0-9]",
34
+ scope: ["--exclude-path", "chrono_forge-dashboard/**"],
35
+ extra_files: []
36
+ },
37
+ "dashboard" => {
38
+ name: "chrono_forge-dashboard",
39
+ version_file: "chrono_forge-dashboard/lib/chrono_forge/dashboard/version.rb",
40
+ changelog: "chrono_forge-dashboard/CHANGELOG.md",
41
+ gemspec: "chrono_forge-dashboard.gemspec",
42
+ build_dir: "chrono_forge-dashboard",
43
+ tag_prefix: "chrono_forge-dashboard-v",
44
+ tag_pattern: "^chrono_forge-dashboard-v[0-9]",
45
+ scope: ["--include-path", "chrono_forge-dashboard/**"],
46
+ # The dashboard ships a compiled stylesheet — recompile it so the tagged
47
+ # tree (and the gem) never ship CSS that lags the source/views.
48
+ assets: -> {
49
+ Dir.chdir("chrono_forge-dashboard") do
50
+ system("bundle", "exec", "rake", "tailwind:build") || abort("tailwind:build failed")
51
+ end
52
+ },
53
+ extra_files: ["chrono_forge-dashboard/app/assets/chrono_forge/dashboard/dashboard.css"]
54
+ }
55
+ }
56
+
57
+ namespace :release do
58
+ # --- helpers --------------------------------------------------------------
59
+
60
+ def git_cliff?
61
+ system("which git-cliff > /dev/null 2>&1")
62
+ end
63
+
64
+ def release_current_version(cfg)
65
+ File.read(cfg[:version_file])[/VERSION = "([\d.]+)"/, 1] ||
66
+ abort("Could not read VERSION from #{cfg[:version_file]}")
67
+ end
68
+
69
+ def release_cliff_cmd(cfg, *extra)
70
+ ["git-cliff", "--config", RELEASE_CLIFF_CONFIG, "--tag-pattern", cfg[:tag_pattern], *cfg[:scope], *extra]
71
+ end
72
+
73
+ # Capture git-cliff stdout, discarding the stderr update-check chatter.
74
+ def release_capture(cmd)
75
+ IO.popen(cmd, err: File::NULL, &:read)
76
+ end
77
+
78
+ # Next version per conventional commits. git-cliff owns the semver math
79
+ # (including the pre-1.0 rules under [bump] in cliff.toml). Returns it
80
+ # without the gem's tag prefix.
81
+ def release_next_version(cfg)
82
+ abort "git-cliff not found. Install with: brew install git-cliff" unless git_cliff?
83
+ bumped = release_capture(release_cliff_cmd(cfg, "--bumped-version")).strip
84
+ abort "git-cliff could not compute a version (no conventional commits since the last #{cfg[:name]} tag?)" if bumped.empty?
85
+ bumped.delete_prefix(cfg[:tag_prefix])
86
+ end
87
+
88
+ def release_gem_published?(cfg, version)
89
+ out = `gem list --remote --exact --all #{cfg[:name]} 2>/dev/null`
90
+ out.include?("#{version},") || out.include?("#{version})") || out.include?(" #{version} ")
91
+ end
92
+
93
+ # Inject ONLY this version's section above the latest entry. A full -o regen
94
+ # would misattribute past releases because path-filtering confuses cliff's
95
+ # historical tag boundaries; existing entries are preserved verbatim.
96
+ def release_prepend_changelog(path, section)
97
+ body = File.read(path)
98
+ block = "#{section.strip}\n\n"
99
+ updated = (body =~ /^## \[/) ? body.sub(/^## \[/, "#{block}## [") : "#{body.rstrip}\n\n#{block}"
100
+ File.write(path, updated)
101
+ end
102
+
103
+ # --- per-gem tasks --------------------------------------------------------
104
+
105
+ RELEASE_GEMS.each do |key, cfg|
106
+ namespace key do
107
+ desc "Show #{cfg[:name]}'s next version computed from conventional commits"
108
+ task :version do
109
+ puts "#{cfg[:name]} current: #{release_current_version(cfg)}"
110
+ puts "#{cfg[:name]} next: #{release_next_version(cfg)}"
111
+ end
112
+
113
+ desc "Prepare a #{cfg[:name]} release commit (bump + changelog + assets). Version optional; git-cliff computes it."
114
+ task :prepare, [:version] do |_t, args|
115
+ version = args[:version] || release_next_version(cfg)
116
+ abort "Error: version must be in format X.Y.Z (got #{version.inspect})" unless version.match?(/^\d+\.\d+\.\d+$/)
117
+ abort "Error: working tree is dirty. Commit or stash first." unless `git status --porcelain`.strip.empty?
118
+ abort "Error: not on main." unless `git rev-parse --abbrev-ref HEAD`.strip == "main"
119
+
120
+ system("git fetch -q origin")
121
+ abort "Error: main is not in sync with origin/main." unless `git rev-parse HEAD`.strip == `git rev-parse origin/main`.strip
122
+
123
+ tag = "#{cfg[:tag_prefix]}#{version}"
124
+ abort "Error: tag #{tag} already exists." if system("git rev-parse #{tag} >/dev/null 2>&1")
125
+
126
+ puts "Preparing #{cfg[:name]} #{version} (tag #{tag})..."
127
+
128
+ # Bump version.
129
+ content = File.read(cfg[:version_file])
130
+ File.write(cfg[:version_file], content.gsub(/VERSION = "[\d.]+"/, %(VERSION = "#{version}")))
131
+ puts "✓ #{cfg[:version_file]}"
132
+
133
+ # Compile assets (dashboard CSS), if any.
134
+ cfg[:assets]&.call
135
+
136
+ # Changelog — same config CI uses for the notes, so they agree.
137
+ section = release_capture(release_cliff_cmd(cfg, "--tag", tag, "--unreleased", "--strip", "all"))
138
+ abort "git-cliff found no entries since the last #{cfg[:name]} tag — nothing to release." if section.strip.empty?
139
+ release_prepend_changelog(cfg[:changelog], section)
140
+ puts "✓ #{cfg[:changelog]}"
141
+
142
+ # Leave everything uncommitted so the bump + changelog can be reviewed
143
+ # before anything is committed or published. publish makes the commit.
144
+ files = [cfg[:version_file], cfg[:changelog], *cfg[:extra_files]]
145
+
146
+ puts "\n✓ Prepared #{cfg[:name]} #{version} — nothing committed yet."
147
+ puts "Next:"
148
+ puts " git diff -- #{files.join(" ")}"
149
+ puts " rake release:#{key}:publish # commit, build + push gem, then tag + push"
150
+ puts " (abort with: git checkout -- #{files.join(" ")})"
151
+ end
152
+
153
+ desc "Publish #{cfg[:name]} (build + push gem, then tag + push). Idempotent + resumable."
154
+ task :publish do
155
+ version = release_current_version(cfg)
156
+ tag = "#{cfg[:tag_prefix]}#{version}"
157
+ files = [cfg[:version_file], cfg[:changelog], *cfg[:extra_files]]
158
+
159
+ # Commit the prepared changes (you review the diff between prepare and
160
+ # here). Resumable: if they're already committed — e.g. a re-run after a
161
+ # partial failure — skip straight to publishing.
162
+ if `git status --porcelain -- #{files.join(" ")}`.strip.empty?
163
+ unless `git log -1 --format=%s`.strip == "chore(release): #{cfg[:name]} #{version}"
164
+ abort "Nothing prepared — run rake release:#{key}:prepare first."
165
+ end
166
+ else
167
+ system("git", "add", *files) || abort("git add failed")
168
+ system("git", "commit", "-m", "chore(release): #{cfg[:name]} #{version}") || abort("git commit failed")
169
+ puts "✓ Committed #{cfg[:name]} #{version}"
170
+ end
171
+
172
+ if release_gem_published?(cfg, version)
173
+ puts "• #{cfg[:name]} #{version} already on RubyGems — skipping"
174
+ else
175
+ puts "Building + pushing gem..."
176
+ Dir.chdir(cfg[:build_dir]) do
177
+ system("gem build #{cfg[:gemspec]}") || abort("Gem build failed")
178
+ gem_file = "#{cfg[:name]}-#{version}.gem"
179
+ system("gem push #{gem_file}") || abort("Gem push failed")
180
+ File.delete(gem_file) if File.exist?(gem_file)
181
+ end
182
+ puts "✓ Published #{cfg[:name]} #{version} to RubyGems"
183
+ end
184
+
185
+ # Tag + push last, so CI cuts the Release only once the gem is live.
186
+ branch = `git branch --show-current`.strip
187
+ if system("git rev-parse #{tag} >/dev/null 2>&1")
188
+ puts "• tag #{tag} already exists — skipping tag"
189
+ else
190
+ system("git", "tag", tag) || abort("git tag failed")
191
+ end
192
+ system("git", "push", "origin", branch) || abort("git push branch failed")
193
+ system("git", "push", "origin", tag) || abort("git push tag failed")
194
+
195
+ puts "\n✓ Released #{tag}. GitHub Actions will cut the Release from the tag."
196
+ if key == "core"
197
+ puts " Next: bump the dashboard's chrono_forge floor if it should require #{version}, then release:dashboard:*"
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ # Neutralize the dangerous bare `rake release` that bundler/gem_tasks defines
205
+ # (it would tag + gem push directly). Point people at the real flow instead.
206
+ if Rake::Task.task_defined?("release")
207
+ Rake::Task["release"].clear
208
+ desc "Disabled — use release:core:* or release:dashboard:* (see lib/tasks/release.rake)"
209
+ task :release do
210
+ warn "Use `rake release:core:prepare` then `rake release:core:publish` (or :dashboard). See lib/tasks/release.rake."
211
+ end
212
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chrono_forge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-27 00:00:00.000000000 Z
11
+ date: 2026-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: prism
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -194,7 +208,10 @@ files:
194
208
  - LICENSE.txt
195
209
  - README.md
196
210
  - Rakefile
211
+ - cliff.toml
197
212
  - config.ru
213
+ - docs/design/per-child-commit-overhead.md
214
+ - docs/fanout-scale-test.md
198
215
  - docs/superpowers/plans/2026-06-25-chrono_forge-dashboard.md
199
216
  - docs/superpowers/plans/2026-06-25-chrono_forge-dashboard.md.tasks.json
200
217
  - docs/superpowers/plans/2026-06-25-composite-retry-policies.md
@@ -205,6 +222,10 @@ files:
205
222
  - docs/superpowers/plans/2026-06-26-branches-spawn-merge.md.tasks.json
206
223
  - docs/superpowers/plans/2026-06-26-deferral-continuation-race-and-catchup.md
207
224
  - docs/superpowers/plans/2026-06-26-deferral-continuation-race-and-catchup.md.tasks.json
225
+ - docs/superpowers/plans/2026-06-30-poller-rekick-and-eta-cadence.md
226
+ - docs/superpowers/plans/2026-06-30-poller-rekick-and-eta-cadence.md.tasks.json
227
+ - docs/superpowers/plans/2026-07-01-workflow-definition-dag.md
228
+ - docs/superpowers/plans/2026-07-01-workflow-definition-dag.md.tasks.json
208
229
  - docs/superpowers/specs/2026-06-03-unified-retry-policy-design.md
209
230
  - docs/superpowers/specs/2026-06-25-chrono_forge-dashboard-design.md
210
231
  - docs/superpowers/specs/2026-06-25-composite-retry-policies-design.md
@@ -212,6 +233,7 @@ files:
212
233
  - docs/superpowers/specs/2026-06-25-spawn-merge-branches-design.md
213
234
  - docs/superpowers/specs/2026-06-26-dashboard-branch-view-design.md
214
235
  - docs/superpowers/specs/2026-06-26-deferral-continuation-race-and-catchup-design.md
236
+ - docs/superpowers/specs/2026-07-01-workflow-definition-dag-design.md
215
237
  - examples/continue_if_webhook_example.rb
216
238
  - gemfiles/rails_7.1.gemfile
217
239
  - gemfiles/rails_7.1.gemfile.lock
@@ -220,6 +242,9 @@ files:
220
242
  - lib/chrono_forge/branch_probe.rb
221
243
  - lib/chrono_forge/cleanup.rb
222
244
  - lib/chrono_forge/cleanup_job.rb
245
+ - lib/chrono_forge/configuration.rb
246
+ - lib/chrono_forge/definition.rb
247
+ - lib/chrono_forge/definition_analyzer.rb
223
248
  - lib/chrono_forge/error_log.rb
224
249
  - lib/chrono_forge/execution_log.rb
225
250
  - lib/chrono_forge/executor.rb
@@ -248,6 +273,7 @@ files:
248
273
  - lib/generators/chrono_forge/templates/install_chrono_forge.rb
249
274
  - lib/generators/chrono_forge/upgrade/USAGE
250
275
  - lib/generators/chrono_forge/upgrade/upgrade_generator.rb
276
+ - lib/tasks/release.rake
251
277
  - sig/chrono_forge.rbs
252
278
  homepage: https://github.com/radioactive-labs/chrono_forge
253
279
  licenses: