@titan-design/hitl 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Henry Jewkes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # @titan-design/hitl
2
+
3
+ Pause work on a human, and let a different process resume it. The primitive is
4
+ "the same work, paused": `openGate()` from inside a task hands back something to
5
+ await, and `resolveGate()` from anywhere else lets it continue.
6
+
7
+ Tier 1 of the titan-platform DAG (TP-11). Depends on
8
+ `@titan-design/store-sqlite`; `zod` is a peer (v4).
9
+
10
+ ## A gate is a row, not a promise
11
+
12
+ The promise `wait()` returns is a local convenience. The gate itself is a
13
+ database row, and that is what makes the primitive worth having: the process that
14
+ opened the gate can crash, redeploy, or exit, and the pending gate is still there
15
+ to be answered. A gate is addressable by id from any process, which is the whole
16
+ point. The waiter polls, because the resolver may be a different process writing
17
+ the same SQLite file.
18
+
19
+ ```ts
20
+ import { openDatabase } from "@titan-design/store-sqlite";
21
+ import { SqliteGateStore, openGate } from "@titan-design/hitl";
22
+ import { z } from "zod";
23
+
24
+ const store = new SqliteGateStore(openDatabase("~/.local/state/thing/gates.sqlite3"));
25
+
26
+ const gate = openGate(store, {
27
+ id: "deploy-approval",
28
+ prompt: "Ship 1.4.0 to production?",
29
+ schema: z.object({ approved: z.boolean(), note: z.string().optional() }),
30
+ expiresAt: new Date(Date.now() + 3_600_000),
31
+ });
32
+
33
+ const answer = await gate.wait(); // { approved: true }
34
+ ```
35
+
36
+ Somewhere else entirely, in a CLI, an MCP tool, or a dashboard route:
37
+
38
+ ```ts
39
+ import { resolveGate, cancelGate } from "@titan-design/hitl";
40
+
41
+ resolveGate(store, "deploy-approval", { approved: true });
42
+ ```
43
+
44
+ After a restart, re-attach by id instead of re-opening:
45
+
46
+ ```ts
47
+ import { waitForGate } from "@titan-design/hitl";
48
+
49
+ for (const pending of store.listPending()) {
50
+ void waitForGate(store, pending.id, { schema: approvalSchema });
51
+ }
52
+ ```
53
+
54
+ ## Two schema checks, on purpose
55
+
56
+ The resolving process does not have your zod schema; by definition it is
57
+ somewhere else. So `openGate` stores the schema as JSON Schema
58
+ (`z.toJSONSchema`) on the row, and `resolveGate` runs `checkAgainstJsonSchema`
59
+ against it. That is the boundary check: a bad payload is rejected where it is
60
+ submitted, with a `GatePayloadInvalid` listing the offending paths.
61
+
62
+ The waiter then re-validates with the real zod schema before `wait()` resolves.
63
+ That is the type guarantee. The checker covers only the subset zod emits
64
+ (`type`, `required`, `properties`, `items`, `enum`, `const`, `anyOf`,
65
+ `additionalProperties: false`) and is not a general JSON Schema validator.
66
+
67
+ ## Stores
68
+
69
+ Both implementations extend `BaseGateStore`, which owns every settle rule, and
70
+ both pass the same behaviour suite.
71
+
72
+ | Store | Use when |
73
+ |---|---|
74
+ | `MemoryGateStore` | tests, and single-process work that needs the pause but not the durability |
75
+ | `SqliteGateStore` | anything that must survive a restart or be answered by another process |
76
+
77
+ `SqliteGateStore` installs a `hitl_gate` table through store-sqlite's
78
+ `runMigrations` on construction. Pass `migrate: false` and put `gateMigration(n)`
79
+ in the product's own migration list when hitl shares a database with domain
80
+ tables. `table` renames the table so one database can host several gate spaces.
81
+ Timestamps are ISO-8601 strings, the shape store-sqlite writes and any surface
82
+ can send on as-is.
83
+
84
+ The row carries a `reason` column beyond the minimum, so a cancelled gate can
85
+ tell its waiter why.
86
+
87
+ ## Settling
88
+
89
+ A gate is `pending`, then exactly one of `resolved`, `cancelled`, or `expired`.
90
+ A second `resolve` or `cancel` throws `GateAlreadySettled` and leaves the first
91
+ answer intact.
92
+
93
+ Expiry is **lazy**: nothing sweeps the table, so a read is what notices the
94
+ deadline passed and flips the row to `expired`. The instant named by `expiresAt`
95
+ counts as expired. Both stores take an injectable `now` so expiry is testable
96
+ without waiting.
97
+
98
+ `wait` rejects with `GateCancelled`, `GateExpired`, `GateNotFound`,
99
+ `GatePayloadInvalid`, or `GateAborted` (when the caller's `AbortSignal` fires).
100
+ Every one of them extends `GateError` and carries `gateId`. Aborting a wait does
101
+ not touch the gate; the row stays pending for whoever picks it up next.
@@ -0,0 +1,176 @@
1
+ import { ZodType } from 'zod';
2
+ import { Db, Migration } from '@titan-design/store-sqlite';
3
+
4
+ /** A gate is a row, never a promise: pending work survives the process that opened it. */
5
+ type GateStatus = "pending" | "resolved" | "cancelled" | "expired";
6
+ /** A JSON Schema document, stored so any process can validate a payload without the original zod schema. */
7
+ type JsonSchema = Record<string, unknown>;
8
+ interface GateRecord {
9
+ id: string;
10
+ prompt: string;
11
+ schema: JsonSchema | undefined;
12
+ status: GateStatus;
13
+ payload: unknown;
14
+ /** Why the gate was cancelled; unset for every other status. */
15
+ reason: string | undefined;
16
+ /** ISO-8601 with milliseconds, the shape store-sqlite writes and any surface can send as-is. */
17
+ createdAt: string;
18
+ resolvedAt: string | undefined;
19
+ expiresAt: string | undefined;
20
+ }
21
+ interface GateInput {
22
+ /** Defaults to a random UUID. Supply one to make the gate addressable by a name you already own. */
23
+ id?: string;
24
+ prompt: string;
25
+ schema?: JsonSchema;
26
+ expiresAt?: Date | string;
27
+ }
28
+ /**
29
+ * Synchronous on purpose: both implementations are local (a Map, and
30
+ * better-sqlite3), and `wait` is the only thing that needs to be async.
31
+ */
32
+ interface GateStore {
33
+ create(input: GateInput): GateRecord;
34
+ get(id: string): GateRecord | undefined;
35
+ resolve(id: string, payload: unknown): GateRecord;
36
+ cancel(id: string, reason: string): GateRecord;
37
+ listPending(): GateRecord[];
38
+ }
39
+ declare class GateError extends Error {
40
+ readonly gateId: string;
41
+ constructor(message: string, gateId: string);
42
+ }
43
+ declare class GateNotFound extends GateError {
44
+ constructor(gateId: string);
45
+ }
46
+ declare class GateAlreadyExists extends GateError {
47
+ constructor(gateId: string);
48
+ }
49
+ declare class GateAlreadySettled extends GateError {
50
+ readonly status: GateStatus;
51
+ constructor(gateId: string, status: GateStatus);
52
+ }
53
+ declare class GateCancelled extends GateError {
54
+ readonly reason: string;
55
+ constructor(gateId: string, reason: string);
56
+ }
57
+ declare class GateExpired extends GateError {
58
+ constructor(gateId: string);
59
+ }
60
+ declare class GatePayloadInvalid extends GateError {
61
+ readonly issues: string[];
62
+ constructor(gateId: string, issues: string[]);
63
+ }
64
+ declare class GateAborted extends GateError {
65
+ constructor(gateId: string, reason: string);
66
+ }
67
+
68
+ declare const DEFAULT_POLL_MS = 250;
69
+ interface OpenGateOptions<T> {
70
+ id?: string;
71
+ prompt: string;
72
+ /** Stored as JSON Schema so the resolving process can validate without it, and reused here to type the answer. */
73
+ schema?: ZodType<T>;
74
+ expiresAt?: Date | string;
75
+ }
76
+ interface WaitOptions<T> {
77
+ pollMs?: number;
78
+ signal?: AbortSignal;
79
+ /** Needed only when re-attaching to a gate this process did not open. */
80
+ schema?: ZodType<T>;
81
+ }
82
+ interface GateHandle<T> {
83
+ id: string;
84
+ record: GateRecord;
85
+ wait(options?: Omit<WaitOptions<T>, "schema">): Promise<T>;
86
+ }
87
+ /**
88
+ * Open a gate and hand back something to await. The row exists before this
89
+ * returns, so a crash here leaves a resolvable gate rather than a lost one.
90
+ */
91
+ declare function openGate<T = unknown>(store: GateStore, options: OpenGateOptions<T>): GateHandle<T>;
92
+ /**
93
+ * Await a gate by id from any process, including one that restarted after the
94
+ * gate was opened. Polls rather than subscribes because the resolver may be a
95
+ * different process writing the same SQLite file.
96
+ */
97
+ declare function waitForGate<T = unknown>(store: GateStore, id: string, options?: WaitOptions<T>): Promise<T>;
98
+ /** Resolve a gate from wherever the human answered: a CLI, an MCP tool, a dashboard route. */
99
+ declare function resolveGate(store: GateStore, id: string, payload: unknown): GateRecord;
100
+ declare function cancelGate(store: GateStore, id: string, reason: string): GateRecord;
101
+
102
+ /**
103
+ * Every settle rule lives here so the memory and SQLite stores cannot drift
104
+ * apart. Subclasses only move rows.
105
+ */
106
+ declare abstract class BaseGateStore implements GateStore {
107
+ protected readonly clock: () => number;
108
+ protected constructor(clock: () => number);
109
+ protected abstract insert(record: GateRecord): void;
110
+ protected abstract read(id: string): GateRecord | undefined;
111
+ protected abstract update(record: GateRecord): void;
112
+ protected abstract readByStatus(status: GateRecord["status"]): GateRecord[];
113
+ create(input: GateInput): GateRecord;
114
+ get(id: string): GateRecord | undefined;
115
+ resolve(id: string, payload: unknown): GateRecord;
116
+ cancel(id: string, reason: string): GateRecord;
117
+ listPending(): GateRecord[];
118
+ private requirePending;
119
+ /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */
120
+ private lapseIfExpired;
121
+ protected nowIso(): string;
122
+ }
123
+
124
+ interface MemoryGateStoreOptions {
125
+ /** Epoch-millis clock, injectable so expiry is testable without waiting. */
126
+ now?: () => number;
127
+ }
128
+ /**
129
+ * In-process gates. Right for tests and for a single-process product that only
130
+ * needs the pause, not the durability; anything that must survive a restart
131
+ * wants `SqliteGateStore`.
132
+ */
133
+ declare class MemoryGateStore extends BaseGateStore {
134
+ private readonly rows;
135
+ constructor(options?: MemoryGateStoreOptions);
136
+ protected insert(record: GateRecord): void;
137
+ protected read(id: string): GateRecord | undefined;
138
+ protected update(record: GateRecord): void;
139
+ protected readByStatus(status: GateRecord["status"]): GateRecord[];
140
+ }
141
+
142
+ declare const DEFAULT_GATE_TABLE = "hitl_gate";
143
+ declare function gateTableDdl(name?: string): string;
144
+ /** Drop into the product's own `runMigrations` list so hitl shares one database with its domain tables. */
145
+ declare function gateMigration(version: number, name?: string): Migration;
146
+ interface SqliteGateStoreOptions {
147
+ table?: string;
148
+ /** Run `gateMigration` on construction. Off when the product owns its migration list. */
149
+ migrate?: boolean;
150
+ now?: () => number;
151
+ }
152
+ /**
153
+ * Durable gates. Two instances on the same file are two processes as far as the
154
+ * gate is concerned, which is the entire point: whoever resolves the row does
155
+ * not have to be whoever opened it.
156
+ */
157
+ declare class SqliteGateStore extends BaseGateStore {
158
+ private readonly db;
159
+ private readonly table;
160
+ constructor(db: Db, options?: SqliteGateStoreOptions);
161
+ protected insert(record: GateRecord): void;
162
+ protected read(id: string): GateRecord | undefined;
163
+ protected update(record: GateRecord): void;
164
+ protected readByStatus(status: GateStatus): GateRecord[];
165
+ }
166
+
167
+ /**
168
+ * A deliberately small structural check over the JSON Schema subset `z.toJSONSchema`
169
+ * emits. It exists so the process that resolves a gate can reject a bad payload
170
+ * without holding the original zod schema, which by definition lives in another
171
+ * process. The waiter still re-validates with the real zod schema, so this is the
172
+ * boundary check, not the type guarantee.
173
+ */
174
+ declare function checkAgainstJsonSchema(schema: JsonSchema, value: unknown, path?: string): string[];
175
+
176
+ export { BaseGateStore, DEFAULT_GATE_TABLE, DEFAULT_POLL_MS, GateAborted, GateAlreadyExists, GateAlreadySettled, GateCancelled, GateError, GateExpired, type GateHandle, type GateInput, GateNotFound, GatePayloadInvalid, type GateRecord, type GateStatus, type GateStore, type JsonSchema, MemoryGateStore, type MemoryGateStoreOptions, type OpenGateOptions, SqliteGateStore, type SqliteGateStoreOptions, type WaitOptions, cancelGate, checkAgainstJsonSchema, gateMigration, gateTableDdl, openGate, resolveGate, waitForGate };
package/dist/index.js ADDED
@@ -0,0 +1,387 @@
1
+ // src/gate.ts
2
+ import { toJSONSchema } from "zod";
3
+
4
+ // src/types.ts
5
+ var GateError = class extends Error {
6
+ constructor(message, gateId) {
7
+ super(message);
8
+ this.gateId = gateId;
9
+ this.name = new.target.name;
10
+ }
11
+ gateId;
12
+ };
13
+ var GateNotFound = class extends GateError {
14
+ constructor(gateId) {
15
+ super(`no gate with id ${gateId}`, gateId);
16
+ }
17
+ };
18
+ var GateAlreadyExists = class extends GateError {
19
+ constructor(gateId) {
20
+ super(`a gate with id ${gateId} already exists`, gateId);
21
+ }
22
+ };
23
+ var GateAlreadySettled = class extends GateError {
24
+ constructor(gateId, status) {
25
+ super(`gate ${gateId} is already ${status}`, gateId);
26
+ this.status = status;
27
+ }
28
+ status;
29
+ };
30
+ var GateCancelled = class extends GateError {
31
+ constructor(gateId, reason) {
32
+ super(`gate ${gateId} was cancelled: ${reason}`, gateId);
33
+ this.reason = reason;
34
+ }
35
+ reason;
36
+ };
37
+ var GateExpired = class extends GateError {
38
+ constructor(gateId) {
39
+ super(`gate ${gateId} expired before anyone resolved it`, gateId);
40
+ }
41
+ };
42
+ var GatePayloadInvalid = class extends GateError {
43
+ constructor(gateId, issues) {
44
+ super(`payload for gate ${gateId} does not match its schema: ${issues.join("; ")}`, gateId);
45
+ this.issues = issues;
46
+ }
47
+ issues;
48
+ };
49
+ var GateAborted = class extends GateError {
50
+ constructor(gateId, reason) {
51
+ super(`stopped waiting on gate ${gateId}: ${reason}`, gateId);
52
+ }
53
+ };
54
+
55
+ // src/gate.ts
56
+ var DEFAULT_POLL_MS = 250;
57
+ function openGate(store, options) {
58
+ const input = { prompt: options.prompt };
59
+ if (options.id !== void 0) input.id = options.id;
60
+ if (options.expiresAt !== void 0) input.expiresAt = options.expiresAt;
61
+ if (options.schema) input.schema = toJSONSchema(options.schema);
62
+ const record = store.create(input);
63
+ return {
64
+ id: record.id,
65
+ record,
66
+ wait: (waitOptions = {}) => {
67
+ const merged = { ...waitOptions };
68
+ if (options.schema) merged.schema = options.schema;
69
+ return waitForGate(store, record.id, merged);
70
+ }
71
+ };
72
+ }
73
+ async function waitForGate(store, id, options = {}) {
74
+ const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
75
+ for (; ; ) {
76
+ throwIfAborted(id, options.signal);
77
+ const record = store.get(id);
78
+ if (!record) throw new GateNotFound(id);
79
+ if (record.status === "resolved") return validate(record, options.schema);
80
+ if (record.status === "cancelled") throw new GateCancelled(id, record.reason ?? "no reason given");
81
+ if (record.status === "expired") throw new GateExpired(id);
82
+ await sleep(pollMs, options.signal);
83
+ }
84
+ }
85
+ function resolveGate(store, id, payload) {
86
+ return store.resolve(id, payload);
87
+ }
88
+ function cancelGate(store, id, reason) {
89
+ return store.cancel(id, reason);
90
+ }
91
+ function validate(record, schema) {
92
+ if (!schema) return record.payload;
93
+ const parsed = schema.safeParse(record.payload);
94
+ if (parsed.success) return parsed.data;
95
+ throw new GatePayloadInvalid(
96
+ record.id,
97
+ parsed.error.issues.map((issue) => `${issue.path.join(".") || "$"}: ${issue.message}`)
98
+ );
99
+ }
100
+ function throwIfAborted(id, signal) {
101
+ if (signal?.aborted) throw new GateAborted(id, String(signal.reason ?? "aborted"));
102
+ }
103
+ function sleep(ms, signal) {
104
+ return new Promise((resolve) => {
105
+ const timer = setTimeout(finish, ms);
106
+ signal?.addEventListener("abort", finish, { once: true });
107
+ function finish() {
108
+ clearTimeout(timer);
109
+ signal?.removeEventListener("abort", finish);
110
+ resolve();
111
+ }
112
+ });
113
+ }
114
+
115
+ // src/base-store.ts
116
+ import { randomUUID } from "crypto";
117
+
118
+ // src/json-schema.ts
119
+ function checkAgainstJsonSchema(schema, value, path = "$") {
120
+ const issues = [];
121
+ collect(schema, value, path, issues);
122
+ return issues;
123
+ }
124
+ function collect(schema, value, path, issues) {
125
+ if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {
126
+ checkUnion(schema.anyOf ?? schema.oneOf, value, path, issues);
127
+ return;
128
+ }
129
+ if ("const" in schema && !Object.is(schema.const, value)) {
130
+ issues.push(`${path}: expected ${JSON.stringify(schema.const)}`);
131
+ return;
132
+ }
133
+ if (Array.isArray(schema.enum) && !schema.enum.some((option) => Object.is(option, value))) {
134
+ issues.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);
135
+ return;
136
+ }
137
+ if (typeof schema.type !== "string") return;
138
+ if (!matchesType(schema.type, value)) {
139
+ issues.push(`${path}: expected ${schema.type}, received ${describe(value)}`);
140
+ return;
141
+ }
142
+ if (schema.type === "object") checkObject(schema, value, path, issues);
143
+ if (schema.type === "array") checkArray(schema, value, path, issues);
144
+ }
145
+ function checkUnion(branches, value, path, issues) {
146
+ const matched = branches.some((branch) => checkAgainstJsonSchema(branch, value, path).length === 0);
147
+ if (!matched) issues.push(`${path}: matched none of the ${branches.length} allowed shapes`);
148
+ }
149
+ function checkObject(schema, value, path, issues) {
150
+ const properties = schema.properties ?? {};
151
+ for (const key of schema.required ?? []) {
152
+ if (!(key in value)) issues.push(`${path}.${key}: required`);
153
+ }
154
+ if (schema.additionalProperties === false) {
155
+ for (const key of Object.keys(value)) {
156
+ if (!(key in properties)) issues.push(`${path}.${key}: unexpected property`);
157
+ }
158
+ }
159
+ for (const [key, sub] of Object.entries(properties)) {
160
+ if (key in value) collect(sub, value[key], `${path}.${key}`, issues);
161
+ }
162
+ }
163
+ function checkArray(schema, value, path, issues) {
164
+ const items = schema.items;
165
+ if (!items) return;
166
+ value.forEach((entry, index) => collect(items, entry, `${path}[${index}]`, issues));
167
+ }
168
+ function matchesType(type, value) {
169
+ switch (type) {
170
+ case "object":
171
+ return typeof value === "object" && value !== null && !Array.isArray(value);
172
+ case "array":
173
+ return Array.isArray(value);
174
+ case "integer":
175
+ return Number.isInteger(value);
176
+ case "number":
177
+ return typeof value === "number" && Number.isFinite(value);
178
+ case "null":
179
+ return value === null;
180
+ default:
181
+ return typeof value === type;
182
+ }
183
+ }
184
+ function describe(value) {
185
+ if (value === null) return "null";
186
+ return Array.isArray(value) ? "array" : typeof value;
187
+ }
188
+
189
+ // src/base-store.ts
190
+ var BaseGateStore = class {
191
+ constructor(clock) {
192
+ this.clock = clock;
193
+ }
194
+ clock;
195
+ create(input) {
196
+ const id = input.id ?? randomUUID();
197
+ if (this.read(id)) throw new GateAlreadyExists(id);
198
+ const record = {
199
+ id,
200
+ prompt: input.prompt,
201
+ schema: input.schema,
202
+ status: "pending",
203
+ payload: void 0,
204
+ reason: void 0,
205
+ createdAt: this.nowIso(),
206
+ resolvedAt: void 0,
207
+ expiresAt: toIso(input.expiresAt)
208
+ };
209
+ this.insert(record);
210
+ return record;
211
+ }
212
+ get(id) {
213
+ const record = this.read(id);
214
+ return record ? this.lapseIfExpired(record) : void 0;
215
+ }
216
+ resolve(id, payload) {
217
+ const record = this.requirePending(id);
218
+ if (record.schema) {
219
+ const issues = checkAgainstJsonSchema(record.schema, payload);
220
+ if (issues.length > 0) throw new GatePayloadInvalid(id, issues);
221
+ }
222
+ const resolved = { ...record, status: "resolved", payload, resolvedAt: this.nowIso() };
223
+ this.update(resolved);
224
+ return resolved;
225
+ }
226
+ cancel(id, reason) {
227
+ const record = this.requirePending(id);
228
+ const cancelled = { ...record, status: "cancelled", reason, resolvedAt: this.nowIso() };
229
+ this.update(cancelled);
230
+ return cancelled;
231
+ }
232
+ listPending() {
233
+ return this.readByStatus("pending").map((record) => this.lapseIfExpired(record)).filter((record) => record.status === "pending").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
234
+ }
235
+ requirePending(id) {
236
+ const record = this.get(id);
237
+ if (!record) throw new GateNotFound(id);
238
+ if (record.status === "expired") throw new GateExpired(id);
239
+ if (record.status !== "pending") throw new GateAlreadySettled(id, record.status);
240
+ return record;
241
+ }
242
+ /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */
243
+ lapseIfExpired(record) {
244
+ if (record.status !== "pending" || !record.expiresAt) return record;
245
+ if (Date.parse(record.expiresAt) > this.clock()) return record;
246
+ const expired = { ...record, status: "expired", resolvedAt: this.nowIso() };
247
+ this.update(expired);
248
+ return expired;
249
+ }
250
+ nowIso() {
251
+ return new Date(this.clock()).toISOString();
252
+ }
253
+ };
254
+ function toIso(value) {
255
+ if (value === void 0) return void 0;
256
+ return typeof value === "string" ? value : value.toISOString();
257
+ }
258
+
259
+ // src/memory-store.ts
260
+ var MemoryGateStore = class extends BaseGateStore {
261
+ rows = /* @__PURE__ */ new Map();
262
+ constructor(options = {}) {
263
+ super(options.now ?? Date.now);
264
+ }
265
+ insert(record) {
266
+ this.rows.set(record.id, { ...record });
267
+ }
268
+ read(id) {
269
+ const row = this.rows.get(id);
270
+ return row ? { ...row } : void 0;
271
+ }
272
+ update(record) {
273
+ this.rows.set(record.id, { ...record });
274
+ }
275
+ readByStatus(status) {
276
+ return [...this.rows.values()].filter((row) => row.status === status).map((row) => ({ ...row }));
277
+ }
278
+ };
279
+
280
+ // src/sqlite-store.ts
281
+ import { quoteIdent, runMigrations } from "@titan-design/store-sqlite";
282
+ var DEFAULT_GATE_TABLE = "hitl_gate";
283
+ function gateTableDdl(name = DEFAULT_GATE_TABLE) {
284
+ const t = quoteIdent(name);
285
+ return `
286
+ CREATE TABLE IF NOT EXISTS ${t} (
287
+ id TEXT PRIMARY KEY,
288
+ prompt TEXT NOT NULL,
289
+ schema TEXT,
290
+ status TEXT NOT NULL DEFAULT 'pending',
291
+ payload TEXT,
292
+ reason TEXT,
293
+ created_at TEXT NOT NULL,
294
+ resolved_at TEXT,
295
+ expires_at TEXT
296
+ );
297
+ CREATE INDEX IF NOT EXISTS ${quoteIdent(`${name}_pending`)}
298
+ ON ${t} (created_at) WHERE status = 'pending';
299
+ `;
300
+ }
301
+ function gateMigration(version, name = DEFAULT_GATE_TABLE) {
302
+ return { version, name: `hitl:${name}`, up: (db) => db.exec(gateTableDdl(name)) };
303
+ }
304
+ var SqliteGateStore = class extends BaseGateStore {
305
+ constructor(db, options = {}) {
306
+ super(options.now ?? Date.now);
307
+ this.db = db;
308
+ this.table = options.table ?? DEFAULT_GATE_TABLE;
309
+ if (options.migrate ?? true) runMigrations(db, [gateMigration(1, this.table)]);
310
+ }
311
+ db;
312
+ table;
313
+ insert(record) {
314
+ this.db.prepare(
315
+ `INSERT INTO ${quoteIdent(this.table)}
316
+ (id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at)
317
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
318
+ ).run(...columns(record));
319
+ }
320
+ read(id) {
321
+ const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id);
322
+ return row ? toRecord(row) : void 0;
323
+ }
324
+ update(record) {
325
+ this.db.prepare(
326
+ `UPDATE ${quoteIdent(this.table)}
327
+ SET status = ?, payload = ?, reason = ?, resolved_at = ?
328
+ WHERE id = ?`
329
+ ).run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, record.id);
330
+ }
331
+ readByStatus(status) {
332
+ const rows = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`).all(status);
333
+ return rows.map(toRecord);
334
+ }
335
+ };
336
+ function columns(record) {
337
+ return [
338
+ record.id,
339
+ record.prompt,
340
+ record.schema ? JSON.stringify(record.schema) : null,
341
+ record.status,
342
+ toJson(record.payload),
343
+ record.reason ?? null,
344
+ record.createdAt,
345
+ record.resolvedAt ?? null,
346
+ record.expiresAt ?? null
347
+ ];
348
+ }
349
+ function toJson(payload) {
350
+ return payload === void 0 ? null : JSON.stringify(payload);
351
+ }
352
+ function toRecord(row) {
353
+ return {
354
+ id: row.id,
355
+ prompt: row.prompt,
356
+ schema: row.schema ? JSON.parse(row.schema) : void 0,
357
+ status: row.status,
358
+ payload: row.payload === null ? void 0 : JSON.parse(row.payload),
359
+ reason: row.reason ?? void 0,
360
+ createdAt: row.created_at,
361
+ resolvedAt: row.resolved_at ?? void 0,
362
+ expiresAt: row.expires_at ?? void 0
363
+ };
364
+ }
365
+ export {
366
+ BaseGateStore,
367
+ DEFAULT_GATE_TABLE,
368
+ DEFAULT_POLL_MS,
369
+ GateAborted,
370
+ GateAlreadyExists,
371
+ GateAlreadySettled,
372
+ GateCancelled,
373
+ GateError,
374
+ GateExpired,
375
+ GateNotFound,
376
+ GatePayloadInvalid,
377
+ MemoryGateStore,
378
+ SqliteGateStore,
379
+ cancelGate,
380
+ checkAgainstJsonSchema,
381
+ gateMigration,
382
+ gateTableDdl,
383
+ openGate,
384
+ resolveGate,
385
+ waitForGate
386
+ };
387
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/gate.ts","../src/types.ts","../src/base-store.ts","../src/json-schema.ts","../src/memory-store.ts","../src/sqlite-store.ts"],"sourcesContent":["import { toJSONSchema, type ZodType } from \"zod\";\nimport {\n GateAborted,\n GateCancelled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n type GateRecord,\n type GateStore,\n} from \"./types.js\";\n\nexport const DEFAULT_POLL_MS = 250;\n\nexport interface OpenGateOptions<T> {\n id?: string;\n prompt: string;\n /** Stored as JSON Schema so the resolving process can validate without it, and reused here to type the answer. */\n schema?: ZodType<T>;\n expiresAt?: Date | string;\n}\n\nexport interface WaitOptions<T> {\n pollMs?: number;\n signal?: AbortSignal;\n /** Needed only when re-attaching to a gate this process did not open. */\n schema?: ZodType<T>;\n}\n\nexport interface GateHandle<T> {\n id: string;\n record: GateRecord;\n wait(options?: Omit<WaitOptions<T>, \"schema\">): Promise<T>;\n}\n\n/**\n * Open a gate and hand back something to await. The row exists before this\n * returns, so a crash here leaves a resolvable gate rather than a lost one.\n */\nexport function openGate<T = unknown>(store: GateStore, options: OpenGateOptions<T>): GateHandle<T> {\n const input: Parameters<GateStore[\"create\"]>[0] = { prompt: options.prompt };\n if (options.id !== undefined) input.id = options.id;\n if (options.expiresAt !== undefined) input.expiresAt = options.expiresAt;\n if (options.schema) input.schema = toJSONSchema(options.schema) as Record<string, unknown>;\n\n const record = store.create(input);\n return {\n id: record.id,\n record,\n wait: (waitOptions = {}) => {\n const merged: WaitOptions<T> = { ...waitOptions };\n if (options.schema) merged.schema = options.schema;\n return waitForGate<T>(store, record.id, merged);\n },\n };\n}\n\n/**\n * Await a gate by id from any process, including one that restarted after the\n * gate was opened. Polls rather than subscribes because the resolver may be a\n * different process writing the same SQLite file.\n */\nexport async function waitForGate<T = unknown>(\n store: GateStore,\n id: string,\n options: WaitOptions<T> = {},\n): Promise<T> {\n const pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n for (;;) {\n throwIfAborted(id, options.signal);\n const record = store.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"resolved\") return validate(record, options.schema);\n if (record.status === \"cancelled\") throw new GateCancelled(id, record.reason ?? \"no reason given\");\n if (record.status === \"expired\") throw new GateExpired(id);\n await sleep(pollMs, options.signal);\n }\n}\n\n/** Resolve a gate from wherever the human answered: a CLI, an MCP tool, a dashboard route. */\nexport function resolveGate(store: GateStore, id: string, payload: unknown): GateRecord {\n return store.resolve(id, payload);\n}\n\nexport function cancelGate(store: GateStore, id: string, reason: string): GateRecord {\n return store.cancel(id, reason);\n}\n\nfunction validate<T>(record: GateRecord, schema: ZodType<T> | undefined): T {\n if (!schema) return record.payload as T;\n const parsed = schema.safeParse(record.payload);\n if (parsed.success) return parsed.data;\n throw new GatePayloadInvalid(\n record.id,\n parsed.error.issues.map((issue) => `${issue.path.join(\".\") || \"$\"}: ${issue.message}`),\n );\n}\n\nfunction throwIfAborted(id: string, signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new GateAborted(id, String(signal.reason ?? \"aborted\"));\n}\n\nfunction sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(finish, ms);\n signal?.addEventListener(\"abort\", finish, { once: true });\n function finish(): void {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", finish);\n resolve();\n }\n });\n}\n","/** A gate is a row, never a promise: pending work survives the process that opened it. */\nexport type GateStatus = \"pending\" | \"resolved\" | \"cancelled\" | \"expired\";\n\n/** A JSON Schema document, stored so any process can validate a payload without the original zod schema. */\nexport type JsonSchema = Record<string, unknown>;\n\nexport interface GateRecord {\n id: string;\n prompt: string;\n schema: JsonSchema | undefined;\n status: GateStatus;\n payload: unknown;\n /** Why the gate was cancelled; unset for every other status. */\n reason: string | undefined;\n /** ISO-8601 with milliseconds, the shape store-sqlite writes and any surface can send as-is. */\n createdAt: string;\n resolvedAt: string | undefined;\n expiresAt: string | undefined;\n}\n\nexport interface GateInput {\n /** Defaults to a random UUID. Supply one to make the gate addressable by a name you already own. */\n id?: string;\n prompt: string;\n schema?: JsonSchema;\n expiresAt?: Date | string;\n}\n\n/**\n * Synchronous on purpose: both implementations are local (a Map, and\n * better-sqlite3), and `wait` is the only thing that needs to be async.\n */\nexport interface GateStore {\n create(input: GateInput): GateRecord;\n get(id: string): GateRecord | undefined;\n resolve(id: string, payload: unknown): GateRecord;\n cancel(id: string, reason: string): GateRecord;\n listPending(): GateRecord[];\n}\n\nexport class GateError extends Error {\n constructor(\n message: string,\n readonly gateId: string,\n ) {\n super(message);\n this.name = new.target.name;\n }\n}\n\nexport class GateNotFound extends GateError {\n constructor(gateId: string) {\n super(`no gate with id ${gateId}`, gateId);\n }\n}\n\nexport class GateAlreadyExists extends GateError {\n constructor(gateId: string) {\n super(`a gate with id ${gateId} already exists`, gateId);\n }\n}\n\nexport class GateAlreadySettled extends GateError {\n constructor(\n gateId: string,\n readonly status: GateStatus,\n ) {\n super(`gate ${gateId} is already ${status}`, gateId);\n }\n}\n\nexport class GateCancelled extends GateError {\n constructor(\n gateId: string,\n readonly reason: string,\n ) {\n super(`gate ${gateId} was cancelled: ${reason}`, gateId);\n }\n}\n\nexport class GateExpired extends GateError {\n constructor(gateId: string) {\n super(`gate ${gateId} expired before anyone resolved it`, gateId);\n }\n}\n\nexport class GatePayloadInvalid extends GateError {\n constructor(\n gateId: string,\n readonly issues: string[],\n ) {\n super(`payload for gate ${gateId} does not match its schema: ${issues.join(\"; \")}`, gateId);\n }\n}\n\nexport class GateAborted extends GateError {\n constructor(gateId: string, reason: string) {\n super(`stopped waiting on gate ${gateId}: ${reason}`, gateId);\n }\n}\n","import { randomUUID } from \"node:crypto\";\nimport { checkAgainstJsonSchema } from \"./json-schema.js\";\nimport {\n GateAlreadyExists,\n GateAlreadySettled,\n GateExpired,\n GateNotFound,\n GatePayloadInvalid,\n type GateInput,\n type GateRecord,\n type GateStore,\n} from \"./types.js\";\n\n/**\n * Every settle rule lives here so the memory and SQLite stores cannot drift\n * apart. Subclasses only move rows.\n */\nexport abstract class BaseGateStore implements GateStore {\n protected constructor(protected readonly clock: () => number) {}\n\n protected abstract insert(record: GateRecord): void;\n protected abstract read(id: string): GateRecord | undefined;\n protected abstract update(record: GateRecord): void;\n protected abstract readByStatus(status: GateRecord[\"status\"]): GateRecord[];\n\n create(input: GateInput): GateRecord {\n const id = input.id ?? randomUUID();\n if (this.read(id)) throw new GateAlreadyExists(id);\n const record: GateRecord = {\n id,\n prompt: input.prompt,\n schema: input.schema,\n status: \"pending\",\n payload: undefined,\n reason: undefined,\n createdAt: this.nowIso(),\n resolvedAt: undefined,\n expiresAt: toIso(input.expiresAt),\n };\n this.insert(record);\n return record;\n }\n\n get(id: string): GateRecord | undefined {\n const record = this.read(id);\n return record ? this.lapseIfExpired(record) : undefined;\n }\n\n resolve(id: string, payload: unknown): GateRecord {\n const record = this.requirePending(id);\n if (record.schema) {\n const issues = checkAgainstJsonSchema(record.schema, payload);\n if (issues.length > 0) throw new GatePayloadInvalid(id, issues);\n }\n const resolved: GateRecord = { ...record, status: \"resolved\", payload, resolvedAt: this.nowIso() };\n this.update(resolved);\n return resolved;\n }\n\n cancel(id: string, reason: string): GateRecord {\n const record = this.requirePending(id);\n const cancelled: GateRecord = { ...record, status: \"cancelled\", reason, resolvedAt: this.nowIso() };\n this.update(cancelled);\n return cancelled;\n }\n\n listPending(): GateRecord[] {\n return this.readByStatus(\"pending\")\n .map((record) => this.lapseIfExpired(record))\n .filter((record) => record.status === \"pending\")\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n }\n\n private requirePending(id: string): GateRecord {\n const record = this.get(id);\n if (!record) throw new GateNotFound(id);\n if (record.status === \"expired\") throw new GateExpired(id);\n if (record.status !== \"pending\") throw new GateAlreadySettled(id, record.status);\n return record;\n }\n\n /** Expiry is lazy: nothing sweeps the table, so a read is what notices the deadline passed. */\n private lapseIfExpired(record: GateRecord): GateRecord {\n if (record.status !== \"pending\" || !record.expiresAt) return record;\n if (Date.parse(record.expiresAt) > this.clock()) return record;\n const expired: GateRecord = { ...record, status: \"expired\", resolvedAt: this.nowIso() };\n this.update(expired);\n return expired;\n }\n\n protected nowIso(): string {\n return new Date(this.clock()).toISOString();\n }\n}\n\nfunction toIso(value: Date | string | undefined): string | undefined {\n if (value === undefined) return undefined;\n return typeof value === \"string\" ? value : value.toISOString();\n}\n","import type { JsonSchema } from \"./types.js\";\n\n/**\n * A deliberately small structural check over the JSON Schema subset `z.toJSONSchema`\n * emits. It exists so the process that resolves a gate can reject a bad payload\n * without holding the original zod schema, which by definition lives in another\n * process. The waiter still re-validates with the real zod schema, so this is the\n * boundary check, not the type guarantee.\n */\nexport function checkAgainstJsonSchema(schema: JsonSchema, value: unknown, path = \"$\"): string[] {\n const issues: string[] = [];\n collect(schema, value, path, issues);\n return issues;\n}\n\nfunction collect(schema: JsonSchema, value: unknown, path: string, issues: string[]): void {\n if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {\n checkUnion((schema.anyOf ?? schema.oneOf) as JsonSchema[], value, path, issues);\n return;\n }\n if (\"const\" in schema && !Object.is(schema.const, value)) {\n issues.push(`${path}: expected ${JSON.stringify(schema.const)}`);\n return;\n }\n if (Array.isArray(schema.enum) && !schema.enum.some((option) => Object.is(option, value))) {\n issues.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);\n return;\n }\n if (typeof schema.type !== \"string\") return;\n if (!matchesType(schema.type, value)) {\n issues.push(`${path}: expected ${schema.type}, received ${describe(value)}`);\n return;\n }\n if (schema.type === \"object\") checkObject(schema, value as Record<string, unknown>, path, issues);\n if (schema.type === \"array\") checkArray(schema, value as unknown[], path, issues);\n}\n\nfunction checkUnion(branches: JsonSchema[], value: unknown, path: string, issues: string[]): void {\n const matched = branches.some((branch) => checkAgainstJsonSchema(branch, value, path).length === 0);\n if (!matched) issues.push(`${path}: matched none of the ${branches.length} allowed shapes`);\n}\n\nfunction checkObject(schema: JsonSchema, value: Record<string, unknown>, path: string, issues: string[]): void {\n const properties = (schema.properties ?? {}) as Record<string, JsonSchema>;\n for (const key of (schema.required ?? []) as string[]) {\n if (!(key in value)) issues.push(`${path}.${key}: required`);\n }\n if (schema.additionalProperties === false) {\n for (const key of Object.keys(value)) {\n if (!(key in properties)) issues.push(`${path}.${key}: unexpected property`);\n }\n }\n for (const [key, sub] of Object.entries(properties)) {\n if (key in value) collect(sub, value[key], `${path}.${key}`, issues);\n }\n}\n\nfunction checkArray(schema: JsonSchema, value: unknown[], path: string, issues: string[]): void {\n const items = schema.items as JsonSchema | undefined;\n if (!items) return;\n value.forEach((entry, index) => collect(items, entry, `${path}[${index}]`, issues));\n}\n\nfunction matchesType(type: string, value: unknown): boolean {\n switch (type) {\n case \"object\":\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n case \"array\":\n return Array.isArray(value);\n case \"integer\":\n return Number.isInteger(value);\n case \"number\":\n return typeof value === \"number\" && Number.isFinite(value);\n case \"null\":\n return value === null;\n default:\n return typeof value === type;\n }\n}\n\nfunction describe(value: unknown): string {\n if (value === null) return \"null\";\n return Array.isArray(value) ? \"array\" : typeof value;\n}\n","import { BaseGateStore } from \"./base-store.js\";\nimport type { GateRecord } from \"./types.js\";\n\nexport interface MemoryGateStoreOptions {\n /** Epoch-millis clock, injectable so expiry is testable without waiting. */\n now?: () => number;\n}\n\n/**\n * In-process gates. Right for tests and for a single-process product that only\n * needs the pause, not the durability; anything that must survive a restart\n * wants `SqliteGateStore`.\n */\nexport class MemoryGateStore extends BaseGateStore {\n private readonly rows = new Map<string, GateRecord>();\n\n constructor(options: MemoryGateStoreOptions = {}) {\n super(options.now ?? Date.now);\n }\n\n protected insert(record: GateRecord): void {\n this.rows.set(record.id, { ...record });\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.rows.get(id);\n return row ? { ...row } : undefined;\n }\n\n protected update(record: GateRecord): void {\n this.rows.set(record.id, { ...record });\n }\n\n protected readByStatus(status: GateRecord[\"status\"]): GateRecord[] {\n return [...this.rows.values()].filter((row) => row.status === status).map((row) => ({ ...row }));\n }\n}\n","import { quoteIdent, runMigrations, type Db, type Migration } from \"@titan-design/store-sqlite\";\nimport { BaseGateStore } from \"./base-store.js\";\nimport type { GateRecord, GateStatus, JsonSchema } from \"./types.js\";\n\nexport const DEFAULT_GATE_TABLE = \"hitl_gate\";\n\nexport function gateTableDdl(name: string = DEFAULT_GATE_TABLE): string {\n const t = quoteIdent(name);\n return `\n CREATE TABLE IF NOT EXISTS ${t} (\n id TEXT PRIMARY KEY,\n prompt TEXT NOT NULL,\n schema TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n payload TEXT,\n reason TEXT,\n created_at TEXT NOT NULL,\n resolved_at TEXT,\n expires_at TEXT\n );\n CREATE INDEX IF NOT EXISTS ${quoteIdent(`${name}_pending`)}\n ON ${t} (created_at) WHERE status = 'pending';\n `;\n}\n\n/** Drop into the product's own `runMigrations` list so hitl shares one database with its domain tables. */\nexport function gateMigration(version: number, name: string = DEFAULT_GATE_TABLE): Migration {\n return { version, name: `hitl:${name}`, up: (db) => db.exec(gateTableDdl(name)) };\n}\n\nexport interface SqliteGateStoreOptions {\n table?: string;\n /** Run `gateMigration` on construction. Off when the product owns its migration list. */\n migrate?: boolean;\n now?: () => number;\n}\n\ninterface RawGateRow {\n id: string;\n prompt: string;\n schema: string | null;\n status: GateStatus;\n payload: string | null;\n reason: string | null;\n created_at: string;\n resolved_at: string | null;\n expires_at: string | null;\n}\n\n/**\n * Durable gates. Two instances on the same file are two processes as far as the\n * gate is concerned, which is the entire point: whoever resolves the row does\n * not have to be whoever opened it.\n */\nexport class SqliteGateStore extends BaseGateStore {\n private readonly table: string;\n\n constructor(\n private readonly db: Db,\n options: SqliteGateStoreOptions = {},\n ) {\n super(options.now ?? Date.now);\n this.table = options.table ?? DEFAULT_GATE_TABLE;\n if (options.migrate ?? true) runMigrations(db, [gateMigration(1, this.table)]);\n }\n\n protected insert(record: GateRecord): void {\n this.db\n .prepare(\n `INSERT INTO ${quoteIdent(this.table)}\n (id, prompt, schema, status, payload, reason, created_at, resolved_at, expires_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n .run(...columns(record));\n }\n\n protected read(id: string): GateRecord | undefined {\n const row = this.db.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE id = ?`).get(id) as\n | RawGateRow\n | undefined;\n return row ? toRecord(row) : undefined;\n }\n\n protected update(record: GateRecord): void {\n this.db\n .prepare(\n `UPDATE ${quoteIdent(this.table)}\n SET status = ?, payload = ?, reason = ?, resolved_at = ?\n WHERE id = ?`,\n )\n .run(record.status, toJson(record.payload), record.reason ?? null, record.resolvedAt ?? null, record.id);\n }\n\n protected readByStatus(status: GateStatus): GateRecord[] {\n const rows = this.db\n .prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE status = ? ORDER BY created_at`)\n .all(status) as RawGateRow[];\n return rows.map(toRecord);\n }\n}\n\nfunction columns(record: GateRecord): unknown[] {\n return [\n record.id,\n record.prompt,\n record.schema ? JSON.stringify(record.schema) : null,\n record.status,\n toJson(record.payload),\n record.reason ?? null,\n record.createdAt,\n record.resolvedAt ?? null,\n record.expiresAt ?? null,\n ];\n}\n\n/** `undefined` and a stored JSON `null` are different states, so only the former becomes a SQL NULL. */\nfunction toJson(payload: unknown): string | null {\n return payload === undefined ? null : JSON.stringify(payload);\n}\n\nfunction toRecord(row: RawGateRow): GateRecord {\n return {\n id: row.id,\n prompt: row.prompt,\n schema: row.schema ? (JSON.parse(row.schema) as JsonSchema) : undefined,\n status: row.status,\n payload: row.payload === null ? undefined : JSON.parse(row.payload),\n reason: row.reason ?? undefined,\n createdAt: row.created_at,\n resolvedAt: row.resolved_at ?? undefined,\n expiresAt: row.expires_at ?? undefined,\n };\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@titan-design/hitl",
3
+ "version": "0.1.0",
4
+ "description": "Human-in-the-loop gate()/resolve() primitive",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "author": "Henry Jewkes",
19
+ "license": "MIT",
20
+ "homepage": "https://github.com/HJewkes/titan-platform#readme",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/HJewkes/titan-platform.git",
24
+ "directory": "packages/hitl"
25
+ },
26
+ "bugs": "https://github.com/HJewkes/titan-platform/issues",
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@titan-design/store-sqlite": "0.1.0"
35
+ },
36
+ "peerDependencies": {
37
+ "zod": "^4.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "zod": "^4.4.3"
41
+ },
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "typecheck": "tsc --noEmit",
45
+ "lint": "eslint src"
46
+ }
47
+ }