@integrity-labs/agt-cli 0.28.420 → 0.28.422

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.
@@ -100,7 +100,7 @@ async function spawnPairSession(session) {
100
100
  return { ok: true };
101
101
  } catch {
102
102
  }
103
- const { resolveClaudeBinary } = await import("./persistent-session-YNQQHPFS.js");
103
+ const { resolveClaudeBinary } = await import("./persistent-session-HWCKGPYF.js");
104
104
  const claudeBin = resolveClaudeBinary();
105
105
  const pairEnv = {
106
106
  ...process.env,
@@ -373,4 +373,4 @@ export {
373
373
  startClaudePair,
374
374
  submitClaudePairCode
375
375
  };
376
- //# sourceMappingURL=claude-pair-runtime-EDHQS7BP.js.map
376
+ //# sourceMappingURL=claude-pair-runtime-3JHCWZZD.js.map
@@ -45,7 +45,7 @@ import {
45
45
  resolveEffectivePinRaw,
46
46
  safeWriteJsonAtomic,
47
47
  setConfigHash
48
- } from "../chunk-4D4VKZPK.js";
48
+ } from "../chunk-6JC5YLU5.js";
49
49
  import {
50
50
  getProjectDir as getProjectDir2,
51
51
  getReadyTasks,
@@ -150,7 +150,7 @@ import {
150
150
  toOpencodeModel,
151
151
  transcriptActivityAgeSeconds,
152
152
  writeEgressAllowlist
153
- } from "../chunk-LLIMZG67.js";
153
+ } from "../chunk-XFC7YVGS.js";
154
154
  import {
155
155
  reapOrphanChannelMcps
156
156
  } from "../chunk-XWVM4KPK.js";
@@ -9458,7 +9458,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
9458
9458
  var lastVersionCheckAt = 0;
9459
9459
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
9460
9460
  var lastResponsivenessProbeAt = 0;
9461
- var agtCliVersion = true ? "0.28.420" : "dev";
9461
+ var agtCliVersion = true ? "0.28.422" : "dev";
9462
9462
  function resolveBrewPath(execFileSync2) {
9463
9463
  try {
9464
9464
  const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -10606,7 +10606,7 @@ function flushRestartedAgentDiagnostics(hostId, codeNames) {
10606
10606
  if (codeNames.length === 0) return;
10607
10607
  void (async () => {
10608
10608
  try {
10609
- const { collectDiagnostics } = await import("../persistent-session-YNQQHPFS.js");
10609
+ const { collectDiagnostics } = await import("../persistent-session-HWCKGPYF.js");
10610
10610
  await api.post("/host/heartbeat", {
10611
10611
  host_id: hostId,
10612
10612
  agent_diagnostics: collectDiagnostics(codeNames, quarantineEntriesFor)
@@ -10713,7 +10713,7 @@ async function pollCycle() {
10713
10713
  }
10714
10714
  try {
10715
10715
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
10716
- const { collectDiagnostics } = await import("../persistent-session-YNQQHPFS.js");
10716
+ const { collectDiagnostics } = await import("../persistent-session-HWCKGPYF.js");
10717
10717
  const diagCodeNames = [...agentState.persistentSessionAgents];
10718
10718
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames, quarantineEntriesFor) : void 0;
10719
10719
  let tailscaleHostname;
@@ -10827,7 +10827,7 @@ async function pollCycle() {
10827
10827
  collectResponsivenessProbes,
10828
10828
  collectPanelessActivityProbes,
10829
10829
  getResponsivenessIntervalMs
10830
- } = await import("../responsiveness-probe-KVEPIRMG.js");
10830
+ } = await import("../responsiveness-probe-4CGMUXOH.js");
10831
10831
  const probeIntervalMs = getResponsivenessIntervalMs();
10832
10832
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
10833
10833
  const probeCodeNames = [...agentState.persistentSessionAgents];
@@ -10866,7 +10866,7 @@ async function pollCycle() {
10866
10866
  collectResponsivenessProbes,
10867
10867
  livePendingInboundOldestAgeSeconds,
10868
10868
  parkPendingInbound
10869
- } = await import("../responsiveness-probe-KVEPIRMG.js");
10869
+ } = await import("../responsiveness-probe-4CGMUXOH.js");
10870
10870
  const { getProjectDir: wedgeProjectDir } = await import("../scheduler-engine-HYOOQV4A.js");
10871
10871
  const wedgeNow = /* @__PURE__ */ new Date();
10872
10872
  const liveAgents = agentState.persistentSessionAgents;
@@ -14177,7 +14177,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
14177
14177
  void api.post("/host/restart-ack", { host_id: hostId, agent_id: agentId, restart_requested_at: requestedAt }).catch((err) => log(`[restart-lane] ack failed for '${codeName}': ${err.message}`));
14178
14178
  void (async () => {
14179
14179
  try {
14180
- const { collectDiagnostics } = await import("../persistent-session-YNQQHPFS.js");
14180
+ const { collectDiagnostics } = await import("../persistent-session-HWCKGPYF.js");
14181
14181
  await api.post("/host/heartbeat", {
14182
14182
  host_id: hostId,
14183
14183
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor)
@@ -14227,7 +14227,7 @@ async function respawnAgentAfterMcpStop(codeName, reason) {
14227
14227
  }
14228
14228
  try {
14229
14229
  const hostId = await getHostId();
14230
- const { collectDiagnostics } = await import("../persistent-session-YNQQHPFS.js");
14230
+ const { collectDiagnostics } = await import("../persistent-session-HWCKGPYF.js");
14231
14231
  await api.post("/host/heartbeat", {
14232
14232
  host_id: hostId,
14233
14233
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor)
@@ -14680,7 +14680,7 @@ async function processClaudePairSessions(agents) {
14680
14680
  killPairSession,
14681
14681
  pairTmuxSession,
14682
14682
  finalizeClaudePairOnboarding
14683
- } = await import("../claude-pair-runtime-EDHQS7BP.js");
14683
+ } = await import("../claude-pair-runtime-3JHCWZZD.js");
14684
14684
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
14685
14685
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
14686
14686
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -34662,6 +34662,34 @@ var FLAG_REGISTRY = [
34662
34662
  // change (ADR-0022 sensitive-flag confirm).
34663
34663
  sensitive: true
34664
34664
  },
34665
+ {
34666
+ key: "slack-scheduled-channel-guard",
34667
+ description: 'Guard on slack.reply that refuses to let an active scheduled-task destination silently move a reply to a DIFFERENT channel than the agent named (ENG-8137). CS-1505 gave an explicit inbound_id priority over the task destination but left the no-inbound_id case treating "no inbound reference" as "no destination specified" - and `channel` is a REQUIRED argument, so every call names one. The symptom: a question asked in one channel answered in another, and a reply meant as a DM landing in a public channel. Fires only when the marker carries an EXPLICIT target, the named channel differs from it, and neither an inbound_id, an enforce binding, nor an ENG-7542 channel correction is available to arbitrate. off = never runs (today behaviour: silent retarget). shadow = count + log the would-block but STILL route as today, so the false-positive rate is measurable before anyone refuses a send. enforce = block the reply and tell the agent to retry. Read live from the heartbeat flags-cache (or the env override). NOTE: enforce is currently CLAMPED to shadow host-side and setting it here will not take effect - it stays blocked until ENG-8142 teaches the Stop hook to tell a refused reply from a delivered one, because until then a blocked send fails silently (no recovery, no re-prompt, nothing in the channel). Lifting the clamp is a code change, not a flag flip.',
34668
+ flagType: "enum",
34669
+ allowedValues: ["off", "shadow", "enforce"],
34670
+ // Ships in SHADOW, not off: the counter is the whole point - the fleet metric
34671
+ // is currently blind to this misroute, which is why it went unnoticed. shadow
34672
+ // is a free local log line + counter (no model spend, no behaviour change), so
34673
+ // it measures the fire rate immediately. enforce turns a misroute into a
34674
+ // REFUSED send, which trades a confidentiality risk for an availability one -
34675
+ // that is the audited per-org flip, taken after reading the shadow numbers and
34676
+ // after ENG-8143 shrinks the stale-marker window that dominates the false
34677
+ // positives.
34678
+ defaultValue: "shadow",
34679
+ // Enum override AGT_SLACK_SCHEDULED_CHANNEL_GUARD_MODE (off|shadow|enforce),
34680
+ // resolved by resolveSlackScheduledChannelGuardMode in the channel-server bundle.
34681
+ envVar: "AGT_SLACK_SCHEDULED_CHANNEL_GUARD_MODE",
34682
+ // enforce REFUSES a send the agent asked to make - a visible availability
34683
+ // change, so flipping toward it is deliberate (ADR-0022 sensitive-flag confirm).
34684
+ sensitive: true,
34685
+ // ENG-8149: the agt-cli that first carries resolveSlackScheduledChannelGuardMode
34686
+ // (published from the ENG-8137 merge c6ff71cd). Without this the flip-reach modal
34687
+ // reports FULL reach, because an undefined `since` means "every host can honour
34688
+ // it" - true for flags that predate the reach work, wrong for a NEW host-read
34689
+ // flag whose reader older hosts simply do not have. Excluded from
34690
+ // projectDefinition, so setting it does not roll FLAGS_SCHEMA_VERSION.
34691
+ since: "0.28.421"
34692
+ },
34665
34693
  {
34666
34694
  key: "slack-hot-thread-guard",
34667
34695
  description: "Server-side hot-thread guard on the slack.reply surface (ENG-7462). Prevents an agent posting a NEW top-level Slack message when it meant to reply inside the thread it is already working in - a prompt/memory rule proved insufficient (the agent had the rule and still slipped). When a reply would otherwise post to channel ROOT (no thread_ts / message_ts / inbound_id, no active kanban card) and the agent has a recent active thread in that channel (its last bot-posted thread, from the persisted trackedThreads cache, within a freshness window), the reply is redirected into that thread. proactive:true no longer implies channel root; posting at root becomes a deliberate action (the to_channel_root flag, or the thread_ts:null sentinel). off = guard never runs, replies with no coords root exactly as today (ships dark). shadow = compute + log the would-redirect but STILL post to root (measure the fire rate before acting). enforce = apply the redirect (a soft-block: redirect + inform, never a hard rejection). Read live from the heartbeat flags-cache (or the env override); enforce is a deliberate per-org flip after a shadow soak.",
@@ -35272,6 +35300,17 @@ var FLAG_REGISTRY = [
35272
35300
  // direction - a flag-DB read error must not resurrect the mis-threading/noise the kill
35273
35301
  // fixes. Net-new gate, registry-only (ADR-0022); API-evaluated, no env materialization.
35274
35302
  defaultValue: false
35303
+ },
35304
+ {
35305
+ key: "onboarding-msteams-channel",
35306
+ description: `Offer Microsoft Teams as a selectable channel on onboarding step 3 ("which channels should your org use?", ENG-8146). OFF (default) hides the Teams option entirely: it is not rendered, not selectable, and not named in the plan-upgrade note. Turn it ON per org to restore the option for a customer who genuinely runs on Teams. Resolved SERVER-SIDE in the step-3 server component for the URL's orgId (webapp lib/feature-flags-org-server.ts), NOT via the browser public map \u2014 the client flag path evaluates at the active-org cookie, which onboarding deliberately does not pin to the org being onboarded, so a client read would resolve the wrong org for a multi-org user. Hiding the option never revokes a channel: an org that already saved msteams keeps it (step 3 carries hidden-but-saved channels through the write). Ships dark.`,
35307
+ flagType: "boolean",
35308
+ // Declared safe value is `false`: Teams hidden. `false` is also the fail-closed
35309
+ // direction - a flag-DB read error must show the narrower channel set, never
35310
+ // surface an option the operator has not deliberately turned on. Net-new gate,
35311
+ // registry-only (ADR-0022). NOT `public`: it is resolved server-side only, so it
35312
+ // must never be serialized into the browser map.
35313
+ defaultValue: false
35275
35314
  }
35276
35315
  ];
35277
35316
  var REGISTRY_BY_KEY = new Map(FLAG_REGISTRY.map((definition) => [definition.key, definition]));
@@ -40278,6 +40278,34 @@ var FLAG_REGISTRY = [
40278
40278
  // change (ADR-0022 sensitive-flag confirm).
40279
40279
  sensitive: true
40280
40280
  },
40281
+ {
40282
+ key: "slack-scheduled-channel-guard",
40283
+ description: 'Guard on slack.reply that refuses to let an active scheduled-task destination silently move a reply to a DIFFERENT channel than the agent named (ENG-8137). CS-1505 gave an explicit inbound_id priority over the task destination but left the no-inbound_id case treating "no inbound reference" as "no destination specified" - and `channel` is a REQUIRED argument, so every call names one. The symptom: a question asked in one channel answered in another, and a reply meant as a DM landing in a public channel. Fires only when the marker carries an EXPLICIT target, the named channel differs from it, and neither an inbound_id, an enforce binding, nor an ENG-7542 channel correction is available to arbitrate. off = never runs (today behaviour: silent retarget). shadow = count + log the would-block but STILL route as today, so the false-positive rate is measurable before anyone refuses a send. enforce = block the reply and tell the agent to retry. Read live from the heartbeat flags-cache (or the env override). NOTE: enforce is currently CLAMPED to shadow host-side and setting it here will not take effect - it stays blocked until ENG-8142 teaches the Stop hook to tell a refused reply from a delivered one, because until then a blocked send fails silently (no recovery, no re-prompt, nothing in the channel). Lifting the clamp is a code change, not a flag flip.',
40284
+ flagType: "enum",
40285
+ allowedValues: ["off", "shadow", "enforce"],
40286
+ // Ships in SHADOW, not off: the counter is the whole point - the fleet metric
40287
+ // is currently blind to this misroute, which is why it went unnoticed. shadow
40288
+ // is a free local log line + counter (no model spend, no behaviour change), so
40289
+ // it measures the fire rate immediately. enforce turns a misroute into a
40290
+ // REFUSED send, which trades a confidentiality risk for an availability one -
40291
+ // that is the audited per-org flip, taken after reading the shadow numbers and
40292
+ // after ENG-8143 shrinks the stale-marker window that dominates the false
40293
+ // positives.
40294
+ defaultValue: "shadow",
40295
+ // Enum override AGT_SLACK_SCHEDULED_CHANNEL_GUARD_MODE (off|shadow|enforce),
40296
+ // resolved by resolveSlackScheduledChannelGuardMode in the channel-server bundle.
40297
+ envVar: "AGT_SLACK_SCHEDULED_CHANNEL_GUARD_MODE",
40298
+ // enforce REFUSES a send the agent asked to make - a visible availability
40299
+ // change, so flipping toward it is deliberate (ADR-0022 sensitive-flag confirm).
40300
+ sensitive: true,
40301
+ // ENG-8149: the agt-cli that first carries resolveSlackScheduledChannelGuardMode
40302
+ // (published from the ENG-8137 merge c6ff71cd). Without this the flip-reach modal
40303
+ // reports FULL reach, because an undefined `since` means "every host can honour
40304
+ // it" - true for flags that predate the reach work, wrong for a NEW host-read
40305
+ // flag whose reader older hosts simply do not have. Excluded from
40306
+ // projectDefinition, so setting it does not roll FLAGS_SCHEMA_VERSION.
40307
+ since: "0.28.421"
40308
+ },
40281
40309
  {
40282
40310
  key: "slack-hot-thread-guard",
40283
40311
  description: "Server-side hot-thread guard on the slack.reply surface (ENG-7462). Prevents an agent posting a NEW top-level Slack message when it meant to reply inside the thread it is already working in - a prompt/memory rule proved insufficient (the agent had the rule and still slipped). When a reply would otherwise post to channel ROOT (no thread_ts / message_ts / inbound_id, no active kanban card) and the agent has a recent active thread in that channel (its last bot-posted thread, from the persisted trackedThreads cache, within a freshness window), the reply is redirected into that thread. proactive:true no longer implies channel root; posting at root becomes a deliberate action (the to_channel_root flag, or the thread_ts:null sentinel). off = guard never runs, replies with no coords root exactly as today (ships dark). shadow = compute + log the would-redirect but STILL post to root (measure the fire rate before acting). enforce = apply the redirect (a soft-block: redirect + inform, never a hard rejection). Read live from the heartbeat flags-cache (or the env override); enforce is a deliberate per-org flip after a shadow soak.",
@@ -40888,6 +40916,17 @@ var FLAG_REGISTRY = [
40888
40916
  // direction - a flag-DB read error must not resurrect the mis-threading/noise the kill
40889
40917
  // fixes. Net-new gate, registry-only (ADR-0022); API-evaluated, no env materialization.
40890
40918
  defaultValue: false
40919
+ },
40920
+ {
40921
+ key: "onboarding-msteams-channel",
40922
+ description: `Offer Microsoft Teams as a selectable channel on onboarding step 3 ("which channels should your org use?", ENG-8146). OFF (default) hides the Teams option entirely: it is not rendered, not selectable, and not named in the plan-upgrade note. Turn it ON per org to restore the option for a customer who genuinely runs on Teams. Resolved SERVER-SIDE in the step-3 server component for the URL's orgId (webapp lib/feature-flags-org-server.ts), NOT via the browser public map \u2014 the client flag path evaluates at the active-org cookie, which onboarding deliberately does not pin to the org being onboarded, so a client read would resolve the wrong org for a multi-org user. Hiding the option never revokes a channel: an org that already saved msteams keeps it (step 3 carries hidden-but-saved channels through the write). Ships dark.`,
40923
+ flagType: "boolean",
40924
+ // Declared safe value is `false`: Teams hidden. `false` is also the fail-closed
40925
+ // direction - a flag-DB read error must show the narrower channel set, never
40926
+ // surface an option the operator has not deliberately turned on. Net-new gate,
40927
+ // registry-only (ADR-0022). NOT `public`: it is resolved server-side only, so it
40928
+ // must never be serialized into the browser map.
40929
+ defaultValue: false
40891
40930
  }
40892
40931
  ];
40893
40932
  var REGISTRY_BY_KEY = new Map(FLAG_REGISTRY.map((definition) => [definition.key, definition]));
@@ -30345,6 +30345,54 @@ function resolveSlackHotThreadMode(opts) {
30345
30345
  if (cached2 !== void 0) return cached2;
30346
30346
  return "off";
30347
30347
  }
30348
+ var SLACK_SCHEDULED_CHANNEL_GUARD_FLAG_KEY = "slack-scheduled-channel-guard";
30349
+ function parseSlackScheduledChannelGuardMode(raw) {
30350
+ if (raw === void 0) return void 0;
30351
+ const v = raw.trim().toLowerCase();
30352
+ if (v === "") return void 0;
30353
+ if (v === "off" || v === "shadow" || v === "enforce") return v;
30354
+ const b = envBoolean(raw);
30355
+ if (b === true) return "enforce";
30356
+ if (b === false) return "off";
30357
+ return void 0;
30358
+ }
30359
+ function cachedSlackScheduledChannelGuardMode(path) {
30360
+ try {
30361
+ if (!existsSync(path)) return void 0;
30362
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
30363
+ if (!parsed || typeof parsed !== "object") return void 0;
30364
+ const flags = parsed.flags;
30365
+ if (!flags || typeof flags !== "object") return void 0;
30366
+ const value = flags[SLACK_SCHEDULED_CHANNEL_GUARD_FLAG_KEY];
30367
+ if (typeof value === "string") return parseSlackScheduledChannelGuardMode(value);
30368
+ if (typeof value === "boolean") return value ? "enforce" : "off";
30369
+ return void 0;
30370
+ } catch {
30371
+ return void 0;
30372
+ }
30373
+ }
30374
+ var SCHEDULED_CHANNEL_GUARD_ENFORCE_READY = false;
30375
+ var warnedEnforceClamped = false;
30376
+ function clampScheduledChannelGuardMode(mode) {
30377
+ if (mode !== "enforce" || SCHEDULED_CHANNEL_GUARD_ENFORCE_READY) return mode;
30378
+ if (!warnedEnforceClamped) {
30379
+ warnedEnforceClamped = true;
30380
+ process.stderr.write(
30381
+ "slack-channel: slack-scheduled-channel-guard=enforce requested but CLAMPED to shadow - enforce is blocked until ENG-8142 (the Stop hook treats a refused reply as a delivered one, so a blocked send would fail silently). Detection and counters still run.\n"
30382
+ );
30383
+ }
30384
+ return "shadow";
30385
+ }
30386
+ function resolveSlackScheduledChannelGuardMode(opts) {
30387
+ const env2 = opts?.env ?? process.env;
30388
+ const modeOverride = parseSlackScheduledChannelGuardMode(
30389
+ env2["AGT_SLACK_SCHEDULED_CHANNEL_GUARD_MODE"]
30390
+ );
30391
+ if (modeOverride !== void 0) return clampScheduledChannelGuardMode(modeOverride);
30392
+ const cached2 = cachedSlackScheduledChannelGuardMode(opts?.cachePath ?? defaultFlagsCachePath());
30393
+ if (cached2 !== void 0) return clampScheduledChannelGuardMode(cached2);
30394
+ return "shadow";
30395
+ }
30348
30396
 
30349
30397
  // src/maintenance-mode.ts
30350
30398
  var FLAG_KEY = "platform-maintenance-mode";
@@ -34197,6 +34245,34 @@ var FLAG_REGISTRY = [
34197
34245
  // change (ADR-0022 sensitive-flag confirm).
34198
34246
  sensitive: true
34199
34247
  },
34248
+ {
34249
+ key: "slack-scheduled-channel-guard",
34250
+ description: 'Guard on slack.reply that refuses to let an active scheduled-task destination silently move a reply to a DIFFERENT channel than the agent named (ENG-8137). CS-1505 gave an explicit inbound_id priority over the task destination but left the no-inbound_id case treating "no inbound reference" as "no destination specified" - and `channel` is a REQUIRED argument, so every call names one. The symptom: a question asked in one channel answered in another, and a reply meant as a DM landing in a public channel. Fires only when the marker carries an EXPLICIT target, the named channel differs from it, and neither an inbound_id, an enforce binding, nor an ENG-7542 channel correction is available to arbitrate. off = never runs (today behaviour: silent retarget). shadow = count + log the would-block but STILL route as today, so the false-positive rate is measurable before anyone refuses a send. enforce = block the reply and tell the agent to retry. Read live from the heartbeat flags-cache (or the env override). NOTE: enforce is currently CLAMPED to shadow host-side and setting it here will not take effect - it stays blocked until ENG-8142 teaches the Stop hook to tell a refused reply from a delivered one, because until then a blocked send fails silently (no recovery, no re-prompt, nothing in the channel). Lifting the clamp is a code change, not a flag flip.',
34251
+ flagType: "enum",
34252
+ allowedValues: ["off", "shadow", "enforce"],
34253
+ // Ships in SHADOW, not off: the counter is the whole point - the fleet metric
34254
+ // is currently blind to this misroute, which is why it went unnoticed. shadow
34255
+ // is a free local log line + counter (no model spend, no behaviour change), so
34256
+ // it measures the fire rate immediately. enforce turns a misroute into a
34257
+ // REFUSED send, which trades a confidentiality risk for an availability one -
34258
+ // that is the audited per-org flip, taken after reading the shadow numbers and
34259
+ // after ENG-8143 shrinks the stale-marker window that dominates the false
34260
+ // positives.
34261
+ defaultValue: "shadow",
34262
+ // Enum override AGT_SLACK_SCHEDULED_CHANNEL_GUARD_MODE (off|shadow|enforce),
34263
+ // resolved by resolveSlackScheduledChannelGuardMode in the channel-server bundle.
34264
+ envVar: "AGT_SLACK_SCHEDULED_CHANNEL_GUARD_MODE",
34265
+ // enforce REFUSES a send the agent asked to make - a visible availability
34266
+ // change, so flipping toward it is deliberate (ADR-0022 sensitive-flag confirm).
34267
+ sensitive: true,
34268
+ // ENG-8149: the agt-cli that first carries resolveSlackScheduledChannelGuardMode
34269
+ // (published from the ENG-8137 merge c6ff71cd). Without this the flip-reach modal
34270
+ // reports FULL reach, because an undefined `since` means "every host can honour
34271
+ // it" - true for flags that predate the reach work, wrong for a NEW host-read
34272
+ // flag whose reader older hosts simply do not have. Excluded from
34273
+ // projectDefinition, so setting it does not roll FLAGS_SCHEMA_VERSION.
34274
+ since: "0.28.421"
34275
+ },
34200
34276
  {
34201
34277
  key: "slack-hot-thread-guard",
34202
34278
  description: "Server-side hot-thread guard on the slack.reply surface (ENG-7462). Prevents an agent posting a NEW top-level Slack message when it meant to reply inside the thread it is already working in - a prompt/memory rule proved insufficient (the agent had the rule and still slipped). When a reply would otherwise post to channel ROOT (no thread_ts / message_ts / inbound_id, no active kanban card) and the agent has a recent active thread in that channel (its last bot-posted thread, from the persisted trackedThreads cache, within a freshness window), the reply is redirected into that thread. proactive:true no longer implies channel root; posting at root becomes a deliberate action (the to_channel_root flag, or the thread_ts:null sentinel). off = guard never runs, replies with no coords root exactly as today (ships dark). shadow = compute + log the would-redirect but STILL post to root (measure the fire rate before acting). enforce = apply the redirect (a soft-block: redirect + inform, never a hard rejection). Read live from the heartbeat flags-cache (or the env override); enforce is a deliberate per-org flip after a shadow soak.",
@@ -34807,6 +34883,17 @@ var FLAG_REGISTRY = [
34807
34883
  // direction - a flag-DB read error must not resurrect the mis-threading/noise the kill
34808
34884
  // fixes. Net-new gate, registry-only (ADR-0022); API-evaluated, no env materialization.
34809
34885
  defaultValue: false
34886
+ },
34887
+ {
34888
+ key: "onboarding-msteams-channel",
34889
+ description: `Offer Microsoft Teams as a selectable channel on onboarding step 3 ("which channels should your org use?", ENG-8146). OFF (default) hides the Teams option entirely: it is not rendered, not selectable, and not named in the plan-upgrade note. Turn it ON per org to restore the option for a customer who genuinely runs on Teams. Resolved SERVER-SIDE in the step-3 server component for the URL's orgId (webapp lib/feature-flags-org-server.ts), NOT via the browser public map \u2014 the client flag path evaluates at the active-org cookie, which onboarding deliberately does not pin to the org being onboarded, so a client read would resolve the wrong org for a multi-org user. Hiding the option never revokes a channel: an org that already saved msteams keeps it (step 3 carries hidden-but-saved channels through the write). Ships dark.`,
34890
+ flagType: "boolean",
34891
+ // Declared safe value is `false`: Teams hidden. `false` is also the fail-closed
34892
+ // direction - a flag-DB read error must show the narrower channel set, never
34893
+ // surface an option the operator has not deliberately turned on. Net-new gate,
34894
+ // registry-only (ADR-0022). NOT `public`: it is resolved server-side only, so it
34895
+ // must never be serialized into the browser map.
34896
+ defaultValue: false
34810
34897
  }
34811
34898
  ];
34812
34899
  var REGISTRY_BY_KEY = new Map(FLAG_REGISTRY.map((definition) => [definition.key, definition]));
@@ -37598,6 +37685,8 @@ var REPLY_BINDING_CLASSIFICATION_COUNTER_SUFFIX = "-reply-binding-classification
37598
37685
  var UNKNOWN_INBOUND_ID_KEY = "unknown_inbound_id";
37599
37686
  var CHANNEL_MISTARGET_CORRECTED_KEY = "channel_mistarget_corrected";
37600
37687
  var DM_CHANNEL_MISTARGET_KEY = "dm_channel_mistarget";
37688
+ var SCHEDULED_CHANNEL_OVERRIDE_SHADOW_KEY = "scheduled_channel_override_shadow";
37689
+ var SCHEDULED_CHANNEL_OVERRIDE_BLOCKED_KEY = "scheduled_channel_override_blocked";
37601
37690
  var VALID_MODES = /* @__PURE__ */ new Set(["shadow", "warn", "enforce"]);
37602
37691
  function slackReplyBindingMode() {
37603
37692
  const raw = process.env["AGT_SLACK_REPLY_BINDING"];
@@ -37622,6 +37711,22 @@ function recordReplyBindingClassification(agentDir, channel, input) {
37622
37711
  } catch {
37623
37712
  }
37624
37713
  }
37714
+ function recordScheduledChannelOverride(agentDir, channel, input) {
37715
+ if (!agentDir) return;
37716
+ const path = join13(agentDir, `${channel}${REPLY_BINDING_CLASSIFICATION_COUNTER_SUFFIX}`);
37717
+ let counts = {};
37718
+ try {
37719
+ const parsed = JSON.parse(readFileSync13(path, "utf-8"));
37720
+ if (parsed && typeof parsed === "object") counts = parsed;
37721
+ } catch {
37722
+ }
37723
+ const key2 = input.enforced ? SCHEDULED_CHANNEL_OVERRIDE_BLOCKED_KEY : SCHEDULED_CHANNEL_OVERRIDE_SHADOW_KEY;
37724
+ counts[key2] = (counts[key2] ?? 0) + 1;
37725
+ try {
37726
+ writeFileSync10(path, JSON.stringify(counts), { mode: 384 });
37727
+ } catch {
37728
+ }
37729
+ }
37625
37730
  function recordChannelMistarget(agentDir, channel, input) {
37626
37731
  if (!agentDir) return;
37627
37732
  const path = join13(agentDir, `${channel}${REPLY_BINDING_CLASSIFICATION_COUNTER_SUFFIX}`);
@@ -37750,6 +37855,13 @@ function clearScheduledTurnMarker(agentDir) {
37750
37855
  }
37751
37856
 
37752
37857
  // src/slack-reply-scheduled-priority.ts
37858
+ function scheduledDestOverridesNamedChannel(opts) {
37859
+ if (opts.inboundId || opts.enforceBound) return false;
37860
+ if (opts.channelGuardCorrected) return false;
37861
+ if (!opts.scheduledTargetChannel) return false;
37862
+ if (!opts.namedChannel) return false;
37863
+ return opts.scheduledTargetChannel !== opts.namedChannel;
37864
+ }
37753
37865
  function resolveSlackScheduledPriority(opts) {
37754
37866
  const inboundWins = !!(opts.inboundId || opts.enforceBound);
37755
37867
  const effectiveChannel = inboundWins ? opts.bindingChannel ?? opts.fallbackChannel : opts.scheduledDest ? opts.scheduledDest.channel : opts.fallbackChannel;
@@ -41567,8 +41679,41 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
41567
41679
  );
41568
41680
  }
41569
41681
  const enforceBound = bindingMode === "enforce" && (binding.classification === "bound" || binding.classification === "coords_corrected");
41682
+ const resolvedFallbackChannel = channelGuard.corrected && channelGuard.channel ? channelGuard.channel : channel;
41570
41683
  const scheduledMarker = readScheduledTurnMarker(SLACK_AGENT_DIR);
41571
41684
  const scheduledDest = scheduledMarker ? resolveScheduledTurnDestination(scheduledMarker, { channel, thread_ts }) : null;
41685
+ const scheduledChannelGuardMode = resolveSlackScheduledChannelGuardMode();
41686
+ const scheduledChannelOverride = scheduledChannelGuardMode !== "off" && scheduledDestOverridesNamedChannel({
41687
+ inboundId: inbound_id,
41688
+ enforceBound,
41689
+ // ENG-7542 resolved a registry-VERIFIED destination - it arbitrates like
41690
+ // enforceBound rather than being treated as just another named channel.
41691
+ channelGuardCorrected: channelGuard.corrected,
41692
+ // The marker's OWN target, not the resolved dest: the resolver echoes the
41693
+ // requested channel for a target-less marker, so comparing the resolved
41694
+ // value compared the raw arg against the corrected one.
41695
+ scheduledTargetChannel: scheduledMarker?.target?.channel_id ?? null,
41696
+ namedChannel: resolvedFallbackChannel
41697
+ });
41698
+ if (scheduledChannelOverride) {
41699
+ const enforced = scheduledChannelGuardMode === "enforce";
41700
+ recordScheduledChannelOverride(SLACK_AGENT_DIR, "slack", { enforced });
41701
+ process.stderr.write(
41702
+ `slack-channel(${AGENT_CODE_NAME}): scheduled_dest_channel_override_${enforced ? "blocked" : "shadow"} task=${scheduledMarker?.task_id ?? "?"} named=${redactSlackId(resolvedFallbackChannel)} task_dest=${redactSlackId(scheduledMarker?.target?.channel_id)}
41703
+ `
41704
+ );
41705
+ if (enforced) {
41706
+ return {
41707
+ content: [
41708
+ {
41709
+ type: "text",
41710
+ text: "This reply was blocked: a scheduled task is active and its destination channel is not the channel you named, so this message would have gone somewhere you did not ask for. If you are answering someone, send again with the inbound_id from their <channel> tag - that always wins over the task destination. If this is the scheduled task's own output, write it on the task card (kanban_update / kanban_done) instead of sending it - the card delivers it for you. If this is deliberate outreach that has nothing to do with the running task, it can be sent once the task finishes."
41711
+ }
41712
+ ],
41713
+ isError: true
41714
+ };
41715
+ }
41716
+ }
41572
41717
  if (scheduledDest?.retargeted) {
41573
41718
  process.stderr.write(
41574
41719
  `slack-channel(${AGENT_CODE_NAME}): scheduled_turn_retargeted task=${scheduledMarker?.task_id ?? "?"} requested=${redactSlackId(channel)}/${redactSlackId(thread_ts)} routed=${redactSlackId(scheduledDest.channel)}/${redactSlackId(scheduledDest.thread_ts)}
@@ -41634,7 +41779,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
41634
41779
  scheduledDest: scheduledDest ?? null,
41635
41780
  bindingChannel: binding.channel,
41636
41781
  bindingThreadTs: binding.threadTs,
41637
- fallbackChannel: channelGuard.corrected && channelGuard.channel ? channelGuard.channel : channel,
41782
+ fallbackChannel: resolvedFallbackChannel,
41638
41783
  fallbackThreadTs: channelGuard.corrected ? channelGuard.threadTs : hotThreadMode === "enforce" && hotGuard ? hotGuard.threadTs : baseThreadTs,
41639
41784
  fallbackSettleThreadTs: channelGuard.corrected ? channelGuard.threadTs : thread_ts
41640
41785
  });
@@ -34547,6 +34547,34 @@ var FLAG_REGISTRY = [
34547
34547
  // change (ADR-0022 sensitive-flag confirm).
34548
34548
  sensitive: true
34549
34549
  },
34550
+ {
34551
+ key: "slack-scheduled-channel-guard",
34552
+ description: 'Guard on slack.reply that refuses to let an active scheduled-task destination silently move a reply to a DIFFERENT channel than the agent named (ENG-8137). CS-1505 gave an explicit inbound_id priority over the task destination but left the no-inbound_id case treating "no inbound reference" as "no destination specified" - and `channel` is a REQUIRED argument, so every call names one. The symptom: a question asked in one channel answered in another, and a reply meant as a DM landing in a public channel. Fires only when the marker carries an EXPLICIT target, the named channel differs from it, and neither an inbound_id, an enforce binding, nor an ENG-7542 channel correction is available to arbitrate. off = never runs (today behaviour: silent retarget). shadow = count + log the would-block but STILL route as today, so the false-positive rate is measurable before anyone refuses a send. enforce = block the reply and tell the agent to retry. Read live from the heartbeat flags-cache (or the env override). NOTE: enforce is currently CLAMPED to shadow host-side and setting it here will not take effect - it stays blocked until ENG-8142 teaches the Stop hook to tell a refused reply from a delivered one, because until then a blocked send fails silently (no recovery, no re-prompt, nothing in the channel). Lifting the clamp is a code change, not a flag flip.',
34553
+ flagType: "enum",
34554
+ allowedValues: ["off", "shadow", "enforce"],
34555
+ // Ships in SHADOW, not off: the counter is the whole point - the fleet metric
34556
+ // is currently blind to this misroute, which is why it went unnoticed. shadow
34557
+ // is a free local log line + counter (no model spend, no behaviour change), so
34558
+ // it measures the fire rate immediately. enforce turns a misroute into a
34559
+ // REFUSED send, which trades a confidentiality risk for an availability one -
34560
+ // that is the audited per-org flip, taken after reading the shadow numbers and
34561
+ // after ENG-8143 shrinks the stale-marker window that dominates the false
34562
+ // positives.
34563
+ defaultValue: "shadow",
34564
+ // Enum override AGT_SLACK_SCHEDULED_CHANNEL_GUARD_MODE (off|shadow|enforce),
34565
+ // resolved by resolveSlackScheduledChannelGuardMode in the channel-server bundle.
34566
+ envVar: "AGT_SLACK_SCHEDULED_CHANNEL_GUARD_MODE",
34567
+ // enforce REFUSES a send the agent asked to make - a visible availability
34568
+ // change, so flipping toward it is deliberate (ADR-0022 sensitive-flag confirm).
34569
+ sensitive: true,
34570
+ // ENG-8149: the agt-cli that first carries resolveSlackScheduledChannelGuardMode
34571
+ // (published from the ENG-8137 merge c6ff71cd). Without this the flip-reach modal
34572
+ // reports FULL reach, because an undefined `since` means "every host can honour
34573
+ // it" - true for flags that predate the reach work, wrong for a NEW host-read
34574
+ // flag whose reader older hosts simply do not have. Excluded from
34575
+ // projectDefinition, so setting it does not roll FLAGS_SCHEMA_VERSION.
34576
+ since: "0.28.421"
34577
+ },
34550
34578
  {
34551
34579
  key: "slack-hot-thread-guard",
34552
34580
  description: "Server-side hot-thread guard on the slack.reply surface (ENG-7462). Prevents an agent posting a NEW top-level Slack message when it meant to reply inside the thread it is already working in - a prompt/memory rule proved insufficient (the agent had the rule and still slipped). When a reply would otherwise post to channel ROOT (no thread_ts / message_ts / inbound_id, no active kanban card) and the agent has a recent active thread in that channel (its last bot-posted thread, from the persisted trackedThreads cache, within a freshness window), the reply is redirected into that thread. proactive:true no longer implies channel root; posting at root becomes a deliberate action (the to_channel_root flag, or the thread_ts:null sentinel). off = guard never runs, replies with no coords root exactly as today (ships dark). shadow = compute + log the would-redirect but STILL post to root (measure the fire rate before acting). enforce = apply the redirect (a soft-block: redirect + inform, never a hard rejection). Read live from the heartbeat flags-cache (or the env override); enforce is a deliberate per-org flip after a shadow soak.",
@@ -35157,6 +35185,17 @@ var FLAG_REGISTRY = [
35157
35185
  // direction - a flag-DB read error must not resurrect the mis-threading/noise the kill
35158
35186
  // fixes. Net-new gate, registry-only (ADR-0022); API-evaluated, no env materialization.
35159
35187
  defaultValue: false
35188
+ },
35189
+ {
35190
+ key: "onboarding-msteams-channel",
35191
+ description: `Offer Microsoft Teams as a selectable channel on onboarding step 3 ("which channels should your org use?", ENG-8146). OFF (default) hides the Teams option entirely: it is not rendered, not selectable, and not named in the plan-upgrade note. Turn it ON per org to restore the option for a customer who genuinely runs on Teams. Resolved SERVER-SIDE in the step-3 server component for the URL's orgId (webapp lib/feature-flags-org-server.ts), NOT via the browser public map \u2014 the client flag path evaluates at the active-org cookie, which onboarding deliberately does not pin to the org being onboarded, so a client read would resolve the wrong org for a multi-org user. Hiding the option never revokes a channel: an org that already saved msteams keeps it (step 3 carries hidden-but-saved channels through the write). Ships dark.`,
35192
+ flagType: "boolean",
35193
+ // Declared safe value is `false`: Teams hidden. `false` is also the fail-closed
35194
+ // direction - a flag-DB read error must show the narrower channel set, never
35195
+ // surface an option the operator has not deliberately turned on. Net-new gate,
35196
+ // registry-only (ADR-0022). NOT `public`: it is resolved server-side only, so it
35197
+ // must never be serialized into the browser map.
35198
+ defaultValue: false
35160
35199
  }
35161
35200
  ];
35162
35201
  var REGISTRY_BY_KEY = new Map(FLAG_REGISTRY.map((definition) => [definition.key, definition]));
@@ -36,7 +36,7 @@ import {
36
36
  writeDirectChatSessionState,
37
37
  writeEgressAllowlist,
38
38
  writePersistentClaudeWrapper
39
- } from "./chunk-LLIMZG67.js";
39
+ } from "./chunk-XFC7YVGS.js";
40
40
  import "./chunk-XWVM4KPK.js";
41
41
  export {
42
42
  EGRESS_BASELINE_DOMAINS,
@@ -77,4 +77,4 @@ export {
77
77
  writeEgressAllowlist,
78
78
  writePersistentClaudeWrapper
79
79
  };
80
- //# sourceMappingURL=persistent-session-YNQQHPFS.js.map
80
+ //# sourceMappingURL=persistent-session-HWCKGPYF.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  paneLogPath
3
- } from "./chunk-LLIMZG67.js";
3
+ } from "./chunk-XFC7YVGS.js";
4
4
  import "./chunk-XWVM4KPK.js";
5
5
 
6
6
  // src/lib/responsiveness-probe.ts
@@ -185,7 +185,16 @@ var KNOWN_REPLY_BINDING_KEYS = /* @__PURE__ */ new Set([
185
185
  "delivery_ledger_self",
186
186
  "delivery_ledger_moved_on",
187
187
  "delivery_ledger_clear",
188
- "delivery_ledger_fallback"
188
+ "delivery_ledger_fallback",
189
+ // ENG-8137: the scheduled-turn channel guard writes these into the same counter
190
+ // file (recordScheduledChannelOverride). `_shadow` counts detections while the
191
+ // flag observes without changing routing - the false-positive signal that gates
192
+ // the enforce flip; `_blocked` counts sends actually refused under enforce.
193
+ // Allowlisted so both reach CloudWatch: without them this guard would fire
194
+ // fleet-wide with no observable rate, which is the blindness that let the
195
+ // original misroute reach customers unnoticed.
196
+ "scheduled_channel_override_shadow",
197
+ "scheduled_channel_override_blocked"
189
198
  ]);
190
199
  function readAndResetSlackReplyBindingClassifications(agentHomeDir) {
191
200
  let names;
@@ -462,4 +471,4 @@ export {
462
471
  readAndResetSlackReplyBindingClassifications,
463
472
  readAndResetSlackReplyTargetClassifications
464
473
  };
465
- //# sourceMappingURL=responsiveness-probe-KVEPIRMG.js.map
474
+ //# sourceMappingURL=responsiveness-probe-4CGMUXOH.js.map