wicked-crew 0.6.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 (99) hide show
  1. package/dist/api/audit.d.ts +13 -0
  2. package/dist/api/audit.d.ts.map +1 -1
  3. package/dist/api/audit.js +18 -2
  4. package/dist/api/audit.js.map +1 -1
  5. package/dist/api/guidance-index.d.ts +39 -0
  6. package/dist/api/guidance-index.d.ts.map +1 -0
  7. package/dist/api/guidance-index.js +67 -0
  8. package/dist/api/guidance-index.js.map +1 -0
  9. package/dist/api/open-path.d.ts +16 -0
  10. package/dist/api/open-path.d.ts.map +1 -1
  11. package/dist/api/open-path.js +22 -0
  12. package/dist/api/open-path.js.map +1 -1
  13. package/dist/api/retry-index.d.ts +30 -0
  14. package/dist/api/retry-index.d.ts.map +1 -0
  15. package/dist/api/retry-index.js +45 -0
  16. package/dist/api/retry-index.js.map +1 -0
  17. package/dist/api/routes.d.ts +53 -1
  18. package/dist/api/routes.d.ts.map +1 -1
  19. package/dist/api/routes.js +352 -23
  20. package/dist/api/routes.js.map +1 -1
  21. package/dist/api/run-files.d.ts +63 -0
  22. package/dist/api/run-files.d.ts.map +1 -0
  23. package/dist/api/run-files.js +271 -0
  24. package/dist/api/run-files.js.map +1 -0
  25. package/dist/api/server.d.ts +79 -0
  26. package/dist/api/server.d.ts.map +1 -1
  27. package/dist/api/server.js +135 -6
  28. package/dist/api/server.js.map +1 -1
  29. package/dist/api/stall-watchdog.d.ts +62 -0
  30. package/dist/api/stall-watchdog.d.ts.map +1 -0
  31. package/dist/api/stall-watchdog.js +138 -0
  32. package/dist/api/stall-watchdog.js.map +1 -0
  33. package/dist/cli/index.js +78 -13
  34. package/dist/cli/index.js.map +1 -1
  35. package/dist/core/adapter.d.ts +24 -10
  36. package/dist/core/adapter.d.ts.map +1 -1
  37. package/dist/core/adapter.js +191 -30
  38. package/dist/core/adapter.js.map +1 -1
  39. package/dist/core/bridge-reaper.d.ts +134 -0
  40. package/dist/core/bridge-reaper.d.ts.map +1 -0
  41. package/dist/core/bridge-reaper.js +286 -0
  42. package/dist/core/bridge-reaper.js.map +1 -0
  43. package/dist/core/deliver.d.ts +118 -0
  44. package/dist/core/deliver.d.ts.map +1 -0
  45. package/dist/core/deliver.js +241 -0
  46. package/dist/core/deliver.js.map +1 -0
  47. package/dist/core/deliverable-floor.d.ts +103 -0
  48. package/dist/core/deliverable-floor.d.ts.map +1 -0
  49. package/dist/core/deliverable-floor.js +173 -0
  50. package/dist/core/deliverable-floor.js.map +1 -0
  51. package/dist/core/exec.d.ts +2 -0
  52. package/dist/core/exec.d.ts.map +1 -1
  53. package/dist/core/exec.js.map +1 -1
  54. package/dist/core/types.d.ts +79 -1
  55. package/dist/core/types.d.ts.map +1 -1
  56. package/dist/core/types.js +3 -0
  57. package/dist/core/types.js.map +1 -1
  58. package/dist/interactive/bridge-pool.d.ts +28 -0
  59. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  60. package/dist/interactive/bridge-pool.js +67 -10
  61. package/dist/interactive/bridge-pool.js.map +1 -1
  62. package/dist/interactive/chat-events.d.ts +207 -0
  63. package/dist/interactive/chat-events.d.ts.map +1 -0
  64. package/dist/interactive/chat-events.js +769 -0
  65. package/dist/interactive/chat-events.js.map +1 -0
  66. package/dist/interactive/demo-events.d.ts +283 -0
  67. package/dist/interactive/demo-events.d.ts.map +1 -0
  68. package/dist/interactive/demo-events.js +889 -0
  69. package/dist/interactive/demo-events.js.map +1 -0
  70. package/dist/interactive/draft-events.d.ts +87 -7
  71. package/dist/interactive/draft-events.d.ts.map +1 -1
  72. package/dist/interactive/draft-events.js +352 -49
  73. package/dist/interactive/draft-events.js.map +1 -1
  74. package/dist/interactive/edit-events.d.ts +22 -0
  75. package/dist/interactive/edit-events.d.ts.map +1 -1
  76. package/dist/interactive/edit-events.js +73 -2
  77. package/dist/interactive/edit-events.js.map +1 -1
  78. package/dist/interactive/repo-snapshot.d.ts +100 -0
  79. package/dist/interactive/repo-snapshot.d.ts.map +1 -0
  80. package/dist/interactive/repo-snapshot.js +289 -0
  81. package/dist/interactive/repo-snapshot.js.map +1 -0
  82. package/dist/projects/graph-paths.d.ts +92 -0
  83. package/dist/projects/graph-paths.d.ts.map +1 -0
  84. package/dist/projects/graph-paths.js +130 -0
  85. package/dist/projects/graph-paths.js.map +1 -0
  86. package/dist/projects/graph.d.ts +179 -0
  87. package/dist/projects/graph.d.ts.map +1 -0
  88. package/dist/projects/graph.js +775 -0
  89. package/dist/projects/graph.js.map +1 -0
  90. package/dist/projects/routes.d.ts +7 -0
  91. package/dist/projects/routes.d.ts.map +1 -1
  92. package/dist/projects/routes.js +122 -0
  93. package/dist/projects/routes.js.map +1 -1
  94. package/dist/studio/assets/index-8p8uwCxG.js +530 -0
  95. package/dist/studio/assets/index-D6S9zUtO.css +32 -0
  96. package/dist/studio/index.html +5 -3
  97. package/package.json +3 -3
  98. package/dist/studio/assets/index-CCwXa1cn.js +0 -428
  99. package/dist/studio/assets/index-HWxo0h41.css +0 -32
@@ -0,0 +1,271 @@
1
+ // GET /runs/:id/files + GET /runs/:id/diff machinery (DES-FEEDBACK-002 CREW-1).
2
+ //
3
+ // The studio's in-app file & diff viewer cannot read the daemon host's filesystem, so the daemon
4
+ // serves the bytes — which is exactly why every path is contained FIRST against the same root set
5
+ // `POST /open` validates (`allowedRootsFor` + `isInsideRoot`, open-path.ts), every payload is
6
+ // capped, and git runs through `execCapped` with argv arrays only (never a shell string) and
7
+ // `--no-ext-diff` (a configured external diff driver must never execute here). Both routes are
8
+ // read-only by construction: `fs` reads and `git diff`/`git status` — zero write capability, a
9
+ // strictly smaller threat surface than `/open` handing the path to an OS opener.
10
+ import { promises as fsp } from 'node:fs';
11
+ import { execCapped, ExecOutputTooLarge } from '../core/exec.js';
12
+ /** File-content cap (DES-FEEDBACK-002 §3.3): past this, `content` holds the first 512 KB and
13
+ * `truncated: true` — the studio renders a labeled truncation banner, never a silent amputation. */
14
+ export const FILE_CONTENT_CAP_BYTES = 512 * 1024;
15
+ /** Binary sniff window: a NUL byte in the first 8 KB ⇒ `binary: true`, `content: ""` — the studio
16
+ * falls back to the external-open affordance rather than rendering mojibake. */
17
+ export const BINARY_SNIFF_BYTES = 8 * 1024;
18
+ /** Diff output cap (§3.3): past this the diff is cut at 1 MB and `truncated: true`. */
19
+ export const DIFF_OUTPUT_CAP_BYTES = 1024 * 1024;
20
+ /** The target exists but is not a regular file (directory, socket, …) — the route's 400, distinct
21
+ * from ENOENT's 404. Serving directory listings is explicitly out of scope (§3.3). */
22
+ export class NotARegularFileError extends Error {
23
+ path;
24
+ constructor(path) {
25
+ super(`not a regular file: ${path}`);
26
+ this.path = path;
27
+ this.name = 'NotARegularFileError';
28
+ }
29
+ }
30
+ /**
31
+ * Read at most `FILE_CONTENT_CAP_BYTES` of `target`. The open-then-fstat order (not stat-then-open)
32
+ * closes the classic swap race: the size and the bytes come from the SAME open file description.
33
+ * Callers must have contained `target` already — this function does filesystem work only.
34
+ */
35
+ export async function readFileCapped(target) {
36
+ const fh = await fsp.open(target, 'r');
37
+ try {
38
+ const st = await fh.stat();
39
+ if (!st.isFile())
40
+ throw new NotARegularFileError(target);
41
+ const size = st.size;
42
+ const toRead = Math.min(size, FILE_CONTENT_CAP_BYTES);
43
+ const buf = Buffer.alloc(toRead);
44
+ let off = 0;
45
+ while (off < toRead) {
46
+ const { bytesRead } = await fh.read(buf, off, toRead - off, off);
47
+ if (bytesRead === 0)
48
+ break; // file shrank underneath us — serve what exists
49
+ off += bytesRead;
50
+ }
51
+ const got = buf.subarray(0, off);
52
+ if (got.subarray(0, Math.min(off, BINARY_SNIFF_BYTES)).includes(0)) {
53
+ return { content: '', size, truncated: false, binary: true };
54
+ }
55
+ return { content: got.toString('utf8'), size, truncated: size > FILE_CONTENT_CAP_BYTES, binary: false };
56
+ }
57
+ finally {
58
+ await fh.close();
59
+ }
60
+ }
61
+ const GIT_TIMEOUT_MS = 10_000;
62
+ // ── Diff base (DES-UX-001 §8.1, CREW-UX-1) ─────────────────────────────────────────────────────
63
+ /** `?base=merge-base` — the one non-ref literal: diff against `git merge-base <default-branch>
64
+ * HEAD`, i.e. the run branch's fork point, so committed run work is visible (§8.1). */
65
+ export const MERGE_BASE_LITERAL = 'merge-base';
66
+ /** `base` is not a plain ref — a flag, a path, a revision range, a separator. Rejected BEFORE any
67
+ * git process sees it: the query parameter is a diff baseline, never a command surface (§8.1). */
68
+ export class InvalidDiffBaseError extends Error {
69
+ base;
70
+ constructor(base) {
71
+ super('`base` must be a plain git ref (branch, tag, or commit id) — ' +
72
+ 'flags, paths, revision ranges, and separators are rejected');
73
+ this.base = base;
74
+ this.name = 'InvalidDiffBaseError';
75
+ }
76
+ }
77
+ /** `base` is well-formed but does not resolve to a commit inside the run's repo — containment:
78
+ * a ref from another repo (or thin air) is a 400, never a git error surfaced as a 500. */
79
+ export class UnresolvableDiffBaseError extends Error {
80
+ base;
81
+ constructor(base, detail) {
82
+ super(`unresolvable \`base\`: ${detail}`);
83
+ this.base = base;
84
+ this.name = 'UnresolvableDiffBaseError';
85
+ }
86
+ }
87
+ /** A plain ref: git's own charset for the safe subset — alphanumeric start (excludes `-flag`,
88
+ * `.hidden`, `/abs/path`), then word/dot/slash/dash. `..` (ranges AND traversal), `~`/`^`/`@{}`
89
+ * (rev operators), `:`, whitespace, and `;` are all outside the charset or explicitly banned. */
90
+ const PLAIN_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$/;
91
+ export function isPlainRef(base) {
92
+ return PLAIN_REF_RE.test(base) && !base.includes('..') && !base.endsWith('/');
93
+ }
94
+ /** One contained git read: argv array (never a shell string), capped, cwd-scoped to the run's
95
+ * workdir. Returns trimmed stdout, or `null` ONLY when git ran to completion and answered "no"
96
+ * (unresolvable ref, no merge base, …) — a plain non-zero exit. Operational failures are never
97
+ * `null`: ENOENT (git missing), "not a git repository", a timed-out/killed process (`killed`/
98
+ * `signal` set), and output-cap overflow all rethrow, so they surface as the route's 500 rather
99
+ * than masquerading as a 400 "unresolvable base" (Copilot, #307). */
100
+ async function gitQuery(workdir, args) {
101
+ try {
102
+ const { stdout } = await execCapped('git', args, { timeout: GIT_TIMEOUT_MS, cwd: workdir });
103
+ return stdout.trim();
104
+ }
105
+ catch (err) {
106
+ if (err.code === 'ENOENT')
107
+ throw err;
108
+ if (err instanceof ExecOutputTooLarge)
109
+ throw err;
110
+ const proc = err;
111
+ if (proc.killed === true || (proc.signal !== undefined && proc.signal !== null))
112
+ throw err;
113
+ const msg = err instanceof Error ? err.message : String(err);
114
+ if (msg.toLowerCase().includes('not a git repository'))
115
+ throw err;
116
+ return null;
117
+ }
118
+ }
119
+ /** SHA shapes rev-parse may emit (SHA-1 today, SHA-256 repos exist). Anything else is treated as
120
+ * "did not resolve" — the resolved value is what reaches `git diff`, never the caller's text. */
121
+ const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/;
122
+ /**
123
+ * Resolve `?base=` to a commit SHA inside the run's repo, or throw the route's named 400s.
124
+ * Two accepted spellings (§8.1): the literal `merge-base` → `git merge-base <default-branch>
125
+ * HEAD`; or a plain ref that `git rev-parse --verify` resolves IN THIS repo. Everything git
126
+ * ultimately diffs against is the resolved SHA — the raw parameter never reaches `git diff`,
127
+ * and `--end-of-options` keeps even a validated ref from ever parsing as a flag.
128
+ */
129
+ async function resolveDiffBase(workdir, base) {
130
+ if (base === MERGE_BASE_LITERAL) {
131
+ // Default branch: origin's HEAD when the repo has one, else local main/master.
132
+ let branch = null;
133
+ const symref = await gitQuery(workdir, ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD']);
134
+ if (symref !== null && symref.startsWith('refs/remotes/')) {
135
+ branch = symref.slice('refs/remotes/'.length);
136
+ }
137
+ else {
138
+ for (const cand of ['main', 'master']) {
139
+ if ((await gitQuery(workdir, ['rev-parse', '--verify', `refs/heads/${cand}`])) !== null) {
140
+ branch = cand;
141
+ break;
142
+ }
143
+ }
144
+ }
145
+ if (branch === null) {
146
+ throw new UnresolvableDiffBaseError(base, 'no default branch found in this repo to merge-base against');
147
+ }
148
+ const sha = await gitQuery(workdir, ['merge-base', '--end-of-options', branch, 'HEAD']);
149
+ if (sha === null || !COMMIT_SHA_RE.test(sha)) {
150
+ throw new UnresolvableDiffBaseError(base, `no merge base between ${branch} and HEAD`);
151
+ }
152
+ return sha;
153
+ }
154
+ if (!isPlainRef(base))
155
+ throw new InvalidDiffBaseError(base);
156
+ const sha = await gitQuery(workdir, [
157
+ 'rev-parse',
158
+ '--verify',
159
+ '--end-of-options',
160
+ `${base}^{commit}`,
161
+ ]);
162
+ if (sha === null || !COMMIT_SHA_RE.test(sha)) {
163
+ throw new UnresolvableDiffBaseError(base, `ref does not resolve inside the run's repo: ${base}`);
164
+ }
165
+ return sha;
166
+ }
167
+ /** Git's ways of saying "there is nothing here to diff" — a real answer (`diff: ""`), not an
168
+ * error. Mirrors the git-history route's tolerance (routes.ts): a workdir that is not a repo, or
169
+ * a repo with no commits yet (`HEAD` unresolvable — every file is untracked and the untracked
170
+ * pass still runs for the commit-less case). */
171
+ function isEmptyDiffAnswer(msg) {
172
+ // Case-insensitive on the repo check: `git log` says "fatal: not a git repository" but
173
+ // `git diff` says "warning: Not a git repository. Use --no-index …" — measured, not assumed.
174
+ const lower = msg.toLowerCase();
175
+ return (lower.includes('not a git repository') ||
176
+ msg.includes('does not have any commits') ||
177
+ msg.includes("ambiguous argument 'HEAD'") ||
178
+ msg.includes('unknown revision'));
179
+ }
180
+ /**
181
+ * The run's worktree diff against `base` (default HEAD) — staged + unstaged in one answer — with
182
+ * untracked files appended as `git diff --no-index /dev/null <file>` all-addition hunks (§3.3: a
183
+ * run's *created* files must diff as additions rather than vanish). `relPath`, when given, narrows
184
+ * both passes to that one file; it is always a single argv element after `--` (the open-path rule
185
+ * — no shell string is ever composed). The literal `/dev/null` is git's own cross-platform
186
+ * null-device spelling (git special-cases it, including on Windows).
187
+ *
188
+ * `base` (CREW-UX-1, DES-UX-001 §8.1): the literal `merge-base` diffs from the run branch's fork
189
+ * point off the default branch — committed run work becomes visible; a plain ref diffs from that
190
+ * ref, resolved inside this repo first (`resolveDiffBase` — the raw parameter never reaches
191
+ * `git diff`). Throws `InvalidDiffBaseError` / `UnresolvableDiffBaseError` for the route's 400s.
192
+ */
193
+ export async function worktreeDiff(workdir, relPath, base) {
194
+ const limit = relPath === undefined ? [] : ['--', relPath];
195
+ let out = '';
196
+ // Resolve the baseline BEFORE any diff pass: a bad `base` must 400 without partial output.
197
+ let baseRev = 'HEAD';
198
+ if (base !== undefined && base.length > 0) {
199
+ try {
200
+ baseRev = await resolveDiffBase(workdir, base);
201
+ }
202
+ catch (err) {
203
+ if (err.code === 'ENOENT')
204
+ throw err; // git missing — 500
205
+ const msg = err instanceof Error ? err.message : String(err);
206
+ // The route's standing tolerance: a non-repo workdir answers an empty diff for any
207
+ // WELL-FORMED base — a malformed base has already thrown InvalidDiffBaseError above,
208
+ // and that 400 wins over the tolerance (Copilot, #307).
209
+ if (msg.toLowerCase().includes('not a git repository'))
210
+ return { diff: '', truncated: false };
211
+ throw err;
212
+ }
213
+ }
214
+ // Tracked changes (staged + unstaged, plus committed-since-base when a base was resolved):
215
+ // diff the worktree against the baseline.
216
+ try {
217
+ const { stdout } = await execCapped('git', ['diff', '--no-color', '--no-ext-diff', baseRev, ...limit], { timeout: GIT_TIMEOUT_MS, cwd: workdir });
218
+ out = stdout;
219
+ }
220
+ catch (err) {
221
+ if (err.code === 'ENOENT')
222
+ throw err; // git missing — the route's 500
223
+ const msg = err instanceof Error ? err.message : String(err);
224
+ if (msg.toLowerCase().includes('not a git repository'))
225
+ return { diff: '', truncated: false };
226
+ if (!isEmptyDiffAnswer(msg))
227
+ throw err;
228
+ // No commits yet: tracked diff is vacuously empty; fall through to the untracked pass.
229
+ }
230
+ // Untracked files → all-addition hunks. `-z` (NUL separators, no quoting) + `-uall` (individual
231
+ // files, never a collapsed `?? dir/` entry that --no-index could not diff).
232
+ const { stdout: statusOut } = await execCapped('git', ['status', '--porcelain', '-z', '-uall', ...limit], { timeout: GIT_TIMEOUT_MS, cwd: workdir });
233
+ const untracked = statusOut
234
+ .split('\0')
235
+ .filter((entry) => entry.startsWith('?? '))
236
+ .map((entry) => entry.slice(3));
237
+ for (const file of untracked) {
238
+ // Cap is in BYTES; `out.length` counts UTF-16 code units. `>=`: exactly-at-cap is
239
+ // already done — spawning one more git only to discard its output is waste (Copilot, #305).
240
+ if (Buffer.byteLength(out, 'utf8') >= DIFF_OUTPUT_CAP_BYTES)
241
+ break;
242
+ try {
243
+ const { stdout } = await execCapped('git', ['diff', '--no-color', '--no-ext-diff', '--no-index', '--', '/dev/null', file], { timeout: GIT_TIMEOUT_MS, cwd: workdir });
244
+ out += stdout;
245
+ }
246
+ catch (err) {
247
+ // `--no-index` exits 1 WHEN THE FILES DIFFER — which is every hit here. The diff is still
248
+ // on stdout; only a stdout-less failure is a real error.
249
+ const stdout = err.stdout;
250
+ if (typeof stdout === 'string' && stdout.length > 0)
251
+ out += stdout;
252
+ else if (err.code === 'ENOENT')
253
+ throw err;
254
+ else
255
+ throw err;
256
+ }
257
+ }
258
+ // Byte-accurate cap (Copilot, #305): `slice` on the string counts UTF-16 code units, which
259
+ // under-enforces for non-ASCII diffs. Cut on the UTF-8 byte buffer, then back off any
260
+ // incomplete trailing multibyte sequence — decoding a torn tail yields U+FFFD, which
261
+ // re-encodes LARGER and would breach the cap by a byte.
262
+ const outBytes = Buffer.from(out, 'utf8');
263
+ if (outBytes.byteLength > DIFF_OUTPUT_CAP_BYTES) {
264
+ let end = DIFF_OUTPUT_CAP_BYTES;
265
+ while (end > 0 && (outBytes[end] & 0xc0) === 0x80)
266
+ end--; // land on a sequence boundary
267
+ return { diff: outBytes.subarray(0, end).toString('utf8'), truncated: true };
268
+ }
269
+ return { diff: out, truncated: false };
270
+ }
271
+ //# sourceMappingURL=run-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-files.js","sourceRoot":"","sources":["../../src/api/run-files.ts"],"names":[],"mappings":"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"}
@@ -71,6 +71,69 @@ export interface CreateServerOptions {
71
71
  editDir?: string;
72
72
  /** Seat roster override (JSON array); omit for the production council roster. */
73
73
  clisJson?: string;
74
+ /** Docs-root resolver override (tests); default = per-project `interactiveRoot` setting.
75
+ * Used ONLY as the demo-kind gate (CREW-UX-9) — demo docs' step feedback belongs to the
76
+ * demo seam. */
77
+ resolveDocsRoot?: (projectId: string | undefined) => string;
78
+ };
79
+ /**
80
+ * Opt-in governed answering of wicked-interactive DEMO docs (CREW-UX-9 — video generation's
81
+ * missing brain, the retired assist agent's Step 8). When enabled, a durable subscriber
82
+ * answers `wicked.interactive.doc.created` (kind:demo) with a governed `interactive-demo`
83
+ * run that authors `demo.spec.mjs`, installs it into the doc workspace, and ends in
84
+ * `wicked.interactive.demo.requested` — the model-free service then records the video
85
+ * (Playwright + ffmpeg) and lands the storyboard version itself. Step feedback on a demo doc
86
+ * (`feedback.processed`, manifest kind demo) is answered by RE-authoring the spec and
87
+ * re-emitting demo.requested (assist SKILL.md Step 8c). When absent (the default), demo docs
88
+ * keep their "Learning …" placeholder — the pre-CREW-UX-9 state.
89
+ */
90
+ interactiveDemoEvents?: {
91
+ enabled: boolean;
92
+ /** Bus db path; omit for wicked-bus's own default resolution (honors WICKED_BUS_DATA_DIR). */
93
+ dbPath?: string;
94
+ /** Poll cadence, ms (tests shorten it). */
95
+ pollIntervalMs?: number;
96
+ /** Heartbeat narration cadence, ms (default 15000). */
97
+ heartbeatMs?: number;
98
+ /** Durable replay-dedup ledger path (default ~/.wicked-crew/interactive-demo-ledger.json). */
99
+ ledgerPath?: string;
100
+ /** Where governed workers write specs (default ~/.wicked-crew/interactive-demos). */
101
+ demoDir?: string;
102
+ /** Seat roster override (JSON array); omit for the production council roster. */
103
+ clisJson?: string;
104
+ /** Docs-root resolver override (tests); default = per-project `interactiveRoot` setting. */
105
+ resolveDocsRoot?: (projectId: string | undefined) => string;
106
+ };
107
+ /**
108
+ * Opt-in governed answering of wicked-interactive's conversational ITERATION asks
109
+ * (CREW-UX-5, the doc thread's plain send). When enabled, a durable subscriber answers
110
+ * `wicked.interactive.chat.posted` (role:user, existing kind:source doc, not an in-flight
111
+ * doc, not a feedback-batch echo) with a governed `interactive-chat` run —
112
+ * understand-the-ask → revise — that ends in `wicked.interactive.draft.completed` (the
113
+ * service lands the revised full HTML as a generated version). Asks on a busy doc queue
114
+ * FIFO per doc. When absent (the default), the topic goes unanswered — the pre-CREW-UX-5
115
+ * state.
116
+ */
117
+ interactiveChatEvents?: {
118
+ enabled: boolean;
119
+ /** Bus db path; omit for wicked-bus's own default resolution (honors WICKED_BUS_DATA_DIR). */
120
+ dbPath?: string;
121
+ /** Poll cadence, ms (tests shorten it). */
122
+ pollIntervalMs?: number;
123
+ /** Heartbeat narration cadence, ms (default 15000). */
124
+ heartbeatMs?: number;
125
+ /** Durable replay-dedup ledger path (default ~/.wicked-crew/interactive-chat-ledger.json). */
126
+ ledgerPath?: string;
127
+ /** Where head snapshots land and workers write revisions (default ~/.wicked-crew/interactive-chats). */
128
+ chatDir?: string;
129
+ /** Seat roster override (JSON array); omit for the production council roster. */
130
+ clisJson?: string;
131
+ /** Queue-drain sweep cadence, ms (tests shorten it). */
132
+ queueSweepMs?: number;
133
+ /** Post-completion landing-gate timeout, ms (tests shorten it). */
134
+ landingGateMs?: number;
135
+ /** Docs-root resolver override (tests); default = per-project `interactiveRoot` setting. */
136
+ resolveDocsRoot?: (projectId: string | undefined) => string;
74
137
  };
75
138
  /**
76
139
  * The project bus seam (DES-PROJECT-001 §4/§5.2). DEFAULT-ON, unlike the opt-in seams
@@ -117,6 +180,22 @@ export interface CreateServerOptions {
117
180
  intervalMs?: number;
118
181
  timeoutMs?: number;
119
182
  };
183
+ /**
184
+ * The worker stall watchdog (crew#287): DETECTION ONLY. For every run whose engine status is
185
+ * `executing`, the daemon tracks the last CoreEvent observed on its own relay (any frame for
186
+ * that run, `unitOutputDelta` included); silence past `workerStallMinutes` (setting, default
187
+ * 15) broadcasts ONE synthetic `{ type: "workerStalled", session, ord?, quietForMs }` frame
188
+ * on /ws per quiet period and logs at warn. Any new event re-arms. The run is never killed
189
+ * or mutated — the operator decides. `enabled` defaults to ON in the daemon and OFF under a
190
+ * test runner (VITEST / NODE_ENV=test), the seat-health-probe posture.
191
+ */
192
+ stallWatchdog?: {
193
+ enabled?: boolean;
194
+ /** Sweep cadence, ms (default 30 s; tests shorten it). */
195
+ sweepIntervalMs?: number;
196
+ /** Threshold override, minutes — bypasses the settings read (tests). */
197
+ stallMinutes?: number;
198
+ };
120
199
  }
121
200
  export declare function createServer(adapter: CoreAdapter, options?: CreateServerOptions): Promise<ReturnType<typeof Fastify>>;
122
201
  export interface StartedServer {
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"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"}
@@ -8,18 +8,26 @@ import { GateCache } from './gate-cache.js';
8
8
  import { ElicitationCache } from './elicitation-cache.js';
9
9
  import { registerAuthHooks, resolveAuth } from './auth.js';
10
10
  import { AuditLog } from './audit.js';
11
+ import { RetryIndex } from './retry-index.js';
12
+ import { GuidanceIndex } from './guidance-index.js';
11
13
  import { registerClient, broadcast } from '../events/bus.js';
12
14
  import { TerminalHub, registerTerminalWs } from '../events/terminals.js';
13
15
  import { QeGateCache, startQeGateSubscriber } from '../qe/gate-events.js';
14
16
  import { startInteractiveDraftSubscriber } from '../interactive/draft-events.js';
15
17
  import { startInteractiveEditSubscriber } from '../interactive/edit-events.js';
18
+ import { startInteractiveChatSubscriber } from '../interactive/chat-events.js';
19
+ import { startInteractiveDemoSubscriber } from '../interactive/demo-events.js';
20
+ import { resolveInteractiveRoot } from '../interactive/bridge-root.js';
21
+ import { ProjectSettingsStore } from '../projects/settings.js';
16
22
  import { startProjectBus, MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.js';
17
23
  import { startInteractiveWsRelay, registerInteractiveEventRoutes } from '../interactive/ws-relay.js';
18
24
  import { MembershipIndex } from '../projects/membership-index.js';
19
25
  import { writeRunEvidencePointer } from '../projects/charter.js';
20
26
  import { CoreAdapter } from '../core/adapter.js';
21
27
  import { SeatHealthTracker, startSeatHealthProbe } from './seat-health.js';
28
+ import { WorkerStallWatchdog } from './stall-watchdog.js';
22
29
  import { applyWorkerConfigRoot } from './seat-signin.js';
30
+ import { DEFAULT_WORKER_STALL_MINUTES } from '../core/types.js';
23
31
  // Allow the studio (a separate localhost origin, e.g. :4200) to call the
24
32
  // daemon's REST API. Restricted to loopback origins — the daemon only binds
25
33
  // 127.0.0.1, so this never widens exposure beyond the local machine.
@@ -68,6 +76,19 @@ export async function createServer(adapter, options) {
68
76
  // post-commit half (index tag + membership.attached emit) via `fileRun`.
69
77
  const membershipIndex = new MembershipIndex();
70
78
  await membershipIndex.hydrate(adapter, (m) => app.log.warn(m));
79
+ // Per-project crew-side settings (DES-MERGE-001 §7.1's `interactiveRoot`). ONE instance,
80
+ // created here so the chat seam's docs-root resolution below and the routes (project PATCH +
81
+ // interactive proxy) all read/write the same store — two instances over one file would let a
82
+ // PATCH land in one while the other keeps serving the stale root.
83
+ const projectSettings = new ProjectSettingsStore();
84
+ // Retry lineage (CREW-UX-3): hydrated from the audit trail — the durable record the launch
85
+ // route writes — so a restarted daemon still echoes `retry_of` on prior runs' DTOs.
86
+ const retryIndex = new RetryIndex();
87
+ await retryIndex.hydrate(audit, (m) => app.log.warn(m));
88
+ // Operator guidance (CREW-UX-7, DES-UX-002 §7.2): same durable pattern — hydrated from the
89
+ // trail's `guidance.set` entries so notes survive a daemon restart.
90
+ const guidanceIndex = new GuidanceIndex();
91
+ await guidanceIndex.hydrate(audit, (m) => app.log.warn(m));
71
92
  const projectBus = options?.projectEvents?.disabled === true
72
93
  ? null
73
94
  : await startProjectBus({
@@ -118,6 +139,11 @@ export async function createServer(adapter, options) {
118
139
  // under the task #88 rule that event actors are never caller-supplied.
119
140
  { project_id: projectId, member: { kind: 'crew.run', ref: runId }, actor: 'interactive' }, membershipAttachedKey(projectId, 'crew.run', runId, Date.now()));
120
141
  };
142
+ /** The docs root a project's interactive docs live under — the SAME per-project settings
143
+ * resolution the project routes, the interactive proxy, and the chat seam share
144
+ * (DES-MERGE-001 §7.1/§7.2). Shared by the edit seam (demo-kind gate, CREW-UX-9) and the
145
+ * demo seam (spec install + manifest reads). */
146
+ const interactiveDocsRoot = (projectId) => resolveInteractiveRoot(projectId !== undefined ? projectSettings.get(projectId) : null);
121
147
  // Arm the opt-in QE gate-event subscription (crew's bus seam). Failure to
122
148
  // arm is LOUD but non-fatal: the acceptance route never depends on the bus.
123
149
  if (options?.qeGateEvents?.enabled === true) {
@@ -136,10 +162,12 @@ export async function createServer(adapter, options) {
136
162
  }
137
163
  // Arm the opt-in interactive-draft answering seam (task #86 spike). Same posture as the QE
138
164
  // seam: failure to arm is LOUD but non-fatal — interactive's assist loop is the fallback
139
- // answerer, and this daemon must boot on a machine whose bus is broken.
165
+ // answerer, and this daemon must boot on a machine whose bus is broken. The handle is kept:
166
+ // the chat seam below consults its in-flight docs (CREW-UX-5 per-doc serialization).
167
+ let draftSub = null;
140
168
  if (options?.interactiveDraftEvents?.enabled === true) {
141
169
  const o = options.interactiveDraftEvents;
142
- const sub = await startInteractiveDraftSubscriber(adapter, {
170
+ draftSub = await startInteractiveDraftSubscriber(adapter, {
143
171
  ...(o.dbPath !== undefined ? { dbPath: o.dbPath } : {}),
144
172
  ...(o.pollIntervalMs !== undefined ? { pollIntervalMs: o.pollIntervalMs } : {}),
145
173
  ...(o.heartbeatMs !== undefined ? { heartbeatMs: o.heartbeatMs } : {}),
@@ -149,35 +177,118 @@ export async function createServer(adapter, options) {
149
177
  onRunFiled: fileRun,
150
178
  log: (m) => app.log.warn(m),
151
179
  });
152
- if (sub !== null) {
180
+ if (draftSub !== null) {
181
+ const sub = draftSub;
153
182
  app.log.info('interactive-draft subscription armed (filter wicked.interactive.doc.created)');
154
183
  app.addHook('onClose', async () => {
155
184
  await sub.stop();
156
185
  });
157
186
  }
158
187
  }
188
+ // Declared BEFORE the edit seam it is probed by: the demo seam arms after (it must, so the
189
+ // per-doc serialization contract can see the edit seam's in-flight set), but the edit seam's
190
+ // demo-kind gate needs to know at EVENT time whether the demo seam actually came up. A
191
+ // closure over this binding reads the settled value; a boolean captured here would not.
192
+ let demoSub = null;
159
193
  // Arm the opt-in interactive STRUCTURAL-edit answering seam (task #86 final leg). Same
160
194
  // posture as the draft seam: failure to arm is LOUD but non-fatal — interactive's assist
161
195
  // loop is the fallback answerer, and this daemon must boot on a machine whose bus is broken.
196
+ let editSub = null;
162
197
  if (options?.interactiveEditEvents?.enabled === true) {
163
198
  const o = options.interactiveEditEvents;
164
- const sub = await startInteractiveEditSubscriber(adapter, {
199
+ editSub = await startInteractiveEditSubscriber(adapter, {
165
200
  ...(o.dbPath !== undefined ? { dbPath: o.dbPath } : {}),
166
201
  ...(o.pollIntervalMs !== undefined ? { pollIntervalMs: o.pollIntervalMs } : {}),
167
202
  ...(o.heartbeatMs !== undefined ? { heartbeatMs: o.heartbeatMs } : {}),
168
203
  ...(o.ledgerPath !== undefined ? { ledgerPath: o.ledgerPath } : {}),
169
204
  ...(o.editDir !== undefined ? { editDir: o.editDir } : {}),
170
205
  ...(o.clisJson !== undefined ? { clisJson: o.clisJson } : {}),
206
+ // The demo-kind gate (CREW-UX-9): a demo doc's step feedback is the demo seam's — but
207
+ // only when that seam is actually up. Probed per event (the demo seam arms below), so an
208
+ // un-armed demo seam gets an honest error status instead of a silent, unanswerable drop.
209
+ resolveDocsRoot: o.resolveDocsRoot ?? interactiveDocsRoot,
210
+ demoSeamArmed: () => demoSub !== null,
171
211
  onRunFiled: fileRun,
172
212
  log: (m) => app.log.warn(m),
173
213
  });
174
- if (sub !== null) {
214
+ if (editSub !== null) {
215
+ const sub = editSub;
175
216
  app.log.info('interactive-edit subscription armed (filter wicked.interactive.feedback.processed)');
176
217
  app.addHook('onClose', async () => {
177
218
  await sub.stop();
178
219
  });
179
220
  }
180
221
  }
222
+ // Arm the opt-in interactive DEMO answering seam (CREW-UX-9 — the retired assist agent's
223
+ // Step 8). Same posture as the sibling seams: failure to arm is LOUD but non-fatal — a demo
224
+ // doc then keeps its placeholder, and this daemon must boot on a machine whose bus is
225
+ // broken. Armed beside the edit seam because the two split `feedback.processed` by the doc
226
+ // manifest's kind (demo → re-author the spec here; everything else → structural edit there).
227
+ if (options?.interactiveDemoEvents?.enabled === true) {
228
+ const o = options.interactiveDemoEvents;
229
+ demoSub = await startInteractiveDemoSubscriber(adapter, {
230
+ ...(o.dbPath !== undefined ? { dbPath: o.dbPath } : {}),
231
+ ...(o.pollIntervalMs !== undefined ? { pollIntervalMs: o.pollIntervalMs } : {}),
232
+ ...(o.heartbeatMs !== undefined ? { heartbeatMs: o.heartbeatMs } : {}),
233
+ ...(o.ledgerPath !== undefined ? { ledgerPath: o.ledgerPath } : {}),
234
+ ...(o.demoDir !== undefined ? { demoDir: o.demoDir } : {}),
235
+ ...(o.clisJson !== undefined ? { clisJson: o.clisJson } : {}),
236
+ resolveDocsRoot: o.resolveDocsRoot ?? interactiveDocsRoot,
237
+ onRunFiled: fileRun,
238
+ log: (m) => app.log.warn(m),
239
+ });
240
+ if (demoSub !== null) {
241
+ const sub = demoSub;
242
+ app.log.info('interactive-demo subscription armed (filters wicked.interactive.doc.created + feedback.processed, kind:demo)');
243
+ app.addHook('onClose', async () => {
244
+ await sub.stop();
245
+ });
246
+ }
247
+ }
248
+ // Arm the opt-in interactive CHAT answering seam (CREW-UX-5 — the iteration ask). Same
249
+ // posture again: failure to arm is LOUD but non-fatal. Armed AFTER the sibling seams so the
250
+ // per-doc serialization contract (no draft/edit/chat run races another on one doc) can
251
+ // consult their in-flight sets; docs roots resolve through the SAME per-project settings
252
+ // store the project routes and the interactive proxy share.
253
+ if (options?.interactiveChatEvents?.enabled === true) {
254
+ const o = options.interactiveChatEvents;
255
+ const sub = await startInteractiveChatSubscriber(adapter, {
256
+ ...(o.dbPath !== undefined ? { dbPath: o.dbPath } : {}),
257
+ ...(o.pollIntervalMs !== undefined ? { pollIntervalMs: o.pollIntervalMs } : {}),
258
+ ...(o.heartbeatMs !== undefined ? { heartbeatMs: o.heartbeatMs } : {}),
259
+ ...(o.ledgerPath !== undefined ? { ledgerPath: o.ledgerPath } : {}),
260
+ ...(o.chatDir !== undefined ? { chatDir: o.chatDir } : {}),
261
+ ...(o.clisJson !== undefined ? { clisJson: o.clisJson } : {}),
262
+ ...(o.queueSweepMs !== undefined ? { queueSweepMs: o.queueSweepMs } : {}),
263
+ ...(o.landingGateMs !== undefined ? { landingGateMs: o.landingGateMs } : {}),
264
+ resolveDocsRoot: o.resolveDocsRoot ??
265
+ ((projectId) => resolveInteractiveRoot(projectId !== undefined ? projectSettings.get(projectId) : null)),
266
+ isDocBusy: (documentId) => (draftSub?.inFlightDocs().includes(documentId) ?? false) ||
267
+ (editSub?.inFlightDocs().includes(documentId) ?? false),
268
+ onRunFiled: fileRun,
269
+ log: (m) => app.log.warn(m),
270
+ });
271
+ if (sub !== null) {
272
+ app.log.info('interactive-chat subscription armed (filter wicked.interactive.chat.posted)');
273
+ app.addHook('onClose', async () => {
274
+ await sub.stop();
275
+ });
276
+ }
277
+ }
278
+ // The worker stall watchdog (crew#287). Built BEFORE the single CoreEvent subscription below
279
+ // so every relayed frame stamps its run's liveness clock; armed (sweep interval) further down
280
+ // beside the seat-health probe, under the same test-runner gate. Detection only: its sole
281
+ // outputs are one synthetic `workerStalled` /ws frame per quiet period and a warn log.
282
+ const stallWatchdog = new WorkerStallWatchdog({
283
+ listExecuting: async () => (await adapter.sessionsDetail())
284
+ .filter((v) => v.session.status === 'executing')
285
+ .map((v) => ({ id: v.session.id, ord: v.session.unit_ix })),
286
+ broadcast: (frame) => broadcast(frame),
287
+ stallMinutes: async () => options?.stallWatchdog?.stallMinutes ??
288
+ (await adapter.getSettings()).workerStallMinutes ??
289
+ DEFAULT_WORKER_STALL_MINUTES,
290
+ log: (m) => app.log.warn(m),
291
+ });
181
292
  // The daemon's single CoreEvent subscription fans out here: cache gate prompts
182
293
  // (§3.3), cache elicitation prompts (DES-002), route terminal frames to their owning
183
294
  // per-terminal socket (by id, DES-TERMINAL-001 §6), then forward every frame — tagged
@@ -187,10 +298,17 @@ export async function createServer(adapter, options) {
187
298
  // Unregistered on close: a process can build more than one server over the same
188
299
  // adapter (tests do), and a closed server's caches must stop consuming events —
189
300
  // otherwise every discarded server keeps folding state forever (listener leak).
301
+ const stallWatchdogArmed = options?.stallWatchdog?.enabled ??
302
+ !(process.env['VITEST'] !== undefined || process.env['NODE_ENV'] === 'test');
190
303
  const offEvent = adapter.onEvent((event) => {
191
304
  gateCache.ingest(event);
192
305
  elicitationCache.ingest(event);
193
306
  seatHealth.ingest(event);
307
+ // Only feed the watchdog when its sweep is (or will be) armed: sweeping is what
308
+ // prunes its per-run maps, so ingesting while disabled grows without bound
309
+ // (Copilot on #301).
310
+ if (stallWatchdogArmed)
311
+ stallWatchdog.ingest(event);
194
312
  terminals.route(event);
195
313
  const session = typeof event.session === 'string' ? event.session : undefined;
196
314
  const projectId = session !== undefined ? membershipIndex.projectOf(session) : undefined;
@@ -235,6 +353,16 @@ export async function createServer(adapter, options) {
235
353
  probe.stop();
236
354
  });
237
355
  }
356
+ // Arm the stall watchdog's sweep (crew#287). Same gate as the probe: ON in the daemon, OFF
357
+ // under a test runner unless a test opts in — a suite building servers over stub adapters
358
+ // must not have a background interval calling `sessionsDetail()` on them.
359
+ const stallCfg = options?.stallWatchdog;
360
+ if (stallWatchdogArmed) {
361
+ stallWatchdog.start(stallCfg?.sweepIntervalMs);
362
+ app.addHook('onClose', async () => {
363
+ stallWatchdog.stop();
364
+ });
365
+ }
238
366
  app.addHook('onRequest', async (req, reply) => {
239
367
  const origin = req.headers.origin;
240
368
  // Loopback origins are always allowed (the studio on another localhost
@@ -288,7 +416,8 @@ export async function createServer(adapter, options) {
288
416
  bus: projectBus,
289
417
  index: membershipIndex,
290
418
  log: (m) => app.log.warn(m),
291
- }, { audit, authMode: auth.mode }, { seatHealth });
419
+ settings: projectSettings,
420
+ }, { audit, authMode: auth.mode }, { seatHealth, retryIndex, guidanceIndex });
292
421
  // The UI-emittable direction of the interactive seam. Registered unconditionally (a null relay
293
422
  // answers 503, not 404) and BEFORE the static/SPA fallback below, like every other API route.
294
423
  registerInteractiveEventRoutes(app, interactiveRelay);