hafez 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -1
- package/dist/apply.d.ts +12 -0
- package/dist/apply.js +82 -17
- package/dist/batch-ops.d.ts +31 -27
- package/dist/batch-ops.js +80 -15
- package/dist/cli/block-hook.d.ts +6 -0
- package/dist/cli/block-hook.js +3 -3
- package/dist/cli/commands.d.ts +2 -0
- package/dist/cli/commands.js +98 -46
- package/dist/cli/format.d.ts +4 -1
- package/dist/cli/format.js +19 -0
- package/dist/cli/index.js +11 -2
- package/dist/cli/upgrade.js +1 -1
- package/dist/contracts.d.ts +9 -0
- package/dist/contracts.js +9 -0
- package/dist/dates.d.ts +3 -0
- package/dist/dates.js +9 -0
- package/dist/db.d.ts +34 -4
- package/dist/db.js +163 -21
- package/dist/digest.js +14 -7
- package/dist/document.d.ts +2 -0
- package/dist/document.js +2 -2
- package/dist/git.d.ts +13 -2
- package/dist/git.js +47 -5
- package/dist/graph/activity.d.ts +5 -0
- package/dist/graph/activity.js +70 -0
- package/dist/graph/client.js +326 -0
- package/dist/graph/copy-fonts.d.ts +1 -0
- package/dist/graph/copy-fonts.js +13 -0
- package/dist/graph/fields.d.ts +8 -0
- package/dist/graph/fields.js +12 -0
- package/dist/graph/fonts/geist-OFL.txt +93 -0
- package/dist/graph/fonts/geist-mono-OFL.txt +93 -0
- package/dist/graph/fonts/geist-mono.woff2 +0 -0
- package/dist/graph/fonts/geist.woff2 +0 -0
- package/dist/graph/fonts/outfit-OFL.txt +93 -0
- package/dist/graph/fonts/outfit.woff2 +0 -0
- package/dist/graph/fonts.d.ts +14 -0
- package/dist/graph/fonts.js +17 -0
- package/dist/graph/live.d.ts +16 -0
- package/dist/graph/live.js +99 -0
- package/dist/graph/open.d.ts +1 -0
- package/dist/graph/open.js +20 -0
- package/dist/graph/server.d.ts +17 -0
- package/dist/graph/server.js +189 -0
- package/dist/health.d.ts +6 -0
- package/dist/health.js +310 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +37 -9
- package/dist/internal.d.ts +21 -0
- package/dist/internal.js +9 -0
- package/dist/read-json.d.ts +26 -0
- package/dist/read-json.js +53 -0
- package/dist/stale.d.ts +2 -2
- package/dist/stale.js +4 -9
- package/dist/surfaces/items.d.ts +44 -0
- package/dist/surfaces/items.js +68 -9
- package/dist/surfaces/surfaces.js +3 -1
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +6 -1
- package/dist/types.d.ts +178 -8
- package/dist/vault.d.ts +9 -2
- package/dist/vault.js +10 -0
- package/dist/workstream.d.ts +17 -0
- package/dist/workstream.js +78 -0
- package/package.json +9 -3
- package/skill/hafez/SKILL.md +12 -6
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Hafez, ParsedFile, ReadDepth } from './types.js';
|
|
2
|
+
import { type Workstream } from './workstream.js';
|
|
3
|
+
import { type ChildLine, type FooterInput } from './surfaces/items.js';
|
|
4
|
+
/** `read --json`'s object for a note read at `depth`: what the CLI prints and
|
|
5
|
+
* what the graph server's `/note/:slug` serves. `whole`, its full-depth read
|
|
6
|
+
* when the caller holds one, saves reading it again. */
|
|
7
|
+
export declare function readJsonOf(os: Hafez, slug: string, depth: ReadDepth, result: ParsedFile, whole?: ParsedFile): Promise<object>;
|
|
8
|
+
/** What hangs off an entity elsewhere: the sessions that touched it (the
|
|
9
|
+
* look back, #35), its archived log entries (#34), its children (#230) and
|
|
10
|
+
* the prefix families its own Next Actions carry unsplit against the
|
|
11
|
+
* entities whose `parent` or `related` names it (#230). Counted at read
|
|
12
|
+
* time, nothing written. */
|
|
13
|
+
export declare function pointersFor(os: Hafez, slug: string, body: string): Promise<{
|
|
14
|
+
sessions: {
|
|
15
|
+
count: number;
|
|
16
|
+
latest: string | null;
|
|
17
|
+
};
|
|
18
|
+
archive_entries: number;
|
|
19
|
+
children: ChildLine[];
|
|
20
|
+
workstreams: Workstream[];
|
|
21
|
+
}>;
|
|
22
|
+
/** The Footer for a file read at full depth: its Handle comes from the
|
|
23
|
+
* whole file. A session note has none: it is never written to. */
|
|
24
|
+
export declare function footerFor(slug: string, result: Exclude<ParsedFile, {
|
|
25
|
+
kind: 'session';
|
|
26
|
+
}>): FooterInput;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { detectAgent } from './harness.js';
|
|
2
|
+
import { getActionItems, parseSessionLogHeading } from './document.js';
|
|
3
|
+
import { agedActions } from './stale.js';
|
|
4
|
+
import { workstreams } from './workstream.js';
|
|
5
|
+
import { CHILDREN_SHOWN, footerOp } from './surfaces/items.js';
|
|
6
|
+
/** `read --json`'s object for a note read at `depth`: what the CLI prints and
|
|
7
|
+
* what the graph server's `/note/:slug` serves. `whole`, its full-depth read
|
|
8
|
+
* when the caller holds one, saves reading it again. */
|
|
9
|
+
export async function readJsonOf(os, slug, depth, result, whole) {
|
|
10
|
+
// A session note is read, never written: no Footer (#35).
|
|
11
|
+
if (result.kind === 'session')
|
|
12
|
+
return result;
|
|
13
|
+
// The Footer and the pointers are the same at every depth: they read the
|
|
14
|
+
// whole file, not the body this depth shows.
|
|
15
|
+
const full = (whole ?? (depth === 'full' ? result : await os.read(slug, 'full')));
|
|
16
|
+
const writes = footerOp(footerFor(slug, full));
|
|
17
|
+
const pointers = full.kind === 'entity' ? await pointersFor(os, slug, full.body) : null;
|
|
18
|
+
const json = pointers && { ...pointers, children: pointers.children.slice(0, CHILDREN_SHOWN), children_total: pointers.children.length };
|
|
19
|
+
// A frontmatter-depth read carries no body, so it has no actions to list.
|
|
20
|
+
if (result.kind !== 'entity' || depth === 'frontmatter')
|
|
21
|
+
return { ...result, ...json, writes };
|
|
22
|
+
return { ...result, actions: agedActions(result.body, result.frontmatter), ...json, writes };
|
|
23
|
+
}
|
|
24
|
+
/** What hangs off an entity elsewhere: the sessions that touched it (the
|
|
25
|
+
* look back, #35), its archived log entries (#34), its children (#230) and
|
|
26
|
+
* the prefix families its own Next Actions carry unsplit against the
|
|
27
|
+
* entities whose `parent` or `related` names it (#230). Counted at read
|
|
28
|
+
* time, nothing written. */
|
|
29
|
+
export async function pointersFor(os, slug, body) {
|
|
30
|
+
const relatedTo = (await os.related_to(slug)).items;
|
|
31
|
+
const sessions = relatedTo.filter((i) => i.kind === 'session');
|
|
32
|
+
const latest = sessions.reduce((max, s) => (max === null || s.session_date > max ? s.session_date : max), null);
|
|
33
|
+
const archive = await os.archive(slug);
|
|
34
|
+
const entries = archive ? archive.split('\n').filter(line => parseSessionLogHeading(line)).length : 0;
|
|
35
|
+
// The children block (#230): the index's children query, last-touched
|
|
36
|
+
// descending then slug for a stable order; the renders cap what is shown.
|
|
37
|
+
const childItems = (await os.children(slug)).items;
|
|
38
|
+
const children = [...childItems]
|
|
39
|
+
.sort((a, b) => b.last_touched.localeCompare(a.last_touched) || a.slug.localeCompare(b.slug))
|
|
40
|
+
.map(c => ({ slug: c.slug, description: c.description, open_actions: c.next_action_count, last_touched: c.last_touched }));
|
|
41
|
+
const naming = [...childItems, ...relatedTo.filter(i => i.kind === 'entity')];
|
|
42
|
+
return { sessions: { count: sessions.length, latest }, archive_entries: entries, children, workstreams: workstreams(body, naming) };
|
|
43
|
+
}
|
|
44
|
+
/** The Footer for a file read at full depth: its Handle comes from the
|
|
45
|
+
* whole file. A session note has none: it is never written to. */
|
|
46
|
+
export function footerFor(slug, result) {
|
|
47
|
+
return { slug, agent: detectAgent(), ...(result.kind === 'entity'
|
|
48
|
+
? {
|
|
49
|
+
target: { kind: 'entity', type: result.frontmatter.type },
|
|
50
|
+
handle: getActionItems(result.body).find(a => a.handle)?.handle,
|
|
51
|
+
}
|
|
52
|
+
: { target: { kind: 'knowledge', subtype: result.frontmatter.subtype } }) };
|
|
53
|
+
}
|
package/dist/stale.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ export interface ActionAge {
|
|
|
9
9
|
}
|
|
10
10
|
/** The entity side of the rule, as SQL over an index row `i` with `@today`
|
|
11
11
|
* bound to localDay(): whole days from its last-touched day to today.
|
|
12
|
-
* julianday of two bare YYYY-MM-DD days differs by exactly `
|
|
13
|
-
*
|
|
12
|
+
* julianday of two bare YYYY-MM-DD days differs by exactly `daysBetween`
|
|
13
|
+
* (src/dates.ts), so entities and their actions count the same days. */
|
|
14
14
|
export declare const ENTITY_AGE_DAYS_SQL = "CAST(julianday(@today) - julianday(i.last_touched) AS INTEGER)";
|
|
15
15
|
/** An entity row is stale when its age is past its threshold, not equal to
|
|
16
16
|
* it — `actionAge`'s rule. */
|
package/dist/stale.js
CHANGED
|
@@ -2,19 +2,14 @@
|
|
|
2
2
|
// Stale (see CONTEXT.md): past the entity's `staleness-days` without
|
|
3
3
|
// movement. An open next action is stale when unticked that long since its
|
|
4
4
|
// added day; an undated one is stale. Ages count local days (src/dates.ts).
|
|
5
|
-
import { localDay } from './dates.js';
|
|
5
|
+
import { daysBetween, localDay } from './dates.js';
|
|
6
6
|
import { getActionItems } from './document.js';
|
|
7
7
|
/** The threshold an entity without `staleness-days` gets. */
|
|
8
8
|
export const DEFAULT_STALENESS_DAYS = 14;
|
|
9
|
-
/** A YYYY-MM-DD day as a whole day count, so differences ignore DST. */
|
|
10
|
-
const dayNumber = (day) => {
|
|
11
|
-
const [y, m, d] = day.split('-').map(Number);
|
|
12
|
-
return Date.UTC(y, m - 1, d) / 86_400_000;
|
|
13
|
-
};
|
|
14
9
|
/** The entity side of the rule, as SQL over an index row `i` with `@today`
|
|
15
10
|
* bound to localDay(): whole days from its last-touched day to today.
|
|
16
|
-
* julianday of two bare YYYY-MM-DD days differs by exactly `
|
|
17
|
-
*
|
|
11
|
+
* julianday of two bare YYYY-MM-DD days differs by exactly `daysBetween`
|
|
12
|
+
* (src/dates.ts), so entities and their actions count the same days. */
|
|
18
13
|
export const ENTITY_AGE_DAYS_SQL = 'CAST(julianday(@today) - julianday(i.last_touched) AS INTEGER)';
|
|
19
14
|
/** An entity row is stale when its age is past its threshold, not equal to
|
|
20
15
|
* it — `actionAge`'s rule. */
|
|
@@ -24,7 +19,7 @@ export const ENTITY_STALE_SQL = `${ENTITY_AGE_DAYS_SQL} > COALESCE(i.staleness_d
|
|
|
24
19
|
export function actionAge(added, stalenessDays, today = localDay()) {
|
|
25
20
|
if (added === null)
|
|
26
21
|
return { added, age_days: null, stale: true };
|
|
27
|
-
const age_days =
|
|
22
|
+
const age_days = daysBetween(added, today);
|
|
28
23
|
return { added, age_days, stale: age_days > stalenessDays };
|
|
29
24
|
}
|
|
30
25
|
/** An entity's threshold: its `staleness-days`, else the default. */
|
package/dist/surfaces/items.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type DocTarget, type WriteLineField } from '../contracts.js';
|
|
2
2
|
import { renderActionHandles } from '../document.js';
|
|
3
3
|
import type { ActionAge } from '../stale.js';
|
|
4
|
+
import type { Workstream } from '../workstream.js';
|
|
4
5
|
import type { KnowledgeSubtype, UpdateFields, UpdateResult } from '../types.js';
|
|
5
6
|
import { type OnboardHarness } from '../harness.js';
|
|
6
7
|
import type { Mode } from '../cli/install.js';
|
|
@@ -112,6 +113,28 @@ declare function renderArchivePointer({ slug, entries }: {
|
|
|
112
113
|
slug: string;
|
|
113
114
|
entries: number;
|
|
114
115
|
}): string;
|
|
116
|
+
/** A child as the `read` header shows it. */
|
|
117
|
+
export interface ChildLine {
|
|
118
|
+
slug: string;
|
|
119
|
+
description: string | null;
|
|
120
|
+
open_actions: number;
|
|
121
|
+
last_touched: string;
|
|
122
|
+
}
|
|
123
|
+
/** How many children the header lists; the rest are one overflow line. */
|
|
124
|
+
export declare const CHILDREN_SHOWN = 5;
|
|
125
|
+
/** The `read` header's children block: the count line (children, open
|
|
126
|
+
* actions across all of them), one `hafez read` line per child (description,
|
|
127
|
+
* open count, last-touched day) for the first `CHILDREN_SHOWN`, and the
|
|
128
|
+
* overflow line naming the query that lists the rest. Children arrive sorted
|
|
129
|
+
* by last-touched descending. No children: no block. */
|
|
130
|
+
declare function renderChildrenBlock({ slug, children }: {
|
|
131
|
+
slug: string;
|
|
132
|
+
children: ChildLine[];
|
|
133
|
+
}): string;
|
|
134
|
+
/** The `read` header's fact line for one unsplit prefix family (#230):
|
|
135
|
+
* its open-line count and the family, and that no child carries it. A
|
|
136
|
+
* fact, never a filled create line or a question. */
|
|
137
|
+
declare function renderWorkstreamLine({ prefix, open_actions }: Workstream): string;
|
|
115
138
|
/** The line after an update's success while Stale lines remain on the entity. */
|
|
116
139
|
declare function renderStaleReminder({ handles }: {
|
|
117
140
|
handles: string[];
|
|
@@ -186,6 +209,20 @@ export declare const ITEMS: {
|
|
|
186
209
|
description: string | null;
|
|
187
210
|
}) => string;
|
|
188
211
|
};
|
|
212
|
+
childrenBlock: {
|
|
213
|
+
label: string;
|
|
214
|
+
owner: "readHeader";
|
|
215
|
+
invokeOnly: string;
|
|
216
|
+
evidence: string;
|
|
217
|
+
render: typeof renderChildrenBlock;
|
|
218
|
+
};
|
|
219
|
+
workstreamLine: {
|
|
220
|
+
label: string;
|
|
221
|
+
owner: "readHeader";
|
|
222
|
+
invokeOnly: string;
|
|
223
|
+
evidence: string;
|
|
224
|
+
render: typeof renderWorkstreamLine;
|
|
225
|
+
};
|
|
189
226
|
sessionsLine: {
|
|
190
227
|
label: string;
|
|
191
228
|
owner: "readHeader";
|
|
@@ -360,6 +397,13 @@ export declare const ITEMS: {
|
|
|
360
397
|
evidence: string;
|
|
361
398
|
text: string;
|
|
362
399
|
};
|
|
400
|
+
healthProcedure: {
|
|
401
|
+
label: string;
|
|
402
|
+
owner: "hafezSkillBody";
|
|
403
|
+
invokeOnly: string;
|
|
404
|
+
evidence: string;
|
|
405
|
+
text: string;
|
|
406
|
+
};
|
|
363
407
|
batchShape: {
|
|
364
408
|
label: string;
|
|
365
409
|
owner: "hafezSkillBody";
|
package/dist/surfaces/items.js
CHANGED
|
@@ -176,6 +176,35 @@ function renderArchivePointer({ slug, entries }) {
|
|
|
176
176
|
return '';
|
|
177
177
|
return `archive: ${entries} older log ${entries === 1 ? 'entry' : 'entries'} · hafez read ${slug} --archive`;
|
|
178
178
|
}
|
|
179
|
+
/** How many children the header lists; the rest are one overflow line. */
|
|
180
|
+
export const CHILDREN_SHOWN = 5;
|
|
181
|
+
/** The `read` header's children block: the count line (children, open
|
|
182
|
+
* actions across all of them), one `hafez read` line per child (description,
|
|
183
|
+
* open count, last-touched day) for the first `CHILDREN_SHOWN`, and the
|
|
184
|
+
* overflow line naming the query that lists the rest. Children arrive sorted
|
|
185
|
+
* by last-touched descending. No children: no block. */
|
|
186
|
+
function renderChildrenBlock({ slug, children }) {
|
|
187
|
+
if (children.length === 0)
|
|
188
|
+
return '';
|
|
189
|
+
const open = children.reduce((sum, c) => sum + c.open_actions, 0);
|
|
190
|
+
const shown = children.slice(0, CHILDREN_SHOWN);
|
|
191
|
+
const width = Math.max(...shown.map(c => c.slug.length));
|
|
192
|
+
const lines = [`children: ${children.length} · ${open} open action${open === 1 ? '' : 's'}`];
|
|
193
|
+
for (const c of shown) {
|
|
194
|
+
const facts = [c.description, `${c.open_actions} open`, c.last_touched].filter(Boolean).join(' · ');
|
|
195
|
+
lines.push(` hafez read ${c.slug.padEnd(width)} ${facts}`);
|
|
196
|
+
}
|
|
197
|
+
const rest = children.length - shown.length;
|
|
198
|
+
if (rest > 0)
|
|
199
|
+
lines.push(` … and ${rest} more · hafez query --parent ${slug}`);
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
202
|
+
/** The `read` header's fact line for one unsplit prefix family (#230):
|
|
203
|
+
* its open-line count and the family, and that no child carries it. A
|
|
204
|
+
* fact, never a filled create line or a question. */
|
|
205
|
+
function renderWorkstreamLine({ prefix, open_actions }) {
|
|
206
|
+
return `workstream: ${open_actions} open action${open_actions === 1 ? '' : 's'} with \`${prefix}\`; no child named it`;
|
|
207
|
+
}
|
|
179
208
|
/** The line after an update's success while Stale lines remain on the entity. */
|
|
180
209
|
function renderStaleReminder({ handles }) {
|
|
181
210
|
const n = handles.length;
|
|
@@ -193,7 +222,10 @@ function renderUpdateMisuse(input) {
|
|
|
193
222
|
}
|
|
194
223
|
const CREATE_TYPES = ['project', 'entity'];
|
|
195
224
|
const RELATIONS = ['parent', 'related'];
|
|
196
|
-
|
|
225
|
+
/** The entity create line, one text everywhere: the Block, the skill body and
|
|
226
|
+
* the `read` miss. `--parent` carries the placement rule in its placeholder
|
|
227
|
+
* (#226, #230); at a miss with nothing read the agent drops it. */
|
|
228
|
+
const createEntityLine = (name) => `hafez create entity "${name}" --type ${oneOf(CREATE_TYPES)} --parent <slug you read, or its parent> --description "<one sentence>" --brief "<where things stand>" --add-action "<next step>"`;
|
|
197
229
|
const CREATE_LINES = fence([
|
|
198
230
|
createEntityLine('<name>'),
|
|
199
231
|
`hafez create knowledge "<name>" --synthesis "<the insight>" --confidence ${oneOf(CONFIDENCE_LEVELS)} --related <slug>`,
|
|
@@ -305,6 +337,20 @@ export const ITEMS = {
|
|
|
305
337
|
* in brackets when it has one. */
|
|
306
338
|
render: ({ slug, description }) => description ? `${slug} (${description})` : slug,
|
|
307
339
|
},
|
|
340
|
+
childrenBlock: {
|
|
341
|
+
label: 'Children block: the Hub\'s children as read lines',
|
|
342
|
+
owner: 'readHeader',
|
|
343
|
+
invokeOnly: 'same',
|
|
344
|
+
evidence: '#230',
|
|
345
|
+
render: renderChildrenBlock,
|
|
346
|
+
},
|
|
347
|
+
workstreamLine: {
|
|
348
|
+
label: 'Workstream fact line: an unsplit prefix family',
|
|
349
|
+
owner: 'readHeader',
|
|
350
|
+
invokeOnly: 'same',
|
|
351
|
+
evidence: '#230',
|
|
352
|
+
render: renderWorkstreamLine,
|
|
353
|
+
},
|
|
308
354
|
sessionsLine: {
|
|
309
355
|
label: 'Sessions line: the look back from an entity',
|
|
310
356
|
owner: 'readHeader',
|
|
@@ -320,14 +366,15 @@ export const ITEMS = {
|
|
|
320
366
|
render: renderArchivePointer,
|
|
321
367
|
},
|
|
322
368
|
writeTriggers: {
|
|
323
|
-
label: 'Write triggers: work landed, decision made,
|
|
369
|
+
label: 'Write triggers: work landed, a split, decision made, a lesson, weighing, "note this"',
|
|
324
370
|
owner: 'block',
|
|
325
371
|
invokeOnly: 'absent',
|
|
326
|
-
evidence: 'M11, M2, never-ask decision #42, decision-record miss #159',
|
|
372
|
+
evidence: 'M11, M2, never-ask decision #42, decision-record miss #159; placement #226, #229',
|
|
327
373
|
text: place => [
|
|
328
|
-
'- Work landed (a commit, a merged PR, CI green): update the entity that owns the work. That is the entity you read
|
|
329
|
-
'- A
|
|
330
|
-
|
|
374
|
+
'- Work landed (a commit, a merged PR, CI green): update the entity that owns the work. That is the entity you read; the entity you read has a child that owns it: write there; the work belongs to another: read that one first. The end of its `read` output shows the command.',
|
|
375
|
+
'- A workstream your own actions or the user already name, too big for one action, gets a Child with that name under the entity you read: create it with `--parent`, move its open actions there (one `update` on the parent: `--remove-action` each, `--log "decision: split <child> out, open actions moved"`), and write there from now on.',
|
|
376
|
+
'- A decision was made: one `decision` log entry, now, on the entity that owns the work. The repo keeps its own decisions record: write it there and the log entry points at it (`hafez update <slug> --log "decision: <one line>, see decisions.md"`).',
|
|
377
|
+
`- A lesson that holds without this entity: create a knowledge note; what we chose is a decision entry, not a note. ${place('knowledgeBoundary')}`,
|
|
331
378
|
'- The user says "note this" or "record that": write it.',
|
|
332
379
|
`- You are weighing whether something is worth writing: write it, as knowledge with \`--confidence ${CONFIDENCE_LEVELS[0]}\`.`,
|
|
333
380
|
].join('\n'),
|
|
@@ -390,7 +437,7 @@ export const ITEMS = {
|
|
|
390
437
|
owner: 'block',
|
|
391
438
|
fallback: 'hafezSkillBody',
|
|
392
439
|
invokeOnly: 'via the skill body',
|
|
393
|
-
evidence: 'fields: kind/name 18, synthesis 16, subtype 15',
|
|
440
|
+
evidence: 'fields: kind/name 18, synthesis 16, subtype 15; --parent #230',
|
|
394
441
|
text: CREATE_LINES + '\n\nA plan is `--subtype plan` without `--synthesis`. Cannot tell entity from knowledge: `hafez capture "<name>"`.',
|
|
395
442
|
/** The entity create line for one name: what the `read` miss places. */
|
|
396
443
|
render: createEntityLine,
|
|
@@ -506,7 +553,7 @@ export const ITEMS = {
|
|
|
506
553
|
+ '\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.',
|
|
507
554
|
},
|
|
508
555
|
rareOps: {
|
|
509
|
-
label: 'Rare ops: promote, link, unlink, status, delete, validate',
|
|
556
|
+
label: 'Rare ops: promote, link, unlink, status, delete, validate, health',
|
|
510
557
|
owner: 'hafezSkillBody',
|
|
511
558
|
invokeOnly: 'same',
|
|
512
559
|
evidence: 'help decision #49; owner decision #43',
|
|
@@ -517,8 +564,20 @@ export const ITEMS = {
|
|
|
517
564
|
`hafez update <slug> --status ${oneOf(ENTITY_STATUSES)}`,
|
|
518
565
|
'hafez delete <slug>',
|
|
519
566
|
'hafez validate',
|
|
567
|
+
'hafez health',
|
|
520
568
|
].join('\n'))
|
|
521
|
-
+ `\n\nA plan's status is \`${PLAN_STATUSES.join('|')}\`. \`delete\` removes a knowledge or session note only; an entity is retired with \`--status done
|
|
569
|
+
+ `\n\nA plan's status is \`${PLAN_STATUSES.join('|')}\`. \`delete\` removes a knowledge or session note only; an entity is retired with \`--status done\`. \`health\` lists what placement would change and applies nothing.`,
|
|
570
|
+
},
|
|
571
|
+
healthProcedure: {
|
|
572
|
+
label: 'Health procedure: run with --json, propose by kind, apply nothing unconfirmed',
|
|
573
|
+
owner: 'hafezSkillBody',
|
|
574
|
+
invokeOnly: 'same',
|
|
575
|
+
evidence: '#248; health is agent input, the owner never reads the raw report',
|
|
576
|
+
text: [
|
|
577
|
+
'A vault check ("check the vault", "clean up the vault", "what has drifted"): run `hafez health --json` and bring back proposals grouped by kind, never the rows. In a session that read an entity, lead with that entity\'s rows; the rest of the vault as counts.',
|
|
578
|
+
'One line per kind, with the count: the Workstreams to split (a `create entity … --parent` line each, named as the actions name it); the one-way edges and the unlinked mentions as one `hafez batch` of `link` ops, one commit; the restated decisions and the leakage pairs as a short list for the user to judge, nothing proposed.',
|
|
579
|
+
'Apply nothing until the user confirms a kind, not even a `link` line that looks safe; then one `batch` per kind.',
|
|
580
|
+
].join('\n'),
|
|
522
581
|
},
|
|
523
582
|
batchShape: {
|
|
524
583
|
label: 'Batch shape',
|
|
@@ -40,7 +40,7 @@ export const SURFACES = {
|
|
|
40
40
|
},
|
|
41
41
|
hafezSkillDescription: {
|
|
42
42
|
label: '`hafez` skill description',
|
|
43
|
-
compose: () => 'Hafez vault: the less common operations and the tool itself. Use when the user asks what is in the vault ("what am I working on?", "what\'s stale?"), wants to promote, link, unlink, delete, change a status or validate, or wants to update or reconfigure Hafez ("upgrade hafez", "set up hafez"). Also the reference to read when no Hafez block is in your instructions.',
|
|
43
|
+
compose: () => 'Hafez vault: the less common operations and the tool itself. Use when the user asks what is in the vault ("what am I working on?", "what\'s stale?"), wants to promote, link, unlink, delete, change a status or validate, asks to check the vault or clean it up, or wants to update or reconfigure Hafez ("upgrade hafez", "set up hafez"). Also the reference to read when no Hafez block is in your instructions.',
|
|
44
44
|
},
|
|
45
45
|
hafezSkillBody: {
|
|
46
46
|
label: '`hafez` skill body',
|
|
@@ -68,6 +68,8 @@ export const SURFACES = {
|
|
|
68
68
|
'',
|
|
69
69
|
place('rareOps'),
|
|
70
70
|
'',
|
|
71
|
+
place('healthProcedure'),
|
|
72
|
+
'',
|
|
71
73
|
`${place('batchShape')} Machine-readable shape of any op: \`hafez schema <op>\`, with \`--examples\` for a sample.`,
|
|
72
74
|
'',
|
|
73
75
|
'## The tool itself',
|
package/dist/templates.d.ts
CHANGED
|
@@ -11,4 +11,6 @@ export declare function knowledgeBodyTemplate(subtype?: KnowledgeSubtype, fields
|
|
|
11
11
|
* every other section set, and this renders it. */
|
|
12
12
|
export declare function sessionBodyTemplate(fields?: {
|
|
13
13
|
synthesis?: string;
|
|
14
|
+
decisions?: string[];
|
|
15
|
+
entities_touched?: string[];
|
|
14
16
|
}): string;
|
package/dist/templates.js
CHANGED
|
@@ -35,5 +35,10 @@ export function knowledgeBodyTemplate(subtype, fields) {
|
|
|
35
35
|
* structural for Document splitting — so the list lives in contracts with
|
|
36
36
|
* every other section set, and this renders it. */
|
|
37
37
|
export function sessionBodyTemplate(fields) {
|
|
38
|
-
|
|
38
|
+
const bullets = (items) => items?.map(i => `- ${i}`).join('\n');
|
|
39
|
+
return render([...SESSION_SECTIONS], {
|
|
40
|
+
Summary: fields?.synthesis,
|
|
41
|
+
'Entities Touched': bullets(fields?.entities_touched?.map(slug => `[[${slug}]]`)),
|
|
42
|
+
Decisions: bullets(fields?.decisions),
|
|
43
|
+
});
|
|
39
44
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ENTITY_TYPES, ENTITY_STATUSES, CONFIDENCE_LEVELS, SESSION_LOG_TYPES, VALID_SUBTYPES, PLAN_STATUSES } from './contracts.js';
|
|
1
|
+
import type { ENTITY_TYPES, ENTITY_STATUSES, CONFIDENCE_LEVELS, SESSION_LOG_TYPES, VALID_SUBTYPES, PLAN_STATUSES, GRAPH_LAYERS, SYNTHETIC_LAYERS, GRAPH_NOTE_RELATIONS, CHANGE_OPS } from './contracts.js';
|
|
2
2
|
export type EntityType = typeof ENTITY_TYPES[number];
|
|
3
3
|
export type EntityStatus = typeof ENTITY_STATUSES[number];
|
|
4
4
|
export type ConfidenceLevel = typeof CONFIDENCE_LEVELS[number];
|
|
@@ -9,6 +9,10 @@ export type LinkRelation = 'parent' | 'related';
|
|
|
9
9
|
export type KnowledgeSubtype = typeof VALID_SUBTYPES[number];
|
|
10
10
|
/** Plans are the one knowledge subtype with a lifecycle (see PLAN_STATUSES). */
|
|
11
11
|
export type PlanStatus = typeof PLAN_STATUSES[number];
|
|
12
|
+
/** The kinds of note the Vault holds, one directory each (the Layout,
|
|
13
|
+
* vault.ts, re-exports it). Declared here so this file imports no module
|
|
14
|
+
* with runtime dependencies: the graph page's bundle reads its types. */
|
|
15
|
+
export type VaultKind = 'entity' | 'knowledge' | 'session';
|
|
12
16
|
/** Sort fields, and the two an entity query cannot honour — reinforcement
|
|
13
17
|
* lives on knowledge notes only. Asking for one on the entity branch is a
|
|
14
18
|
* validation error, not a silent fall back to recency, so the entity opts
|
|
@@ -208,6 +212,14 @@ export interface CreateKnowledgeFields {
|
|
|
208
212
|
tags?: string[];
|
|
209
213
|
'session-date'?: string;
|
|
210
214
|
}
|
|
215
|
+
export interface CreateSessionFields {
|
|
216
|
+
synthesis?: string;
|
|
217
|
+
insight?: string;
|
|
218
|
+
related?: string[];
|
|
219
|
+
'session-date'?: string;
|
|
220
|
+
decisions?: string[];
|
|
221
|
+
entities_touched?: string[];
|
|
222
|
+
}
|
|
211
223
|
export interface QueryResult {
|
|
212
224
|
slug: string;
|
|
213
225
|
name: string;
|
|
@@ -267,6 +279,95 @@ export interface ValidationReport {
|
|
|
267
279
|
total_knowledge: number;
|
|
268
280
|
total_sessions: number;
|
|
269
281
|
}
|
|
282
|
+
/** Check (i): an unsplit prefix family on an entity, from the family module. */
|
|
283
|
+
export interface HealthWorkstream {
|
|
284
|
+
entity: string;
|
|
285
|
+
prefix: string;
|
|
286
|
+
open_actions: number;
|
|
287
|
+
}
|
|
288
|
+
/** Check (ii): a name recurring in `units` units of one entity across `days`. */
|
|
289
|
+
export interface HealthRecurringName {
|
|
290
|
+
entity: string;
|
|
291
|
+
term: string;
|
|
292
|
+
units: number;
|
|
293
|
+
days: string[];
|
|
294
|
+
}
|
|
295
|
+
/** Check (iv): `hits` of the parent's log entries dated on or after the
|
|
296
|
+
* child's `created` (`entries` of them) name the child. */
|
|
297
|
+
export interface HealthLeak {
|
|
298
|
+
parent: string;
|
|
299
|
+
child: string;
|
|
300
|
+
relation: 'parent' | 'related';
|
|
301
|
+
hits: number;
|
|
302
|
+
entries: number;
|
|
303
|
+
}
|
|
304
|
+
/** Check (iii): `from`'s `related` names `to`, a note of the same kind,
|
|
305
|
+
* whose `related` does not name `from`. Check (v): `from`'s body mentions
|
|
306
|
+
* `[[to]]`, a note of the same kind, with no `related` edge either way.
|
|
307
|
+
* Neither lists a parent and its child: parent subsumes related. */
|
|
308
|
+
export interface HealthEdge {
|
|
309
|
+
from: string;
|
|
310
|
+
to: string;
|
|
311
|
+
}
|
|
312
|
+
/** Check (vi): the knowledge note `note`, created on `day` and related to
|
|
313
|
+
* `entity`, shares `share` (0..1) of the summary tokens of a `decision`
|
|
314
|
+
* entry of that day on the entity. */
|
|
315
|
+
export interface HealthRestatedDecision {
|
|
316
|
+
day: string;
|
|
317
|
+
entity: string;
|
|
318
|
+
note: string;
|
|
319
|
+
share: number;
|
|
320
|
+
summary: string;
|
|
321
|
+
}
|
|
322
|
+
export interface HealthReport {
|
|
323
|
+
workstreams: HealthWorkstream[];
|
|
324
|
+
recurring_names: HealthRecurringName[];
|
|
325
|
+
leakage: HealthLeak[];
|
|
326
|
+
one_way_edges: HealthEdge[];
|
|
327
|
+
unlinked_mentions: HealthEdge[];
|
|
328
|
+
restated_decisions: HealthRestatedDecision[];
|
|
329
|
+
}
|
|
330
|
+
export type GraphLayer = typeof GRAPH_LAYERS[number];
|
|
331
|
+
export type SyntheticLayer = typeof SYNTHETIC_LAYERS[number];
|
|
332
|
+
export interface GraphNode {
|
|
333
|
+
/** The slug; `tag:<t>` / `domain:<d>` for a layer node (a slug never holds `:`). */
|
|
334
|
+
id: string;
|
|
335
|
+
kind: VaultKind | SyntheticLayer;
|
|
336
|
+
name: string;
|
|
337
|
+
type?: EntityType;
|
|
338
|
+
status?: EntityStatus;
|
|
339
|
+
subtype?: KnowledgeSubtype;
|
|
340
|
+
confidence?: ConfidenceLevel;
|
|
341
|
+
last_touched?: string;
|
|
342
|
+
description?: string;
|
|
343
|
+
/** An active or paused Entity's open Next Actions. */
|
|
344
|
+
open_actions?: number;
|
|
345
|
+
/** An active Entity untouched past its staleness-days: the index's stale rule. */
|
|
346
|
+
stale?: boolean;
|
|
347
|
+
/** An active or paused Entity's open actions the Stale rule marks, undated ones included. */
|
|
348
|
+
stale_actions?: number;
|
|
349
|
+
/** Every Entity's effective staleness-days: its own, else 14. */
|
|
350
|
+
staleness_days?: number;
|
|
351
|
+
/** An Entity or Knowledge note with no Parent, Related or Wiki-link edge either way. */
|
|
352
|
+
unlinked?: true;
|
|
353
|
+
/** On an Unlinked note: the closest other linked note by name and description,
|
|
354
|
+
* the same kind when one matches; absent when none does. */
|
|
355
|
+
best?: string;
|
|
356
|
+
}
|
|
357
|
+
export interface GraphEdge {
|
|
358
|
+
source: string;
|
|
359
|
+
target: string;
|
|
360
|
+
relation: typeof GRAPH_NOTE_RELATIONS[number] | SyntheticLayer;
|
|
361
|
+
}
|
|
362
|
+
export interface Graph {
|
|
363
|
+
nodes: GraphNode[];
|
|
364
|
+
edges: GraphEdge[];
|
|
365
|
+
}
|
|
366
|
+
/** Any other opt is rejected, never dropped. */
|
|
367
|
+
export interface GraphOpts {
|
|
368
|
+
/** Optional layers added to the default graph; they combine. */
|
|
369
|
+
layers?: GraphLayer[];
|
|
370
|
+
}
|
|
270
371
|
export interface SearchResult {
|
|
271
372
|
slug: string;
|
|
272
373
|
kind: 'entity' | 'knowledge' | 'session';
|
|
@@ -308,6 +409,48 @@ export interface ChangelogEntry {
|
|
|
308
409
|
operation: 'created' | 'updated' | 'deleted';
|
|
309
410
|
timestamp: string;
|
|
310
411
|
commit_message: string;
|
|
412
|
+
/** The commit's hash: what tells the entries of one commit from another's. */
|
|
413
|
+
commit: string;
|
|
414
|
+
}
|
|
415
|
+
/** What a write did to a note, as history tells it. */
|
|
416
|
+
export type ChangeOp = typeof CHANGE_OPS[number];
|
|
417
|
+
/** A file history changed between two points, as the Journal names it:
|
|
418
|
+
* vault-relative and '/'-separated, any file (Layout decides which are notes). */
|
|
419
|
+
export interface FileChange {
|
|
420
|
+
path: string;
|
|
421
|
+
op: ChangeOp;
|
|
422
|
+
}
|
|
423
|
+
/** A note created, updated or deleted since a cursor (`changesSince`). */
|
|
424
|
+
export interface NoteChange {
|
|
425
|
+
slug: string;
|
|
426
|
+
kind: VaultKind;
|
|
427
|
+
op: ChangeOp;
|
|
428
|
+
}
|
|
429
|
+
/** What the graph server's `/events` pushes, one per poll that found
|
|
430
|
+
* changes (src/graph/live.ts); the page reads it too. */
|
|
431
|
+
export interface CommitEvent {
|
|
432
|
+
type: 'commit';
|
|
433
|
+
changes: NoteChange[];
|
|
434
|
+
}
|
|
435
|
+
/** One row of the graph server's `/activity` (#278): a note's run of
|
|
436
|
+
* consecutive writes, newest first. `op`, `kind` and `timestamp` are the
|
|
437
|
+
* newest write's; `name` is the slug once the note is gone; `agent` is the
|
|
438
|
+
* agent of the Entity's newest Session Log entry dated the write's local day,
|
|
439
|
+
* absent where the Vault records none. */
|
|
440
|
+
export interface ActivityRow {
|
|
441
|
+
slug: string;
|
|
442
|
+
kind: VaultKind;
|
|
443
|
+
op: ChangeOp;
|
|
444
|
+
timestamp: string;
|
|
445
|
+
name: string;
|
|
446
|
+
count: number;
|
|
447
|
+
agent?: string;
|
|
448
|
+
}
|
|
449
|
+
export interface ChangesSince {
|
|
450
|
+
/** The history point now: pass it back to get what changed after it.
|
|
451
|
+
* Empty when the Vault has no history yet. */
|
|
452
|
+
cursor: string;
|
|
453
|
+
changes: NoteChange[];
|
|
311
454
|
}
|
|
312
455
|
export interface BatchResult {
|
|
313
456
|
op: string;
|
|
@@ -342,7 +485,7 @@ export type BatchOperation = {
|
|
|
342
485
|
op: 'create';
|
|
343
486
|
kind: 'session';
|
|
344
487
|
name: string;
|
|
345
|
-
fields?:
|
|
488
|
+
fields?: CreateSessionFields;
|
|
346
489
|
} | {
|
|
347
490
|
op: 'capture';
|
|
348
491
|
name: string;
|
|
@@ -395,8 +538,10 @@ export interface GitConfig {
|
|
|
395
538
|
}
|
|
396
539
|
/**
|
|
397
540
|
* The persistence seam (see CONTEXT.md: Journal) — everything core needs
|
|
398
|
-
* from the history layer behind vault
|
|
399
|
-
*
|
|
541
|
+
* from the history layer: the commit behind every vault mutation, sync, and
|
|
542
|
+
* the history reads (changelog, file times, the changes since a cursor). Git
|
|
543
|
+
* satisfies it in production (src/git.ts); tests may inject an in-memory
|
|
544
|
+
* adapter.
|
|
400
545
|
*/
|
|
401
546
|
export interface Journal {
|
|
402
547
|
/** Persist one logical change: stage written+deleted paths (vault-relative,
|
|
@@ -411,12 +556,23 @@ export interface Journal {
|
|
|
411
556
|
pushed: boolean;
|
|
412
557
|
remote: boolean;
|
|
413
558
|
}>;
|
|
414
|
-
/** History-derived changelog for the indexed dirs since a date
|
|
415
|
-
*
|
|
416
|
-
|
|
559
|
+
/** History-derived changelog for the indexed dirs since a date ('' = all
|
|
560
|
+
* history), newest first; `limit` reads only the newest that many commits.
|
|
561
|
+
* Empty when the repo has no commits; any other git failure throws `GIT_READ_FAILED`. */
|
|
562
|
+
changelog(since: string, limit?: number): Promise<ChangelogEntry[]>;
|
|
417
563
|
/** Newest history timestamp per vault file (modified = any change, added = creation).
|
|
418
564
|
* Empty when the repo has no commits; any other git failure throws `GIT_READ_FAILED`. */
|
|
419
565
|
fileTimes(mode: 'modified' | 'added'): Promise<Map<string, string>>;
|
|
566
|
+
/** The history point now (`cursor`, '' before the first commit) and the
|
|
567
|
+
* indexed-dir files whose content differs between `from` and it, each with
|
|
568
|
+
* its net status across the whole span (a rename is a delete and a
|
|
569
|
+
* create). `from` unset: the cursor only, no files; `from` '': every file
|
|
570
|
+
* in history now is a create. An unknown `from` or any other git failure
|
|
571
|
+
* throws `GIT_READ_FAILED`. */
|
|
572
|
+
changesSince(from?: string): Promise<{
|
|
573
|
+
cursor: string;
|
|
574
|
+
files: FileChange[];
|
|
575
|
+
}>;
|
|
420
576
|
}
|
|
421
577
|
export interface HafezConfig {
|
|
422
578
|
vaultPath: string;
|
|
@@ -473,10 +629,24 @@ export interface Hafez {
|
|
|
473
629
|
* archive is reached from its entity on purpose, never found by search. */
|
|
474
630
|
archive(slug: string): Promise<string | null>;
|
|
475
631
|
validate(): Promise<ValidationReport>;
|
|
632
|
+
/** The Health pass: what placement would change, from the files on disk
|
|
633
|
+
* (never the index); applies nothing. */
|
|
634
|
+
health(): Promise<HealthReport>;
|
|
476
635
|
search(query: string, kind?: SearchKind): Promise<SearchResult[]>;
|
|
636
|
+
/** The Vault's structure: Entity and Knowledge nodes, Parent, Related (one
|
|
637
|
+
* per unordered pair) and Wiki-link edges among them; `layers` adds tag,
|
|
638
|
+
* domain and Session nodes with their edges. */
|
|
639
|
+
graph(opts?: GraphOpts): Promise<Graph>;
|
|
477
640
|
rebuildIndex(): Promise<void>;
|
|
478
641
|
stats(): Promise<VaultStats>;
|
|
479
|
-
|
|
642
|
+
/** The changes since a date ('' = all history), newest first; `limit`
|
|
643
|
+
* reads only the newest that many commits, a positive whole number
|
|
644
|
+
* (anything else is VALIDATION_FAILED). */
|
|
645
|
+
changelog(since: string, limit?: number): Promise<ChangelogEntry[]>;
|
|
646
|
+
/** The notes created, updated or deleted since `cursor`, from history, and
|
|
647
|
+
* the cursor to pass next time. No cursor: the current cursor and no
|
|
648
|
+
* changes, so a follower starts from now. */
|
|
649
|
+
changesSince(cursor?: string): Promise<ChangesSince>;
|
|
480
650
|
/** Release the SQLite index handle. Idempotent; the next call that needs
|
|
481
651
|
* the index reopens it. Call before deleting the vault: Windows cannot
|
|
482
652
|
* remove a directory holding an open file. */
|
package/dist/vault.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ParsedVaultFile } from './types.js';
|
|
1
|
+
import type { ParsedVaultFile, VaultKind } from './types.js';
|
|
2
2
|
export declare function slugify(name: string): string;
|
|
3
|
-
export type VaultKind
|
|
3
|
+
export type { VaultKind };
|
|
4
4
|
/** Kinds that live in the SQLite read index: every kind. Sessions joined in
|
|
5
5
|
* #35 — before that they were files-and-git only, for no recorded reason. */
|
|
6
6
|
export declare const INDEXED_KINDS: ReadonlyArray<VaultKind>;
|
|
@@ -22,6 +22,13 @@ export declare function classifyIndexedRelPath(relPath: string): {
|
|
|
22
22
|
kind: VaultKind;
|
|
23
23
|
slug: string;
|
|
24
24
|
} | null;
|
|
25
|
+
/** The note a vault-relative ('/'-separated) path is, or null: the files
|
|
26
|
+
* scanVaultDir lists — a non-dot `.md` directly in a kind dir. Archive logs,
|
|
27
|
+
* scaffold files and anything outside the kind dirs are not notes. */
|
|
28
|
+
export declare function noteAtRelPath(relPath: string): {
|
|
29
|
+
kind: VaultKind;
|
|
30
|
+
slug: string;
|
|
31
|
+
} | null;
|
|
25
32
|
/** All .md files directly in dir — non-recursive (archive/ subdirs are
|
|
26
33
|
* invisible by construction) and dotfile-free: dotfile slugs are rejected by
|
|
27
34
|
* resolveFilePath, so listing them would surface files no operation can reach. */
|