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
package/dist/vault.js
CHANGED
|
@@ -47,6 +47,16 @@ export function classifyIndexedRelPath(relPath) {
|
|
|
47
47
|
}
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
|
+
/** The note a vault-relative ('/'-separated) path is, or null: the files
|
|
51
|
+
* scanVaultDir lists — a non-dot `.md` directly in a kind dir. Archive logs,
|
|
52
|
+
* scaffold files and anything outside the kind dirs are not notes. */
|
|
53
|
+
export function noteAtRelPath(relPath) {
|
|
54
|
+
const match = relPath.match(/^([^/]+)\/([^/]+)\.md$/);
|
|
55
|
+
if (!match || match[2].startsWith('.'))
|
|
56
|
+
return null;
|
|
57
|
+
const kind = INDEXED_KINDS.find(k => KIND_DIRS[k] === match[1]);
|
|
58
|
+
return kind ? { kind, slug: match[2] } : null;
|
|
59
|
+
}
|
|
50
60
|
/** All .md files directly in dir — non-recursive (archive/ subdirs are
|
|
51
61
|
* invisible by construction) and dotfile-free: dotfile slugs are rejected by
|
|
52
62
|
* resolveFilePath, so listing them would surface files no operation can reach. */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The family lines fire at: Number A's threshold. */
|
|
2
|
+
export declare const FAMILY_MIN_LINES = 3;
|
|
3
|
+
/** The family a Next Actions line opens with, or null. */
|
|
4
|
+
export declare function familyOf(text: string): string | null;
|
|
5
|
+
/** One firing family and its open-line count, as `read --json` gives it. */
|
|
6
|
+
export interface Workstream {
|
|
7
|
+
prefix: string;
|
|
8
|
+
open_actions: number;
|
|
9
|
+
}
|
|
10
|
+
/** The `[a-z0-9]+` word tokens of a slug and name, lowercased. */
|
|
11
|
+
export declare const wordTokens: (text: string) => Set<string>;
|
|
12
|
+
/** The families an entity body carries unsplit, in first-appearance order,
|
|
13
|
+
* against the entities whose `parent` or `related` names it (`naming`). */
|
|
14
|
+
export declare function workstreams(body: string, naming: Array<{
|
|
15
|
+
slug: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}>): Workstream[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/workstream.ts
|
|
2
|
+
// Workstream (see CONTEXT.md): a prefix family on an entity's Next Actions
|
|
3
|
+
// with no Child or Related entity carrying it, Number A of the
|
|
4
|
+
// workstream-signal research (#230, #240). The reference definition is
|
|
5
|
+
// tools/research/ws_caps_prefix.py; the Health pass shares this module.
|
|
6
|
+
//
|
|
7
|
+
// A line's family is its leading run of ALL-CAPS tokens (two or more
|
|
8
|
+
// characters; digits, hyphen, apostrophe allowed; a `WS4`/`WS-A` code is one
|
|
9
|
+
// such token), minus a trailing step id (`A3`, `B1`); a lone step id is not
|
|
10
|
+
// a family; a trailing possessive `'S` is stripped from a token. A family
|
|
11
|
+
// fires at three or more lines, open or ticked; it stops once an entity
|
|
12
|
+
// whose `parent` or `related` names this one carries a family token among
|
|
13
|
+
// the word tokens of its slug or name. A code family (`WS4`, `WS-A`) stops
|
|
14
|
+
// on its whole code only — a segment of the slug (`ws-a-dynamics`) or a
|
|
15
|
+
// word of the name (`WS-A dynamics`) — never on `ws` or `a` alone. The
|
|
16
|
+
// count reported is the family's OPEN lines — what is left to split — so a
|
|
17
|
+
// family with none does not render at all.
|
|
18
|
+
import { getActionItems } from './document.js';
|
|
19
|
+
const TOKEN = "[A-Z][A-Z0-9'’\\-]+";
|
|
20
|
+
const PREFIX_RE = new RegExp(`^((?:${TOKEN})(?: ${TOKEN})*)(?=[,:;]|\\s[—–-]\\s|\\s|$)`);
|
|
21
|
+
const STEP_RE = /^[A-Z]\d+$/;
|
|
22
|
+
const POSSESSIVE_RE = /['’]S$/;
|
|
23
|
+
const CODE_RE = /^WS(\d+|-[A-Z])$/;
|
|
24
|
+
/** The family lines fire at: Number A's threshold. */
|
|
25
|
+
export const FAMILY_MIN_LINES = 3;
|
|
26
|
+
/** The family a Next Actions line opens with, or null. */
|
|
27
|
+
export function familyOf(text) {
|
|
28
|
+
const m = PREFIX_RE.exec(text);
|
|
29
|
+
if (!m)
|
|
30
|
+
return null;
|
|
31
|
+
const toks = m[1].split(' ').map(t => t.replace(POSSESSIVE_RE, ''));
|
|
32
|
+
while (toks.length > 1 && STEP_RE.test(toks[toks.length - 1]))
|
|
33
|
+
toks.pop();
|
|
34
|
+
const fam = toks.join(' ');
|
|
35
|
+
if (STEP_RE.test(fam))
|
|
36
|
+
return null;
|
|
37
|
+
return fam.length >= 2 ? fam : null;
|
|
38
|
+
}
|
|
39
|
+
/** The `[a-z0-9]+` word tokens of a slug and name, lowercased. */
|
|
40
|
+
export const wordTokens = (text) => new Set(text.toLowerCase().match(/[a-z0-9]+/g) ?? []);
|
|
41
|
+
/** Whether the entity carries the whole lowercased code: as a segment of
|
|
42
|
+
* its slug, or as a word of its name (hyphen kept). */
|
|
43
|
+
function carriesCode(e, code) {
|
|
44
|
+
return `-${e.slug.toLowerCase()}-`.includes(`-${code}-`)
|
|
45
|
+
|| (e.name.toLowerCase().match(/[a-z0-9]+(?:-[a-z0-9]+)*/g) ?? []).some(w => w === code);
|
|
46
|
+
}
|
|
47
|
+
/** The families an entity body carries unsplit, in first-appearance order,
|
|
48
|
+
* against the entities whose `parent` or `related` names it (`naming`). */
|
|
49
|
+
export function workstreams(body, naming) {
|
|
50
|
+
const families = new Map();
|
|
51
|
+
for (const item of getActionItems(body)) {
|
|
52
|
+
const fam = familyOf(item.text);
|
|
53
|
+
if (fam === null)
|
|
54
|
+
continue;
|
|
55
|
+
const f = families.get(fam) ?? { lines: 0, open: 0 };
|
|
56
|
+
f.lines++;
|
|
57
|
+
if ('handle' in item)
|
|
58
|
+
f.open++;
|
|
59
|
+
families.set(fam, f);
|
|
60
|
+
}
|
|
61
|
+
const named = naming.map(e => wordTokens(`${e.slug} ${e.name}`));
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const [prefix, f] of families) {
|
|
64
|
+
if (f.lines < FAMILY_MIN_LINES || f.open === 0)
|
|
65
|
+
continue;
|
|
66
|
+
if (CODE_RE.test(prefix)) {
|
|
67
|
+
if (naming.some(e => carriesCode(e, prefix.toLowerCase())))
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const tokens = Array.from(wordTokens(prefix));
|
|
72
|
+
if (named.some(set => tokens.some(t => set.has(t))))
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
out.push({ prefix, open_actions: f.open });
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hafez",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"LICENSE"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
|
20
|
+
"build": "rm -rf dist && tsc && esbuild src/graph/client/main.ts --bundle --minify --format=iife --target=es2020 --log-level=warning --outfile=dist/graph/client.js && node dist/graph/copy-fonts.js && chmod +x dist/cli.js",
|
|
21
21
|
"install:local": "chmod +x \"$(pwd)/dist/cli.js\" && mkdir -p ~/.local/bin && ln -sf \"$(pwd)/dist/cli.js\" ~/.local/bin/hafez && for d in ~/.claude/skills ~/.agents/skills; do for s in hafez hafez-save; do mkdir -p \"$d/$s\" && cp \"$(pwd)/skill/$s/SKILL.md\" \"$d/$s/SKILL.md\"; done; done",
|
|
22
|
-
"typecheck": "tsc -p tsconfig.test.json",
|
|
22
|
+
"typecheck": "tsc -p tsconfig.test.json && tsc -p src/graph/client/tsconfig.json",
|
|
23
23
|
"pretest": "npm run build && npm run typecheck",
|
|
24
24
|
"test": "vitest run",
|
|
25
25
|
"test:watch": "vitest",
|
|
@@ -34,10 +34,16 @@
|
|
|
34
34
|
"zod": "^4.3.6"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
+
"@fontsource-variable/geist": "^5.3.0",
|
|
38
|
+
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
39
|
+
"@fontsource-variable/outfit": "^5.3.0",
|
|
37
40
|
"@types/better-sqlite3": "^7.6.13",
|
|
38
41
|
"@types/js-yaml": "^4.0.9",
|
|
39
42
|
"@types/node": "^25.4.0",
|
|
40
43
|
"@types/proper-lockfile": "^4.1.4",
|
|
44
|
+
"esbuild": "^0.28.2",
|
|
45
|
+
"force-graph": "^1.52.0",
|
|
46
|
+
"markdown-it": "^15.0.3",
|
|
41
47
|
"tsx": "^4.21.0",
|
|
42
48
|
"typescript": "^5.9.3",
|
|
43
49
|
"vitest": "^4.0.18"
|
package/skill/hafez/SKILL.md
CHANGED
|
@@ -3,10 +3,11 @@ name: hafez
|
|
|
3
3
|
description: >
|
|
4
4
|
Hafez vault: the less common operations and the tool itself. Use when the
|
|
5
5
|
user asks what is in the vault ("what am I working on?", "what's stale?"),
|
|
6
|
-
wants to promote, link, unlink, delete, change a status or validate,
|
|
7
|
-
wants to update or reconfigure Hafez
|
|
8
|
-
Also the reference to read when no
|
|
9
|
-
|
|
6
|
+
wants to promote, link, unlink, delete, change a status or validate, asks
|
|
7
|
+
to check the vault or clean it up, or wants to update or reconfigure Hafez
|
|
8
|
+
("upgrade hafez", "set up hafez"). Also the reference to read when no
|
|
9
|
+
Hafez block is in your instructions.
|
|
10
|
+
hash: c52f079e667f
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# Hafez
|
|
@@ -22,7 +23,7 @@ Open actions on an entity you read are yours: tick what landed, drop what is dea
|
|
|
22
23
|
## Create
|
|
23
24
|
|
|
24
25
|
```
|
|
25
|
-
hafez create entity "<name>" --type <project|entity> --description "<one sentence>" --brief "<where things stand>" --add-action "<next step>"
|
|
26
|
+
hafez create entity "<name>" --type <project|entity> --parent <slug you read, or its parent> --description "<one sentence>" --brief "<where things stand>" --add-action "<next step>"
|
|
26
27
|
hafez create knowledge "<name>" --synthesis "<the insight>" --confidence <observation|pattern|principle> --related <slug>
|
|
27
28
|
```
|
|
28
29
|
|
|
@@ -48,9 +49,14 @@ hafez unlink <slug> <target> --relation <parent|related>
|
|
|
48
49
|
hafez update <slug> --status <active|paused|done>
|
|
49
50
|
hafez delete <slug>
|
|
50
51
|
hafez validate
|
|
52
|
+
hafez health
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
A plan's status is `active|done`. `delete` removes a knowledge or session note only; an entity is retired with `--status done`.
|
|
55
|
+
A plan's status is `active|done`. `delete` removes a knowledge or session note only; an entity is retired with `--status done`. `health` lists what placement would change and applies nothing.
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
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.
|
|
59
|
+
Apply nothing until the user confirms a kind, not even a `link` line that looks safe; then one `batch` per kind.
|
|
54
60
|
|
|
55
61
|
Several writes that must land together go in one batch: a bare array, no wrapper. A slug is the name lowercased, apostrophes dropped, each other run of characters that are not letters or digits turned into one `-` and none left at either end, so a later op can name what an earlier one creates.
|
|
56
62
|
|