wicked-crew 0.5.0 → 0.7.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 (167) hide show
  1. package/dist/api/audit.d.ts +70 -0
  2. package/dist/api/audit.d.ts.map +1 -0
  3. package/dist/api/audit.js +131 -0
  4. package/dist/api/audit.js.map +1 -0
  5. package/dist/api/auth.d.ts +192 -0
  6. package/dist/api/auth.d.ts.map +1 -0
  7. package/dist/api/auth.js +515 -0
  8. package/dist/api/auth.js.map +1 -0
  9. package/dist/api/gate-cache.d.ts +8 -0
  10. package/dist/api/gate-cache.d.ts.map +1 -1
  11. package/dist/api/gate-cache.js +10 -0
  12. package/dist/api/gate-cache.js.map +1 -1
  13. package/dist/api/guidance-index.d.ts +39 -0
  14. package/dist/api/guidance-index.d.ts.map +1 -0
  15. package/dist/api/guidance-index.js +67 -0
  16. package/dist/api/guidance-index.js.map +1 -0
  17. package/dist/api/open-path.d.ts +34 -0
  18. package/dist/api/open-path.d.ts.map +1 -0
  19. package/dist/api/open-path.js +101 -0
  20. package/dist/api/open-path.js.map +1 -0
  21. package/dist/api/retry-index.d.ts +30 -0
  22. package/dist/api/retry-index.d.ts.map +1 -0
  23. package/dist/api/retry-index.js +45 -0
  24. package/dist/api/retry-index.js.map +1 -0
  25. package/dist/api/routes.d.ts +164 -1
  26. package/dist/api/routes.d.ts.map +1 -1
  27. package/dist/api/routes.js +769 -21
  28. package/dist/api/routes.js.map +1 -1
  29. package/dist/api/run-files.d.ts +63 -0
  30. package/dist/api/run-files.d.ts.map +1 -0
  31. package/dist/api/run-files.js +271 -0
  32. package/dist/api/run-files.js.map +1 -0
  33. package/dist/api/seat-health.d.ts +55 -0
  34. package/dist/api/seat-health.d.ts.map +1 -0
  35. package/dist/api/seat-health.js +273 -0
  36. package/dist/api/seat-health.js.map +1 -0
  37. package/dist/api/seat-signin.d.ts +27 -0
  38. package/dist/api/seat-signin.d.ts.map +1 -0
  39. package/dist/api/seat-signin.js +143 -0
  40. package/dist/api/seat-signin.js.map +1 -0
  41. package/dist/api/server.d.ts +190 -3
  42. package/dist/api/server.d.ts.map +1 -1
  43. package/dist/api/server.js +365 -11
  44. package/dist/api/server.js.map +1 -1
  45. package/dist/api/stall-watchdog.d.ts +62 -0
  46. package/dist/api/stall-watchdog.d.ts.map +1 -0
  47. package/dist/api/stall-watchdog.js +138 -0
  48. package/dist/api/stall-watchdog.js.map +1 -0
  49. package/dist/cli/index.js +171 -12
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/cli/mcp.d.ts +14 -0
  52. package/dist/cli/mcp.d.ts.map +1 -0
  53. package/dist/cli/mcp.js +119 -0
  54. package/dist/cli/mcp.js.map +1 -0
  55. package/dist/core/adapter.d.ts +87 -10
  56. package/dist/core/adapter.d.ts.map +1 -1
  57. package/dist/core/adapter.js +347 -28
  58. package/dist/core/adapter.js.map +1 -1
  59. package/dist/core/bridge-reaper.d.ts +134 -0
  60. package/dist/core/bridge-reaper.d.ts.map +1 -0
  61. package/dist/core/bridge-reaper.js +286 -0
  62. package/dist/core/bridge-reaper.js.map +1 -0
  63. package/dist/core/deliver.d.ts +118 -0
  64. package/dist/core/deliver.d.ts.map +1 -0
  65. package/dist/core/deliver.js +241 -0
  66. package/dist/core/deliver.js.map +1 -0
  67. package/dist/core/deliverable-floor.d.ts +103 -0
  68. package/dist/core/deliverable-floor.d.ts.map +1 -0
  69. package/dist/core/deliverable-floor.js +173 -0
  70. package/dist/core/deliverable-floor.js.map +1 -0
  71. package/dist/core/exec.d.ts +2 -0
  72. package/dist/core/exec.d.ts.map +1 -1
  73. package/dist/core/exec.js.map +1 -1
  74. package/dist/core/types.d.ts +92 -352
  75. package/dist/core/types.d.ts.map +1 -1
  76. package/dist/core/types.js +14 -4
  77. package/dist/core/types.js.map +1 -1
  78. package/dist/interactive/bridge-pool.d.ts +99 -0
  79. package/dist/interactive/bridge-pool.d.ts.map +1 -0
  80. package/dist/interactive/bridge-pool.js +244 -0
  81. package/dist/interactive/bridge-pool.js.map +1 -0
  82. package/dist/interactive/bridge-root.d.ts +36 -0
  83. package/dist/interactive/bridge-root.d.ts.map +1 -0
  84. package/dist/interactive/bridge-root.js +48 -0
  85. package/dist/interactive/bridge-root.js.map +1 -0
  86. package/dist/interactive/chat-events.d.ts +207 -0
  87. package/dist/interactive/chat-events.d.ts.map +1 -0
  88. package/dist/interactive/chat-events.js +769 -0
  89. package/dist/interactive/chat-events.js.map +1 -0
  90. package/dist/interactive/demo-events.d.ts +283 -0
  91. package/dist/interactive/demo-events.d.ts.map +1 -0
  92. package/dist/interactive/demo-events.js +889 -0
  93. package/dist/interactive/demo-events.js.map +1 -0
  94. package/dist/interactive/draft-events.d.ts +224 -0
  95. package/dist/interactive/draft-events.d.ts.map +1 -0
  96. package/dist/interactive/draft-events.js +793 -0
  97. package/dist/interactive/draft-events.js.map +1 -0
  98. package/dist/interactive/edit-events.d.ts +194 -0
  99. package/dist/interactive/edit-events.d.ts.map +1 -0
  100. package/dist/interactive/edit-events.js +601 -0
  101. package/dist/interactive/edit-events.js.map +1 -0
  102. package/dist/interactive/ledger.d.ts +39 -0
  103. package/dist/interactive/ledger.d.ts.map +1 -0
  104. package/dist/interactive/ledger.js +93 -0
  105. package/dist/interactive/ledger.js.map +1 -0
  106. package/dist/interactive/proxy-routes.d.ts +39 -0
  107. package/dist/interactive/proxy-routes.d.ts.map +1 -0
  108. package/dist/interactive/proxy-routes.js +189 -0
  109. package/dist/interactive/proxy-routes.js.map +1 -0
  110. package/dist/interactive/repo-snapshot.d.ts +100 -0
  111. package/dist/interactive/repo-snapshot.d.ts.map +1 -0
  112. package/dist/interactive/repo-snapshot.js +289 -0
  113. package/dist/interactive/repo-snapshot.js.map +1 -0
  114. package/dist/interactive/ws-relay.d.ts +85 -0
  115. package/dist/interactive/ws-relay.d.ts.map +1 -0
  116. package/dist/interactive/ws-relay.js +191 -0
  117. package/dist/interactive/ws-relay.js.map +1 -0
  118. package/dist/projects/activity.d.ts +29 -0
  119. package/dist/projects/activity.d.ts.map +1 -0
  120. package/dist/projects/activity.js +172 -0
  121. package/dist/projects/activity.js.map +1 -0
  122. package/dist/projects/charter.d.ts +28 -0
  123. package/dist/projects/charter.d.ts.map +1 -0
  124. package/dist/projects/charter.js +53 -0
  125. package/dist/projects/charter.js.map +1 -0
  126. package/dist/projects/events.d.ts +55 -0
  127. package/dist/projects/events.d.ts.map +1 -0
  128. package/dist/projects/events.js +141 -0
  129. package/dist/projects/events.js.map +1 -0
  130. package/dist/projects/graph-paths.d.ts +92 -0
  131. package/dist/projects/graph-paths.d.ts.map +1 -0
  132. package/dist/projects/graph-paths.js +130 -0
  133. package/dist/projects/graph-paths.js.map +1 -0
  134. package/dist/projects/graph.d.ts +179 -0
  135. package/dist/projects/graph.d.ts.map +1 -0
  136. package/dist/projects/graph.js +775 -0
  137. package/dist/projects/graph.js.map +1 -0
  138. package/dist/projects/membership-index.d.ts +25 -0
  139. package/dist/projects/membership-index.d.ts.map +1 -0
  140. package/dist/projects/membership-index.js +46 -0
  141. package/dist/projects/membership-index.js.map +1 -0
  142. package/dist/projects/routes.d.ts +97 -0
  143. package/dist/projects/routes.d.ts.map +1 -0
  144. package/dist/projects/routes.js +510 -0
  145. package/dist/projects/routes.js.map +1 -0
  146. package/dist/projects/settings.d.ts +32 -0
  147. package/dist/projects/settings.d.ts.map +1 -0
  148. package/dist/projects/settings.js +64 -0
  149. package/dist/projects/settings.js.map +1 -0
  150. package/dist/qe/acceptance.d.ts +137 -0
  151. package/dist/qe/acceptance.d.ts.map +1 -0
  152. package/dist/qe/acceptance.js +249 -0
  153. package/dist/qe/acceptance.js.map +1 -0
  154. package/dist/qe/gate-events.d.ts +111 -0
  155. package/dist/qe/gate-events.d.ts.map +1 -0
  156. package/dist/qe/gate-events.js +168 -0
  157. package/dist/qe/gate-events.js.map +1 -0
  158. package/dist/qe/ledger.d.ts +100 -0
  159. package/dist/qe/ledger.d.ts.map +1 -0
  160. package/dist/qe/ledger.js +154 -0
  161. package/dist/qe/ledger.js.map +1 -0
  162. package/dist/studio/assets/index-8p8uwCxG.js +530 -0
  163. package/dist/studio/assets/index-D6S9zUtO.css +32 -0
  164. package/dist/studio/index.html +5 -3
  165. package/package.json +10 -4
  166. package/dist/studio/assets/index-DaaUU8Ep.css +0 -32
  167. package/dist/studio/assets/index-Fu5DRC00.js +0 -423
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Launch-scoped REPO SNAPSHOTS for grounding governed interactive runs (CREW-UX-8 v4).
3
+ *
4
+ * WHY a snapshot instead of the live repo: the interactive seams launch their governed runs
5
+ * UNBOUND — on a repoRef-bound run the worker's ACP tool-permission stream closes on the first
6
+ * call that needs a permission prompt and the session dies (wicked-core#293) — and an unbound
7
+ * worker's governance boundary is {sandbox, extraWriteRoots, ~/.claude/plugins}, so even READS
8
+ * of the live repo root are governance-denied (wicked-core#294; the v3 "hand the worker the
9
+ * root path to read" design rested on read evidence gathered under a *bound* boundary, which
10
+ * does not transfer). The one location an unbound worker can always read is the inbox the run
11
+ * already writes to — write roots are readable (wicked-core#259) — so grounding means putting
12
+ * a copy of the repo INSIDE the inbox, crew-side, BEFORE the launch, and naming that snapshot
13
+ * in the task.
14
+ *
15
+ * Mechanism, in preference order:
16
+ * 1. `git clone --depth 1 --no-hardlinks file://<root> <dest>` — a shallow LOCAL clone:
17
+ * one commit of history, `--no-hardlinks` so the snapshot never aliases the live object
18
+ * store. Because a clone checks out EVERY tracked path, the result is then swept: tracked
19
+ * `node_modules` trees and every symlink are removed (see below), and the swept snapshot
20
+ * is re-measured against the budget before it counts as usable.
21
+ * 2. A plain file copy that skips `.git`/`node_modules` AND symlinks — for roots that are not
22
+ * git repos (or where git itself is unavailable/failing).
23
+ *
24
+ * SYMLINKS NEVER SURVIVE into a snapshot, on either path (Copilot, crew#313): the snapshot
25
+ * lands inside the worker-visible inbox, so a repo symlink pointing OUTSIDE the repo would
26
+ * hand the worker readable reach beyond the governance boundary — and a link to a huge tree
27
+ * would bypass the size cap the preflight walk (which skips links) enforced. The copy filter
28
+ * refuses them and the post-clone sweep deletes the ones a clone checked out.
29
+ *
30
+ * The snapshot is CAPPED: a working tree whose (`.git`/`node_modules`/symlink-excluded) size
31
+ * exceeds the budget is not snapshotted at all — the caller degrades honestly to an ungrounded
32
+ * launch with a visible status note, never a truncated half-repo the worker would mistake for
33
+ * the whole. Snapshots are launch-scoped: the seams remove them when the run reaches a
34
+ * terminal state (finalize/failure folds), so the inbox never accretes dead clones.
35
+ *
36
+ * OVERLAP REFUSAL: a configured draft dir could place `dest` inside the live repo (or a repo
37
+ * could be registered AT the inbox). Clearing a stale dest with `rm -rf` before noticing that
38
+ * would DELETE live repository content — so source/dest containment is checked (realpath'd,
39
+ * both directions) BEFORE anything is removed, and any overlap refuses the snapshot outright.
40
+ * For the same reason THIS helper creates `dest`'s parent directory (after the check passes) —
41
+ * a caller that pre-created it would already have written into the live repo (Copilot round 2).
42
+ *
43
+ * Fully ASYNC on purpose: this runs inline in a bus handler whose process also feeds ~15s UI
44
+ * heartbeats — a synchronous clone/copy of a big repo would starve the event loop and the
45
+ * canvas would read frozen (Copilot, crew#313). All I/O is fs/promises; the clone goes through
46
+ * the daemon's capped exec chokepoint (`execCapped`, FINDING-016).
47
+ */
48
+ import { cp, lstat, mkdir, readdir, realpath, rm, stat } from 'node:fs/promises';
49
+ import path, { basename, dirname, join, resolve } from 'node:path';
50
+ import { pathToFileURL } from 'node:url';
51
+ import { execCapped } from '../core/exec.js';
52
+ /** Default snapshot budget (~200MB). A repo whose working tree exceeds this is not
53
+ * snapshotted — the launch proceeds ungrounded, honestly narrated. */
54
+ export const REPO_SNAPSHOT_MAX_BYTES = 200 * 1024 * 1024;
55
+ /** Never snapshotted: git's object store (the clone brings its own shallow one; the copy
56
+ * fallback wants working files only) and dependency trees (huge, reproducible, ungrounding). */
57
+ const SNAPSHOT_SKIP = new Set(['.git', 'node_modules']);
58
+ /** `true` when the tree under `root` exceeds `cap` bytes, skipping {@link SNAPSHOT_SKIP} dirs
59
+ * and symlinks (a link to a huge tree — or out of the repo — must not distort the estimate;
60
+ * the copy filter and post-clone sweep exclude exactly the same set, so the preflight counts
61
+ * what a snapshot would actually hold), with an early exit the moment the cap is passed so a
62
+ * 10GB monorepo is never fully walked just to learn it is too big. */
63
+ async function treeExceeds(root, cap) {
64
+ let total = 0;
65
+ const stack = [root];
66
+ while (stack.length > 0) {
67
+ const dir = stack.pop();
68
+ let entries;
69
+ try {
70
+ entries = await readdir(dir, { withFileTypes: true });
71
+ }
72
+ catch {
73
+ continue; // unreadable subdir — the clone/copy will surface a real error if it matters
74
+ }
75
+ for (const entry of entries) {
76
+ if (entry.isSymbolicLink())
77
+ continue;
78
+ if (entry.isDirectory()) {
79
+ if (!SNAPSHOT_SKIP.has(entry.name))
80
+ stack.push(join(dir, entry.name));
81
+ }
82
+ else if (entry.isFile()) {
83
+ try {
84
+ total += (await stat(join(dir, entry.name))).size;
85
+ }
86
+ catch {
87
+ // raced deletion — ignore
88
+ }
89
+ if (total > cap)
90
+ return true;
91
+ }
92
+ }
93
+ }
94
+ return false;
95
+ }
96
+ /** Post-clone sweep: delete every symlink (junctions included — boundary escape, cap bypass)
97
+ * and every {@link SNAPSHOT_SKIP} tree a `git clone` checked out (a clone materializes ALL
98
+ * tracked paths — tracked `node_modules` ignores the skip list otherwise). The clone's own
99
+ * top-level `.git` (its shallow object store) is the one deliberate survivor. */
100
+ async function sweepSnapshot(root) {
101
+ const stack = [root];
102
+ while (stack.length > 0) {
103
+ const dir = stack.pop();
104
+ let entries;
105
+ try {
106
+ entries = await readdir(dir, { withFileTypes: true });
107
+ }
108
+ catch {
109
+ continue;
110
+ }
111
+ for (const entry of entries) {
112
+ const p = join(dir, entry.name);
113
+ if (entry.isSymbolicLink()) {
114
+ await rm(p, { recursive: true, force: true });
115
+ }
116
+ else if (entry.isDirectory()) {
117
+ if (dir === root && entry.name === '.git')
118
+ continue; // the shallow clone's own store
119
+ if (SNAPSHOT_SKIP.has(entry.name)) {
120
+ await rm(p, { recursive: true, force: true });
121
+ }
122
+ else {
123
+ stack.push(p);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ /** Resolve `p` through its nearest EXISTING ancestor's realpath (the path itself may not exist
130
+ * yet — a fresh dest usually does not), so symlinked parents cannot hide an overlap. Only
131
+ * ENOENT falls to the ancestor walk (the `api/open-path.ts` rule, Copilot round 2): any OTHER
132
+ * error (EACCES, ELOOP, …) means an EXISTING component could not be resolved — a lexical
133
+ * reconstruction there could miss an overlap through the unresolved link, so it THROWS and
134
+ * the caller's overlap-check catch fails closed (no snapshot, degrade). */
135
+ async function realpathNearest(p) {
136
+ let existing = resolve(p);
137
+ const tail = [];
138
+ // Walk up until something exists; dirname() at the root returns itself, which always exists.
139
+ for (;;) {
140
+ try {
141
+ const real = await realpath(existing);
142
+ return tail.length > 0 ? join(real, ...tail) : real;
143
+ }
144
+ catch (err) {
145
+ if (err.code !== 'ENOENT')
146
+ throw err;
147
+ const parent = dirname(existing);
148
+ if (parent === existing)
149
+ return existing; // unreachable in practice — a root always resolves
150
+ tail.unshift(basename(existing));
151
+ existing = parent;
152
+ }
153
+ }
154
+ }
155
+ /** Is `child` equal to `parent` or inside it? `relative()`-based (the `api/open-path.ts`
156
+ * containment pattern) — a plain `startsWith(base + sep)` never matches a ROOT base (`/`,
157
+ * `C:\`), whose spelling already ends in the separator (Copilot round 2). */
158
+ function contains(parent, child, p) {
159
+ const rel = p.relative(parent, child);
160
+ if (rel === '')
161
+ return true; // the same path
162
+ if (p.isAbsolute(rel))
163
+ return false; // different drive/tree entirely (win32)
164
+ return rel !== '..' && !rel.startsWith(`..${p.sep}`);
165
+ }
166
+ /** `true` when `a` and `b` name the same path or one contains the other — including when one
167
+ * of them is a filesystem root (`/`, a win32 drive root). Exported for its unit tests. */
168
+ export function pathsOverlap(a, b, p = path) {
169
+ return contains(a, b, p) || contains(b, a, p);
170
+ }
171
+ /**
172
+ * Materialize a snapshot of the repo at `rootPath` into `dest` (creating it), for a worker to
173
+ * READ as grounding context. Resolves `{ok: true}` when `dest` now holds a usable snapshot,
174
+ * `{ok: false, reason}` when it does not — see {@link SnapshotFailureReason}; the reason lets
175
+ * the caller narrate the RIGHT degrade (too large ≠ unreadable ≠ clone failed) — in which case
176
+ * any partial `dest` has been removed and the caller must launch UNGROUNDED (and say so on the
177
+ * thread). Never rejects: a grounding failure must degrade the launch, not kill it.
178
+ *
179
+ * The clone is the git-preferred path, so on a real repo the snapshot is HEAD's tracked
180
+ * content (uncommitted/untracked files are not included) minus swept symlinks/`node_modules`;
181
+ * the copy fallback takes the working tree as-is minus `.git`/`node_modules`/symlinks.
182
+ */
183
+ export async function snapshotRepo(rootPath, dest, opts = {}) {
184
+ const log = opts.log ?? (() => { });
185
+ const maxBytes = opts.maxBytes ?? REPO_SNAPSHOT_MAX_BYTES;
186
+ try {
187
+ if (!(await stat(rootPath)).isDirectory()) {
188
+ log(`[repo-snapshot] repo root ${rootPath} is not a directory — no snapshot`);
189
+ return { ok: false, reason: 'root-unreadable' };
190
+ }
191
+ }
192
+ catch {
193
+ log(`[repo-snapshot] repo root ${rootPath} does not exist — no snapshot`);
194
+ return { ok: false, reason: 'root-unreadable' };
195
+ }
196
+ // Overlap refusal comes BEFORE the stale-dest clear (Copilot, crew#313): with a configurable
197
+ // draft dir, `dest` can sit inside the registered repo (or vice versa) — clearing it first
198
+ // would `rm -rf` live repository content, and a copy could recurse into its own output.
199
+ try {
200
+ const realRoot = await realpath(rootPath);
201
+ const realDest = await realpathNearest(dest);
202
+ if (pathsOverlap(realRoot, realDest)) {
203
+ log(`[repo-snapshot] refusing snapshot: destination ${dest} (${realDest}) overlaps the repo root ${rootPath} (${realRoot})`);
204
+ return { ok: false, reason: 'dest-overlap' };
205
+ }
206
+ }
207
+ catch (err) {
208
+ log(`[repo-snapshot] could not resolve ${rootPath} / ${dest} for the overlap check: ${err instanceof Error ? err.message : String(err)} — no snapshot`);
209
+ return { ok: false, reason: 'root-unreadable' };
210
+ }
211
+ if (await treeExceeds(rootPath, maxBytes)) {
212
+ log(`[repo-snapshot] repo at ${rootPath} exceeds the ${maxBytes}-byte snapshot budget — no snapshot`);
213
+ return { ok: false, reason: 'too-large' };
214
+ }
215
+ // The snapshot's PARENT (the caller's per-run dir) is created HERE — only after containment
216
+ // was validated. The caller must never pre-create it: with an overlapping config that mkdir
217
+ // would write into the live repository before the refusal above could fire (Copilot round 2).
218
+ try {
219
+ await mkdir(dirname(dest), { recursive: true });
220
+ }
221
+ catch (err) {
222
+ log(`[repo-snapshot] could not create the snapshot parent for ${dest}: ${err instanceof Error ? err.message : String(err)} — no snapshot`);
223
+ return { ok: false, reason: 'copy-failed' };
224
+ }
225
+ // A stale snapshot (crashed prior run, replayed key) never survives into a new launch.
226
+ try {
227
+ await rm(dest, { recursive: true, force: true });
228
+ }
229
+ catch (err) {
230
+ log(`[repo-snapshot] could not clear ${dest}: ${err instanceof Error ? err.message : String(err)}`);
231
+ return { ok: false, reason: 'dest-unclearable' };
232
+ }
233
+ // Preferred: shallow local clone. `file://` (not a plain path) is what makes --depth honored
234
+ // on a local source; pathToFileURL keeps the URL spelling right on Windows too.
235
+ try {
236
+ await execCapped('git', ['clone', '--depth', '1', '--no-hardlinks', pathToFileURL(rootPath).href, dest], {
237
+ timeout: 120_000,
238
+ windowsHide: true,
239
+ });
240
+ // The clone checked out EVERY tracked path — sweep what the snapshot contract excludes
241
+ // (tracked node_modules, all symlinks), then verify the SWEPT tree against the budget:
242
+ // the preflight measured the source with these exclusions, so a clean sweep normally
243
+ // passes, but the honest gate is what actually landed on disk.
244
+ await sweepSnapshot(dest);
245
+ if (await treeExceeds(dest, maxBytes)) {
246
+ log(`[repo-snapshot] clone of ${rootPath} exceeds the ${maxBytes}-byte budget after sweep — no snapshot`);
247
+ await rm(dest, { recursive: true, force: true }).catch(() => { });
248
+ return { ok: false, reason: 'too-large' };
249
+ }
250
+ return { ok: true };
251
+ }
252
+ catch (err) {
253
+ log(`[repo-snapshot] git clone of ${rootPath} failed (${err instanceof Error ? err.message : String(err)}) — falling back to a file copy`);
254
+ }
255
+ try {
256
+ await rm(dest, { recursive: true, force: true }); // whatever the failed clone left behind
257
+ await cp(rootPath, dest, {
258
+ recursive: true,
259
+ filter: async (src) => {
260
+ if (SNAPSHOT_SKIP.has(basename(src)))
261
+ return false;
262
+ try {
263
+ // Symlinks NEVER ride into the worker-visible inbox: a link out of the repo is a
264
+ // boundary escape, a link to a huge tree a cap bypass (Copilot, crew#313).
265
+ return !(await lstat(src)).isSymbolicLink();
266
+ }
267
+ catch {
268
+ return false; // raced deletion / unreadable — leave it out
269
+ }
270
+ },
271
+ });
272
+ // The honest gate is what actually LANDED (same rule as the clone path's post-sweep
273
+ // re-measure): the preflight walked the SOURCE, and a file that grew between that walk and
274
+ // the copy would otherwise put an over-budget tree in the worker-visible inbox
275
+ // (Copilot round 2).
276
+ if (await treeExceeds(dest, maxBytes)) {
277
+ log(`[repo-snapshot] copy of ${rootPath} exceeds the ${maxBytes}-byte budget — no snapshot`);
278
+ await rm(dest, { recursive: true, force: true }).catch(() => { });
279
+ return { ok: false, reason: 'too-large' };
280
+ }
281
+ return { ok: true };
282
+ }
283
+ catch (err) {
284
+ log(`[repo-snapshot] file copy of ${rootPath} failed too (${err instanceof Error ? err.message : String(err)}) — no snapshot`);
285
+ await rm(dest, { recursive: true, force: true }).catch(() => { }); // never leave a partial snapshot
286
+ return { ok: false, reason: 'copy-failed' };
287
+ }
288
+ }
289
+ //# sourceMappingURL=repo-snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-snapshot.js","sourceRoot":"","sources":["../../src/interactive/repo-snapshot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * DES-MERGE-001 §5.4 / §6.1 (slice 3) — the `wicked.interactive.*` ↔ `/ws` relay.
3
+ *
4
+ * wicked-studio is absorbing wicked-interactive. The SPA already holds crew's `/ws` socket for
5
+ * CoreEvent frames; interactive's own progress travels on the wicked-bus. This module bridges the
6
+ * two so the skin needs exactly ONE socket — no second websocket, no new port.
7
+ *
8
+ * Two halves, one bus handle:
9
+ *
10
+ * 1. RELAY (bus → /ws). Subscribe to `wicked.interactive.**` and rebroadcast each event as
11
+ * `{ type: 'interactiveEvent', event: <the full bus event> }` — ONE envelope type, the original
12
+ * event nested whole, no field renaming, no project filter. Ordering is as received (the bus
13
+ * hands a batch to the handler in order and `broadcast` is synchronous), and there is no
14
+ * buffering beyond what the existing broadcast path already does.
15
+ *
16
+ * 2. EMIT (/ws client → bus). `POST /projects/:projectId/interactive-events` validates the body
17
+ * against a SERVER-SIDE whitelist ({@link EMITTABLE_TYPES}) and puts the event on the bus with
18
+ * interactive's own envelope conventions (domain `wicked-interactive`, producer `wi-crew`,
19
+ * `ts`-stamped payload — draft-events.ts is the precedent), so interactive's own subscribers
20
+ * see a UI-originated event exactly as they see a service-originated one.
21
+ *
22
+ * Deliberately DISTINCT from the `projectActivity` bridge in projects/events.ts, which reads the
23
+ * same filter: that one is project-FILTERED and reshapes the event into an activity frame for a
24
+ * project view. This one is unfiltered and verbatim, because the studio's document canvas must see
25
+ * the events of a doc that was never filed under a project. Separate plugin identity → separate
26
+ * durable cursor, so neither seam can strand the other.
27
+ *
28
+ * Posture mirrors every other bus seam here: LOUD-non-fatal. No wicked-bus, or a broken db → the
29
+ * factory logs once and returns null, the daemon boots without the relay, and the POST route
30
+ * answers 503 instead of pretending the emit landed.
31
+ */
32
+ import type { FastifyInstance } from 'fastify';
33
+ import { z } from 'zod';
34
+ /** The ONE envelope type this relay puts on `/ws`. Consumers that don't know it ignore it — the
35
+ * CoreEvent contract is additive-safe (DES-STUDIO-001 §2.1). */
36
+ export declare const INTERACTIVE_EVENT_FRAME = "interactiveEvent";
37
+ /**
38
+ * Server-side whitelist of the event types a `/ws` client may emit through
39
+ * `POST /projects/:projectId/interactive-events`. A non-whitelisted type is a 400 that NAMES this
40
+ * array — the UI never has to guess. Extend here; nothing else needs to change.
41
+ */
42
+ export declare const EMITTABLE_TYPES: readonly string[];
43
+ /** The request body: a whitelisted type plus that type's own payload. Strict — an unknown key is
44
+ * a 400, not a silently-dropped field. */
45
+ export declare const EmitInteractiveEventSchema: z.ZodObject<{
46
+ type: z.ZodString;
47
+ payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
48
+ }, "strict", z.ZodTypeAny, {
49
+ type: string;
50
+ payload?: Record<string, unknown> | undefined;
51
+ }, {
52
+ type: string;
53
+ payload?: Record<string, unknown> | undefined;
54
+ }>;
55
+ export interface InteractiveRelay {
56
+ /**
57
+ * Emit one interactive event onto the bus. Returns true on success (or on WB-002, a duplicate
58
+ * key — the emit already happened); logs and returns false otherwise. Never throws.
59
+ */
60
+ emitInteractive(type: string, payload: Record<string, unknown>, idempotencyKey: string): boolean;
61
+ stop(): Promise<void>;
62
+ }
63
+ export interface InteractiveRelayOptions {
64
+ /** Bus db path; omit for wicked-bus's own resolution (honors WICKED_BUS_DATA_DIR). */
65
+ dbPath?: string;
66
+ /** Poll cadence for the relay subscriber, ms (tests shorten it). */
67
+ pollIntervalMs?: number;
68
+ log?: (msg: string) => void;
69
+ }
70
+ /**
71
+ * Open the bus and arm both halves. Returns `null` (logged) when wicked-bus is not importable or
72
+ * the db cannot open — the caller degrades to a daemon whose `/ws` simply carries no interactive
73
+ * frames, exactly as before this slice.
74
+ */
75
+ export declare function startInteractiveWsRelay(opts?: InteractiveRelayOptions): Promise<InteractiveRelay | null>;
76
+ /** Occurrence-unique, type-inclusive (DES-EXEC-001 rev0.2 #4a): two types sharing a key would
77
+ * silently drop one on the bus's UNIQUE constraint, and every UI emission is a new fact. */
78
+ export declare function interactiveEmitKey(projectId: string, type: string): string;
79
+ /**
80
+ * `POST /api/v1/projects/:projectId/interactive-events` — the UI-emittable direction (§6.1).
81
+ * Registered even when the relay is null, so the failure is an honest 503 rather than a 404 that
82
+ * reads like "this daemon doesn't have the feature".
83
+ */
84
+ export declare function registerInteractiveEventRoutes(app: FastifyInstance, relay: InteractiveRelay | null): void;
85
+ //# sourceMappingURL=ws-relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-relay.d.ts","sourceRoot":"","sources":["../../src/interactive/ws-relay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiBxB;iEACiE;AACjE,eAAO,MAAM,uBAAuB,qBAAqB,CAAC;AAE1D;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAG5C,CAAC;AAEF;2CAC2C;AAC3C,eAAO,MAAM,0BAA0B;;;;;;;;;EAK5B,CAAC;AAEZ,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC;IACjG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,GAAE,uBAA4B,GACjC,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAgGlC;AAED;6FAC6F;AAC7F,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAC7B,IAAI,CAmCN"}
@@ -0,0 +1,191 @@
1
+ /**
2
+ * DES-MERGE-001 §5.4 / §6.1 (slice 3) — the `wicked.interactive.*` ↔ `/ws` relay.
3
+ *
4
+ * wicked-studio is absorbing wicked-interactive. The SPA already holds crew's `/ws` socket for
5
+ * CoreEvent frames; interactive's own progress travels on the wicked-bus. This module bridges the
6
+ * two so the skin needs exactly ONE socket — no second websocket, no new port.
7
+ *
8
+ * Two halves, one bus handle:
9
+ *
10
+ * 1. RELAY (bus → /ws). Subscribe to `wicked.interactive.**` and rebroadcast each event as
11
+ * `{ type: 'interactiveEvent', event: <the full bus event> }` — ONE envelope type, the original
12
+ * event nested whole, no field renaming, no project filter. Ordering is as received (the bus
13
+ * hands a batch to the handler in order and `broadcast` is synchronous), and there is no
14
+ * buffering beyond what the existing broadcast path already does.
15
+ *
16
+ * 2. EMIT (/ws client → bus). `POST /projects/:projectId/interactive-events` validates the body
17
+ * against a SERVER-SIDE whitelist ({@link EMITTABLE_TYPES}) and puts the event on the bus with
18
+ * interactive's own envelope conventions (domain `wicked-interactive`, producer `wi-crew`,
19
+ * `ts`-stamped payload — draft-events.ts is the precedent), so interactive's own subscribers
20
+ * see a UI-originated event exactly as they see a service-originated one.
21
+ *
22
+ * Deliberately DISTINCT from the `projectActivity` bridge in projects/events.ts, which reads the
23
+ * same filter: that one is project-FILTERED and reshapes the event into an activity frame for a
24
+ * project view. This one is unfiltered and verbatim, because the studio's document canvas must see
25
+ * the events of a doc that was never filed under a project. Separate plugin identity → separate
26
+ * durable cursor, so neither seam can strand the other.
27
+ *
28
+ * Posture mirrors every other bus seam here: LOUD-non-fatal. No wicked-bus, or a broken db → the
29
+ * factory logs once and returns null, the daemon boots without the relay, and the POST route
30
+ * answers 503 instead of pretending the emit landed.
31
+ */
32
+ import { randomUUID } from 'node:crypto';
33
+ import { z } from 'zod';
34
+ import { broadcast } from '../events/bus.js';
35
+ import { API_PREFIX } from '../api/api-prefix.js';
36
+ import { LOCAL_ACTOR } from '../api/auth.js';
37
+ import { INTERACTIVE_DOMAIN, INTERACTIVE_PRODUCER } from './draft-events.js';
38
+ const V = API_PREFIX;
39
+ /** Dedicated durable-cursor identity — NOT the projects bridge's, so the two seams that read
40
+ * `wicked.interactive.**` advance independent cursors. */
41
+ const RELAY_PLUGIN = 'wicked-crew-interactive-relay';
42
+ const RELAY_FILTER = 'wicked.interactive.**';
43
+ /** The scope guard, restated locally: the property "only interactive events are relayed" is this
44
+ * module's, not the bus glob's. */
45
+ const RELAY_TYPE_PREFIX = 'wicked.interactive.';
46
+ /** The ONE envelope type this relay puts on `/ws`. Consumers that don't know it ignore it — the
47
+ * CoreEvent contract is additive-safe (DES-STUDIO-001 §2.1). */
48
+ export const INTERACTIVE_EVENT_FRAME = 'interactiveEvent';
49
+ /**
50
+ * Server-side whitelist of the event types a `/ws` client may emit through
51
+ * `POST /projects/:projectId/interactive-events`. A non-whitelisted type is a 400 that NAMES this
52
+ * array — the UI never has to guess. Extend here; nothing else needs to change.
53
+ */
54
+ export const EMITTABLE_TYPES = [
55
+ 'wicked.interactive.feedback.submitted',
56
+ 'wicked.interactive.status.requested',
57
+ ];
58
+ /** The request body: a whitelisted type plus that type's own payload. Strict — an unknown key is
59
+ * a 400, not a silently-dropped field. */
60
+ export const EmitInteractiveEventSchema = z
61
+ .object({
62
+ type: z.string().min(1).max(128),
63
+ payload: z.record(z.string(), z.unknown()).optional(),
64
+ })
65
+ .strict();
66
+ /**
67
+ * Open the bus and arm both halves. Returns `null` (logged) when wicked-bus is not importable or
68
+ * the db cannot open — the caller degrades to a daemon whose `/ws` simply carries no interactive
69
+ * frames, exactly as before this slice.
70
+ */
71
+ export async function startInteractiveWsRelay(opts = {}) {
72
+ const log = opts.log ?? (() => undefined);
73
+ let bus;
74
+ try {
75
+ bus = await import('wicked-bus');
76
+ }
77
+ catch (err) {
78
+ log(`[interactive-relay] wicked-bus is not importable — the interactive /ws relay is disabled: ${err instanceof Error ? err.message : String(err)}`);
79
+ return null;
80
+ }
81
+ let db;
82
+ let config;
83
+ try {
84
+ const override = opts.dbPath !== undefined ? { db_path: opts.dbPath } : {};
85
+ config = bus.loadConfig(override);
86
+ db = bus.openDb(override);
87
+ }
88
+ catch (err) {
89
+ log(`[interactive-relay] could not open the bus db${opts.dbPath !== undefined ? ` at ${opts.dbPath}` : ''} — the interactive /ws relay is disabled: ${err instanceof Error ? err.message : String(err)}`);
90
+ return null;
91
+ }
92
+ // Half 1 — bus → /ws. maxRetries: 0 for the same reason the projects bridge uses it: a retry
93
+ // loop or a DLQ entry for a lost liveness frame is noise, and the durable feed is the record.
94
+ //
95
+ // This subscription also sees the events emitted by half 2 (they match the same filter). That
96
+ // is not a loop — the relay only broadcasts, never re-emits — and the echo is the UI's
97
+ // confirmation that its own emission actually landed on the bus.
98
+ let subscription = null;
99
+ try {
100
+ subscription = bus.subscribe({
101
+ db,
102
+ plugin: RELAY_PLUGIN,
103
+ filter: RELAY_FILTER,
104
+ cursor_init: 'latest',
105
+ pollIntervalMs: opts.pollIntervalMs ?? 2000,
106
+ maxRetries: 0,
107
+ handler: (event) => {
108
+ if (!event.event_type.startsWith(RELAY_TYPE_PREFIX))
109
+ return;
110
+ broadcast({ type: INTERACTIVE_EVENT_FRAME, event });
111
+ },
112
+ onError: (err, event) => {
113
+ log(`[interactive-relay] relay error on event ${String(event?.event_id ?? '?')}: ${err.message}`);
114
+ },
115
+ });
116
+ }
117
+ catch (err) {
118
+ log(`[interactive-relay] could not arm the /ws relay subscriber (the emit direction still works): ${err instanceof Error ? err.message : String(err)}`);
119
+ }
120
+ // Half 2 — the emit surface the POST route calls after the whitelist check. The subdomain is
121
+ // the third segment of interactive's own type grammar (`wicked.interactive.<subdomain>.<verb>`).
122
+ function emitInteractive(type, payload, idempotencyKey) {
123
+ try {
124
+ bus.emit(db, config, {
125
+ event_type: type,
126
+ domain: INTERACTIVE_DOMAIN,
127
+ subdomain: type.split('.')[2] ?? 'status',
128
+ payload: { ts: new Date().toISOString(), ...payload },
129
+ producer_id: INTERACTIVE_PRODUCER,
130
+ idempotency_key: idempotencyKey,
131
+ });
132
+ return true;
133
+ }
134
+ catch (err) {
135
+ const code = err.error;
136
+ if (code === 'WB-002')
137
+ return true; // duplicate key — the emit already happened
138
+ log(`[interactive-relay] emit ${type} failed: ${err instanceof Error ? err.message : String(err)}`);
139
+ return false;
140
+ }
141
+ }
142
+ return {
143
+ emitInteractive,
144
+ stop: async () => {
145
+ if (subscription !== null)
146
+ await subscription.stop();
147
+ },
148
+ };
149
+ }
150
+ /** Occurrence-unique, type-inclusive (DES-EXEC-001 rev0.2 #4a): two types sharing a key would
151
+ * silently drop one on the bus's UNIQUE constraint, and every UI emission is a new fact. */
152
+ export function interactiveEmitKey(projectId, type) {
153
+ return `crew:interactive.ui:${projectId}:${type}:${randomUUID()}`;
154
+ }
155
+ /**
156
+ * `POST /api/v1/projects/:projectId/interactive-events` — the UI-emittable direction (§6.1).
157
+ * Registered even when the relay is null, so the failure is an honest 503 rather than a 404 that
158
+ * reads like "this daemon doesn't have the feature".
159
+ */
160
+ export function registerInteractiveEventRoutes(app, relay) {
161
+ const actorOf = (req) => req.actor ?? LOCAL_ACTOR;
162
+ app.post(`${V}/projects/:projectId/interactive-events`, async (req, reply) => {
163
+ const { projectId } = req.params;
164
+ const parsed = EmitInteractiveEventSchema.safeParse(req.body);
165
+ if (!parsed.success) {
166
+ return reply.code(400).send({ error: 'Invalid request body', details: parsed.error.issues });
167
+ }
168
+ const { type } = parsed.data;
169
+ if (!EMITTABLE_TYPES.includes(type)) {
170
+ return reply.code(400).send({
171
+ error: `Event type '${type}' is not emittable from a client`,
172
+ allowed: EMITTABLE_TYPES,
173
+ });
174
+ }
175
+ if (relay === null) {
176
+ return reply
177
+ .code(503)
178
+ .send({ error: 'the interactive bus seam is not armed on this daemon' });
179
+ }
180
+ // Server-side fields go LAST: `project_id` and `actor` are the daemon's to state, never the
181
+ // caller's (task #88 — event actors are not caller-supplied), so a payload that names either
182
+ // one cannot override them.
183
+ const idempotencyKey = interactiveEmitKey(projectId, type);
184
+ const emitted = relay.emitInteractive(type, { ...(parsed.data.payload ?? {}), project_id: projectId, actor: actorOf(req).id }, idempotencyKey);
185
+ if (!emitted) {
186
+ return reply.code(502).send({ error: `the bus refused the ${type} emit` });
187
+ }
188
+ return reply.code(202).send({ emitted: true, type, projectId, idempotencyKey });
189
+ });
190
+ }
191
+ //# sourceMappingURL=ws-relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-relay.js","sourceRoot":"","sources":["../../src/interactive/ws-relay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The project activity feed (DES-PROJECT-001 §5.2) — a READ-SIDE MERGE, not a new store.
3
+ *
4
+ * Two sources, one normalized shape:
5
+ * - CREW: the durable core-event log of the project's `crew.run`/`crew.chat` members
6
+ * (`adapter.runEvents` — the same log `/runs/:id/events` serves), entry id `crew:<run>:<seq>`.
7
+ * - INTERACTIVE: `wicked.interactive.*` bus events carrying this `project_id`, read directly
8
+ * from the bus SQLite READ-ONLY (`node:sqlite`, the `requirements.ts` precedent — wicked-bus
9
+ * exposes subscribe-cursors, not ad-hoc history queries, and a durable cursor per feed-read
10
+ * would turn a GET into a write), entry id `bus:<event_id>`.
11
+ *
12
+ * Newest-first, cursor on `(ts, id)` — an opaque `<ts>:<id>` token, base64url. The merge is
13
+ * recomputed per read; members are few and the log excludes high-volume frames, so the simple
14
+ * full-merge is the honest v1 (the ADR explicitly rejects a new store here).
15
+ */
16
+ import type { CoreAdapter } from '../core/adapter.js';
17
+ import type { ActivityEntry } from '../core/types.js';
18
+ /** Opaque cursor: base64url of `<ts>:<id>`. */
19
+ export declare function encodeCursor(ts: number, id: string): string;
20
+ export declare function decodeCursor(cursor: string): {
21
+ ts: number;
22
+ id: string;
23
+ } | null;
24
+ /** Build one page of the merged feed. `refs` = the project's run/chat member refs. */
25
+ export declare function buildActivityPage(adapter: CoreAdapter, projectId: string, refs: string[], busDbPath: string | null, cursor: string | undefined, limit: number): Promise<{
26
+ entries: ActivityEntry[];
27
+ nextCursor: string | null;
28
+ }>;
29
+ //# sourceMappingURL=activity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/projects/activity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAuDtD,+CAA+C;AAC/C,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAW9E;AAsFD,sFAAsF;AACtF,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAgBlE"}