@scopebond/hook 0.8.1 → 0.10.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 (110) hide show
  1. package/README.md +239 -0
  2. package/dist/budget-load.d.ts +68 -0
  3. package/dist/budget-load.d.ts.map +1 -0
  4. package/dist/budget-load.js +164 -0
  5. package/dist/budget-load.js.map +1 -0
  6. package/dist/capabilities.d.ts +103 -0
  7. package/dist/capabilities.d.ts.map +1 -0
  8. package/dist/capabilities.js +212 -0
  9. package/dist/capabilities.js.map +1 -0
  10. package/dist/classify.d.ts +17 -0
  11. package/dist/classify.d.ts.map +1 -0
  12. package/dist/classify.js +78 -0
  13. package/dist/classify.js.map +1 -0
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +585 -13
  16. package/dist/cli.js.map +1 -1
  17. package/dist/cloud.d.ts +8 -0
  18. package/dist/cloud.d.ts.map +1 -1
  19. package/dist/cloud.js.map +1 -1
  20. package/dist/dispatch-cli.d.ts +2 -0
  21. package/dist/dispatch-cli.d.ts.map +1 -0
  22. package/dist/dispatch-cli.js +128 -0
  23. package/dist/dispatch-cli.js.map +1 -0
  24. package/dist/explain.d.ts.map +1 -1
  25. package/dist/explain.js +2 -1
  26. package/dist/explain.js.map +1 -1
  27. package/dist/group.d.ts +12 -0
  28. package/dist/group.d.ts.map +1 -0
  29. package/dist/group.js +55 -0
  30. package/dist/group.js.map +1 -0
  31. package/dist/index.d.ts +29 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +19 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/install.d.ts +10 -0
  36. package/dist/install.d.ts.map +1 -1
  37. package/dist/install.js +56 -0
  38. package/dist/install.js.map +1 -1
  39. package/dist/managed.d.ts +69 -0
  40. package/dist/managed.d.ts.map +1 -0
  41. package/dist/managed.js +184 -0
  42. package/dist/managed.js.map +1 -0
  43. package/dist/map.d.ts +4 -0
  44. package/dist/map.d.ts.map +1 -1
  45. package/dist/map.js +29 -1
  46. package/dist/map.js.map +1 -1
  47. package/dist/obs-emitter.d.ts +132 -0
  48. package/dist/obs-emitter.d.ts.map +1 -0
  49. package/dist/obs-emitter.js +415 -0
  50. package/dist/obs-emitter.js.map +1 -0
  51. package/dist/obs-store.d.ts +160 -0
  52. package/dist/obs-store.d.ts.map +1 -0
  53. package/dist/obs-store.js +367 -0
  54. package/dist/obs-store.js.map +1 -0
  55. package/dist/obs-upload.d.ts +24 -0
  56. package/dist/obs-upload.d.ts.map +1 -0
  57. package/dist/obs-upload.js +231 -0
  58. package/dist/obs-upload.js.map +1 -0
  59. package/dist/observation.d.ts +232 -0
  60. package/dist/observation.d.ts.map +1 -0
  61. package/dist/observation.js +308 -0
  62. package/dist/observation.js.map +1 -0
  63. package/dist/paths.d.ts +15 -0
  64. package/dist/paths.d.ts.map +1 -0
  65. package/dist/paths.js +79 -0
  66. package/dist/paths.js.map +1 -0
  67. package/dist/policy-load.d.ts +60 -0
  68. package/dist/policy-load.d.ts.map +1 -0
  69. package/dist/policy-load.js +151 -0
  70. package/dist/policy-load.js.map +1 -0
  71. package/dist/policy-sync.d.ts +45 -0
  72. package/dist/policy-sync.d.ts.map +1 -0
  73. package/dist/policy-sync.js +171 -0
  74. package/dist/policy-sync.js.map +1 -0
  75. package/dist/proof.d.ts +30 -0
  76. package/dist/proof.d.ts.map +1 -0
  77. package/dist/proof.js +186 -0
  78. package/dist/proof.js.map +1 -0
  79. package/dist/rules.d.ts +20 -0
  80. package/dist/rules.d.ts.map +1 -1
  81. package/dist/rules.js +50 -0
  82. package/dist/rules.js.map +1 -1
  83. package/dist/runtime.d.ts +21 -1
  84. package/dist/runtime.d.ts.map +1 -1
  85. package/dist/runtime.js +77 -10
  86. package/dist/runtime.js.map +1 -1
  87. package/dist/scan.d.ts +7 -0
  88. package/dist/scan.d.ts.map +1 -0
  89. package/dist/scan.js +38 -0
  90. package/dist/scan.js.map +1 -0
  91. package/dist/shell.d.ts.map +1 -1
  92. package/dist/shell.js +12 -3
  93. package/dist/shell.js.map +1 -1
  94. package/dist/sql-classify.d.ts +11 -0
  95. package/dist/sql-classify.d.ts.map +1 -0
  96. package/dist/sql-classify.js +251 -0
  97. package/dist/sql-classify.js.map +1 -0
  98. package/dist/typed-infra.d.ts +73 -0
  99. package/dist/typed-infra.d.ts.map +1 -0
  100. package/dist/typed-infra.js +758 -0
  101. package/dist/typed-infra.js.map +1 -0
  102. package/dist/typed-ops.d.ts +140 -0
  103. package/dist/typed-ops.d.ts.map +1 -0
  104. package/dist/typed-ops.js +549 -0
  105. package/dist/typed-ops.js.map +1 -0
  106. package/dist/vectors.d.ts +40 -0
  107. package/dist/vectors.d.ts.map +1 -0
  108. package/dist/vectors.js +219 -0
  109. package/dist/vectors.js.map +1 -0
  110. package/package.json +5 -5
@@ -0,0 +1,132 @@
1
+ import { type HookConnection } from "./cloud.js";
2
+ import { type BindingKey, type CallRequest, type ExitCategory, type ObservationSigner, type PolicyAckInput, type SessionStopReason, type CapabilityProofInput } from "./observation.js";
3
+ import { ObservationStore, type EnqueueResult } from "./obs-store.js";
4
+ import type { GitProbe } from "./typed-ops.js";
5
+ import { type UploadOutcome } from "./obs-upload.js";
6
+ export declare const OBSERVATIONS_SCOPE = "observations:write";
7
+ export declare const HEARTBEAT_INTERVAL_MS = 60000;
8
+ /** A loop that finds the clock moved by more than this since its last tick treats the host as slept. */
9
+ export declare const SLEEP_GAP_MS: number;
10
+ /** Heartbeats continue this long after the last hook activity, then the lease is released. */
11
+ export declare const IDLE_LIMIT_MS: number;
12
+ export declare const HEARTBEAT_LEASE_MS: number;
13
+ export declare const QUEUE_TELEMETRY_INTERVAL_MS: number;
14
+ /** At most this many typed operations are recorded per tool call; the rest are counted. */
15
+ export declare const MAX_INTENTS_PER_CALL = 10;
16
+ export type ObservationStatus = {
17
+ state: "off";
18
+ reason: string;
19
+ } | {
20
+ state: "unsupported";
21
+ reason: string;
22
+ } | {
23
+ state: "on";
24
+ };
25
+ /** The installation id is the enrollment's `installation_id`, or its `gateway_id` (the same
26
+ * identifier, which is what an enrollment answer carries today). The generation has no such
27
+ * fallback: only the workspace can say what it is. */
28
+ export declare function withInstallationId(connection: HookConnection): HookConnection;
29
+ /** Decide from the connection alone whether emission is enabled. Pure; reads nothing. */
30
+ export declare function observationStatus(input: HookConnection | null, keyKid?: string): ObservationStatus;
31
+ export interface EmitterOptions {
32
+ fetch?: typeof fetch;
33
+ now?: () => number;
34
+ adapterVersion?: string;
35
+ /** Tests: do not start the heartbeat helper process. */
36
+ spawnHeartbeat?: boolean;
37
+ /** Bound on one upload attempt from a hook call. */
38
+ flushTimeoutMs?: number;
39
+ }
40
+ export interface OpenResult {
41
+ status: ObservationStatus;
42
+ emitter?: ObservationEmitter;
43
+ }
44
+ /** Open the emitter for a hook config dir, or explain why not. Never throws: an
45
+ * unreadable key or store simply leaves observations off. */
46
+ export declare function openObservations(dir: string, options?: EmitterOptions): OpenResult;
47
+ export declare class ObservationEmitter {
48
+ readonly dir: string;
49
+ readonly connection: HookConnection;
50
+ readonly store: ObservationStore;
51
+ private readonly signer;
52
+ readonly binding: BindingKey;
53
+ private readonly options;
54
+ constructor(dir: string, connection: HookConnection, store: ObservationStore, signer: ObservationSigner, binding: BindingKey, options?: EmitterOptions);
55
+ private now;
56
+ private get context();
57
+ private get adapterVersion();
58
+ /** Queue one observation. Any failure is contained: emission never throws to the caller. */
59
+ emit(draft: Parameters<ObservationStore["enqueue"]>[0]): EnqueueResult | null;
60
+ /** Try to deliver the outbox, bounded. Never throws. */
61
+ flush(timeoutMs?: number): Promise<UploadOutcome | null>;
62
+ close(): void;
63
+ sessionIdOf(harnessSessionId: string): string;
64
+ /** A local key for a harness tool call, so the after-action hook finds what the
65
+ * before-action hook recorded. Keyed, so the raw ids are not written to the local store. */
66
+ private callKey;
67
+ /** SessionStart: record one start per session and make sure the heartbeat helper runs. */
68
+ sessionStart(harnessSessionId: string, cwd: string): void;
69
+ /** SessionEnd: record one stop, with its reason, only for a session that was active. */
70
+ sessionStop(harnessSessionId: string, reason: SessionStopReason): void;
71
+ /** Hook activity in a session: keeps the lease alive, and resumes a session that was
72
+ * stopped only because the host slept. A session that was ended, or never started
73
+ * through the lifecycle hooks, is left alone. */
74
+ activity(harnessSessionId: string, cwd: string): void;
75
+ /** Start the single heartbeat helper for a session unless one already holds the lease. */
76
+ ensureHeartbeat(sessionId: string): void;
77
+ private policyDigest;
78
+ /** Queue oldest-pending-receipt time and count, at most every five minutes, and once more
79
+ * when a backlog drains. */
80
+ queueTelemetry(): void;
81
+ /**
82
+ * One heartbeat-loop tick. Returns whether the loop should keep running.
83
+ * - session no longer active: stop quietly
84
+ * - the clock moved more than SLEEP_GAP_MS since the last tick: the host slept; record a
85
+ * stop (reason sleep) at the last known time and emit no heartbeat for the gap
86
+ * - no hook activity for IDLE_LIMIT_MS: send a final heartbeat that releases the lease
87
+ * - otherwise: heartbeat with the lease active
88
+ */
89
+ heartbeatTick(sessionId: string, lastTickAt: number): "continue" | "stop";
90
+ /**
91
+ * Record the request binding of each dispatched action, linked to its receipt, and keep
92
+ * the operation so the after-action hook can echo it. `dispatched` is what was actually
93
+ * evaluated: the final intent (after root scoping and grouping) and its receipt.
94
+ */
95
+ toolIntents(input: {
96
+ harnessSessionId?: string;
97
+ callId?: string;
98
+ cwd: string;
99
+ dispatched: Array<{
100
+ action: {
101
+ action_type: string;
102
+ params: Record<string, unknown>;
103
+ };
104
+ receipt?: unknown;
105
+ }>;
106
+ /** The tool call as sent, so typed operations read the actual request: a shell tool's raw
107
+ * command, or an MCP tool's server, name and input. */
108
+ request?: CallRequest;
109
+ probe?: GitProbe;
110
+ }): void;
111
+ /** After-action result for a call whose intents were recorded. `exit` comes from the
112
+ * harness's own success/failure event, never from model text. */
113
+ toolOutcomes(harnessSessionId: string | undefined, callId: string, exit: ExitCategory): void;
114
+ policyAck(input: PolicyAckInput): EnqueueResult | null;
115
+ capabilityProofs(proofs: CapabilityProofInput[]): number;
116
+ }
117
+ /** HEAD of the workspace repository, or null when it cannot be read quickly. */
118
+ export declare function gitHead(cwd: string): string | null;
119
+ /** Count and oldest enqueue time of receipts not yet delivered to the workspace, from the
120
+ * receipt outbox beside the local receipts. Null when there is no outbox. */
121
+ export declare function receiptBacklog(dir: string): {
122
+ count: number;
123
+ oldestAt: number;
124
+ } | null;
125
+ /** Claude Code's SessionEnd `reason` mapped to the stop-reason vocabulary. An unlisted or
126
+ * missing reason is `unknown`, never a guess. */
127
+ export declare function stopReasonFromClaude(reason: unknown): SessionStopReason;
128
+ /** Claude Code's failed-tool event to an exit category. */
129
+ export declare const exitFromClaudeFailure: (interrupted: unknown) => ExitCategory;
130
+ /** A one-line, human description for `status`. */
131
+ export declare function describeObservations(dir: string): string[];
132
+ //# sourceMappingURL=obs-emitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"obs-emitter.d.ts","sourceRoot":"","sources":["../src/obs-emitter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,415 @@
1
+ // Observation emitters: when the hook may emit, and what it emits.
2
+ //
3
+ // Emission is opt-in through the workspace enrollment. It is on only when the connection
4
+ // (`cloud.json`) carries the `observations:write` scope AND the enrollment handoff gave an
5
+ // installation id and generation AND the agent key's id matches the one the workspace
6
+ // enrolled. Otherwise it is silently off (with a status line) or reported unsupported;
7
+ // nothing is guessed. None of it can change a policy decision: every failure here is
8
+ // swallowed by the caller and the local receipt path is untouched.
9
+ //
10
+ // Kinds emitted (an agent-adapter key may emit these and no others):
11
+ // session start / stop, from Claude Code's SessionStart and SessionEnd hooks
12
+ // health heartbeat (60 s while a session is explicitly active), queue telemetry
13
+ // capability proof, from the `capabilities --prove` runner
14
+ // policy_ack loaded / rejected, from `policy load` once an exported policy is verified or refused
15
+ // tool_intent the request binding of each dispatched action, linked to its receipt
16
+ // tool_outcome the after-action result, echoing the stored binding (Claude Code)
17
+ //
18
+ // There is no always-on process. Heartbeats come from one short helper per active session,
19
+ // claimed under a lease so hook calls cannot multiply it; it ends when the session ends,
20
+ // after ten minutes without hook activity (it then releases the lease with a final
21
+ // `lease_active: false` heartbeat), or when the host clock jumps (the host slept: it records
22
+ // a stop with reason `sleep` and emits nothing for the gap).
23
+ import { existsSync, readFileSync } from "node:fs";
24
+ import { createRequire } from "node:module";
25
+ import { spawn, execFileSync } from "node:child_process";
26
+ import { join } from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+ import { loadConnection } from "./cloud.js";
29
+ import { operationsForCall, capabilityProofData, digestPolicy, heartbeatData, intentData, loadOrCreateBindingKey, observationSigner, outcomeData, policyAckData, queueData, sessionStartData, sessionStopData, sourceReceiptHash, } from "./observation.js";
30
+ import { openApprovalBinder } from "@scopebond/gateway/node";
31
+ import { OBSERVATION_DB, ObservationStore } from "./obs-store.js";
32
+ import { isProtectedBranch, loadRules } from "./rules.js";
33
+ import { uploadPending } from "./obs-upload.js";
34
+ export const OBSERVATIONS_SCOPE = "observations:write";
35
+ export const HEARTBEAT_INTERVAL_MS = 60_000;
36
+ /** A loop that finds the clock moved by more than this since its last tick treats the host as slept. */
37
+ export const SLEEP_GAP_MS = 3 * HEARTBEAT_INTERVAL_MS;
38
+ /** Heartbeats continue this long after the last hook activity, then the lease is released. */
39
+ export const IDLE_LIMIT_MS = 10 * 60_000;
40
+ export const HEARTBEAT_LEASE_MS = 2.5 * HEARTBEAT_INTERVAL_MS;
41
+ export const QUEUE_TELEMETRY_INTERVAL_MS = 5 * 60_000;
42
+ /** At most this many typed operations are recorded per tool call; the rest are counted. */
43
+ export const MAX_INTENTS_PER_CALL = 10;
44
+ /** The installation id is the enrollment's `installation_id`, or its `gateway_id` (the same
45
+ * identifier, which is what an enrollment answer carries today). The generation has no such
46
+ * fallback: only the workspace can say what it is. */
47
+ export function withInstallationId(connection) {
48
+ return connection.installation_id === undefined && typeof connection.gateway_id === "string"
49
+ ? { ...connection, installation_id: connection.gateway_id } : connection;
50
+ }
51
+ /** Decide from the connection alone whether emission is enabled. Pure; reads nothing. */
52
+ export function observationStatus(input, keyKid) {
53
+ if (!input)
54
+ return { state: "off", reason: "not connected to a workspace" };
55
+ const connection = withInstallationId(input);
56
+ if (!Array.isArray(connection.scopes) || !connection.scopes.includes(OBSERVATIONS_SCOPE)) {
57
+ return { state: "off", reason: `this enrollment does not grant ${OBSERVATIONS_SCOPE}` };
58
+ }
59
+ if (typeof connection.installation_id !== "string" || !/^[\x21-\x7e]{1,200}$/.test(connection.installation_id)) {
60
+ return { state: "unsupported", reason: "the enrollment did not include an installation id; reconnect after the workspace supports observations" };
61
+ }
62
+ if (!Number.isInteger(connection.installation_generation) || connection.installation_generation < 1 || connection.installation_generation > 2_147_483_647) {
63
+ return { state: "unsupported", reason: "the enrollment did not include an installation generation; reconnect after the workspace supports observations" };
64
+ }
65
+ if (typeof connection.agent_kid !== "string" || !/^[A-Za-z0-9:._-]{1,200}$/.test(connection.agent_kid)) {
66
+ return { state: "unsupported", reason: "the enrollment did not register the agent signing key" };
67
+ }
68
+ if (keyKid !== undefined && keyKid !== connection.agent_kid) {
69
+ return { state: "unsupported", reason: "the local agent key is not the key the workspace enrolled" };
70
+ }
71
+ return { state: "on" };
72
+ }
73
+ /** Open the emitter for a hook config dir, or explain why not. Never throws: an
74
+ * unreadable key or store simply leaves observations off. */
75
+ export function openObservations(dir, options = {}) {
76
+ const loaded = loadConnection(dir);
77
+ const early = observationStatus(loaded);
78
+ if (early.state !== "on" || !loaded)
79
+ return { status: early };
80
+ const connection = withInstallationId(loaded);
81
+ let store;
82
+ try {
83
+ const keyPem = readFileSync(join(dir, "agent.key"), "utf8");
84
+ const signer = observationSigner(keyPem, connection.agent_kid);
85
+ store = new ObservationStore(join(dir, OBSERVATION_DB), options.now);
86
+ const bound = store.bindGeneration(connection.installation_generation);
87
+ if (!bound.ok) {
88
+ store.close();
89
+ return { status: { state: "unsupported", reason: bound.reason } };
90
+ }
91
+ const emitter = new ObservationEmitter(dir, connection, store, signer, loadOrCreateBindingKey(dir), options);
92
+ return { status: { state: "on" }, emitter };
93
+ }
94
+ catch (error) {
95
+ try {
96
+ store?.close();
97
+ }
98
+ catch { /* nothing to release */ }
99
+ return { status: { state: "unsupported", reason: `observation setup failed: ${error.message.slice(0, 120)}` } };
100
+ }
101
+ }
102
+ export class ObservationEmitter {
103
+ dir;
104
+ connection;
105
+ store;
106
+ signer;
107
+ binding;
108
+ options;
109
+ constructor(dir, connection, store, signer, binding, options = {}) {
110
+ this.dir = dir;
111
+ this.connection = connection;
112
+ this.store = store;
113
+ this.signer = signer;
114
+ this.binding = binding;
115
+ this.options = options;
116
+ }
117
+ now() { return (this.options.now ?? Date.now)(); }
118
+ get context() {
119
+ return { installationId: this.connection.installation_id, generation: this.connection.installation_generation };
120
+ }
121
+ get adapterVersion() { return this.options.adapterVersion ?? "unknown"; }
122
+ /** Queue one observation. Any failure is contained: emission never throws to the caller. */
123
+ emit(draft) {
124
+ try {
125
+ return this.store.enqueue(draft, this.context, this.signer);
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
131
+ /** Try to deliver the outbox, bounded. Never throws. */
132
+ async flush(timeoutMs = this.options.flushTimeoutMs ?? 800) {
133
+ try {
134
+ return await uploadPending(this.store, {
135
+ url: this.connection.url, credential: this.connection.credential,
136
+ fetch: this.options.fetch, now: this.options.now, timeoutMs,
137
+ });
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ close() { this.store.close(); }
144
+ // ---- session lifecycle ---------------------------------------------------------------------
145
+ sessionIdOf(harnessSessionId) { return this.binding.sessionId(harnessSessionId); }
146
+ /** A local key for a harness tool call, so the after-action hook finds what the
147
+ * before-action hook recorded. Keyed, so the raw ids are not written to the local store. */
148
+ callKey(harnessSessionId, callId) {
149
+ return this.binding.resourceId("call", `${harnessSessionId ?? ""}\u0000${callId}`);
150
+ }
151
+ /** SessionStart: record one start per session and make sure the heartbeat helper runs. */
152
+ sessionStart(harnessSessionId, cwd) {
153
+ const sessionId = this.sessionIdOf(harnessSessionId);
154
+ const repositoryId = this.binding.resourceId("repo", cwd);
155
+ if (this.store.activateSession(sessionId, repositoryId)) {
156
+ this.emit({ kind: "session", occurredAt: this.now(), sessionId, data: sessionStartData(repositoryId) });
157
+ }
158
+ this.ensureHeartbeat(sessionId);
159
+ }
160
+ /** SessionEnd: record one stop, with its reason, only for a session that was active. */
161
+ sessionStop(harnessSessionId, reason) {
162
+ const sessionId = this.sessionIdOf(harnessSessionId);
163
+ const row = this.store.session(sessionId);
164
+ if (this.store.deactivateSession(sessionId, reason)) {
165
+ this.emit({ kind: "session", occurredAt: this.now(), sessionId, data: sessionStopData(reason, row?.repository_id ?? undefined) });
166
+ }
167
+ }
168
+ /** Hook activity in a session: keeps the lease alive, and resumes a session that was
169
+ * stopped only because the host slept. A session that was ended, or never started
170
+ * through the lifecycle hooks, is left alone. */
171
+ activity(harnessSessionId, cwd) {
172
+ const sessionId = this.sessionIdOf(harnessSessionId);
173
+ const row = this.store.session(sessionId);
174
+ if (!row)
175
+ return;
176
+ if (row.state === "stopped" && row.stopped_reason === "sleep") {
177
+ this.sessionStart(harnessSessionId, cwd);
178
+ return;
179
+ }
180
+ if (row.state !== "active")
181
+ return;
182
+ this.store.touchSession(sessionId);
183
+ this.ensureHeartbeat(sessionId);
184
+ }
185
+ /** Start the single heartbeat helper for a session unless one already holds the lease. */
186
+ ensureHeartbeat(sessionId) {
187
+ // SCOPEBOND_OBSERVATIONS_HEARTBEAT=off keeps session start/stop and tool observations but
188
+ // runs no helper process (tests, or a host where a background process is unwelcome).
189
+ if (this.options.spawnHeartbeat === false || process.env.SCOPEBOND_OBSERVATIONS_HEARTBEAT === "off")
190
+ return;
191
+ try {
192
+ if (!this.store.claimHeartbeat(sessionId, HEARTBEAT_LEASE_MS))
193
+ return;
194
+ const cli = fileURLToPath(new URL("./cli.js", import.meta.url));
195
+ const child = spawn(process.execPath, [cli, "observations", "heartbeat", sessionId], {
196
+ detached: true, stdio: "ignore", windowsHide: true, env: { ...process.env, SCOPEBOND_HOOK_DIR: this.dir },
197
+ });
198
+ child.on("error", () => { });
199
+ child.unref();
200
+ }
201
+ catch { /* best effort */ }
202
+ }
203
+ // ---- health --------------------------------------------------------------------------------
204
+ policyDigest() {
205
+ try {
206
+ return digestPolicy(JSON.parse(readFileSync(join(this.dir, "policy.json"), "utf8")));
207
+ }
208
+ catch {
209
+ return undefined;
210
+ }
211
+ }
212
+ /** Queue oldest-pending-receipt time and count, at most every five minutes, and once more
213
+ * when a backlog drains. */
214
+ queueTelemetry() {
215
+ const at = this.now();
216
+ const last = this.store.getMark("queue_at");
217
+ const backlog = receiptBacklog(this.dir);
218
+ if (!backlog)
219
+ return;
220
+ const previous = this.store.getMark("queue_pending") ?? 0;
221
+ if (backlog.count === 0 && previous === 0)
222
+ return;
223
+ if (backlog.count > 0 && last !== null && at - last < QUEUE_TELEMETRY_INTERVAL_MS)
224
+ return;
225
+ this.emit({ kind: "health", occurredAt: at, data: queueData(backlog.count > 0 ? backlog.oldestAt : at, backlog.count, this.policyDigest()) });
226
+ this.store.mark("queue_at", at);
227
+ this.store.mark("queue_pending", backlog.count);
228
+ }
229
+ /**
230
+ * One heartbeat-loop tick. Returns whether the loop should keep running.
231
+ * - session no longer active: stop quietly
232
+ * - the clock moved more than SLEEP_GAP_MS since the last tick: the host slept; record a
233
+ * stop (reason sleep) at the last known time and emit no heartbeat for the gap
234
+ * - no hook activity for IDLE_LIMIT_MS: send a final heartbeat that releases the lease
235
+ * - otherwise: heartbeat with the lease active
236
+ */
237
+ heartbeatTick(sessionId, lastTickAt) {
238
+ const at = this.now();
239
+ const row = this.store.session(sessionId);
240
+ if (!row || row.state !== "active") {
241
+ this.store.releaseHeartbeat(sessionId);
242
+ return "stop";
243
+ }
244
+ if (at - lastTickAt > SLEEP_GAP_MS) {
245
+ if (this.store.deactivateSession(sessionId, "sleep")) {
246
+ this.emit({ kind: "session", occurredAt: lastTickAt, sessionId, data: sessionStopData("sleep", row.repository_id ?? undefined) });
247
+ }
248
+ return "stop";
249
+ }
250
+ if (at - row.last_activity_at > IDLE_LIMIT_MS) {
251
+ this.emit({ kind: "health", occurredAt: at, sessionId, data: heartbeatData(false, { policyDigest: this.policyDigest() }) });
252
+ this.store.releaseHeartbeat(sessionId);
253
+ return "stop";
254
+ }
255
+ this.emit({ kind: "health", occurredAt: at, sessionId, data: heartbeatData(true, { policyDigest: this.policyDigest() }) });
256
+ this.queueTelemetry();
257
+ return this.store.renewHeartbeat(sessionId, HEARTBEAT_LEASE_MS, true) ? "continue" : "stop";
258
+ }
259
+ // ---- actions -------------------------------------------------------------------------------
260
+ /**
261
+ * Record the request binding of each dispatched action, linked to its receipt, and keep
262
+ * the operation so the after-action hook can echo it. `dispatched` is what was actually
263
+ * evaluated: the final intent (after root scoping and grouping) and its receipt.
264
+ */
265
+ toolIntents(input) {
266
+ const sessionId = input.harnessSessionId ? this.sessionIdOf(input.harnessSessionId) : undefined;
267
+ const repositoryId = this.binding.resourceId("repo", input.cwd);
268
+ const rules = loadRules(this.dir);
269
+ const operations = operationsForCall({ dispatched: input.dispatched, request: input.request }, {
270
+ key: this.binding, cwd: input.cwd, repositoryId,
271
+ ...(input.probe ? { probe: input.probe } : {}), ...(rules ? { isProtectedRef: (ref) => isProtectedBranch(rules, ref) } : {}),
272
+ ...(process.env.SCOPEBOND_REQUIRED_CHECK_POLICY_VERSION ? { requiredCheckPolicyVersion: process.env.SCOPEBOND_REQUIRED_CHECK_POLICY_VERSION.slice(0, 200) } : {}),
273
+ });
274
+ // With approvals held in the workspace, an operation carries the hash the dispatch guard asks it to consume, so a consumed approval can be correlated.
275
+ const binder = openApprovalBinder(this.dir);
276
+ let recorded = 0;
277
+ input.dispatched.forEach((item, index) => {
278
+ const receipt = item.receipt;
279
+ const actionId = receipt?.payload?.action_ref?.action_id;
280
+ if (typeof actionId !== "string" || actionId === "" || actionId.length > 200)
281
+ return;
282
+ if (recorded >= MAX_INTENTS_PER_CALL) {
283
+ this.store.mark("omitted_intents", (this.store.getMark("omitted_intents") ?? 0) + 1);
284
+ return;
285
+ }
286
+ const plain = operations[index];
287
+ if (!plain)
288
+ return;
289
+ const operation = binder ? binder.bind(plain, item.action) : plain;
290
+ const linked = sourceReceiptHash(item.receipt);
291
+ const result = this.emit({ kind: "tool_intent", occurredAt: this.now(), sessionId, parentActionId: actionId, sourceReceiptHash: linked, data: intentData(operation) });
292
+ recorded += 1;
293
+ if (result?.queued && input.callId) {
294
+ try {
295
+ this.store.recordCall({ call_key: `${this.callKey(input.harnessSessionId, input.callId)}:${String(index).padStart(3, "0")}`, parent_action_id: actionId, source_receipt_hash: linked, session_id: sessionId ?? null, operation });
296
+ }
297
+ catch { /* the outcome is then simply not recorded */ }
298
+ }
299
+ });
300
+ }
301
+ /** After-action result for a call whose intents were recorded. `exit` comes from the
302
+ * harness's own success/failure event, never from model text. */
303
+ toolOutcomes(harnessSessionId, callId, exit) {
304
+ let calls;
305
+ try {
306
+ calls = this.store.takeCalls(`${this.callKey(harnessSessionId, callId)}:`);
307
+ }
308
+ catch {
309
+ return;
310
+ }
311
+ const at = this.now();
312
+ for (const call of calls) {
313
+ this.emit({
314
+ kind: "tool_outcome", occurredAt: at, sessionId: call.session_id ?? undefined, parentActionId: call.parent_action_id,
315
+ sourceReceiptHash: call.source_receipt_hash,
316
+ data: outcomeData(call.operation, exit, this.adapterVersion, at - call.at),
317
+ });
318
+ }
319
+ }
320
+ // ---- policy acknowledgement and capability proof ------------------------------------------------
321
+ policyAck(input) {
322
+ return this.emit({ kind: "policy_ack", occurredAt: this.now(), data: policyAckData(input) });
323
+ }
324
+ capabilityProofs(proofs) {
325
+ let queued = 0;
326
+ for (const proof of proofs)
327
+ if (this.emit({ kind: "capability", occurredAt: this.now(), data: capabilityProofData(proof) })?.queued)
328
+ queued += 1;
329
+ return queued;
330
+ }
331
+ }
332
+ // ---- helpers ---------------------------------------------------------------------------------
333
+ /** HEAD of the workspace repository, or null when it cannot be read quickly. */
334
+ export function gitHead(cwd) {
335
+ try {
336
+ const sha = execFileSync("git", ["-C", cwd, "rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000 }).trim();
337
+ return /^([0-9a-f]{40}|[0-9a-f]{64})$/.test(sha) ? sha : null;
338
+ }
339
+ catch {
340
+ return null;
341
+ }
342
+ }
343
+ /** Count and oldest enqueue time of receipts not yet delivered to the workspace, from the
344
+ * receipt outbox beside the local receipts. Null when there is no outbox. */
345
+ export function receiptBacklog(dir) {
346
+ const file = join(dir, "receipts.db.cloud-outbox.db");
347
+ if (!existsSync(file))
348
+ return null;
349
+ try {
350
+ const require = createRequire(import.meta.url);
351
+ const { DatabaseSync } = require("node:sqlite");
352
+ const db = new DatabaseSync(file);
353
+ try {
354
+ const row = db.prepare("SELECT COUNT(*) AS n, MIN(enqueued_at) AS o FROM cloud_outbox").get();
355
+ return { count: row.n, oldestAt: row.o ?? Date.now() };
356
+ }
357
+ finally {
358
+ db.close();
359
+ }
360
+ }
361
+ catch {
362
+ return null;
363
+ }
364
+ }
365
+ /** Claude Code's SessionEnd `reason` mapped to the stop-reason vocabulary. An unlisted or
366
+ * missing reason is `unknown`, never a guess. */
367
+ export function stopReasonFromClaude(reason) {
368
+ switch (reason) {
369
+ case "clear":
370
+ case "prompt_input_exit": return "completed";
371
+ case "logout": return "cancelled";
372
+ default: return "unknown";
373
+ }
374
+ }
375
+ /** Claude Code's failed-tool event to an exit category. */
376
+ export const exitFromClaudeFailure = (interrupted) => (interrupted === true ? "cancelled" : "error");
377
+ /** A one-line, human description for `status`. */
378
+ export function describeObservations(dir) {
379
+ const connection = loadConnection(dir);
380
+ const status = observationStatus(connection);
381
+ if (status.state === "off")
382
+ return [`off (${status.reason})`];
383
+ if (status.state === "unsupported")
384
+ return [`unsupported (${status.reason})`];
385
+ const file = join(dir, OBSERVATION_DB);
386
+ if (!existsSync(file))
387
+ return ["on (nothing recorded yet)"];
388
+ let store;
389
+ try {
390
+ store = new ObservationStore(file);
391
+ const state = store.state();
392
+ if (!state)
393
+ return ["on (nothing recorded yet)"];
394
+ const pending = store.pendingSummary();
395
+ const terminal = store.terminalCounts();
396
+ const terminalTotal = Object.values(terminal).reduce((a, b) => a + b, 0);
397
+ const lines = [];
398
+ const head = state.capability === "active" ? "on" : state.capability === "unsupported" ? "unsupported by this workspace" : "paused";
399
+ lines.push(`${head}${state.reason ? ` (${state.reason})` : ""}; generation ${state.generation}; ${pending.count} pending${pending.oldest_at ? ` since ${new Date(pending.oldest_at).toISOString()}` : ""}`);
400
+ if (terminalTotal > 0)
401
+ lines.push(`${terminalTotal} refused or unsendable, kept locally: ${Object.entries(terminal).map(([code, n]) => `${code} ${n}`).join(", ")}`);
402
+ if (state.dropped > 0)
403
+ lines.push(`${state.dropped} not queued (local queue full)`);
404
+ if (state.last_error)
405
+ lines.push(`last upload problem: ${state.last_error}`);
406
+ return lines;
407
+ }
408
+ catch (error) {
409
+ return [`on (status unavailable: ${error.message.slice(0, 80)})`];
410
+ }
411
+ finally {
412
+ store?.close();
413
+ }
414
+ }
415
+ //# sourceMappingURL=obs-emitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"obs-emitter.js","sourceRoot":"","sources":["../src/obs-emitter.ts"],"names":[],"mappings":"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","sourcesContent":["// Observation emitters: when the hook may emit, and what it emits.\n//\n// Emission is opt-in through the workspace enrollment. It is on only when the connection\n// (`cloud.json`) carries the `observations:write` scope AND the enrollment handoff gave an\n// installation id and generation AND the agent key's id matches the one the workspace\n// enrolled. Otherwise it is silently off (with a status line) or reported unsupported;\n// nothing is guessed. None of it can change a policy decision: every failure here is\n// swallowed by the caller and the local receipt path is untouched.\n//\n// Kinds emitted (an agent-adapter key may emit these and no others):\n// session start / stop, from Claude Code's SessionStart and SessionEnd hooks\n// health heartbeat (60 s while a session is explicitly active), queue telemetry\n// capability proof, from the `capabilities --prove` runner\n// policy_ack loaded / rejected, from `policy load` once an exported policy is verified or refused\n// tool_intent the request binding of each dispatched action, linked to its receipt\n// tool_outcome the after-action result, echoing the stored binding (Claude Code)\n//\n// There is no always-on process. Heartbeats come from one short helper per active session,\n// claimed under a lease so hook calls cannot multiply it; it ends when the session ends,\n// after ten minutes without hook activity (it then releases the lease with a final\n// `lease_active: false` heartbeat), or when the host clock jumps (the host slept: it records\n// a stop with reason `sleep` and emits nothing for the gap).\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { spawn, execFileSync } from \"node:child_process\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { loadConnection, type HookConnection } from \"./cloud.js\";\nimport {\n operationsForCall, capabilityProofData, digestPolicy, heartbeatData, intentData, loadOrCreateBindingKey, observationSigner,\n outcomeData, policyAckData, queueData, sessionStartData, sessionStopData, sourceReceiptHash,\n type BindingKey, type CallRequest, type ExitCategory, type ObservationSigner, type PolicyAckInput, type SessionStopReason, type CapabilityProofInput,\n} from \"./observation.js\";\nimport { openApprovalBinder } from \"@scopebond/gateway/node\";\nimport { OBSERVATION_DB, ObservationStore, type EnqueueResult } from \"./obs-store.js\";\nimport type { GitProbe } from \"./typed-ops.js\";\nimport { isProtectedBranch, loadRules } from \"./rules.js\";\nimport { uploadPending, type UploadOutcome } from \"./obs-upload.js\";\n\nexport const OBSERVATIONS_SCOPE = \"observations:write\";\nexport const HEARTBEAT_INTERVAL_MS = 60_000;\n/** A loop that finds the clock moved by more than this since its last tick treats the host as slept. */\nexport const SLEEP_GAP_MS = 3 * HEARTBEAT_INTERVAL_MS;\n/** Heartbeats continue this long after the last hook activity, then the lease is released. */\nexport const IDLE_LIMIT_MS = 10 * 60_000;\nexport const HEARTBEAT_LEASE_MS = 2.5 * HEARTBEAT_INTERVAL_MS;\nexport const QUEUE_TELEMETRY_INTERVAL_MS = 5 * 60_000;\n/** At most this many typed operations are recorded per tool call; the rest are counted. */\nexport const MAX_INTENTS_PER_CALL = 10;\n\nexport type ObservationStatus =\n | { state: \"off\"; reason: string }\n | { state: \"unsupported\"; reason: string }\n | { state: \"on\" };\n\n/** The installation id is the enrollment's `installation_id`, or its `gateway_id` (the same\n * identifier, which is what an enrollment answer carries today). The generation has no such\n * fallback: only the workspace can say what it is. */\nexport function withInstallationId(connection: HookConnection): HookConnection {\n return connection.installation_id === undefined && typeof connection.gateway_id === \"string\"\n ? { ...connection, installation_id: connection.gateway_id } : connection;\n}\n\n/** Decide from the connection alone whether emission is enabled. Pure; reads nothing. */\nexport function observationStatus(input: HookConnection | null, keyKid?: string): ObservationStatus {\n if (!input) return { state: \"off\", reason: \"not connected to a workspace\" };\n const connection = withInstallationId(input);\n if (!Array.isArray(connection.scopes) || !connection.scopes.includes(OBSERVATIONS_SCOPE)) {\n return { state: \"off\", reason: `this enrollment does not grant ${OBSERVATIONS_SCOPE}` };\n }\n if (typeof connection.installation_id !== \"string\" || !/^[\\x21-\\x7e]{1,200}$/.test(connection.installation_id)) {\n return { state: \"unsupported\", reason: \"the enrollment did not include an installation id; reconnect after the workspace supports observations\" };\n }\n if (!Number.isInteger(connection.installation_generation) || (connection.installation_generation as number) < 1 || (connection.installation_generation as number) > 2_147_483_647) {\n return { state: \"unsupported\", reason: \"the enrollment did not include an installation generation; reconnect after the workspace supports observations\" };\n }\n if (typeof connection.agent_kid !== \"string\" || !/^[A-Za-z0-9:._-]{1,200}$/.test(connection.agent_kid)) {\n return { state: \"unsupported\", reason: \"the enrollment did not register the agent signing key\" };\n }\n if (keyKid !== undefined && keyKid !== connection.agent_kid) {\n return { state: \"unsupported\", reason: \"the local agent key is not the key the workspace enrolled\" };\n }\n return { state: \"on\" };\n}\n\nexport interface EmitterOptions {\n fetch?: typeof fetch;\n now?: () => number;\n adapterVersion?: string;\n /** Tests: do not start the heartbeat helper process. */\n spawnHeartbeat?: boolean;\n /** Bound on one upload attempt from a hook call. */\n flushTimeoutMs?: number;\n}\n\nexport interface OpenResult {\n status: ObservationStatus;\n emitter?: ObservationEmitter;\n}\n\n/** Open the emitter for a hook config dir, or explain why not. Never throws: an\n * unreadable key or store simply leaves observations off. */\nexport function openObservations(dir: string, options: EmitterOptions = {}): OpenResult {\n const loaded = loadConnection(dir);\n const early = observationStatus(loaded);\n if (early.state !== \"on\" || !loaded) return { status: early };\n const connection = withInstallationId(loaded);\n let store: ObservationStore | undefined;\n try {\n const keyPem = readFileSync(join(dir, \"agent.key\"), \"utf8\");\n const signer = observationSigner(keyPem, connection.agent_kid as string);\n store = new ObservationStore(join(dir, OBSERVATION_DB), options.now);\n const bound = store.bindGeneration(connection.installation_generation as number);\n if (!bound.ok) { store.close(); return { status: { state: \"unsupported\", reason: bound.reason } }; }\n const emitter = new ObservationEmitter(dir, connection, store, signer, loadOrCreateBindingKey(dir), options);\n return { status: { state: \"on\" }, emitter };\n } catch (error) {\n try { store?.close(); } catch { /* nothing to release */ }\n return { status: { state: \"unsupported\", reason: `observation setup failed: ${(error as Error).message.slice(0, 120)}` } };\n }\n}\n\nexport class ObservationEmitter {\n constructor(\n readonly dir: string,\n readonly connection: HookConnection,\n readonly store: ObservationStore,\n private readonly signer: ObservationSigner,\n readonly binding: BindingKey,\n private readonly options: EmitterOptions = {},\n ) {}\n\n private now(): number { return (this.options.now ?? Date.now)(); }\n private get context() {\n return { installationId: this.connection.installation_id as string, generation: this.connection.installation_generation as number };\n }\n private get adapterVersion(): string { return this.options.adapterVersion ?? \"unknown\"; }\n\n /** Queue one observation. Any failure is contained: emission never throws to the caller. */\n emit(draft: Parameters<ObservationStore[\"enqueue\"]>[0]): EnqueueResult | null {\n try { return this.store.enqueue(draft, this.context, this.signer); } catch { return null; }\n }\n\n /** Try to deliver the outbox, bounded. Never throws. */\n async flush(timeoutMs = this.options.flushTimeoutMs ?? 800): Promise<UploadOutcome | null> {\n try {\n return await uploadPending(this.store, {\n url: this.connection.url, credential: this.connection.credential,\n fetch: this.options.fetch, now: this.options.now, timeoutMs,\n });\n } catch { return null; }\n }\n\n close(): void { this.store.close(); }\n\n // ---- session lifecycle ---------------------------------------------------------------------\n\n sessionIdOf(harnessSessionId: string): string { return this.binding.sessionId(harnessSessionId); }\n\n /** A local key for a harness tool call, so the after-action hook finds what the\n * before-action hook recorded. Keyed, so the raw ids are not written to the local store. */\n private callKey(harnessSessionId: string | undefined, callId: string): string {\n return this.binding.resourceId(\"call\", `${harnessSessionId ?? \"\"}\\u0000${callId}`);\n }\n\n /** SessionStart: record one start per session and make sure the heartbeat helper runs. */\n sessionStart(harnessSessionId: string, cwd: string): void {\n const sessionId = this.sessionIdOf(harnessSessionId);\n const repositoryId = this.binding.resourceId(\"repo\", cwd);\n if (this.store.activateSession(sessionId, repositoryId)) {\n this.emit({ kind: \"session\", occurredAt: this.now(), sessionId, data: sessionStartData(repositoryId) });\n }\n this.ensureHeartbeat(sessionId);\n }\n\n /** SessionEnd: record one stop, with its reason, only for a session that was active. */\n sessionStop(harnessSessionId: string, reason: SessionStopReason): void {\n const sessionId = this.sessionIdOf(harnessSessionId);\n const row = this.store.session(sessionId);\n if (this.store.deactivateSession(sessionId, reason)) {\n this.emit({ kind: \"session\", occurredAt: this.now(), sessionId, data: sessionStopData(reason, row?.repository_id ?? undefined) });\n }\n }\n\n /** Hook activity in a session: keeps the lease alive, and resumes a session that was\n * stopped only because the host slept. A session that was ended, or never started\n * through the lifecycle hooks, is left alone. */\n activity(harnessSessionId: string, cwd: string): void {\n const sessionId = this.sessionIdOf(harnessSessionId);\n const row = this.store.session(sessionId);\n if (!row) return;\n if (row.state === \"stopped\" && row.stopped_reason === \"sleep\") { this.sessionStart(harnessSessionId, cwd); return; }\n if (row.state !== \"active\") return;\n this.store.touchSession(sessionId);\n this.ensureHeartbeat(sessionId);\n }\n\n /** Start the single heartbeat helper for a session unless one already holds the lease. */\n ensureHeartbeat(sessionId: string): void {\n // SCOPEBOND_OBSERVATIONS_HEARTBEAT=off keeps session start/stop and tool observations but\n // runs no helper process (tests, or a host where a background process is unwelcome).\n if (this.options.spawnHeartbeat === false || process.env.SCOPEBOND_OBSERVATIONS_HEARTBEAT === \"off\") return;\n try {\n if (!this.store.claimHeartbeat(sessionId, HEARTBEAT_LEASE_MS)) return;\n const cli = fileURLToPath(new URL(\"./cli.js\", import.meta.url));\n const child = spawn(process.execPath, [cli, \"observations\", \"heartbeat\", sessionId], {\n detached: true, stdio: \"ignore\", windowsHide: true, env: { ...process.env, SCOPEBOND_HOOK_DIR: this.dir },\n });\n child.on(\"error\", () => { /* the lease lapses and the next hook call tries again */ });\n child.unref();\n } catch { /* best effort */ }\n }\n\n // ---- health --------------------------------------------------------------------------------\n\n private policyDigest(): string | undefined {\n try { return digestPolicy(JSON.parse(readFileSync(join(this.dir, \"policy.json\"), \"utf8\"))); } catch { return undefined; }\n }\n\n /** Queue oldest-pending-receipt time and count, at most every five minutes, and once more\n * when a backlog drains. */\n queueTelemetry(): void {\n const at = this.now();\n const last = this.store.getMark(\"queue_at\");\n const backlog = receiptBacklog(this.dir);\n if (!backlog) return;\n const previous = this.store.getMark(\"queue_pending\") ?? 0;\n if (backlog.count === 0 && previous === 0) return;\n if (backlog.count > 0 && last !== null && at - last < QUEUE_TELEMETRY_INTERVAL_MS) return;\n this.emit({ kind: \"health\", occurredAt: at, data: queueData(backlog.count > 0 ? backlog.oldestAt : at, backlog.count, this.policyDigest()) });\n this.store.mark(\"queue_at\", at);\n this.store.mark(\"queue_pending\", backlog.count);\n }\n\n /**\n * One heartbeat-loop tick. Returns whether the loop should keep running.\n * - session no longer active: stop quietly\n * - the clock moved more than SLEEP_GAP_MS since the last tick: the host slept; record a\n * stop (reason sleep) at the last known time and emit no heartbeat for the gap\n * - no hook activity for IDLE_LIMIT_MS: send a final heartbeat that releases the lease\n * - otherwise: heartbeat with the lease active\n */\n heartbeatTick(sessionId: string, lastTickAt: number): \"continue\" | \"stop\" {\n const at = this.now();\n const row = this.store.session(sessionId);\n if (!row || row.state !== \"active\") { this.store.releaseHeartbeat(sessionId); return \"stop\"; }\n if (at - lastTickAt > SLEEP_GAP_MS) {\n if (this.store.deactivateSession(sessionId, \"sleep\")) {\n this.emit({ kind: \"session\", occurredAt: lastTickAt, sessionId, data: sessionStopData(\"sleep\", row.repository_id ?? undefined) });\n }\n return \"stop\";\n }\n if (at - row.last_activity_at > IDLE_LIMIT_MS) {\n this.emit({ kind: \"health\", occurredAt: at, sessionId, data: heartbeatData(false, { policyDigest: this.policyDigest() }) });\n this.store.releaseHeartbeat(sessionId);\n return \"stop\";\n }\n this.emit({ kind: \"health\", occurredAt: at, sessionId, data: heartbeatData(true, { policyDigest: this.policyDigest() }) });\n this.queueTelemetry();\n return this.store.renewHeartbeat(sessionId, HEARTBEAT_LEASE_MS, true) ? \"continue\" : \"stop\";\n }\n\n // ---- actions -------------------------------------------------------------------------------\n\n /**\n * Record the request binding of each dispatched action, linked to its receipt, and keep\n * the operation so the after-action hook can echo it. `dispatched` is what was actually\n * evaluated: the final intent (after root scoping and grouping) and its receipt.\n */\n toolIntents(input: {\n harnessSessionId?: string; callId?: string; cwd: string;\n dispatched: Array<{ action: { action_type: string; params: Record<string, unknown> }; receipt?: unknown }>;\n /** The tool call as sent, so typed operations read the actual request: a shell tool's raw\n * command, or an MCP tool's server, name and input. */\n request?: CallRequest;\n probe?: GitProbe;\n }): void {\n const sessionId = input.harnessSessionId ? this.sessionIdOf(input.harnessSessionId) : undefined;\n const repositoryId = this.binding.resourceId(\"repo\", input.cwd);\n const rules = loadRules(this.dir);\n const operations = operationsForCall({ dispatched: input.dispatched, request: input.request }, {\n key: this.binding, cwd: input.cwd, repositoryId,\n ...(input.probe ? { probe: input.probe } : {}), ...(rules ? { isProtectedRef: (ref: string) => isProtectedBranch(rules, ref) } : {}),\n ...(process.env.SCOPEBOND_REQUIRED_CHECK_POLICY_VERSION ? { requiredCheckPolicyVersion: process.env.SCOPEBOND_REQUIRED_CHECK_POLICY_VERSION.slice(0, 200) } : {}),\n });\n // With approvals held in the workspace, an operation carries the hash the dispatch guard asks it to consume, so a consumed approval can be correlated.\n const binder = openApprovalBinder(this.dir);\n let recorded = 0;\n input.dispatched.forEach((item, index) => {\n const receipt = item.receipt as { payload?: { action_ref?: { action_id?: unknown } } } | undefined;\n const actionId = receipt?.payload?.action_ref?.action_id;\n if (typeof actionId !== \"string\" || actionId === \"\" || actionId.length > 200) return;\n if (recorded >= MAX_INTENTS_PER_CALL) { this.store.mark(\"omitted_intents\", (this.store.getMark(\"omitted_intents\") ?? 0) + 1); return; }\n const plain = operations[index];\n if (!plain) return;\n const operation = binder ? binder.bind(plain, item.action) : plain;\n const linked = sourceReceiptHash(item.receipt);\n const result = this.emit({ kind: \"tool_intent\", occurredAt: this.now(), sessionId, parentActionId: actionId, sourceReceiptHash: linked, data: intentData(operation) });\n recorded += 1;\n if (result?.queued && input.callId) {\n try {\n this.store.recordCall({ call_key: `${this.callKey(input.harnessSessionId, input.callId)}:${String(index).padStart(3, \"0\")}`, parent_action_id: actionId, source_receipt_hash: linked, session_id: sessionId ?? null, operation });\n } catch { /* the outcome is then simply not recorded */ }\n }\n });\n }\n\n /** After-action result for a call whose intents were recorded. `exit` comes from the\n * harness's own success/failure event, never from model text. */\n toolOutcomes(harnessSessionId: string | undefined, callId: string, exit: ExitCategory): void {\n let calls;\n try { calls = this.store.takeCalls(`${this.callKey(harnessSessionId, callId)}:`); } catch { return; }\n const at = this.now();\n for (const call of calls) {\n this.emit({\n kind: \"tool_outcome\", occurredAt: at, sessionId: call.session_id ?? undefined, parentActionId: call.parent_action_id,\n sourceReceiptHash: call.source_receipt_hash,\n data: outcomeData(call.operation, exit, this.adapterVersion, at - call.at),\n });\n }\n }\n\n // ---- policy acknowledgement and capability proof ------------------------------------------------\n\n policyAck(input: PolicyAckInput): EnqueueResult | null {\n return this.emit({ kind: \"policy_ack\", occurredAt: this.now(), data: policyAckData(input) });\n }\n\n capabilityProofs(proofs: CapabilityProofInput[]): number {\n let queued = 0;\n for (const proof of proofs) if (this.emit({ kind: \"capability\", occurredAt: this.now(), data: capabilityProofData(proof) })?.queued) queued += 1;\n return queued;\n }\n}\n\n// ---- helpers ---------------------------------------------------------------------------------\n\n/** HEAD of the workspace repository, or null when it cannot be read quickly. */\nexport function gitHead(cwd: string): string | null {\n try {\n const sha = execFileSync(\"git\", [\"-C\", cwd, \"rev-parse\", \"HEAD\"], { encoding: \"utf8\", stdio: [\"ignore\", \"pipe\", \"ignore\"], timeout: 2000 }).trim();\n return /^([0-9a-f]{40}|[0-9a-f]{64})$/.test(sha) ? sha : null;\n } catch { return null; }\n}\n\n/** Count and oldest enqueue time of receipts not yet delivered to the workspace, from the\n * receipt outbox beside the local receipts. Null when there is no outbox. */\nexport function receiptBacklog(dir: string): { count: number; oldestAt: number } | null {\n const file = join(dir, \"receipts.db.cloud-outbox.db\");\n if (!existsSync(file)) return null;\n try {\n const require = createRequire(import.meta.url);\n const { DatabaseSync } = require(\"node:sqlite\") as { DatabaseSync: new (p: string) => { prepare(s: string): { get(): unknown }; close(): void } };\n const db = new DatabaseSync(file);\n try {\n const row = db.prepare(\"SELECT COUNT(*) AS n, MIN(enqueued_at) AS o FROM cloud_outbox\").get() as { n: number; o: number | null };\n return { count: row.n, oldestAt: row.o ?? Date.now() };\n } finally { db.close(); }\n } catch { return null; }\n}\n\n/** Claude Code's SessionEnd `reason` mapped to the stop-reason vocabulary. An unlisted or\n * missing reason is `unknown`, never a guess. */\nexport function stopReasonFromClaude(reason: unknown): SessionStopReason {\n switch (reason) {\n case \"clear\": case \"prompt_input_exit\": return \"completed\";\n case \"logout\": return \"cancelled\";\n default: return \"unknown\";\n }\n}\n\n/** Claude Code's failed-tool event to an exit category. */\nexport const exitFromClaudeFailure = (interrupted: unknown): ExitCategory => (interrupted === true ? \"cancelled\" : \"error\");\n\n/** A one-line, human description for `status`. */\nexport function describeObservations(dir: string): string[] {\n const connection = loadConnection(dir);\n const status = observationStatus(connection);\n if (status.state === \"off\") return [`off (${status.reason})`];\n if (status.state === \"unsupported\") return [`unsupported (${status.reason})`];\n const file = join(dir, OBSERVATION_DB);\n if (!existsSync(file)) return [\"on (nothing recorded yet)\"];\n let store: ObservationStore | undefined;\n try {\n store = new ObservationStore(file);\n const state = store.state();\n if (!state) return [\"on (nothing recorded yet)\"];\n const pending = store.pendingSummary();\n const terminal = store.terminalCounts();\n const terminalTotal = Object.values(terminal).reduce((a, b) => a + b, 0);\n const lines: string[] = [];\n const head = state.capability === \"active\" ? \"on\" : state.capability === \"unsupported\" ? \"unsupported by this workspace\" : \"paused\";\n lines.push(`${head}${state.reason ? ` (${state.reason})` : \"\"}; generation ${state.generation}; ${pending.count} pending${pending.oldest_at ? ` since ${new Date(pending.oldest_at).toISOString()}` : \"\"}`);\n if (terminalTotal > 0) lines.push(`${terminalTotal} refused or unsendable, kept locally: ${Object.entries(terminal).map(([code, n]) => `${code} ${n}`).join(\", \")}`);\n if (state.dropped > 0) lines.push(`${state.dropped} not queued (local queue full)`);\n if (state.last_error) lines.push(`last upload problem: ${state.last_error}`);\n return lines;\n } catch (error) {\n return [`on (status unavailable: ${(error as Error).message.slice(0, 80)})`];\n } finally { store?.close(); }\n}\n"]}