@nbardy/oompa 0.1.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.
@@ -0,0 +1,141 @@
1
+ (ns agentnet.schema
2
+ "Data schemas for AgentNet using simple predicates.
3
+
4
+ Design Philosophy (Hickey-style):
5
+ - Data is the interface
6
+ - Schemas are documentation
7
+ - Validate at boundaries, trust inside
8
+ - Prefer maps over positional args
9
+
10
+ Note: Using simple predicates instead of Malli to avoid
11
+ external dependencies (Malli needs Java for deps.clj)."
12
+ (:require [clojure.string :as str]))
13
+
14
+ ;; =============================================================================
15
+ ;; Primitive Validators
16
+ ;; =============================================================================
17
+
18
+ (defn non-blank-string? [x]
19
+ (and (string? x) (not (str/blank? x))))
20
+
21
+ (defn pos-int? [x]
22
+ (and (int? x) (pos? x)))
23
+
24
+ (defn nat-int? [x]
25
+ (and (int? x) (>= x 0)))
26
+
27
+ (defn timestamp? [x]
28
+ (nat-int? x))
29
+
30
+ (defn file-path? [x]
31
+ (non-blank-string? x))
32
+
33
+ (defn git-branch? [x]
34
+ (non-blank-string? x))
35
+
36
+ (defn git-sha? [x]
37
+ (and (string? x) (re-matches #"^[a-f0-9]{7,40}$" x)))
38
+
39
+ ;; =============================================================================
40
+ ;; Enum Validators
41
+ ;; =============================================================================
42
+
43
+ (def agent-types #{:codex :claude})
44
+ (def agent-roles #{:proposer :reviewer :cto})
45
+ (def task-statuses #{:pending :in-progress :review :approved :merged :failed :blocked})
46
+ (def worktree-statuses #{:available :busy :dirty :stale})
47
+ (def review-verdicts #{:approved :needs-changes :rejected})
48
+ (def merge-strategies #{:fast-forward :no-ff :squash :rebase})
49
+ (def conflict-resolutions #{:ours :theirs :manual :abort})
50
+
51
+ (defn agent-type? [x] (contains? agent-types x))
52
+ (defn agent-role? [x] (contains? agent-roles x))
53
+ (defn task-status? [x] (contains? task-statuses x))
54
+ (defn worktree-status? [x] (contains? worktree-statuses x))
55
+ (defn review-verdict? [x] (contains? review-verdicts x))
56
+ (defn merge-strategy? [x] (contains? merge-strategies x))
57
+ (defn conflict-resolution? [x] (contains? conflict-resolutions x))
58
+
59
+ ;; =============================================================================
60
+ ;; Map Validators
61
+ ;; =============================================================================
62
+
63
+ (defn has-keys? [m required-keys]
64
+ (every? #(contains? m %) required-keys))
65
+
66
+ (defn valid-task? [x]
67
+ (and (map? x)
68
+ (has-keys? x [:id :summary])
69
+ (non-blank-string? (:id x))
70
+ (non-blank-string? (:summary x))))
71
+
72
+ (defn valid-worktree? [x]
73
+ (and (map? x)
74
+ (has-keys? x [:id :path :branch :status])
75
+ (non-blank-string? (:id x))
76
+ (file-path? (:path x))
77
+ (git-branch? (:branch x))
78
+ (worktree-status? (:status x))))
79
+
80
+ (defn valid-agent-config? [x]
81
+ (and (map? x)
82
+ (has-keys? x [:type])
83
+ (agent-type? (:type x))))
84
+
85
+ (defn valid-review-feedback? [x]
86
+ (and (map? x)
87
+ (has-keys? x [:verdict])
88
+ (review-verdict? (:verdict x))))
89
+
90
+ (defn valid-merge-result? [x]
91
+ (and (map? x)
92
+ (has-keys? x [:status :source-branch :target-branch])
93
+ (contains? #{:merged :conflict :failed :skipped} (:status x))))
94
+
95
+ (defn valid-orchestrator-config? [x]
96
+ (and (map? x)
97
+ (has-keys? x [:worker-count :harness])
98
+ (pos-int? (:worker-count x))
99
+ (agent-type? (:harness x))))
100
+
101
+ ;; =============================================================================
102
+ ;; Validation Helpers
103
+ ;; =============================================================================
104
+
105
+ (defn validate
106
+ "Validate data against a predicate, return [valid? errors]"
107
+ [pred data context]
108
+ (if (pred data)
109
+ [true nil]
110
+ [false {:context context :data data :error "Validation failed"}]))
111
+
112
+ (defn assert-valid
113
+ "Throw if data doesn't match predicate"
114
+ [pred data context]
115
+ (when-not (pred data)
116
+ (throw (ex-info (str "Invalid " context)
117
+ {:context context
118
+ :data data
119
+ :type (type data)}))))
120
+
121
+ ;; =============================================================================
122
+ ;; Schema Reference (for documentation)
123
+ ;; =============================================================================
124
+
125
+ ;; TaskId <- non-blank-string?
126
+ ;; AgentType <- agent-type? (:codex, :claude)
127
+ ;; AgentRole <- agent-role? (:proposer, :reviewer, :cto)
128
+ ;; TaskStatus <- task-status?
129
+ ;; WorktreeStatus <- worktree-status?
130
+ ;; ReviewVerdict <- review-verdict?
131
+ ;; MergeStrategy <- merge-strategy?
132
+ ;; ConflictResolution <- conflict-resolution?
133
+ ;; GitBranch <- git-branch?
134
+ ;; GitSha <- git-sha?
135
+ ;;
136
+ ;; Task <- {:id string, :summary string, :targets [string], ...}
137
+ ;; Worktree <- {:id string, :path string, :branch string, :status keyword}
138
+ ;; AgentConfig <- {:type :codex|:claude, :model string, :sandbox keyword}
139
+ ;; ReviewFeedback <- {:verdict :approved|:needs-changes|:rejected, :comments [string]}
140
+ ;; MergeResult <- {:status :merged|:conflict|:failed, :source-branch string, ...}
141
+ ;; OrchestratorConfig <- {:worker-count int, :harness :codex|:claude, :model string, :dry-run bool}
@@ -0,0 +1,198 @@
1
+ (ns agentnet.tasks
2
+ "Folder-based task management.
3
+
4
+ Tasks live in tasks/{pending,current,complete}/*.edn
5
+
6
+ Workers:
7
+ - Claim tasks: mv pending/foo.edn → current/foo.edn
8
+ - Complete tasks: mv current/foo.edn → complete/foo.edn
9
+ - Create tasks: write new .edn to pending/
10
+
11
+ Task format:
12
+ {:id \"task-001\"
13
+ :summary \"Add authentication\"
14
+ :description \"Implement JWT auth...\"
15
+ :files [\"src/auth.py\"]
16
+ :acceptance [\"Login works\" \"Tests pass\"]}"
17
+ (:require [clojure.edn :as edn]
18
+ [clojure.java.io :as io]
19
+ [clojure.string :as str]))
20
+
21
+ ;; =============================================================================
22
+ ;; Paths
23
+ ;; =============================================================================
24
+
25
+ (def ^:const TASKS_ROOT "tasks")
26
+ (def ^:const PENDING_DIR (str TASKS_ROOT "/pending"))
27
+ (def ^:const CURRENT_DIR (str TASKS_ROOT "/current"))
28
+ (def ^:const COMPLETE_DIR (str TASKS_ROOT "/complete"))
29
+
30
+ (defn ensure-dirs!
31
+ "Create task directories if they don't exist"
32
+ []
33
+ (doseq [dir [PENDING_DIR CURRENT_DIR COMPLETE_DIR]]
34
+ (.mkdirs (io/file dir))))
35
+
36
+ ;; =============================================================================
37
+ ;; Task I/O
38
+ ;; =============================================================================
39
+
40
+ (defn- read-task-file
41
+ "Read a single task .edn file"
42
+ [^java.io.File f]
43
+ (when (.exists f)
44
+ (try
45
+ (with-open [r (java.io.PushbackReader. (io/reader f))]
46
+ (let [task (edn/read {:eof nil} r)]
47
+ (assoc task :_file (.getPath f))))
48
+ (catch Exception e
49
+ (println (format "[warn] Failed to read task %s: %s" (.getName f) (.getMessage e)))
50
+ nil))))
51
+
52
+ (defn- list-task-files
53
+ "List all .edn files in a directory"
54
+ [dir]
55
+ (let [d (io/file dir)]
56
+ (when (.exists d)
57
+ (->> (.listFiles d)
58
+ (filter #(str/ends-with? (.getName %) ".edn"))
59
+ (sort-by #(.getName %))))))
60
+
61
+ (defn list-pending
62
+ "List all pending tasks"
63
+ []
64
+ (->> (list-task-files PENDING_DIR)
65
+ (keep read-task-file)
66
+ vec))
67
+
68
+ (defn list-current
69
+ "List all in-progress tasks"
70
+ []
71
+ (->> (list-task-files CURRENT_DIR)
72
+ (keep read-task-file)
73
+ vec))
74
+
75
+ (defn list-complete
76
+ "List all completed tasks"
77
+ []
78
+ (->> (list-task-files COMPLETE_DIR)
79
+ (keep read-task-file)
80
+ vec))
81
+
82
+ (defn list-all
83
+ "List all tasks with their status"
84
+ []
85
+ (concat
86
+ (map #(assoc % :status :pending) (list-pending))
87
+ (map #(assoc % :status :current) (list-current))
88
+ (map #(assoc % :status :complete) (list-complete))))
89
+
90
+ ;; =============================================================================
91
+ ;; Task Operations
92
+ ;; =============================================================================
93
+
94
+ (defn- move-task!
95
+ "Move a task file from one dir to another"
96
+ [task from-dir to-dir]
97
+ (let [filename (-> (:_file task) io/file .getName)
98
+ from-path (io/file from-dir filename)
99
+ to-path (io/file to-dir filename)]
100
+ (when (.exists from-path)
101
+ (.renameTo from-path to-path)
102
+ (assoc task :_file (.getPath to-path)))))
103
+
104
+ (defn claim-task!
105
+ "Claim a pending task (mv pending → current). Returns task or nil."
106
+ [task]
107
+ (move-task! task PENDING_DIR CURRENT_DIR))
108
+
109
+ (defn complete-task!
110
+ "Mark a task complete (mv current → complete). Returns task or nil."
111
+ [task]
112
+ (move-task! task CURRENT_DIR COMPLETE_DIR))
113
+
114
+ (defn unclaim-task!
115
+ "Return a task to pending (mv current → pending). Returns task or nil."
116
+ [task]
117
+ (move-task! task CURRENT_DIR PENDING_DIR))
118
+
119
+ (defn claim-next!
120
+ "Claim the next available pending task. Returns task or nil."
121
+ []
122
+ (when-let [task (first (list-pending))]
123
+ (claim-task! task)))
124
+
125
+ ;; =============================================================================
126
+ ;; Task Creation
127
+ ;; =============================================================================
128
+
129
+ (defn- generate-task-id
130
+ "Generate a unique task ID"
131
+ []
132
+ (format "task-%d" (System/currentTimeMillis)))
133
+
134
+ (defn- task->filename
135
+ "Convert task to filename"
136
+ [task]
137
+ (let [id (or (:id task) (generate-task-id))
138
+ safe-id (str/replace id #"[^a-zA-Z0-9-_]" "-")]
139
+ (str safe-id ".edn")))
140
+
141
+ (defn create-task!
142
+ "Create a new task in pending/. Returns the task with :_file set."
143
+ [{:keys [id summary] :as task}]
144
+ (ensure-dirs!)
145
+ (let [task-id (or id (generate-task-id))
146
+ task (assoc task :id task-id)
147
+ filename (task->filename task)
148
+ path (io/file PENDING_DIR filename)]
149
+ (spit path (pr-str task))
150
+ (assoc task :_file (.getPath path))))
151
+
152
+ (defn create-tasks!
153
+ "Create multiple tasks. Returns list of created tasks."
154
+ [tasks]
155
+ (mapv create-task! tasks))
156
+
157
+ ;; =============================================================================
158
+ ;; Status Checks
159
+ ;; =============================================================================
160
+
161
+ (defn pending-count [] (count (list-task-files PENDING_DIR)))
162
+ (defn current-count [] (count (list-task-files CURRENT_DIR)))
163
+ (defn complete-count [] (count (list-task-files COMPLETE_DIR)))
164
+
165
+ (defn all-complete?
166
+ "True if no pending or current tasks"
167
+ []
168
+ (and (zero? (pending-count))
169
+ (zero? (current-count))))
170
+
171
+ (defn has-pending?
172
+ "True if there are pending tasks"
173
+ []
174
+ (pos? (pending-count)))
175
+
176
+ (defn status-summary
177
+ "Return status counts"
178
+ []
179
+ {:pending (pending-count)
180
+ :current (current-count)
181
+ :complete (complete-count)})
182
+
183
+ ;; =============================================================================
184
+ ;; Migration from old format
185
+ ;; =============================================================================
186
+
187
+ (defn migrate-from-tasks-edn!
188
+ "Migrate from config/tasks.edn to folder structure"
189
+ []
190
+ (let [old-file (io/file "config/tasks.edn")]
191
+ (when (.exists old-file)
192
+ (println "Migrating from config/tasks.edn...")
193
+ (ensure-dirs!)
194
+ (let [tasks (with-open [r (java.io.PushbackReader. (io/reader old-file))]
195
+ (edn/read {:eof nil} r))]
196
+ (doseq [task tasks]
197
+ (create-task! task))
198
+ (println (format "Migrated %d tasks to tasks/pending/" (count tasks)))))))