@hypercarrier/rarebit 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AGENTS.md +14 -0
  2. package/CHANGELOG.md +10 -0
  3. package/LICENSE +21 -0
  4. package/README.md +142 -0
  5. package/RUNBOOK.md +36 -0
  6. package/SECURITY.md +11 -0
  7. package/VISUAL-LANGUAGE.md +89 -0
  8. package/bin/rarebit.mjs +20 -0
  9. package/package.json +96 -0
  10. package/schemas/rarebit.schema.json +618 -0
  11. package/scripts/e2e-recall-pi083.mjs +199 -0
  12. package/scripts/e2e-summary-cache.mjs +257 -0
  13. package/scripts/verify-package.mjs +81 -0
  14. package/src/automatic-summary-policy.mjs +210 -0
  15. package/src/extension.mjs +516 -0
  16. package/src/herdr-activity.mjs +111 -0
  17. package/src/index.mjs +13 -0
  18. package/src/lifecycle.mjs +322 -0
  19. package/src/rarebit-activity.mjs +48 -0
  20. package/src/rarebit-artifact-state.mjs +839 -0
  21. package/src/rarebit-cli.mjs +305 -0
  22. package/src/rarebit-command.mjs +281 -0
  23. package/src/rarebit-core.mjs +525 -0
  24. package/src/rarebit-model.mjs +226 -0
  25. package/src/rarebit-recall.mjs +182 -0
  26. package/src/rarebit-service.mjs +579 -0
  27. package/src/rarebit-session.mjs +227 -0
  28. package/src/rarebit-settings.mjs +79 -0
  29. package/src/rarebit-store.mjs +771 -0
  30. package/src/rarebit-visual-language.mjs +110 -0
  31. package/src/types.d.ts +601 -0
  32. package/test/automatic-summary-policy.test.mjs +476 -0
  33. package/test/extension-activity.test.mjs +73 -0
  34. package/test/fixtures/branch-mixed.json +117 -0
  35. package/test/fixtures/conformance-corpus.v1.json +306 -0
  36. package/test/fixtures/truncated-mixed.jsonl +2 -0
  37. package/test/herdr-activity.test.mjs +93 -0
  38. package/test/lifecycle-contract-independent.test.mjs +627 -0
  39. package/test/rarebit-activity.test.mjs +82 -0
  40. package/test/rarebit-artifact-state.test.mjs +440 -0
  41. package/test/rarebit-cli.test.mjs +222 -0
  42. package/test/rarebit-command.test.mjs +127 -0
  43. package/test/rarebit-core.test.mjs +461 -0
  44. package/test/rarebit-cross-consumer-conformance.test.mjs +150 -0
  45. package/test/rarebit-independent-contract.test.mjs +317 -0
  46. package/test/rarebit-recall.test.mjs +434 -0
  47. package/test/rarebit-sidecar-protocol.test.mjs +538 -0
  48. package/test/rarebit-visual-language.test.mjs +75 -0
  49. package/test/summary-attention-independent.test.mjs +172 -0
  50. package/test/types-history.ts +13 -0
@@ -0,0 +1,440 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import {
5
+ projectRarebitArtifactState,
6
+ validateRarebitArtifactReceipt,
7
+ } from "../src/rarebit-artifact-state.mjs";
8
+ import { sha256 } from "../src/rarebit-core.mjs";
9
+ import { extractRarebitSynthesisReceipt } from "../src/rarebit-model.mjs";
10
+
11
+ const hash = (char) => char.repeat(64);
12
+ const owner = {
13
+ occurrenceId: "owner:0",
14
+ sourceEntryId: "owner",
15
+ order: 0,
16
+ role: "user",
17
+ outcome: "user",
18
+ timestamp: "2026-07-25T10:00:00.000Z",
19
+ contentHash: hash("a"),
20
+ };
21
+ const continuation = {
22
+ occurrenceId: "continuation:1",
23
+ sourceEntryId: "continuation",
24
+ order: 1,
25
+ role: "assistant",
26
+ outcome: "continuation",
27
+ timestamp: "2026-07-25T10:01:00.000Z",
28
+ contentHash: hash("b"),
29
+ };
30
+ const stop = {
31
+ ...continuation,
32
+ occurrenceId: "stop:2",
33
+ sourceEntryId: "stop",
34
+ order: 2,
35
+ outcome: "stop",
36
+ contentHash: hash("c"),
37
+ };
38
+ const laterOwner = {
39
+ ...owner,
40
+ occurrenceId: "later:2",
41
+ sourceEntryId: "later",
42
+ order: 2,
43
+ contentHash: hash("d"),
44
+ };
45
+ const selection = (occurrences) => {
46
+ const selectorVersion = "rarebit-selector-v1";
47
+ const payloads = [
48
+ ...new Set(occurrences.map(({ contentHash }) => contentHash)),
49
+ ].map((contentHash) => ({
50
+ contentHash,
51
+ occurrenceIds: occurrences
52
+ .filter((occurrence) => occurrence.contentHash === contentHash)
53
+ .map((occurrence) => occurrence.occurrenceId),
54
+ }));
55
+ return {
56
+ manifestHash: sha256({ selectorVersion, occurrences, payloads }),
57
+ selectorVersion,
58
+ occurrenceCount: occurrences.length,
59
+ uniquePayloadCount: payloads.length,
60
+ latestUserSourceEntryId:
61
+ occurrences
62
+ .filter(
63
+ (occurrence) =>
64
+ occurrence.role === "user" && occurrence.outcome === "user",
65
+ )
66
+ .at(-1)?.sourceEntryId ?? null,
67
+ // Test-only native evidence remains available for prefix recomputation.
68
+ occurrences,
69
+ payloads,
70
+ };
71
+ };
72
+ function receipt({
73
+ sessionId = "session-1",
74
+ occurrences = [owner],
75
+ boundary = "owner_request",
76
+ observedAt = "2026-07-25T10:00:00.000Z",
77
+ status = "ok",
78
+ sessionStatus = boundary === "owner_request" ? "user_requested" : "finished",
79
+ statusReason = boundary === "owner_request"
80
+ ? "owner_request_recorded"
81
+ : "all_requests_accomplished",
82
+ summary = "This prose must never affect the state machine.",
83
+ } = {}) {
84
+ const selected = selection(occurrences);
85
+ const base = {
86
+ type: "rarebit_summary",
87
+ schemaVersion: 4,
88
+ status,
89
+ jobId: hash("e"),
90
+ sessionId,
91
+ branch: {
92
+ leafId: "leaf",
93
+ entryCount: occurrences.length,
94
+ pathHash: hash("f"),
95
+ },
96
+ implementationVersion: "hc-rarebit-summary-v4",
97
+ synthesisMode: "forced",
98
+ inputCoveragePolicy: {
99
+ strategy: "complete_or_explicit_overflow",
100
+ maxPromptChars: 1_000,
101
+ },
102
+ promptVersion: "rarebit-summary-v4",
103
+ model: { provider: "test", id: "model" },
104
+ modelProvenance: { source: "test", status: "resolved" },
105
+ lifecycleBoundary: boundary,
106
+ observedAt,
107
+ selection: {
108
+ manifestHash: selected.manifestHash,
109
+ selectorVersion: selected.selectorVersion,
110
+ occurrenceCount: selected.occurrenceCount,
111
+ uniquePayloadCount: selected.uniquePayloadCount,
112
+ latestUserSourceEntryId: selected.latestUserSourceEntryId,
113
+ },
114
+ };
115
+ if (status === "ok")
116
+ return {
117
+ ...base,
118
+ summary,
119
+ sessionStatus,
120
+ statusReason,
121
+ synthesis: extractRarebitSynthesisReceipt(
122
+ {},
123
+ {
124
+ requestedModel: base.model,
125
+ startedAt: observedAt,
126
+ completedAt: observedAt,
127
+ durationMs: 0,
128
+ },
129
+ ),
130
+ };
131
+ if (status === "failure")
132
+ return {
133
+ ...base,
134
+ retryable: true,
135
+ error: { name: "Error", message: "technical failure" },
136
+ };
137
+ if (status === "unavailable_overflow")
138
+ return {
139
+ ...base,
140
+ overflow: { promptChars: 2, maxPromptChars: 1, strategy: "none" },
141
+ };
142
+ return base;
143
+ }
144
+ const native = (occurrences, sessionId = "session-1") => ({
145
+ availability: "available",
146
+ sessionId,
147
+ selection: selection(occurrences),
148
+ });
149
+
150
+ test("artifact state documents awaiting, source-pending, current, settlement-pending, and terminal states", () => {
151
+ assert.equal(projectRarebitArtifactState().syncState, "awaiting_artifacts");
152
+ const requested = receipt();
153
+ const sourcePending = projectRarebitArtifactState({
154
+ native: { availability: "missing" },
155
+ materialization: { availability: "available", records: [requested] },
156
+ expectation: "owner_request",
157
+ });
158
+ assert.deepEqual(
159
+ {
160
+ state: sourcePending.syncState,
161
+ status: sourcePending.projection.status,
162
+ applicability: sourcePending.applicability,
163
+ },
164
+ {
165
+ state: "request_source_pending",
166
+ status: "user_requested",
167
+ applicability: "request_cut",
168
+ },
169
+ );
170
+ const current = projectRarebitArtifactState({
171
+ native: native([owner]),
172
+ materialization: { availability: "available", records: [requested] },
173
+ });
174
+ assert.equal(current.syncState, "request_current");
175
+ assert.equal(current.applicability, "request_generation");
176
+ const settled = receipt({
177
+ occurrences: [owner, continuation],
178
+ boundary: "agent_settled",
179
+ observedAt: "2026-07-25T10:02:00.000Z",
180
+ });
181
+ const assessment = projectRarebitArtifactState({
182
+ native: native([owner, continuation]),
183
+ materialization: {
184
+ availability: "available",
185
+ records: [requested, settled],
186
+ },
187
+ expectation: "agent_settled",
188
+ });
189
+ assert.equal(assessment.syncState, "assessment_current");
190
+ assert.equal(assessment.applicability, "exact_selection");
191
+ assert.equal(
192
+ projectRarebitArtifactState({
193
+ native: native([owner]),
194
+ materialization: { availability: "missing", records: [] },
195
+ deadlineExpired: true,
196
+ }).syncState,
197
+ "terminal_error",
198
+ );
199
+ });
200
+
201
+ test("sidecar-only selection accepts exactly one session ID and fails closed when native later conflicts", () => {
202
+ const requested = receipt();
203
+ assert.equal(
204
+ projectRarebitArtifactState({
205
+ native: { availability: "missing" },
206
+ materialization: { availability: "available", records: [requested] },
207
+ expectation: "owner_request",
208
+ }).syncState,
209
+ "request_source_pending",
210
+ );
211
+ const mismatch = projectRarebitArtifactState({
212
+ native: native([owner], "session-2"),
213
+ materialization: { availability: "available", records: [requested] },
214
+ });
215
+ assert.equal(mismatch.projection.reason, "session_conflict");
216
+ const multiple = projectRarebitArtifactState({
217
+ native: { availability: "missing" },
218
+ materialization: {
219
+ availability: "available",
220
+ records: [requested, receipt({ sessionId: "session-2" })],
221
+ },
222
+ });
223
+ assert.equal(multiple.projection.reason, "session_conflict");
224
+ });
225
+
226
+ test("request generation survives assistant continuations but a later user invalidates it", () => {
227
+ const requested = receipt({ occurrences: [owner] });
228
+ const continuationState = projectRarebitArtifactState({
229
+ native: native([owner, continuation]),
230
+ materialization: { availability: "available", records: [requested] },
231
+ });
232
+ assert.equal(continuationState.syncState, "request_current");
233
+ assert.equal(continuationState.projection.status, "user_requested");
234
+ const superseded = projectRarebitArtifactState({
235
+ native: native([owner, continuation, laterOwner]),
236
+ materialization: { availability: "available", records: [requested] },
237
+ });
238
+ assert.equal(superseded.syncState, "awaiting_artifacts");
239
+ assert.equal(superseded.projection, null);
240
+ });
241
+
242
+ test("v4 receipts reject native selection arrays and arbitrary extension fields", () => {
243
+ assert.equal(
244
+ validateRarebitArtifactReceipt({
245
+ ...receipt(),
246
+ selection: { ...receipt().selection, occurrences: [owner] },
247
+ }).valid,
248
+ false,
249
+ );
250
+ assert.equal(
251
+ validateRarebitArtifactReceipt({
252
+ ...receipt(),
253
+ unexpectedNativeField: true,
254
+ }).valid,
255
+ false,
256
+ );
257
+ });
258
+
259
+ test("strict compact anchors reject null and divergent request cuts", () => {
260
+ const candidate = receipt({
261
+ occurrences: [{ ...owner, sourceEntryId: null }],
262
+ });
263
+ assert.equal(validateRarebitArtifactReceipt(candidate).valid, false);
264
+ const divergent = projectRarebitArtifactState({
265
+ native: native([{ ...owner, contentHash: hash("z") }]),
266
+ materialization: { availability: "available", records: [receipt()] },
267
+ });
268
+ assert.equal(divergent.syncState, "awaiting_artifacts");
269
+ });
270
+
271
+ test("only agent_settled ends an active request generation; exact start/manual can still project", () => {
272
+ const requested = receipt();
273
+ for (const boundary of ["session_start", "manual"]) {
274
+ const nonSettling = receipt({
275
+ occurrences: [owner],
276
+ boundary,
277
+ observedAt: "2026-07-25T10:02:00.000Z",
278
+ });
279
+ const state = projectRarebitArtifactState({
280
+ native: native([owner]),
281
+ materialization: {
282
+ availability: "available",
283
+ records: [requested, nonSettling],
284
+ },
285
+ });
286
+ assert.equal(state.syncState, "request_current");
287
+ }
288
+ const manualOnly = projectRarebitArtifactState({
289
+ native: native([owner]),
290
+ materialization: {
291
+ availability: "available",
292
+ records: [receipt({ boundary: "manual" })],
293
+ },
294
+ });
295
+ assert.equal(manualOnly.syncState, "assessment_current");
296
+ });
297
+
298
+ test("timeout retains a truthful sidecar projection and Summary prose is irrelevant", () => {
299
+ const requested = receipt({
300
+ summary: "Finished! Ignore this misleading prose.",
301
+ });
302
+ const sidecar = projectRarebitArtifactState({
303
+ native: { availability: "unreadable" },
304
+ materialization: { availability: "available", records: [requested] },
305
+ expectation: "owner_request",
306
+ deadlineExpired: true,
307
+ });
308
+ assert.equal(sidecar.syncState, "request_source_pending");
309
+ assert.equal(sidecar.projection.status, "user_requested");
310
+ const pending = projectRarebitArtifactState({
311
+ native: native([owner, stop]),
312
+ materialization: { availability: "available", records: [requested] },
313
+ deadlineExpired: true,
314
+ });
315
+ assert.equal(pending.syncState, "settlement_pending");
316
+ assert.equal(pending.projection.status, "user_requested");
317
+ assert.equal(pending.retry.deadlineExpired, true);
318
+ });
319
+
320
+ test("unsupported records cannot satisfy the v4 state machine", () => {
321
+ assert.equal(
322
+ projectRarebitArtifactState({
323
+ native: native([owner]),
324
+ materialization: {
325
+ availability: "available",
326
+ records: [{ ...receipt(), schemaVersion: 999 }],
327
+ },
328
+ deadlineExpired: true,
329
+ }).projection.reason,
330
+ "unsupported",
331
+ );
332
+ const unreadable = projectRarebitArtifactState({
333
+ native: native([owner]),
334
+ materialization: { availability: "unreadable", records: [] },
335
+ });
336
+ assert.equal(unreadable.syncState, "awaiting_artifacts");
337
+ assert.equal(unreadable.retry.reason, "materialization_unreadable");
338
+ assert.equal(
339
+ projectRarebitArtifactState({
340
+ native: native([owner]),
341
+ materialization: { availability: "unreadable", records: [] },
342
+ deadlineExpired: true,
343
+ }).projection.reason,
344
+ "materialization_unreadable",
345
+ );
346
+ });
347
+
348
+ test("request prefix requires selector semantics and complete occurrence identity and position", () => {
349
+ for (const nativeOccurrence of [
350
+ { ...owner, occurrenceId: "owner:99" },
351
+ { ...owner, order: 99 },
352
+ ]) {
353
+ const state = projectRarebitArtifactState({
354
+ native: native([nativeOccurrence]),
355
+ materialization: { availability: "available", records: [receipt()] },
356
+ });
357
+ assert.equal(state.syncState, "awaiting_artifacts");
358
+ }
359
+ const selectorMismatch = projectRarebitArtifactState({
360
+ native: {
361
+ ...native([owner]),
362
+ selection: { ...selection([owner]), selectorVersion: "other-selector" },
363
+ },
364
+ materialization: { availability: "available", records: [receipt()] },
365
+ });
366
+ assert.equal(selectorMismatch.syncState, "awaiting_artifacts");
367
+ const settledSelectorMismatch = projectRarebitArtifactState({
368
+ native: {
369
+ ...native([owner]),
370
+ selection: { ...selection([owner]), selectorVersion: "other-selector" },
371
+ },
372
+ materialization: {
373
+ availability: "available",
374
+ records: [receipt({ boundary: "agent_settled" })],
375
+ },
376
+ });
377
+ assert.equal(settledSelectorMismatch.syncState, "awaiting_artifacts");
378
+ });
379
+
380
+ test("sidecar-only expectations reject an older lifecycle generation", () => {
381
+ const olderOwner = receipt({ observedAt: "2026-07-25T10:00:00.000Z" });
382
+ const newerSettlement = receipt({
383
+ occurrences: [owner, continuation],
384
+ boundary: "agent_settled",
385
+ observedAt: "2026-07-25T10:01:00.000Z",
386
+ });
387
+ const olderSettlement = receipt({
388
+ occurrences: [owner, continuation],
389
+ boundary: "agent_settled",
390
+ observedAt: "2026-07-25T10:00:00.000Z",
391
+ });
392
+ const newerOwner = receipt({ observedAt: "2026-07-25T10:01:00.000Z" });
393
+ for (const [expectation, records] of [
394
+ ["owner_request", [olderOwner, newerSettlement]],
395
+ ["agent_settled", [olderSettlement, newerOwner]],
396
+ ]) {
397
+ const state = projectRarebitArtifactState({
398
+ native: { availability: "missing" },
399
+ materialization: { availability: "available", records },
400
+ expectation,
401
+ });
402
+ assert.equal(state.syncState, "awaiting_artifacts");
403
+ }
404
+ const equalCut = projectRarebitArtifactState({
405
+ native: native([owner]),
406
+ materialization: {
407
+ availability: "available",
408
+ records: [
409
+ receipt({ boundary: "agent_settled" }),
410
+ receipt({ boundary: "owner_request" }),
411
+ ],
412
+ },
413
+ });
414
+ assert.equal(equalCut.syncState, "request_current");
415
+ });
416
+
417
+ test("artifact refs retain the complete declared assessment lineage", () => {
418
+ const record = receipt({
419
+ boundary: "agent_settled",
420
+ occurrences: [owner],
421
+ observedAt: "2026-07-25T10:02:00.000Z",
422
+ });
423
+ const state = projectRarebitArtifactState({
424
+ native: native([owner]),
425
+ materialization: { availability: "available", records: [record] },
426
+ });
427
+ assert.deepEqual(state.receiptRef, {
428
+ jobId: hash("e"),
429
+ sessionId: "session-1",
430
+ branchLeafId: "leaf",
431
+ selectionManifestHash: record.selection.manifestHash,
432
+ selectorVersion: "rarebit-selector-v1",
433
+ lifecycleBoundary: "agent_settled",
434
+ promptVersion: "rarebit-summary-v4",
435
+ model: { provider: "test", id: "model" },
436
+ observedAt: "2026-07-25T10:02:00.000Z",
437
+ schemaVersion: 4,
438
+ implementationVersion: "hc-rarebit-summary-v4",
439
+ });
440
+ });
@@ -0,0 +1,222 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import {
8
+ parseRarebitCliArgs,
9
+ readRarebitCliSettings,
10
+ runRarebitCli,
11
+ } from "../src/rarebit-cli.mjs";
12
+ import { readConfiguredRarebitSettings } from "../src/extension.mjs";
13
+
14
+ const header = {
15
+ type: "session",
16
+ version: 3,
17
+ id: "rarebit-cli",
18
+ timestamp: "2026-07-16T00:00:00.000Z",
19
+ };
20
+ const user = {
21
+ type: "message",
22
+ id: "u",
23
+ parentId: null,
24
+ timestamp: "2026-07-16T00:00:01.000Z",
25
+ producer: "owner-device",
26
+ message: { role: "user", content: "Find the slow path" },
27
+ };
28
+ const tool = {
29
+ type: "message",
30
+ id: "tool",
31
+ parentId: "u",
32
+ timestamp: "2026-07-16T00:00:02.000Z",
33
+ message: {
34
+ role: "toolResult",
35
+ content: [{ type: "text", text: "verbose tool output" }],
36
+ },
37
+ };
38
+ const stop = {
39
+ type: "message",
40
+ id: "a",
41
+ parentId: "tool",
42
+ timestamp: "2026-07-16T00:00:03.000Z",
43
+ message: {
44
+ role: "assistant",
45
+ stopReason: "stop",
46
+ content: "Found the slow path",
47
+ },
48
+ };
49
+
50
+ async function fixture() {
51
+ const root = await mkdtemp(join(tmpdir(), "hc-rarebit-"));
52
+ const project = join(root, "project");
53
+ await mkdir(project);
54
+ const file = join(project, "session.jsonl");
55
+ await writeFile(
56
+ file,
57
+ [header, user, tool, stop].map(JSON.stringify).join("\n"),
58
+ );
59
+ return file;
60
+ }
61
+
62
+ test("query hides raw Rarebit prose while extract returns the deterministic raw evidence", async () => {
63
+ const file = await fixture();
64
+ const query = await runRarebitCli({ command: "query", session: file });
65
+ assert.equal(query.rarebitCount, 2);
66
+ assert.equal(JSON.stringify(query).includes("Find the slow path"), false);
67
+ assert.equal(JSON.stringify(query).includes("producer"), false);
68
+ const extract = await runRarebitCli({ command: "extract", session: file });
69
+ assert.deepEqual(
70
+ extract.rarebits.map(({ text }) => text),
71
+ ["Find the slow path", "Found the slow path"],
72
+ );
73
+ assert.equal(JSON.stringify(extract).includes("producer"), false);
74
+ });
75
+
76
+ test("on-demand summary is policy-gated unless a caller explicitly forces it", async () => {
77
+ const file = await fixture();
78
+ const calls = [];
79
+ const result = await runRarebitCli(
80
+ {
81
+ command: "summarize",
82
+ session: file,
83
+ modelArgs: [],
84
+ force: false,
85
+ },
86
+ {
87
+ readSettings: async () => ({
88
+ model: { provider: "test", id: "cheap" },
89
+ summaryPolicy: {},
90
+ }),
91
+ processSummary: async (_ctx, config) => {
92
+ calls.push(config);
93
+ return {
94
+ record: {
95
+ status: config.forceSynthesis ? "ok" : "ineligible",
96
+ summary: "fake",
97
+ },
98
+ };
99
+ },
100
+ },
101
+ );
102
+ assert.equal(result.status, "ineligible");
103
+ assert.equal(calls.length, 1);
104
+ const forced = await runRarebitCli(
105
+ {
106
+ command: "summarize",
107
+ session: file,
108
+ modelArgs: [],
109
+ force: true,
110
+ },
111
+ {
112
+ readSettings: async () => ({
113
+ model: { provider: "test", id: "cheap" },
114
+ summaryPolicy: {},
115
+ }),
116
+ processSummary: async (_ctx, config) => {
117
+ calls.push(config);
118
+ return {
119
+ record: {
120
+ status: config.forceSynthesis ? "ok" : "ineligible",
121
+ summary: "fake",
122
+ },
123
+ };
124
+ },
125
+ },
126
+ );
127
+ assert.equal(forced.status, "ok");
128
+ assert.equal(calls.length, 2);
129
+ assert.equal(calls[1].forceSynthesis, true);
130
+ });
131
+
132
+ test("title generates only a proposal with an explicit date prefix and never mutates the Session", async () => {
133
+ const file = await fixture();
134
+ const result = await runRarebitCli(
135
+ {
136
+ command: "title",
137
+ session: file,
138
+ modelArgs: [],
139
+ date: "2026-07-16",
140
+ },
141
+ {
142
+ readSettings: async () => ({
143
+ model: { provider: "test", id: "cheap" },
144
+ summaryPolicy: {},
145
+ }),
146
+ runPiModel: async () => "Title: Slow-path investigation",
147
+ rarebitRoot: join(file, "..", "rarebit-test-state"),
148
+ },
149
+ );
150
+ assert.equal(result.status, "proposal");
151
+ assert.equal(result.title, "20260716-Slow-path investigation");
152
+ });
153
+
154
+ test("binary requires JSON and query never leaks an absolute source path", async () => {
155
+ const file = await fixture();
156
+ const cli = new URL("../bin/rarebit.mjs", import.meta.url);
157
+ const output = spawnSync(
158
+ process.execPath,
159
+ [cli.pathname, "query", "--session", file, "--json"],
160
+ { encoding: "utf8" },
161
+ );
162
+ assert.equal(output.status, 0, output.stderr);
163
+ const help = spawnSync(process.execPath, [cli.pathname, "--help"], {
164
+ encoding: "utf8",
165
+ });
166
+ assert.equal(help.status, 0, help.stderr);
167
+ assert.match(help.stdout, /rarebit query/);
168
+ assert.doesNotMatch(help.stdout, /hc-rarebit/);
169
+ assert.equal(output.stdout.includes(file), false);
170
+ assert.equal(JSON.parse(output.stdout).rarebitCount, 2);
171
+ assert.throws(
172
+ () => parseRarebitCliArgs(["query", "--session", file]),
173
+ /--json is required/,
174
+ );
175
+ });
176
+
177
+ test("CLI and Pi extension resolve nested Rarebit settings identically", async () => {
178
+ const root = await mkdtemp(join(tmpdir(), "hc-rarebit-settings-"));
179
+ const agentDir = join(root, "agent");
180
+ const project = join(root, "project");
181
+ await mkdir(join(project, ".pi"), { recursive: true });
182
+ await mkdir(agentDir, { recursive: true });
183
+ await writeFile(
184
+ join(agentDir, "settings.json"),
185
+ JSON.stringify({
186
+ rarebit: {
187
+ model: "test-provider/global-cheap",
188
+ min_total_length: 80_000,
189
+ auto_title: true,
190
+ },
191
+ }),
192
+ );
193
+ await writeFile(
194
+ join(project, ".pi", "settings.json"),
195
+ JSON.stringify({
196
+ rarebit: {
197
+ min_total_length: 12_345,
198
+ max_rarebit_ratio: 0.25,
199
+ auto_title: false,
200
+ },
201
+ }),
202
+ );
203
+
204
+ const [cli, extension] = await Promise.all([
205
+ readRarebitCliSettings({ agentDir, cwd: project }),
206
+ readConfiguredRarebitSettings({
207
+ agentDir,
208
+ cwd: project,
209
+ projectTrusted: true,
210
+ }),
211
+ ]);
212
+ assert.deepEqual(cli.model, extension.model);
213
+ assert.deepEqual(cli.summaryPolicy, extension.summaryPolicy);
214
+ assert.equal(cli.autoTitle, extension.autoTitle);
215
+ assert.deepEqual(cli.model, {
216
+ provider: "test-provider",
217
+ id: "global-cheap",
218
+ });
219
+ assert.equal(cli.summaryPolicy.minTotalLength, 12_345);
220
+ assert.equal(cli.summaryPolicy.maxRarebitRatio, 0.25);
221
+ assert.equal(cli.autoTitle, false);
222
+ });