wicked-crew 0.7.28 → 0.7.30
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 +12 -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 +728 -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 +171 -0
- package/dist/api/governance-health.d.ts.map +1 -0
- package/dist/api/governance-health.js +379 -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 +70 -12
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +375 -60
- package/dist/api/routes.js.map +1 -1
- package/dist/api/seat-health.d.ts +38 -0
- package/dist/api/seat-health.d.ts.map +1 -1
- package/dist/api/seat-health.js +88 -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/seat-standing.d.ts +101 -0
- package/dist/api/seat-standing.d.ts.map +1 -0
- package/dist/api/seat-standing.js +138 -0
- package/dist/api/seat-standing.js.map +1 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +37 -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 +102 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +99 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +149 -8
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-path.d.ts +21 -0
- package/dist/core/bridge-path.d.ts.map +1 -1
- package/dist/core/bridge-path.js +37 -0
- package/dist/core/bridge-path.js.map +1 -1
- package/dist/core/bridge-reaper.d.ts.map +1 -1
- package/dist/core/bridge-reaper.js +11 -4
- 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 +103 -9
- package/dist/core/deliver.d.ts.map +1 -1
- package/dist/core/deliver.js +244 -46
- package/dist/core/deliver.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 +219 -0
- package/dist/core/governance-store.d.ts.map +1 -0
- package/dist/core/governance-store.js +290 -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 +29 -24
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +74 -7
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/chat-events.d.ts.map +1 -1
- package/dist/interactive/chat-events.js +5 -2
- package/dist/interactive/chat-events.js.map +1 -1
- package/dist/interactive/council-outcome.d.ts +28 -0
- package/dist/interactive/council-outcome.d.ts.map +1 -0
- package/dist/interactive/council-outcome.js +57 -0
- package/dist/interactive/council-outcome.js.map +1 -0
- package/dist/interactive/demo-events.d.ts.map +1 -1
- package/dist/interactive/demo-events.js +5 -2
- package/dist/interactive/demo-events.js.map +1 -1
- package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
- package/dist/interactive/doc-delete-routes.js +3 -2
- package/dist/interactive/doc-delete-routes.js.map +1 -1
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +5 -2
- 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 +5 -2
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/projects/auto-refresh.d.ts +54 -0
- package/dist/projects/auto-refresh.d.ts.map +1 -0
- package/dist/projects/auto-refresh.js +76 -0
- package/dist/projects/auto-refresh.js.map +1 -0
- 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 +20 -6
- package/dist/projects/graph.d.ts.map +1 -1
- package/dist/projects/graph.js +40 -13
- package/dist/projects/graph.js.map +1 -1
- package/dist/projects/routes.d.ts.map +1 -1
- package/dist/projects/routes.js +30 -11
- package/dist/projects/routes.js.map +1 -1
- package/dist/projects/state-home.d.ts +6 -0
- package/dist/projects/state-home.d.ts.map +1 -1
- package/dist/projects/state-home.js +9 -1
- package/dist/projects/state-home.js.map +1 -1
- package/dist/skills/guards.d.ts +17 -6
- package/dist/skills/guards.d.ts.map +1 -1
- package/dist/skills/guards.js +42 -15
- package/dist/skills/guards.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/skills/refs.d.ts +181 -17
- package/dist/skills/refs.d.ts.map +1 -1
- package/dist/skills/refs.js +345 -26
- package/dist/skills/refs.js.map +1 -1
- package/dist/skills/store.d.ts +22 -1
- package/dist/skills/store.d.ts.map +1 -1
- package/dist/skills/store.js +161 -23
- package/dist/skills/store.js.map +1 -1
- package/dist/studio/assets/index-CWUS9Ump.js +542 -0
- package/dist/studio/assets/{index-CK1uJWs2.css → index-_ydd-jKd.css} +1 -1
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +567 -6
- package/endpoint-manifest.json +67 -14
- package/package.json +4 -4
- package/dist/studio/assets/index-CdmCMh2f.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,8 @@ 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 { chatSeatAdmission, seatStanding } from './seat-standing.js';
|
|
21
|
+
import { ChatScopeIndex, chatScopeDeps, prepareChatScratch, removeChatScratch, resolveChatScope, } from './chat-scope.js';
|
|
20
22
|
import { allowedRootsFor, isInsideRoot, openWithSystemDefault } from './open-path.js';
|
|
21
23
|
import { InvalidDiffBaseError, NotARegularFileError, UnresolvableDiffBaseError, readFileCapped, worktreeDiff, } from './run-files.js';
|
|
22
24
|
import { resolveProjectGraphBinding } from '../projects/graph.js';
|
|
@@ -30,6 +32,7 @@ import { registerSkillsRoutes } from './skills.js';
|
|
|
30
32
|
import { disabledSkillsHealth } from '../skills/runtime.js';
|
|
31
33
|
import { ProjectSettingsStore } from '../projects/settings.js';
|
|
32
34
|
import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
|
|
35
|
+
import { composeDeliverText, factsFromRun, framedDeliverText, runUrlFor } from '../core/deliver-text.js';
|
|
33
36
|
import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
|
|
34
37
|
import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
|
|
35
38
|
import { registerInteractiveDocList } from '../interactive/doc-list-routes.js';
|
|
@@ -37,6 +40,8 @@ import { MembershipIndex } from '../projects/membership-index.js';
|
|
|
37
40
|
import { MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.js';
|
|
38
41
|
import { AuditLog } from './audit.js';
|
|
39
42
|
import { AcpFoldCache, EngineVersionCache, eventsDirOf, installedPackageVersion, listStoreFiles, readStudioBundleVersion, } from './diagnostics.js';
|
|
43
|
+
import { GovernanceDiagnostics } from './governance-health.js';
|
|
44
|
+
import { legacyHomeOutboxPath } from '../core/governance-store.js';
|
|
40
45
|
import { RetryIndex } from './retry-index.js';
|
|
41
46
|
import { GroupIndex } from './group-index.js';
|
|
42
47
|
import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
|
|
@@ -69,6 +74,18 @@ const STATUS_ORDER = {
|
|
|
69
74
|
function sortActionableFirst(views) {
|
|
70
75
|
return [...views].sort((a, b) => (STATUS_ORDER[a.session.status] ?? 9) - (STATUS_ORDER[b.session.status] ?? 9));
|
|
71
76
|
}
|
|
77
|
+
function codeGraphDbOr503(repo, reply) {
|
|
78
|
+
try {
|
|
79
|
+
return codeGraphDb(repo);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const status = codeGraphErrorStatus(err);
|
|
83
|
+
if (status === null)
|
|
84
|
+
throw err;
|
|
85
|
+
void reply.code(status).send({ error: message(err) });
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
72
89
|
function message(err) {
|
|
73
90
|
return err instanceof Error ? err.message : String(err);
|
|
74
91
|
}
|
|
@@ -411,6 +428,21 @@ const STUDIO_SETTINGS_MAX_BYTES = 512 * 1024;
|
|
|
411
428
|
* The daemon REST surface. Every endpoint is a thin wrapper over one adapter /
|
|
412
429
|
* core-ts call (DES-STUDIO-001 §2). `session`/`phase` nouns are now `run`/`unit`.
|
|
413
430
|
*/
|
|
431
|
+
/**
|
|
432
|
+
* `POST /chats` body (crew#165; `projectId` DES-PROJECT-001 §2.2; `repoRefs` + scope semantics
|
|
433
|
+
* crew#502). Exported for the wire-contract drift guard (`tests/wire-contract.test.ts`).
|
|
434
|
+
*/
|
|
435
|
+
export const ChatOpenSchema = z.object({
|
|
436
|
+
chatId: z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
437
|
+
clis: z.array(z.string().min(1)).min(1).max(8).optional(),
|
|
438
|
+
/** One repo in scope — the legacy spelling, merged into `repoRefs` (crew#502). */
|
|
439
|
+
repoRef: z.string().min(1).optional(),
|
|
440
|
+
/** The repos in scope, by registry id or name (crew#502). */
|
|
441
|
+
repoRefs: z.array(z.string().min(1)).min(1).max(32).optional(),
|
|
442
|
+
/** File the chat into a project (`crew.chat` membership) — and, with no `repoRefs`, scope it to
|
|
443
|
+
* every registered `crew.repo` member of that project (crew#502). */
|
|
444
|
+
projectId: z.string().min(1).optional(),
|
|
445
|
+
}).strict();
|
|
414
446
|
export function registerRoutes(app, adapter, gateCache, elicitationCache,
|
|
415
447
|
// Defaulted so a caller that never arms the bus seam (tests drive this
|
|
416
448
|
// function directly) gets the same behavior as an unarmed daemon: an empty
|
|
@@ -435,6 +467,7 @@ runtime = {}) {
|
|
|
435
467
|
const groupIndex = runtime.groupIndex ?? new GroupIndex();
|
|
436
468
|
const runTimingIndex = runtime.runTimingIndex ?? new RunTimingIndex();
|
|
437
469
|
const guidanceIndex = runtime.guidanceIndex ?? new GuidanceIndex();
|
|
470
|
+
const chatScopes = runtime.chatScopes ?? new ChatScopeIndex();
|
|
438
471
|
const deliveryIndex = runtime.deliveryIndex ?? new DeliveryIndex();
|
|
439
472
|
const worktreeExists = runtime.worktreeExists ?? ((p) => existsSync(p));
|
|
440
473
|
const vacuityProbes = {
|
|
@@ -457,6 +490,12 @@ runtime = {}) {
|
|
|
457
490
|
isDelivered: (runId) => deliveryIndex.urlFor(runId) !== undefined,
|
|
458
491
|
});
|
|
459
492
|
const deliverExec = runtime.deliverExec ?? runDeliverScript;
|
|
493
|
+
// crew#524: the deliver phase's script asks THIS daemon for the run-derived PR text, and the PR
|
|
494
|
+
// body links the run here — so the adapter learns the bound origin lazily (the server has not
|
|
495
|
+
// listened yet when routes register). Guarded: directly-driven route sets hand in fakes.
|
|
496
|
+
if (typeof adapter.setDeliverApiOrigin === 'function') {
|
|
497
|
+
adapter.setDeliverApiOrigin(() => boundOrigin(app.server.address()));
|
|
498
|
+
}
|
|
460
499
|
const reprovisionWorktree = runtime.reprovisionWorktree ?? gitReprovisionWorktree;
|
|
461
500
|
// The estate MCP client behind `/proposals*` AND `/memory*` (DES-MEM-FACETED-001) — one seam. The
|
|
462
501
|
// default is the real spawn-per-call `wicked-estate-mcp` client; route tests inject a stub so no
|
|
@@ -572,16 +611,22 @@ runtime = {}) {
|
|
|
572
611
|
// can never turn this GET into an event-log re-reader or a `--version` spawner per request.
|
|
573
612
|
const acpFoldCache = new AcpFoldCache();
|
|
574
613
|
const engineVersionCache = new EngineVersionCache();
|
|
614
|
+
// The governance block (crew#495): the store the engine's emit seam writes to (as the adapter
|
|
615
|
+
// exported it — `null` on a boot that resolved none), its record count through the engine
|
|
616
|
+
// binding when the addon has one, and the dead-letter outbox folded on change. Built at boot so
|
|
617
|
+
// the record baseline is the boot baseline.
|
|
618
|
+
const governanceDiagnostics = new GovernanceDiagnostics(adapter.governanceStore ?? null, CoreAdapter.eventStoreCounter(), runtime.governanceLegacyOutboxPath === undefined ? legacyHomeOutboxPath() : runtime.governanceLegacyOutboxPath);
|
|
575
619
|
app.get(`${V}/diagnostics`, { config: { manifest: { responseType: 'DiagnosticsResponse', statusCodes: [200] } } }, async () => {
|
|
576
620
|
// `dbPath` is a readonly field of the real adapter; a stub-driven route set may lack it,
|
|
577
621
|
// and diagnostics over an unknown store honestly reports no stores and no ACP record.
|
|
578
622
|
const dbPath = typeof adapter.dbPath === 'string' && adapter.dbPath !== '' ? adapter.dbPath : null;
|
|
579
|
-
const [stores, byCli, engineBinaries] = await Promise.all([
|
|
623
|
+
const [stores, byCli, engineBinaries, governance] = await Promise.all([
|
|
580
624
|
dbPath !== null ? listStoreFiles(dbPath) : Promise.resolve([]),
|
|
581
625
|
dbPath !== null
|
|
582
626
|
? acpFoldCache.get(eventsDirOf(dbPath))
|
|
583
627
|
: Promise.resolve({}),
|
|
584
628
|
engineVersionCache.get(),
|
|
629
|
+
governanceDiagnostics.health(),
|
|
585
630
|
]);
|
|
586
631
|
const uptimeMs = Math.round(process.uptime() * 1000);
|
|
587
632
|
const addr = app.server.address();
|
|
@@ -601,6 +646,9 @@ runtime = {}) {
|
|
|
601
646
|
// config-error, with the `skills.*` findings the ladder produced — the operator's one
|
|
602
647
|
// read-only answer to "why do launches refuse the snapshot".
|
|
603
648
|
skills: runtime.skills?.health() ?? disabledSkillsHealth(),
|
|
649
|
+
// Is the governance evidence LANDING (crew#495): the store, the records on it, the dead
|
|
650
|
+
// letters — with a `governance.deadletter` finding the moment the outbox holds one.
|
|
651
|
+
governance,
|
|
604
652
|
};
|
|
605
653
|
});
|
|
606
654
|
// Who am I talking to the daemon as? (task #88). In local mode this is
|
|
@@ -653,16 +701,29 @@ runtime = {}) {
|
|
|
653
701
|
// gains `signed_in`: the cheap file/env presence heuristic, computed against the LIVE
|
|
654
702
|
// `WICKED_WORKER_HOME` env — the same value the engine reads at the next worker spawn, kept
|
|
655
703
|
// current by `applyWorkerConfigRoot` at boot and on every settings change.
|
|
656
|
-
|
|
704
|
+
//
|
|
705
|
+
// Each seat ALSO carries its STANDING (F-2R2-009): `auth` re-reads `signed_in` for what it means
|
|
706
|
+
// (a `signed_out` codex is benched on its first ballot; a `not_required` opencode answers on its
|
|
707
|
+
// free tier), and `council_eligible` says what a council would do with the seat as far as the
|
|
708
|
+
// daemon can tell. `POST /chats` seats its defaults through the SAME standing, so the roster and
|
|
709
|
+
// the chat never disagree about a seat.
|
|
710
|
+
const rosterWithStanding = () => {
|
|
657
711
|
const workerRoot = process.env['WICKED_WORKER_HOME'];
|
|
658
|
-
return {
|
|
659
|
-
|
|
712
|
+
return CoreAdapter.roster().map((seat) => {
|
|
713
|
+
const key = String(seat.key);
|
|
714
|
+
const health = seatHealth.healthFor(key);
|
|
715
|
+
const signed = signedIn(key, workerRoot === '' ? undefined : workerRoot);
|
|
716
|
+
return {
|
|
660
717
|
...seat,
|
|
661
|
-
health
|
|
662
|
-
signed_in:
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
718
|
+
health,
|
|
719
|
+
signed_in: signed,
|
|
720
|
+
// The bench is THIS daemon's council evidence (councilSeatFailed, bounded window) — the
|
|
721
|
+
// prediction learns from what the engine actually did with the seat (#533 review, F-1).
|
|
722
|
+
...seatStanding(seat, signed, health, seatHealth.councilBenchFor(key)),
|
|
723
|
+
};
|
|
724
|
+
});
|
|
725
|
+
};
|
|
726
|
+
app.get(`${V}/roster`, async () => ({ roster: rosterWithStanding() }));
|
|
666
727
|
// Open a file/folder with the OS default application (crew#273) — the studio Files tab's
|
|
667
728
|
// click-to-open. The open MUST happen daemon-side (the SPA cannot spawn a process), which is
|
|
668
729
|
// why the path is validated first: absolute, and inside one of the caller-visible roots — the
|
|
@@ -1181,6 +1242,28 @@ runtime = {}) {
|
|
|
1181
1242
|
return reply.code(404).send({ error: 'Run not found' });
|
|
1182
1243
|
return { run: decorateRun(run) };
|
|
1183
1244
|
});
|
|
1245
|
+
/** The full workflow registry, or `[]` on a directly-driven route set whose fake adapter has none. */
|
|
1246
|
+
const listWorkflowsSafe = () => typeof adapter.listWorkflows === 'function' ? adapter.listWorkflows() : [];
|
|
1247
|
+
// ── Deliver text (crew#524 / F-3R2-014) — the PR title + body a run's delivery carries ──
|
|
1248
|
+
// `gh pr create --fill` gave wicked-studio#249 a mid-word title and an EMPTY body. The deliver
|
|
1249
|
+
// script now asks this route for the text composed from the PERSISTED RUN RECORD: the intent,
|
|
1250
|
+
// `Fixes #N`, the run link, every phase with its seat and gate outcome, the repo checks with
|
|
1251
|
+
// their exit codes, the evaluator verdict (`core/deliver-text.ts`). text/plain, FRAMED as line 1
|
|
1252
|
+
// title, line 2 blank, then the body — the same shape the script's embedded fallback has, so the
|
|
1253
|
+
// script parses exactly one thing. Read-only; the run's `deliver` unit is still running when it
|
|
1254
|
+
// asks, and is listed as `this PR`.
|
|
1255
|
+
app.get(`${V}/runs/:id/deliver-text`, { config: { manifest: { statusCodes: [200, 404] } } }, async (req, reply) => {
|
|
1256
|
+
const { id } = req.params;
|
|
1257
|
+
const views = await adapter.sessionsDetail();
|
|
1258
|
+
const run = views.find((v) => v.session.id === id);
|
|
1259
|
+
if (!run)
|
|
1260
|
+
return reply.code(404).send({ error: 'Run not found' });
|
|
1261
|
+
const origin = boundOrigin(app.server.address());
|
|
1262
|
+
// The workflow DEFINITION (a user-registered workflow's view carries the engine instance id).
|
|
1263
|
+
const def = resolveRunWorkflow(run, listWorkflowsSafe());
|
|
1264
|
+
const text = composeDeliverText(factsFromRun(decorateRun(run), runUrlFor(origin, id), { workflowId: def?.id ?? null }));
|
|
1265
|
+
return reply.type('text/plain; charset=utf-8').send(framedDeliverText(text));
|
|
1266
|
+
});
|
|
1184
1267
|
// ── Post-hoc delivery (crew#393) — lift a stranded run's worktree into a PR ──
|
|
1185
1268
|
// The recovery path for the run 83052f0b class: a COMPLETED repo-scoped run whose reviewable
|
|
1186
1269
|
// work was never lifted (`delivery: 'stranded'` on the wire — including runs recorded long
|
|
@@ -1299,7 +1382,15 @@ runtime = {}) {
|
|
|
1299
1382
|
workdir = reprov.workdir;
|
|
1300
1383
|
cw = reprov.cleanup;
|
|
1301
1384
|
}
|
|
1302
|
-
|
|
1385
|
+
// crew#524: the post-hoc lift composes its PR text from the run record it already
|
|
1386
|
+
// holds (the fallback), and names this daemon so the script can re-ask at delivery.
|
|
1387
|
+
const origin = boundOrigin(app.server.address());
|
|
1388
|
+
const def = resolveRunWorkflow(run, listWorkflowsSafe());
|
|
1389
|
+
result = await deliverExec(workdir, s.problem ?? undefined, {
|
|
1390
|
+
runId: id,
|
|
1391
|
+
apiOrigin: origin,
|
|
1392
|
+
facts: factsFromRun(run, runUrlFor(origin, id), { workflowId: def?.id ?? null }),
|
|
1393
|
+
});
|
|
1303
1394
|
}
|
|
1304
1395
|
finally {
|
|
1305
1396
|
if (cw !== null)
|
|
@@ -1394,28 +1485,27 @@ runtime = {}) {
|
|
|
1394
1485
|
// ── Chat sessions (crew#165): warm ACP seat pool + group fan-out (core#134) ──
|
|
1395
1486
|
// A chat is NOT a run: no council, no gates, no units. Seats warm on open;
|
|
1396
1487
|
// messages fan out to warm seats; replies stream on /ws as chatDelta/chatReply.
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1488
|
+
//
|
|
1489
|
+
// A chat is SCOPED (crew#502, F-067): its seats run in a private scratch root — never a repo,
|
|
1490
|
+
// never the daemon's cwd — with the scoped repos as read roots and, where a graph binds, the
|
|
1491
|
+
// READ-ONLY estate MCP over it (the grounding governed runs get). The scope is resolved and
|
|
1492
|
+
// validated BEFORE any seat warms, stated to the seats in the scratch root's AGENTS.md /
|
|
1493
|
+
// CLAUDE.md, and returned as `scope` (see `chat-scope.ts`).
|
|
1494
|
+
app.post(`${V}/chats`, {
|
|
1495
|
+
config: {
|
|
1496
|
+
manifest: {
|
|
1497
|
+
requestType: 'ChatOpenBody',
|
|
1498
|
+
responseType: 'ChatOpenResponse',
|
|
1499
|
+
statusCodes: [201, 400, 404, 409, 500, 501],
|
|
1500
|
+
},
|
|
1501
|
+
},
|
|
1502
|
+
}, async (req, reply) => {
|
|
1405
1503
|
const parsed = ChatOpenSchema.safeParse(req.body ?? {});
|
|
1406
1504
|
if (!parsed.success) {
|
|
1407
1505
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
1408
1506
|
}
|
|
1409
1507
|
const b = parsed.data;
|
|
1410
1508
|
const chatId = b.chatId ?? randomUUID();
|
|
1411
|
-
let cwd;
|
|
1412
|
-
if (b.repoRef !== undefined) {
|
|
1413
|
-
const repos = await adapter.listRepos();
|
|
1414
|
-
const repo = repos.find((r) => r.id === b.repoRef);
|
|
1415
|
-
if (!repo)
|
|
1416
|
-
return reply.code(404).send({ error: `Repo ${b.repoRef} not found` });
|
|
1417
|
-
cwd = repo.root_path;
|
|
1418
|
-
}
|
|
1419
1509
|
// Validate the project BEFORE opening seats: a chat has no launch record for the engine to
|
|
1420
1510
|
// attach against atomically (chats are an in-memory seat pool), so the route validates
|
|
1421
1511
|
// up-front and attaches right after open — the one non-atomic attach, documented in the ADR
|
|
@@ -1436,32 +1526,205 @@ runtime = {}) {
|
|
|
1436
1526
|
return reply.code(501).send({ error: message(err) });
|
|
1437
1527
|
}
|
|
1438
1528
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1529
|
+
// A chat id this daemon already holds a scope for is LIVE: re-preparing its scratch root and
|
|
1530
|
+
// re-opening it would overwrite the statement its seats are reading and (on a changed scope)
|
|
1531
|
+
// evict them mid-conversation (Copilot, #518). Close it first, or let the daemon mint the id.
|
|
1532
|
+
const token = chatScopes.reserve(chatId);
|
|
1533
|
+
if (token === null) {
|
|
1534
|
+
const state = chatScopes.stateOf(chatId);
|
|
1535
|
+
return reply.code(409).send({
|
|
1536
|
+
error: state === 'closing'
|
|
1537
|
+
? `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`
|
|
1538
|
+
: `chat ${chatId} is already open on this daemon; DELETE /chats/${chatId} first, or omit chatId to mint a fresh one`,
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
// Everything below either ends in `chatScopes.set(chatId, …, token)` or releases the reservation.
|
|
1443
1542
|
try {
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1543
|
+
// The scope (crew#502): explicit repos (`repoRefs`, the legacy `repoRef` merged in) or the
|
|
1544
|
+
// project's members — each ref checked against the registry, EVERY missing one named — else
|
|
1545
|
+
// none. Resolved before any seat warms: a 404 here warmed nothing.
|
|
1546
|
+
const resolution = await resolveChatScope({
|
|
1547
|
+
chatId,
|
|
1548
|
+
...(b.projectId !== undefined ? { projectId: b.projectId } : {}),
|
|
1549
|
+
repoRefs: [...(b.repoRef !== undefined ? [b.repoRef] : []), ...(b.repoRefs ?? [])],
|
|
1550
|
+
}, { ...chatScopeDeps(adapter), scratchBase: chatScopes.base, log: (m) => req.log.warn(m) });
|
|
1551
|
+
if (!resolution.ok) {
|
|
1552
|
+
chatScopes.release(chatId, token);
|
|
1553
|
+
return reply.code(resolution.status).send({
|
|
1554
|
+
error: resolution.error,
|
|
1555
|
+
...(resolution.missing !== undefined ? { missing: resolution.missing } : {}),
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
const { scope, engine } = resolution;
|
|
1559
|
+
try {
|
|
1560
|
+
// Cleans up only what it created itself on failure (Copilot, #518).
|
|
1561
|
+
prepareChatScratch(chatId, scope);
|
|
1562
|
+
}
|
|
1563
|
+
catch (err) {
|
|
1564
|
+
chatScopes.release(chatId, token);
|
|
1565
|
+
return reply
|
|
1566
|
+
.code(500)
|
|
1567
|
+
.send({ error: `cannot prepare the chat's scratch root ${scope.cwd}: ${message(err)}` });
|
|
1568
|
+
}
|
|
1569
|
+
// The DEFAULT seats are the roster seats the daemon's own admission admits (F-2R2-007,
|
|
1570
|
+
// F-2R2-009): the seat's auth standing (a signed-out seat is refused up front, with the
|
|
1571
|
+
// reason, instead of failing its first turn — the same predicate `GET /roster` reports as
|
|
1572
|
+
// `council_eligible`) and, for a SCOPED chat, the engine's rule restated (only a seat whose
|
|
1573
|
+
// ACP adapter asks permissions or whose record arms the kernel write floor can be held to
|
|
1574
|
+
// read-only roots — with the default roster that is claude and opencode). Every seat dropped
|
|
1575
|
+
// here is NAMED on the response (`refused`) and in the thread (`chatSeatRefused`), so a
|
|
1576
|
+
// person can see why pi is missing. Explicit `clis` are passed through as asked; the engine
|
|
1577
|
+
// refuses per seat with its reason, which is copied into `refused` too.
|
|
1578
|
+
const scoped = scope.kind !== 'none';
|
|
1579
|
+
const refused = [];
|
|
1580
|
+
let clis;
|
|
1581
|
+
if (b.clis !== undefined) {
|
|
1582
|
+
clis = b.clis;
|
|
1583
|
+
}
|
|
1584
|
+
else {
|
|
1585
|
+
clis = [];
|
|
1586
|
+
for (const seat of rosterWithStanding()) {
|
|
1587
|
+
const key = String(seat.key);
|
|
1588
|
+
const admission = chatSeatAdmission(seat, seat.auth ?? 'unknown', scoped);
|
|
1589
|
+
if (admission.ok)
|
|
1590
|
+
clis.push(key);
|
|
1591
|
+
else
|
|
1592
|
+
refused.push({ cliKey: key, reason: admission.reason });
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (clis.length === 0) {
|
|
1596
|
+
chatScopes.release(chatId, token);
|
|
1597
|
+
removeChatScratch(scope.cwd, chatScopes.base);
|
|
1598
|
+
return reply.code(409).send({
|
|
1599
|
+
error: (scoped
|
|
1600
|
+
? 'no seat in the roster can be held to a scoped chat (an ACP adapter admitted to input ' +
|
|
1601
|
+
'governance, or `os_sandbox = true` on its [cli.acp] record) and take a turn; open the ' +
|
|
1602
|
+
'chat unscoped, sign a seat in, or name seats with `clis`'
|
|
1603
|
+
: 'no seat in the roster can take a turn (every seat is signed out); sign a seat in from ' +
|
|
1604
|
+
'the System page, or name seats with `clis`') + ' — see refused',
|
|
1605
|
+
refused,
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
try {
|
|
1609
|
+
const seats = await adapter.chatOpen(chatId, clis, engine.cwd, {
|
|
1610
|
+
codeGraphDb: engine.codeGraphDb,
|
|
1611
|
+
readRoots: engine.readRoots,
|
|
1612
|
+
});
|
|
1613
|
+
// A REQUESTED seat the engine refused joins the same list, with the engine's reason.
|
|
1614
|
+
for (const s of seats) {
|
|
1615
|
+
if (!s.ok)
|
|
1616
|
+
refused.push({ cliKey: s.cliKey, reason: s.error ?? 'the engine refused the seat' });
|
|
1617
|
+
}
|
|
1618
|
+
// Nothing warmed (independent review, W1): the engine holds no pool row and has dropped the
|
|
1619
|
+
// scope itself (no `chatClosed` will come) — report the PER-SEAT reasons, never an
|
|
1620
|
+
// engine-version guess; the root goes, the id is free again.
|
|
1621
|
+
if (seats.every((s) => !s.ok)) {
|
|
1622
|
+
chatScopes.release(chatId, token);
|
|
1623
|
+
removeChatScratch(scope.cwd, chatScopes.base);
|
|
1624
|
+
return reply.code(409).send({
|
|
1625
|
+
error: `chat ${chatId}: no seat warmed (${seats.length} failed) — see seats`,
|
|
1626
|
+
seats,
|
|
1627
|
+
refused,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
// Honest scope (Copilot, #518): a SCOPED chat is a promise — the roots are read-only, the
|
|
1631
|
+
// graph is attached, the seats see nothing else — and only an engine that CONFIRMS it
|
|
1632
|
+
// recorded the scope (its `chatList` row carries the scope fields) can keep it. An engine
|
|
1633
|
+
// predating chat scope (wicked-core#410) dropped `scopeJson` and runs the seats unbounded in
|
|
1634
|
+
// the scratch root; an engine that cannot be asked has confirmed nothing. Neither may hold a
|
|
1635
|
+
// scoped chat: it is closed again, its root removed, and the caller gets a 501 naming the
|
|
1636
|
+
// remedy — never a chat whose statement promises what its seats do not enforce. An UNSCOPED
|
|
1637
|
+
// chat (`kind: 'none'`) promises nothing beyond its scratch root and proceeds.
|
|
1638
|
+
const applied = await adapter.chatScopeApplied(chatId);
|
|
1639
|
+
if (applied !== true && scoped) {
|
|
1640
|
+
await adapter.chatClose(chatId).catch(() => undefined);
|
|
1641
|
+
// The engine's `chatClosed` for this id is still on its way: park, do not release
|
|
1642
|
+
// (Copilot, #518), so a reuse cannot have its root removed by the late event.
|
|
1643
|
+
chatScopes.abortToClosing(chatId, token);
|
|
1644
|
+
removeChatScratch(scope.cwd, chatScopes.base);
|
|
1645
|
+
if (applied === false) {
|
|
1646
|
+
// A row WITHOUT the scope fields: the engine predates chat scope (wicked-core#410).
|
|
1647
|
+
return reply.code(501).send({
|
|
1648
|
+
error: 'the installed wicked-core-ts predates chat scope (wicked-core#410): it cannot ground ' +
|
|
1649
|
+
'a scoped chat or hold its read roots read-only — upgrade the engine, or open the ' +
|
|
1650
|
+
'chat without projectId/repoRefs.',
|
|
1651
|
+
seats,
|
|
1652
|
+
});
|
|
1451
1653
|
}
|
|
1654
|
+
// NO row although a seat reported warm: nothing is actually held for this chat.
|
|
1655
|
+
return reply.code(409).send({
|
|
1656
|
+
error: `chat ${chatId}: a seat reported warm but the engine holds no row for the chat — ` +
|
|
1657
|
+
'nothing warmed; open it again (see seats)',
|
|
1658
|
+
seats,
|
|
1659
|
+
});
|
|
1452
1660
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1661
|
+
// File the chat into its project WHILE the id is still reserved (Copilot, #518): publishing
|
|
1662
|
+
// first would let a concurrent DELETE / engine `chatClosed` land during this await and leave
|
|
1663
|
+
// a 201 with a stale scope and a membership attached to a closed chat.
|
|
1664
|
+
let projectAttachError;
|
|
1665
|
+
let attachedMemberId;
|
|
1666
|
+
if (b.projectId !== undefined) {
|
|
1667
|
+
try {
|
|
1668
|
+
const { member, created } = await adapter.projectMemberAttach(b.projectId, 'crew.chat', chatId);
|
|
1669
|
+
if (created)
|
|
1670
|
+
attachedMemberId = member.id;
|
|
1671
|
+
if (created) {
|
|
1672
|
+
projects.index.set(chatId, b.projectId);
|
|
1673
|
+
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));
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
catch (err) {
|
|
1677
|
+
// The chat is open and usable; the filing failed. Said on the 201 rather than failing
|
|
1678
|
+
// the open — the caller can re-attach via POST /projects/:id/members.
|
|
1679
|
+
projectAttachError = message(err);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
if (!chatScopes.set(chatId, scope, token)) {
|
|
1683
|
+
// The reservation was cancelled while the open was in flight — an engine `chatClosed` or a
|
|
1684
|
+
// `DELETE` for this id (Copilot, #518). Nothing was recorded; tear the chat down (engine
|
|
1685
|
+
// session, scratch root, and the filing just made) instead of returning a stale 201.
|
|
1686
|
+
await adapter.chatClose(chatId).catch(() => undefined);
|
|
1687
|
+
removeChatScratch(scope.cwd, chatScopes.base);
|
|
1688
|
+
if (b.projectId !== undefined && attachedMemberId !== undefined) {
|
|
1689
|
+
await adapter.projectMemberDetach(b.projectId, attachedMemberId).catch(() => false);
|
|
1690
|
+
projects.index.delete(chatId);
|
|
1691
|
+
}
|
|
1692
|
+
return reply.code(409).send({
|
|
1693
|
+
error: `chat ${chatId} was closed while it was being opened; open it again`,
|
|
1694
|
+
});
|
|
1459
1695
|
}
|
|
1696
|
+
// The thread learns of every refused seat the way it learns of everything else — a frame
|
|
1697
|
+
// on /ws — AFTER the scope is published, so a reader never sees a refusal for a chat it
|
|
1698
|
+
// cannot yet look up.
|
|
1699
|
+
for (const r of refused) {
|
|
1700
|
+
runtime.broadcast?.({
|
|
1701
|
+
type: 'chatSeatRefused',
|
|
1702
|
+
chat: chatId,
|
|
1703
|
+
cliKey: r.cliKey,
|
|
1704
|
+
reason: r.reason,
|
|
1705
|
+
...(b.projectId !== undefined ? { project_id: b.projectId } : {}),
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
return reply.code(201).send({
|
|
1709
|
+
chatId,
|
|
1710
|
+
seats,
|
|
1711
|
+
scope,
|
|
1712
|
+
refused,
|
|
1713
|
+
...(projectAttachError !== undefined ? { projectAttachError } : {}),
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
catch (err) {
|
|
1717
|
+
// Nothing warmed: the scratch root prepared above must not linger — removed under the SAME
|
|
1718
|
+
// base the resolver created it in (Copilot, #518), fail-closed like every removal.
|
|
1719
|
+
chatScopes.release(chatId, token);
|
|
1720
|
+
removeChatScratch(scope.cwd, chatScopes.base);
|
|
1721
|
+
return reply.code(400).send({ error: message(err) });
|
|
1460
1722
|
}
|
|
1461
|
-
return reply.code(201).send({ chatId, seats });
|
|
1462
1723
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1724
|
+
finally {
|
|
1725
|
+
// A reservation that never became a scope (any early return above) must not pin the id —
|
|
1726
|
+
// token-guarded, so a reservation that is no longer ours is left alone.
|
|
1727
|
+
chatScopes.release(chatId, token);
|
|
1465
1728
|
}
|
|
1466
1729
|
});
|
|
1467
1730
|
const ChatMessageSchema = z.object({
|
|
@@ -1488,7 +1751,7 @@ runtime = {}) {
|
|
|
1488
1751
|
// lived in the tab that abandoned it, and an operator could not reclaim one without restarting
|
|
1489
1752
|
// the daemon. Registered BEFORE `/chats/:id` is irrelevant to fastify (it routes on the literal
|
|
1490
1753
|
// segment first), but the order reads the way the routes nest.
|
|
1491
|
-
app.get(`${V}/chats`, async (_req, reply) => {
|
|
1754
|
+
app.get(`${V}/chats`, { config: { manifest: { responseType: 'ChatListResponse', statusCodes: [200, 400, 501] } } }, async (_req, reply) => {
|
|
1492
1755
|
try {
|
|
1493
1756
|
return { chats: await adapter.chatList() };
|
|
1494
1757
|
}
|
|
@@ -1503,10 +1766,11 @@ runtime = {}) {
|
|
|
1503
1766
|
.send({ error: message(err) });
|
|
1504
1767
|
}
|
|
1505
1768
|
});
|
|
1506
|
-
app.get(`${V}/chats/:id`, async (req, reply) => {
|
|
1769
|
+
app.get(`${V}/chats/:id`, { config: { manifest: { responseType: 'ChatDetailResponse', statusCodes: [200, 400] } } }, async (req, reply) => {
|
|
1507
1770
|
const { id } = req.params;
|
|
1508
1771
|
try {
|
|
1509
|
-
|
|
1772
|
+
// The scope recorded at open (crew#502) — `null` for a chat this daemon did not open.
|
|
1773
|
+
return { chatId: id, seats: await adapter.chatSeats(id), scope: chatScopes.get(id) ?? null };
|
|
1510
1774
|
}
|
|
1511
1775
|
catch (err) {
|
|
1512
1776
|
return reply.code(400).send({ error: message(err) });
|
|
@@ -1514,6 +1778,10 @@ runtime = {}) {
|
|
|
1514
1778
|
});
|
|
1515
1779
|
app.delete(`${V}/chats/:id`, async (req, reply) => {
|
|
1516
1780
|
const { id } = req.params;
|
|
1781
|
+
// The scratch root goes with the chat at once, whether or not the engine still knew it
|
|
1782
|
+
// (crew#502); the id stays parked until the engine's `chatClosed` is observed, so a delayed
|
|
1783
|
+
// close can never land on a chat that reused it (Copilot, #518).
|
|
1784
|
+
chatScopes.beginClose(id);
|
|
1517
1785
|
try {
|
|
1518
1786
|
await adapter.chatClose(id);
|
|
1519
1787
|
return { ok: true };
|
|
@@ -2411,7 +2679,9 @@ runtime = {}) {
|
|
|
2411
2679
|
if (!repo)
|
|
2412
2680
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
2413
2681
|
const graphPath = requirementsGraph(repo);
|
|
2414
|
-
const dbPath =
|
|
2682
|
+
const dbPath = codeGraphDbOr503(repo, reply);
|
|
2683
|
+
if (dbPath === null)
|
|
2684
|
+
return reply;
|
|
2415
2685
|
// Coverage from the live estate store — computed by wicked-core governance layer.
|
|
2416
2686
|
let coverage = null;
|
|
2417
2687
|
if (existsSync(dbPath)) {
|
|
@@ -2469,7 +2739,17 @@ runtime = {}) {
|
|
|
2469
2739
|
if (!parsed.success) {
|
|
2470
2740
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid query'));
|
|
2471
2741
|
}
|
|
2472
|
-
|
|
2742
|
+
let page;
|
|
2743
|
+
try {
|
|
2744
|
+
page = await listRequirements(repo, parsed.data);
|
|
2745
|
+
}
|
|
2746
|
+
catch (err) {
|
|
2747
|
+
// wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
|
|
2748
|
+
const status = codeGraphErrorStatus(err);
|
|
2749
|
+
if (status === null)
|
|
2750
|
+
throw err;
|
|
2751
|
+
return reply.code(status).send({ error: message(err) });
|
|
2752
|
+
}
|
|
2473
2753
|
if (page === null) {
|
|
2474
2754
|
return reply.code(404).send({ error: 'requirements_graph.json not generated for this repo yet' });
|
|
2475
2755
|
}
|
|
@@ -2494,7 +2774,17 @@ runtime = {}) {
|
|
|
2494
2774
|
catch {
|
|
2495
2775
|
return reply.code(400).send({ error: 'Malformed requirement key encoding' });
|
|
2496
2776
|
}
|
|
2497
|
-
|
|
2777
|
+
let detail;
|
|
2778
|
+
try {
|
|
2779
|
+
detail = await getRequirement(repo, decoded);
|
|
2780
|
+
}
|
|
2781
|
+
catch (err) {
|
|
2782
|
+
// wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
|
|
2783
|
+
const status = codeGraphErrorStatus(err);
|
|
2784
|
+
if (status === null)
|
|
2785
|
+
throw err;
|
|
2786
|
+
return reply.code(status).send({ error: message(err) });
|
|
2787
|
+
}
|
|
2498
2788
|
if (detail === null)
|
|
2499
2789
|
return reply.code(404).send({ error: 'Requirement not found' });
|
|
2500
2790
|
return { requirement: detail };
|
|
@@ -2529,20 +2819,42 @@ runtime = {}) {
|
|
|
2529
2819
|
catch {
|
|
2530
2820
|
return reply.code(400).send({ error: 'Malformed requirement key encoding' });
|
|
2531
2821
|
}
|
|
2532
|
-
|
|
2822
|
+
let detail;
|
|
2823
|
+
try {
|
|
2824
|
+
detail = await patchRequirement(repo, decodedKey, parsed.data);
|
|
2825
|
+
}
|
|
2826
|
+
catch (err) {
|
|
2827
|
+
// wicked-core#406: a CURRENT engine with no repo-graph root is a 503, not a 500.
|
|
2828
|
+
const status = codeGraphErrorStatus(err);
|
|
2829
|
+
if (status === null)
|
|
2830
|
+
throw err;
|
|
2831
|
+
return reply.code(status).send({ error: message(err) });
|
|
2832
|
+
}
|
|
2533
2833
|
if (detail === null)
|
|
2534
2834
|
return reply.code(404).send({ error: 'Requirement not found' });
|
|
2535
2835
|
return { requirement: detail };
|
|
2536
2836
|
});
|
|
2537
|
-
app.get(`${V}/repos/:id/graph`, async (req, reply) => {
|
|
2837
|
+
app.get(`${V}/repos/:id/graph`, { config: { manifest: { responseType: 'RepoGraphResponse', statusCodes: [200, 404, 500, 503] } } }, async (req, reply) => {
|
|
2538
2838
|
const { id } = req.params;
|
|
2539
2839
|
const repos = await adapter.listRepos();
|
|
2540
2840
|
const repo = repos.find((r) => r.id === id);
|
|
2541
2841
|
if (!repo)
|
|
2542
2842
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
2543
|
-
const dbPath =
|
|
2843
|
+
const dbPath = codeGraphDbOr503(repo, reply);
|
|
2844
|
+
if (dbPath === null)
|
|
2845
|
+
return reply;
|
|
2544
2846
|
if (!existsSync(dbPath)) {
|
|
2545
|
-
|
|
2847
|
+
// `graph: null` alone cannot tell "not indexed" from "empty" (F-2R2-005): say WHY, with the
|
|
2848
|
+
// same finding text the repos wire carries when the engine has one.
|
|
2849
|
+
const finding = (repo.findings ?? []).find((f) => f.code === 'in_tree_code_graph_ignored' || f.code === 'code_graph_root_unresolvable');
|
|
2850
|
+
const unbuilt = {
|
|
2851
|
+
graph: null,
|
|
2852
|
+
reason: finding?.message ??
|
|
2853
|
+
`no code graph has been built for '${repo.name}' yet (nothing at ${dbPath}) — run onboarding ` +
|
|
2854
|
+
`(POST /api/v1/repos/${repo.id}/onboard) to index it`,
|
|
2855
|
+
...(finding !== undefined ? { finding } : {}),
|
|
2856
|
+
};
|
|
2857
|
+
return reply.send(unbuilt);
|
|
2546
2858
|
}
|
|
2547
2859
|
try {
|
|
2548
2860
|
const settings = await adapter.getSettings();
|
|
@@ -2563,13 +2875,14 @@ runtime = {}) {
|
|
|
2563
2875
|
});
|
|
2564
2876
|
const raw = JSON.parse(stdout);
|
|
2565
2877
|
const fileCount = new Set(raw.nodes.map((n) => n.file)).size;
|
|
2566
|
-
|
|
2878
|
+
const built = {
|
|
2567
2879
|
graph: {
|
|
2568
2880
|
nodes: raw.nodes,
|
|
2569
2881
|
edges: raw.edges,
|
|
2570
2882
|
stats: { nodeCount: raw.nodes.length, edgeCount: raw.edges.length, fileCount },
|
|
2571
2883
|
},
|
|
2572
|
-
}
|
|
2884
|
+
};
|
|
2885
|
+
return reply.send(built);
|
|
2573
2886
|
}
|
|
2574
2887
|
catch (err) {
|
|
2575
2888
|
return reply.code(500).send({ error: message(err) });
|
|
@@ -2591,7 +2904,9 @@ runtime = {}) {
|
|
|
2591
2904
|
const repo = repos.find((r) => r.id === id);
|
|
2592
2905
|
if (!repo)
|
|
2593
2906
|
return reply.code(404).send({ error: `Repo ${id} not found` });
|
|
2594
|
-
const dbPath =
|
|
2907
|
+
const dbPath = codeGraphDbOr503(repo, reply);
|
|
2908
|
+
if (dbPath === null)
|
|
2909
|
+
return reply;
|
|
2595
2910
|
if (!existsSync(dbPath)) {
|
|
2596
2911
|
return reply.code(404).send({ error: 'Code graph not built for this repo yet' });
|
|
2597
2912
|
}
|