@assuredloop/orca-bot-kit 0.1.0 → 0.3.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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +548 -3
  3. package/SETUP.md +275 -0
  4. package/package.json +32 -13
  5. package/rules/evidence.md +15 -0
  6. package/rules/finishing.md +23 -0
  7. package/rules/limits.md +18 -0
  8. package/rules/mail.md +17 -0
  9. package/rules/profiles.md +9 -0
  10. package/rules/review.md +14 -0
  11. package/rules/scope.md +22 -0
  12. package/rules/simple.md +14 -0
  13. package/rules/talk.md +13 -0
  14. package/rules/tests-first.md +17 -0
  15. package/rules/the-ask.md +19 -0
  16. package/skills/obk-arch/LICENSES.md +52 -0
  17. package/skills/obk-arch/NOTICE.md +242 -0
  18. package/skills/obk-arch/SKILL.md +598 -0
  19. package/skills/obk-bot-building/LICENSES.md +21 -0
  20. package/skills/obk-bot-building/NOTICE.md +113 -0
  21. package/skills/obk-bot-building/SKILL.md +312 -0
  22. package/skills/obk-debugging/LICENSES.md +67 -0
  23. package/skills/obk-debugging/NOTICE.md +115 -0
  24. package/skills/obk-debugging/SKILL.md +434 -0
  25. package/skills/obk-decision-memo/LICENSES.md +49 -0
  26. package/skills/obk-decision-memo/NOTICE.md +181 -0
  27. package/skills/obk-decision-memo/SKILL.md +241 -0
  28. package/skills/obk-finops/LICENSES.md +19 -0
  29. package/skills/obk-finops/NOTICE.md +77 -0
  30. package/skills/obk-finops/SKILL.md +148 -0
  31. package/skills/obk-fleet-review/LICENSES.md +52 -0
  32. package/skills/obk-fleet-review/NOTICE.md +104 -0
  33. package/skills/obk-fleet-review/SKILL.md +256 -0
  34. package/skills/obk-grilling/LICENSES.md +42 -0
  35. package/skills/obk-grilling/NOTICE.md +111 -0
  36. package/skills/obk-grilling/SKILL.md +221 -0
  37. package/skills/obk-grooming/LICENSES.md +52 -0
  38. package/skills/obk-grooming/NOTICE.md +111 -0
  39. package/skills/obk-grooming/SKILL.md +222 -0
  40. package/skills/obk-handoff/LICENSES.md +45 -0
  41. package/skills/obk-handoff/NOTICE.md +51 -0
  42. package/skills/obk-handoff/SKILL.md +119 -0
  43. package/skills/obk-personal-facilitation/LICENSES.md +53 -0
  44. package/skills/obk-personal-facilitation/NOTICE.md +62 -0
  45. package/skills/obk-personal-facilitation/SKILL.md +86 -0
  46. package/skills/obk-recall/LICENSES.md +42 -0
  47. package/skills/obk-recall/NOTICE.md +53 -0
  48. package/skills/obk-recall/SKILL.md +129 -0
  49. package/skills/obk-researching/LICENSES.md +67 -0
  50. package/skills/obk-researching/NOTICE.md +170 -0
  51. package/skills/obk-researching/SKILL.md +269 -0
  52. package/skills/obk-reviewing/LICENSES.md +66 -0
  53. package/skills/obk-reviewing/NOTICE.md +58 -0
  54. package/skills/obk-reviewing/SKILL.md +411 -0
  55. package/skills/obk-tdd/LICENSES.md +68 -0
  56. package/skills/obk-tdd/NOTICE.md +54 -0
  57. package/skills/obk-tdd/SKILL.md +547 -0
  58. package/skills/obk-tdd/mutation-tools.md +80 -0
  59. package/skills/obk-teach/LICENSES.md +42 -0
  60. package/skills/obk-teach/NOTICE.md +65 -0
  61. package/skills/obk-teach/SKILL.md +106 -0
  62. package/skills/obk-why/LICENSES.md +42 -0
  63. package/skills/obk-why/NOTICE.md +74 -0
  64. package/skills/obk-why/SKILL.md +180 -0
  65. package/skills/obk-writing/LICENSES.md +272 -0
  66. package/skills/obk-writing/NOTICE.md +161 -0
  67. package/skills/obk-writing/SKILL.md +254 -0
  68. package/src/book.js +268 -0
  69. package/src/bot.js +451 -0
  70. package/src/cli.js +1169 -0
  71. package/src/conversations.js +150 -0
  72. package/src/groom.js +159 -0
  73. package/src/health.js +309 -0
  74. package/src/hooks.js +257 -0
  75. package/src/init.js +314 -0
  76. package/src/launch.js +299 -0
  77. package/src/message.js +402 -0
  78. package/src/orca.js +362 -0
  79. package/src/pause.js +63 -0
  80. package/src/record.js +209 -0
  81. package/src/restart.js +193 -0
  82. package/src/retire.js +92 -0
  83. package/src/roster.js +71 -0
  84. package/src/rules.js +408 -0
  85. package/src/skills.js +630 -0
  86. package/src/sources.js +371 -0
  87. package/src/up.js +500 -0
  88. package/src/usage.js +401 -0
  89. package/bin/okb.js +0 -4
  90. package/src/index.js +0 -11
@@ -0,0 +1,254 @@
1
+ ---
2
+ name: obk-writing
3
+ description: >-
4
+ Writing something a particular person will actually read: finding out who it
5
+ is for and what it has to do, choosing one kind of piece and staying in it,
6
+ leading with the point, keeping every sentence to one job and one reading,
7
+ checking the claims that carry weight, and testing it on someone who was not
8
+ in the room. Use when the job is to produce writing for people rather than
9
+ code: a page, a note, an update, an announcement, a summary, a message, or
10
+ editing a draft that already exists.
11
+ ---
12
+
13
+ # Writing for a reader
14
+
15
+ Everything here serves one test. A particular person, in a particular place,
16
+ with the amount of attention they actually have, gets what they came for. A
17
+ piece that is accurate and complete and does not survive that test has failed
18
+ at the only thing it was for.
19
+
20
+ These are defaults for work where nothing says otherwise. A user who asks for
21
+ something else gets what they asked for; say which of these you left and why.
22
+
23
+ ## Find out who it is for
24
+
25
+ Four things change almost every sentence, so settle them before drafting: who
26
+ reads it, what they should be able to do or decide afterwards, where it will be
27
+ read, and what tone fits. Ask when the answer would change the piece. Where
28
+ asking is not possible, write down the reading you took, so the first correction
29
+ is cheap.
30
+
31
+ Settling those is not the same as having the facts. Look a missing fact up
32
+ where you can. Where it is simply not to be had (a name, a time, a link), do
33
+ not fill it in and do not stop:
34
+ put a visible marker where the fact belongs, and list what you need at the end
35
+ so one reply closes all of them at once. A piece with its holes marked is a
36
+ useful thing to hand back. A piece with its holes filled in from imagination is
37
+ worse than no piece, because nobody can see which parts to check.
38
+
39
+ The place it will be read is a constraint, not decoration. Something skimmed on
40
+ a phone between meetings, by people with a little context and no patience, is a
41
+ different piece from the same facts written as a page someone lands on from a
42
+ search. The place sets the length, the register, and how much can be assumed.
43
+
44
+ Find out what they already know, and what they call things. Writing in the
45
+ reader's vocabulary rather than the system's is most of what makes a piece feel
46
+ written for them. Where the two differ, the reader's word wins, and you say the
47
+ other one once.
48
+
49
+ ## Choose one kind of piece
50
+
51
+ Decide what the piece is before you write it, because the kinds do not mix
52
+ well:
53
+
54
+ - Something to **follow**, which walks one person through one path and cares
55
+ that they arrive.
56
+ - Something to **use**, which assumes competence and gives the steps for a job
57
+ someone already understands.
58
+ - Something to **look up**, which is complete, dry and accurate, and makes no
59
+ argument.
60
+ - Something to **understand**, which gives the reasons, the history and the
61
+ choices, and is allowed a view.
62
+ - Something to **decide on**, which leads with what is being asked, gives the
63
+ options with honest trade-offs, and says what would change the answer.
64
+
65
+ Two audiences can need two pieces of the same kind. Where that happens, decide
66
+ what each one gets that the other does not, and say whether they also get the
67
+ other piece, so the second does not repeat the first. Then look for the
68
+ asymmetry: where one group is told something that makes the other group's job
69
+ harder, whoever has to square that needs to know it is coming.
70
+
71
+ Know which one is the job, because the piece is organised around that. Losing
72
+ it is how a piece gets long and satisfies nobody: an argument in the middle of
73
+ a set of instructions, hand-holding inside something written for experts, a
74
+ reference that keeps stopping to persuade.
75
+
76
+ This is not a rule against a piece containing more than one thing. A short
77
+ announcement can carry the reason for the change, the dates and what to do, and
78
+ sending three linked pieces instead would serve nobody. What matters is whether
79
+ the extra material helps the same reader do the same job. Split when the blend
80
+ makes it unclear who the piece is for, what they are supposed to do, or whether
81
+ they have been told everything. Otherwise keep it in one thing and keep the
82
+ supporting material in its place.
83
+
84
+ ## Lead with the point
85
+
86
+ The first thing anyone reads should tell them what this is, why it matters to
87
+ them, and what to do next. Readers skim before they commit, and a piece earns
88
+ the full read by answering those three in the first screen.
89
+
90
+ Put the most important thing first, then the common case, then the exceptions.
91
+ A conclusion at the bottom is a conclusion most people will not reach.
92
+
93
+ Importance order and the order things happen in are not the same, and a piece
94
+ that turns on dates or steps needs both. Sorting three dates by how much each
95
+ matters leaves the reader re-sorting them into a sequence themselves. Give them
96
+ the sequence in one place, then take the parts in whatever order serves them.
97
+
98
+ Headings carry the point rather than the topic. "Gates run in CI" tells a
99
+ skimmer something; "CI integration" only tells them where they are. A heading
100
+ earns its place by being a useful landmark, not by filling a quota.
101
+
102
+ Shorten by cutting what does not change what the reader does next, never by
103
+ compressing prose into shorthand they have to decode. That is the answer when
104
+ "keep it short" meets a sentence that needs its small words: the sentence keeps
105
+ them, and something else goes entirely. Dropped articles, arrows
106
+ and abbreviations make a page look tight and read slowly. Complete sentences
107
+ with their small words are faster to read, and that is the only speed that
108
+ counts.
109
+
110
+ ## Ground what the piece leans on
111
+
112
+ Every idea a paragraph rests on was either something the reader walked in with
113
+ or something an earlier paragraph gave them. A paragraph that leans on an idea
114
+ the reader does not have loses them, whether or not there is any jargon in it.
115
+
116
+ That is the real lever on difficulty. Assume too much at the start and you shut
117
+ people out. Explain too much and the opening drowns before it reaches the
118
+ point. Decide deliberately what the reader is expected to bring, say so, and
119
+ introduce the rest where it is first needed rather than in a block at the
120
+ front.
121
+
122
+ Gloss a term the first time it appears, even where the reader used it first.
123
+ People use words they have not pinned down.
124
+
125
+ ## One job per sentence
126
+
127
+ - Say who does what. "The build rejects it" rather than "it is rejected".
128
+ Passive is for when the actor is genuinely unknown or beside the point.
129
+ - Write instructions as instructions, and put the condition first, so a reader
130
+ can skip what does not apply to them. "To cancel, press Escape."
131
+ - One instruction per sentence, one thought per sentence everywhere else. Split
132
+ the sentence carrying two.
133
+ - Keep sentences short. A longer one that carries one idea with its condition
134
+ is not a fault.
135
+ - Cut any word the sentence survives without. "In order to" is "to". "It is
136
+ important to note that" is nothing.
137
+ - Use the short everyday word. A long word has to buy its length with
138
+ precision.
139
+ - Cut the adverb or find the right verb. An adverb propping up a weak verb
140
+ means the verb is wrong.
141
+ - Give one thing one name and keep it. Cycling through synonyms to avoid
142
+ repetition teaches the reader that there are three things.
143
+
144
+ Where a rule makes a sentence worse, fix the sentence another way or leave it.
145
+ A sentence that obeys every rule and sounds like nobody wrote it has failed.
146
+
147
+ ## Leave no sentence open to two readings
148
+
149
+ - Keep "only" and "not" beside the word they change. "Only fails on growth" and
150
+ "fails only on growth" are different claims.
151
+ - Make every "it", "they" and "this" point at one obvious thing. Repeat the
152
+ noun rather than making the reader work it out, and never let "this" stand
153
+ for a whole preceding clause.
154
+ - Break up stacked nouns. "The import budget check script" becomes "the script
155
+ that checks the import budget".
156
+ - Say which parts an "and" or an "or" joins where a sentence could group two
157
+ ways. "Either ... or" and "both ... and" cost nothing.
158
+ - Keep the small words that hold a sentence together, even where a shorter
159
+ version would scan. Clarity is worth more than the word count.
160
+ - Prefer a full stop. Where a sentence is carrying a second clause on a dash or
161
+ a semicolon, it is usually two sentences.
162
+ - Skip idioms, metaphors and stock phrases. Someone reading in a second
163
+ language, and anything translating or summarising the piece, all do better
164
+ with the plain construction.
165
+
166
+ ## Cut the sentence that asserts nothing
167
+
168
+ The hardest habit to see in your own draft is the sentence that sounds
169
+ substantial and says nothing. This is about empty sentences and not about
170
+ whether a piece may speak to how someone feels; naming what a reader is feeling
171
+ is a fact about them, and it belongs wherever it is true and useful.
172
+
173
+ "Designed for performance", "a seamless experience", "robust error handling".
174
+ Ask what the sentence tells the reader to do or know. If you can restate it as
175
+ a fact, a number or an instruction, do that instead. If you cannot, cut it.
176
+
177
+ One test catches most of them: if the sentence could appear word for word in
178
+ something about a different subject, it says nothing about this one.
179
+
180
+ Name the source or drop the claim. "Experts agree", "studies show" and "it is
181
+ widely considered" are not attributions, and a reader who wants to check one
182
+ finds nothing to check.
183
+
184
+ ## Check what you are asserting
185
+
186
+ A piece that describes something is a claim about it, and the claims that carry
187
+ weight get checked before it goes out. What something does, what state it is in
188
+ now, what it works with, and any number at all.
189
+
190
+ - Check against the thing itself rather than against memory, and where checking
191
+ is cheap and safe, actually run or open it.
192
+ - Where the only source is the person who asked, say so, and check what you
193
+ still can: the dates against a calendar, the arithmetic, and whether the
194
+ piece contradicts itself. A brief is one person's account and it can be
195
+ wrong, so the questions it raises go back to them rather than into the text
196
+ as fact.
197
+ - Give the exact string. People under pressure copy what you wrote, so the
198
+ command, the address, the error text and the name have to be right rather
199
+ than approximately right.
200
+ - Numbers and versions go stale. Either date the claim, tie it to something
201
+ that regenerates it, or leave it out. A count that was true when you wrote it
202
+ becomes a lie without anyone touching it.
203
+ - Distinguish nothing to do from nothing done. A report that says zero in a way
204
+ that reads like success is a lie made out of layout.
205
+ - Say the limits where the reader makes the decision, not in a section at the
206
+ end. Where the piece could be read as promising something it does not cover,
207
+ that belongs beside the promise.
208
+ - Where a fact could not be checked, say so in the text. A hedge that a skimmer
209
+ reads as confidence is worse than a plain "not verified".
210
+
211
+ Where you change something, change what describes it in the same breath. A
212
+ description left stale is the same defect as a wrong one, and harder to spot.
213
+
214
+ ## Choose the form deliberately
215
+
216
+ Prose carries an argument. A list carries things that are genuinely parallel,
217
+ and turning an argument into bullets throws away the connections that were the
218
+ argument. Where items are not parallel, write the paragraph.
219
+
220
+ Numbers claim that order matters. Use them for a sequence and bullets for
221
+ everything else. Introduce a list with a full sentence and keep the items the
222
+ same shape.
223
+
224
+ Where the same handful of fields repeats three times or more, that is a table.
225
+ Where the exact words are the point, quote them; otherwise say it in your own
226
+ words and cite where it came from.
227
+
228
+ Use emphasis for the one phrase a skimmer must not miss. When everything is
229
+ bold, nothing is.
230
+
231
+ ## Test it on someone who was not there
232
+
233
+ You cannot read your own draft as a stranger, and the gaps that matter are the
234
+ ones you filled in from what you already knew. Worth doing when people who were
235
+ not there will read it and a misreading would cost something; a short reply to
236
+ someone already in the conversation does not need it.
237
+
238
+ Give them the piece by itself: not the conversation that produced it, not your
239
+ summary, and not what you think of it, since handing over your own reading gets
240
+ your own reading back. Anyone counts who comes to it with only what a real
241
+ reader will have. Ask them to find what is wrong with it, not what they think
242
+ of it.
243
+
244
+ Read what comes back against the piece rather than deferring to it. A misreading
245
+ usually means the piece does not say it; sometimes the reader lacked context a
246
+ real reader will have, and then it is noise. Stop when a pass brings back only
247
+ small things or the holes you already knew about and have marked, and after
248
+ about three passes at most: a piece still turning up real problems then is not
249
+ ready, and that is worth saying rather than going round again.
250
+
251
+ Then hand it back. Whoever asked for it owns it and is the one to read it
252
+ through before it goes anywhere.
253
+
254
+ Sources and licences: [NOTICE.md](NOTICE.md).
package/src/book.js ADDED
@@ -0,0 +1,268 @@
1
+ // The book: `sessions.yaml` in a bot's home. It is the authority for what the
2
+ // kit knows about a bot's sessions (ADR 0012) — Orca forgets a tab's resume
3
+ // record the moment the tab is closed, so the kit keeps its own record.
4
+ //
5
+ // It is an ordinary file of the user's bots repo: committed, readable, and
6
+ // theirs. Beside each session's tab it holds the harness session id that
7
+ // session is running under, and every id it ran under before.
8
+
9
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { DatabaseSync } from 'node:sqlite';
12
+ import { isDeepStrictEqual } from 'node:util';
13
+ import { parse, stringify } from 'yaml';
14
+
15
+ const HEADER = `# What Orca calls this bot on this machine, and where each of its sessions
16
+ # lives. \`obk up\` writes this file; it is committed with the rest of the repo.
17
+ #
18
+ # A session is its Orca tab id. Titles are set, never read, so renaming a tab
19
+ # in Orca changes nothing here.
20
+ #
21
+ # Each session also carries the harness session it runs as, and the ones it ran
22
+ # as before it: a clear makes a new one, and the old ones are kept.
23
+ #
24
+ # \`mailbox\` is where fleet mail for the session is left — an Orca Run, made once
25
+ # and kept, because an address that is a tab dies with the tab. \`address\` is what
26
+ # a Claude session is called, which is what another Claude session writes to.
27
+ #
28
+ # \`retired\` holds what the book knew about each session \`obk retire\` took off
29
+ # the bot, with when, so the conversations it had are still accounted for.
30
+ `;
31
+
32
+ export const bookFile = (home) => path.join(home, 'sessions.yaml');
33
+
34
+ /** How long a writer waits for the one before it to finish. */
35
+ const WAIT_MS = 10_000;
36
+
37
+ /** What the book says, or an empty book when there is none yet. */
38
+ export function readBook(home) {
39
+ const file = bookFile(home);
40
+ if (!existsSync(file)) return { orca: {}, sessions: {} };
41
+
42
+ const parsed = parse(readFileSync(file, 'utf8'));
43
+ // A book someone emptied, or filled with something else, is treated as one
44
+ // that says nothing: `up` then makes what is missing, which is its job.
45
+ const book = parsed !== null && typeof parsed === 'object' ? parsed : {};
46
+ return {
47
+ ...book,
48
+ orca: asRecord(book.orca),
49
+ sessions: Object.fromEntries(Object.entries(asRecord(book.sessions)).map(([name, entry]) => [name, withHistoryListed(entry)])),
50
+ // A retired session's entry is the same entry, and its history is read the same way.
51
+ ...(Array.isArray(book.retired) ? { retired: book.retired.map(withHistoryListed) } : {}),
52
+ };
53
+ }
54
+
55
+ /**
56
+ * A session entry whose history a person typed as one id, `history: <id>`,
57
+ * rather than as the list the kit writes, read as that one earlier
58
+ * conversation. The book is theirs to edit by hand (README), and one id where a
59
+ * list goes is the edit a person makes.
60
+ */
61
+ function withHistoryListed(entry) {
62
+ const history = entry?.history;
63
+ if (history === undefined || history === null || Array.isArray(history)) return entry;
64
+ // One entry written without the list around it.
65
+ if (typeof history === 'object') return { ...entry, history: [history] };
66
+ // One id, which YAML reads as a number when it is all digits.
67
+ const id = String(history).trim();
68
+ return { ...entry, history: id === '' ? [] : [{ session: id }] };
69
+ }
70
+
71
+ /**
72
+ * Write the book in one step. A reader either sees the book as it was or as it
73
+ * is, never half of it, and a writer that dies leaves the old file in place:
74
+ * the new text goes to a file of this process's own and is moved over the book,
75
+ * which is one operation the file system either did or did not do.
76
+ */
77
+ export function writeBook(home, book) {
78
+ const file = bookFile(home);
79
+ const half = `${file}.${process.pid}.part`;
80
+ writeFileSync(half, `${HEADER}${stringify(book)}`);
81
+ renameSync(half, file);
82
+ }
83
+
84
+ /**
85
+ * Change the book with nothing else changing it at the same time: `up` writing
86
+ * a tab down and a session's own hook writing its id are two programs on one
87
+ * file, and either one holding a copy from before would undo the other.
88
+ *
89
+ * So every change is made here, and made on what the file says now: the lock is
90
+ * taken, the book is read, `change` is given it, and what comes back is
91
+ * written. `change` may return a book or change the one it was handed, and may
92
+ * be asynchronous — it is awaited under the lock.
93
+ *
94
+ * Nothing slow belongs inside it. Orca calls, tabs and prompts are the caller's
95
+ * work; this holds the lock for one read and one write.
96
+ */
97
+ export async function updateBook(home, change) {
98
+ const lock = takeLock(home);
99
+ try {
100
+ // Nothing writes the book outside this lock, the first write included. A
101
+ // missing book used to be written before the lock was taken: two first
102
+ // writers could both find no book, one take the lock and record its id, and
103
+ // the other then put the empty book it had read in its place (#161,
104
+ // reproduced with a real second process).
105
+ const book = readBook(home);
106
+ const before = structuredClone(book);
107
+ // Awaited, because a change that takes time must hold the lock while it
108
+ // does: an unawaited one would let the next writer in and then write over it.
109
+ const next = (await change(book)) ?? book;
110
+ // A run that changes nothing writes nothing: the file keeps its bytes and
111
+ // its time, and nothing else waiting on the lock has to read it again.
112
+ if (!isDeepStrictEqual(before, next)) writeBook(home, next);
113
+ return next;
114
+ } finally {
115
+ lock.release();
116
+ }
117
+ }
118
+
119
+ /**
120
+ * The right to change what belongs to one bot, held from before it is read until
121
+ * after it is written — a SQLite write transaction on a file of its own beside
122
+ * the book. The book is what it usually guards, and `obk groom` takes it too,
123
+ * for the one thing Orca will not do for us: look for an automation and make one
124
+ * if it is missing, without a second run doing the same between the two.
125
+ *
126
+ * SQLite is in Node itself and does this with the operating system's own file
127
+ * locks. That matters for one reason: the lock belongs to the process, so it is
128
+ * held while that process is stopped — swapped out, suspended, sitting in a
129
+ * debugger — and it is let go when the process ends, whether it ended well or
130
+ * not. Nothing has to be refreshed and nothing has to be declared abandoned.
131
+ *
132
+ * Three attempts at this have failed, each because the lock could be taken from a
133
+ * writer that was still going to write: a lease with a fixed timeout, then a
134
+ * lease kept alive by a timer (which a stopped process cannot run), then a check
135
+ * of the file just before replacing it (which a pause between the check and the
136
+ * replace defeats). A lock the kernel holds for the process has none of those
137
+ * seams, so there is nothing left to enumerate.
138
+ *
139
+ * The lock file holds no data — it is opened, locked and closed — so it stays
140
+ * empty and leaves no journal beside it.
141
+ */
142
+ export function takeLock(home) {
143
+ const file = lockFile(home);
144
+ mkdirSync(path.dirname(file), { recursive: true });
145
+
146
+ const db = new DatabaseSync(file);
147
+ try {
148
+ // A writer that arrives while another is working waits for it rather than
149
+ // failing at once, and gives up saying so rather than waiting for ever.
150
+ db.exec(`PRAGMA busy_timeout = ${WAIT_MS}`);
151
+ db.exec('BEGIN IMMEDIATE');
152
+ } catch (error) {
153
+ db.close();
154
+ throw waitedTooLong(home, error);
155
+ }
156
+
157
+ return {
158
+ release() {
159
+ try {
160
+ db.exec('COMMIT');
161
+ } finally {
162
+ db.close();
163
+ }
164
+ },
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Where a writer takes its turn: a folder of the kit's own beside the bots repo,
170
+ * carrying that folder's own name, the way start-prompt files sit beside it
171
+ * (PRD 6.3). SQLite keeps the file it locks, so this one stays, and the places it
172
+ * cannot stay are: the bot home, which is the user's repo and gains nothing a
173
+ * session did not ask for; and their home directory, where the kit writes
174
+ * nothing.
175
+ *
176
+ * Not the machine's temporary directory either, though it looks like the natural
177
+ * home for state that lasts as long as a write. A temporary directory is swept.
178
+ * A lock swept away while a writer holds it does not cost that writer anything —
179
+ * it costs the next one, which creates the file again and takes a turn on a
180
+ * different file while the first is still writing. Two writers, two locks,
181
+ * neither knowing: the failure four rounds of review have been about. A sibling
182
+ * is nobody's to sweep, and the user can see it and delete it.
183
+ *
184
+ * One folder per bots folder, so two bots folders each holding an api-bot cannot
185
+ * take each other's turn.
186
+ */
187
+ const lockFile = (home) => path.join(
188
+ `${path.dirname(path.dirname(home))}.locks`,
189
+ `${encodeURIComponent(path.basename(home))}.lock`,
190
+ );
191
+
192
+ /**
193
+ * Something else has been writing the book for longer than this run is prepared
194
+ * to wait. Every writer holds its turn for one read and one write, so this is a
195
+ * writer that is stuck or stopped rather than busy — said in the kit's own words,
196
+ * because the database's are about a database.
197
+ */
198
+ const waitedTooLong = (home, why) => new Error(
199
+ `waited ${WAIT_MS / 1000} seconds for something else to finish writing ${bookFile(home)} and it did not (${why.message}). Nothing was written. Try again; if nothing is running, remove ${lockFile(home)}, which is where writers take their turn.`,
200
+ );
201
+
202
+ /**
203
+ * The note of conversations nobody claims, with what a scan has just found added
204
+ * to it. It only ever grows: a scan can only see conversations since the kit last
205
+ * started a harness in the tab, so anything found earlier would fall out of range
206
+ * and be forgotten — which is exactly how an unresolved conversation went missing
207
+ * across a relaunch (round 3 review, finding 1). Ids leave this note one way
208
+ * only, in `forgetClaimed`, when a session claims them.
209
+ */
210
+ export function withUnclaimed(entry, found) {
211
+ const kept = [...new Set([...(Array.isArray(entry.unclaimed) ? entry.unclaimed : []), ...found])];
212
+ if (kept.length === 0) {
213
+ const { unclaimed: none, ...rest } = entry;
214
+ return rest;
215
+ }
216
+ return { ...entry, unclaimed: kept };
217
+ }
218
+
219
+ /**
220
+ * Take out of every session's note whatever some session now claims: a note is
221
+ * only ever about a conversation nobody owns, so an id that has found its owner
222
+ * has no business in one. This is the only way an id leaves a note.
223
+ */
224
+ export function forgetClaimed(book) {
225
+ const claimed = sessionIdsIn(book);
226
+ for (const [name, entry] of Object.entries(book.sessions)) {
227
+ if (!Array.isArray(entry?.unclaimed)) continue;
228
+ const kept = entry.unclaimed.filter((one) => !claimed.has(one));
229
+ book.sessions[name] = withUnclaimed({ ...entry, unclaimed: [] }, kept);
230
+ }
231
+ return book;
232
+ }
233
+
234
+ /**
235
+ * Every harness session the book accounts for, the ones running now, the ones
236
+ * that ran before, and the ones of sessions since retired. A conversation in
237
+ * here belongs to a session already.
238
+ */
239
+ export function sessionIdsIn(book) {
240
+ const retired = Array.isArray(book.retired) ? book.retired : [];
241
+ const ids = [...Object.values(book.sessions), ...retired].flatMap((entry) => [
242
+ entry?.session,
243
+ ...(Array.isArray(entry?.history) ? entry.history.map((old) => old?.session) : []),
244
+ ]);
245
+ return new Set(ids.filter((id) => typeof id === 'string'));
246
+ }
247
+
248
+ /** Every tab id the book holds. What is not in here is not the kit's session. */
249
+ export const tabIdsIn = (book) =>
250
+ new Set(Object.values(book.sessions).map((session) => session?.tab).filter((tab) => typeof tab === 'string'));
251
+
252
+ const asRecord = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {});
253
+
254
+ /**
255
+ * The session entry to keep once a harness has named the session it is running
256
+ * as. The id the harness gave replaces the one the book held, and the one it
257
+ * held goes into the history with the reason it was replaced (ADR 0012): old
258
+ * ids are what recall, auditing and finops read later.
259
+ *
260
+ * The same id come back is a session resumed, not a new one, so nothing moves.
261
+ */
262
+ export function rememberSession(entry = {}, id, ended) {
263
+ if (entry.session === id) return entry;
264
+ if (entry.session === undefined) return { ...entry, session: id };
265
+
266
+ const was = { session: entry.session, ended: ended ?? 'replaced', at: new Date().toISOString() };
267
+ return { ...entry, session: id, history: [...(entry.history ?? []), was] };
268
+ }