@sigrun/sigrun 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/CONTEXT.md +163 -0
  3. package/LICENSE +21 -0
  4. package/README.md +691 -2
  5. package/THIRD_PARTY_NOTICES.md +28 -0
  6. package/dist/backup/marker.js +80 -0
  7. package/dist/backup/run-backup.js +339 -0
  8. package/dist/cli.js +952 -0
  9. package/dist/commands/expand.js +245 -0
  10. package/dist/commands/templates.js +272 -0
  11. package/dist/config.js +1146 -0
  12. package/dist/extension.d.ts +431 -0
  13. package/dist/harness/activity.js +42 -0
  14. package/dist/harness/ambient.js +261 -0
  15. package/dist/harness/auth.js +130 -0
  16. package/dist/harness/boot.js +865 -0
  17. package/dist/harness/builtins.js +191 -0
  18. package/dist/harness/event-bus.js +1203 -0
  19. package/dist/harness/health.js +30 -0
  20. package/dist/harness/identity/index.js +37 -0
  21. package/dist/harness/identity/invites.js +94 -0
  22. package/dist/harness/identity/json-store.js +108 -0
  23. package/dist/harness/identity/setup-code.js +81 -0
  24. package/dist/harness/identity/tokens.js +101 -0
  25. package/dist/harness/identity/users.js +93 -0
  26. package/dist/harness/interactions.js +283 -0
  27. package/dist/harness/loader.js +1236 -0
  28. package/dist/harness/memory-default.js +35 -0
  29. package/dist/harness/naming.js +193 -0
  30. package/dist/harness/packages.js +1002 -0
  31. package/dist/harness/providers/auth-bridge.js +60 -0
  32. package/dist/harness/providers/index.js +277 -0
  33. package/dist/harness/providers/models-store.js +105 -0
  34. package/dist/harness/providers/refresh.js +161 -0
  35. package/dist/harness/providers/scheduler.js +89 -0
  36. package/dist/harness/registry.js +9 -0
  37. package/dist/harness/reserved-sections.js +24 -0
  38. package/dist/harness/seam-host.js +239 -0
  39. package/dist/harness/sections/context-files.js +78 -0
  40. package/dist/harness/sections/model-guidance.js +77 -0
  41. package/dist/harness/settings.js +1053 -0
  42. package/dist/harness/stores.js +450 -0
  43. package/dist/harness/tools.js +500 -0
  44. package/dist/harness/workspace.js +73 -0
  45. package/dist/index.js +39 -0
  46. package/dist/jobs/docs.js +257 -0
  47. package/dist/jobs/fire.js +342 -0
  48. package/dist/jobs/index.js +228 -0
  49. package/dist/jobs/observe.js +165 -0
  50. package/dist/jobs/reconcile.js +245 -0
  51. package/dist/jobs/schedule.js +77 -0
  52. package/dist/jobs/spec.js +248 -0
  53. package/dist/jobs/task.js +190 -0
  54. package/dist/jobs/transfer.js +53 -0
  55. package/dist/serve.js +164 -0
  56. package/dist/server/auth/claim-lock.js +28 -0
  57. package/dist/server/auth/ip.js +51 -0
  58. package/dist/server/auth/login-limiter.js +52 -0
  59. package/dist/server/auth/loopback.js +47 -0
  60. package/dist/server/auth/pipeline.js +330 -0
  61. package/dist/server/auth/principal.js +26 -0
  62. package/dist/server/commands.js +198 -0
  63. package/dist/server/content-parts.js +142 -0
  64. package/dist/server/context.js +13 -0
  65. package/dist/server/conversation-handler.js +61 -0
  66. package/dist/server/conversation-ops.js +310 -0
  67. package/dist/server/events/hub.js +39 -0
  68. package/dist/server/events/types.js +8 -0
  69. package/dist/server/helpers.js +351 -0
  70. package/dist/server/host-bridge.js +131 -0
  71. package/dist/server/index.js +232 -0
  72. package/dist/server/notifications.js +185 -0
  73. package/dist/server/ownership.js +67 -0
  74. package/dist/server/readiness.js +36 -0
  75. package/dist/server/routes/auth.js +172 -0
  76. package/dist/server/routes/conversations.js +341 -0
  77. package/dist/server/routes/events.js +161 -0
  78. package/dist/server/routes/extension-routes.js +127 -0
  79. package/dist/server/routes/extensions.js +168 -0
  80. package/dist/server/routes/files.js +308 -0
  81. package/dist/server/routes/interactions.js +260 -0
  82. package/dist/server/routes/jobs.js +271 -0
  83. package/dist/server/routes/meta.js +146 -0
  84. package/dist/server/routes/metrics.js +219 -0
  85. package/dist/server/routes/onboarding.js +268 -0
  86. package/dist/server/routes/probes.js +53 -0
  87. package/dist/server/routes/providers.js +198 -0
  88. package/dist/server/routes/settings.js +48 -0
  89. package/dist/server/routes/stream.js +131 -0
  90. package/dist/server/routes/submissions.js +317 -0
  91. package/dist/server/routes/users.js +169 -0
  92. package/dist/server/schema-fixture.js +950 -0
  93. package/dist/server/session-meta.js +247 -0
  94. package/dist/server/static.js +169 -0
  95. package/dist/server/views.js +71 -0
  96. package/dist/skills/index.js +418 -0
  97. package/node_modules/@sigrun/shared/CHANGELOG.md +16 -0
  98. package/node_modules/@sigrun/shared/LICENSE +21 -0
  99. package/node_modules/@sigrun/shared/README.md +10 -0
  100. package/node_modules/@sigrun/shared/dist/access.d.ts +55 -0
  101. package/node_modules/@sigrun/shared/dist/access.js +72 -0
  102. package/node_modules/@sigrun/shared/dist/ambient.d.ts +111 -0
  103. package/node_modules/@sigrun/shared/dist/ambient.js +23 -0
  104. package/node_modules/@sigrun/shared/dist/commands.d.ts +75 -0
  105. package/node_modules/@sigrun/shared/dist/commands.js +25 -0
  106. package/node_modules/@sigrun/shared/dist/conversations.d.ts +31 -0
  107. package/node_modules/@sigrun/shared/dist/conversations.js +94 -0
  108. package/node_modules/@sigrun/shared/dist/interactions.d.ts +91 -0
  109. package/node_modules/@sigrun/shared/dist/interactions.js +21 -0
  110. package/node_modules/@sigrun/shared/dist/jobs.d.ts +100 -0
  111. package/node_modules/@sigrun/shared/dist/jobs.js +19 -0
  112. package/node_modules/@sigrun/shared/dist/json.d.ts +20 -0
  113. package/node_modules/@sigrun/shared/dist/json.js +13 -0
  114. package/node_modules/@sigrun/shared/dist/session-meta.d.ts +103 -0
  115. package/node_modules/@sigrun/shared/dist/session-meta.js +13 -0
  116. package/node_modules/@sigrun/shared/dist/settings.d.ts +401 -0
  117. package/node_modules/@sigrun/shared/dist/settings.js +19 -0
  118. package/node_modules/@sigrun/shared/dist/themes.d.ts +50 -0
  119. package/node_modules/@sigrun/shared/dist/themes.js +156 -0
  120. package/node_modules/@sigrun/shared/dist/turns.d.ts +102 -0
  121. package/node_modules/@sigrun/shared/dist/turns.js +166 -0
  122. package/node_modules/@sigrun/shared/dist/view-delta.d.ts +54 -0
  123. package/node_modules/@sigrun/shared/dist/view-delta.js +135 -0
  124. package/node_modules/@sigrun/shared/dist/views.d.ts +742 -0
  125. package/node_modules/@sigrun/shared/dist/views.js +38 -0
  126. package/node_modules/@sigrun/shared/package.json +83 -0
  127. package/package.json +90 -5
  128. package/webui/dist/assets/index-Rl0KhCLa.css +1 -0
  129. package/webui/dist/assets/index-rOiARZLF.js +50 -0
  130. package/webui/dist/index.html +37 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The ambient seam TYPES (spec: docs/approval-ux-seams.md, "Ambient
3
+ * surfaces"): the channel-neutral record shapes for persistent
4
+ * read-mostly state — statusline slots and panel widgets — written by
5
+ * extensions through the host context and rendered by channels
6
+ * generically.
7
+ *
8
+ * The CANONICAL type declarations live here (the shared leaf package);
9
+ * `src/harness/ambient.ts` imports them (type-only) for the doc
10
+ * definitions and validation runtime. The records ride the §5 wire
11
+ * AS-IS (envelope + body + details — never projected), so these shapes
12
+ * are the channel contract too (see `@sigrun/shared/views`).
13
+ *
14
+ * Lifecycle difference from interactions: an ambient surface is replaced
15
+ * repeatedly (write = FULL state replacement, never a delta) and never
16
+ * blocks. One slot key is owned by exactly one extension (allocation,
17
+ * not silent composition — the pi-zentui chrome-skin lesson).
18
+ *
19
+ * Layering mirrors the interaction seam: strict envelope (key, place,
20
+ * budget, refresh, expires) around a body composed only from the closed
21
+ * ambient primitives (line, rows, gauge, ledger), with opaque `details`.
22
+ */
23
+ import type { JsonObject } from "./json.js";
24
+ /** Where a channel places the surface. */
25
+ export type AmbientPlace = "status" | "footer" | "panel";
26
+ /** One widget row (primitive B). */
27
+ export type AmbientRow = {
28
+ readonly id: string;
29
+ readonly label: string;
30
+ /** Channel-schemed glyph state (todo statuses, agent run states). */
31
+ readonly status?: string;
32
+ /** Severity ordering/color: 0 = severe .. 2 = quiet. */
33
+ readonly severity?: 0 | 1 | 2;
34
+ /** Folded away by default (warden's quiet verdicts). */
35
+ readonly quiet?: boolean;
36
+ readonly detail?: string;
37
+ /** Nesting depth for tree rendering (pi-goal-x task tree). */
38
+ readonly depth?: number;
39
+ /** Dependency ids for marker rendering (rpiv-todo blockedBy). */
40
+ readonly deps?: string[];
41
+ /** Present-continuous spinner label while the row is active. */
42
+ readonly spinnerLabel?: string;
43
+ readonly metrics?: AmbientMetrics;
44
+ };
45
+ /** Numeric metrics with an optional derived-format hint. */
46
+ export type AmbientMetrics = {
47
+ readonly tokens?: number;
48
+ readonly cost?: number;
49
+ readonly current?: number;
50
+ readonly total?: number;
51
+ readonly completed?: number;
52
+ /** Derived display hint for numeric fields ("2.5k", currency) — the
53
+ * host-reuse lesson from every footer extension. */
54
+ readonly format?: "compact" | "usd" | "percent";
55
+ };
56
+ /** The `line` primitive: one keyed statusline/footer line, optionally segmented. */
57
+ export type AmbientLine = {
58
+ readonly text: string;
59
+ /** Segmented composition for powerline-style slots (pi-powerline,
60
+ * pi-zentui): sub-segments with placement zones. */
61
+ readonly segments?: {
62
+ readonly id: string;
63
+ readonly text: string;
64
+ readonly place?: "left" | "middle" | "right";
65
+ }[];
66
+ };
67
+ /** The `gauge` primitive: live progress with optional check rows. */
68
+ export type AmbientGauge = {
69
+ readonly label: string;
70
+ readonly percent?: number;
71
+ readonly phase?: string;
72
+ readonly elapsedMs?: number;
73
+ readonly recentOutput?: string[];
74
+ /** Check rows (pi-goal-x auditor): row-shaped, same schema as rows[]. */
75
+ readonly checks?: AmbientRow[];
76
+ };
77
+ /** One ledger entry (primitive D): append-style feed with per-entry amend. */
78
+ export type AmbientLedgerEntry = {
79
+ readonly id: string;
80
+ readonly at: number;
81
+ readonly kind: string;
82
+ readonly summary: string;
83
+ readonly detail?: string[];
84
+ };
85
+ /** The ambient body: ONLY these keys, each from the closed primitives. */
86
+ export type AmbientBody = {
87
+ readonly line?: AmbientLine;
88
+ readonly rows?: AmbientRow[];
89
+ readonly gauge?: AmbientGauge;
90
+ readonly ledger?: AmbientLedgerEntry[];
91
+ };
92
+ /** The strict envelope + closed-primitive body of one ambient surface. */
93
+ export type AmbientRecord = {
94
+ /** The keyed slot id ("goal", "rpiv-todos", "fabric"). */
95
+ readonly key: string;
96
+ /** Which extension owns this key (set by the host context, not the author). */
97
+ readonly owner: string;
98
+ readonly place: AmbientPlace;
99
+ /** Panel row budget / line-slot width guidance (channel advisory). */
100
+ readonly maxRows?: number;
101
+ /** Overflow policy for panels (rpiv-todo's drop-completed-first rule). */
102
+ readonly overflow?: "drop" | "wrap" | "summarize";
103
+ /** Channel re-render hint between data writes (countdowns, clocks), ms. */
104
+ readonly refreshMs?: number;
105
+ /** Auto-clear rule: "never" stays until replaced with an empty body. */
106
+ readonly expires: "never" | "idle";
107
+ readonly updatedAt: number;
108
+ readonly body?: AmbientBody;
109
+ /** Opaque extension-owned payload; rendered collapsed, never interpreted. */
110
+ readonly details?: JsonObject;
111
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The ambient seam TYPES (spec: docs/approval-ux-seams.md, "Ambient
3
+ * surfaces"): the channel-neutral record shapes for persistent
4
+ * read-mostly state — statusline slots and panel widgets — written by
5
+ * extensions through the host context and rendered by channels
6
+ * generically.
7
+ *
8
+ * The CANONICAL type declarations live here (the shared leaf package);
9
+ * `src/harness/ambient.ts` imports them (type-only) for the doc
10
+ * definitions and validation runtime. The records ride the §5 wire
11
+ * AS-IS (envelope + body + details — never projected), so these shapes
12
+ * are the channel contract too (see `@sigrun/shared/views`).
13
+ *
14
+ * Lifecycle difference from interactions: an ambient surface is replaced
15
+ * repeatedly (write = FULL state replacement, never a delta) and never
16
+ * blocks. One slot key is owned by exactly one extension (allocation,
17
+ * not silent composition — the pi-zentui chrome-skin lesson).
18
+ *
19
+ * Layering mirrors the interaction seam: strict envelope (key, place,
20
+ * budget, refresh, expires) around a body composed only from the closed
21
+ * ambient primitives (line, rows, gauge, ledger), with opaque `details`.
22
+ */
23
+ export {};
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The slash-command WIRE types (Task 7, pi parity) — the client-facing
3
+ * half of `src/server/commands.ts` (which owns the catalog runtime).
4
+ *
5
+ * This module is part of the shared leaf package (`@sigrun/shared`): the
6
+ * tui and webui import these shapes from here, never from server source
7
+ * (the load-bearing decoupling — the tui is a pure client of the wire).
8
+ *
9
+ * PURE module: no imports at all (the views-purity guard enforces this
10
+ * across `shared/src`).
11
+ *
12
+ * Registry header (from the server module — the catalog itself lives
13
+ * there): a THIN registry of command METADATA — name, description, args
14
+ * hint, and the §5 endpoint each command drives. Deliberately NO
15
+ * server-side `execute` indirection: the composer owns the client calls,
16
+ * the registry only tells it what exists and where it goes. Scope
17
+ * (controller rulings): ONLY commands mapping to EXISTING endpoints.
18
+ * `/login`/`/logout` are core-spec §6.4 exclusions (credentials never
19
+ * cross the API); `/skill:name` and prompt templates are Task 2c's
20
+ * `kind: "expand"` entries (no endpoint — the server expands the text
21
+ * at admission). `/model` and `/thinking` ARE included: both map
22
+ * cleanly onto `POST .../configure`'s existing
23
+ * `{model?, thinking?}` body (each sends exactly one of the two fields).
24
+ */
25
+ /** The HTTP method + literal route path a command drives. */
26
+ export interface SlashCommandEndpoint {
27
+ readonly method: "GET" | "POST" | "PATCH";
28
+ /**
29
+ * The REGISTERED route path (Fastify `:param` form, e.g.
30
+ * `/v1/conversations/:id/fork`) — the composer substitutes the active
31
+ * conversation id; `server-commands.test.ts` asserts every entry
32
+ * resolves against the live route table.
33
+ */
34
+ readonly path: string;
35
+ }
36
+ /** One slash command's catalog entry. */
37
+ export interface SlashCommand {
38
+ /** The bare name (no leading slash) — the `Map` key. */
39
+ readonly name: string;
40
+ readonly description: string;
41
+ /** Argument shape shown in the menu/usage errors; absent = no args. */
42
+ readonly argsHint?: string;
43
+ /**
44
+ * The contributing extension's slug (W5 Task 3); ABSENT on core
45
+ * commands. An extension entry's `endpoint` is its invoke route
46
+ * (`POST /v1/conversations/:id/commands/<name>`) — the client POSTs
47
+ * there with `{args}` and renders a returned `notice`.
48
+ */
49
+ readonly extension?: string;
50
+ /**
51
+ * How the composer satisfies the command. ABSENT means `"endpoint"`
52
+ * (every pre-existing entry — the composer calls `endpoint`);
53
+ * `"expand"` (Task 2c) means the client inserts `/name ` into the
54
+ * composer and the SERVER expands the submitted text at admission
55
+ * (`/skill:name` and prompt templates) — no endpoint exists and none
56
+ * is needed.
57
+ */
58
+ readonly kind?: "endpoint" | "expand";
59
+ /** The endpoint an `endpoint`-kind entry drives; absent when `kind === "expand"`. */
60
+ readonly endpoint?: SlashCommandEndpoint;
61
+ }
62
+ /** The `GET /v1/commands` response (spec §5 response conventions). */
63
+ export interface CommandsResponse {
64
+ readonly commands: readonly SlashCommand[];
65
+ }
66
+ /**
67
+ * The `POST /v1/conversations/:id/commands/:name` response (W5 Task 3,
68
+ * extension commands): the command's optional human-facing notice —
69
+ * the client renders it as a transient banner (webui) / status-bar
70
+ * flash (tui). An empty body means the command returned nothing to
71
+ * say.
72
+ */
73
+ export interface CommandRunResponse {
74
+ readonly notice?: string;
75
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The slash-command WIRE types (Task 7, pi parity) — the client-facing
3
+ * half of `src/server/commands.ts` (which owns the catalog runtime).
4
+ *
5
+ * This module is part of the shared leaf package (`@sigrun/shared`): the
6
+ * tui and webui import these shapes from here, never from server source
7
+ * (the load-bearing decoupling — the tui is a pure client of the wire).
8
+ *
9
+ * PURE module: no imports at all (the views-purity guard enforces this
10
+ * across `shared/src`).
11
+ *
12
+ * Registry header (from the server module — the catalog itself lives
13
+ * there): a THIN registry of command METADATA — name, description, args
14
+ * hint, and the §5 endpoint each command drives. Deliberately NO
15
+ * server-side `execute` indirection: the composer owns the client calls,
16
+ * the registry only tells it what exists and where it goes. Scope
17
+ * (controller rulings): ONLY commands mapping to EXISTING endpoints.
18
+ * `/login`/`/logout` are core-spec §6.4 exclusions (credentials never
19
+ * cross the API); `/skill:name` and prompt templates are Task 2c's
20
+ * `kind: "expand"` entries (no endpoint — the server expands the text
21
+ * at admission). `/model` and `/thinking` ARE included: both map
22
+ * cleanly onto `POST .../configure`'s existing
23
+ * `{model?, thinking?}` body (each sends exactly one of the two fields).
24
+ */
25
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The conversation list tree (Task B1, UI refresh): sidebar shaping —
3
+ * fork nesting, recency ordering, pinned grouping and age rendering.
4
+ *
5
+ * PURE module: no imports at all (test/views-purity.test.ts enforces
6
+ * this). `ListRow` is structural — both clients' wire row types fit —
7
+ * and the only fields read are the ones the list endpoint ships.
8
+ */
9
+ export interface ListRow {
10
+ readonly id: number;
11
+ readonly name?: string;
12
+ readonly lastActivityAt?: string;
13
+ readonly parent?: {
14
+ readonly id: number;
15
+ };
16
+ readonly archived: boolean;
17
+ readonly pinned: boolean;
18
+ }
19
+ export interface TreeNode<R extends ListRow> {
20
+ readonly row: R;
21
+ readonly children: readonly TreeNode<R>[];
22
+ /** Max epoch ms over the whole subtree; 0 when nothing was ever used. */
23
+ readonly newestActivity: number;
24
+ }
25
+ export type AgeGroup = "Pinned" | "Today" | "Yesterday" | "Earlier";
26
+ export declare function buildConversationTree<R extends ListRow>(rows: readonly R[], options: {
27
+ includeArchived: boolean;
28
+ }): TreeNode<R>[];
29
+ export declare function ageGroup(node: TreeNode<ListRow>, now: Date): AgeGroup;
30
+ export declare function formatAge(iso: string | undefined, now: Date): string;
31
+ export declare function displayName(row: ListRow): string;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The conversation list tree (Task B1, UI refresh): sidebar shaping —
3
+ * fork nesting, recency ordering, pinned grouping and age rendering.
4
+ *
5
+ * PURE module: no imports at all (test/views-purity.test.ts enforces
6
+ * this). `ListRow` is structural — both clients' wire row types fit —
7
+ * and the only fields read are the ones the list endpoint ships.
8
+ */
9
+ /** Sort: newest first, ties by id descending. */
10
+ function byRecency(a, b) {
11
+ return b.newestActivity - a.newestActivity || b.row.id - a.row.id;
12
+ }
13
+ /** Epoch ms of a row's activity stamp; 0 when absent or unparsable. */
14
+ function epoch(iso) {
15
+ if (iso === undefined)
16
+ return 0;
17
+ const parsed = Date.parse(iso);
18
+ return Number.isNaN(parsed) ? 0 : parsed;
19
+ }
20
+ export function buildConversationTree(rows, options) {
21
+ const nodes = new Map();
22
+ for (const row of rows) {
23
+ // `parent` is fork lineage — an ancestor by construction (set once,
24
+ // at fork time), so the resulting forest is acyclic.
25
+ if (options.includeArchived || !row.archived) {
26
+ nodes.set(row.id, { row, children: [] });
27
+ }
28
+ }
29
+ const roots = [];
30
+ for (const node of nodes.values()) {
31
+ const parent = node.row.parent;
32
+ const parentNode = parent === undefined ? undefined : nodes.get(parent.id);
33
+ if (parentNode === undefined || parentNode === node) {
34
+ roots.push(node);
35
+ }
36
+ else {
37
+ parentNode.children.push(node);
38
+ }
39
+ }
40
+ const toNode = (node) => {
41
+ const children = node.children.map(toNode).sort(byRecency);
42
+ const own = epoch(node.row.lastActivityAt);
43
+ const newestActivity = children.reduce((max, child) => Math.max(max, child.newestActivity), own);
44
+ return { row: node.row, children, newestActivity };
45
+ };
46
+ return roots
47
+ .map(toNode)
48
+ .sort((a, b) => Number(b.row.pinned) - Number(a.row.pinned) || byRecency(a, b));
49
+ }
50
+ /** Local-calendar-day distance between two instants. */
51
+ function dayDistance(then, now) {
52
+ const midnight = (value) => {
53
+ const date = new Date(value);
54
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
55
+ };
56
+ // `round` absorbs DST's 23/25-hour days around the midnight anchor.
57
+ return Math.round((midnight(now) - midnight(then)) / 86_400_000);
58
+ }
59
+ export function ageGroup(node, now) {
60
+ if (node.row.pinned)
61
+ return "Pinned";
62
+ const days = dayDistance(node.newestActivity, now);
63
+ if (days <= 0)
64
+ return "Today";
65
+ if (days === 1)
66
+ return "Yesterday";
67
+ return "Earlier";
68
+ }
69
+ const MINUTE = 60_000;
70
+ const HOUR = 3_600_000;
71
+ const DAY = 86_400_000;
72
+ export function formatAge(iso, now) {
73
+ if (iso === undefined)
74
+ return "";
75
+ const at = Date.parse(iso);
76
+ if (Number.isNaN(at))
77
+ return "";
78
+ const age = now.getTime() - at;
79
+ if (age < MINUTE)
80
+ return "now";
81
+ if (age < HOUR)
82
+ return `${Math.floor(age / MINUTE)}m`;
83
+ if (age < DAY)
84
+ return `${Math.floor(age / HOUR)}h`;
85
+ if (age < 7 * DAY)
86
+ return `${Math.floor(age / DAY)}d`;
87
+ return new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric" }).format(at);
88
+ }
89
+ export function displayName(row) {
90
+ const flattened = row.name?.replace(/[\r\n]+/g, " ").trim();
91
+ return flattened === undefined || flattened.length === 0
92
+ ? "New conversation"
93
+ : flattened;
94
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The interaction seam TYPES (spec: docs/approval-ux-seams.md): the
3
+ * channel-neutral record shapes for "this conversation is waiting on a
4
+ * human decision", written by extensions through the host context and
5
+ * answered by channels through one generic route.
6
+ *
7
+ * The CANONICAL type declarations live here (the shared leaf package);
8
+ * `src/harness/interactions.ts` imports them (type-only) for the doc
9
+ * definitions and validation runtime. The records ride the §5 wire
10
+ * AS-IS (envelope + body + details — never projected), so these shapes
11
+ * are the channel contract too (see `@sigrun/shared/views`).
12
+ *
13
+ * Layering (strict envelope / closed primitives / opaque details):
14
+ *
15
+ * - envelope: kind, callId, status, timestamps, answer — load-bearing for
16
+ * joining tool slots and lifecycle arbitration. Strictly validated.
17
+ * - body: composed ONLY from the closed primitives (text, options,
18
+ * fields, diff). Structurally validated; unknown body keys quarantine.
19
+ * - details: opaque extension-owned JSON. Never interpreted.
20
+ */
21
+ import type { JsonObject } from "./json.js";
22
+ /** Lifecycle of one interaction record (spec statuses). */
23
+ export type InteractionStatus = "pending" | "answered" | "skipped" | "expired" | "cancelled";
24
+ /** One choice in the `options` primitive. */
25
+ export type InteractionOption = {
26
+ readonly id: string;
27
+ readonly label: string;
28
+ /** Second line under the label (pi-advisor presets, rpiv options). */
29
+ readonly description?: string;
30
+ /** Channel-schemed color/severity hint: "danger" | "warn" | "ok" | "neutral". */
31
+ readonly tone?: "danger" | "warn" | "ok" | "neutral";
32
+ /** Pre-selected by the extension's recommendation (pi-interview conviction). */
33
+ readonly recommended?: boolean;
34
+ };
35
+ /** One light-form field in the `fields` primitive. */
36
+ export type InteractionField = {
37
+ readonly name: string;
38
+ readonly type: "string" | "number" | "boolean" | "secret";
39
+ readonly label?: string;
40
+ readonly required?: boolean;
41
+ };
42
+ /** The `diff` primitive: before/after content, optionally path-scoped. */
43
+ export type InteractionDiff = {
44
+ readonly before: string;
45
+ readonly after: string;
46
+ readonly path?: string;
47
+ readonly lang?: string;
48
+ };
49
+ /**
50
+ * The interaction body: ONLY these four keys, each built only from the
51
+ * closed primitives. Everything else belongs in `details`.
52
+ */
53
+ export type InteractionBody = {
54
+ /** Prose prompt body (markdown). */
55
+ readonly text?: string;
56
+ readonly options?: InteractionOption[];
57
+ readonly fields?: InteractionField[];
58
+ readonly diff?: InteractionDiff;
59
+ };
60
+ /** What an answer may carry: chosen ids, form values, and/or free text. */
61
+ export type InteractionAnswer = {
62
+ readonly ids?: string[];
63
+ readonly values?: Readonly<Record<string, string | number | boolean>>;
64
+ readonly text?: string;
65
+ };
66
+ /** The strict envelope + closed-primitive body of one interaction. */
67
+ export type InteractionRecord = {
68
+ /** Extension-declared kind id (e.g. "gating.confirm", "questions.form"). */
69
+ readonly kind: string;
70
+ /** Stable join key to the pi.live tool slot (`ToolCall.id`). */
71
+ readonly callId: string;
72
+ readonly status: InteractionStatus;
73
+ /** Epoch ms when seeded. */
74
+ readonly createdAt: number;
75
+ /** Epoch ms after which channels may show it as expired. */
76
+ readonly expiresAt?: number;
77
+ /** Epoch ms when a terminal status landed. */
78
+ readonly settledAt?: number;
79
+ readonly answer?: InteractionAnswer;
80
+ /** Who answered: "human" via the route, or the extension itself. */
81
+ readonly answeredBy?: "human" | "extension";
82
+ /**
83
+ * The answering principal's directory username (spec §5.9): stamped by
84
+ * the answer route from `request.principal.user` — host-assigned, never
85
+ * model-asserted. Absent on the naive single-operator path (no user).
86
+ */
87
+ readonly answeredByUser?: string;
88
+ readonly body?: InteractionBody;
89
+ /** Opaque extension-owned payload; rendered collapsed, never interpreted. */
90
+ readonly details?: JsonObject;
91
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The interaction seam TYPES (spec: docs/approval-ux-seams.md): the
3
+ * channel-neutral record shapes for "this conversation is waiting on a
4
+ * human decision", written by extensions through the host context and
5
+ * answered by channels through one generic route.
6
+ *
7
+ * The CANONICAL type declarations live here (the shared leaf package);
8
+ * `src/harness/interactions.ts` imports them (type-only) for the doc
9
+ * definitions and validation runtime. The records ride the §5 wire
10
+ * AS-IS (envelope + body + details — never projected), so these shapes
11
+ * are the channel contract too (see `@sigrun/shared/views`).
12
+ *
13
+ * Layering (strict envelope / closed primitives / opaque details):
14
+ *
15
+ * - envelope: kind, callId, status, timestamps, answer — load-bearing for
16
+ * joining tool slots and lifecycle arbitration. Strictly validated.
17
+ * - body: composed ONLY from the closed primitives (text, options,
18
+ * fields, diff). Structurally validated; unknown body keys quarantine.
19
+ * - details: opaque extension-owned JSON. Never interpreted.
20
+ */
21
+ export {};
@@ -0,0 +1,100 @@
1
+ /**
2
+ * §8.2 job wire types — THE jobs channel contract.
3
+ *
4
+ * Part of the shared leaf package (`@sigrun/shared`): the server owns
5
+ * the runtime half (src/jobs/* — validation, schedule math) and both
6
+ * clients (webui, tui) import these types directly. Like every wire
7
+ * module, imports are TYPE-ONLY from sibling shared modules (the
8
+ * views-purity guard enforces this), so the module erases at compile
9
+ * time and contributes no runtime.
10
+ *
11
+ * The shapes follow spec §8.2's schedule/action grammar verbatim:
12
+ * a schedule is exactly one of a 5-field `cron` (with an optional IANA
13
+ * `timezone`, defaulting to the server timezone), an `every` interval
14
+ * (`^\\d+(s|m|h|d)$`, minimum 60 s), or a one-shot ISO 8601 `at`; an
15
+ * action is a `prompt` (optionally pinning the model/thinking of a NEW
16
+ * conversation, or targeting a fixed conversation id) or a `task`
17
+ * (a registered extension task name plus optional JSON input).
18
+ */
19
+ import type { Sharing } from "./access.js";
20
+ import type { JsonValue } from "./json.js";
21
+ export type { JsonValue };
22
+ /** When a job fires: a cron (5-field, TZ-aware), an interval, or one instant. */
23
+ export type JobSchedule = {
24
+ cron: string;
25
+ timezone?: string;
26
+ } | {
27
+ every: string;
28
+ } | {
29
+ at: string;
30
+ };
31
+ /** What a job does when it fires (spec §8.2). */
32
+ export type JobAction = {
33
+ kind: "prompt";
34
+ prompt: string;
35
+ model?: string;
36
+ thinking?: string;
37
+ conversation: "new" | number;
38
+ runSharing?: Sharing;
39
+ } | {
40
+ kind: "task";
41
+ task: string;
42
+ input?: JsonValue;
43
+ };
44
+ /** A job's declared shape — what every source (api/file/extension) supplies. */
45
+ export type JobSpec = {
46
+ name: string;
47
+ schedule: JobSchedule;
48
+ action: JobAction;
49
+ overlap?: "skip" | "queue";
50
+ catchUp?: "once" | "none";
51
+ enabled?: boolean;
52
+ owner?: string;
53
+ sharing?: Sharing;
54
+ };
55
+ /** One job as `GET /v1/jobs` serves it (the list row and the detail). */
56
+ export type JobWire = {
57
+ id: string;
58
+ name: string;
59
+ source: "api" | "file" | "extension";
60
+ owner?: string;
61
+ sharing?: Sharing;
62
+ schedule: JobSchedule;
63
+ action: JobAction;
64
+ enabled: boolean;
65
+ state: "waiting" | "firing" | "disabled" | "blocked";
66
+ reason?: string;
67
+ version: number;
68
+ nextRun?: string;
69
+ lastRun?: JobRunWire;
70
+ lastError?: string;
71
+ access: "read" | "write";
72
+ };
73
+ /** One fire of a job, as the job's `lastRun` copy serves it. */
74
+ export type JobRunWire = {
75
+ jobId: string;
76
+ version: number;
77
+ fireAt: string;
78
+ manual: boolean;
79
+ conversationId?: number;
80
+ taskId?: number;
81
+ outcome: "running" | "succeeded" | "failed" | "skipped";
82
+ startedAt: string;
83
+ settledAt?: string;
84
+ error?: string;
85
+ };
86
+ /**
87
+ * One immutable revision of a job (`sigrun.job.revision`, keyed
88
+ * `<jobId>:<version>`): the spec exactly as it stood at that version.
89
+ */
90
+ export type JobRevisionWire = {
91
+ jobId: string;
92
+ version: number;
93
+ source: "api" | "file" | "extension";
94
+ spec: JobSpec;
95
+ owner?: string;
96
+ sharing?: Sharing;
97
+ enabled: boolean;
98
+ changedAt: string;
99
+ changedBy: string;
100
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * §8.2 job wire types — THE jobs channel contract.
3
+ *
4
+ * Part of the shared leaf package (`@sigrun/shared`): the server owns
5
+ * the runtime half (src/jobs/* — validation, schedule math) and both
6
+ * clients (webui, tui) import these types directly. Like every wire
7
+ * module, imports are TYPE-ONLY from sibling shared modules (the
8
+ * views-purity guard enforces this), so the module erases at compile
9
+ * time and contributes no runtime.
10
+ *
11
+ * The shapes follow spec §8.2's schedule/action grammar verbatim:
12
+ * a schedule is exactly one of a 5-field `cron` (with an optional IANA
13
+ * `timezone`, defaulting to the server timezone), an `every` interval
14
+ * (`^\\d+(s|m|h|d)$`, minimum 60 s), or a one-shot ISO 8601 `at`; an
15
+ * action is a `prompt` (optionally pinning the model/thinking of a NEW
16
+ * conversation, or targeting a fixed conversation id) or a `task`
17
+ * (a registered extension task name plus optional JSON input).
18
+ */
19
+ export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The JSON value mirrors (the shared leaf's local declarations of
3
+ * chord/pi-durable's `JsonValue`/`JsonObject`).
4
+ *
5
+ * Declared LOCALLY — never imported — so every shared module stays
6
+ * import-free at runtime (the shared-package invariant: the webui
7
+ * compiles DOM-only against these files, and a dependency import would
8
+ * drag a module only one client can resolve). The shapes mirror
9
+ * `@earendil-works/chord`'s `JsonValue` exactly (structural identity —
10
+ * the server-side doc definitions accept records built from these types
11
+ * wherever pi-durable's own `JsonObject` is required).
12
+ */
13
+ /** A strict-JSON value: primitives, arrays, or a JSON object. */
14
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
15
+ [key: string]: JsonValue;
16
+ };
17
+ /** JSON object used as the root of every durable document. */
18
+ export type JsonObject = {
19
+ [key: string]: JsonValue;
20
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The JSON value mirrors (the shared leaf's local declarations of
3
+ * chord/pi-durable's `JsonValue`/`JsonObject`).
4
+ *
5
+ * Declared LOCALLY — never imported — so every shared module stays
6
+ * import-free at runtime (the shared-package invariant: the webui
7
+ * compiles DOM-only against these files, and a dependency import would
8
+ * drag a module only one client can resolve). The shapes mirror
9
+ * `@earendil-works/chord`'s `JsonValue` exactly (structural identity —
10
+ * the server-side doc definitions accept records built from these types
11
+ * wherever pi-durable's own `JsonObject` is required).
12
+ */
13
+ export {};