@smithers-orchestrator/gateway 0.24.0 → 0.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/gateway",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Stable Smithers Gateway RPC contracts, auth scopes, and deployment metadata",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -8,6 +8,12 @@ export const GATEWAY_SCOPE_VALUES = [
8
8
  "signal:submit",
9
9
  "cron:read",
10
10
  "cron:write",
11
+ "account:read",
12
+ "memory:read",
13
+ "prompt:read",
14
+ "score:read",
15
+ "ticket:read",
16
+ "ticket:write",
11
17
  "observability:read",
12
18
  ] as const;
13
19
 
@@ -21,11 +27,18 @@ export const GATEWAY_SCOPE_DESCRIPTIONS: Record<GatewayScope, string> = {
21
27
  "signal:submit": "Submit workflow signals.",
22
28
  "cron:read": "List cron schedules.",
23
29
  "cron:write": "Create, delete, and trigger cron schedules.",
30
+ "account:read": "List registered agent accounts (API keys redacted).",
31
+ "memory:read": "List cross-run memory facts.",
32
+ "prompt:read": "List registered prompts.",
33
+ "score:read": "List scorer/eval results for a run.",
34
+ "ticket:read": "List work docs (tickets/plans/specs/proposals).",
35
+ "ticket:write": "Create, update, and soft-delete work docs.",
24
36
  "observability:read": "Read DevTools and other observability streams.",
25
37
  };
26
38
 
27
39
  const RUN_SCOPE_ORDER: GatewayScope[] = ["run:read", "run:write", "run:admin"];
28
40
  const CRON_SCOPE_ORDER: GatewayScope[] = ["cron:read", "cron:write"];
41
+ const TICKET_SCOPE_ORDER: GatewayScope[] = ["ticket:read", "ticket:write"];
29
42
 
30
43
  function normalizeScope(scope: string): string {
31
44
  return scope.trim();
@@ -45,15 +58,18 @@ function gatewayScopeImplies(granted: GatewayScope, required: GatewayScope): boo
45
58
  if (granted.startsWith("cron:") && required.startsWith("cron:")) {
46
59
  return CRON_SCOPE_ORDER.indexOf(granted) >= CRON_SCOPE_ORDER.indexOf(required);
47
60
  }
61
+ if (granted.startsWith("ticket:") && required.startsWith("ticket:")) {
62
+ return TICKET_SCOPE_ORDER.indexOf(granted) >= TICKET_SCOPE_ORDER.indexOf(required);
63
+ }
48
64
  return false;
49
65
  }
50
66
 
51
67
  function legacyAccessImplies(scope: string, required: GatewayScope): boolean {
52
68
  switch (scope) {
53
69
  case "read":
54
- return required === "run:read" || required === "cron:read" || required === "observability:read";
70
+ return required === "run:read" || required === "cron:read" || required === "account:read" || required === "memory:read" || required === "prompt:read" || required === "score:read" || required === "ticket:read" || required === "observability:read";
55
71
  case "execute":
56
- return required === "run:read" || required === "run:write" || required === "signal:submit" || required === "cron:read" || required === "cron:write";
72
+ return required === "run:read" || required === "run:write" || required === "signal:submit" || required === "cron:read" || required === "cron:write" || required === "ticket:read" || required === "ticket:write";
57
73
  case "approve":
58
74
  return required === "approval:submit" || legacyAccessImplies("execute", required);
59
75
  case "admin":
package/src/rpc/index.ts CHANGED
@@ -32,6 +32,7 @@ export type GatewayRpcErrorCode =
32
32
  | "RunNotFound"
33
33
  | "RUN_NOT_ACTIVE"
34
34
  | "CronNotFound"
35
+ | "TicketNotFound"
35
36
  | "NodeNotFound"
36
37
  | "IterationNotFound"
37
38
  | "NodeHasNoOutput"
@@ -79,8 +80,10 @@ export type GatewayRpcMethod =
79
80
  | "submitSignal"
80
81
  | "getRun"
81
82
  | "listRuns"
83
+ | "getSchemaSignature"
82
84
  | "listWorkflows"
83
85
  | "listApprovals"
86
+ | "listDocs"
84
87
  | "streamRunEvents"
85
88
  | "streamDevTools"
86
89
  | "getNodeOutput"
@@ -88,7 +91,15 @@ export type GatewayRpcMethod =
88
91
  | "cronList"
89
92
  | "cronCreate"
90
93
  | "cronDelete"
91
- | "cronRun";
94
+ | "cronRun"
95
+ | "listAccounts"
96
+ | "listMemoryFacts"
97
+ | "listPrompts"
98
+ | "listScores"
99
+ | "listTickets"
100
+ | "createTicket"
101
+ | "updateTicket"
102
+ | "deleteTicket";
92
103
 
93
104
  export type LaunchRunRequest = {
94
105
  workflow: string;
@@ -178,6 +189,14 @@ export type ListRunsRequest = {
178
189
  };
179
190
  };
180
191
 
192
+ export type GetSchemaSignatureRequest = Record<string, never>;
193
+
194
+ export type GetSchemaSignatureResponse = {
195
+ schemaVersion: string;
196
+ signature: string;
197
+ components?: Record<string, string>;
198
+ };
199
+
181
200
  export type GatewayWorkflowSummary = {
182
201
  key: string;
183
202
  readableName?: string;
@@ -219,6 +238,26 @@ export type ListApprovalsRequest = {
219
238
 
220
239
  export type ListApprovalsResponse = GatewayApprovalSummary[];
221
240
 
241
+ export type GatewayDocRow = {
242
+ path: string;
243
+ kind: "ticket" | "plan" | "spec" | "proposal" | "conflict" | string;
244
+ content: string;
245
+ contentHash: string;
246
+ updatedAtMs: number;
247
+ deletedAtMs: number | null;
248
+ };
249
+
250
+ export type ListDocsRequest = {
251
+ filter?: {
252
+ kind?: string;
253
+ includeDeleted?: boolean;
254
+ updatedAfterMs?: number;
255
+ limit?: number;
256
+ };
257
+ };
258
+
259
+ export type ListDocsResponse = GatewayDocRow[];
260
+
222
261
  export type StreamRunEventsRequest = {
223
262
  runId: string;
224
263
  afterSeq?: number;
@@ -266,6 +305,158 @@ export type CronRunRequest = {
266
305
  input?: Record<string, unknown>;
267
306
  };
268
307
 
308
+ /**
309
+ * One registered Smithers agent account — a row in the user-level
310
+ * `~/.smithers/accounts.json` registry that the `smithers agents` CLI manages,
311
+ * surfaced read-only by the `listAccounts` server handler (via the
312
+ * `@smithers-orchestrator/accounts` package). Each account is either a
313
+ * subscription provider (a per-account CLI `configDir`) or an API provider (an
314
+ * `apiKey`); the two are mutually exclusive.
315
+ *
316
+ * SECRET REDACTION: the raw `apiKey` is NEVER sent over the wire — it is a
317
+ * plaintext credential stored mode-600 on disk. Instead `hasApiKey` reports
318
+ * whether an api-key account carries a non-empty key, and `hasConfigDir`
319
+ * reports whether a subscription account has a config dir, so a client can
320
+ * render the account's auth posture without ever receiving the secret.
321
+ */
322
+ export type GatewayAccount = {
323
+ /** Unique account label (the registry key, `--label`). */
324
+ label: string;
325
+ /** Provider id, one of the fixed `smithers agents` catalog. */
326
+ provider:
327
+ | "claude-code"
328
+ | "antigravity"
329
+ | "codex"
330
+ | "gemini"
331
+ | "kimi"
332
+ | "anthropic-api"
333
+ | "openai-api"
334
+ | "gemini-api";
335
+ /** Per-account CLI config dir for subscription providers (absent for api-key accounts). */
336
+ configDir?: string | null;
337
+ /** True when a subscription account has a non-empty config dir. */
338
+ hasConfigDir: boolean;
339
+ /** True when an api-key account carries a non-empty key (the key itself is NEVER returned). */
340
+ hasApiKey: boolean;
341
+ /** Optional default model baked into the account. */
342
+ model?: string | null;
343
+ /** ISO timestamp of when the account was added, when known. */
344
+ addedAt?: string | null;
345
+ };
346
+
347
+ export type ListAccountsRequest = Record<string, never>;
348
+
349
+ export type ListAccountsResponse = GatewayAccount[];
350
+
351
+ /** One cross-run memory fact row (the `_smithers_memory_facts` table, snake→camel cased). */
352
+ export type GatewayMemoryFact = {
353
+ namespace: string;
354
+ key: string;
355
+ valueJson: string;
356
+ schemaSig?: string | null;
357
+ createdAtMs: number;
358
+ updatedAtMs: number;
359
+ ttlMs?: number | null;
360
+ };
361
+
362
+ export type ListMemoryFactsRequest = {
363
+ namespace?: string;
364
+ };
365
+
366
+ export type ListMemoryFactsResponse = GatewayMemoryFact[];
367
+
368
+ /**
369
+ * One registered prompt row — a `.md`/`.mdx` file walked from the project's
370
+ * `.smithers/prompts/` directory by the `listPrompts` server handler. `id` is the
371
+ * prompt's relative path without extension (e.g. `refactor` or
372
+ * `release-content/changelog`); `entryFile` is the workspace-relative source path
373
+ * (e.g. `prompts/refactor.mdx`); `source` is the raw file text. The timestamps
374
+ * come from `fs.stat` (`birthtimeMs`/`mtimeMs`) so a freshly-edited prompt sorts
375
+ * recent.
376
+ */
377
+ export type GatewayPrompt = {
378
+ id: string;
379
+ entryFile: string;
380
+ source: string;
381
+ createdAtMs?: number;
382
+ updatedAtMs?: number;
383
+ };
384
+
385
+ export type ListPromptsRequest = Record<string, never>;
386
+
387
+ export type ListPromptsResponse = GatewayPrompt[];
388
+
389
+ /**
390
+ * One scorer/eval result row (the `_smithers_scorers` table, snake→camel cased).
391
+ * `score` is the scorer's verdict; `latencyMs`/`durationMs` are the only timing
392
+ * metrics the table carries (there is NO token/cost data — those tiles are
393
+ * computed client-side and em-dashed when absent).
394
+ */
395
+ export type GatewayScoreRow = {
396
+ runId: string;
397
+ nodeId: string;
398
+ iteration: number;
399
+ attempt: number;
400
+ scorerId: string;
401
+ scorerName: string;
402
+ source: string;
403
+ score: number;
404
+ reason?: string | null;
405
+ scoredAtMs: number;
406
+ latencyMs?: number | null;
407
+ durationMs?: number | null;
408
+ };
409
+
410
+ export type ListScoresRequest = {
411
+ runId: string;
412
+ nodeId?: string;
413
+ };
414
+
415
+ export type ListScoresResponse = GatewayScoreRow[];
416
+
417
+ /** A doc kind stored in `_smithers_docs`; the tickets surface uses `ticket`. */
418
+ export type GatewayDocKind = "ticket" | "plan" | "spec" | "proposal";
419
+
420
+ /**
421
+ * One LIVE doc row (the `_smithers_docs` table, snake→camel cased) returned by
422
+ * `listTickets`. Tombstones (`deletedAtMs != null`) are filtered server-side and
423
+ * NEVER appear here. `status` rides the row so a ticket's status survives reload
424
+ * (LOCKED Path A); `contentHash` is `sha256(content)`.
425
+ */
426
+ export type GatewayTicketRow = {
427
+ path: string;
428
+ kind: GatewayDocKind;
429
+ content: string;
430
+ contentHash: string;
431
+ status?: string | null;
432
+ updatedAtMs: number;
433
+ };
434
+
435
+ export type ListTicketsRequest = {
436
+ /** Optional doc-kind filter; omit to list every kind. Defaults to all. */
437
+ kind?: GatewayDocKind;
438
+ };
439
+
440
+ export type ListTicketsResponse = GatewayTicketRow[];
441
+
442
+ export type CreateTicketRequest = {
443
+ /** Doc identity (PK); e.g. a ticket id like `feat-issues-card`. */
444
+ path: string;
445
+ content: string;
446
+ kind?: GatewayDocKind;
447
+ status?: string;
448
+ };
449
+
450
+ export type UpdateTicketRequest = {
451
+ path: string;
452
+ content?: string;
453
+ status?: string;
454
+ };
455
+
456
+ export type DeleteTicketRequest = {
457
+ path: string;
458
+ };
459
+
269
460
  const stringSchema = (description: string): JsonSchema => ({ type: "string", description });
270
461
  const booleanSchema = (description: string): JsonSchema => ({ type: "boolean", description });
271
462
  const integerSchema = (description: string, minimum = 0): JsonSchema => ({
@@ -361,6 +552,7 @@ export const GATEWAY_RPC_ERRORS: Record<GatewayRpcErrorCode, GatewayRpcErrorDefi
361
552
  RunNotFound: { version: SMITHERS_API_VERSION, code: "RunNotFound", httpStatus: 404, description: "The run does not exist." },
362
553
  RUN_NOT_ACTIVE: { version: SMITHERS_API_VERSION, code: "RUN_NOT_ACTIVE", httpStatus: 409, description: "The run is not currently active and cannot be cancelled." },
363
554
  CronNotFound: { version: SMITHERS_API_VERSION, code: "CronNotFound", httpStatus: 404, description: "The cron schedule does not exist." },
555
+ TicketNotFound: { version: SMITHERS_API_VERSION, code: "TicketNotFound", httpStatus: 404, description: "The ticket/work doc does not exist." },
364
556
  NodeNotFound: { version: SMITHERS_API_VERSION, code: "NodeNotFound", httpStatus: 404, description: "The node does not exist on the run." },
365
557
  IterationNotFound: { version: SMITHERS_API_VERSION, code: "IterationNotFound", httpStatus: 404, description: "The requested node iteration does not exist." },
366
558
  NodeHasNoOutput: { version: SMITHERS_API_VERSION, code: "NodeHasNoOutput", httpStatus: 404, description: "The node has not produced output." },
@@ -543,6 +735,24 @@ export const GATEWAY_RPC_DEFINITIONS: readonly GatewayRpcDefinition[] = [
543
735
  exampleRequest: { filter: { status: "finished", limit: 20 } },
544
736
  exampleResponse: [{ runId: "run_01", workflowKey: "deploy", status: "finished", createdAtMs: 1710000000000 }],
545
737
  },
738
+ {
739
+ version: SMITHERS_API_VERSION,
740
+ method: "getSchemaSignature",
741
+ title: "Get Schema Signature",
742
+ description: "Return the server Smithers schema migration head and table-catalog signature used by client persistence.",
743
+ maturity: "stable",
744
+ transport: "http+websocket",
745
+ requiredScope: "run:read",
746
+ requestSchema: objectSchema({}),
747
+ responseSchema: objectSchema({
748
+ schemaVersion: stringSchema("Current _smithers_schema_migrations head id."),
749
+ signature: stringSchema("Stable hash of the server schema catalog."),
750
+ components: objectSchema({}, [], "Per-table schema component hashes.", true),
751
+ }, ["schemaVersion", "signature"]),
752
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
753
+ exampleRequest: {},
754
+ exampleResponse: { schemaVersion: "0016", signature: "sha256" },
755
+ },
546
756
  {
547
757
  version: SMITHERS_API_VERSION,
548
758
  method: "listWorkflows",
@@ -587,6 +797,34 @@ export const GATEWAY_RPC_DEFINITIONS: readonly GatewayRpcDefinition[] = [
587
797
  exampleRequest: { filter: { workflow: "deploy", limit: 20 } },
588
798
  exampleResponse: [{ runId: "run_01", workflowKey: "deploy", nodeId: "approve", iteration: 0, requestTitle: "Approve deploy", requestedAtMs: 1710000000000 }],
589
799
  },
800
+ {
801
+ version: SMITHERS_API_VERSION,
802
+ method: "listDocs",
803
+ title: "List Docs",
804
+ description: "List DB-backed Smithers markdown artifacts for tickets, plans, specs, proposals, and conflict markers.",
805
+ maturity: "stable",
806
+ transport: "http+websocket",
807
+ requiredScope: "run:read",
808
+ requestSchema: objectSchema({
809
+ filter: objectSchema({
810
+ kind: stringSchema("Optional doc kind filter."),
811
+ includeDeleted: booleanSchema("Include tombstone rows."),
812
+ updatedAfterMs: integerSchema("Only return docs updated after this millisecond timestamp.", 0),
813
+ limit: integerSchema("Maximum number of docs.", 1),
814
+ }),
815
+ }),
816
+ responseSchema: arraySchema(objectSchema({
817
+ path: stringSchema("Path relative to .smithers, e.g. tickets/smithers/0030.md."),
818
+ kind: stringSchema("ticket, plan, spec, proposal, or conflict."),
819
+ content: stringSchema("Markdown or conflict marker content."),
820
+ contentHash: stringSchema("SHA-256 hash of content."),
821
+ updatedAtMs: integerSchema("Last DB update time in milliseconds.", 0),
822
+ deletedAtMs: { type: ["integer", "null"], description: "Tombstone timestamp when deleted." },
823
+ }, ["path", "kind", "content", "contentHash", "updatedAtMs"]), "Smithers docs rows."),
824
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
825
+ exampleRequest: { filter: { kind: "ticket", limit: 100 } },
826
+ exampleResponse: [{ path: "tickets/smithers/0030-jjhub-sse-seam.md", kind: "ticket", content: "# Ticket", contentHash: "sha256", updatedAtMs: 1710000000000, deletedAtMs: null }],
827
+ },
590
828
  {
591
829
  version: SMITHERS_API_VERSION,
592
830
  method: "streamRunEvents",
@@ -704,6 +942,195 @@ export const GATEWAY_RPC_DEFINITIONS: readonly GatewayRpcDefinition[] = [
704
942
  exampleRequest: { cronId: "cron_01", input: { dryRun: true } },
705
943
  exampleResponse: { runId: "run_02", workflow: "deploy" },
706
944
  },
945
+ {
946
+ version: SMITHERS_API_VERSION,
947
+ method: "listAccounts",
948
+ title: "List Accounts",
949
+ description: "List the registered Smithers agent accounts — the entries in the user-level `~/.smithers/accounts.json` registry that the `smithers agents` CLI manages. Each account's raw API key is redacted; `hasApiKey`/`hasConfigDir` report its auth posture instead.",
950
+ maturity: "stable",
951
+ transport: "http+websocket",
952
+ requiredScope: "account:read",
953
+ requestSchema: objectSchema({}),
954
+ responseSchema: arraySchema(objectSchema({
955
+ label: stringSchema("Unique account label (the registry key)."),
956
+ provider: {
957
+ type: "string",
958
+ enum: ["claude-code", "antigravity", "codex", "gemini", "kimi", "anthropic-api", "openai-api", "gemini-api"],
959
+ description: "Provider id, one of the fixed `smithers agents` catalog.",
960
+ },
961
+ configDir: { type: ["string", "null"], description: "Per-account CLI config dir for subscription providers (null for api-key accounts)." },
962
+ hasConfigDir: booleanSchema("True when a subscription account has a non-empty config dir."),
963
+ hasApiKey: booleanSchema("True when an api-key account carries a non-empty key (the key itself is never returned)."),
964
+ model: { type: ["string", "null"], description: "Optional default model baked into the account." },
965
+ addedAt: { type: ["string", "null"], description: "ISO timestamp of when the account was added, when known." },
966
+ }, ["label", "provider", "hasConfigDir", "hasApiKey"]), "Registered agent accounts."),
967
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
968
+ exampleRequest: {},
969
+ exampleResponse: [{ label: "claude-work", provider: "claude-code", configDir: "/Users/me/.claude", hasConfigDir: true, hasApiKey: false, model: "claude-opus-4-8", addedAt: "2026-01-01T00:00:00.000Z" }],
970
+ },
971
+ {
972
+ version: SMITHERS_API_VERSION,
973
+ method: "listMemoryFacts",
974
+ title: "List Memory Facts",
975
+ description: "List cross-run memory facts, optionally scoped to a namespace.",
976
+ maturity: "stable",
977
+ transport: "http+websocket",
978
+ requiredScope: "memory:read",
979
+ requestSchema: objectSchema({ namespace: stringSchema("Optional namespace filter (e.g. 'workflow:deploy').") }),
980
+ responseSchema: arraySchema(objectSchema({
981
+ namespace: stringSchema("Fact namespace."),
982
+ key: stringSchema("Fact key (unique within the namespace)."),
983
+ valueJson: stringSchema("Stored value as a JSON string."),
984
+ schemaSig: { type: ["string", "null"], description: "Optional value-schema signature." },
985
+ createdAtMs: integerSchema("Unix epoch milliseconds when first written.", 0),
986
+ updatedAtMs: integerSchema("Unix epoch milliseconds when last written.", 0),
987
+ ttlMs: { type: ["integer", "null"], description: "Time-to-live in milliseconds, or null when the fact does not expire." },
988
+ }, ["namespace", "key", "valueJson", "createdAtMs", "updatedAtMs"]), "Memory facts."),
989
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
990
+ exampleRequest: { namespace: "workflow:deploy" },
991
+ exampleResponse: [{ namespace: "workflow:deploy", key: "last-sha", valueJson: "\"abc123\"", createdAtMs: 1710000000000, updatedAtMs: 1710000000000 }],
992
+ },
993
+ {
994
+ version: SMITHERS_API_VERSION,
995
+ method: "listPrompts",
996
+ title: "List Prompts",
997
+ description: "List registered prompts — the `.md`/`.mdx` files under the project's `.smithers/prompts/` directory, each returned with its source.",
998
+ maturity: "stable",
999
+ transport: "http+websocket",
1000
+ requiredScope: "prompt:read",
1001
+ requestSchema: objectSchema({}),
1002
+ responseSchema: arraySchema(objectSchema({
1003
+ id: stringSchema("Prompt id — the relative path under `.smithers/prompts/` without its extension (e.g. 'refactor')."),
1004
+ entryFile: stringSchema("Workspace-relative source path (e.g. 'prompts/refactor.mdx')."),
1005
+ source: stringSchema("Raw prompt file text."),
1006
+ createdAtMs: integerSchema("Unix epoch milliseconds the file was created (fs birthtime).", 0),
1007
+ updatedAtMs: integerSchema("Unix epoch milliseconds the file was last modified (fs mtime).", 0),
1008
+ }, ["id", "entryFile", "source"]), "Registered prompts."),
1009
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1010
+ exampleRequest: {},
1011
+ exampleResponse: [{ id: "refactor", entryFile: "prompts/refactor.mdx", source: "# Refactor\n\nRefactor {{file}}.", createdAtMs: 1710000000000, updatedAtMs: 1710000000000 }],
1012
+ },
1013
+ {
1014
+ version: SMITHERS_API_VERSION,
1015
+ method: "listScores",
1016
+ title: "List Scores",
1017
+ description: "List scorer/eval results for one run, optionally scoped to a node.",
1018
+ maturity: "stable",
1019
+ transport: "http+websocket",
1020
+ requiredScope: "score:read",
1021
+ requestSchema: objectSchema({
1022
+ runId: stringSchema("Run id whose scorer results to list."),
1023
+ nodeId: stringSchema("Optional node id filter."),
1024
+ }, ["runId"]),
1025
+ responseSchema: arraySchema(objectSchema({
1026
+ runId: stringSchema("Run id the score belongs to."),
1027
+ nodeId: stringSchema("Node id the score was produced for."),
1028
+ iteration: integerSchema("Node iteration the score belongs to.", 0),
1029
+ attempt: integerSchema("Node attempt the score belongs to.", 0),
1030
+ scorerId: stringSchema("Stable scorer id."),
1031
+ scorerName: stringSchema("Human scorer name."),
1032
+ source: stringSchema("Where the score came from (e.g. 'scorer', 'eval')."),
1033
+ score: { type: "number", description: "The scorer's numeric verdict." },
1034
+ reason: { type: ["string", "null"], description: "Optional human reason for the score." },
1035
+ scoredAtMs: integerSchema("Unix epoch milliseconds when the score was recorded.", 0),
1036
+ latencyMs: { type: ["number", "null"], description: "Optional scorer latency in milliseconds." },
1037
+ durationMs: { type: ["number", "null"], description: "Optional node duration in milliseconds." },
1038
+ }, ["runId", "nodeId", "iteration", "attempt", "scorerId", "scorerName", "source", "score", "scoredAtMs"]), "Scorer results."),
1039
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "RunNotFound", "Internal"],
1040
+ exampleRequest: { runId: "run_01", nodeId: "review" },
1041
+ exampleResponse: [{ runId: "run_01", nodeId: "review", iteration: 0, attempt: 0, scorerId: "correctness", scorerName: "correctness", source: "scorer", score: 0.92, scoredAtMs: 1710000000000 }],
1042
+ },
1043
+ {
1044
+ version: SMITHERS_API_VERSION,
1045
+ method: "listTickets",
1046
+ title: "List Tickets",
1047
+ description: "List live work docs (tickets/plans/specs/proposals) from `_smithers_docs`; soft-deleted tombstones are never returned.",
1048
+ maturity: "stable",
1049
+ transport: "http+websocket",
1050
+ requiredScope: "ticket:read",
1051
+ requestSchema: objectSchema({
1052
+ kind: { type: "string", enum: ["ticket", "plan", "spec", "proposal"], description: "Optional doc-kind filter; omit to list every kind." },
1053
+ }),
1054
+ responseSchema: arraySchema(objectSchema({
1055
+ path: stringSchema("Doc identity (primary key); e.g. a ticket id."),
1056
+ kind: { type: "string", enum: ["ticket", "plan", "spec", "proposal"], description: "Doc kind." },
1057
+ content: stringSchema("Full markdown body."),
1058
+ contentHash: stringSchema("sha256(content), lowercase hex."),
1059
+ status: { type: ["string", "null"], description: "Free-form status (e.g. todo/in-progress/done); rides the row so it survives reload." },
1060
+ updatedAtMs: integerSchema("Unix epoch milliseconds of the last write.", 0),
1061
+ }, ["path", "kind", "content", "contentHash", "updatedAtMs"]), "Live work docs."),
1062
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1063
+ exampleRequest: { kind: "ticket" },
1064
+ exampleResponse: [{ path: "feat-issues-card", kind: "ticket", content: "# Issues card", contentHash: "0000000000000000000000000000000000000000000000000000000000000000", status: "in-progress", updatedAtMs: 1710000000000 }],
1065
+ },
1066
+ {
1067
+ version: SMITHERS_API_VERSION,
1068
+ method: "createTicket",
1069
+ title: "Create Ticket",
1070
+ description: "Create or replace a work doc by `path`. Stamps `content_hash = sha256(content)` and `updated_at_ms = now`; reviving a previously soft-deleted path is intentional.",
1071
+ maturity: "stable",
1072
+ transport: "http+websocket",
1073
+ requiredScope: "ticket:write",
1074
+ requestSchema: objectSchema({
1075
+ path: stringSchema("Doc identity (primary key); e.g. `feat-issues-card`."),
1076
+ content: stringSchema("Full markdown body."),
1077
+ kind: { type: "string", enum: ["ticket", "plan", "spec", "proposal"], description: "Doc kind (default `ticket`)." },
1078
+ status: stringSchema("Optional initial status."),
1079
+ }, ["path", "content"]),
1080
+ responseSchema: objectSchema({
1081
+ path: stringSchema("Doc identity (primary key)."),
1082
+ kind: { type: "string", enum: ["ticket", "plan", "spec", "proposal"], description: "Doc kind." },
1083
+ content: stringSchema("Full markdown body."),
1084
+ contentHash: stringSchema("sha256(content), lowercase hex."),
1085
+ status: { type: ["string", "null"], description: "Free-form status." },
1086
+ updatedAtMs: integerSchema("Unix epoch milliseconds of the write.", 0),
1087
+ }, ["path", "kind", "content", "contentHash", "updatedAtMs"], "The created doc row."),
1088
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "Internal"],
1089
+ exampleRequest: { path: "feat-issues-card", content: "# Issues card", kind: "ticket", status: "todo" },
1090
+ exampleResponse: { path: "feat-issues-card", kind: "ticket", content: "# Issues card", contentHash: "0000000000000000000000000000000000000000000000000000000000000000", status: "todo", updatedAtMs: 1710000000000 },
1091
+ },
1092
+ {
1093
+ version: SMITHERS_API_VERSION,
1094
+ method: "updateTicket",
1095
+ title: "Update Ticket",
1096
+ description: "Patch a work doc's `content` and/or `status` by `path`. Re-stamps `content_hash` + `updated_at_ms` when content changes.",
1097
+ maturity: "stable",
1098
+ transport: "http+websocket",
1099
+ requiredScope: "ticket:write",
1100
+ requestSchema: objectSchema({
1101
+ path: stringSchema("Doc identity (primary key)."),
1102
+ content: stringSchema("Optional new markdown body."),
1103
+ status: stringSchema("Optional new status."),
1104
+ }, ["path"]),
1105
+ responseSchema: objectSchema({
1106
+ path: stringSchema("Doc identity (primary key)."),
1107
+ kind: { type: "string", enum: ["ticket", "plan", "spec", "proposal"], description: "Doc kind." },
1108
+ content: stringSchema("Full markdown body."),
1109
+ contentHash: stringSchema("sha256(content), lowercase hex."),
1110
+ status: { type: ["string", "null"], description: "Free-form status." },
1111
+ updatedAtMs: integerSchema("Unix epoch milliseconds of the write.", 0),
1112
+ }, ["path", "kind", "content", "contentHash", "updatedAtMs"], "The updated doc row."),
1113
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "TicketNotFound", "Internal"],
1114
+ exampleRequest: { path: "feat-issues-card", status: "in-progress" },
1115
+ exampleResponse: { path: "feat-issues-card", kind: "ticket", content: "# Issues card", contentHash: "0000000000000000000000000000000000000000000000000000000000000000", status: "in-progress", updatedAtMs: 1710000000000 },
1116
+ },
1117
+ {
1118
+ version: SMITHERS_API_VERSION,
1119
+ method: "deleteTicket",
1120
+ title: "Delete Ticket",
1121
+ description: "Soft-delete a work doc by `path` (stamps a `deleted_at_ms` tombstone). The row survives so `listTickets` hides it without losing history; the watcher never materializes a tombstone to disk.",
1122
+ maturity: "stable",
1123
+ transport: "http+websocket",
1124
+ requiredScope: "ticket:write",
1125
+ requestSchema: objectSchema({ path: stringSchema("Doc identity (primary key).") }, ["path"]),
1126
+ responseSchema: objectSchema({
1127
+ path: stringSchema("Doc identity (primary key)."),
1128
+ deleted: booleanSchema("True when the doc was soft-deleted."),
1129
+ }, ["path", "deleted"], "Soft-delete acknowledgement."),
1130
+ errors: ["InvalidRequest", "Unauthorized", "Forbidden", "TicketNotFound", "Internal"],
1131
+ exampleRequest: { path: "feat-issues-card" },
1132
+ exampleResponse: { path: "feat-issues-card", deleted: true },
1133
+ },
707
1134
  ] as const;
708
1135
 
709
1136
  const definitionByMethod = new Map<string, GatewayRpcDefinition>(