@lumenflow/cli 5.4.0 → 5.7.12

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 (227) hide show
  1. package/README.md +42 -40
  2. package/dist/db-journal-recover.js +400 -0
  3. package/dist/db-journal-recover.js.map +1 -0
  4. package/dist/docs-sync.js +8 -3
  5. package/dist/docs-sync.js.map +1 -1
  6. package/dist/gate-defaults.js +191 -9
  7. package/dist/gate-defaults.js.map +1 -1
  8. package/dist/gate-registry.js.map +1 -1
  9. package/dist/gates/monolithic-file-contention-guard.js +167 -0
  10. package/dist/gates/monolithic-file-contention-guard.js.map +1 -0
  11. package/dist/gates/prod-migration-drift.js +207 -0
  12. package/dist/gates/prod-migration-drift.js.map +1 -0
  13. package/dist/gates/test-over-deletion-guard.js +255 -0
  14. package/dist/gates/test-over-deletion-guard.js.map +1 -0
  15. package/dist/gates-runners.js +401 -2
  16. package/dist/gates-runners.js.map +1 -1
  17. package/dist/gates.js +349 -4
  18. package/dist/gates.js.map +1 -1
  19. package/dist/lumenflow-setup.js +144 -0
  20. package/dist/lumenflow-setup.js.map +1 -0
  21. package/dist/lumenflow-upgrade.js +2 -1
  22. package/dist/lumenflow-upgrade.js.map +1 -1
  23. package/dist/mem-create.js +10 -1
  24. package/dist/mem-create.js.map +1 -1
  25. package/dist/mem-signal.js +21 -4
  26. package/dist/mem-signal.js.map +1 -1
  27. package/dist/metrics-cli.js +19 -2
  28. package/dist/metrics-cli.js.map +1 -1
  29. package/dist/metrics-snapshot.js +25 -2
  30. package/dist/metrics-snapshot.js.map +1 -1
  31. package/dist/orchestrate-initiative.js +28 -3
  32. package/dist/orchestrate-initiative.js.map +1 -1
  33. package/dist/public-manifest.js +17 -0
  34. package/dist/public-manifest.js.map +1 -1
  35. package/dist/release.js +53 -18
  36. package/dist/release.js.map +1 -1
  37. package/dist/wu-done-gates.js +121 -8
  38. package/dist/wu-done-gates.js.map +1 -1
  39. package/dist/wu-done.js +30 -6
  40. package/dist/wu-done.js.map +1 -1
  41. package/dist/wu-edit-operations.js +74 -0
  42. package/dist/wu-edit-operations.js.map +1 -1
  43. package/dist/wu-edit-validators.js +58 -0
  44. package/dist/wu-edit-validators.js.map +1 -1
  45. package/dist/wu-edit.js +106 -4
  46. package/dist/wu-edit.js.map +1 -1
  47. package/dist/wu-prep.js +132 -8
  48. package/dist/wu-prep.js.map +1 -1
  49. package/dist/wu-recover.js +6 -0
  50. package/dist/wu-recover.js.map +1 -1
  51. package/dist/wu-release.js +120 -2
  52. package/dist/wu-release.js.map +1 -1
  53. package/dist/wu-sizing-validation.js +47 -17
  54. package/dist/wu-sizing-validation.js.map +1 -1
  55. package/dist/wu-status.js +33 -0
  56. package/dist/wu-status.js.map +1 -1
  57. package/package.json +13 -11
  58. package/packs/agent-runtime/package.json +1 -1
  59. package/packs/sidekick/package.json +1 -1
  60. package/packs/software-delivery/package.json +1 -1
  61. package/templates/core/AGENTS.md.template +162 -26
  62. package/templates/core/LUMENFLOW.md.template +381 -70
  63. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +0 -5
  64. package/templates/core/ai/onboarding/agent-safety-card.md.template +63 -17
  65. package/templates/core/ai/onboarding/initiative-orchestration.md.template +4 -0
  66. package/templates/core/ai/onboarding/release-process.md.template +7 -7
  67. package/templates/core/ai/onboarding/vendor-support.md.template +74 -10
  68. package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +1 -1
  69. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +28 -0
  70. package/packs/agent-runtime/agent-heartbeat.ts +0 -163
  71. package/packs/agent-runtime/auto-session-integration.ts +0 -888
  72. package/packs/agent-runtime/capability-factory.ts +0 -104
  73. package/packs/agent-runtime/constants.ts +0 -21
  74. package/packs/agent-runtime/delegation-registry-schema.ts +0 -220
  75. package/packs/agent-runtime/delegation-registry-store.ts +0 -269
  76. package/packs/agent-runtime/delegation-tree.ts +0 -328
  77. package/packs/agent-runtime/index.ts +0 -20
  78. package/packs/agent-runtime/manifest.ts +0 -348
  79. package/packs/agent-runtime/memory-coordination-contract.ts +0 -86
  80. package/packs/agent-runtime/orchestration.ts +0 -2027
  81. package/packs/agent-runtime/pack-registration.ts +0 -110
  82. package/packs/agent-runtime/policy-factory.ts +0 -165
  83. package/packs/agent-runtime/remote-controls/index.ts +0 -7
  84. package/packs/agent-runtime/remote-controls/operations.ts +0 -405
  85. package/packs/agent-runtime/remote-controls/port.ts +0 -48
  86. package/packs/agent-runtime/remote-controls/state-store.ts +0 -258
  87. package/packs/agent-runtime/remote-controls/types.ts +0 -105
  88. package/packs/agent-runtime/session-schema.ts +0 -467
  89. package/packs/agent-runtime/tool-impl/agent-turn-tools.ts +0 -793
  90. package/packs/agent-runtime/tool-impl/index.ts +0 -6
  91. package/packs/agent-runtime/tool-impl/provider-adapters.ts +0 -1245
  92. package/packs/agent-runtime/tool-impl/remote-controls.mock.ts +0 -256
  93. package/packs/agent-runtime/tool-impl/remote-controls.ts +0 -273
  94. package/packs/agent-runtime/tools/index.ts +0 -4
  95. package/packs/agent-runtime/tools/types.ts +0 -47
  96. package/packs/agent-runtime/turn-lifecycle-events.ts +0 -590
  97. package/packs/agent-runtime/types.ts +0 -128
  98. package/packs/agent-runtime/vitest.config.ts +0 -11
  99. package/packs/sidekick/channel-ingress.ts +0 -137
  100. package/packs/sidekick/constants.ts +0 -10
  101. package/packs/sidekick/index.ts +0 -8
  102. package/packs/sidekick/manifest-schema.ts +0 -49
  103. package/packs/sidekick/manifest.ts +0 -512
  104. package/packs/sidekick/pack-registration.ts +0 -110
  105. package/packs/sidekick/policy-factory.ts +0 -38
  106. package/packs/sidekick/sidekick-events.ts +0 -694
  107. package/packs/sidekick/src/adapters/cloud-queue.ts +0 -101
  108. package/packs/sidekick/src/adapters/control-plane-bridge.adapter.ts +0 -386
  109. package/packs/sidekick/src/adapters/filesystem-bridge.adapter.ts +0 -228
  110. package/packs/sidekick/src/domain/channel.types.ts +0 -64
  111. package/packs/sidekick/src/ports/channel-bridge.port.ts +0 -92
  112. package/packs/sidekick/src/routines/commit.ts +0 -74
  113. package/packs/sidekick/tool-impl/channel-tools.ts +0 -577
  114. package/packs/sidekick/tool-impl/channel-transports.ts +0 -75
  115. package/packs/sidekick/tool-impl/index.ts +0 -29
  116. package/packs/sidekick/tool-impl/memory-tools.ts +0 -290
  117. package/packs/sidekick/tool-impl/routine-commit.ts +0 -102
  118. package/packs/sidekick/tool-impl/routine-tools.ts +0 -440
  119. package/packs/sidekick/tool-impl/runtime-context.ts +0 -28
  120. package/packs/sidekick/tool-impl/shared.ts +0 -125
  121. package/packs/sidekick/tool-impl/storage.ts +0 -325
  122. package/packs/sidekick/tool-impl/system-tools.ts +0 -160
  123. package/packs/sidekick/tool-impl/task-tools.ts +0 -506
  124. package/packs/sidekick/tools/channel-tools.ts +0 -53
  125. package/packs/sidekick/tools/index.ts +0 -9
  126. package/packs/sidekick/tools/memory-tools.ts +0 -53
  127. package/packs/sidekick/tools/routine-tools.ts +0 -53
  128. package/packs/sidekick/tools/system-tools.ts +0 -47
  129. package/packs/sidekick/tools/task-tools.ts +0 -61
  130. package/packs/sidekick/tools/types.ts +0 -57
  131. package/packs/sidekick/vitest.config.ts +0 -11
  132. package/packs/software-delivery/constants.ts +0 -10
  133. package/packs/software-delivery/extensions.ts +0 -140
  134. package/packs/software-delivery/gate-policies.ts +0 -134
  135. package/packs/software-delivery/index.ts +0 -8
  136. package/packs/software-delivery/manifest-schema.ts +0 -268
  137. package/packs/software-delivery/manifest.ts +0 -657
  138. package/packs/software-delivery/pack-registration.ts +0 -113
  139. package/packs/software-delivery/src/commands/index.ts +0 -5
  140. package/packs/software-delivery/src/config/delivery-review-contract.ts +0 -256
  141. package/packs/software-delivery/src/config/env-accessors.ts +0 -66
  142. package/packs/software-delivery/src/config/index.ts +0 -8
  143. package/packs/software-delivery/src/config/normalize-config-keys.ts +0 -9
  144. package/packs/software-delivery/src/config/schemas/lumenflow-config-schema-types.ts +0 -460
  145. package/packs/software-delivery/src/config/workspace-reader.ts +0 -375
  146. package/packs/software-delivery/src/constants/backlog-patterns.ts +0 -31
  147. package/packs/software-delivery/src/constants/client-ids.ts +0 -19
  148. package/packs/software-delivery/src/constants/config-contract.ts +0 -7
  149. package/packs/software-delivery/src/constants/docs-layout-presets.ts +0 -50
  150. package/packs/software-delivery/src/constants/duration-constants.ts +0 -20
  151. package/packs/software-delivery/src/constants/gate-constants.ts +0 -32
  152. package/packs/software-delivery/src/constants/index.ts +0 -29
  153. package/packs/software-delivery/src/constants/lock-constants.ts +0 -35
  154. package/packs/software-delivery/src/constants/object-guards.ts +0 -12
  155. package/packs/software-delivery/src/constants/section-headings.ts +0 -107
  156. package/packs/software-delivery/src/constants/wu-cli-constants.ts +0 -488
  157. package/packs/software-delivery/src/constants/wu-domain-constants.ts +0 -466
  158. package/packs/software-delivery/src/constants/wu-git-constants.ts +0 -7
  159. package/packs/software-delivery/src/constants/wu-id-format.ts +0 -327
  160. package/packs/software-delivery/src/constants/wu-paths-constants.ts +0 -384
  161. package/packs/software-delivery/src/constants/wu-statuses.ts +0 -287
  162. package/packs/software-delivery/src/constants/wu-type-helpers.ts +0 -67
  163. package/packs/software-delivery/src/constants/wu-ui-constants.ts +0 -267
  164. package/packs/software-delivery/src/constants/wu-validation-constants.ts +0 -73
  165. package/packs/software-delivery/src/domain/index.ts +0 -5
  166. package/packs/software-delivery/src/domain/orchestration.constants.ts +0 -166
  167. package/packs/software-delivery/src/domain/orchestration.schemas.ts +0 -238
  168. package/packs/software-delivery/src/domain/orchestration.types.ts +0 -176
  169. package/packs/software-delivery/src/methodology/incremental-test.ts +0 -122
  170. package/packs/software-delivery/src/methodology/index.ts +0 -6
  171. package/packs/software-delivery/src/methodology/manual-test-validator.ts +0 -292
  172. package/packs/software-delivery/src/policy/coverage-gate.ts +0 -270
  173. package/packs/software-delivery/src/policy/gates-agent-mode.ts +0 -223
  174. package/packs/software-delivery/src/policy/gates-config-internal.ts +0 -121
  175. package/packs/software-delivery/src/policy/gates-config.ts +0 -300
  176. package/packs/software-delivery/src/policy/gates-coverage.ts +0 -356
  177. package/packs/software-delivery/src/policy/gates-presets.ts +0 -134
  178. package/packs/software-delivery/src/policy/gates-schemas.ts +0 -173
  179. package/packs/software-delivery/src/policy/index.ts +0 -22
  180. package/packs/software-delivery/src/policy/package-manager-resolver.ts +0 -319
  181. package/packs/software-delivery/src/policy/resolve-policy.ts +0 -601
  182. package/packs/software-delivery/src/ports/config.ports.ts +0 -90
  183. package/packs/software-delivery/src/ports/dashboard-renderer.port.ts +0 -125
  184. package/packs/software-delivery/src/ports/index.ts +0 -10
  185. package/packs/software-delivery/src/ports/sync-validator.ports.ts +0 -59
  186. package/packs/software-delivery/src/ports/wu-helpers.ports.ts +0 -168
  187. package/packs/software-delivery/src/ports/wu-state.ports.ts +0 -241
  188. package/packs/software-delivery/src/primitives/index.ts +0 -5
  189. package/packs/software-delivery/src/runtime/index.ts +0 -6
  190. package/packs/software-delivery/src/runtime/work-classifier.ts +0 -561
  191. package/packs/software-delivery/src/sandbox/index.ts +0 -10
  192. package/packs/software-delivery/src/sandbox/sandbox-allowlist.ts +0 -118
  193. package/packs/software-delivery/src/sandbox/sandbox-backend-linux.ts +0 -88
  194. package/packs/software-delivery/src/sandbox/sandbox-backend-macos.ts +0 -154
  195. package/packs/software-delivery/src/sandbox/sandbox-backend-windows.ts +0 -47
  196. package/packs/software-delivery/src/sandbox/sandbox-profile.ts +0 -153
  197. package/packs/software-delivery/src/schemas/index.ts +0 -5
  198. package/packs/software-delivery/src/state/date-utils.ts +0 -158
  199. package/packs/software-delivery/src/state/index.ts +0 -15
  200. package/packs/software-delivery/src/state/state-machine.ts +0 -119
  201. package/packs/software-delivery/src/state/wu-doc-types.ts +0 -51
  202. package/packs/software-delivery/src/state/wu-paths.ts +0 -381
  203. package/packs/software-delivery/src/state/wu-schema.ts +0 -1139
  204. package/packs/software-delivery/src/state/wu-state-schema.ts +0 -255
  205. package/packs/software-delivery/src/state/wu-yaml.ts +0 -338
  206. package/packs/software-delivery/tool-impl/agent-tools.ts +0 -263
  207. package/packs/software-delivery/tool-impl/delegation-tools.ts +0 -66
  208. package/packs/software-delivery/tool-impl/flow-metrics-tools.ts +0 -219
  209. package/packs/software-delivery/tool-impl/git-runner.ts +0 -113
  210. package/packs/software-delivery/tool-impl/git-tools.ts +0 -316
  211. package/packs/software-delivery/tool-impl/index.ts +0 -15
  212. package/packs/software-delivery/tool-impl/initiative-orchestration-tools.ts +0 -720
  213. package/packs/software-delivery/tool-impl/lane-lock.ts +0 -246
  214. package/packs/software-delivery/tool-impl/memory-tools.ts +0 -470
  215. package/packs/software-delivery/tool-impl/pending-runtime-tools.ts +0 -21
  216. package/packs/software-delivery/tool-impl/runtime-cli-adapter.ts +0 -329
  217. package/packs/software-delivery/tool-impl/runtime-native-tools.ts +0 -687
  218. package/packs/software-delivery/tool-impl/worker-loader.ts +0 -52
  219. package/packs/software-delivery/tool-impl/worktree-tools.ts +0 -46
  220. package/packs/software-delivery/tool-impl/wu-lifecycle-tools.ts +0 -807
  221. package/packs/software-delivery/tools/delegation-tools.ts +0 -23
  222. package/packs/software-delivery/tools/git-tools.ts +0 -55
  223. package/packs/software-delivery/tools/index.ts +0 -8
  224. package/packs/software-delivery/tools/lane-lock-tool.ts +0 -37
  225. package/packs/software-delivery/tools/types.ts +0 -71
  226. package/packs/software-delivery/tools/worktree-tools.ts +0 -49
  227. package/packs/software-delivery/vitest.config.ts +0 -11
@@ -1,101 +0,0 @@
1
- // Copyright (c) 2026 Hellmai Ltd
2
- // SPDX-License-Identifier: LicenseRef-LumenFlow-Proprietary
3
-
4
- /**
5
- * WU-2737 (INIT-060 WU-7b, ADR-013 §ChannelBridge):
6
- * Local outbox for queue-kind envelopes when the cloud is unreachable.
7
- *
8
- * Backing layout:
9
- * <outboxDir>/<channel-id>/<timestamp>-<envelope-id>.json
10
- *
11
- * Each file is a JSON blob `{ channel_id, envelope }` matching the request
12
- * body shape published in `channel-bridge-cloud-stub.md`. FIFO ordering comes
13
- * from the sorted filename (lexicographic on timestamp + envelope id).
14
- *
15
- * The outbox is intentionally dumb: no retry state, no backoff, no watcher.
16
- * The adapter drives drains explicitly via `flush()` or reactively on the
17
- * next successful POST; ADR-013 §4 treats the outbox as a passive spool.
18
- */
19
-
20
- import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
21
- import path from 'node:path';
22
-
23
- import type { ChannelEnvelope } from '../domain/channel.types.js';
24
-
25
- export interface OutboxRecord {
26
- channel_id: string;
27
- envelope: ChannelEnvelope;
28
- /** Internal filename so the adapter can remove the entry after a successful replay. */
29
- _filename: string;
30
- }
31
-
32
- /**
33
- * Seconds-precision epoch plus a short random suffix keeps filenames sortable
34
- * FIFO while avoiding collisions when two envelopes enqueue in the same
35
- * millisecond.
36
- */
37
- function mintFilename(envelopeId: string): string {
38
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
39
- const safeId = envelopeId.replace(/[^a-zA-Z0-9_-]/g, '_');
40
- const suffix = Math.random().toString(36).slice(2, 8);
41
- return `${ts}-${safeId}-${suffix}.json`;
42
- }
43
-
44
- export class CloudOutbox {
45
- constructor(private readonly outboxDir: string) {}
46
-
47
- async enqueue(channelId: string, envelope: ChannelEnvelope): Promise<void> {
48
- const dir = path.resolve(this.outboxDir);
49
- await mkdir(dir, { recursive: true });
50
- const filename = mintFilename(envelope.id);
51
- const tmpPath = path.join(dir, `.tmp-${process.pid}-${filename}`);
52
- const finalPath = path.join(dir, filename);
53
- const payload = JSON.stringify({ channel_id: channelId, envelope });
54
- await writeFile(tmpPath, payload, 'utf8');
55
- await rename(tmpPath, finalPath);
56
- }
57
-
58
- /**
59
- * Lists outbox records in FIFO order. The returned filename is opaque; pass
60
- * it back to `remove()` after a successful replay.
61
- */
62
- async list(): Promise<OutboxRecord[]> {
63
- const dir = path.resolve(this.outboxDir);
64
- let entries: string[];
65
- try {
66
- entries = await readdir(dir);
67
- } catch (error) {
68
- const err = error as NodeJS.ErrnoException;
69
- if (err.code === 'ENOENT') {
70
- return [];
71
- }
72
- throw error;
73
- }
74
- const records: OutboxRecord[] = [];
75
- const sortedEntries = entries
76
- .filter((e) => e.endsWith('.json') && !e.startsWith('.tmp-'))
77
- .sort();
78
- for (const name of sortedEntries) {
79
- const full = path.join(dir, name);
80
- try {
81
- const raw = await readFile(full, 'utf8');
82
- const parsed = JSON.parse(raw) as { channel_id: string; envelope: ChannelEnvelope };
83
- records.push({ ...parsed, _filename: name });
84
- } catch {
85
- // Corrupt or partial file — skip (will not block FIFO drain).
86
- continue;
87
- }
88
- }
89
- return records;
90
- }
91
-
92
- async remove(filename: string): Promise<void> {
93
- const full = path.join(path.resolve(this.outboxDir), filename);
94
- await rm(full, { force: true });
95
- }
96
-
97
- async isEmpty(): Promise<boolean> {
98
- const records = await this.list();
99
- return records.length === 0;
100
- }
101
- }
@@ -1,386 +0,0 @@
1
- // Copyright (c) 2026 Hellmai Ltd
2
- // SPDX-License-Identifier: LicenseRef-LumenFlow-Proprietary
3
-
4
- /**
5
- * WU-2737 (INIT-060 WU-7b, ADR-013 §ChannelBridge):
6
- * Control-plane ChannelBridge adapter.
7
- *
8
- * Cloud-backed implementation of the ChannelBridge port. Codes directly
9
- * against the contract published in
10
- * `docs/operations/coordination/channel-bridge-cloud-stub.md` (WU-2736):
11
- *
12
- * POST /api/v1/workspaces/{workspace_id}/sidekick/channel
13
- *
14
- * Responsibilities:
15
- * - Outbound `send()` POSTs each envelope with `Bearer <enrollment_token>`.
16
- * - At-least-once delivery scoped by `(channel_id, envelope.id)` dedup key;
17
- * safe to replay on transport failure (cloud returns `deduped: true`).
18
- * - Backpressure split (ADR-013 §4):
19
- * - `kind: "ephemeral"` fails silently on 5xx / 429 / network errors
20
- * (no outbox, no retry, no elevated logging).
21
- * - `kind: "queue"` parks in the local outbox on any non-2xx and drains
22
- * FIFO on reconnect / explicit `flush()` / `disconnect()`.
23
- * - Inbound `receive()` drains a loopback-friendly poll endpoint. The stub
24
- * contract (WU-2736) does not yet freeze the inbound wire shape; the
25
- * adapter exposes `pollIntervalMs` + a minimal GET fetcher so a mock
26
- * server or future coord-stub-signed inbound path can plug in without
27
- * changing the port.
28
- * - `register()` is idempotent on BridgeConfig identity (deterministic id
29
- * from provider/name/options hash — matches the filesystem adapter pattern).
30
- * - `disconnect()` flushes queued envelopes before resolving; subsequent
31
- * `send()` on the same channel id rejects (port contract).
32
- */
33
-
34
- import { createHash } from 'node:crypto';
35
- import { setTimeout as delay } from 'node:timers/promises';
36
-
37
- import type {
38
- BridgeConfig,
39
- ChannelEnvelope,
40
- ChannelId,
41
- SendResult,
42
- } from '../domain/channel.types.js';
43
- import type { ChannelBridge } from '../ports/channel-bridge.port.js';
44
-
45
- import { CloudOutbox } from './cloud-queue.js';
46
-
47
- // ---------------------------------------------------------------------------
48
- // Configuration surface
49
- // ---------------------------------------------------------------------------
50
-
51
- export interface ControlPlaneChannelBridgeOptions {
52
- /** Cloud API base URL, e.g. `https://app.lumenflow.dev` (no trailing slash). */
53
- baseUrl: string;
54
- /** Workspace id — path parameter on the POST route. */
55
- workspaceId: string;
56
- /**
57
- * Returns the current enrollment token. Called on every request so token
58
- * rotation (re-auth) is transparent to callers.
59
- */
60
- tokenProvider: () => Promise<string>;
61
- /** Local outbox root for queue-kind envelopes. */
62
- outboxDir: string;
63
- /** Polling interval for `receive()`. Defaults to 1000ms. */
64
- pollIntervalMs?: number;
65
- /**
66
- * Optional override for `fetch`. Primarily a test seam; production wiring
67
- * uses the platform global `fetch`.
68
- */
69
- fetchImpl?: typeof fetch;
70
- }
71
-
72
- /**
73
- * `ChannelBridge` extended with `flush()` so operators (and the disconnect
74
- * path) can explicitly drain the outbox. The port contract itself does NOT
75
- * require `flush`; it's an adapter affordance.
76
- */
77
- export interface ControlPlaneChannelBridge extends ChannelBridge {
78
- flush(): Promise<void>;
79
- }
80
-
81
- // ---------------------------------------------------------------------------
82
- // Internal helpers
83
- // ---------------------------------------------------------------------------
84
-
85
- const DEFAULT_POLL_INTERVAL_MS = 1000;
86
- const QUEUED_REASON = 'queued_for_replay';
87
- const DROPPED_EPHEMERAL_REASON = 'dropped_ephemeral';
88
- const INVALID_ENVELOPE_REASON = 'invalid_envelope';
89
- const DISCONNECTED_REASON = 'disconnected';
90
-
91
- interface CloudSuccessBody {
92
- accepted?: boolean;
93
- delivery_id?: string;
94
- deduped?: boolean;
95
- trace_id?: string;
96
- reason?: string;
97
- }
98
-
99
- interface CloudErrorBody {
100
- error?: {
101
- code?: string;
102
- message?: string;
103
- retryable?: boolean;
104
- trace_id?: string;
105
- };
106
- }
107
-
108
- function hashOptions(options: Record<string, unknown> | undefined): string {
109
- const payload = JSON.stringify(options ?? {}, Object.keys(options ?? {}).sort());
110
- return createHash('sha256').update(payload).digest('hex').slice(0, 16);
111
- }
112
-
113
- function mintChannelId(config: BridgeConfig): ChannelId {
114
- const payload = `${config.provider}::${config.name}::${hashOptions(
115
- config.options as Record<string, unknown> | undefined,
116
- )}`;
117
- const digest = createHash('sha256').update(payload).digest('hex').slice(0, 24);
118
- return `chan-${digest}` as ChannelId;
119
- }
120
-
121
- function safeParseJson<T>(text: string): T | null {
122
- try {
123
- return JSON.parse(text) as T;
124
- } catch {
125
- return null;
126
- }
127
- }
128
-
129
- // ---------------------------------------------------------------------------
130
- // Adapter factory
131
- // ---------------------------------------------------------------------------
132
-
133
- export function createControlPlaneChannelBridge(
134
- options: ControlPlaneChannelBridgeOptions,
135
- ): ControlPlaneChannelBridge {
136
- const baseUrl = options.baseUrl.replace(/\/+$/, '');
137
- const workspaceId = options.workspaceId;
138
- const tokenProvider = options.tokenProvider;
139
- const outbox = new CloudOutbox(options.outboxDir);
140
- const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
141
- const fetchImpl = options.fetchImpl ?? fetch;
142
-
143
- const registry = new Map<string, ChannelId>();
144
- const disconnected = new Set<ChannelId>();
145
-
146
- function postUrl(): string {
147
- return `${baseUrl}/api/v1/workspaces/${encodeURIComponent(workspaceId)}/sidekick/channel`;
148
- }
149
-
150
- function inboundUrl(channelId: ChannelId): string {
151
- return `${postUrl()}?channel_id=${encodeURIComponent(channelId)}`;
152
- }
153
-
154
- async function authHeaders(): Promise<Record<string, string>> {
155
- const token = await tokenProvider();
156
- return {
157
- 'Content-Type': 'application/json',
158
- Authorization: `Bearer ${token}`,
159
- };
160
- }
161
-
162
- /**
163
- * One attempt at a single POST. Returns a discriminated result so the
164
- * caller decides what to do with retryable / non-retryable outcomes.
165
- */
166
- type PostOutcome =
167
- | { kind: 'accepted'; body: CloudSuccessBody }
168
- | {
169
- kind: 'retryable';
170
- status: number;
171
- error: CloudErrorBody['error'];
172
- retryAfterSeconds?: number;
173
- }
174
- | { kind: 'non_retryable'; status: number; error: CloudErrorBody['error'] }
175
- | { kind: 'network_error'; cause: unknown };
176
-
177
- async function postOnce(channelId: ChannelId, envelope: ChannelEnvelope): Promise<PostOutcome> {
178
- let response: Response;
179
- try {
180
- response = await fetchImpl(postUrl(), {
181
- method: 'POST',
182
- headers: await authHeaders(),
183
- body: JSON.stringify({ channel_id: channelId, envelope }),
184
- });
185
- } catch (error) {
186
- return { kind: 'network_error', cause: error };
187
- }
188
-
189
- const text = await response.text();
190
- if (response.ok) {
191
- const body = safeParseJson<CloudSuccessBody>(text) ?? {};
192
- return { kind: 'accepted', body };
193
- }
194
-
195
- const errorBody = safeParseJson<CloudErrorBody>(text) ?? {};
196
- const retryAfterHeader = response.headers.get('retry-after');
197
- const retryAfterSeconds = retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : undefined;
198
-
199
- // 4xx non-retryable per contract §Error Response Shape — everything
200
- // other than 429 is a correctness bug / auth failure we don't retry.
201
- if (response.status >= 400 && response.status < 500 && response.status !== 429) {
202
- return { kind: 'non_retryable', status: response.status, error: errorBody.error };
203
- }
204
-
205
- return {
206
- kind: 'retryable',
207
- status: response.status,
208
- error: errorBody.error,
209
- retryAfterSeconds: Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined,
210
- };
211
- }
212
-
213
- function nonRetryableReason(error: CloudErrorBody['error']): string {
214
- // Contract codes map 1:1 onto SendResult.reason for observability. Unknown
215
- // codes collapse into a stable catch-all so audit logs stay readable.
216
- const code = error?.code;
217
- if (code === 'invalid_envelope') return INVALID_ENVELOPE_REASON;
218
- if (code) return code;
219
- return 'rejected_non_retryable';
220
- }
221
-
222
- async function drainOutbox(): Promise<void> {
223
- const pending = await outbox.list();
224
- for (const record of pending) {
225
- // Cast: outbox entries are keyed by the same branded ids we minted.
226
- const cid = record.channel_id as ChannelId;
227
- const outcome = await postOnce(cid, record.envelope);
228
- if (outcome.kind === 'accepted') {
229
- await outbox.remove(record._filename);
230
- continue;
231
- }
232
- if (outcome.kind === 'non_retryable') {
233
- // Correctness bug on a parked envelope — drop it so the outbox does
234
- // not grow unbounded on permanent errors (matches contract table:
235
- // `invalid_envelope` / `workspace_mismatch` are log-and-drop).
236
- await outbox.remove(record._filename);
237
- continue;
238
- }
239
- // Retryable / network error — stop the drain; caller retries later.
240
- return;
241
- }
242
- }
243
-
244
- async function sendInternal(
245
- channelId: ChannelId,
246
- envelope: ChannelEnvelope,
247
- ): Promise<SendResult> {
248
- // Opportunistic drain: a successful path clears any previous parked
249
- // envelopes before the new one POSTs, preserving FIFO order under the
250
- // "replay-on-reconnect" rule (§4).
251
- if (envelope.kind === 'queue') {
252
- await drainOutbox();
253
- }
254
-
255
- const outcome = await postOnce(channelId, envelope);
256
-
257
- if (outcome.kind === 'accepted') {
258
- const body = outcome.body;
259
- const result: SendResult = {
260
- accepted: body.accepted ?? true,
261
- };
262
- if (body.delivery_id !== undefined) {
263
- result.delivery_id = body.delivery_id;
264
- }
265
- if (body.deduped === true) {
266
- result.deduped = true;
267
- }
268
- if (body.reason !== undefined) {
269
- result.reason = body.reason;
270
- }
271
- return result;
272
- }
273
-
274
- if (outcome.kind === 'non_retryable') {
275
- // Both kinds: log-and-drop per §Error Response Shape.
276
- return {
277
- accepted: false,
278
- reason: nonRetryableReason(outcome.error),
279
- };
280
- }
281
-
282
- // Retryable (5xx / 429) OR network error: split by envelope.kind.
283
- if (envelope.kind === 'ephemeral') {
284
- // Fail-silent per §4; no outbox, no throw.
285
- return { accepted: false, reason: DROPPED_EPHEMERAL_REASON };
286
- }
287
-
288
- // queue: park for replay.
289
- await outbox.enqueue(channelId, envelope);
290
- return { accepted: false, reason: QUEUED_REASON };
291
- }
292
-
293
- // -------------------------------------------------------------------------
294
- // Port methods
295
- // -------------------------------------------------------------------------
296
-
297
- async function registerInternal(bridgeConfig: BridgeConfig): Promise<ChannelId> {
298
- const key = `${bridgeConfig.provider}::${bridgeConfig.name}::${hashOptions(
299
- bridgeConfig.options as Record<string, unknown> | undefined,
300
- )}`;
301
- const existing = registry.get(key);
302
- if (existing) {
303
- return existing;
304
- }
305
- const id = mintChannelId(bridgeConfig);
306
- registry.set(key, id);
307
- return id;
308
- }
309
-
310
- return {
311
- async connect(bridgeConfig: BridgeConfig): Promise<ChannelId> {
312
- return registerInternal(bridgeConfig);
313
- },
314
-
315
- async register(bridgeConfig: BridgeConfig): Promise<ChannelId> {
316
- return registerInternal(bridgeConfig);
317
- },
318
-
319
- async send(channelId: ChannelId, envelope: ChannelEnvelope): Promise<SendResult> {
320
- if (disconnected.has(channelId)) {
321
- throw new Error(
322
- `ControlPlaneChannelBridge: channel ${channelId} is disconnected; send rejected (reason=${DISCONNECTED_REASON}).`,
323
- );
324
- }
325
- return sendInternal(channelId, envelope);
326
- },
327
-
328
- async *receive(channelId: ChannelId): AsyncIterable<ChannelEnvelope> {
329
- // Poll-based inbound. One pass drains whatever is currently queued on
330
- // the cloud side; the loop continues until `disconnect()` flips the
331
- // flag so the iterator terminates without hanging (port contract).
332
- while (!disconnected.has(channelId)) {
333
- let response: Response;
334
- try {
335
- response = await fetchImpl(inboundUrl(channelId), {
336
- method: 'GET',
337
- headers: await authHeaders(),
338
- });
339
- } catch {
340
- // Transient network error — back off and retry.
341
- await delay(pollIntervalMs);
342
- continue;
343
- }
344
-
345
- if (!response.ok) {
346
- // 4xx/5xx on poll: back off. Errors here do NOT terminate the
347
- // iterator; the port contract treats the iterator as long-lived
348
- // until `disconnect()`.
349
- await delay(pollIntervalMs);
350
- continue;
351
- }
352
-
353
- const text = await response.text();
354
- const body = safeParseJson<{ envelopes?: ChannelEnvelope[] }>(text) ?? {};
355
- const envelopes = body.envelopes ?? [];
356
-
357
- for (const env of envelopes) {
358
- if (disconnected.has(channelId)) {
359
- return;
360
- }
361
- yield env;
362
- }
363
-
364
- if (envelopes.length === 0) {
365
- await delay(pollIntervalMs);
366
- }
367
- }
368
- },
369
-
370
- async disconnect(channelId: ChannelId): Promise<void> {
371
- // Flush outbox before closing so queued envelopes for any channel have
372
- // a chance to drain (port contract: "queued envelopes flush before
373
- // this resolves").
374
- try {
375
- await drainOutbox();
376
- } catch {
377
- // Drain best-effort; disconnect must always resolve.
378
- }
379
- disconnected.add(channelId);
380
- },
381
-
382
- async flush(): Promise<void> {
383
- await drainOutbox();
384
- },
385
- };
386
- }