hafez 2.1.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 +72 -5
- package/dist/apply.d.ts +14 -1
- package/dist/apply.js +94 -32
- package/dist/batch-ops.d.ts +31 -27
- package/dist/batch-ops.js +83 -16
- package/dist/cli/block-hook.d.ts +6 -0
- package/dist/cli/block-hook.js +27 -12
- package/dist/cli/commands.d.ts +3 -1
- package/dist/cli/commands.js +183 -73
- package/dist/cli/format.d.ts +11 -3
- package/dist/cli/format.js +61 -4
- package/dist/cli/index.js +40 -19
- package/dist/cli/install.d.ts +9 -0
- package/dist/cli/install.js +36 -15
- package/dist/cli/onboard.js +30 -9
- package/dist/cli/upgrade.js +5 -4
- package/dist/contracts.d.ts +9 -0
- package/dist/contracts.js +9 -0
- package/dist/dates.d.ts +7 -0
- package/dist/dates.js +21 -1
- package/dist/db.d.ts +50 -10
- package/dist/db.js +251 -65
- 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 +121 -14
- 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 +66 -18
- package/dist/internal.d.ts +21 -0
- package/dist/internal.js +9 -0
- package/dist/knowledge-index.d.ts +1 -1
- package/dist/read-json.d.ts +26 -0
- package/dist/read-json.js +53 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +11 -0
- package/dist/stale.d.ts +2 -2
- package/dist/stale.js +4 -9
- package/dist/surfaces/items.d.ts +100 -1
- package/dist/surfaces/items.js +148 -10
- package/dist/surfaces/surfaces.d.ts +8 -5
- package/dist/surfaces/surfaces.js +34 -17
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +6 -1
- package/dist/types.d.ts +239 -25
- package/dist/validate.js +44 -56
- package/dist/vault.d.ts +29 -9
- package/dist/vault.js +39 -8
- package/dist/workstream.d.ts +17 -0
- package/dist/workstream.js +78 -0
- package/package.json +10 -4
- package/skill/hafez/SKILL.md +16 -7
package/dist/surfaces/items.js
CHANGED
|
@@ -162,6 +162,49 @@ 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
|
+
}
|
|
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
|
+
}
|
|
165
208
|
/** The line after an update's success while Stale lines remain on the entity. */
|
|
166
209
|
function renderStaleReminder({ handles }) {
|
|
167
210
|
const n = handles.length;
|
|
@@ -179,7 +222,10 @@ function renderUpdateMisuse(input) {
|
|
|
179
222
|
}
|
|
180
223
|
const CREATE_TYPES = ['project', 'entity'];
|
|
181
224
|
const RELATIONS = ['parent', 'related'];
|
|
182
|
-
|
|
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>"`;
|
|
183
229
|
const CREATE_LINES = fence([
|
|
184
230
|
createEntityLine('<name>'),
|
|
185
231
|
`hafez create knowledge "<name>" --synthesis "<the insight>" --confidence ${oneOf(CONFIDENCE_LEVELS)} --related <slug>`,
|
|
@@ -221,6 +267,38 @@ function renderTeachingLines({ harness, mode, line2Only = false }) {
|
|
|
221
267
|
'4. If a hafez command ever asks for permission, setup is unfinished: run `hafez onboard` again.',
|
|
222
268
|
].join('\n');
|
|
223
269
|
}
|
|
270
|
+
/** A GitHub `<owner>/<repo>`, as one shell word safe to print in a command
|
|
271
|
+
* and to name a directory by: not `.` or `..`, not a clone URL's `.git`. */
|
|
272
|
+
export const isVaultRepo = (value) => /^[A-Za-z0-9-]+\/[\w.-]+$/.test(value) && !/\/\.\.?$/.test(value) && !value.endsWith('.git');
|
|
273
|
+
/** Where a cloud container clones a repo attached to the session. */
|
|
274
|
+
const CLOUD_HOME = '/home/user';
|
|
275
|
+
const repoName = (vaultRepo) => vaultRepo.slice(vaultRepo.indexOf('/') + 1);
|
|
276
|
+
/** The Block's self-heal rule in a cloud container: a session started without
|
|
277
|
+
* the vault attached attaches, clones and registers it (verified, #160). */
|
|
278
|
+
function renderCloudVault(vaultRepo) {
|
|
279
|
+
const path = `${CLOUD_HOME}/${repoName(vaultRepo)}`;
|
|
280
|
+
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.`;
|
|
281
|
+
}
|
|
282
|
+
/** `onboard --cloud`: the cloud environment's setup script. It cannot clone
|
|
283
|
+
* (it runs without GitHub credentials) and no step may fail session start: a
|
|
284
|
+
* failed install exits 0, leaving a session without Hafez rather than none. */
|
|
285
|
+
function renderCloudSetup({ vaultRepo }) {
|
|
286
|
+
return [
|
|
287
|
+
`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.`,
|
|
288
|
+
'',
|
|
289
|
+
fence([
|
|
290
|
+
'#!/bin/bash',
|
|
291
|
+
'npm install -g hafez --allow-scripts=better-sqlite3 || exit 0',
|
|
292
|
+
`VAULT=${CLOUD_HOME}/${repoName(vaultRepo)}`,
|
|
293
|
+
'if [ -d "$VAULT/.git" ]; then',
|
|
294
|
+
' hafez init --register "$VAULT"',
|
|
295
|
+
'fi',
|
|
296
|
+
`hafez onboard --install --mode always-on --harness claude-code --vault-repo ${vaultRepo} || true`,
|
|
297
|
+
].join('\n')),
|
|
298
|
+
'',
|
|
299
|
+
`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.`,
|
|
300
|
+
].join('\n');
|
|
301
|
+
}
|
|
224
302
|
const BATCH_EXAMPLE_OP = '{"op":"create","kind":"entity","name":"Token Rotation","fields":{"type":"entity"}}';
|
|
225
303
|
export const ITEMS = {
|
|
226
304
|
loadTriggers: {
|
|
@@ -259,14 +337,44 @@ export const ITEMS = {
|
|
|
259
337
|
* in brackets when it has one. */
|
|
260
338
|
render: ({ slug, description }) => description ? `${slug} (${description})` : slug,
|
|
261
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
|
+
},
|
|
354
|
+
sessionsLine: {
|
|
355
|
+
label: 'Sessions line: the look back from an entity',
|
|
356
|
+
owner: 'readHeader',
|
|
357
|
+
invokeOnly: 'same',
|
|
358
|
+
evidence: '#35',
|
|
359
|
+
render: renderSessionsLine,
|
|
360
|
+
},
|
|
361
|
+
archivePointer: {
|
|
362
|
+
label: 'Archive pointer',
|
|
363
|
+
owner: 'readHeader',
|
|
364
|
+
invokeOnly: 'same',
|
|
365
|
+
evidence: '#34',
|
|
366
|
+
render: renderArchivePointer,
|
|
367
|
+
},
|
|
262
368
|
writeTriggers: {
|
|
263
|
-
label: 'Write triggers: work landed,
|
|
369
|
+
label: 'Write triggers: work landed, a split, decision made, a lesson, weighing, "note this"',
|
|
264
370
|
owner: 'block',
|
|
265
371
|
invokeOnly: 'absent',
|
|
266
|
-
evidence: 'M11, M2, never-ask decision #42',
|
|
372
|
+
evidence: 'M11, M2, never-ask decision #42, decision-record miss #159; placement #226, #229',
|
|
267
373
|
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.',
|
|
269
|
-
|
|
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')}`,
|
|
270
378
|
'- The user says "note this" or "record that": write it.',
|
|
271
379
|
`- You are weighing whether something is worth writing: write it, as knowledge with \`--confidence ${CONFIDENCE_LEVELS[0]}\`.`,
|
|
272
380
|
].join('\n'),
|
|
@@ -329,7 +437,7 @@ export const ITEMS = {
|
|
|
329
437
|
owner: 'block',
|
|
330
438
|
fallback: 'hafezSkillBody',
|
|
331
439
|
invokeOnly: 'via the skill body',
|
|
332
|
-
evidence: 'fields: kind/name 18, synthesis 16, subtype 15',
|
|
440
|
+
evidence: 'fields: kind/name 18, synthesis 16, subtype 15; --parent #230',
|
|
333
441
|
text: CREATE_LINES + '\n\nA plan is `--subtype plan` without `--synthesis`. Cannot tell entity from knowledge: `hafez capture "<name>"`.',
|
|
334
442
|
/** The entity create line for one name: what the `read` miss places. */
|
|
335
443
|
render: createEntityLine,
|
|
@@ -438,12 +546,14 @@ export const ITEMS = {
|
|
|
438
546
|
evidence: 'help decision #49',
|
|
439
547
|
text: fence([
|
|
440
548
|
`hafez query --filter ${oneOf([...ENTITY_STATUSES, 'stale', 'capture'])}`,
|
|
441
|
-
'hafez search "<terms>" --kind <entity|knowledge>',
|
|
549
|
+
'hafez search "<terms>" --kind <entity|knowledge|session|all>',
|
|
550
|
+
'hafez query --kind session --related-to <slug>',
|
|
442
551
|
'hafez stats',
|
|
443
|
-
].join('\n'))
|
|
552
|
+
].join('\n'))
|
|
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.',
|
|
444
554
|
},
|
|
445
555
|
rareOps: {
|
|
446
|
-
label: 'Rare ops: promote, link, unlink, status, delete, validate',
|
|
556
|
+
label: 'Rare ops: promote, link, unlink, status, delete, validate, health',
|
|
447
557
|
owner: 'hafezSkillBody',
|
|
448
558
|
invokeOnly: 'same',
|
|
449
559
|
evidence: 'help decision #49; owner decision #43',
|
|
@@ -454,8 +564,20 @@ export const ITEMS = {
|
|
|
454
564
|
`hafez update <slug> --status ${oneOf(ENTITY_STATUSES)}`,
|
|
455
565
|
'hafez delete <slug>',
|
|
456
566
|
'hafez validate',
|
|
567
|
+
'hafez health',
|
|
457
568
|
].join('\n'))
|
|
458
|
-
+ `\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'),
|
|
459
581
|
},
|
|
460
582
|
batchShape: {
|
|
461
583
|
label: 'Batch shape',
|
|
@@ -517,6 +639,22 @@ export const ITEMS = {
|
|
|
517
639
|
evidence: 'never-ask decision #42; onboard decision #58',
|
|
518
640
|
render: renderHarnessPermissions,
|
|
519
641
|
},
|
|
642
|
+
cloudVault: {
|
|
643
|
+
label: 'Cloud session: no vault registered, attach the vault repo, clone, register',
|
|
644
|
+
owner: 'block',
|
|
645
|
+
invokeOnly: 'absent',
|
|
646
|
+
evidence: '#160, verified 2026-10-16',
|
|
647
|
+
// Placed only in a Block composed for a cloud container, whose begin
|
|
648
|
+
// marker names the repo (renderBlock).
|
|
649
|
+
text: (_place, _at, { vaultRepo = '<owner>/<repo>' }) => renderCloudVault(vaultRepo),
|
|
650
|
+
},
|
|
651
|
+
cloudSetup: {
|
|
652
|
+
label: 'The cloud environment\'s setup script',
|
|
653
|
+
owner: 'onboard',
|
|
654
|
+
invokeOnly: 'not offered',
|
|
655
|
+
evidence: '#160, verified 2026-10-16',
|
|
656
|
+
render: renderCloudSetup,
|
|
657
|
+
},
|
|
520
658
|
teachingLines: {
|
|
521
659
|
label: 'The four teaching lines (to the user, not the agent)',
|
|
522
660
|
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
|
-
|
|
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
|
|
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:',
|
|
@@ -39,7 +40,7 @@ export const SURFACES = {
|
|
|
39
40
|
},
|
|
40
41
|
hafezSkillDescription: {
|
|
41
42
|
label: '`hafez` skill description',
|
|
42
|
-
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.',
|
|
43
44
|
},
|
|
44
45
|
hafezSkillBody: {
|
|
45
46
|
label: '`hafez` skill body',
|
|
@@ -67,6 +68,8 @@ export const SURFACES = {
|
|
|
67
68
|
'',
|
|
68
69
|
place('rareOps'),
|
|
69
70
|
'',
|
|
71
|
+
place('healthProcedure'),
|
|
72
|
+
'',
|
|
70
73
|
`${place('batchShape')} Machine-readable shape of any op: \`hafez schema <op>\`, with \`--examples\` for a sample.`,
|
|
71
74
|
'',
|
|
72
75
|
'## The tool itself',
|
|
@@ -96,11 +99,14 @@ export const SURFACES = {
|
|
|
96
99
|
schema: { label: '`schema`' },
|
|
97
100
|
onboard: { label: '`onboard`' },
|
|
98
101
|
};
|
|
102
|
+
/** Every context a static Surface is composed in: the default, and the
|
|
103
|
+
* Block for a cloud container. */
|
|
104
|
+
const CONTEXTS = [{}, { vaultRepo: '<owner>/<repo>' }];
|
|
99
105
|
export const contentHash = (text) => createHash('sha256').update(text).digest('hex').slice(0, 12);
|
|
100
106
|
/** Compose a static Surface, placing each Item's text in the form its role
|
|
101
107
|
* (Owner or fallback) calls for. `onPlace` sees every placement, nested ones
|
|
102
108
|
* included. */
|
|
103
|
-
function composeWith(items, surfaceId, surface, onPlace = () => { }) {
|
|
109
|
+
function composeWith(items, surfaceId, surface, ctx = {}, onPlace = () => { }) {
|
|
104
110
|
const place = (id) => {
|
|
105
111
|
onPlace(id);
|
|
106
112
|
const item = items[id];
|
|
@@ -108,16 +114,16 @@ function composeWith(items, surfaceId, surface, onPlace = () => { }) {
|
|
|
108
114
|
throw new Error(`${surfaceId} places unknown Item ${id}`);
|
|
109
115
|
const text = item.text;
|
|
110
116
|
if (typeof text === 'function')
|
|
111
|
-
return text(place, item.owner === surfaceId ? 'owner' : 'fallback');
|
|
117
|
+
return text(place, item.owner === surfaceId ? 'owner' : 'fallback', ctx);
|
|
112
118
|
if (text === undefined)
|
|
113
119
|
throw new Error(`${surfaceId} places ${id}, which has no text`);
|
|
114
120
|
return text;
|
|
115
121
|
};
|
|
116
122
|
if (!surface.compose)
|
|
117
123
|
throw new Error(`${surfaceId} is not a static surface`);
|
|
118
|
-
return surface.compose(place);
|
|
124
|
+
return surface.compose(place, ctx);
|
|
119
125
|
}
|
|
120
|
-
const compose = (id) => composeWith(ITEMS, id, SURFACES[id]);
|
|
126
|
+
const compose = (id, ctx = {}) => composeWith(ITEMS, id, SURFACES[id], ctx);
|
|
121
127
|
/** An Item's text as a binary Surface places it at run time, in the form its
|
|
122
128
|
* role there (Owner or fallback) calls for. */
|
|
123
129
|
export function placeIn(surfaceId, id) {
|
|
@@ -147,9 +153,13 @@ function skillFile(name, description, body) {
|
|
|
147
153
|
const text = compose(body);
|
|
148
154
|
return `---\nname: ${name}\ndescription: >\n${fold(compose(description))}\nhash: ${contentHash(text)}\n---\n${text}`;
|
|
149
155
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
/** The Block; with `vaultRepo`, the Block for a cloud container whose vault
|
|
157
|
+
* is that repo. Its begin marker names the repo, so a check reads it from
|
|
158
|
+
* there and never from the Block's wording, which a release may change. */
|
|
159
|
+
export function renderBlock(vaultRepo) {
|
|
160
|
+
const inner = compose('block', { vaultRepo });
|
|
161
|
+
const repo = vaultRepo === undefined ? '' : ` vault-repo=${vaultRepo}`;
|
|
162
|
+
return `<!-- hafez:begin ${contentHash(inner)}${repo} -->\n${inner}<!-- hafez:end -->\n`;
|
|
153
163
|
}
|
|
154
164
|
/** `onboard --block`, what the SessionStart hook prints: the Block, then the
|
|
155
165
|
* read line for each entity matching the folder. */
|
|
@@ -185,21 +195,28 @@ export function spliceOwnerTable(doc) {
|
|
|
185
195
|
}
|
|
186
196
|
// --- Drift check 2: the typed Item table ---
|
|
187
197
|
/** 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
|
|
189
|
-
* Surface or in a Surface that is neither its Owner nor its declared fallback.
|
|
198
|
+
* pointer, an Item its Owner or static fallback never places in any context, or one placed more
|
|
199
|
+
* than once in a Surface or in a Surface that is neither its Owner nor its declared fallback.
|
|
190
200
|
* Placement is checked for static Surfaces only; binary ones place at run time. */
|
|
191
201
|
export function itemProblems(items, surfaces) {
|
|
192
202
|
const problems = [];
|
|
203
|
+
// Per Surface, the most times each Item is placed in one of its contexts.
|
|
193
204
|
const placed = {};
|
|
194
205
|
for (const [sid, surface] of Object.entries(surfaces)) {
|
|
195
206
|
if (!surface.compose)
|
|
196
207
|
continue;
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
const most = (placed[sid] = {});
|
|
209
|
+
for (const ctx of CONTEXTS) {
|
|
210
|
+
const counts = {};
|
|
211
|
+
try {
|
|
212
|
+
composeWith(items, sid, surface, ctx, id => { counts[id] = (counts[id] ?? 0) + 1; });
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
if (!problems.includes(err.message))
|
|
216
|
+
problems.push(err.message);
|
|
217
|
+
}
|
|
218
|
+
for (const [id, n] of Object.entries(counts))
|
|
219
|
+
most[id] = Math.max(most[id] ?? 0, n);
|
|
203
220
|
}
|
|
204
221
|
}
|
|
205
222
|
for (const [id, item] of Object.entries(items)) {
|
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
|
}
|