wicked-crew 0.7.38 → 0.7.40
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 +37 -1
- package/dist/api/chat-scope.d.ts.map +1 -1
- package/dist/api/chat-scope.js +133 -9
- package/dist/api/chat-scope.js.map +1 -1
- package/dist/api/chat-transcripts.d.ts +45 -2
- package/dist/api/chat-transcripts.d.ts.map +1 -1
- package/dist/api/chat-transcripts.js +89 -5
- package/dist/api/chat-transcripts.js.map +1 -1
- package/dist/api/routes.d.ts +70 -10
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +206 -20
- 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 +63 -0
- package/dist/api/run-timing-index.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +120 -17
- package/dist/api/server.js.map +1 -1
- package/dist/api/testing.d.ts +26 -0
- package/dist/api/testing.d.ts.map +1 -1
- package/dist/api/testing.js +48 -2
- package/dist/api/testing.js.map +1 -1
- package/dist/api/worktree-sweep.d.ts +12 -0
- package/dist/api/worktree-sweep.d.ts.map +1 -0
- package/dist/api/worktree-sweep.js +41 -0
- package/dist/api/worktree-sweep.js.map +1 -0
- package/dist/cli/bearer.d.ts +8 -0
- package/dist/cli/bearer.d.ts.map +1 -0
- package/dist/cli/bearer.js +16 -0
- package/dist/cli/bearer.js.map +1 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +21 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +11 -2
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +23 -4
- package/dist/core/adapter.js.map +1 -1
- package/dist/interactive/demo-events.d.ts +3 -0
- package/dist/interactive/demo-events.d.ts.map +1 -1
- package/dist/interactive/demo-events.js +14 -1
- package/dist/interactive/demo-events.js.map +1 -1
- package/dist/interactive/doc-grounding.d.ts +6 -0
- package/dist/interactive/doc-grounding.d.ts.map +1 -1
- package/dist/interactive/doc-grounding.js +4 -0
- package/dist/interactive/doc-grounding.js.map +1 -1
- package/dist/interactive/draft-events.d.ts +4 -0
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +12 -2
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/proxy-routes.d.ts +14 -0
- package/dist/interactive/proxy-routes.d.ts.map +1 -1
- package/dist/interactive/proxy-routes.js +98 -3
- package/dist/interactive/proxy-routes.js.map +1 -1
- package/dist/interactive/repo-snapshot.d.ts.map +1 -1
- package/dist/interactive/repo-snapshot.js +3 -2
- package/dist/interactive/repo-snapshot.js.map +1 -1
- package/dist/projects/graph.d.ts +11 -0
- package/dist/projects/graph.d.ts.map +1 -1
- package/dist/projects/graph.js +12 -0
- package/dist/projects/graph.js.map +1 -1
- package/dist/qe/author-workflow.d.ts +2 -0
- package/dist/qe/author-workflow.d.ts.map +1 -1
- package/dist/qe/author-workflow.js +55 -12
- package/dist/qe/author-workflow.js.map +1 -1
- package/dist/skills/phase-skill-gaps.d.ts +108 -0
- package/dist/skills/phase-skill-gaps.d.ts.map +1 -0
- package/dist/skills/phase-skill-gaps.js +199 -0
- package/dist/skills/phase-skill-gaps.js.map +1 -0
- package/dist/skills/runtime.d.ts +1 -1
- package/dist/skills/runtime.d.ts.map +1 -1
- package/dist/skills/runtime.js +2 -1
- package/dist/skills/runtime.js.map +1 -1
- package/dist/studio/assets/index-Bg7Z3BR-.css +32 -0
- package/dist/studio/assets/index-BvOFMjf9.js +554 -0
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +100 -10
- package/endpoint-manifest.json +1 -1
- package/package.json +3 -3
- package/dist/studio/assets/index-C12tVMLX.css +0 -32
- package/dist/studio/assets/index-DFkL6Rzf.js +0 -546
package/dist/api/routes.js
CHANGED
|
@@ -20,13 +20,13 @@ import { applyWorkerConfigRoot, signedInHeuristic } from './seat-signin.js';
|
|
|
20
20
|
import { chatSeatAdmission } from './seat-standing.js';
|
|
21
21
|
import { rosterWithStandingFactory } from './roster-standing.js';
|
|
22
22
|
import { ChatTurnIndex } from './chat-turns.js';
|
|
23
|
-
import { ChatScopeIndex, chatScopeDeps, prepareChatScratch, removeChatScratch, resolveChatScope, } from './chat-scope.js';
|
|
23
|
+
import { ChatScopeIndex, chatScopeDeps, prepareChatScratch, removeChatScratch, namedKindRefusal, resolveChatScope, } from './chat-scope.js';
|
|
24
24
|
import { allowedRootsFor, isInsideRoot, openWithSystemDefault } from './open-path.js';
|
|
25
25
|
import { InvalidDiffBaseError, MERGE_BASE_LITERAL, NotARegularFileError, UnresolvableDiffBaseError, branchDiff, isPlainRef, readFileCapped, worktreeDiff, } from './run-files.js';
|
|
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 { estateExe, resolveProjectGraphBinding } from '../projects/graph.js';
|
|
29
|
+
import { estateExe, parseEstateTotals, 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';
|
|
@@ -35,6 +35,7 @@ import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js
|
|
|
35
35
|
import { registerTestingRoutes } from './testing.js';
|
|
36
36
|
import { registerSkillsRoutes } from './skills.js';
|
|
37
37
|
import { disabledSkillsHealth } from '../skills/runtime.js';
|
|
38
|
+
import { phaseSkillFindings, withPhaseSkillGaps } from '../skills/phase-skill-gaps.js';
|
|
38
39
|
import { STATE_HOME_BLOCKER_CODE, STATE_HOME_REMEDY, isEngineStateHomeRefusal, stateHomeBlockerBody, } from '../projects/state-home-preflight.js';
|
|
39
40
|
import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../skills/base-skill.js';
|
|
40
41
|
import { noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
|
|
@@ -83,17 +84,10 @@ const STATUS_ORDER = {
|
|
|
83
84
|
function sortActionableFirst(views) {
|
|
84
85
|
return [...views].sort((a, b) => (STATUS_ORDER[a.session.status] ?? 9) - (STATUS_ORDER[b.session.status] ?? 9));
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
}
|
|
87
|
+
// parseEstateTotals lives in projects/graph.ts — shared with chat-scope.ts (crew#642) to avoid
|
|
88
|
+
// a circular import (routes.ts → chat-scope.ts). Re-exported here for backward compat
|
|
89
|
+
// (tests import it from routes.js; see top-of-file import for local use).
|
|
90
|
+
export { parseEstateTotals };
|
|
97
91
|
function codeGraphDbOr503(repo, reply) {
|
|
98
92
|
try {
|
|
99
93
|
return codeGraphDb(repo);
|
|
@@ -383,6 +377,18 @@ export const LaunchSchema = z.object({
|
|
|
383
377
|
* Resolved via `gh pr view` at launch — not OPEN / a fork / gh failure ⇒ 409, nothing launched.
|
|
384
378
|
* Send it only when `GET /health.capabilities.revisesPr === true` (engine ≥ 0.7.27). */
|
|
385
379
|
revisesPr: z.number().int().positive().optional(),
|
|
380
|
+
/** crew#619 — the chat this run was promoted from; the daemon retains that chat's transcript
|
|
381
|
+
* on disk until this run reaches a terminal state so the Continue-in-Build prefill is
|
|
382
|
+
* always reproducible. Optional; omit when the launch is not promoted from a chat. */
|
|
383
|
+
chatId: z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
384
|
+
/** crew#632 — the surface that triggered this launch: `studio` (the web UI), `cli` (the
|
|
385
|
+
* wicked-crew CLI), or `api` (a programmatic caller). Persisted on the `run.launched` audit
|
|
386
|
+
* entry (`detail.channel`) and served on the run DTO; absent = origin unknown. */
|
|
387
|
+
channel: z.enum(['studio', 'cli', 'api']).optional(),
|
|
388
|
+
/** crew#632 — an opaque caller-supplied identifier for the user or system that triggered the
|
|
389
|
+
* launch (e.g. a CI job name or a studio tab id). Persisted on the `run.launched` audit entry
|
|
390
|
+
* (`detail.actor`) and served on the run DTO; absent = caller omitted it. */
|
|
391
|
+
actor: z.string().min(1).max(256).optional(),
|
|
386
392
|
}).strict().refine((b) => b.deliver !== 'pr' || b.workflow !== undefined, {
|
|
387
393
|
message: 'deliver: "pr" requires a workflow — a free-text run has no def to append the deliver phase to',
|
|
388
394
|
path: ['deliver'],
|
|
@@ -514,7 +520,82 @@ export const ChatOpenSchema = z.object({
|
|
|
514
520
|
/** File the chat into a project (`crew.chat` membership) — and, with no `repoRefs`, scope it to
|
|
515
521
|
* every registered `crew.repo` member of that project (crew#502). */
|
|
516
522
|
projectId: z.string().min(1).optional(),
|
|
523
|
+
/** The scope the caller NAMES (studio#323 R4) — omitted ⇒ the legacy inference. Shape rules
|
|
524
|
+
* (which kinds need / refuse `projectId` / `repoRefs`) are the resolver's (`chat-scope.ts`). */
|
|
525
|
+
scopeKind: z.enum(['system', 'everything', 'project', 'repo', 'repos', 'none']).optional(),
|
|
517
526
|
}).strict();
|
|
527
|
+
/**
|
|
528
|
+
* The `singleSeat` disclosure (crew#641, hole closed in crew#650) — built in ONE place so the 201
|
|
529
|
+
* and the 202 cannot drift.
|
|
530
|
+
*
|
|
531
|
+
* The property this field states is **"this chat has one seat; it cannot disagree with itself"**,
|
|
532
|
+
* and that is true of every one-warm-seat chat. The first cut also required `refused.length > 0`,
|
|
533
|
+
* so the case an operator is most likely to create — a chat deliberately opened with a single seat
|
|
534
|
+
* — returned `ok: true` with no signal at all, which is the exact failure mode #641 was filed
|
|
535
|
+
* about. It also made the 202 go quiet after a restart, when `refusedOf` no longer knows the
|
|
536
|
+
* refusals, even though the chat is just as single-seated as it was a minute earlier.
|
|
537
|
+
*
|
|
538
|
+
* `refused` therefore rides along as EVIDENCE when there is any — it never decides whether the
|
|
539
|
+
* caller is told.
|
|
540
|
+
*
|
|
541
|
+
* THREE states, not two (review of #658). The refusal list has a third possibility that `[]` cannot
|
|
542
|
+
* express: this daemon does not KNOW. `chatScopes.refusedOf` answers `undefined` for a chat this
|
|
543
|
+
* daemon did not open — the engine keeps the warm session across a restart, the in-memory index does
|
|
544
|
+
* not — and the first cut collapsed that to `[]` with `?? []`, which made the 202 tell an operator
|
|
545
|
+
* *"It was opened with a single seat — no other seat was refused"* about a chat that was opened with
|
|
546
|
+
* two seats, one of which was refused. Both halves false, stated confidently. It is the same
|
|
547
|
+
* distinction #651 got right for chat-promotion provenance (`.catch(() => null)`, never `[]`):
|
|
548
|
+
* unreadable and empty are different answers, and only one of them may be asserted.
|
|
549
|
+
*
|
|
550
|
+
* TWO MOMENTS, and only one route witnessed each (third review of #658). `refused` describes the
|
|
551
|
+
* daemon's record — what the OPEN (or the last re-seat) observed; `warmRoster` describes NOW. Any
|
|
552
|
+
* sentence built from both fabricates the instant they diverge, and they diverge routinely: a seat
|
|
553
|
+
* that blows its turn budget is RELEASED by the engine (`chat-turns.ts`), a re-seat clears an entry
|
|
554
|
+
* from the record, a restart drops the record entirely. A chat opened with `['claude','opencode']`
|
|
555
|
+
* — both warm, nothing refused, nothing unknown — whose `opencode` is later released would have been
|
|
556
|
+
* told *"It was opened with a single seat — no other seat was refused"*: false in both halves, with
|
|
557
|
+
* no `undefined` anywhere near it. So `at` names the moment the caller is asking about:
|
|
558
|
+
*
|
|
559
|
+
* • `'open'` — the 201. This route just performed the open, so it is the ONLY one with standing to
|
|
560
|
+
* describe it: refusals known and empty ⇒ it may say the chat was opened with a single seat.
|
|
561
|
+
* • `'turn'` — the 202. It knows the roster NOW and the refusal record; it knows nothing about how
|
|
562
|
+
* the chat came to look like this. Refusals known and empty ⇒ it says NOTHING about opening.
|
|
563
|
+
* It still names refusals that are on record, and still says so when the record is unavailable —
|
|
564
|
+
* both are statements about the record it holds, not about a moment it did not see.
|
|
565
|
+
*
|
|
566
|
+
* `refusalsKnown` carries the known/unknown distinction for a machine reader, because `refused: []`
|
|
567
|
+
* is exactly as ambiguous on the wire as the sentence was in prose.
|
|
568
|
+
*/
|
|
569
|
+
function singleSeatDisclosure(warmed,
|
|
570
|
+
/** `undefined` = this daemon has no refusal record for the chat (it did not open it). */
|
|
571
|
+
refused,
|
|
572
|
+
/** Which moment the caller is asking about — see the note above; only `'open'` may describe one. */
|
|
573
|
+
at) {
|
|
574
|
+
const known = refused !== undefined;
|
|
575
|
+
const list = refused ?? [];
|
|
576
|
+
const refusalClause = list.length > 0
|
|
577
|
+
? `Refused: ${list.map((r) => `${r.cliKey} (${r.reason})`).join('; ')}. `
|
|
578
|
+
: known
|
|
579
|
+
? at === 'open'
|
|
580
|
+
? 'It was opened with a single seat — no other seat was refused. '
|
|
581
|
+
: // A turn has no standing to say how the chat was opened: this seat may be the survivor of
|
|
582
|
+
// a two-seat chat whose other seat was released mid-session. Say nothing.
|
|
583
|
+
''
|
|
584
|
+
: 'This daemon holds no refusal record for this chat — it was opened before a restart — so ' +
|
|
585
|
+
'whether another seat was refused is UNKNOWN: the empty refused[] means the record is gone, ' +
|
|
586
|
+
'not that nothing was refused. ';
|
|
587
|
+
return {
|
|
588
|
+
degraded: true,
|
|
589
|
+
warmed,
|
|
590
|
+
refused: list,
|
|
591
|
+
refusalsKnown: known,
|
|
592
|
+
message:
|
|
593
|
+
// "warm" on a turn: the claim is about the roster as it stands, not about how it got there.
|
|
594
|
+
`This chat has one ${at === 'open' ? 'seat' : 'warm seat'} (${warmed}); it cannot disagree with itself. ` +
|
|
595
|
+
refusalClause +
|
|
596
|
+
'The single-seat root cause is tracked as wicked-core#563; this chat makes it visible.',
|
|
597
|
+
};
|
|
598
|
+
}
|
|
518
599
|
export function registerRoutes(app, adapter, gateCache, elicitationCache,
|
|
519
600
|
// Defaulted so a caller that never arms the bus seam (tests drive this
|
|
520
601
|
// function directly) gets the same behavior as an unarmed daemon: an empty
|
|
@@ -661,6 +742,10 @@ runtime = {}) {
|
|
|
661
742
|
const guidance = guidanceIndex.guidanceFor(view.session.id);
|
|
662
743
|
if (guidance !== undefined)
|
|
663
744
|
view.session.guidance = guidance;
|
|
745
|
+
// crew#661: the declared skills this run's phases ran WITHOUT (degrade-and-disclose) — ABSENT when none.
|
|
746
|
+
const skillGaps = runtime.runSkillGaps?.gapsFor(view.session.id);
|
|
747
|
+
if (skillGaps !== undefined)
|
|
748
|
+
view.session.skill_gaps = skillGaps;
|
|
664
749
|
// Run launch time (home command-center run metrics): unix SECONDS from the `run.launched`
|
|
665
750
|
// audit entry, ABSENT when the daemon has no launch record for this run (onboarding/campaign
|
|
666
751
|
// runs launched off POST /runs, pre-field runs) — never fabricated, so a bucketed KPI can
|
|
@@ -673,10 +758,24 @@ runtime = {}) {
|
|
|
673
758
|
const endedAt = runTimingIndex.endedAtFor(view.session.id);
|
|
674
759
|
if (endedAt !== undefined)
|
|
675
760
|
view.session.ended_at = endedAt;
|
|
761
|
+
// crew#632: launch surface and caller identity — served ABSENT when absent, never null.
|
|
762
|
+
const channel = runTimingIndex.channelFor(view.session.id);
|
|
763
|
+
if (channel !== undefined)
|
|
764
|
+
view.session.channel = channel;
|
|
765
|
+
const launchActor = runTimingIndex.launchActorFor(view.session.id);
|
|
766
|
+
if (launchActor !== undefined)
|
|
767
|
+
view.session.launch_actor = launchActor;
|
|
676
768
|
const state = resolveDelivery(view, conflictStrand);
|
|
677
769
|
view.session.delivery = state.delivery;
|
|
678
770
|
if (state.deliverUrl !== undefined)
|
|
679
771
|
view.session.deliverUrl = state.deliverUrl;
|
|
772
|
+
// crew#641/#642 (item 5): chat promotion provenance — ABSENT when not a chat-promoted run.
|
|
773
|
+
const chatSeatCount = runTimingIndex.chatSeatCountFor(view.session.id);
|
|
774
|
+
if (chatSeatCount !== undefined)
|
|
775
|
+
view.session.chat_seat_count = chatSeatCount;
|
|
776
|
+
const chatGrounded = runTimingIndex.chatGroundedFor(view.session.id);
|
|
777
|
+
if (chatGrounded !== undefined)
|
|
778
|
+
view.session.chat_grounded = chatGrounded;
|
|
680
779
|
return view;
|
|
681
780
|
};
|
|
682
781
|
// Resolved ONCE and shared by the project routes (which read/write `interactiveRoot`) and the
|
|
@@ -700,7 +799,7 @@ runtime = {}) {
|
|
|
700
799
|
// probe honestly reports no gate.
|
|
701
800
|
const capabilities = typeof adapter.engineCapabilities === 'function'
|
|
702
801
|
? adapter.engineCapabilities()
|
|
703
|
-
: { deliverGate: false, revisesPr: false };
|
|
802
|
+
: { deliverGate: false, revisesPr: false, chatIdOnLaunch: false, seatChipOnCreate: false };
|
|
704
803
|
// wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
|
|
705
804
|
// daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
|
|
706
805
|
// launch while the state home holds an entry the worker Read fence cannot classify. Re-surveyed
|
|
@@ -716,9 +815,12 @@ runtime = {}) {
|
|
|
716
815
|
// `baseSkill.finding` and `/diagnostics.skills.findings[]` (one surface, no new field;
|
|
717
816
|
// `HealthWarning.kind` is open); status stays ok because the daemon serves and studio must load
|
|
718
817
|
// and show it — exactly the state-home blocker's precedent above.
|
|
818
|
+
// crew#661: a drafting seam that armed WITHOUT its declared skill rides `warnings` as well — its
|
|
819
|
+
// runs proceed degraded, so "status ok, no warnings" would be the silent answer the issue names.
|
|
719
820
|
const warnings = [
|
|
720
821
|
...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
|
|
721
822
|
...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
|
|
823
|
+
...phaseSkillFindings(runtime.phaseSkills?.gaps() ?? []),
|
|
722
824
|
];
|
|
723
825
|
return {
|
|
724
826
|
status: 'ok',
|
|
@@ -771,7 +873,9 @@ runtime = {}) {
|
|
|
771
873
|
// The skills seam's last outcome (skills keystone): published / fallback / blocked /
|
|
772
874
|
// config-error, with the `skills.*` findings the ladder produced — the operator's one
|
|
773
875
|
// read-only answer to "why do launches refuse the snapshot".
|
|
774
|
-
|
|
876
|
+
// crew#661: plus every subsystem whose phases ARMED without a declared skill (`phaseSkillGaps`,
|
|
877
|
+
// one `skills.phase-skill` finding each) — the degraded runs' operator-facing record.
|
|
878
|
+
skills: withPhaseSkillGaps(runtime.skills?.health() ?? disabledSkillsHealth(), runtime.phaseSkills),
|
|
775
879
|
// Is the governance evidence LANDING (crew#495): the store, the records on it, the dead
|
|
776
880
|
// letters — with a `governance.deadletter` finding the moment the outbox holds one.
|
|
777
881
|
governance,
|
|
@@ -1293,6 +1397,23 @@ runtime = {}) {
|
|
|
1293
1397
|
}
|
|
1294
1398
|
try {
|
|
1295
1399
|
const runId = await adapter.launchRun(input);
|
|
1400
|
+
// crew#641/#642 (item 5): when a run is promoted from a chat, record how many seats
|
|
1401
|
+
// participated and whether the chat was grounded — so an intent authored by a single
|
|
1402
|
+
// ungrounded model is distinguishable from one that survived a disagreement.
|
|
1403
|
+
let chatSeatCount;
|
|
1404
|
+
let chatGrounded;
|
|
1405
|
+
// Both stay ABSENT unless the chat actually resolves: a closed or unknown chatId must read as
|
|
1406
|
+
// "provenance unavailable", not as "promoted from an ungrounded zero-seat chat". A valid chat
|
|
1407
|
+
// always has at least one warm seat (`crew-api-types` ChatOpenResponse), so 0 is not a value
|
|
1408
|
+
// `chat_seat_count` can honestly hold.
|
|
1409
|
+
if (b.chatId !== undefined) {
|
|
1410
|
+
const promotedFrom = await adapter.chatSeats(b.chatId).catch(() => null);
|
|
1411
|
+
const scope = chatScopes.engineOf(b.chatId);
|
|
1412
|
+
if (promotedFrom !== null && promotedFrom.length > 0 && scope !== undefined) {
|
|
1413
|
+
chatSeatCount = promotedFrom.length;
|
|
1414
|
+
chatGrounded = scope.codeGraphDb != null;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1296
1417
|
// Who launched it — the engine's LaunchOptions carries no actor field
|
|
1297
1418
|
// (checked, wicked-core-ts 0.6.0), so the crew-side trail is the system
|
|
1298
1419
|
// of record for run provenance (task #88).
|
|
@@ -1322,7 +1443,20 @@ runtime = {}) {
|
|
|
1322
1443
|
// the group index (and a restarted daemon's hydrate) reads it back from here.
|
|
1323
1444
|
...(b.campaignId !== undefined ? { campaignId: b.campaignId } : {}),
|
|
1324
1445
|
...(b.groupLabel !== undefined ? { groupLabel: b.groupLabel } : {}),
|
|
1446
|
+
// crew#619: the chat↔run link is durable so the retention maps can be rehydrated after a
|
|
1447
|
+
// daemon restart (rehydration reads `run.launched` entries and keeps non-terminal entries).
|
|
1448
|
+
...(b.chatId !== undefined ? { chatId: b.chatId } : {}),
|
|
1449
|
+
// crew#641/#642 (item 5): chat promotion provenance — seat count and grounding at launch.
|
|
1450
|
+
...(chatSeatCount !== undefined ? { chatSeatCount } : {}),
|
|
1451
|
+
...(chatGrounded !== undefined ? { chatGrounded } : {}),
|
|
1452
|
+
// crew#632: launch surface and caller identity — human-readable provenance on the audit trail.
|
|
1453
|
+
...(b.channel !== undefined ? { channel: b.channel } : {}),
|
|
1454
|
+
...(b.actor !== undefined ? { actor: b.actor } : {}),
|
|
1325
1455
|
});
|
|
1456
|
+
// Stamp the index live so the field is served without waiting for a daemon restart.
|
|
1457
|
+
if (chatSeatCount !== undefined && chatGrounded !== undefined) {
|
|
1458
|
+
runTimingIndex.setChatPromotion(runId, chatSeatCount, chatGrounded);
|
|
1459
|
+
}
|
|
1326
1460
|
if (b.retryOf !== undefined)
|
|
1327
1461
|
retryIndex.set(runId, b.retryOf);
|
|
1328
1462
|
if (revisesPr !== undefined)
|
|
@@ -1341,6 +1475,10 @@ runtime = {}) {
|
|
|
1341
1475
|
// decision #6 replaces spoofable actor strings on the event surface.
|
|
1342
1476
|
{ project_id: b.projectId, member: { kind: 'crew.run', ref: runId }, actor: actorOf(req).id }, membershipAttachedKey(b.projectId, 'crew.run', runId, Date.now()));
|
|
1343
1477
|
}
|
|
1478
|
+
// crew#619: retain the chat transcript for the run's lifetime so Continue-in-Build prefill
|
|
1479
|
+
// is always reproducible even if the chat is idle-reclaimed before the run finishes.
|
|
1480
|
+
if (b.chatId !== undefined)
|
|
1481
|
+
runtime.linkChatRun?.(b.chatId, runId);
|
|
1344
1482
|
return reply.code(201).send({ runId });
|
|
1345
1483
|
}
|
|
1346
1484
|
catch (err) {
|
|
@@ -1797,6 +1935,12 @@ runtime = {}) {
|
|
|
1797
1935
|
}
|
|
1798
1936
|
const b = parsed.data;
|
|
1799
1937
|
const chatId = b.chatId ?? randomUUID();
|
|
1938
|
+
const requestedRefs = [...(b.repoRef !== undefined ? [b.repoRef] : []), ...(b.repoRefs ?? [])];
|
|
1939
|
+
// A named scope's SHAPE first (studio#323 R4, codex on #664): a body whose kind refuses its own
|
|
1940
|
+
// projectId/repoRefs is a 400 — never masked by the project lookup's 404/409 below.
|
|
1941
|
+
const shape = namedKindRefusal(b.scopeKind, b.projectId, [...new Set(requestedRefs)]);
|
|
1942
|
+
if (shape !== null)
|
|
1943
|
+
return reply.code(400).send({ error: shape.error });
|
|
1800
1944
|
// Validate the project BEFORE opening seats: a chat has no launch record for the engine to
|
|
1801
1945
|
// attach against atomically (chats are an in-memory seat pool), so the route validates
|
|
1802
1946
|
// up-front and attaches right after open — the one non-atomic attach, documented in the ADR
|
|
@@ -1837,8 +1981,14 @@ runtime = {}) {
|
|
|
1837
1981
|
const resolution = await resolveChatScope({
|
|
1838
1982
|
chatId,
|
|
1839
1983
|
...(b.projectId !== undefined ? { projectId: b.projectId } : {}),
|
|
1840
|
-
repoRefs:
|
|
1841
|
-
|
|
1984
|
+
repoRefs: requestedRefs,
|
|
1985
|
+
...(b.scopeKind !== undefined ? { kind: b.scopeKind } : {}),
|
|
1986
|
+
}, {
|
|
1987
|
+
...chatScopeDeps(adapter),
|
|
1988
|
+
scratchBase: chatScopes.base,
|
|
1989
|
+
log: (m) => req.log.warn(m),
|
|
1990
|
+
...(runtime.entityCount !== undefined ? { entityCount: runtime.entityCount } : {}),
|
|
1991
|
+
});
|
|
1842
1992
|
if (!resolution.ok) {
|
|
1843
1993
|
chatScopes.release(chatId, token);
|
|
1844
1994
|
return reply.code(resolution.status).send({
|
|
@@ -1866,7 +2016,10 @@ runtime = {}) {
|
|
|
1866
2016
|
// here is NAMED on the response (`refused`) and in the thread (`chatSeatRefused`), so a
|
|
1867
2017
|
// person can see why pi is missing. Explicit `clis` are passed through as asked; the engine
|
|
1868
2018
|
// refuses per seat with its reason, which is copied into `refused` too.
|
|
1869
|
-
|
|
2019
|
+
// A chat is SCOPED — held to read-only roots — when it reads repositories by its kind. `none`
|
|
2020
|
+
// and `system` (studio#323 R4: the platform itself, no repository) read none, so they take the
|
|
2021
|
+
// unscoped admission and open on an engine that predates chat scope.
|
|
2022
|
+
const scoped = scope.kind !== 'none' && scope.kind !== 'system';
|
|
1870
2023
|
const refused = [];
|
|
1871
2024
|
// The standing roster, read ONCE: the default admission below and the engine-drop
|
|
1872
2025
|
// attribution after `chatOpen` both consult it (F-A45-011).
|
|
@@ -2019,6 +2172,12 @@ runtime = {}) {
|
|
|
2019
2172
|
error: `chat ${chatId} was closed while it was being opened; open it again`,
|
|
2020
2173
|
});
|
|
2021
2174
|
}
|
|
2175
|
+
// Register roots for path rewriting (crew#618): absolute host paths in seat replies are
|
|
2176
|
+
// rewritten to repo-relative form before being stored in the transcript.
|
|
2177
|
+
if (chatTranscripts !== undefined && scope.repos.length > 0) {
|
|
2178
|
+
const roots = scope.repos.map((r) => ({ absRoot: resolve(r.rootPath), name: r.name }));
|
|
2179
|
+
chatTranscripts.registerRoots(chatId, roots);
|
|
2180
|
+
}
|
|
2022
2181
|
// The thread learns of every refused seat the way it learns of everything else — a frame
|
|
2023
2182
|
// on /ws — AFTER the scope is published, so a reader never sees a refusal for a chat it
|
|
2024
2183
|
// cannot yet look up.
|
|
@@ -2032,12 +2191,17 @@ runtime = {}) {
|
|
|
2032
2191
|
...(b.projectId !== undefined ? { project_id: b.projectId } : {}),
|
|
2033
2192
|
});
|
|
2034
2193
|
}
|
|
2194
|
+
// crew#641: a chat with exactly ONE warm seat cannot disagree with itself — disclosed
|
|
2195
|
+
// prominently so the caller does not have to reason about the refused[] array.
|
|
2196
|
+
const warm = seats.filter((s) => s.ok);
|
|
2197
|
+
const singleSeat = warm.length === 1 ? singleSeatDisclosure(warm[0].cliKey, refused, 'open') : undefined;
|
|
2035
2198
|
return reply.code(201).send({
|
|
2036
2199
|
chatId,
|
|
2037
2200
|
seats,
|
|
2038
2201
|
scope,
|
|
2039
2202
|
refused,
|
|
2040
2203
|
...(projectAttachError !== undefined ? { projectAttachError } : {}),
|
|
2204
|
+
...(singleSeat !== undefined ? { singleSeat } : {}),
|
|
2041
2205
|
});
|
|
2042
2206
|
}
|
|
2043
2207
|
catch (err) {
|
|
@@ -2088,7 +2252,12 @@ runtime = {}) {
|
|
|
2088
2252
|
// warm seats — so the turn can be opened BEFORE the engine call, in the same tick as the
|
|
2089
2253
|
// predicate below: two racing sends can no longer both pass `inFlight` during the await, and a
|
|
2090
2254
|
// `chatDelta` that lands before `chatSend` resolves is already stamped with its `turn_id`.
|
|
2091
|
-
|
|
2255
|
+
// crew#641 (review): the ROSTER and the AUDIENCE are different things and only one of them can
|
|
2256
|
+
// answer "can this chat disagree with itself". Resolved ONCE here: `audience` is who this turn
|
|
2257
|
+
// goes to (the named targets, else everyone warm), `warmRoster` is who is warm at all. A
|
|
2258
|
+
// targeted send to one seat of a two-seat chat is not a degraded chat.
|
|
2259
|
+
const warmRoster = await adapter.chatSeats(id);
|
|
2260
|
+
const audience = parsed.data.targets ?? warmRoster;
|
|
2092
2261
|
if (audience.length === 0) {
|
|
2093
2262
|
return reply.code(409).send({ error: `chat '${id}' has no warm seats — open it first` });
|
|
2094
2263
|
}
|
|
@@ -2106,7 +2275,22 @@ runtime = {}) {
|
|
|
2106
2275
|
chatTurns.reconcile(id, turn.turnId, seats);
|
|
2107
2276
|
chatTranscripts?.appendUser(id, turn.turnId, parsed.data.text, seats);
|
|
2108
2277
|
}
|
|
2109
|
-
|
|
2278
|
+
// crew#641: re-state single-seat degradation on every turn so it is visible in the transcript.
|
|
2279
|
+
// Decided from `warmRoster` — the seats that are WARM — never from `seats`, which is only the
|
|
2280
|
+
// seats this turn reached: a targeted send to one seat of a two-seat chat would otherwise
|
|
2281
|
+
// announce a degradation that does not exist, and a false "you are degraded" sends an operator
|
|
2282
|
+
// after a problem they do not have.
|
|
2283
|
+
// crew#650: `refusedOf` is `undefined` for a chat THIS daemon did not open (a restart drops
|
|
2284
|
+
// the index) — that is "the refusals are unknown", not "the chat gained a seat". The
|
|
2285
|
+
// disclosure is decided by the warm roster alone, and the `undefined` is handed STRAIGHT to
|
|
2286
|
+
// the builder (never `?? []`, review of #658): only it may decide what an unknown record is
|
|
2287
|
+
// allowed to claim.
|
|
2288
|
+
const singleSeat202 = warmRoster.length === 1 ? singleSeatDisclosure(warmRoster[0], chatScopes.refusedOf(id), 'turn') : undefined;
|
|
2289
|
+
return reply.code(202).send({
|
|
2290
|
+
seats,
|
|
2291
|
+
...(turn !== null ? { turnId: turn.turnId } : {}),
|
|
2292
|
+
...(singleSeat202 !== undefined ? { singleSeat: singleSeat202 } : {}),
|
|
2293
|
+
});
|
|
2110
2294
|
}
|
|
2111
2295
|
catch (err) {
|
|
2112
2296
|
// Nothing went out: the reservation is retracted so the next send is not refused for it.
|
|
@@ -4136,6 +4320,8 @@ runtime = {}) {
|
|
|
4136
4320
|
settings: projectSettings,
|
|
4137
4321
|
pool: interactiveBridges,
|
|
4138
4322
|
...(runtime.docGrounding !== undefined ? { grounding: runtime.docGrounding } : {}),
|
|
4323
|
+
// crew#631: supply the standing roster so doc/demo creates can refuse an unavailable seat.
|
|
4324
|
+
roster: () => rosterWithStanding(),
|
|
4139
4325
|
log: (m) => app.log.warn(m),
|
|
4140
4326
|
});
|
|
4141
4327
|
// ── Governed doc delete (crew#338) — the one door that changes BOTH stores ──
|