@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.
- package/README.md +197 -0
- package/agentnet/src/agentnet/agent.clj +293 -0
- package/agentnet/src/agentnet/cli.clj +436 -0
- package/agentnet/src/agentnet/core.clj +211 -0
- package/agentnet/src/agentnet/merge.clj +355 -0
- package/agentnet/src/agentnet/notes.clj +123 -0
- package/agentnet/src/agentnet/orchestrator.clj +367 -0
- package/agentnet/src/agentnet/review.clj +263 -0
- package/agentnet/src/agentnet/schema.clj +141 -0
- package/agentnet/src/agentnet/tasks.clj +198 -0
- package/agentnet/src/agentnet/worker.clj +427 -0
- package/agentnet/src/agentnet/worktree.clj +342 -0
- package/bin/oompa.js +37 -0
- package/config/prompts/cto.md +45 -0
- package/config/prompts/engineer.md +44 -0
- package/config/prompts/executor.md +32 -0
- package/config/prompts/planner.md +35 -0
- package/config/prompts/reviewer.md +49 -0
- package/config/prompts/worker.md +31 -0
- package/oompa.example.json +18 -0
- package/package.json +45 -0
- package/swarm.bb +18 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
(ns agentnet.merge
|
|
2
|
+
"Git merge operations for integrating approved changes.
|
|
3
|
+
|
|
4
|
+
After a task passes review, its worktree branch needs to be merged
|
|
5
|
+
into main. This module handles:
|
|
6
|
+
- Fast-forward merges (ideal case)
|
|
7
|
+
- Merge commits (divergent histories)
|
|
8
|
+
- Conflict detection and resolution
|
|
9
|
+
- Rollback on failure
|
|
10
|
+
|
|
11
|
+
Design:
|
|
12
|
+
- Merges happen in main repo, not worktrees
|
|
13
|
+
- Conflicts can be auto-resolved or bounced back
|
|
14
|
+
- Each merge is atomic (succeed or rollback)
|
|
15
|
+
- Merge results are logged for audit"
|
|
16
|
+
(:require [agentnet.schema :as schema]
|
|
17
|
+
[agentnet.worktree :as worktree]
|
|
18
|
+
[babashka.process :as process]
|
|
19
|
+
[clojure.java.io :as io]
|
|
20
|
+
[clojure.string :as str]))
|
|
21
|
+
|
|
22
|
+
;; =============================================================================
|
|
23
|
+
;; Function Specs
|
|
24
|
+
;; =============================================================================
|
|
25
|
+
|
|
26
|
+
;; merge-branch! : GitBranch, MergeStrategy, Options -> MergeResult
|
|
27
|
+
;; Merge a branch into current branch (main)
|
|
28
|
+
|
|
29
|
+
;; detect-conflicts : GitBranch -> [FilePath]
|
|
30
|
+
;; Check what files would conflict if merged
|
|
31
|
+
|
|
32
|
+
;; resolve-conflicts! : ConflictResolution -> Boolean
|
|
33
|
+
;; Attempt to resolve merge conflicts
|
|
34
|
+
|
|
35
|
+
;; abort-merge! : -> nil
|
|
36
|
+
;; Abort an in-progress merge
|
|
37
|
+
|
|
38
|
+
;; =============================================================================
|
|
39
|
+
;; Git Helpers
|
|
40
|
+
;; =============================================================================
|
|
41
|
+
|
|
42
|
+
(defn- git
|
|
43
|
+
"Run git command, return {:exit :out :err}"
|
|
44
|
+
[& args]
|
|
45
|
+
(let [cmd (into ["git"] args)
|
|
46
|
+
{:keys [exit out err]} (process/sh cmd {:out :string :err :string})]
|
|
47
|
+
{:exit exit
|
|
48
|
+
:out (str/trim (or out ""))
|
|
49
|
+
:err (str/trim (or err ""))}))
|
|
50
|
+
|
|
51
|
+
(defn- git!
|
|
52
|
+
"Run git command, throw on failure"
|
|
53
|
+
[& args]
|
|
54
|
+
(let [{:keys [exit out err]} (apply git args)]
|
|
55
|
+
(if (zero? exit)
|
|
56
|
+
out
|
|
57
|
+
(throw (ex-info (str "git failed: " err)
|
|
58
|
+
{:command args :exit exit :err err})))))
|
|
59
|
+
|
|
60
|
+
(defn- current-branch []
|
|
61
|
+
(git! "rev-parse" "--abbrev-ref" "HEAD"))
|
|
62
|
+
|
|
63
|
+
(defn- current-sha []
|
|
64
|
+
(git! "rev-parse" "HEAD"))
|
|
65
|
+
|
|
66
|
+
(defn- branch-sha [branch]
|
|
67
|
+
(:out (git "rev-parse" branch)))
|
|
68
|
+
|
|
69
|
+
(defn- merge-in-progress? []
|
|
70
|
+
(.exists (io/file ".git/MERGE_HEAD")))
|
|
71
|
+
|
|
72
|
+
(defn- stash-changes! []
|
|
73
|
+
(let [{:keys [exit]} (git "stash" "push" "-m" "agentnet-auto-stash")]
|
|
74
|
+
(zero? exit)))
|
|
75
|
+
|
|
76
|
+
(defn- pop-stash! []
|
|
77
|
+
(git "stash" "pop"))
|
|
78
|
+
|
|
79
|
+
;; =============================================================================
|
|
80
|
+
;; Conflict Detection
|
|
81
|
+
;; =============================================================================
|
|
82
|
+
|
|
83
|
+
(defn detect-conflicts
|
|
84
|
+
"Check what files would conflict if branch were merged.
|
|
85
|
+
Returns vector of file paths, empty if no conflicts."
|
|
86
|
+
[source-branch]
|
|
87
|
+
(let [;; Dry-run merge to detect conflicts
|
|
88
|
+
{:keys [exit out err]} (git "merge" "--no-commit" "--no-ff" source-branch)]
|
|
89
|
+
(if (zero? exit)
|
|
90
|
+
;; No conflicts, abort the merge we started
|
|
91
|
+
(do (git "merge" "--abort")
|
|
92
|
+
[])
|
|
93
|
+
;; Parse conflict files from output
|
|
94
|
+
(do
|
|
95
|
+
(git "merge" "--abort")
|
|
96
|
+
(->> (str/split-lines (str out "\n" err))
|
|
97
|
+
(filter #(re-find #"CONFLICT|Merge conflict" %))
|
|
98
|
+
(map #(second (re-find #"(?:in|Merge conflict in)\s+(.+)" %)))
|
|
99
|
+
(remove nil?)
|
|
100
|
+
vec)))))
|
|
101
|
+
|
|
102
|
+
(defn can-fast-forward?
|
|
103
|
+
"Check if source branch can be fast-forwarded into current branch"
|
|
104
|
+
[source-branch]
|
|
105
|
+
(let [current (current-sha)
|
|
106
|
+
merge-base (:out (git "merge-base" (current-branch) source-branch))]
|
|
107
|
+
(= current merge-base)))
|
|
108
|
+
|
|
109
|
+
;; =============================================================================
|
|
110
|
+
;; Merge Strategies
|
|
111
|
+
;; =============================================================================
|
|
112
|
+
|
|
113
|
+
(defmulti execute-merge
|
|
114
|
+
"Execute merge with specified strategy"
|
|
115
|
+
(fn [source-branch strategy _opts] strategy))
|
|
116
|
+
|
|
117
|
+
(defmethod execute-merge :fast-forward
|
|
118
|
+
[source-branch _ _opts]
|
|
119
|
+
(let [{:keys [exit err]} (git "merge" "--ff-only" source-branch)]
|
|
120
|
+
(if (zero? exit)
|
|
121
|
+
{:status :merged
|
|
122
|
+
:source-branch source-branch
|
|
123
|
+
:target-branch (current-branch)
|
|
124
|
+
:commit-sha (current-sha)}
|
|
125
|
+
{:status :failed
|
|
126
|
+
:source-branch source-branch
|
|
127
|
+
:target-branch (current-branch)
|
|
128
|
+
:error (str "Fast-forward not possible: " err)})))
|
|
129
|
+
|
|
130
|
+
(defmethod execute-merge :no-ff
|
|
131
|
+
[source-branch _ {:keys [message]}]
|
|
132
|
+
(let [msg (or message (str "Merge branch '" source-branch "'"))
|
|
133
|
+
{:keys [exit err]} (git "merge" "--no-ff" "-m" msg source-branch)]
|
|
134
|
+
(if (zero? exit)
|
|
135
|
+
{:status :merged
|
|
136
|
+
:source-branch source-branch
|
|
137
|
+
:target-branch (current-branch)
|
|
138
|
+
:commit-sha (current-sha)}
|
|
139
|
+
{:status :conflict
|
|
140
|
+
:source-branch source-branch
|
|
141
|
+
:target-branch (current-branch)
|
|
142
|
+
:conflicts (detect-conflicts source-branch)
|
|
143
|
+
:error err})))
|
|
144
|
+
|
|
145
|
+
(defmethod execute-merge :squash
|
|
146
|
+
[source-branch _ {:keys [message]}]
|
|
147
|
+
(let [{:keys [exit err]} (git "merge" "--squash" source-branch)]
|
|
148
|
+
(if (zero? exit)
|
|
149
|
+
;; Squash stages but doesn't commit
|
|
150
|
+
(let [msg (or message (str "Squash merge: " source-branch))
|
|
151
|
+
{:keys [exit]} (git "commit" "-m" msg)]
|
|
152
|
+
(if (zero? exit)
|
|
153
|
+
{:status :merged
|
|
154
|
+
:source-branch source-branch
|
|
155
|
+
:target-branch (current-branch)
|
|
156
|
+
:commit-sha (current-sha)}
|
|
157
|
+
{:status :failed
|
|
158
|
+
:source-branch source-branch
|
|
159
|
+
:target-branch (current-branch)
|
|
160
|
+
:error "Squash commit failed"}))
|
|
161
|
+
{:status :conflict
|
|
162
|
+
:source-branch source-branch
|
|
163
|
+
:target-branch (current-branch)
|
|
164
|
+
:error err})))
|
|
165
|
+
|
|
166
|
+
(defmethod execute-merge :rebase
|
|
167
|
+
[source-branch _ _opts]
|
|
168
|
+
;; Rebase source onto current, then fast-forward
|
|
169
|
+
(let [current (current-branch)
|
|
170
|
+
{:keys [exit err]} (git "rebase" current source-branch)]
|
|
171
|
+
(if (zero? exit)
|
|
172
|
+
;; Switch back and fast-forward
|
|
173
|
+
(do
|
|
174
|
+
(git! "checkout" current)
|
|
175
|
+
(execute-merge source-branch :fast-forward {}))
|
|
176
|
+
(do
|
|
177
|
+
(git "rebase" "--abort")
|
|
178
|
+
{:status :conflict
|
|
179
|
+
:source-branch source-branch
|
|
180
|
+
:target-branch current
|
|
181
|
+
:error (str "Rebase failed: " err)}))))
|
|
182
|
+
|
|
183
|
+
(defmethod execute-merge :default
|
|
184
|
+
[source-branch strategy _opts]
|
|
185
|
+
(throw (ex-info (str "Unknown merge strategy: " strategy)
|
|
186
|
+
{:strategy strategy :source-branch source-branch})))
|
|
187
|
+
|
|
188
|
+
;; =============================================================================
|
|
189
|
+
;; Conflict Resolution
|
|
190
|
+
;; =============================================================================
|
|
191
|
+
|
|
192
|
+
(defn resolve-conflicts!
|
|
193
|
+
"Attempt to resolve merge conflicts.
|
|
194
|
+
|
|
195
|
+
Strategies:
|
|
196
|
+
:ours - Accept current branch version
|
|
197
|
+
:theirs - Accept incoming branch version
|
|
198
|
+
:manual - Leave for human intervention
|
|
199
|
+
:abort - Abort the merge entirely"
|
|
200
|
+
[resolution]
|
|
201
|
+
(when-not (merge-in-progress?)
|
|
202
|
+
(throw (ex-info "No merge in progress" {})))
|
|
203
|
+
|
|
204
|
+
(case resolution
|
|
205
|
+
:ours
|
|
206
|
+
(do
|
|
207
|
+
(git! "checkout" "--ours" ".")
|
|
208
|
+
(git! "add" "-A")
|
|
209
|
+
(let [{:keys [exit]} (git "commit" "--no-edit")]
|
|
210
|
+
(zero? exit)))
|
|
211
|
+
|
|
212
|
+
:theirs
|
|
213
|
+
(do
|
|
214
|
+
(git! "checkout" "--theirs" ".")
|
|
215
|
+
(git! "add" "-A")
|
|
216
|
+
(let [{:keys [exit]} (git "commit" "--no-edit")]
|
|
217
|
+
(zero? exit)))
|
|
218
|
+
|
|
219
|
+
:abort
|
|
220
|
+
(do
|
|
221
|
+
(git! "merge" "--abort")
|
|
222
|
+
false)
|
|
223
|
+
|
|
224
|
+
:manual
|
|
225
|
+
false ; Leave conflicts for human
|
|
226
|
+
|
|
227
|
+
(throw (ex-info (str "Unknown resolution: " resolution)
|
|
228
|
+
{:resolution resolution}))))
|
|
229
|
+
|
|
230
|
+
(defn abort-merge!
|
|
231
|
+
"Abort an in-progress merge"
|
|
232
|
+
[]
|
|
233
|
+
(when (merge-in-progress?)
|
|
234
|
+
(git! "merge" "--abort")))
|
|
235
|
+
|
|
236
|
+
;; =============================================================================
|
|
237
|
+
;; Main API
|
|
238
|
+
;; =============================================================================
|
|
239
|
+
|
|
240
|
+
(defn merge-branch!
|
|
241
|
+
"Merge a branch into current branch.
|
|
242
|
+
|
|
243
|
+
Arguments:
|
|
244
|
+
source-branch - Branch to merge from
|
|
245
|
+
strategy - :fast-forward, :no-ff, :squash, :rebase
|
|
246
|
+
opts - {:message str, :conflict-resolution keyword, :dry-run bool}
|
|
247
|
+
|
|
248
|
+
Returns MergeResult"
|
|
249
|
+
[source-branch strategy opts]
|
|
250
|
+
(schema/assert-valid schema/git-branch? source-branch "source-branch")
|
|
251
|
+
(schema/assert-valid schema/merge-strategy? strategy "strategy")
|
|
252
|
+
|
|
253
|
+
(if (:dry-run opts)
|
|
254
|
+
(let [conflicts (detect-conflicts source-branch)]
|
|
255
|
+
{:status (if (empty? conflicts) :would-merge :would-conflict)
|
|
256
|
+
:source-branch source-branch
|
|
257
|
+
:target-branch (current-branch)
|
|
258
|
+
:conflicts conflicts})
|
|
259
|
+
;; else: actually merge
|
|
260
|
+
(let [result (execute-merge source-branch strategy opts)]
|
|
261
|
+
;; Handle conflicts if resolution strategy specified
|
|
262
|
+
(if (and (= :conflict (:status result))
|
|
263
|
+
(:conflict-resolution opts))
|
|
264
|
+
(if (resolve-conflicts! (:conflict-resolution opts))
|
|
265
|
+
(assoc result :status :merged :commit-sha (current-sha))
|
|
266
|
+
result)
|
|
267
|
+
result))))
|
|
268
|
+
|
|
269
|
+
(defn merge-worktree!
|
|
270
|
+
"Merge a worktree's branch into main.
|
|
271
|
+
|
|
272
|
+
Arguments:
|
|
273
|
+
worktree - Worktree map with :branch
|
|
274
|
+
opts - Merge options
|
|
275
|
+
|
|
276
|
+
Returns MergeResult"
|
|
277
|
+
[worktree opts]
|
|
278
|
+
(let [{:keys [branch]} worktree
|
|
279
|
+
strategy (or (:strategy opts) :no-ff)
|
|
280
|
+
message (or (:message opts)
|
|
281
|
+
(format "Merge %s: %s"
|
|
282
|
+
(:id worktree)
|
|
283
|
+
(or (:current-task worktree) "task")))]
|
|
284
|
+
(merge-branch! branch strategy (assoc opts :message message))))
|
|
285
|
+
|
|
286
|
+
;; =============================================================================
|
|
287
|
+
;; Safe Merge with Rollback
|
|
288
|
+
;; =============================================================================
|
|
289
|
+
|
|
290
|
+
(defn safe-merge!
|
|
291
|
+
"Merge with automatic rollback on failure.
|
|
292
|
+
|
|
293
|
+
Creates a backup ref before merge, restores on failure."
|
|
294
|
+
[source-branch strategy opts]
|
|
295
|
+
(let [backup-ref (str "refs/agentnet/backup/" (System/currentTimeMillis))
|
|
296
|
+
original-sha (current-sha)]
|
|
297
|
+
;; Create backup
|
|
298
|
+
(git! "update-ref" backup-ref original-sha)
|
|
299
|
+
|
|
300
|
+
(try
|
|
301
|
+
(let [result (merge-branch! source-branch strategy opts)]
|
|
302
|
+
(if (#{:merged} (:status result))
|
|
303
|
+
(do
|
|
304
|
+
;; Clean up backup on success
|
|
305
|
+
(git "update-ref" "-d" backup-ref)
|
|
306
|
+
result)
|
|
307
|
+
(do
|
|
308
|
+
;; Rollback on failure
|
|
309
|
+
(git! "reset" "--hard" original-sha)
|
|
310
|
+
(git "update-ref" "-d" backup-ref)
|
|
311
|
+
result)))
|
|
312
|
+
(catch Exception e
|
|
313
|
+
;; Rollback on exception
|
|
314
|
+
(git! "reset" "--hard" original-sha)
|
|
315
|
+
(git "update-ref" "-d" backup-ref)
|
|
316
|
+
(throw e)))))
|
|
317
|
+
|
|
318
|
+
;; =============================================================================
|
|
319
|
+
;; Batch Merge
|
|
320
|
+
;; =============================================================================
|
|
321
|
+
|
|
322
|
+
(defn merge-all!
|
|
323
|
+
"Merge multiple worktrees in sequence.
|
|
324
|
+
|
|
325
|
+
Stops on first conflict unless :continue-on-conflict is true.
|
|
326
|
+
Returns vector of MergeResults."
|
|
327
|
+
[worktrees opts]
|
|
328
|
+
(loop [remaining worktrees
|
|
329
|
+
results []]
|
|
330
|
+
(if-let [wt (first remaining)]
|
|
331
|
+
(let [result (safe-merge! (:branch wt) (or (:strategy opts) :no-ff) opts)]
|
|
332
|
+
(if (and (= :conflict (:status result))
|
|
333
|
+
(not (:continue-on-conflict opts)))
|
|
334
|
+
(conj results result) ; Stop on conflict
|
|
335
|
+
(recur (rest remaining) (conj results result))))
|
|
336
|
+
results)))
|
|
337
|
+
|
|
338
|
+
;; =============================================================================
|
|
339
|
+
;; Cleanup
|
|
340
|
+
;; =============================================================================
|
|
341
|
+
|
|
342
|
+
(defn delete-merged-branch!
|
|
343
|
+
"Delete a branch that has been merged"
|
|
344
|
+
[branch]
|
|
345
|
+
(let [{:keys [exit]} (git "branch" "-d" branch)]
|
|
346
|
+
(zero? exit)))
|
|
347
|
+
|
|
348
|
+
(defn cleanup-after-merge!
|
|
349
|
+
"Clean up worktree and branch after successful merge"
|
|
350
|
+
[worktree pool]
|
|
351
|
+
(let [{:keys [id branch]} worktree]
|
|
352
|
+
;; Release worktree (resets to main)
|
|
353
|
+
(worktree/release! pool id {:reset? true})
|
|
354
|
+
;; Delete the merged branch
|
|
355
|
+
(delete-merged-branch! branch)))
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
(ns agentnet.notes
|
|
2
|
+
"Filesystem-based queue helpers for the agent_notes workflow."
|
|
3
|
+
(:require [clojure.java.io :as io]
|
|
4
|
+
[clojure.string :as str]))
|
|
5
|
+
|
|
6
|
+
(def NOTES
|
|
7
|
+
"Root directory that stores all note queues."
|
|
8
|
+
(io/file "agent_notes"))
|
|
9
|
+
|
|
10
|
+
(defn- fm-frontmatter
|
|
11
|
+
"Parse lightweight YAML-ish front-matter between --- lines.
|
|
12
|
+
Returns a map of keyword -> string, or {} when not present or malformed."
|
|
13
|
+
[^String s]
|
|
14
|
+
(try
|
|
15
|
+
(let [[_ block] (re-find #"(?s)^---\n(.*?)\n---\n" s)]
|
|
16
|
+
(if-not block
|
|
17
|
+
{}
|
|
18
|
+
(->> (str/split-lines block)
|
|
19
|
+
(keep #(when-let [[_ k v] (re-matches #"^\s*([A-Za-z0-9_]+)\s*:\s*(.+)\s*$" %)]
|
|
20
|
+
[(keyword k) (str/trim v)]))
|
|
21
|
+
(into {}))))
|
|
22
|
+
(catch Exception _
|
|
23
|
+
{})))
|
|
24
|
+
|
|
25
|
+
(defn- read-note
|
|
26
|
+
"Return a metadata map for note file `f` with parsed front-matter."
|
|
27
|
+
[^java.io.File f]
|
|
28
|
+
(let [s (slurp f)
|
|
29
|
+
fm (fm-frontmatter s)]
|
|
30
|
+
{:file (.getAbsolutePath f)
|
|
31
|
+
:name (.getName f)
|
|
32
|
+
:dir (.getName (.getParentFile f))
|
|
33
|
+
:mtime (.lastModified f)
|
|
34
|
+
:fm fm
|
|
35
|
+
:status (keyword (or (:status fm) "green"))
|
|
36
|
+
:id (or (:id fm)
|
|
37
|
+
(second (re-find #"__([^_]+)__" (.getName f))))
|
|
38
|
+
:targets (let [t (:targets fm)]
|
|
39
|
+
(when (and t (string? t) (not (str/blank? t)))
|
|
40
|
+
(let [parts (-> t
|
|
41
|
+
(str/replace #"[\[\]\"]" "")
|
|
42
|
+
(str/split #",\s*"))]
|
|
43
|
+
(->> parts
|
|
44
|
+
(remove str/blank?)
|
|
45
|
+
vec))))}))
|
|
46
|
+
|
|
47
|
+
(defn list-notes
|
|
48
|
+
"Return newest-first vector of note metadata maps from `subdir`."
|
|
49
|
+
[subdir]
|
|
50
|
+
(let [d (io/file NOTES subdir)]
|
|
51
|
+
(when (.exists d)
|
|
52
|
+
(->> (.listFiles d)
|
|
53
|
+
(filter (fn [^java.io.File f]
|
|
54
|
+
(and (.isFile f)
|
|
55
|
+
(not (.startsWith (.getName f) ".")))))
|
|
56
|
+
(map read-note)
|
|
57
|
+
(sort-by :mtime >)
|
|
58
|
+
vec))))
|
|
59
|
+
|
|
60
|
+
(defn green-ready
|
|
61
|
+
"Return newest-first notes from ready_for_review/ that are not marked red."
|
|
62
|
+
[]
|
|
63
|
+
(->> (list-notes "ready_for_review")
|
|
64
|
+
(remove #(= (:status %) :red))))
|
|
65
|
+
|
|
66
|
+
(defn proposed-green
|
|
67
|
+
"Return green proposals sorted by rank when available."
|
|
68
|
+
[]
|
|
69
|
+
(->> (list-notes "proposed_tasks")
|
|
70
|
+
(filter #(= (:status %) :green))
|
|
71
|
+
(sort-by (comp #(try
|
|
72
|
+
(Integer/parseInt %)
|
|
73
|
+
(catch Exception _
|
|
74
|
+
9999))
|
|
75
|
+
:rank :fm))))
|
|
76
|
+
|
|
77
|
+
(defn cto-feedback-latest
|
|
78
|
+
"Return up to 20 newest feedback notes from notes_FROM_CTO/."
|
|
79
|
+
[]
|
|
80
|
+
(take 20 (list-notes "notes_FROM_CTO")))
|
|
81
|
+
|
|
82
|
+
(defn scratch-latest
|
|
83
|
+
"Return scratch notes touched within RECENT-SEC seconds, newest first."
|
|
84
|
+
[recent-sec]
|
|
85
|
+
(let [cut (- (System/currentTimeMillis) (* 1000 recent-sec))]
|
|
86
|
+
(->> (list-notes "scratch")
|
|
87
|
+
(filter #(>= (:mtime %) cut))
|
|
88
|
+
(take 50)
|
|
89
|
+
vec)))
|
|
90
|
+
|
|
91
|
+
(defn pick-files-for-prompt
|
|
92
|
+
"Select distinct filenames that should be surfaced in agent prompts.
|
|
93
|
+
recent-sec defaults to 120 seconds when unspecified."
|
|
94
|
+
[{:keys [recent-sec]}]
|
|
95
|
+
(let [recent (or recent-sec 120)
|
|
96
|
+
a (map :name (take 10 (green-ready)))
|
|
97
|
+
b (map :name (take 10 (proposed-green)))
|
|
98
|
+
c (map :name (take 5 (cto-feedback-latest)))
|
|
99
|
+
d (map :name (take 5 (scratch-latest recent)))]
|
|
100
|
+
(->> (concat a b c d)
|
|
101
|
+
distinct
|
|
102
|
+
vec)))
|
|
103
|
+
|
|
104
|
+
(defn mark-note-red!
|
|
105
|
+
"Ensure the given note map is marked red in place.
|
|
106
|
+
Expects the note to have been produced by read-note."
|
|
107
|
+
[note]
|
|
108
|
+
(let [f (io/file (:file note))
|
|
109
|
+
s (slurp f)
|
|
110
|
+
[_ front body] (re-matches #"(?s)^---\n(.*?)\n---\n(.*)$" s)
|
|
111
|
+
ensure-trailing-newline (fn [text] (if (str/blank? text) "" (str text (when-not (str/ends-with? text "\n") "\n"))))]
|
|
112
|
+
(if front
|
|
113
|
+
(let [lines (str/split-lines front)
|
|
114
|
+
has-status? (some #(re-matches #"(?i)^\s*status\s*:" %) lines)
|
|
115
|
+
updated-lines (if has-status?
|
|
116
|
+
(map #(if (re-matches #"(?i)^\s*status\s*:" %)
|
|
117
|
+
"status: red"
|
|
118
|
+
%)
|
|
119
|
+
lines)
|
|
120
|
+
(conj (vec lines) "status: red"))
|
|
121
|
+
new-front (str/join "\n" updated-lines)]
|
|
122
|
+
(spit f (str "---\n" (ensure-trailing-newline new-front) "---\n" body)))
|
|
123
|
+
(spit f (str "---\nstatus: red\n---\n" s)))))
|