baesian 0.0.0-stage → 1.0.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 (69) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +217 -2
  3. package/dist/agents.js +249 -0
  4. package/dist/api.js +13 -0
  5. package/dist/bin.js +14 -0
  6. package/dist/ci.js +82 -0
  7. package/dist/cite.js +66 -0
  8. package/dist/cli.js +831 -0
  9. package/dist/client/assets/alegreya-cyrillic-ext-wght-italic-DzWRXZb0.woff2 +0 -0
  10. package/dist/client/assets/alegreya-cyrillic-ext-wght-normal-CN0-BfWD.woff2 +0 -0
  11. package/dist/client/assets/alegreya-cyrillic-wght-italic-Dhrv2xfk.woff2 +0 -0
  12. package/dist/client/assets/alegreya-cyrillic-wght-normal-CU5lb38f.woff2 +0 -0
  13. package/dist/client/assets/alegreya-greek-ext-wght-italic-CoUI9EEu.woff2 +0 -0
  14. package/dist/client/assets/alegreya-greek-ext-wght-normal-C8PPRmwJ.woff2 +0 -0
  15. package/dist/client/assets/alegreya-greek-wght-italic-CVt9-Jr4.woff2 +0 -0
  16. package/dist/client/assets/alegreya-greek-wght-normal-BPx8yWcZ.woff2 +0 -0
  17. package/dist/client/assets/alegreya-latin-ext-wght-italic-BIuo_2sR.woff2 +0 -0
  18. package/dist/client/assets/alegreya-latin-ext-wght-normal-o8axeAGu.woff2 +0 -0
  19. package/dist/client/assets/alegreya-latin-wght-italic-Bxkcgmge.woff2 +0 -0
  20. package/dist/client/assets/alegreya-latin-wght-normal-BG9rTJo4.woff2 +0 -0
  21. package/dist/client/assets/alegreya-vietnamese-wght-italic-BAPuWgnQ.woff2 +0 -0
  22. package/dist/client/assets/alegreya-vietnamese-wght-normal-DOV9JYup.woff2 +0 -0
  23. package/dist/client/assets/archivo-latin-ext-wdth-normal-7khWdh9v.woff2 +0 -0
  24. package/dist/client/assets/archivo-latin-wdth-normal-DY7AcnAa.woff2 +0 -0
  25. package/dist/client/assets/archivo-vietnamese-wdth-normal-rJmnGBSt.woff2 +0 -0
  26. package/dist/client/assets/index-B3CFhLg-.css +1 -0
  27. package/dist/client/assets/index-DUcPUPI_.js +15 -0
  28. package/dist/client/index.html +15 -0
  29. package/dist/facts.js +278 -0
  30. package/dist/folder-storage.js +71 -0
  31. package/dist/frontmatter.js +38 -0
  32. package/dist/gate-paths.js +77 -0
  33. package/dist/gate.js +237 -0
  34. package/dist/git.js +100 -0
  35. package/dist/globs.js +120 -0
  36. package/dist/hookblock.js +146 -0
  37. package/dist/hooks.js +911 -0
  38. package/dist/ids.js +23 -0
  39. package/dist/login.js +301 -0
  40. package/dist/mcp.js +303 -0
  41. package/dist/merges.js +849 -0
  42. package/dist/modes.js +245 -0
  43. package/dist/notify.js +91 -0
  44. package/dist/ops.js +381 -0
  45. package/dist/passkey.js +86 -0
  46. package/dist/personal.js +163 -0
  47. package/dist/practice-answers.js +42 -0
  48. package/dist/proposals.js +197 -0
  49. package/dist/quote.js +28 -0
  50. package/dist/report.js +261 -0
  51. package/dist/routing.js +411 -0
  52. package/dist/schema.js +1178 -0
  53. package/dist/server.js +514 -0
  54. package/dist/similarity.js +45 -0
  55. package/dist/sshsig.js +159 -0
  56. package/dist/state.js +135 -0
  57. package/dist/storage.js +61 -0
  58. package/dist/store.js +2259 -0
  59. package/dist/style.js +408 -0
  60. package/dist/sync.js +797 -0
  61. package/dist/trailers.js +121 -0
  62. package/dist/trust.js +222 -0
  63. package/dist/usage-fields.js +161 -0
  64. package/dist/vault.js +849 -0
  65. package/dist/verify.js +443 -0
  66. package/dist/version.js +70 -0
  67. package/dist/wait.js +449 -0
  68. package/dist/wikilinks.js +25 -0
  69. package/package.json +71 -4
package/dist/wait.js ADDED
@@ -0,0 +1,449 @@
1
+ import { byLine, q, reviewerOf, said } from "./quote.js";
2
+ import { attention, cmd, failed, head, id, indent, lit, next, ok, text } from "./style.js";
3
+ import { NoCard, WAITING_BEAT_MS } from "./store.js";
4
+ /** The client beats every 30 s; three missed beats means the reviewer has stepped away (D41). */
5
+ export const PRESENCE_STALE_MS = 90_000;
6
+ /** How long a wait lasts in all, across its runs, before the agent falls back (D42, D46). */
7
+ export const DEFAULT_TIMEOUT_MS = 9 * 60_000;
8
+ /**
9
+ * How long one run of `baesian wait` lasts (D101): under every harness's command limit we know of, including
10
+ * Claude Code's default of 2 minutes. While the reviewer is there, a run that ends exits 7, and the agent runs it
11
+ * again.
12
+ */
13
+ export const SLICE_MS = 100_000;
14
+ export const POLL_MS = 1_000;
15
+ /** `baesian wait` exit codes. The agent-facing instructions in AGENTS.md list them too. */
16
+ export const WAIT_EXIT = {
17
+ /** Resolved: use the decision. */
18
+ resolved: 0,
19
+ /** Answered, but not a go-ahead: returned, deferred, withdrawn or superseded. Read the verdict. */
20
+ answered: 4,
21
+ /** No verdict yet; proceed provisionally on the proposal. */
22
+ proceed: 5,
23
+ /** No verdict yet on a hard card: don't proceed. */
24
+ park: 6,
25
+ /** No verdict yet, and the reviewer is still there: run `baesian wait` again (D101). */
26
+ again: 7,
27
+ /**
28
+ * Held back by the repo's preferences (D150, B-chq61): go ahead with your pick, as provisional. It counts as settled at
29
+ * merge, and the reviewer may still answer it later.
30
+ */
31
+ held: 8,
32
+ };
33
+ /** The wait was stopped through its signal before it had an outcome. */
34
+ export class WaitCancelled extends Error {
35
+ id;
36
+ constructor(id) {
37
+ super(`stopped waiting on ${id}`);
38
+ this.id = id;
39
+ }
40
+ }
41
+ /** A sleep that ends early when the signal aborts. */
42
+ function realSleep(ms, signal) {
43
+ return new Promise((done) => {
44
+ if (signal?.aborted)
45
+ return done();
46
+ const finish = () => {
47
+ clearTimeout(timer);
48
+ signal?.removeEventListener("abort", finish);
49
+ done();
50
+ };
51
+ const timer = setTimeout(finish, ms);
52
+ signal?.addEventListener("abort", finish, { once: true });
53
+ });
54
+ }
55
+ /**
56
+ * Block until the reviewer answers `id`, the timeout passes, or the client reports the reviewer away.
57
+ * Rerouted cards keep waiting: another reviewer is still to answer. With no verdict, what the agent may
58
+ * do depends on the level (D42): provisional and blocking proceed (a blocking card records a fallback,
59
+ * D46); hard never does. It's `waitForAll()` on one card.
60
+ */
61
+ export async function waitForVerdict(store, id, opts) {
62
+ const all = await waitForAll(store, [id], opts);
63
+ if (all.ended === "again")
64
+ return { kind: "again", id, waitedMs: all.waitedMs, leftMs: all.leftMs ?? 0 };
65
+ const card = all.cards[0];
66
+ // One card ends a wait only by being answered, held, or given up on: it's never still open at the end.
67
+ if (card.kind === "open")
68
+ throw new Error(`the wait on ${id} ended with the card still open`);
69
+ return card;
70
+ }
71
+ /** A card's outcome now, inside a view: resolved, answered otherwise or held, or nothing while it's still open. */
72
+ function outcomeOf(store, id) {
73
+ const status = store.status(id);
74
+ if (status === "held")
75
+ return { kind: "held", id };
76
+ if (status === "open" || status === "rerouted")
77
+ return undefined;
78
+ const latest = store.card(id);
79
+ const verdict = store
80
+ .verdicts(id)
81
+ .filter((v) => v.revision === latest.meta.revision)
82
+ .at(-1);
83
+ if (status === "resolved") {
84
+ return { kind: "resolved", id, decision: store.decision(id), verdict, briefs: store.briefs(id), canReply: store.canReply(id), ruleAsked: store.ruleAsked(id) };
85
+ }
86
+ return { kind: "answered", id, status, level: latest.meta.level, verdict, briefs: store.briefs(id), ruleAsked: store.ruleAsked(id) };
87
+ }
88
+ /** A card that comes back to its agent ends a wait on several at once: returned, so it's to revise or withdraw. */
89
+ const cameBack = (o) => o.kind === "answered" && o.status === "returned";
90
+ /**
91
+ * Block until every card in `ids` is answered, or sooner, when one comes back to the agent (returned) or is held back
92
+ * (B-xnae9); or, as a wait on one card does, until this run's slice is over, the wait's time runs out, or the reviewer
93
+ * is away. Each card still open keeps a waiting marker (D90), removed as soon as it's answered. Rerouted cards keep
94
+ * waiting.
95
+ *
96
+ * With `untilAnswered` it has no slice, no time limit and no presence check, so it ends only when the cards are answered
97
+ * or one comes back: for a run in the background that wakes its agent when it ends (Claude Code). Its markers show for
98
+ * the wait's budget (`timeoutMs`, from the first wait), as a wait the agent ran itself would, and are then removed while
99
+ * it goes on listening: an agent that ended its turn hours ago isn't one that has just stopped for the reviewer.
100
+ */
101
+ export async function waitForAll(store, ids, opts) {
102
+ const now = opts.now ?? Date.now;
103
+ const sleep = opts.sleep ?? ((ms) => realSleep(ms, opts.signal));
104
+ const pollMs = opts.pollMs ?? POLL_MS;
105
+ const until = opts.untilAnswered === true;
106
+ const wanted = [...new Set(ids)];
107
+ if (!wanted.length)
108
+ throw new Error("no cards to wait on");
109
+ for (const id of wanted)
110
+ if (!store.card(id))
111
+ throw new NoCard(id);
112
+ if (opts.signal?.aborted)
113
+ throw new WaitCancelled(wanted.join(" "));
114
+ const settled = new Map();
115
+ const look = () => store.view(() => {
116
+ for (const id of wanted) {
117
+ if (settled.has(id))
118
+ continue;
119
+ const o = outcomeOf(store, id);
120
+ if (o)
121
+ settled.set(id, o);
122
+ }
123
+ });
124
+ const ending = () => {
125
+ const all = [...settled.values()];
126
+ if (all.some(cameBack))
127
+ return "returned";
128
+ if (all.some((o) => o.kind === "held"))
129
+ return "held";
130
+ return settled.size === wanted.length ? "answered" : undefined;
131
+ };
132
+ const started = now();
133
+ const result = (ended, extra = {}) => ({
134
+ kind: "several",
135
+ ids: wanted,
136
+ ended,
137
+ cards: wanted.map((id) => settled.get(id) ?? { kind: "open", id, level: store.card(id).meta.level }),
138
+ waitedMs: extra.waitedMs ?? now() - started,
139
+ ...(extra.leftMs !== undefined ? { leftMs: extra.leftMs } : {}),
140
+ untilAnswered: until,
141
+ });
142
+ // A card already answered, or held, needs no marker: nobody waits on it. A held card answers the agent at once: go
143
+ // ahead, as settled at merge (B-chq61). A marker this agent left on it, from a run that ended with "again", goes.
144
+ look();
145
+ for (const id of settled.keys()) {
146
+ if (!store.waitingOn(id, now()).some((w) => w.marker.agent === opts.agent && !w.stale))
147
+ continue;
148
+ const left = store.startWaiting(id, opts.agent, new Date(now()).toISOString());
149
+ left.stop();
150
+ opts.onMarker?.({ path: left.path, removed: true });
151
+ }
152
+ const first = ending();
153
+ if (first)
154
+ return result(first, { waitedMs: 0 });
155
+ // The waiting markers (D90): while one is fresh, "stopped for you" is true of its card. A run that ends with "again"
156
+ // keeps them, and the next run picks them up, so the wait's budget counts from when it first started (D101). Each is
157
+ // removed when its card is answered, or the wait is over; an agent killed mid-wait leaves them behind, to go stale.
158
+ const markers = new Map();
159
+ for (const id of wanted.filter((i) => !settled.has(i))) {
160
+ const m = store.startWaiting(id, opts.agent, new Date(now()).toISOString());
161
+ const told = (removed) => opts.onMarker?.({ path: m.path, removed });
162
+ markers.set(id, {
163
+ since: m.since,
164
+ refresh(at) {
165
+ m.refresh(at);
166
+ told(false);
167
+ },
168
+ stop() {
169
+ m.stop();
170
+ told(true);
171
+ },
172
+ });
173
+ told(false);
174
+ }
175
+ const since = Math.min(...[...markers.values()].map((m) => Date.parse(m.since)));
176
+ const budgetEnd = since + (opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
177
+ const sliceEnd = until ? Number.POSITIVE_INFINITY : now() + (opts.sliceMs ?? SLICE_MS);
178
+ const stopMarker = (id) => {
179
+ markers.get(id)?.stop();
180
+ markers.delete(id);
181
+ };
182
+ let beat = now();
183
+ let keep = false;
184
+ try {
185
+ let reason;
186
+ for (;;) {
187
+ // Each poll is one fresh view of the live layer: a card's status reads the others, once each.
188
+ look();
189
+ for (const id of [...markers.keys()])
190
+ if (settled.has(id))
191
+ stopMarker(id);
192
+ const ended = ending();
193
+ if (ended)
194
+ return result(ended, { waitedMs: now() - since });
195
+ if (until) {
196
+ // Past the budget, it listens on without markers: the reviewer no longer sees an agent stopped for these cards.
197
+ if (markers.size && now() >= budgetEnd)
198
+ for (const id of [...markers.keys()])
199
+ stopMarker(id);
200
+ }
201
+ else {
202
+ if (opts.usePresence !== false) {
203
+ const p = store.presence();
204
+ // No heartbeat at all means the client has never run: unknown, so keep waiting. A stale one means away.
205
+ if (p && now() - Date.parse(p.at) > PRESENCE_STALE_MS) {
206
+ reason = "away";
207
+ break;
208
+ }
209
+ }
210
+ if (now() >= budgetEnd) {
211
+ reason = "timeout";
212
+ break;
213
+ }
214
+ if (now() >= sliceEnd) {
215
+ keep = true;
216
+ for (const m of markers.values())
217
+ m.refresh(new Date(now()).toISOString());
218
+ return result("again", { waitedMs: now() - since, leftMs: budgetEnd - now() });
219
+ }
220
+ }
221
+ const next = until ? (markers.size ? budgetEnd - now() : pollMs) : Math.min(sliceEnd - now(), budgetEnd - now());
222
+ await sleep(Math.max(0, Math.min(pollMs, next)));
223
+ // Cancelled: the finally below removes the markers, since the agent has stopped waiting.
224
+ if (opts.signal?.aborted)
225
+ throw new WaitCancelled(wanted.join(" "));
226
+ if (markers.size && now() - beat >= WAITING_BEAT_MS) {
227
+ beat = now();
228
+ for (const m of markers.values())
229
+ m.refresh(new Date(beat).toISOString());
230
+ opts.onBeat?.({ waitedMs: beat - since, leftMs: budgetEnd - beat });
231
+ }
232
+ }
233
+ // The level in force: in a hosted repo, the relay's signed gate record may have raised it (P16 section 3).
234
+ for (const id of wanted.filter((i) => !settled.has(i))) {
235
+ const level = store.view(() => store.levelOf(id)) ?? store.card(id).meta.level;
236
+ const proceed = level !== "hard";
237
+ let recorded = false;
238
+ if (level === "blocking") {
239
+ store.recordFallback(id, reason, opts.agent);
240
+ recorded = true;
241
+ }
242
+ settled.set(id, { kind: "no-verdict", id, reason, level, proceed, recorded });
243
+ }
244
+ return result("no-verdict", { waitedMs: now() - since });
245
+ }
246
+ finally {
247
+ if (!keep)
248
+ for (const id of [...markers.keys()])
249
+ stopMarker(id);
250
+ }
251
+ }
252
+ export function exitCodeFor(outcome) {
253
+ if (outcome.kind === "resolved")
254
+ return WAIT_EXIT.resolved;
255
+ if (outcome.kind === "again")
256
+ return WAIT_EXIT.again;
257
+ if (outcome.kind === "held")
258
+ return WAIT_EXIT.held;
259
+ if (outcome.kind === "answered")
260
+ return WAIT_EXIT.answered;
261
+ return outcome.proceed ? WAIT_EXIT.proceed : WAIT_EXIT.park;
262
+ }
263
+ /**
264
+ * A wait on several cards' exit code (B-xnae9): 0 when every card is resolved; 4 as soon as one comes back (returned),
265
+ * or when every card is answered and one isn't a go-ahead; 8 as soon as one is held back; 7 when the run's slice is over;
266
+ * and when the time runs out, or the reviewer is away, 6 if a card still open is hard, 4 if an answered one isn't a
267
+ * go-ahead, and otherwise 5.
268
+ */
269
+ export function exitCodeForAll(o) {
270
+ const notGoAhead = o.cards.some((c) => c.kind === "answered");
271
+ switch (o.ended) {
272
+ case "returned":
273
+ return WAIT_EXIT.answered;
274
+ case "held":
275
+ return WAIT_EXIT.held;
276
+ case "again":
277
+ return WAIT_EXIT.again;
278
+ case "answered":
279
+ return notGoAhead ? WAIT_EXIT.answered : WAIT_EXIT.resolved;
280
+ case "no-verdict":
281
+ if (o.cards.some((c) => c.kind === "no-verdict" && !c.proceed))
282
+ return WAIT_EXIT.park;
283
+ return notGoAhead ? WAIT_EXIT.answered : WAIT_EXIT.proceed;
284
+ }
285
+ }
286
+ /**
287
+ * A brief in the words an agent reads: what changed, and what to do about it (D80, D81). The texts others wrote are
288
+ * quoted with their authors (D169).
289
+ */
290
+ export function describeBrief(b) {
291
+ if (b.kind === "parent-decided-otherwise") {
292
+ const parent = { agent: b.parentAgent, branch: b.parentBranch };
293
+ return indent(2, [
294
+ text(id(b.parent), `, which ${b.card} builds on, was decided the other way. ${said("Its title", parent, b.parentTitle)}`),
295
+ text(`${said("Decided", reviewerOf(b), b.decided)}; ${said("not the proposal", parent, b.assumed)}`),
296
+ next(`baesian revise ${b.card} card.md`, "re-propose it on that basis, or withdraw it"),
297
+ ]);
298
+ }
299
+ const name = (c) => `${c.commit?.slice(0, 7) ?? "uncommitted"} (${c.branch})`;
300
+ const live = b.commits.filter((c) => !c.rewritten);
301
+ const gone = b.commits.filter((c) => c.rewritten);
302
+ const commits = live.length
303
+ ? `Commits that cite ${b.card}, built on the old answer: ${live.map(name).join(", ")}. Rework them to match. Only commits that cite the card are listed: check for other work built on it.`
304
+ : gone.length
305
+ ? `No commit on a branch cites ${b.card} now; check for work built on the old answer all the same.`
306
+ : `No commit cites ${b.card} yet; check for work built on the old answer all the same.`;
307
+ // B-9v5xj: the ledger's commits that a rebase or a squash took off every branch.
308
+ const rewritten = gone.length
309
+ ? [text(`Rewritten since, and on no branch now: ${gone.map(name).join(", ")}. Their work may be in ${live.length ? "the commits above, or in ones" : "commits"} that don't cite ${b.card}.`)]
310
+ : [];
311
+ const now = b.now !== undefined ? said(b.kind === "held-answered" ? "decided" : "one", reviewerOf(b), b.now) : `not a go-ahead (${b.response} by ${byLine(reviewerOf(b))})`;
312
+ if (b.kind === "held-answered") {
313
+ // D150, D151: the gate held it back, so the agent went ahead with its pick; the reviewer answered it later.
314
+ const pick = said("the pick", { agent: b.agent, branch: b.branch }, b.assumed);
315
+ return indent(2, [text(id(b.card), ` was held back, so its agent went ahead with ${pick}. The reviewer has answered it since: ${now}.`), text(commits), ...rewritten]);
316
+ }
317
+ const was = said("an answer", { person: b.wasReviewer }, b.was);
318
+ return indent(2, [text("The answer on ", id(b.card), ` changed. It was ${was}; now it's ${now}.`), text(commits), ...rewritten]);
319
+ }
320
+ /** What an agent is told when the reviewer asked for a rule from a decision (D16, D109, D110). */
321
+ export const ruleHint = (cardId) => [
322
+ text("They asked for a rule from this decision: draft it in a line, with the gate patterns it covers, if any (such as dep:pytest-* or schema:migrations/*). It goes to them as a card:"),
323
+ next(`baesian rule ${cardId} "<the rule>" --covers <pattern>`),
324
+ ];
325
+ /** What an agent is told when an answer went against its pick and it may still reply (D89, D104). */
326
+ export const replyHint = (cardId) => [
327
+ text("It went against your pick: proceed with their decision. If you hold a fact they lacked, or a check that would settle it, you may reply once (not with your pick again):"),
328
+ next(`baesian reply ${cardId} "<the fact or check>"`),
329
+ ];
330
+ /** How a returned card came back: "rejected by …", "with a question by …". */
331
+ function returnedHow(v) {
332
+ if (!v)
333
+ return "";
334
+ const by = byLine(reviewerOf(v));
335
+ const how = {
336
+ reject: "rejected",
337
+ question: "with a question",
338
+ need_context: "asking for more context,",
339
+ see_it_run: "asking to see it run,",
340
+ };
341
+ return `, ${how[v.response] ?? v.response} by ${by}`;
342
+ }
343
+ /** The trailer a commit that builds on a card carries, at the end of a sentence, so it copies whole. */
344
+ const carries = (cardId) => ["Commits that build on it carry the trailer ", lit(`Baesian-Card: ${cardId}`)];
345
+ /** What the agent should read: what happened, and what to do next. What the reviewer wrote is quoted (D169). */
346
+ export function describeOutcome(o) {
347
+ const comment = (v) => (v?.comment ? [indent(2, text(said("Comment", reviewerOf(v), v.comment)))] : []);
348
+ const by = (v) => (v ? ` by ${byLine(reviewerOf(v))}` : "");
349
+ if (o.kind === "held") {
350
+ return [
351
+ head("done", id(o.id), " is held back: this repo's preferences let the agent decide this kind of decision, whatever level the card asked for."),
352
+ text("Go ahead with your pick, as provisional: it counts as settled at merge. ", ...carries(o.id)),
353
+ text("The reviewer may still answer it later; ", cmd("baesian inbox"), " brings that answer if they do."),
354
+ ];
355
+ }
356
+ if (o.kind === "again") {
357
+ const min = Math.max(1, Math.round(o.leftMs / 60_000));
358
+ return [head("next", "no verdict on ", id(o.id), " yet, and the reviewer is still there"), next(`baesian wait ${o.id}`, `again: the wait goes on for about ${min} more minute${min === 1 ? "" : "s"}`)];
359
+ }
360
+ const briefs = o.kind === "no-verdict" ? [] : o.briefs.flatMap(describeBrief);
361
+ switch (o.kind) {
362
+ case "resolved":
363
+ // Word for word what it has always said: agents and their tests read it.
364
+ return [
365
+ head("done", id(o.id), " ", ok("resolved"), `${by(o.verdict)}: ${q(o.decision)}`),
366
+ ...comment(o.verdict),
367
+ ...(o.verdict?.retire_rules?.length
368
+ ? [indent(2, text(`The reviewer retired the rule${o.verdict.retire_rules.length === 1 ? "" : "s"} it breaks, ${o.verdict.retire_rules.join(", ")}: don't cite ${o.verdict.retire_rules.length === 1 ? "it" : "them"} again.`))]
369
+ : []),
370
+ ...briefs,
371
+ text("Proceed with that decision."),
372
+ ...(o.ruleAsked ? ruleHint(o.id) : []),
373
+ ...(o.canReply ? replyHint(o.id) : []),
374
+ ];
375
+ case "answered":
376
+ if (o.status === "returned" && o.briefs.some((b) => b.kind === "parent-decided-otherwise")) {
377
+ return [head("attention", id(o.id), " ", attention("returned"), ": what it builds on was decided the other way"), ...briefs];
378
+ }
379
+ if (o.status === "returned") {
380
+ return [
381
+ head("attention", id(o.id), " ", attention("returned"), returnedHow(o.verdict)),
382
+ ...comment(o.verdict),
383
+ ...briefs,
384
+ next(`baesian revise ${o.id} card.md`, "reply with the card's next revision"),
385
+ next(`baesian withdraw ${o.id} --reason "<why>"`, "or withdraw it, if it no longer applies"),
386
+ ...(o.ruleAsked ? ruleHint(o.id) : []),
387
+ ];
388
+ }
389
+ if (o.status === "deferred") {
390
+ return [
391
+ head("attention", id(o.id), " ", attention("deferred"), by(o.verdict)),
392
+ ...comment(o.verdict),
393
+ text(o.level === "hard" ? "It is hard: don't proceed with it." : "Proceed provisionally; it must be resolved before the branch merges."),
394
+ ];
395
+ }
396
+ return [head("attention", id(o.id), ` is ${o.status}: it no longer applies. Don't build on it.`)];
397
+ case "no-verdict": {
398
+ const why = o.reason === "away" ? "the reviewer is away" : "timed out";
399
+ if (!o.proceed) {
400
+ return [
401
+ failed("no verdict on ", id(o.id), ` yet (${why})`),
402
+ text("It is hard: do not proceed with it. Park this task and work on something else, or stop."),
403
+ next(`baesian wait ${o.id}`, "later, to wait again"),
404
+ ];
405
+ }
406
+ return [
407
+ head("attention", "no verdict on ", id(o.id), ` yet (${why})`),
408
+ text("Proceed provisionally on your proposal. ", ...carries(o.id)),
409
+ ...(o.recorded ? [text("Recorded on the card: you proceeded without a verdict.")] : []),
410
+ ];
411
+ }
412
+ }
413
+ }
414
+ /** IDs as an agent reads them in a sentence: "B-a", "B-a and B-b", "B-a, B-b and B-c". */
415
+ const named = (ids) => (ids.length < 2 ? ids.join("") : `${ids.slice(0, -1).join(", ")} and ${ids.at(-1)}`);
416
+ /**
417
+ * What the agent reads after a wait on several cards (B-xnae9): why it ended, then what happened to each card, in the
418
+ * words a wait on one card uses. A run that ends with "again" gives no answers yet: they all come when the wait ends,
419
+ * so the agent doesn't start on half of them.
420
+ */
421
+ export function describeAll(o) {
422
+ const open = o.cards.filter((c) => c.kind === "open").map((c) => c.id);
423
+ const answered = o.cards.filter((c) => c.kind !== "open");
424
+ if (o.ended === "again") {
425
+ const min = Math.max(1, Math.round((o.leftMs ?? 0) / 60_000));
426
+ const sofar = answered.length ? ` (${answered.length} of ${o.cards.length} answered so far; every answer comes when the wait ends)` : "";
427
+ return [
428
+ head("next", `no verdict yet on ${named(open)}, and the reviewer is still there${sofar}`),
429
+ next(`baesian wait ${o.ids.join(" ")}`, `again: the wait goes on for about ${min} more minute${min === 1 ? "" : "s"}`),
430
+ ];
431
+ }
432
+ // What ended it comes first: the cards that came back, or were held back.
433
+ const first = answered.filter((c) => (o.ended === "returned" ? cameBack(c) : o.ended === "held" ? c.kind === "held" : false));
434
+ const rest = answered.filter((c) => !first.includes(c));
435
+ const parked = o.cards.some((c) => c.kind === "no-verdict" && !c.proceed);
436
+ const head_ = {
437
+ answered: head(answered.every((c) => c.kind === "resolved") ? "done" : "attention", `every card you waited on has an answer: ${named(o.ids)}`),
438
+ returned: head("attention", `${named(first.map((c) => c.id))} came back to you, so the wait ended before every card was answered`),
439
+ held: head("done", `${named(first.map((c) => c.id))} ${first.length === 1 ? "is" : "are"} held back, so the wait ended before every card was answered`),
440
+ "no-verdict": head(parked ? "failed" : "attention", `the wait is over with no verdict yet on ${named(o.cards.filter((c) => c.kind === "no-verdict").map((c) => c.id))}`),
441
+ }[o.ended];
442
+ const lines = [head_, ...[...first, ...rest].flatMap(describeOutcome)];
443
+ if (open.length) {
444
+ lines.push(text(`Still with the reviewer: ${named(open)}. To wait on ${open.length === 1 ? "it" : "them"} again once you've acted on ${named(first.map((c) => c.id))}, with any card you revise added:`));
445
+ lines.push(next(`baesian wait ${open.join(" ")}${o.untilAnswered ? " --until-answered" : ""}`));
446
+ }
447
+ return lines;
448
+ }
449
+ //# sourceMappingURL=wait.js.map
@@ -0,0 +1,25 @@
1
+ // Wikilinks, as a deck's context notes name them (P17 §8): how one is read, and what it shows. No Node imports, so the
2
+ // state module (src/state.ts) shows them where there's no vault to resolve them in, as on the relay.
3
+ /** A wikilink's parts: `[[Folder/Name#Heading|text]]`. A bare name is read as a link's target. */
4
+ export function parseWikilink(raw) {
5
+ let s = raw.trim();
6
+ if (s.startsWith("!"))
7
+ s = s.slice(1);
8
+ if (s.startsWith("[[") && s.endsWith("]]"))
9
+ s = s.slice(2, -2);
10
+ const bar = s.indexOf("|");
11
+ const text = bar >= 0 ? s.slice(bar + 1).trim() : undefined;
12
+ if (bar >= 0)
13
+ s = s.slice(0, bar);
14
+ const hash = s.indexOf("#");
15
+ const heading = hash >= 0 ? s.slice(hash + 1).trim() : undefined;
16
+ if (hash >= 0)
17
+ s = s.slice(0, hash);
18
+ return { target: s.trim(), ...(heading ? { heading } : {}), ...(text ? { text } : {}) };
19
+ }
20
+ /** What a link shows: its own text, or its note's name and heading. */
21
+ export function linkText(raw) {
22
+ const { target, heading, text } = parseWikilink(raw);
23
+ return text ?? [target.split("/").at(-1), heading].filter(Boolean).join(" › ");
24
+ }
25
+ //# sourceMappingURL=wikilinks.js.map
package/package.json CHANGED
@@ -1,6 +1,73 @@
1
1
  {
2
2
  "name": "baesian",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "license": "FSL-1.1-ALv2",
5
+ "description": "Swipe review for agent decisions: agents file decision cards, a human answers them, verdicts flow back through git.",
6
+ "keywords": [
7
+ "decisions",
8
+ "review",
9
+ "agents",
10
+ "coding-agents",
11
+ "claude-code",
12
+ "codex",
13
+ "mcp",
14
+ "git-hooks"
15
+ ],
16
+ "homepage": "https://baesian.dev",
17
+ "bugs": {
18
+ "url": "https://github.com/GusEllerm/baesian/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/GusEllerm/baesian.git"
23
+ },
24
+ "type": "module",
25
+ "bin": {
26
+ "baesian": "dist/bin.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "!dist/site",
31
+ "!dist/**/*.map"
32
+ ],
33
+ "engines": {
34
+ "node": ">=22"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.build.json && vite build --config client/vite.config.ts && vite build --config client/vite.config.ts --mode site --logLevel warn",
38
+ "prepack": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && npm run build",
39
+ "typecheck": "tsc --noEmit && tsc -p client --noEmit",
40
+ "test": "vitest run",
41
+ "test:service": "npm --prefix service run typecheck && npm --prefix service test",
42
+ "e2e": "npm run build && playwright test",
43
+ "dev:client": "vite --config client/vite.config.ts",
44
+ "demo": "vite build --config client/vite.config.ts --logLevel warn && tsx scripts/demo.ts",
45
+ "readme-media": "vite build --config client/vite.config.ts --logLevel warn && tsx scripts/readme-media.ts",
46
+ "baesian": "tsx src/bin.ts",
47
+ "version": "node scripts/set-version.mjs"
48
+ },
49
+ "dependencies": {
50
+ "@modelcontextprotocol/server": "^2.4.0",
51
+ "commander": "^15.0.0",
52
+ "smol-toml": "^1.9.1",
53
+ "yaml": "^2.9.1",
54
+ "zod": "^4.6.7"
55
+ },
56
+ "devDependencies": {
57
+ "@fontsource-variable/alegreya": "5.3.0",
58
+ "@fontsource-variable/archivo": "5.3.0",
59
+ "@modelcontextprotocol/client": "^2.4.0",
60
+ "@playwright/test": "1.64.1",
61
+ "@types/node": "^22.20.5",
62
+ "@types/react": "19.3.0",
63
+ "@types/react-dom": "19.3.0",
64
+ "@vitejs/plugin-react": "6.1.2",
65
+ "motion": "14.9.0",
66
+ "react": "19.3.1",
67
+ "react-dom": "19.3.1",
68
+ "tsx": "^4.23.15",
69
+ "typescript": "^7.0.2",
70
+ "vite": "8.3.5",
71
+ "vitest": "^5.0.5"
72
+ }
73
+ }