@assuredloop/orca-bot-kit 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +545 -3
- package/SETUP.md +270 -0
- package/package.json +32 -13
- package/rules/evidence.md +15 -0
- package/rules/finishing.md +20 -0
- package/rules/limits.md +18 -0
- package/rules/mail.md +17 -0
- package/rules/profiles.md +9 -0
- package/rules/review.md +14 -0
- package/rules/scope.md +20 -0
- package/rules/simple.md +14 -0
- package/rules/talk.md +13 -0
- package/rules/tests-first.md +15 -0
- package/rules/the-ask.md +19 -0
- package/skills/obk-arch/LICENSES.md +45 -0
- package/skills/obk-arch/NOTICE.md +134 -0
- package/skills/obk-arch/SKILL.md +387 -0
- package/skills/obk-bot-building/LICENSES.md +21 -0
- package/skills/obk-bot-building/NOTICE.md +113 -0
- package/skills/obk-bot-building/SKILL.md +235 -0
- package/skills/obk-debugging/LICENSES.md +67 -0
- package/skills/obk-debugging/NOTICE.md +115 -0
- package/skills/obk-debugging/SKILL.md +419 -0
- package/skills/obk-decision-memo/LICENSES.md +49 -0
- package/skills/obk-decision-memo/NOTICE.md +181 -0
- package/skills/obk-decision-memo/SKILL.md +241 -0
- package/skills/obk-finops/LICENSES.md +19 -0
- package/skills/obk-finops/NOTICE.md +77 -0
- package/skills/obk-finops/SKILL.md +146 -0
- package/skills/obk-fleet-review/LICENSES.md +52 -0
- package/skills/obk-fleet-review/NOTICE.md +104 -0
- package/skills/obk-fleet-review/SKILL.md +219 -0
- package/skills/obk-grilling/LICENSES.md +42 -0
- package/skills/obk-grilling/NOTICE.md +103 -0
- package/skills/obk-grilling/SKILL.md +217 -0
- package/skills/obk-grooming/LICENSES.md +52 -0
- package/skills/obk-grooming/NOTICE.md +111 -0
- package/skills/obk-grooming/SKILL.md +219 -0
- package/skills/obk-handoff/LICENSES.md +45 -0
- package/skills/obk-handoff/NOTICE.md +51 -0
- package/skills/obk-handoff/SKILL.md +119 -0
- package/skills/obk-personal-facilitation/LICENSES.md +53 -0
- package/skills/obk-personal-facilitation/NOTICE.md +62 -0
- package/skills/obk-personal-facilitation/SKILL.md +86 -0
- package/skills/obk-recall/LICENSES.md +42 -0
- package/skills/obk-recall/NOTICE.md +53 -0
- package/skills/obk-recall/SKILL.md +129 -0
- package/skills/obk-researching/LICENSES.md +67 -0
- package/skills/obk-researching/NOTICE.md +170 -0
- package/skills/obk-researching/SKILL.md +259 -0
- package/skills/obk-reviewing/LICENSES.md +66 -0
- package/skills/obk-reviewing/NOTICE.md +58 -0
- package/skills/obk-reviewing/SKILL.md +408 -0
- package/skills/obk-tdd/LICENSES.md +68 -0
- package/skills/obk-tdd/NOTICE.md +54 -0
- package/skills/obk-tdd/SKILL.md +516 -0
- package/skills/obk-tdd/mutation-tools.md +80 -0
- package/skills/obk-teach/LICENSES.md +42 -0
- package/skills/obk-teach/NOTICE.md +65 -0
- package/skills/obk-teach/SKILL.md +106 -0
- package/skills/obk-why/LICENSES.md +42 -0
- package/skills/obk-why/NOTICE.md +74 -0
- package/skills/obk-why/SKILL.md +169 -0
- package/skills/obk-writing/LICENSES.md +272 -0
- package/skills/obk-writing/NOTICE.md +161 -0
- package/skills/obk-writing/SKILL.md +254 -0
- package/src/book.js +268 -0
- package/src/bot.js +451 -0
- package/src/cli.js +1130 -0
- package/src/conversations.js +150 -0
- package/src/groom.js +157 -0
- package/src/health.js +309 -0
- package/src/hooks.js +202 -0
- package/src/init.js +314 -0
- package/src/launch.js +276 -0
- package/src/message.js +380 -0
- package/src/orca.js +291 -0
- package/src/pause.js +58 -0
- package/src/record.js +209 -0
- package/src/restart.js +192 -0
- package/src/retire.js +92 -0
- package/src/roster.js +71 -0
- package/src/rules.js +408 -0
- package/src/skills.js +629 -0
- package/src/sources.js +370 -0
- package/src/up.js +475 -0
- package/src/usage.js +401 -0
- package/bin/okb.js +0 -4
- 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 0002) — 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 0002): 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
|
+
}
|