wicked-crew 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/api/ask-paths.d.ts +7 -3
  2. package/dist/api/ask-paths.d.ts.map +1 -1
  3. package/dist/api/ask-paths.js +2 -1
  4. package/dist/api/ask-paths.js.map +1 -1
  5. package/dist/api/capture.d.ts +2 -2
  6. package/dist/api/chat-scope.d.ts +14 -3
  7. package/dist/api/chat-scope.d.ts.map +1 -1
  8. package/dist/api/chat-scope.js +27 -5
  9. package/dist/api/chat-scope.js.map +1 -1
  10. package/dist/api/delivery-assurance.d.ts +1 -6
  11. package/dist/api/delivery-assurance.d.ts.map +1 -1
  12. package/dist/api/delivery-assurance.js +7 -14
  13. package/dist/api/delivery-assurance.js.map +1 -1
  14. package/dist/api/governance-steering.d.ts +2 -2
  15. package/dist/api/mcp.d.ts +4 -4
  16. package/dist/api/recording.d.ts +29 -3
  17. package/dist/api/recording.d.ts.map +1 -1
  18. package/dist/api/recording.js +188 -0
  19. package/dist/api/recording.js.map +1 -1
  20. package/dist/api/roster-standing.d.ts +21 -2
  21. package/dist/api/roster-standing.d.ts.map +1 -1
  22. package/dist/api/roster-standing.js +108 -9
  23. package/dist/api/roster-standing.js.map +1 -1
  24. package/dist/api/routes.d.ts +112 -112
  25. package/dist/api/routes.d.ts.map +1 -1
  26. package/dist/api/routes.js +203 -65
  27. package/dist/api/routes.js.map +1 -1
  28. package/dist/api/seat-probe.d.ts +7 -0
  29. package/dist/api/seat-probe.d.ts.map +1 -1
  30. package/dist/api/seat-probe.js +27 -5
  31. package/dist/api/seat-probe.js.map +1 -1
  32. package/dist/api/seat-version.d.ts +8 -0
  33. package/dist/api/seat-version.d.ts.map +1 -1
  34. package/dist/api/seat-version.js +36 -4
  35. package/dist/api/seat-version.js.map +1 -1
  36. package/dist/api/skills.d.ts.map +1 -1
  37. package/dist/api/skills.js +4 -5
  38. package/dist/api/skills.js.map +1 -1
  39. package/dist/api/testing.d.ts +4 -4
  40. package/dist/campaigns/routes.d.ts +2 -2
  41. package/dist/core/adapter.d.ts +25 -0
  42. package/dist/core/adapter.d.ts.map +1 -1
  43. package/dist/core/adapter.js +121 -5
  44. package/dist/core/adapter.js.map +1 -1
  45. package/dist/core/builtin-catalog.d.ts.map +1 -1
  46. package/dist/core/builtin-catalog.js +6 -1
  47. package/dist/core/builtin-catalog.js.map +1 -1
  48. package/dist/core/deliver-text.d.ts +5 -1
  49. package/dist/core/deliver-text.d.ts.map +1 -1
  50. package/dist/core/deliver-text.js +14 -1
  51. package/dist/core/deliver-text.js.map +1 -1
  52. package/dist/core/qe-acceptance-setting.d.ts +65 -0
  53. package/dist/core/qe-acceptance-setting.d.ts.map +1 -0
  54. package/dist/core/qe-acceptance-setting.js +93 -0
  55. package/dist/core/qe-acceptance-setting.js.map +1 -0
  56. package/dist/core/types.d.ts.map +1 -1
  57. package/dist/core/types.js +3 -0
  58. package/dist/core/types.js.map +1 -1
  59. package/dist/editors/pack-install.d.ts +30 -3
  60. package/dist/editors/pack-install.d.ts.map +1 -1
  61. package/dist/editors/pack-install.js +59 -4
  62. package/dist/editors/pack-install.js.map +1 -1
  63. package/dist/events/terminals.d.ts +42 -2
  64. package/dist/events/terminals.d.ts.map +1 -1
  65. package/dist/events/terminals.js +136 -3
  66. package/dist/events/terminals.js.map +1 -1
  67. package/dist/interactive/chat-events.d.ts.map +1 -1
  68. package/dist/interactive/chat-events.js +6 -1
  69. package/dist/interactive/chat-events.js.map +1 -1
  70. package/dist/interactive/draft-events.d.ts.map +1 -1
  71. package/dist/interactive/draft-events.js +6 -1
  72. package/dist/interactive/draft-events.js.map +1 -1
  73. package/dist/interactive/edit-events.d.ts.map +1 -1
  74. package/dist/interactive/edit-events.js +6 -1
  75. package/dist/interactive/edit-events.js.map +1 -1
  76. package/dist/projects/state-home-preflight.d.ts +5 -1
  77. package/dist/projects/state-home-preflight.d.ts.map +1 -1
  78. package/dist/projects/state-home-preflight.js +43 -13
  79. package/dist/projects/state-home-preflight.js.map +1 -1
  80. package/dist/skills/bundle.d.ts +2 -0
  81. package/dist/skills/bundle.d.ts.map +1 -1
  82. package/dist/skills/bundle.js +9 -2
  83. package/dist/skills/bundle.js.map +1 -1
  84. package/dist/skills/installed.d.ts +8 -1
  85. package/dist/skills/installed.d.ts.map +1 -1
  86. package/dist/skills/installed.js +25 -4
  87. package/dist/skills/installed.js.map +1 -1
  88. package/dist/skills/plugin-source.d.ts +2 -0
  89. package/dist/skills/plugin-source.d.ts.map +1 -1
  90. package/dist/skills/plugin-source.js +17 -1
  91. package/dist/skills/plugin-source.js.map +1 -1
  92. package/dist/skills/runtime.d.ts +24 -4
  93. package/dist/skills/runtime.d.ts.map +1 -1
  94. package/dist/skills/runtime.js +67 -30
  95. package/dist/skills/runtime.js.map +1 -1
  96. package/dist/skills/store.d.ts +2 -0
  97. package/dist/skills/store.d.ts.map +1 -1
  98. package/dist/skills/store.js +6 -1
  99. package/dist/skills/store.js.map +1 -1
  100. package/dist/skills/tree.d.ts +4 -0
  101. package/dist/skills/tree.d.ts.map +1 -1
  102. package/dist/skills/tree.js +39 -1
  103. package/dist/skills/tree.js.map +1 -1
  104. package/dist/studio/assets/index-CRbl0LAp.js +770 -0
  105. package/dist/studio/assets/index-D1WKb58Z.css +32 -0
  106. package/dist/studio/editors/wicked-deck/editor.json +56 -3
  107. package/dist/studio/editors/wicked-deck/index.html +53 -7
  108. package/dist/studio/editors/wicked-doc/editor.json +91 -0
  109. package/dist/studio/editors/wicked-doc/index.html +161 -0
  110. package/dist/studio/editors/wicked-page/editor.json +42 -4
  111. package/dist/studio/editors/wicked-page/index.html +24 -7
  112. package/dist/studio/editors/wicked-video/editor.json +271 -0
  113. package/dist/studio/editors/wicked-video/index.html +28 -0
  114. package/dist/studio/index.html +2 -2
  115. package/dist/studio/testid-inventory.json +763 -122
  116. package/dist/team/routes.d.ts +2 -2
  117. package/endpoint-manifest.json +14 -1
  118. package/package.json +3 -3
  119. package/dist/studio/assets/index-BSPM1fFW.css +0 -32
  120. package/dist/studio/assets/index-R8uK_NlG.js +0 -848
@@ -48,7 +48,8 @@ import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js
48
48
  import { inventoryComplete, parseInventoryClaims } from '../core/inventory-claim.js';
49
49
  import { ApproveEditSchema, approveEdited, captureLaunchRoots, editInFlight, FileProposalSchema, registerCaptureRoutes } from './capture.js';
50
50
  import { registerTestingRoutes } from './testing.js';
51
- import { DEMO_PRESET, demoLaunchRoots, registerDemoRoutes, registerWalkthroughRoutes, walkthroughAcceptance } from './recording.js';
51
+ import { DEMO_PRESET, demoLaunchRoots, registerDemoDocumentRoute, registerDemoRoutes, registerWalkthroughRoutes, walkthroughAcceptance } from './recording.js';
52
+ import { projectDocsRoot } from '../interactive/project-root.js';
52
53
  import { registerSkillsRoutes } from './skills.js';
53
54
  import { registerMcpRoutes } from './mcp.js';
54
55
  import { McpPolicies } from '../mcp/policies.js';
@@ -59,7 +60,8 @@ import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../s
59
60
  import { REQUIRED_GARDEN_VERSION } from '../skills/plugin-source.js';
60
61
  import { engineRosterJson, noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
61
62
  import { launchAssuranceNotice } from '../core/assurance.js';
62
- import { QE_ACCEPTANCE, QE_PRESETS, aggregateReceipt, postHocDeliveryAssurance, qeAcceptanceFromView, qeAcceptanceRefusal, receiptOf, qeAcceptanceNotRequired, qeDecisionBypasses, qeDecisionOf, unreadableQeAcceptance, } from './delivery-assurance.js';
63
+ import { launchRequiresQe, mayRequireQe, qeAcceptanceMode, qeSettingSkipReason } from '../core/qe-acceptance-setting.js';
64
+ import { aggregateReceipt, postHocDeliveryAssurance, qeAcceptanceFromView, qeAcceptanceRefusal, QE_ACCEPTANCE_REQUIRED_CODE, receiptOf, qeAcceptanceNotRequired, qeDecisionBypasses, qeDecisionOf, unreadableQeAcceptance, } from './delivery-assurance.js';
63
65
  import { ProjectSettingsStore } from '../projects/settings.js';
64
66
  import { isHumanOperator } from './watch-routes.js';
65
67
  import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
@@ -1079,6 +1081,9 @@ runtime = {}) {
1079
1081
  // spellings where the contract has no name, statusCodes list every code the route answers on
1080
1082
  // purpose. Declared on the highest-traffic run-lifecycle routes first; the manifest records
1081
1083
  // null / [] for the rest ("where declared", never invented). See src/api/endpoint-manifest.ts.
1084
+ // The daemon's QE acceptance setting as read now ('required' when the settings cannot be read:
1085
+ // fail closed, never off by accident).
1086
+ const qeAcceptanceNow = async () => qeAcceptanceMode(await Promise.resolve().then(() => adapter.getSettings()).catch(() => null));
1082
1087
  app.get(`${V}/health`, { config: { manifest: { statusCodes: [200] } } }, async () => {
1083
1088
  const ping = await enginePing();
1084
1089
  // F-E2E-030: what this deployment can keep. A composer reads `capabilities.deliverGate`
@@ -1105,6 +1110,9 @@ runtime = {}) {
1105
1110
  // QE-IN-APP-WORKFLOWS: `LaunchRunBody.skipQeAcceptance` / `forceQeAcceptance` reach an addon
1106
1111
  // that carries the QE decision; never an invented capability.
1107
1112
  qeAcceptanceOverride: typeof adapter.engineCapabilities === 'function' && addonSupportsQeOverride(),
1113
+ // api-types 0.120.0: the daemon's QE acceptance setting, read per probe ('off' = every launch
1114
+ // that requires QE acceptance skips it, labelled with the setting).
1115
+ qeAcceptance: await qeAcceptanceNow(),
1108
1116
  };
1109
1117
  // wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
1110
1118
  // daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
@@ -1657,6 +1665,11 @@ runtime = {}) {
1657
1665
  }
1658
1666
  return null;
1659
1667
  };
1668
+ /**
1669
+ * crew#1019: registration is durable even when onboarding cannot launch (core has no unregister).
1670
+ * A landed local repo returns 201 with `onboardRunId: null` and `onboardError` on launch failure, so
1671
+ * callers can still attach it to their project and retry onboarding separately.
1672
+ */
1660
1673
  app.post(`${V}/repos`, async (req, reply) => {
1661
1674
  const parsed = RegisterRepoSchema.safeParse(req.body);
1662
1675
  if (!parsed.success) {
@@ -1672,6 +1685,8 @@ runtime = {}) {
1672
1685
  if (gitUrl) {
1673
1686
  // Remote: clone to rootPath (if provided) or default ~/.wicked/repos/<name>,
1674
1687
  // register, and launch onboarding run.
1688
+ // cloneAndRegisterRepo registers and launches in one call. Distinguishing a landed
1689
+ // registration from a failed clone requires splitting that adapter call (follow-up).
1675
1690
  const cloned = await adapter.cloneAndRegisterRepo(name, gitUrl, rootPath);
1676
1691
  runId = cloned.runId;
1677
1692
  const repos = await adapter.listRepos();
@@ -1686,7 +1701,11 @@ runtime = {}) {
1686
1701
  }
1687
1702
  }
1688
1703
  catch (err) {
1689
- return reply.code(400).send({ error: message(err) });
1704
+ if (repo === undefined)
1705
+ return reply.code(400).send({ error: message(err) });
1706
+ req.log.warn({ repoId: repo.id }, `repo registered but onboarding launch failed: ${message(err)}`);
1707
+ // No run exists to record or file, even when projectId was supplied (crew#1019).
1708
+ return reply.code(201).send({ repo, onboardRunId: null, onboardError: message(err) });
1690
1709
  }
1691
1710
  const projectAttachError = await recordOnboardingLaunch(req, runId, repo.id, { projectId, channel, actor, autoCapture });
1692
1711
  return reply.code(201).send({ repo, onboardRunId: runId, ...(projectAttachError !== undefined ? { projectAttachError } : {}) });
@@ -1823,16 +1842,27 @@ runtime = {}) {
1823
1842
  // F-E2E-030: only the explicit `'auto'` opts out of the engine's deliver gate. `'human'` and
1824
1843
  // an omitted field both leave `autoDeliver` off the input — the gate is the engine's default,
1825
1844
  // so the wire never has to say "gate me" to be gated.
1845
+ // The daemon's QE acceptance setting, read ONCE for this launch: the deliverGate check below and
1846
+ // the skip handed to the engine come from the same reading (core/qe-acceptance-setting.ts).
1847
+ // An addon without the QE decision enforces none, so the setting has nothing to skip on it.
1848
+ const qeMode = addonSupportsQeOverride() ? await qeAcceptanceNow() : 'required';
1849
+ const qeRequired = b.deliverGate === 'auto' || qeMode === 'off' ? await launchRequiresQe(adapter, b.workflow, b.projectId) : false;
1850
+ const settingSkip = qeSettingSkipReason(qeMode, { skipQeAcceptanceReason: b.skipQeAcceptance?.reason, forceQeAcceptance: b.forceQeAcceptance }, qeRequired);
1826
1851
  if (b.deliverGate === 'auto') {
1827
1852
  // wicked-core#850 EX-03: a workflow that requires QE acceptance delivers only through the deliver
1828
1853
  // gate, where crew checks the verdict. Unattended delivery would push before anyone could.
1829
1854
  // QE-IN-APP-WORKFLOWS: an explicit operator skip leaves no verdict to check, so it may. A name
1830
1855
  // the engine resolves to a built-in preset that requires QE acceptance is judged by that preset
1831
1856
  // (the engine resolves a preset before any def, so a def of the same name never speaks for it).
1832
- const required = b.workflow !== undefined ? adapter.getWorkflow(b.workflow)?.required_instruments : undefined;
1833
- const qeRequired = required?.includes(QE_ACCEPTANCE) === true || (b.workflow !== undefined && QE_PRESETS.has(b.workflow));
1834
- if (qeRequired && b.skipQeAcceptance === undefined) {
1857
+ // The daemon's `qeAcceptance: 'off'` setting skips it the same way (resolved once, above, and
1858
+ // handed to the engine with this launch), unless this launch forces QE acceptance.
1859
+ // Refused on the CONSERVATIVE reading (a built-in QE name or a QE def counts even when a project
1860
+ // preset shadows it now): only an explicit or setting skip lets QE work deliver unattended.
1861
+ if (mayRequireQe(adapter, b.workflow, qeRequired) && b.skipQeAcceptance === undefined && settingSkip === undefined) {
1862
+ // `code` is the same stable token a refused delivery carries (QeAcceptanceRefusal.code), so a
1863
+ // client can say "this failed because QE acceptance is required" without parsing the sentence.
1835
1864
  return reply.code(400).send({
1865
+ code: QE_ACCEPTANCE_REQUIRED_CODE,
1836
1866
  error: `workflow ${b.workflow} requires QE acceptance (required_instruments: qe_acceptance), so it delivers only ` +
1837
1867
  'through the deliver gate, where the verdict is checked — deliverGate: "auto" cannot apply to it ' +
1838
1868
  '(unless the launch explicitly skips QE acceptance with a reason: skipQeAcceptance)',
@@ -1913,6 +1943,8 @@ runtime = {}) {
1913
1943
  // when sent, never inferred.
1914
1944
  if (b.skipQeAcceptance !== undefined)
1915
1945
  input.skipQeAcceptanceReason = b.skipQeAcceptance.reason;
1946
+ else if (settingSkip !== undefined)
1947
+ input.skipQeAcceptanceReason = settingSkip;
1916
1948
  if (b.forceQeAcceptance === true)
1917
1949
  input.forceQeAcceptance = true;
1918
1950
  // DES-L9 / crew#550 — REVISION: `revisesPr` names an OPEN same-repository pull request whose
@@ -2096,7 +2128,7 @@ runtime = {}) {
2096
2128
  const freeText = input.workflow === undefined && input.plan === undefined ? { freeText: { notice: FREE_TEXT_NOTICE } } : {};
2097
2129
  // crew#874: the run uses the PUBLISHED generation; when the installed garden is not the one it
2098
2130
  // was built from, the launch answer says so (a recorded proof can disclose which garden ran).
2099
- const skillsAhead = runtime.skills?.installedAheadFinding({ fresh: true }) ?? null; // fresh: an upgrade just before Send counts (codex r1)
2131
+ const skillsAhead = await runtime.skills?.installedAheadFinding({ fresh: true }) ?? null; // fresh: an upgrade just before Send counts (codex r1)
2100
2132
  const skillsWarning = skillsAhead === null ? {} : { skillsWarning: { kind: skillsAhead.kind, severity: skillsAhead.severity, message: skillsAhead.message } };
2101
2133
  // wicked-core#850: a full-assurance run on a one-CLI roster will wait at a gate for a seat that
2102
2134
  // can evaluate or judge it; the answer names the explicit opt-in to relaunch with.
@@ -2669,6 +2701,14 @@ runtime = {}) {
2669
2701
  // READ-ONLY estate MCP over it (the grounding governed runs get). The scope is resolved and
2670
2702
  // validated BEFORE any seat warms, stated to the seats in the scratch root's AGENTS.md /
2671
2703
  // CLAUDE.md, and returned as `scope` (see `chat-scope.ts`).
2704
+ // crew#1020: open and refresh use the same registry, graph, liveness and checkout dependencies.
2705
+ const resolveScope = (request, req) => resolveChatScope(request, {
2706
+ ...chatScopeDeps(adapter),
2707
+ scratchBase: chatScopes.base,
2708
+ log: (m) => req.log.warn(m),
2709
+ ...(runtime.entityCount !== undefined ? { entityCount: runtime.entityCount } : {}),
2710
+ ...(runtime.freshenCheckout !== undefined ? { freshen: runtime.freshenCheckout } : {}),
2711
+ });
2672
2712
  app.post(`${V}/chats`, {
2673
2713
  config: {
2674
2714
  manifest: {
@@ -2731,18 +2771,13 @@ runtime = {}) {
2731
2771
  // The scope (crew#502): explicit repos (`repoRefs`, the legacy `repoRef` merged in) or the
2732
2772
  // project's members — each ref checked against the registry, EVERY missing one named — else
2733
2773
  // none. Resolved before any seat warms: a 404 here warmed nothing.
2734
- const resolution = await resolveChatScope({
2774
+ const scopeRequest = {
2735
2775
  chatId,
2736
2776
  ...(b.projectId !== undefined ? { projectId: b.projectId } : {}),
2737
2777
  repoRefs: requestedRefs,
2738
2778
  ...(b.scopeKind !== undefined ? { kind: b.scopeKind } : {}),
2739
- }, {
2740
- ...chatScopeDeps(adapter),
2741
- scratchBase: chatScopes.base,
2742
- log: (m) => req.log.warn(m),
2743
- ...(runtime.entityCount !== undefined ? { entityCount: runtime.entityCount } : {}),
2744
- ...(runtime.freshenCheckout !== undefined ? { freshen: runtime.freshenCheckout } : {}),
2745
- });
2779
+ };
2780
+ const resolution = await resolveScope(scopeRequest, req);
2746
2781
  if (!resolution.ok) {
2747
2782
  chatScopes.release(chatId, token);
2748
2783
  return reply.code(resolution.status).send({
@@ -2871,7 +2906,7 @@ runtime = {}) {
2871
2906
  projectAttachError = message(err);
2872
2907
  }
2873
2908
  }
2874
- if (!chatScopes.set(chatId, scope, token, refused, engine)) {
2909
+ if (!chatScopes.set(chatId, scope, token, refused, engine, scopeRequest)) {
2875
2910
  // The reservation was cancelled while the open was in flight — an engine `chatClosed` or a
2876
2911
  // `DELETE` for this id (Copilot, #518). Nothing was recorded; tear the chat down (scratch
2877
2912
  // root and the filing just made) instead of returning a stale 201. No path record exists
@@ -2983,24 +3018,20 @@ runtime = {}) {
2983
3018
  // later message CONTINUES the same path: `propose_plan(answer-N)` then `confirm_gate(Approve)`
2984
3019
  // at the turn gate the previous answer paused on. Nothing is fanned out: the PA is the one
2985
3020
  // voice, helpers talk to the PA, the reviewer raises quiet findings — all on the bus.
2986
- const path = askPaths.get(id);
2987
- if (path === undefined) {
3021
+ const openedPath = askPaths.get(id);
3022
+ if (openedPath === undefined) {
2988
3023
  return reply.code(409).send({ error: `chat '${id}' is not open on this daemon — open it first (POST /chats)` });
2989
3024
  }
3025
+ const path = openedPath;
2990
3026
  if (path.eligible.length === 0) {
2991
3027
  return reply.code(409).send({ error: `chat '${id}' has no eligible seat — sign a seat in and re-seat it (POST /chats/:id/seats)` });
2992
3028
  }
2993
3029
  const text = parsed.data.text;
2994
- const scope = chatScopes.get(id);
2995
- // A repo-bound path: exactly one repo in scope grounds the run (`repoRef`) and makes it
2996
- // deliverable; a project or a multi-repo scope binds the project graph only (§5.1 Grounding).
2997
- const repoRef = scope !== undefined && scope.kind === 'repos' && scope.repos.length === 1 ? scope.repos[0].id : undefined;
2998
- const projectId = scope?.projectId;
2999
- const voice = path.pa !== null ? [path.pa] : [];
3000
- // The turn is reserved for the voice — or, for a random pick (no PA yet), for the whole
3001
- // eligible roster: the relay narrows it to the PA when `path.started` names one (ASK-C2), so
3002
- // the PA's frames are stamped with this turn and a second message is refused meanwhile.
3003
- const audience = voice.length > 0 ? voice : path.eligible;
3030
+ let scope = chatScopes.get(id);
3031
+ const storedScope = scope;
3032
+ let engine = chatScopes.engineOf(id);
3033
+ let voice = [];
3034
+ let audience = [];
3004
3035
  // crew#641/#650 re-stated for a path: ONE eligible seat means no distinct reviewer and no
3005
3036
  // helper — said on every turn (the engine's `member.joined{seat:null}` says it on the bus).
3006
3037
  const singleSeat202 = path.eligible.length === 1
@@ -3026,8 +3057,8 @@ runtime = {}) {
3026
3057
  });
3027
3058
  }
3028
3059
  // crew#826: the turn lives as long as its answer step may (`budget_secs`), not 3× the pool knob.
3029
- const turn = chatTurns.begin(id, audience, text, ASK_TURN_BUDGET_SECS);
3030
- const turnId = turn?.turnId ?? randomUUID();
3060
+ let turn = null;
3061
+ let turnId = randomUUID();
3031
3062
  // What an ACCEPTED message records — the engine has it, so a persistence failure here is
3032
3063
  // logged, never a retraction of a turn that is already being answered (codex on #808 r2, 9).
3033
3064
  const persistAccepted = (preface, after) => {
@@ -3055,12 +3086,97 @@ runtime = {}) {
3055
3086
  // preface rode a pool send; told only once the send is accepted (`commit`).
3056
3087
  const withPreface = (preface) => (preface !== null ? `${preface}\n\n${text}` : text);
3057
3088
  try {
3089
+ const rules = (await considerations.inForceFor(scope?.projectId ?? null)).rules;
3090
+ // crew#1020: a project/everything chat must see repos attached since open. Admission was
3091
+ // under this scope KIND and the kind stays the same, so do not re-run seat admission.
3092
+ const scopeRequest = chatScopes.requestOf(id);
3093
+ if (scope !== undefined && (scope.kind === 'project' || scope.kind === 'everything') && scopeRequest !== undefined) {
3094
+ try {
3095
+ if (scopeRequest.projectId !== undefined && await adapter.projectGet(scopeRequest.projectId) === null) {
3096
+ throw new Error(`Project ${scopeRequest.projectId} not found`);
3097
+ }
3098
+ const resolution = await resolveScope(scopeRequest, req);
3099
+ if (!resolution.ok)
3100
+ throw new Error(resolution.error);
3101
+ // Retain this chat's scratch root even if the resolver's defaults change (crew#1020).
3102
+ // Resolve only: a refused message must leave the stored scope, executing seats' files
3103
+ // and transcript roots untouched. Publish after every refusal check, just before send.
3104
+ engine = { ...resolution.engine, cwd: scope.cwd };
3105
+ scope = { ...resolution.scope, cwd: scope.cwd };
3106
+ }
3107
+ catch (err) {
3108
+ req.log.warn({ chatId: id }, `chat scope refresh failed; keeping stored scope: ${message(err)}`);
3109
+ }
3110
+ }
3111
+ if (askPaths.get(id)?.generation !== lease) {
3112
+ return reply.code(409).send({ error: `chat '${id}' was closed while the message was in flight; nothing was sent` });
3113
+ }
3114
+ if (path.runId !== undefined) {
3115
+ const view = (await adapter.sessionsDetail()).find((v) => v.session.id === path.runId);
3116
+ if (view === undefined) {
3117
+ askPaths.release(id, lease);
3118
+ return reply.code(409).send({ error: `chat '${id}': its path ${path.runId} is gone — open a new chat` });
3119
+ }
3120
+ const status = String(view.session.status);
3121
+ if (status !== 'awaiting_human') {
3122
+ askPaths.release(id, lease);
3123
+ const who = path.pa ?? 'the primary agent';
3124
+ return reply.code(409).send({
3125
+ code: 'turn_in_flight',
3126
+ error: status === 'executing' || status === 'running'
3127
+ ? `${who} is still answering the previous message in this chat — wait for the reply. Nothing was sent.`
3128
+ : `chat '${id}': its path ${path.runId} is ${status}, not waiting for your turn — nothing was sent`,
3129
+ chatId: id,
3130
+ runId: path.runId,
3131
+ status,
3132
+ });
3133
+ }
3134
+ // The status read awaited: the chat may have been closed (and its id reopened) meanwhile —
3135
+ // nothing of this message may land on the newcomer (codex on #808 r3, 2).
3136
+ if (askPaths.get(id)?.generation !== lease) {
3137
+ return reply.code(409).send({ error: `chat '${id}' was closed while the message was in flight; nothing was sent` });
3138
+ }
3139
+ }
3140
+ const codeGraphDb = engine?.codeGraphDb ?? null;
3141
+ // Only project scopes refresh a project graph; fixed repo scopes may use the engine's
3142
+ // own repo graph instead of input.projectGraph, and everything scopes have no graph.
3143
+ if (scope?.kind === 'project' && path.runId !== undefined && codeGraphDb !== path.launchGraphBinding) {
3144
+ askPaths.release(id, lease);
3145
+ return reply.code(409).send({
3146
+ code: 'chat_scope_changed',
3147
+ error: `This chat's repositories or their code graph changed since it started (${scope?.repos.length ?? 0} now in scope). Start a new chat to read them — this one keeps its original scope.`,
3148
+ });
3149
+ }
3150
+ // Sort repository identities so membership order alone does not trigger another statement.
3151
+ const scopeKey = JSON.stringify([
3152
+ scope?.repos.map((repo) => [repo.id, repo.name, repo.rootPath]).sort((a, b) => a[0].localeCompare(b[0])) ?? [],
3153
+ codeGraphDb,
3154
+ ]);
3155
+ const pending = await considerations.pendingPreface(id, { inForce: rules });
3156
+ // The last await before delivery may also have closed/reopened the chat. From this check
3157
+ // through publication and the send there is no await, so no executing turn's files change.
3158
+ if (askPaths.get(id)?.generation !== lease || chatScopes.get(id) !== storedScope) {
3159
+ return reply.code(409).send({ error: `chat '${id}' was closed while the message was in flight; nothing was sent` });
3160
+ }
3161
+ if (storedScope !== undefined && scope !== undefined && engine !== undefined &&
3162
+ (JSON.stringify(scope) !== JSON.stringify(storedScope) || JSON.stringify(engine) !== JSON.stringify(chatScopes.engineOf(id)))) {
3163
+ prepareChatScratch(id, scope, rules);
3164
+ if (chatScopes.refresh(id, storedScope, scope, engine)) {
3165
+ chatTranscripts?.registerRoots(id, scope.repos.map((r) => ({ absRoot: resolve(r.rootPath), name: r.name })));
3166
+ }
3167
+ }
3168
+ voice = path.pa !== null ? [path.pa] : [];
3169
+ // A random pick reserves every eligible seat until the relay names the PA.
3170
+ audience = voice.length > 0 ? voice : path.eligible;
3171
+ turn = chatTurns.begin(id, audience, text, ASK_TURN_BUDGET_SECS);
3172
+ turnId = turn?.turnId ?? turnId;
3173
+ // A repo-bound path can deliver; project and multi-repo scopes remain read-only asks.
3174
+ const repoRef = scope?.kind === 'repos' && scope.repos.length === 1 ? scope.repos[0].id : undefined;
3175
+ const projectId = scope?.projectId;
3058
3176
  if (path.runId === undefined) {
3059
3177
  const stepId = 'answer-1';
3060
3178
  const generation = path.generation;
3061
3179
  const roster = rosterWithStanding().filter((seat) => path.eligible.includes(String(seat.key)));
3062
- const rules = (await considerations.inForceFor(scope?.projectId ?? null)).rules;
3063
- const pending = await considerations.pendingPreface(id, { inForce: rules });
3064
3180
  // crew#823: the run is titled by the operator's question (every reader titles a run from
3065
3181
  // `problem`); the scope statement grounds the seats from the first answer step's
3066
3182
  // instructions instead of standing in as the run's title.
@@ -3073,15 +3189,17 @@ runtime = {}) {
3073
3189
  ...(path.primary !== undefined ? { primary: path.primary } : {}),
3074
3190
  ...(repoRef !== undefined ? { repoRef, deliver: 'pr' } : {}),
3075
3191
  ...(projectId !== undefined ? { projectId } : {}),
3192
+ ...(scope?.kind === 'project' && codeGraphDb !== null ? { projectGraph: { dbPath: codeGraphDb } } : {}),
3076
3193
  };
3077
3194
  const runId = await adapter.launchRun(input);
3078
- if (!askPaths.started(id, generation, runId, stepId)) {
3195
+ if (!askPaths.started(id, generation, runId, stepId, input.projectGraph?.dbPath ?? null)) {
3079
3196
  // The chat was closed (End) while the launch was in flight: the run must not outlive it.
3080
3197
  await adapter.cancelRun(runId).catch(() => undefined);
3081
3198
  if (turn !== null)
3082
3199
  chatTurns.abort(id, turn.turnId);
3083
3200
  return reply.code(409).send({ error: `chat '${id}' was closed while its path was launching; the run was cancelled` });
3084
3201
  }
3202
+ path.lastToldScopeKey = scopeKey;
3085
3203
  pending.commit();
3086
3204
  persistAccepted(pending.preface, () => {
3087
3205
  runtime.linkChatRun?.(id, runId);
@@ -3100,47 +3218,24 @@ runtime = {}) {
3100
3218
  deliver: repoRef !== undefined ? 'pr' : 'none',
3101
3219
  });
3102
3220
  });
3103
- return reply.code(202).send({ seats: voice, turnId, runId, stepId, ...singleSeat202 });
3104
- }
3105
- const view = (await adapter.sessionsDetail()).find((v) => v.session.id === path.runId);
3106
- if (view === undefined) {
3107
- askPaths.release(id, lease);
3108
- if (turn !== null)
3109
- chatTurns.abort(id, turn.turnId);
3110
- return reply.code(409).send({ error: `chat '${id}': its path ${path.runId} is gone — open a new chat` });
3111
- }
3112
- const status = String(view.session.status);
3113
- if (status !== 'awaiting_human') {
3114
- askPaths.release(id, lease);
3115
- if (turn !== null)
3116
- chatTurns.abort(id, turn.turnId);
3117
- const who = path.pa ?? 'the primary agent';
3118
- return reply.code(409).send({
3119
- code: 'turn_in_flight',
3120
- error: status === 'executing' || status === 'running'
3121
- ? `${who} is still answering the previous message in this chat — wait for the reply. Nothing was sent.`
3122
- : `chat '${id}': its path ${path.runId} is ${status}, not waiting for your turn — nothing was sent`,
3123
- chatId: id,
3124
- runId: path.runId,
3125
- status,
3221
+ return reply.code(202).send({
3222
+ seats: voice, turnId, runId, stepId, ...singleSeat202,
3126
3223
  });
3127
3224
  }
3128
- // The status read awaited: the chat may have been closed (and its id reopened) meanwhile —
3129
- // nothing of this message may land on the newcomer (codex on #808 r3, 2).
3130
- if (askPaths.get(id)?.generation !== lease) {
3131
- if (turn !== null)
3132
- chatTurns.abort(id, turn.turnId);
3133
- return reply.code(409).send({ error: `chat '${id}' was closed while the message was in flight; nothing was sent` });
3134
- }
3135
3225
  const stepId = askPaths.nextStep(id);
3136
- const pending = await considerations.pendingPreface(id);
3137
- await adapter.proposePlan(path.runId, { steps: [answerStep(stepId, withPreface(pending.preface))] }, turnId);
3226
+ // crew#1020: tell the run about a refreshed scope until a continuation accepts it.
3227
+ let instructions = withPreface(pending.preface);
3228
+ if (scope !== undefined && scopeKey !== path.lastToldScopeKey) {
3229
+ instructions = `${chatScopeStatement(id, scope, rules)}\n\n${instructions}`;
3230
+ }
3231
+ await adapter.proposePlan(path.runId, { steps: [answerStep(stepId, instructions)] }, turnId);
3138
3232
  await adapter.confirmGate(path.runId, true);
3139
3233
  askPaths.release(id, lease);
3140
3234
  if (!askPaths.continued(id, lease, stepId)) {
3141
3235
  // Closed between the proposal and here: the DELETE cancelled the run this was accepted on.
3142
3236
  return reply.code(409).send({ error: `chat '${id}' was closed while the message was in flight; its path was cancelled` });
3143
3237
  }
3238
+ path.lastToldScopeKey = scopeKey;
3144
3239
  pending.commit();
3145
3240
  persistAccepted(pending.preface, () => undefined);
3146
3241
  return reply.code(202).send({ seats: voice, turnId, runId: path.runId, stepId, ...singleSeat202 });
@@ -5438,6 +5533,19 @@ runtime = {}) {
5438
5533
  .send({ error: "deliverDefault must be 'pr' or 'none'" });
5439
5534
  }
5440
5535
  }
5536
+ // qeAcceptance (api-types 0.120.0): 'required' | 'off' only. 'off' skips QE acceptance on every
5537
+ // launch that requires it (labelled with the setting), so a typo must be a 400, never a dropped key.
5538
+ // Turning it off waives an assurance instrument on every launch after it, so only a PERSON may
5539
+ // change it (the predicate the watch switches use); a worker's bearer gets 403.
5540
+ if (Object.hasOwn(patch, 'qeAcceptance')) {
5541
+ const q = patch.qeAcceptance;
5542
+ if (q !== 'required' && q !== 'off') {
5543
+ return reply.code(400).send({ error: "qeAcceptance must be 'required' or 'off'" });
5544
+ }
5545
+ if (!isHumanOperator(actorOf(req))) {
5546
+ return reply.code(403).send({ error: 'Only a person can change QE acceptance (qeAcceptance); a worker or system actor cannot' });
5547
+ }
5548
+ }
5441
5549
  // onboardingAutoCapture (crew#552): whether a completed onboarding chains capture-learnings.
5442
5550
  if (Object.hasOwn(patch, 'onboardingAutoCapture') && typeof patch.onboardingAutoCapture !== 'boolean') {
5443
5551
  return reply.code(400).send({ error: 'onboardingAutoCapture must be true or false' });
@@ -5522,6 +5630,7 @@ runtime = {}) {
5522
5630
  'workerStallEscalateAction',
5523
5631
  'workerStallMaxEscalations',
5524
5632
  'deliverDefault',
5633
+ 'qeAcceptance',
5525
5634
  'onboardingAutoCapture',
5526
5635
  'deliverIdentityLogin',
5527
5636
  'deliverIdentityByRepo',
@@ -6195,6 +6304,35 @@ runtime = {}) {
6195
6304
  dropDocLedgerRows: runtime.dropDocLedgerRows ?? (() => ({ ok: true, removed_keys: [] })),
6196
6305
  log: (m) => app.log.warn(m),
6197
6306
  });
6307
+ // ── A demo run's video as an editable document (the video editor re-cuts it through E1's per-doc API) ──
6308
+ registerDemoDocumentRoute(app, adapter, {
6309
+ bridgeFor: async (projectId) => {
6310
+ const root = await projectDocsRoot(adapter, projectSettings, projectId);
6311
+ if (root === null)
6312
+ return null;
6313
+ const bridge = await interactiveBridges.ensure(root);
6314
+ return async (method, path, body) => {
6315
+ const raw = Buffer.isBuffer(body);
6316
+ const res = await fetch(`http://${bridge.host}:${bridge.port}${path}`, {
6317
+ method,
6318
+ headers: { 'content-type': raw ? 'application/octet-stream' : 'application/json' },
6319
+ body: raw ? new Uint8Array(body) : JSON.stringify(body),
6320
+ // A segment upload or the import's render (ffmpeg) can take minutes on a loaded host.
6321
+ signal: AbortSignal.timeout(600_000),
6322
+ });
6323
+ let parsed = {};
6324
+ try {
6325
+ const j = await res.json();
6326
+ if (j !== null && typeof j === 'object' && !Array.isArray(j))
6327
+ parsed = j;
6328
+ }
6329
+ catch {
6330
+ parsed = {};
6331
+ }
6332
+ return { status: res.status, body: parsed };
6333
+ };
6334
+ },
6335
+ });
6198
6336
  // ── Project-attributed docs list (crew#472) ──
6199
6337
  // `GET /projects/:id/interactive/api/docs` relays the bridge's list and stamps each row with
6200
6338
  // the mount's `projectId` — the attribution the bridge cannot know. Same static-over-wildcard