wicked-crew 0.7.27 → 0.7.29
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.
- package/README.md +9 -1
- package/dist/api/chat-scope.d.ts +210 -0
- package/dist/api/chat-scope.d.ts.map +1 -0
- package/dist/api/chat-scope.js +724 -0
- package/dist/api/chat-scope.js.map +1 -0
- package/dist/api/diagnostics.d.ts +30 -2
- package/dist/api/diagnostics.d.ts.map +1 -1
- package/dist/api/diagnostics.js +73 -5
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/api/governance-health.d.ts +163 -0
- package/dist/api/governance-health.d.ts.map +1 -0
- package/dist/api/governance-health.js +348 -0
- package/dist/api/governance-health.js.map +1 -0
- package/dist/api/open-path.d.ts.map +1 -1
- package/dist/api/open-path.js +2 -1
- package/dist/api/open-path.js.map +1 -1
- package/dist/api/post-hoc-deliver.d.ts +9 -5
- package/dist/api/post-hoc-deliver.d.ts.map +1 -1
- package/dist/api/post-hoc-deliver.js +21 -7
- package/dist/api/post-hoc-deliver.js.map +1 -1
- package/dist/api/routes.d.ts +56 -12
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +318 -50
- package/dist/api/routes.js.map +1 -1
- package/dist/api/seat-health.d.ts.map +1 -1
- package/dist/api/seat-health.js +25 -5
- package/dist/api/seat-health.js.map +1 -1
- package/dist/api/seat-signin.d.ts.map +1 -1
- package/dist/api/seat-signin.js +49 -23
- package/dist/api/seat-signin.js.map +1 -1
- package/dist/api/server.d.ts +13 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +34 -0
- package/dist/api/server.js.map +1 -1
- package/dist/campaigns/routes.d.ts +4 -4
- package/dist/cli/governance.d.ts +131 -0
- package/dist/cli/governance.d.ts.map +1 -0
- package/dist/cli/governance.js +354 -0
- package/dist/cli/governance.js.map +1 -0
- package/dist/cli/index.js +114 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +89 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +145 -25
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-reaper.d.ts.map +1 -1
- package/dist/core/bridge-reaper.js +4 -1
- package/dist/core/bridge-reaper.js.map +1 -1
- package/dist/core/deliver-text.d.ts +184 -0
- package/dist/core/deliver-text.d.ts.map +1 -0
- package/dist/core/deliver-text.js +434 -0
- package/dist/core/deliver-text.js.map +1 -0
- package/dist/core/deliver-triage.d.ts +90 -0
- package/dist/core/deliver-triage.d.ts.map +1 -0
- package/dist/core/deliver-triage.js +106 -0
- package/dist/core/deliver-triage.js.map +1 -0
- package/dist/core/deliver.d.ts +129 -19
- package/dist/core/deliver.d.ts.map +1 -1
- package/dist/core/deliver.js +271 -57
- package/dist/core/deliver.js.map +1 -1
- package/dist/core/deliverable-floor.d.ts.map +1 -1
- package/dist/core/deliverable-floor.js +5 -4
- package/dist/core/deliverable-floor.js.map +1 -1
- package/dist/core/estate-mcp-client.d.ts.map +1 -1
- package/dist/core/estate-mcp-client.js +7 -2
- package/dist/core/estate-mcp-client.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +11 -1
- package/dist/core/exec.js.map +1 -1
- package/dist/core/governance-store.d.ts +197 -0
- package/dist/core/governance-store.d.ts.map +1 -0
- package/dist/core/governance-store.js +283 -0
- package/dist/core/governance-store.js.map +1 -0
- package/dist/core/repoPaths.d.ts +36 -5
- package/dist/core/repoPaths.d.ts.map +1 -1
- package/dist/core/repoPaths.js +46 -4
- package/dist/core/repoPaths.js.map +1 -1
- package/dist/interactive/bridge-pool.d.ts +130 -2
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +286 -8
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/bus-location.d.ts +65 -0
- package/dist/interactive/bus-location.d.ts.map +1 -0
- package/dist/interactive/bus-location.js +75 -0
- package/dist/interactive/bus-location.js.map +1 -0
- package/dist/interactive/chat-events.d.ts.map +1 -1
- package/dist/interactive/chat-events.js +29 -23
- package/dist/interactive/chat-events.js.map +1 -1
- package/dist/interactive/demo-events.d.ts +16 -1
- package/dist/interactive/demo-events.d.ts.map +1 -1
- package/dist/interactive/demo-events.js +262 -43
- package/dist/interactive/demo-events.js.map +1 -1
- package/dist/interactive/doc-grounding.d.ts +202 -0
- package/dist/interactive/doc-grounding.d.ts.map +1 -0
- package/dist/interactive/doc-grounding.js +647 -0
- package/dist/interactive/doc-grounding.js.map +1 -0
- package/dist/interactive/draft-events.d.ts +54 -1
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +234 -117
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/edit-events.d.ts.map +1 -1
- package/dist/interactive/edit-events.js +27 -21
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/interactive/proxy-routes.d.ts +78 -0
- package/dist/interactive/proxy-routes.d.ts.map +1 -1
- package/dist/interactive/proxy-routes.js +355 -14
- package/dist/interactive/proxy-routes.js.map +1 -1
- package/dist/interactive/repo-snapshot.d.ts +31 -0
- package/dist/interactive/repo-snapshot.d.ts.map +1 -1
- package/dist/interactive/repo-snapshot.js +56 -1
- package/dist/interactive/repo-snapshot.js.map +1 -1
- package/dist/projects/graph-paths.d.ts +4 -2
- package/dist/projects/graph-paths.d.ts.map +1 -1
- package/dist/projects/graph-paths.js +4 -2
- package/dist/projects/graph-paths.js.map +1 -1
- package/dist/projects/graph.d.ts +5 -4
- package/dist/projects/graph.d.ts.map +1 -1
- package/dist/projects/graph.js +22 -5
- package/dist/projects/graph.js.map +1 -1
- package/dist/projects/routes.d.ts.map +1 -1
- package/dist/projects/routes.js +18 -9
- package/dist/projects/routes.js.map +1 -1
- package/dist/qe/conformance.d.ts +10 -1
- package/dist/qe/conformance.d.ts.map +1 -1
- package/dist/qe/conformance.js +57 -4
- package/dist/qe/conformance.js.map +1 -1
- package/dist/skills/plugin-source.d.ts.map +1 -1
- package/dist/skills/plugin-source.js +3 -0
- package/dist/skills/plugin-source.js.map +1 -1
- package/dist/studio/assets/index-CeJTutxP.js +540 -0
- package/dist/studio/index.html +1 -1
- package/dist/studio/testid-inventory.json +333 -7
- package/endpoint-manifest.json +75 -12
- package/package.json +3 -3
- package/dist/studio/assets/index-FcZg3WDR.js +0 -539
package/dist/api/routes.js
CHANGED
|
@@ -6,7 +6,7 @@ import { promises as fsp } from 'node:fs';
|
|
|
6
6
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds } from '../core/adapter.js';
|
|
9
|
-
import { codeGraphDb, requirementsGraph } from '../core/repoPaths.js';
|
|
9
|
+
import { codeGraphDb, codeGraphErrorStatus, requirementsGraph } from '../core/repoPaths.js';
|
|
10
10
|
import { resolveCursorUnit } from '../core/cursor.js';
|
|
11
11
|
import { detectRefusal } from './gate-cache.js';
|
|
12
12
|
import { QeGateCache } from '../qe/gate-events.js';
|
|
@@ -17,6 +17,7 @@ import { execCapped, ExecOutputTooLarge } from '../core/exec.js';
|
|
|
17
17
|
import { callEstateTool, EstateMcpError } from '../core/estate-mcp-client.js';
|
|
18
18
|
import { SeatHealthTracker } from './seat-health.js';
|
|
19
19
|
import { applyWorkerConfigRoot, signedInHeuristic } from './seat-signin.js';
|
|
20
|
+
import { ChatScopeIndex, chatScopeDeps, prepareChatScratch, removeChatScratch, resolveChatScope, } from './chat-scope.js';
|
|
20
21
|
import { allowedRootsFor, isInsideRoot, openWithSystemDefault } from './open-path.js';
|
|
21
22
|
import { InvalidDiffBaseError, NotARegularFileError, UnresolvableDiffBaseError, readFileCapped, worktreeDiff, } from './run-files.js';
|
|
22
23
|
import { resolveProjectGraphBinding } from '../projects/graph.js';
|
|
@@ -30,6 +31,7 @@ import { registerSkillsRoutes } from './skills.js';
|
|
|
30
31
|
import { disabledSkillsHealth } from '../skills/runtime.js';
|
|
31
32
|
import { ProjectSettingsStore } from '../projects/settings.js';
|
|
32
33
|
import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
|
|
34
|
+
import { composeDeliverText, factsFromRun, framedDeliverText, runUrlFor } from '../core/deliver-text.js';
|
|
33
35
|
import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
|
|
34
36
|
import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
|
|
35
37
|
import { registerInteractiveDocList } from '../interactive/doc-list-routes.js';
|
|
@@ -37,6 +39,8 @@ import { MembershipIndex } from '../projects/membership-index.js';
|
|
|
37
39
|
import { MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.js';
|
|
38
40
|
import { AuditLog } from './audit.js';
|
|
39
41
|
import { AcpFoldCache, EngineVersionCache, eventsDirOf, installedPackageVersion, listStoreFiles, readStudioBundleVersion, } from './diagnostics.js';
|
|
42
|
+
import { GovernanceDiagnostics } from './governance-health.js';
|
|
43
|
+
import { legacyHomeOutboxPath } from '../core/governance-store.js';
|
|
40
44
|
import { RetryIndex } from './retry-index.js';
|
|
41
45
|
import { GroupIndex } from './group-index.js';
|
|
42
46
|
import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
|
|
@@ -69,6 +73,25 @@ const STATUS_ORDER = {
|
|
|
69
73
|
function sortActionableFirst(views) {
|
|
70
74
|
return [...views].sort((a, b) => (STATUS_ORDER[a.session.status] ?? 9) - (STATUS_ORDER[b.session.status] ?? 9));
|
|
71
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* `codeGraphDb(repo)` for a repo surface, or `null` after answering **503** — a CURRENT engine that
|
|
78
|
+
* resolved no repo-graph root (`code_graph_root_unresolvable`, wicked-core#406) is a daemon-
|
|
79
|
+
* environment fault, not a bad request and not Fastify's generic 500. The mapping is the shared
|
|
80
|
+
* `codeGraphErrorStatus`; anything it does not classify rethrows unchanged (the stale-addon error
|
|
81
|
+
* keeps its pre-existing shape here).
|
|
82
|
+
*/
|
|
83
|
+
function codeGraphDbOr503(repo, reply) {
|
|
84
|
+
try {
|
|
85
|
+
return codeGraphDb(repo);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const status = codeGraphErrorStatus(err);
|
|
89
|
+
if (status === null)
|
|
90
|
+
throw err;
|
|
91
|
+
void reply.code(status).send({ error: message(err) });
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
72
95
|
function message(err) {
|
|
73
96
|
return err instanceof Error ? err.message : String(err);
|
|
74
97
|
}
|
|
@@ -411,6 +434,21 @@ const STUDIO_SETTINGS_MAX_BYTES = 512 * 1024;
|
|
|
411
434
|
* The daemon REST surface. Every endpoint is a thin wrapper over one adapter /
|
|
412
435
|
* core-ts call (DES-STUDIO-001 §2). `session`/`phase` nouns are now `run`/`unit`.
|
|
413
436
|
*/
|
|
437
|
+
/**
|
|
438
|
+
* `POST /chats` body (crew#165; `projectId` DES-PROJECT-001 §2.2; `repoRefs` + scope semantics
|
|
439
|
+
* crew#502). Exported for the wire-contract drift guard (`tests/wire-contract.test.ts`).
|
|
440
|
+
*/
|
|
441
|
+
export const ChatOpenSchema = z.object({
|
|
442
|
+
chatId: z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
443
|
+
clis: z.array(z.string().min(1)).min(1).max(8).optional(),
|
|
444
|
+
/** One repo in scope — the legacy spelling, merged into `repoRefs` (crew#502). */
|
|
445
|
+
repoRef: z.string().min(1).optional(),
|
|
446
|
+
/** The repos in scope, by registry id or name (crew#502). */
|
|
447
|
+
repoRefs: z.array(z.string().min(1)).min(1).max(32).optional(),
|
|
448
|
+
/** File the chat into a project (`crew.chat` membership) — and, with no `repoRefs`, scope it to
|
|
449
|
+
* every registered `crew.repo` member of that project (crew#502). */
|
|
450
|
+
projectId: z.string().min(1).optional(),
|
|
451
|
+
}).strict();
|
|
414
452
|
export function registerRoutes(app, adapter, gateCache, elicitationCache,
|
|
415
453
|
// Defaulted so a caller that never arms the bus seam (tests drive this
|
|
416
454
|
// function directly) gets the same behavior as an unarmed daemon: an empty
|
|
@@ -435,6 +473,7 @@ runtime = {}) {
|
|
|
435
473
|
const groupIndex = runtime.groupIndex ?? new GroupIndex();
|
|
436
474
|
const runTimingIndex = runtime.runTimingIndex ?? new RunTimingIndex();
|
|
437
475
|
const guidanceIndex = runtime.guidanceIndex ?? new GuidanceIndex();
|
|
476
|
+
const chatScopes = runtime.chatScopes ?? new ChatScopeIndex();
|
|
438
477
|
const deliveryIndex = runtime.deliveryIndex ?? new DeliveryIndex();
|
|
439
478
|
const worktreeExists = runtime.worktreeExists ?? ((p) => existsSync(p));
|
|
440
479
|
const vacuityProbes = {
|
|
@@ -457,6 +496,12 @@ runtime = {}) {
|
|
|
457
496
|
isDelivered: (runId) => deliveryIndex.urlFor(runId) !== undefined,
|
|
458
497
|
});
|
|
459
498
|
const deliverExec = runtime.deliverExec ?? runDeliverScript;
|
|
499
|
+
// crew#524: the deliver phase's script asks THIS daemon for the run-derived PR text, and the PR
|
|
500
|
+
// body links the run here — so the adapter learns the bound origin lazily (the server has not
|
|
501
|
+
// listened yet when routes register). Guarded: directly-driven route sets hand in fakes.
|
|
502
|
+
if (typeof adapter.setDeliverApiOrigin === 'function') {
|
|
503
|
+
adapter.setDeliverApiOrigin(() => boundOrigin(app.server.address()));
|
|
504
|
+
}
|
|
460
505
|
const reprovisionWorktree = runtime.reprovisionWorktree ?? gitReprovisionWorktree;
|
|
461
506
|
// The estate MCP client behind `/proposals*` AND `/memory*` (DES-MEM-FACETED-001) — one seam. The
|
|
462
507
|
// default is the real spawn-per-call `wicked-estate-mcp` client; route tests inject a stub so no
|
|
@@ -572,16 +617,22 @@ runtime = {}) {
|
|
|
572
617
|
// can never turn this GET into an event-log re-reader or a `--version` spawner per request.
|
|
573
618
|
const acpFoldCache = new AcpFoldCache();
|
|
574
619
|
const engineVersionCache = new EngineVersionCache();
|
|
620
|
+
// The governance block (crew#495): the store the engine's emit seam writes to (as the adapter
|
|
621
|
+
// exported it — `null` on a boot that resolved none), its record count through the engine
|
|
622
|
+
// binding when the addon has one, and the dead-letter outbox folded on change. Built at boot so
|
|
623
|
+
// the record baseline is the boot baseline.
|
|
624
|
+
const governanceDiagnostics = new GovernanceDiagnostics(adapter.governanceStore ?? null, CoreAdapter.eventStoreCounter(), runtime.governanceLegacyOutboxPath === undefined ? legacyHomeOutboxPath() : runtime.governanceLegacyOutboxPath);
|
|
575
625
|
app.get(`${V}/diagnostics`, { config: { manifest: { responseType: 'DiagnosticsResponse', statusCodes: [200] } } }, async () => {
|
|
576
626
|
// `dbPath` is a readonly field of the real adapter; a stub-driven route set may lack it,
|
|
577
627
|
// and diagnostics over an unknown store honestly reports no stores and no ACP record.
|
|
578
628
|
const dbPath = typeof adapter.dbPath === 'string' && adapter.dbPath !== '' ? adapter.dbPath : null;
|
|
579
|
-
const [stores, byCli, engineBinaries] = await Promise.all([
|
|
629
|
+
const [stores, byCli, engineBinaries, governance] = await Promise.all([
|
|
580
630
|
dbPath !== null ? listStoreFiles(dbPath) : Promise.resolve([]),
|
|
581
631
|
dbPath !== null
|
|
582
632
|
? acpFoldCache.get(eventsDirOf(dbPath))
|
|
583
633
|
: Promise.resolve({}),
|
|
584
634
|
engineVersionCache.get(),
|
|
635
|
+
governanceDiagnostics.health(),
|
|
585
636
|
]);
|
|
586
637
|
const uptimeMs = Math.round(process.uptime() * 1000);
|
|
587
638
|
const addr = app.server.address();
|
|
@@ -601,6 +652,9 @@ runtime = {}) {
|
|
|
601
652
|
// config-error, with the `skills.*` findings the ladder produced — the operator's one
|
|
602
653
|
// read-only answer to "why do launches refuse the snapshot".
|
|
603
654
|
skills: runtime.skills?.health() ?? disabledSkillsHealth(),
|
|
655
|
+
// Is the governance evidence LANDING (crew#495): the store, the records on it, the dead
|
|
656
|
+
// letters — with a `governance.deadletter` finding the moment the outbox holds one.
|
|
657
|
+
governance,
|
|
604
658
|
};
|
|
605
659
|
});
|
|
606
660
|
// Who am I talking to the daemon as? (task #88). In local mode this is
|
|
@@ -950,7 +1004,10 @@ runtime = {}) {
|
|
|
950
1004
|
// HERE, at the boundary, for every launch:
|
|
951
1005
|
// - explicit 'pr' / 'none' wins (the operator decided);
|
|
952
1006
|
// - omitted + repo-scoped + a CODE-WORK workflow (the def carries at least one
|
|
953
|
-
// `executes_code` phase — feature/bug/migration
|
|
1007
|
+
// `executes_code` phase that is NOT an evaluator — feature/bug/migration and a Tool
|
|
1008
|
+
// phase that writes, not chat/onboarding/recon, and not domain-extraction, whose only
|
|
1009
|
+
// code phase is the `coverage` EVALUATOR, which executes code solely to write its own
|
|
1010
|
+
// report into the tree for its validator — nothing to deliver) ⇒ the
|
|
954
1011
|
// daemon's `deliverDefault` setting ('pr' unless the operator flipped it) — the default
|
|
955
1012
|
// that keeps run 83052f0b's work from stranding invisibly again. The code-work guard is
|
|
956
1013
|
// the issue's own scope ("default deliver:'pr' for CODE-WORK launches"): a read-only
|
|
@@ -972,7 +1029,8 @@ runtime = {}) {
|
|
|
972
1029
|
// Unknown def ⇒ no default (the launch fails at workflow resolution with its own error —
|
|
973
1030
|
// defaulting 'pr' onto it would swap that for a misleading deliver-flavored one).
|
|
974
1031
|
const def = adapter.getWorkflow(b.workflow);
|
|
975
|
-
const codeWork = def !== null &&
|
|
1032
|
+
const codeWork = def !== null &&
|
|
1033
|
+
def.phases.some((p) => p.executes_code === true && p.role !== 'evaluator');
|
|
976
1034
|
deliver =
|
|
977
1035
|
codeWork && (await adapter.getSettings()).deliverDefault !== 'none' ? 'pr' : 'none';
|
|
978
1036
|
deliverDefaulted = true;
|
|
@@ -1177,6 +1235,28 @@ runtime = {}) {
|
|
|
1177
1235
|
return reply.code(404).send({ error: 'Run not found' });
|
|
1178
1236
|
return { run: decorateRun(run) };
|
|
1179
1237
|
});
|
|
1238
|
+
/** The full workflow registry, or `[]` on a directly-driven route set whose fake adapter has none. */
|
|
1239
|
+
const listWorkflowsSafe = () => typeof adapter.listWorkflows === 'function' ? adapter.listWorkflows() : [];
|
|
1240
|
+
// ── Deliver text (crew#524 / F-3R2-014) — the PR title + body a run's delivery carries ──
|
|
1241
|
+
// `gh pr create --fill` gave wicked-studio#249 a mid-word title and an EMPTY body. The deliver
|
|
1242
|
+
// script now asks this route for the text composed from the PERSISTED RUN RECORD: the intent,
|
|
1243
|
+
// `Fixes #N`, the run link, every phase with its seat and gate outcome, the repo checks with
|
|
1244
|
+
// their exit codes, the evaluator verdict (`core/deliver-text.ts`). text/plain, FRAMED as line 1
|
|
1245
|
+
// title, line 2 blank, then the body — the same shape the script's embedded fallback has, so the
|
|
1246
|
+
// script parses exactly one thing. Read-only; the run's `deliver` unit is still running when it
|
|
1247
|
+
// asks, and is listed as `this PR`.
|
|
1248
|
+
app.get(`${V}/runs/:id/deliver-text`, { config: { manifest: { statusCodes: [200, 404] } } }, async (req, reply) => {
|
|
1249
|
+
const { id } = req.params;
|
|
1250
|
+
const views = await adapter.sessionsDetail();
|
|
1251
|
+
const run = views.find((v) => v.session.id === id);
|
|
1252
|
+
if (!run)
|
|
1253
|
+
return reply.code(404).send({ error: 'Run not found' });
|
|
1254
|
+
const origin = boundOrigin(app.server.address());
|
|
1255
|
+
// The workflow DEFINITION (a user-registered workflow's view carries the engine instance id).
|
|
1256
|
+
const def = resolveRunWorkflow(run, listWorkflowsSafe());
|
|
1257
|
+
const text = composeDeliverText(factsFromRun(decorateRun(run), runUrlFor(origin, id), { workflowId: def?.id ?? null }));
|
|
1258
|
+
return reply.type('text/plain; charset=utf-8').send(framedDeliverText(text));
|
|
1259
|
+
});
|
|
1180
1260
|
// ── Post-hoc delivery (crew#393) — lift a stranded run's worktree into a PR ──
|
|
1181
1261
|
// The recovery path for the run 83052f0b class: a COMPLETED repo-scoped run whose reviewable
|
|
1182
1262
|
// work was never lifted (`delivery: 'stranded'` on the wire — including runs recorded long
|
|
@@ -1295,7 +1375,15 @@ runtime = {}) {
|
|
|
1295
1375
|
workdir = reprov.workdir;
|
|
1296
1376
|
cw = reprov.cleanup;
|
|
1297
1377
|
}
|
|
1298
|
-
|
|
1378
|
+
// crew#524: the post-hoc lift composes its PR text from the run record it already
|
|
1379
|
+
// holds (the fallback), and names this daemon so the script can re-ask at delivery.
|
|
1380
|
+
const origin = boundOrigin(app.server.address());
|
|
1381
|
+
const def = resolveRunWorkflow(run, listWorkflowsSafe());
|
|
1382
|
+
result = await deliverExec(workdir, s.problem ?? undefined, {
|
|
1383
|
+
runId: id,
|
|
1384
|
+
apiOrigin: origin,
|
|
1385
|
+
facts: factsFromRun(run, runUrlFor(origin, id), { workflowId: def?.id ?? null }),
|
|
1386
|
+
});
|
|
1299
1387
|
}
|
|
1300
1388
|
finally {
|
|
1301
1389
|
if (cw !== null)
|
|
@@ -1390,28 +1478,27 @@ runtime = {}) {
|
|
|
1390
1478
|
// ── Chat sessions (crew#165): warm ACP seat pool + group fan-out (core#134) ──
|
|
1391
1479
|
// A chat is NOT a run: no council, no gates, no units. Seats warm on open;
|
|
1392
1480
|
// messages fan out to warm seats; replies stream on /ws as chatDelta/chatReply.
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1481
|
+
//
|
|
1482
|
+
// A chat is SCOPED (crew#502, F-067): its seats run in a private scratch root — never a repo,
|
|
1483
|
+
// never the daemon's cwd — with the scoped repos as read roots and, where a graph binds, the
|
|
1484
|
+
// READ-ONLY estate MCP over it (the grounding governed runs get). The scope is resolved and
|
|
1485
|
+
// validated BEFORE any seat warms, stated to the seats in the scratch root's AGENTS.md /
|
|
1486
|
+
// CLAUDE.md, and returned as `scope` (see `chat-scope.ts`).
|
|
1487
|
+
app.post(`${V}/chats`, {
|
|
1488
|
+
config: {
|
|
1489
|
+
manifest: {
|
|
1490
|
+
requestType: 'ChatOpenBody',
|
|
1491
|
+
responseType: 'ChatOpenResponse',
|
|
1492
|
+
statusCodes: [201, 400, 404, 409, 500, 501],
|
|
1493
|
+
},
|
|
1494
|
+
},
|
|
1495
|
+
}, async (req, reply) => {
|
|
1401
1496
|
const parsed = ChatOpenSchema.safeParse(req.body ?? {});
|
|
1402
1497
|
if (!parsed.success) {
|
|
1403
1498
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
1404
1499
|
}
|
|
1405
1500
|
const b = parsed.data;
|
|
1406
1501
|
const chatId = b.chatId ?? randomUUID();
|
|
1407
|
-
let cwd;
|
|
1408
|
-
if (b.repoRef !== undefined) {
|
|
1409
|
-
const repos = await adapter.listRepos();
|
|
1410
|
-
const repo = repos.find((r) => r.id === b.repoRef);
|
|
1411
|
-
if (!repo)
|
|
1412
|
-
return reply.code(404).send({ error: `Repo ${b.repoRef} not found` });
|
|
1413
|
-
cwd = repo.root_path;
|
|
1414
|
-
}
|
|
1415
1502
|
// Validate the project BEFORE opening seats: a chat has no launch record for the engine to
|
|
1416
1503
|
// attach against atomically (chats are an in-memory seat pool), so the route validates
|
|
1417
1504
|
// up-front and attaches right after open — the one non-atomic attach, documented in the ADR
|
|
@@ -1432,32 +1519,169 @@ runtime = {}) {
|
|
|
1432
1519
|
return reply.code(501).send({ error: message(err) });
|
|
1433
1520
|
}
|
|
1434
1521
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1522
|
+
// A chat id this daemon already holds a scope for is LIVE: re-preparing its scratch root and
|
|
1523
|
+
// re-opening it would overwrite the statement its seats are reading and (on a changed scope)
|
|
1524
|
+
// evict them mid-conversation (Copilot, #518). Close it first, or let the daemon mint the id.
|
|
1525
|
+
const token = chatScopes.reserve(chatId);
|
|
1526
|
+
if (token === null) {
|
|
1527
|
+
const state = chatScopes.stateOf(chatId);
|
|
1528
|
+
return reply.code(409).send({
|
|
1529
|
+
error: state === 'closing'
|
|
1530
|
+
? `chat ${chatId} is closing; wait for its chatClosed (a few seconds at most) before reusing the id, or omit chatId to mint a fresh one`
|
|
1531
|
+
: `chat ${chatId} is already open on this daemon; DELETE /chats/${chatId} first, or omit chatId to mint a fresh one`,
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
// Everything below either ends in `chatScopes.set(chatId, …, token)` or releases the reservation.
|
|
1439
1535
|
try {
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1536
|
+
// The scope (crew#502): explicit repos (`repoRefs`, the legacy `repoRef` merged in) or the
|
|
1537
|
+
// project's members — each ref checked against the registry, EVERY missing one named — else
|
|
1538
|
+
// none. Resolved before any seat warms: a 404 here warmed nothing.
|
|
1539
|
+
const resolution = await resolveChatScope({
|
|
1540
|
+
chatId,
|
|
1541
|
+
...(b.projectId !== undefined ? { projectId: b.projectId } : {}),
|
|
1542
|
+
repoRefs: [...(b.repoRef !== undefined ? [b.repoRef] : []), ...(b.repoRefs ?? [])],
|
|
1543
|
+
}, { ...chatScopeDeps(adapter), scratchBase: chatScopes.base, log: (m) => req.log.warn(m) });
|
|
1544
|
+
if (!resolution.ok) {
|
|
1545
|
+
chatScopes.release(chatId, token);
|
|
1546
|
+
return reply.code(resolution.status).send({
|
|
1547
|
+
error: resolution.error,
|
|
1548
|
+
...(resolution.missing !== undefined ? { missing: resolution.missing } : {}),
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
const { scope, engine } = resolution;
|
|
1552
|
+
try {
|
|
1553
|
+
// Cleans up only what it created itself on failure (Copilot, #518).
|
|
1554
|
+
prepareChatScratch(chatId, scope);
|
|
1555
|
+
}
|
|
1556
|
+
catch (err) {
|
|
1557
|
+
chatScopes.release(chatId, token);
|
|
1558
|
+
return reply
|
|
1559
|
+
.code(500)
|
|
1560
|
+
.send({ error: `cannot prepare the chat's scratch root ${scope.cwd}: ${message(err)}` });
|
|
1561
|
+
}
|
|
1562
|
+
// The default seats of a SCOPED chat are pre-filtered by admissibility (independent review,
|
|
1563
|
+
// W5): the engine admits a seat to a scoped chat only when its ACP adapter asks permissions
|
|
1564
|
+
// (`acp_input_governance`) or its record arms the kernel write floor (`os_sandbox`) — with the
|
|
1565
|
+
// default roster that is claude and opencode — so a default project-scoped open does not show
|
|
1566
|
+
// four refused seats. Explicit `clis` are passed through as asked; the engine refuses per seat
|
|
1567
|
+
// with the reason.
|
|
1568
|
+
const scoped = scope.kind !== 'none';
|
|
1569
|
+
const roster = CoreAdapter.roster();
|
|
1570
|
+
const clis = b.clis ??
|
|
1571
|
+
roster
|
|
1572
|
+
.filter((s) => !scoped || s.acp?.acp_input_governance === true || s.acp?.os_sandbox === true)
|
|
1573
|
+
.map((s) => s.key)
|
|
1574
|
+
.filter((k) => typeof k === 'string');
|
|
1575
|
+
if (clis.length === 0) {
|
|
1576
|
+
chatScopes.release(chatId, token);
|
|
1577
|
+
removeChatScratch(scope.cwd, chatScopes.base);
|
|
1578
|
+
return reply.code(409).send({
|
|
1579
|
+
error: 'no seat in the roster can be held to a scoped chat (an ACP adapter admitted to input ' +
|
|
1580
|
+
'governance, or `os_sandbox = true` on its [cli.acp] record); open the chat unscoped, or ' +
|
|
1581
|
+
'name seats with `clis`',
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
try {
|
|
1585
|
+
const seats = await adapter.chatOpen(chatId, clis, engine.cwd, {
|
|
1586
|
+
codeGraphDb: engine.codeGraphDb,
|
|
1587
|
+
readRoots: engine.readRoots,
|
|
1588
|
+
});
|
|
1589
|
+
// Nothing warmed (independent review, W1): the engine holds no pool row and has dropped the
|
|
1590
|
+
// scope itself (no `chatClosed` will come) — report the PER-SEAT reasons, never an
|
|
1591
|
+
// engine-version guess; the root goes, the id is free again.
|
|
1592
|
+
if (seats.every((s) => !s.ok)) {
|
|
1593
|
+
chatScopes.release(chatId, token);
|
|
1594
|
+
removeChatScratch(scope.cwd, chatScopes.base);
|
|
1595
|
+
return reply.code(409).send({
|
|
1596
|
+
error: `chat ${chatId}: no seat warmed (${seats.length} failed) — see seats`,
|
|
1597
|
+
seats,
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
// Honest scope (Copilot, #518): a SCOPED chat is a promise — the roots are read-only, the
|
|
1601
|
+
// graph is attached, the seats see nothing else — and only an engine that CONFIRMS it
|
|
1602
|
+
// recorded the scope (its `chatList` row carries the scope fields) can keep it. An engine
|
|
1603
|
+
// predating chat scope (wicked-core#410) dropped `scopeJson` and runs the seats unbounded in
|
|
1604
|
+
// the scratch root; an engine that cannot be asked has confirmed nothing. Neither may hold a
|
|
1605
|
+
// scoped chat: it is closed again, its root removed, and the caller gets a 501 naming the
|
|
1606
|
+
// remedy — never a chat whose statement promises what its seats do not enforce. An UNSCOPED
|
|
1607
|
+
// chat (`kind: 'none'`) promises nothing beyond its scratch root and proceeds.
|
|
1608
|
+
const applied = await adapter.chatScopeApplied(chatId);
|
|
1609
|
+
if (applied !== true && scoped) {
|
|
1610
|
+
await adapter.chatClose(chatId).catch(() => undefined);
|
|
1611
|
+
// The engine's `chatClosed` for this id is still on its way: park, do not release
|
|
1612
|
+
// (Copilot, #518), so a reuse cannot have its root removed by the late event.
|
|
1613
|
+
chatScopes.abortToClosing(chatId, token);
|
|
1614
|
+
removeChatScratch(scope.cwd, chatScopes.base);
|
|
1615
|
+
if (applied === false) {
|
|
1616
|
+
// A row WITHOUT the scope fields: the engine predates chat scope (wicked-core#410).
|
|
1617
|
+
return reply.code(501).send({
|
|
1618
|
+
error: 'the installed wicked-core-ts predates chat scope (wicked-core#410): it cannot ground ' +
|
|
1619
|
+
'a scoped chat or hold its read roots read-only — upgrade the engine, or open the ' +
|
|
1620
|
+
'chat without projectId/repoRefs.',
|
|
1621
|
+
seats,
|
|
1622
|
+
});
|
|
1447
1623
|
}
|
|
1624
|
+
// NO row although a seat reported warm: nothing is actually held for this chat.
|
|
1625
|
+
return reply.code(409).send({
|
|
1626
|
+
error: `chat ${chatId}: a seat reported warm but the engine holds no row for the chat — ` +
|
|
1627
|
+
'nothing warmed; open it again (see seats)',
|
|
1628
|
+
seats,
|
|
1629
|
+
});
|
|
1448
1630
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1631
|
+
// File the chat into its project WHILE the id is still reserved (Copilot, #518): publishing
|
|
1632
|
+
// first would let a concurrent DELETE / engine `chatClosed` land during this await and leave
|
|
1633
|
+
// a 201 with a stale scope and a membership attached to a closed chat.
|
|
1634
|
+
let projectAttachError;
|
|
1635
|
+
let attachedMemberId;
|
|
1636
|
+
if (b.projectId !== undefined) {
|
|
1637
|
+
try {
|
|
1638
|
+
const { member, created } = await adapter.projectMemberAttach(b.projectId, 'crew.chat', chatId);
|
|
1639
|
+
if (created)
|
|
1640
|
+
attachedMemberId = member.id;
|
|
1641
|
+
if (created) {
|
|
1642
|
+
projects.index.set(chatId, b.projectId);
|
|
1643
|
+
projects.bus?.emit(MEMBERSHIP_ATTACHED, { project_id: b.projectId, member: { kind: 'crew.chat', ref: chatId }, actor: actorOf(req).id }, membershipAttachedKey(b.projectId, 'crew.chat', chatId, member.attached_at));
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
catch (err) {
|
|
1647
|
+
// The chat is open and usable; the filing failed. Said on the 201 rather than failing
|
|
1648
|
+
// the open — the caller can re-attach via POST /projects/:id/members.
|
|
1649
|
+
projectAttachError = message(err);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (!chatScopes.set(chatId, scope, token)) {
|
|
1653
|
+
// The reservation was cancelled while the open was in flight — an engine `chatClosed` or a
|
|
1654
|
+
// `DELETE` for this id (Copilot, #518). Nothing was recorded; tear the chat down (engine
|
|
1655
|
+
// session, scratch root, and the filing just made) instead of returning a stale 201.
|
|
1656
|
+
await adapter.chatClose(chatId).catch(() => undefined);
|
|
1657
|
+
removeChatScratch(scope.cwd, chatScopes.base);
|
|
1658
|
+
if (b.projectId !== undefined && attachedMemberId !== undefined) {
|
|
1659
|
+
await adapter.projectMemberDetach(b.projectId, attachedMemberId).catch(() => false);
|
|
1660
|
+
projects.index.delete(chatId);
|
|
1661
|
+
}
|
|
1662
|
+
return reply.code(409).send({
|
|
1663
|
+
error: `chat ${chatId} was closed while it was being opened; open it again`,
|
|
1664
|
+
});
|
|
1455
1665
|
}
|
|
1666
|
+
return reply.code(201).send({
|
|
1667
|
+
chatId,
|
|
1668
|
+
seats,
|
|
1669
|
+
scope,
|
|
1670
|
+
...(projectAttachError !== undefined ? { projectAttachError } : {}),
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
catch (err) {
|
|
1674
|
+
// Nothing warmed: the scratch root prepared above must not linger — removed under the SAME
|
|
1675
|
+
// base the resolver created it in (Copilot, #518), fail-closed like every removal.
|
|
1676
|
+
chatScopes.release(chatId, token);
|
|
1677
|
+
removeChatScratch(scope.cwd, chatScopes.base);
|
|
1678
|
+
return reply.code(400).send({ error: message(err) });
|
|
1456
1679
|
}
|
|
1457
|
-
return reply.code(201).send({ chatId, seats });
|
|
1458
1680
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1681
|
+
finally {
|
|
1682
|
+
// A reservation that never became a scope (any early return above) must not pin the id —
|
|
1683
|
+
// token-guarded, so a reservation that is no longer ours is left alone.
|
|
1684
|
+
chatScopes.release(chatId, token);
|
|
1461
1685
|
}
|
|
1462
1686
|
});
|
|
1463
1687
|
const ChatMessageSchema = z.object({
|
|
@@ -1484,7 +1708,7 @@ runtime = {}) {
|
|
|
1484
1708
|
// lived in the tab that abandoned it, and an operator could not reclaim one without restarting
|
|
1485
1709
|
// the daemon. Registered BEFORE `/chats/:id` is irrelevant to fastify (it routes on the literal
|
|
1486
1710
|
// segment first), but the order reads the way the routes nest.
|
|
1487
|
-
app.get(`${V}/chats`, async (_req, reply) => {
|
|
1711
|
+
app.get(`${V}/chats`, { config: { manifest: { responseType: 'ChatListResponse', statusCodes: [200, 400, 501] } } }, async (_req, reply) => {
|
|
1488
1712
|
try {
|
|
1489
1713
|
return { chats: await adapter.chatList() };
|
|
1490
1714
|
}
|
|
@@ -1499,10 +1723,11 @@ runtime = {}) {
|
|
|
1499
1723
|
.send({ error: message(err) });
|
|
1500
1724
|
}
|
|
1501
1725
|
});
|
|
1502
|
-
app.get(`${V}/chats/:id`, async (req, reply) => {
|
|
1726
|
+
app.get(`${V}/chats/:id`, { config: { manifest: { responseType: 'ChatDetailResponse', statusCodes: [200, 400] } } }, async (req, reply) => {
|
|
1503
1727
|
const { id } = req.params;
|
|
1504
1728
|
try {
|
|
1505
|
-
|
|
1729
|
+
// The scope recorded at open (crew#502) — `null` for a chat this daemon did not open.
|
|
1730
|
+
return { chatId: id, seats: await adapter.chatSeats(id), scope: chatScopes.get(id) ?? null };
|
|
1506
1731
|
}
|
|
1507
1732
|
catch (err) {
|
|
1508
1733
|
return reply.code(400).send({ error: message(err) });
|
|
@@ -1510,6 +1735,10 @@ runtime = {}) {
|
|
|
1510
1735
|
});
|
|
1511
1736
|
app.delete(`${V}/chats/:id`, async (req, reply) => {
|
|
1512
1737
|
const { id } = req.params;
|
|
1738
|
+
// The scratch root goes with the chat at once, whether or not the engine still knew it
|
|
1739
|
+
// (crew#502); the id stays parked until the engine's `chatClosed` is observed, so a delayed
|
|
1740
|
+
// close can never land on a chat that reused it (Copilot, #518).
|
|
1741
|
+
chatScopes.beginClose(id);
|
|
1513
1742
|
try {
|
|
1514
1743
|
await adapter.chatClose(id);
|
|
1515
1744
|
return { ok: true };
|
|
@@ -2407,7 +2636,9 @@ runtime = {}) {
|
|
|
2407
2636
|
if (!repo)
|
|
2408
2637
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
2409
2638
|
const graphPath = requirementsGraph(repo);
|
|
2410
|
-
const dbPath =
|
|
2639
|
+
const dbPath = codeGraphDbOr503(repo, reply);
|
|
2640
|
+
if (dbPath === null)
|
|
2641
|
+
return reply;
|
|
2411
2642
|
// Coverage from the live estate store — computed by wicked-core governance layer.
|
|
2412
2643
|
let coverage = null;
|
|
2413
2644
|
if (existsSync(dbPath)) {
|
|
@@ -2465,7 +2696,17 @@ runtime = {}) {
|
|
|
2465
2696
|
if (!parsed.success) {
|
|
2466
2697
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid query'));
|
|
2467
2698
|
}
|
|
2468
|
-
|
|
2699
|
+
let page;
|
|
2700
|
+
try {
|
|
2701
|
+
page = await listRequirements(repo, parsed.data);
|
|
2702
|
+
}
|
|
2703
|
+
catch (err) {
|
|
2704
|
+
// wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
|
|
2705
|
+
const status = codeGraphErrorStatus(err);
|
|
2706
|
+
if (status === null)
|
|
2707
|
+
throw err;
|
|
2708
|
+
return reply.code(status).send({ error: message(err) });
|
|
2709
|
+
}
|
|
2469
2710
|
if (page === null) {
|
|
2470
2711
|
return reply.code(404).send({ error: 'requirements_graph.json not generated for this repo yet' });
|
|
2471
2712
|
}
|
|
@@ -2490,7 +2731,17 @@ runtime = {}) {
|
|
|
2490
2731
|
catch {
|
|
2491
2732
|
return reply.code(400).send({ error: 'Malformed requirement key encoding' });
|
|
2492
2733
|
}
|
|
2493
|
-
|
|
2734
|
+
let detail;
|
|
2735
|
+
try {
|
|
2736
|
+
detail = await getRequirement(repo, decoded);
|
|
2737
|
+
}
|
|
2738
|
+
catch (err) {
|
|
2739
|
+
// wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
|
|
2740
|
+
const status = codeGraphErrorStatus(err);
|
|
2741
|
+
if (status === null)
|
|
2742
|
+
throw err;
|
|
2743
|
+
return reply.code(status).send({ error: message(err) });
|
|
2744
|
+
}
|
|
2494
2745
|
if (detail === null)
|
|
2495
2746
|
return reply.code(404).send({ error: 'Requirement not found' });
|
|
2496
2747
|
return { requirement: detail };
|
|
@@ -2525,7 +2776,17 @@ runtime = {}) {
|
|
|
2525
2776
|
catch {
|
|
2526
2777
|
return reply.code(400).send({ error: 'Malformed requirement key encoding' });
|
|
2527
2778
|
}
|
|
2528
|
-
|
|
2779
|
+
let detail;
|
|
2780
|
+
try {
|
|
2781
|
+
detail = await patchRequirement(repo, decodedKey, parsed.data);
|
|
2782
|
+
}
|
|
2783
|
+
catch (err) {
|
|
2784
|
+
// wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
|
|
2785
|
+
const status = codeGraphErrorStatus(err);
|
|
2786
|
+
if (status === null)
|
|
2787
|
+
throw err;
|
|
2788
|
+
return reply.code(status).send({ error: message(err) });
|
|
2789
|
+
}
|
|
2529
2790
|
if (detail === null)
|
|
2530
2791
|
return reply.code(404).send({ error: 'Requirement not found' });
|
|
2531
2792
|
return { requirement: detail };
|
|
@@ -2536,7 +2797,9 @@ runtime = {}) {
|
|
|
2536
2797
|
const repo = repos.find((r) => r.id === id);
|
|
2537
2798
|
if (!repo)
|
|
2538
2799
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
2539
|
-
const dbPath =
|
|
2800
|
+
const dbPath = codeGraphDbOr503(repo, reply);
|
|
2801
|
+
if (dbPath === null)
|
|
2802
|
+
return reply;
|
|
2540
2803
|
if (!existsSync(dbPath)) {
|
|
2541
2804
|
return reply.send({ graph: null });
|
|
2542
2805
|
}
|
|
@@ -2587,7 +2850,9 @@ runtime = {}) {
|
|
|
2587
2850
|
const repo = repos.find((r) => r.id === id);
|
|
2588
2851
|
if (!repo)
|
|
2589
2852
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
2590
|
-
const dbPath =
|
|
2853
|
+
const dbPath = codeGraphDbOr503(repo, reply);
|
|
2854
|
+
if (dbPath === null)
|
|
2855
|
+
return reply;
|
|
2591
2856
|
if (!existsSync(dbPath)) {
|
|
2592
2857
|
return reply.code(404).send({ error: 'Code graph not built for this repo yet' });
|
|
2593
2858
|
}
|
|
@@ -3239,10 +3504,13 @@ runtime = {}) {
|
|
|
3239
3504
|
// pool POSTs it to the bridge's /api/studio-origin on start/adopt so the bridge's
|
|
3240
3505
|
// `GET /` redirects into studio.
|
|
3241
3506
|
studioOrigin: () => boundOrigin(app.server.address()),
|
|
3507
|
+
// F-043: the bridge emits where this daemon's interactive seams read — one bus per daemon.
|
|
3508
|
+
busDataDir: runtime.interactiveBridgeBusDataDir ?? null,
|
|
3242
3509
|
});
|
|
3243
3510
|
registerInteractiveProxy(app, adapter, {
|
|
3244
3511
|
settings: projectSettings,
|
|
3245
3512
|
pool: interactiveBridges,
|
|
3513
|
+
...(runtime.docGrounding !== undefined ? { grounding: runtime.docGrounding } : {}),
|
|
3246
3514
|
log: (m) => app.log.warn(m),
|
|
3247
3515
|
});
|
|
3248
3516
|
// ── Governed doc delete (crew#338) — the one door that changes BOTH stores ──
|