214
198
(goto-char drawer-start)
215
199
(forward-line 1)
216
200
(let ((item-start (point)))
217
-
Removed:
(insert (format "- %s %s\n" ts note))
218
-
Removed:
(save-excursion
219
-
Removed:
(goto-char item-start)
220
-
Removed:
(org-fill-paragraph))))
201
+
Added:
(insert (format "- %s\n" line))
202
+
Added:
(unless nofill
203
+
Added:
(save-excursion
204
+
Added:
(goto-char item-start)
205
+
Added:
(org-fill-paragraph)))))
221
206
(set-marker subtree-end nil))))
222
207
208
+
Added:
(defun rail-tools--append-log (note)
209
+
Added:
"Append NOTE as a timestamped item to the entry's `:LOGBOOK:' drawer.
210
+
Added:
Insert the newest item first and wrap it to 72 columns. This is
211
+
Added:
append-only. It never edits an existing item or the body. Point must
212
+
Added:
be on the entry heading."
213
+
Added:
(let ((ts (format-time-string "[%Y-%m-%d %a %H:%M]")))
214
+
Added:
(rail-tools--drawer-insert (format "%s %s" ts note))))
215
+
Added:
216
+
Added:
(defun rail-tools--closing-note-text (commit tests &optional model notes)
217
+
Added:
"Compose the closing-note text for a completed item.
218
+
Added:
COMMIT is a commit hash. TESTS is a short recap such as \"215 pass\".
219
+
Added:
MODEL names the agent that did the work, and NOTES adds a free-text
220
+
Added:
tail after a semicolon. Both are optional. Return one line, because
221
+
Added:
the reader reads one line."
222
+
Added:
(let ((model (rail-tools--nonblank model))
223
+
Added:
(notes (rail-tools--nonblank notes)))
224
+
Added:
(concat (if model (format "model=%s " model) "")
225
+
Added:
(format "commit=%s tests=%s" commit tests)
226
+
Added:
(if notes (format "; %s" notes) ""))))
227
+
Added:
228
+
Added:
(defun rail-tools--append-closing-note (purpose text)
229
+
Added:
"Write a closing note into the entry's `:LOGBOOK:' drawer.
230
+
Added:
PURPOSE is `done', `blocked', or `cancelled'. TEXT is the note body.
231
+
Added:
The note reads `LABEL [timestamp] :: TEXT', with LABEL from
232
+
Added:
`rail-tools--closing-labels'. It lands in the drawer next to the
233
+
Added:
state timestamp, and it replaces the old body-line evidence. When the
234
+
Added:
entry already carries a note of the same PURPOSE, replace that note in
235
+
Added:
place, so a re-run does not stack a second note. Point must be on the
236
+
Added:
entry heading."
237
+
Added:
(let ((label (or (cdr (assq purpose rail-tools--closing-labels))
238
+
Added:
(error "Unknown closing purpose: %s" purpose)))
239
+
Added:
(ts (format-time-string "[%Y-%m-%d %a %H:%M]")))
240
+
Added:
(org-back-to-heading t)
241
+
Added:
(let ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
242
+
Added:
(line (format "%s %s :: %s" label ts text)))
243
+
Added:
(org-back-to-heading t)
244
+
Added:
(if (re-search-forward
245
+
Added:
(format "^[ \t]*- %s \\[[^]]*\\] ::.*$" (regexp-quote label))
246
+
Added:
subtree-end t)
247
+
Added:
;; Replace an existing note of this purpose in place. Keep the
248
+
Added:
;; note on one line, because the reader reads one line.
249
+
Added:
(replace-match (format "- %s" line) t t)
250
+
Added:
(rail-tools--drawer-insert line 'nofill))
251
+
Added:
(set-marker subtree-end nil))))
252
+
Added:
253
+
Added:
(defun rail-tools--closing-note-of (purpose)
254
+
Added:
"Return the text of the newest closing note of PURPOSE at point.
255
+
Added:
PURPOSE is `done', `blocked', or `cancelled'. Return nil when the
256
+
Added:
entry carries no such note. Point must be on the entry heading."
257
+
Added:
(let ((label (cdr (assq purpose rail-tools--closing-labels))))
258
+
Added:
(org-back-to-heading t)
259
+
Added:
(let ((subtree-end (save-excursion (org-end-of-subtree t t) (point))))
260
+
Added:
(org-back-to-heading t)
261
+
Added:
(when (re-search-forward
262
+
Added:
(format "^[ \t]*- %s \\[[^]]*\\] :: \\(.*\\)$"
263
+
Added:
(regexp-quote label))
264
+
Added:
subtree-end t)
265
+
Added:
(string-trim (match-string-no-properties 1))))))
266
+
Added:
223
267
(defmacro rail-tools--json (&rest body)
224
268
"Evaluate BODY and return its value as a JSON string.
225
269
Catch any error and return a JSON object with an `error' field."