@integrity-labs/agt-cli 0.27.25 → 0.27.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/agt.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  success,
28
28
  table,
29
29
  warn
30
- } from "../chunk-GI73VOGA.js";
30
+ } from "../chunk-ZYFZYWPV.js";
31
31
  import {
32
32
  CHANNEL_REGISTRY,
33
33
  DEPLOYMENT_TEMPLATES,
@@ -4643,7 +4643,7 @@ import { execFileSync, execSync } from "child_process";
4643
4643
  import { existsSync as existsSync10, realpathSync as realpathSync2 } from "fs";
4644
4644
  import chalk18 from "chalk";
4645
4645
  import ora16 from "ora";
4646
- var cliVersion = true ? "0.27.25" : "dev";
4646
+ var cliVersion = true ? "0.27.27" : "dev";
4647
4647
  async function fetchLatestVersion() {
4648
4648
  const host2 = getHost();
4649
4649
  if (!host2) return null;
@@ -5175,7 +5175,7 @@ function handleError(err) {
5175
5175
  }
5176
5176
 
5177
5177
  // src/bin/agt.ts
5178
- var cliVersion2 = true ? "0.27.25" : "dev";
5178
+ var cliVersion2 = true ? "0.27.27" : "dev";
5179
5179
  var program = new Command();
5180
5180
  program.name("agt").description("Augmented CLI \u2014 agent provisioning and management").version(cliVersion2).option("--json", "Emit machine-readable JSON output (suppress spinners and colors)").option("--skip-update-check", "Skip the automatic update check on startup");
5181
5181
  program.hook("preAction", (thisCommand) => {
@@ -6945,4 +6945,4 @@ export {
6945
6945
  managerInstallSystemUnitCommand,
6946
6946
  managerUninstallSystemUnitCommand
6947
6947
  };
6948
- //# sourceMappingURL=chunk-GI73VOGA.js.map
6948
+ //# sourceMappingURL=chunk-ZYFZYWPV.js.map
@@ -15,7 +15,7 @@ import {
15
15
  provisionOrientHook,
16
16
  provisionStopHook,
17
17
  requireHost
18
- } from "../chunk-GI73VOGA.js";
18
+ } from "../chunk-ZYFZYWPV.js";
19
19
  import {
20
20
  getProjectDir as getProjectDir2,
21
21
  getReadyTasks,
@@ -3208,7 +3208,7 @@ var cachedFrameworkVersion = null;
3208
3208
  var lastVersionCheckAt = 0;
3209
3209
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
3210
3210
  var lastResponsivenessProbeAt = 0;
3211
- var agtCliVersion = true ? "0.27.25" : "dev";
3211
+ var agtCliVersion = true ? "0.27.27" : "dev";
3212
3212
  function resolveBrewPath(execFileSync4) {
3213
3213
  try {
3214
3214
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -14261,11 +14261,62 @@ function decideSenderPolicyForward(evt, policy) {
14261
14261
  return decideModeForward(evt, policy);
14262
14262
  }
14263
14263
 
14264
+ // src/sender-policy-decline.ts
14265
+ function classifySlackPolicyBlock(evt, policy) {
14266
+ if (policy.internalOnly && (!policy.homeTeamId || evt.team !== policy.homeTeamId)) {
14267
+ return "internal_only";
14268
+ }
14269
+ switch (policy.mode) {
14270
+ case "manager_only":
14271
+ return "mode_manager_only";
14272
+ case "team_agents_only":
14273
+ return "mode_team_agents_only";
14274
+ case "agents_only":
14275
+ return "mode_agents_only";
14276
+ case "all":
14277
+ return "internal_only";
14278
+ }
14279
+ }
14280
+ function politeDeclineCopy(reason) {
14281
+ switch (reason) {
14282
+ case "internal_only":
14283
+ return "Sorry, I can only respond to people inside our organisation. This conversation appears to be from outside.";
14284
+ case "mode_manager_only":
14285
+ return "Sorry, I only respond to my manager in this channel.";
14286
+ case "mode_team_agents_only":
14287
+ return "Sorry, I only respond to agents on my team in this channel.";
14288
+ case "mode_agents_only":
14289
+ return "Sorry, I only respond to other agents in this channel.";
14290
+ }
14291
+ }
14292
+ function decideDeclineReply(input) {
14293
+ const last = input.cache.get(input.key);
14294
+ if (last !== void 0) {
14295
+ const elapsed = input.now - last;
14296
+ if (elapsed < input.cooldownMs) {
14297
+ return { reply: false, remainingMs: input.cooldownMs - elapsed };
14298
+ }
14299
+ }
14300
+ input.cache.set(input.key, input.now);
14301
+ return { reply: true };
14302
+ }
14303
+ function declineCacheKey(channelId, senderId, reason) {
14304
+ return `${channelId}|${senderId}|${reason}`;
14305
+ }
14306
+ function readDeclineCooldownMs(envVarName, defaultSeconds = 1800) {
14307
+ const raw = process.env[envVarName];
14308
+ if (!raw) return defaultSeconds * 1e3;
14309
+ const parsed = Number(raw);
14310
+ if (!Number.isFinite(parsed) || parsed < 0) return defaultSeconds * 1e3;
14311
+ return parsed * 1e3;
14312
+ }
14313
+
14264
14314
  // src/ack-reaction.ts
14265
14315
  import { readdirSync, readFileSync } from "fs";
14266
14316
  import { join } from "path";
14267
14317
  var REPLY_WEDGED_THRESHOLD_MS = 5 * 60 * 1e3;
14268
14318
  var ACK_STARTUP_GRACE_MS = 6e4;
14319
+ var ACK_PANE_FRESH_THRESHOLD_MS = 6e4;
14269
14320
  function decideAckReaction(i) {
14270
14321
  if (!i.hasTarget) return "none";
14271
14322
  if (!i.integrationReady) return "undeliverable";
@@ -14273,6 +14324,11 @@ function decideAckReaction(i) {
14273
14324
  if (!i.withinStartupGrace && i.claude === "dead") return "undeliverable";
14274
14325
  const threshold = i.pendingStaleThresholdMs ?? REPLY_WEDGED_THRESHOLD_MS;
14275
14326
  if (i.oldestPendingAgeMs != null && i.oldestPendingAgeMs > threshold) {
14327
+ const paneFreshThreshold = i.paneFreshThresholdMs ?? ACK_PANE_FRESH_THRESHOLD_MS;
14328
+ const paneIsFresh = i.paneLogFreshAgeMs != null && i.paneLogFreshAgeMs <= paneFreshThreshold;
14329
+ if (paneIsFresh && i.tmux === "alive" && i.claude === "alive") {
14330
+ return "ack";
14331
+ }
14276
14332
  return "undeliverable";
14277
14333
  }
14278
14334
  return "ack";
@@ -14567,6 +14623,69 @@ var SLACK_EGRESS_TOOLS = /* @__PURE__ */ new Set([
14567
14623
  "slack.upload_file"
14568
14624
  ]);
14569
14625
 
14626
+ // src/slack-pending-inbound-cleanup.ts
14627
+ import { existsSync, readdirSync as readdirSync2, statSync, unlinkSync } from "fs";
14628
+ import { join as join2 } from "path";
14629
+ function sanitizeMarkerSegment(value) {
14630
+ return value.replace(/[^A-Za-z0-9_-]/g, "_");
14631
+ }
14632
+ var defaultClearMarkerFile = (fullPath) => {
14633
+ try {
14634
+ if (existsSync(fullPath)) unlinkSync(fullPath);
14635
+ } catch {
14636
+ }
14637
+ };
14638
+ function clearAllSlackPendingMarkersForThread(dir, channel, threadTs, clear = defaultClearMarkerFile) {
14639
+ if (!dir) return 0;
14640
+ const prefix = `${sanitizeMarkerSegment(channel)}__${sanitizeMarkerSegment(threadTs)}__`;
14641
+ let cleared = 0;
14642
+ try {
14643
+ for (const f of readdirSync2(dir)) {
14644
+ if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
14645
+ clear(join2(dir, f));
14646
+ cleared += 1;
14647
+ }
14648
+ } catch {
14649
+ }
14650
+ return cleared;
14651
+ }
14652
+ function clearSlackPendingMarkerByMessageTs(dir, channel, messageTs, clear = defaultClearMarkerFile) {
14653
+ if (!dir) return 0;
14654
+ const channelPrefix = `${sanitizeMarkerSegment(channel)}__`;
14655
+ const messageSuffix = `__${sanitizeMarkerSegment(messageTs)}.json`;
14656
+ let cleared = 0;
14657
+ try {
14658
+ for (const f of readdirSync2(dir)) {
14659
+ if (!f.startsWith(channelPrefix) || !f.endsWith(messageSuffix)) continue;
14660
+ clear(join2(dir, f));
14661
+ cleared += 1;
14662
+ }
14663
+ } catch {
14664
+ }
14665
+ return cleared;
14666
+ }
14667
+ function clearOldestSlackPendingMarkerInChannel(dir, channel, clear = defaultClearMarkerFile) {
14668
+ if (!dir) return null;
14669
+ const channelPrefix = `${sanitizeMarkerSegment(channel)}__`;
14670
+ try {
14671
+ const entries = readdirSync2(dir).filter((f) => f.startsWith(channelPrefix) && f.endsWith(".json")).map((f) => {
14672
+ const full = join2(dir, f);
14673
+ let mtime = 0;
14674
+ try {
14675
+ mtime = statSync(full).mtimeMs;
14676
+ } catch {
14677
+ }
14678
+ return { name: f, full, mtime };
14679
+ }).filter((e) => e.mtime > 0).sort((a, b) => a.mtime - b.mtime);
14680
+ const oldest = entries[0];
14681
+ if (!oldest) return null;
14682
+ clear(oldest.full);
14683
+ return oldest.name;
14684
+ } catch {
14685
+ return null;
14686
+ }
14687
+ }
14688
+
14570
14689
  // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
14571
14690
  import process2 from "process";
14572
14691
 
@@ -14663,17 +14782,17 @@ var StdioServerTransport = class {
14663
14782
  import {
14664
14783
  chmodSync,
14665
14784
  createWriteStream,
14666
- existsSync as existsSync2,
14785
+ existsSync as existsSync3,
14667
14786
  mkdirSync as mkdirSync3,
14668
14787
  readFileSync as readFileSync4,
14669
- readdirSync as readdirSync2,
14788
+ readdirSync as readdirSync3,
14670
14789
  renameSync as renameSync2,
14671
- statSync,
14672
- unlinkSync as unlinkSync2,
14790
+ statSync as statSync2,
14791
+ unlinkSync as unlinkSync3,
14673
14792
  watch,
14674
14793
  writeFileSync as writeFileSync3
14675
14794
  } from "fs";
14676
- import { basename, join as join4, resolve as resolve2 } from "path";
14795
+ import { basename, join as join5, resolve as resolve2 } from "path";
14677
14796
  import { homedir as homedir2 } from "os";
14678
14797
  import { createHash, randomUUID } from "crypto";
14679
14798
 
@@ -14804,9 +14923,9 @@ async function runOrRetry(fn, opts) {
14804
14923
 
14805
14924
  // src/channel-attachments.ts
14806
14925
  import { homedir } from "os";
14807
- import { join as join2, resolve, sep } from "path";
14926
+ import { join as join3, resolve, sep } from "path";
14808
14927
  function resolveChannelInboundDir(codeName, channelSlug) {
14809
- const base = join2(homedir(), ".augmented");
14928
+ const base = join3(homedir(), ".augmented");
14810
14929
  const allowedSegment = /^[A-Za-z0-9_-]+$/;
14811
14930
  if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
14812
14931
  throw new Error(
@@ -15450,14 +15569,14 @@ function createSlackBotUserIdClient(args) {
15450
15569
 
15451
15570
  // src/mcp-spawn-lock.ts
15452
15571
  import {
15453
- existsSync,
15572
+ existsSync as existsSync2,
15454
15573
  mkdirSync as mkdirSync2,
15455
15574
  readFileSync as readFileSync3,
15456
15575
  renameSync,
15457
- unlinkSync,
15576
+ unlinkSync as unlinkSync2,
15458
15577
  writeFileSync as writeFileSync2
15459
15578
  } from "fs";
15460
- import { join as join3 } from "path";
15579
+ import { join as join4 } from "path";
15461
15580
  function defaultIsPidAlive(pid) {
15462
15581
  if (!Number.isFinite(pid) || pid <= 0) return false;
15463
15582
  try {
@@ -15475,7 +15594,7 @@ function acquireMcpSpawnLock(args) {
15475
15594
  const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
15476
15595
  const selfPid = options.selfPid ?? process.pid;
15477
15596
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
15478
- const path = join3(agentDir, basename2);
15597
+ const path = join4(agentDir, basename2);
15479
15598
  const existing = readLockHolder(path);
15480
15599
  if (existing) {
15481
15600
  if (existing.pid === selfPid) {
@@ -15499,12 +15618,12 @@ function releaseMcpSpawnLock(lockPath, opts = {}) {
15499
15618
  if (!existing) return;
15500
15619
  if (existing.pid !== selfPid) return;
15501
15620
  try {
15502
- unlinkSync(lockPath);
15621
+ unlinkSync2(lockPath);
15503
15622
  } catch {
15504
15623
  }
15505
15624
  }
15506
15625
  function readLockHolder(path) {
15507
- if (!existsSync(path)) return null;
15626
+ if (!existsSync2(path)) return null;
15508
15627
  try {
15509
15628
  const raw = readFileSync3(path, "utf8");
15510
15629
  const parsed = JSON.parse(raw);
@@ -15557,6 +15676,58 @@ var SLACK_SENDER_POLICY = (() => {
15557
15676
  }
15558
15677
  throw new Error(`Invalid SLACK_SENDER_POLICY=${JSON.stringify(process.env.SLACK_SENDER_POLICY)}`);
15559
15678
  })();
15679
+ var SLACK_POLICY_DECLINE_CACHE = /* @__PURE__ */ new Map();
15680
+ var SLACK_POLICY_DECLINE_COOLDOWN_MS = readDeclineCooldownMs(
15681
+ "SLACK_SENDER_POLICY_REPLY_COOLDOWN_SEC"
15682
+ );
15683
+ async function maybeSendSenderPolicyDecline(args) {
15684
+ if (!BOT_TOKEN) return;
15685
+ if (!args.channel || !args.senderId) return;
15686
+ const key2 = declineCacheKey(args.channel, args.senderId, args.subReason);
15687
+ const decision = decideDeclineReply({
15688
+ cache: SLACK_POLICY_DECLINE_CACHE,
15689
+ key: key2,
15690
+ cooldownMs: SLACK_POLICY_DECLINE_COOLDOWN_MS,
15691
+ now: Date.now()
15692
+ });
15693
+ if (!decision.reply) {
15694
+ process.stderr.write(
15695
+ `slack-channel(${AGENT_CODE_NAME}): decline suppressed by cooldown (channel=${redactSlackId(args.channel)}, sender=${redactSlackId(args.senderId)}, reason=${args.subReason}, remaining=${decision.remainingMs}ms)
15696
+ `
15697
+ );
15698
+ return;
15699
+ }
15700
+ const text = politeDeclineCopy(args.subReason);
15701
+ const controller = new AbortController();
15702
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
15703
+ try {
15704
+ const res = await fetch("https://slack.com/api/chat.postMessage", {
15705
+ method: "POST",
15706
+ signal: controller.signal,
15707
+ headers: {
15708
+ "Content-Type": "application/json",
15709
+ Authorization: `Bearer ${BOT_TOKEN}`
15710
+ },
15711
+ body: JSON.stringify({
15712
+ channel: args.channel,
15713
+ text,
15714
+ // Reply in-thread if the inbound was a thread message — keeps
15715
+ // the decline next to the message that caused it. For top-level
15716
+ // posts (DMs / channel root) omit thread_ts and post inline.
15717
+ ...args.threadTs ? { thread_ts: args.threadTs } : {}
15718
+ })
15719
+ });
15720
+ const data = await res.json();
15721
+ if (!data.ok) {
15722
+ process.stderr.write(
15723
+ `slack-channel(${AGENT_CODE_NAME}): decline post failed: ${data.error ?? "unknown"}
15724
+ `
15725
+ );
15726
+ }
15727
+ } finally {
15728
+ clearTimeout(timeoutId);
15729
+ }
15730
+ }
15560
15731
  var BLOCK_KIT_ENABLED = process.env.SLACK_BLOCK_KIT_ENABLED === "true";
15561
15732
  var BLOCK_KIT_ASK_USER_ENABLED = process.env.SLACK_BLOCK_KIT_ASK_USER_ENABLED === "true";
15562
15733
  var BLOCK_KIT_DISABLED = process.env.SLACK_BLOCK_KIT_DISABLED === "true";
@@ -15608,9 +15779,9 @@ var SLACK_PEER_CLASSIFIER_CONFIG = {
15608
15779
  peers: parsePeersEnv(process.env.SLACK_PEERS, process.env.SLACK_PEERS_GATE),
15609
15780
  peer_disabled_mode: SLACK_PEER_DISABLED_MODE
15610
15781
  };
15611
- var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join4(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
15612
- var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join4(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
15613
- var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join4(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
15782
+ var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join5(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
15783
+ var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join5(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
15784
+ var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join5(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
15614
15785
  var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
15615
15786
  function redactSlackId(id) {
15616
15787
  if (!id) return "<none>";
@@ -15622,7 +15793,7 @@ function safeSlackMarkerName(channel, threadTs, messageTs) {
15622
15793
  }
15623
15794
  function slackPendingInboundPath(channel, threadTs, messageTs) {
15624
15795
  if (!SLACK_PENDING_INBOUND_DIR) return null;
15625
- return join4(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
15796
+ return join5(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
15626
15797
  }
15627
15798
  function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable = false) {
15628
15799
  const path = slackPendingInboundPath(channel, threadTs, messageTs);
@@ -15678,22 +15849,32 @@ function clearSlackMarkerFileWithHeal(fullPath) {
15678
15849
  healSlackUndeliverable(marker.channel, marker.message_ts);
15679
15850
  }
15680
15851
  try {
15681
- if (existsSync2(fullPath)) unlinkSync2(fullPath);
15852
+ if (existsSync3(fullPath)) unlinkSync3(fullPath);
15682
15853
  } catch {
15683
15854
  }
15684
15855
  }
15685
- function clearAllSlackPendingMarkersForThread(channel, threadTs) {
15686
- if (!SLACK_PENDING_INBOUND_DIR) return;
15687
- const safeChan = channel.replace(/[^A-Za-z0-9_-]/g, "_");
15688
- const safeThread = threadTs.replace(/[^A-Za-z0-9_-]/g, "_");
15689
- const prefix = `${safeChan}__${safeThread}__`;
15690
- try {
15691
- for (const f of readdirSync2(SLACK_PENDING_INBOUND_DIR)) {
15692
- if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
15693
- clearSlackMarkerFileWithHeal(join4(SLACK_PENDING_INBOUND_DIR, f));
15694
- }
15695
- } catch {
15696
- }
15856
+ function clearAllSlackPendingMarkersForThread2(channel, threadTs) {
15857
+ clearAllSlackPendingMarkersForThread(
15858
+ SLACK_PENDING_INBOUND_DIR,
15859
+ channel,
15860
+ threadTs,
15861
+ clearSlackMarkerFileWithHeal
15862
+ );
15863
+ }
15864
+ function clearSlackPendingMarkerByMessageTs2(channel, messageTs) {
15865
+ clearSlackPendingMarkerByMessageTs(
15866
+ SLACK_PENDING_INBOUND_DIR,
15867
+ channel,
15868
+ messageTs,
15869
+ clearSlackMarkerFileWithHeal
15870
+ );
15871
+ }
15872
+ function clearOldestSlackPendingMarkerInChannel2(channel) {
15873
+ clearOldestSlackPendingMarkerInChannel(
15874
+ SLACK_PENDING_INBOUND_DIR,
15875
+ channel,
15876
+ clearSlackMarkerFileWithHeal
15877
+ );
15697
15878
  }
15698
15879
  function slackNextRetryName(filename) {
15699
15880
  const match = filename.match(/^(.*?)(?:\.retry-(\d+))?\.json$/);
@@ -15709,7 +15890,7 @@ function slackNextRetryName(filename) {
15709
15890
  async function processSlackRecoveryOutboxFile(filename) {
15710
15891
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
15711
15892
  if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
15712
- const fullPath = join4(SLACK_RECOVERY_OUTBOX_DIR, filename);
15893
+ const fullPath = join5(SLACK_RECOVERY_OUTBOX_DIR, filename);
15713
15894
  let payload;
15714
15895
  try {
15715
15896
  payload = JSON.parse(readFileSync4(fullPath, "utf-8"));
@@ -15778,7 +15959,7 @@ async function processSlackRecoveryOutboxFile(filename) {
15778
15959
  }
15779
15960
  if (sendSucceeded) {
15780
15961
  try {
15781
- unlinkSync2(fullPath);
15962
+ unlinkSync3(fullPath);
15782
15963
  } catch {
15783
15964
  }
15784
15965
  return;
@@ -15786,7 +15967,7 @@ async function processSlackRecoveryOutboxFile(filename) {
15786
15967
  const next = slackNextRetryName(filename);
15787
15968
  if (next) {
15788
15969
  try {
15789
- renameSync2(fullPath, join4(SLACK_RECOVERY_OUTBOX_DIR, next.next));
15970
+ renameSync2(fullPath, join5(SLACK_RECOVERY_OUTBOX_DIR, next.next));
15790
15971
  if (next.attempt >= SLACK_MAX_RECOVERY_ATTEMPTS) {
15791
15972
  process.stderr.write(
15792
15973
  `slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
@@ -15816,7 +15997,7 @@ function scanSlackRecoveryRetries() {
15816
15997
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
15817
15998
  let entries;
15818
15999
  try {
15819
- entries = readdirSync2(SLACK_RECOVERY_OUTBOX_DIR);
16000
+ entries = readdirSync3(SLACK_RECOVERY_OUTBOX_DIR);
15820
16001
  } catch {
15821
16002
  return;
15822
16003
  }
@@ -15825,7 +16006,7 @@ function scanSlackRecoveryRetries() {
15825
16006
  if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
15826
16007
  let mtimeMs;
15827
16008
  try {
15828
- mtimeMs = statSync(join4(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
16009
+ mtimeMs = statSync2(join5(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
15829
16010
  } catch {
15830
16011
  continue;
15831
16012
  }
@@ -15846,7 +16027,7 @@ function startSlackRecoveryOutboxWatcher() {
15846
16027
  return;
15847
16028
  }
15848
16029
  try {
15849
- for (const f of readdirSync2(SLACK_RECOVERY_OUTBOX_DIR)) {
16030
+ for (const f of readdirSync3(SLACK_RECOVERY_OUTBOX_DIR)) {
15850
16031
  if (isFirstAttemptSlackOutboxFile(f)) void processSlackRecoveryOutboxFile(f);
15851
16032
  }
15852
16033
  } catch {
@@ -15855,7 +16036,7 @@ function startSlackRecoveryOutboxWatcher() {
15855
16036
  const watcher = watch(SLACK_RECOVERY_OUTBOX_DIR, (event, filename) => {
15856
16037
  if (event !== "rename" || !filename) return;
15857
16038
  if (!isFirstAttemptSlackOutboxFile(filename)) return;
15858
- if (existsSync2(join4(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
16039
+ if (existsSync3(join5(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
15859
16040
  void processSlackRecoveryOutboxFile(filename);
15860
16041
  }
15861
16042
  });
@@ -15876,10 +16057,10 @@ function trackPendingMessage(channel, threadTs, messageTs, undeliverable = false
15876
16057
  }
15877
16058
  function sweepSlackStaleMarkersOnBoot() {
15878
16059
  if (!SLACK_PENDING_INBOUND_DIR) return;
15879
- if (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
16060
+ if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
15880
16061
  let filenames;
15881
16062
  try {
15882
- filenames = readdirSync2(SLACK_PENDING_INBOUND_DIR);
16063
+ filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
15883
16064
  } catch (err) {
15884
16065
  process.stderr.write(
15885
16066
  `slack-channel(${AGENT_CODE_NAME}): stale-marker readdir failed: ${err.message}
@@ -15892,7 +16073,7 @@ function sweepSlackStaleMarkersOnBoot() {
15892
16073
  for (const filename of filenames) {
15893
16074
  if (!filename.endsWith(".json")) continue;
15894
16075
  if (filename.endsWith(".tmp")) continue;
15895
- const fullPath = join4(SLACK_PENDING_INBOUND_DIR, filename);
16076
+ const fullPath = join5(SLACK_PENDING_INBOUND_DIR, filename);
15896
16077
  let marker;
15897
16078
  try {
15898
16079
  marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
@@ -15902,7 +16083,7 @@ function sweepSlackStaleMarkersOnBoot() {
15902
16083
  `
15903
16084
  );
15904
16085
  try {
15905
- unlinkSync2(fullPath);
16086
+ unlinkSync3(fullPath);
15906
16087
  } catch {
15907
16088
  }
15908
16089
  cleared++;
@@ -15911,7 +16092,7 @@ function sweepSlackStaleMarkersOnBoot() {
15911
16092
  const { channel, thread_ts, message_ts, received_at } = marker;
15912
16093
  if (!channel || !thread_ts || !message_ts || !received_at) {
15913
16094
  try {
15914
- unlinkSync2(fullPath);
16095
+ unlinkSync3(fullPath);
15915
16096
  } catch {
15916
16097
  }
15917
16098
  cleared++;
@@ -15920,7 +16101,7 @@ function sweepSlackStaleMarkersOnBoot() {
15920
16101
  const receivedAtMs = Date.parse(received_at);
15921
16102
  if (!Number.isFinite(receivedAtMs) || receivedAtMs > now || now - receivedAtMs > STALE_MARKER_MS) {
15922
16103
  try {
15923
- unlinkSync2(fullPath);
16104
+ unlinkSync3(fullPath);
15924
16105
  } catch {
15925
16106
  }
15926
16107
  cleared++;
@@ -15935,7 +16116,7 @@ function sweepSlackStaleMarkersOnBoot() {
15935
16116
  }
15936
16117
  sweepSlackStaleMarkersOnBoot();
15937
16118
  function clearPendingMessage(channel, threadTs) {
15938
- clearAllSlackPendingMarkersForThread(channel, threadTs);
16119
+ clearAllSlackPendingMarkersForThread2(channel, threadTs);
15939
16120
  }
15940
16121
  function noteThreadActivity(channel, threadTs) {
15941
16122
  if (!channel || !threadTs) return;
@@ -15945,10 +16126,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
15945
16126
  if (!channel || !messageTs) return;
15946
16127
  clearPendingMessage(channel, messageTs);
15947
16128
  if (!SLACK_PENDING_INBOUND_DIR) return;
15948
- if (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
16129
+ if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
15949
16130
  let filenames;
15950
16131
  try {
15951
- filenames = readdirSync2(SLACK_PENDING_INBOUND_DIR);
16132
+ filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
15952
16133
  } catch {
15953
16134
  return;
15954
16135
  }
@@ -15959,10 +16140,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
15959
16140
  for (const filename of filenames) {
15960
16141
  if (!filename.startsWith(channelPrefix)) continue;
15961
16142
  if (!filename.endsWith(messageSuffix)) continue;
15962
- clearSlackMarkerFileWithHeal(join4(SLACK_PENDING_INBOUND_DIR, filename));
16143
+ clearSlackMarkerFileWithHeal(join5(SLACK_PENDING_INBOUND_DIR, filename));
15963
16144
  }
15964
16145
  }
15965
- var RESTART_FLAGS_DIR = join4(homedir2(), ".augmented", "restart-flags");
16146
+ var RESTART_FLAGS_DIR = join5(homedir2(), ".augmented", "restart-flags");
15966
16147
  function buildAugmentedSlackMetadata() {
15967
16148
  if (!AGT_TEAM_ID) return void 0;
15968
16149
  return {
@@ -16141,10 +16322,10 @@ async function handleSlashCommandEnvelope(payload) {
16141
16322
  return;
16142
16323
  }
16143
16324
  try {
16144
- if (!existsSync2(RESTART_FLAGS_DIR)) {
16325
+ if (!existsSync3(RESTART_FLAGS_DIR)) {
16145
16326
  mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
16146
16327
  }
16147
- const flagPath = join4(RESTART_FLAGS_DIR, `${codeName}.flag`);
16328
+ const flagPath = join5(RESTART_FLAGS_DIR, `${codeName}.flag`);
16148
16329
  const flag = {
16149
16330
  codeName,
16150
16331
  source: "slack",
@@ -16248,10 +16429,10 @@ async function handleHelpCommand(opts) {
16248
16429
  async function handleRestartCommand(opts) {
16249
16430
  const codeName = AGENT_CODE_NAME ?? "unknown";
16250
16431
  try {
16251
- if (!existsSync2(RESTART_FLAGS_DIR)) {
16432
+ if (!existsSync3(RESTART_FLAGS_DIR)) {
16252
16433
  mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
16253
16434
  }
16254
- const flagPath = join4(RESTART_FLAGS_DIR, `${codeName}.flag`);
16435
+ const flagPath = join5(RESTART_FLAGS_DIR, `${codeName}.flag`);
16255
16436
  const flag = {
16256
16437
  codeName,
16257
16438
  source: "slack",
@@ -16310,7 +16491,7 @@ var THREAD_STORE_TTL_DAYS = parseTtlDays(process.env.SLACK_THREAD_FOLLOW_TTL_DAY
16310
16491
  var threadPersister = null;
16311
16492
  function resolveThreadStorePath() {
16312
16493
  if (!AGENT_CODE_NAME) return null;
16313
- return join4(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
16494
+ return join5(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
16314
16495
  }
16315
16496
  function parseTtlDays(raw) {
16316
16497
  if (!raw) return void 0;
@@ -16345,9 +16526,9 @@ if (!BOT_TOKEN || !APP_TOKEN) {
16345
16526
  var slackStderrLogStream = null;
16346
16527
  if (AGENT_CODE_NAME) {
16347
16528
  try {
16348
- const logDir = join4(homedir2(), ".augmented", AGENT_CODE_NAME);
16529
+ const logDir = join5(homedir2(), ".augmented", AGENT_CODE_NAME);
16349
16530
  mkdirSync3(logDir, { recursive: true });
16350
- slackStderrLogStream = createWriteStream(join4(logDir, "slack-channel-stderr.log"), {
16531
+ slackStderrLogStream = createWriteStream(join5(logDir, "slack-channel-stderr.log"), {
16351
16532
  flags: "a",
16352
16533
  mode: 384
16353
16534
  });
@@ -16431,7 +16612,7 @@ var mcp = new Server(
16431
16612
  // Highest-priority lines first — Claude Code truncates this string at
16432
16613
  // 2048 chars, so anything appended late silently disappears.
16433
16614
  "CRITICAL: every response to a Slack <channel> tag MUST go through slack.reply. Text in your session WITHOUT a slack.reply call never reaches the user \u2014 the message dies inside the agent process.",
16434
- `Inbound: <channel ... thread_ts="..." [thread_context="..."]>. Pass channel + thread_ts to slack.reply on threads. thread_context = thread pre-loaded; ground replies ONLY in it, never another channel's.`,
16615
+ `Inbound: <channel ... thread_ts="..." message_ts="..." [thread_context="..."]>. Pass channel + message_ts to slack.reply (and thread_ts on threads). thread_context = thread pre-loaded; ground replies ONLY in it, never another channel's.`,
16435
16616
  "Inbound attachments: <channel> `files` is a JSON-serialised array \u2014 JSON.parse it. If an entry has `path`, the image is already downloaded \u2014 Read it directly, do NOT call slack.download_attachment. Use that tool only for entries with `file_id` but NO `path` (PDF, docx, csv): pass file_id + channel verbatim, then Read the returned path. Single-image messages also get a top-level `image_path`. Don't surface internal file-handling errors that don't affect the answer.",
16436
16617
  "Address users by user_name, never by raw user ID. In multi-participant threads the CURRENT speaker is the one on the latest <channel> tag.",
16437
16618
  'Mentioned in a channel \u2192 respond in that thread. DM \u2192 respond directly. auto_followed="true" \u2192 only reply if useful, OR if your own bot user is @-mentioned (counts even in auto_followed).',
@@ -16456,7 +16637,18 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16456
16637
  text: { type: "string", description: "The message to send" },
16457
16638
  thread_ts: {
16458
16639
  type: "string",
16459
- description: "Thread timestamp for threaded replies (from the thread_ts attribute)"
16640
+ description: "Thread timestamp for threaded replies (from the thread_ts attribute). Omit for a top-level reply (e.g. a fresh DM)."
16641
+ },
16642
+ // ENG-5861: explicit message_ts so the cleanup gate can match the
16643
+ // exact pending-inbound marker — necessary because top-level DM
16644
+ // replies legitimately have no thread_ts and the marker filename
16645
+ // is `<channel>__<thread_ts>__<message_ts>.json`. Without this
16646
+ // the marker sat undrained, eventually tripping ENG-5832\'s
16647
+ // "wedged" threshold and firing a false-positive red ✗ on the
16648
+ // next inbound.
16649
+ message_ts: {
16650
+ type: "string",
16651
+ description: "The message_ts of the specific inbound this reply addresses (from the message_ts attribute on the <channel> tag). Used by the pending-inbound cleanup gate so the marker is reliably cleared even for top-level DMs that have no thread_ts."
16460
16652
  }
16461
16653
  },
16462
16654
  required: ["channel", "text"]
@@ -16694,7 +16886,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16694
16886
  return buildImpersonationRefusal(name);
16695
16887
  }
16696
16888
  if (name === "slack.reply") {
16697
- const { channel, text, thread_ts } = args;
16889
+ const { channel, text, thread_ts, message_ts } = args;
16698
16890
  if (channel && thread_ts) {
16699
16891
  const killed = await isThreadKilled({
16700
16892
  channelType: "slack",
@@ -16753,8 +16945,15 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16753
16945
  isError: true
16754
16946
  };
16755
16947
  }
16756
- if (channel && thread_ts) {
16757
- clearPendingMessage(channel, thread_ts);
16948
+ if (channel) {
16949
+ if (message_ts) {
16950
+ clearSlackPendingMarkerByMessageTs2(channel, message_ts);
16951
+ if (thread_ts) clearPendingMessage(channel, thread_ts);
16952
+ } else if (thread_ts) {
16953
+ clearPendingMessage(channel, thread_ts);
16954
+ } else {
16955
+ clearOldestSlackPendingMarkerInChannel2(channel);
16956
+ }
16758
16957
  }
16759
16958
  try {
16760
16959
  const res = await fetch("https://slack.com/api/chat.postMessage", {
@@ -16906,7 +17105,7 @@ ${result.formatted}` : "Thread is empty or not found."
16906
17105
  let bytes;
16907
17106
  let size;
16908
17107
  try {
16909
- const stat = statSync(resolvedPath);
17108
+ const stat = statSync2(resolvedPath);
16910
17109
  if (!stat.isFile()) {
16911
17110
  return {
16912
17111
  content: [{ type: "text", text: `Upload refused: ${resolvedPath} is not a regular file.` }],
@@ -17807,8 +18006,27 @@ async function connectSocketMode() {
17807
18006
  }
17808
18007
  const senderPolicyDecision = decideSenderPolicyForward(evt, SLACK_SENDER_POLICY);
17809
18008
  if (!senderPolicyDecision.forward) {
17810
- process.stderr.write(`slack-channel: dropped message event (reason=sender_policy, mode=${SLACK_SENDER_POLICY.mode}, ts=${evt.ts ?? "n/a"})
18009
+ const subReason = classifySlackPolicyBlock(evt, SLACK_SENDER_POLICY);
18010
+ process.stderr.write(`slack-channel: dropped message event (reason=sender_policy, sub=${subReason}, mode=${SLACK_SENDER_POLICY.mode}, ts=${evt.ts ?? "n/a"})
17811
18011
  `);
18012
+ await maybeSendSenderPolicyDecline({
18013
+ channel: evt.channel,
18014
+ senderId: evt.user,
18015
+ // CR on PR #1623: top-level posts (DMs, channel root messages)
18016
+ // arrive with no `thread_ts` and MUST decline inline, not in a
18017
+ // new thread off the inbound message. The `?? evt.ts` fallback
18018
+ // forced every root message into a thread reply — exactly the
18019
+ // case the helper's "omit thread_ts when undefined" branch was
18020
+ // meant to handle. Pass through as-is: in-thread for thread
18021
+ // replies (thread_ts present), inline for everything else.
18022
+ threadTs: evt.thread_ts,
18023
+ subReason
18024
+ }).catch((err) => {
18025
+ process.stderr.write(
18026
+ `slack-channel(${AGENT_CODE_NAME}): decline reply failed: ${err.message}
18027
+ `
18028
+ );
18029
+ });
17812
18030
  return;
17813
18031
  }
17814
18032
  recordActivity("inbound");
@@ -17935,13 +18153,22 @@ async function connectSocketMode() {
17935
18153
  botUserId
17936
18154
  });
17937
18155
  const ackProbe = process.env.TMUX && AGENT_CODE_NAME ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
18156
+ let paneLogFreshAgeMs = null;
18157
+ if (SLACK_AGENT_DIR) {
18158
+ try {
18159
+ const paneMtimeMs = statSync2(join5(SLACK_AGENT_DIR, "pane.log")).mtimeMs;
18160
+ paneLogFreshAgeMs = Math.max(0, Date.now() - paneMtimeMs);
18161
+ } catch {
18162
+ }
18163
+ }
17938
18164
  const ackDecision = decideAckReaction({
17939
18165
  hasTarget: decideSlackAckReaction({ channel, ts }),
17940
18166
  integrationReady: Boolean(BOT_TOKEN),
17941
18167
  tmux: ackProbe.tmux,
17942
18168
  claude: ackProbe.claude,
17943
18169
  withinStartupGrace: process.uptime() * 1e3 < ACK_STARTUP_GRACE_MS,
17944
- oldestPendingAgeMs: oldestPendingMarkerAgeMs(SLACK_PENDING_INBOUND_DIR)
18170
+ oldestPendingAgeMs: oldestPendingMarkerAgeMs(SLACK_PENDING_INBOUND_DIR),
18171
+ paneLogFreshAgeMs
17945
18172
  });
17946
18173
  if (ackDecision !== "none") {
17947
18174
  const reactionName = ackDecision === "undeliverable" ? "x" : "eyes";
@@ -17996,6 +18223,13 @@ async function connectSocketMode() {
17996
18223
  user_name: userName,
17997
18224
  channel,
17998
18225
  thread_ts: threadTs,
18226
+ // ENG-5861: explicit message_ts so the agent can pass it back
18227
+ // to slack.reply for reliable pending-inbound cleanup. For
18228
+ // top-level messages threadTs === ts; for threaded replies
18229
+ // threadTs is the thread root and message_ts is the specific
18230
+ // reply being addressed — different keys, both needed by the
18231
+ // marker-cleanup gate.
18232
+ message_ts: ts,
17999
18233
  event_type: evt.type,
18000
18234
  ...isAutoFollowed ? { auto_followed: "true" } : {},
18001
18235
  // Only set these when we actually have attachments to avoid
@@ -15506,6 +15506,7 @@ import { readdirSync, readFileSync as readFileSync2 } from "fs";
15506
15506
  import { join as join3 } from "path";
15507
15507
  var REPLY_WEDGED_THRESHOLD_MS = 5 * 60 * 1e3;
15508
15508
  var ACK_STARTUP_GRACE_MS = 6e4;
15509
+ var ACK_PANE_FRESH_THRESHOLD_MS = 6e4;
15509
15510
  function decideAckReaction(i) {
15510
15511
  if (!i.hasTarget) return "none";
15511
15512
  if (!i.integrationReady) return "undeliverable";
@@ -15513,6 +15514,11 @@ function decideAckReaction(i) {
15513
15514
  if (!i.withinStartupGrace && i.claude === "dead") return "undeliverable";
15514
15515
  const threshold = i.pendingStaleThresholdMs ?? REPLY_WEDGED_THRESHOLD_MS;
15515
15516
  if (i.oldestPendingAgeMs != null && i.oldestPendingAgeMs > threshold) {
15517
+ const paneFreshThreshold = i.paneFreshThresholdMs ?? ACK_PANE_FRESH_THRESHOLD_MS;
15518
+ const paneIsFresh = i.paneLogFreshAgeMs != null && i.paneLogFreshAgeMs <= paneFreshThreshold;
15519
+ if (paneIsFresh && i.tmux === "alive" && i.claude === "alive") {
15520
+ return "ack";
15521
+ }
15516
15522
  return "undeliverable";
15517
15523
  }
15518
15524
  return "ack";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.27.25",
3
+ "version": "0.27.27",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {