chrono_forge 0.9.1 → 0.10.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +305 -44
  4. data/docs/superpowers/plans/2026-06-25-chrono_forge-dashboard.md +1748 -0
  5. data/docs/superpowers/plans/2026-06-25-chrono_forge-dashboard.md.tasks.json +17 -0
  6. data/docs/superpowers/plans/2026-06-25-composite-retry-policies.md +930 -0
  7. data/docs/superpowers/plans/2026-06-25-composite-retry-policies.md.tasks.json +54 -0
  8. data/docs/superpowers/plans/2026-06-25-reserved-kwarg-guard.md +241 -0
  9. data/docs/superpowers/plans/2026-06-25-reserved-kwarg-guard.md.tasks.json +12 -0
  10. data/docs/superpowers/plans/2026-06-26-branches-spawn-merge.md +1378 -0
  11. data/docs/superpowers/plans/2026-06-26-branches-spawn-merge.md.tasks.json +67 -0
  12. data/docs/superpowers/plans/2026-06-26-deferral-continuation-race-and-catchup.md +709 -0
  13. data/docs/superpowers/plans/2026-06-26-deferral-continuation-race-and-catchup.md.tasks.json +19 -0
  14. data/docs/superpowers/specs/2026-06-03-unified-retry-policy-design.md +226 -0
  15. data/docs/superpowers/specs/2026-06-25-chrono_forge-dashboard-design.md +190 -0
  16. data/docs/superpowers/specs/2026-06-25-composite-retry-policies-design.md +228 -0
  17. data/docs/superpowers/specs/2026-06-25-reserved-kwarg-guard-design.md +169 -0
  18. data/docs/superpowers/specs/2026-06-25-spawn-merge-branches-design.md +468 -0
  19. data/docs/superpowers/specs/2026-06-26-dashboard-branch-view-design.md +142 -0
  20. data/docs/superpowers/specs/2026-06-26-deferral-continuation-race-and-catchup-design.md +265 -0
  21. data/lib/chrono_forge/branch_merge_job.rb +138 -0
  22. data/lib/chrono_forge/branch_probe.rb +26 -0
  23. data/lib/chrono_forge/cleanup.rb +6 -0
  24. data/lib/chrono_forge/execution_log.rb +6 -0
  25. data/lib/chrono_forge/executor/composite_retry_policy.rb +47 -0
  26. data/lib/chrono_forge/executor/methods/branch.rb +185 -0
  27. data/lib/chrono_forge/executor/methods/durably_execute.rb +21 -19
  28. data/lib/chrono_forge/executor/methods/durably_repeat.rb +118 -25
  29. data/lib/chrono_forge/executor/methods/merge_branches.rb +83 -0
  30. data/lib/chrono_forge/executor/methods/wait.rb +2 -4
  31. data/lib/chrono_forge/executor/methods/wait_until.rb +25 -25
  32. data/lib/chrono_forge/executor/methods/workflow_states.rb +16 -0
  33. data/lib/chrono_forge/executor/methods.rb +2 -0
  34. data/lib/chrono_forge/executor/retry_policy.rb +111 -0
  35. data/lib/chrono_forge/executor.rb +216 -28
  36. data/lib/chrono_forge/version.rb +1 -1
  37. data/lib/chrono_forge/workflow.rb +10 -1
  38. data/lib/generators/chrono_forge/migration_actions.rb +1 -0
  39. data/lib/generators/chrono_forge/templates/add_chrono_forge_parent_execution_log.rb +38 -0
  40. metadata +42 -5
  41. data/lib/chrono_forge/executor/retry_strategy.rb +0 -29
@@ -0,0 +1,54 @@
1
+ {
2
+ "planPath": "docs/superpowers/plans/2026-06-25-composite-retry-policies.md",
3
+ "tasks": [
4
+ {
5
+ "id": 1,
6
+ "subject": "Task 1: RetryPolicy — matches?, retry_backoff, compose",
7
+ "status": "completed",
8
+ "description": "**Goal:** Add matches?, retry_backoff(error, attempts:), and self.compose to RetryPolicy without changing existing behavior.\n\n**Files:** Modify lib/chrono_forge/executor/retry_policy.rb; Test test/retry_policy_test.rb\n\n**Verify:** bin/rails test test/retry_policy_test.rb\n\n```json:metadata\n{\"files\": [\"lib/chrono_forge/executor/retry_policy.rb\", \"test/retry_policy_test.rb\"], \"verifyCommand\": \"bin/rails test test/retry_policy_test.rb\", \"acceptanceCriteria\": [\"matches? routing predicate\", \"retry_backoff returns Duration/nil and ignores block\", \"compose returns CompositeRetryPolicy\", \"existing tests pass\"], \"requiresUserVerification\": false}\n```"
9
+ },
10
+ {
11
+ "id": 2,
12
+ "subject": "Task 2: CompositeRetryPolicy class",
13
+ "status": "completed",
14
+ "blockedBy": [1],
15
+ "description": "**Goal:** Add pure CompositeRetryPolicy with policy_for, block-driven retry_backoff, coarse max_attempts, empty-list guard.\n\n**Files:** Create lib/chrono_forge/executor/composite_retry_policy.rb; Test test/composite_retry_policy_test.rb\n\n**Verify:** bin/rails test test/composite_retry_policy_test.rb\n\n```json:metadata\n{\"files\": [\"lib/chrono_forge/executor/composite_retry_policy.rb\", \"test/composite_retry_policy_test.rb\"], \"verifyCommand\": \"bin/rails test test/composite_retry_policy_test.rb\", \"acceptanceCriteria\": [\"policy_for first-match/subclass/nil\", \"retry_backoff yields index and uses count\", \"no-block falls back to attempts\", \"max_attempts coarse bound\", \"empty list raises\"], \"requiresUserVerification\": false}\n```"
16
+ },
17
+ {
18
+ "id": 3,
19
+ "subject": "Task 3: Executor coercion, class DSL overload, bump_retry_count!",
20
+ "status": "completed",
21
+ "blockedBy": [1, 2],
22
+ "description": "**Goal:** Accept arrays as composites, extend class DSL for positional policies, add metadata counter helper.\n\n**Files:** Modify lib/chrono_forge/executor.rb; Test test/composite_retry_policy_executor_test.rb\n\n**Verify:** bin/rails test test/composite_retry_policy_executor_test.rb\n\n```json:metadata\n{\"files\": [\"lib/chrono_forge/executor.rb\", \"test/composite_retry_policy_executor_test.rb\"], \"verifyCommand\": \"bin/rails test test/composite_retry_policy_executor_test.rb\", \"acceptanceCriteria\": [\"coerce_policy wraps array/passes through/nil\", \"resolvers coerce\", \"class DSL positional vs kwargs vs mixed\", \"bump_retry_count! increments+persists+nil-safe\"], \"requiresUserVerification\": false}\n```"
23
+ },
24
+ {
25
+ "id": 4,
26
+ "subject": "Task 4: Wire three step sites to retry_backoff",
27
+ "status": "completed",
28
+ "blockedBy": [3],
29
+ "description": "**Goal:** Switch durably_execute, wait_until, durably_repeat to retry_backoff + bump_retry_count!; terminal branches unchanged.\n\n**Files:** Modify lib/chrono_forge/executor/methods/{durably_execute,wait_until,durably_repeat}.rb\n\n**Verify:** bin/rails test test/retry_policy_integration_test.rb test/workflow_retry_api_test.rb\n\n```json:metadata\n{\"files\": [\"lib/chrono_forge/executor/methods/durably_execute.rb\", \"lib/chrono_forge/executor/methods/wait_until.rb\", \"lib/chrono_forge/executor/methods/durably_repeat.rb\"], \"verifyCommand\": \"bin/rails test test/retry_policy_integration_test.rb test/workflow_retry_api_test.rb\", \"acceptanceCriteria\": [\"each step site uses retry_backoff + bump_retry_count!\", \"terminal branches unchanged\", \"single-policy integration tests pass\"], \"requiresUserVerification\": false}\n```"
30
+ },
31
+ {
32
+ "id": 5,
33
+ "subject": "Task 5: Wire workflow-level perform site",
34
+ "status": "completed",
35
+ "blockedBy": [3],
36
+ "description": "**Goal:** Thread retry_counts job arg for per-error budgets at workflow level; keep safety-net guard correct for composites.\n\n**Files:** Modify lib/chrono_forge/executor.rb (perform signature + rescue)\n\n**Verify:** bin/rails test test/workflow_retry_api_test.rb test/retry_policy_integration_test.rb\n\n```json:metadata\n{\"files\": [\"lib/chrono_forge/executor.rb\"], \"verifyCommand\": \"bin/rails test test/workflow_retry_api_test.rb test/retry_policy_integration_test.rb\", \"acceptanceCriteria\": [\"perform threads retry_counts\", \"rescue uses retry_backoff\", \"safety-net guard honors coarse max_attempts\", \"single-policy workflow tests pass\"], \"requiresUserVerification\": false}\n```"
37
+ },
38
+ {
39
+ "id": 6,
40
+ "subject": "Task 6: Integration tests for composite behavior",
41
+ "status": "completed",
42
+ "blockedBy": [4, 5],
43
+ "description": "**Goal:** Prove per-error budgets, fail-fast, subclass routing, array coercion, single-policy regression end-to-end.\n\n**Files:** Create test/composite_retry_policy_integration_test.rb\n\n**Verify:** bin/rails test\n\n```json:metadata\n{\"files\": [\"test/composite_retry_policy_integration_test.rb\"], \"verifyCommand\": \"bin/rails test\", \"acceptanceCriteria\": [\"independent per-error budgets\", \"fail-fast max_attempts:1\", \"subclass routes to parent budget\", \"array coerced\", \"single policy writes no retry_counts\", \"full suite green\"], \"requiresUserVerification\": false}\n```"
44
+ },
45
+ {
46
+ "id": 7,
47
+ "subject": "Task 7: Document composite policies in README",
48
+ "status": "completed",
49
+ "blockedBy": [6],
50
+ "description": "**Goal:** Add \"Composite policies (per-error budgets)\" subsection to README Retry Policies section.\n\n**Files:** Modify README.md\n\n**Verify:** grep -n 'Composite policies' README.md\n\n```json:metadata\n{\"files\": [\"README.md\"], \"verifyCommand\": \"grep -n 'Composite policies' README.md\", \"acceptanceCriteria\": [\"worked array example\", \"ordering + catch-all + fail-fast stated\", \"independent budget/backoff stated\", \"class DSL positional form shown\"], \"requiresUserVerification\": false}\n```"
51
+ }
52
+ ],
53
+ "lastUpdated": "2026-06-25"
54
+ }
@@ -0,0 +1,241 @@
1
+ # Reserved-keyword Guard + Keywords-only Enqueue Contract — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (recommended) or superpowers-extended-cc:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make the public `perform_now`/`perform_later` of `Executor`-prepended jobs reject ChronoForge's reserved internal kwargs and any extra positional argument, while keeping `options`/user kwargs and the `retry_now`/`retry_later` helpers working.
6
+
7
+ **Architecture:** All changes live in `lib/chrono_forge/executor.rb` inside the `class << base` block (plus one module-level constant). A shared private `__validate_enqueue!` guard backs both public enqueue methods. `retry_now`/`retry_later` are rewritten to enqueue through `.set(...)`, whose ActiveJob `ConfiguredJob` proxy bypasses the class-level override, letting the framework inject the reserved `retry_workflow: true` flag past the guard. Framework continuations already use `.set(...)` and need no change.
8
+
9
+ **Tech Stack:** Ruby, Rails/ActiveJob 7.1.3.4, Minitest (`ActiveJob::TestCase`), ChaoticJob test helpers, Combustion test harness.
10
+
11
+ **User Verification:** NO — no user verification required.
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ - **Modify** `lib/chrono_forge/executor.rb`
18
+ - Add module-level constant `RESERVED_KWARGS` near `STEP_NAME_DELIMITER` (line 19).
19
+ - Replace the `perform_now`, `perform_later`, `retry_now`, `retry_later` definitions in the `class << base` block (lines ~29–54). Leave `retry_policy` (lines ~56–68) untouched.
20
+ - Append a `private` section with `__validate_enqueue!` at the **end** of the `class << base` block (after `retry_policy`) so `retry_policy` stays public.
21
+ - **Create** `test/enqueue_contract_test.rb` — covers reserved-key rejection, keywords-only contract, non-string key, `options`/kwargs pass-through, and retry-helper reserved-key rejection.
22
+
23
+ ---
24
+
25
+ ### Task 1: Reserved-key + keywords-only enqueue guard, with retry-helper rerouting
26
+
27
+ **Goal:** Public `perform_now`/`perform_later` reject `attempt`/`retry_counts`/`retry_workflow` and extra positionals; `options` and user kwargs still flow through; `retry_now`/`retry_later` keep working by routing past the guard.
28
+
29
+ **Files:**
30
+ - Modify: `lib/chrono_forge/executor.rb` (constant near line 19; `class << base` block lines ~29–54; append private helper before the block's closing `end` at line ~69)
31
+ - Test: `test/enqueue_contract_test.rb` (create)
32
+
33
+ **Acceptance Criteria:**
34
+ - [ ] `perform_later`/`perform_now` raise `ArgumentError` (message names the key, contains "reserved") when passed `attempt:`, `retry_counts:`, or `retry_workflow:`, and enqueue nothing.
35
+ - [ ] `perform_later`/`perform_now` raise `ArgumentError` (message mentions "keyword") when given a second positional argument.
36
+ - [ ] Non-String `key` still raises `ArgumentError`.
37
+ - [ ] `perform_later(key, foo:, options:)` enqueues; `options` reaches `workflow.options` and user kwargs reach `workflow.kwargs`/the job body.
38
+ - [ ] `retry_now`/`retry_later` reject reserved keys supplied by the caller, and the existing retry end-to-end tests still pass.
39
+ - [ ] Full suite green: `bundle exec rake test`.
40
+
41
+ **Verify:** `bundle exec rake test TEST=test/enqueue_contract_test.rb` → all pass, then `bundle exec rake test` → 139+ tests, 0 failures, 0 errors.
42
+
43
+ **Steps:**
44
+
45
+ - [ ] **Step 1: Write the failing tests**
46
+
47
+ Create `test/enqueue_contract_test.rb`:
48
+
49
+ ```ruby
50
+ require "test_helper"
51
+
52
+ # Public enqueue contract for Executor-prepended jobs: perform_now/perform_later
53
+ # accept exactly one positional (`key`) plus keywords, reject ChronoForge's
54
+ # reserved internal kwargs, and pass `options`/user kwargs through to the
55
+ # workflow record. retry_now/retry_later route past the guard via `.set(...)`.
56
+ class EnqueueContractTest < ActiveJob::TestCase
57
+ include ChaoticJob::Helpers
58
+
59
+ RESERVED = %i[attempt retry_counts retry_workflow].freeze
60
+
61
+ def setup
62
+ ChronoForge::Workflow.destroy_all
63
+ end
64
+
65
+ class ContractJob < WorkflowJob
66
+ prepend ChronoForge::Executor
67
+ def perform(foo: nil)
68
+ context[:foo] = foo
69
+ end
70
+ end
71
+
72
+ # --- reserved-key rejection ------------------------------------------------
73
+
74
+ def test_perform_later_rejects_reserved_keys
75
+ RESERVED.each do |reserved|
76
+ err = assert_raises(ArgumentError) do
77
+ assert_no_enqueued_jobs do
78
+ ContractJob.perform_later("k-#{reserved}", reserved => 1)
79
+ end
80
+ end
81
+ assert_match(/reserved/, err.message)
82
+ assert_match(reserved.to_s, err.message)
83
+ end
84
+ end
85
+
86
+ def test_perform_now_rejects_reserved_keys
87
+ RESERVED.each do |reserved|
88
+ err = assert_raises(ArgumentError) do
89
+ ContractJob.perform_now("k-#{reserved}", reserved => 1)
90
+ end
91
+ assert_match(/reserved/, err.message)
92
+ end
93
+ end
94
+
95
+ # --- keywords-only contract ------------------------------------------------
96
+
97
+ def test_perform_later_rejects_extra_positional
98
+ err = assert_raises(ArgumentError) do
99
+ assert_no_enqueued_jobs { ContractJob.perform_later("k", 99) }
100
+ end
101
+ assert_match(/keyword/, err.message)
102
+ end
103
+
104
+ def test_perform_now_rejects_extra_positional
105
+ err = assert_raises(ArgumentError) { ContractJob.perform_now("k", 99) }
106
+ assert_match(/keyword/, err.message)
107
+ end
108
+
109
+ def test_non_string_key_still_rejected
110
+ assert_raises(ArgumentError) { ContractJob.perform_later(123) }
111
+ assert_raises(ArgumentError) { ContractJob.perform_now(123) }
112
+ end
113
+
114
+ # --- public kwargs pass through --------------------------------------------
115
+
116
+ def test_options_and_user_kwargs_pass_through
117
+ key = "contract-#{SecureRandom.hex(4)}"
118
+ ContractJob.perform_later(key, foo: "bar", options: {plan: "pro"})
119
+ perform_all_jobs
120
+
121
+ wf = ChronoForge::Workflow.find_by(key: key)
122
+ assert_equal({"plan" => "pro"}, wf.options)
123
+ assert_equal "bar", wf.kwargs["foo"]
124
+ assert_equal "bar", wf.context["foo"]
125
+ end
126
+
127
+ # --- retry helpers route past the guard ------------------------------------
128
+
129
+ def test_retry_helpers_reject_reserved_keys_from_caller
130
+ assert_raises(ArgumentError) { ContractJob.retry_now("k", attempt: 1) }
131
+ assert_raises(ArgumentError) { ContractJob.retry_later("k", attempt: 1) }
132
+ end
133
+ end
134
+ ```
135
+
136
+ - [ ] **Step 2: Run tests to verify they fail**
137
+
138
+ Run: `bundle exec rake test TEST=test/enqueue_contract_test.rb`
139
+ Expected: FAIL — reserved-key/positional rejection tests fail because the current guard only checks `key.is_a?(String)` (reserved kwargs are silently swallowed; a second positional raises Ruby's generic arity error, not our message — so the `/keyword/` match fails).
140
+
141
+ - [ ] **Step 3: Add the `RESERVED_KWARGS` constant**
142
+
143
+ In `lib/chrono_forge/executor.rb`, after the `STEP_NAME_DELIMITER` definition (line 19), add:
144
+
145
+ ```ruby
146
+ # Keyword args ChronoForge threads through job args internally. Users must
147
+ # not pass these to perform_now/perform_later; the framework injects them
148
+ # via `.set(...)` continuations, whose ConfiguredJob proxy bypasses the
149
+ # class-level guard in `prepended` below.
150
+ RESERVED_KWARGS = %i[attempt retry_counts retry_workflow].freeze
151
+ ```
152
+
153
+ - [ ] **Step 4: Rewrite the enqueue/retry methods**
154
+
155
+ In the `class << base` block, replace the existing `perform_now`, `perform_later`, `retry_now`, and `retry_later` definitions (lines ~29–54) with:
156
+
157
+ ```ruby
158
+ # Public enqueue contract: exactly one positional (`key`) plus keywords.
159
+ # Reserved internal kwargs (RESERVED_KWARGS) are rejected here; the
160
+ # framework injects them only via `.set(...)` continuations, whose
161
+ # ActiveJob ConfiguredJob proxy bypasses these class-level overrides.
162
+ def perform_now(key, *extra, **kwargs)
163
+ __validate_enqueue!(key, extra, kwargs)
164
+ super(key, **kwargs)
165
+ end
166
+
167
+ def perform_later(key, *extra, **kwargs)
168
+ __validate_enqueue!(key, extra, kwargs)
169
+ super(key, **kwargs)
170
+ end
171
+
172
+ # Re-run a failed/stalled workflow. Routes through `.set(...)` so the
173
+ # reserved `retry_workflow: true` flag reaches the instance perform
174
+ # without tripping the public guard above.
175
+ def retry_now(key, **kwargs)
176
+ __validate_enqueue!(key, [], kwargs)
177
+ set.perform_now(key, retry_workflow: true, **kwargs)
178
+ end
179
+
180
+ def retry_later(key, **kwargs)
181
+ __validate_enqueue!(key, [], kwargs)
182
+ set.perform_later(key, retry_workflow: true, **kwargs)
183
+ end
184
+ ```
185
+
186
+ Leave the `retry_policy` method that follows **unchanged**.
187
+
188
+ - [ ] **Step 5: Append the private guard at the end of the `class << base` block**
189
+
190
+ Immediately before the `end` that closes `class << base` (after `retry_policy`, line ~69), add:
191
+
192
+ ```ruby
193
+
194
+ private
195
+
196
+ def __validate_enqueue!(key, extra, kwargs)
197
+ unless key.is_a?(String)
198
+ raise ArgumentError, "Workflow key must be a string as the first argument"
199
+ end
200
+ unless extra.empty?
201
+ raise ArgumentError,
202
+ "ChronoForge workflows accept only `key` positionally; pass " \
203
+ "everything else as keywords (got #{extra.size} extra positional arg(s))"
204
+ end
205
+ reserved = kwargs.keys & RESERVED_KWARGS
206
+ if reserved.any?
207
+ raise ArgumentError,
208
+ "#{reserved.join(", ")} #{reserved.one? ? "is a reserved" : "are reserved"} " \
209
+ "ChronoForge keyword(s) and cannot be passed to perform_now/perform_later"
210
+ end
211
+ end
212
+ ```
213
+
214
+ - [ ] **Step 6: Run the new tests to verify they pass**
215
+
216
+ Run: `bundle exec rake test TEST=test/enqueue_contract_test.rb`
217
+ Expected: PASS (all tests).
218
+
219
+ - [ ] **Step 7: Run the full suite (regression — confirms retry rewrite is safe)**
220
+
221
+ Run: `bundle exec rake test`
222
+ Expected: PASS — 139 prior tests + new file, 0 failures, 0 errors. This is the safety net for the `retry_now`/`retry_later` rewrite (existing retry e2e tests in `test/workflow_retry_api_test.rb` and `test/chrono_forge_test.rb` exercise the happy path).
223
+
224
+ - [ ] **Step 8: Commit**
225
+
226
+ ```bash
227
+ git add lib/chrono_forge/executor.rb test/enqueue_contract_test.rb docs/superpowers/specs/2026-06-25-reserved-kwarg-guard-design.md docs/superpowers/plans/2026-06-25-reserved-kwarg-guard.md
228
+ git commit -m "feat(executor): guard reserved kwargs and enforce keywords-only enqueue"
229
+ ```
230
+
231
+ ```json:metadata
232
+ {"files": ["lib/chrono_forge/executor.rb", "test/enqueue_contract_test.rb"], "verifyCommand": "bundle exec rake test", "acceptanceCriteria": ["perform_now/perform_later reject attempt/retry_counts/retry_workflow with a clear ArgumentError and enqueue nothing", "extra positional args rejected with a keywords-only message", "non-string key still rejected", "options and user kwargs pass through to the workflow record", "retry_now/retry_later reject caller-supplied reserved keys and existing retry e2e tests still pass", "full suite green"], "requiresUserVerification": false}
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Notes / Out of Scope
238
+
239
+ - `wait_condition` (internal kwarg in `wait_until`) is intentionally **not** added to `RESERVED_KWARGS`: it only travels via `.set(...)` and never reaches the guard.
240
+ - `ChronoForge::CleanupJob` is a plain `ActiveJob::Base` (does not prepend `Executor`); the guard does not apply to it.
241
+ - The gemspec leaves `activerecord` unpinned; a fresh `bundle install` can resolve to Rails 8.1 and conflict with `sqlite3 ~> 1.4`. Unrelated to this change — keep the worktree on main's known-good `Gemfile.lock` (Rails 7.1.3.4).
@@ -0,0 +1,12 @@
1
+ {
2
+ "planPath": "docs/superpowers/plans/2026-06-25-reserved-kwarg-guard.md",
3
+ "tasks": [
4
+ {
5
+ "id": 1,
6
+ "subject": "Task 1: Reserved-key + keywords-only enqueue guard with retry-helper rerouting",
7
+ "status": "completed",
8
+ "description": "**Goal:** Public perform_now/perform_later of Executor-prepended jobs reject attempt/retry_counts/retry_workflow and extra positionals; options and user kwargs still flow through; retry_now/retry_later keep working by routing past the guard via `.set(...)`.\n\n**Files:**\n- Modify: `lib/chrono_forge/executor.rb` (RESERVED_KWARGS constant near line 19; rewrite perform_now/perform_later/retry_now/retry_later in `class << base` ~29-54; append private `__validate_enqueue!` at end of block after retry_policy)\n- Test: `test/enqueue_contract_test.rb` (create)\n\n**Verify:** `bundle exec rake test TEST=test/enqueue_contract_test.rb` then `bundle exec rake test`\n\n```json:metadata\n{\"files\": [\"lib/chrono_forge/executor.rb\", \"test/enqueue_contract_test.rb\"], \"verifyCommand\": \"bundle exec rake test\", \"acceptanceCriteria\": [\"perform_now/perform_later reject attempt/retry_counts/retry_workflow with a clear ArgumentError and enqueue nothing\", \"extra positional args rejected with a keywords-only message\", \"non-string key still rejected\", \"options and user kwargs pass through to the workflow record\", \"retry_now/retry_later reject caller-supplied reserved keys and existing retry e2e tests still pass\", \"full suite green\"], \"requiresUserVerification\": false}\n```"
9
+ }
10
+ ],
11
+ "lastUpdated": "2026-06-25T00:00:00Z"
12
+ }