hafez 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +83 -32
  2. package/dist/apply.d.ts +33 -0
  3. package/dist/apply.js +693 -0
  4. package/dist/batch-ops.d.ts +372 -26
  5. package/dist/batch-ops.js +355 -73
  6. package/dist/cli/args.d.ts +48 -6
  7. package/dist/cli/args.js +74 -9
  8. package/dist/cli/batch-errors.js +19 -11
  9. package/dist/cli/block-hook.d.ts +9 -0
  10. package/dist/cli/block-hook.js +90 -0
  11. package/dist/cli/commands-schema.js +2 -0
  12. package/dist/cli/commands.d.ts +28 -3
  13. package/dist/cli/commands.js +440 -157
  14. package/dist/cli/format.d.ts +7 -3
  15. package/dist/cli/format.js +27 -10
  16. package/dist/cli/index.d.ts +55 -0
  17. package/dist/cli/index.js +215 -153
  18. package/dist/cli/install.d.ts +53 -0
  19. package/dist/cli/install.js +297 -0
  20. package/dist/cli/onboard.d.ts +15 -2
  21. package/dist/cli/onboard.js +265 -90
  22. package/dist/cli/resolve-vault.d.ts +11 -4
  23. package/dist/cli/resolve-vault.js +13 -11
  24. package/dist/cli/schema-introspect.js +4 -3
  25. package/dist/cli/upgrade.d.ts +51 -0
  26. package/dist/cli/upgrade.js +351 -0
  27. package/dist/closest.d.ts +8 -0
  28. package/dist/closest.js +65 -0
  29. package/dist/contracts.d.ts +65 -6
  30. package/dist/contracts.js +148 -23
  31. package/dist/dates.d.ts +10 -0
  32. package/dist/dates.js +27 -0
  33. package/dist/db.d.ts +40 -2
  34. package/dist/db.js +363 -66
  35. package/dist/digest.d.ts +6 -4
  36. package/dist/digest.js +28 -17
  37. package/dist/document.d.ts +135 -88
  38. package/dist/document.js +665 -379
  39. package/dist/export-okf.d.ts +13 -0
  40. package/dist/export-okf.js +50 -20
  41. package/dist/git.js +165 -48
  42. package/dist/harness.d.ts +58 -0
  43. package/dist/harness.js +50 -0
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.js +301 -826
  46. package/dist/knowledge-index.d.ts +23 -1
  47. package/dist/knowledge-index.js +62 -6
  48. package/dist/merge.d.ts +1 -1
  49. package/dist/merge.js +98 -64
  50. package/dist/near-match.d.ts +9 -0
  51. package/dist/near-match.js +34 -0
  52. package/dist/schema.js +4 -4
  53. package/dist/stale.d.ts +41 -0
  54. package/dist/stale.js +43 -0
  55. package/dist/stopwords.d.ts +6 -0
  56. package/dist/stopwords.js +25 -0
  57. package/dist/surfaces/items.d.ts +366 -0
  58. package/dist/surfaces/items.js +523 -0
  59. package/dist/surfaces/surfaces.d.ts +32 -0
  60. package/dist/surfaces/surfaces.js +230 -0
  61. package/dist/templates.d.ts +6 -0
  62. package/dist/templates.js +27 -27
  63. package/dist/types.d.ts +78 -22
  64. package/dist/validate.d.ts +7 -0
  65. package/dist/validate.js +109 -0
  66. package/dist/vault.d.ts +18 -0
  67. package/dist/vault.js +51 -5
  68. package/package.json +4 -3
  69. package/skill/hafez/SKILL.md +68 -0
  70. package/skill/hafez-save/SKILL.md +29 -0
  71. package/dist/cli/help-agent.d.ts +0 -1
  72. package/dist/cli/help-agent.js +0 -186
  73. package/dist/query.d.ts +0 -24
  74. package/dist/query.js +0 -169
  75. package/skill/SKILL.md +0 -196
package/dist/digest.js CHANGED
@@ -1,9 +1,13 @@
1
1
  // src/digest.ts
2
+ import { createHash } from 'crypto';
2
3
  import { z } from 'zod';
4
+ import { HafezError } from './types.js';
5
+ import { detectAgent } from './harness.js';
6
+ import { ITEMS } from './surfaces/items.js';
3
7
  // --- Schema ---
4
8
  export const DigestInputSchema = z.object({
5
9
  entities_touched: z.array(z.string())
6
- .describe('Slugs of entities worked on. Each gets a session log entry; the session note links them all. Unknown slugs warn on stderr and are omitted.'),
10
+ .describe('Slugs of entities worked on. Each gets a session log entry; the session note links them all. An unknown slug fails the digest and is named.'),
7
11
  decisions: z.array(z.string())
8
12
  .describe('Key decisions made this session (may be empty). Recorded in the session note.')
9
13
  .default([]),
@@ -12,8 +16,8 @@ export const DigestInputSchema = z.object({
12
16
  session_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Must be YYYY-MM-DD')
13
17
  .describe('Session date, YYYY-MM-DD.'),
14
18
  agent: z.string()
15
- .describe('Agent identifier recorded in session logs.')
16
- .default('claude'),
19
+ .describe('Agent identifier recorded in session logs. Detected from the Harness when absent.')
20
+ .optional(),
17
21
  });
18
22
  /**
19
23
  * Validate and parse raw digest input from stdin.
@@ -39,27 +43,32 @@ export function parseDigestInput(raw) {
39
43
  /**
40
44
  * Convert session context into a hafez batch payload.
41
45
  * Pure function — no file I/O, no vault access.
42
- * Unknown slugs in entities_touched are silently omitted (caller should warn).
46
+ * An unknown slug in entities_touched is a NOT_FOUND naming it (and only the
47
+ * unknown ones), never a session log that points nowhere. A knowledge slug is
48
+ * known: the note links it, but a session log is entity-only.
43
49
  */
44
- export function digest(input, existingSlugs, now = new Date()) {
50
+ export function digest(input, entitySlugs, knowledgeSlugs = new Set()) {
51
+ const touched = [...new Set(input.entities_touched)];
52
+ const unknown = touched.filter(s => !entitySlugs.has(s) && !knowledgeSlugs.has(s));
53
+ if (unknown.length > 0)
54
+ throw new HafezError('NOT_FOUND', ITEMS.unknownSlugAtSave.render(unknown));
45
55
  const ops = [];
46
- // 1. Session log updates for each known slug
47
- const knownTouched = input.entities_touched.filter(s => existingSlugs.has(s));
48
- for (const slug of knownTouched) {
56
+ const agent = input.agent || detectAgent();
57
+ // 1. Session log updates for each touched entity
58
+ for (const slug of touched.filter(s => entitySlugs.has(s))) {
49
59
  const sessionLogSummary = buildSessionLogSummary(input);
50
60
  const fields = {
51
61
  session_log: {
52
62
  type: 'progress',
53
63
  summary: sessionLogSummary,
54
- agent: input.agent,
64
+ agent,
55
65
  },
56
66
  };
57
67
  ops.push({ op: 'update', slug, fields });
58
68
  }
59
- // 2. Session note — built from known slugs only, so unknown slugs never
60
- // leak into the note body (the schema promises they are omitted).
61
- const noteBody = buildSessionNoteBody(input, knownTouched);
62
- const related = knownTouched;
69
+ // 2. Session note
70
+ const noteBody = buildSessionNoteBody(input, touched);
71
+ const related = touched;
63
72
  // Typechecked against the Op Spec's session-create schema so digest output
64
73
  // can never silently diverge from what batch validation accepts.
65
74
  const noteFields = {
@@ -67,7 +76,9 @@ export function digest(input, existingSlugs, now = new Date()) {
67
76
  'session-date': input.session_date,
68
77
  ...(related.length > 0 ? { related } : {}),
69
78
  };
70
- const noteName = `Session ${input.session_date} ${now.toISOString().slice(11, 19)}`;
79
+ // Named by the session date and its note, so a second save of the same
80
+ // session derives the same slug and batch skips it as a duplicate.
81
+ const noteName = `Session ${input.session_date} ${createHash('sha256').update(noteBody).digest('hex').slice(0, 8)}`;
71
82
  ops.push({ op: 'create', kind: 'session', name: noteName, fields: noteFields });
72
83
  return ops;
73
84
  }
@@ -79,7 +90,7 @@ function buildSessionLogSummary(input) {
79
90
  }
80
91
  return parts.join(' | ');
81
92
  }
82
- function buildSessionNoteBody(input, knownTouched) {
93
+ function buildSessionNoteBody(input, touched) {
83
94
  const lines = [input.narrative.trim()];
84
95
  if (input.decisions.length > 0) {
85
96
  lines.push('');
@@ -88,9 +99,9 @@ function buildSessionNoteBody(input, knownTouched) {
88
99
  lines.push(`- ${d}`);
89
100
  }
90
101
  }
91
- if (knownTouched.length > 0) {
102
+ if (touched.length > 0) {
92
103
  lines.push('');
93
- lines.push(`**Entities touched:** ${knownTouched.join(', ')}`);
104
+ lines.push(`**Entities touched:** ${touched.join(', ')}`);
94
105
  }
95
106
  return lines.join('\n');
96
107
  }
@@ -1,96 +1,151 @@
1
- import type { SessionLogEntry, SessionLogType } from './types.js';
2
- /**
3
- * Known structural section headings in Hafez entities, knowledge notes, and
4
- * session files. Derived from contracts.ts (defaultSections + optionalSections
5
- * + session sections) plus auto-generated sections not tracked in contracts
6
- * (Related). Only these are treated as section boundaries. Any other `## `
7
- * heading inside section content (e.g. in a brief or session log) is
8
- * preserved as-is.
9
- */
10
- export declare const STRUCTURAL_SECTIONS: Set<string>;
11
- /**
12
- * Find the next structural `## Heading` boundary after a given position.
13
- * Skips `## ` headings that are not known structural sections (content headings).
14
- * Returns the index of the `\n` before the `## `, or -1 if no structural heading found.
15
- */
16
- export declare function findNextStructuralHeading(body: string, from: number): number;
17
- /**
18
- * Find a ## Heading section and its content.
19
- * Returns { start, headingEnd, end, content } or null if not found.
20
- * Matches heading exactly (e.g. "## Brief" won't match "## Briefing").
21
- * Trailing whitespace on the heading line is tolerated — the splitter's
22
- * boundary tokenizer trims headings, so anything it treats as a boundary
23
- * must also be findable here (a hand-edited `## Session Log ` otherwise
24
- * gets a duplicate section and archival never fires).
25
- * Only treats known structural headings as section boundaries -- content
26
- * inside a section may contain arbitrary `## ` headings without corruption.
27
- */
28
- export declare function findSection(body: string, heading: string): {
29
- start: number;
30
- headingEnd: number;
31
- end: number;
32
- content: string;
33
- } | null;
34
- export interface DocSection {
35
- heading: string;
36
- content: string;
37
- }
38
- /**
39
- * Split a body into preamble + structural sections. Non-structural `## `
40
- * headings stay inside the enclosing section's content — sections are
41
- * atomic units of authored content, never split at user headings.
42
- * Headings and content come back trimmed; reassemble with
43
- * `## ${heading}\n\n${content}` joined by blank lines.
44
- */
45
- export declare function splitStructural(body: string): {
46
- preamble: string;
47
- sections: DocSection[];
1
+ import { type SessionLogEntry, type SessionLogType } from './types.js';
2
+ /** The action text without its trailing added-date stamp, if any. */
3
+ export declare function stripActionStamp(text: string): string;
4
+ /** The day an action's stamp names; null when the line is undated. */
5
+ export declare function actionAdded(text: string): string | null;
6
+ /** Stamp an action with its added date; text that already ends in a stamp is
7
+ * returned as is, so a carried-over line is never double-stamped. */
8
+ export declare function stampAction(text: string, day: string): string;
9
+ /** One checkbox line of Next Actions as a reader sees it: unstamped text, and
10
+ * a Handle and added day (null when undated) when the line is open. */
11
+ export type ActionItem = {
12
+ handle: string;
13
+ text: string;
14
+ added: string | null;
15
+ } | {
16
+ handle?: undefined;
17
+ text: string;
48
18
  };
49
- /**
50
- * Replace a structural section's content, or create the section: before
51
- * `insertBefore` when that section exists, at EOF otherwise.
52
- */
53
- export declare function setSection(body: string, heading: string, content: string, insertBefore?: string): string;
54
- /** Append content at the end of a structural section (creating it at EOF if absent). */
55
- export declare function appendToSection(body: string, heading: string, content: string): string;
56
- /** Insert a formatted session-log entry newest-first (creating the section at EOF if absent). */
57
- export declare function prependSessionLogEntry(body: string, entry: string): string;
58
- /** Body up to the real Session Log section — the `summary` read depth. */
59
- export declare function bodyBeforeSessionLog(body: string): string;
19
+ /** The open action a query named: its line's position within the Next
20
+ * Actions section, and its unstamped text. Editing by position rather than
21
+ * re-matching the text keeps a Handle-named line unambiguous when its text
22
+ * is a substring of another line's, or a duplicate of one. */
23
+ export interface ActionMatch {
24
+ line: number;
25
+ text: string;
26
+ }
60
27
  export declare function parseSessionLogHeading(line: string): {
61
28
  date: string;
62
29
  agent: string;
63
30
  type: SessionLogType;
64
31
  } | null;
65
32
  export declare function formatSessionLogEntry(entry: SessionLogEntry): string;
66
- /** Entries inside the real Session Log section only — entry-shaped lines
67
- * elsewhere in the body (e.g. quoted in a Brief) do not count. */
68
- export declare function countSessionLogEntries(body: string): number;
33
+ /**
34
+ * Split a Session Log section's content into raw entry blocks, each beginning
35
+ * with a parseable entry heading. Non-entry text before the first heading is
36
+ * not an entry and is dropped. Document owns entry block splitting — merge's
37
+ * dedup/sort policy stays in merge.ts.
38
+ */
39
+ export declare function splitLogEntries(content: string): string[];
40
+ export declare function firstNonEmptyLine(s: string | null | undefined): string;
41
+ /** The one wiki-link syntax the vault recognizes: [[slug]] or
42
+ * [[slug|display text]] — slug in group 1, display text in group 2.
43
+ * Padding inside the brackets is Obsidian-idiomatic ([[ slug ]], [[slug | Label]])
44
+ * and is absorbed by the pattern, so both captures come out already trimmed —
45
+ * every consumer (Related coverage, mention harvesting, export) sees the bare slug.
46
+ * Non-global — wrap with `new RegExp(WIKI_LINK_RE.source, 'g')` for iteration. */
47
+ export declare const WIKI_LINK_RE: RegExp;
48
+ /** What documentFor needs to know about a file to bind its section set.
49
+ *
50
+ * KIND is the whole context — deliberately no type/subtype narrowing. A file
51
+ * can legitimately carry sections outside its current type's contract (a
52
+ * promote race merged in a capture's Notes, a plan-shaped note never had its
53
+ * subtype persisted, a hand edit added Open Questions to a plain entity), and
54
+ * a narrower binding demotes those headings to non-structural text that the
55
+ * next section rewrite silently deletes. The kind-level union makes every
56
+ * same-kind heading a boundary, so no same-kind section is ever swallowed;
57
+ * the cost is that a same-kind heading quoted at line start inside a section
58
+ * strands as its own section on rewrite — visible and recoverable, unlike
59
+ * deletion. Cross-KIND headings (a session-only ## Decisions quoted in an
60
+ * entity Brief) are still not boundaries. */
61
+ export interface DocContext {
62
+ kind: 'entity' | 'knowledge' | 'session';
63
+ }
64
+ /** The structural set a context binds — exposed for introspection and the
65
+ * contract-coverage tests; runtime consumers go through documentFor. */
66
+ export declare function structuralSectionsFor(ctx: DocContext): ReadonlySet<string>;
67
+ /** The Document operations bound to one file's structural section set. */
68
+ export declare function documentFor(ctx: DocContext): Doc;
69
+ /** Bind from a parsed vault file — kind from Layout. */
70
+ export declare function documentForFile(parsed: {
71
+ kind: 'entity' | 'knowledge' | 'session';
72
+ }): Doc;
73
+ export interface DocSection {
74
+ heading: string;
75
+ content: string;
76
+ }
77
+ export interface Doc {
78
+ findNextStructuralHeading(body: string, from: number): number;
79
+ findSection(body: string, heading: string): {
80
+ start: number;
81
+ headingEnd: number;
82
+ end: number;
83
+ content: string;
84
+ } | null;
85
+ splitStructural(body: string): {
86
+ preamble: string;
87
+ sections: DocSection[];
88
+ };
89
+ setSection(body: string, heading: string, content: string, insertBefore?: string): string;
90
+ appendToSection(body: string, heading: string, content: string): string;
91
+ renameSection(body: string, from: string, to: string): string;
92
+ ensureSection(body: string, heading: string, opts?: {
93
+ after?: string;
94
+ }): string;
95
+ prependSessionLogEntry(body: string, entry: string): string;
96
+ bodyBeforeSessionLog(body: string): string;
97
+ countSessionLogEntries(body: string): number;
98
+ parseSessionLog(body: string): Array<{
99
+ date: string;
100
+ agent: string;
101
+ type: SessionLogType;
102
+ summary: string;
103
+ }>;
104
+ extractOldestSessionLogEntry(body: string): {
105
+ entry: string;
106
+ remaining: string;
107
+ } | null;
108
+ getBrief(body: string): string | null;
109
+ setBrief(body: string, content: string): string;
110
+ removeBrief(body: string): string;
111
+ getNextActions(body: string): string[];
112
+ getActionItems(body: string): ActionItem[];
113
+ renderActionHandles(body: string, mark?: (added: string | null) => string): string;
114
+ addNextAction(body: string, action: string): string;
115
+ matchNextAction(body: string, query: string): ActionMatch;
116
+ completeNextAction(body: string, action: string | ActionMatch): {
117
+ matched: string;
118
+ body: string;
119
+ };
120
+ removeNextAction(body: string, action: string | ActionMatch): {
121
+ matched: string;
122
+ line: string;
123
+ body: string;
124
+ };
125
+ actionLines(body: string): string[];
126
+ extractOldestTickedActions(body: string, keep: number): {
127
+ lines: string[];
128
+ body: string;
129
+ };
130
+ pruneNextActions(body: string): string;
131
+ clearNextActions(body: string): string;
132
+ stripNextActionStamps(body: string): string;
133
+ syncRelatedSection(body: string, frontmatter: {
134
+ parent?: string;
135
+ related?: string[];
136
+ }): string;
137
+ }
138
+ export declare function getBrief(body: string): string | null;
139
+ export declare function getNextActions(body: string): string[];
140
+ export declare function getActionItems(body: string): ActionItem[];
141
+ export declare function renderActionHandles(body: string, mark?: (added: string | null) => string): string;
69
142
  export declare function parseSessionLog(body: string): Array<{
70
143
  date: string;
71
144
  agent: string;
72
145
  type: SessionLogType;
73
146
  summary: string;
74
147
  }>;
75
- export declare function extractOldestSessionLogEntry(body: string): {
76
- entry: string;
77
- remaining: string;
78
- } | null;
79
- export declare function getBrief(body: string): string | null;
80
- export declare function setBrief(body: string, content: string): string;
81
- export declare function removeBrief(body: string): string;
82
- export declare function getNextActions(body: string): string[];
83
- export declare function addNextAction(body: string, action: string): string;
84
- export declare function completeNextAction(body: string, query: string): {
85
- matched: string;
86
- body: string;
87
- };
88
- export declare function removeNextAction(body: string, query: string): {
89
- matched: string;
90
- body: string;
91
- };
92
- export declare function clearNextActions(body: string): string;
93
- export declare function firstNonEmptyLine(s: string | null | undefined): string;
148
+ export declare function countSessionLogEntries(body: string): number;
94
149
  /**
95
150
  * One-line summary for a doc: frontmatter description first, then the first
96
151
  * non-empty line of the kind's primary section (entity: Brief; knowledge:
@@ -98,11 +153,3 @@ export declare function firstNonEmptyLine(s: string | null | undefined): string;
98
153
  * Shared by the vault index and the OKF export so the two surfaces agree.
99
154
  */
100
155
  export declare function deriveSummary(kind: 'entity' | 'knowledge' | 'session', fm: Record<string, any>, body: string): string;
101
- /** The one wiki-link syntax the vault recognizes: [[slug]] or
102
- * [[slug|display text]] — slug in group 1, display text in group 2.
103
- * Non-global — wrap with `new RegExp(WIKI_LINK_RE.source, 'g')` for iteration. */
104
- export declare const WIKI_LINK_RE: RegExp;
105
- export declare function syncRelatedSection(body: string, frontmatter: {
106
- parent?: string;
107
- related?: string[];
108
- }): string;