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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +305 -44
- data/docs/superpowers/plans/2026-06-25-chrono_forge-dashboard.md +1748 -0
- data/docs/superpowers/plans/2026-06-25-chrono_forge-dashboard.md.tasks.json +17 -0
- data/docs/superpowers/plans/2026-06-25-composite-retry-policies.md +930 -0
- data/docs/superpowers/plans/2026-06-25-composite-retry-policies.md.tasks.json +54 -0
- data/docs/superpowers/plans/2026-06-25-reserved-kwarg-guard.md +241 -0
- data/docs/superpowers/plans/2026-06-25-reserved-kwarg-guard.md.tasks.json +12 -0
- data/docs/superpowers/plans/2026-06-26-branches-spawn-merge.md +1378 -0
- data/docs/superpowers/plans/2026-06-26-branches-spawn-merge.md.tasks.json +67 -0
- data/docs/superpowers/plans/2026-06-26-deferral-continuation-race-and-catchup.md +709 -0
- data/docs/superpowers/plans/2026-06-26-deferral-continuation-race-and-catchup.md.tasks.json +19 -0
- data/docs/superpowers/specs/2026-06-03-unified-retry-policy-design.md +226 -0
- data/docs/superpowers/specs/2026-06-25-chrono_forge-dashboard-design.md +190 -0
- data/docs/superpowers/specs/2026-06-25-composite-retry-policies-design.md +228 -0
- data/docs/superpowers/specs/2026-06-25-reserved-kwarg-guard-design.md +169 -0
- data/docs/superpowers/specs/2026-06-25-spawn-merge-branches-design.md +468 -0
- data/docs/superpowers/specs/2026-06-26-dashboard-branch-view-design.md +142 -0
- data/docs/superpowers/specs/2026-06-26-deferral-continuation-race-and-catchup-design.md +265 -0
- data/lib/chrono_forge/branch_merge_job.rb +138 -0
- data/lib/chrono_forge/branch_probe.rb +26 -0
- data/lib/chrono_forge/cleanup.rb +6 -0
- data/lib/chrono_forge/execution_log.rb +6 -0
- data/lib/chrono_forge/executor/composite_retry_policy.rb +47 -0
- data/lib/chrono_forge/executor/methods/branch.rb +185 -0
- data/lib/chrono_forge/executor/methods/durably_execute.rb +21 -19
- data/lib/chrono_forge/executor/methods/durably_repeat.rb +118 -25
- data/lib/chrono_forge/executor/methods/merge_branches.rb +83 -0
- data/lib/chrono_forge/executor/methods/wait.rb +2 -4
- data/lib/chrono_forge/executor/methods/wait_until.rb +25 -25
- data/lib/chrono_forge/executor/methods/workflow_states.rb +16 -0
- data/lib/chrono_forge/executor/methods.rb +2 -0
- data/lib/chrono_forge/executor/retry_policy.rb +111 -0
- data/lib/chrono_forge/executor.rb +216 -28
- data/lib/chrono_forge/version.rb +1 -1
- data/lib/chrono_forge/workflow.rb +10 -1
- data/lib/generators/chrono_forge/migration_actions.rb +1 -0
- data/lib/generators/chrono_forge/templates/add_chrono_forge_parent_execution_log.rb +38 -0
- metadata +42 -5
- 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
|
+
}
|