hafez 2.1.0 → 2.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.
@@ -9,6 +9,11 @@ import type { Mode } from '../cli/install.js';
9
9
  export type SurfaceId = 'block' | 'hafezSkillDescription' | 'hafezSkillBody' | 'hafezSaveDescription' | 'hafezSaveBody' | 'read' | 'readHeader' | 'readFooter' | 'readMiss' | 'updateSuccess' | 'updateMisuse' | 'createKnowledge' | 'digestError' | 'batchMisuse' | 'mechanised' | 'schema' | 'onboard';
10
10
  /** Places another Item inside this one's text, counted against the same Surface. */
11
11
  export type Place = (id: string) => string;
12
+ /** What a static Surface is composed for. `vaultRepo`: the Block for a cloud
13
+ * container, whose vault is that GitHub repo (#160). */
14
+ export interface SurfaceCtx {
15
+ vaultRepo?: string;
16
+ }
12
17
  export interface Item {
13
18
  /** The Item's row text in the owner table. */
14
19
  label: string;
@@ -21,7 +26,7 @@ export interface Item {
21
26
  evidence: string;
22
27
  /** Static text. A function form may place nested Items, and may give the
23
28
  * fallback a shorter form than the Owner. */
24
- text?: string | ((place: Place, at: 'owner' | 'fallback') => string);
29
+ text?: string | ((place: Place, at: 'owner' | 'fallback', ctx: SurfaceCtx) => string);
25
30
  /** What the binary computes: for Items owned by command output, and for a
26
31
  * static Item a binary Surface places filled in (the create line in the
27
32
  * `read` miss). */
@@ -94,6 +99,19 @@ declare function renderUpdateSummary({ slug, fields, result }: {
94
99
  /** The suffix a Stale open line carries on `read`: its age, or that it is
95
100
  * undated. A fresh line carries none. */
96
101
  declare function renderStaleMarker({ stale, age_days }: ActionAge): string;
102
+ /** The `read` header line naming the sessions that touched an entity, with
103
+ * the look back from it. None: no line. */
104
+ declare function renderSessionsLine({ slug, count, latest }: {
105
+ slug: string;
106
+ count: number;
107
+ latest: string | null;
108
+ }): string;
109
+ /** The `read` header line naming an entity's archived log entries, with the
110
+ * command that reads them. No archive: no line. */
111
+ declare function renderArchivePointer({ slug, entries }: {
112
+ slug: string;
113
+ entries: number;
114
+ }): string;
97
115
  /** The line after an update's success while Stale lines remain on the entity. */
98
116
  declare function renderStaleReminder({ handles }: {
99
117
  handles: string[];
@@ -129,6 +147,15 @@ declare function renderTeachingLines({ harness, mode, line2Only }: {
129
147
  mode: Mode;
130
148
  line2Only?: boolean;
131
149
  }): string;
150
+ /** A GitHub `<owner>/<repo>`, as one shell word safe to print in a command
151
+ * and to name a directory by: not `.` or `..`, not a clone URL's `.git`. */
152
+ export declare const isVaultRepo: (value: string) => boolean;
153
+ /** `onboard --cloud`: the cloud environment's setup script. It cannot clone
154
+ * (it runs without GitHub credentials) and no step may fail session start: a
155
+ * failed install exits 0, leaving a session without Hafez rather than none. */
156
+ declare function renderCloudSetup({ vaultRepo }: {
157
+ vaultRepo: string;
158
+ }): string;
132
159
  export declare const ITEMS: {
133
160
  loadTriggers: {
134
161
  label: string;
@@ -159,6 +186,20 @@ export declare const ITEMS: {
159
186
  description: string | null;
160
187
  }) => string;
161
188
  };
189
+ sessionsLine: {
190
+ label: string;
191
+ owner: "readHeader";
192
+ invokeOnly: string;
193
+ evidence: string;
194
+ render: typeof renderSessionsLine;
195
+ };
196
+ archivePointer: {
197
+ label: string;
198
+ owner: "readHeader";
199
+ invokeOnly: string;
200
+ evidence: string;
201
+ render: typeof renderArchivePointer;
202
+ };
162
203
  writeTriggers: {
163
204
  label: string;
164
205
  owner: "block";
@@ -354,6 +395,20 @@ export declare const ITEMS: {
354
395
  evidence: string;
355
396
  render: typeof renderHarnessPermissions;
356
397
  };
398
+ cloudVault: {
399
+ label: string;
400
+ owner: "block";
401
+ invokeOnly: string;
402
+ evidence: string;
403
+ text: (_place: Place, _at: "owner" | "fallback", { vaultRepo }: SurfaceCtx) => string;
404
+ };
405
+ cloudSetup: {
406
+ label: string;
407
+ owner: "onboard";
408
+ invokeOnly: string;
409
+ evidence: string;
410
+ render: typeof renderCloudSetup;
411
+ };
357
412
  teachingLines: {
358
413
  label: string;
359
414
  owner: "onboard";
@@ -162,6 +162,20 @@ function renderStaleMarker({ stale, age_days }) {
162
162
  return '';
163
163
  return age_days === null ? ' · stale, undated' : ` · stale ${age_days}d`;
164
164
  }
165
+ /** The `read` header line naming the sessions that touched an entity, with
166
+ * the look back from it. None: no line. */
167
+ function renderSessionsLine({ slug, count, latest }) {
168
+ if (count === 0)
169
+ return '';
170
+ return `sessions: ${count}${latest ? ` · latest ${latest}` : ''} · hafez query --kind session --related-to ${slug}`;
171
+ }
172
+ /** The `read` header line naming an entity's archived log entries, with the
173
+ * command that reads them. No archive: no line. */
174
+ function renderArchivePointer({ slug, entries }) {
175
+ if (entries === 0)
176
+ return '';
177
+ return `archive: ${entries} older log ${entries === 1 ? 'entry' : 'entries'} · hafez read ${slug} --archive`;
178
+ }
165
179
  /** The line after an update's success while Stale lines remain on the entity. */
166
180
  function renderStaleReminder({ handles }) {
167
181
  const n = handles.length;
@@ -221,6 +235,38 @@ function renderTeachingLines({ harness, mode, line2Only = false }) {
221
235
  '4. If a hafez command ever asks for permission, setup is unfinished: run `hafez onboard` again.',
222
236
  ].join('\n');
223
237
  }
238
+ /** A GitHub `<owner>/<repo>`, as one shell word safe to print in a command
239
+ * and to name a directory by: not `.` or `..`, not a clone URL's `.git`. */
240
+ export const isVaultRepo = (value) => /^[A-Za-z0-9-]+\/[\w.-]+$/.test(value) && !/\/\.\.?$/.test(value) && !value.endsWith('.git');
241
+ /** Where a cloud container clones a repo attached to the session. */
242
+ const CLOUD_HOME = '/home/user';
243
+ const repoName = (vaultRepo) => vaultRepo.slice(vaultRepo.indexOf('/') + 1);
244
+ /** The Block's self-heal rule in a cloud container: a session started without
245
+ * the vault attached attaches, clones and registers it (verified, #160). */
246
+ function renderCloudVault(vaultRepo) {
247
+ const path = `${CLOUD_HOME}/${repoName(vaultRepo)}`;
248
+ return `If \`hafez init\` shows no registered vault: attach \`${vaultRepo}\` to this session (add_repo, push access), clone it to ${path}, run \`hafez init --register ${path}\`, then continue. Never create a throwaway vault.`;
249
+ }
250
+ /** `onboard --cloud`: the cloud environment's setup script. It cannot clone
251
+ * (it runs without GitHub credentials) and no step may fail session start: a
252
+ * failed install exits 0, leaving a session without Hafez rather than none. */
253
+ function renderCloudSetup({ vaultRepo }) {
254
+ return [
255
+ `Each Claude Code cloud session is a fresh container: no \`hafez\`, no vault, no Block. Paste this into the cloud environment's Setup script; the Trusted network policy is enough.`,
256
+ '',
257
+ fence([
258
+ '#!/bin/bash',
259
+ 'npm install -g hafez --allow-scripts=better-sqlite3 || exit 0',
260
+ `VAULT=${CLOUD_HOME}/${repoName(vaultRepo)}`,
261
+ 'if [ -d "$VAULT/.git" ]; then',
262
+ ' hafez init --register "$VAULT"',
263
+ 'fi',
264
+ `hafez onboard --install --mode always-on --harness claude-code --vault-repo ${vaultRepo} || true`,
265
+ ].join('\n')),
266
+ '',
267
+ `Attach ${vaultRepo} when you start a session, with push access, and the script registers it. The script cannot clone it: it runs without GitHub credentials. A session started without it attached is told by the Block to attach, clone and register it.`,
268
+ ].join('\n');
269
+ }
224
270
  const BATCH_EXAMPLE_OP = '{"op":"create","kind":"entity","name":"Token Rotation","fields":{"type":"entity"}}';
225
271
  export const ITEMS = {
226
272
  loadTriggers: {
@@ -259,13 +305,28 @@ export const ITEMS = {
259
305
  * in brackets when it has one. */
260
306
  render: ({ slug, description }) => description ? `${slug} (${description})` : slug,
261
307
  },
308
+ sessionsLine: {
309
+ label: 'Sessions line: the look back from an entity',
310
+ owner: 'readHeader',
311
+ invokeOnly: 'same',
312
+ evidence: '#35',
313
+ render: renderSessionsLine,
314
+ },
315
+ archivePointer: {
316
+ label: 'Archive pointer',
317
+ owner: 'readHeader',
318
+ invokeOnly: 'same',
319
+ evidence: '#34',
320
+ render: renderArchivePointer,
321
+ },
262
322
  writeTriggers: {
263
- label: 'Write triggers: work landed, insight, weighing, "note this"',
323
+ label: 'Write triggers: work landed, decision made, insight, weighing, "note this"',
264
324
  owner: 'block',
265
325
  invokeOnly: 'absent',
266
- evidence: 'M11, M2, never-ask decision #42',
326
+ evidence: 'M11, M2, never-ask decision #42, decision-record miss #159',
267
327
  text: place => [
268
- '- Work landed (a commit, a merged PR, CI green): update the entity you read. The end of its `read` output shows the command.',
328
+ '- Work landed (a commit, a merged PR, CI green): update the entity that owns the work. That is the entity you read, unless the work belongs to another: read that one first. The end of its `read` output shows the command.',
329
+ '- A decision was made: if the repo keeps its own decisions record, write it there now and give Hafez one line that points at it (`hafez update <slug> --log "decision: <one line>, see decisions.md"`). Otherwise it goes in the save\'s `decisions`.',
269
330
  `- An insight emerged that is useful beyond this repo: create a knowledge note. ${place('knowledgeBoundary')}`,
270
331
  '- The user says "note this" or "record that": write it.',
271
332
  `- You are weighing whether something is worth writing: write it, as knowledge with \`--confidence ${CONFIDENCE_LEVELS[0]}\`.`,
@@ -438,9 +499,11 @@ export const ITEMS = {
438
499
  evidence: 'help decision #49',
439
500
  text: fence([
440
501
  `hafez query --filter ${oneOf([...ENTITY_STATUSES, 'stale', 'capture'])}`,
441
- 'hafez search "<terms>" --kind <entity|knowledge>',
502
+ 'hafez search "<terms>" --kind <entity|knowledge|session|all>',
503
+ 'hafez query --kind session --related-to <slug>',
442
504
  'hafez stats',
443
- ].join('\n')),
505
+ ].join('\n'))
506
+ + '\n\n`search` and `query` reach entities and knowledge notes; `--kind all` adds the session notes, `--kind session` lists them alone, newest first. `hafez read <slug> --archive` prints an entity\'s archived log entries.',
444
507
  },
445
508
  rareOps: {
446
509
  label: 'Rare ops: promote, link, unlink, status, delete, validate',
@@ -517,6 +580,22 @@ export const ITEMS = {
517
580
  evidence: 'never-ask decision #42; onboard decision #58',
518
581
  render: renderHarnessPermissions,
519
582
  },
583
+ cloudVault: {
584
+ label: 'Cloud session: no vault registered, attach the vault repo, clone, register',
585
+ owner: 'block',
586
+ invokeOnly: 'absent',
587
+ evidence: '#160, verified 2026-10-16',
588
+ // Placed only in a Block composed for a cloud container, whose begin
589
+ // marker names the repo (renderBlock).
590
+ text: (_place, _at, { vaultRepo = '<owner>/<repo>' }) => renderCloudVault(vaultRepo),
591
+ },
592
+ cloudSetup: {
593
+ label: 'The cloud environment\'s setup script',
594
+ owner: 'onboard',
595
+ invokeOnly: 'not offered',
596
+ evidence: '#160, verified 2026-10-16',
597
+ render: renderCloudSetup,
598
+ },
520
599
  teachingLines: {
521
600
  label: 'The four teaching lines (to the user, not the agent)',
522
601
  owner: 'onboard',
@@ -1,16 +1,19 @@
1
- import { type FolderMatch, type Item, type ItemId, type SurfaceId } from './items.js';
1
+ import { type FolderMatch, type Item, type ItemId, type SurfaceCtx, type SurfaceId } from './items.js';
2
2
  export interface Surface<Id extends string = ItemId> {
3
3
  /** How the owner table names this Surface. */
4
4
  label: string;
5
5
  /** Static Surfaces only: the text, built by placing Items. */
6
- compose?: (place: (id: Id) => string) => string;
6
+ compose?: (place: (id: Id) => string, ctx: SurfaceCtx) => string;
7
7
  }
8
8
  export declare const SURFACES: Record<SurfaceId, Surface>;
9
9
  export declare const contentHash: (text: string) => string;
10
10
  /** An Item's text as a binary Surface places it at run time, in the form its
11
11
  * role there (Owner or fallback) calls for. */
12
12
  export declare function placeIn(surfaceId: SurfaceId, id: ItemId): string;
13
- export declare function renderBlock(): string;
13
+ /** The Block; with `vaultRepo`, the Block for a cloud container whose vault
14
+ * is that repo. Its begin marker names the repo, so a check reads it from
15
+ * there and never from the Block's wording, which a release may change. */
16
+ export declare function renderBlock(vaultRepo?: string): string;
14
17
  /** `onboard --block`, what the SessionStart hook prints: the Block, then the
15
18
  * read line for each entity matching the folder. */
16
19
  export declare function renderBlockHook(matches: FolderMatch[]): string;
@@ -26,7 +29,7 @@ export declare function renderOwnerTable(): string;
26
29
  /** Replace the marked owner-table region of `doc` with a fresh render. */
27
30
  export declare function spliceOwnerTable(doc: string): string;
28
31
  /** Every problem with the Item table: an Item with no Owner or no evidence
29
- * pointer, an Item its Owner or static fallback never places, or one placed more than once in a
30
- * Surface or in a Surface that is neither its Owner nor its declared fallback.
32
+ * pointer, an Item its Owner or static fallback never places in any context, or one placed more
33
+ * than once in a Surface or in a Surface that is neither its Owner nor its declared fallback.
31
34
  * Placement is checked for static Surfaces only; binary ones place at run time. */
32
35
  export declare function itemProblems(items: Record<string, Item>, surfaces: Record<string, Surface<string>>): string[];
@@ -13,11 +13,12 @@ import { ITEMS } from './items.js';
13
13
  export const SURFACES = {
14
14
  block: {
15
15
  label: 'Block',
16
- compose: place => [
16
+ compose: (place, { vaultRepo }) => [
17
17
  '## Hafez',
18
18
  '',
19
19
  'Hafez is this user\'s vault: what they are working on (entities) and what they have learned (knowledge). You reach it only through the `hafez` command.',
20
20
  '',
21
+ ...(vaultRepo === undefined ? [] : [`**Cloud session.** ${place('cloudVault')}`, '']),
21
22
  `**Load.** ${place('loadTriggers')}`,
22
23
  '',
23
24
  '**Write as it happens.** Each of these is due the moment it occurs:',
@@ -96,11 +97,14 @@ export const SURFACES = {
96
97
  schema: { label: '`schema`' },
97
98
  onboard: { label: '`onboard`' },
98
99
  };
100
+ /** Every context a static Surface is composed in: the default, and the
101
+ * Block for a cloud container. */
102
+ const CONTEXTS = [{}, { vaultRepo: '<owner>/<repo>' }];
99
103
  export const contentHash = (text) => createHash('sha256').update(text).digest('hex').slice(0, 12);
100
104
  /** Compose a static Surface, placing each Item's text in the form its role
101
105
  * (Owner or fallback) calls for. `onPlace` sees every placement, nested ones
102
106
  * included. */
103
- function composeWith(items, surfaceId, surface, onPlace = () => { }) {
107
+ function composeWith(items, surfaceId, surface, ctx = {}, onPlace = () => { }) {
104
108
  const place = (id) => {
105
109
  onPlace(id);
106
110
  const item = items[id];
@@ -108,16 +112,16 @@ function composeWith(items, surfaceId, surface, onPlace = () => { }) {
108
112
  throw new Error(`${surfaceId} places unknown Item ${id}`);
109
113
  const text = item.text;
110
114
  if (typeof text === 'function')
111
- return text(place, item.owner === surfaceId ? 'owner' : 'fallback');
115
+ return text(place, item.owner === surfaceId ? 'owner' : 'fallback', ctx);
112
116
  if (text === undefined)
113
117
  throw new Error(`${surfaceId} places ${id}, which has no text`);
114
118
  return text;
115
119
  };
116
120
  if (!surface.compose)
117
121
  throw new Error(`${surfaceId} is not a static surface`);
118
- return surface.compose(place);
122
+ return surface.compose(place, ctx);
119
123
  }
120
- const compose = (id) => composeWith(ITEMS, id, SURFACES[id]);
124
+ const compose = (id, ctx = {}) => composeWith(ITEMS, id, SURFACES[id], ctx);
121
125
  /** An Item's text as a binary Surface places it at run time, in the form its
122
126
  * role there (Owner or fallback) calls for. */
123
127
  export function placeIn(surfaceId, id) {
@@ -147,9 +151,13 @@ function skillFile(name, description, body) {
147
151
  const text = compose(body);
148
152
  return `---\nname: ${name}\ndescription: >\n${fold(compose(description))}\nhash: ${contentHash(text)}\n---\n${text}`;
149
153
  }
150
- export function renderBlock() {
151
- const inner = compose('block');
152
- return `<!-- hafez:begin ${contentHash(inner)} -->\n${inner}<!-- hafez:end -->\n`;
154
+ /** The Block; with `vaultRepo`, the Block for a cloud container whose vault
155
+ * is that repo. Its begin marker names the repo, so a check reads it from
156
+ * there and never from the Block's wording, which a release may change. */
157
+ export function renderBlock(vaultRepo) {
158
+ const inner = compose('block', { vaultRepo });
159
+ const repo = vaultRepo === undefined ? '' : ` vault-repo=${vaultRepo}`;
160
+ return `<!-- hafez:begin ${contentHash(inner)}${repo} -->\n${inner}<!-- hafez:end -->\n`;
153
161
  }
154
162
  /** `onboard --block`, what the SessionStart hook prints: the Block, then the
155
163
  * read line for each entity matching the folder. */
@@ -185,21 +193,28 @@ export function spliceOwnerTable(doc) {
185
193
  }
186
194
  // --- Drift check 2: the typed Item table ---
187
195
  /** Every problem with the Item table: an Item with no Owner or no evidence
188
- * pointer, an Item its Owner or static fallback never places, or one placed more than once in a
189
- * Surface or in a Surface that is neither its Owner nor its declared fallback.
196
+ * pointer, an Item its Owner or static fallback never places in any context, or one placed more
197
+ * than once in a Surface or in a Surface that is neither its Owner nor its declared fallback.
190
198
  * Placement is checked for static Surfaces only; binary ones place at run time. */
191
199
  export function itemProblems(items, surfaces) {
192
200
  const problems = [];
201
+ // Per Surface, the most times each Item is placed in one of its contexts.
193
202
  const placed = {};
194
203
  for (const [sid, surface] of Object.entries(surfaces)) {
195
204
  if (!surface.compose)
196
205
  continue;
197
- const counts = (placed[sid] = {});
198
- try {
199
- composeWith(items, sid, surface, id => { counts[id] = (counts[id] ?? 0) + 1; });
200
- }
201
- catch (err) {
202
- problems.push(err.message);
206
+ const most = (placed[sid] = {});
207
+ for (const ctx of CONTEXTS) {
208
+ const counts = {};
209
+ try {
210
+ composeWith(items, sid, surface, ctx, id => { counts[id] = (counts[id] ?? 0) + 1; });
211
+ }
212
+ catch (err) {
213
+ if (!problems.includes(err.message))
214
+ problems.push(err.message);
215
+ }
216
+ for (const [id, n] of Object.entries(counts))
217
+ most[id] = Math.max(most[id] ?? 0, n);
203
218
  }
204
219
  }
205
220
  for (const [id, item] of Object.entries(items)) {
package/dist/types.d.ts CHANGED
@@ -13,8 +13,14 @@ export type PlanStatus = typeof PLAN_STATUSES[number];
13
13
  * lives on knowledge notes only. Asking for one on the entity branch is a
14
14
  * validation error, not a silent fall back to recency, so the entity opts
15
15
  * type declares only what that branch can actually sort by. */
16
- export type SortField = 'last_touched' | 'created' | 'name' | 'staleness' | 'last_reinforced' | 'reinforcement_count';
17
- export type EntitySortField = Exclude<SortField, 'last_reinforced' | 'reinforcement_count'>;
16
+ export type SortField = 'last_touched' | 'created' | 'name' | 'staleness' | 'last_reinforced' | 'reinforcement_count' | 'session_date';
17
+ export type EntitySortField = Exclude<SortField, 'last_reinforced' | 'reinforcement_count' | 'session_date'>;
18
+ export type KnowledgeSortField = Exclude<SortField, 'session_date'>;
19
+ /** A session note carries only its dates and its name. */
20
+ export type SessionSortField = 'session_date' | 'created' | 'name';
21
+ /** What `search` reaches. Unset is the working set, entity + knowledge;
22
+ * `all` is literal, sessions included. */
23
+ export type SearchKind = 'entity' | 'knowledge' | 'session' | 'all';
18
24
  export interface EntityQueryOpts {
19
25
  filter?: QueryFilter;
20
26
  type?: EntityType;
@@ -39,14 +45,28 @@ export interface KnowledgeQueryOpts {
39
45
  createdSince?: string;
40
46
  createdBefore?: string;
41
47
  tags?: string[];
42
- sort_by?: SortField;
48
+ sort_by?: KnowledgeSortField;
49
+ sort_order?: 'asc' | 'desc';
50
+ limit?: number;
51
+ offset?: number;
52
+ }
53
+ /** Sessions carry no status, domain or tags: the time window (`since`/`before`
54
+ * against `session-date`), sort and paging are all a session query takes. */
55
+ export interface SessionQueryOpts {
56
+ since?: string;
57
+ before?: string;
58
+ createdSince?: string;
59
+ createdBefore?: string;
60
+ sort_by?: SessionSortField;
43
61
  sort_order?: 'asc' | 'desc';
44
62
  limit?: number;
45
63
  offset?: number;
46
64
  }
47
65
  /** `kind: 'all'` (or unset): reaches both sides, so it takes opts from both
48
66
  * and can honour every sort field. Status filters narrow the entity side only
49
- * — knowledge notes carry no status and are never excluded by one. */
67
+ * — knowledge notes carry no status and are never excluded by one. Unset
68
+ * is the working set, entity + knowledge; `all` is literal and reaches the
69
+ * sessions too. */
50
70
  export interface AllQueryOpts extends Omit<EntityQueryOpts, 'sort_by'> {
51
71
  kind?: 'all';
52
72
  confidence?: ConfidenceLevel;
@@ -63,6 +83,8 @@ export type UnifiedQueryOpts = AllQueryOpts | (EntityQueryOpts & {
63
83
  kind: 'entity';
64
84
  }) | (KnowledgeQueryOpts & {
65
85
  kind: 'knowledge';
86
+ }) | (SessionQueryOpts & {
87
+ kind: 'session';
66
88
  });
67
89
  export interface EntityFrontmatter {
68
90
  name: string;
@@ -114,11 +136,8 @@ export type ParsedVaultFile = {
114
136
  frontmatter: SessionFrontmatter;
115
137
  body: string;
116
138
  };
117
- /** What read() returns: the indexed arms of ParsedVaultFile (a session slug is
118
- * never resolvable through read()), so consumers narrow on `kind`. */
119
- export type ParsedFile = Extract<ParsedVaultFile, {
120
- kind: 'entity' | 'knowledge';
121
- }>;
139
+ /** What read() returns: any vault file, so consumers narrow on `kind`. */
140
+ export type ParsedFile = ParsedVaultFile;
122
141
  export interface SessionLogEntry {
123
142
  type: SessionLogType;
124
143
  summary: string;
@@ -221,10 +240,19 @@ export interface KnowledgeQueryResult {
221
240
  last_reinforced: string | null;
222
241
  last_touched: string | null;
223
242
  }
243
+ export interface SessionQueryResult {
244
+ slug: string;
245
+ name: string;
246
+ session_date: string;
247
+ created: string;
248
+ related: string[];
249
+ }
224
250
  export type UnifiedResult = (QueryResult & {
225
251
  kind: 'entity';
226
252
  }) | (KnowledgeQueryResult & {
227
253
  kind: 'knowledge';
254
+ }) | (SessionQueryResult & {
255
+ kind: 'session';
228
256
  });
229
257
  export interface ValidationIssue {
230
258
  slug: string;
@@ -237,10 +265,11 @@ export interface ValidationReport {
237
265
  missing_fields: ValidationIssue[];
238
266
  total_entities: number;
239
267
  total_knowledge: number;
268
+ total_sessions: number;
240
269
  }
241
270
  export interface SearchResult {
242
271
  slug: string;
243
- kind: 'entity' | 'knowledge';
272
+ kind: 'entity' | 'knowledge' | 'session';
244
273
  name: string;
245
274
  snippet: string;
246
275
  type?: EntityType;
@@ -275,7 +304,7 @@ export interface VaultStats {
275
304
  }
276
305
  export interface ChangelogEntry {
277
306
  slug: string;
278
- kind: 'entity' | 'knowledge';
307
+ kind: 'entity' | 'knowledge' | 'session';
279
308
  operation: 'created' | 'updated' | 'deleted';
280
309
  timestamp: string;
281
310
  commit_message: string;
@@ -355,7 +384,7 @@ export interface BatchValidationReport {
355
384
  valid: boolean;
356
385
  operations: BatchOpValidation[];
357
386
  }
358
- export type HafezErrorCode = 'NOT_FOUND' | 'SLUG_EXISTS' | 'VALIDATION_FAILED' | 'GIT_PUSH_FAILED' | 'GIT_COMMIT_FAILED' | 'VAULT_LOCKED';
387
+ export type HafezErrorCode = 'NOT_FOUND' | 'SLUG_EXISTS' | 'VALIDATION_FAILED' | 'GIT_PUSH_FAILED' | 'GIT_COMMIT_FAILED' | 'GIT_READ_FAILED' | 'VAULT_LOCKED';
359
388
  export declare class HafezError extends Error {
360
389
  code: HafezErrorCode;
361
390
  details?: string[] | undefined;
@@ -370,10 +399,11 @@ export interface GitConfig {
370
399
  * production (src/git.ts); tests may inject an in-memory adapter.
371
400
  */
372
401
  export interface Journal {
373
- /** Persist one logical change: stage written+deleted paths (vault-relative)
374
- * and commit under message. Must clean up its own partial state (e.g.
375
- * staged files) before rethrowing on a commit-stage failure; a push-stage
376
- * failure surfaces as GIT_PUSH_FAILED with the commit kept. */
402
+ /** Persist one logical change: stage written+deleted paths (vault-relative,
403
+ * '/'-separated on every platform) and commit under message. Must clean up
404
+ * its own partial state (e.g. staged files) before rethrowing on a
405
+ * commit-stage failure; a push-stage failure surfaces as GIT_PUSH_FAILED
406
+ * with the commit kept. */
377
407
  commit(written: string[], deleted: string[], message: string): Promise<void>;
378
408
  /** Bidirectional sync with the remote, if any. */
379
409
  sync(): Promise<{
@@ -381,9 +411,11 @@ export interface Journal {
381
411
  pushed: boolean;
382
412
  remote: boolean;
383
413
  }>;
384
- /** History-derived changelog for entities/ and knowledge/ since a date. */
414
+ /** History-derived changelog for the indexed dirs since a date. Empty when
415
+ * the repo has no commits; any other git failure throws `GIT_READ_FAILED`. */
385
416
  changelog(since: string): Promise<ChangelogEntry[]>;
386
- /** Newest history timestamp per vault file (modified = any change, added = creation). */
417
+ /** Newest history timestamp per vault file (modified = any change, added = creation).
418
+ * Empty when the repo has no commits; any other git failure throws `GIT_READ_FAILED`. */
387
419
  fileTimes(mode: 'modified' | 'added'): Promise<Map<string, string>>;
388
420
  }
389
421
  export interface HafezConfig {
@@ -432,9 +464,21 @@ export interface Hafez {
432
464
  items: KnowledgeQueryResult[];
433
465
  total: number;
434
466
  }>;
467
+ query_sessions(opts?: SessionQueryOpts): Promise<{
468
+ items: SessionQueryResult[];
469
+ total: number;
470
+ }>;
471
+ /** An entity's archived log entries (`entities/archive/<slug>-log.md`), as
472
+ * written; null when it has none. Read from disk, never indexed: the
473
+ * archive is reached from its entity on purpose, never found by search. */
474
+ archive(slug: string): Promise<string | null>;
435
475
  validate(): Promise<ValidationReport>;
436
- search(query: string, kind?: 'entity' | 'knowledge' | 'all'): Promise<SearchResult[]>;
476
+ search(query: string, kind?: SearchKind): Promise<SearchResult[]>;
437
477
  rebuildIndex(): Promise<void>;
438
478
  stats(): Promise<VaultStats>;
439
479
  changelog(since: string): Promise<ChangelogEntry[]>;
480
+ /** Release the SQLite index handle. Idempotent; the next call that needs
481
+ * the index reopens it. Call before deleting the vault: Windows cannot
482
+ * remove a directory holding an open file. */
483
+ close(): void;
440
484
  }