@quantiya/codevibe-claude-plugin 2.0.42 → 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 (19) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/node_modules/@quantiya/codevibe-core/dist/index.js +446 -437
  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 +52 -20
  6. package/node_modules/@quantiya/codevibe-core/dist/local-executor/types.d.ts +7 -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 +1190 -359
  9. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/components/InputBar.d.ts +1 -0
  10. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/gate-decision-submit.d.ts +26 -5
  11. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/index.d.ts +17 -0
  12. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/non-tty-fallback.d.ts +1 -1
  13. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/quorum-loop.d.ts +223 -51
  14. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/reducer.d.ts +7 -0
  15. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/slash-routes/continuation.d.ts +5 -0
  16. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/task-label.d.ts +25 -0
  17. package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/types.d.ts +83 -0
  18. package/node_modules/@quantiya/codevibe-core/package.json +1 -1
  19. 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,
@@ -26444,7 +26584,7 @@ async function requestSubcommand(deps, rawTarget) {
26444
26584
  return { exitCode: 1, stdout: "Continuation request is unavailable in this session." };
26445
26585
  let ctx = deps.getActiveRequestContext();
26446
26586
  if (!ctx)
26447
- return { exitCode: 1, stdout: "No active task to hand off." };
26587
+ return { exitCode: 1, stdout: deps.explainNoActiveRequest?.() ?? "No active task to hand off." };
26448
26588
  try {
26449
26589
  return await deps.request(ctx, targetAgent) ? {
26450
26590
  exitCode: 0,
@@ -31786,8 +31926,8 @@ var HookBridge = class {
31786
31926
  // EXECUTOR_REFUSAL (Class A audit) and THEN LOCAL_AUTHORITY_REFUSAL (shell-
31787
31927
  // visible). Reverse order would race the user-visible message ahead of the
31788
31928
  // audit trail. Tests assert the ordering invariant.
31789
- async bridgeAuthorityRefusal(err, args) {
31790
- let ctx = this.contextOrNull();
31929
+ async bridgeAuthorityRefusal(err, args, taskCtx) {
31930
+ let ctx = taskCtx ?? this.contextOrNull();
31791
31931
  ctx !== null && await this.deps.emitter.emitExecutorRefusal(ctx, {
31792
31932
  refusal: err.refusal,
31793
31933
  refusedMessageId: args.refusedMessageId
@@ -32369,7 +32509,7 @@ var ClassBConsumer = class {
32369
32509
  }
32370
32510
  }
32371
32511
  async handlePolicyRejection(packet, envelopeTaskId) {
32372
- 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";
32373
32513
  await this.deps.emitter.emitExecutorRefusal(ctx, {
32374
32514
  refusal: {
32375
32515
  category: "policy_rejection",
@@ -38624,6 +38764,23 @@ var WorkspaceShadow = class _WorkspaceShadow {
38624
38764
  )
38625
38765
  );
38626
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
+ }
38627
38784
  /** Read the crash-durable reviewed artifact, or null for an unreviewed snapshot. */
38628
38785
  async readReviewedDiff() {
38629
38786
  return (await this.readStateMarker())?.reviewedDiff?.map((file) => ({ ...file })) ?? null;
@@ -47534,7 +47691,15 @@ function makeNoopTrackHandle(agentKind) {
47534
47691
  }
47535
47692
  var LocalExecutorImpl = class {
47536
47693
  constructor(deps) {
47537
- 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();
47538
47703
  // CP-12 W2.b — per-track registry (idempotent N-track spawn) + a snapshot of
47539
47704
  // the most-recent TrackAssigned per track (for state-dir + scope on
47540
47705
  // MergeGate). EMPTY for single-track sessions (never touched).
@@ -47578,7 +47743,7 @@ var LocalExecutorImpl = class {
47578
47743
  // re-drain from a duplicate without relying on wall-clock resolution.
47579
47744
  this._lastMergeGateEmitMs = 0;
47580
47745
  this.memberReviewEvidenceByTask = /* @__PURE__ */ new Map();
47581
- 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) => {
47582
47747
  this.logger.warn("[LocalExecutorImpl] startup background replay of pending team deliveries failed (non-fatal)", {
47583
47748
  err: err?.message
47584
47749
  });
@@ -47606,8 +47771,8 @@ var LocalExecutorImpl = class {
47606
47771
  )), this.bridge = new HookBridge({
47607
47772
  emitter: this.emitter,
47608
47773
  emitShellEvent: this.emitShellEvent,
47609
- getAuthorityScope: () => this.scope,
47610
- getCurrentTaskId: () => this.taskId,
47774
+ getAuthorityScope: () => this.baseScope,
47775
+ getCurrentTaskId: () => null,
47611
47776
  getContextWithoutTask: () => this.baseCtx,
47612
47777
  adapter: this.adapter
47613
47778
  }), this.classBConsumer = new ClassBConsumer({
@@ -47615,7 +47780,7 @@ var LocalExecutorImpl = class {
47615
47780
  emitShellEvent: this.emitShellEvent,
47616
47781
  getContext: () => this.contextOrFallback(),
47617
47782
  advanceAuthorityScope: (newScope, taskId) => {
47618
- this.scope = mergeAuthorityScope(this.scope, newScope), this.taskId = taskId;
47783
+ this.scopeByTask.set(taskId, mergeAuthorityScope(this.baseScope, newScope));
47619
47784
  },
47620
47785
  notifyPolicyRejection: (detail, category, recommendedRecovery, rejectedTaskId) => {
47621
47786
  this.emitShellEvent({
@@ -47707,19 +47872,20 @@ var LocalExecutorImpl = class {
47707
47872
  * is responsible for routing to `bridgeAuthorityErrorToShellRefusal` /
47708
47873
  * `HookBridge.bridgeAuthorityRefusal` per LOCK #C4-3.
47709
47874
  */
47710
- async enforceAuthority(action) {
47875
+ async enforceAuthority(action, taskId) {
47876
+ let scope = this.scopeFor(taskId);
47711
47877
  switch (action.kind) {
47712
47878
  case "Write":
47713
- await enforceWrite(this.scope, action.path);
47879
+ await enforceWrite(scope, action.path);
47714
47880
  return;
47715
47881
  case "Read":
47716
- await enforceRead(this.scope, action.path);
47882
+ await enforceRead(scope, action.path);
47717
47883
  return;
47718
47884
  case "Network":
47719
- enforceNetwork(this.scope);
47885
+ enforceNetwork(scope);
47720
47886
  return;
47721
47887
  case "Command":
47722
- enforceCommand(this.scope, action.argv);
47888
+ enforceCommand(scope, action.argv);
47723
47889
  return;
47724
47890
  default: {
47725
47891
  let _exhaustive = action;
@@ -47728,36 +47894,64 @@ var LocalExecutorImpl = class {
47728
47894
  }
47729
47895
  }
47730
47896
  /**
47731
- * 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).
47732
47909
  * Returns a deep-frozen copy so callers cannot mutate the LE's state.
47733
47910
  */
47734
47911
  authorityScope() {
47735
47912
  return Object.freeze({
47736
- writeScopes: [...this.scope.writeScopes],
47737
- readScopes: [...this.scope.readScopes],
47738
- networkAllowed: this.scope.networkAllowed,
47739
- commandAllowlist: [...this.scope.commandAllowlist],
47740
- 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
47741
47933
  });
47742
47934
  }
47743
47935
  // --- Authority surface (re-exports through this class for the bound scope) ---
47744
- enforceCommand(argv) {
47745
- 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);
47746
47940
  }
47747
- enforceNetwork() {
47748
- enforceNetwork(this.scope);
47941
+ enforceNetwork(taskId) {
47942
+ enforceNetwork(this.scopeFor(taskId));
47749
47943
  }
47750
- async enforceWrite(p) {
47751
- return enforceWrite(this.scope, p);
47944
+ async enforceWrite(p, taskId) {
47945
+ return enforceWrite(this.scopeFor(taskId), p);
47752
47946
  }
47753
- async enforceRead(p) {
47754
- return enforceRead(this.scope, p);
47947
+ async enforceRead(p, taskId) {
47948
+ return enforceRead(this.scopeFor(taskId), p);
47755
47949
  }
47756
- async safeWriteFile(p, data) {
47757
- return safeWriteFile(this.scope, p, data);
47950
+ async safeWriteFile(p, data, taskId) {
47951
+ return safeWriteFile(this.scopeFor(taskId), p, data);
47758
47952
  }
47759
- async safeReadFile(p, opts) {
47760
- return safeReadFile(this.scope, p, opts);
47953
+ async safeReadFile(p, opts, taskId) {
47954
+ return safeReadFile(this.scopeFor(taskId), p, opts);
47761
47955
  }
47762
47956
  // --- Spawn surface (wires lifecycle hooks to the emitter) ---
47763
47957
  // Per master CP-1 §9:1281-1283 — all three spawn methods take full
@@ -47788,11 +47982,11 @@ var LocalExecutorImpl = class {
47788
47982
  signal: advisoryAdmission.controller.signal
47789
47983
  });
47790
47984
  try {
47791
- let {
47985
+ let spawnTaskScope = this.scopeFor(promptBoundArgs.taskId ?? null), {
47792
47986
  args: launchArgs,
47793
47987
  onExit,
47794
47988
  substrateEngaged
47795
- } = await this.engageSubstrateForSpawn(promptBoundArgs, this.scope), outerBoundaryConfines = substrateEngaged || isTrustedContainerBoundary(), finalArgs = {
47989
+ } = await this.engageSubstrateForSpawn(promptBoundArgs, spawnTaskScope), outerBoundaryConfines = substrateEngaged || isTrustedContainerBoundary(), finalArgs = {
47796
47990
  ...launchArgs,
47797
47991
  argv: applyOuterBoundarySandbox(
47798
47992
  relocateCodexLastMessageForSubstrate(
@@ -47803,7 +47997,7 @@ var LocalExecutorImpl = class {
47803
47997
  launchArgs.agentKind ?? this.adapter,
47804
47998
  outerBoundaryConfines
47805
47999
  )
47806
- }, spawnScope = outerBoundaryConfines ? withBoundaryConfinedCommandAuthority(this.scope) : this.scope, handle = await this.spawnWithLifecycle(
48000
+ }, spawnScope = outerBoundaryConfines ? withBoundaryConfinedCommandAuthority(spawnTaskScope) : spawnTaskScope, handle = await this.spawnWithLifecycle(
47807
48001
  finalArgs,
47808
48002
  (a) => spawnImplementor(spawnScope, a),
47809
48003
  onExit
@@ -47821,7 +48015,7 @@ var LocalExecutorImpl = class {
47821
48015
  async spawnHealthProbe(args) {
47822
48016
  if (this.workspaceShutdown)
47823
48017
  throw new Error("LocalExecutor session shutdown has fenced new health probes");
47824
- return this.spawnWithLifecycle(args, (a) => spawnHealthProbe(this.scope, a));
48018
+ return this.spawnWithLifecycle(args, (a) => spawnHealthProbe(this.baseScope, a));
47825
48019
  }
47826
48020
  /**
47827
48021
  * Publish advisory ownership synchronously before the first spawn await.
@@ -47924,7 +48118,7 @@ var LocalExecutorImpl = class {
47924
48118
  );
47925
48119
  return { args, substrateEngaged: !1 };
47926
48120
  }
47927
- let effectiveTaskId = args.taskId ?? this.taskId;
48121
+ let effectiveTaskId = args.taskId ?? null;
47928
48122
  if (effectiveTaskId === null) {
47929
48123
  if (args.requireConfined)
47930
48124
  throw new SpawnConfinementUnavailable(
@@ -48075,7 +48269,7 @@ var LocalExecutorImpl = class {
48075
48269
  }
48076
48270
  }
48077
48271
  async spawnWithLifecycle(args, spawnFn, onSubstrateExit) {
48078
- 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 () => {
48079
48273
  onSubstrateExit && (substrateCleanupPromise || (substrateCleanupPromise = onSubstrateExit().catch((error) => {
48080
48274
  throw substrateCleanupError = error, error;
48081
48275
  })), await substrateCleanupPromise);
@@ -48128,11 +48322,15 @@ var LocalExecutorImpl = class {
48128
48322
  ...err.refusal,
48129
48323
  detail: safeDetail
48130
48324
  });
48131
- await this.bridge.bridgeAuthorityRefusal(safeError, {
48132
- refusedMessageId: "spawn:" + role + ":" + Date.now(),
48133
- shellContent: `Refused spawn (${role}): ${safeDetail}`,
48134
- shellMetadata: { role, argv: safeArgv }
48135
- }), 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;
48136
48334
  }
48137
48335
  throw substrateCleanupError !== void 0 ? Object.assign(
48138
48336
  new Error("implementor spawn failed and substrate cleanup also failed"),
@@ -49531,11 +49729,12 @@ var LocalExecutorImpl = class {
49531
49729
  ];
49532
49730
  return {
49533
49731
  writeScopes,
49534
- // Reads cover the shared workspace (current read scopes) + own writes.
49535
- readScopes: [...this.scope.readScopes, ...writeScopes],
49536
- networkAllowed: this.scope.networkAllowed,
49537
- commandAllowlist: [...this.scope.commandAllowlist],
49538
- 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
49539
49738
  };
49540
49739
  }
49541
49740
  /**
@@ -49661,13 +49860,18 @@ var LocalExecutorImpl = class {
49661
49860
  }
49662
49861
  }
49663
49862
  // --- Test seams ------------------------------------------------------------
49664
- /** Set the active taskId (production: CP-1.e on TASK_AUTHORIZED). */
49665
- setActiveTaskId(taskId) {
49666
- this.taskId = taskId;
49667
- }
49668
- /** 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
+ */
49669
49869
  setAuthorityScope(scope) {
49670
- 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));
49671
49875
  }
49672
49876
  /** Expose emitter for §8.5 integration test introspection. */
49673
49877
  get emitterForTests() {
@@ -49685,21 +49889,18 @@ var LocalExecutorImpl = class {
49685
49889
  get trackHeartbeatCountForTests() {
49686
49890
  return this.trackHeartbeats.size;
49687
49891
  }
49688
- contextOrNull() {
49689
- return this.taskId === null ? null : { taskId: this.taskId, ...this.baseCtx };
49690
- }
49691
49892
  /**
49692
49893
  * Context resolver for ClassBConsumer emit paths (refusals + PolicyRejection).
49693
- * Class B inbound packets MAY arrive before a TaskAuthorized has set
49694
- * `this.taskId` (in fact, TaskAuthorized is the variant that SETS it). For
49695
- * the signature_invalid / ulid_replay / policy_rejection / malformed
49696
- * refusal-audit emit path we still need a valid envelope context, so we
49697
- * fall back to `pending` when no task is active. CP-2 may refine this; for
49698
- * 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`).
49699
49900
  */
49700
49901
  contextOrFallback() {
49701
49902
  return {
49702
- taskId: this.taskId ?? "pending",
49903
+ taskId: "pending",
49703
49904
  ...this.baseCtx
49704
49905
  };
49705
49906
  }
@@ -52101,6 +52302,97 @@ function resolveTeamExecution(_override, _env = process.env) {
52101
52302
  // src/orchestration-shell/quorum-loop.ts
52102
52303
  init_env_scrub();
52103
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
+
52104
52396
  // src/orchestration-shell/context-items.ts
52105
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");
52106
52398
  init_logger2();
@@ -53976,97 +54268,6 @@ function renderRepoSliceCompact(repos, maxChars) {
53976
54268
  var fs40 = __toESM(require("fs/promises")), path58 = __toESM(require("path"));
53977
54269
  init_logger2();
53978
54270
 
53979
- // src/credential-broker/scrubber.ts
53980
- var REDACTION = "[REDACTED-CP7]", KEY_REDACTION = "[REDACTED-CP7-KEY]", SECRET_PATTERNS = [
53981
- {
53982
- // PEM private-key block (RSA / EC / OPENSSH / generic PRIVATE KEY).
53983
- patternClass: "private_key_pem",
53984
- regex: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY-----/g
53985
- },
53986
- {
53987
- // Anthropic API key: sk-ant-... .
53988
- patternClass: "anthropic_api_key",
53989
- regex: /sk-ant-[A-Za-z0-9_-]{20,}/g
53990
- },
53991
- {
53992
- // OpenAI API key: sk-... or sk-proj-... (>= 20 trailing chars).
53993
- patternClass: "openai_api_key",
53994
- regex: /sk-(?:proj-)?[A-Za-z0-9_-]{20,}/g
53995
- },
53996
- {
53997
- // AWS access key id (AKIA / ASIA + 16 uppercase alphanumerics).
53998
- patternClass: "aws_access_key_id",
53999
- regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g
54000
- },
54001
- {
54002
- // AWS secret access key VALUE assignment — a 40-char base64-ish secret
54003
- // bound to an `aws_secret_access_key`/`AWS_SECRET_ACCESS_KEY` key. Only
54004
- // the secret value is redacted, not the assignment label.
54005
- patternClass: "aws_secret_access_key",
54006
- regex: /(aws_secret_access_key|AWS_SECRET_ACCESS_KEY)(\s*[=:]\s*["']?)([A-Za-z0-9/+]{40})(["']?)/g
54007
- },
54008
- {
54009
- // GitHub tokens (ghp_/gho_/ghu_/ghs_/ghr_ + 36 chars).
54010
- patternClass: "github_token",
54011
- regex: /\bgh[pousr]_[A-Za-z0-9]{36}\b/g
54012
- },
54013
- {
54014
- // Authorization: Bearer <token> embedded in content (a bearer token
54015
- // riding in a model-bound field). Redacts the token, keeps the scheme.
54016
- patternClass: "bearer_token",
54017
- regex: /(Bearer\s+)([A-Za-z0-9._~+/=-]{16,})/g
54018
- }
54019
- ];
54020
- function scrubString(value) {
54021
- return redactSecretShapes(value, REDACTION);
54022
- }
54023
- function redactSecretShapes(value, placeholder) {
54024
- let redacted = value, classes = [];
54025
- for (let pattern of SECRET_PATTERNS)
54026
- 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));
54027
- return { redacted, classes };
54028
- }
54029
- function redactSecretShapesInText(value, placeholder = "[redacted secret]") {
54030
- return redactSecretShapes(value, placeholder).redacted;
54031
- }
54032
- function keyIsSecret(key) {
54033
- for (let pattern of SECRET_PATTERNS) {
54034
- pattern.regex.lastIndex = 0;
54035
- let hit = pattern.regex.test(key);
54036
- if (pattern.regex.lastIndex = 0, hit) return !0;
54037
- }
54038
- return !1;
54039
- }
54040
- function joinPath(base, key) {
54041
- return typeof key == "number" ? `${base}[${key}]` : /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ? `${base}.${key}` : `${base}['${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}']`;
54042
- }
54043
- function scrubValue(value, path75, findings) {
54044
- if (typeof value == "string") {
54045
- let { redacted, classes } = scrubString(value);
54046
- for (let patternClass of classes)
54047
- findings.push({ field: path75, patternClass });
54048
- return redacted;
54049
- }
54050
- if (Array.isArray(value))
54051
- return value.map((item, i) => scrubValue(item, joinPath(path75, i), findings));
54052
- if (value !== null && typeof value == "object") {
54053
- let out = /* @__PURE__ */ Object.create(null), redactedKeyCount = 0;
54054
- for (let [k, v] of Object.entries(value))
54055
- if (keyIsSecret(k)) {
54056
- redactedKeyCount += 1;
54057
- let placeholder = `${KEY_REDACTION}-${redactedKeyCount}`, placeholderPath = joinPath(path75, placeholder);
54058
- findings.push({ field: placeholderPath, patternClass: "secret_object_key" }), out[placeholder] = scrubValue(v, placeholderPath, findings);
54059
- } else
54060
- out[k] = scrubValue(v, joinPath(path75, k), findings);
54061
- return out;
54062
- }
54063
- return value;
54064
- }
54065
- function scrubRequestBody(body) {
54066
- let findings = [];
54067
- return { scrubbed: scrubValue(body, "$", findings), findings };
54068
- }
54069
-
54070
54271
  // src/orchestration-shell/user-rules.ts
54071
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(
54072
54273
  `(?:^|\\n)${WORKFLOW_HANDOFF_SECTION_LABEL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\nSource: `
@@ -57014,10 +57215,74 @@ function teamRound0GateId(taskId, taskGroupId, trackIndex, dispatchGeneration) {
57014
57215
  TEAM_ROUND0_GATE_NAMESPACE
57015
57216
  );
57016
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
+ }
57017
57263
  var QuorumLoop = class _QuorumLoop {
57018
57264
  constructor(deps) {
57019
- /** The active task this desktop is driving (set at start_task). */
57020
- 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 = [];
57021
57286
  /** Tasks whose next automatic revise round must start a fresh review baseline. */
57022
57287
  this.reviewScopeResetTasks = /* @__PURE__ */ new Set();
57023
57288
  /** Non-terminal tasks for which `/review-reset` may arm the next revise round. */
@@ -57041,16 +57306,9 @@ var QuorumLoop = class _QuorumLoop {
57041
57306
  // only per QuorumLoop life; a `0`/absent entry omits the ` · ↓ <tokens>` segment.
57042
57307
  this.tokensByTaskId = /* @__PURE__ */ new Map();
57043
57308
  this.tokensCountedGates = /* @__PURE__ */ new Set();
57044
- /**
57045
- * The implementor brief for the active task (set at start_task). The
57046
- * GATE_DISPATCH consumer reads this to drive the round-0 implementor spawn —
57047
- * the brief is no longer threaded inline through the spawn call (the spawn
57048
- * moved out of `startTask` into the GATE_DISPATCH consumer, §3.C.20 FIX).
57049
- */
57050
- this.activeBrief = null;
57051
57309
  /**
57052
57310
  * IMAGE-ATTACHMENT-DESIGN.md §5 — TASK-SCOPED image attachments (parallel to
57053
- * `activeBrief`/`activeBriefByTask`). The single-impl path uses `activeAttachments`;
57311
+ * `singleTaskContextByTask`/`activeBriefByTask`). The single-impl path uses `activeAttachments`;
57054
57312
  * the team path keys by the child `taskId` in `activeAttachmentsByTask` (armed at
57055
57313
  * `registerTeamTaskBrief`, before its buffered GATE_DISPATCH drains). Re-copied
57056
57314
  * into EACH round's shadow at `runImplementorRound` (round-0, revise, continuation,
@@ -57105,20 +57363,44 @@ var QuorumLoop = class _QuorumLoop {
57105
57363
  this.recoveredBriefsByAuthority = /* @__PURE__ */ new Map();
57106
57364
  this.activeBriefByTask = /* @__PURE__ */ new Map();
57107
57365
  /**
57108
- * P45 D1 (M7 E2E step 8, 2026-09-14) — the brief + agent of EVERY single-
57109
- * implementor task this loop started, keyed by task id. `activeTaskId` /
57110
- * `activeBrief` / `activeImplementorAgent` are the LATEST task only: when a
57111
- * second single task started while the first still ran (the planner routed a
57112
- * mid-task turn as a task), the first task's revise round was composed from
57113
- * the second task's request (`buildReviseBrief` fell back to `activeBrief`)
57114
- * and its round-0 dispatch whose envelope names the FIRST task — would have
57115
- * been mis-read as a team dispatch. Round-0 spawns, revise rounds and the
57116
- * continuation context resolve through this map first; the singletons stay
57117
- * as the legacy fallback (an empty envelope task id, the test seam). Entries
57118
- * 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).
57119
57374
  */
57120
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). */
57121
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 = [];
57122
57404
  /**
57123
57405
  * CP-1.f revise-context fix (dogfood task 2ac68708, 2026-06-11) — the BINDING
57124
57406
  * user-requested changes for a task, accumulated across ALL revise rounds.
@@ -57581,8 +57863,6 @@ var QuorumLoop = class _QuorumLoop {
57581
57863
  this.lastWorkspaceOutcome = null;
57582
57864
  // ─── §3.C.18 — real Ed25519 signature verify (cached key) ───────────────
57583
57865
  this.cachedSigningKey = null;
57584
- /** The implementor agent the loop spawns (set at start_task). */
57585
- this.activeImplementorAgent = "CLAUDE";
57586
57866
  this.deps = deps, this.sleep = deps.sleep ?? realSleep2, this.loadCompletedSummariesFromDisk();
57587
57867
  }
57588
57868
  getCompletedSummariesPath() {
@@ -58171,13 +58451,95 @@ var QuorumLoop = class _QuorumLoop {
58171
58451
  isTaskTerminallyRetired(taskId) {
58172
58452
  return this.isWorkspaceTaskTerminallyRetired(taskId);
58173
58453
  }
58174
- /** 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
+ */
58175
58459
  get activeTask() {
58176
- 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;
58484
+ }
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;
58177
58514
  }
58178
- /** Arm an explicit, one-shot comprehensive baseline for the active task. */
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
+ */
58179
58538
  requestReviewScopeReset() {
58180
- 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
+ ) };
58181
58543
  }
58182
58544
  /**
58183
58545
  * Audited-path fix (Fix 5) — the host's detected implementor agents
@@ -58359,8 +58721,7 @@ var QuorumLoop = class _QuorumLoop {
58359
58721
  return logger.warn("[QuorumLoop] startTask aborted \u2014 no session key", {
58360
58722
  sessionId: this.deps.session.sessionId
58361
58723
  }), null;
58362
- 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;
58363
- this.activeTaskId = taskId, this.activeBrief = args.brief, appendContextItem(this.deps.session.sessionId, {
58724
+ appendContextItem(this.deps.session.sessionId, {
58364
58725
  kind: "task_spec",
58365
58726
  author: { role: "engine" },
58366
58727
  sensitivity: "user",
@@ -58368,7 +58729,14 @@ var QuorumLoop = class _QuorumLoop {
58368
58729
  task_id: taskId,
58369
58730
  body: { text: args.brief }
58370
58731
  }).catch(() => {
58371
- }), 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;
58372
58740
  let workflowState;
58373
58741
  try {
58374
58742
  workflowState = (await this.deps.appsyncClient.startTask({
@@ -58381,31 +58749,40 @@ var QuorumLoop = class _QuorumLoop {
58381
58749
  // an empty `[]` (host detected nothing) is sent verbatim →
58382
58750
  // Claude-only fail-safe.
58383
58751
  availableAgents: this.deps.detectedAgents,
58384
- implementorAgent: this.activeImplementorAgent.toLowerCase()
58752
+ implementorAgent: implementorAgent.toLowerCase()
58385
58753
  })).workflowState;
58386
58754
  } catch (err) {
58387
- let wasNewest = this.activeTaskId === taskId, restoreTo = previousActive && this.singleTaskContextByTask.has(previousActive.taskId) ? previousActive : null;
58388
- wasNewest && (this.activeTaskId = restoreTo?.taskId ?? null, this.activeBrief = restoreTo?.brief ?? null, restoreTo && (this.activeImplementorAgent = restoreTo.agent)), this.singleTaskContextByTask.delete(taskId), this.webAccessByTask.delete(taskId), this.startTasksInFlight.delete(taskId);
58755
+ let wasLatestStart2 = this.latestInFlightStart === taskId;
58756
+ this.forgetSingleTask(taskId, "rejected"), this.startTasksInFlight.delete(taskId), this.dropInFlightStart(taskId);
58389
58757
  for (let i = this.pendingGateDispatches.length - 1; i >= 0; i--) {
58390
58758
  let pending = this.pendingGateDispatches[i];
58391
- (pending.envelopeTaskId === taskId || !pending.envelopeTaskId && wasNewest) && this.pendingGateDispatches.splice(i, 1);
58759
+ (pending.envelopeTaskId === taskId || !pending.envelopeTaskId && wasLatestStart2) && this.pendingGateDispatches.splice(i, 1);
58392
58760
  }
58393
- 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", {
58394
58762
  err: err.message
58395
58763
  }), null;
58396
58764
  }
58397
- 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", {
58398
58773
  taskId,
58774
+ ...ordinal !== void 0 ? { ordinal } : {},
58775
+ ...args.retryOf ? { retryOf: args.retryOf } : {},
58399
58776
  workflowState,
58400
58777
  buffered: this.pendingGateDispatches.length
58401
58778
  });
58402
58779
  let drained = [];
58403
58780
  for (let i = this.pendingGateDispatches.length - 1; i >= 0; i--) {
58404
58781
  let entry = this.pendingGateDispatches[i];
58405
- (!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]);
58406
58783
  }
58407
58784
  for (let entry of drained)
58408
- await this.handleGateDispatch(entry.payload, entry.envelopeTaskId);
58785
+ await this.handleGateDispatch(entry.payload, entry.envelopeTaskId ?? taskId);
58409
58786
  return { taskId };
58410
58787
  }
58411
58788
  // ─── §3.C.20 — GATE_DISPATCH → round-0 implementor spawn ────────────────
@@ -58428,7 +58805,7 @@ var QuorumLoop = class _QuorumLoop {
58428
58805
  * never thrown upward.
58429
58806
  */
58430
58807
  async handleGateDispatch(payload, envelopeTaskId) {
58431
- let dispatchTaskId = envelopeTaskId || this.activeTaskId;
58808
+ let dispatchTaskId = envelopeTaskId || this.latestInFlightStart;
58432
58809
  if (dispatchTaskId && Array.isArray(payload.reviewerSeats) && payload.reviewerSeats.length > 0) {
58433
58810
  let roster = this.expectedRosterByTask.get(dispatchTaskId);
58434
58811
  roster || (roster = /* @__PURE__ */ new Map(), this.expectedRosterByTask.set(dispatchTaskId, roster));
@@ -58446,11 +58823,12 @@ var QuorumLoop = class _QuorumLoop {
58446
58823
  logger.info("[QuorumLoop] GateDispatch dedup no-op", { gateRunId: payload.gateRunId });
58447
58824
  return;
58448
58825
  }
58449
- if (typeof envelopeTaskId == "string" && envelopeTaskId.length > 0 && envelopeTaskId !== this.activeTaskId && // P45 D1 — a dispatch for an EARLIER single task (a later `startTask` moved
58450
- // `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.
58451
58829
  !this.singleTaskContextByTask.has(envelopeTaskId))
58452
58830
  return this.bufferTeamPacketDuringSeeding("gateDispatch", payload, envelopeTaskId) ? void 0 : this.handleTeamGateDispatch(payload, envelopeTaskId);
58453
- let inFlightFor = typeof envelopeTaskId == "string" && envelopeTaskId.length > 0 ? envelopeTaskId : this.activeTaskId;
58831
+ let inFlightFor = typeof envelopeTaskId == "string" && envelopeTaskId.length > 0 ? envelopeTaskId : this.latestInFlightStart;
58454
58832
  if (inFlightFor !== null && this.startTasksInFlight.has(inFlightFor)) {
58455
58833
  logger.info("[QuorumLoop] GateDispatch buffered \u2014 startTask in flight (early-packet race)", {
58456
58834
  gateRunId: payload.gateRunId,
@@ -58458,15 +58836,15 @@ var QuorumLoop = class _QuorumLoop {
58458
58836
  }), this.pendingGateDispatches.push({ payload, envelopeTaskId });
58459
58837
  return;
58460
58838
  }
58461
- 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;
58462
- if (!taskId || !brief) {
58463
- 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)", {
58464
58842
  gateRunId: payload.gateRunId,
58465
- hasTaskId: !!taskId,
58466
- hasBrief: !!brief
58843
+ hasEnvelopeTaskId: typeof envelopeTaskId == "string" && envelopeTaskId.length > 0
58467
58844
  });
58468
58845
  return;
58469
58846
  }
58847
+ let taskId = envelopeTaskId, brief = single.brief, agent = single.agent;
58470
58848
  this.seenGateDispatchRunIds.add(payload.gateRunId);
58471
58849
  let sessionKey = await this.deps.getSessionKey(this.deps.session.sessionId);
58472
58850
  if (!sessionKey) {
@@ -59023,7 +59401,8 @@ ${section}`);
59023
59401
  ...args.teamAuthority ? { teamAuthority: args.teamAuthority } : {}
59024
59402
  }, needsWeb = this.webAccessByTask.get(args.taskId);
59025
59403
  if (needsWeb === void 0) {
59026
- let originalPrompt = this.singleTaskContextByTask.get(args.taskId)?.brief ?? this.activeBriefByTask.get(args.taskId)?.brief ?? (args.roundNumber === 0 ? args.brief : this.activeBrief ?? "");
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 : "");
59027
59406
  needsWeb = isWebAccessNeeded(originalPrompt), this.webAccessByTask.set(args.taskId, needsWeb);
59028
59407
  }
59029
59408
  if (needsWeb && (args.agent === "CLAUDE" || args.agent === "ANTIGRAVITY"))
@@ -59057,12 +59436,11 @@ ${section}`);
59057
59436
  workingDirAuthority: snapshotAuthority,
59058
59437
  role: "implementor",
59059
59438
  agentKind: args.agent,
59060
- // CP-12 W2.b — thread THIS round's task id as per-spawn substrate/audit
59061
- // authority. On the TEAM path `args.taskId` is the per-track child
59062
- // `task_id` (the LE singleton stays unset/clobber-free for concurrent
59063
- // tracks); on the single-impl path it equals the armed singleton
59064
- // (`setActiveTaskId`), so the LE's `args.taskId ?? this.taskId` fallback
59065
- // 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.
59066
59444
  taskId: args.taskId,
59067
59445
  webTurnActive: !!agentWebTurn?.mcpConfigPath,
59068
59446
  timeoutMs: null,
@@ -60365,7 +60743,7 @@ ${section}`);
60365
60743
  });
60366
60744
  return;
60367
60745
  }
60368
- 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.webAccessByTask.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, {
60369
60747
  kind: "verdict",
60370
60748
  author: { role: "engine" },
60371
60749
  sensitivity: "user",
@@ -60411,7 +60789,123 @@ ${section}`);
60411
60789
  */
60412
60790
  async promoteShadow(taskId) {
60413
60791
  if (!(this.shuttingDown && (this.terminalClassBAdmissionsByTask.get(taskId)?.size ?? 0) === 0))
60414
- 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);
60415
60909
  }
60416
60910
  /**
60417
60911
  * CP-12 W2.b (§3.C.2b (d) / H3-4) — promote-quiescence BARRIER for `taskId`.
@@ -60434,6 +60928,12 @@ ${section}`);
60434
60928
  return this.promoteShadowLockedInner(taskId, originEpoch);
60435
60929
  }
60436
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;
60437
60937
  try {
60438
60938
  let teamAuthority = this.teamAuthorityForTask(taskId);
60439
60939
  if (this.teamRunIsHalted(teamAuthority)) return;
@@ -60446,9 +60946,15 @@ ${section}`);
60446
60946
  error: err?.message
60447
60947
  }), historySnapshot = null;
60448
60948
  }
60449
- this.userNotesByTask.delete(taskId), this.reviewScopeResetTasks.delete(taskId), this.reviewScopeResetEligibleTasks.delete(taskId), this.roundHistoryByTask.delete(taskId), this.singleTaskContextByTask.delete(taskId), this.webAccessByTask.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);
60450
60950
  let shadow = this.shadowsByTask.get(taskId);
60451
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
+ }
60452
60958
  logger.info("[QuorumLoop] promote no-op \u2014 no in-memory shadow for task", { taskId });
60453
60959
  return;
60454
60960
  }
@@ -60698,7 +61204,8 @@ ${section}`);
60698
61204
  shadow,
60699
61205
  diff,
60700
61206
  terminalOutcomeReserved,
60701
- historySnapshot
61207
+ historySnapshot,
61208
+ retryContext
60702
61209
  );
60703
61210
  } finally {
60704
61211
  this.emitProgress({ phase: "progress_cleared" }, originEpoch);
@@ -60716,7 +61223,7 @@ ${section}`);
60716
61223
  * no-op/re-delivery/discard returns (which must stay silent). All emits carry the
60717
61224
  * caller-resolved `originEpoch`.
60718
61225
  */
60719
- async promoteApplyLocked(taskId, originEpoch, shadow, diff, terminalOutcomeReserved, historySnapshot) {
61226
+ async promoteApplyLocked(taskId, originEpoch, shadow, diff, terminalOutcomeReserved, historySnapshot, retryContext) {
60720
61227
  this.emitProgress({ phase: "promoting", files: diff.length }, originEpoch);
60721
61228
  let teamPromoteEntry = this.activeBriefByTask.get(taskId), captureManifest = this.deps.durableStore && teamPromoteEntry && teamPromoteEntry.taskGroupId.length > 0 ? {
60722
61229
  trackIndex: teamPromoteEntry.trackIndex,
@@ -60736,21 +61243,85 @@ ${section}`);
60736
61243
  }
60737
61244
  } : void 0, res = await shadow.promote(diff, captureManifest ? { captureManifest } : void 0);
60738
61245
  if (res.conflicts.length > 0 || res.errors.length > 0) {
60739
- let paths = [...res.conflicts, ...res.errors.map((e) => e.path)];
60740
- if (this.surfaceHalt(
60741
- `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.`
60742
- ), res.mutationState === "unresolved") {
60743
- 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", {
60744
61251
  taskId,
60745
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
60746
61290
  });
60747
61291
  return;
60748
61292
  }
60749
- 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
+ }
60750
61321
  return;
60751
61322
  }
60752
61323
  if (!res.materialized)
60753
- if (res.recovered)
61324
+ if (this.markTaskOutcome(taskId, "applied"), res.recovered)
60754
61325
  logger.info("[QuorumLoop] recovered fully-applied snapshot promotion", { taskId });
60755
61326
  else {
60756
61327
  logger.debug("[QuorumLoop] promote no-op \u2014 marker flipped to promoted mid-call", { taskId }), terminalOutcomeReserved && await this.workspaceOutcomeSink.completeTask(taskId, "applied", {
@@ -60769,6 +61340,19 @@ ${section}`);
60769
61340
  return;
60770
61341
  }
60771
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
+ }
60772
61356
  terminalOutcomeReserved && await this.workspaceOutcomeSink.completeTask(taskId, "applied", {
60773
61357
  filesApplied: appliedPaths.length
60774
61358
  }), logger.info("[QuorumLoop] shadow promoted to real tree", {
@@ -61021,7 +61605,11 @@ ${section}`);
61021
61605
  });
61022
61606
  return;
61023
61607
  }
61024
- if (dispatchTaskId) {
61608
+ if (logger.info("[QuorumLoop] ReviewerDispatch received", {
61609
+ gateId: payload.gateId,
61610
+ seatId: seat,
61611
+ ...dispatchTaskId ? { taskId: dispatchTaskId } : {}
61612
+ }), dispatchTaskId) {
61025
61613
  let roster = this.expectedRosterByTask.get(dispatchTaskId);
61026
61614
  roster || (roster = /* @__PURE__ */ new Map(), this.expectedRosterByTask.set(dispatchTaskId, roster)), roster.set(String(seat), String(payload.role || "reviewer"));
61027
61615
  }
@@ -61306,7 +61894,7 @@ ${section}`);
61306
61894
  "[QuorumLoop] reviewer-sandboxing DISABLED by operator opt-out (CODEVIBE_SANDBOX_REVIEWERS=0) \u2014 legacy path",
61307
61895
  { key }
61308
61896
  ), NO_TEARDOWN;
61309
- let taskId = this.taskByGateId.get(args.gateId) ?? this.activeTaskId;
61897
+ let taskId = this.taskByGateId.get(args.gateId);
61310
61898
  if (!taskId)
61311
61899
  return this.surfaceReviewerReducedTrust(
61312
61900
  args,
@@ -61871,32 +62459,51 @@ ${section}`);
61871
62459
  // ─── §3.C.21b — dropped-packet recovery ─────────────────────────────────
61872
62460
  /**
61873
62461
  * On WS (re)connect/startup, poll for the seats assigned to this desktop for
61874
- * any in_review gate of the active task and spawn any seat NEITHER already
61875
- * running-in-memory NOR already verdict-submitted (double-guard). Without
61876
- * 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.
61877
62468
  */
61878
62469
  async recoverInReviewAssignments() {
61879
- if (this.shuttingDown || !this.activeTaskId) return;
61880
- let assignments;
61881
- try {
61882
- assignments = await this.deps.appsyncClient.getInReviewAssignments(this.activeTaskId);
61883
- } catch (err) {
61884
- logger.warn("[QuorumLoop] getInReviewAssignments failed (non-fatal)", {
61885
- err: err.message
61886
- });
61887
- return;
61888
- }
61889
- if (!this.shuttingDown)
61890
- for (let a of assignments) {
61891
- if (this.shuttingDown) break;
61892
- let key = seatKey(a.gateId, a.seatId);
61893
- this.runningSeats.has(key) || this.submittedSeats.has(key) || this.spawnOneSeat({
61894
- gateId: a.gateId,
61895
- seatId: a.seatId,
61896
- role: a.role,
61897
- agentKind: a.agentKind
61898
- });
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
+ }
61899
62505
  }
62506
+ }
61900
62507
  }
61901
62508
  // ─── §4 / #PR-4 / #GC-1 — shadow recovery + GC on reconnect/startup ──────
61902
62509
  /**
@@ -61936,7 +62543,7 @@ ${section}`);
61936
62543
  ]), signal);
61937
62544
  if (!this.admittedSessionWorkMayContinue(admission)) return;
61938
62545
  let now = Date.now(), ttlMs = env.ttlMs ?? DEFAULT_SHADOW_TTL_MS, keep = /* @__PURE__ */ new Set();
61939
- this.activeTaskId && keep.add(this.activeTaskId);
62546
+ for (let taskId of this.singleTaskContextByTask.keys()) keep.add(taskId);
61940
62547
  for (let taskId of this.shadowsByTask.keys()) keep.add(taskId);
61941
62548
  for (let taskId of this.shadowCreateInFlight.keys()) keep.add(taskId);
61942
62549
  let failures = [], sameWorkspace = recoverable.filter(
@@ -62821,7 +63428,7 @@ ${section}`);
62821
63428
  });
62822
63429
  return;
62823
63430
  }
62824
- 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);
62825
63432
  if (taskId === "") {
62826
63433
  logger.warn(
62827
63434
  "[QuorumLoop] ReviseFeedback could not resolve a task (in-memory state lost after restart?) \u2014 failing loud",
@@ -62831,7 +63438,17 @@ ${section}`);
62831
63438
  ), this.emitProgress({ phase: "progress_cleared" }, originEpoch);
62832
63439
  return;
62833
63440
  }
62834
- 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);
62835
63452
  if (parentTeamAuthority && (!trackEntry || !this.teamEntryMayContinue(taskId, trackEntry, parentTeamAuthority))) {
62836
63453
  logger.info("[QuorumLoop] stale team ReviseFeedback refused \u2014 generation retired", {
62837
63454
  taskId,
@@ -62866,7 +63483,9 @@ ${section}`);
62866
63483
  gateId: payload.nextGateId,
62867
63484
  roundNumber: payload.nextRound,
62868
63485
  brief,
62869
- 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,
62870
63489
  sessionKey,
62871
63490
  ...teamAuthority ? { teamAuthority } : {},
62872
63491
  ...priorRationale ? { priorRationale } : {},
@@ -62882,32 +63501,53 @@ ${section}`);
62882
63501
  } : {}
62883
63502
  });
62884
63503
  }
62885
- setActiveImplementorAgent(agent) {
62886
- 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);
62887
63514
  }
62888
63515
  // ─── PHASE-CP-10-MIN (#585) — continuation handoff ────────────────────────
62889
63516
  /**
62890
- * #585 #C10M-9 — resolve the active task's request context for `/continue
62891
- * request` (the entrypoint wires this to the slash route's
62892
- * `getActiveRequestContext`). Returns `null` when no implementor round is
62893
- * active (no gate to hand off from). The `gateId`/`roundNumber` come from the
62894
- * ACTIVE implementor registry (the round currently running); the brief +
62895
- * 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.
62896
63524
  */
62897
- getActiveContinuationRequestContext() {
62898
- let taskId = this.activeTaskId;
62899
- if (!taskId) return null;
62900
- let active = this.activeImplementorByTask.get(taskId);
62901
- if (!active) return null;
62902
- 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);
62903
63535
  return {
62904
- taskId,
62905
- gateId: active.gateId,
62906
- roundNumber: active.roundNumber,
62907
- brief,
62908
- 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
+ }
62909
63544
  };
62910
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
+ }
62911
63551
  /**
62912
63552
  * #585 L-A — mint a fresh `expiresAt = now + TTL` at millisecond precision
62913
63553
  * (RFC-3339). Fresh per request so a CANCEL→re-request produces a distinct
@@ -63192,9 +63832,7 @@ ${section}`);
63192
63832
  }
63193
63833
  if (trackEntry && teamAuthority && !this.teamEntryMayContinue(input.taskId, trackEntry, teamAuthority) || this.shuttingDown) return !1;
63194
63834
  let brief = this.buildResumeBrief(packet);
63195
- this.setActiveImplementorAgent(input.targetAgent);
63196
- let resumedSingle = this.singleTaskContextByTask.get(input.taskId);
63197
- 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", {
63198
63836
  taskId: input.taskId,
63199
63837
  sourceAgent: packet.sourceAgent,
63200
63838
  targetAgent: input.targetAgent,
@@ -63319,7 +63957,7 @@ ${section}`);
63319
63957
  * user requirement.
63320
63958
  */
63321
63959
  buildReviseBrief(taskId, payload) {
63322
- 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(
63323
63961
  (r) => r.round < payload.nextRound - 1
63324
63962
  );
63325
63963
  return composeReviseBrief(originalBrief, userNotes, payload, history);
@@ -63480,9 +64118,16 @@ ${section}`);
63480
64118
  get _submittedSeatsForTests() {
63481
64119
  return this.submittedSeats;
63482
64120
  }
63483
- /** @internal — test seam to set the active task without start_task. */
63484
- _setActiveTaskForTests(taskId, brief) {
63485
- 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);
63486
64131
  }
63487
64132
  /** @internal — read a task's accumulated binding user notes. */
63488
64133
  _userNotesForTests(taskId) {
@@ -64331,6 +64976,23 @@ function resolveBrowseUrls(modelUrls, prompt) {
64331
64976
  let usable = modelUrls.filter(isWellFormedHttpUrl);
64332
64977
  return usable.length > 0 ? usable : void 0;
64333
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
+ }
64334
64996
  var OrchestrationShellStartupError = class extends Error {
64335
64997
  constructor(message, cause) {
64336
64998
  super(message), this.name = "OrchestrationShellStartupError", this.cause = cause;
@@ -64433,11 +65095,21 @@ function wireTeamMergeAuditSummary(store, target) {
64433
65095
  }
64434
65096
  async function runOrchestrationShell(args) {
64435
65097
  let store = createOrchestrationStore({ session: args.session }), originalStoreDispatch = store.dispatch.bind(store);
64436
- store.dispatch = ((action) => (action && action.type === "GATE_PROMPT_RECEIVED" && action.envelope && args.quorumLoop && typeof args.quorumLoop.registerActiveGate == "function" && args.quorumLoop.registerActiveGate(
64437
- action.envelope.taskId,
64438
- action.envelope.gateId,
64439
- action.envelope.currentRound ?? 0
64440
- ), 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
+ });
64441
65113
  let advisoryAttachmentJournal = advisoryAttachmentJournalFor(args);
64442
65114
  try {
64443
65115
  await advisoryAttachmentJournal.recoverDeadOwners();
@@ -64455,7 +65127,23 @@ async function runOrchestrationShell(args) {
64455
65127
  store.dispatch({ type: "SHELL_ADVISORY", source: "shell", text: message });
64456
65128
  }
64457
65129
  }) : void 0;
64458
- 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(
64459
65147
  (promptIds) => args.appsyncClient.refreshOpenPromptTtl(promptIds)
64460
65148
  );
64461
65149
  let unsubscribeWaitingUser = null;
@@ -64959,8 +65647,13 @@ async function runOrchestrationShell(args) {
64959
65647
  if (shellSubmissionsFenced) return;
64960
65648
  let fromMobile = options?.fromMobile === !0, convLenBefore = store.getState().conversation.length, userTurnTimestamp = (/* @__PURE__ */ new Date()).toISOString(), turnOwnership = { brainstormPanelOwned: !1 }, ownEntries = [];
64961
65649
  await turnAuthoringContext.run({ ownEntries }, async () => {
64962
- let handledByGate = !1;
64963
- 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({
64964
65657
  type: "SHELL_ADVISORY",
64965
65658
  source: "shell",
64966
65659
  text: "The desktop is still preparing the interactive prompt. Please send your choice again.",
@@ -65292,9 +65985,13 @@ async function runOrchestrationShell(args) {
65292
65985
  }
65293
65986
  }));
65294
65987
  }, onTerminalDecision = args.quorumLoop ? async (resolvedTaskId, postAction, expectedShadow, canonicalDecision, gateResolution) => {
65295
- let taskId = resolvedTaskId || args.quorumLoop?.activeTask;
65296
- if (!taskId) return;
65297
- 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";
65298
65995
  workspaceTerminalCoordinator?.hasTaskResolution(taskId) && (finalApprovalCancelled || terminalAbort) && await workspaceTerminalCoordinator.reserveTask(
65299
65996
  taskId,
65300
65997
  finalApprovalCancelled ? "CANCEL" : "ABORT"
@@ -65560,7 +66257,7 @@ async function runOrchestrationShell(args) {
65560
66257
  // Codex LOW). Only `handled:false` falls through to the planner.
65561
66258
  resolveGateInput: (text2) => routeGatePromptInput(
65562
66259
  gateDecisionDeps,
65563
- findActiveGatePromptEntry(store.getState().conversation),
66260
+ findActiveGatePromptEntry(store.getState().conversation, store.getState().activeGatePromptId),
65564
66261
  text2
65565
66262
  ),
65566
66263
  signal: nonTtyAbort.signal
@@ -66097,11 +66794,11 @@ async function routeMobileUserPrompt(event, sessionKeyResolver, submit, updateEv
66097
66794
  }
66098
66795
  }
66099
66796
  async function routeMobileGatePromptInput(deps, text2) {
66100
- let before = findActiveGatePromptEntry(deps.store.getState().conversation);
66797
+ let before = findActiveGatePromptEntry(deps.store.getState().conversation, deps.store.getState().activeGatePromptId);
66101
66798
  if (!before) return !1;
66102
66799
  let beforePhase = before.uiState.phase;
66103
66800
  if (!(await routeGatePromptInput(deps, before, text2)).handled) return !1;
66104
- let after = findActiveGatePromptEntry(deps.store.getState().conversation);
66801
+ let after = findActiveGatePromptEntry(deps.store.getState().conversation, deps.store.getState().activeGatePromptId);
66105
66802
  return beforePhase === "awaiting-number" && after?.id === before.id && after.uiState.phase === "awaiting-notes" && deps.store.dispatch({
66106
66803
  type: "SHELL_ADVISORY",
66107
66804
  source: "shell",
@@ -66280,14 +66977,28 @@ function routeTeamShellEventToStore(store, evt) {
66280
66977
  }
66281
66978
  function parseTaskCommand(text2) {
66282
66979
  let rest = text2.trim().slice(5).trim();
66283
- 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
+ };
66284
66995
  let tokens = rest.split(/\s+/), agent = null, startIdx = 0;
66285
66996
  if (tokens[0] === "--agent") {
66286
66997
  let value = (tokens[1] ?? "").toUpperCase();
66287
66998
  value === "CLAUDE" || value === "CODEX" || value === "ANTIGRAVITY" ? (agent = value, startIdx = 2) : startIdx = 1;
66288
66999
  }
66289
67000
  let brief = tokens.slice(startIdx).join(" ").trim();
66290
- return { agent, brief };
67001
+ return { agent, brief, focus: null, conflictChoice: null };
66291
67002
  }
66292
67003
  function resolveEnvImplementorAgentOverride() {
66293
67004
  let raw = (process.env.CODEVIBE_IMPLEMENTOR_AGENT ?? "").trim().toUpperCase();
@@ -67084,10 +67795,10 @@ async function launchTeamFromWorkItems(deps) {
67084
67795
  });
67085
67796
  if (result.accepted && result.taskGroupId) {
67086
67797
  let gid = result.taskGroupId;
67087
- 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({
67088
67799
  type: "SHELL_ADVISORY",
67089
67800
  source: "shell",
67090
- 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.`
67091
67802
  });
67092
67803
  try {
67093
67804
  createShellEventEmitter(appsyncClient)({
@@ -67207,10 +67918,10 @@ async function dispatchSynthesizedSingleStartTask(deps) {
67207
67918
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
67208
67919
  status: "running"
67209
67920
  }
67210
- }), note && store.dispatch({
67921
+ }), store.dispatch({
67211
67922
  type: "SHELL_ADVISORY",
67212
67923
  source: "shell",
67213
- text: note
67924
+ text: `Task ${taskLabel(store.getState(), result.taskId)} started with ${selectedAgent}.${note ? ` ${note}` : ""}`
67214
67925
  })) : store.dispatch({
67215
67926
  type: "SHELL_ADVISORY",
67216
67927
  source: "shell",
@@ -67853,6 +68564,12 @@ function buildContinuationActionDeps(args, store) {
67853
68564
  let loop = args.quorumLoop;
67854
68565
  return loop ? {
67855
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
+ },
67856
68573
  request: async (ctx, targetAgent) => loop.requestContinuation({
67857
68574
  taskId: ctx.taskId,
67858
68575
  gateId: ctx.gateId,
@@ -67863,8 +68580,8 @@ function buildContinuationActionDeps(args, store) {
67863
68580
  ...targetAgent ? { targetAgent } : {}
67864
68581
  }),
67865
68582
  getActiveOfferContext: () => {
67866
- let entry = findActiveGatePromptEntry(store.getState().conversation);
67867
- if (!entry || entry.envelope.promptKind !== CONTINUATION_OFFER_HANDOFF_PROMPT_KIND)
68583
+ let entry = findActiveContinuationOfferEntry(store.getState().conversation);
68584
+ if (!entry)
67868
68585
  return null;
67869
68586
  let offerId = entry.envelope.offerId;
67870
68587
  return typeof offerId != "string" || offerId.length === 0 ? null : {
@@ -68151,6 +68868,42 @@ function dispatchRefusalAdvisory(store, decision) {
68151
68868
  function isInteractiveTty() {
68152
68869
  return !!process.stdout.isTTY && process.env.CODEVIBE_NO_TUI !== "1";
68153
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
+ }
68154
68907
  async function resolvePlannerOffer(deps) {
68155
68908
  let { store, args, emitShellEventBound, generator, ensureFreshContextStoreFn, offer, choice } = deps;
68156
68909
  await plannerOfferE1Ledger.resolveOffer(
@@ -68304,7 +69057,38 @@ async function handleShellUserInput(deps) {
68304
69057
  browseSignal = deps.browseSignal
68305
69058
  } = deps;
68306
69059
  if (text2.trim().length === 0) return;
68307
- 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;
68308
69092
  if (pendingOffer) {
68309
69093
  let interactionAdvisoryFlags = deps.inputOrigin === "mobile" ? { handoffExcluded: !0 } : { localOnly: !0 }, offerReply = text2.trim(), optionCount = pendingOffer.options.length, offerHint = () => {
68310
69094
  store.dispatch({
@@ -68550,7 +69334,12 @@ async function handleShellUserInput(deps) {
68550
69334
  kind: "gated",
68551
69335
  headline: AUDIT_BROWSER_MAX_HEADLINE,
68552
69336
  upgradeHint: AUDIT_BROWSER_UPGRADE_HINT
68553
- }) : 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
+ );
68554
69343
  else
68555
69344
  try {
68556
69345
  let effectiveSessionId = explicitSessionId || args.session.sessionId, result = runAuditBrowserFn ? await runAuditBrowserFn(taskId, {
@@ -68639,14 +69428,33 @@ async function handleShellUserInput(deps) {
68639
69428
  }), output.output = "") : output.output = loaded.message;
68640
69429
  }
68641
69430
  else if (output.command === "/review-reset" && !output.sideEffect && output.output === "PENDING \u2014 entrypoint arms comprehensive review")
68642
- 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
+ }
68643
69437
  else if (output.command === "/task" && !output.sideEffect && output.output === "PENDING \u2014 entrypoint drives startTask")
68644
69438
  if (!args.quorumLoop)
68645
69439
  output.output = "/task requires an orchestration session with a running loop (Pro/Max). It is unavailable in this session.";
68646
69440
  else {
68647
69441
  let parsed = parseTaskCommand(slashCommandText);
68648
- if (!parsed.brief)
68649
- 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.";
68650
69458
  else {
68651
69459
  let detected = typeof args.quorumLoop.getDetectedAgents == "function" ? args.quorumLoop.getDetectedAgents() : [], { agent, note } = selectImplementorAgent(detected, parsed.agent);
68652
69460
  try {
@@ -68666,16 +69474,21 @@ async function handleShellUserInput(deps) {
68666
69474
  agent,
68667
69475
  ...turnAttachments.length ? { attachments: turnAttachments } : {}
68668
69476
  });
68669
- result ? (store.dispatch({
68670
- type: "TASK_LIFECYCLE",
68671
- task: {
68672
- taskId: result.taskId,
68673
- agentKind: agent,
68674
- pid: 0,
68675
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
68676
- status: "running"
68677
- }
68678
- }), 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.";
68679
69492
  } catch (err) {
68680
69493
  output.output = `Failed to start task: ${err.message ?? String(err)}`, logger.warn("[orchestration-shell] /task dispatch failed", {
68681
69494
  error: err.message
@@ -69135,10 +69948,10 @@ async function handleShellUserInput(deps) {
69135
69948
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
69136
69949
  status: "running"
69137
69950
  }
69138
- }), note && store.dispatch({
69951
+ }), store.dispatch({
69139
69952
  type: "SHELL_ADVISORY",
69140
69953
  source: "shell",
69141
- text: note
69954
+ text: `Task ${taskLabel(store.getState(), result.taskId)} started with ${selectedAgent}.${note ? ` ${note}` : ""}`
69142
69955
  })) : store.dispatch({
69143
69956
  type: "SHELL_ADVISORY",
69144
69957
  source: "shell",
@@ -69676,17 +70489,20 @@ async function emitWithProvenNoWriteDowngrade(emitter, args) {
69676
70489
  let result = await emitter(args);
69677
70490
  !result.emitted && args.disableWriterAttestation !== !0 && (result.reason === "writer_ineligible" || result.reason === "definitive_rejection") && await emitter({ ...args, disableWriterAttestation: !0 });
69678
70491
  }
69679
- function renderSessionTaskList(state, activeTaskId = null) {
70492
+ function renderSessionTaskList(state, liveTaskIds = []) {
69680
70493
  let tasks = [...state.sessionTasks.values()].sort(
69681
70494
  (a, b) => b.startedAt.localeCompare(a.startedAt)
69682
- ), activeUnlisted = activeTaskId !== null && !state.sessionTasks.has(activeTaskId) ? activeTaskId : null;
69683
- if (tasks.length === 0 && activeUnlisted === null)
70495
+ ), unlisted = liveTaskIds.filter((id) => !state.sessionTasks.has(id));
70496
+ if (tasks.length === 0 && unlisted.length === 0)
69684
70497
  return "No tasks have run in this session yet. Once a task runs, `/audit <task-id>` opens its full review history.";
69685
70498
  let lines = ["Tasks this session (newest first):"];
69686
- 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
+ }
69687
70503
  for (let t of tasks)
69688
70504
  lines.push(
69689
- ` ${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}`
69690
70506
  );
69691
70507
  return lines.push("Run `/audit <task-id>` to open a task\u2019s full review history."), lines.join(`
69692
70508
  `);
@@ -69702,7 +70518,20 @@ function parseIsoTimestamp(iso) {
69702
70518
  return Number.isFinite(t) ? t : 0;
69703
70519
  }
69704
70520
  function buildStatusSummary(state, quorumLoop) {
69705
- 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
+ }
69706
70535
  if (state.team) {
69707
70536
  let t = state.team, indices = [...t.tracks.keys()].sort((a, b) => a - b), n = indices.length, trackParts = indices.map((i) => {
69708
70537
  let e = t.tracks.get(i), details = [e.agent, e.taskId ? truncate(e.taskId, 12) : null].filter(
@@ -69717,21 +70546,27 @@ function buildStatusSummary(state, quorumLoop) {
69717
70546
  let teamLine = `Agent Teams group${groupRef} \u2014 ${n} ${n === 1 ? "track" : "tracks"} (MergeGate: ${MERGE_GATE_STATUS_LABEL[t.mergeGate]}${mergeElapsedStr})`;
69718
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);
69719
70548
  }
69720
- 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;
69721
70550
  if (state.progress) {
69722
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)}` : "";
69723
70552
  lines.push(
69724
70553
  `Current task: ${state.progress.text}${elapsed ? ` \xB7 ${elapsed}` : ""}${tokenStr}.`
69725
70554
  );
69726
- } 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
+ );
69727
70558
  !state.progress && state.lastReviewerProgress && lines.push(`Reviewers: ${state.lastReviewerProgress.text}.`);
69728
70559
  for (let entry of state.conversation) {
69729
70560
  if (entry.kind !== "gate-prompt" || entry.final) continue;
69730
70561
  let label = GATE_PROMPT_KIND_LABEL[entry.envelope.promptKind] ?? entry.envelope.promptKind, queued = entry.queue.length > 0 ? `; ${entry.queue.length} more queued` : "";
69731
70562
  lines.push(
69732
- `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}.`
69733
70564
  );
69734
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
+ );
69735
70570
  let rawOutcome = quorumLoop?.getLastWorkspaceOutcome() ?? null, outcome = null;
69736
70571
  if (rawOutcome !== null && !teamActive)
69737
70572
  if (rawOutcome.taskId) {
@@ -74926,14 +75761,10 @@ function buildAndArmQuorumLoop(args) {
74926
75761
  // manifest BEFORE the tree mutates (the C1 capture is already gated on
74927
75762
  // `this.deps.durableStore` inside `promoteShadowLocked`).
74928
75763
  ...args.durableStore ? { durableStore: args.durableStore } : {},
74929
- // CP-7 Stage-2 r3 (Codex) HIGH ARM the LE's current/authorized task id at
74930
- // start_task so the round-0 implementor spawn engages the CP-7 substrate
74931
- // (sandbox + creditless badge) instead of the pre-TaskAuthorized
74932
- // reduced-trust path (local-executor-impl.ts:566). Rolled back on a START
74933
- // rejection by the loop. This is the SAME `taskId` state TaskAuthorized later
74934
- // advances; arming it early closes the round-0 window where the implementor
74935
- // would otherwise spawn unbadged/unsandboxed.
74936
- 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.
74937
75768
  // CP-7 §8 (Stage-1-resolved) — wire the REVIEWER substrate engager so each
74938
75769
  // Trusted-agent reviewer seat runs INSIDE the CP-7 sandbox + broker (no
74939
75770
  // ambient vendor creds; model call via the loopback broker), exactly like