@quantiya/codevibe-claude-plugin 2.0.41 → 2.0.43

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 (21) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/node_modules/@quantiya/codevibe-core/dist/index.js +454 -445
  3. package/node_modules/@quantiya/codevibe-core/dist/local-executor/class-b-consumer.d.ts +5 -5
  4. package/node_modules/@quantiya/codevibe-core/dist/local-executor/hook-bridge.d.ts +1 -1
  5. package/node_modules/@quantiya/codevibe-core/dist/local-executor/local-executor-impl.d.ts +53 -20
  6. package/node_modules/@quantiya/codevibe-core/dist/local-executor/types.d.ts +8 -2
  7. package/node_modules/@quantiya/codevibe-core/dist/local-executor/workspace-shadow.d.ts +16 -0
  8. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/cli.js +1302 -374
  9. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/command-intent.d.ts +18 -0
  10. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/components/InputBar.d.ts +1 -0
  11. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/gate-decision-submit.d.ts +26 -5
  12. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/index.d.ts +17 -0
  13. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/non-tty-fallback.d.ts +1 -1
  14. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/quorum-loop.d.ts +224 -51
  15. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/reducer.d.ts +7 -0
  16. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/slash-routes/continuation.d.ts +5 -0
  17. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/task-label.d.ts +25 -0
  18. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/types.d.ts +83 -0
  19. package/node_modules/@quantiya/codevibe-core/dist/substrate-launch/engage-substrate.d.ts +2 -0
  20. package/node_modules/@quantiya/codevibe-core/package.json +1 -1
  21. package/package.json +2 -2
@@ -7955,6 +7955,39 @@ function createExplicitTtyExitCoordinator(deps) {
7955
7955
  // src/orchestration-shell/reducer.ts
7956
7956
  var import_ulid = require("ulid");
7957
7957
 
7958
+ // src/orchestration-shell/task-label.ts
7959
+ var GROUP_GATE_PREFIX = "group:";
7960
+ function shortTaskId(taskId) {
7961
+ return taskId.length > 8 ? taskId.slice(0, 8) : taskId;
7962
+ }
7963
+ function taskLabel(state, taskId) {
7964
+ let direct = state.taskOrdinals.get(taskId);
7965
+ if (direct)
7966
+ return direct.retryOf ? `#${direct.ordinal} retry` : `#${direct.ordinal}`;
7967
+ if (taskId.startsWith(GROUP_GATE_PREFIX)) {
7968
+ let group = state.taskOrdinals.get(taskId.slice(GROUP_GATE_PREFIX.length));
7969
+ if (group) return `#${group.ordinal}`;
7970
+ }
7971
+ let team = state.team;
7972
+ if (team && team.taskGroupId) {
7973
+ let group = state.taskOrdinals.get(team.taskGroupId);
7974
+ if (group) {
7975
+ for (let [trackIndex, track] of team.tracks)
7976
+ if (track.taskId === taskId) return `#${group.ordinal}.${trackIndex + 1}`;
7977
+ }
7978
+ }
7979
+ return shortTaskId(taskId);
7980
+ }
7981
+ function taskIdForOrdinal(state, ordinal) {
7982
+ for (let [id, record] of state.taskOrdinals)
7983
+ if (record.ordinal === ordinal) return id;
7984
+ return null;
7985
+ }
7986
+ function endedTaskAdvisory(described, taskId) {
7987
+ let ended = described.find((t) => t.taskId === taskId && t.outcome === "discarded");
7988
+ return ended ? `Task ${ended.label} ended on this side \u2014 its workspace copy was discarded; accepting this review applies nothing. Choose Abort (or Cancel) and re-run the request.` : null;
7989
+ }
7990
+
7958
7991
  // src/orchestration-shell/web/sanitize.ts
7959
7992
  function sanitizeForTerminal(input) {
7960
7993
  if (!input) return "";
@@ -8270,6 +8303,23 @@ function liveLabel(event) {
8270
8303
  // src/orchestration-shell/reducer.ts
8271
8304
  var CONVERSATION_BUFFER_MAX = 500;
8272
8305
  function reducer(state, action) {
8306
+ return normalizeActiveGatePrompt(reduceAction(state, action));
8307
+ }
8308
+ function isAnswerableGatePrompt(entry) {
8309
+ if (entry.kind !== "gate-prompt" || entry.final !== !1) return !1;
8310
+ let phase = entry.uiState.phase;
8311
+ return phase === "awaiting-number" || phase === "awaiting-notes";
8312
+ }
8313
+ function normalizeActiveGatePrompt(state) {
8314
+ let current = state.activeGatePromptId;
8315
+ if (current !== null) {
8316
+ let entry = state.conversation.find((e) => e.id === current);
8317
+ if (entry && isAnswerableGatePrompt(entry)) return state;
8318
+ }
8319
+ let next = state.conversation.find(isAnswerableGatePrompt)?.id ?? null;
8320
+ return next === current ? state : { ...state, activeGatePromptId: next };
8321
+ }
8322
+ function reduceAction(state, action) {
8273
8323
  switch (action.type) {
8274
8324
  case "USER_INPUT":
8275
8325
  return reduceUserInput(state, action.text, action.attachments, action.imagePaths);
@@ -8302,6 +8352,16 @@ function reducer(state, action) {
8302
8352
  return reducePlannerOfferPresented(state, action.offer);
8303
8353
  case "CLEAR_PENDING_PLANNER_OFFER":
8304
8354
  return { ...state, pendingPlannerOffer: null };
8355
+ case "TASK_ORDINAL_ASSIGNED":
8356
+ return reduceTaskOrdinalAssigned(state, action.taskId, action.kind, action.retryOf);
8357
+ case "APPLY_CONFLICT_PRESENTED":
8358
+ return reduceApplyConflictPresented(state, action.menu);
8359
+ case "CLEAR_PENDING_APPLY_CONFLICT": {
8360
+ if (action.taskId === void 0) return { ...state, pendingApplyConflicts: /* @__PURE__ */ new Map() };
8361
+ if (!state.pendingApplyConflicts.has(action.taskId)) return state;
8362
+ let rest = new Map(state.pendingApplyConflicts);
8363
+ return rest.delete(action.taskId), { ...state, pendingApplyConflicts: rest };
8364
+ }
8305
8365
  case "STRUCTURAL_SUMMARY_GENERATED":
8306
8366
  return {
8307
8367
  ...state,
@@ -8545,13 +8605,13 @@ function reduceReviewerStatusLine(state, event) {
8545
8605
  function reduceTaskProgress(state, event) {
8546
8606
  let s = state;
8547
8607
  if (MILESTONE_SCROLLBACK_PHASES.has(event.phase)) {
8548
- let milestone = {
8608
+ let milestoneTaskId = "taskId" in event && typeof event.taskId == "string" ? event.taskId : null, milestoneLabel = milestoneTaskId ? taskLabel(state, milestoneTaskId) : "", milestonePrefix = milestoneLabel.startsWith("#") ? `[Task ${milestoneLabel}] ` : "", milestone = {
8549
8609
  kind: "advisory",
8550
8610
  id: (0, import_ulid.ulid)(),
8551
8611
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8552
8612
  final: !0,
8553
8613
  source: "shell",
8554
- text: renderProgressLine(event),
8614
+ text: `${milestonePrefix}${renderProgressLine(event)}`,
8555
8615
  // R7 A4 (Stage-2 r4 Codex MED) — the halt_notice echo is DESKTOP-ONLY by
8556
8616
  // contract: `localOnly` keeps it out of the turn mirror + handoff briefs
8557
8617
  // (index.ts outbound-context filters), so operational halt text can never
@@ -8732,6 +8792,43 @@ function reducePlannerOfferPresented(state, offer) {
8732
8792
  pendingClarification: null
8733
8793
  };
8734
8794
  }
8795
+ function reduceTaskOrdinalAssigned(state, taskId, kind, retryOf) {
8796
+ if (state.taskOrdinals.has(taskId)) return state;
8797
+ let taskOrdinals = new Map(state.taskOrdinals);
8798
+ return taskOrdinals.set(taskId, {
8799
+ ordinal: state.nextTaskOrdinal,
8800
+ kind,
8801
+ ...retryOf ? { retryOf } : {}
8802
+ }), { ...state, taskOrdinals, nextTaskOrdinal: state.nextTaskOrdinal + 1 };
8803
+ }
8804
+ var APPLY_CONFLICT_MENU_HINT = "Reply with a number 1-2 (or /task retry <n> / /task discard <n> while another review is open); other text closes this menu.";
8805
+ function reduceApplyConflictPresented(state, menu) {
8806
+ let text2 = [
8807
+ ...menu.conflicts.map(
8808
+ (c) => ` ${c.path} \u2014 ${c.cause === "session-task" ? `changed by task ${c.byTaskLabel ?? "?"}` : c.cause === "write-error" ? "could not be written" : "changed outside CodeVibe"}`
8809
+ ),
8810
+ "",
8811
+ " 1. Retry as a new task \u2014 re-run the request on a fresh snapshot (reviewed and approved again)",
8812
+ " 2. Discard \u2014 keep your files as they are",
8813
+ "",
8814
+ APPLY_CONFLICT_MENU_HINT
8815
+ ].join(`
8816
+ `), advisoryEntry = {
8817
+ kind: "advisory",
8818
+ id: (0, import_ulid.ulid)(),
8819
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8820
+ final: !0,
8821
+ source: "shell",
8822
+ text: text2,
8823
+ // Desktop-only by contract (design §3 C3 / §9): never mirrored, never folded
8824
+ // into a workflow-handoff brief.
8825
+ localOnly: !0
8826
+ }, withEntry = appendConversation(state, advisoryEntry), menus = new Map(state.pendingApplyConflicts);
8827
+ return menus.set(menu.taskId, { ...menu, conversationEntryId: advisoryEntry.id }), {
8828
+ ...withEntry,
8829
+ pendingApplyConflicts: menus
8830
+ };
8831
+ }
8735
8832
  function reduceEventReceived(state, event, role) {
8736
8833
  if (state.conversation.some(
8737
8834
  (entry2) => entry2.kind === "subagent-event" && entry2.event.eventId === event.eventId
@@ -8841,13 +8938,18 @@ function findUnfinalizedGatePromptIndex(state, taskId) {
8841
8938
  }
8842
8939
  return -1;
8843
8940
  }
8844
- function makeGatePromptPanel(envelope) {
8941
+ function makeGatePromptPanel(envelope, state) {
8942
+ let label = state ? taskLabel(state, envelope.taskId) : "";
8845
8943
  return {
8846
8944
  kind: "gate-panel",
8847
8945
  id: (0, import_ulid.ulid)(),
8848
8946
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8849
8947
  final: !0,
8850
- panel: { variant: "prompt", envelope }
8948
+ panel: {
8949
+ variant: "prompt",
8950
+ envelope,
8951
+ ...label.startsWith("#") ? { taskLabel: `Task ${label}` } : {}
8952
+ }
8851
8953
  };
8852
8954
  }
8853
8955
  function makeGateSummaryPanel(reviewSummary) {
@@ -8876,7 +8978,7 @@ function promoteQueuedGatePrompt(state, idx, target) {
8876
8978
  ...state.conversation.slice(idx + 1)
8877
8979
  ], promoted = appendConversation(
8878
8980
  { ...state, conversation: nextConversation },
8879
- makeGatePromptPanel(head)
8981
+ makeGatePromptPanel(head, state)
8880
8982
  );
8881
8983
  return promotedReviewSummary !== void 0 && promotedReviewSummary.rounds.length > 0 && (promoted = appendConversation(promoted, makeGateSummaryPanel(promotedReviewSummary))), promoted;
8882
8984
  }
@@ -8909,7 +9011,7 @@ function reduceGatePromptReceived(state, envelope) {
8909
9011
  queue: [],
8910
9012
  uiState: durableResolution ? { phase: "resolved", resolvedBy: "durable", resolution: durableResolution } : isClaimed ? { phase: "claimed", decisionClaimId: state.gateActionClaims.get(envelope.gateId) ?? null } : { phase: "awaiting-number" }
8911
9013
  }, withControl = appendConversation(state, entry);
8912
- return appendConversation(withControl, makeGatePromptPanel(envelope));
9014
+ return appendConversation(withControl, makeGatePromptPanel(envelope, state));
8913
9015
  }
8914
9016
  function reduceGatePromptDecisionFenced(state, promptEntryId, gateId, decisionAttemptId, canonicalRequestHash, decisionClaimId) {
8915
9017
  let idx = state.conversation.findIndex(
@@ -9326,6 +9428,12 @@ function buildInitialState(args) {
9326
9428
  // A1a #3/#4: the lightweight local interactive offer. null until the shell
9327
9429
  // presents a team-decomposition / build-transition offer.
9328
9430
  pendingPlannerOffer: null,
9431
+ // P46a D1/C3: in-session task numbers (one counter, starts at 1) + the
9432
+ // desktop-local apply-conflict menu (null until a promote collides).
9433
+ taskOrdinals: /* @__PURE__ */ new Map(),
9434
+ nextTaskOrdinal: 1,
9435
+ pendingApplyConflicts: /* @__PURE__ */ new Map(),
9436
+ activeGatePromptId: null,
9329
9437
  inputHistory: [],
9330
9438
  // CP-1.d §4.8 LOCK: structuralSummary stays null until session-start
9331
9439
  // generation completes; structuralSummaryError populated on failure.
@@ -10687,8 +10795,8 @@ function formatHaltReason3(reason) {
10687
10795
  function formatPromptKind2(promptKind) {
10688
10796
  return promptKind === ORCHESTRATION_ESCALATED_GATE_PROMPT_KIND ? "Escalated review" : promptKind === ORCHESTRATION_FINAL_APPROVAL_PROMPT_KIND ? "Final approval" : promptKind === CONTINUATION_OFFER_HANDOFF_PROMPT_KIND ? "Continue with which agent?" : promptKind;
10689
10797
  }
10690
- function renderPromptVariant(envelope) {
10691
- let { ink } = getInkRuntime(), { Box, Text } = ink, haltReasonText = envelope.promptKind === ORCHESTRATION_ESCALATED_GATE_PROMPT_KIND ? formatHaltReason3(envelope.reason) : null, kindLabel = formatPromptKind2(envelope.promptKind), children = [], context = GateContextPanel({ envelope });
10798
+ function renderPromptVariant(envelope, taskLabel2) {
10799
+ let { ink } = getInkRuntime(), { Box, Text } = ink, haltReasonText = envelope.promptKind === ORCHESTRATION_ESCALATED_GATE_PROMPT_KIND ? formatHaltReason3(envelope.reason) : null, kindLabel = taskLabel2 ? `${formatPromptKind2(envelope.promptKind)} \xB7 ${taskLabel2}` : formatPromptKind2(envelope.promptKind), children = [], context = GateContextPanel({ envelope });
10692
10800
  context && children.push(
10693
10801
  React11.createElement(
10694
10802
  Box,
@@ -10758,7 +10866,7 @@ function renderPromptVariant(envelope) {
10758
10866
  }
10759
10867
  function GatePanelEntry(props) {
10760
10868
  let { panel } = props.entry;
10761
- return panel.variant === "prompt" ? renderPromptVariant(panel.envelope) : renderReviewSummarySection(panel.reviewSummary);
10869
+ return panel.variant === "prompt" ? renderPromptVariant(panel.envelope, panel.taskLabel) : renderReviewSummarySection(panel.reviewSummary);
10762
10870
  }
10763
10871
 
10764
10872
  // src/orchestration-shell/components/ConversationPane.tsx
@@ -17006,7 +17114,7 @@ function InputBar(props) {
17006
17114
  )
17007
17115
  );
17008
17116
  let placeholder = props.placeholder ?? "";
17009
- gatePromptMode?.kind === "awaiting-number" ? placeholder = `Type a number 1..${gatePromptMode.maxOption}` : gatePromptMode?.kind === "awaiting-notes" && (placeholder = "Type your notes + Enter");
17117
+ gatePromptMode?.kind === "awaiting-number" ? placeholder = gatePromptMode.taskLabel ? `Type a number 1..${gatePromptMode.maxOption} \xB7 ${gatePromptMode.taskLabel}` : `Type a number 1..${gatePromptMode.maxOption}` : gatePromptMode?.kind === "awaiting-notes" && (placeholder = "Type your notes + Enter");
17010
17118
  let shown = displayValue(value), cur = clampCursor2(cursor, shown.length), valueWidth = Math.max(1, columns - 6), win = computeInputWindow(shown, cur, valueWidth, scrollRef.current);
17011
17119
  scrollRef.current = win.scroll;
17012
17120
  let origin = rowOrigin ?? { left: 0, top: 0 };
@@ -17383,13 +17491,20 @@ function decisionToGroupOutcome(decision) {
17383
17491
  // src/orchestration-shell/gate-decision-submit.ts
17384
17492
  var import_uuid3 = require("uuid");
17385
17493
  var defaultDecisionAttemptReceiptStore = new DecisionAttemptReceiptStore();
17386
- function findActiveGatePromptEntry(conversation) {
17387
- for (let i = conversation.length - 1; i >= 0; i--) {
17388
- let entry = conversation[i];
17389
- if (entry.kind === "gate-prompt" && entry.final === !1)
17390
- return entry;
17494
+ function findActiveGatePromptEntry(conversation, activeGatePromptId = null) {
17495
+ if (activeGatePromptId !== null) {
17496
+ for (let entry of conversation)
17497
+ if (entry.id === activeGatePromptId && entry.kind === "gate-prompt" && entry.final === !1)
17498
+ return entry;
17391
17499
  }
17392
- return null;
17500
+ let oldestOpen = null;
17501
+ for (let entry of conversation) {
17502
+ if (entry.kind !== "gate-prompt" || entry.final !== !1) continue;
17503
+ let phase = entry.uiState.phase;
17504
+ if (phase === "awaiting-number" || phase === "awaiting-notes") return entry;
17505
+ oldestOpen ??= entry;
17506
+ }
17507
+ return oldestOpen;
17393
17508
  }
17394
17509
  function getStoreSessionId(store) {
17395
17510
  return store.getState().session.sessionId;
@@ -18019,7 +18134,7 @@ var readline = __toESM(require("readline"));
18019
18134
  function plannerDecisionRenderEnabled(env = process.env) {
18020
18135
  return env.CODEVIBE_PLANNER_DEBUG === "1";
18021
18136
  }
18022
- function renderEntryAsLine(entry) {
18137
+ function renderEntryAsLine(entry, labelFor) {
18023
18138
  switch (entry.kind) {
18024
18139
  case "user-message":
18025
18140
  return `> ${entry.text}`;
@@ -18040,9 +18155,9 @@ function renderEntryAsLine(entry) {
18040
18155
  case "advisory":
18041
18156
  return `[${entry.source}] ${entry.text}`;
18042
18157
  case "gate-prompt": {
18043
- let lines = [], halt = entry.envelope.reason ? ` (${entry.envelope.reason})` : "";
18158
+ let lines = [], halt = entry.envelope.reason ? ` (${entry.envelope.reason})` : "", label = labelFor?.(entry.envelope.taskId);
18044
18159
  lines.push(
18045
- `[gate-prompt ${entry.envelope.promptKind}${halt}] ${entry.envelope.summary ?? ""}`
18160
+ `[gate-prompt ${entry.envelope.promptKind}${halt}]${label ? ` [${label}]` : ""} ${entry.envelope.summary ?? ""}`
18046
18161
  );
18047
18162
  for (let i = 0; i < entry.envelope.options.length; i++) {
18048
18163
  let opt = entry.envelope.options[i];
@@ -18086,8 +18201,13 @@ async function runLineLogFallback(args) {
18086
18201
  let newEntries = state.conversation.slice(printedUpTo);
18087
18202
  printedUpTo = state.conversation.length;
18088
18203
  for (let entry of newEntries)
18089
- entry.kind !== "gate-panel" && (entry.kind === "planner-decision" && !plannerDecisionRenderEnabled() || out.write(renderEntryAsLine(entry) + `
18090
- `));
18204
+ entry.kind !== "gate-panel" && (entry.kind === "planner-decision" && !plannerDecisionRenderEnabled() || out.write(
18205
+ renderEntryAsLine(entry, (taskId) => {
18206
+ let label = taskLabel(state, taskId);
18207
+ return label.startsWith("#") ? `Task ${label}` : void 0;
18208
+ }) + `
18209
+ `
18210
+ ));
18091
18211
  }), rl = readline.createInterface({ input: inp, output: out, terminal: !1 }), onAbort = null;
18092
18212
  args.signal && (args.signal.aborted ? rl.close() : (onAbort = () => {
18093
18213
  try {
@@ -18146,13 +18266,32 @@ function buildTrackLabelByTaskId(team) {
18146
18266
  entry.taskId && map.set(entry.taskId, `Track ${displayTrackNumber(idx)}`);
18147
18267
  return map.size > 0 ? map : null;
18148
18268
  }
18149
- function deriveGatePromptMode(active, gateActionRecoveryBlocked = !1) {
18269
+ function buildGateLabelByTaskId(state) {
18270
+ let trackLabels = buildTrackLabelByTaskId(state.team), map = new Map(trackLabels ?? []);
18271
+ for (let entry of state.conversation) {
18272
+ if (entry.kind !== "gate-prompt") continue;
18273
+ let id = entry.envelope.taskId;
18274
+ if (map.has(id) && map.get(id).startsWith("Task ")) continue;
18275
+ let label = taskLabel(state, id);
18276
+ if (!label.startsWith("#")) continue;
18277
+ let track = trackLabels?.get(id);
18278
+ map.set(id, track ? `Task ${label} \xB7 ${track}` : `Task ${label}`);
18279
+ }
18280
+ return map.size > 0 ? map : null;
18281
+ }
18282
+ function deriveGatePromptMode(active, gateActionRecoveryBlocked = !1, labelByTaskId = null) {
18150
18283
  if (!active) return null;
18151
18284
  if (gateActionRecoveryBlocked && active.uiState.phase !== "resolved")
18152
18285
  return { kind: "submitting" };
18153
18286
  switch (active.uiState.phase) {
18154
- case "awaiting-number":
18155
- return { kind: "awaiting-number", maxOption: active.envelope.options.length };
18287
+ case "awaiting-number": {
18288
+ let taskLabel2 = labelByTaskId?.get(active.envelope.taskId);
18289
+ return {
18290
+ kind: "awaiting-number",
18291
+ maxOption: active.envelope.options.length,
18292
+ ...taskLabel2 ? { taskLabel: taskLabel2 } : {}
18293
+ };
18294
+ }
18156
18295
  case "awaiting-notes":
18157
18296
  return { kind: "awaiting-notes", decisionDraft: active.uiState.decisionDraft };
18158
18297
  case "submitting":
@@ -18184,7 +18323,7 @@ function OrchestrationApp(props) {
18184
18323
  }, [wizardOpen, isRawModeSupported]);
18185
18324
  let staticOrderRef = React18.useRef(null);
18186
18325
  staticOrderRef.current || (staticOrderRef.current = makeStaticOrderAccum());
18187
- let trackLabelByTaskId = buildTrackLabelByTaskId(state.team), staticConversation = plannerDecisionRenderEnabled() ? state.conversation : state.conversation.filter((e) => e.kind !== "planner-decision");
18326
+ let trackLabelByTaskId = buildGateLabelByTaskId(state), staticConversation = plannerDecisionRenderEnabled() ? state.conversation : state.conversation.filter((e) => e.kind !== "planner-decision");
18188
18327
  appendNewlyFinal(staticOrderRef.current, staticConversation, (entry) => ({
18189
18328
  key: entry.id,
18190
18329
  element: renderConversationEntry(entry, trackLabelByTaskId),
@@ -18195,9 +18334,10 @@ function OrchestrationApp(props) {
18195
18334
  ...staticOrderRef.current.items
18196
18335
  ], nonFinal = state.conversation.filter(
18197
18336
  (e) => !e.final
18198
- ), activeGateEntry = findActiveGatePromptEntry(state.conversation), gatePromptMode = deriveGatePromptMode(
18337
+ ), activeGateEntry = findActiveGatePromptEntry(state.conversation, state.activeGatePromptId), gatePromptMode = deriveGatePromptMode(
18199
18338
  activeGateEntry,
18200
- state.gateActionRecoveryBlocked
18339
+ state.gateActionRecoveryBlocked,
18340
+ trackLabelByTaskId
18201
18341
  ), terminalRows = process.stdout.rows ?? 24, FIXED_CHROME_ROWS = 8, teamReserveRows = state.team ? 5 + state.team.tracks.size : 0, dropdownFits = terminalRows - FIXED_CHROME_ROWS - teamReserveRows >= DROPDOWN_MAX_ROWS + MIN_LIVE_ROWS, dropdownActive = state.progress === null && gatePromptMode === null && state.reviewerWizard === null && dropdownFits, dropdownReserveRows = dropdownActive ? DROPDOWN_MAX_ROWS : 0, wizardReserveRows = state.reviewerWizard ? WIZARD_MAX_ROWS : 0, { rendered: cappedNonFinal, hiddenCount } = capLiveEntries(
18202
18342
  nonFinal,
18203
18343
  terminalRows,
@@ -18616,11 +18756,11 @@ function renderLocalGemmaPlannerPrompt(input, options) {
18616
18756
  "Never refuse merely because the user asks about local repository files, the current directory, the workspace, or the project. Refuse only for clearly unsafe requests such as exposing secrets/credentials, destructive root/home deletion, malware, bypassing auth/paywalls, or exfiltration.",
18617
18757
  "",
18618
18758
  "Routing rules:",
18619
- '- start_task: user asks to create, add, edit, fix, implement, refactor, test, run tests, or review a SPECIFIC, BOUNDED change \u2014 such as their pending diff, staged/uncommitted changes, a named file, or a pull request. (A BROAD "review/audit the WHOLE codebase for bugs" is read-only understanding \u2192 familiarize, NOT start_task \u2014 see below.) If the user says current directory, current working directory, repo root, workspace, ".", "./", or an absolute path, treat the target as sufficiently specified. If the requested content is obvious, such as a JavaScript hello-world file, do not ask for extra content. A SINGLE task is start_task even when it has multiple steps; choose team_decompose ONLY when the user explicitly asks for parallel work (see below).',
18759
+ '- start_task: user asks to create, add, edit, fix, implement, refactor, test, run tests, or review a SPECIFIC, BOUNDED change \u2014 such as their pending diff, staged/uncommitted changes, a named file, or a pull request. (A BROAD "review/audit the WHOLE codebase for bugs" is read-only understanding \u2192 familiarize, NOT start_task \u2014 see below.) If the user says current directory, current working directory, repo root, workspace, ".", "./", or an absolute path, treat the target as sufficiently specified. If the requested content is obvious, such as a JavaScript hello-world file, do not ask for extra content. A SINGLE task is start_task even when it has multiple steps; choose team_decompose ONLY when the user explicitly asks for parallel work (see below). This ALSO includes requests that fetch/check an external URL or search online AND write, save, create, or update local files, reports, or code (e.g. "check https://... and write a report into report.md" -> start_task).',
18620
18760
  '- team_decompose: user EXPLICITLY asks to split the work into MULTIPLE PARALLEL tasks or tracks, run an "agent team", do things "in parallel", or describes 2+ INDEPENDENT pieces (typically touching different files) to run concurrently. Prefer team_decompose over start_task whenever the request names an agent team or parallel/separate tracks. A single multi-step task is start_task, NOT team_decompose.',
18621
18761
  '- familiarize: user asks to read, inspect, understand, explain, or summarize the current project, codebase, repository, folder, files, or working directory without asking for a mutation. This ALSO covers a BROAD read-only REVIEW or AUDIT of the WHOLE repository/codebase \u2014 e.g. "review the codebase for bugs", "audit the repo for issues", "look over the whole project for problems": reviewing the ENTIRE repo for bugs/quality/security is a read-only understanding task, so it is familiarize, NOT start_task. (A BOUNDED review of a specific pending DIFF or named file is start_task instead.)',
18622
18762
  `- brainstorm: user asks to explore options, tradeoffs, risks, architecture directions, or recommendations for THIS project's design or implementation before deciding what to design or implement. brainstorm is NOT for opinions or analysis of a web page, article, or answer from earlier in this session \u2014 that is advisory_response. Use brainstorm for exploratory prompts such as "brainstorm ways to build offline support" or "compare approaches to create a local context store". Do NOT use brainstorm when the user asks for immediate mutation, a design artifact, a hard gate, tests, review, commit, deploy, or release; choose the workflow action or ask one clarifying question.`,
18623
- '- browse: user asks to read/open/fetch/summarize a specific web URL (http/https), OR to look something up on the web / search online / find the latest on a topic. Put any explicit URL(s) in "browseUrls" (array) and, when there is no URL, put the search query in "browseQuery". This route fetches the page (or searches) on the user machine and answers from the content; it is NOT a familiarize (which reads the LOCAL repo) and NOT advisory_response.',
18763
+ '- browse: user asks a read-only question to read/open/fetch/summarize a specific web URL (http/https), OR to look something up on the web / search online / find the latest on a topic, WITHOUT asking to create, write, or modify any local files. If the user asks to fetch a URL or search AND save/write/create local files or reports, choose start_task instead. Put any explicit URL(s) in "browseUrls" (array) and, when there is no URL, put the search query in "browseQuery". This route fetches the page (or searches) on the user machine and answers from the content; it is NOT a familiarize (which reads the LOCAL repo) and NOT advisory_response.',
18624
18764
  options?.isSubprocessRunner ? `- advisory_response: user asks a general question that does not require repository context or file changes. Put a COMPLETE, natural, conversational ANSWER to the question in the "advisory_summary" field (a full helpful reply, like a chat assistant \u2014 NOT a one-line label or a restatement of the question); explain core principles accurately: for search/pathfinding, start with the start node in the open set, use strictly standard A* terminology (open set and closed set only; never invent other sets like missed set or turn set), and compare with Dijkstra's algorithm; explain that a more accurate, higher admissible heuristic (closer to the true remaining cost) guides the search more directly to the goal and expands fewer nodes, whereas a smaller or zero heuristic (like Dijkstra's algorithm) explores in all directions and expands more nodes; a heuristic must be admissible (never overestimate the true distance) to guarantee an optimal shortest path; write all mathematical expressions in clean plain text like f(n) = g(n) + h(n) (never use LaTeX math notation, \\text{}, math mode $, or backslashes); format the entire explanation in clean, well-structured markdown prose paragraphs separated by blank lines (do not use bullet lists, numbered sub-lists, or backslash line breaks; write complete narrative paragraphs); never use tab characters or \\t; never use double quotes inside advisory_summary (use single quotes ' if quoting terms); keep "rationale" a short internal classification reason. Answer directly and warmly, e.g. "Yes \u2014 I can \u2026".` : '- advisory_response: user asks a general question or capability question. Put a concise, natural, 1-sentence direct answer or definition in "advisory_summary" (e.g. "Yes, I can write Rust" or "A* is a best-first pathfinding algorithm that expands nodes by f(n) = g(n) + h(n)"); do NOT write preambles like "Here is..." or conversational labels; keep "rationale" a short reason. The downstream local advisory engine generates the full answer.',
18625
18765
  options?.isSubprocessRunner ? '- FOLLOW-UP ON EARLIER CONTENT: when the user asks for your thoughts, opinion, analysis, critique, or a deeper explanation of something already fetched or answered earlier in this session (a web page that was read, a search result, a previous reply) \u2014 e.g. "I am looking for your thoughts", "what do you think about that", "go deeper on that", "is that right?" \u2014 choose advisory_response and put the COMPLETE reply in advisory_summary (no downstream answerer runs for this runner). Never brainstorm, familiarize, or browse again for such a follow-up.' : `- FOLLOW-UP ON EARLIER CONTENT: when the user asks for your thoughts, opinion, analysis, critique, or a deeper explanation of something already fetched or answered earlier in this session (a web page that was read, a search result, a previous reply) \u2014 e.g. "I am looking for your thoughts", "what do you think about that", "go deeper on that", "is that right?" \u2014 choose advisory_response with a one-sentence advisory_summary (the shell's answerer writes the full reply from the page and the conversation). Never brainstorm, familiarize, or browse again for such a follow-up.`,
18626
18766
  options?.isSubprocessRunner ? "- summarize_current_status: user asks what changed, what the last task did, current progress, or workflow status." : "- Questions about progress, what changed, what the last task did, or workflow status are advisory_response too: the shell attaches its own status record to the answer, so never invent task history.",
@@ -18658,6 +18798,8 @@ function renderLocalGemmaPlannerPrompt(input, options) {
18658
18798
  'User: "look up the React 19 release notes online" -> {"action":"browse","rationale":"web lookup","browseQuery":"React 19 release notes"}',
18659
18799
  'User: "what is the latest LTS version of Node.js?" -> {"action":"browse","rationale":"freshness lookup needs current web info, not stale knowledge","browseQuery":"latest LTS version Node.js"}',
18660
18800
  'User: "what is the current stable version of Python?" -> {"action":"browse","rationale":"current version is a freshness web lookup","browseQuery":"current stable version Python"}',
18801
+ 'User: "check https://registry.npmjs.org/@quantiya/codevibe-core/latest and write a report into report.md" -> {"action":"start_task","rationale":"check external URL and write report file"}',
18802
+ 'User: "Please use your codevibe_web_search tool to search for \\"Quantiya AI\\", then use codevibe_web_fetch to read https://quantiya.ai/ and write a 3-bullet summary of the site into a file named claude-web-test.md." -> {"action":"start_task","rationale":"search web and write summary file"}',
18661
18803
  `User: "fyi every FLAGS.md row should also say what the default is" (session.currentTaskState is in_progress) -> {"action":"advisory_response","rationale":"a convention stated while a task runs; acknowledge, no second task","advisory_summary":"Noted \u2014 FLAGS.md rows will also state each flag's default."}`,
18662
18804
  'User: "scratch that \u2014 descriptions should not end with a period" (session.currentTaskState is in_progress) -> {"action":"advisory_response","rationale":"retracts an earlier rule while a task runs","advisory_summary":"Understood \u2014 descriptions will not end with a period."}',
18663
18805
  'User: "also add a --cv-quiet flag to release.sh" (session.currentTaskState is in_progress) -> {"action":"start_task","rationale":"asks for a new deliverable while a task runs"}',
@@ -21326,6 +21468,57 @@ function buildCommandIntentMetadata(text2) {
21326
21468
  if (intent)
21327
21469
  return { command_intent: buildCommandIntentEnvelope(intent) };
21328
21470
  }
21471
+ function isWebAccessNeeded(text2) {
21472
+ return !text2 || typeof text2 != "string" ? !1 : /https?:\/\/[^\s]+/i.test(text2) || /\bwww\.[^\s]+/i.test(text2) ? !0 : /(?:^|[.?!;])\s*(?:please\s+)?(?:implement|build|create|add|support|develop)\b[\s\S]{0,50}?\b(?:feature|functionality|function|button|capability|endpoint|command|module|ui|api|service)\b/i.test(
21473
+ text2
21474
+ ) || /\b(?:feature|functionality|function|capability)\b[\s\S]{0,40}?\b(?:to|that|which)\s+(?:search|browse|look\s+up)\b/i.test(
21475
+ text2
21476
+ ) || /(?:实现|添加|开发|支持|做个|写个)[\s\S]{0,30}?(?:功能|特性)/.test(text2) || /(?:功能|特性)[\s\S]{0,30}?(?:实现|添加|开发|支持)/.test(text2) || /(?:機能|기능)[\s\S]{0,30}?(?:実装|追加|作成|開発|구현|추가)/.test(text2) || /(?:実装|追加|作成|開発|구현|추가)[\s\S]{0,30}?(?:機能|기능)/.test(text2) ? !1 : !!(/(?:^|[.?!;])\s*(?:please\s+)?(?:search|browse)\s+(?:the\s+(?:web|internet)\b|online)(?:\s+(?:for|about)\b|\s*[,.;?!]?$)/i.test(
21477
+ text2
21478
+ ) || /(?:^|[.?!;])\s*(?:please\s+)?look\s+up\s+(?!where\b|how\b|who\b|why\b)[\w\s.-]{1,60}?\s+(?:online|on\s+the\s+web\b|on\s+the\s+internet\b)(?:\s+(?:for|about)\b|\s*[,.;?!]?$)/i.test(
21479
+ text2
21480
+ ) && !/\bonline\s+(?:checkout|users?|status|banking|store|shop|service|mode|game|player|account|system|portal|flow|documentation)\b/i.test(
21481
+ text2
21482
+ ) || /(?:^|[.?!;])\s*(?:please\s+)?(?:use|try|run|perform|do)\s+(?:a\s+)?(?:web|internet|online)\s+search(?:\s+(?:for|about|on)\b|\s*[,.;?!]?$)(?!\s+(?:api|service|endpoint|tests?|feature|sdk|library|component|function|code)\b)/i.test(
21483
+ text2
21484
+ ) || /(?:^|[.?!;])\s*(?:please\s+)?(?:google\s+(?:this|it|that)\b|search\s+(?:on\s+google|google)\s+for\b)/i.test(
21485
+ text2
21486
+ ) || /(?:^|[.?!;,。!?;])\s*(?:请|帮我|麻烦)?\s*(?:(?:去?上网|在网上|去官网|在官网|在官方网站)(?:搜索|查找|查阅|查一下|搜一下)(?!到|过|出|的|结果)|去网上搜(?:一下|索)|谷歌一下|(?:查阅|查找|搜索)(?:官网|官方网站)(?:的)?(?:文档|说明|api)|(?:在线|官网|联网)搜索一下)/i.test(
21487
+ text2
21488
+ ) || /(?:^|[.?!;])\s*(?:(?:ネット|オンライン)で\s*(?:検索して(?:みて|ください)?|調べて(?:みて|ください)?|探して(?:みて|ください)?)(?=$|[\s。、.!?])|ウェブ検索で\s*[\u4E00-\u9FFF\u30A0-\u30FF\w ]{1,20}を(?:調べて|探して))|(?:^|[.?!;])\s*[\w\s\u3040-\u30FF\u4E00-\u9FFF]{0,20}?ググ[るっ](?=$|[\s.?!;。~])/i.test(
21489
+ text2
21490
+ ) || /(?:^|[.?!;])\s*(?:[\w\s가-힣]{0,40}?\s+)?(?:인터넷|온라인|구글)에서\s*(?:검색해(?:봐|줘|요)|찾아(?:봐|줘)|조회해(?:봐|줘))(?=$|[\s.?!;。~])/i.test(
21491
+ text2
21492
+ ));
21493
+ }
21494
+ function hasFileMutationIntent(text2) {
21495
+ if (!text2 || typeof text2 != "string") return !1;
21496
+ let stripped = stripLeadingAgentControlToken(text2).trim(), textWithoutUrls = stripped.replace(/https?:\/\/[^\s]+/gi, " ").trim();
21497
+ if (/\b(?:how\s+(?:to|do\s+I|can\s+I)|explain\s+how\s+to|tell\s+me\s+how\s+to)\b/i.test(stripped) || /(?:^|[.?!;])\s*(?:what\s+(?:is|are)\b|c[oó]mo\s+(?:hacer|puedo|se\s+puede|escribir|guardar|crear)|comment\s+(?:faire|écrire|sauvegarder|créer|enregistrer)|wie\s+(?:kann\s+ich|macht\s+man|schreibt\s+man|speichert\s+man|erstellt\s+man))\b/i.test(stripped) || /(?:如何|怎么|怎样|告诉我(?:如何|怎么|怎样)|解释如何|请问|什么是)/.test(stripped) || /(?:どうやって|方法(?:を(?:教えて|説明して))|やり方を教えて|でしょうか|とは(?:何|なん)ですか)/.test(stripped) || /(?:어떻게|방법을?\s*(?:알려줘|설명해줘)|법\s*설명|하는지\s*알려줘)/.test(stripped))
21498
+ return !1;
21499
+ if (/>{1,2}\s*[\w./-]+\b/.test(textWithoutUrls))
21500
+ return !0;
21501
+ let fileExt = "(?:md|markdown|txt|json|csv|ts|js|tsx|jsx|py|html|css|yaml|yml|log|sh|xml|toml|sql|env)", writeToFilePatternEn = new RegExp(
21502
+ `\\b(?:write|save|create|dump|output|put|export|store|record|guardar|escribir|crear|enregistrer|\xE9crire|cr\xE9er|sauvegarder|speichern|schreiben|erstellen)\\b[\\s\\S]*?\\b(?:into|to|in|as|en|dans|auf|zu)\\b(?!\\s+(?:chat|console|stdout|screen|terminal)\\b)[\\s\\S]*?(?:(?:a|an|the|un|une|ein|eine|el|la)?\\s*file(?:\\s+named|\\s+called)?\\s+)?[\\w./-]+\\.${fileExt}\\b`,
21503
+ "i"
21504
+ ), writeToFilePatternGermanic = new RegExp(
21505
+ `\\b(?:in|auf|zu|into|to)\\s+[\\w./-]+\\.${fileExt}[\\s\\S]*?\\b(?:speichern|schreiben|erstellen|ablegen)\\b`,
21506
+ "i"
21507
+ ), namedFilePatternEn = /\b(?:into|to|in|en|dans|auf|zu)\s+(?:a|an|un|une|ein|eine|el|la)?\s*file(?:\s+(?:named|called))?\s+[\w./-]+\b/i, createFilePatternEn = /\b(?:create|add|touch)\s+(?:a|an|the)?\s*(?:new\s+)?(?:file|markdown\s+file|script)\b/i, directFileVerbPatternEn = new RegExp(
21508
+ `\\b(?:create|write|touch|add|crear|escribir|cr\xE9er|\xE9crire|erstellen)\\s+[\\w./-]+\\.${fileExt}\\b`,
21509
+ "i"
21510
+ ), writeToFilePatternZh = new RegExp(
21511
+ `(?:\u5199\u5165|\u4FDD\u5B58\u5230?|\u521B\u5EFA|\u751F\u6210|\u8F93\u51FA\u5230|\u5B58\u5165|\u5BFC\u51FA\u5230?|\u8BB0\u5F55\u5230?)[\\s\\S]*?[\\w./-]+\\.${fileExt}\\b`,
21512
+ "i"
21513
+ ), writeToFilePatternJa = new RegExp(
21514
+ `(?:[\\w./-]+\\.${fileExt}[\\s\\S]*?(?:\u306B|\u3078)?\\s*(?:\u66F8\u304D\u51FA|\u4FDD\u5B58|\u4F5C\u6210|\u751F\u6210|\u51FA\u529B|\u8A18\u9332|\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8)|(?:\u66F8\u304D\u51FA|\u4FDD\u5B58|\u4F5C\u6210|\u751F\u6210|\u51FA\u529B|\u8A18\u9332|\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8)[\\s\\S]*?[\\w./-]+\\.${fileExt})`,
21515
+ "i"
21516
+ ), writeToFilePatternKo = new RegExp(
21517
+ `(?:[\\w./-]+\\.${fileExt}[\\s\\S]*?(?:\uC5D0|\uC73C\uB85C|\uB85C)?\\s*(?:\uC800\uC7A5|\uC791\uC131|\uC0DD\uC131|\uAE30\uB85D|\uCD9C\uB825|\uB0B4\uBCF4\uB0B4\uAE30)|(?:\uC800\uC7A5|\uC791\uC131|\uC0DD\uC131|\uAE30\uB85D|\uCD9C\uB825|\uB0B4\uBCF4\uB0B4\uAE30)[\\s\\S]*?[\\w./-]+\\.${fileExt})`,
21518
+ "i"
21519
+ );
21520
+ return writeToFilePatternEn.test(textWithoutUrls) || writeToFilePatternGermanic.test(textWithoutUrls) || namedFilePatternEn.test(textWithoutUrls) || createFilePatternEn.test(textWithoutUrls) || directFileVerbPatternEn.test(textWithoutUrls) || writeToFilePatternZh.test(textWithoutUrls) || writeToFilePatternJa.test(textWithoutUrls) || writeToFilePatternKo.test(textWithoutUrls);
21521
+ }
21329
21522
 
21330
21523
  // src/orchestration-shell/route-browse.ts
21331
21524
  var RETAINED_PAGE_MAX_CHARS = 12e4, NO_MODEL_PREFIX = "Local CodeVibe model is required to read and summarize web pages.", RUN_INSTALL = "Run `codevibe model install` from a Pro/Max account, then ask again. No hosted model was called and no code was changed.";
@@ -21390,17 +21583,29 @@ function fetchErrorMessage(err, url) {
21390
21583
  async function readUrl(deps, runner, url) {
21391
21584
  let { store, userPrompt, signal } = deps, dispUrl = sanitizeForTerminal(url);
21392
21585
  advise(store, `Reading ${dispUrl}\u2026`);
21393
- let body, finalUrl, fetch2 = deps.guardedFetchFn ?? guardedFetch;
21586
+ let body, finalUrl, contentType = "", fetch2 = deps.guardedFetchFn ?? guardedFetch;
21394
21587
  try {
21395
- let res = await fetch2(url, signal);
21396
- body = res.body, finalUrl = res.finalUrl;
21588
+ let res = await fetch2(url, signal, { allowJson: !0 });
21589
+ body = res.body, finalUrl = res.finalUrl, contentType = res.contentType;
21397
21590
  } catch (err) {
21398
21591
  if (signal?.aborted) return;
21399
21592
  let suffix = deps.delegateAnswer ? " The mentioned agent was not consulted." : "";
21400
21593
  err instanceof FetchError ? advise(store, `${fetchErrorMessage(err, dispUrl)}${suffix}`) : advise(store, `Couldn't read ${dispUrl} (${sanitizeForTerminal(err.message)}). No code was changed.${suffix}`);
21401
21594
  return;
21402
21595
  }
21403
- let dispFinal = sanitizeForTerminal(finalUrl), { title, text: text2 } = await htmlToText(body), safeTitle = sanitizeForTerminal(title).slice(0, 200), fullSafeText = sanitizeForTerminal(text2);
21596
+ let dispFinal = sanitizeForTerminal(finalUrl), title = dispFinal, text2 = "";
21597
+ if (contentType.toLowerCase().includes("application/json"))
21598
+ try {
21599
+ let parsed = JSON.parse(body);
21600
+ text2 = JSON.stringify(parsed, null, 2);
21601
+ } catch {
21602
+ text2 = body;
21603
+ }
21604
+ else {
21605
+ let extracted = await htmlToText(body);
21606
+ title = extracted.title || dispFinal, text2 = extracted.text || "";
21607
+ }
21608
+ let safeTitle = sanitizeForTerminal(title).slice(0, 200), fullSafeText = sanitizeForTerminal(text2);
21404
21609
  if (signal?.aborted) return;
21405
21610
  if (!fullSafeText.trim()) {
21406
21611
  let suffix = deps.delegateAnswer ? " The mentioned agent was not consulted." : "";
@@ -21529,7 +21734,19 @@ async function readSearchResults(deps, runner, query, results) {
21529
21734
  advise(store, `Reading top ${targetResults.length} web pages in parallel\u2026`);
21530
21735
  let fetch2 = deps.guardedFetchFn ?? guardedFetch, fetchPromises = targetResults.map(async (res) => {
21531
21736
  try {
21532
- let fetched = await fetch2(res.url, signal), { title, text: text2 } = await htmlToText(fetched.body), safeTitle = sanitizeForTerminal(title || res.title || "").slice(0, 200), rawText = text2 || "";
21737
+ let fetched = await fetch2(res.url, signal, { allowJson: !0 }), title = res.title || "", text2 = "";
21738
+ if ((fetched.contentType || "").toLowerCase().includes("application/json"))
21739
+ try {
21740
+ let parsed = JSON.parse(fetched.body);
21741
+ text2 = JSON.stringify(parsed, null, 2);
21742
+ } catch {
21743
+ text2 = fetched.body;
21744
+ }
21745
+ else {
21746
+ let extracted = await htmlToText(fetched.body);
21747
+ title = extracted.title || title, text2 = extracted.text || "";
21748
+ }
21749
+ let safeTitle = sanitizeForTerminal(title).slice(0, 200), rawText = text2 || "";
21533
21750
  return rawText.trim() ? {
21534
21751
  finalUrl: fetched.finalUrl,
21535
21752
  safeTitle,
@@ -26367,7 +26584,7 @@ async function requestSubcommand(deps, rawTarget) {
26367
26584
  return { exitCode: 1, stdout: "Continuation request is unavailable in this session." };
26368
26585
  let ctx = deps.getActiveRequestContext();
26369
26586
  if (!ctx)
26370
- return { exitCode: 1, stdout: "No active task to hand off." };
26587
+ return { exitCode: 1, stdout: deps.explainNoActiveRequest?.() ?? "No active task to hand off." };
26371
26588
  try {
26372
26589
  return await deps.request(ctx, targetAgent) ? {
26373
26590
  exitCode: 0,
@@ -31273,7 +31490,7 @@ function stripAgyWebPlugin(workdir) {
31273
31490
  function buildImplementorArgv(agent, mode, workdir, options) {
31274
31491
  switch (agent) {
31275
31492
  case "CLAUDE": {
31276
- if (process.env.CODEVIBE_ENABLE_AGENT_WEB === "1" && !!options?.mcpConfigPath) {
31493
+ if (!!options?.mcpConfigPath) {
31277
31494
  let disallowedTools = mode === "plan" ? "WebFetch,WebSearch,Bash,Agent,Task,Workflow,SendMessage,ListAgents" : "WebFetch,WebSearch,Agent,Task,Workflow,SendMessage,ListAgents", baseTools = mode === "plan" ? "Read,Grep,Glob" : "Read,Grep,Glob,Edit,Write,Bash", allowedTools = `${baseTools},mcp__codevibe-web__codevibe_web_search,mcp__codevibe-web__codevibe_web_fetch`;
31278
31495
  return {
31279
31496
  argv: [
@@ -31310,7 +31527,7 @@ function buildImplementorArgv(agent, mode, workdir, options) {
31310
31527
  let sandbox = mode === "plan" ? "read-only" : "workspace-write", lastMessagePath = import_node_path.default.join(
31311
31528
  import_node_os.default.tmpdir(),
31312
31529
  `quorum-impl-codex-${process.pid}-${(0, import_uuid4.v4)()}.txt`
31313
- ), webEnabled = process.env.CODEVIBE_ENABLE_AGENT_WEB === "1" && !!options?.mcpConfigPath, mcpServer = webEnabled ? extractMcpServerConfig(options?.mcpConfigPath) : null;
31530
+ ), webEnabled = !!options?.mcpConfigPath, mcpServer = webEnabled ? extractMcpServerConfig(options?.mcpConfigPath) : null;
31314
31531
  return webEnabled && mcpServer ? {
31315
31532
  argv: [
31316
31533
  "codex",
@@ -31360,7 +31577,7 @@ function buildImplementorArgv(agent, mode, workdir, options) {
31360
31577
  throw new Error(
31361
31578
  "buildImplementorArgv: ANTIGRAVITY requires an absolute workdir (agy is workspace-centric \u2014 --add-dir is its only view of the tree); the call site must thread it (AGY-2.0 D3)"
31362
31579
  );
31363
- let webEnabled = process.env.CODEVIBE_ENABLE_AGENT_WEB === "1" && !!options?.mcpConfigPath, mcpServer = webEnabled ? extractMcpServerConfig(options?.mcpConfigPath) : null;
31580
+ let webEnabled = !!options?.mcpConfigPath, mcpServer = webEnabled ? extractMcpServerConfig(options?.mcpConfigPath) : null;
31364
31581
  return webEnabled && mcpServer && setupAgyWebPlugin(workdir, mcpServer), {
31365
31582
  argv: [...agyImplementorArgvPrefix(mode), workdir, "--print", ""],
31366
31583
  capture: { kind: "stdout" }
@@ -31709,8 +31926,8 @@ var HookBridge = class {
31709
31926
  // EXECUTOR_REFUSAL (Class A audit) and THEN LOCAL_AUTHORITY_REFUSAL (shell-
31710
31927
  // visible). Reverse order would race the user-visible message ahead of the
31711
31928
  // audit trail. Tests assert the ordering invariant.
31712
- async bridgeAuthorityRefusal(err, args) {
31713
- let ctx = this.contextOrNull();
31929
+ async bridgeAuthorityRefusal(err, args, taskCtx) {
31930
+ let ctx = taskCtx ?? this.contextOrNull();
31714
31931
  ctx !== null && await this.deps.emitter.emitExecutorRefusal(ctx, {
31715
31932
  refusal: err.refusal,
31716
31933
  refusedMessageId: args.refusedMessageId
@@ -32292,7 +32509,7 @@ var ClassBConsumer = class {
32292
32509
  }
32293
32510
  }
32294
32511
  async handlePolicyRejection(packet, envelopeTaskId) {
32295
- let payload = packet.payload, ctx = this.deps.getContext(), hostedRecovery = payload?.recommendedRecovery, recommendedRecovery = hostedRecovery === "reauthorize_locally" || hostedRecovery === "abort_task" || hostedRecovery === "retry_after_resync" ? hostedRecovery : "ask_user";
32512
+ let payload = packet.payload, ctx = envelopeTaskId ? { ...this.deps.getContext(), taskId: envelopeTaskId } : this.deps.getContext(), hostedRecovery = payload?.recommendedRecovery, recommendedRecovery = hostedRecovery === "reauthorize_locally" || hostedRecovery === "abort_task" || hostedRecovery === "retry_after_resync" ? hostedRecovery : "ask_user";
32296
32513
  await this.deps.emitter.emitExecutorRefusal(ctx, {
32297
32514
  refusal: {
32298
32515
  category: "policy_rejection",
@@ -38547,6 +38764,23 @@ var WorkspaceShadow = class _WorkspaceShadow {
38547
38764
  )
38548
38765
  );
38549
38766
  }
38767
+ /**
38768
+ * P46a C1 — the real tree's current content-hash + mode for a workspace-
38769
+ * relative path, read exactly as the promote compare-and-set reads it
38770
+ * (`readRealFileMetadata`; `{ hash: null, mode: null }` when absent). Used to
38771
+ * record a promoted path's post-image and to attribute a later collision.
38772
+ */
38773
+ async readRealMetadata(rel) {
38774
+ return assertSafeRelativePath(rel), assertNotInternalShadowPath(rel), readRealFileMetadata(this.workingDir, rel, this.workspaceRootAuthority);
38775
+ }
38776
+ /**
38777
+ * P46a C2 — the real tree's current text content for a workspace-relative
38778
+ * path (`null` when absent), for the retry brief's "files changed since your
38779
+ * first attempt" diff. Bounded by `MAX_REVIEW_FILE_BYTES`; never logged.
38780
+ */
38781
+ async readRealFileText(rel) {
38782
+ return assertSafeRelativePath(rel), assertNotInternalShadowPath(rel), (await readRealFileForManifest(this.workingDir, rel, this.workspaceRootAuthority)).content;
38783
+ }
38550
38784
  /** Read the crash-durable reviewed artifact, or null for an unreviewed snapshot. */
38551
38785
  async readReviewedDiff() {
38552
38786
  return (await this.readStateMarker())?.reviewedDiff?.map((file) => ({ ...file })) ?? null;
@@ -47457,7 +47691,15 @@ function makeNoopTrackHandle(agentKind) {
47457
47691
  }
47458
47692
  var LocalExecutorImpl = class {
47459
47693
  constructor(deps) {
47460
- this.taskId = null;
47694
+ /**
47695
+ * P46a B2 — PER-TASK authority scopes: each task's TaskAuthorized path scope
47696
+ * merged over the seeded scope (`mergeAuthorityScope`), keyed by task id. A
47697
+ * spawn reads its OWN task's scope, never a union; authority never
47698
+ * accumulates across tasks. Replaces the singleton `this.scope`/`this.taskId`
47699
+ * pair, which made the SECOND concurrent task's spawn run under the FIRST
47700
+ * task's authorized paths (and mis-attributed its audit context).
47701
+ */
47702
+ this.scopeByTask = /* @__PURE__ */ new Map();
47461
47703
  // CP-12 W2.b — per-track registry (idempotent N-track spawn) + a snapshot of
47462
47704
  // the most-recent TrackAssigned per track (for state-dir + scope on
47463
47705
  // MergeGate). EMPTY for single-track sessions (never touched).
@@ -47501,7 +47743,7 @@ var LocalExecutorImpl = class {
47501
47743
  // re-drain from a duplicate without relying on wall-clock resolution.
47502
47744
  this._lastMergeGateEmitMs = 0;
47503
47745
  this.memberReviewEvidenceByTask = /* @__PURE__ */ new Map();
47504
- this.sessionId = deps.sessionId, this.substrateEngager = deps.substrateEngager, this.strictBadgeSink = deps.strictBadgeSink, this.scope = deps.initialScope ?? makeEmptyScope(), this.baseCtx = deps.baseCtx, this.adapter = deps.adapter, this.emitShellEvent = deps.emitShellEvent, this.teamDeps = deps, this.onTeamMergeAuditSummary = deps.onTeamMergeAuditSummary, this.logger = deps.logger ?? { warn: (m, x) => console.warn(m, x) }, this.teamModeEnabled = !!(deps.spawnTrackImplementor && deps.appsyncClient && deps.repoRoot && deps.withSnapshotMergeGateFence), this.teamModeEnabled && typeof this.emitShellEvent == "function" && this.sessionId && this.replayPendingTeamDeliveries().catch((err) => {
47746
+ this.sessionId = deps.sessionId, this.substrateEngager = deps.substrateEngager, this.strictBadgeSink = deps.strictBadgeSink, this.baseScope = deps.initialScope ?? makeEmptyScope(), this.baseCtx = deps.baseCtx, this.adapter = deps.adapter, this.emitShellEvent = deps.emitShellEvent, this.teamDeps = deps, this.onTeamMergeAuditSummary = deps.onTeamMergeAuditSummary, this.logger = deps.logger ?? { warn: (m, x) => console.warn(m, x) }, this.teamModeEnabled = !!(deps.spawnTrackImplementor && deps.appsyncClient && deps.repoRoot && deps.withSnapshotMergeGateFence), this.teamModeEnabled && typeof this.emitShellEvent == "function" && this.sessionId && this.replayPendingTeamDeliveries().catch((err) => {
47505
47747
  this.logger.warn("[LocalExecutorImpl] startup background replay of pending team deliveries failed (non-fatal)", {
47506
47748
  err: err?.message
47507
47749
  });
@@ -47529,8 +47771,8 @@ var LocalExecutorImpl = class {
47529
47771
  )), this.bridge = new HookBridge({
47530
47772
  emitter: this.emitter,
47531
47773
  emitShellEvent: this.emitShellEvent,
47532
- getAuthorityScope: () => this.scope,
47533
- getCurrentTaskId: () => this.taskId,
47774
+ getAuthorityScope: () => this.baseScope,
47775
+ getCurrentTaskId: () => null,
47534
47776
  getContextWithoutTask: () => this.baseCtx,
47535
47777
  adapter: this.adapter
47536
47778
  }), this.classBConsumer = new ClassBConsumer({
@@ -47538,7 +47780,7 @@ var LocalExecutorImpl = class {
47538
47780
  emitShellEvent: this.emitShellEvent,
47539
47781
  getContext: () => this.contextOrFallback(),
47540
47782
  advanceAuthorityScope: (newScope, taskId) => {
47541
- this.scope = mergeAuthorityScope(this.scope, newScope), this.taskId = taskId;
47783
+ this.scopeByTask.set(taskId, mergeAuthorityScope(this.baseScope, newScope));
47542
47784
  },
47543
47785
  notifyPolicyRejection: (detail, category, recommendedRecovery, rejectedTaskId) => {
47544
47786
  this.emitShellEvent({
@@ -47630,19 +47872,20 @@ var LocalExecutorImpl = class {
47630
47872
  * is responsible for routing to `bridgeAuthorityErrorToShellRefusal` /
47631
47873
  * `HookBridge.bridgeAuthorityRefusal` per LOCK #C4-3.
47632
47874
  */
47633
- async enforceAuthority(action) {
47875
+ async enforceAuthority(action, taskId) {
47876
+ let scope = this.scopeFor(taskId);
47634
47877
  switch (action.kind) {
47635
47878
  case "Write":
47636
- await enforceWrite(this.scope, action.path);
47879
+ await enforceWrite(scope, action.path);
47637
47880
  return;
47638
47881
  case "Read":
47639
- await enforceRead(this.scope, action.path);
47882
+ await enforceRead(scope, action.path);
47640
47883
  return;
47641
47884
  case "Network":
47642
- enforceNetwork(this.scope);
47885
+ enforceNetwork(scope);
47643
47886
  return;
47644
47887
  case "Command":
47645
- enforceCommand(this.scope, action.argv);
47888
+ enforceCommand(scope, action.argv);
47646
47889
  return;
47647
47890
  default: {
47648
47891
  let _exhaustive = action;
@@ -47651,36 +47894,64 @@ var LocalExecutorImpl = class {
47651
47894
  }
47652
47895
  }
47653
47896
  /**
47654
- * Read-only view of the current authority scope per master §9:1286.
47897
+ * P46a B2 resolve the authority scope a caller runs under: the task's OWN
47898
+ * merged scope once ITS TaskAuthorized arrived, else the seeded scope (before
47899
+ * authorization, for a task this executor never saw authorized, and for a
47900
+ * task-less caller). Never a union across tasks.
47901
+ */
47902
+ scopeFor(taskId) {
47903
+ return (taskId ? this.scopeByTask.get(taskId) : void 0) ?? this.baseScope;
47904
+ }
47905
+ /**
47906
+ * Read-only view of the SEEDED authority scope per master §9:1286 (P46a B2:
47907
+ * `authorityScope()` keeps today's value at its only production caller — the
47908
+ * continuation writer's clamp, frozen at wiring — which IS the seeded scope).
47655
47909
  * Returns a deep-frozen copy so callers cannot mutate the LE's state.
47656
47910
  */
47657
47911
  authorityScope() {
47658
47912
  return Object.freeze({
47659
- writeScopes: [...this.scope.writeScopes],
47660
- readScopes: [...this.scope.readScopes],
47661
- networkAllowed: this.scope.networkAllowed,
47662
- commandAllowlist: [...this.scope.commandAllowlist],
47663
- expiresAt: this.scope.expiresAt
47913
+ writeScopes: [...this.baseScope.writeScopes],
47914
+ readScopes: [...this.baseScope.readScopes],
47915
+ networkAllowed: this.baseScope.networkAllowed,
47916
+ commandAllowlist: [...this.baseScope.commandAllowlist],
47917
+ expiresAt: this.baseScope.expiresAt
47918
+ });
47919
+ }
47920
+ /** P46a B2 (Stage 1 r1 F7) — drop a task's merged scope when the task ends. */
47921
+ forgetTaskScope(taskId) {
47922
+ this.scopeByTask.delete(taskId);
47923
+ }
47924
+ /** P46a B2 — read-only view of ONE task's merged scope (the seeded scope when unauthorized). */
47925
+ authorityScopeForTask(taskId) {
47926
+ let scope = this.scopeFor(taskId);
47927
+ return Object.freeze({
47928
+ writeScopes: [...scope.writeScopes],
47929
+ readScopes: [...scope.readScopes],
47930
+ networkAllowed: scope.networkAllowed,
47931
+ commandAllowlist: [...scope.commandAllowlist],
47932
+ expiresAt: scope.expiresAt
47664
47933
  });
47665
47934
  }
47666
47935
  // --- Authority surface (re-exports through this class for the bound scope) ---
47667
- enforceCommand(argv) {
47668
- enforceCommand(this.scope, argv);
47936
+ // P46a B2 — each takes an optional task id and resolves the seeded scope
47937
+ // without one (none has a production caller today).
47938
+ enforceCommand(argv, taskId) {
47939
+ enforceCommand(this.scopeFor(taskId), argv);
47669
47940
  }
47670
- enforceNetwork() {
47671
- enforceNetwork(this.scope);
47941
+ enforceNetwork(taskId) {
47942
+ enforceNetwork(this.scopeFor(taskId));
47672
47943
  }
47673
- async enforceWrite(p) {
47674
- return enforceWrite(this.scope, p);
47944
+ async enforceWrite(p, taskId) {
47945
+ return enforceWrite(this.scopeFor(taskId), p);
47675
47946
  }
47676
- async enforceRead(p) {
47677
- return enforceRead(this.scope, p);
47947
+ async enforceRead(p, taskId) {
47948
+ return enforceRead(this.scopeFor(taskId), p);
47678
47949
  }
47679
- async safeWriteFile(p, data) {
47680
- return safeWriteFile(this.scope, p, data);
47950
+ async safeWriteFile(p, data, taskId) {
47951
+ return safeWriteFile(this.scopeFor(taskId), p, data);
47681
47952
  }
47682
- async safeReadFile(p, opts) {
47683
- return safeReadFile(this.scope, p, opts);
47953
+ async safeReadFile(p, opts, taskId) {
47954
+ return safeReadFile(this.scopeFor(taskId), p, opts);
47684
47955
  }
47685
47956
  // --- Spawn surface (wires lifecycle hooks to the emitter) ---
47686
47957
  // Per master CP-1 §9:1281-1283 — all three spawn methods take full
@@ -47711,11 +47982,11 @@ var LocalExecutorImpl = class {
47711
47982
  signal: advisoryAdmission.controller.signal
47712
47983
  });
47713
47984
  try {
47714
- let {
47985
+ let spawnTaskScope = this.scopeFor(promptBoundArgs.taskId ?? null), {
47715
47986
  args: launchArgs,
47716
47987
  onExit,
47717
47988
  substrateEngaged
47718
- } = await this.engageSubstrateForSpawn(promptBoundArgs, this.scope), outerBoundaryConfines = substrateEngaged || isTrustedContainerBoundary(), finalArgs = {
47989
+ } = await this.engageSubstrateForSpawn(promptBoundArgs, spawnTaskScope), outerBoundaryConfines = substrateEngaged || isTrustedContainerBoundary(), finalArgs = {
47719
47990
  ...launchArgs,
47720
47991
  argv: applyOuterBoundarySandbox(
47721
47992
  relocateCodexLastMessageForSubstrate(
@@ -47726,7 +47997,7 @@ var LocalExecutorImpl = class {
47726
47997
  launchArgs.agentKind ?? this.adapter,
47727
47998
  outerBoundaryConfines
47728
47999
  )
47729
- }, spawnScope = outerBoundaryConfines ? withBoundaryConfinedCommandAuthority(this.scope) : this.scope, handle = await this.spawnWithLifecycle(
48000
+ }, spawnScope = outerBoundaryConfines ? withBoundaryConfinedCommandAuthority(spawnTaskScope) : spawnTaskScope, handle = await this.spawnWithLifecycle(
47730
48001
  finalArgs,
47731
48002
  (a) => spawnImplementor(spawnScope, a),
47732
48003
  onExit
@@ -47744,7 +48015,7 @@ var LocalExecutorImpl = class {
47744
48015
  async spawnHealthProbe(args) {
47745
48016
  if (this.workspaceShutdown)
47746
48017
  throw new Error("LocalExecutor session shutdown has fenced new health probes");
47747
- return this.spawnWithLifecycle(args, (a) => spawnHealthProbe(this.scope, a));
48018
+ return this.spawnWithLifecycle(args, (a) => spawnHealthProbe(this.baseScope, a));
47748
48019
  }
47749
48020
  /**
47750
48021
  * Publish advisory ownership synchronously before the first spawn await.
@@ -47847,7 +48118,7 @@ var LocalExecutorImpl = class {
47847
48118
  );
47848
48119
  return { args, substrateEngaged: !1 };
47849
48120
  }
47850
- let effectiveTaskId = args.taskId ?? this.taskId;
48121
+ let effectiveTaskId = args.taskId ?? null;
47851
48122
  if (effectiveTaskId === null) {
47852
48123
  if (args.requireConfined)
47853
48124
  throw new SpawnConfinementUnavailable(
@@ -47859,7 +48130,7 @@ var LocalExecutorImpl = class {
47859
48130
  "none"
47860
48131
  ), { args, substrateEngaged: !1 };
47861
48132
  }
47862
- let result = await this.substrateEngager({
48133
+ let isWebActive = args.webTurnActive === !0, result = await this.substrateEngager({
47863
48134
  taskId: effectiveTaskId,
47864
48135
  agentKind,
47865
48136
  workdir: args.workingDir,
@@ -47868,7 +48139,8 @@ var LocalExecutorImpl = class {
47868
48139
  // A1d (design §8.1) — thread the spawn's confinement demand so the engager
47869
48140
  // can build the WRITE-CONFINED resolver row for a broker-less agent
47870
48141
  // (GEMINI/ANTIGRAVITY) rather than immediately returning `reduced_trust`.
47871
- requireConfined: args.requireConfined === !0
48142
+ requireConfined: args.requireConfined === !0,
48143
+ webBrowsingActive: isWebActive
47872
48144
  });
47873
48145
  if (result.mode === "reduced_trust") {
47874
48146
  if (args.requireConfined)
@@ -47997,7 +48269,7 @@ var LocalExecutorImpl = class {
47997
48269
  }
47998
48270
  }
47999
48271
  async spawnWithLifecycle(args, spawnFn, onSubstrateExit) {
48000
- let role = args.role, lifecycleAudit = args.lifecycleAudit ?? "active_task", lifecycleTaskId = args.taskId ?? this.taskId, lifecycleCtx = lifecycleTaskId ? { taskId: lifecycleTaskId, ...this.baseCtx } : null, substrateCleanupError, substrateCleanupPromise, cleanupSubstrate = async () => {
48272
+ let role = args.role, lifecycleAudit = args.lifecycleAudit ?? "active_task", lifecycleTaskId = args.taskId ?? null, lifecycleCtx = lifecycleTaskId ? { taskId: lifecycleTaskId, ...this.baseCtx } : null, substrateCleanupError, substrateCleanupPromise, cleanupSubstrate = async () => {
48001
48273
  onSubstrateExit && (substrateCleanupPromise || (substrateCleanupPromise = onSubstrateExit().catch((error) => {
48002
48274
  throw substrateCleanupError = error, error;
48003
48275
  })), await substrateCleanupPromise);
@@ -48050,11 +48322,15 @@ var LocalExecutorImpl = class {
48050
48322
  ...err.refusal,
48051
48323
  detail: safeDetail
48052
48324
  });
48053
- await this.bridge.bridgeAuthorityRefusal(safeError, {
48054
- refusedMessageId: "spawn:" + role + ":" + Date.now(),
48055
- shellContent: `Refused spawn (${role}): ${safeDetail}`,
48056
- shellMetadata: { role, argv: safeArgv }
48057
- }), primary = safeError;
48325
+ await this.bridge.bridgeAuthorityRefusal(
48326
+ safeError,
48327
+ {
48328
+ refusedMessageId: "spawn:" + role + ":" + Date.now(),
48329
+ shellContent: `Refused spawn (${role}): ${safeDetail}`,
48330
+ shellMetadata: { role, argv: safeArgv }
48331
+ },
48332
+ lifecycleCtx
48333
+ ), primary = safeError;
48058
48334
  }
48059
48335
  throw substrateCleanupError !== void 0 ? Object.assign(
48060
48336
  new Error("implementor spawn failed and substrate cleanup also failed"),
@@ -49453,11 +49729,12 @@ var LocalExecutorImpl = class {
49453
49729
  ];
49454
49730
  return {
49455
49731
  writeScopes,
49456
- // Reads cover the shared workspace (current read scopes) + own writes.
49457
- readScopes: [...this.scope.readScopes, ...writeScopes],
49458
- networkAllowed: this.scope.networkAllowed,
49459
- commandAllowlist: [...this.scope.commandAllowlist],
49460
- expiresAt: this.scope.expiresAt
49732
+ // Reads cover the shared workspace (the seeded read scopes) + own writes.
49733
+ // P46a B2 — derived from the SEEDED scope, never another task's grants.
49734
+ readScopes: [...this.baseScope.readScopes, ...writeScopes],
49735
+ networkAllowed: this.baseScope.networkAllowed,
49736
+ commandAllowlist: [...this.baseScope.commandAllowlist],
49737
+ expiresAt: this.baseScope.expiresAt
49461
49738
  };
49462
49739
  }
49463
49740
  /**
@@ -49583,13 +49860,18 @@ var LocalExecutorImpl = class {
49583
49860
  }
49584
49861
  }
49585
49862
  // --- Test seams ------------------------------------------------------------
49586
- /** Set the active taskId (production: CP-1.e on TASK_AUTHORIZED). */
49587
- setActiveTaskId(taskId) {
49588
- this.taskId = taskId;
49589
- }
49590
- /** Replace the active authority scope (production: CP-1.e on TASK_AUTHORIZED). */
49863
+ /**
49864
+ * Replace the SEEDED authority scope (test seam; production seeds it at
49865
+ * construction via `initialScope`). P46a B2: per-task scopes are set only by
49866
+ * a TaskAuthorized (`advanceAuthorityScope`) — use `setAuthorityScopeForTask`
49867
+ * to simulate one in tests.
49868
+ */
49591
49869
  setAuthorityScope(scope) {
49592
- this.scope = scope;
49870
+ this.baseScope = scope;
49871
+ }
49872
+ /** P46a B2 test seam — install ONE task's merged scope as a TaskAuthorized would. */
49873
+ setAuthorityScopeForTask(taskId, scope) {
49874
+ this.scopeByTask.set(taskId, mergeAuthorityScope(this.baseScope, scope));
49593
49875
  }
49594
49876
  /** Expose emitter for §8.5 integration test introspection. */
49595
49877
  get emitterForTests() {
@@ -49607,21 +49889,18 @@ var LocalExecutorImpl = class {
49607
49889
  get trackHeartbeatCountForTests() {
49608
49890
  return this.trackHeartbeats.size;
49609
49891
  }
49610
- contextOrNull() {
49611
- return this.taskId === null ? null : { taskId: this.taskId, ...this.baseCtx };
49612
- }
49613
49892
  /**
49614
49893
  * Context resolver for ClassBConsumer emit paths (refusals + PolicyRejection).
49615
- * Class B inbound packets MAY arrive before a TaskAuthorized has set
49616
- * `this.taskId` (in fact, TaskAuthorized is the variant that SETS it). For
49617
- * the signature_invalid / ulid_replay / policy_rejection / malformed
49618
- * refusal-audit emit path we still need a valid envelope context, so we
49619
- * fall back to `pending` when no task is active. CP-2 may refine this; for
49620
- * CP-1.e the audit emit always succeeds with a deterministic placeholder.
49894
+ * Class B inbound packets carry no task the executor could attribute a
49895
+ * refusal to before they are verified (signature_invalid / ulid_replay /
49896
+ * policy_rejection / malformed), and P46a B2 removed the singleton task id,
49897
+ * so the refusal-audit emit uses the deterministic `pending` placeholder
49898
+ * never another task's id. Spawn-attributed emits resolve their task from
49899
+ * the per-spawn `args.taskId` (`lifecycleCtx`).
49621
49900
  */
49622
49901
  contextOrFallback() {
49623
49902
  return {
49624
- taskId: this.taskId ?? "pending",
49903
+ taskId: "pending",
49625
49904
  ...this.baseCtx
49626
49905
  };
49627
49906
  }
@@ -52023,6 +52302,97 @@ function resolveTeamExecution(_override, _env = process.env) {
52023
52302
  // src/orchestration-shell/quorum-loop.ts
52024
52303
  init_env_scrub();
52025
52304
 
52305
+ // src/credential-broker/scrubber.ts
52306
+ var REDACTION = "[REDACTED-CP7]", KEY_REDACTION = "[REDACTED-CP7-KEY]", SECRET_PATTERNS = [
52307
+ {
52308
+ // PEM private-key block (RSA / EC / OPENSSH / generic PRIVATE KEY).
52309
+ patternClass: "private_key_pem",
52310
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY-----/g
52311
+ },
52312
+ {
52313
+ // Anthropic API key: sk-ant-... .
52314
+ patternClass: "anthropic_api_key",
52315
+ regex: /sk-ant-[A-Za-z0-9_-]{20,}/g
52316
+ },
52317
+ {
52318
+ // OpenAI API key: sk-... or sk-proj-... (>= 20 trailing chars).
52319
+ patternClass: "openai_api_key",
52320
+ regex: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g
52321
+ },
52322
+ {
52323
+ // AWS access key id (AKIA / ASIA + 16 uppercase alphanumerics).
52324
+ patternClass: "aws_access_key_id",
52325
+ regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g
52326
+ },
52327
+ {
52328
+ // AWS secret access key VALUE assignment — a 40-char base64-ish secret
52329
+ // bound to an `aws_secret_access_key`/`AWS_SECRET_ACCESS_KEY` key. Only
52330
+ // the secret value is redacted, not the assignment label.
52331
+ patternClass: "aws_secret_access_key",
52332
+ regex: /(aws_secret_access_key|AWS_SECRET_ACCESS_KEY)(\s*[=:]\s*["']?)([A-Za-z0-9/+]{40})(["']?)/g
52333
+ },
52334
+ {
52335
+ // GitHub tokens (ghp_/gho_/ghu_/ghs_/ghr_ + 36 chars).
52336
+ patternClass: "github_token",
52337
+ regex: /\bgh[pousr]_[A-Za-z0-9]{36}\b/g
52338
+ },
52339
+ {
52340
+ // Authorization: Bearer <token> embedded in content (a bearer token
52341
+ // riding in a model-bound field). Redacts the token, keeps the scheme.
52342
+ patternClass: "bearer_token",
52343
+ regex: /(Bearer\s+)([A-Za-z0-9._~+/=-]{16,})/g
52344
+ }
52345
+ ];
52346
+ function scrubString(value) {
52347
+ return redactSecretShapes(value, REDACTION);
52348
+ }
52349
+ function redactSecretShapes(value, placeholder) {
52350
+ let redacted = value, classes = [];
52351
+ for (let pattern of SECRET_PATTERNS)
52352
+ pattern.regex.lastIndex = 0, pattern.regex.test(redacted) && (pattern.regex.lastIndex = 0, pattern.patternClass === "aws_secret_access_key" ? redacted = redacted.replace(pattern.regex, `$1$2${placeholder}$4`) : pattern.patternClass === "bearer_token" ? redacted = redacted.replace(pattern.regex, `$1${placeholder}`) : redacted = redacted.replace(pattern.regex, placeholder), classes.push(pattern.patternClass));
52353
+ return { redacted, classes };
52354
+ }
52355
+ function redactSecretShapesInText(value, placeholder = "[redacted secret]") {
52356
+ return redactSecretShapes(value, placeholder).redacted;
52357
+ }
52358
+ function keyIsSecret(key) {
52359
+ for (let pattern of SECRET_PATTERNS) {
52360
+ pattern.regex.lastIndex = 0;
52361
+ let hit = pattern.regex.test(key);
52362
+ if (pattern.regex.lastIndex = 0, hit) return !0;
52363
+ }
52364
+ return !1;
52365
+ }
52366
+ function joinPath(base, key) {
52367
+ return typeof key == "number" ? `${base}[${key}]` : /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ? `${base}.${key}` : `${base}['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
52368
+ }
52369
+ function scrubValue(value, path75, findings) {
52370
+ if (typeof value == "string") {
52371
+ let { redacted, classes } = scrubString(value);
52372
+ for (let patternClass of classes)
52373
+ findings.push({ field: path75, patternClass });
52374
+ return redacted;
52375
+ }
52376
+ if (Array.isArray(value))
52377
+ return value.map((item, i) => scrubValue(item, joinPath(path75, i), findings));
52378
+ if (value !== null && typeof value == "object") {
52379
+ let out = /* @__PURE__ */ Object.create(null), redactedKeyCount = 0;
52380
+ for (let [k, v] of Object.entries(value))
52381
+ if (keyIsSecret(k)) {
52382
+ redactedKeyCount += 1;
52383
+ let placeholder = `${KEY_REDACTION}-${redactedKeyCount}`, placeholderPath = joinPath(path75, placeholder);
52384
+ findings.push({ field: placeholderPath, patternClass: "secret_object_key" }), out[placeholder] = scrubValue(v, placeholderPath, findings);
52385
+ } else
52386
+ out[k] = scrubValue(v, joinPath(path75, k), findings);
52387
+ return out;
52388
+ }
52389
+ return value;
52390
+ }
52391
+ function scrubRequestBody(body) {
52392
+ let findings = [];
52393
+ return { scrubbed: scrubValue(body, "$", findings), findings };
52394
+ }
52395
+
52026
52396
  // src/orchestration-shell/context-items.ts
52027
52397
  var fs39 = __toESM(require("fs/promises")), import_fs2 = require("fs"), path57 = __toESM(require("path")), os32 = __toESM(require("os")), crypto27 = __toESM(require("crypto")), import_ulid3 = require("ulid"), import_uuid7 = require("uuid");
52028
52398
  init_logger2();
@@ -53898,97 +54268,6 @@ function renderRepoSliceCompact(repos, maxChars) {
53898
54268
  var fs40 = __toESM(require("fs/promises")), path58 = __toESM(require("path"));
53899
54269
  init_logger2();
53900
54270
 
53901
- // src/credential-broker/scrubber.ts
53902
- var REDACTION = "[REDACTED-CP7]", KEY_REDACTION = "[REDACTED-CP7-KEY]", SECRET_PATTERNS = [
53903
- {
53904
- // PEM private-key block (RSA / EC / OPENSSH / generic PRIVATE KEY).
53905
- patternClass: "private_key_pem",
53906
- regex: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY-----/g
53907
- },
53908
- {
53909
- // Anthropic API key: sk-ant-... .
53910
- patternClass: "anthropic_api_key",
53911
- regex: /sk-ant-[A-Za-z0-9_-]{20,}/g
53912
- },
53913
- {
53914
- // OpenAI API key: sk-... or sk-proj-... (>= 20 trailing chars).
53915
- patternClass: "openai_api_key",
53916
- regex: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g
53917
- },
53918
- {
53919
- // AWS access key id (AKIA / ASIA + 16 uppercase alphanumerics).
53920
- patternClass: "aws_access_key_id",
53921
- regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g
53922
- },
53923
- {
53924
- // AWS secret access key VALUE assignment — a 40-char base64-ish secret
53925
- // bound to an `aws_secret_access_key`/`AWS_SECRET_ACCESS_KEY` key. Only
53926
- // the secret value is redacted, not the assignment label.
53927
- patternClass: "aws_secret_access_key",
53928
- regex: /(aws_secret_access_key|AWS_SECRET_ACCESS_KEY)(\s*[=:]\s*["']?)([A-Za-z0-9/+]{40})(["']?)/g
53929
- },
53930
- {
53931
- // GitHub tokens (ghp_/gho_/ghu_/ghs_/ghr_ + 36 chars).
53932
- patternClass: "github_token",
53933
- regex: /\bgh[pousr]_[A-Za-z0-9]{36}\b/g
53934
- },
53935
- {
53936
- // Authorization: Bearer <token> embedded in content (a bearer token
53937
- // riding in a model-bound field). Redacts the token, keeps the scheme.
53938
- patternClass: "bearer_token",
53939
- regex: /(Bearer\s+)([A-Za-z0-9._~+/=-]{16,})/g
53940
- }
53941
- ];
53942
- function scrubString(value) {
53943
- return redactSecretShapes(value, REDACTION);
53944
- }
53945
- function redactSecretShapes(value, placeholder) {
53946
- let redacted = value, classes = [];
53947
- for (let pattern of SECRET_PATTERNS)
53948
- pattern.regex.lastIndex = 0, pattern.regex.test(redacted) && (pattern.regex.lastIndex = 0, pattern.patternClass === "aws_secret_access_key" ? redacted = redacted.replace(pattern.regex, `$1$2${placeholder}$4`) : pattern.patternClass === "bearer_token" ? redacted = redacted.replace(pattern.regex, `$1${placeholder}`) : redacted = redacted.replace(pattern.regex, placeholder), classes.push(pattern.patternClass));
53949
- return { redacted, classes };
53950
- }
53951
- function redactSecretShapesInText(value, placeholder = "[redacted secret]") {
53952
- return redactSecretShapes(value, placeholder).redacted;
53953
- }
53954
- function keyIsSecret(key) {
53955
- for (let pattern of SECRET_PATTERNS) {
53956
- pattern.regex.lastIndex = 0;
53957
- let hit = pattern.regex.test(key);
53958
- if (pattern.regex.lastIndex = 0, hit) return !0;
53959
- }
53960
- return !1;
53961
- }
53962
- function joinPath(base, key) {
53963
- return typeof key == "number" ? `${base}[${key}]` : /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ? `${base}.${key}` : `${base}['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
53964
- }
53965
- function scrubValue(value, path75, findings) {
53966
- if (typeof value == "string") {
53967
- let { redacted, classes } = scrubString(value);
53968
- for (let patternClass of classes)
53969
- findings.push({ field: path75, patternClass });
53970
- return redacted;
53971
- }
53972
- if (Array.isArray(value))
53973
- return value.map((item, i) => scrubValue(item, joinPath(path75, i), findings));
53974
- if (value !== null && typeof value == "object") {
53975
- let out = /* @__PURE__ */ Object.create(null), redactedKeyCount = 0;
53976
- for (let [k, v] of Object.entries(value))
53977
- if (keyIsSecret(k)) {
53978
- redactedKeyCount += 1;
53979
- let placeholder = `${KEY_REDACTION}-${redactedKeyCount}`, placeholderPath = joinPath(path75, placeholder);
53980
- findings.push({ field: placeholderPath, patternClass: "secret_object_key" }), out[placeholder] = scrubValue(v, placeholderPath, findings);
53981
- } else
53982
- out[k] = scrubValue(v, joinPath(path75, k), findings);
53983
- return out;
53984
- }
53985
- return value;
53986
- }
53987
- function scrubRequestBody(body) {
53988
- let findings = [];
53989
- return { scrubbed: scrubValue(body, "$", findings), findings };
53990
- }
53991
-
53992
54271
  // src/orchestration-shell/user-rules.ts
53993
54272
  var USER_RULE_ACTION = "user_rule", USER_RULE_RETIRED_ACTION = "user_rule_retired", USER_WORDS_VISIBLE_TO = ["planner", "brainstorm", "implementor"], USER_RULE_MAX_CHARS = 600, WORKFLOW_HANDOFF_SECTION_LABEL = "Workflow handoff context:", HANDOFF_SECTION_START = new RegExp(
53994
54273
  `(?:^|\\n)${WORKFLOW_HANDOFF_SECTION_LABEL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\nSource: `
@@ -56936,10 +57215,74 @@ function teamRound0GateId(taskId, taskGroupId, trackIndex, dispatchGeneration) {
56936
57215
  TEAM_ROUND0_GATE_NAMESPACE
56937
57216
  );
56938
57217
  }
57218
+ var RETRY_BRIEF_SECTION_CAP = 8 * 1024, RETRY_BRIEF_TOTAL_CAP = 64 * 1024, UNIFIED_DIFF_MAX_CELLS = 4e6;
57219
+ function capText2(text2, cap) {
57220
+ if (Buffer.byteLength(text2, "utf8") <= cap) return text2;
57221
+ let marker = `
57222
+ \u2026 [truncated]`, cut = text2;
57223
+ for (; Buffer.byteLength(cut, "utf8") + Buffer.byteLength(marker, "utf8") > cap && cut.length > 0; )
57224
+ cut = cut.slice(0, Math.max(0, Math.floor(cut.length * 0.9) - 1));
57225
+ return cut + marker;
57226
+ }
57227
+ function unifiedDiff(fromLabel, toLabel, fromText, toText) {
57228
+ let a = fromText.length === 0 ? [] : fromText.split(`
57229
+ `), b = toText.length === 0 ? [] : toText.split(`
57230
+ `), ops = [];
57231
+ if (a.length * b.length > UNIFIED_DIFF_MAX_CELLS) {
57232
+ for (let i = 0; i < a.length; i++) ops.push({ kind: "-", line: a[i], ai: i, bi: 0 });
57233
+ for (let j = 0; j < b.length; j++) ops.push({ kind: "+", line: b[j], ai: a.length, bi: j });
57234
+ } else {
57235
+ let rows = a.length + 1, cols = b.length + 1, table = new Uint32Array(rows * cols);
57236
+ for (let i2 = a.length - 1; i2 >= 0; i2--)
57237
+ for (let j2 = b.length - 1; j2 >= 0; j2--)
57238
+ table[i2 * cols + j2] = a[i2] === b[j2] ? table[(i2 + 1) * cols + j2 + 1] + 1 : Math.max(table[(i2 + 1) * cols + j2], table[i2 * cols + j2 + 1]);
57239
+ let i = 0, j = 0;
57240
+ for (; i < a.length && j < b.length; )
57241
+ a[i] === b[j] ? (ops.push({ kind: " ", line: a[i], ai: i, bi: j }), i++, j++) : table[(i + 1) * cols + j] >= table[i * cols + j + 1] ? (ops.push({ kind: "-", line: a[i], ai: i, bi: j }), i++) : (ops.push({ kind: "+", line: b[j], ai: i, bi: j }), j++);
57242
+ for (; i < a.length; ) ops.push({ kind: "-", line: a[i], ai: i, bi: j }), i++;
57243
+ for (; j < b.length; ) ops.push({ kind: "+", line: b[j], ai: i, bi: j }), j++;
57244
+ }
57245
+ let out = [`--- ${fromLabel}`, `+++ ${toLabel}`], CONTEXT = 3, k = 0;
57246
+ for (; k < ops.length; ) {
57247
+ if (ops[k].kind === " ") {
57248
+ k++;
57249
+ continue;
57250
+ }
57251
+ let start = Math.max(0, k - CONTEXT), end = k, lastChange = k;
57252
+ for (; end < ops.length && (ops[end].kind !== " " && (lastChange = end), !(end - lastChange > 2 * CONTEXT)); )
57253
+ end++;
57254
+ end = Math.min(ops.length, lastChange + CONTEXT + 1);
57255
+ let slice = ops.slice(start, end), fromStart = (slice.find((o) => o.kind !== "+")?.ai ?? ops[start].ai) + 1, toStart = (slice.find((o) => o.kind !== "-")?.bi ?? ops[start].bi) + 1, fromCount = slice.filter((o) => o.kind !== "+").length, toCount = slice.filter((o) => o.kind !== "-").length;
57256
+ out.push(`@@ -${fromStart},${fromCount} +${toStart},${toCount} @@`);
57257
+ for (let o of slice) out.push(`${o.kind}${o.line}`);
57258
+ k = end, start = end;
57259
+ }
57260
+ return out.length === 2 && out.push("(no textual difference)"), out.join(`
57261
+ `);
57262
+ }
56939
57263
  var QuorumLoop = class _QuorumLoop {
56940
57264
  constructor(deps) {
56941
- /** The active task this desktop is driving (set at start_task). */
56942
- this.activeTaskId = null;
57265
+ /**
57266
+ * P46a B1 — the FOCUSED single task: the task the UI means when a command
57267
+ * names none (`/continue`, `/review-reset`, the status "starting" row).
57268
+ * Default = the most recently CONFIRMED START; `/task focus <n>` sets it
57269
+ * explicitly; it advances to the newest remaining single task when the
57270
+ * focused task ends; a rejected START never moves it. It replaces the three
57271
+ * P45 singletons (`activeTaskId` / `activeBrief` / `activeImplementorAgent`):
57272
+ * every reader now resolves through the task's OWN record
57273
+ * (`singleTaskContextByTask`), never through "the latest task".
57274
+ */
57275
+ this.focusedTaskId = null;
57276
+ /** P46a B1 — confirmed single tasks in START-confirmation order (focus fallback). */
57277
+ this.confirmedSingleOrder = [];
57278
+ /**
57279
+ * P46a B1 — the most recent START still in flight (or null). A legacy
57280
+ * GATE_DISPATCH with an EMPTY envelope task id can only belong to it (the P45
57281
+ * rule); with no START in flight such a packet is a no-op left to recovery.
57282
+ */
57283
+ this.latestInFlightStart = null;
57284
+ /** Stage 1 r1 F13 — every START still in flight, in issue order; `latestInFlightStart` is its last entry. */
57285
+ this.startOrderInFlight = [];
56943
57286
  /** Tasks whose next automatic revise round must start a fresh review baseline. */
56944
57287
  this.reviewScopeResetTasks = /* @__PURE__ */ new Set();
56945
57288
  /** Non-terminal tasks for which `/review-reset` may arm the next revise round. */
@@ -56963,16 +57306,9 @@ var QuorumLoop = class _QuorumLoop {
56963
57306
  // only per QuorumLoop life; a `0`/absent entry omits the ` · ↓ <tokens>` segment.
56964
57307
  this.tokensByTaskId = /* @__PURE__ */ new Map();
56965
57308
  this.tokensCountedGates = /* @__PURE__ */ new Set();
56966
- /**
56967
- * The implementor brief for the active task (set at start_task). The
56968
- * GATE_DISPATCH consumer reads this to drive the round-0 implementor spawn —
56969
- * the brief is no longer threaded inline through the spawn call (the spawn
56970
- * moved out of `startTask` into the GATE_DISPATCH consumer, §3.C.20 FIX).
56971
- */
56972
- this.activeBrief = null;
56973
57309
  /**
56974
57310
  * IMAGE-ATTACHMENT-DESIGN.md §5 — TASK-SCOPED image attachments (parallel to
56975
- * `activeBrief`/`activeBriefByTask`). The single-impl path uses `activeAttachments`;
57311
+ * `singleTaskContextByTask`/`activeBriefByTask`). The single-impl path uses `activeAttachments`;
56976
57312
  * the team path keys by the child `taskId` in `activeAttachmentsByTask` (armed at
56977
57313
  * `registerTeamTaskBrief`, before its buffered GATE_DISPATCH drains). Re-copied
56978
57314
  * into EACH round's shadow at `runImplementorRound` (round-0, revise, continuation,
@@ -57027,19 +57363,44 @@ var QuorumLoop = class _QuorumLoop {
57027
57363
  this.recoveredBriefsByAuthority = /* @__PURE__ */ new Map();
57028
57364
  this.activeBriefByTask = /* @__PURE__ */ new Map();
57029
57365
  /**
57030
- * P45 D1 (M7 E2E step 8, 2026-09-14) — the brief + agent of EVERY single-
57031
- * implementor task this loop started, keyed by task id. `activeTaskId` /
57032
- * `activeBrief` / `activeImplementorAgent` are the LATEST task only: when a
57033
- * second single task started while the first still ran (the planner routed a
57034
- * mid-task turn as a task), the first task's revise round was composed from
57035
- * the second task's request (`buildReviseBrief` fell back to `activeBrief`)
57036
- * and its round-0 dispatch whose envelope names the FIRST task — would have
57037
- * been mis-read as a team dispatch. Round-0 spawns, revise rounds and the
57038
- * continuation context resolve through this map first; the singletons stay
57039
- * as the legacy fallback (an empty envelope task id, the test seam). Entries
57040
- * are removed with the task's other per-task state at its terminal cleanup.
57366
+ * P45 D1 (M7 E2E step 8, 2026-09-14) / P46a B1 — the brief + agent of EVERY
57367
+ * LIVE single-implementor task this loop started, keyed by task id. P45 added
57368
+ * it beside the three "latest task" singletons (a second single task's
57369
+ * request had replaced the first task's in its revise round); P46a deletes
57370
+ * the singletons round-0 spawns, revise rounds, the continuation context and
57371
+ * every other reader resolve ONLY through this map. An entry exists from the
57372
+ * START call (in flight) until the task's terminal cleanup; `retryOf` links a
57373
+ * `#n retry` task to its `apply_failed` original (P46a C2/C3).
57041
57374
  */
57042
57375
  this.singleTaskContextByTask = /* @__PURE__ */ new Map();
57376
+ /** Agent web browsing (upstream 2.0.37) — whether a task's ORIGINAL request needs web access; ends with the task (`forgetSingleTask`, team cleanup). */
57377
+ this.webAccessByTask = /* @__PURE__ */ new Map();
57378
+ /**
57379
+ * P46a D2 — append-only per-loop-life record of every single task this loop
57380
+ * started (confirmed or in flight) with its terminal outcome once known, for
57381
+ * `/status` (`describeTasks`). Never deleted (bounded by the session's tasks).
57382
+ */
57383
+ this.singleTaskHistory = /* @__PURE__ */ new Map();
57384
+ /**
57385
+ * P46a C1 — every single-task promote THIS session landed, by workspace-
57386
+ * relative path: the promoting task + the post-image hash/mode the promote
57387
+ * compare-and-set reads (`readRealMetadata`; `null` hash = a deletion). Used
57388
+ * to attribute a later collision to a `session-task` (the path's current
57389
+ * metadata equals a recorded post-image) or `external` (anything else —
57390
+ * including a session-promoted file the user edited afterwards, and every
57391
+ * promote by another session). In-memory, fail-safe: empty after a desktop
57392
+ * restart, so every collision then classifies as `external`.
57393
+ */
57394
+ this.promotedByPath = /* @__PURE__ */ new Map();
57395
+ /** P46a C2 — originals whose ONE automatic retry has been issued (never a second). */
57396
+ this.autoRetryIssuedFor = /* @__PURE__ */ new Set();
57397
+ /**
57398
+ * P46a C2 — retry STARTs planned inside the promote critical section and
57399
+ * issued only after it (and the durable `apply_failed` record) completed:
57400
+ * `startTask` runs the round-0 spawn before returning, so it must never run
57401
+ * inside `promoteShadowLocked`.
57402
+ */
57403
+ this.pendingPromoteRetries = [];
57043
57404
  /**
57044
57405
  * CP-1.f revise-context fix (dogfood task 2ac68708, 2026-06-11) — the BINDING
57045
57406
  * user-requested changes for a task, accumulated across ALL revise rounds.
@@ -57502,8 +57863,6 @@ var QuorumLoop = class _QuorumLoop {
57502
57863
  this.lastWorkspaceOutcome = null;
57503
57864
  // ─── §3.C.18 — real Ed25519 signature verify (cached key) ───────────────
57504
57865
  this.cachedSigningKey = null;
57505
- /** The implementor agent the loop spawns (set at start_task). */
57506
- this.activeImplementorAgent = "CLAUDE";
57507
57866
  this.deps = deps, this.sleep = deps.sleep ?? realSleep2, this.loadCompletedSummariesFromDisk();
57508
57867
  }
57509
57868
  getCompletedSummariesPath() {
@@ -58092,13 +58451,95 @@ var QuorumLoop = class _QuorumLoop {
58092
58451
  isTaskTerminallyRetired(taskId) {
58093
58452
  return this.isWorkspaceTaskTerminallyRetired(taskId);
58094
58453
  }
58095
- /** The current active task id (for the recovery poll). */
58454
+ /**
58455
+ * P46a B1 — the FOCUSED single task (null when no single task is live). The
58456
+ * P45 `activeTask` name is kept for the shell's readers; it now means the
58457
+ * focused task, never "the latest task".
58458
+ */
58096
58459
  get activeTask() {
58097
- return this.activeTaskId;
58460
+ return this.focusedTaskId;
58461
+ }
58462
+ /** P46a B1 — every LIVE single task this loop started (in flight or confirmed). */
58463
+ knownSingleTasks() {
58464
+ return [...this.singleTaskContextByTask.keys()];
58465
+ }
58466
+ /**
58467
+ * P46a B1 — `/task focus <n>`: make `taskId` the focused task. Refused (false)
58468
+ * for a task this loop does not know as a live single task.
58469
+ */
58470
+ setFocusedTask(taskId) {
58471
+ return this.singleTaskContextByTask.has(taskId) ? (this.focusedTaskId = taskId, !0) : !1;
58472
+ }
58473
+ /** P46a D1 — install the store-owned ordinal sink (the shell, after construction). */
58474
+ setTaskOrdinalSink(sink) {
58475
+ this.taskOrdinalSink = sink;
58476
+ }
58477
+ /** P46a C3 — install the desktop-local apply-conflict menu sink. */
58478
+ setApplyConflictSink(sink) {
58479
+ this.applyConflictSink = sink;
58480
+ }
58481
+ /** P46a C2 — install the retry-started notifier (the shell records the lifecycle row). */
58482
+ setRetryStartedSink(sink) {
58483
+ this.retryStartedSink = sink;
58098
58484
  }
58099
- /** Arm an explicit, one-shot comprehensive baseline for the active task. */
58485
+ /** P46a D1 `#n` / `#n retry` / short id for any task id (never throws). */
58486
+ labelFor(taskId) {
58487
+ try {
58488
+ let label = this.taskOrdinalSink?.label(taskId);
58489
+ if (label) return label;
58490
+ } catch {
58491
+ }
58492
+ return taskId.length > 8 ? taskId.slice(0, 8) : taskId;
58493
+ }
58494
+ /**
58495
+ * P46a D2 — every single task this loop started, for `/status`: its label,
58496
+ * agent, request (desktop-local plaintext — the user's own words), link to an
58497
+ * `apply_failed` original, loop state and whether it is the focused task.
58498
+ */
58499
+ describeTasks() {
58500
+ let rows = [];
58501
+ for (let [taskId, rec] of this.singleTaskHistory)
58502
+ rows.push({
58503
+ taskId,
58504
+ label: this.labelFor(taskId),
58505
+ agent: rec.agent,
58506
+ // Stage 1 r3 F1 — a retry's excerpt is the user's own request, not its composed brief.
58507
+ brief: rec.request ?? rec.brief,
58508
+ ...rec.retryOf ? { retryOf: rec.retryOf } : {},
58509
+ outcome: rec.outcome,
58510
+ implementorActive: this.activeImplementorByTask.has(taskId),
58511
+ focused: taskId === this.focusedTaskId
58512
+ });
58513
+ return rows;
58514
+ }
58515
+ /** Stage 1 r1 F13 — a START confirmed or rejected: the newest START still in flight (if any) becomes the empty-envelope target. */
58516
+ dropInFlightStart(taskId) {
58517
+ let at = this.startOrderInFlight.indexOf(taskId);
58518
+ at >= 0 && this.startOrderInFlight.splice(at, 1), this.latestInFlightStart = this.startOrderInFlight.length > 0 ? this.startOrderInFlight[this.startOrderInFlight.length - 1] : null;
58519
+ }
58520
+ /**
58521
+ * P46a B1 — a single task ended (promote, terminal discard, rejected START):
58522
+ * drop its live record and, when it was the focused task, advance the focus
58523
+ * to the newest remaining confirmed single task (or none).
58524
+ */
58525
+ forgetSingleTask(taskId, outcome) {
58526
+ this.singleTaskContextByTask.delete(taskId), this.webAccessByTask.delete(taskId), this.deps.localExecutor.forgetTaskScope?.(taskId);
58527
+ let history = this.singleTaskHistory.get(taskId);
58528
+ history && history.outcome === "running" && (history.outcome = outcome);
58529
+ let at = this.confirmedSingleOrder.indexOf(taskId);
58530
+ at >= 0 && this.confirmedSingleOrder.splice(at, 1), this.focusedTaskId === taskId && (this.focusedTaskId = this.confirmedSingleOrder.length > 0 ? this.confirmedSingleOrder[this.confirmedSingleOrder.length - 1] : null);
58531
+ }
58532
+ /**
58533
+ * Arm an explicit, one-shot comprehensive baseline for the FOCUSED task
58534
+ * (P46a B1). Refused with the candidate list when the focused task has no
58535
+ * eligible gate (or no single task is focused) — the shell renders the
58536
+ * `#n` candidates so the user can `/task focus <n>` first.
58537
+ */
58100
58538
  requestReviewScopeReset() {
58101
- return !this.activeTaskId || !this.reviewScopeResetEligibleTasks.has(this.activeTaskId) ? !1 : (this.reviewScopeResetTasks.add(this.activeTaskId), !0);
58539
+ let focused = this.focusedTaskId;
58540
+ return focused && this.reviewScopeResetEligibleTasks.has(focused) ? (this.reviewScopeResetTasks.add(focused), { armed: !0, taskId: focused }) : { armed: !1, candidates: [...this.singleTaskContextByTask.keys()].filter(
58541
+ (id) => id !== focused && this.reviewScopeResetEligibleTasks.has(id)
58542
+ ) };
58102
58543
  }
58103
58544
  /**
58104
58545
  * Audited-path fix (Fix 5) — the host's detected implementor agents
@@ -58280,8 +58721,7 @@ var QuorumLoop = class _QuorumLoop {
58280
58721
  return logger.warn("[QuorumLoop] startTask aborted \u2014 no session key", {
58281
58722
  sessionId: this.deps.session.sessionId
58282
58723
  }), null;
58283
- let previousSingle = this.activeTaskId !== null ? this.singleTaskContextByTask.get(this.activeTaskId) : void 0, previousActive = this.activeTaskId !== null && previousSingle ? { taskId: this.activeTaskId, brief: previousSingle.brief, agent: previousSingle.agent } : null;
58284
- this.activeTaskId = taskId, this.activeBrief = args.brief, appendContextItem(this.deps.session.sessionId, {
58724
+ appendContextItem(this.deps.session.sessionId, {
58285
58725
  kind: "task_spec",
58286
58726
  author: { role: "engine" },
58287
58727
  sensitivity: "user",
@@ -58289,7 +58729,14 @@ var QuorumLoop = class _QuorumLoop {
58289
58729
  task_id: taskId,
58290
58730
  body: { text: args.brief }
58291
58731
  }).catch(() => {
58292
- }), this.activeAttachments = args.attachments ?? [], this.activeAttachmentsByTask.set(taskId, this.activeAttachments), this.activeImplementorAgent = this.resolveForcedImplementorAgent() ?? args.agent, this.singleTaskContextByTask.set(taskId, { brief: args.brief, agent: this.activeImplementorAgent }), this.startTasksInFlight.add(taskId), this.deps.armLeTaskId?.(taskId);
58732
+ }), this.activeAttachments = args.attachments ?? [], this.activeAttachmentsByTask.set(taskId, this.activeAttachments);
58733
+ let implementorAgent = this.resolveForcedImplementorAgent() ?? args.agent, record = {
58734
+ brief: args.brief,
58735
+ agent: implementorAgent,
58736
+ ...args.retryOf ? { retryOf: args.retryOf } : {},
58737
+ ...args.request !== void 0 ? { request: args.request } : {}
58738
+ };
58739
+ this.singleTaskContextByTask.set(taskId, record), this.singleTaskHistory.set(taskId, { ...record, outcome: "running" }), this.startTasksInFlight.add(taskId), this.startOrderInFlight.push(taskId), this.latestInFlightStart = taskId;
58293
58740
  let workflowState;
58294
58741
  try {
58295
58742
  workflowState = (await this.deps.appsyncClient.startTask({
@@ -58302,31 +58749,40 @@ var QuorumLoop = class _QuorumLoop {
58302
58749
  // an empty `[]` (host detected nothing) is sent verbatim →
58303
58750
  // Claude-only fail-safe.
58304
58751
  availableAgents: this.deps.detectedAgents,
58305
- implementorAgent: this.activeImplementorAgent.toLowerCase()
58752
+ implementorAgent: implementorAgent.toLowerCase()
58306
58753
  })).workflowState;
58307
58754
  } catch (err) {
58308
- let wasNewest = this.activeTaskId === taskId, restoreTo = previousActive && this.singleTaskContextByTask.has(previousActive.taskId) ? previousActive : null;
58309
- wasNewest && (this.activeTaskId = restoreTo?.taskId ?? null, this.activeBrief = restoreTo?.brief ?? null, restoreTo && (this.activeImplementorAgent = restoreTo.agent)), this.singleTaskContextByTask.delete(taskId), this.startTasksInFlight.delete(taskId);
58755
+ let wasLatestStart2 = this.latestInFlightStart === taskId;
58756
+ this.forgetSingleTask(taskId, "rejected"), this.startTasksInFlight.delete(taskId), this.dropInFlightStart(taskId);
58310
58757
  for (let i = this.pendingGateDispatches.length - 1; i >= 0; i--) {
58311
58758
  let pending = this.pendingGateDispatches[i];
58312
- (pending.envelopeTaskId === taskId || !pending.envelopeTaskId && wasNewest) && this.pendingGateDispatches.splice(i, 1);
58759
+ (pending.envelopeTaskId === taskId || !pending.envelopeTaskId && wasLatestStart2) && this.pendingGateDispatches.splice(i, 1);
58313
58760
  }
58314
- return wasNewest && this.deps.armLeTaskId?.(restoreTo?.taskId ?? null), logger.warn("[QuorumLoop] startTask failed \u2014 rolled back armed task + discarded buffered GATE_DISPATCH", {
58761
+ return logger.warn("[QuorumLoop] startTask failed \u2014 dropped the task record + discarded its buffered GATE_DISPATCH", {
58315
58762
  err: err.message
58316
58763
  }), null;
58317
58764
  }
58318
- this.startTasksInFlight.delete(taskId), this.reviewScopeResetEligibleTasks.add(taskId), logger.info("[QuorumLoop] startTask succeeded \u2014 draining buffered GATE_DISPATCH (if any) then awaiting more", {
58765
+ this.startTasksInFlight.delete(taskId);
58766
+ let wasLatestStart = this.latestInFlightStart === taskId;
58767
+ this.dropInFlightStart(taskId), this.reviewScopeResetEligibleTasks.add(taskId);
58768
+ let ordinal = this.taskOrdinalSink?.assign(taskId, {
58769
+ kind: "single",
58770
+ ...args.retryOf ? { retryOf: args.retryOf } : {}
58771
+ });
58772
+ this.confirmedSingleOrder.push(taskId), this.focusedTaskId = taskId, logger.info("[QuorumLoop] startTask succeeded \u2014 draining buffered GATE_DISPATCH (if any) then awaiting more", {
58319
58773
  taskId,
58774
+ ...ordinal !== void 0 ? { ordinal } : {},
58775
+ ...args.retryOf ? { retryOf: args.retryOf } : {},
58320
58776
  workflowState,
58321
58777
  buffered: this.pendingGateDispatches.length
58322
58778
  });
58323
58779
  let drained = [];
58324
58780
  for (let i = this.pendingGateDispatches.length - 1; i >= 0; i--) {
58325
58781
  let entry = this.pendingGateDispatches[i];
58326
- (!entry.envelopeTaskId || entry.envelopeTaskId === taskId) && drained.unshift(this.pendingGateDispatches.splice(i, 1)[0]);
58782
+ (!entry.envelopeTaskId && wasLatestStart || entry.envelopeTaskId === taskId) && drained.unshift(this.pendingGateDispatches.splice(i, 1)[0]);
58327
58783
  }
58328
58784
  for (let entry of drained)
58329
- await this.handleGateDispatch(entry.payload, entry.envelopeTaskId);
58785
+ await this.handleGateDispatch(entry.payload, entry.envelopeTaskId ?? taskId);
58330
58786
  return { taskId };
58331
58787
  }
58332
58788
  // ─── §3.C.20 — GATE_DISPATCH → round-0 implementor spawn ────────────────
@@ -58349,7 +58805,7 @@ var QuorumLoop = class _QuorumLoop {
58349
58805
  * never thrown upward.
58350
58806
  */
58351
58807
  async handleGateDispatch(payload, envelopeTaskId) {
58352
- let dispatchTaskId = envelopeTaskId || this.activeTaskId;
58808
+ let dispatchTaskId = envelopeTaskId || this.latestInFlightStart;
58353
58809
  if (dispatchTaskId && Array.isArray(payload.reviewerSeats) && payload.reviewerSeats.length > 0) {
58354
58810
  let roster = this.expectedRosterByTask.get(dispatchTaskId);
58355
58811
  roster || (roster = /* @__PURE__ */ new Map(), this.expectedRosterByTask.set(dispatchTaskId, roster));
@@ -58367,11 +58823,12 @@ var QuorumLoop = class _QuorumLoop {
58367
58823
  logger.info("[QuorumLoop] GateDispatch dedup no-op", { gateRunId: payload.gateRunId });
58368
58824
  return;
58369
58825
  }
58370
- if (typeof envelopeTaskId == "string" && envelopeTaskId.length > 0 && envelopeTaskId !== this.activeTaskId && // P45 D1 — a dispatch for an EARLIER single task (a later `startTask` moved
58371
- // `activeTaskId` on) is still a single-implementor dispatch, not a team one.
58826
+ if (typeof envelopeTaskId == "string" && envelopeTaskId.length > 0 && // P45 D1 / P46a B1 — a dispatch for ANY single task this loop started (in
58827
+ // flight or confirmed, earlier or later) is a single-implementor dispatch;
58828
+ // only an unknown task id routes to the team handler.
58372
58829
  !this.singleTaskContextByTask.has(envelopeTaskId))
58373
58830
  return this.bufferTeamPacketDuringSeeding("gateDispatch", payload, envelopeTaskId) ? void 0 : this.handleTeamGateDispatch(payload, envelopeTaskId);
58374
- let inFlightFor = typeof envelopeTaskId == "string" && envelopeTaskId.length > 0 ? envelopeTaskId : this.activeTaskId;
58831
+ let inFlightFor = typeof envelopeTaskId == "string" && envelopeTaskId.length > 0 ? envelopeTaskId : this.latestInFlightStart;
58375
58832
  if (inFlightFor !== null && this.startTasksInFlight.has(inFlightFor)) {
58376
58833
  logger.info("[QuorumLoop] GateDispatch buffered \u2014 startTask in flight (early-packet race)", {
58377
58834
  gateRunId: payload.gateRunId,
@@ -58379,15 +58836,15 @@ var QuorumLoop = class _QuorumLoop {
58379
58836
  }), this.pendingGateDispatches.push({ payload, envelopeTaskId });
58380
58837
  return;
58381
58838
  }
58382
- let single = typeof envelopeTaskId == "string" && envelopeTaskId.length > 0 ? this.singleTaskContextByTask.get(envelopeTaskId) : void 0, taskId = single ? envelopeTaskId : this.activeTaskId, brief = single?.brief ?? this.activeBrief, agent = single?.agent ?? this.activeImplementorAgent;
58383
- if (!taskId || !brief) {
58384
- logger.warn("[QuorumLoop] GateDispatch arrived before start_task recorded the active task \u2014 no-op (recovery will handle)", {
58839
+ let single = typeof envelopeTaskId == "string" && envelopeTaskId.length > 0 ? this.singleTaskContextByTask.get(envelopeTaskId) : void 0;
58840
+ if (!single || !envelopeTaskId) {
58841
+ logger.warn("[QuorumLoop] GateDispatch names no task this loop started \u2014 no-op (recovery will handle)", {
58385
58842
  gateRunId: payload.gateRunId,
58386
- hasTaskId: !!taskId,
58387
- hasBrief: !!brief
58843
+ hasEnvelopeTaskId: typeof envelopeTaskId == "string" && envelopeTaskId.length > 0
58388
58844
  });
58389
58845
  return;
58390
58846
  }
58847
+ let taskId = envelopeTaskId, brief = single.brief, agent = single.agent;
58391
58848
  this.seenGateDispatchRunIds.add(payload.gateRunId);
58392
58849
  let sessionKey = await this.deps.getSessionKey(this.deps.session.sessionId);
58393
58850
  if (!sessionKey) {
@@ -58942,8 +59399,13 @@ ${section}`);
58942
59399
  settlePending = resolve23;
58943
59400
  }),
58944
59401
  ...args.teamAuthority ? { teamAuthority: args.teamAuthority } : {}
58945
- };
58946
- if (process.env.CODEVIBE_ENABLE_AGENT_WEB === "1" && (args.agent === "CLAUDE" || args.agent === "ANTIGRAVITY"))
59402
+ }, needsWeb = this.webAccessByTask.get(args.taskId);
59403
+ if (needsWeb === void 0) {
59404
+ let singleRecord = this.singleTaskContextByTask.get(args.taskId), originalPrompt = (singleRecord ? singleRecord.request ?? singleRecord.brief : void 0) ?? this.activeBriefByTask.get(args.taskId)?.brief ?? // P46a B1 — no singleton fallback: a task without a record reads as no request.
59405
+ (args.roundNumber === 0 ? args.brief : "");
59406
+ needsWeb = isWebAccessNeeded(originalPrompt), this.webAccessByTask.set(args.taskId, needsWeb);
59407
+ }
59408
+ if (needsWeb && (args.agent === "CLAUDE" || args.agent === "ANTIGRAVITY"))
58947
59409
  try {
58948
59410
  agentWebTurn = await startAgentWebTurn({
58949
59411
  sessionId: this.deps.session.sessionId,
@@ -58974,13 +59436,13 @@ ${section}`);
58974
59436
  workingDirAuthority: snapshotAuthority,
58975
59437
  role: "implementor",
58976
59438
  agentKind: args.agent,
58977
- // CP-12 W2.b — thread THIS round's task id as per-spawn substrate/audit
58978
- // authority. On the TEAM path `args.taskId` is the per-track child
58979
- // `task_id` (the LE singleton stays unset/clobber-free for concurrent
58980
- // tracks); on the single-impl path it equals the armed singleton
58981
- // (`setActiveTaskId`), so the LE's `args.taskId ?? this.taskId` fallback
58982
- // yields byte-identical single-impl substrate engagement.
59439
+ // CP-12 W2.b / P46a B2 — thread THIS round's task id as the per-spawn
59440
+ // substrate/audit/authority identity. On the TEAM path `args.taskId` is
59441
+ // the per-track child `task_id`; on the single-impl path it is the task's
59442
+ // own id the executor keeps no singleton any more and resolves the
59443
+ // per-task authority scope (seeded until ITS TaskAuthorized) from it.
58983
59444
  taskId: args.taskId,
59445
+ webTurnActive: !!agentWebTurn?.mcpConfigPath,
58984
59446
  timeoutMs: null,
58985
59447
  stdinTty: !0,
58986
59448
  // PHASE-CP-10-MIN (#585) — the cooperative abort signal (#C10M-16).
@@ -60203,7 +60665,7 @@ ${section}`);
60203
60665
  ));
60204
60666
  }
60205
60667
  for (let taskId of taskIds)
60206
- this.activeBriefByTask.get(taskId)?.taskGroupId === taskGroupId && (this.activeBriefByTask.delete(taskId), this.activeAttachmentsByTask.delete(taskId));
60668
+ this.activeBriefByTask.get(taskId)?.taskGroupId === taskGroupId && (this.activeBriefByTask.delete(taskId), this.activeAttachmentsByTask.delete(taskId), this.webAccessByTask.delete(taskId));
60207
60669
  if (this.teamAttachmentsByGroup.delete(taskGroupId), failures.length > 0) {
60208
60670
  try {
60209
60671
  this.surfaceHalt(
@@ -60281,7 +60743,7 @@ ${section}`);
60281
60743
  });
60282
60744
  return;
60283
60745
  }
60284
- if (opts?.preserveReviseContext || (this.userNotesByTask.delete(taskId), this.reviewScopeResetTasks.delete(taskId), this.reviewScopeResetEligibleTasks.delete(taskId), this.roundHistoryByTask.delete(taskId), opts?.taskContinues !== !0 && this.singleTaskContextByTask.delete(taskId), this.rationaleByTask.delete(taskId), this.taskDegradedMetadataByTask.delete(taskId), this.expectedRosterByTask.delete(taskId), this.activeGateByTaskId.delete(taskId), this.conflictedSeatsByTask.delete(taskId), this.nonApprovedSeatsByTask.delete(taskId), this.restartFenceByTaskId.delete(taskId), appendContextItem(this.deps.session.sessionId, {
60746
+ if (opts?.preserveReviseContext || (this.userNotesByTask.delete(taskId), this.reviewScopeResetTasks.delete(taskId), this.reviewScopeResetEligibleTasks.delete(taskId), this.roundHistoryByTask.delete(taskId), opts?.taskContinues !== !0 && this.forgetSingleTask(taskId, "discarded"), this.rationaleByTask.delete(taskId), this.taskDegradedMetadataByTask.delete(taskId), this.expectedRosterByTask.delete(taskId), this.activeGateByTaskId.delete(taskId), this.conflictedSeatsByTask.delete(taskId), this.nonApprovedSeatsByTask.delete(taskId), this.restartFenceByTaskId.delete(taskId), appendContextItem(this.deps.session.sessionId, {
60285
60747
  kind: "verdict",
60286
60748
  author: { role: "engine" },
60287
60749
  sensitivity: "user",
@@ -60327,7 +60789,123 @@ ${section}`);
60327
60789
  */
60328
60790
  async promoteShadow(taskId) {
60329
60791
  if (!(this.shuttingDown && (this.terminalClassBAdmissionsByTask.get(taskId)?.size ?? 0) === 0))
60330
- return this.runExclusiveShadowOp(taskId, () => this.promoteShadowLocked(taskId));
60792
+ try {
60793
+ await this.runExclusiveShadowOp(taskId, () => this.promoteShadowLocked(taskId));
60794
+ } finally {
60795
+ this.pendingPromoteRetries.length > 0 && setImmediate(() => {
60796
+ this.drainPromoteRetries().catch((err) => {
60797
+ logger.warn("[QuorumLoop] automatic retry drain failed", { err: err.message });
60798
+ });
60799
+ });
60800
+ }
60801
+ }
60802
+ /** P46a C2 — start every planned automatic retry as a new linked task. */
60803
+ async drainPromoteRetries() {
60804
+ for (; this.pendingPromoteRetries.length > 0; ) {
60805
+ let plan = this.pendingPromoteRetries.shift();
60806
+ if (this.shuttingDown) {
60807
+ logger.info("[QuorumLoop] automatic retry skipped \u2014 shutting down", { retryOf: plan.retryOf });
60808
+ continue;
60809
+ }
60810
+ try {
60811
+ let started = await this.startTask({
60812
+ decision: { action: "start_task", rationale: "P46a automatic retry after a session-task collision" },
60813
+ brief: plan.brief,
60814
+ agent: plan.agent,
60815
+ retryOf: plan.retryOf,
60816
+ request: plan.request
60817
+ });
60818
+ started ? (this.retryStartedSink?.({ taskId: started.taskId, retryOf: plan.retryOf, agent: plan.agent }), this.surfaceHalt(
60819
+ `Task ${this.labelFor(started.taskId)} started \u2014 the retry of task ${this.labelFor(plan.retryOf)}.`
60820
+ )) : this.surfaceHalt(
60821
+ `Could not start the automatic retry of task ${this.labelFor(plan.retryOf)} (the START was rejected). Re-issue the request to try again.`
60822
+ );
60823
+ } catch (err) {
60824
+ logger.warn("[QuorumLoop] automatic retry START threw (non-fatal)", {
60825
+ retryOf: plan.retryOf,
60826
+ err: err.message
60827
+ }), this.surfaceHalt(
60828
+ `Could not start the automatic retry of task ${this.labelFor(plan.retryOf)}. Re-issue the request to try again.`
60829
+ );
60830
+ }
60831
+ }
60832
+ }
60833
+ /** P46a D2 — correct a task's recorded outcome after its terminal cleanup ran. */
60834
+ markTaskOutcome(taskId, outcome) {
60835
+ let history = this.singleTaskHistory.get(taskId);
60836
+ history && (history.outcome = outcome);
60837
+ }
60838
+ /**
60839
+ * P46a C1 — classify each conflicting path: `session-task` when its current
60840
+ * real-tree metadata equals a post-image a single-task promote of THIS
60841
+ * session recorded (`promotedByPath`), else `external`.
60842
+ */
60843
+ async attributeConflicts(shadow, paths) {
60844
+ let out = [];
60845
+ for (let rel of paths) {
60846
+ let recorded = this.promotedByPath.get(rel), cause = "external";
60847
+ if (recorded)
60848
+ try {
60849
+ let current = await shadow.readRealMetadata(rel);
60850
+ current.hash === recorded.postHash && current.mode === recorded.postMode && (cause = "session-task");
60851
+ } catch {
60852
+ cause = "external";
60853
+ }
60854
+ out.push({ path: rel, cause, ...cause === "session-task" && recorded ? { byTaskId: recorded.taskId } : {} });
60855
+ }
60856
+ return out;
60857
+ }
60858
+ /**
60859
+ * P46a C2 — the retry brief: the original request, a "Files changed since
60860
+ * your first attempt" section (a unified diff from the failed task's REVIEWED
60861
+ * contents to the CURRENT real contents, per conflicting path, naming the
60862
+ * session task that changed it), and the reviewed contents themselves. Every
60863
+ * section is capped (`RETRY_BRIEF_SECTION_CAP` per path,
60864
+ * `RETRY_BRIEF_TOTAL_CAP` overall — the brief becomes the retry task's
60865
+ * durable `task_spec`, whose write is fire-and-forget) and secret-scrubbed
60866
+ * (`redactSecretShapesInText`) before insertion.
60867
+ */
60868
+ async buildRetryBrief(shadow, failedTaskId, originalBrief, attributed, reviewed) {
60869
+ let failedLabel = this.labelFor(failedTaskId), reviewedByPath = new Map(reviewed.map((file) => [file.path, file])), changed = [], contents = [];
60870
+ for (let conflict of attributed) {
60871
+ let who = conflict.cause === "session-task" && conflict.byTaskId ? `changed by task ${this.labelFor(conflict.byTaskId)} in this session` : "changed outside CodeVibe", reviewedFile = reviewedByPath.get(conflict.path), reviewedText = reviewedFile && reviewedFile.change_kind !== "deleted" ? reviewedFile.content : "", currentText = null, currentState = "";
60872
+ try {
60873
+ currentText = await shadow.readRealFileText(conflict.path);
60874
+ } catch (err) {
60875
+ currentText = null, currentState = err.code === "ENOENT" ? "; now absent" : "; unreadable now";
60876
+ }
60877
+ let diffText = unifiedDiff(
60878
+ `reviewed (task ${failedLabel})`,
60879
+ "current",
60880
+ reviewedText,
60881
+ currentText ?? ""
60882
+ );
60883
+ changed.push(
60884
+ `### ${conflict.path} (${who}${currentState})`,
60885
+ "```diff",
60886
+ capText2(diffText, RETRY_BRIEF_SECTION_CAP),
60887
+ "```"
60888
+ ), reviewedFile && contents.push(
60889
+ `### ${conflict.path}${reviewedFile.change_kind === "deleted" ? " (you deleted this file)" : ""}`,
60890
+ "```",
60891
+ capText2(reviewedText, RETRY_BRIEF_SECTION_CAP),
60892
+ "```"
60893
+ );
60894
+ }
60895
+ let changedSection = redactSecretShapesInText(changed.join(`
60896
+ `)), contentsSection = redactSecretShapesInText(contents.join(`
60897
+ `)), assembled = [
60898
+ originalBrief.trim(),
60899
+ "",
60900
+ "## Files changed since your first attempt",
60901
+ `Your first attempt (task ${failedLabel}) was reviewed and approved, but could not be applied: ${attributed.length} file(s) changed in the workspace after your attempt. Re-do the request against the CURRENT files. Each diff below goes from the contents you produced (your reviewed version) to the contents now in the workspace.`,
60902
+ changedSection,
60903
+ "",
60904
+ "## Your reviewed contents from the first attempt",
60905
+ contentsSection
60906
+ ].join(`
60907
+ `);
60908
+ return capText2(assembled, RETRY_BRIEF_TOTAL_CAP);
60331
60909
  }
60332
60910
  /**
60333
60911
  * CP-12 W2.b (§3.C.2b (d) / H3-4) — promote-quiescence BARRIER for `taskId`.
@@ -60350,6 +60928,12 @@ ${section}`);
60350
60928
  return this.promoteShadowLockedInner(taskId, originEpoch);
60351
60929
  }
60352
60930
  async promoteShadowLockedInner(taskId, originEpoch) {
60931
+ let singleContext = this.singleTaskContextByTask.get(taskId), retryContext = singleContext ? {
60932
+ brief: singleContext.brief,
60933
+ agent: singleContext.agent,
60934
+ ...singleContext.retryOf ? { retryOf: singleContext.retryOf } : {},
60935
+ ...singleContext.request !== void 0 ? { request: singleContext.request } : {}
60936
+ } : void 0;
60353
60937
  try {
60354
60938
  let teamAuthority = this.teamAuthorityForTask(taskId);
60355
60939
  if (this.teamRunIsHalted(teamAuthority)) return;
@@ -60362,9 +60946,15 @@ ${section}`);
60362
60946
  error: err?.message
60363
60947
  }), historySnapshot = null;
60364
60948
  }
60365
- this.userNotesByTask.delete(taskId), this.reviewScopeResetTasks.delete(taskId), this.reviewScopeResetEligibleTasks.delete(taskId), this.roundHistoryByTask.delete(taskId), this.singleTaskContextByTask.delete(taskId), this.rationaleByTask.delete(taskId), this.taskDegradedMetadataByTask.delete(taskId), this.expectedRosterByTask.delete(taskId), this.activeGateByTaskId.delete(taskId), this.conflictedSeatsByTask.delete(taskId), this.nonApprovedSeatsByTask.delete(taskId), this.restartFenceByTaskId.delete(taskId);
60949
+ this.userNotesByTask.delete(taskId), this.reviewScopeResetTasks.delete(taskId), this.reviewScopeResetEligibleTasks.delete(taskId), this.roundHistoryByTask.delete(taskId), this.forgetSingleTask(taskId, "apply_failed"), this.rationaleByTask.delete(taskId), this.taskDegradedMetadataByTask.delete(taskId), this.expectedRosterByTask.delete(taskId), this.activeGateByTaskId.delete(taskId), this.conflictedSeatsByTask.delete(taskId), this.nonApprovedSeatsByTask.delete(taskId), this.restartFenceByTaskId.delete(taskId);
60366
60950
  let shadow = this.shadowsByTask.get(taskId);
60367
60951
  if (!shadow) {
60952
+ if (this.singleTaskHistory.get(taskId)?.outcome === "discarded") {
60953
+ this.surfaceHalt(
60954
+ `Could not apply task ${this.labelFor(taskId)} \u2014 its workspace copy was discarded after its implementor round failed on this side; nothing was applied. Re-run the request.`
60955
+ ), logger.warn("[QuorumLoop] promote on a discarded task \u2014 nothing applied", { taskId });
60956
+ return;
60957
+ }
60368
60958
  logger.info("[QuorumLoop] promote no-op \u2014 no in-memory shadow for task", { taskId });
60369
60959
  return;
60370
60960
  }
@@ -60614,7 +61204,8 @@ ${section}`);
60614
61204
  shadow,
60615
61205
  diff,
60616
61206
  terminalOutcomeReserved,
60617
- historySnapshot
61207
+ historySnapshot,
61208
+ retryContext
60618
61209
  );
60619
61210
  } finally {
60620
61211
  this.emitProgress({ phase: "progress_cleared" }, originEpoch);
@@ -60632,7 +61223,7 @@ ${section}`);
60632
61223
  * no-op/re-delivery/discard returns (which must stay silent). All emits carry the
60633
61224
  * caller-resolved `originEpoch`.
60634
61225
  */
60635
- async promoteApplyLocked(taskId, originEpoch, shadow, diff, terminalOutcomeReserved, historySnapshot) {
61226
+ async promoteApplyLocked(taskId, originEpoch, shadow, diff, terminalOutcomeReserved, historySnapshot, retryContext) {
60636
61227
  this.emitProgress({ phase: "promoting", files: diff.length }, originEpoch);
60637
61228
  let teamPromoteEntry = this.activeBriefByTask.get(taskId), captureManifest = this.deps.durableStore && teamPromoteEntry && teamPromoteEntry.taskGroupId.length > 0 ? {
60638
61229
  trackIndex: teamPromoteEntry.trackIndex,
@@ -60652,21 +61243,85 @@ ${section}`);
60652
61243
  }
60653
61244
  } : void 0, res = await shadow.promote(diff, captureManifest ? { captureManifest } : void 0);
60654
61245
  if (res.conflicts.length > 0 || res.errors.length > 0) {
60655
- let paths = [...res.conflicts, ...res.errors.map((e) => e.path)];
60656
- if (this.surfaceHalt(
60657
- `Could not promote \u2014 ${paths.length} file(s) at ${paths.join(", ")} changed under the task or failed to write. The approved change was not fully applied.`
60658
- ), res.mutationState === "unresolved") {
60659
- logger.warn("[QuorumLoop] unresolved promotion retained for reconciliation", {
61246
+ let paths = [...res.conflicts, ...res.errors.map((e) => e.path)], label = this.labelFor(taskId);
61247
+ if (res.mutationState === "unresolved") {
61248
+ this.surfaceHalt(
61249
+ `Could not apply task ${label} \u2014 ${paths.length} file(s) at ${paths.join(", ")} changed under the task or failed to write; the snapshot is retained until it is reconciled.`
61250
+ ), logger.warn("[QuorumLoop] unresolved promotion retained for reconciliation", {
60660
61251
  taskId,
60661
61252
  paths
61253
+ }), this.markTaskOutcome(taskId, "unresolved");
61254
+ return;
61255
+ }
61256
+ let isSingleTask = retryContext !== void 0 && !this.activeBriefByTask.has(taskId), attributed = isSingleTask ? await this.attributeConflicts(shadow, res.conflicts) : [], writeErrors = res.errors.length > 0, retryBrief = null, rootRequest = retryContext ? retryContext.request ?? retryContext.brief : "";
61257
+ if (isSingleTask)
61258
+ if (attributed.length > 0)
61259
+ try {
61260
+ retryBrief = await this.buildRetryBrief(
61261
+ shadow,
61262
+ taskId,
61263
+ rootRequest,
61264
+ attributed,
61265
+ this.reviewedSnapshotByTask.get(taskId) ?? []
61266
+ );
61267
+ } catch (err) {
61268
+ logger.warn("[QuorumLoop] retry brief composition failed (offering the original request only)", {
61269
+ taskId,
61270
+ err: err.message
61271
+ }), retryBrief = rootRequest;
61272
+ }
61273
+ else
61274
+ retryBrief = rootRequest;
61275
+ if (await this.discardShadowLocked(taskId, shadow), this.markTaskOutcome(taskId, "apply_failed"), terminalOutcomeReserved && this.terminallyRetiredWorkspaceTasks.has(taskId) && await this.workspaceOutcomeSink.completeTask(taskId, "apply_failed"), !isSingleTask || retryBrief === null) {
61276
+ this.surfaceHalt(
61277
+ `Could not apply task ${label} \u2014 ${paths.length} file(s) at ${paths.join(", ")} changed under the task or failed to write. The approved change was not applied.`
61278
+ );
61279
+ return;
61280
+ }
61281
+ let sessionChanged = attributed.filter((c) => c.cause === "session-task"), byLabels = [...new Set(sessionChanged.map((c) => this.labelFor(c.byTaskId)))], causes = [];
61282
+ sessionChanged.length > 0 && causes.push(`${sessionChanged.length} changed by task ${byLabels.join(", ")}`), attributed.length - sessionChanged.length > 0 && causes.push(`${attributed.length - sessionChanged.length} changed outside CodeVibe`), writeErrors && causes.push(`${res.errors.length} could not be written`);
61283
+ let total = attributed.length + res.errors.length, summary = `${total} ${total === 1 ? "file" : "files"} (${causes.join("; ")})`;
61284
+ if (!writeErrors && attributed.every((c) => c.cause === "session-task") && retryContext.retryOf === void 0 && !this.autoRetryIssuedFor.has(taskId)) {
61285
+ this.autoRetryIssuedFor.add(taskId), this.pendingPromoteRetries.push({ retryOf: taskId, brief: retryBrief, agent: retryContext.agent, request: rootRequest }), this.surfaceHalt(
61286
+ `Could not apply task ${label}: ${summary} after it was reviewed. Retrying automatically as a new task on a fresh snapshot (it will be reviewed and approved again).`
61287
+ ), logger.info("[QuorumLoop] promote collided with a session task \u2014 automatic retry planned", {
61288
+ taskId,
61289
+ conflicts: attributed.length
60662
61290
  });
60663
61291
  return;
60664
61292
  }
60665
- await this.discardShadowLocked(taskId, shadow), terminalOutcomeReserved && this.terminallyRetiredWorkspaceTasks.has(taskId) && await this.workspaceOutcomeSink.completeTask(taskId, "apply_failed");
61293
+ let autoRetried = retryContext.retryOf !== void 0 && this.autoRetryIssuedFor.has(retryContext.retryOf);
61294
+ this.surfaceHalt(
61295
+ `Could not apply task ${label}: ${summary}` + (autoRetried ? " \u2014 the automatic retry collided too." : ".")
61296
+ );
61297
+ let menu = {
61298
+ taskId,
61299
+ taskLabel: label,
61300
+ conflicts: [
61301
+ ...attributed.map((c) => ({
61302
+ path: c.path,
61303
+ cause: c.cause,
61304
+ ...c.byTaskId ? { byTaskLabel: this.labelFor(c.byTaskId) } : {}
61305
+ })),
61306
+ ...res.errors.map((e) => ({ path: e.path, cause: "write-error" }))
61307
+ ],
61308
+ autoRetried,
61309
+ retryBrief,
61310
+ request: rootRequest,
61311
+ agent: retryContext.agent
61312
+ };
61313
+ try {
61314
+ this.applyConflictSink?.(menu);
61315
+ } catch (err) {
61316
+ logger.warn("[QuorumLoop] apply-conflict menu sink threw (ignored)", {
61317
+ taskId,
61318
+ err: err.message
61319
+ });
61320
+ }
60666
61321
  return;
60667
61322
  }
60668
61323
  if (!res.materialized)
60669
- if (res.recovered)
61324
+ if (this.markTaskOutcome(taskId, "applied"), res.recovered)
60670
61325
  logger.info("[QuorumLoop] recovered fully-applied snapshot promotion", { taskId });
60671
61326
  else {
60672
61327
  logger.debug("[QuorumLoop] promote no-op \u2014 marker flipped to promoted mid-call", { taskId }), terminalOutcomeReserved && await this.workspaceOutcomeSink.completeTask(taskId, "applied", {
@@ -60685,6 +61340,19 @@ ${section}`);
60685
61340
  return;
60686
61341
  }
60687
61342
  let appliedPaths = res.recovered ? diff.map((file) => file.path) : res.promoted;
61343
+ if (this.markTaskOutcome(taskId, "applied"), retryContext !== void 0 && !this.activeBriefByTask.has(taskId)) {
61344
+ let at = (/* @__PURE__ */ new Date()).toISOString();
61345
+ for (let rel of appliedPaths)
61346
+ try {
61347
+ let meta = await shadow.readRealMetadata(rel);
61348
+ this.promotedByPath.set(rel, { taskId, postHash: meta.hash, postMode: meta.mode, at });
61349
+ } catch (err) {
61350
+ this.promotedByPath.delete(rel), logger.debug("[QuorumLoop] promoted-path post-image read failed (left unattributed)", {
61351
+ taskId,
61352
+ err: err.message
61353
+ });
61354
+ }
61355
+ }
60688
61356
  terminalOutcomeReserved && await this.workspaceOutcomeSink.completeTask(taskId, "applied", {
60689
61357
  filesApplied: appliedPaths.length
60690
61358
  }), logger.info("[QuorumLoop] shadow promoted to real tree", {
@@ -60937,7 +61605,11 @@ ${section}`);
60937
61605
  });
60938
61606
  return;
60939
61607
  }
60940
- if (dispatchTaskId) {
61608
+ if (logger.info("[QuorumLoop] ReviewerDispatch received", {
61609
+ gateId: payload.gateId,
61610
+ seatId: seat,
61611
+ ...dispatchTaskId ? { taskId: dispatchTaskId } : {}
61612
+ }), dispatchTaskId) {
60941
61613
  let roster = this.expectedRosterByTask.get(dispatchTaskId);
60942
61614
  roster || (roster = /* @__PURE__ */ new Map(), this.expectedRosterByTask.set(dispatchTaskId, roster)), roster.set(String(seat), String(payload.role || "reviewer"));
60943
61615
  }
@@ -61222,7 +61894,7 @@ ${section}`);
61222
61894
  "[QuorumLoop] reviewer-sandboxing DISABLED by operator opt-out (CODEVIBE_SANDBOX_REVIEWERS=0) \u2014 legacy path",
61223
61895
  { key }
61224
61896
  ), NO_TEARDOWN;
61225
- let taskId = this.taskByGateId.get(args.gateId) ?? this.activeTaskId;
61897
+ let taskId = this.taskByGateId.get(args.gateId);
61226
61898
  if (!taskId)
61227
61899
  return this.surfaceReviewerReducedTrust(
61228
61900
  args,
@@ -61787,32 +62459,51 @@ ${section}`);
61787
62459
  // ─── §3.C.21b — dropped-packet recovery ─────────────────────────────────
61788
62460
  /**
61789
62461
  * On WS (re)connect/startup, poll for the seats assigned to this desktop for
61790
- * any in_review gate of the active task and spawn any seat NEITHER already
61791
- * running-in-memory NOR already verdict-submitted (double-guard). Without
61792
- * this, a REVIEWER_DISPATCH dropped during a WS gap hangs the gate forever.
62462
+ * any in_review gate of EVERY in-flight task (P46a B3: the live single tasks,
62463
+ * the tasks with an active implementor, and the registered team tracks — one
62464
+ * query each) and spawn any seat NEITHER already running-in-memory NOR already
62465
+ * verdict-submitted (double-guard). Without this, a REVIEWER_DISPATCH dropped
62466
+ * during a WS gap hangs the gate forever. Residual (unchanged): after a full
62467
+ * desktop restart the in-memory maps are empty.
61793
62468
  */
61794
62469
  async recoverInReviewAssignments() {
61795
- if (this.shuttingDown || !this.activeTaskId) return;
61796
- let assignments;
61797
- try {
61798
- assignments = await this.deps.appsyncClient.getInReviewAssignments(this.activeTaskId);
61799
- } catch (err) {
61800
- logger.warn("[QuorumLoop] getInReviewAssignments failed (non-fatal)", {
61801
- err: err.message
61802
- });
61803
- return;
61804
- }
61805
- if (!this.shuttingDown)
61806
- for (let a of assignments) {
61807
- if (this.shuttingDown) break;
61808
- let key = seatKey(a.gateId, a.seatId);
61809
- this.runningSeats.has(key) || this.submittedSeats.has(key) || this.spawnOneSeat({
61810
- gateId: a.gateId,
61811
- seatId: a.seatId,
61812
- role: a.role,
61813
- agentKind: a.agentKind
61814
- });
62470
+ if (this.shuttingDown) return;
62471
+ let taskIds = /* @__PURE__ */ new Set([
62472
+ ...this.singleTaskContextByTask.keys(),
62473
+ ...this.activeImplementorByTask.keys(),
62474
+ ...this.activeBriefByTask.keys()
62475
+ ]);
62476
+ if (taskIds.size !== 0) {
62477
+ logger.info("[QuorumLoop] recovering in-review assignments", { taskIds: [...taskIds] });
62478
+ for (let taskId of taskIds) {
62479
+ if (this.shuttingDown) return;
62480
+ let assignments;
62481
+ try {
62482
+ assignments = await this.deps.appsyncClient.getInReviewAssignments(taskId);
62483
+ } catch (err) {
62484
+ logger.warn("[QuorumLoop] getInReviewAssignments failed (non-fatal)", {
62485
+ taskId,
62486
+ err: err.message
62487
+ });
62488
+ continue;
62489
+ }
62490
+ if (this.shuttingDown) return;
62491
+ for (let a of assignments) {
62492
+ if (this.shuttingDown) break;
62493
+ let key = seatKey(a.gateId, a.seatId);
62494
+ this.runningSeats.has(key) || this.submittedSeats.has(key) || (logger.info("[QuorumLoop] recovered in-review seat (its REVIEWER_DISPATCH was not consumed)", {
62495
+ taskId,
62496
+ gateId: a.gateId,
62497
+ seatId: a.seatId
62498
+ }), this.spawnOneSeat({
62499
+ gateId: a.gateId,
62500
+ seatId: a.seatId,
62501
+ role: a.role,
62502
+ agentKind: a.agentKind
62503
+ }));
62504
+ }
61815
62505
  }
62506
+ }
61816
62507
  }
61817
62508
  // ─── §4 / #PR-4 / #GC-1 — shadow recovery + GC on reconnect/startup ──────
61818
62509
  /**
@@ -61852,7 +62543,7 @@ ${section}`);
61852
62543
  ]), signal);
61853
62544
  if (!this.admittedSessionWorkMayContinue(admission)) return;
61854
62545
  let now = Date.now(), ttlMs = env.ttlMs ?? DEFAULT_SHADOW_TTL_MS, keep = /* @__PURE__ */ new Set();
61855
- this.activeTaskId && keep.add(this.activeTaskId);
62546
+ for (let taskId of this.singleTaskContextByTask.keys()) keep.add(taskId);
61856
62547
  for (let taskId of this.shadowsByTask.keys()) keep.add(taskId);
61857
62548
  for (let taskId of this.shadowCreateInFlight.keys()) keep.add(taskId);
61858
62549
  let failures = [], sameWorkspace = recoverable.filter(
@@ -62737,7 +63428,7 @@ ${section}`);
62737
63428
  });
62738
63429
  return;
62739
63430
  }
62740
- let taskId = this.taskByGateId.get(payload.gateId) ?? this.activeTaskId ?? "", originEpoch = this.originEpochFor(taskId);
63431
+ let taskId = this.taskByGateId.get(payload.gateId) ?? "", originEpoch = this.originEpochFor(taskId);
62741
63432
  if (taskId === "") {
62742
63433
  logger.warn(
62743
63434
  "[QuorumLoop] ReviseFeedback could not resolve a task (in-memory state lost after restart?) \u2014 failing loud",
@@ -62747,7 +63438,17 @@ ${section}`);
62747
63438
  ), this.emitProgress({ phase: "progress_cleared" }, originEpoch);
62748
63439
  return;
62749
63440
  }
62750
- let trackEntry = this.activeBriefByTask.get(taskId), parentTeamAuthority = this.teamAuthorityByGateId.get(payload.gateId), teamAuthority = parentTeamAuthority ?? (trackEntry ? _QuorumLoop.teamAuthorityForEntry(trackEntry) : void 0);
63441
+ let trackEntry = this.activeBriefByTask.get(taskId), singleEntry = this.singleTaskContextByTask.get(taskId);
63442
+ if (!trackEntry && !singleEntry) {
63443
+ logger.warn(
63444
+ "[QuorumLoop] ReviseFeedback names a task with no live record (ended, or lost after restart?) \u2014 failing loud",
63445
+ { taskId, gateId: payload.gateId, nextGateId: payload.nextGateId }
63446
+ ), this.surfaceHalt(
63447
+ `Could not resume this revise for task ${this.labelFor(taskId)} \u2014 its context is gone (re-issue the task to continue).`
63448
+ ), this.emitProgress({ phase: "progress_cleared" }, originEpoch);
63449
+ return;
63450
+ }
63451
+ let parentTeamAuthority = this.teamAuthorityByGateId.get(payload.gateId), teamAuthority = parentTeamAuthority ?? (trackEntry ? _QuorumLoop.teamAuthorityForEntry(trackEntry) : void 0);
62751
63452
  if (parentTeamAuthority && (!trackEntry || !this.teamEntryMayContinue(taskId, trackEntry, parentTeamAuthority))) {
62752
63453
  logger.info("[QuorumLoop] stale team ReviseFeedback refused \u2014 generation retired", {
62753
63454
  taskId,
@@ -62782,7 +63483,9 @@ ${section}`);
62782
63483
  gateId: payload.nextGateId,
62783
63484
  roundNumber: payload.nextRound,
62784
63485
  brief,
62785
- agent: trackEntry?.agent ?? this.singleTaskContextByTask.get(taskId)?.agent ?? this.activeImplementorAgent,
63486
+ // P46a B1 — the TRACK's agent (team) or the single task's OWN agent (its
63487
+ // record is guaranteed above); never a session-wide "active agent".
63488
+ agent: trackEntry?.agent ?? singleEntry.agent,
62786
63489
  sessionKey,
62787
63490
  ...teamAuthority ? { teamAuthority } : {},
62788
63491
  ...priorRationale ? { priorRationale } : {},
@@ -62798,32 +63501,53 @@ ${section}`);
62798
63501
  } : {}
62799
63502
  });
62800
63503
  }
62801
- setActiveImplementorAgent(agent) {
62802
- this.activeImplementorAgent = agent;
63504
+ /**
63505
+ * P46a B1 — set a single task's implementor agent on ITS record (the only
63506
+ * caller is `resumeFromContinuation`, after the user chose a target agent).
63507
+ * Replaces the session-wide `setActiveImplementorAgent` singleton.
63508
+ */
63509
+ setTaskImplementorAgent(taskId, agent) {
63510
+ let current = this.singleTaskContextByTask.get(taskId);
63511
+ current && this.singleTaskContextByTask.set(taskId, { ...current, agent });
63512
+ let history = this.singleTaskHistory.get(taskId);
63513
+ history && (history.agent = agent);
62803
63514
  }
62804
63515
  // ─── PHASE-CP-10-MIN (#585) — continuation handoff ────────────────────────
62805
63516
  /**
62806
- * #585 #C10M-9 — resolve the active task's request context for `/continue
62807
- * request` (the entrypoint wires this to the slash route's
62808
- * `getActiveRequestContext`). Returns `null` when no implementor round is
62809
- * active (no gate to hand off from). The `gateId`/`roundNumber` come from the
62810
- * ACTIVE implementor registry (the round currently running); the brief +
62811
- * source agent come from the loop's active state.
63517
+ * #585 #C10M-9 / P46a B1 (OQ-1, decided) — resolve WHICH single task a
63518
+ * `/continue request` means: the FOCUSED task if it has an active implementor
63519
+ * round; else the single task that has one; else refuse — `ambiguous` lists
63520
+ * the candidates (the shell renders their `#n` so the user can
63521
+ * `/task focus <n>` first), `none` means no task is running a round. The
63522
+ * `gateId`/`roundNumber` come from the ACTIVE implementor registry (the round
63523
+ * currently running); the brief + source agent from the task's own record.
62812
63524
  */
62813
- getActiveContinuationRequestContext() {
62814
- let taskId = this.activeTaskId;
62815
- if (!taskId) return null;
62816
- let active = this.activeImplementorByTask.get(taskId);
62817
- if (!active) return null;
62818
- let single = this.singleTaskContextByTask.get(taskId), brief = this.activeBriefByTask.get(taskId)?.brief ?? single?.brief ?? this.activeBrief ?? "";
63525
+ resolveContinuationRequest() {
63526
+ let withActiveRound = [...this.singleTaskContextByTask.keys()].filter(
63527
+ (id) => this.activeImplementorByTask.has(id)
63528
+ ), taskId = null;
63529
+ if (this.focusedTaskId && this.activeImplementorByTask.has(this.focusedTaskId))
63530
+ taskId = this.focusedTaskId;
63531
+ else if (withActiveRound.length === 1)
63532
+ taskId = withActiveRound[0];
63533
+ else return withActiveRound.length === 0 ? { kind: "none" } : { kind: "ambiguous", candidates: withActiveRound };
63534
+ let active = this.activeImplementorByTask.get(taskId), single = this.singleTaskContextByTask.get(taskId);
62819
63535
  return {
62820
- taskId,
62821
- gateId: active.gateId,
62822
- roundNumber: active.roundNumber,
62823
- brief,
62824
- sourceAgent: single?.agent ?? this.activeImplementorAgent
63536
+ kind: "ok",
63537
+ ctx: {
63538
+ taskId,
63539
+ gateId: active.gateId,
63540
+ roundNumber: active.roundNumber,
63541
+ brief: this.activeBriefByTask.get(taskId)?.brief ?? single.brief,
63542
+ sourceAgent: single.agent
63543
+ }
62825
63544
  };
62826
63545
  }
63546
+ /** The `ok` projection of {@link resolveContinuationRequest} (null otherwise). */
63547
+ getActiveContinuationRequestContext() {
63548
+ let resolved = this.resolveContinuationRequest();
63549
+ return resolved.kind === "ok" ? resolved.ctx : null;
63550
+ }
62827
63551
  /**
62828
63552
  * #585 L-A — mint a fresh `expiresAt = now + TTL` at millisecond precision
62829
63553
  * (RFC-3339). Fresh per request so a CANCEL→re-request produces a distinct
@@ -63108,9 +63832,7 @@ ${section}`);
63108
63832
  }
63109
63833
  if (trackEntry && teamAuthority && !this.teamEntryMayContinue(input.taskId, trackEntry, teamAuthority) || this.shuttingDown) return !1;
63110
63834
  let brief = this.buildResumeBrief(packet);
63111
- this.setActiveImplementorAgent(input.targetAgent);
63112
- let resumedSingle = this.singleTaskContextByTask.get(input.taskId);
63113
- resumedSingle && this.singleTaskContextByTask.set(input.taskId, { brief: resumedSingle.brief, agent: input.targetAgent }), logger.info("[QuorumLoop] resuming from continuation", {
63835
+ this.setTaskImplementorAgent(input.taskId, input.targetAgent), logger.info("[QuorumLoop] resuming from continuation", {
63114
63836
  taskId: input.taskId,
63115
63837
  sourceAgent: packet.sourceAgent,
63116
63838
  targetAgent: input.targetAgent,
@@ -63235,7 +63957,7 @@ ${section}`);
63235
63957
  * user requirement.
63236
63958
  */
63237
63959
  buildReviseBrief(taskId, payload) {
63238
- let originalBrief = this.activeBriefByTask.get(taskId)?.brief ?? this.singleTaskContextByTask.get(taskId)?.brief ?? this.activeBrief ?? "", userNotes = this.userNotesByTask.get(taskId) ?? [], history = (this.roundHistoryByTask.get(taskId) ?? []).filter(
63960
+ let originalBrief = this.activeBriefByTask.get(taskId)?.brief ?? this.singleTaskContextByTask.get(taskId)?.brief ?? "", userNotes = this.userNotesByTask.get(taskId) ?? [], history = (this.roundHistoryByTask.get(taskId) ?? []).filter(
63239
63961
  (r) => r.round < payload.nextRound - 1
63240
63962
  );
63241
63963
  return composeReviseBrief(originalBrief, userNotes, payload, history);
@@ -63396,9 +64118,16 @@ ${section}`);
63396
64118
  get _submittedSeatsForTests() {
63397
64119
  return this.submittedSeats;
63398
64120
  }
63399
- /** @internal — test seam to set the active task without start_task. */
63400
- _setActiveTaskForTests(taskId, brief) {
63401
- this.activeTaskId = taskId, this.reviewScopeResetEligibleTasks.add(taskId), brief !== void 0 && (this.activeBrief = brief, this.singleTaskContextByTask.set(taskId, { brief, agent: this.activeImplementorAgent }));
64121
+ /**
64122
+ * @internal — test seam to register a single task (its record + focus)
64123
+ * without start_task. P46a B1: the record is what every reader resolves, so
64124
+ * the seam always creates one (an empty brief when none is given).
64125
+ */
64126
+ _setActiveTaskForTests(taskId, brief, agent = "CLAUDE", gateIds = []) {
64127
+ this.reviewScopeResetEligibleTasks.add(taskId);
64128
+ let existing = this.singleTaskContextByTask.get(taskId), record = { brief: brief ?? existing?.brief ?? "", agent: existing?.agent ?? agent };
64129
+ this.singleTaskContextByTask.set(taskId, record), this.singleTaskHistory.has(taskId) || this.singleTaskHistory.set(taskId, { ...record, outcome: "running" }), this.confirmedSingleOrder.includes(taskId) || this.confirmedSingleOrder.push(taskId), this.focusedTaskId = taskId;
64130
+ for (let gateId of gateIds) this.taskByGateId.set(gateId, taskId);
63402
64131
  }
63403
64132
  /** @internal — read a task's accumulated binding user notes. */
63404
64133
  _userNotesForTests(taskId) {
@@ -64247,6 +64976,23 @@ function resolveBrowseUrls(modelUrls, prompt) {
64247
64976
  let usable = modelUrls.filter(isWellFormedHttpUrl);
64248
64977
  return usable.length > 0 ? usable : void 0;
64249
64978
  }
64979
+ function numberedLabel(state, taskId) {
64980
+ let label = taskLabel(state, taskId);
64981
+ return label.startsWith("#") ? label : null;
64982
+ }
64983
+ function findActiveContinuationOfferEntry(conversation) {
64984
+ for (let entry of conversation)
64985
+ if (entry.kind === "gate-prompt" && entry.final === !1 && entry.envelope.promptKind === CONTINUATION_OFFER_HANDOFF_PROMPT_KIND)
64986
+ return entry;
64987
+ return null;
64988
+ }
64989
+ function mobileGateReplyAmbiguity(state, text2) {
64990
+ if (!/^\s*\d+\s*$/.test(text2)) return null;
64991
+ let open15 = state.conversation.filter(
64992
+ (e) => e.kind === "gate-prompt" && e.final === !1 && (e.uiState.phase === "awaiting-number" || e.uiState.phase === "awaiting-notes")
64993
+ );
64994
+ return open15.length <= 1 ? null : `Several reviews are open on the desktop (${[...new Set(open15.map((e) => taskLabel(state, e.envelope.taskId)))].map((l) => `task ${l}`).join(", ")}); a number from the phone cannot choose between them yet \u2014 answer on the desktop, or wait until one review is left.`;
64995
+ }
64250
64996
  var OrchestrationShellStartupError = class extends Error {
64251
64997
  constructor(message, cause) {
64252
64998
  super(message), this.name = "OrchestrationShellStartupError", this.cause = cause;
@@ -64349,11 +65095,21 @@ function wireTeamMergeAuditSummary(store, target) {
64349
65095
  }
64350
65096
  async function runOrchestrationShell(args) {
64351
65097
  let store = createOrchestrationStore({ session: args.session }), originalStoreDispatch = store.dispatch.bind(store);
64352
- store.dispatch = ((action) => (action && action.type === "GATE_PROMPT_RECEIVED" && action.envelope && args.quorumLoop && typeof args.quorumLoop.registerActiveGate == "function" && args.quorumLoop.registerActiveGate(
64353
- action.envelope.taskId,
64354
- action.envelope.gateId,
64355
- action.envelope.currentRound ?? 0
64356
- ), originalStoreDispatch(action)));
65098
+ store.dispatch = ((action) => {
65099
+ action && action.type === "GATE_PROMPT_RECEIVED" && action.envelope && args.quorumLoop && typeof args.quorumLoop.registerActiveGate == "function" && args.quorumLoop.registerActiveGate(
65100
+ action.envelope.taskId,
65101
+ action.envelope.gateId,
65102
+ action.envelope.currentRound ?? 0
65103
+ );
65104
+ let gateAlreadyShown = action && action.type === "GATE_PROMPT_RECEIVED" && action.envelope ? store.getState().conversation.some(
65105
+ (e) => e.kind === "gate-prompt" && (e.envelope.gateId === action.envelope.gateId || e.queue.some((queued) => queued.gateId === action.envelope.gateId))
65106
+ ) : !0, dispatched = originalStoreDispatch(action);
65107
+ if (action && action.type === "GATE_PROMPT_RECEIVED" && action.envelope && !gateAlreadyShown) {
65108
+ let described = typeof args.quorumLoop?.describeTasks == "function" ? args.quorumLoop.describeTasks() : [], advisory = endedTaskAdvisory(described, action.envelope.taskId);
65109
+ advisory && originalStoreDispatch({ type: "SHELL_ADVISORY", source: "shell", text: advisory, localOnly: !0 });
65110
+ }
65111
+ return dispatched;
65112
+ });
64357
65113
  let advisoryAttachmentJournal = advisoryAttachmentJournalFor(args);
64358
65114
  try {
64359
65115
  await advisoryAttachmentJournal.recoverDeadOwners();
@@ -64371,7 +65127,23 @@ async function runOrchestrationShell(args) {
64371
65127
  store.dispatch({ type: "SHELL_ADVISORY", source: "shell", text: message });
64372
65128
  }
64373
65129
  }) : void 0;
64374
- args.workspaceOutcomeTap && (args.workspaceOutcomeTap.sink = workspaceTerminalCoordinator), args.quorumLoop?.setWorkspaceOutcomeSink?.(workspaceTerminalCoordinator), plannerOfferE1Ledger.beginRun(
65130
+ args.workspaceOutcomeTap && (args.workspaceOutcomeTap.sink = workspaceTerminalCoordinator), args.quorumLoop?.setWorkspaceOutcomeSink?.(workspaceTerminalCoordinator), args.quorumLoop?.setTaskOrdinalSink?.({
65131
+ assign: (taskId, opts) => (store.dispatch({ type: "TASK_ORDINAL_ASSIGNED", taskId, kind: opts.kind, ...opts.retryOf ? { retryOf: opts.retryOf } : {} }), store.getState().taskOrdinals.get(taskId)?.ordinal),
65132
+ label: (taskId) => taskLabel(store.getState(), taskId)
65133
+ }), args.quorumLoop?.setApplyConflictSink?.((menu) => {
65134
+ store.dispatch({ type: "APPLY_CONFLICT_PRESENTED", menu });
65135
+ }), args.quorumLoop?.setRetryStartedSink?.((info) => {
65136
+ store.dispatch({
65137
+ type: "TASK_LIFECYCLE",
65138
+ task: {
65139
+ taskId: info.taskId,
65140
+ agentKind: info.agent,
65141
+ pid: 0,
65142
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
65143
+ status: "running"
65144
+ }
65145
+ });
65146
+ }), plannerOfferE1Ledger.beginRun(
64375
65147
  (promptIds) => args.appsyncClient.refreshOpenPromptTtl(promptIds)
64376
65148
  );
64377
65149
  let unsubscribeWaitingUser = null;
@@ -64875,8 +65647,13 @@ async function runOrchestrationShell(args) {
64875
65647
  if (shellSubmissionsFenced) return;
64876
65648
  let fromMobile = options?.fromMobile === !0, convLenBefore = store.getState().conversation.length, userTurnTimestamp = (/* @__PURE__ */ new Date()).toISOString(), turnOwnership = { brainstormPanelOwned: !1 }, ownEntries = [];
64877
65649
  await turnAuthoringContext.run({ ownEntries }, async () => {
64878
- let handledByGate = !1;
64879
- if (fromMobile && findActiveGatePromptEntry(store.getState().conversation) && (mobileGateDecisionDeps === null ? (store.dispatch({
65650
+ let handledByGate = !1, mobileAmbiguity = fromMobile ? mobileGateReplyAmbiguity(store.getState(), text2) : null;
65651
+ if (mobileAmbiguity ? (store.dispatch({
65652
+ type: "SHELL_ADVISORY",
65653
+ source: "shell",
65654
+ text: mobileAmbiguity,
65655
+ handoffExcluded: !0
65656
+ }), handledByGate = !0) : fromMobile && findActiveGatePromptEntry(store.getState().conversation, store.getState().activeGatePromptId) && (mobileGateDecisionDeps === null ? (store.dispatch({
64880
65657
  type: "SHELL_ADVISORY",
64881
65658
  source: "shell",
64882
65659
  text: "The desktop is still preparing the interactive prompt. Please send your choice again.",
@@ -65208,9 +65985,13 @@ async function runOrchestrationShell(args) {
65208
65985
  }
65209
65986
  }));
65210
65987
  }, onTerminalDecision = args.quorumLoop ? async (resolvedTaskId, postAction, expectedShadow, canonicalDecision, gateResolution) => {
65211
- let taskId = resolvedTaskId || args.quorumLoop?.activeTask;
65212
- if (!taskId) return;
65213
- let finalApprovalCancelled = postAction.kind === "final_approval_resolved" && postAction.decision.trim().toLowerCase() !== "approve", terminalAbort = postAction.kind === "abort_task";
65988
+ if (!resolvedTaskId) {
65989
+ logger.warn("[orchestration-shell] terminal decision without a task id \u2014 no shadow effect applied", {
65990
+ kind: postAction.kind
65991
+ });
65992
+ return;
65993
+ }
65994
+ let taskId = resolvedTaskId, finalApprovalCancelled = postAction.kind === "final_approval_resolved" && postAction.decision.trim().toLowerCase() !== "approve", terminalAbort = postAction.kind === "abort_task";
65214
65995
  workspaceTerminalCoordinator?.hasTaskResolution(taskId) && (finalApprovalCancelled || terminalAbort) && await workspaceTerminalCoordinator.reserveTask(
65215
65996
  taskId,
65216
65997
  finalApprovalCancelled ? "CANCEL" : "ABORT"
@@ -65476,7 +66257,7 @@ async function runOrchestrationShell(args) {
65476
66257
  // Codex LOW). Only `handled:false` falls through to the planner.
65477
66258
  resolveGateInput: (text2) => routeGatePromptInput(
65478
66259
  gateDecisionDeps,
65479
- findActiveGatePromptEntry(store.getState().conversation),
66260
+ findActiveGatePromptEntry(store.getState().conversation, store.getState().activeGatePromptId),
65480
66261
  text2
65481
66262
  ),
65482
66263
  signal: nonTtyAbort.signal
@@ -66013,11 +66794,11 @@ async function routeMobileUserPrompt(event, sessionKeyResolver, submit, updateEv
66013
66794
  }
66014
66795
  }
66015
66796
  async function routeMobileGatePromptInput(deps, text2) {
66016
- let before = findActiveGatePromptEntry(deps.store.getState().conversation);
66797
+ let before = findActiveGatePromptEntry(deps.store.getState().conversation, deps.store.getState().activeGatePromptId);
66017
66798
  if (!before) return !1;
66018
66799
  let beforePhase = before.uiState.phase;
66019
66800
  if (!(await routeGatePromptInput(deps, before, text2)).handled) return !1;
66020
- let after = findActiveGatePromptEntry(deps.store.getState().conversation);
66801
+ let after = findActiveGatePromptEntry(deps.store.getState().conversation, deps.store.getState().activeGatePromptId);
66021
66802
  return beforePhase === "awaiting-number" && after?.id === before.id && after.uiState.phase === "awaiting-notes" && deps.store.dispatch({
66022
66803
  type: "SHELL_ADVISORY",
66023
66804
  source: "shell",
@@ -66196,14 +66977,28 @@ function routeTeamShellEventToStore(store, evt) {
66196
66977
  }
66197
66978
  function parseTaskCommand(text2) {
66198
66979
  let rest = text2.trim().slice(5).trim();
66199
- if (rest.length === 0) return { agent: null, brief: "" };
66980
+ if (rest.length === 0) return { agent: null, brief: "", focus: null, conflictChoice: null };
66981
+ let focusMatch = /^focus\s+#?(\d{1,6})$/i.exec(rest);
66982
+ if (focusMatch)
66983
+ return { agent: null, brief: "", focus: Number.parseInt(focusMatch[1], 10), conflictChoice: null };
66984
+ let conflictMatch = /^(retry|discard)\s+#?(\d{1,6})$/i.exec(rest);
66985
+ if (conflictMatch)
66986
+ return {
66987
+ agent: null,
66988
+ brief: "",
66989
+ focus: null,
66990
+ conflictChoice: {
66991
+ choice: conflictMatch[1].toLowerCase() === "retry" ? 1 : 2,
66992
+ ordinal: Number.parseInt(conflictMatch[2], 10)
66993
+ }
66994
+ };
66200
66995
  let tokens = rest.split(/\s+/), agent = null, startIdx = 0;
66201
66996
  if (tokens[0] === "--agent") {
66202
66997
  let value = (tokens[1] ?? "").toUpperCase();
66203
66998
  value === "CLAUDE" || value === "CODEX" || value === "ANTIGRAVITY" ? (agent = value, startIdx = 2) : startIdx = 1;
66204
66999
  }
66205
67000
  let brief = tokens.slice(startIdx).join(" ").trim();
66206
- return { agent, brief };
67001
+ return { agent, brief, focus: null, conflictChoice: null };
66207
67002
  }
66208
67003
  function resolveEnvImplementorAgentOverride() {
66209
67004
  let raw = (process.env.CODEVIBE_IMPLEMENTOR_AGENT ?? "").trim().toUpperCase();
@@ -67000,10 +67795,10 @@ async function launchTeamFromWorkItems(deps) {
67000
67795
  });
67001
67796
  if (result.accepted && result.taskGroupId) {
67002
67797
  let gid = result.taskGroupId;
67003
- store.dispatch({ type: "TEAM_STARTED", taskGroupId: gid }), store.dispatch({
67798
+ store.dispatch({ type: "TEAM_STARTED", taskGroupId: gid }), store.dispatch({ type: "TASK_ORDINAL_ASSIGNED", taskId: gid, kind: "group" }), store.dispatch({
67004
67799
  type: "SHELL_ADVISORY",
67005
67800
  source: "shell",
67006
- text: `Agent Teams group started (${gid}) \u2014 ${result.dispatchedTracks ?? workItems.length} tracks dispatched.`
67801
+ text: `Task ${taskLabel(store.getState(), gid)} started \u2014 Agent Teams group (${gid}), ${result.dispatchedTracks ?? workItems.length} tracks dispatched.`
67007
67802
  });
67008
67803
  try {
67009
67804
  createShellEventEmitter(appsyncClient)({
@@ -67123,10 +67918,10 @@ async function dispatchSynthesizedSingleStartTask(deps) {
67123
67918
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
67124
67919
  status: "running"
67125
67920
  }
67126
- }), note && store.dispatch({
67921
+ }), store.dispatch({
67127
67922
  type: "SHELL_ADVISORY",
67128
67923
  source: "shell",
67129
- text: note
67924
+ text: `Task ${taskLabel(store.getState(), result.taskId)} started with ${selectedAgent}.${note ? ` ${note}` : ""}`
67130
67925
  })) : store.dispatch({
67131
67926
  type: "SHELL_ADVISORY",
67132
67927
  source: "shell",
@@ -67769,6 +68564,12 @@ function buildContinuationActionDeps(args, store) {
67769
68564
  let loop = args.quorumLoop;
67770
68565
  return loop ? {
67771
68566
  getActiveRequestContext: () => loop.getActiveContinuationRequestContext(),
68567
+ // P46a B1 (OQ-1) — when no single task resolves, say why: several tasks are
68568
+ // running a round (list their `#n` so the user can `/task focus <n>`), or none.
68569
+ explainNoActiveRequest: () => {
68570
+ let resolved = loop.resolveContinuationRequest();
68571
+ return resolved.kind === "ambiguous" ? "Several tasks are running \u2014 focus one first with `/task focus <n>`: " + resolved.candidates.map((id) => taskLabel(store.getState(), id)).join(", ") + "." : "No active task to hand off.";
68572
+ },
67772
68573
  request: async (ctx, targetAgent) => loop.requestContinuation({
67773
68574
  taskId: ctx.taskId,
67774
68575
  gateId: ctx.gateId,
@@ -67779,8 +68580,8 @@ function buildContinuationActionDeps(args, store) {
67779
68580
  ...targetAgent ? { targetAgent } : {}
67780
68581
  }),
67781
68582
  getActiveOfferContext: () => {
67782
- let entry = findActiveGatePromptEntry(store.getState().conversation);
67783
- if (!entry || entry.envelope.promptKind !== CONTINUATION_OFFER_HANDOFF_PROMPT_KIND)
68583
+ let entry = findActiveContinuationOfferEntry(store.getState().conversation);
68584
+ if (!entry)
67784
68585
  return null;
67785
68586
  let offerId = entry.envelope.offerId;
67786
68587
  return typeof offerId != "string" || offerId.length === 0 ? null : {
@@ -68067,6 +68868,42 @@ function dispatchRefusalAdvisory(store, decision) {
68067
68868
  function isInteractiveTty() {
68068
68869
  return !!process.stdout.isTTY && process.env.CODEVIBE_NO_TUI !== "1";
68069
68870
  }
68871
+ async function resolveApplyConflict(deps) {
68872
+ let { store, args, pending, choice, emit } = deps;
68873
+ store.dispatch({ type: "CLEAR_PENDING_APPLY_CONFLICT", taskId: pending.taskId });
68874
+ let echo = `\u2192 ${choice}. ${choice === 1 ? "Retry as a new task" : "Discard"}`;
68875
+ emit === "advisory" && store.dispatch({ type: "SHELL_ADVISORY", source: "shell", text: echo, localOnly: !0 });
68876
+ let result = choice === 1 ? await dispatchApplyConflictRetry({ store, quorumLoop: args.quorumLoop, menu: pending }) : `Discarded task ${pending.taskLabel} \u2014 your files are unchanged.`;
68877
+ return emit === "advisory" && store.dispatch({ type: "SHELL_ADVISORY", source: "shell", text: result }), [result];
68878
+ }
68879
+ async function dispatchApplyConflictRetry(deps) {
68880
+ let { store, quorumLoop, menu } = deps;
68881
+ if (!quorumLoop)
68882
+ return "Task start is unavailable in this session.";
68883
+ try {
68884
+ let result = await quorumLoop.startTask({
68885
+ decision: { action: "start_task", rationale: `retry of task ${menu.taskLabel} after an apply conflict` },
68886
+ brief: menu.retryBrief,
68887
+ agent: menu.agent,
68888
+ retryOf: menu.taskId,
68889
+ request: menu.request
68890
+ });
68891
+ return result ? (store.dispatch({
68892
+ type: "TASK_LIFECYCLE",
68893
+ task: {
68894
+ taskId: result.taskId,
68895
+ agentKind: menu.agent,
68896
+ pid: 0,
68897
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
68898
+ status: "running"
68899
+ }
68900
+ }), `Task ${taskLabel(store.getState(), result.taskId)} started with ${menu.agent} \u2014 the retry of task ${menu.taskLabel}.`) : `Couldn't start the retry of task ${menu.taskLabel} (the task START was rejected). Please re-issue the request.`;
68901
+ } catch (err) {
68902
+ return logger.warn("[orchestration-shell] apply-conflict retry dispatch failed (non-fatal)", {
68903
+ error: err.message
68904
+ }), `Couldn't start the retry of task ${menu.taskLabel}: ${err.message}`;
68905
+ }
68906
+ }
68070
68907
  async function resolvePlannerOffer(deps) {
68071
68908
  let { store, args, emitShellEventBound, generator, ensureFreshContextStoreFn, offer, choice } = deps;
68072
68909
  await plannerOfferE1Ledger.resolveOffer(
@@ -68220,7 +69057,38 @@ async function handleShellUserInput(deps) {
68220
69057
  browseSignal = deps.browseSignal
68221
69058
  } = deps;
68222
69059
  if (text2.trim().length === 0) return;
68223
- let slashCommandText = text2.trimStart(), pendingOffer = store.getState().pendingPlannerOffer;
69060
+ let slashCommandText = text2.trimStart(), pendingConflicts = [...store.getState().pendingApplyConflicts.values()];
69061
+ if (pendingConflicts.length > 0 && deps.inputOrigin !== "mobile") {
69062
+ let reply = text2.trim(), openLabels = pendingConflicts.map((m) => `task ${m.taskLabel}`).join(", "), conflictHint = () => {
69063
+ store.dispatch({
69064
+ type: "SHELL_ADVISORY",
69065
+ source: "shell",
69066
+ text: pendingConflicts.length === 1 ? `Reply 1 to retry task ${pendingConflicts[0].taskLabel} as a new task or 2 to discard it (or /task retry <n> / /task discard <n>); other text closes the menu.` : `Several apply-conflict menus are open (${openLabels}) \u2014 answer each with /task retry <n> or /task discard <n>; other text closes them.` + // Stage 1 r3 N2 — a pending suggestion's number is taken by the menus until they close.
69067
+ (store.getState().pendingPlannerOffer ? " The pending suggestion can be answered by number once they are closed." : ""),
69068
+ localOnly: !0
69069
+ });
69070
+ };
69071
+ if (/^[0-9]$/.test(reply)) {
69072
+ let choice = Number.parseInt(reply, 10);
69073
+ if (pendingConflicts.length === 1 && (choice === 1 || choice === 2)) {
69074
+ await resolveApplyConflict({ store, args, pending: pendingConflicts[0], choice, emit: "advisory" });
69075
+ return;
69076
+ }
69077
+ conflictHint();
69078
+ return;
69079
+ }
69080
+ if (/^[0-9]/.test(reply)) {
69081
+ conflictHint();
69082
+ return;
69083
+ }
69084
+ reply.startsWith("/") || (store.dispatch({ type: "CLEAR_PENDING_APPLY_CONFLICT" }), store.dispatch({
69085
+ type: "SHELL_ADVISORY",
69086
+ source: "shell",
69087
+ text: pendingConflicts.length === 1 ? `Closed the apply-conflict menu for task ${pendingConflicts[0].taskLabel} \u2014 no retry was started; re-run the request if you still want that change.` : `Closed the apply-conflict menus for ${openLabels} \u2014 no retry was started; re-run the requests if you still want those changes.`,
69088
+ localOnly: !0
69089
+ }));
69090
+ }
69091
+ let pendingOffer = store.getState().pendingPlannerOffer;
68224
69092
  if (pendingOffer) {
68225
69093
  let interactionAdvisoryFlags = deps.inputOrigin === "mobile" ? { handoffExcluded: !0 } : { localOnly: !0 }, offerReply = text2.trim(), optionCount = pendingOffer.options.length, offerHint = () => {
68226
69094
  store.dispatch({
@@ -68466,7 +69334,12 @@ async function handleShellUserInput(deps) {
68466
69334
  kind: "gated",
68467
69335
  headline: AUDIT_BROWSER_MAX_HEADLINE,
68468
69336
  upgradeHint: AUDIT_BROWSER_UPGRADE_HINT
68469
- }) : renderSessionTaskList(store.getState(), args.quorumLoop?.activeTask ?? null);
69337
+ }) : renderSessionTaskList(
69338
+ store.getState(),
69339
+ // P46a B1 — every live single task (not only "the latest") may
69340
+ // still be inside its `startTask` without a lifecycle row.
69341
+ typeof args.quorumLoop?.knownSingleTasks == "function" ? args.quorumLoop.knownSingleTasks() : args.quorumLoop?.activeTask ? [args.quorumLoop.activeTask] : []
69342
+ );
68470
69343
  else
68471
69344
  try {
68472
69345
  let effectiveSessionId = explicitSessionId || args.session.sessionId, result = runAuditBrowserFn ? await runAuditBrowserFn(taskId, {
@@ -68555,14 +69428,33 @@ async function handleShellUserInput(deps) {
68555
69428
  }), output.output = "") : output.output = loaded.message;
68556
69429
  }
68557
69430
  else if (output.command === "/review-reset" && !output.sideEffect && output.output === "PENDING \u2014 entrypoint arms comprehensive review")
68558
- args.quorumLoop ? args.quorumLoop.requestReviewScopeReset() ? output.output = "The next revise round will start a fresh comprehensive review baseline." : output.output = "/review-reset requires an active task. Start a task before requesting a reset." : output.output = "/review-reset requires an orchestration session with a running loop (Pro/Max). It is unavailable in this session.";
69431
+ if (!args.quorumLoop)
69432
+ output.output = "/review-reset requires an orchestration session with a running loop (Pro/Max). It is unavailable in this session.";
69433
+ else {
69434
+ let reset = args.quorumLoop.requestReviewScopeReset();
69435
+ reset.armed ? output.output = `Task ${taskLabel(store.getState(), reset.taskId)}: the next revise round will start a fresh comprehensive review baseline.` : reset.candidates.length > 0 ? output.output = "The focused task has no review round to reset. Tasks that do: " + reset.candidates.map((id) => taskLabel(store.getState(), id)).join(", ") + " \u2014 run `/task focus <n>` first." : output.output = "/review-reset requires an active task. Start a task before requesting a reset.";
69436
+ }
68559
69437
  else if (output.command === "/task" && !output.sideEffect && output.output === "PENDING \u2014 entrypoint drives startTask")
68560
69438
  if (!args.quorumLoop)
68561
69439
  output.output = "/task requires an orchestration session with a running loop (Pro/Max). It is unavailable in this session.";
68562
69440
  else {
68563
69441
  let parsed = parseTaskCommand(slashCommandText);
68564
- if (!parsed.brief)
68565
- output.output = "Usage: /task [--agent claude|codex|antigravity] <implementation-request>. Pro/Max only; runs without planner classification.";
69442
+ if (parsed.conflictChoice !== null) {
69443
+ let { choice, ordinal } = parsed.conflictChoice, open15 = store.getState().pendingApplyConflicts, target = taskIdForOrdinal(store.getState(), ordinal), pending2 = target === null ? void 0 : open15.get(target);
69444
+ if (deps.inputOrigin === "mobile")
69445
+ output.output = "The apply-conflict menu is answered on the desktop (it is not mirrored to the phone yet).";
69446
+ else if (pending2)
69447
+ output.output = (await resolveApplyConflict({ store, args, pending: pending2, choice, emit: "collect" })).join(`
69448
+ `);
69449
+ else {
69450
+ let openLabels = [...open15.values()].map((m) => `task ${m.taskLabel}`).join(", ");
69451
+ output.output = `No apply-conflict menu is open for task #${ordinal}.` + (open15.size > 0 ? ` Open: ${openLabels}.` : "");
69452
+ }
69453
+ } else if (parsed.focus !== null) {
69454
+ let target = taskIdForOrdinal(store.getState(), parsed.focus);
69455
+ target === null ? output.output = `No task #${parsed.focus} in this session. Run /status to list the tasks.` : args.quorumLoop.setFocusedTask(target) ? output.output = `Focused task ${taskLabel(store.getState(), target)}.` : output.output = `Task ${taskLabel(store.getState(), target)} is not a running single task (it has ended, or it is a team group).`;
69456
+ } else if (!parsed.brief)
69457
+ output.output = "Usage: /task [--agent claude|codex|antigravity] <implementation-request> | /task focus <n> | /task retry <n> | /task discard <n>. Pro/Max only; runs without planner classification.";
68566
69458
  else {
68567
69459
  let detected = typeof args.quorumLoop.getDetectedAgents == "function" ? args.quorumLoop.getDetectedAgents() : [], { agent, note } = selectImplementorAgent(detected, parsed.agent);
68568
69460
  try {
@@ -68582,16 +69474,21 @@ async function handleShellUserInput(deps) {
68582
69474
  agent,
68583
69475
  ...turnAttachments.length ? { attachments: turnAttachments } : {}
68584
69476
  });
68585
- result ? (store.dispatch({
68586
- type: "TASK_LIFECYCLE",
68587
- task: {
68588
- taskId: result.taskId,
68589
- agentKind: agent,
68590
- pid: 0,
68591
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
68592
- status: "running"
68593
- }
68594
- }), output.output = note ? `Started task with ${agent}. ${note}` : `Started task with ${agent}.`) : output.output = "Could not start the task (no session key / START rejected). Check that you are signed in and try again.";
69477
+ if (result) {
69478
+ store.dispatch({
69479
+ type: "TASK_LIFECYCLE",
69480
+ task: {
69481
+ taskId: result.taskId,
69482
+ agentKind: agent,
69483
+ pid: 0,
69484
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
69485
+ status: "running"
69486
+ }
69487
+ });
69488
+ let startedLabel = taskLabel(store.getState(), result.taskId);
69489
+ output.output = note ? `Started task ${startedLabel} with ${agent}. ${note}` : `Started task ${startedLabel} with ${agent}.`;
69490
+ } else
69491
+ output.output = "Could not start the task (no session key / START rejected). Check that you are signed in and try again.";
68595
69492
  } catch (err) {
68596
69493
  output.output = `Failed to start task: ${err.message ?? String(err)}`, logger.warn("[orchestration-shell] /task dispatch failed", {
68597
69494
  error: err.message
@@ -68776,7 +69673,10 @@ async function handleShellUserInput(deps) {
68776
69673
  }, decision, showClassifySpinner = store.getState().progress === null;
68777
69674
  showClassifySpinner && store.dispatch({ type: "TASK_PROGRESS", event: { phase: "planner_classifying" } });
68778
69675
  try {
68779
- decision = await dispatchClassify(plannerInput), isLocalPlannerRuntime(args) && decision.action === "start_task" && plannerInput.clarifications.length === 0 && isDestructiveFileRequest(plannerInput.prompt) && (logger.warn(
69676
+ decision = await dispatchClassify(plannerInput), isLocalPlannerRuntime(args) && decision.action === "browse" && hasFileMutationIntent(plannerInput.prompt) && (decision = {
69677
+ action: "start_task",
69678
+ rationale: decision.rationale ? `${decision.rationale} (promoted to start_task due to file mutation intent)` : "web request with file mutation intent"
69679
+ }), isLocalPlannerRuntime(args) && decision.action === "start_task" && plannerInput.clarifications.length === 0 && isDestructiveFileRequest(plannerInput.prompt) && (logger.warn(
68780
69680
  "[orchestration-shell] P43 backstop: a destructive file request was classified start_task without a confirmation this turn; asking first",
68781
69681
  { rationale: decision.rationale }
68782
69682
  ), decision = {
@@ -69048,10 +69948,10 @@ async function handleShellUserInput(deps) {
69048
69948
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
69049
69949
  status: "running"
69050
69950
  }
69051
- }), note && store.dispatch({
69951
+ }), store.dispatch({
69052
69952
  type: "SHELL_ADVISORY",
69053
69953
  source: "shell",
69054
- text: note
69954
+ text: `Task ${taskLabel(store.getState(), result.taskId)} started with ${selectedAgent}.${note ? ` ${note}` : ""}`
69055
69955
  })) : store.dispatch({
69056
69956
  type: "SHELL_ADVISORY",
69057
69957
  source: "shell",
@@ -69589,17 +70489,20 @@ async function emitWithProvenNoWriteDowngrade(emitter, args) {
69589
70489
  let result = await emitter(args);
69590
70490
  !result.emitted && args.disableWriterAttestation !== !0 && (result.reason === "writer_ineligible" || result.reason === "definitive_rejection") && await emitter({ ...args, disableWriterAttestation: !0 });
69591
70491
  }
69592
- function renderSessionTaskList(state, activeTaskId = null) {
70492
+ function renderSessionTaskList(state, liveTaskIds = []) {
69593
70493
  let tasks = [...state.sessionTasks.values()].sort(
69594
70494
  (a, b) => b.startedAt.localeCompare(a.startedAt)
69595
- ), activeUnlisted = activeTaskId !== null && !state.sessionTasks.has(activeTaskId) ? activeTaskId : null;
69596
- if (tasks.length === 0 && activeUnlisted === null)
70495
+ ), unlisted = liveTaskIds.filter((id) => !state.sessionTasks.has(id));
70496
+ if (tasks.length === 0 && unlisted.length === 0)
69597
70497
  return "No tasks have run in this session yet. Once a task runs, `/audit <task-id>` opens its full review history.";
69598
70498
  let lines = ["Tasks this session (newest first):"];
69599
- activeUnlisted !== null && lines.push(` ${activeUnlisted} \u2014 task \xB7 running \xB7 starting`);
70499
+ for (let id of [...unlisted].reverse()) {
70500
+ let n = numberedLabel(state, id);
70501
+ lines.push(` ${id} \u2014 task${n ? ` ${n}` : ""} \xB7 running \xB7 starting`);
70502
+ }
69600
70503
  for (let t of tasks)
69601
70504
  lines.push(
69602
- ` ${t.taskId} \u2014 ${t.agentKind ?? "team"} \xB7 ${t.status} \xB7 started ${t.startedAt}`
70505
+ ` ${t.taskId} \u2014 ${numberedLabel(state, t.taskId) ? `${numberedLabel(state, t.taskId)} \xB7 ` : ""}${t.agentKind ?? "team"} \xB7 ${t.status} \xB7 started ${t.startedAt}`
69603
70506
  );
69604
70507
  return lines.push("Run `/audit <task-id>` to open a task\u2019s full review history."), lines.join(`
69605
70508
  `);
@@ -69615,7 +70518,20 @@ function parseIsoTimestamp(iso) {
69615
70518
  return Number.isFinite(t) ? t : 0;
69616
70519
  }
69617
70520
  function buildStatusSummary(state, quorumLoop) {
69618
- let lines = [];
70521
+ let lines = [], described = typeof quorumLoop?.describeTasks == "function" ? quorumLoop.describeTasks() : [];
70522
+ if (described.length > 0) {
70523
+ let awaiting = /* @__PURE__ */ new Set();
70524
+ for (let entry of state.conversation)
70525
+ entry.kind === "gate-prompt" && !entry.final && awaiting.add(entry.envelope.taskId);
70526
+ for (let taskId of state.pendingApplyConflicts.keys()) awaiting.add(taskId);
70527
+ lines.push("Tasks this session:");
70528
+ for (let t of described) {
70529
+ let stateLabel = t.outcome === "running" ? awaiting.has(t.taskId) ? "awaiting you" : t.implementorActive ? "implementing" : "in review" : t.outcome === "applied" ? "done" : t.outcome === "apply_failed" ? awaiting.has(t.taskId) ? "apply failed \u2014 awaiting you" : "apply failed" : t.outcome === "unresolved" ? "apply failed \xB7 retained" : t.outcome === "rejected" ? "not started" : "discarded", retry = t.retryOf ? ` \xB7 retry of ${taskLabel(state, t.retryOf)}` : "";
70530
+ lines.push(
70531
+ ` ${t.label} \xB7 ${t.agent} \xB7 ${stateLabel}${retry} \xB7 ${truncate(sanitizeForTerminal(t.brief.replace(/\s+/g, " ").trim()), 60)}` + (t.focused ? " \u25C0 focused" : "")
70532
+ );
70533
+ }
70534
+ }
69619
70535
  if (state.team) {
69620
70536
  let t = state.team, indices = [...t.tracks.keys()].sort((a, b) => a - b), n = indices.length, trackParts = indices.map((i) => {
69621
70537
  let e = t.tracks.get(i), details = [e.agent, e.taskId ? truncate(e.taskId, 12) : null].filter(
@@ -69630,21 +70546,27 @@ function buildStatusSummary(state, quorumLoop) {
69630
70546
  let teamLine = `Agent Teams group${groupRef} \u2014 ${n} ${n === 1 ? "track" : "tracks"} (MergeGate: ${MERGE_GATE_STATUS_LABEL[t.mergeGate]}${mergeElapsedStr})`;
69631
70547
  n > 0 && (teamLine += `: ${trackParts.join(", ")}`), teamLine += ".", t.groupResolved ? teamLine += t.outcome === "complete" ? " Team complete." : ` Team halted \u2014 ${t.outcome ?? t.haltReason ?? "unknown"}.` : t.haltReason && (teamLine += ` Halted: ${t.haltReason}.`), lines.push(teamLine);
69632
70548
  }
69633
- let hasRunning = hasRunningSingleTask(state), activeTask = quorumLoop?.activeTask ?? null, teamActive = state.team !== null && !state.team.groupResolved && state.team.haltReason === null, taskStarting = !hasRunning && !teamActive && activeTask !== null && !state.sessionTasks.has(activeTask);
70549
+ let hasRunning = hasRunningSingleTask(state), startingTasks = (typeof quorumLoop?.knownSingleTasks == "function" ? quorumLoop.knownSingleTasks() : quorumLoop?.activeTask ? [quorumLoop.activeTask] : []).filter((id) => !state.sessionTasks.has(id)), activeTask = startingTasks.length > 0 ? startingTasks[startingTasks.length - 1] : null, teamActive = state.team !== null && !state.team.groupResolved && state.team.haltReason === null, taskStarting = !hasRunning && !teamActive && activeTask !== null;
69634
70550
  if (state.progress) {
69635
70551
  let startedAt = state.progress.startedAt, elapsed = !startedAt || isNaN(Date.parse(startedAt)) ? null : formatElapsed(Date.now() - Date.parse(startedAt)), tokens = state.progress.tokens, tokenStr = tokens && tokens > 0 ? ` \xB7 \u2193 ${formatTokens(tokens)}` : "";
69636
70552
  lines.push(
69637
70553
  `Current task: ${state.progress.text}${elapsed ? ` \xB7 ${elapsed}` : ""}${tokenStr}.`
69638
70554
  );
69639
- } else hasRunning ? lines.push("A task is running.") : taskStarting && lines.push(`A task is starting (${activeTask}).`);
70555
+ } else hasRunning ? lines.push("A task is running.") : taskStarting && lines.push(
70556
+ `A task is starting (${startingTasks.map((id) => numberedLabel(state, id) ? `${numberedLabel(state, id)} ${id}` : id).join(", ")}).`
70557
+ );
69640
70558
  !state.progress && state.lastReviewerProgress && lines.push(`Reviewers: ${state.lastReviewerProgress.text}.`);
69641
70559
  for (let entry of state.conversation) {
69642
70560
  if (entry.kind !== "gate-prompt" || entry.final) continue;
69643
70561
  let label = GATE_PROMPT_KIND_LABEL[entry.envelope.promptKind] ?? entry.envelope.promptKind, queued = entry.queue.length > 0 ? `; ${entry.queue.length} more queued` : "";
69644
70562
  lines.push(
69645
- `Awaiting your decision: ${label} for task ${entry.envelope.taskId} \u2014 reply with an option number (1-${entry.envelope.options.length})${queued}.`
70563
+ `Awaiting your decision: ${label} for task ${numberedLabel(state, entry.envelope.taskId) ? `${numberedLabel(state, entry.envelope.taskId)} (${entry.envelope.taskId})` : entry.envelope.taskId} \u2014 reply with an option number (1-${entry.envelope.options.length})${queued}.`
69646
70564
  );
69647
70565
  }
70566
+ for (let menu of state.pendingApplyConflicts.values())
70567
+ lines.push(
70568
+ `Awaiting your decision: task ${menu.taskLabel} could not be applied \u2014 ` + (state.pendingApplyConflicts.size === 1 ? "reply 1 to retry as a new task or 2 to discard (or /task retry|discard <n>)." : `/task retry ${menu.taskLabel.replace(/^#/, "").split(" ")[0]} or /task discard ${menu.taskLabel.replace(/^#/, "").split(" ")[0]}.`)
70569
+ );
69648
70570
  let rawOutcome = quorumLoop?.getLastWorkspaceOutcome() ?? null, outcome = null;
69649
70571
  if (rawOutcome !== null && !teamActive)
69650
70572
  if (rawOutcome.taskId) {
@@ -72365,6 +73287,14 @@ async function assertAuditOutsideWorkdir(auditPath, workdir) {
72365
73287
  return resolvedAuditPath;
72366
73288
  }
72367
73289
  async function engageSubstrate(input) {
73290
+ if (input.webBrowsingActive)
73291
+ return {
73292
+ mode: "reduced_trust",
73293
+ reducedTrust: !0,
73294
+ reducedTrustReason: `Agent web browsing active for ${input.agentKind} \u2014 running in reduced-trust mode with shadow-diff capture and guarded web tools`,
73295
+ teardown: async () => {
73296
+ }
73297
+ };
72368
73298
  let provider = providerForAgent(input.agentKind);
72369
73299
  if (provider === null)
72370
73300
  return input.requireConfined ? engageWriteConfinedResolverSandbox(input) : {
@@ -74831,14 +75761,10 @@ function buildAndArmQuorumLoop(args) {
74831
75761
  // manifest BEFORE the tree mutates (the C1 capture is already gated on
74832
75762
  // `this.deps.durableStore` inside `promoteShadowLocked`).
74833
75763
  ...args.durableStore ? { durableStore: args.durableStore } : {},
74834
- // CP-7 Stage-2 r3 (Codex) HIGH ARM the LE's current/authorized task id at
74835
- // start_task so the round-0 implementor spawn engages the CP-7 substrate
74836
- // (sandbox + creditless badge) instead of the pre-TaskAuthorized
74837
- // reduced-trust path (local-executor-impl.ts:566). Rolled back on a START
74838
- // rejection by the loop. This is the SAME `taskId` state TaskAuthorized later
74839
- // advances; arming it early closes the round-0 window where the implementor
74840
- // would otherwise spawn unbadged/unsandboxed.
74841
- armLeTaskId: (taskId) => localExecutor.setActiveTaskId(taskId),
75764
+ // P46a B2 the executor keeps no singleton task id any more (the CP-7
75765
+ // `armLeTaskId` arming is gone): every implementor spawn carries its own
75766
+ // `taskId`, which engages the CP-7 substrate and resolves the per-task
75767
+ // authority scope, single tasks included.
74842
75768
  // CP-7 §8 (Stage-1-resolved) — wire the REVIEWER substrate engager so each
74843
75769
  // Trusted-agent reviewer seat runs INSIDE the CP-7 sandbox + broker (no
74844
75770
  // ambient vendor creds; model call via the loopback broker), exactly like
@@ -74904,6 +75830,7 @@ async function buildQuorumLoopExecutor(args) {
74904
75830
  userContextFn: async () => ""
74905
75831
  }, localExecutor = new LocalExecutorImpl({
74906
75832
  sessionId: session.sessionId,
75833
+ logger,
74907
75834
  // CP-1.f Hybrid authority split (mirrors the team LE) — the DESKTOP seeds
74908
75835
  // the implementor command allowlist so `enforceCommand` permits the spawn;
74909
75836
  // write/read/network stay empty (the engine authorizes PATH scopes via a
@@ -75145,6 +76072,7 @@ async function buildTeamLocalExecutor(args) {
75145
76072
  userContextFn: async () => ""
75146
76073
  }, localExecutor = new LocalExecutorImpl({
75147
76074
  sessionId: session.sessionId,
76075
+ logger,
75148
76076
  initialScope,
75149
76077
  baseCtx: {
75150
76078
  sessionId: session.sessionId,