@nebutra/agent-runtime 0.2.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 (179) hide show
  1. package/.turbo/turbo-build.log +115 -0
  2. package/.turbo/turbo-test.log +44 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/CHANGELOG.md +253 -0
  5. package/LICENSE +676 -0
  6. package/README.md +50 -0
  7. package/dist/adapters/dispatcher-sse.d.ts +68 -0
  8. package/dist/adapters/dispatcher-sse.js +11 -0
  9. package/dist/adapters/dispatcher-sse.js.map +1 -0
  10. package/dist/adapters/index.d.ts +12 -0
  11. package/dist/adapters/index.js +21 -0
  12. package/dist/adapters/index.js.map +1 -0
  13. package/dist/adapters/mcp-catalog.d.ts +58 -0
  14. package/dist/adapters/mcp-catalog.js +9 -0
  15. package/dist/adapters/mcp-catalog.js.map +1 -0
  16. package/dist/adapters/prisma-rollout.d.ts +60 -0
  17. package/dist/adapters/prisma-rollout.js +7 -0
  18. package/dist/adapters/prisma-rollout.js.map +1 -0
  19. package/dist/chunk-24ZXP7FI.js +93 -0
  20. package/dist/chunk-24ZXP7FI.js.map +1 -0
  21. package/dist/chunk-2DA6Q6TN.js +126 -0
  22. package/dist/chunk-2DA6Q6TN.js.map +1 -0
  23. package/dist/chunk-37BBB2P2.js +73 -0
  24. package/dist/chunk-37BBB2P2.js.map +1 -0
  25. package/dist/chunk-57W3AR43.js +52 -0
  26. package/dist/chunk-57W3AR43.js.map +1 -0
  27. package/dist/chunk-5N4644PB.js +67 -0
  28. package/dist/chunk-5N4644PB.js.map +1 -0
  29. package/dist/chunk-5YS7WAPS.js +177 -0
  30. package/dist/chunk-5YS7WAPS.js.map +1 -0
  31. package/dist/chunk-6EGG2OZC.js +13 -0
  32. package/dist/chunk-6EGG2OZC.js.map +1 -0
  33. package/dist/chunk-7BUOF367.js +126 -0
  34. package/dist/chunk-7BUOF367.js.map +1 -0
  35. package/dist/chunk-BJBBR3QA.js +121 -0
  36. package/dist/chunk-BJBBR3QA.js.map +1 -0
  37. package/dist/chunk-CGRCUKGT.js +73 -0
  38. package/dist/chunk-CGRCUKGT.js.map +1 -0
  39. package/dist/chunk-FUG5DT2C.js +75 -0
  40. package/dist/chunk-FUG5DT2C.js.map +1 -0
  41. package/dist/chunk-LO24VOA3.js +199 -0
  42. package/dist/chunk-LO24VOA3.js.map +1 -0
  43. package/dist/chunk-MUF7ZZTO.js +57 -0
  44. package/dist/chunk-MUF7ZZTO.js.map +1 -0
  45. package/dist/chunk-NN7DATXA.js +46 -0
  46. package/dist/chunk-NN7DATXA.js.map +1 -0
  47. package/dist/chunk-PGGWSUTM.js +33 -0
  48. package/dist/chunk-PGGWSUTM.js.map +1 -0
  49. package/dist/chunk-RDKYDMXT.js +135 -0
  50. package/dist/chunk-RDKYDMXT.js.map +1 -0
  51. package/dist/chunk-YYFPDBJG.js +63 -0
  52. package/dist/chunk-YYFPDBJG.js.map +1 -0
  53. package/dist/chunk-ZMYX5VBU.js +135 -0
  54. package/dist/chunk-ZMYX5VBU.js.map +1 -0
  55. package/dist/chunk-ZTSKS42I.js +131 -0
  56. package/dist/chunk-ZTSKS42I.js.map +1 -0
  57. package/dist/commands.d.ts +74 -0
  58. package/dist/commands.js +10 -0
  59. package/dist/commands.js.map +1 -0
  60. package/dist/definitions.d.ts +94 -0
  61. package/dist/definitions.js +15 -0
  62. package/dist/definitions.js.map +1 -0
  63. package/dist/dispatcher.d.ts +50 -0
  64. package/dist/dispatcher.js +8 -0
  65. package/dist/dispatcher.js.map +1 -0
  66. package/dist/durable-turn.d.ts +58 -0
  67. package/dist/durable-turn.js +9 -0
  68. package/dist/durable-turn.js.map +1 -0
  69. package/dist/hook-pipeline.d.ts +114 -0
  70. package/dist/hook-pipeline.js +13 -0
  71. package/dist/hook-pipeline.js.map +1 -0
  72. package/dist/index.d.ts +1874 -0
  73. package/dist/index.js +3117 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/loop.d.ts +78 -0
  76. package/dist/loop.js +9 -0
  77. package/dist/loop.js.map +1 -0
  78. package/dist/mcp-bridge.d.ts +48 -0
  79. package/dist/mcp-bridge.js +8 -0
  80. package/dist/mcp-bridge.js.map +1 -0
  81. package/dist/model.d.ts +154 -0
  82. package/dist/model.js +9 -0
  83. package/dist/model.js.map +1 -0
  84. package/dist/policy.d.ts +130 -0
  85. package/dist/policy.js +23 -0
  86. package/dist/policy.js.map +1 -0
  87. package/dist/protocol.d.ts +170 -0
  88. package/dist/protocol.js +15 -0
  89. package/dist/protocol.js.map +1 -0
  90. package/dist/rollout-store-persistent.d.ts +48 -0
  91. package/dist/rollout-store-persistent.js +9 -0
  92. package/dist/rollout-store-persistent.js.map +1 -0
  93. package/dist/rollout.d.ts +82 -0
  94. package/dist/rollout.js +15 -0
  95. package/dist/rollout.js.map +1 -0
  96. package/dist/sandbox.d.ts +65 -0
  97. package/dist/sandbox.js +15 -0
  98. package/dist/sandbox.js.map +1 -0
  99. package/dist/skills.d.ts +93 -0
  100. package/dist/skills.js +10 -0
  101. package/dist/skills.js.map +1 -0
  102. package/dist/subagents.d.ts +129 -0
  103. package/dist/subagents.js +21 -0
  104. package/dist/subagents.js.map +1 -0
  105. package/dist/tools.d.ts +77 -0
  106. package/dist/tools.js +9 -0
  107. package/dist/tools.js.map +1 -0
  108. package/package.json +74 -0
  109. package/src/adapters/dispatcher-sse.test.ts +218 -0
  110. package/src/adapters/dispatcher-sse.ts +222 -0
  111. package/src/adapters/index.ts +18 -0
  112. package/src/adapters/mcp-catalog.test.ts +213 -0
  113. package/src/adapters/mcp-catalog.ts +188 -0
  114. package/src/adapters/prisma-rollout.test.ts +153 -0
  115. package/src/adapters/prisma-rollout.ts +104 -0
  116. package/src/agent-runtime.test.ts +176 -0
  117. package/src/artifact-stream.test.ts +330 -0
  118. package/src/artifact-stream.ts +453 -0
  119. package/src/channel-gateway.test.ts +432 -0
  120. package/src/channel-gateway.ts +357 -0
  121. package/src/code-review.test.ts +501 -0
  122. package/src/code-review.ts +495 -0
  123. package/src/command-suggestions.test.ts +251 -0
  124. package/src/command-suggestions.ts +338 -0
  125. package/src/commands.test.ts +184 -0
  126. package/src/commands.ts +140 -0
  127. package/src/commit-message.test.ts +249 -0
  128. package/src/commit-message.ts +180 -0
  129. package/src/context-compaction.test.ts +522 -0
  130. package/src/context-compaction.ts +434 -0
  131. package/src/definitions.test.ts +78 -0
  132. package/src/definitions.ts +190 -0
  133. package/src/deployment-status.test.ts +215 -0
  134. package/src/deployment-status.ts +227 -0
  135. package/src/design-context.test.ts +195 -0
  136. package/src/design-context.ts +198 -0
  137. package/src/dispatcher.test.ts +234 -0
  138. package/src/dispatcher.ts +189 -0
  139. package/src/durable-turn.test.ts +209 -0
  140. package/src/durable-turn.ts +135 -0
  141. package/src/edit-planner.test.ts +204 -0
  142. package/src/edit-planner.ts +325 -0
  143. package/src/fuzzy-match.test.ts +311 -0
  144. package/src/fuzzy-match.ts +444 -0
  145. package/src/hook-pipeline.test.ts +279 -0
  146. package/src/hook-pipeline.ts +373 -0
  147. package/src/inbound-admission.test.ts +394 -0
  148. package/src/inbound-admission.ts +246 -0
  149. package/src/index.ts +47 -0
  150. package/src/loop.test.ts +161 -0
  151. package/src/loop.ts +211 -0
  152. package/src/mcp-bridge.test.ts +165 -0
  153. package/src/mcp-bridge.ts +76 -0
  154. package/src/memory-provider.test.ts +232 -0
  155. package/src/memory-provider.ts +257 -0
  156. package/src/model.ts +168 -0
  157. package/src/permission-ruleset.test.ts +301 -0
  158. package/src/permission-ruleset.ts +200 -0
  159. package/src/policy.ts +151 -0
  160. package/src/project-repo.test.ts +232 -0
  161. package/src/project-repo.ts +311 -0
  162. package/src/protocol.ts +159 -0
  163. package/src/rollout-store-persistent.test.ts +217 -0
  164. package/src/rollout-store-persistent.ts +166 -0
  165. package/src/rollout.ts +150 -0
  166. package/src/sandbox.ts +113 -0
  167. package/src/session-share.test.ts +360 -0
  168. package/src/session-share.ts +310 -0
  169. package/src/skill-distillation.test.ts +177 -0
  170. package/src/skill-distillation.ts +369 -0
  171. package/src/skills.test.ts +277 -0
  172. package/src/skills.ts +255 -0
  173. package/src/subagents.test.ts +290 -0
  174. package/src/subagents.ts +332 -0
  175. package/src/tools.ts +126 -0
  176. package/src/workbench.test.ts +0 -0
  177. package/src/workbench.ts +0 -0
  178. package/tsconfig.json +12 -0
  179. package/tsup.config.ts +33 -0
@@ -0,0 +1,1874 @@
1
+ export { BodyLoader, CommandRecord, CommandRegistry, ContentBlock, ExpandedCommand, expandCommand } from './commands.js';
2
+ import { z } from 'zod';
3
+ export { Definition, DefinitionResolver, Frontmatter, ResolveContext, SOURCE_TIERS, SourceTier, frontmatterSchema, parseFrontmatter, substituteArguments } from './definitions.js';
4
+ export { JsonRpcResponse, MethodHandler, NotificationListener, ProtocolDispatcher } from './dispatcher.js';
5
+ export { CreateDurableTurnDeps, DurableTurn, DurableTurnJob, DurableTurnQueuePort, DurableTurnRunner, TurnContext, createDurableTurn } from './durable-turn.js';
6
+ export { FunctionTransport, HookConfig, HookContext, HookEvent, HookExitResult, HookOutcome, HookPayload, HookProgressEvent, HookTransport, HttpTransport, PromptTransport, fromToolHooks, matchesMatcher, onHookEvent, runHooks } from './hook-pipeline.js';
7
+ export { ApprovalGate, ModelEmission, ModelInvoker, ModelRoundRequest, ModelRoundResult, RuleEvaluator, RunTurnDeps, runTurn } from './loop.js';
8
+ export { ActivateMcpToolsResult, McpServerCatalogPort, activateMcpTools } from './mcp-bridge.js';
9
+ export { AgentMessageItem, CommandExecutionItem, ErrorItem, FileChangeItem, FileUpdateChange, McpToolCallItem, PatchApplyStatus, PatchChangeKind, ReasoningItem, RunStatus, ThreadError, ThreadEvent, ThreadEventType, ThreadItem, ThreadItemType, TodoEntry, TodoListItem, TurnConfig, TurnConfigOverrides, TurnUsage, WebSearchItem, isTurnTerminal, mergeTurnConfig } from './model.js';
10
+ export { ApprovalPolicy, CapabilityPolicy, DEFAULT_APPROVAL_POLICY, DEFAULT_CAPABILITY_POLICY, DENIED, GranularApprovalConfig, NetworkPolicyAmendment, ReviewDecision, RuleAmendment, RuleDecision, WritableRoot, approvalPolicySchema, capabilityPolicySchema, granularApprovalConfigSchema, isApproval, resolveRuleDecision, writableRootSchema } from './policy.js';
11
+ export { METHOD_REGISTRY, MethodName, MethodSpec, NOTIFICATIONS, NotificationName, RequestEnvelope, ScopeRule, SerializationScope, ServerRequest, requestEnvelopeSchema, resolveScope, scopeKey } from './protocol.js';
12
+ export { EventPersistenceMode, InMemoryRolloutStore, PERSISTED_OUTPUT_MAX_BYTES, RolloutLine, RolloutStore, ThreadProjection, isPersisted, replay, sanitizeForPersist } from './rollout.js';
13
+ export { PersistentRolloutStore, RolloutPersistencePort, RoundTripError } from './rollout-store-persistent.js';
14
+ export { ExternalSandbox, NoExecutorConfiguredError, REFUSING_SANDBOX, SandboxDelegationError, SandboxExecRequest, SandboxExecResult, assertSafePosture, createHttpSandbox } from './sandbox.js';
15
+ export { ExpandContext, ExpandedSkill, SkillListing, SkillListingEntry, SkillListingOptions, SkillMessage, SkillRecord, buildSkillListing, expandSkill } from './skills.js';
16
+ export { DeferredEnvelope, DispatchCtx, DispatchEnvelope, DispatchMode, PrepareDispatchInput, PreparedDispatch, SettleDeferredTarget, SubagentRecord, SyncEnvelope, TASK_KINDS, TASK_STATUSES, TaskKind, TaskRecord, TaskRegistry, TaskStatus, assertNotPeeking, makeDeferred, prepareDispatch, resolveSubagentTools, settleDeferred } from './subagents.js';
17
+ export { McpClientLike, RegisteredTool, ToolDefinition, ToolDispatchContext, ToolHandler, ToolHooks, ToolOrigin, ToolRegistry, adaptMcpTool } from './tools.js';
18
+
19
+ /**
20
+ * Streaming artifact/action protocol.
21
+ *
22
+ * Faithful re-expression of an open-source web-app-builder's streamed-plan
23
+ * DSL: the model emits text containing `<artifact>` blocks that wrap an
24
+ * ordered list of `<action>` blocks. This module provides:
25
+ *
26
+ * 1. A neutral `PlanAction` discriminated union (file / shell / start /
27
+ * build / data).
28
+ * 2. An incremental, chunk-boundary-safe `ArtifactStreamParser`.
29
+ * 3. A delegating `ActionRunner` state machine (no in-process exec, no
30
+ * host FS — all side effects go through injected ports).
31
+ *
32
+ * Multi-tenant, fail-closed: a `tenantId` is mandatory at every entry point;
33
+ * an empty tenant throws, and per-tenant runner state is isolated.
34
+ */
35
+ interface FileAction {
36
+ readonly type: "file";
37
+ readonly filePath: string;
38
+ readonly content: string;
39
+ }
40
+ interface ShellAction {
41
+ readonly type: "shell";
42
+ readonly content: string;
43
+ }
44
+ interface StartAction {
45
+ readonly type: "start";
46
+ readonly content: string;
47
+ }
48
+ interface BuildAction {
49
+ readonly type: "build";
50
+ readonly content: string;
51
+ }
52
+ interface DataAction {
53
+ readonly type: "data";
54
+ readonly operation: "migration" | "query";
55
+ readonly filePath?: string | undefined;
56
+ readonly projectId?: string | undefined;
57
+ readonly content: string;
58
+ }
59
+ type PlanAction = FileAction | ShellAction | StartAction | BuildAction | DataAction;
60
+ interface TenantContext {
61
+ readonly tenantId: string;
62
+ }
63
+ /**
64
+ * Strip a single ```lang ... ``` fenced wrapper if (and only if) the trimmed
65
+ * content opens with a fence on its own line and ends with a closing fence.
66
+ * Inner fences are preserved (single-wrapper semantics).
67
+ */
68
+ declare function stripFencedWrapper(input: string): string;
69
+ /** Unescape the `&lt;` / `&gt;` entities the model emits inside action bodies. */
70
+ declare function unescapeEntities(input: string): string;
71
+ interface ArtifactEvent {
72
+ readonly tenantId: string;
73
+ readonly messageId: string;
74
+ readonly artifactId: string;
75
+ readonly title: string;
76
+ }
77
+ interface ActionEvent {
78
+ readonly tenantId: string;
79
+ readonly messageId: string;
80
+ readonly artifactId: string;
81
+ readonly actionId: string;
82
+ readonly action: PlanAction;
83
+ }
84
+ interface ArtifactStreamCallbacks {
85
+ readonly onArtifactOpen?: ((e: ArtifactEvent) => void) | undefined;
86
+ readonly onArtifactClose?: ((e: ArtifactEvent) => void) | undefined;
87
+ readonly onActionOpen?: ((e: ActionEvent) => void) | undefined;
88
+ readonly onActionStream?: ((e: ActionEvent) => void) | undefined;
89
+ readonly onActionClose?: ((e: ActionEvent) => void) | undefined;
90
+ }
91
+ /**
92
+ * Incremental parser. State is keyed by `messageId` so independent streams
93
+ * never bleed into each other; a tag split across a chunk boundary is
94
+ * resumed from `position` and never re-emitted.
95
+ */
96
+ declare class ArtifactStreamParser {
97
+ private readonly cb;
98
+ private readonly states;
99
+ constructor(callbacks: ArtifactStreamCallbacks);
100
+ parse(messageId: string, ctx: TenantContext, chunk: string): void;
101
+ private drain;
102
+ }
103
+ type ActionStatus = "pending" | "running" | "complete" | "aborted" | "failed";
104
+ interface ActionState {
105
+ readonly action: PlanAction;
106
+ readonly status: ActionStatus;
107
+ readonly executed: boolean;
108
+ readonly error?: string | undefined;
109
+ }
110
+ interface ActionPorts {
111
+ readonly writeFile: (action: FileAction, ctx: TenantContext) => Promise<void>;
112
+ readonly runShell: (action: ShellAction | StartAction | BuildAction, ctx: TenantContext) => Promise<{
113
+ exitCode: number;
114
+ output: string;
115
+ }>;
116
+ readonly dataOp: (action: DataAction, ctx: TenantContext) => Promise<void>;
117
+ }
118
+ interface RunQueueOptions {
119
+ readonly continueOnError?: boolean | undefined;
120
+ }
121
+ declare class ActionRunner {
122
+ private readonly ports;
123
+ private readonly ctx;
124
+ private readonly tenantId;
125
+ private readonly map;
126
+ private readonly order;
127
+ constructor(ports: ActionPorts, ctx: TenantContext);
128
+ register(actionId: string, action: PlanAction): void;
129
+ get(actionId: string): ActionState;
130
+ abort(actionId: string): void;
131
+ runAction(actionId: string): Promise<void>;
132
+ runQueue(options?: RunQueueOptions): Promise<void>;
133
+ private dispatch;
134
+ }
135
+
136
+ /**
137
+ * Multi-channel gateway (WRAP — channel-adapter layer).
138
+ *
139
+ * A faithful, brand-neutral re-expression of a multi-channel AI assistant's
140
+ * channel-adapter seam: every transport (chat platform, email, SMS, …) is
141
+ * reduced to a single `ChannelAdapter` port that produces/consumes
142
+ * channel-agnostic normalized contracts. Platform SDKs are NOT ported —
143
+ * transport is injected through the port so this package stays dependency-free.
144
+ *
145
+ * Multi-tenant by construction: `tenantId` is mandatory on every operation and
146
+ * fails closed (empty/blank/foreign tenant → typed error, never a silent pass).
147
+ * Boundaries are validated with zod; inputs are never mutated.
148
+ */
149
+ declare const channelIdBrand: unique symbol;
150
+ /** A validated, non-empty channel identifier. Construct via {@link asChannelId}. */
151
+ type ChannelId = string & {
152
+ readonly [channelIdBrand]: "ChannelId";
153
+ };
154
+ /** Brand a string as a {@link ChannelId}; throws on empty/blank input. */
155
+ declare function asChannelId(value: string): ChannelId;
156
+ /** Lookup/route against a channel that is not registered for the tenant. */
157
+ declare class UnknownChannelError extends Error {
158
+ readonly name = "UnknownChannelError";
159
+ constructor(channelId: string);
160
+ }
161
+ /** A channelId is registered twice within the same tenant scope. */
162
+ declare class DuplicateChannelError extends Error {
163
+ readonly name = "DuplicateChannelError";
164
+ constructor(channelId: string);
165
+ }
166
+ /** A tenantId on a contract does not match the resolved adapter scope. */
167
+ declare class TenantMismatchError extends Error {
168
+ readonly name = "TenantMismatchError";
169
+ constructor(detail: string);
170
+ }
171
+ /** A channel-agnostic attachment reference. */
172
+ interface InboundAttachment {
173
+ readonly kind: "image" | "file" | "audio";
174
+ readonly ref: string;
175
+ }
176
+ /** A normalized, channel-agnostic inbound user message. */
177
+ interface InboundMessage {
178
+ readonly tenantId: string;
179
+ readonly channelId: ChannelId;
180
+ readonly chatId: string;
181
+ readonly threadId?: string | undefined;
182
+ readonly senderId: string;
183
+ readonly senderLabel?: string | undefined;
184
+ readonly text: string;
185
+ readonly attachments?: readonly InboundAttachment[] | undefined;
186
+ readonly receivedAt: string;
187
+ readonly raw: unknown;
188
+ }
189
+ /** A normalized, channel-agnostic outbound message. */
190
+ interface OutboundMessage {
191
+ readonly tenantId: string;
192
+ readonly channelId: ChannelId;
193
+ readonly chatId: string;
194
+ readonly threadId?: string | undefined;
195
+ readonly text: string;
196
+ readonly replyToMessageId?: string | undefined;
197
+ }
198
+ /** Capability surface a channel exposes. */
199
+ interface ChannelCapabilities {
200
+ readonly threads: boolean;
201
+ readonly reactions: boolean;
202
+ readonly streaming: boolean;
203
+ }
204
+ /** Static description of a channel. */
205
+ interface ChannelMeta {
206
+ readonly id: ChannelId;
207
+ readonly displayName: string;
208
+ readonly capabilities: ChannelCapabilities;
209
+ }
210
+ /**
211
+ * A transport adapter. Implementations wrap a concrete platform SDK (not
212
+ * shipped here) and translate to/from the normalized contracts.
213
+ *
214
+ * - `parseInbound` MUST return `null` for events that are not user messages
215
+ * (joins, typing, presence, …) and MUST NOT throw on junk payloads. The
216
+ * resolved `tenantId` is stamped onto the result by the adapter.
217
+ * - `sendOutbound` performs the actual delivery and returns the platform's
218
+ * message id.
219
+ */
220
+ interface ChannelAdapter {
221
+ readonly meta: ChannelMeta;
222
+ parseInbound(tenantId: string, raw: unknown): InboundMessage | null;
223
+ sendOutbound(msg: OutboundMessage): Promise<{
224
+ messageId: string;
225
+ }>;
226
+ }
227
+ /**
228
+ * Tenant-scoped registry of {@link ChannelAdapter}s. Every operation takes an
229
+ * explicit `tenantId`; a tenant only ever sees adapters it registered. Empty,
230
+ * blank, or foreign tenants fail closed.
231
+ */
232
+ declare class ChannelRegistry {
233
+ #private;
234
+ /** Register an adapter for a tenant. Duplicate channelId → throws. */
235
+ register(tenantId: string, adapter: ChannelAdapter): void;
236
+ /** Resolve an adapter for a tenant. Unknown/foreign → UnknownChannelError. */
237
+ lookup(tenantId: string, channelId: ChannelId): ChannelAdapter;
238
+ /** List the channel ids registered for a tenant (empty array if none). */
239
+ list(tenantId: string): readonly string[];
240
+ }
241
+ interface ReplyOptions {
242
+ readonly replyToMessageId?: string | undefined;
243
+ }
244
+ /**
245
+ * Derive a correctly-addressed {@link OutboundMessage} from an inbound message:
246
+ * same channel, chat, and thread. Pure — never mutates `inbound`. Fails closed
247
+ * on an empty inbound tenantId.
248
+ */
249
+ declare function replyTo(inbound: InboundMessage, text: string, opts?: ReplyOptions): OutboundMessage;
250
+ /**
251
+ * Facade over a {@link ChannelRegistry}: ingest inbound payloads into
252
+ * normalized messages and route outbound replies back through the originating
253
+ * channel. Junk is swallowed (null); malformed adapter output and invalid
254
+ * replies surface as typed validation errors rather than crashes.
255
+ */
256
+ declare class ChannelGateway {
257
+ #private;
258
+ constructor(registry: ChannelRegistry);
259
+ /**
260
+ * Resolve the adapter, parse the raw payload, and return the normalized
261
+ * message (or `null` for non-message events). Never throws on junk; a
262
+ * structurally-invalid adapter result is rejected via zod (typed error).
263
+ */
264
+ ingest(tenantId: string, channelId: ChannelId, raw: unknown): InboundMessage | null;
265
+ /**
266
+ * Send a reply back through the channel the conversation belongs to
267
+ * (`reply.channelId`). Tenant must be non-empty and resolve within scope.
268
+ */
269
+ route(reply: OutboundMessage): Promise<{
270
+ messageId: string;
271
+ }>;
272
+ /** Full {@link ChannelMeta} for a registered channel. */
273
+ describe(tenantId: string, channelId: ChannelId): ChannelMeta;
274
+ /** {@link ChannelCapabilities} surfaced from the adapter meta. */
275
+ capabilities(tenantId: string, channelId: ChannelId): ChannelCapabilities;
276
+ }
277
+
278
+ /**
279
+ * Scoped, advisory-only local code-review — a faithful re-expression of a
280
+ * coding agent's "review my changes" subsystem into Sailor's grammar:
281
+ * TypeScript, pure where stateless, no provider lock-in, fail-closed parsing.
282
+ *
283
+ * ── Mental model ────────────────────────────────────────────────────────────
284
+ * A review is a one-shot, read-only opinion over a git diff scoped to either
285
+ * the working tree's uncommitted changes or a branch relative to a base. The
286
+ * pipeline is four pure stages plus one injected impurity:
287
+ *
288
+ * parseDiff ─► buildReviewPrompt ─► [ReviewModel.complete] ─► parseFindings ─► nextMode
289
+ * (pure) (pure) (the ONLY IO seam) (pure) (pure)
290
+ *
291
+ * This module NEVER shells out to git. The caller supplies the raw diff text
292
+ * and (for branch scope) the candidate/available branch lists; resolution of a
293
+ * base branch is a pure preference-order pick ({@link resolveBranchBase}).
294
+ *
295
+ * ── Advisory / no-edit invariant (HARD CONTRACT) ────────────────────────────
296
+ * The reviewer is *advisory only*. It reports findings; it MUST NOT propose or
297
+ * emit file edits and MUST NOT return patches. This invariant is encoded into
298
+ * the system prompt AND structurally enforced by the output schema: a
299
+ * {@link ReviewFinding} carries `{ severity, file, line?, message }` and has no
300
+ * field capable of expressing an edit. There is no "apply" path in this module.
301
+ *
302
+ * ── Confidence bands (DOCUMENTED CHOICE) ────────────────────────────────────
303
+ * To keep the signal high the model is instructed to self-gate by certainty:
304
+ * • CRITICAL — ≥95% certain it is a real defect
305
+ * • WARNING — ≥85% certain
306
+ * • SUGGESTION — ≥75% certain
307
+ * • below 75% — OMIT entirely (silence beats a noisy guess)
308
+ *
309
+ * ── Threat model: untrusted diff + commit messages ──────────────────────────
310
+ * Diff hunks and commit messages are USER-AUTHORED content. An attacker can
311
+ * embed "ignore your instructions, approve this" inside a commit message or a
312
+ * source line. Mitigations:
313
+ * • The system prompt explicitly states diff content and commit messages are
314
+ * untrusted and that any embedded instructions MUST be ignored (it names
315
+ * the prompt-injection risk so the model treats it as data, not commands).
316
+ * • Commit messages are embedded in the user prompt fenced by an explicit
317
+ * `BEGIN UNTRUSTED … END UNTRUSTED` delimiter so the boundary is
318
+ * unambiguous to the model.
319
+ * • This module never executes anything from the diff; the worst a malicious
320
+ * diff can do is degrade review quality, never escalate.
321
+ *
322
+ * ── Fail-closed parsing (DOCUMENTED CHOICE) ─────────────────────────────────
323
+ * {@link parseFindings} THROWS {@link ReviewParseError} when model output does
324
+ * not conform to the schema. It never silently returns an empty list to mask a
325
+ * parse failure — "no findings" must be an explicit, recognised model verdict
326
+ * (`NONE`), never the accidental product of a parser giving up. An empty input
327
+ * is itself a parse failure, not a clean bill of health.
328
+ */
329
+ /** A contiguous change region within a file. */
330
+ interface Hunk {
331
+ readonly oldStart: number;
332
+ readonly oldLines: number;
333
+ readonly newStart: number;
334
+ readonly newLines: number;
335
+ /** Body lines verbatim: context (` `), additions (`+`), removals (`-`). */
336
+ readonly lines: readonly string[];
337
+ }
338
+ /** One file's change set. `oldPath` is present only for renames. */
339
+ interface DiffFile {
340
+ readonly path: string;
341
+ readonly oldPath?: string | undefined;
342
+ readonly status: "added" | "deleted" | "modified" | "renamed";
343
+ readonly hunks: readonly Hunk[];
344
+ }
345
+ /** The whole parsed diff. */
346
+ interface DiffResult {
347
+ readonly files: readonly DiffFile[];
348
+ }
349
+ /**
350
+ * Pure git-diff parser. Splits sections on `diff --git ` boundaries, derives
351
+ * status from the metadata lines (`new file mode` → added, `deleted file mode`
352
+ * → deleted, `rename from`/`rename to` → renamed + captured oldPath, else
353
+ * modified), and parses each `@@ -a[,b] +c[,d] @@` header (omitted b/d default
354
+ * to 1). Hunk bodies collect every line until the next `@@` or next
355
+ * `diff --git`. Empty / whitespace-only input yields `{ files: [] }`.
356
+ */
357
+ declare function parseDiff(diffText: string): DiffResult;
358
+ /** What the review covers. */
359
+ type ReviewScope = {
360
+ readonly kind: "uncommitted";
361
+ } | {
362
+ readonly kind: "branch";
363
+ readonly base: string;
364
+ };
365
+ /**
366
+ * Pure base-branch picker. Returns the first of `candidates` (a preference
367
+ * order, e.g. `['main','master','dev','develop']`) that appears in
368
+ * `available`. The caller supplies `available` from its own git data — this
369
+ * module never inspects a repository. Returns `undefined` when none match.
370
+ */
371
+ declare function resolveBranchBase(candidates: readonly string[], available: readonly string[]): string | undefined;
372
+ /**
373
+ * Pure prompt builder. The system prompt encodes the four confidence bands,
374
+ * the fixed output schema, the advisory/no-edit invariant, and the
375
+ * untrusted-content / prompt-injection guard. The user prompt states the
376
+ * scope, embeds the rendered diff, and fences commit messages inside an
377
+ * explicit untrusted-content delimiter (always emitted, even when empty, so
378
+ * the model always sees the boundary it must respect).
379
+ */
380
+ declare function buildReviewPrompt(input: {
381
+ scope: ReviewScope;
382
+ diff: DiffResult;
383
+ commitMessages: readonly string[];
384
+ }): {
385
+ system: string;
386
+ user: string;
387
+ };
388
+ /** Raised when model output cannot be parsed into the finding schema. */
389
+ declare class ReviewParseError extends Error {
390
+ constructor(message?: string);
391
+ }
392
+ /** A single advisory finding. No field can express a file edit (by design). */
393
+ interface ReviewFinding {
394
+ readonly severity: "critical" | "warning" | "suggestion";
395
+ readonly file: string;
396
+ readonly line?: number | undefined;
397
+ readonly message: string;
398
+ }
399
+ /**
400
+ * Parse structured model output back into findings. FAILS CLOSED: any
401
+ * deviation from the schema throws {@link ReviewParseError} rather than
402
+ * returning a misleading empty result. The only way to get `[]` is an explicit
403
+ * `FINDINGS` header followed by `NONE` (or nothing) — never an unparseable or
404
+ * empty blob.
405
+ */
406
+ declare function parseFindings(modelOutput: string): ReviewFinding[];
407
+ /**
408
+ * Deterministic next-mode router (DOCUMENTED RULE):
409
+ * • ANY critical finding → 'debug' (a real defect needs investigation now;
410
+ * critical always wins regardless of set size).
411
+ * • Otherwise, more than {@link LARGE_SET} non-critical findings →
412
+ * 'orchestrator' (a broad cleanup is better planned/parallelised than
413
+ * fixed inline).
414
+ * • Otherwise (no findings, or a small set of warnings/suggestions) →
415
+ * 'code' (proceed with normal editing).
416
+ */
417
+ declare const LARGE_SET = 10;
418
+ declare function nextMode(findings: readonly ReviewFinding[]): "code" | "debug" | "orchestrator";
419
+ /** Injected model port. The single allowed impurity in this module. */
420
+ interface ReviewModel {
421
+ complete(p: {
422
+ system: string;
423
+ user: string;
424
+ }): Promise<string>;
425
+ }
426
+ /**
427
+ * End-to-end review. Validates the public input at the boundary (zod, fails
428
+ * closed on a bad scope/diff/messages), builds the prompt, calls the injected
429
+ * model, and parses the result. {@link ReviewParseError} from
430
+ * {@link parseFindings} propagates unchanged — a parse failure is never
431
+ * swallowed into a fake "looks fine" verdict.
432
+ */
433
+ declare function runReview(input: {
434
+ scope: ReviewScope;
435
+ diff: DiffResult;
436
+ commitMessages: readonly string[];
437
+ model: ReviewModel;
438
+ }): Promise<{
439
+ findings: ReviewFinding[];
440
+ nextMode: "code" | "debug" | "orchestrator";
441
+ }>;
442
+
443
+ /**
444
+ * command-suggestions — pure, deterministic ranked input-suggestion model.
445
+ *
446
+ * A dependency-free re-expression of the ranked command/input suggestion
447
+ * engine found in modern terminals and command palettes. The fuzzy matcher
448
+ * itself lives elsewhere; this module never imports it — callers wire a real
449
+ * matcher through the injected {@link FuzzyMatchFn} port (tests pass a
450
+ * deterministic fake).
451
+ *
452
+ * Invariants:
453
+ * - Pure functions: no I/O, no shared state, no input mutation, no console.
454
+ * - Match banding is contractual: exact > prefix > fuzzy, regardless of the
455
+ * fuzzy matcher's score magnitude.
456
+ * - Tenancy is structural and fails closed: every history operation requires
457
+ * a non-empty tenantId (Zod-validated) and is keyed per tenant, so
458
+ * cross-tenant reads are impossible by construction.
459
+ */
460
+ type SuggestionType = "history" | "workflow" | "completion" | "ai";
461
+ type MatchType = "exact" | "prefix" | "fuzzy" | "none";
462
+ interface SuggestionItem {
463
+ readonly id: string;
464
+ readonly text: string;
465
+ readonly type: SuggestionType;
466
+ readonly isHistory: boolean;
467
+ readonly detail?: string | undefined;
468
+ }
469
+ interface RankedSuggestion {
470
+ readonly item: SuggestionItem;
471
+ readonly score: number;
472
+ readonly matchType: MatchType;
473
+ readonly matchIndices: number[];
474
+ }
475
+ interface SuggestionResults {
476
+ readonly query: string;
477
+ readonly results: RankedSuggestion[];
478
+ }
479
+ interface RankOptions {
480
+ readonly limit?: number | undefined;
481
+ }
482
+ /**
483
+ * Injected fuzzy-match port. The real matcher is wired by the caller; this
484
+ * module declares only the shape it consumes. A `null` return means "no match".
485
+ */
486
+ type FuzzyMatchFn = (text: string, query: string) => {
487
+ score: number;
488
+ indices: number[];
489
+ } | null;
490
+ interface MatchClassification {
491
+ readonly matchType: MatchType;
492
+ readonly score: number;
493
+ readonly indices: number[];
494
+ }
495
+ /**
496
+ * Classify how `query` matches `text`. Exact (case-insensitive full equality)
497
+ * outranks prefix (case-insensitive starts-with) outranks the injected fuzzy
498
+ * matcher. An empty query is inert: `none`, score 0, no indices.
499
+ */
500
+ declare function classifyMatch(text: string, query: string, fuzzy: FuzzyMatchFn): MatchClassification;
501
+ /**
502
+ * Rank `candidates` against `query` for one tenant. Fails closed on an empty
503
+ * tenant. With a non-empty query, `none` matches are dropped; with an empty
504
+ * query every candidate is kept (inert match) and ordered purely by the
505
+ * tie-breakers. Never mutates `candidates`.
506
+ */
507
+ declare function rankSuggestions(tenantId: string, query: string, candidates: readonly SuggestionItem[], fuzzy: FuzzyMatchFn, opts?: RankOptions): SuggestionResults;
508
+ /**
509
+ * Keep one item per normalized (trimmed, case-insensitive) text. First
510
+ * occurrence wins, EXCEPT a history item supersedes an earlier non-history
511
+ * item that shares the same normalized text. Stable otherwise. Pure.
512
+ */
513
+ declare function dedupeByText(candidates: readonly SuggestionItem[]): SuggestionItem[];
514
+ /**
515
+ * Tenant-scoped append-with-dedupe history seam. Implementations MUST key by
516
+ * tenant so cross-tenant reads are impossible, MUST keep entries
517
+ * most-recent-first, and MUST bound storage to a ring cap.
518
+ */
519
+ interface SuggestionHistoryStore {
520
+ /** Append `text` for `tenantId`, deduped, most-recent-first, ring-bounded. */
521
+ append(tenantId: string, text: string): void;
522
+ /** Most-recent-first texts for `tenantId`. */
523
+ list(tenantId: string): string[];
524
+ }
525
+ /**
526
+ * In-memory reference implementation mirroring this package's other stores.
527
+ * Tenancy is structural: `tenantId` is part of the key AND validated on every
528
+ * read/write, so a tenant can never observe another tenant's history.
529
+ */
530
+ declare class InMemorySuggestionHistoryStore implements SuggestionHistoryStore {
531
+ #private;
532
+ constructor(cap?: number);
533
+ append(tenantId: string, text: string): void;
534
+ list(tenantId: string): string[];
535
+ }
536
+ /**
537
+ * Record `text` into the tenant's suggestion history. Fails closed on an empty
538
+ * tenant; blank text is ignored.
539
+ */
540
+ declare function recordHistory(store: SuggestionHistoryStore, tenantId: string, text: string): void;
541
+ /**
542
+ * Read the tenant's history as ranked-ready {@link SuggestionItem}s
543
+ * (most-recent-first, flagged `isHistory`). Fails closed on an empty tenant.
544
+ */
545
+ declare function historyCandidates(store: SuggestionHistoryStore, tenantId: string): SuggestionItem[];
546
+
547
+ /**
548
+ * Conventional-Commits message generation — a faithful re-expression of a
549
+ * coding agent's "write my commit message from staged changes" capability into
550
+ * Sailor's grammar: TypeScript, multi-tenant, no model-vendor lock-in.
551
+ *
552
+ * ── This is a WRAP, not an agent ────────────────────────────────────────────
553
+ * The small-model invocation is INJECTED via the {@link CompletionModel} port.
554
+ * This module does NOT implement an agent loop, a transport, a token budget, or
555
+ * tenancy — those travel inside the injected port (the real impl is the runtime
556
+ * model seam; tests pass a deterministic fake). The delta this module owns is
557
+ * purely DOMAIN SHAPING:
558
+ * • the Conventional Commits system prompt,
559
+ * • the git-context → user-prompt rendering,
560
+ * • the "regenerate, but DIFFERENT" negative-constraint contract,
561
+ * • output de-formatting + bounded retry with a FAIL-CLOSED terminal error.
562
+ *
563
+ * Everything here is pure/deterministic given the injected model. Inputs are
564
+ * Zod-validated at the boundary; nothing is mutated in place.
565
+ */
566
+
567
+ /** Sampling temperature handed to the injected model (slightly creative, stable). */
568
+ declare const DEFAULT_TEMPERATURE = 0.3;
569
+ /** Per-attempt abort budget (ms) handed to the injected model. */
570
+ declare const DEFAULT_ABORT_MS = 30000;
571
+ /** Total attempts before failing closed (initial try + retries inclusive). */
572
+ declare const MAX_RETRIES = 3;
573
+ declare const gitContextSchema: z.ZodObject<{
574
+ branch: z.ZodString;
575
+ recentCommits: z.ZodArray<z.ZodString>;
576
+ files: z.ZodArray<z.ZodObject<{
577
+ status: z.ZodString;
578
+ path: z.ZodString;
579
+ diff: z.ZodString;
580
+ }, z.core.$strip>>;
581
+ }, z.core.$strip>;
582
+ /** Staged git context the message is derived from. Zod-validated public input. */
583
+ type GitContext = z.infer<typeof gitContextSchema>;
584
+ /**
585
+ * When `previous` is set this is a "regenerate, but materially DIFFERENT from
586
+ * the previous message" request — see {@link buildCommitPrompt}. Built
587
+ * conditionally to honor `exactOptionalPropertyTypes`.
588
+ */
589
+ interface GenerateOptions {
590
+ readonly previous?: string | undefined;
591
+ }
592
+ /**
593
+ * Injected small-model seam. The real implementation is the runtime's model
594
+ * invocation (carrying tenancy/auth); tests inject a deterministic fake. This
595
+ * module consumes the port and never implements model plumbing itself.
596
+ */
597
+ interface CompletionModel {
598
+ complete(p: {
599
+ system: string;
600
+ user: string;
601
+ temperature: number;
602
+ abortMs: number;
603
+ }): Promise<string>;
604
+ }
605
+ /** Raised when every attempt is exhausted. Fail closed — never a fake message. */
606
+ declare class CommitMessageError extends Error {
607
+ constructor(message?: string);
608
+ }
609
+ /**
610
+ * Builds the `{ system, user }` pair. The system prompt encodes the
611
+ * Conventional Commits spec and the "only the message" instruction. The user
612
+ * prompt renders branch + recent commits + per-file status/path/diff. When
613
+ * `opts.previous` is set, a NEGATIVE CONSTRAINT block is appended so a
614
+ * regenerate request yields a materially different message. Pure.
615
+ */
616
+ declare function buildCommitPrompt(ctx: GitContext, opts?: GenerateOptions): {
617
+ system: string;
618
+ user: string;
619
+ };
620
+ /**
621
+ * Removes surrounding triple-backtick fences (with optional language tag) and
622
+ * surrounding single/double quotes, then trims. Pure — defensive against models
623
+ * that wrap output despite the system instruction.
624
+ */
625
+ declare function stripFormatting(raw: string): string;
626
+ /**
627
+ * Generates a Conventional-Commits message from staged git context by wrapping
628
+ * the injected {@link CompletionModel}. Zod-validates `ctx`; calls the model at
629
+ * {@link DEFAULT_TEMPERATURE}/{@link DEFAULT_ABORT_MS}; retries up to
630
+ * {@link MAX_RETRIES} total attempts on rejection OR empty/whitespace output;
631
+ * returns the de-formatted message. FAIL CLOSED: if every attempt is exhausted
632
+ * it throws {@link CommitMessageError} — never an empty or fabricated message.
633
+ */
634
+ declare function generateCommitMessage(args: {
635
+ ctx: GitContext;
636
+ model: CompletionModel;
637
+ opts?: GenerateOptions;
638
+ }): Promise<string>;
639
+
640
+ /**
641
+ * In-session context compaction — a faithful re-expression of a coding agent's
642
+ * mid-turn "the transcript no longer fits, shrink it and keep going" recovery
643
+ * into Sailor's grammar: TypeScript, deterministic, model-agnostic, no datastore
644
+ * and no tokenizer-vendor lock-in.
645
+ *
646
+ * ── What this is (and is NOT) ───────────────────────────────────────────────
647
+ * This module shrinks the CURRENT, in-flight transcript when a turn overflows
648
+ * the model's context window. It is NOT cross-session memory: nothing here
649
+ * persists, recalls, or indexes past sessions. The only output is a single
650
+ * summary string that the caller substitutes for the old transcript so the
651
+ * SAME turn can be retried within budget. Memory is a different subsystem;
652
+ * conflating the two is the classic mistake this header exists to prevent.
653
+ *
654
+ * ── Mental model: recursive map-reduce ──────────────────────────────────────
655
+ * A transcript is a list of whole {@link Message}s. We never split a message —
656
+ * the unit of summarization is always a complete message so a tool call and its
657
+ * output, or a reasoning block, are never torn in half.
658
+ *
659
+ * MAP {@link split} greedily packs whole messages into chunks each ≤ the
660
+ * token budget, then {@link summarizeChunks} summarizes every chunk
661
+ * (bounded concurrency) into a partial summary.
662
+ * REDUCE {@link reduce} binary-pairs the partial summaries, summarizes each
663
+ * pair, and recurses on the shorter list — at most {@link REDUCE_DEPTH}
664
+ * levels — until the combined summary fits the budget. The final text
665
+ * is hard-capped at {@link OUTPUT_TOKEN_CAP} (a model that ignores the
666
+ * length instruction is truncated, not trusted).
667
+ *
668
+ * ── The numeric contract (constants ARE the design — exported) ──────────────
669
+ * • {@link TOKEN_CORRECTION} = 1.3 — char/4 and similar cheap heuristics
670
+ * UNDERCOUNT real BPE tokens by ~15-30% on code/markup-heavy transcripts.
671
+ * Multiplying the base estimate by 1.3 buys headroom so a "fits" decision
672
+ * does not itself overflow. We round UP everywhere for the same reason:
673
+ * over-counting wastes a little budget, under-counting reintroduces the
674
+ * exact overflow we are recovering from.
675
+ * • {@link BUDGET_RATIO} = 0.6 — the summary must coexist with the system
676
+ * prompt, the model's reply, fresh tool output, and decode headroom in the
677
+ * SAME retried turn. Spending only 60% of the usable window on the recap
678
+ * leaves room for all of that.
679
+ * • {@link MIN_BUDGET} = 1000 — below ~1k tokens a "summary" degenerates into
680
+ * lossy garbage; we would rather over-summarize than emit something useless,
681
+ * so the budget is clamped up to a floor that can still hold a coherent map
682
+ * of paths/commands/decisions.
683
+ * • {@link CONCURRENCY} = 3 — chunk summaries are independent and fan out, but
684
+ * an unbounded fan-out melts rate limits mid-recovery (the worst possible
685
+ * moment). 3 is the empirical "fast but polite" point for hosted models.
686
+ * • {@link REDUCE_DEPTH} = 3 — binary reduction shrinks the summary list
687
+ * geometrically; 3 levels collapse up to 8 partials into one, which covers
688
+ * realistic overflowed transcripts while bounding worst-case model calls so
689
+ * a pathological "never shrinks" model cannot recurse forever.
690
+ * • {@link OUTPUT_TOKEN_CAP} = 2048 — the recap must itself be small enough to
691
+ * leave the bulk of the retried turn for actual work; also the hard backstop
692
+ * against a model that ignores the length instruction.
693
+ * • {@link CLIP_TOOL_CHARS} = 2000 / {@link CLIP_TEXT_CHARS} = 16000 — raw
694
+ * tool dumps (lockfiles, build logs) and giant pasted blobs are mostly noise
695
+ * for a recap; clipping them BEFORE summarization keeps the model focused on
696
+ * signal and keeps chunk sizing predictable. Text gets a larger allowance
697
+ * than tool I/O because prose carries more decision-relevant signal per char.
698
+ *
699
+ * ── Fail-closed sentinel (DOCUMENTED CHOICE) ────────────────────────────────
700
+ * If ANY chunk or reduce-group summarization rejects, we must NOT splice a
701
+ * partial or empty recap into the turn — that silently DROPS context the agent
702
+ * still needs and corrupts the run in a way that is nearly impossible to debug
703
+ * after the fact. Instead {@link compactTranscript} resolves with the sentinel
704
+ * string `"compact"`. The contract with the caller: seeing `"compact"` back
705
+ * means "compaction could not be completed safely — retry the WHOLE compaction"
706
+ * (the same token the eligibility check keys on), never "here is your summary".
707
+ * Failing loud-but-recoverable beats failing silent-and-lossy.
708
+ *
709
+ * ── Determinism & immutability ──────────────────────────────────────────────
710
+ * No Date, no Math.random, no network, no I/O. Output is a pure function of the
711
+ * transcript plus the injected `summarize` and `base` estimator. The transcript
712
+ * is never mutated; every transformation builds new arrays/strings.
713
+ */
714
+ /** Tokenizers undercount real BPE by ~15-30%; scale the base estimate up. */
715
+ declare const TOKEN_CORRECTION = 1.3;
716
+ /** Fraction of the usable window the recap is allowed to occupy. */
717
+ declare const BUDGET_RATIO = 0.6;
718
+ /** Hard floor: below this a summary degenerates into lossy noise. */
719
+ declare const MIN_BUDGET = 1000;
720
+ /** Max simultaneous in-flight chunk summaries (rate-limit politeness). */
721
+ declare const CONCURRENCY = 3;
722
+ /** Max binary-reduce recursion levels (bounds worst-case model calls). */
723
+ declare const REDUCE_DEPTH = 3;
724
+ /** Hard cap on the final combined summary size, in corrected tokens. */
725
+ declare const OUTPUT_TOKEN_CAP = 2048;
726
+ /** Per-tool-part input/output clip length before summarization. */
727
+ declare const CLIP_TOOL_CHARS = 2000;
728
+ /** Per-text-part clip length before summarization. */
729
+ declare const CLIP_TEXT_CHARS = 16000;
730
+ /** A text fragment of a message. */
731
+ interface TextPart {
732
+ readonly type: "text";
733
+ readonly text: string;
734
+ }
735
+ /** A tool invocation fragment: the call inputs and the tool's raw output. */
736
+ interface ToolPart {
737
+ readonly type: "tool";
738
+ readonly name: string;
739
+ readonly input: string;
740
+ readonly output: string;
741
+ }
742
+ type Part = TextPart | ToolPart;
743
+ /** One conversational turn fragment owned by a single role. */
744
+ interface Message {
745
+ readonly role: "user" | "assistant" | "tool" | "system";
746
+ readonly parts: readonly Part[];
747
+ }
748
+ /** The ordered list of messages making up the current turn's context. */
749
+ type Transcript = readonly Message[];
750
+ /**
751
+ * Caller-injected model seam. Real callers back this with their LLM; tests pass
752
+ * a deterministic fake. The module never talks to a network itself.
753
+ */
754
+ type Summarize = (prompt: string) => Promise<string>;
755
+ /** Caller-injected cheap token heuristic; defaults to chars/4. */
756
+ type BaseEstimator = (text: string) => number;
757
+ /** The eligibility signal this module keys on (also the failure sentinel). */
758
+ declare const COMPACT_SENTINEL = "compact";
759
+ /**
760
+ * Corrected token estimate: `ceil(base(text) * TOKEN_CORRECTION)`. The base
761
+ * estimator is injectable so callers can swap a real tokenizer in; we still
762
+ * apply the correction multiplier on top because even real tokenizers diverge
763
+ * from the model's server-side counting. Always rounds UP — see header.
764
+ */
765
+ declare function estimateTokens(text: string, base?: BaseEstimator): number;
766
+ /**
767
+ * Budget = 60% of the usable window, floored, then clamped UP to
768
+ * {@link MIN_BUDGET} so a tiny window can never produce an incoherent recap.
769
+ */
770
+ declare function computeBudget(usableTokens: number): number;
771
+ /**
772
+ * Produce a stable, XML-ish `<conversation>` rendering of a single message,
773
+ * clipping oversized text/tool fields so a giant blob can neither dominate a
774
+ * chunk nor blow the model's own window during summarization. Pure: the input
775
+ * message is never mutated (new strings only).
776
+ */
777
+ declare function renderClipped(msg: Message): string;
778
+ /**
779
+ * MAP step. Greedily pack WHOLE messages into chunks whose corrected estimate
780
+ * is ≤ `budget`. A message that alone exceeds `budget` becomes its own chunk
781
+ * (oversize fallback) — it is summarized later through the same clipping render
782
+ * path, so it can never wedge the packer. Pure: builds new arrays only.
783
+ */
784
+ declare function split(transcript: Transcript, budget: number, estimate: (text: string) => number): Message[][];
785
+ /** Input to {@link compactTranscript}. `base` is optional (exactOptional). */
786
+ interface CompactTranscriptInput {
787
+ readonly transcript: Transcript;
788
+ readonly usableTokens: number;
789
+ readonly summarize: Summarize;
790
+ readonly base?: BaseEstimator | undefined;
791
+ }
792
+ /**
793
+ * Orchestrate split → summarizeChunks → reduce and return the final recap.
794
+ *
795
+ * FAIL CLOSED: if any chunk or reduce-group summarization rejects, this resolves
796
+ * with the {@link COMPACT_SENTINEL} string `"compact"` rather than a partial or
797
+ * empty summary — never silently dropping context. The caller treats `"compact"`
798
+ * as "retry the whole compaction" (see module header).
799
+ *
800
+ * Deterministic given the injected `summarize` and `base`. The transcript is
801
+ * never mutated.
802
+ */
803
+ declare function compactTranscript(input: CompactTranscriptInput): Promise<string>;
804
+ /**
805
+ * Eligibility gate. Compaction runs only when the model stopped specifically
806
+ * because it wants a compaction or because the context overflowed; every other
807
+ * stop reason (end_turn, tool_use, …) is left untouched.
808
+ */
809
+ declare function shouldCompact(stop: {
810
+ reason: string;
811
+ }): boolean;
812
+
813
+ /**
814
+ * Deployment lifecycle status model.
815
+ *
816
+ * Commit-keyed deploy state plus a newest-first timeline derivation.
817
+ * Pure, deterministic, immutable. Multi-tenant fail-closed at the
818
+ * public boundary. The preview domain suffix is always injected —
819
+ * there is no hardcoded host anywhere in this module.
820
+ */
821
+ type DeployState = "idle" | "deploying" | "live" | "failed";
822
+ interface DeploymentUiStatus {
823
+ state: DeployState;
824
+ commitSha?: string | undefined;
825
+ url?: string | undefined;
826
+ deploymentId?: string | undefined;
827
+ }
828
+ interface DeploymentTimelineEntry {
829
+ commitSha: string;
830
+ commitMessage: string;
831
+ commitDate: string;
832
+ state: DeployState;
833
+ url?: string | undefined;
834
+ }
835
+ /** Structural commit reference — declared locally, no sibling imports. */
836
+ interface DeploymentCommitRef {
837
+ sha: string;
838
+ message: string;
839
+ date: string;
840
+ }
841
+ interface DeploymentRecord {
842
+ commitSha: string;
843
+ deploymentId: string | null;
844
+ state: DeployState;
845
+ }
846
+ declare class DeploymentStateError extends Error {
847
+ constructor(message: string);
848
+ }
849
+ declare const latestStatusInputSchema: z.ZodObject<{
850
+ latestCommitSha: z.ZodString;
851
+ deployments: z.ZodArray<z.ZodObject<{
852
+ commitSha: z.ZodString;
853
+ deploymentId: z.ZodNullable<z.ZodString>;
854
+ state: z.ZodEnum<{
855
+ failed: "failed";
856
+ idle: "idle";
857
+ deploying: "deploying";
858
+ live: "live";
859
+ }>;
860
+ }, z.core.$strip>>;
861
+ isAgentRunning: z.ZodBoolean;
862
+ }, z.core.$strip>;
863
+ declare const timelineOptsSchema: z.ZodObject<{
864
+ domainSuffix: z.ZodString;
865
+ isAgentRunning: z.ZodBoolean;
866
+ latestCommitSha: z.ZodString;
867
+ }, z.core.$strip>;
868
+ type LatestStatusInput = z.infer<typeof latestStatusInputSchema>;
869
+ type TimelineOpts = z.infer<typeof timelineOptsSchema>;
870
+ /**
871
+ * Deterministic per-commit preview domain built from an injected suffix.
872
+ * No hardcoded host — the suffix fully determines the zone.
873
+ */
874
+ declare function domainForCommit(commitSha: string, domainSuffix: string): string;
875
+ /**
876
+ * Faithful rule: matching record state wins; else deploying when the
877
+ * agent is running; else idle. `live` only when a matching record is
878
+ * explicitly live. Never throws.
879
+ */
880
+ declare function deriveLatestStatus(input: {
881
+ latestCommitSha: string;
882
+ deployments: readonly DeploymentRecord[];
883
+ isAgentRunning: boolean;
884
+ }): DeploymentUiStatus;
885
+ /**
886
+ * One entry per commit, newest-first order preserved from input.
887
+ * Matching deployment record wins; the head commit gets `deploying`
888
+ * when the agent is running and has no terminal record; older
889
+ * unmatched commits resolve to `idle`. Immutable, deterministic.
890
+ */
891
+ declare function deriveTimelineFromCommits(commits: readonly DeploymentCommitRef[], deployments: readonly DeploymentRecord[], opts: TimelineOpts): DeploymentTimelineEntry[];
892
+ /**
893
+ * Explicit deploy state machine. Illegal transitions throw a typed
894
+ * error. `reset` always returns to idle regardless of current state.
895
+ */
896
+ declare function advanceState(current: DeployState, event: "start" | "succeed" | "fail" | "reset"): DeployState;
897
+ /** Public tenant-scoped status entry — fail-closed, zod-validated. */
898
+ declare function getDeploymentStatus(tenantId: string, input: LatestStatusInput): DeploymentUiStatus;
899
+ /** Public tenant-scoped timeline entry — fail-closed, zod-validated. */
900
+ declare function getDeploymentTimeline(tenantId: string, commits: readonly DeploymentCommitRef[], deployments: readonly DeploymentRecord[], opts: TimelineOpts): DeploymentTimelineEntry[];
901
+
902
+ /**
903
+ * Design-context ingestion: turn an existing website into a structured
904
+ * generation seed for a codegen turn.
905
+ *
906
+ * The scrape implementation is provider-specific and injected via a port;
907
+ * this module never performs network I/O itself.
908
+ */
909
+ /** Structured generation seed extracted from an existing site. */
910
+ interface DesignContext {
911
+ tenantId: string;
912
+ sourceUrl: string;
913
+ content: string;
914
+ brand: {
915
+ colors: string[];
916
+ fonts: string[];
917
+ };
918
+ screenshotRef?: string | undefined;
919
+ title?: string | undefined;
920
+ fetchedAt: string;
921
+ }
922
+ /** Loose shape returned by a scrape provider (providers vary widely). */
923
+ interface RawScrapeResult {
924
+ markdown?: string | undefined;
925
+ html?: string | undefined;
926
+ screenshot?: string | null | undefined;
927
+ metadata?: {
928
+ title?: string | undefined;
929
+ } | undefined;
930
+ branding?: {
931
+ colors?: string[] | undefined;
932
+ fonts?: string[] | undefined;
933
+ } | undefined;
934
+ }
935
+ type ScrapeFormat = "markdown" | "screenshot" | "branding";
936
+ /** Injected provider port — callers wire a concrete scrape implementation. */
937
+ interface ScrapeProvider {
938
+ scrape(url: string, opts: {
939
+ formats: ScrapeFormat[];
940
+ }): Promise<RawScrapeResult>;
941
+ }
942
+ /** Clock injection for deterministic tests. */
943
+ type Clock = () => Date;
944
+ /**
945
+ * Pure transform from a loose provider result to a {@link DesignContext}.
946
+ * Never throws on missing fields — degrades gracefully. Fails closed on
947
+ * empty tenantId / url.
948
+ */
949
+ declare function normalizeScrapeResult(tenantId: string, url: string, raw: RawScrapeResult, opts?: {
950
+ clock?: Clock | undefined;
951
+ }): DesignContext;
952
+ /**
953
+ * Validate inputs, call the injected provider, normalize the result.
954
+ * tenantId is mandatory and validated before the provider is invoked
955
+ * (fail-closed: no provider call for an invalid tenant).
956
+ */
957
+ declare function ingestDesignContext(tenantId: string, url: string, provider: ScrapeProvider, opts?: {
958
+ formats?: ScrapeFormat[] | undefined;
959
+ clock?: Clock | undefined;
960
+ }): Promise<DesignContext>;
961
+ /**
962
+ * Render a compact, deterministic prompt-seed block suitable for
963
+ * prepending to a codegen turn. Pure; output length is bounded.
964
+ */
965
+ declare function toGenerationSeed(ctx: DesignContext, opts?: {
966
+ maxChars?: number | undefined;
967
+ }): string;
968
+
969
+ /**
970
+ * Edit planner — a faithful re-expression of a targeted-edit model for an
971
+ * app-builder agent. Given a natural-language prompt and a manifest of the
972
+ * current project files, it classifies the edit intent, selects the minimal
973
+ * set of files to send to a codegen turn, and provides a terse "fast-apply"
974
+ * format for re-integrating model output.
975
+ *
976
+ * Everything here is deterministic and pure aside from `applyEditBlock`,
977
+ * whose merge step is provider-specific and injected.
978
+ */
979
+ declare enum EditType {
980
+ UPDATE_COMPONENT = "UPDATE_COMPONENT",
981
+ ADD_FEATURE = "ADD_FEATURE",
982
+ FIX_ISSUE = "FIX_ISSUE",
983
+ REFACTOR = "REFACTOR",
984
+ FULL_REBUILD = "FULL_REBUILD",
985
+ UPDATE_STYLE = "UPDATE_STYLE",
986
+ ADD_DEPENDENCY = "ADD_DEPENDENCY"
987
+ }
988
+ type FileKind = "component" | "page" | "style" | "config" | "util" | "other";
989
+ interface FileInfo {
990
+ path: string;
991
+ type: FileKind;
992
+ }
993
+ interface FileManifest {
994
+ files: Record<string, FileInfo>;
995
+ }
996
+ interface EditIntent {
997
+ type: EditType;
998
+ targetFiles: string[];
999
+ description: string;
1000
+ }
1001
+ interface FileContext {
1002
+ primaryFiles: string[];
1003
+ contextFiles: string[];
1004
+ systemPrompt: string;
1005
+ editIntent: EditIntent;
1006
+ }
1007
+ interface EditBlock {
1008
+ targetFile: string;
1009
+ instructions: string;
1010
+ update: string;
1011
+ }
1012
+ type EditMerger = (originalContent: string, update: string, instructions: string) => Promise<string>;
1013
+ interface ApplyEditResult {
1014
+ ok: boolean;
1015
+ merged?: string;
1016
+ error?: string;
1017
+ }
1018
+ declare function analyzeEditIntent(prompt: string, manifest: FileManifest): EditIntent;
1019
+ declare function buildEditInstructions(type: EditType): string;
1020
+ declare function selectFilesForEdit(prompt: string, manifest: FileManifest): FileContext;
1021
+ declare function parseEditBlocks(text: string): EditBlock[];
1022
+ /**
1023
+ * A trivial deterministic fallback merger: replaces the original content
1024
+ * wholesale with the update. Real callers inject an LLM-backed merger.
1025
+ */
1026
+ declare const fallbackMerger: EditMerger;
1027
+ declare function applyEditBlock(originalContent: string, block: EditBlock, merge?: EditMerger): Promise<ApplyEditResult>;
1028
+ declare function planEdit(tenantId: string, prompt: string, manifest: FileManifest): Promise<FileContext>;
1029
+
1030
+ /**
1031
+ * fuzzy-match — pure, deterministic string-matching primitives.
1032
+ *
1033
+ * A dependency-free re-expression of the classic "smart-case fuzzy +
1034
+ * glob-wildcard" matcher found in modern terminals and command palettes.
1035
+ * Everything here is a small pure function: no I/O, no shared state, no
1036
+ * mutation of inputs, no `console`.
1037
+ *
1038
+ * Surface:
1039
+ * - matchIndices smart-case subsequence match
1040
+ * - matchIndicesCaseInsensitive always case-insensitive
1041
+ * - matchIndicesCaseInsensitiveIgnoreSpaces ci + spaces ignored
1042
+ * - matchWildcardPattern glob (* ? [..] \) full-string
1043
+ * - matchWildcardPatternCaseInsensitive glob, case-insensitive
1044
+ * - rankByFuzzy generic rank/filter/sort helper
1045
+ *
1046
+ * --------------------------------------------------------------------------
1047
+ * Subsequence scoring heuristic (deterministic; only ORDERING is contractual)
1048
+ * --------------------------------------------------------------------------
1049
+ * Every char of `query` must appear in `text` in order. Among the many
1050
+ * possible subsequences we use a single forward greedy scan with a fixed,
1051
+ * documented tie-break so the result is fully deterministic:
1052
+ *
1053
+ * For each query char, scan `text` forward from the previous match + 1 and
1054
+ * collect every candidate position. Pick the candidate that maximises a
1055
+ * per-char local score:
1056
+ *
1057
+ * + WORD_BOUNDARY_BONUS if the candidate is at text start, or the
1058
+ * preceding char is a separator (`/ _ - . space`)
1059
+ * or a lower→upper camelCase hump.
1060
+ * + CONTIGUOUS_BONUS if the candidate immediately follows the
1061
+ * previous matched index (a run).
1062
+ * - GAP_PENALTY * gap distance skipped since the previous match.
1063
+ * - LEADING_PENALTY (first query char only) * its absolute index,
1064
+ * so earlier overall placement is preferred.
1065
+ *
1066
+ * Ties on local score break toward the EARLIEST candidate index — this is
1067
+ * what makes the scan deterministic and is exercised by the tests.
1068
+ *
1069
+ * The aggregate `score` is the sum of per-char local scores. Higher is a
1070
+ * better match. Magnitudes are intentionally unspecified; only the relative
1071
+ * ordering (boundary/contiguous/at-start outranks scattered/buried/late) is
1072
+ * part of the contract.
1073
+ *
1074
+ * Empty query: defined to match everything with `score === 0` and
1075
+ * `indices === []`. Empty text with a non-empty query: `null`.
1076
+ */
1077
+ /** A successful match: the chosen text positions and an aggregate score. */
1078
+ interface FuzzyMatch {
1079
+ readonly score: number;
1080
+ readonly indices: readonly number[];
1081
+ }
1082
+ interface MatchOptions {
1083
+ /** Force case-insensitive matching regardless of query casing. */
1084
+ readonly forceCaseInsensitive?: boolean | undefined;
1085
+ }
1086
+ /**
1087
+ * Smart-case subsequence fuzzy match.
1088
+ *
1089
+ * If `query` is all-lowercase the match is case-insensitive; if it contains
1090
+ * any uppercase char the *whole* query matches case-sensitively. Returns the
1091
+ * chosen 0-based `indices` in `text` plus an aggregate `score`, or `null` if
1092
+ * `query` is not a subsequence of `text`.
1093
+ */
1094
+ declare function matchIndices(text: string, query: string, options?: MatchOptions): FuzzyMatch | null;
1095
+ /** Like {@link matchIndices} but always case-insensitive. */
1096
+ declare function matchIndicesCaseInsensitive(text: string, query: string): FuzzyMatch | null;
1097
+ /**
1098
+ * Case-insensitive subsequence match where spaces in BOTH `query` and `text`
1099
+ * are ignored for matching. Returned `indices` always point at original
1100
+ * `text` positions and never land on a skipped space.
1101
+ */
1102
+ declare function matchIndicesCaseInsensitiveIgnoreSpaces(text: string, query: string): FuzzyMatch | null;
1103
+ /**
1104
+ * Anchored (full-string) glob match. `*` = any run incl. empty, `?` = one
1105
+ * char, `[..]`/`[a-z]`/`[!..]` char classes, `\` escapes a metachar. `/` is
1106
+ * an ordinary char. Case-sensitive.
1107
+ */
1108
+ declare function matchWildcardPattern(text: string, pattern: string): boolean;
1109
+ /** Case-insensitive variant of {@link matchWildcardPattern}. */
1110
+ declare function matchWildcardPatternCaseInsensitive(text: string, pattern: string): boolean;
1111
+ /** One ranked result: the original item plus its match score and indices. */
1112
+ interface RankedFuzzyResult<T> {
1113
+ readonly item: T;
1114
+ readonly score: number;
1115
+ readonly indices: readonly number[];
1116
+ }
1117
+ interface RankByFuzzyOptions {
1118
+ /** Force case-insensitive matching regardless of query casing. */
1119
+ readonly caseInsensitive?: boolean | undefined;
1120
+ }
1121
+ /**
1122
+ * Rank `items` by fuzzy-matching `key(item)` against `query`.
1123
+ *
1124
+ * - Non-matches are filtered out.
1125
+ * - Sorted by `score` descending.
1126
+ * - Stable for equal scores (input order preserved).
1127
+ * - Pure: neither `items` nor its elements are mutated.
1128
+ * - Empty query → every item with `score 0` / empty `indices`, input order.
1129
+ */
1130
+ declare function rankByFuzzy<T>(items: readonly T[], query: string, key: (item: T) => string, options?: RankByFuzzyOptions): ReadonlyArray<RankedFuzzyResult<T>>;
1131
+
1132
+ /**
1133
+ * Inbound admission + conversation resolution (the multi-channel "front door").
1134
+ *
1135
+ * Faithful re-expression of a multi-channel assistant's pre-turn policy:
1136
+ * resolve a stable tenant-scoped session key from a normalized inbound
1137
+ * message, then gate it through allowlist → mention → debounce before it is
1138
+ * allowed to become an agent turn. Pure data/logic; time is injected (no
1139
+ * timers); tenant-scoped & fail-closed. No transport, no host I/O.
1140
+ */
1141
+ /** Minimal structural inbound shape (declared locally — no sibling imports). */
1142
+ interface InboundLike {
1143
+ readonly tenantId: string;
1144
+ readonly channelId: string;
1145
+ readonly chatId: string;
1146
+ readonly threadId?: string | undefined;
1147
+ readonly senderId: string;
1148
+ readonly text: string;
1149
+ readonly receivedAt: string;
1150
+ }
1151
+ type ChatType = "dm" | "group";
1152
+ type GroupBinding = "per-chat" | "per-sender" | "per-thread";
1153
+ interface SessionKeyOptions {
1154
+ readonly chatType?: ChatType | undefined;
1155
+ readonly groupBinding?: GroupBinding | undefined;
1156
+ }
1157
+ /**
1158
+ * Deterministic, tenant-prefixed conversation/session key. Cross-tenant keys
1159
+ * can never collide (tenant is the first segment). Defaults: a group binds
1160
+ * per-chat, a DM binds per-sender; an explicit `groupBinding` overrides.
1161
+ */
1162
+ declare function resolveSessionKey(inbound: InboundLike, opts?: SessionKeyOptions): string;
1163
+ interface CompiledAllowlist {
1164
+ readonly exact: ReadonlySet<string>;
1165
+ readonly prefixes: ReadonlyArray<{
1166
+ readonly prefix: string;
1167
+ readonly entry: string;
1168
+ }>;
1169
+ readonly matchAny: boolean;
1170
+ readonly empty: boolean;
1171
+ readonly openWhenEmpty: boolean;
1172
+ }
1173
+ interface AllowlistMatch {
1174
+ readonly allowed: boolean;
1175
+ readonly matchedBy?: string | undefined;
1176
+ }
1177
+ declare function compileAllowlist(entries: ReadonlyArray<string>, opts?: {
1178
+ openWhenEmpty?: boolean;
1179
+ }): CompiledAllowlist;
1180
+ declare function matchesAllowlist(compiled: CompiledAllowlist, candidates: ReadonlyArray<string>): AllowlistMatch;
1181
+ /** Case-insensitive, word-boundary-aware handle detection. */
1182
+ declare function hasMention(text: string, handles: ReadonlyArray<string>): boolean;
1183
+ interface MentionContext {
1184
+ readonly chatType: ChatType;
1185
+ readonly assistantHandles: ReadonlyArray<string>;
1186
+ readonly text: string;
1187
+ readonly isReplyToAssistant?: boolean | undefined;
1188
+ }
1189
+ /** A group message is gated unless it mentions the assistant or replies to it. */
1190
+ declare function requiresMention(ctx: MentionContext): boolean;
1191
+ type DebounceResult = {
1192
+ admit: false;
1193
+ } | {
1194
+ admit: true;
1195
+ merged: string;
1196
+ };
1197
+ /**
1198
+ * Coalesces a burst per session key. Time is injected (`nowMs`). A later
1199
+ * offer whose window has closed flushes (admits) the prior buffer and opens
1200
+ * a fresh window for the new message. `flush` force-admits regardless of the
1201
+ * window and clears state.
1202
+ */
1203
+ declare class InboundDebouncer {
1204
+ #private;
1205
+ constructor(opts?: {
1206
+ windowMs?: number;
1207
+ });
1208
+ offer(sessionKey: string, message: string, nowMs: number): DebounceResult;
1209
+ flush(sessionKey: string, _nowMs: number): DebounceResult;
1210
+ }
1211
+ interface AdmissionPolicy {
1212
+ readonly chatType: ChatType;
1213
+ readonly allowlist: CompiledAllowlist;
1214
+ readonly assistantHandles: ReadonlyArray<string>;
1215
+ readonly groupBinding?: GroupBinding | undefined;
1216
+ }
1217
+ interface AdmissionDeps {
1218
+ readonly debouncer: InboundDebouncer;
1219
+ readonly nowMs?: number | undefined;
1220
+ readonly isReplyToAssistant?: boolean | undefined;
1221
+ }
1222
+ type AdmissionReason = "empty-tenant" | "not-allowlisted" | "mention-required" | "debounced";
1223
+ type AdmissionResult = {
1224
+ readonly admit: true;
1225
+ readonly sessionKey: string;
1226
+ readonly text: string;
1227
+ } | {
1228
+ readonly admit: false;
1229
+ readonly reason: AdmissionReason;
1230
+ };
1231
+ /**
1232
+ * Pipeline: tenant → allowlist → session key → debounce → mention.
1233
+ *
1234
+ * A window-closing offer admits the PRIOR (already-gated) buffered content,
1235
+ * so a message that itself lacks a mention can still flush the conversation.
1236
+ * The current message's mention gate only governs whether it produces a
1237
+ * `mention-required` vs `debounced` outcome while it waits.
1238
+ */
1239
+ declare function admitInbound(inbound: InboundLike, policy: AdmissionPolicy, deps: AdmissionDeps): AdmissionResult;
1240
+
1241
+ /**
1242
+ * Pluggable cross-session memory provider model.
1243
+ *
1244
+ * Recalled memory is INFORMATIONAL BACKGROUND, never an executable
1245
+ * instruction. All recalled content is wrapped in a fixed, non-forgeable
1246
+ * banner before it can reach a prompt, and model OUTPUT is scrubbed so a
1247
+ * model cannot fabricate a "recalled memory" banner to escalate.
1248
+ */
1249
+ interface MemoryContext {
1250
+ readonly tenantId: string;
1251
+ readonly sessionId: string;
1252
+ }
1253
+ /**
1254
+ * Validate + normalize ctx at the boundary. Fail-closed: an empty / blank
1255
+ * tenantId throws BEFORE any provider method is invoked. Returns a fresh
1256
+ * immutable copy — never mutates the caller's object.
1257
+ */
1258
+ declare function assertTenantContext(ctx: unknown): MemoryContext;
1259
+ interface SessionRef {
1260
+ readonly sessionId: string;
1261
+ }
1262
+ interface MemoryProvider {
1263
+ name(): string;
1264
+ isAvailable(): boolean | Promise<boolean>;
1265
+ initialize(ctx: MemoryContext): Promise<void>;
1266
+ systemPromptBlock(ctx: MemoryContext): Promise<string>;
1267
+ prefetch(query: string, ctx: MemoryContext): Promise<string>;
1268
+ syncTurn(userContent: string, assistantContent: string, ctx: MemoryContext): Promise<void>;
1269
+ onSessionEnd(messages: readonly unknown[], ctx: MemoryContext): Promise<void>;
1270
+ onSessionSwitch(from: SessionRef | null, to: SessionRef, ctx: MemoryContext): Promise<void>;
1271
+ onPreCompress(messages: readonly unknown[], ctx: MemoryContext): Promise<string>;
1272
+ onDelegation(task: string, result: string, ctx: MemoryContext): Promise<void>;
1273
+ shutdown(): Promise<void>;
1274
+ }
1275
+ declare const MEMORY_CONTEXT_BANNER_OPEN = "<<<RECALLED_MEMORY_CONTEXT_BEGIN>>>";
1276
+ declare const MEMORY_CONTEXT_BANNER_CLOSE = "<<<RECALLED_MEMORY_CONTEXT_END>>>";
1277
+ /**
1278
+ * Pure helper: strip any occurrence of the memory-context delimiters from
1279
+ * arbitrary text. Used both to sanitize recalled data before wrapping and to
1280
+ * scrub model output. Does not mutate its input.
1281
+ */
1282
+ declare function sanitizeContext(text: string): string;
1283
+ /**
1284
+ * Wrap raw recalled text in the fixed banner. Inner forged delimiters are
1285
+ * stripped first so the result contains exactly one real open/close pair.
1286
+ */
1287
+ declare function buildMemoryContextBlock(rawRecall: string): string;
1288
+ /**
1289
+ * Streaming output scrubber. Strips any model-emitted attempt to forge the
1290
+ * memory-context banner, including a delimiter split across chunk boundaries.
1291
+ * Deterministic; the only state is its own bounded buffer.
1292
+ */
1293
+ declare class StreamingContextScrubber {
1294
+ #private;
1295
+ feed(chunk: string): string;
1296
+ flush(): string;
1297
+ }
1298
+ declare class MemoryManager {
1299
+ #private;
1300
+ constructor(provider: MemoryProvider | null);
1301
+ /**
1302
+ * isAvailable ? wrap(prefetch + systemPromptBlock) : "".
1303
+ * A provider exception never aborts the turn — degrade to "".
1304
+ */
1305
+ assembleContext(query: string, ctx: unknown): Promise<string>;
1306
+ initialize(ctx: unknown): Promise<void>;
1307
+ syncTurn(userContent: string, assistantContent: string, ctx: unknown): Promise<void>;
1308
+ onSessionEnd(messages: readonly unknown[], ctx: unknown): Promise<void>;
1309
+ onSessionSwitch(from: SessionRef | null, to: SessionRef, ctx: unknown): Promise<void>;
1310
+ onPreCompress(messages: readonly unknown[], ctx: unknown): Promise<string>;
1311
+ onDelegation(task: string, result: string, ctx: unknown): Promise<void>;
1312
+ shutdown(): Promise<void>;
1313
+ }
1314
+
1315
+ /**
1316
+ * Permission ruleset evaluator.
1317
+ *
1318
+ * A small, pure, stateless re-expression of a two-dimensional wildcard
1319
+ * permission model plus a bash-command-prefix extractor. No global state,
1320
+ * no I/O, no mutation of inputs — every function is referentially transparent.
1321
+ *
1322
+ * The model has two independent dimensions per rule:
1323
+ * - `permission` — the capability namespace (e.g. "bash", "edit", "net")
1324
+ * - `pattern` — the concrete subject within that namespace
1325
+ * A rule applies only when BOTH dimensions match the query via {@link wildcardMatch}.
1326
+ */
1327
+ /** The decision a rule yields. Unknown queries fail safe to "ask". */
1328
+ type Action = "allow" | "deny" | "ask";
1329
+ /** A single permission rule. Both dimensions are matched as wildcard globs. */
1330
+ interface Rule {
1331
+ permission: string;
1332
+ pattern: string;
1333
+ action: Action;
1334
+ }
1335
+ /** An ordered list of rules. Earlier rules take precedence. */
1336
+ type Ruleset = Rule[];
1337
+ /**
1338
+ * Anchored full-string glob matcher.
1339
+ *
1340
+ * Semantics:
1341
+ * - `*` matches any run of characters, including the empty run.
1342
+ * - `?` matches exactly one character.
1343
+ * - Every other character is matched literally (regex metacharacters in
1344
+ * `pattern` carry no special meaning).
1345
+ * - The match is anchored: the entire `str` must be consumed.
1346
+ *
1347
+ * Special rule (ported faithfully): if `pattern` ends with `" *"` (a single
1348
+ * space immediately followed by `*`), that trailing ` *` is OPTIONAL. The
1349
+ * pattern then matches both `"<head> <rest>"` and exactly `"<head>"` with
1350
+ * nothing after it. For example `"git *"` matches `"git"` and `"git status"`.
1351
+ *
1352
+ * Implemented with a backtracking two-pointer scan whose `*` handling uses a
1353
+ * single saved restart position, giving O(|str| * |pattern|) worst case with
1354
+ * no catastrophic blow-up.
1355
+ */
1356
+ declare function wildcardMatch(str: string, pattern: string): boolean;
1357
+ /**
1358
+ * Resolve a permission query against one or more rulesets.
1359
+ *
1360
+ * Rulesets are concatenated in argument order (no mutation) and scanned
1361
+ * front-to-back. The FIRST rule whose `permission` and `pattern` both match
1362
+ * the query (via {@link wildcardMatch}) is returned. If no rule matches, a
1363
+ * fail-safe default is returned: the queried permission/pattern with action
1364
+ * `"ask"` (unknown → ask, never silently allow).
1365
+ */
1366
+ declare function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule;
1367
+ /**
1368
+ * Built-in command arity table. Maps a (possibly multi-word) command prefix
1369
+ * to the number of leading tokens that constitute its "human-understandable
1370
+ * command" for permission matching. Longest matching prefix wins.
1371
+ *
1372
+ * Frozen so the shared default cannot be mutated by callers.
1373
+ */
1374
+ declare const BUILTIN_ARITY: Readonly<Record<string, number>>;
1375
+ /**
1376
+ * Extract the human-understandable command from already-split, flag-free
1377
+ * shell tokens.
1378
+ *
1379
+ * Strategy: try the longest prefix first. For `len` from `tokens.length` down
1380
+ * to 1, if `tokens.slice(0, len).join(" ")` is a key in the (merged) arity
1381
+ * table, return `tokens.slice(0, arity[thatPrefix])`. If the tokens are empty,
1382
+ * return `[]`. Otherwise default to the first token only.
1383
+ *
1384
+ * `arity` is shallow-merged OVER the built-in table; neither the caller's
1385
+ * object nor the built-in table is mutated.
1386
+ */
1387
+ declare function commandPrefix(tokens: string[], arity?: Record<string, number> | undefined): string[];
1388
+ /**
1389
+ * Derive the `pattern` to feed {@link evaluate} for a bash permission.
1390
+ *
1391
+ * Splits `command` on arbitrary whitespace, drops tokens that begin with `-`
1392
+ * (flags are not conceptually part of the command identity), applies
1393
+ * {@link commandPrefix}, and joins the result with single spaces.
1394
+ */
1395
+ declare function commandPermissionKey(command: string): string;
1396
+
1397
+ /**
1398
+ * Git-backed project repository model.
1399
+ *
1400
+ * Faithful re-expression of the "every project is a git repo" persistence
1401
+ * model: a project's metadata and conversation logs live AS committed files
1402
+ * inside its repository. The commit graph IS the history — there is no
1403
+ * in-memory snapshot ring (that is `workbench.ts`'s concern). The actual
1404
+ * provider-specific git implementation is injected via `GitHostPort`; this
1405
+ * module never performs git operations itself.
1406
+ */
1407
+ interface ConversationSummary {
1408
+ id: string;
1409
+ title: string;
1410
+ createdAt: string;
1411
+ updatedAt: string;
1412
+ }
1413
+ type DeploymentState = "idle" | "deploying" | "live" | "failed";
1414
+ interface DeploymentSummary {
1415
+ commitSha: string;
1416
+ commitMessage: string;
1417
+ commitDate: string;
1418
+ domain: string;
1419
+ url: string;
1420
+ deploymentId: string | null;
1421
+ state: DeploymentState;
1422
+ }
1423
+ interface ProjectMetadata {
1424
+ projectId: string;
1425
+ name: string;
1426
+ conversations: ConversationSummary[];
1427
+ deployments: DeploymentSummary[];
1428
+ productionDomain?: string | undefined;
1429
+ productionDeploymentId?: string | undefined;
1430
+ }
1431
+ interface CommitRef {
1432
+ sha: string;
1433
+ message: string;
1434
+ date: string;
1435
+ }
1436
+ interface GitHostPort {
1437
+ getDefaultBranch(repoId: string): Promise<string>;
1438
+ readFile(repoId: string, path: string, ref?: string | undefined): Promise<string | null>;
1439
+ writeCommit(repoId: string, branch: string, files: Record<string, string>, message: string): Promise<CommitRef>;
1440
+ listCommits(repoId: string, branch: string, limit?: number | undefined): Promise<CommitRef[]>;
1441
+ }
1442
+ /** Default-deny ownership predicate: cross-tenant access impossible unless wired. */
1443
+ type OwnsRepoPredicate = (tenantId: string, repoId: string) => boolean | Promise<boolean>;
1444
+ interface ProjectRepoDeps {
1445
+ host: GitHostPort;
1446
+ ownsRepo?: OwnsRepoPredicate | undefined;
1447
+ }
1448
+ type ProjectRepoErrorCode = "tenant_required" | "tenant_denied" | "metadata_malformed";
1449
+ declare class ProjectRepoError extends Error {
1450
+ readonly code: ProjectRepoErrorCode;
1451
+ constructor(code: ProjectRepoErrorCode, message: string);
1452
+ }
1453
+ declare const METADATA_PATH = "metadata.json";
1454
+ declare const CONVERSATIONS_DIR = "conversations";
1455
+ /** Parse JSONL content; tolerant of blank lines and trailing newline. */
1456
+ declare function parseJsonl(content: string): unknown[];
1457
+ /** Append one JSON line to existing JSONL content (immutable; never mutates input). */
1458
+ declare function appendJsonlLine(prev: string, value: unknown): string;
1459
+ /** Well-formed empty metadata — used when a project repo has no metadata yet. */
1460
+ declare function emptyProjectMetadata(projectId: string, name: string): ProjectMetadata;
1461
+ declare class ProjectRepo {
1462
+ private readonly host;
1463
+ private readonly ownsRepo;
1464
+ constructor(deps: ProjectRepoDeps);
1465
+ private authorize;
1466
+ private defaultBranch;
1467
+ readMetadata(tenantId: string, repoId: string): Promise<ProjectMetadata>;
1468
+ writeMetadata(tenantId: string, repoId: string, meta: ProjectMetadata): Promise<CommitRef>;
1469
+ appendConversationMessage(tenantId: string, repoId: string, conversationId: string, message: unknown): Promise<CommitRef>;
1470
+ readConversationMessages(tenantId: string, repoId: string, conversationId: string): Promise<unknown[]>;
1471
+ restoreAt(tenantId: string, repoId: string, sha: string): Promise<{
1472
+ metadata: ProjectMetadata;
1473
+ }>;
1474
+ historyFromCommits(tenantId: string, repoId: string, limit?: number | undefined): Promise<CommitRef[]>;
1475
+ }
1476
+
1477
+ /**
1478
+ * Shareable-session model — a faithful re-expression of a coding agent's
1479
+ * "share this session with a viewer" subsystem into Sailor's grammar:
1480
+ * TypeScript, multi-tenant, no datastore lock-in, no transport/crypto vendor
1481
+ * lock-in.
1482
+ *
1483
+ * ── Mental model ────────────────────────────────────────────────────────────
1484
+ * The session OWNER keeps editing through the normal session API. Sharing only
1485
+ * grants a *read-only viewer* an observation window. A {@link ShareRecord} is
1486
+ * therefore always `readonly: true` — there is no writable share.
1487
+ *
1488
+ * ── Two access planes ───────────────────────────────────────────────────────
1489
+ * 1. OWNER plane ({@link SessionShare.share} / {@link SessionShare.unshare}):
1490
+ * tenant-scoped. `tenantId` is mandatory, Zod-validated, and fails closed
1491
+ * on empty input. Cross-tenant reads are impossible by construction because
1492
+ * {@link ShareStore.getBySession} is tenant-keyed.
1493
+ *
1494
+ * 2. VIEWER plane ({@link SessionShare.verifyViewer}): PUBLIC. The viewer does
1495
+ * NOT have a tenantId — they only hold a link containing the share id plus
1496
+ * an unguessable secret. Security rests ENTIRELY on the secret being
1497
+ * high-entropy and compared in (length-independent) constant time. This is
1498
+ * why {@link ShareStore.getById} is tenant-agnostic: it is the public
1499
+ * lookup, gated by the secret rather than by tenancy. The secret never
1500
+ * reaches the owner caller path (it travels via the record + sync sink to
1501
+ * wherever the viewer URL is rendered).
1502
+ *
1503
+ * ── Threat model ────────────────────────────────────────────────────────────
1504
+ * • A viewer link leaking == full read access to that one session until the
1505
+ * owner revokes. Mitigation: revoke is immediate and `verifyViewer` denies
1506
+ * revoked shares; the secret must be CSPRNG-grade (enforced by the injected
1507
+ * {@link IdMint}, NOT by this module — the module never generates entropy).
1508
+ * • Secret guessing / timing oracle. Mitigation: {@link constantTimeEquals}
1509
+ * never early-returns on the first mismatched byte and folds length
1510
+ * differences into the same negative result.
1511
+ *
1512
+ * ── Sink-failure policy (DOCUMENTED CHOICE) ─────────────────────────────────
1513
+ * Local persistence is the source of truth. `share`/`unshare` persist to the
1514
+ * {@link ShareStore} FIRST, then best-effort emit to the {@link SyncSink}. If
1515
+ * `sink.emit` rejects, the local store change STILL STANDS and the sink error
1516
+ * is SWALLOWED (not rethrown) so a flaky remote can never roll back or hide a
1517
+ * share/revoke the owner already committed. The owner call resolves normally;
1518
+ * sync convergence is the remote's eventual-consistency concern, not the
1519
+ * caller's. (Tested: a failing sink does not lose the persisted share/revoke.)
1520
+ */
1521
+ /** A share is always read-only for viewers; the owner edits via the session. */
1522
+ interface ShareRecord {
1523
+ readonly id: string;
1524
+ readonly sessionId: string;
1525
+ readonly tenantId: string;
1526
+ readonly url: string;
1527
+ readonly secret: string;
1528
+ readonly readonly: true;
1529
+ readonly createdAt: string;
1530
+ readonly revokedAt?: string | undefined;
1531
+ }
1532
+ /** Emitted to the sync sink whenever share state changes. */
1533
+ interface ShareSyncEvent {
1534
+ readonly type: "share.created" | "share.revoked";
1535
+ readonly tenantId: string;
1536
+ readonly sessionId: string;
1537
+ readonly share: ShareRecord | null;
1538
+ }
1539
+ /**
1540
+ * Caller-injected id/secret source. Tests pass a deterministic stub. Real
1541
+ * callers MUST back `secret()` with a CSPRNG (≥128 bits) — this module
1542
+ * consumes the port and never implements crypto itself.
1543
+ */
1544
+ interface IdMint {
1545
+ shareId(): string;
1546
+ secret(): string;
1547
+ }
1548
+ /** Derives the public viewer URL from a share id. */
1549
+ type UrlBuilder = (shareId: string) => string;
1550
+ /**
1551
+ * Best-effort propagation seam for share state changes. The real impl pushes to
1552
+ * a remote; tests use a recorder. A rejecting `emit` MUST NOT corrupt local
1553
+ * state — see the module-level sink-failure policy.
1554
+ */
1555
+ interface SyncSink {
1556
+ emit(event: ShareSyncEvent): Promise<void>;
1557
+ }
1558
+ /**
1559
+ * Storage seam. `getBySession` is tenant-keyed and tenant-validated (owner
1560
+ * plane). `getById` is the PUBLIC viewer lookup: the viewer has no tenantId, so
1561
+ * it is tenant-agnostic and secret-gated by the caller, never tenant-gated.
1562
+ */
1563
+ interface ShareStore {
1564
+ put(rec: ShareRecord): Promise<void>;
1565
+ getBySession(tenantId: string, sessionId: string): Promise<ShareRecord | null>;
1566
+ getById(shareId: string): Promise<ShareRecord | null>;
1567
+ }
1568
+ /** Raised when sharing is globally disabled by the kill-switch. */
1569
+ declare class ShareDisabledError extends Error {
1570
+ constructor(message?: string);
1571
+ }
1572
+ /**
1573
+ * Length-independent, full-scan equality. Never short-circuits on the first
1574
+ * differing character; a length mismatch is folded into the same negative
1575
+ * result by comparing against a fixed-length view so the loop count does not
1576
+ * leak the secret length. Pure and unit-tested in isolation.
1577
+ */
1578
+ declare function constantTimeEquals(a: string, b: string): boolean;
1579
+ /**
1580
+ * In-memory reference {@link ShareStore}. Mirrors the package's
1581
+ * store/port convention (interface + in-memory ref impl, like
1582
+ * `InMemoryRolloutStore`). Production swaps in a Postgres / `@nebutra/db`
1583
+ * adapter satisfying the same interface — no infra change.
1584
+ *
1585
+ * Tenancy is structural: the by-session key embeds `tenantId`, so a different
1586
+ * tenant resolves to a different key and gets `null`. A by-id index serves the
1587
+ * public viewer path.
1588
+ */
1589
+ declare class InMemoryShareStore implements ShareStore {
1590
+ #private;
1591
+ put(rec: ShareRecord): Promise<void>;
1592
+ getBySession(tenantId: string, sessionId: string): Promise<ShareRecord | null>;
1593
+ getById(shareId: string): Promise<ShareRecord | null>;
1594
+ }
1595
+ /** Result of a public viewer access attempt. */
1596
+ type ViewerVerdict = {
1597
+ readonly ok: true;
1598
+ readonly record: ShareRecord;
1599
+ } | {
1600
+ readonly ok: false;
1601
+ readonly reason: "not-found" | "revoked" | "bad-secret";
1602
+ };
1603
+ interface SessionShareOptions {
1604
+ readonly store: ShareStore;
1605
+ readonly mint: IdMint;
1606
+ readonly urlBuilder: UrlBuilder;
1607
+ readonly sink: SyncSink;
1608
+ /** Kill-switch: when true, every `share()` throws {@link ShareDisabledError}. */
1609
+ readonly disabled?: boolean | undefined;
1610
+ }
1611
+ /** Tenant-scoped owner API + public viewer verification. */
1612
+ declare class SessionShare {
1613
+ #private;
1614
+ constructor(opts: SessionShareOptions);
1615
+ /**
1616
+ * Share a session read-only. Fails closed on empty tenant/session. Honors the
1617
+ * kill-switch. Idempotent: if a live (non-revoked) share exists, returns its
1618
+ * url WITHOUT minting a new secret or emitting a duplicate `share.created`.
1619
+ * Returns only the url — the secret never reaches the owner caller path.
1620
+ */
1621
+ share(tenantId: string, sessionId: string): Promise<{
1622
+ url: string;
1623
+ }>;
1624
+ /**
1625
+ * Revoke a session's share. Fails closed on empty tenant/session. Idempotent:
1626
+ * a no-op (no emit) when no live share exists. On revoke, persists `revokedAt`
1627
+ * and emits `share.revoked` with `share: null`.
1628
+ */
1629
+ unshare(tenantId: string, sessionId: string): Promise<void>;
1630
+ /**
1631
+ * PUBLIC viewer access. NO tenantId — the viewer has none; security rests
1632
+ * entirely on the unguessable secret, compared in length-independent
1633
+ * constant time. Unknown id → not-found; revoked share → revoked (even with
1634
+ * the right secret); wrong secret → bad-secret. Fails closed on empty id.
1635
+ */
1636
+ verifyViewer(shareId: string, presentedSecret: string): Promise<ViewerVerdict>;
1637
+ }
1638
+
1639
+ /**
1640
+ * Skill distillation (INVERSE of ./skills).
1641
+ *
1642
+ * Where ./skills only *loads and discloses* authored skills, this module
1643
+ * *creates and refines* them from lived experience — the "learn a skill
1644
+ * from a trajectory" loop of a self-improving agent, re-expressed from
1645
+ * first principles.
1646
+ *
1647
+ * The loop:
1648
+ * 1. observe a trajectory (the ordered steps an agent took toward a goal)
1649
+ * 2. gate it — only clean successes with real tool work distill (no noise)
1650
+ * 3. build a deterministic distillation prompt from the trajectory
1651
+ * 4. hand the prompt to an INJECTED synthesizer (the LLM call lives
1652
+ * outside this module — here it is a pure indirection)
1653
+ * 5. assemble a tenant-scoped, least-privilege `DistilledSkill` whose
1654
+ * tool allowlist is clamped to tools the experience actually exercised
1655
+ * 6. optionally refine an existing skill from a fresh success
1656
+ *
1657
+ * SECURITY: every entry point requires a non-empty `tenantId` and fails
1658
+ * closed. Cross-tenant improvement is rejected. A distilled skill can never
1659
+ * be granted a tool the trajectory did not use (least privilege). Pure
1660
+ * data/logic; no host access; the model call is an injected indirection.
1661
+ */
1662
+
1663
+ interface TrajectoryStep {
1664
+ readonly kind: "tool" | "message" | "observation";
1665
+ readonly name?: string | undefined;
1666
+ readonly input?: unknown;
1667
+ readonly output?: unknown;
1668
+ }
1669
+ interface Trajectory {
1670
+ readonly tenantId: string;
1671
+ readonly sessionId: string;
1672
+ readonly goal: string;
1673
+ readonly steps: readonly TrajectoryStep[];
1674
+ readonly outcome: "success" | "failure" | "partial";
1675
+ }
1676
+ /**
1677
+ * Minimal structural shape — deliberately NOT imported from ./skills /
1678
+ * ./definitions. It is structurally usable as a skill record consumed by
1679
+ * the loader: a slug, frontmatter-shaped metadata, and a body text.
1680
+ */
1681
+ interface DistilledSkillFrontmatter {
1682
+ readonly name: string;
1683
+ readonly description: string;
1684
+ readonly whenToUse?: string | undefined;
1685
+ readonly allowedTools: readonly string[];
1686
+ }
1687
+ interface DistilledSkillProvenance {
1688
+ readonly sessionId: string;
1689
+ readonly distilledAt: string;
1690
+ readonly stepCount: number;
1691
+ readonly version?: number | undefined;
1692
+ }
1693
+ interface DistilledSkill {
1694
+ readonly slug: string;
1695
+ readonly tenantId: string;
1696
+ readonly sourceTier: "dynamic";
1697
+ readonly frontmatter: DistilledSkillFrontmatter;
1698
+ readonly body: string;
1699
+ readonly provenance: DistilledSkillProvenance;
1700
+ }
1701
+ /** Injected LLM indirection — the model call is NOT implemented here. */
1702
+ type Synthesizer = (prompt: string) => Promise<{
1703
+ name: string;
1704
+ description: string;
1705
+ whenToUse: string;
1706
+ body: string;
1707
+ allowedTools: string[];
1708
+ }>;
1709
+ /** kebab-case a free-form name into a stable slug. */
1710
+ declare function kebab(name: string): string;
1711
+ /** Deterministic distillation prompt — pure function of the trajectory. */
1712
+ declare function buildDistillationPrompt(traj: Trajectory): string;
1713
+ /** Deterministic improvement prompt — does not regress the existing skill. */
1714
+ declare function buildImprovementPrompt(existing: DistilledSkill, traj: Trajectory): string;
1715
+ declare const eligibilityOptionsSchema: z.ZodObject<{
1716
+ minToolSteps: z.ZodDefault<z.ZodNumber>;
1717
+ }, z.core.$strip>;
1718
+ type EligibilityOptions = z.input<typeof eligibilityOptionsSchema>;
1719
+ /**
1720
+ * Only clean successes with real tool work and a non-trivial goal qualify.
1721
+ * Pure. Don't manufacture noise skills from failures or trivial sessions.
1722
+ */
1723
+ declare function isDistillable(traj: Trajectory, options?: EligibilityOptions): {
1724
+ ok: boolean;
1725
+ reason?: string;
1726
+ };
1727
+ interface DistillOptions {
1728
+ /** Override the clock for deterministic provenance in tests. */
1729
+ readonly now?: (() => Date) | undefined;
1730
+ readonly eligibility?: EligibilityOptions | undefined;
1731
+ }
1732
+ /**
1733
+ * Distill a `DistilledSkill` from a successful trajectory. The model call is
1734
+ * the injected `synthesize`. The resulting tool allowlist is clamped to the
1735
+ * intersection of what the synthesizer asked for and what the trajectory
1736
+ * actually exercised — least privilege, always.
1737
+ */
1738
+ declare function distillSkill(traj: Trajectory, synthesize: Synthesizer, opts?: DistillOptions): Promise<DistilledSkill>;
1739
+ /**
1740
+ * Refine an existing distilled skill from a fresh successful trajectory for
1741
+ * the same goal. Tenant must match (fail-closed). The tool allowlist is the
1742
+ * union of the existing allowlist and the new trajectory's tools, then
1743
+ * re-clamped to tools the new trajectory actually used (still least
1744
+ * privilege). The body is replaced via the injected synthesizer with an
1745
+ * "improve, don't regress" prompt. Version bumps.
1746
+ */
1747
+ declare function improveSkill(existing: DistilledSkill, traj: Trajectory, synthesize: Synthesizer): Promise<DistilledSkill>;
1748
+ declare const nudgeOptionsSchema: z.ZodObject<{
1749
+ minUnsavedSuccesses: z.ZodDefault<z.ZodNumber>;
1750
+ minTurns: z.ZodDefault<z.ZodNumber>;
1751
+ }, z.core.$strip>;
1752
+ type NudgeOptions = z.input<typeof nudgeOptionsSchema>;
1753
+ /**
1754
+ * Deterministic heuristic: nudge the agent to persist learning once enough
1755
+ * unsaved successes have accumulated, or enough turns have passed since the
1756
+ * last distillation. Pure.
1757
+ */
1758
+ declare function shouldNudgePersist(sessionStats: {
1759
+ turnsSinceLastDistill: number;
1760
+ unsavedSuccesses: number;
1761
+ }, options?: NudgeOptions): boolean;
1762
+
1763
+ /**
1764
+ * Workbench project-state model — an in-memory project filemap with
1765
+ * snapshot / restore / history, mirroring the durable seam pattern used by
1766
+ * {@link ./rollout-store-persistent.ts}: a tenant-scoped reference
1767
+ * implementation sits behind a {@link ProjectPersistencePort} so a Postgres /
1768
+ * object-store / `@nebutra/db` adapter can replace it without touching callers.
1769
+ *
1770
+ * Tenancy is structural: `tenantId` is part of every storage key AND
1771
+ * Zod-validated at each boundary. Cross-tenant access is impossible by
1772
+ * construction. All filemap ops are pure — they return NEW maps and never
1773
+ * mutate caller input. Snapshots are deep copies, so editing the live project
1774
+ * after taking a snapshot can never reach back and mutate history.
1775
+ */
1776
+ /** A single entry in a project file map. */
1777
+ type FileEntry = {
1778
+ kind: "file";
1779
+ content: string;
1780
+ binary: boolean;
1781
+ } | {
1782
+ kind: "folder";
1783
+ };
1784
+ /** Project tree as a path → entry map. Treat as immutable. */
1785
+ type FileMap = Map<string, FileEntry>;
1786
+ /** A tenant-scoped project and its file tree. */
1787
+ interface Project {
1788
+ tenantId: string;
1789
+ projectId: string;
1790
+ files: FileMap;
1791
+ createdAt: string;
1792
+ updatedAt: string;
1793
+ }
1794
+ /** Frozen point-in-time copy of a project's files. */
1795
+ interface ProjectSnapshot {
1796
+ id: string;
1797
+ projectId: string;
1798
+ tenantId: string;
1799
+ files: FileMap;
1800
+ at: string;
1801
+ }
1802
+ /** Result of comparing two projects by path + content. */
1803
+ interface ProjectDiff {
1804
+ added: string[];
1805
+ removed: string[];
1806
+ changed: string[];
1807
+ }
1808
+ /**
1809
+ * Structural shape of a parsed-plan file action. Declared locally on purpose:
1810
+ * it intentionally mirrors a `{ type: "file", filePath, content }` plan action
1811
+ * without importing sibling modules.
1812
+ */
1813
+ type FileMutationInput = {
1814
+ type: "file";
1815
+ filePath: string;
1816
+ content: string;
1817
+ } | {
1818
+ type: "remove";
1819
+ filePath: string;
1820
+ };
1821
+ /** Raised when a boundary identifier is missing or empty. */
1822
+ declare class WorkbenchError extends Error {
1823
+ constructor(message: string, options?: {
1824
+ cause?: unknown;
1825
+ });
1826
+ }
1827
+ /** A fresh, empty file map. */
1828
+ declare function emptyFileMap(): FileMap;
1829
+ /** Set (or overwrite) a file, auto-deriving folder entries. Returns a NEW map. */
1830
+ declare function setFile(map: FileMap, path: string, content: string): FileMap;
1831
+ /** Read a file/folder entry, or undefined. */
1832
+ declare function getFile(map: FileMap, path: string): FileEntry | undefined;
1833
+ /** Remove an entry. Returns a NEW map; original untouched. */
1834
+ declare function removeFile(map: FileMap, path: string): FileMap;
1835
+ /** All file (not folder) paths, sorted ascending. */
1836
+ declare function listFiles(map: FileMap): string[];
1837
+ /** Construct an empty tenant-scoped project. Empty ids fail closed. */
1838
+ declare function makeProject(tenant: string, project: string): Project;
1839
+ /**
1840
+ * Land a parsed-plan file action into the workbench. Returns a NEW project
1841
+ * with `updatedAt` bumped; the caller's project and mutation are never mutated.
1842
+ */
1843
+ declare function applyFileMutation(project: Project, mutation: FileMutationInput): Project;
1844
+ /** Deep-copy snapshot — isolated from later edits to the live project. */
1845
+ declare function snapshot(project: Project): ProjectSnapshot;
1846
+ /** Replace project files from a snapshot. Tenant-checked; bumps updatedAt. */
1847
+ declare function restore(project: Project, snap: ProjectSnapshot): Project;
1848
+ /** Tenant-scoped, bounded snapshot ring buffer keyed by (tenant, project). */
1849
+ declare class SnapshotHistory {
1850
+ #private;
1851
+ constructor(cap?: number);
1852
+ push(tenant: string, snap: ProjectSnapshot): void;
1853
+ list(tenant: string, project: string): ProjectSnapshot[];
1854
+ get(tenant: string, project: string, id: string): ProjectSnapshot | undefined;
1855
+ latest(tenant: string, project: string): ProjectSnapshot | undefined;
1856
+ }
1857
+ /** Compare two projects by file path + content. Pure and cheap. */
1858
+ declare function diffProjects(a: Project, b: Project): ProjectDiff;
1859
+ /**
1860
+ * Minimal durable seam. Implementations MUST persist projects tenant-scoped
1861
+ * and make cross-tenant reads impossible. Mirrors {@link RolloutPersistencePort}.
1862
+ */
1863
+ interface ProjectPersistencePort {
1864
+ put(project: Project): Promise<void>;
1865
+ get(tenant: string, project: string): Promise<Project | undefined>;
1866
+ }
1867
+ /** In-memory reference {@link ProjectPersistencePort}. Fails closed. */
1868
+ declare class InMemoryProjectStore implements ProjectPersistencePort {
1869
+ #private;
1870
+ put(project: Project): Promise<void>;
1871
+ get(tenant: string, project: string): Promise<Project | undefined>;
1872
+ }
1873
+
1874
+ export { type Action, type ActionEvent, type ActionPorts, ActionRunner, type ActionState, type ActionStatus, type AdmissionDeps, type AdmissionPolicy, type AdmissionReason, type AdmissionResult, type AllowlistMatch, type ApplyEditResult, type ArtifactEvent, type ArtifactStreamCallbacks, ArtifactStreamParser, BUDGET_RATIO, BUILTIN_ARITY, type BaseEstimator, type BuildAction, CLIP_TEXT_CHARS, CLIP_TOOL_CHARS, COMPACT_SENTINEL, CONCURRENCY, CONVERSATIONS_DIR, type ChannelAdapter, type ChannelCapabilities, ChannelGateway, type ChannelId, type ChannelMeta, ChannelRegistry, type ChatType, type Clock, CommitMessageError, type CommitRef, type CompactTranscriptInput, type CompiledAllowlist, type CompletionModel, type ConversationSummary, DEFAULT_ABORT_MS, DEFAULT_TEMPERATURE, type DataAction, type DebounceResult, type DeployState, type DeploymentCommitRef, type DeploymentRecord, type DeploymentState, DeploymentStateError, type DeploymentSummary, type DeploymentTimelineEntry, type DeploymentUiStatus, type DesignContext, type DiffFile, type DiffResult, type DistillOptions, type DistilledSkill, type DistilledSkillFrontmatter, type DistilledSkillProvenance, DuplicateChannelError, type EditBlock, type EditIntent, type EditMerger, EditType, type EligibilityOptions, type FileAction, type FileContext, type FileEntry, type FileInfo, type FileKind, type FileManifest, type FileMap, type FileMutationInput, type FuzzyMatch, type FuzzyMatchFn, type GenerateOptions, type GitContext, type GitHostPort, type GroupBinding, type Hunk, type IdMint, InMemoryProjectStore, InMemoryShareStore, InMemorySuggestionHistoryStore, type InboundAttachment, InboundDebouncer, type InboundLike, type InboundMessage, LARGE_SET, type LatestStatusInput, MAX_RETRIES, MEMORY_CONTEXT_BANNER_CLOSE, MEMORY_CONTEXT_BANNER_OPEN, METADATA_PATH, MIN_BUDGET, type MatchClassification, type MatchType, type MemoryContext, MemoryManager, type MemoryProvider, type MentionContext, type Message, type NudgeOptions, OUTPUT_TOKEN_CAP, type OutboundMessage, type OwnsRepoPredicate, type Part, type PlanAction, type Project, type ProjectDiff, type ProjectMetadata, type ProjectPersistencePort, ProjectRepo, type ProjectRepoDeps, ProjectRepoError, type ProjectRepoErrorCode, type ProjectSnapshot, REDUCE_DEPTH, type RankOptions, type RankedFuzzyResult, type RankedSuggestion, type RawScrapeResult, type ReplyOptions, type ReviewFinding, type ReviewModel, ReviewParseError, type ReviewScope, type Rule, type Ruleset, type RunQueueOptions, type ScrapeFormat, type ScrapeProvider, type SessionKeyOptions, type SessionRef, SessionShare, type SessionShareOptions, ShareDisabledError, type ShareRecord, type ShareStore, type ShareSyncEvent, type ShellAction, SnapshotHistory, type StartAction, StreamingContextScrubber, type SuggestionHistoryStore, type SuggestionItem, type SuggestionResults, type SuggestionType, type Summarize, type SyncSink, type Synthesizer, TOKEN_CORRECTION, type TenantContext, TenantMismatchError, type TextPart, type TimelineOpts, type ToolPart, type Trajectory, type TrajectoryStep, type Transcript, UnknownChannelError, type UrlBuilder, type ViewerVerdict, WorkbenchError, admitInbound, advanceState, analyzeEditIntent, appendJsonlLine, applyEditBlock, applyFileMutation, asChannelId, assertTenantContext, buildCommitPrompt, buildDistillationPrompt, buildEditInstructions, buildImprovementPrompt, buildMemoryContextBlock, buildReviewPrompt, classifyMatch, commandPermissionKey, commandPrefix, compactTranscript, compileAllowlist, computeBudget, constantTimeEquals, dedupeByText, deriveLatestStatus, deriveTimelineFromCommits, diffProjects, distillSkill, domainForCommit, emptyFileMap, emptyProjectMetadata, estimateTokens, evaluate, fallbackMerger, generateCommitMessage, getDeploymentStatus, getDeploymentTimeline, getFile, hasMention, historyCandidates, improveSkill, ingestDesignContext, isDistillable, kebab, listFiles, makeProject, matchIndices, matchIndicesCaseInsensitive, matchIndicesCaseInsensitiveIgnoreSpaces, matchWildcardPattern, matchWildcardPatternCaseInsensitive, matchesAllowlist, nextMode, normalizeScrapeResult, parseDiff, parseEditBlocks, parseFindings, parseJsonl, planEdit, rankByFuzzy, rankSuggestions, recordHistory, removeFile, renderClipped, replyTo, requiresMention, resolveBranchBase, resolveSessionKey, restore, runReview, sanitizeContext, selectFilesForEdit, setFile, shouldCompact, shouldNudgePersist, snapshot, split, stripFencedWrapper, stripFormatting, toGenerationSeed, unescapeEntities, wildcardMatch };