heedvane 0.0.0-stage → 0.1.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 (94) hide show
  1. package/README.md +27 -2
  2. package/dist/apply.d.ts +104 -0
  3. package/dist/apply.js +388 -0
  4. package/dist/backend/client.d.ts +40 -0
  5. package/dist/backend/client.js +32 -0
  6. package/dist/backend/credentials.d.ts +15 -0
  7. package/dist/backend/credentials.js +76 -0
  8. package/dist/backend/login.d.ts +33 -0
  9. package/dist/backend/login.js +265 -0
  10. package/dist/backend/repo-remote.d.ts +15 -0
  11. package/dist/backend/repo-remote.js +61 -0
  12. package/dist/backend/sign-in-change.d.ts +12 -0
  13. package/dist/backend/sign-in-change.js +72 -0
  14. package/dist/best-effort.d.ts +4 -0
  15. package/dist/best-effort.js +25 -0
  16. package/dist/cli.d.ts +24 -0
  17. package/dist/cli.js +544 -0
  18. package/dist/clients/definitions.d.ts +113 -0
  19. package/dist/clients/definitions.js +427 -0
  20. package/dist/clients/hermes-plugin.d.ts +33 -0
  21. package/dist/clients/hermes-plugin.js +247 -0
  22. package/dist/commands/heedvane.d.ts +15 -0
  23. package/dist/commands/heedvane.js +74 -0
  24. package/dist/commands/memory.d.ts +10 -0
  25. package/dist/commands/memory.js +106 -0
  26. package/dist/config-doc.d.ts +97 -0
  27. package/dist/config-doc.js +465 -0
  28. package/dist/dispatchers/posix.d.ts +32 -0
  29. package/dist/dispatchers/posix.js +191 -0
  30. package/dist/dispatchers/snippets.d.ts +6 -0
  31. package/dist/dispatchers/snippets.js +39 -0
  32. package/dist/dispatchers/windows.d.ts +15 -0
  33. package/dist/dispatchers/windows.js +50 -0
  34. package/dist/doctor.d.ts +37 -0
  35. package/dist/doctor.js +316 -0
  36. package/dist/events/contract.d.ts +54 -0
  37. package/dist/events/contract.js +87 -0
  38. package/dist/events/normalize.d.ts +36 -0
  39. package/dist/events/normalize.js +143 -0
  40. package/dist/guards.d.ts +18 -0
  41. package/dist/guards.js +28 -0
  42. package/dist/heed-file.d.ts +67 -0
  43. package/dist/heed-file.js +295 -0
  44. package/dist/listener/endpoint.d.ts +21 -0
  45. package/dist/listener/endpoint.js +112 -0
  46. package/dist/listener/forward.d.ts +36 -0
  47. package/dist/listener/forward.js +257 -0
  48. package/dist/listener/serve.d.ts +2 -0
  49. package/dist/listener/serve.js +155 -0
  50. package/dist/listener/server.d.ts +48 -0
  51. package/dist/listener/server.js +251 -0
  52. package/dist/listener/spool.d.ts +28 -0
  53. package/dist/listener/spool.js +60 -0
  54. package/dist/mcp-server.d.ts +9 -0
  55. package/dist/mcp-server.js +187 -0
  56. package/dist/mcp.d.ts +57 -0
  57. package/dist/mcp.js +83 -0
  58. package/dist/memory/private-memory.d.ts +12 -0
  59. package/dist/memory/private-memory.js +54 -0
  60. package/dist/memory/table-of-contents.d.ts +10 -0
  61. package/dist/memory/table-of-contents.js +80 -0
  62. package/dist/memory/team-memory.d.ts +60 -0
  63. package/dist/memory/team-memory.js +54 -0
  64. package/dist/plan.d.ts +77 -0
  65. package/dist/plan.js +288 -0
  66. package/dist/render.d.ts +30 -0
  67. package/dist/render.js +131 -0
  68. package/dist/reply/harness-output.d.ts +57 -0
  69. package/dist/reply/harness-output.js +88 -0
  70. package/dist/reply/reply.d.ts +65 -0
  71. package/dist/reply/reply.js +169 -0
  72. package/dist/reply/verdict.d.ts +28 -0
  73. package/dist/reply/verdict.js +17 -0
  74. package/dist/skill/bundle.d.ts +23 -0
  75. package/dist/skill/bundle.js +102 -0
  76. package/dist/skill/content.d.ts +16 -0
  77. package/dist/skill/content.js +182 -0
  78. package/dist/skills/approved.d.ts +31 -0
  79. package/dist/skills/approved.js +123 -0
  80. package/dist/skills/check.d.ts +21 -0
  81. package/dist/skills/check.js +83 -0
  82. package/dist/state.d.ts +51 -0
  83. package/dist/state.js +118 -0
  84. package/dist/steering/rules.d.ts +49 -0
  85. package/dist/steering/rules.js +165 -0
  86. package/dist/steering/steer.d.ts +29 -0
  87. package/dist/steering/steer.js +64 -0
  88. package/dist/steering/violations.d.ts +27 -0
  89. package/dist/steering/violations.js +24 -0
  90. package/dist/uninstall.d.ts +23 -0
  91. package/dist/uninstall.js +159 -0
  92. package/package.json +34 -4
  93. package/skills/heedvane-hooks/SKILL.md +87 -0
  94. package/skills/heedvane-hooks/references/events.md +69 -0
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Canonical event contract (v1) — the harness-neutral layer everything else talks to.
3
+ *
4
+ * Events are snake_case; the envelope is the single shape the listener emits and
5
+ * the spool carries. Field caps (8 KB strings, 500 nodes, depth 5) are Orca's
6
+ * numbers: ingest must never be blown up by a hostile payload.
7
+ */
8
+ export declare const CANONICAL_EVENTS: readonly ["session_start", "session_end", "user_prompt_submit", "pre_tool_use", "post_tool_use", "post_tool_use_failure", "permission_request", "stop", "stop_failure", "subagent_start", "subagent_stop", "notification", "pre_compact"];
9
+ export type CanonicalEvent = (typeof CANONICAL_EVENTS)[number];
10
+ /** Events that no harness maps to land here as `unknown_event` (data preserved in payload). */
11
+ export declare const UNKNOWN_EVENT = "unknown_event";
12
+ export declare const MAX_STRING_LENGTH = 8192;
13
+ export declare const MAX_OBJECT_NODES = 500;
14
+ export declare const TRUNCATION_SUFFIX = "...[truncated]";
15
+ export interface Envelope {
16
+ v: 1;
17
+ event: string;
18
+ agent: string;
19
+ session: string;
20
+ pane: string;
21
+ receivedAt: string;
22
+ payload: Record<string, unknown>;
23
+ /**
24
+ * Unique per event: generated by the dispatcher (`X-Heed-Event-Id`), kept through spool and replay,
25
+ * so Heedvane can drop a duplicate. Set by the listener; `normalize` alone leaves it out.
26
+ */
27
+ id?: string;
28
+ /** The checkout's `git remote get-url origin`, credentials stripped; set when forwarding to ingestion. */
29
+ gitOrigin?: string;
30
+ /** The session's checkout top level (`git rev-parse --show-toplevel`, absolute); set when forwarding, absent outside a checkout. */
31
+ repoRoot?: string;
32
+ /**
33
+ * SHA-256 hex of the exact bytes of the SKILL.md a tool event loaded, set when forwarding, so
34
+ * statistics can tell an approved copy from a modified one. Absent when the file cannot be read.
35
+ */
36
+ skillSha256?: string;
37
+ }
38
+ export interface CapBudget {
39
+ nodes: number;
40
+ }
41
+ export declare function isCanonicalEvent(name: string): name is CanonicalEvent;
42
+ /**
43
+ * Truncate a string to MAX_STRING_LENGTH, appending '...[truncated]' when cut. The cut never splits a
44
+ * surrogate pair, NUL characters are removed and lone surrogates become U+FFFD: Postgres jsonb refuses
45
+ * `\u0000` and unpaired surrogates, and ingestion would refuse the whole batch for one of them.
46
+ */
47
+ export declare function capString(value: string): string;
48
+ /**
49
+ * Cap a value for ingest: strings to 8 KB, the whole value to 500 nodes, nesting
50
+ * to depth 5 (depth 0 = the top-level value). Over-budget subtrees are replaced
51
+ * with '...[truncated]'. Pass a shared {@link CapBudget} to count nodes across
52
+ * several top-level fields of the same payload.
53
+ */
54
+ export declare function capValue(value: unknown, budget?: CapBudget, depth?: number): unknown;
@@ -0,0 +1,87 @@
1
+ import { isRecord } from "../guards.js";
2
+ /**
3
+ * Canonical event contract (v1) — the harness-neutral layer everything else talks to.
4
+ *
5
+ * Events are snake_case; the envelope is the single shape the listener emits and
6
+ * the spool carries. Field caps (8 KB strings, 500 nodes, depth 5) are Orca's
7
+ * numbers: ingest must never be blown up by a hostile payload.
8
+ */
9
+ export const CANONICAL_EVENTS = [
10
+ "session_start",
11
+ "session_end",
12
+ "user_prompt_submit",
13
+ "pre_tool_use",
14
+ "post_tool_use",
15
+ "post_tool_use_failure",
16
+ "permission_request",
17
+ "stop",
18
+ "stop_failure",
19
+ "subagent_start",
20
+ "subagent_stop",
21
+ "notification",
22
+ "pre_compact",
23
+ ];
24
+ /** Events that no harness maps to land here as `unknown_event` (data preserved in payload). */
25
+ export const UNKNOWN_EVENT = "unknown_event";
26
+ export const MAX_STRING_LENGTH = 8192;
27
+ export const MAX_OBJECT_NODES = 500;
28
+ const MAX_OBJECT_DEPTH = 5;
29
+ export const TRUNCATION_SUFFIX = "...[truncated]";
30
+ export function isCanonicalEvent(name) {
31
+ return CANONICAL_EVENTS.includes(name);
32
+ }
33
+ const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
34
+ /**
35
+ * Truncate a string to MAX_STRING_LENGTH, appending '...[truncated]' when cut. The cut never splits a
36
+ * surrogate pair, NUL characters are removed and lone surrogates become U+FFFD: Postgres jsonb refuses
37
+ * `\u0000` and unpaired surrogates, and ingestion would refuse the whole batch for one of them.
38
+ */
39
+ export function capString(value) {
40
+ const clean = value.replaceAll("\0", "").replace(LONE_SURROGATE, "\uFFFD");
41
+ if (clean.length <= MAX_STRING_LENGTH)
42
+ return clean;
43
+ const end = /[\uD800-\uDBFF]/.test(clean[MAX_STRING_LENGTH - 1]) ? MAX_STRING_LENGTH - 1 : MAX_STRING_LENGTH;
44
+ return clean.slice(0, end) + TRUNCATION_SUFFIX;
45
+ }
46
+ /** The scalar / fallback arm of capValue: everything that is not an array or record. */
47
+ function capScalar(value) {
48
+ if (value === null || typeof value === "number" || typeof value === "boolean")
49
+ return value;
50
+ if (typeof value === "bigint")
51
+ return capString(value.toString());
52
+ if (typeof value === "string")
53
+ return capString(value);
54
+ // Functions, symbols, undefined: nothing serializable to keep.
55
+ return TRUNCATION_SUFFIX;
56
+ }
57
+ /** Cap a record's entries; once the shared node budget is spent, signal that keys were dropped. */
58
+ function capRecord(value, budget, depth) {
59
+ const out = {};
60
+ for (const [key, item] of Object.entries(value)) {
61
+ if (budget.nodes >= MAX_OBJECT_NODES) {
62
+ // Signal explicitly that keys were dropped rather than silently truncating.
63
+ out[TRUNCATION_SUFFIX] = TRUNCATION_SUFFIX;
64
+ break;
65
+ }
66
+ out[capString(key)] = capValue(item, budget, depth + 1);
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * Cap a value for ingest: strings to 8 KB, the whole value to 500 nodes, nesting
72
+ * to depth 5 (depth 0 = the top-level value). Over-budget subtrees are replaced
73
+ * with '...[truncated]'. Pass a shared {@link CapBudget} to count nodes across
74
+ * several top-level fields of the same payload.
75
+ */
76
+ export function capValue(value, budget = { nodes: 0 }, depth = 0) {
77
+ budget.nodes += 1;
78
+ if (budget.nodes > MAX_OBJECT_NODES)
79
+ return TRUNCATION_SUFFIX;
80
+ if (depth > MAX_OBJECT_DEPTH)
81
+ return TRUNCATION_SUFFIX;
82
+ if (Array.isArray(value))
83
+ return value.map((item) => capValue(item, budget, depth + 1));
84
+ if (isRecord(value))
85
+ return capRecord(value, budget, depth);
86
+ return capScalar(value);
87
+ }
@@ -0,0 +1,36 @@
1
+ import { type Envelope } from "./contract.js";
2
+ /**
3
+ * Per-source payload → canonical envelope.
4
+ *
5
+ * One name-normalizer maps every harness's event name onto the canonical taxonomy
6
+ * (trim, camelCase→snake_case, hyphens→underscores, lowercase) so 'PreToolUse'
7
+ * (claude) and 'pre_tool_use' (codex/gemini/cursor) converge. Unrecognized events
8
+ * become `unknown_event` passthroughs — new harness events never drop data.
9
+ */
10
+ export interface NormalizeOptions {
11
+ /** Override receivedAt (ISO string); used when replaying spooled events. */
12
+ receivedAt?: string;
13
+ /** The event's unique id (Envelope.id). */
14
+ id?: string;
15
+ }
16
+ /** Trim + lowercase the agent/source id. */
17
+ export declare function normalizeAgent(source: string): string;
18
+ /**
19
+ * Map any harness event name onto the canonical snake_case taxonomy:
20
+ * trims, converts camelCase/PascalCase boundaries to underscores, converts
21
+ * hyphens/whitespace to underscores, lowercases. Already-canonical names
22
+ * round-trip unchanged.
23
+ */
24
+ export declare function normalizeEventName(name: string): string;
25
+ /** The tool input as the harness sent it, before the payload caps: deny rules match on this, never on the capped copy that is forwarded. */
26
+ export declare function uncappedToolInput(raw: Record<string, unknown>): unknown;
27
+ /**
28
+ * Normalize one raw harness payload into the canonical envelope:
29
+ * `{ v: 1, event, agent, session, pane, receivedAt, payload }`.
30
+ *
31
+ * Known harness fields are mapped onto canonical payload keys (tool, tool_input,
32
+ * tool_response, cwd); every other field passes through capped so nothing is
33
+ * silently dropped. Strings > 8192 chars, payloads > 500 nodes and nesting > 5
34
+ * are truncated with '...[truncated]'.
35
+ */
36
+ export declare function normalize(source: string, raw: Record<string, unknown>, options?: NormalizeOptions): Envelope;
@@ -0,0 +1,143 @@
1
+ import { capString, capValue, isCanonicalEvent, UNKNOWN_EVENT, } from "./contract.js";
2
+ const SHARED_FIELDS = {
3
+ event: ["hook_event_name"],
4
+ session: ["session_id", "session"],
5
+ };
6
+ const SOURCE_FIELDS = {
7
+ claude: SHARED_FIELDS,
8
+ codex: SHARED_FIELDS,
9
+ // Gemini CLI's own event names (src/clients/definitions.ts GEMINI_EVENTS); it has no subagent stop.
10
+ gemini: {
11
+ ...SHARED_FIELDS,
12
+ aliases: {
13
+ before_tool: "pre_tool_use",
14
+ after_tool: "post_tool_use",
15
+ before_agent: "user_prompt_submit",
16
+ after_agent: "stop",
17
+ pre_compress: "pre_compact",
18
+ },
19
+ },
20
+ // Cursor names its session `conversation_id`; only sessionStart also carries a session_id.
21
+ cursor: {
22
+ event: SHARED_FIELDS.event,
23
+ session: ["conversation_id", ...SHARED_FIELDS.session],
24
+ aliases: { before_submit_prompt: "user_prompt_submit" },
25
+ },
26
+ // Hermes plugin hook names (src/clients/hermes-plugin.ts HERMES_EVENTS). on_session_end fires once per
27
+ // turn and post_approval_response has no canonical twin: both pass through as unknown_event.
28
+ hermes: {
29
+ ...SHARED_FIELDS,
30
+ aliases: {
31
+ on_session_start: "session_start",
32
+ on_session_finalize: "session_end",
33
+ pre_llm_call: "user_prompt_submit",
34
+ pre_tool_call: "pre_tool_use",
35
+ post_tool_call: "post_tool_use",
36
+ pre_approval_request: "permission_request",
37
+ },
38
+ },
39
+ };
40
+ const DEFAULT_FIELDS = SHARED_FIELDS;
41
+ /** Keys consumed by the envelope itself; never duplicated into the payload. */
42
+ const RESERVED_KEYS = new Set([
43
+ "hook_event_name",
44
+ "event",
45
+ "session_id",
46
+ "session",
47
+ "pane",
48
+ "pane_key",
49
+ ]);
50
+ /** Trim + lowercase the agent/source id. */
51
+ export function normalizeAgent(source) {
52
+ return source.trim().toLowerCase();
53
+ }
54
+ /**
55
+ * Map any harness event name onto the canonical snake_case taxonomy:
56
+ * trims, converts camelCase/PascalCase boundaries to underscores, converts
57
+ * hyphens/whitespace to underscores, lowercases. Already-canonical names
58
+ * round-trip unchanged.
59
+ */
60
+ export function normalizeEventName(name) {
61
+ return name
62
+ .trim()
63
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
64
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
65
+ .replace(/[-\s]+/g, "_")
66
+ .toLowerCase();
67
+ }
68
+ function pick(raw, keys) {
69
+ for (const key of keys) {
70
+ if (raw[key] !== undefined)
71
+ return raw[key];
72
+ }
73
+ return undefined;
74
+ }
75
+ function pickString(raw, keys) {
76
+ const value = pick(raw, keys);
77
+ return typeof value === "string" ? value : undefined;
78
+ }
79
+ /** The tool input as the harness sent it, before the payload caps: deny rules match on this, never on the capped copy that is forwarded. */
80
+ export function uncappedToolInput(raw) {
81
+ return pick(raw, ["tool_input", "toolInput"]);
82
+ }
83
+ /** The canonical event: the harness's own name normalized, or unknown_event passthrough. */
84
+ function canonicalEvent(raw, fields) {
85
+ const rawEvent = pickString(raw, [...fields.event, "event"]);
86
+ const normalized = rawEvent === undefined ? null : normalizeEventName(rawEvent);
87
+ const canonical = normalized === null ? null : fields.aliases?.[normalized] ?? normalized;
88
+ const event = canonical !== null && isCanonicalEvent(canonical) ? canonical : UNKNOWN_EVENT;
89
+ return { event, canonical };
90
+ }
91
+ /** Map the known harness fields + everything unreserved onto the payload, capped by the shared budget. */
92
+ function canonicalPayload(raw, fields, budget) {
93
+ const payload = {};
94
+ const tool = pick(raw, ["tool_name", "tool"]);
95
+ if (tool !== undefined)
96
+ payload["tool"] = capValue(tool, budget);
97
+ const toolInput = pick(raw, ["tool_input", "toolInput"]);
98
+ if (toolInput !== undefined)
99
+ payload["tool_input"] = capValue(toolInput, budget);
100
+ const toolResponse = pick(raw, ["tool_response", "toolResponse"]);
101
+ if (toolResponse !== undefined)
102
+ payload["tool_response"] = capValue(toolResponse, budget);
103
+ const cwd = pickString(raw, ["cwd", "working_dir", "workingDir"]);
104
+ if (cwd !== undefined)
105
+ payload["cwd"] = capValue(cwd, budget);
106
+ for (const [key, value] of Object.entries(raw)) {
107
+ if (RESERVED_KEYS.has(key) || key in payload || fields.event.includes(key))
108
+ continue;
109
+ payload[key] = capValue(value, budget);
110
+ }
111
+ return payload;
112
+ }
113
+ /**
114
+ * Normalize one raw harness payload into the canonical envelope:
115
+ * `{ v: 1, event, agent, session, pane, receivedAt, payload }`.
116
+ *
117
+ * Known harness fields are mapped onto canonical payload keys (tool, tool_input,
118
+ * tool_response, cwd); every other field passes through capped so nothing is
119
+ * silently dropped. Strings > 8192 chars, payloads > 500 nodes and nesting > 5
120
+ * are truncated with '...[truncated]'.
121
+ */
122
+ export function normalize(source, raw, options = {}) {
123
+ const fields = SOURCE_FIELDS[normalizeAgent(source)] ?? DEFAULT_FIELDS;
124
+ const { event, canonical } = canonicalEvent(raw, fields);
125
+ // One node budget across the whole payload: 500 nodes total, not per field.
126
+ const budget = { nodes: 0 };
127
+ const payload = canonicalPayload(raw, fields, budget);
128
+ if (event === UNKNOWN_EVENT && canonical !== null) {
129
+ // Preserve which event the harness actually sent (normalized form).
130
+ payload["original_event"] = canonical;
131
+ }
132
+ return {
133
+ v: 1,
134
+ event,
135
+ agent: normalizeAgent(source),
136
+ // Heedvane requires a session: an event without one is filed under its pane, else "unknown".
137
+ session: capString(pickString(raw, fields.session) || pickString(raw, ["pane", "pane_key"]) || "unknown"),
138
+ pane: capString(pickString(raw, ["pane", "pane_key"]) ?? ""),
139
+ receivedAt: options.receivedAt ?? new Date().toISOString(),
140
+ payload,
141
+ ...(options.id === undefined ? {} : { id: options.id }),
142
+ };
143
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The package's one copy of three repository-owned helpers: `isRecord` (owner:
3
+ * packages/type-predicates), `errorMessage` (owner:
4
+ * packages/operator-diagnostics/error-message) and `sha256Hex` (owner: packages/content-digest).
5
+ * All three owners are `private`
6
+ * workspace packages, and heedvane-hooks ships standalone through npm/npx with
7
+ * zero intra-monorepo runtime imports (recorded exception, see
8
+ * docs/engineering/agent-hooks-architecture.md and
9
+ * .hermes/plans/2026-10-10_042212-heedvane-agent-hooks.md "Deliberate choices"),
10
+ * so it cannot depend on them. Every module in this package imports these from
11
+ * here. Keep the bodies identical to the owners'.
12
+ */
13
+ /** Non-null objects other than arrays; arrays must not become records with numeric keys. */
14
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
15
+ /** The one-line text of a thrown value: an Error's own message, any other value's string form. */
16
+ export declare function errorMessage(thrown: unknown): string;
17
+ /** SHA-256 hex of UTF-8 text, or of exact bytes. */
18
+ export declare function sha256Hex(text: string | Uint8Array): string;
package/dist/guards.js ADDED
@@ -0,0 +1,28 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * The package's one copy of three repository-owned helpers: `isRecord` (owner:
4
+ * packages/type-predicates), `errorMessage` (owner:
5
+ * packages/operator-diagnostics/error-message) and `sha256Hex` (owner: packages/content-digest).
6
+ * All three owners are `private`
7
+ * workspace packages, and heedvane-hooks ships standalone through npm/npx with
8
+ * zero intra-monorepo runtime imports (recorded exception, see
9
+ * docs/engineering/agent-hooks-architecture.md and
10
+ * .hermes/plans/2026-10-10_042212-heedvane-agent-hooks.md "Deliberate choices"),
11
+ * so it cannot depend on them. Every module in this package imports these from
12
+ * here. Keep the bodies identical to the owners'.
13
+ */
14
+ /** Non-null objects other than arrays; arrays must not become records with numeric keys. */
15
+ export function isRecord(value) {
16
+ // eslint-disable-next-line local/no-private-helper-copy -- heedvane-hooks ships standalone through npx (no workspace runtime imports), so it keeps its own copy of isRecord
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+ /** The one-line text of a thrown value: an Error's own message, any other value's string form. */
20
+ export function errorMessage(thrown) {
21
+ // eslint-disable-next-line local/no-private-helper-copy -- heedvane-hooks ships standalone through npx (no workspace runtime imports), so it keeps its own copy of errorMessage
22
+ return thrown instanceof Error ? thrown.message : String(thrown);
23
+ }
24
+ /** SHA-256 hex of UTF-8 text, or of exact bytes. */
25
+ // eslint-disable-next-line local/no-private-helper-copy -- heedvane-hooks ships standalone through npx (no workspace runtime imports), so it keeps its own copy of sha256Hex
26
+ export function sha256Hex(text) {
27
+ return createHash("sha256").update(text).digest("hex");
28
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * heed.yaml — the human-owned control file (schema v1).
3
+ *
4
+ * Load with {@link loadHeedFile}; every field is normalized to its default so
5
+ * downstream code never re-implements the schema. See DEFAULT_HEED_YAML_TEMPLATE
6
+ * for the commented file `npx heedvane init` writes.
7
+ */
8
+ export interface HeedListenerConfig {
9
+ /** 0 = pick a free port at first start (resolved value persists to endpoint.env). */
10
+ port: number;
11
+ /** Spool to disk when the listener is unreachable. */
12
+ spool: boolean;
13
+ }
14
+ export interface HeedInstallConfig {
15
+ hooks: boolean;
16
+ skill: boolean;
17
+ mcp: boolean;
18
+ }
19
+ export interface HeedGatingConfig {
20
+ mode: "pane" | "always";
21
+ }
22
+ export interface HeedSkillsConfig {
23
+ /** Deny a skill that is not on the organization's approved list. */
24
+ enforce: boolean;
25
+ /** Also require the local SKILL.md to hash to the approved sha256. */
26
+ strict: boolean;
27
+ /** No synced approved list, or no listener to check it: let the skill run with a warning, or deny it. */
28
+ whenUnknown: "allow" | "deny";
29
+ }
30
+ /** The Heedvane backend the listener asks for steering, approved skills and memory. */
31
+ export interface HeedvaneConfig {
32
+ url: string;
33
+ steering: boolean;
34
+ memory: boolean;
35
+ skills: HeedSkillsConfig;
36
+ }
37
+ export interface HeedFile {
38
+ schemaVersion: "v1";
39
+ root: string;
40
+ listener: HeedListenerConfig;
41
+ agents: "auto" | string[];
42
+ install: HeedInstallConfig;
43
+ gating: HeedGatingConfig;
44
+ heedvane: HeedvaneConfig;
45
+ }
46
+ /**
47
+ * The commented control file written by `npx heedvane init`.
48
+ * Kept loadable: loadHeedFile(DEFAULT_HEED_YAML_TEMPLATE) yields every default.
49
+ */
50
+ export declare const DEFAULT_HEED_YAML_TEMPLATE = "# ~/.heed/heed.yaml \u2014 Heedvane agent hooks control file (v1)\nschemaVersion: v1\n\n# Where managed scripts and spool live. Default below; one install per machine.\nroot: ~/.heed\n\n# The hook listener (Orca's pattern): loopback HTTP, token-authed, per-agent routes.\nlistener:\n port: 0 # 0 = pick a free port at first start, persist to endpoint.env\n spool: true # spool to disk when listener is unreachable (5 MB cap, 7-day TTL)\n\n# Which agents to manage. `auto` = every detected agent on the machine.\nagents: auto # or an explicit list: [claude, codex, warp]\n\n# Which integrations to install per agent where supported.\ninstall:\n hooks: true # native hook config injection (skipped where the agent has none)\n skill: true # heedvane-hooks skill bundle -> ~/.agents/skills/heedvane-hooks/\n mcp: true # heedvane MCP server registration (universal fallback, incl. Warp)\n\n# Session gating (Orca's env-gated dispatcher pattern).\ngating:\n mode: always # always = every session; pane = only sessions launched with HEED_SESSION env\n\n# The Heedvane backend (credentials: npx heedvane auth, stored in ~/.heed/credentials.json).\nheedvane:\n url: https://app.heedvane.com\n steering: true # ask the organization's steering rules before tool calls, prompts and stops\n memory: true # add your private memories (~/.heed/memory, local only) at session start\n skills:\n enforce: true # deny skills that are not on the organization's approved list\n strict: false # also require the local SKILL.md to match the approved revision\n whenUnknown: allow # no synced list (run npx heedvane skills sync) or listener down: allow or deny\n";
51
+ /**
52
+ * Parse and validate a heed.yaml document, returning the fully normalized file.
53
+ * Throws {@link HeedFileError} (message prefixed "invalid heed.yaml:") on any problem:
54
+ * unparseable YAML, non-mapping root, missing/unknown schemaVersion, unknown keys
55
+ * (top-level or nested), or wrong-typed values.
56
+ */
57
+ export declare function loadHeedFile(text: string): HeedFile;
58
+ /** `text` with `heedvane.url` set to `url` (`connect --server`); the file's comments stay. Validate with {@link loadHeedFile}. */
59
+ export declare function withServerUrl(text: string, url: string): string;
60
+ /**
61
+ * Load the heed.yaml at `path`. A retired key an earlier version wrote (`init` put `steeringTimeoutMs`
62
+ * in every heed.yaml) is removed from the file in place, its value never read, and one line says so; if
63
+ * the file cannot be rewritten the key is ignored in memory and the line says that instead.
64
+ */
65
+ export declare function loadHeedFileAt(path: string, log?: (line: string) => void): HeedFile;
66
+ /** heed.yaml's `heedvane` section under `root`; the template's defaults before init has written one. */
67
+ export declare function heedvaneConfigAt(root: string): HeedvaneConfig;