reslop 0.2.7 → 0.3.1

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 (158) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +573 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +17 -0
  7. package/lib/agents/models.js +467 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +151 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +504 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +114 -48
  45. package/lib/{editor.js → input/editor.js} +16 -8
  46. package/lib/{keys.js → input/keys.js} +24 -45
  47. package/lib/input/line-edit.js +78 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +642 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +385 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +320 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +48 -19
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +14 -0
  81. package/lib/review/format.js +420 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +976 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -40
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +189 -147
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +86 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +93 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +78 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +250 -0
  111. package/lib/session/npm/npm.js +836 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +158 -63
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +4 -3
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
package/lib/review.js DELETED
@@ -1,745 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
-
6
- const files = require('./files.js');
7
- const { TASKS_FILE, REVIEW_DIR, TASK_KINDS, taskKind, taskTitle } = files;
8
- const reviewMerge = require('./review-merge.js');
9
- const { captureBaseline, mergeReview } = reviewMerge;
10
- const utilities = require('./utilities.js');
11
- const { pad2, dateStamp } = utilities;
12
-
13
- const createStore = (reviewPath, templates = []) => ({
14
- reviewPath,
15
- dirty: false,
16
- nextTaskId: 1,
17
- feedback: new Map(),
18
- code: new Map(),
19
- tasks: [],
20
- templates: [...templates],
21
- status: 'editing',
22
- baseline: null,
23
- });
24
-
25
- const noteCounts = (store) => {
26
- const counts = { feedback: 0, tasks: 0, tasksDone: 0, code: 0 };
27
- if (!store) return counts;
28
- for (const task of store.tasks) {
29
- if (!task.text.trim()) continue;
30
- counts.tasks += 1;
31
- if (task.done) counts.tasksDone += 1;
32
- }
33
- for (const note of store.feedback.values()) {
34
- if (note.text.trim()) counts.feedback += 1;
35
- }
36
- counts.code = store.code.size;
37
- return counts;
38
- };
39
-
40
- const hasNotes = (store) => {
41
- const counts = noteCounts(store);
42
- return counts.feedback > 0 || counts.tasks > 0 || counts.code > 0;
43
- };
44
-
45
- const toNote = (note, done = false) => ({
46
- file: note.file,
47
- oldStart: note.oldStart ?? 0,
48
- newStart: note.newStart ?? 0,
49
- blockId: note.blockId ?? 0,
50
- origin: note.origin ?? '',
51
- header: note.header ?? '',
52
- text: note.text ?? '',
53
- done: note.done ?? done,
54
- });
55
-
56
- const putNote = (store, notes, key, note, clear) => {
57
- const prev = notes.get(key);
58
- if (clear) {
59
- if (!prev) return;
60
- notes.delete(key);
61
- store.dirty = true;
62
- return;
63
- }
64
- if (prev && prev.text === (note.text ?? '')) return;
65
- notes.set(key, toNote(note, prev?.done));
66
- store.dirty = true;
67
- };
68
-
69
- const setFeedback = (store, key, note) => {
70
- const clear = !(note.text ?? '').trim();
71
- putNote(store, store.feedback, key, note, clear);
72
- };
73
-
74
- const setCode = (store, key, note) => {
75
- putNote(store, store.code, key, note, Boolean(note.clear));
76
- };
77
-
78
- const addTask = (store, file, text = '', done = false, kind = 'features') => {
79
- const id = store.nextTaskId;
80
- store.nextTaskId += 1;
81
- const todo = { id, file, text, done, kind: taskKind({ kind }) };
82
- store.tasks.push(todo);
83
- store.dirty = true;
84
- return todo;
85
- };
86
-
87
- const removeTask = (store, id) => {
88
- const tasks = store.tasks.filter((todo) => todo.id !== id);
89
- if (tasks.length === store.tasks.length) return false;
90
- store.tasks = tasks;
91
- store.dirty = true;
92
- return true;
93
- };
94
-
95
- const setTaskText = (store, id, text) => {
96
- const i = store.tasks.findIndex((todo) => todo.id === id);
97
- if (i < 0) return;
98
- if (!text.trim()) return void removeTask(store, id);
99
- if (store.tasks[i].text === text) return;
100
- store.tasks[i] = { ...store.tasks[i], text };
101
- store.dirty = true;
102
- };
103
-
104
- const setTaskDone = (store, id, done) => {
105
- const i = store.tasks.findIndex((todo) => todo.id === id);
106
- if (i < 0 || store.tasks[i].done === done) return;
107
- store.tasks[i] = { ...store.tasks[i], done };
108
- store.dirty = true;
109
- };
110
-
111
- const checkLabel = (text, done) => {
112
- const mark = done ? 'x' : ' ';
113
- return `[${mark}] ${text}`;
114
- };
115
-
116
- const feedbackKey = (note) => {
117
- const file = note.file ?? '';
118
- const oldStart = note.oldStart ?? 0;
119
- const newStart = note.newStart ?? 0;
120
- const block = note.blockId ?? 0;
121
- return `${file}:${oldStart}:${newStart}:${block}`;
122
- };
123
-
124
- const applyImportedNotes = (store, imported) => {
125
- if (!store || !imported) return;
126
- const feedbacks = imported.feedback ?? [];
127
- for (const note of feedbacks) {
128
- const key = feedbackKey(note);
129
- const prev = store.feedback.get(key);
130
- const text = prev ? `${prev.text}\n\n${note.text}` : note.text;
131
- const done = prev ? prev.done && !!note.done : !!note.done;
132
- setFeedback(store, key, { ...note, text, done });
133
- }
134
- const todos = imported.tasks ?? imported.todos ?? [];
135
- for (const todo of todos) {
136
- addTask(store, todo.file || '', todo.text ?? '', !!todo.done);
137
- }
138
- };
139
-
140
- const setStatus = (store, status) => {
141
- if (!store) return;
142
- store.status = status;
143
- store.dirty = true;
144
- };
145
-
146
- const TEMPLATE_SHOW = 5;
147
-
148
- const rankedTemplates = (templates) => {
149
- const ranked = [...templates];
150
- ranked.sort((a, b) => {
151
- const byCount = b.count - a.count;
152
- if (byCount) return byCount;
153
- return a.text.localeCompare(b.text);
154
- });
155
- return ranked;
156
- };
157
-
158
- const prefixTemplates = (templates, prefix) => {
159
- if (!prefix) return templates;
160
- return templates.filter((entry) => entry.text.startsWith(prefix));
161
- };
162
-
163
- const upsertTemplate = (templates, text) => {
164
- const trimmed = text.trim();
165
- if (!trimmed) return templates;
166
- const next = [...templates];
167
- const i = next.findIndex((entry) => entry.text === trimmed);
168
- if (i < 0) {
169
- next.push({ text: trimmed, count: 1 });
170
- return next;
171
- }
172
- const prev = next[i];
173
- next[i] = { text: prev.text, count: prev.count + 1 };
174
- return next;
175
- };
176
-
177
- const rememberTemplate = (store, prevText, nextText) => {
178
- const trimmed = nextText.trim();
179
- if (!trimmed) return;
180
- const prev = (prevText ?? '').trim();
181
- if (prev === trimmed) return;
182
- store.templates = upsertTemplate(store.templates, trimmed);
183
- store.dirty = true;
184
- };
185
-
186
- const REVIEW_STATUSES = ['editing', 'ready', 'partial', 'done'];
187
- const FEED_HEAD = /^### Feedback `([^`]+)` \+(\d+) \((.*)\)\s*$/;
188
- const FEED_BLOCK = /^(.*), block (\d+)$/;
189
- const FEED_BLOCK_ONLY = /^block (\d+)$/;
190
- const FEED_META = /^<!-- reslop:(.*) -->\s*$/;
191
- const FEED_SUFFIX = /^(.*) - (.+):(\d+):(\d+):(\d+)$/;
192
- const CHECK_ITEM = /^- \[([ xX])\] (.*)$/;
193
- const CODE_HEAD = /^- \[([ xX])\] code `([^`]+)`\s*$/;
194
- const FENCE_LINE = /^(`{3,})\s*$/;
195
- const FILE_QUOTE = /^> (.+)$/;
196
- const STATUS_LINE = /^status:\s*(\S+)\s*$/;
197
- const GIT_ORIGINS = ['unstaged', 'staged', 'untracked', 'commit', 'pr'];
198
- const SECTION_LINE = {
199
- '### Todo': 'todo',
200
- '### Feedback': 'feedback',
201
- };
202
- const BACKLOG_HEAD = 'Backlog';
203
- const LEGACY_BACKLOG = 'TODOs';
204
- const TASK_TITLES = new Map([
205
- [BACKLOG_HEAD, 'debt'],
206
- [LEGACY_BACKLOG, 'debt'],
207
- ['Issues', 'debt'],
208
- ['Bug Reports', 'bugs'],
209
- ['Feature Requests', 'features'],
210
- ...TASK_KINDS.map((kind) => [kind.title, kind.id]),
211
- ]);
212
-
213
- const AGENT_INSTRUCTIONS = [
214
- '- Execute reviews with `status` `ready, partial, editing`',
215
- '- Do not start or change `done` review files',
216
- '- Work through unchecked items. After finishing one, mark it `[x]`',
217
- '- For `code` items, replace added lines at that location',
218
- '- If not `editing`, set `status` to `partial` if some remain, ' +
219
- 'or `done` if all are `[x]`',
220
- '- Do not redo items already marked `[x]`',
221
- '- Run the full check with `npm t`',
222
- '- Run specific test files with `reslop t -- node --test <files>`',
223
- ].join('\n');
224
-
225
- const toInt = (text) => parseInt(text, 10);
226
-
227
- const canonicalStatus = (raw) => {
228
- const name = raw === 'pending' ? 'ready' : raw;
229
- return REVIEW_STATUSES.includes(name) ? name : '';
230
- };
231
-
232
- const parseFrontmatterStatus = (text) => {
233
- if (!text.startsWith('---')) return '';
234
- const nl = text.indexOf('\n');
235
- if (nl < 0) return '';
236
- const rest = text.slice(nl + 1);
237
- const end = rest.indexOf('\n---');
238
- if (end < 0) return '';
239
- const matter = rest.slice(0, end);
240
- for (const line of matter.split('\n')) {
241
- const match = STATUS_LINE.exec(line);
242
- if (match) return canonicalStatus(match[1]);
243
- }
244
- return '';
245
- };
246
-
247
- const isDoneMark = (mark) => mark === 'x' || mark === 'X';
248
-
249
- const checkboxLine = (text, done) => {
250
- const flat = text.trim().replace(/\n/g, ' ');
251
- const mark = done ? 'x' : ' ';
252
- return `- [${mark}] ${flat}`;
253
- };
254
-
255
- const storeNote = (notes, note, key) => {
256
- const next = toNote(note);
257
- notes.set(key || feedbackKey(next), next);
258
- };
259
-
260
- const feedbackLine = (note) => {
261
- const loc = feedbackKey(note);
262
- return `${checkboxLine(note.text, note.done)} - ${loc}`;
263
- };
264
-
265
- const parseLoc = (raw) => {
266
- const parts = raw.split(':');
267
- const n = parts.length;
268
- if (n < 4) return null;
269
- const blockId = toInt(parts[n - 1]);
270
- const newStart = toInt(parts[n - 2]);
271
- const oldStart = toInt(parts[n - 3]);
272
- if (!Number.isFinite(blockId + newStart + oldStart)) return null;
273
- let fileParts = parts.slice(0, n - 3);
274
- let origin = '';
275
- if (fileParts.length > 1 && GIT_ORIGINS.includes(fileParts[0])) {
276
- origin = fileParts[0];
277
- fileParts = fileParts.slice(1);
278
- }
279
- if (!fileParts.length) return null;
280
- const file = fileParts.join(':');
281
- return { origin, file, oldStart, newStart, blockId };
282
- };
283
-
284
- const parseFeedSuffix = (textItem) => {
285
- const match = FEED_SUFFIX.exec(textItem);
286
- if (!match) return null;
287
- const text = match[1];
288
- const file = match[2];
289
- const oldStart = toInt(match[3]);
290
- const newStart = toInt(match[4]);
291
- const blockId = toInt(match[5]);
292
- return { text, file, oldStart, newStart, blockId };
293
- };
294
-
295
- const fileGroup = (groups, file) => {
296
- let group = groups.get(file);
297
- if (!group) {
298
- group = { tasks: [], feedback: [], code: [] };
299
- groups.set(file, group);
300
- }
301
- return group;
302
- };
303
-
304
- const TASK_SECTION = 'task:';
305
-
306
- const taskSection = (id) => `${TASK_SECTION}${id}`;
307
-
308
- const groupReviewNotes = (notes) => {
309
- const groups = new Map();
310
- for (const kind of TASK_KINDS) {
311
- for (const todo of notes.tasks) {
312
- if (!todo.text.trim() || taskKind(todo) !== kind.id) continue;
313
- fileGroup(groups, taskSection(kind.id)).tasks.push(todo);
314
- }
315
- }
316
- for (const key of notes.feedback.keys()) {
317
- const note = notes.feedback.get(key);
318
- if (!note.text.trim()) continue;
319
- fileGroup(groups, note.file).feedback.push({ key, note });
320
- }
321
- for (const key of notes.code.keys()) {
322
- const note = notes.code.get(key);
323
- fileGroup(groups, note.file).code.push({ key, note });
324
- }
325
- return groups;
326
- };
327
-
328
- const reviewHeader = (notes) => {
329
- const name = path.basename(notes.reviewPath, '.md');
330
- const status = canonicalStatus(notes.status) || 'editing';
331
- return [
332
- '---',
333
- `status: ${status}`,
334
- '---',
335
- '',
336
- `# reslop review ${name}`,
337
- '',
338
- '## Agent instructions',
339
- '',
340
- AGENT_INSTRUCTIONS,
341
- '',
342
- ];
343
- };
344
-
345
- const fenceFor = (text) => {
346
- let n = 3;
347
- for (const line of text.split('\n')) {
348
- const match = /^(`{3,})/.exec(line);
349
- if (!match) continue;
350
- if (match[1].length >= n) n = match[1].length + 1;
351
- }
352
- return '`'.repeat(n);
353
- };
354
-
355
- const emitCodeItem = (lines, note) => {
356
- const loc = feedbackKey(note);
357
- const mark = note.done ? 'x' : ' ';
358
- const fence = fenceFor(note.text);
359
- lines.push(`- [${mark}] code \`${loc}\``);
360
- lines.push(fence);
361
- if (note.text !== '') lines.push(note.text);
362
- lines.push(fence);
363
- };
364
-
365
- const sectionTitle = (file) => {
366
- if (!file.startsWith(TASK_SECTION)) return file;
367
- return taskTitle(file.slice(TASK_SECTION.length));
368
- };
369
-
370
- const backlogFile = (name) => {
371
- if (name === BACKLOG_HEAD || name === LEGACY_BACKLOG) return TASKS_FILE;
372
- return name;
373
- };
374
-
375
- const emitReviewGroups = (lines, groups) => {
376
- for (const file of groups.keys()) {
377
- const group = groups.get(file);
378
- lines.push(`## ${sectionTitle(file)}`);
379
- lines.push('');
380
- for (const todo of group.tasks) {
381
- lines.push(checkboxLine(todo.text, todo.done));
382
- }
383
- for (const item of group.feedback) {
384
- lines.push(feedbackLine(item.note));
385
- }
386
- for (const item of group.code) {
387
- emitCodeItem(lines, item.note);
388
- }
389
- lines.push('');
390
- }
391
- };
392
-
393
- const serializeReview = (notes) => {
394
- const lines = reviewHeader(notes);
395
- emitReviewGroups(lines, groupReviewNotes(notes));
396
- return `${lines.join('\n')}\n`;
397
- };
398
-
399
- const parseFeedbackHeading = (line) => {
400
- const match = FEED_HEAD.exec(line);
401
- if (!match) return null;
402
- const file = match[1];
403
- const newStart = toInt(match[2]);
404
- const rest = match[3];
405
- const only = FEED_BLOCK_ONLY.exec(rest);
406
- if (only) {
407
- return { file, newStart, header: '', blockId: toInt(only[1]) };
408
- }
409
- const withHead = FEED_BLOCK.exec(rest);
410
- if (!withHead) {
411
- return { file, newStart, header: rest, blockId: 0 };
412
- }
413
- const header = withHead[1];
414
- const blockId = toInt(withHead[2]);
415
- return { file, newStart, header, blockId };
416
- };
417
-
418
- const flushPendingFeedback = (builder, pending) => {
419
- if (!pending || !(pending.text ?? '').trim()) return null;
420
- storeNote(builder.feedback, pending, pending.key);
421
- return null;
422
- };
423
-
424
- const headingFile = (line) => {
425
- const quoted = FILE_QUOTE.exec(line);
426
- if (quoted) return backlogFile(quoted[1]);
427
- if (line.startsWith('## ') && line !== '## Agent instructions') {
428
- const name = line.slice(3);
429
- const kind = TASK_TITLES.get(name);
430
- if (kind) return taskSection(kind);
431
- return name;
432
- }
433
- return null;
434
- };
435
-
436
- const startFeedbackPending = (heading, file) => ({
437
- file: heading.file || file,
438
- newStart: heading.newStart,
439
- header: heading.header,
440
- blockId: heading.blockId,
441
- oldStart: heading.newStart,
442
- origin: '',
443
- key: '',
444
- text: '',
445
- });
446
-
447
- const applyFeedMeta = (pending, raw) => {
448
- const loc = parseLoc(raw);
449
- if (!loc) return pending;
450
- return Object.assign(pending, loc, { key: feedbackKey(loc) });
451
- };
452
-
453
- const applyCheckItem = (builder, file, section, pending, textItem, done) => {
454
- if (file.startsWith(TASK_SECTION)) {
455
- const kind = file.slice(TASK_SECTION.length);
456
- addTask(builder, TASKS_FILE, textItem, done, kind);
457
- return pending;
458
- }
459
- if (section === 'todo' && file) {
460
- addTask(builder, file, textItem, done, 'backlog');
461
- return pending;
462
- }
463
- const parsed = parseFeedSuffix(textItem);
464
- if (parsed) {
465
- flushPendingFeedback(builder, pending);
466
- storeNote(builder.feedback, { ...parsed, file: parsed.file || file, done });
467
- return null;
468
- }
469
- if (pending) {
470
- pending.text = textItem;
471
- pending.done = done;
472
- return flushPendingFeedback(builder, pending);
473
- }
474
- if (file && section !== 'feedback') {
475
- addTask(builder, file, textItem, done, 'backlog');
476
- }
477
- return pending;
478
- };
479
-
480
- const applyReviewLine = (builder, file, section, pending, line) => {
481
- const nextFile = headingFile(line);
482
- if (nextFile !== null) {
483
- pending = flushPendingFeedback(builder, pending);
484
- return { file: nextFile, section: '', pending };
485
- }
486
- const nextSection = SECTION_LINE[line];
487
- if (nextSection) {
488
- pending = flushPendingFeedback(builder, pending);
489
- return { file, section: nextSection, pending };
490
- }
491
- const heading = parseFeedbackHeading(line);
492
- if (heading) {
493
- flushPendingFeedback(builder, pending);
494
- pending = startFeedbackPending(heading, file);
495
- return { file, section: 'feedback', pending };
496
- }
497
- const meta = FEED_META.exec(line);
498
- if (meta && pending) {
499
- pending = applyFeedMeta(pending, meta[1]);
500
- return { file, section, pending };
501
- }
502
- const item = CHECK_ITEM.exec(line);
503
- if (!item) return { file, section, pending };
504
- const done = isDoneMark(item[1]);
505
- pending = applyCheckItem(builder, file, section, pending, item[2], done);
506
- return { file, section, pending };
507
- };
508
-
509
- const startCodePending = (line) => {
510
- const match = CODE_HEAD.exec(line);
511
- if (!match) return null;
512
- const loc = parseLoc(match[2]);
513
- if (!loc) return null;
514
- return { done: isDoneMark(match[1]), ...loc, open: '', lines: [] };
515
- };
516
-
517
- const storeCodeNote = (builder, pending, text) => {
518
- storeNote(builder.code, { ...pending, text });
519
- };
520
-
521
- const flushPendingCode = (builder, pending) => {
522
- if (!pending) return;
523
- const text = pending.open ? pending.lines.join('\n') : '';
524
- storeCodeNote(builder, pending, text);
525
- };
526
-
527
- const takeCodeLine = (builder, pending, line) => {
528
- const fence = FENCE_LINE.exec(line);
529
- if (!pending.open) {
530
- if (fence) {
531
- pending.open = fence[1];
532
- return pending;
533
- }
534
- if (!line.trim()) return pending;
535
- storeCodeNote(builder, pending, '');
536
- return 'fallthrough';
537
- }
538
- if (fence && fence[1].length >= pending.open.length) {
539
- storeCodeNote(builder, pending, pending.lines.join('\n'));
540
- return null;
541
- }
542
- pending.lines.push(line);
543
- return pending;
544
- };
545
-
546
- const parseReview = (text, reviewPath, templates = []) => {
547
- const builder = createStore(reviewPath, templates);
548
- builder.status = parseFrontmatterStatus(text) || 'editing';
549
- let file = '';
550
- let section = '';
551
- let pending = null;
552
- let pendingCode = null;
553
- for (const line of text.split('\n')) {
554
- if (pendingCode) {
555
- const nextCode = takeCodeLine(builder, pendingCode, line);
556
- if (nextCode === 'fallthrough') {
557
- pendingCode = null;
558
- } else {
559
- pendingCode = nextCode;
560
- continue;
561
- }
562
- }
563
- const started = startCodePending(line);
564
- if (started) {
565
- pending = flushPendingFeedback(builder, pending);
566
- pendingCode = started;
567
- continue;
568
- }
569
- const next = applyReviewLine(builder, file, section, pending, line);
570
- file = next.file;
571
- section = next.section;
572
- pending = next.pending;
573
- }
574
- flushPendingCode(builder, pendingCode);
575
- flushPendingFeedback(builder, pending);
576
- builder.dirty = false;
577
- captureBaseline(builder);
578
- return builder;
579
- };
580
-
581
- const TEMPLATES_FILE = 'templates.json';
582
- const AUTOSAVE_MS = 3000;
583
- const REVIEW_NAME = /^(\d{4}-\d{2}-\d{2})-(\d+)\.md$/;
584
-
585
- const allocateReviewPath = (dir, date, existingNames) => {
586
- const stamp = dateStamp(date);
587
- let max = -1;
588
- for (const name of existingNames) {
589
- const match = REVIEW_NAME.exec(name);
590
- if (!match) continue;
591
- if (match[1] !== stamp) continue;
592
- const n = toInt(match[2]);
593
- if (n > max) max = n;
594
- }
595
- const next = max + 1;
596
- return path.join(dir, REVIEW_DIR, `${stamp}-${pad2(next)}.md`);
597
- };
598
-
599
- const listReviewNames = (dir) => {
600
- const folder = path.join(dir, REVIEW_DIR);
601
- try {
602
- return fs.readdirSync(folder);
603
- } catch {
604
- return [];
605
- }
606
- };
607
-
608
- const compareReviewNames = (left, right) => {
609
- const a = REVIEW_NAME.exec(left);
610
- const b = REVIEW_NAME.exec(right);
611
- if (!a && !b) return 0;
612
- if (!a) return -1;
613
- if (!b) return 1;
614
- if (a[1] < b[1]) return -1;
615
- if (a[1] > b[1]) return 1;
616
- return toInt(a[2]) - toInt(b[2]);
617
- };
618
-
619
- const latestReviewName = (names) => {
620
- let best = '';
621
- for (const name of names) {
622
- if (!REVIEW_NAME.test(name)) continue;
623
- if (!best || compareReviewNames(name, best) > 0) best = name;
624
- }
625
- return best;
626
- };
627
-
628
- const readReviewStatus = (reviewPath) => {
629
- try {
630
- const text = fs.readFileSync(reviewPath, 'utf8');
631
- return parseFrontmatterStatus(text);
632
- } catch {
633
- return '';
634
- }
635
- };
636
-
637
- const resolveReviewPath = (dir, date, existingNames, extra = {}) => {
638
- if (!extra.forceNew) {
639
- const latest = latestReviewName(existingNames);
640
- if (latest) {
641
- const reviewPath = path.join(dir, REVIEW_DIR, latest);
642
- if (readReviewStatus(reviewPath) === 'editing') {
643
- return { reviewPath, resume: true };
644
- }
645
- }
646
- }
647
- return {
648
- reviewPath: allocateReviewPath(dir, date, existingNames),
649
- resume: false,
650
- };
651
- };
652
-
653
- const loadReview = (reviewPath, templates) => {
654
- const raw = fs.readFileSync(reviewPath, 'utf8');
655
- return parseReview(raw, reviewPath, templates);
656
- };
657
-
658
- const loadTemplates = (dir) => {
659
- const file = path.join(dir, REVIEW_DIR, TEMPLATES_FILE);
660
- try {
661
- const raw = fs.readFileSync(file, 'utf8');
662
- const data = JSON.parse(raw);
663
- if (!Array.isArray(data)) return [];
664
- const templates = [];
665
- for (const entry of data) {
666
- if (!entry || typeof entry.text !== 'string') continue;
667
- const count = toInt(entry.count);
668
- templates.push({
669
- text: entry.text,
670
- count: Number.isFinite(count) ? count : 1,
671
- });
672
- }
673
- return templates;
674
- } catch {
675
- return [];
676
- }
677
- };
678
-
679
- const readReviewText = (reviewPath) => {
680
- try {
681
- return fs.readFileSync(reviewPath, 'utf8');
682
- } catch (error) {
683
- if (error.code === 'ENOENT') return '';
684
- throw error;
685
- }
686
- };
687
-
688
- const mergeDiskReview = (notes) => {
689
- const text = readReviewText(notes.reviewPath);
690
- if (!text.startsWith('---')) return;
691
- const disk = parseReview(text, notes.reviewPath, notes.templates);
692
- mergeReview(notes, disk);
693
- };
694
-
695
- const flushReview = (notes, force = false) => {
696
- if (!notes || !notes.dirty) return false;
697
- mergeDiskReview(notes);
698
- if (!hasNotes(notes) && !force) {
699
- notes.dirty = false;
700
- return false;
701
- }
702
- const folder = path.dirname(notes.reviewPath);
703
- fs.mkdirSync(folder, { recursive: true });
704
- fs.writeFileSync(notes.reviewPath, serializeReview(notes), 'utf8');
705
- const top = path.dirname(folder);
706
- const templatesFile = path.join(top, REVIEW_DIR, TEMPLATES_FILE);
707
- const body = `${JSON.stringify(notes.templates, null, 2)}\n`;
708
- fs.writeFileSync(templatesFile, body, 'utf8');
709
- captureBaseline(notes);
710
- notes.dirty = false;
711
- return true;
712
- };
713
-
714
- module.exports = {
715
- TEMPLATE_SHOW,
716
- AUTOSAVE_MS,
717
- allocateReviewPath,
718
- listReviewNames,
719
- latestReviewName,
720
- parseFrontmatterStatus,
721
- resolveReviewPath,
722
- rankedTemplates,
723
- prefixTemplates,
724
- upsertTemplate,
725
- createStore,
726
- noteCounts,
727
- hasNotes,
728
- rememberTemplate,
729
- setFeedback,
730
- setCode,
731
- addTask,
732
- taskKind,
733
- removeTask,
734
- setTaskText,
735
- setTaskDone,
736
- checkLabel,
737
- feedbackKey,
738
- applyImportedNotes,
739
- setStatus,
740
- serializeReview,
741
- parseReview,
742
- loadReview,
743
- loadTemplates,
744
- flushReview,
745
- };