hafez 1.4.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.
- package/README.md +26 -33
- package/dist/apply.d.ts +5 -2
- package/dist/apply.js +225 -114
- package/dist/batch-ops.d.ts +89 -26
- package/dist/batch-ops.js +176 -18
- package/dist/cli/args.d.ts +43 -5
- package/dist/cli/args.js +69 -8
- package/dist/cli/batch-errors.js +19 -11
- package/dist/cli/block-hook.d.ts +9 -0
- package/dist/cli/block-hook.js +90 -0
- package/dist/cli/commands-schema.js +2 -0
- package/dist/cli/commands.d.ts +4 -3
- package/dist/cli/commands.js +300 -99
- package/dist/cli/format.d.ts +7 -3
- package/dist/cli/format.js +27 -10
- package/dist/cli/index.d.ts +18 -4
- package/dist/cli/index.js +72 -30
- package/dist/cli/install.d.ts +53 -0
- package/dist/cli/install.js +297 -0
- package/dist/cli/onboard.d.ts +15 -2
- package/dist/cli/onboard.js +265 -90
- package/dist/cli/resolve-vault.d.ts +11 -4
- package/dist/cli/resolve-vault.js +13 -11
- package/dist/cli/schema-introspect.js +4 -3
- package/dist/cli/upgrade.d.ts +13 -4
- package/dist/cli/upgrade.js +90 -44
- package/dist/closest.d.ts +8 -0
- package/dist/closest.js +65 -0
- package/dist/contracts.d.ts +17 -1
- package/dist/contracts.js +44 -0
- package/dist/db.d.ts +15 -2
- package/dist/db.js +69 -48
- package/dist/digest.d.ts +6 -4
- package/dist/digest.js +28 -17
- package/dist/document.d.ts +41 -3
- package/dist/document.js +218 -62
- package/dist/export-okf.d.ts +13 -0
- package/dist/export-okf.js +11 -7
- package/dist/harness.d.ts +58 -0
- package/dist/harness.js +50 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +94 -104
- package/dist/merge.js +7 -5
- package/dist/near-match.d.ts +9 -0
- package/dist/near-match.js +34 -0
- package/dist/stale.d.ts +41 -0
- package/dist/stale.js +43 -0
- package/dist/stopwords.d.ts +6 -0
- package/dist/stopwords.js +25 -0
- package/dist/surfaces/items.d.ts +366 -0
- package/dist/surfaces/items.js +523 -0
- package/dist/surfaces/surfaces.d.ts +32 -0
- package/dist/surfaces/surfaces.js +230 -0
- package/dist/types.d.ts +45 -16
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +109 -0
- package/dist/vault.js +1 -1
- package/package.json +4 -3
- package/skill/hafez/SKILL.md +68 -0
- package/skill/hafez-save/SKILL.md +29 -0
- package/dist/cli/help-agent.d.ts +0 -1
- package/dist/cli/help-agent.js +0 -210
- package/skill/SKILL.md +0 -137
package/dist/cli/format.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** `parent` is the rendered value of the `parent:` line (the slug with the
|
|
2
|
+
* parent's description); absent, the slug alone. */
|
|
3
|
+
export function formatEntityHeader(fm, body, parent = fm.parent) {
|
|
2
4
|
const lines = [`# ${fm.name}`];
|
|
3
5
|
if (fm.description)
|
|
4
6
|
lines.push(`> ${fm.description}`);
|
|
@@ -11,9 +13,10 @@ export function formatEntityHeader(fm, body) {
|
|
|
11
13
|
meta.push(`domain: ${fm.domain.join(', ')}`);
|
|
12
14
|
if (meta.length)
|
|
13
15
|
lines.push(meta.join(' | '));
|
|
16
|
+
// The parent line stands alone: with its description it is a sentence.
|
|
17
|
+
if (parent)
|
|
18
|
+
lines.push(`parent: ${parent}`);
|
|
14
19
|
const meta2 = [];
|
|
15
|
-
if (fm.parent)
|
|
16
|
-
meta2.push(`parent: ${fm.parent}`);
|
|
17
20
|
if (fm['last-touched'])
|
|
18
21
|
meta2.push(`last-touched: ${fm['last-touched']}`);
|
|
19
22
|
if (fm.created)
|
|
@@ -34,18 +37,33 @@ export function formatKnowledgeHeader(fm, body) {
|
|
|
34
37
|
const lines = [`# ${fm.name}`];
|
|
35
38
|
if (fm.description)
|
|
36
39
|
lines.push(`> ${fm.description}`);
|
|
40
|
+
// A plan has a lifecycle and no reinforcement: its subtype, status and
|
|
41
|
+
// dates; an insight its confidence and reinforcement.
|
|
42
|
+
const plan = fm.subtype === 'plan';
|
|
37
43
|
const meta = [];
|
|
38
|
-
if (
|
|
44
|
+
if (plan)
|
|
45
|
+
meta.push('subtype: plan');
|
|
46
|
+
if (plan && fm.status)
|
|
47
|
+
meta.push(`status: ${fm.status}`);
|
|
48
|
+
if (!plan && fm.confidence)
|
|
39
49
|
meta.push(`confidence: ${fm.confidence}`);
|
|
40
50
|
if (fm.domain?.length)
|
|
41
51
|
meta.push(`domain: ${Array.isArray(fm.domain) ? fm.domain.join(', ') : fm.domain}`);
|
|
42
52
|
if (meta.length)
|
|
43
53
|
lines.push(meta.join(' | '));
|
|
44
54
|
const meta2 = [];
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
if (plan) {
|
|
56
|
+
if (fm.created)
|
|
57
|
+
meta2.push(`created: ${fm.created}`);
|
|
58
|
+
if (fm['last-touched'])
|
|
59
|
+
meta2.push(`last-touched: ${fm['last-touched']}`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
if (fm['reinforcement-count'] != null)
|
|
63
|
+
meta2.push(`reinforcement-count: ${fm['reinforcement-count']}`);
|
|
64
|
+
if (fm['last-reinforced'])
|
|
65
|
+
meta2.push(`last-reinforced: ${fm['last-reinforced']}`);
|
|
66
|
+
}
|
|
49
67
|
if (meta2.length)
|
|
50
68
|
lines.push(meta2.join(' | '));
|
|
51
69
|
if (fm.related?.length)
|
|
@@ -143,7 +161,7 @@ export function formatChangelog(entries) {
|
|
|
143
161
|
}
|
|
144
162
|
export function formatValidation(report) {
|
|
145
163
|
const issues = report.broken_slugs.length + report.orphaned_knowledge.length +
|
|
146
|
-
report.
|
|
164
|
+
report.missing_fields.length;
|
|
147
165
|
if (issues === 0) {
|
|
148
166
|
return `Vault OK — ${report.total_entities} entities, ${report.total_knowledge} knowledge, 0 issues`;
|
|
149
167
|
}
|
|
@@ -157,7 +175,6 @@ export function formatValidation(report) {
|
|
|
157
175
|
};
|
|
158
176
|
addSection('Broken Slugs', report.broken_slugs.map(i => `${i.slug}: ${i.issue}`));
|
|
159
177
|
addSection('Orphaned Knowledge', report.orphaned_knowledge.map(s => `${s} (no related links, not referenced by any entity)`));
|
|
160
|
-
addSection('Oversized Related', report.oversized_related.map(i => `${i.slug}: ${i.issue}`));
|
|
161
178
|
addSection('Missing Fields', report.missing_fields.map(i => `${i.slug}: ${i.issue}`));
|
|
162
179
|
return sections.join('\n');
|
|
163
180
|
}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { HafezError, type HafezErrorCode } from '../types.js';
|
|
2
|
+
import { UsageError } from './args.js';
|
|
1
3
|
import type { CommandOpts } from './commands.js';
|
|
2
4
|
type PreVaultRun = (args: string[], ctx: {
|
|
3
5
|
json: boolean;
|
|
@@ -13,15 +15,24 @@ export type CommandSpec = {
|
|
|
13
15
|
run: VaultRun;
|
|
14
16
|
};
|
|
15
17
|
export declare const COMMANDS: Record<string, CommandSpec>;
|
|
16
|
-
/**
|
|
17
|
-
*
|
|
18
|
-
export declare const USAGE = "Usage: hafez <command> [args]\n\nCommands:\n read <slug> Read entity or knowledge note\n query [--filter ...] ... Filter entities/knowledge\n --kind all Both kinds; --filter matches entity status only,\n so knowledge notes (which have none) always show\n search <terms> Full-text search\n create <kind> <name> Create entity or knowledge note\n capture <name> [--notes] Quick inbox capture\n promote <slug> <target> Promote capture\u2192entity/project/knowledge, entity\u2192project\n update <slug> [--status] Update an entity or knowledge note\n --brief \"text\" Set/replace Brief (use --brief \"\" to clear)\n --description \"text\" Set one-line description (use --description \"\" to clear)\n --resource \"uri\" Set canonical resource URI, entities only (use --resource \"\" to clear)\n --add-action \"text\" Add a next action\n --complete-action \"text\" Complete matching action\n --remove-action \"text\" Remove matching action\n --clear-actions Remove all next actions\n --synthesis \"text\" Set/replace Synthesis section (--insight is an alias)\n --add-evidence \"text\" Append to Evidence section\n --add-source \"text\" Append to Sources section\n batch Apply multiple operations atomically (JSON on stdin)\n digest Apply digest input (JSON stdin) \u2192 batch payload (JSON stdout)\n export --okf [--out <dir>] Export vault as an OKF v0.1 bundle (read-only)\n sync Pull remote changes and push local commits\n link <slug> <target> --relation <rel> Add a link\n unlink <slug> <target> --relation <rel> Remove a link\n stats Vault summary: counts, stale items, recents\n changelog --since <when> Git-derived change history (e.g. --since 7.days.ago)\n schema [op] [--examples] Machine-readable JSON schema for batch operations\n validate Check vault integrity\n index rebuild Rebuild the SQLite index\n init --register <path> Register an existing vault\n init Show current vault resolution\n onboard Agent-directed first run: seed the vault, pick integration level\n upgrade Check whether hafez is outdated and print the update steps\n help --agent Full API reference for agents\n\nGlobal flags:\n --vault <path> Vault path (overrides config)\n --json JSON output (structured, machine-readable)\n --version Print the hafez version\n\nVault discovery: --vault flag > ~/.config/hafez/vault\n";
|
|
19
|
-
/** Exit codes, documented in `hafez help --agent`. 0 = success; 4 is the
|
|
18
|
+
/** The one exit-code table. The error → exit adapter maps HafezError codes
|
|
19
|
+
* through `errorCodes`; `hafez --help` prints every row. 4 is the
|
|
20
20
|
* mutate-succeeded-sync-failed warning path; anything unmapped = 1. */
|
|
21
|
+
export declare const EXIT_TABLE: ReadonlyArray<{
|
|
22
|
+
exit: number;
|
|
23
|
+
meaning: string;
|
|
24
|
+
errorCodes?: readonly HafezErrorCode[];
|
|
25
|
+
}>;
|
|
21
26
|
export declare const EXIT_CODES: Record<string, number>;
|
|
27
|
+
/** Exported so a guard test can assert every registered command is documented
|
|
28
|
+
* here — a registered-but-undocumented command is otherwise invisible. */
|
|
29
|
+
export declare const USAGE: string;
|
|
22
30
|
/** The pure half of the error → exit adapter: which code an error maps to.
|
|
23
31
|
* Exported so the exit-code contract is testable without spawning the CLI. */
|
|
24
32
|
export declare function exitCodeFor(err: unknown): number;
|
|
33
|
+
/** The text half of the error adapter: what it prints for a usage or vault
|
|
34
|
+
* error. A misuse ends with the shape to retry from. */
|
|
35
|
+
export declare function errorText(err: UsageError | HafezError): string;
|
|
25
36
|
/** Does this error mean the native module failed to load/build?
|
|
26
37
|
*
|
|
27
38
|
* Matched on the MESSAGE only, and only on the messages a load/ABI failure
|
|
@@ -38,5 +49,8 @@ export declare function extractGlobalFlags(args: string[]): {
|
|
|
38
49
|
vaultFlag?: string;
|
|
39
50
|
json: boolean;
|
|
40
51
|
};
|
|
52
|
+
/** Is this the SessionStart hook's form, `onboard --block`? Read from the raw
|
|
53
|
+
* args, the token after `--vault` skipped, so it holds when parsing them fails. */
|
|
54
|
+
export declare function isBlockHook(args: string[]): boolean;
|
|
41
55
|
export declare function main(argv: string[]): Promise<void>;
|
|
42
56
|
export {};
|
package/dist/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { readFileSync } from 'fs';
|
|
|
10
10
|
import { dirname, join } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
import { HafezError } from '../types.js';
|
|
13
|
-
import { UsageError } from './args.js';
|
|
13
|
+
import { HelpRequest, MisuseError, RenderedError, UsageError, helpIfAsked } from './args.js';
|
|
14
14
|
/** Bind a vault command to its handler without importing the handler module
|
|
15
15
|
* until the command actually runs. */
|
|
16
16
|
function lazy(pick) {
|
|
@@ -18,8 +18,8 @@ function lazy(pick) {
|
|
|
18
18
|
}
|
|
19
19
|
export const COMMANDS = {
|
|
20
20
|
// --- pre-vault ---
|
|
21
|
-
help: { phase: 'pre-vault', run: async (args) => (args[0] === '--agent' ? (await import('
|
|
22
|
-
onboard: { phase: 'pre-vault', run: async () => (await import('./onboard.js')).
|
|
21
|
+
help: { phase: 'pre-vault', run: async (args) => (args[0] === '--agent' ? (await import('../surfaces/surfaces.js')).renderAgentHelp() : USAGE) },
|
|
22
|
+
onboard: { phase: 'pre-vault', run: async (args, ctx) => (await import('./onboard.js')).cmdOnboard(args, { vaultFlag: ctx.vaultFlag }) },
|
|
23
23
|
schema: { phase: 'pre-vault', run: async (args) => (await import('./commands-schema.js')).cmdSchema(null, args) },
|
|
24
24
|
version: { phase: 'pre-vault', run: () => versionLine() },
|
|
25
25
|
init: { phase: 'pre-vault', run: async (args, ctx) => { (await import('./resolve-vault.js')).cmdInit(args, ctx.vaultFlag); return null; } },
|
|
@@ -38,14 +38,17 @@ export const COMMANDS = {
|
|
|
38
38
|
link: { phase: 'vault', mutating: true, run: lazy(m => m.cmdLink) },
|
|
39
39
|
unlink: { phase: 'vault', mutating: true, run: lazy(m => m.cmdUnlink) },
|
|
40
40
|
promote: { phase: 'vault', mutating: true, run: lazy(m => m.cmdPromote) },
|
|
41
|
+
delete: { phase: 'vault', mutating: true, run: lazy(m => m.cmdDelete) },
|
|
41
42
|
batch: { phase: 'vault', mutating: true, run: lazy(m => m.cmdBatch) },
|
|
42
43
|
validate: { phase: 'vault', run: lazy(m => m.cmdValidate) },
|
|
43
44
|
index: {
|
|
44
45
|
phase: 'vault',
|
|
45
46
|
run: async (os, args) => {
|
|
47
|
+
const usage = 'Usage: hafez index rebuild';
|
|
48
|
+
helpIfAsked(args, usage);
|
|
46
49
|
if (args[0] === 'rebuild')
|
|
47
50
|
return (await import('./commands.js')).cmdIndexRebuild(os);
|
|
48
|
-
throw new UsageError(
|
|
51
|
+
throw new UsageError(usage);
|
|
49
52
|
},
|
|
50
53
|
},
|
|
51
54
|
stats: { phase: 'vault', run: lazy(m => m.cmdStats) },
|
|
@@ -53,6 +56,20 @@ export const COMMANDS = {
|
|
|
53
56
|
digest: { phase: 'vault', run: lazy(m => m.cmdDigest) },
|
|
54
57
|
export: { phase: 'vault', run: lazy(m => m.cmdExport) },
|
|
55
58
|
};
|
|
59
|
+
/** The one exit-code table. The error → exit adapter maps HafezError codes
|
|
60
|
+
* through `errorCodes`; `hafez --help` prints every row. 4 is the
|
|
61
|
+
* mutate-succeeded-sync-failed warning path; anything unmapped = 1. */
|
|
62
|
+
export const EXIT_TABLE = [
|
|
63
|
+
{ exit: 0, meaning: 'success' },
|
|
64
|
+
{ exit: 1, meaning: 'not found (slug, command or vault) / unknown error', errorCodes: ['NOT_FOUND'] },
|
|
65
|
+
{ exit: 2, meaning: 'validation or usage error (bad flags, bad payload, core rejected the write)', errorCodes: ['VALIDATION_FAILED'] },
|
|
66
|
+
{ exit: 3, meaning: 'git push failed — the write IS committed locally', errorCodes: ['GIT_PUSH_FAILED'] },
|
|
67
|
+
{ exit: 4, meaning: 'write succeeded but the auto-sync after it failed' },
|
|
68
|
+
{ exit: 5, meaning: 'slug already exists', errorCodes: ['SLUG_EXISTS'] },
|
|
69
|
+
{ exit: 6, meaning: 'git commit failed — nothing was written to history', errorCodes: ['GIT_COMMIT_FAILED'] },
|
|
70
|
+
{ exit: 7, meaning: 'vault locked by another hafez process — retry when it finishes', errorCodes: ['VAULT_LOCKED'] },
|
|
71
|
+
];
|
|
72
|
+
export const EXIT_CODES = Object.fromEntries(EXIT_TABLE.flatMap(row => (row.errorCodes ?? []).map(code => [code, row.exit])));
|
|
56
73
|
/** Exported so a guard test can assert every registered command is documented
|
|
57
74
|
* here — a registered-but-undocumented command is otherwise invisible. */
|
|
58
75
|
export const USAGE = `Usage: hafez <command> [args]
|
|
@@ -66,19 +83,20 @@ Commands:
|
|
|
66
83
|
create <kind> <name> Create entity or knowledge note
|
|
67
84
|
capture <name> [--notes] Quick inbox capture
|
|
68
85
|
promote <slug> <target> Promote capture→entity/project/knowledge, entity→project
|
|
86
|
+
delete <slug> Delete a knowledge or session note (an entity: update --status done)
|
|
69
87
|
update <slug> [--status] Update an entity or knowledge note
|
|
70
88
|
--brief "text" Set/replace Brief (use --brief "" to clear)
|
|
71
89
|
--description "text" Set one-line description (use --description "" to clear)
|
|
72
90
|
--resource "uri" Set canonical resource URI, entities only (use --resource "" to clear)
|
|
73
91
|
--add-action "text" Add a next action
|
|
74
|
-
--complete-action
|
|
75
|
-
--remove-action
|
|
92
|
+
--complete-action handle-or-text Complete the action with that Handle (as read shows it), else the matching one
|
|
93
|
+
--remove-action handle-or-text Remove the action with that Handle, else the matching one
|
|
76
94
|
--clear-actions Remove all next actions
|
|
77
95
|
--synthesis "text" Set/replace Synthesis section (--insight is an alias)
|
|
78
96
|
--add-evidence "text" Append to Evidence section
|
|
79
97
|
--add-source "text" Append to Sources section
|
|
80
|
-
batch Apply multiple operations atomically (JSON on stdin)
|
|
81
|
-
digest Apply digest input (JSON stdin) → batch payload (JSON stdout)
|
|
98
|
+
batch Apply multiple operations atomically (JSON on stdin, --data or --file)
|
|
99
|
+
digest Apply digest input (JSON on stdin, --data or --file) → batch payload (JSON stdout)
|
|
82
100
|
export --okf [--out <dir>] Export vault as an OKF v0.1 bundle (read-only)
|
|
83
101
|
sync Pull remote changes and push local commits
|
|
84
102
|
link <slug> <target> --relation <rel> Add a link
|
|
@@ -90,9 +108,14 @@ Commands:
|
|
|
90
108
|
index rebuild Rebuild the SQLite index
|
|
91
109
|
init --register <path> Register an existing vault
|
|
92
110
|
init Show current vault resolution
|
|
93
|
-
onboard
|
|
111
|
+
onboard Setup status, then the script your agent follows to set Hafez up
|
|
112
|
+
--install --mode <always-on|invoke-only> [--harness <h>] [--confirm]
|
|
113
|
+
Write both skills and, always-on, the Block
|
|
114
|
+
--remove [--confirm] Remove the skills and the Block region
|
|
115
|
+
--block [--harness <h>] Print the Block and the entities matching this folder
|
|
116
|
+
(the SessionStart hook; prints nothing on any failure)
|
|
94
117
|
upgrade Check whether hafez is outdated and print the update steps
|
|
95
|
-
help --agent
|
|
118
|
+
help --agent The hafez skill body: the reference when the skill is not installed
|
|
96
119
|
|
|
97
120
|
Global flags:
|
|
98
121
|
--vault <path> Vault path (overrides config)
|
|
@@ -100,39 +123,45 @@ Global flags:
|
|
|
100
123
|
--version Print the hafez version
|
|
101
124
|
|
|
102
125
|
Vault discovery: --vault flag > ~/.config/hafez/vault
|
|
126
|
+
|
|
127
|
+
Exit codes:
|
|
128
|
+
${EXIT_TABLE.map(row => ` ${row.exit} ${row.meaning}`).join('\n')}
|
|
103
129
|
`;
|
|
104
|
-
/** Exit codes, documented in `hafez help --agent`. 0 = success; 4 is the
|
|
105
|
-
* mutate-succeeded-sync-failed warning path; anything unmapped = 1. */
|
|
106
|
-
export const EXIT_CODES = {
|
|
107
|
-
NOT_FOUND: 1,
|
|
108
|
-
VALIDATION_FAILED: 2,
|
|
109
|
-
GIT_PUSH_FAILED: 3,
|
|
110
|
-
SLUG_EXISTS: 5,
|
|
111
|
-
GIT_COMMIT_FAILED: 6,
|
|
112
|
-
VAULT_LOCKED: 7,
|
|
113
|
-
};
|
|
114
130
|
/** The pure half of the error → exit adapter: which code an error maps to.
|
|
115
131
|
* Exported so the exit-code contract is testable without spawning the CLI. */
|
|
116
132
|
export function exitCodeFor(err) {
|
|
133
|
+
if (err instanceof HelpRequest)
|
|
134
|
+
return 0;
|
|
117
135
|
if (err instanceof UsageError)
|
|
118
136
|
return 2;
|
|
119
137
|
if (err instanceof HafezError)
|
|
120
138
|
return EXIT_CODES[err.code] ?? 1;
|
|
121
139
|
return 1;
|
|
122
140
|
}
|
|
141
|
+
/** The text half of the error adapter: what it prints for a usage or vault
|
|
142
|
+
* error. A misuse ends with the shape to retry from. */
|
|
143
|
+
export function errorText(err) {
|
|
144
|
+
const lines = [`Error: ${err.message}`];
|
|
145
|
+
if (err instanceof HafezError)
|
|
146
|
+
for (const detail of err.details ?? [])
|
|
147
|
+
lines.push(` - ${detail}`);
|
|
148
|
+
if (err instanceof MisuseError)
|
|
149
|
+
lines.push('', err.shape);
|
|
150
|
+
return lines.join('\n') + '\n';
|
|
151
|
+
}
|
|
123
152
|
/** The one error → exit adapter. Usage errors (bad flags/args) are distinct
|
|
124
153
|
* from vault errors and from unknown crashes. */
|
|
125
154
|
function report(err) {
|
|
126
|
-
if (err instanceof
|
|
127
|
-
process.
|
|
155
|
+
if (err instanceof HelpRequest) {
|
|
156
|
+
process.stdout.write(err.usage + '\n');
|
|
128
157
|
process.exit(exitCodeFor(err));
|
|
129
158
|
}
|
|
130
|
-
if (err instanceof
|
|
131
|
-
process.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
159
|
+
if (err instanceof RenderedError) {
|
|
160
|
+
process[err.stream].write(err.output + '\n');
|
|
161
|
+
process.exit(exitCodeFor(err));
|
|
162
|
+
}
|
|
163
|
+
if (err instanceof UsageError || err instanceof HafezError) {
|
|
164
|
+
process.stderr.write(errorText(err));
|
|
136
165
|
process.exit(exitCodeFor(err));
|
|
137
166
|
}
|
|
138
167
|
failUnknown(err);
|
|
@@ -202,7 +231,16 @@ export function extractGlobalFlags(args) {
|
|
|
202
231
|
}
|
|
203
232
|
return { rest, vaultFlag, json };
|
|
204
233
|
}
|
|
234
|
+
/** Is this the SessionStart hook's form, `onboard --block`? Read from the raw
|
|
235
|
+
* args, the token after `--vault` skipped, so it holds when parsing them fails. */
|
|
236
|
+
export function isBlockHook(args) {
|
|
237
|
+
const command = args.find((a, i) => !a.startsWith('-') && args[i - 1] !== '--vault');
|
|
238
|
+
return command === 'onboard' && args.includes('--block');
|
|
239
|
+
}
|
|
205
240
|
export async function main(argv) {
|
|
241
|
+
// The hook must never block a session: under `onboard --block` any failure
|
|
242
|
+
// at all, a bad flag included, prints nothing and exits 0 (#132).
|
|
243
|
+
const hook = isBlockHook(argv.slice(2));
|
|
206
244
|
try {
|
|
207
245
|
const { rest: args, vaultFlag, json } = extractGlobalFlags(argv.slice(2));
|
|
208
246
|
const command = args.shift();
|
|
@@ -211,9 +249,11 @@ export async function main(argv) {
|
|
|
211
249
|
return;
|
|
212
250
|
}
|
|
213
251
|
const name = command === '--version' || command === '-v' ? 'version' : command;
|
|
214
|
-
const spec = COMMANDS[name];
|
|
252
|
+
const spec = Object.hasOwn(COMMANDS, name) ? COMMANDS[name] : undefined;
|
|
215
253
|
if (!spec) {
|
|
216
|
-
|
|
254
|
+
// `list` is the command agents guess for listing the vault (#30).
|
|
255
|
+
const hint = command === 'list' ? '. To list the vault: hafez query --filter active' : '';
|
|
256
|
+
process.stderr.write(`Unknown command: ${command}${hint}\n${USAGE}`);
|
|
217
257
|
process.exit(1);
|
|
218
258
|
}
|
|
219
259
|
if (spec.phase === 'pre-vault') {
|
|
@@ -256,6 +296,8 @@ export async function main(argv) {
|
|
|
256
296
|
process.stdout.write(result + '\n');
|
|
257
297
|
}
|
|
258
298
|
catch (err) {
|
|
299
|
+
if (hook)
|
|
300
|
+
return;
|
|
259
301
|
report(err);
|
|
260
302
|
}
|
|
261
303
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type HarnessId } from '../harness.js';
|
|
2
|
+
export declare const MODES: readonly ["always-on", "invoke-only"];
|
|
3
|
+
export type Mode = (typeof MODES)[number];
|
|
4
|
+
export declare const isMode: (value: string) => value is Mode;
|
|
5
|
+
export declare const SKILL_DIRS: string[];
|
|
6
|
+
export type HashState = 'absent' | 'current' | 'stale' | 'hand-edited';
|
|
7
|
+
/** What to do about broken markers: a hand edit, then `rerun`. */
|
|
8
|
+
export declare const fixBrokenMarkers: (path: string, rerun: string) => string;
|
|
9
|
+
/** Codex reads an `AGENTS.override.md` in place of `AGENTS.md` when one
|
|
10
|
+
* exists: the first candidate present, else the last. */
|
|
11
|
+
export declare const instructionsFile: (home: string, candidates: readonly string[]) => string;
|
|
12
|
+
export interface InstallState {
|
|
13
|
+
/** In the Harness's instructions file; null where it keeps no file.
|
|
14
|
+
* `broken`: markers `--install` and `--remove` refuse until fixed by hand. */
|
|
15
|
+
block: HashState | 'broken' | null;
|
|
16
|
+
/** Across both skill directories: the one state every file shares, else
|
|
17
|
+
* hand-edited when any file is, else stale. */
|
|
18
|
+
skills: HashState;
|
|
19
|
+
/** Some skill file carries a hash: `onboard --install` has run here. */
|
|
20
|
+
onboarded: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function installState(home: string, harness: HarnessId): InstallState;
|
|
23
|
+
export declare function install(opts: {
|
|
24
|
+
home: string;
|
|
25
|
+
harness: HarnessId;
|
|
26
|
+
mode: Mode;
|
|
27
|
+
confirm: boolean;
|
|
28
|
+
}): string;
|
|
29
|
+
export interface SkillCheck {
|
|
30
|
+
path: string;
|
|
31
|
+
state: HashState;
|
|
32
|
+
}
|
|
33
|
+
export interface BlockCheck {
|
|
34
|
+
harness: HarnessId;
|
|
35
|
+
path: string;
|
|
36
|
+
/** `broken`: markers `--install` would refuse until fixed by hand. */
|
|
37
|
+
state: Exclude<HashState, 'absent'> | 'broken';
|
|
38
|
+
/** The Block Hafez writes has changed since this one was written. */
|
|
39
|
+
renderChanged: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Both skills in both directories, and the Block in the instructions file
|
|
42
|
+
* each Harness reads — the one `--install` would write. Only Blocks that are
|
|
43
|
+
* there are listed: none means invoke-only. */
|
|
44
|
+
export declare function checkInstall(home: string): {
|
|
45
|
+
skills: SkillCheck[];
|
|
46
|
+
blocks: BlockCheck[];
|
|
47
|
+
};
|
|
48
|
+
/** Remove everything Hafez wrote: both skills from both directories, and the
|
|
49
|
+
* Block region from every instructions file a Harness reads. Nothing else. */
|
|
50
|
+
export declare function remove({ home, confirm }: {
|
|
51
|
+
home: string;
|
|
52
|
+
confirm: boolean;
|
|
53
|
+
}): string;
|