wicked-crew 0.7.34 → 0.7.36
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/dist/api/chat-scope.d.ts +29 -1
- package/dist/api/chat-scope.d.ts.map +1 -1
- package/dist/api/chat-scope.js +67 -12
- package/dist/api/chat-scope.js.map +1 -1
- package/dist/api/chat-transcripts.d.ts +60 -0
- package/dist/api/chat-transcripts.d.ts.map +1 -0
- package/dist/api/chat-transcripts.js +139 -0
- package/dist/api/chat-transcripts.js.map +1 -0
- package/dist/api/chat-turns.d.ts +43 -4
- package/dist/api/chat-turns.d.ts.map +1 -1
- package/dist/api/chat-turns.js +73 -6
- package/dist/api/chat-turns.js.map +1 -1
- package/dist/api/delivery-cache.d.ts +23 -6
- package/dist/api/delivery-cache.d.ts.map +1 -1
- package/dist/api/delivery-cache.js +27 -17
- package/dist/api/delivery-cache.js.map +1 -1
- package/dist/api/delivery-index.d.ts +45 -3
- package/dist/api/delivery-index.d.ts.map +1 -1
- package/dist/api/delivery-index.js +79 -4
- package/dist/api/delivery-index.js.map +1 -1
- package/dist/api/requirements.d.ts +3 -5
- package/dist/api/requirements.d.ts.map +1 -1
- package/dist/api/requirements.js +30 -146
- package/dist/api/requirements.js.map +1 -1
- package/dist/api/retry-index.d.ts +14 -0
- package/dist/api/retry-index.d.ts.map +1 -1
- package/dist/api/retry-index.js +29 -0
- package/dist/api/retry-index.js.map +1 -1
- package/dist/api/roster-standing.d.ts +4 -4
- package/dist/api/roster-standing.d.ts.map +1 -1
- package/dist/api/roster-standing.js +24 -12
- package/dist/api/roster-standing.js.map +1 -1
- package/dist/api/routes.d.ts +142 -10
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +292 -44
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-timing-index.d.ts +22 -0
- package/dist/api/run-timing-index.d.ts.map +1 -1
- package/dist/api/run-timing-index.js +34 -0
- package/dist/api/run-timing-index.js.map +1 -1
- package/dist/api/seat-health.d.ts +2 -46
- package/dist/api/seat-health.d.ts.map +1 -1
- package/dist/api/seat-health.js +28 -111
- package/dist/api/seat-health.js.map +1 -1
- package/dist/api/seat-standing.d.ts +9 -8
- package/dist/api/seat-standing.d.ts.map +1 -1
- package/dist/api/seat-standing.js +22 -32
- package/dist/api/seat-standing.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +114 -12
- package/dist/api/server.js.map +1 -1
- package/dist/api/skills.d.ts +2 -2
- package/dist/api/skills.d.ts.map +1 -1
- package/dist/api/skills.js +5 -2
- package/dist/api/skills.js.map +1 -1
- package/dist/api/stall-frame-index.d.ts +68 -0
- package/dist/api/stall-frame-index.d.ts.map +1 -0
- package/dist/api/stall-frame-index.js +106 -0
- package/dist/api/stall-frame-index.js.map +1 -0
- package/dist/api/stall-watchdog.d.ts +22 -2
- package/dist/api/stall-watchdog.d.ts.map +1 -1
- package/dist/api/stall-watchdog.js +41 -11
- package/dist/api/stall-watchdog.js.map +1 -1
- package/dist/campaigns/plan.d.ts +12 -2
- package/dist/campaigns/plan.d.ts.map +1 -1
- package/dist/campaigns/plan.js +3 -0
- package/dist/campaigns/plan.js.map +1 -1
- package/dist/campaigns/rollup.d.ts +5 -0
- package/dist/campaigns/rollup.d.ts.map +1 -1
- package/dist/campaigns/rollup.js +3 -2
- package/dist/campaigns/rollup.js.map +1 -1
- package/dist/campaigns/routes.d.ts +5 -2
- package/dist/campaigns/routes.d.ts.map +1 -1
- package/dist/campaigns/routes.js +13 -3
- package/dist/campaigns/routes.js.map +1 -1
- package/dist/cli/index.js +137 -23
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/port.d.ts +19 -0
- package/dist/cli/port.d.ts.map +1 -0
- package/dist/cli/port.js +39 -0
- package/dist/cli/port.js.map +1 -0
- package/dist/core/adapter.d.ts +68 -2
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +176 -23
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-reaper.d.ts +57 -1
- package/dist/core/bridge-reaper.d.ts.map +1 -1
- package/dist/core/bridge-reaper.js +143 -21
- package/dist/core/bridge-reaper.js.map +1 -1
- package/dist/core/cursor.d.ts +7 -0
- package/dist/core/cursor.d.ts.map +1 -1
- package/dist/core/cursor.js +7 -1
- package/dist/core/cursor.js.map +1 -1
- package/dist/core/deliver-text.d.ts +35 -2
- package/dist/core/deliver-text.d.ts.map +1 -1
- package/dist/core/deliver-text.js +127 -17
- package/dist/core/deliver-text.js.map +1 -1
- package/dist/core/deliver-triage.d.ts.map +1 -1
- package/dist/core/deliver-triage.js +5 -2
- package/dist/core/deliver-triage.js.map +1 -1
- package/dist/core/deliver.d.ts +76 -2
- package/dist/core/deliver.d.ts.map +1 -1
- package/dist/core/deliver.js +209 -16
- package/dist/core/deliver.js.map +1 -1
- package/dist/core/engine-roster.d.ts +4 -3
- package/dist/core/engine-roster.d.ts.map +1 -1
- package/dist/core/engine-roster.js +10 -14
- package/dist/core/engine-roster.js.map +1 -1
- package/dist/core/types.d.ts +30 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +6 -4
- package/dist/core/types.js.map +1 -1
- package/dist/core/versions.d.ts +17 -0
- package/dist/core/versions.d.ts.map +1 -0
- package/dist/core/versions.js +35 -0
- package/dist/core/versions.js.map +1 -0
- package/dist/interactive/bridge-pool.d.ts +66 -14
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +130 -17
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/bridge-root.d.ts +47 -16
- package/dist/interactive/bridge-root.d.ts.map +1 -1
- package/dist/interactive/bridge-root.js +102 -31
- package/dist/interactive/bridge-root.js.map +1 -1
- package/dist/interactive/doc-delete-routes.d.ts +3 -1
- package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
- package/dist/interactive/doc-delete-routes.js.map +1 -1
- package/dist/interactive/doc-list-routes.d.ts +3 -1
- package/dist/interactive/doc-list-routes.d.ts.map +1 -1
- package/dist/interactive/doc-list-routes.js.map +1 -1
- package/dist/interactive/docs-index.d.ts +3 -1
- package/dist/interactive/docs-index.d.ts.map +1 -1
- package/dist/interactive/docs-index.js +1 -1
- package/dist/interactive/docs-index.js.map +1 -1
- package/dist/interactive/project-root.d.ts +8 -2
- package/dist/interactive/project-root.d.ts.map +1 -1
- package/dist/interactive/project-root.js +1 -1
- package/dist/interactive/project-root.js.map +1 -1
- package/dist/interactive/proxy-routes.d.ts +3 -1
- package/dist/interactive/proxy-routes.d.ts.map +1 -1
- package/dist/interactive/proxy-routes.js.map +1 -1
- package/dist/projects/graph.d.ts +4 -0
- package/dist/projects/graph.d.ts.map +1 -1
- package/dist/projects/graph.js +4 -2
- package/dist/projects/graph.js.map +1 -1
- package/dist/projects/routes.d.ts +2 -2
- package/dist/projects/state-home-preflight.d.ts +30 -7
- package/dist/projects/state-home-preflight.d.ts.map +1 -1
- package/dist/projects/state-home-preflight.js +59 -16
- package/dist/projects/state-home-preflight.js.map +1 -1
- package/dist/projects/state-home-registry.d.ts.map +1 -1
- package/dist/projects/state-home-registry.js +2 -0
- package/dist/projects/state-home-registry.js.map +1 -1
- package/dist/qe/acceptance.d.ts +9 -0
- package/dist/qe/acceptance.d.ts.map +1 -1
- package/dist/qe/acceptance.js +16 -1
- package/dist/qe/acceptance.js.map +1 -1
- package/dist/qe/author-workflow.d.ts.map +1 -1
- package/dist/qe/author-workflow.js +2 -1
- package/dist/qe/author-workflow.js.map +1 -1
- package/dist/skills/base-skill.d.ts +32 -10
- package/dist/skills/base-skill.d.ts.map +1 -1
- package/dist/skills/base-skill.js +36 -27
- package/dist/skills/base-skill.js.map +1 -1
- package/dist/skills/guards.d.ts.map +1 -1
- package/dist/skills/guards.js +3 -0
- package/dist/skills/guards.js.map +1 -1
- package/dist/skills/runtime.d.ts +9 -5
- package/dist/skills/runtime.d.ts.map +1 -1
- package/dist/skills/runtime.js +11 -7
- package/dist/skills/runtime.js.map +1 -1
- package/dist/skills/store.d.ts +30 -0
- package/dist/skills/store.d.ts.map +1 -1
- package/dist/skills/store.js +159 -3
- package/dist/skills/store.js.map +1 -1
- package/dist/skills/tree.d.ts +12 -0
- package/dist/skills/tree.d.ts.map +1 -1
- package/dist/skills/tree.js +26 -0
- package/dist/skills/tree.js.map +1 -1
- package/dist/studio/assets/index-Dk98qXKv.js +546 -0
- package/dist/studio/index.html +1 -1
- package/dist/studio/testid-inventory.json +89 -5
- package/endpoint-manifest.json +13 -1
- package/package.json +4 -4
- package/dist/studio/assets/index-DhojBZ1T.js +0 -544
package/dist/api/routes.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
5
5
|
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
|
-
import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds } from '../core/adapter.js';
|
|
8
|
+
import { CampaignsUnsupportedError, ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds, settingsFilePath } from '../core/adapter.js';
|
|
9
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';
|
|
@@ -26,7 +26,7 @@ import { InvalidDiffBaseError, MERGE_BASE_LITERAL, NotARegularFileError, Unresol
|
|
|
26
26
|
import { DocRunIndex } from '../interactive/doc-run-index.js';
|
|
27
27
|
import { listInteractiveDocs } from '../interactive/docs-index.js';
|
|
28
28
|
import { TestSetIndex } from '../qe/test-sets.js';
|
|
29
|
-
import { resolveProjectGraphBinding } from '../projects/graph.js';
|
|
29
|
+
import { estateExe, resolveProjectGraphBinding } from '../projects/graph.js';
|
|
30
30
|
import { registerProjectRoutes } from '../projects/routes.js';
|
|
31
31
|
import { registerCampaignRoutes } from '../campaigns/routes.js';
|
|
32
32
|
import { registerGovernanceWikiRoutes } from './governance-wiki.js';
|
|
@@ -36,11 +36,12 @@ import { registerTestingRoutes } from './testing.js';
|
|
|
36
36
|
import { registerSkillsRoutes } from './skills.js';
|
|
37
37
|
import { disabledSkillsHealth } from '../skills/runtime.js';
|
|
38
38
|
import { STATE_HOME_BLOCKER_CODE, STATE_HOME_REMEDY, isEngineStateHomeRefusal, stateHomeBlockerBody, } from '../projects/state-home-preflight.js';
|
|
39
|
-
import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE } from '../skills/base-skill.js';
|
|
39
|
+
import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../skills/base-skill.js';
|
|
40
40
|
import { noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
|
|
41
41
|
import { ProjectSettingsStore } from '../projects/settings.js';
|
|
42
42
|
import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
|
|
43
43
|
import { composeDeliverText, factsFromRun, framedDeliverText, runUrlFor } from '../core/deliver-text.js';
|
|
44
|
+
import { resolvePullRequest as resolvePullRequestViaGh } from '../core/deliver.js';
|
|
44
45
|
import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
|
|
45
46
|
import { registerInteractiveDocDelete } from '../interactive/doc-delete-routes.js';
|
|
46
47
|
import { registerInteractiveDocList } from '../interactive/doc-list-routes.js';
|
|
@@ -54,7 +55,7 @@ import { RetryIndex } from './retry-index.js';
|
|
|
54
55
|
import { GroupIndex } from './group-index.js';
|
|
55
56
|
import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
|
|
56
57
|
import { GuidanceIndex } from './guidance-index.js';
|
|
57
|
-
import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, prUrlFrom, } from './delivery-index.js';
|
|
58
|
+
import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, prUrlFrom, canDeliverResolver, } from './delivery-index.js';
|
|
58
59
|
import { DeliveryDerivationCache } from './delivery-cache.js';
|
|
59
60
|
import { gitReprovisionWorktree, runDeliverScript, } from './post-hoc-deliver.js';
|
|
60
61
|
import { LOCAL_ACTOR } from './auth.js';
|
|
@@ -82,6 +83,17 @@ const STATUS_ORDER = {
|
|
|
82
83
|
function sortActionableFirst(views) {
|
|
83
84
|
return [...views].sort((a, b) => (STATUS_ORDER[a.session.status] ?? 9) - (STATUS_ORDER[b.session.status] ?? 9));
|
|
84
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The whole-graph counts from `wicked-estate stats --db` stdout (`nodes=N edges=M files=F …`, the
|
|
88
|
+
* first line estate prints — `main.rs`'s `stats` summary). `undefined` when the line does not parse
|
|
89
|
+
* (an older/newer estate, an error message): the caller leaves `CodeGraphData.totals` ABSENT.
|
|
90
|
+
*/
|
|
91
|
+
export function parseEstateTotals(stdout) {
|
|
92
|
+
const m = /^nodes=(\d+) edges=(\d+) files=(\d+)/m.exec(stdout);
|
|
93
|
+
if (m === null)
|
|
94
|
+
return undefined;
|
|
95
|
+
return { nodes: Number(m[1]), edges: Number(m[2]), files: Number(m[3]) };
|
|
96
|
+
}
|
|
85
97
|
function codeGraphDbOr503(repo, reply) {
|
|
86
98
|
try {
|
|
87
99
|
return codeGraphDb(repo);
|
|
@@ -364,6 +376,13 @@ export const LaunchSchema = z.object({
|
|
|
364
376
|
* sharing a label form one `RunGroup` on `GET /campaigns`. Persisted/echoed like
|
|
365
377
|
* `campaignId` (as `AgentSession.group_label`). */
|
|
366
378
|
groupLabel: z.string().min(1).max(200).optional(),
|
|
379
|
+
/** DES-L9 / crew#550 (api-types 0.38.0) — REVISE an OPEN same-repository pull request: its head
|
|
380
|
+
* branch becomes the run's base and the deliver phase pushes the run's commits onto it (no
|
|
381
|
+
* second PR), commenting the run record. Needs `repoRef` + `workflow` and `deliver: "pr"`
|
|
382
|
+
* (explicit `"none"` is a 400; a daemon that resolves the omitted field to `none` is a 409).
|
|
383
|
+
* Resolved via `gh pr view` at launch — not OPEN / a fork / gh failure ⇒ 409, nothing launched.
|
|
384
|
+
* Send it only when `GET /health.capabilities.revisesPr === true` (engine ≥ 0.7.27). */
|
|
385
|
+
revisesPr: z.number().int().positive().optional(),
|
|
367
386
|
}).strict().refine((b) => b.deliver !== 'pr' || b.workflow !== undefined, {
|
|
368
387
|
message: 'deliver: "pr" requires a workflow — a free-text run has no def to append the deliver phase to',
|
|
369
388
|
path: ['deliver'],
|
|
@@ -376,11 +395,26 @@ export const LaunchSchema = z.object({
|
|
|
376
395
|
}).refine((b) => b.campaignId === undefined || b.groupLabel === undefined, {
|
|
377
396
|
message: 'campaignId and groupLabel are mutually exclusive — a run files onto ONE grouping surface (an existing campaign, or a label group)',
|
|
378
397
|
path: ['groupLabel'],
|
|
398
|
+
}).refine((b) => b.revisesPr === undefined || (b.repoRef !== undefined && b.workflow !== undefined), {
|
|
399
|
+
message: 'revisesPr needs repoRef and workflow — a revision bases a repo-scoped, def-driven run on the pull request\'s branch',
|
|
400
|
+
path: ['revisesPr'],
|
|
401
|
+
}).refine((b) => b.revisesPr === undefined || b.deliver !== 'none', {
|
|
402
|
+
message: 'revisesPr needs deliver: "pr" — a launch that declines delivery has no phase to push the revision with',
|
|
403
|
+
path: ['revisesPr'],
|
|
379
404
|
});
|
|
405
|
+
/** `POST /runs/:id/gate` (api-types 0.38.0 `GateDecision`; DES-L1 PR-2). Additive arms: `action`
|
|
406
|
+
* names the arm (`approve` | `request_changes` | `reject`; absent = today's two-arm mapping of
|
|
407
|
+
* `approve`), `amendScope` says where an approve's `amend` lands (`cursor` = the gated unit, today's
|
|
408
|
+
* behaviour | `creator` = the first creator phase at/after the cursor — an intake steer reaches the
|
|
409
|
+
* phase that implements). `request_changes` sends a NOT-PASS review back to the creator with the
|
|
410
|
+
* findings in context (`amend` = the operator's note). A disagreement between `action` and
|
|
411
|
+
* `approve` is a 400 that names both. */
|
|
380
412
|
export const GateSchema = z.object({
|
|
381
413
|
approve: z.boolean(),
|
|
382
414
|
amend: z.string().optional(),
|
|
383
|
-
|
|
415
|
+
action: z.enum(['approve', 'request_changes', 'reject']).optional(),
|
|
416
|
+
amendScope: z.enum(['cursor', 'creator']).optional(),
|
|
417
|
+
}).strict().refine((b) => b.action === undefined || (b.action === 'approve') === b.approve, { message: '`action` disagrees with `approve`: request_changes and reject require approve: false; approve requires approve: true', path: ['action'] }).refine((b) => b.amendScope === undefined || b.approve, { message: '`amendScope` applies to an approve only (approve: true)', path: ['amendScope'] });
|
|
384
418
|
/** `PUT /runs/:id/guidance` (DES-UX-002 §7.2, CREW-UX-7) — the durable pre-gate note body.
|
|
385
419
|
* The empty string is a legal body: it CLEARS the note. The byte cap is checked in the route
|
|
386
420
|
* (not zod's char-counting `max`) so the 400 names the actual limit. */
|
|
@@ -444,6 +478,24 @@ const STUDIO_SETTINGS_KEY = /^studio\.[a-z][a-z0-9-]*$/;
|
|
|
444
478
|
* ceiling on ONE key, not on the patch.
|
|
445
479
|
*/
|
|
446
480
|
const STUDIO_SETTINGS_MAX_BYTES = 512 * 1024;
|
|
481
|
+
/**
|
|
482
|
+
* Stamp the required `seq` onto the daemon-authored frames of a ts-sorted merge. A watchdog frame
|
|
483
|
+
* has no engine `seq` — the engine's log never saw it — so each one rides the seq of the engine
|
|
484
|
+
* record it FOLLOWS (0 before the first). The served array is therefore monotonic non-decreasing in
|
|
485
|
+
* `seq`, and a consumer that re-sorts by seq (studio's narrator) keeps every frame beside the event
|
|
486
|
+
* it was captured after, instead of dropping it to the front on a missing key.
|
|
487
|
+
*/
|
|
488
|
+
function withServedSeq(merged) {
|
|
489
|
+
let seq = 0;
|
|
490
|
+
return merged.map((e) => {
|
|
491
|
+
if (!('daemon' in e)) {
|
|
492
|
+
if (typeof e.seq === 'number')
|
|
493
|
+
seq = e.seq;
|
|
494
|
+
return e;
|
|
495
|
+
}
|
|
496
|
+
return { ...e, seq };
|
|
497
|
+
});
|
|
498
|
+
}
|
|
447
499
|
/**
|
|
448
500
|
* The daemon REST surface. Every endpoint is a thin wrapper over one adapter /
|
|
449
501
|
* core-ts call (DES-STUDIO-001 §2). `session`/`phase` nouns are now `run`/`unit`.
|
|
@@ -489,6 +541,7 @@ runtime = {}) {
|
|
|
489
541
|
const guidanceIndex = runtime.guidanceIndex ?? new GuidanceIndex();
|
|
490
542
|
const chatScopes = runtime.chatScopes ?? new ChatScopeIndex();
|
|
491
543
|
const chatTurns = runtime.chatTurns ?? new ChatTurnIndex();
|
|
544
|
+
const chatTranscripts = runtime.chatTranscripts;
|
|
492
545
|
const deliveryIndex = runtime.deliveryIndex ?? new DeliveryIndex();
|
|
493
546
|
// Wave 6: the doc↔run binding and the registered test sets — `createServer` injects the real
|
|
494
547
|
// ones; a directly-driven route set gets an empty index (every run `document_id: null`, no sets).
|
|
@@ -508,11 +561,17 @@ runtime = {}) {
|
|
|
508
561
|
// the daemon's sweep/warm feed it. The default (a directly-driven route set) is COLD and
|
|
509
562
|
// unstarted — no background timer under a unit test, reads degrade to the stat-only
|
|
510
563
|
// tri-state — while `createServer` injects a started one over the production probes.
|
|
564
|
+
// Def-awareness (crew#481 / D-14): the ONE predicate the run DTOs' cache, the campaigns rollup and
|
|
565
|
+
// the resume 409 classify from. A fake adapter with no registry (`listWorkflows` absent) resolves
|
|
566
|
+
// no def ⇒ `runCanDeliver(view, null)` ⇒ candidate — byte-for-byte today's read.
|
|
567
|
+
const canDeliver = runtime.canDeliver ??
|
|
568
|
+
canDeliverResolver(() => (typeof adapter.listWorkflows === 'function' ? adapter.listWorkflows() : []), (m) => app.log.warn(m));
|
|
511
569
|
const deliveryCache = runtime.deliveryCache ??
|
|
512
570
|
new DeliveryDerivationCache({
|
|
513
571
|
listViews: () => adapter.sessionsDetail(),
|
|
514
572
|
probes: vacuityProbes,
|
|
515
573
|
isDelivered: (runId) => deliveryIndex.urlFor(runId) !== undefined,
|
|
574
|
+
canDeliver,
|
|
516
575
|
});
|
|
517
576
|
const deliverExec = runtime.deliverExec ?? runDeliverScript;
|
|
518
577
|
// crew#524: the deliver phase's script asks THIS daemon for the run-derived PR text, and the PR
|
|
@@ -528,6 +587,8 @@ runtime = {}) {
|
|
|
528
587
|
const estateTool = runtime.callEstateTool ?? callEstateTool;
|
|
529
588
|
/** Repo root for a repo ref, from the registry — shared by the reprovision path below. */
|
|
530
589
|
const repoRootOf = async (repoRef) => (await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path;
|
|
590
|
+
/** DES-L9: PR number → head branch, through `gh pr view` unless the runtime injected an answerer. */
|
|
591
|
+
const resolvePullRequest = runtime.resolvePullRequest ?? resolvePullRequestViaGh;
|
|
531
592
|
// The run-DTO joins (DES-UX-001 §8.2/§8.3, DES-UX-002 §7.2): `project_id` from the membership
|
|
532
593
|
// record — `null` = genuinely unfiled, so the field is ALWAYS present on served runs —
|
|
533
594
|
// `retry_of` from the lineage index and `guidance` from the guidance index, each set only
|
|
@@ -577,7 +638,7 @@ runtime = {}) {
|
|
|
577
638
|
return { delivery: 'delivered', deliverUrl: url };
|
|
578
639
|
if (conflictStrand)
|
|
579
640
|
return { delivery: 'stranded' };
|
|
580
|
-
return deliveryCache.read(view
|
|
641
|
+
return deliveryCache.read(view);
|
|
581
642
|
};
|
|
582
643
|
const decorateRun = (view) => {
|
|
583
644
|
const conflictStrand = normalizeStranded(view);
|
|
@@ -607,6 +668,11 @@ runtime = {}) {
|
|
|
607
668
|
const createdAt = runTimingIndex.createdAtFor(view.session.id);
|
|
608
669
|
if (createdAt !== undefined)
|
|
609
670
|
view.session.created_at = createdAt;
|
|
671
|
+
// `ended_at` (crew#496 / studio#230; api-types 0.38.0): the same posture from the `run.ended`
|
|
672
|
+
// entry the daemon records at the run's terminal frame — ABSENT when it has none.
|
|
673
|
+
const endedAt = runTimingIndex.endedAtFor(view.session.id);
|
|
674
|
+
if (endedAt !== undefined)
|
|
675
|
+
view.session.ended_at = endedAt;
|
|
610
676
|
const state = resolveDelivery(view, conflictStrand);
|
|
611
677
|
view.session.delivery = state.delivery;
|
|
612
678
|
if (state.deliverUrl !== undefined)
|
|
@@ -634,19 +700,26 @@ runtime = {}) {
|
|
|
634
700
|
// probe honestly reports no gate.
|
|
635
701
|
const capabilities = typeof adapter.engineCapabilities === 'function'
|
|
636
702
|
? adapter.engineCapabilities()
|
|
637
|
-
: { deliverGate: false };
|
|
703
|
+
: { deliverGate: false, revisesPr: false };
|
|
638
704
|
// wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
|
|
639
705
|
// daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
|
|
640
706
|
// launch while the state home holds an entry the worker Read fence cannot classify. Re-surveyed
|
|
641
707
|
// per probe (two readdirs) so an entry that appears after boot is reported without a restart;
|
|
642
708
|
// the field is ABSENT when there is nothing to say, and on a route set booted without the watch.
|
|
643
709
|
const stateHome = runtime.stateHome !== undefined ? await runtime.stateHome.refresh() : null;
|
|
644
|
-
const warnings = stateHome === null
|
|
645
|
-
? []
|
|
646
|
-
: stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }));
|
|
647
710
|
// crew#554: the BASE skill posture for the next launch — the composer's confirm line
|
|
648
711
|
// ("discipline skill: <name> gen N" / "MISSING — …"). Cached by the skills runtime: no I/O.
|
|
649
712
|
const baseSkill = runtime.skills?.baseSkill() ?? null;
|
|
713
|
+
// F-W1-102: the base skill's ERROR rides `warnings` too. A daemon under `require` with no published
|
|
714
|
+
// generation holding the skill (the crew-only install: no wicked-garden) REFUSES every launch at
|
|
715
|
+
// intake — it must never read "status ok, no warnings". The SAME finding object as
|
|
716
|
+
// `baseSkill.finding` and `/diagnostics.skills.findings[]` (one surface, no new field;
|
|
717
|
+
// `HealthWarning.kind` is open); status stays ok because the daemon serves and studio must load
|
|
718
|
+
// and show it — exactly the state-home blocker's precedent above.
|
|
719
|
+
const warnings = [
|
|
720
|
+
...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
|
|
721
|
+
...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
|
|
722
|
+
];
|
|
650
723
|
return {
|
|
651
724
|
status: 'ok',
|
|
652
725
|
version: PKG_VERSION,
|
|
@@ -1157,6 +1230,36 @@ runtime = {}) {
|
|
|
1157
1230
|
}
|
|
1158
1231
|
if (deliver === 'pr')
|
|
1159
1232
|
input.deliver = 'pr';
|
|
1233
|
+
// DES-L9 / crew#550 — REVISION: `revisesPr` names an OPEN same-repository pull request whose
|
|
1234
|
+
// head branch becomes the run's base (`baseRef`, crew-internal → the engine's
|
|
1235
|
+
// `LaunchSpec.base_ref`) and the push target of the composed deliver phase — the PR gains
|
|
1236
|
+
// exactly the run's commits, no second PR. Resolved HERE via `gh pr view` (5 s; the engine has
|
|
1237
|
+
// no GitHub client) and refused by name on anything but OPEN + same repo. Judged AFTER the
|
|
1238
|
+
// deliver resolution (F5): a launch this daemon resolved to `none` (deliverDefault) has no
|
|
1239
|
+
// phase to push with — a revision that pushes nothing would be a silent no-op.
|
|
1240
|
+
let revisesPr;
|
|
1241
|
+
if (b.revisesPr !== undefined) {
|
|
1242
|
+
if (deliver !== 'pr') {
|
|
1243
|
+
return reply.code(409).send({
|
|
1244
|
+
error: `revisesPr needs deliver: pr — this daemon resolved the launch to none (deliverDefault); send deliver: "pr"`,
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
const revisedRepo = b.repoRef;
|
|
1248
|
+
if (revisedRepo === undefined) {
|
|
1249
|
+
// Unreachable past the schema refine; kept so the type narrows honestly.
|
|
1250
|
+
return reply.code(400).send({ error: 'revisesPr needs repoRef' });
|
|
1251
|
+
}
|
|
1252
|
+
const root = await repoRootOf(revisedRepo);
|
|
1253
|
+
if (root === undefined) {
|
|
1254
|
+
return reply.code(404).send({ error: `repoRef names an unknown repo: ${revisedRepo}` });
|
|
1255
|
+
}
|
|
1256
|
+
const resolved = await resolvePullRequest(root, b.revisesPr);
|
|
1257
|
+
if (!resolved.ok)
|
|
1258
|
+
return reply.code(409).send({ error: resolved.error });
|
|
1259
|
+
revisesPr = { number: b.revisesPr, headRef: resolved.pr.headRef, url: resolved.pr.url };
|
|
1260
|
+
input.baseRef = revisesPr.headRef;
|
|
1261
|
+
input.revisesPr = revisesPr;
|
|
1262
|
+
}
|
|
1160
1263
|
// Retry lineage (DES-UX-001 §8.3): `retryOf` must name an EXISTING run — recording lineage
|
|
1161
1264
|
// to a run that never existed would be provenance pointing at nothing, so the launch fails
|
|
1162
1265
|
// loudly (400, before anything is committed) rather than filing a dangling edge.
|
|
@@ -1212,6 +1315,9 @@ runtime = {}) {
|
|
|
1212
1315
|
// CREW-UX-3: the trail is the durable record of lineage — the retry index (and a
|
|
1213
1316
|
// restarted daemon's hydrate) reads it back from exactly this entry.
|
|
1214
1317
|
...(b.retryOf !== undefined ? { retryOf: b.retryOf } : {}),
|
|
1318
|
+
// DES-L9: the revised PR (number, head branch, URL) — the retry index (and a restarted
|
|
1319
|
+
// daemon's hydrate) reads it back from here so a stranded revision re-pushes to THAT PR.
|
|
1320
|
+
...(revisesPr !== undefined ? { revisesPr, baseRef: revisesPr.headRef } : {}),
|
|
1215
1321
|
// wicked-studio#27: the trail is likewise the durable record of the group attach —
|
|
1216
1322
|
// the group index (and a restarted daemon's hydrate) reads it back from here.
|
|
1217
1323
|
...(b.campaignId !== undefined ? { campaignId: b.campaignId } : {}),
|
|
@@ -1219,6 +1325,8 @@ runtime = {}) {
|
|
|
1219
1325
|
});
|
|
1220
1326
|
if (b.retryOf !== undefined)
|
|
1221
1327
|
retryIndex.set(runId, b.retryOf);
|
|
1328
|
+
if (revisesPr !== undefined)
|
|
1329
|
+
retryIndex.setRevisesPr(runId, revisesPr);
|
|
1222
1330
|
if (b.campaignId !== undefined)
|
|
1223
1331
|
groupIndex.set(runId, { campaignId: b.campaignId });
|
|
1224
1332
|
else if (b.groupLabel !== undefined)
|
|
@@ -1279,9 +1387,8 @@ runtime = {}) {
|
|
|
1279
1387
|
// composer renders a clear card (skill, policy, remedy) instead of a generic launch 400.
|
|
1280
1388
|
if (/\bbase skill\b.*\brefused at intake\b/is.test(msg)) {
|
|
1281
1389
|
const posture = runtime.skills?.baseSkill() ?? null;
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
: `install a wicked-garden that ships the skill, POST /skills/refresh-baseline, then POST /skills/publish; or set baseSkillPolicy "warn" (PUT /settings) to run without the discipline directive`;
|
|
1390
|
+
// F-W1-102: the SAME remedy sentence the /health finding and `wicked-crew status` carry.
|
|
1391
|
+
const remedy = baseSkillRemedy(posture?.inCatalog ?? false);
|
|
1285
1392
|
return reply.code(422).send({ code: 'base_skill_refused', error: msg, baseSkill: posture, remedy });
|
|
1286
1393
|
}
|
|
1287
1394
|
const busy = /busy|in flight|already/i.test(msg);
|
|
@@ -1419,7 +1526,10 @@ runtime = {}) {
|
|
|
1419
1526
|
const origin = boundOrigin(app.server.address());
|
|
1420
1527
|
// The workflow DEFINITION (a user-registered workflow's view carries the engine instance id).
|
|
1421
1528
|
const def = resolveRunWorkflow(run, listWorkflowsSafe());
|
|
1422
|
-
const text = composeDeliverText(factsFromRun(decorateRun(run), runUrlFor(origin, id), {
|
|
1529
|
+
const text = composeDeliverText(factsFromRun(decorateRun(run), runUrlFor(origin, id), {
|
|
1530
|
+
workflowId: def?.id ?? null,
|
|
1531
|
+
revisesPr: retryIndex.revisesPrFor(id) ?? null,
|
|
1532
|
+
}));
|
|
1423
1533
|
return reply.type('text/plain; charset=utf-8').send(framedDeliverText(text));
|
|
1424
1534
|
});
|
|
1425
1535
|
// ── Post-hoc delivery (crew#393) — lift a stranded run's worktree into a PR ──
|
|
@@ -1520,6 +1630,9 @@ runtime = {}) {
|
|
|
1520
1630
|
// the throwaway down after the lift.
|
|
1521
1631
|
let result;
|
|
1522
1632
|
let worktreeGone = false;
|
|
1633
|
+
// DES-L9: a stranded REVISION re-pushes onto its PR's branch (re-checked OPEN via the same
|
|
1634
|
+
// resolver), never a new PR; a PR that closed meanwhile is a named 409.
|
|
1635
|
+
let revisionRefused;
|
|
1523
1636
|
try {
|
|
1524
1637
|
// The worktree admin (reprovision), the deliver spawn, AND the throwaway teardown all run
|
|
1525
1638
|
// under the per-repo lock, so a second stranded run in this repo cannot touch
|
|
@@ -1544,10 +1657,27 @@ runtime = {}) {
|
|
|
1544
1657
|
// holds (the fallback), and names this daemon so the script can re-ask at delivery.
|
|
1545
1658
|
const origin = boundOrigin(app.server.address());
|
|
1546
1659
|
const def = resolveRunWorkflow(run, listWorkflowsSafe());
|
|
1660
|
+
const revising = retryIndex.revisesPrFor(id);
|
|
1661
|
+
let revisesPr = null;
|
|
1662
|
+
if (revising !== undefined) {
|
|
1663
|
+
const revisedRoot = await repoRootOf(repoRef);
|
|
1664
|
+
const again = revisedRoot === undefined
|
|
1665
|
+
? { ok: false, error: `repo ${repoRef} is no longer registered — cannot re-check pull request #${revising.number}` }
|
|
1666
|
+
: await resolvePullRequest(revisedRoot, revising.number);
|
|
1667
|
+
if (!again.ok) {
|
|
1668
|
+
revisionRefused = again.error;
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
revisesPr = { number: revising.number, headRef: again.pr.headRef, url: again.pr.url };
|
|
1672
|
+
}
|
|
1547
1673
|
result = await deliverExec(workdir, s.problem ?? undefined, {
|
|
1548
1674
|
runId: id,
|
|
1549
1675
|
apiOrigin: origin,
|
|
1550
|
-
facts: factsFromRun(run, runUrlFor(origin, id), {
|
|
1676
|
+
facts: factsFromRun(run, runUrlFor(origin, id), {
|
|
1677
|
+
workflowId: def?.id ?? null,
|
|
1678
|
+
revisesPr: revisesPr === null ? null : { number: revisesPr.number, url: revisesPr.url },
|
|
1679
|
+
}),
|
|
1680
|
+
revisesPr,
|
|
1551
1681
|
});
|
|
1552
1682
|
}
|
|
1553
1683
|
finally {
|
|
@@ -1567,6 +1697,9 @@ runtime = {}) {
|
|
|
1567
1697
|
error: `run ${id}'s worktree is gone (${s.workdir}) — nothing left to deliver`,
|
|
1568
1698
|
});
|
|
1569
1699
|
}
|
|
1700
|
+
if (revisionRefused !== undefined) {
|
|
1701
|
+
return reply.code(409).send({ error: revisionRefused });
|
|
1702
|
+
}
|
|
1570
1703
|
if (result === undefined) {
|
|
1571
1704
|
// Unreachable: the lock body assigns `result` on every path that is not `worktreeGone`.
|
|
1572
1705
|
return reply.code(500).send({ error: `deliver for run ${id} produced no result` });
|
|
@@ -1783,7 +1916,8 @@ runtime = {}) {
|
|
|
1783
1916
|
// taken out by the council-bench / dispatch-timeout path, not by admission, so admission's
|
|
1784
1917
|
// list never named it. Every requested-or-defaulted seat that is not in `seats` is named
|
|
1785
1918
|
// here with the most specific cause the daemon knows: its own "no credential" report
|
|
1786
|
-
// (`auth`),
|
|
1919
|
+
// (`auth`), else the dispatch budget (`budget`). (R5b: crew keeps no council bench of its
|
|
1920
|
+
// own any more — the engine benches per run and says so in `unitDistributed.degradedReason`.)
|
|
1787
1921
|
for (const key of clis) {
|
|
1788
1922
|
if (seats.some((s) => s.cliKey === key) || refused.some((r) => r.cliKey === key))
|
|
1789
1923
|
continue;
|
|
@@ -1798,13 +1932,6 @@ runtime = {}) {
|
|
|
1798
1932
|
source: 'auth',
|
|
1799
1933
|
});
|
|
1800
1934
|
}
|
|
1801
|
-
else if (st?.council_bench !== undefined) {
|
|
1802
|
-
refused.push({
|
|
1803
|
-
cliKey: key,
|
|
1804
|
-
reason: `not seated — ${st.council_ineligible_reason ?? 'benched by this daemon’s recent councils'}`,
|
|
1805
|
-
source: 'bench',
|
|
1806
|
-
});
|
|
1807
|
-
}
|
|
1808
1935
|
else {
|
|
1809
1936
|
refused.push({
|
|
1810
1937
|
cliKey: key,
|
|
@@ -1878,7 +2005,7 @@ runtime = {}) {
|
|
|
1878
2005
|
projectAttachError = message(err);
|
|
1879
2006
|
}
|
|
1880
2007
|
}
|
|
1881
|
-
if (!chatScopes.set(chatId, scope, token, refused)) {
|
|
2008
|
+
if (!chatScopes.set(chatId, scope, token, refused, engine)) {
|
|
1882
2009
|
// The reservation was cancelled while the open was in flight — an engine `chatClosed` or a
|
|
1883
2010
|
// `DELETE` for this id (Copilot, #518). Nothing was recorded; tear the chat down (engine
|
|
1884
2011
|
// session, scratch root, and the filing just made) instead of returning a stale 201.
|
|
@@ -1927,6 +2054,19 @@ runtime = {}) {
|
|
|
1927
2054
|
chatScopes.release(chatId, token);
|
|
1928
2055
|
}
|
|
1929
2056
|
});
|
|
2057
|
+
/**
|
|
2058
|
+
* The ONE 409 `turn_in_flight` body (review NIT): the send route and the re-seat route refuse for
|
|
2059
|
+
* the same reason and promise the same shape, so they build it in one place and cannot drift.
|
|
2060
|
+
* `nothingHappened` names what did NOT happen on this route — their only difference.
|
|
2061
|
+
*/
|
|
2062
|
+
const turnInFlightBody = (chatId, inFlight, nothingHappened) => ({
|
|
2063
|
+
code: 'turn_in_flight',
|
|
2064
|
+
error: `${inFlight.busy.join(', ')} ${inFlight.busy.length === 1 ? 'is' : 'are'} still answering the previous ` +
|
|
2065
|
+
`message in this chat (turn ${inFlight.turnId}, ${Math.round(inFlight.ageMs / 1000)}s ago: ` +
|
|
2066
|
+
`“${inFlight.excerpt}”) — wait for the reply, or target only idle seats. ${nothingHappened}`,
|
|
2067
|
+
chatId,
|
|
2068
|
+
turn: inFlight,
|
|
2069
|
+
});
|
|
1930
2070
|
const ChatMessageSchema = z.object({
|
|
1931
2071
|
text: z.string().min(1).max(65536),
|
|
1932
2072
|
targets: z.array(z.string().min(1)).min(1).max(8).optional(),
|
|
@@ -1944,28 +2084,103 @@ runtime = {}) {
|
|
|
1944
2084
|
if (!parsed.success) {
|
|
1945
2085
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
1946
2086
|
}
|
|
1947
|
-
|
|
2087
|
+
// DES-L5 §5-c (F-E2E-041): the audience is decided HERE — the named targets, else the engine's
|
|
2088
|
+
// warm seats — so the turn can be opened BEFORE the engine call, in the same tick as the
|
|
2089
|
+
// predicate below: two racing sends can no longer both pass `inFlight` during the await, and a
|
|
2090
|
+
// `chatDelta` that lands before `chatSend` resolves is already stamped with its `turn_id`.
|
|
2091
|
+
const audience = parsed.data.targets ?? (await adapter.chatSeats(id));
|
|
2092
|
+
if (audience.length === 0) {
|
|
2093
|
+
return reply.code(409).send({ error: `chat '${id}' has no warm seats — open it first` });
|
|
2094
|
+
}
|
|
2095
|
+
const inFlight = chatTurns.inFlight(id, audience);
|
|
1948
2096
|
if (inFlight !== null) {
|
|
1949
|
-
|
|
1950
|
-
return reply.code(409).send({
|
|
1951
|
-
code: 'turn_in_flight',
|
|
1952
|
-
error: `${inFlight.busy.join(', ')} ${inFlight.busy.length === 1 ? 'is' : 'are'} still answering the previous ` +
|
|
1953
|
-
`message in this chat (turn ${inFlight.turnId}, ${ageS}s ago: “${inFlight.excerpt}”) — wait for the ` +
|
|
1954
|
-
`reply, or target only idle seats. Nothing was sent.`,
|
|
1955
|
-
chatId: id,
|
|
1956
|
-
turn: inFlight,
|
|
1957
|
-
});
|
|
2097
|
+
return reply.code(409).send(turnInFlightBody(id, inFlight, 'Nothing was sent.'));
|
|
1958
2098
|
}
|
|
2099
|
+
// Sync, same tick as `inFlight`: the window is closed before anything yields.
|
|
2100
|
+
const turn = chatTurns.begin(id, audience, parsed.data.text);
|
|
1959
2101
|
try {
|
|
1960
2102
|
const seats = await adapter.chatSend(id, parsed.data.text, parsed.data.targets);
|
|
1961
|
-
|
|
2103
|
+
if (turn !== null) {
|
|
2104
|
+
// The engine's answer is the truth: the reserved audience is squared with the seats it
|
|
2105
|
+
// reached, and the operator's message joins the transcript with exactly those seats.
|
|
2106
|
+
chatTurns.reconcile(id, turn.turnId, seats);
|
|
2107
|
+
chatTranscripts?.appendUser(id, turn.turnId, parsed.data.text, seats);
|
|
2108
|
+
}
|
|
1962
2109
|
return reply.code(202).send({ seats, ...(turn !== null ? { turnId: turn.turnId } : {}) });
|
|
1963
2110
|
}
|
|
1964
2111
|
catch (err) {
|
|
2112
|
+
// Nothing went out: the reservation is retracted so the next send is not refused for it.
|
|
2113
|
+
if (turn !== null)
|
|
2114
|
+
chatTurns.abort(id, turn.turnId);
|
|
1965
2115
|
const msg = message(err);
|
|
1966
2116
|
return reply.code(/no warm seats/.test(msg) ? 409 : 400).send({ error: msg });
|
|
1967
2117
|
}
|
|
1968
2118
|
});
|
|
2119
|
+
// F-W1-005 (wave-1 P6): re-seat NAMED seats on a LIVE chat — the retry lever for a seat the open
|
|
2120
|
+
// refused (or whose session died). Re-runs the engine's per-seat `chat_ensure` through `chatOpen`
|
|
2121
|
+
// with the IDENTICAL scope recorded at open (a different scope would evict every warm seat —
|
|
2122
|
+
// Copilot, #426; the index retains what the engine was handed). Explicit seats bypass the daemon's
|
|
2123
|
+
// default pre-filter exactly as `clis` on `POST /chats` does: the engine refuses per seat and its
|
|
2124
|
+
// reason rides the outcome, then `refused[]` and a `chatSeatRefused` frame — the same shapes the
|
|
2125
|
+
// 201 carries, so the thread and the picker read one story. Never a new chat, never a new pool key.
|
|
2126
|
+
const ChatSeatsSchema = z.object({
|
|
2127
|
+
clis: z.array(z.string().min(1)).min(1).max(8),
|
|
2128
|
+
}).strict();
|
|
2129
|
+
app.post(`${V}/chats/:id/seats`,
|
|
2130
|
+
// Typed like every neighbouring chat route (review HIGH-1): the names bind the 0.39.0 api-types
|
|
2131
|
+
// mirror to this row — `ChatSeatsBody { clis }` in, `ChatSeatsResponse { chatId, seats, refused }`
|
|
2132
|
+
// out (the 201's own `ChatSeatOutcome` / `ChatSeatRefusal` shapes, which 0.38.0 already ships).
|
|
2133
|
+
{
|
|
2134
|
+
config: {
|
|
2135
|
+
manifest: {
|
|
2136
|
+
requestType: 'ChatSeatsBody',
|
|
2137
|
+
responseType: 'ChatSeatsResponse',
|
|
2138
|
+
statusCodes: [200, 400, 404, 409],
|
|
2139
|
+
},
|
|
2140
|
+
},
|
|
2141
|
+
}, async (req, reply) => {
|
|
2142
|
+
const { id } = req.params;
|
|
2143
|
+
const parsed = ChatSeatsSchema.safeParse(req.body);
|
|
2144
|
+
if (!parsed.success) {
|
|
2145
|
+
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
2146
|
+
}
|
|
2147
|
+
const engine = chatScopes.engineOf(id);
|
|
2148
|
+
if (engine === undefined) {
|
|
2149
|
+
return reply.code(404).send({
|
|
2150
|
+
error: `chat ${id} is not open on this daemon — open a chat first, then re-seat into it`,
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
const clis = [...new Set(parsed.data.clis)];
|
|
2154
|
+
// A seat mid-turn is not re-warmed under its own reply (the same rule as a send).
|
|
2155
|
+
const inFlight = chatTurns.inFlight(id, clis);
|
|
2156
|
+
if (inFlight !== null) {
|
|
2157
|
+
return reply.code(409).send(turnInFlightBody(id, inFlight, 'No seat was re-warmed.'));
|
|
2158
|
+
}
|
|
2159
|
+
try {
|
|
2160
|
+
const seats = await adapter.chatOpen(id, clis, engine.cwd, {
|
|
2161
|
+
codeGraphDb: engine.codeGraphDb,
|
|
2162
|
+
readRoots: engine.readRoots,
|
|
2163
|
+
});
|
|
2164
|
+
const refused = chatScopes.foldSeats(id, seats);
|
|
2165
|
+
const projectId = projects.index.projectOf(id) ?? undefined;
|
|
2166
|
+
for (const s of seats) {
|
|
2167
|
+
if (s.ok)
|
|
2168
|
+
continue;
|
|
2169
|
+
runtime.broadcast?.({
|
|
2170
|
+
type: 'chatSeatRefused',
|
|
2171
|
+
chat: id,
|
|
2172
|
+
cliKey: s.cliKey,
|
|
2173
|
+
reason: s.error ?? 'the engine refused the seat',
|
|
2174
|
+
source: 'engine',
|
|
2175
|
+
...(projectId !== undefined ? { project_id: projectId } : {}),
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
return { chatId: id, seats, refused };
|
|
2179
|
+
}
|
|
2180
|
+
catch (err) {
|
|
2181
|
+
return reply.code(400).send({ error: message(err) });
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
1969
2184
|
// Enumerate live chats (FINDING-027 gap 4). Chat sessions deliberately outlive the page, and
|
|
1970
2185
|
// their ids are minted client-side — so before this route the only record of an orphaned seat
|
|
1971
2186
|
// lived in the tab that abandoned it, and an operator could not reclaim one without restarting
|
|
@@ -2012,6 +2227,9 @@ runtime = {}) {
|
|
|
2012
2227
|
seats: await adapter.chatSeats(id),
|
|
2013
2228
|
scope: chatScopes.get(id) ?? null,
|
|
2014
2229
|
refused: chatScopes.refusedOf(id) ?? null,
|
|
2230
|
+
// DES-L5 (D-13): the transcript so far, append order — `[]` before the first persisted
|
|
2231
|
+
// turn; the file goes with the chat on `chatClosed`, so a reclaimed id answers `[]` too.
|
|
2232
|
+
...(chatTranscripts !== undefined ? { messages: chatTranscripts.read(id) } : {}),
|
|
2015
2233
|
};
|
|
2016
2234
|
}
|
|
2017
2235
|
catch (err) {
|
|
@@ -2154,7 +2372,10 @@ runtime = {}) {
|
|
|
2154
2372
|
isSteeringAuthorRun(run, adapter.listWorkflows());
|
|
2155
2373
|
const gatePrompt = steeringPropose ? gateCache.get(id)?.prompt : undefined;
|
|
2156
2374
|
try {
|
|
2157
|
-
|
|
2375
|
+
// All five positionals, explicit `undefined` for the absent ones (DES-L1 PR-2; the
|
|
2376
|
+
// gate-arms tripwire pins the arity). An engine refusal (no creator to send back to,
|
|
2377
|
+
// an engine older than the arm) surfaces as the existing 409 below.
|
|
2378
|
+
const status = await adapter.confirmGate(id, parsed.data.approve, parsed.data.amend, parsed.data.action, parsed.data.amendScope);
|
|
2158
2379
|
// WHO approved/rejected — the gate-decision audit (task #88). The engine
|
|
2159
2380
|
// records THAT the gate resolved (interaction_requests / gateDecided);
|
|
2160
2381
|
// only this HTTP layer knows the authenticated principal behind it.
|
|
@@ -2163,6 +2384,8 @@ runtime = {}) {
|
|
|
2163
2384
|
detail: {
|
|
2164
2385
|
approve: parsed.data.approve,
|
|
2165
2386
|
...(parsed.data.amend !== undefined ? { amend: parsed.data.amend } : {}),
|
|
2387
|
+
...(parsed.data.action !== undefined ? { action: parsed.data.action } : {}),
|
|
2388
|
+
...(parsed.data.amendScope !== undefined ? { amendScope: parsed.data.amendScope } : {}),
|
|
2166
2389
|
status,
|
|
2167
2390
|
},
|
|
2168
2391
|
});
|
|
@@ -2575,14 +2798,22 @@ runtime = {}) {
|
|
|
2575
2798
|
if (!ids.includes(id)) {
|
|
2576
2799
|
return reply.code(404).send({ error: 'Run not found' });
|
|
2577
2800
|
}
|
|
2578
|
-
const
|
|
2579
|
-
if (
|
|
2801
|
+
const engineEvents = await adapter.runEvents(id);
|
|
2802
|
+
if (engineEvents === null) {
|
|
2580
2803
|
// Same shape as the gate route's 503, and for the same reason: "no events" would report a
|
|
2581
2804
|
// missing binding as a fact about the run. The run may well have a rich history.
|
|
2582
2805
|
return reply.code(503).send({
|
|
2583
2806
|
error: 'Run history is unavailable: this wicked-core build has no event-log read binding',
|
|
2584
2807
|
});
|
|
2585
2808
|
}
|
|
2809
|
+
// wicked-studio#284: the stall watchdog's frames are daemon-authored — the engine's log never
|
|
2810
|
+
// saw them — so a reloaded page used to lose the "needs you" facts the live socket carried.
|
|
2811
|
+
// Merge the remembered frames in capture order beside the engine's; they carry `daemon: true`
|
|
2812
|
+
// and no engine `seq`.
|
|
2813
|
+
const daemonFrames = runtime.stallFrames?.(id) ?? [];
|
|
2814
|
+
const events = daemonFrames.length === 0
|
|
2815
|
+
? engineEvents
|
|
2816
|
+
: withServedSeq([...engineEvents, ...daemonFrames].sort((a, b) => a.ts - b.ts));
|
|
2586
2817
|
// An empty array here is a real answer, not a failure: runs that predate the log have no
|
|
2587
2818
|
// history, and saying so is the honest response.
|
|
2588
2819
|
// Both spellings of "several types" mean the same thing: `?type=a,b` and `?type=a&type=b`.
|
|
@@ -3152,17 +3383,28 @@ runtime = {}) {
|
|
|
3152
3383
|
if (q.focus !== undefined && q.focus.trim() !== '') {
|
|
3153
3384
|
args.push('--focus', q.focus.trim());
|
|
3154
3385
|
}
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3386
|
+
// Whole-graph `totals` beside the served slice (crew#505 / F-RC1-100; api-types 0.38.0):
|
|
3387
|
+
// `graph-view --limit` emits only the slice, so `stats` alone read "my repo has 150 symbols".
|
|
3388
|
+
// `wicked-estate stats --db` prints `nodes=N edges=M files=F …`; both spawns run under
|
|
3389
|
+
// `Promise.all` (one round trip, not two), the SAME daemon-side CLI posture as `graph-view`.
|
|
3390
|
+
// A stats spawn that fails or prints something else leaves `totals` ABSENT and the slice
|
|
3391
|
+
// still 200 — the tile then knows only the slice, never a substituted count.
|
|
3392
|
+
const exe = estateExe();
|
|
3393
|
+
const [{ stdout }, statsOut] = await Promise.all([
|
|
3394
|
+
execCapped(exe, args, { timeout: 30_000, cwd: repo.root_path }),
|
|
3395
|
+
execCapped(exe, ['stats', '--db', dbPath], { timeout: 30_000, cwd: repo.root_path })
|
|
3396
|
+
.then((r) => r.stdout)
|
|
3397
|
+
.catch(() => null),
|
|
3398
|
+
]);
|
|
3159
3399
|
const raw = JSON.parse(stdout);
|
|
3160
3400
|
const fileCount = new Set(raw.nodes.map((n) => n.file)).size;
|
|
3401
|
+
const totals = statsOut === null ? undefined : parseEstateTotals(statsOut);
|
|
3161
3402
|
const built = {
|
|
3162
3403
|
graph: {
|
|
3163
3404
|
nodes: raw.nodes,
|
|
3164
3405
|
edges: raw.edges,
|
|
3165
3406
|
stats: { nodeCount: raw.nodes.length, edgeCount: raw.edges.length, fileCount },
|
|
3407
|
+
...(totals !== undefined ? { totals } : {}),
|
|
3166
3408
|
},
|
|
3167
3409
|
};
|
|
3168
3410
|
return reply.send(built);
|
|
@@ -3316,7 +3558,11 @@ runtime = {}) {
|
|
|
3316
3558
|
// The store is SHARED with the skin (crew#323): beside the engine's own keys it round-trips
|
|
3317
3559
|
// the studio's `studio.*` preference blobs verbatim — see `CrewSystemSettings`'s index
|
|
3318
3560
|
// signature in core/types.ts, which states that rather than leaving it to a client comment.
|
|
3319
|
-
|
|
3561
|
+
// crew#494 (F-007 — FIX-IT-ALL L10-6): the System page showed a LITERAL settings path; the daemon
|
|
3562
|
+
// now says where its settings file actually is (`WICKED_CREW_SYSTEM_SETTINGS` honoured) as the
|
|
3563
|
+
// additive `path` (api-types 0.38.0 `SettingsResponse.path?`; adjudicated §4.6 — `path`, not
|
|
3564
|
+
// `settings_path`).
|
|
3565
|
+
app.get(`${V}/settings`, async () => ({ settings: await adapter.getSettings(), path: settingsFilePath() }));
|
|
3320
3566
|
app.put(`${V}/settings`, async (req, reply) => {
|
|
3321
3567
|
const patch = req.body;
|
|
3322
3568
|
if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {
|
|
@@ -3406,7 +3652,7 @@ runtime = {}) {
|
|
|
3406
3652
|
const p = patch.baseSkillPolicy;
|
|
3407
3653
|
if (typeof p !== 'string' || !BASE_SKILL_POLICIES.has(p)) {
|
|
3408
3654
|
return reply.code(400).send({
|
|
3409
|
-
error: "baseSkillPolicy must be '
|
|
3655
|
+
error: "baseSkillPolicy must be 'require' — the only policy (the engine refuses launches at intake until the published snapshot holds the base skill); 'warn' was deleted in crew 0.7.35: it ran seats UNGROUNDED. Set baseSkillRef \"\" to turn the base skill off explicitly",
|
|
3410
3656
|
});
|
|
3411
3657
|
}
|
|
3412
3658
|
}
|
|
@@ -3819,6 +4065,8 @@ runtime = {}) {
|
|
|
3819
4065
|
vacuity: vacuityProbes,
|
|
3820
4066
|
// A non-probe derivation throw in the rollup is a defect — error level, loud in diagnostics.
|
|
3821
4067
|
logDefect: (m) => app.log.error(m),
|
|
4068
|
+
// crew#481: the SAME def-awareness the run DTOs apply — one predicate, every surface.
|
|
4069
|
+
canDeliver,
|
|
3822
4070
|
});
|
|
3823
4071
|
// ── Governance wiki management (wiki-mgmt) — scoreboard + honest empty-state meta ──────────
|
|
3824
4072
|
// Browse (`GET /governance/rules` + facets) and retire (`DELETE /governance/rules/:id`) stay
|