@integrity-labs/agt-cli 0.28.1052 → 0.28.1053

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
@@ -40,7 +40,7 @@ import {
40
40
  success,
41
41
  table,
42
42
  warn
43
- } from "../chunk-3E63PEXT.js";
43
+ } from "../chunk-Z45XEW4H.js";
44
44
  import {
45
45
  getProjectDir,
46
46
  isSessionResumeDisabled,
@@ -5474,7 +5474,7 @@ import { execFileSync, execSync } from "child_process";
5474
5474
  import { existsSync as existsSync11, realpathSync as realpathSync2 } from "fs";
5475
5475
  import chalk18 from "chalk";
5476
5476
  import ora16 from "ora";
5477
- var cliVersion = true ? "0.28.1052" : "dev";
5477
+ var cliVersion = true ? "0.28.1053" : "dev";
5478
5478
  async function fetchLatestVersion() {
5479
5479
  const host2 = getHost();
5480
5480
  if (!host2) return null;
@@ -6734,7 +6734,7 @@ function handleError(err) {
6734
6734
  }
6735
6735
 
6736
6736
  // src/bin/agt.ts
6737
- var cliVersion2 = true ? "0.28.1052" : "dev";
6737
+ var cliVersion2 = true ? "0.28.1053" : "dev";
6738
6738
  var program = new Command();
6739
6739
  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");
6740
6740
  program.hook("preAction", async (thisCommand, actionCommand) => {
@@ -7122,7 +7122,7 @@ function exchangeFailureKind(err) {
7122
7122
  }
7123
7123
 
7124
7124
  // src/lib/api-client.ts
7125
- var agtCliVersion = true ? "0.28.1052" : "dev";
7125
+ var agtCliVersion = true ? "0.28.1053" : "dev";
7126
7126
  var lastConfigHash = null;
7127
7127
  function setConfigHash(hash) {
7128
7128
  lastConfigHash = hash && hash.length > 0 ? hash : null;
@@ -12079,4 +12079,4 @@ export {
12079
12079
  managerInstallSystemUnitCommand,
12080
12080
  managerUninstallSystemUnitCommand
12081
12081
  };
12082
- //# sourceMappingURL=chunk-3E63PEXT.js.map
12082
+ //# sourceMappingURL=chunk-Z45XEW4H.js.map
@@ -66,7 +66,7 @@ import {
66
66
  safeWriteJsonAtomic,
67
67
  setConfigHash,
68
68
  tripClass
69
- } from "../chunk-3E63PEXT.js";
69
+ } from "../chunk-Z45XEW4H.js";
70
70
  import {
71
71
  getProjectDir as getProjectDir2,
72
72
  getReadyTasks,
@@ -14799,7 +14799,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
14799
14799
  var lastVersionCheckAt = 0;
14800
14800
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
14801
14801
  var lastResponsivenessProbeAt = 0;
14802
- var agtCliVersion = true ? "0.28.1052" : "dev";
14802
+ var agtCliVersion = true ? "0.28.1053" : "dev";
14803
14803
  function resolveBrewPath(execFileSync3) {
14804
14804
  try {
14805
14805
  const out = execFileSync3("which", ["brew"], { timeout: 5e3 }).toString().trim();
package/dist/mcp/index.js CHANGED
@@ -50585,6 +50585,8 @@ var LOCAL_TOOL_NAMES = /* @__PURE__ */ new Set([
50585
50585
  "projects_list",
50586
50586
  "projects_get",
50587
50587
  "projects_create",
50588
+ // CS-1900: rename / archive / Linear link on an existing project.
50589
+ "projects_update",
50588
50590
  "artefacts_assign_project",
50589
50591
  "kanban_assign_project",
50590
50592
  // ENG-6229: self-restart. Conditionally registered (dark unless
@@ -51122,6 +51124,33 @@ server.tool(
51122
51124
  content: [{ type: "text", text: await createProjectText(params) }]
51123
51125
  })
51124
51126
  );
51127
+ async function updateProjectText(params) {
51128
+ const data = await apiPost(
51129
+ `/host/projects/${encodeURIComponent(params.project_id)}/update`,
51130
+ {
51131
+ agent_id: AGT_AGENT_ID,
51132
+ name: params.name,
51133
+ status: params.status,
51134
+ linear_project_id: params.linear_project_id
51135
+ }
51136
+ );
51137
+ const p2 = data.project;
51138
+ return `Updated project "${p2.name}" [${p2.status}] (id: ${p2.id}).`;
51139
+ }
51140
+ server.tool(
51141
+ "projects_update",
51142
+ 'Rename a project, archive or unarchive it, or set/clear its Linear link. Pass only the fields to change. A project is shared by your whole team, so ask the human to confirm before renaming or archiving one, naming the project and the change (e.g. archive "Theoretical Store Profitability"). Archiving hides it from active lists but keeps its tasks and artefacts; set status "active" to undo. Projects have no cover image, so there is nothing to set for one.',
51143
+ {
51144
+ project_id: external_exports.string().uuid().describe("The project id returned by projects_list"),
51145
+ // Trim BEFORE the length check, as the host does (CodeRabbit on #6642).
51146
+ name: external_exports.string().trim().min(1).max(200).optional().describe("New project name"),
51147
+ status: external_exports.enum(["active", "archived"]).optional().describe('"archived" to archive, "active" to unarchive'),
51148
+ linear_project_id: external_exports.string().nullable().optional().describe("Linear project id to deep-link, or null to clear the link")
51149
+ },
51150
+ async (params) => ({
51151
+ content: [{ type: "text", text: await updateProjectText(params) }]
51152
+ })
51153
+ );
51125
51154
  async function assignArtefactProjectText(params) {
51126
51155
  const data = await apiPost(
51127
51156
  `/host/artefact/${encodeURIComponent(params.artefact_id)}/assign-project`,
@@ -41181,11 +41181,24 @@ function buildIngressThreadKey(channel, threadTs) {
41181
41181
  }
41182
41182
  function createIngressOnlyThreadRegistry(maxEntries = 500) {
41183
41183
  const marked = /* @__PURE__ */ new Set();
41184
+ const cleared = /* @__PURE__ */ new Set();
41185
+ const remember = (set, key2) => {
41186
+ set.delete(key2);
41187
+ set.add(key2);
41188
+ while (set.size > maxEntries) {
41189
+ const oldest = set.values().next();
41190
+ if (oldest.done)
41191
+ break;
41192
+ set.delete(oldest.value);
41193
+ }
41194
+ };
41184
41195
  return {
41185
41196
  mark(channel, threadTs) {
41186
41197
  const key2 = buildIngressThreadKey(channel, threadTs);
41187
41198
  if (!key2)
41188
41199
  return;
41200
+ if (cleared.has(key2))
41201
+ return;
41189
41202
  marked.delete(key2);
41190
41203
  marked.add(key2);
41191
41204
  while (marked.size > maxEntries) {
@@ -41200,6 +41213,7 @@ function createIngressOnlyThreadRegistry(maxEntries = 500) {
41200
41213
  if (!key2)
41201
41214
  return;
41202
41215
  marked.delete(key2);
41216
+ remember(cleared, key2);
41203
41217
  },
41204
41218
  isIngressOnly(channel, threadTs) {
41205
41219
  const key2 = buildIngressThreadKey(channel, threadTs);
@@ -41212,6 +41226,16 @@ function createIngressOnlyThreadRegistry(maxEntries = 500) {
41212
41226
  }
41213
41227
  };
41214
41228
  }
41229
+ function shouldSuppressDirectPost(input) {
41230
+ if (input.persistedHumanOwed === true)
41231
+ return false;
41232
+ if (input.persistedIngressOnly === true)
41233
+ return true;
41234
+ return input.ingressOnly.isIngressOnly(input.channel, input.threadTs);
41235
+ }
41236
+ function threadMarkersAllIngressOnly(markers) {
41237
+ return markers.length > 0 && markers.every((m) => m.ingress_only === true);
41238
+ }
41215
41239
  var INGRESS_ONLY_REFUSAL = "This thread was opened by a third-party bot (an alert or issue card), which can wake you but is not someone you can talk to - replying would start a bot-to-bot exchange in a shared channel. Act on the content instead: work the issue, or raise it with a person in a thread they are in. If a human posts here, this thread becomes repliable.";
41216
41240
 
41217
41241
  // ../core/dist/channels/governance/mention-echo-twin-audit.js
@@ -44020,6 +44044,24 @@ function channelMessageShouldWake(text, responseMode, notifyMode, channelId, mut
44020
44044
  }
44021
44045
  return channelMessageShouldRespond(text, responseMode);
44022
44046
  }
44047
+ function decideTopLevelChannelWake(input) {
44048
+ const muted = Boolean(input.channelId && input.mutedChannels?.has(input.channelId));
44049
+ if (input.thirdPartyBotGranted) {
44050
+ return muted ? { wake: false, reason: "muted" } : { wake: true, via: "third_party_bot" };
44051
+ }
44052
+ if (channelMessageShouldWake(
44053
+ input.text,
44054
+ input.responseMode,
44055
+ input.notifyMode,
44056
+ input.channelId,
44057
+ input.mutedChannels,
44058
+ input.agent
44059
+ )) {
44060
+ return { wake: true, via: "engagement" };
44061
+ }
44062
+ const muteApplies = input.notifyMode === "filter" || input.notifyMode === "addressed";
44063
+ return { wake: false, reason: muted && muteApplies ? "muted" : "not_engaged" };
44064
+ }
44023
44065
  function parseNotifyCommand(text, currentChannelId) {
44024
44066
  const arg = (text ?? "").trim().toLowerCase().split(/\s+/)[0] ?? "";
44025
44067
  if (arg === "off" || arg === "mute") return { action: "mute", channelId: currentChannelId };
@@ -44035,7 +44077,12 @@ function notifyStatusMessage(muted, mode) {
44035
44077
  return muted ? ":mute: This channel is *muted* \u2014 I won\u2019t wake on its messages, including ones that name me (a direct @-mention still reaches me). `/notify on` to resume." : ":bell: This channel is *active* \u2014 I wake on messages that address me by name, not on every message here (an @-mention always reaches me). `/notify off` to mute.";
44036
44078
  }
44037
44079
  const unblock = "This is the `notify-dispatch` flag: an operator turns it on, and a session already running only picks it up after a restart \u2014 so if it has just been enabled, ask for me to be restarted.";
44038
- return muted ? `:grey_question: Your *mute* for this channel is *saved but not in effect* \u2014 I\u2019m not in a per-channel dispatch mode right now, so it isn\u2019t changing what wakes me. It applies as soon as one is live. ${unblock}` : `:grey_question: *No mute saved* for this channel \u2014 though it wouldn\u2019t apply yet either way: I\u2019m not in a per-channel dispatch mode right now. \`/notify off\` saves one for when it is live. ${unblock}`;
44080
+ return muted ? `:grey_question: Your *mute* for this channel is saved and *only partly in effect*: ${NOTIFY_BOT_MUTE_CLAUSE} Everything else here is unchanged, because I\u2019m not in a per-channel dispatch mode right now; it applies to those as soon as one is live. ${unblock}` : `:grey_question: *No mute saved* for this channel. I\u2019m not in a per-channel dispatch mode right now, so a mute would not change what other messages do here, but \`/notify off\` would still stop any allowlisted third-party bot (an alert or issue integration such as Linear) waking me here straight away. ${unblock}`;
44081
+ }
44082
+ var NOTIFY_BOT_MUTE_CLAUSE = "it already stops any allowlisted third-party bot (an alert or issue integration such as Linear) from waking me here.";
44083
+ function notifyAckNote(mode) {
44084
+ if (mode === "filter" || mode === "addressed") return "";
44085
+ return ` _(note: I\u2019m not in a per-channel dispatch mode right now. A mute here ${NOTIFY_BOT_MUTE_CLAUSE.replace(/^it /, "")} Other messages are unaffected until an operator enables per-channel dispatch.)_`;
44039
44086
  }
44040
44087
 
44041
44088
  // src/slack-list-channels.ts
@@ -45445,7 +45492,8 @@ function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undelivera
45445
45492
  ...discretionary ? { discretionary: true } : {},
45446
45493
  // ENG-5969/ENG-6327: carry the replay payload only when provided (the
45447
45494
  // durable-replay path). Absent ⇒ the marker can't be replayed.
45448
- ...payload ? { payload } : {}
45495
+ ...payload ? { payload } : {},
45496
+ ...ingressOnlyThreads.isIngressOnly(channel, threadTs) ? { ingress_only: true } : {}
45449
45497
  };
45450
45498
  try {
45451
45499
  mkdirSync9(SLACK_PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
@@ -45589,6 +45637,18 @@ function slackConversationReplayFacts(channel, threadTs) {
45589
45637
  }
45590
45638
  function sendUndeliverableNotice(channel, threadTs, conversationKey) {
45591
45639
  if (!BOT_TOKEN || !channel) return;
45640
+ if (shouldSuppressDirectPost({
45641
+ channel,
45642
+ threadTs,
45643
+ ingressOnly: ingressOnlyThreads,
45644
+ persistedHumanOwed: readSlackThreadMarkerState(channel, threadTs).humanOwed
45645
+ })) {
45646
+ process.stderr.write(
45647
+ `slack-channel(${AGENT_CODE_NAME}): undeliverable notice suppressed reason=ingress_only thread=${redactSlackId(threadTs)}
45648
+ `
45649
+ );
45650
+ return;
45651
+ }
45592
45652
  const text = undeliverableNoticeTextForRepeat({
45593
45653
  replay: slackConversationReplayFacts(channel, threadTs) ?? void 0,
45594
45654
  priorNotices: slackUndeliverableNotices.noticeCount(conversationKey)
@@ -45641,6 +45701,18 @@ function deleteSlackUndeliverableNotice(conversationKey) {
45641
45701
  var lastSlackBusyAckNoticeAt = /* @__PURE__ */ new Map();
45642
45702
  function postBusyAckNotice(channel, threadTs, isThreadReply, state) {
45643
45703
  if (!BOT_TOKEN || !channel) return;
45704
+ if (shouldSuppressDirectPost({
45705
+ channel,
45706
+ threadTs,
45707
+ ingressOnly: ingressOnlyThreads,
45708
+ persistedHumanOwed: readSlackThreadMarkerState(channel, threadTs).humanOwed
45709
+ })) {
45710
+ process.stderr.write(
45711
+ `slack-channel(${AGENT_CODE_NAME}): busy-ack suppressed reason=ingress_only thread=${redactSlackId(threadTs)}
45712
+ `
45713
+ );
45714
+ return;
45715
+ }
45644
45716
  const now = Date.now();
45645
45717
  const conversationKey = isThreadReply && threadTs ? `${channel}:${threadTs}` : channel;
45646
45718
  if (!shouldPostUndeliverableNotice(
@@ -45834,6 +45906,32 @@ function slackNextRetryName(filename) {
45834
45906
  }
45835
45907
  return { next: `${base}.retry-${attempt}.json`, attempt };
45836
45908
  }
45909
+ function readSlackThreadMarkerState(channel, threadTs) {
45910
+ const none = { allIngressOnly: false, humanOwed: false };
45911
+ if (!threadTs || !SLACK_PENDING_INBOUND_DIR) return none;
45912
+ const markers = [];
45913
+ try {
45914
+ markSeenAllSlackPendingMarkersForThread(
45915
+ SLACK_PENDING_INBOUND_DIR,
45916
+ channel,
45917
+ threadTs,
45918
+ (fullPath) => {
45919
+ try {
45920
+ const m = JSON.parse(readFileSync18(fullPath, "utf-8"));
45921
+ markers.push({ ingress_only: m.ingress_only });
45922
+ } catch {
45923
+ markers.push({});
45924
+ }
45925
+ }
45926
+ );
45927
+ } catch {
45928
+ return none;
45929
+ }
45930
+ return {
45931
+ allIngressOnly: threadMarkersAllIngressOnly(markers),
45932
+ humanOwed: markers.some((m) => m.ingress_only !== true)
45933
+ };
45934
+ }
45837
45935
  async function processSlackRecoveryOutboxFile(filename) {
45838
45936
  if (!SLACK_RECOVERY_OUTBOX_DIR) return;
45839
45937
  if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
@@ -45877,6 +45975,26 @@ async function processSlackRecoveryOutboxFile(filename) {
45877
45975
  const urlFixed = stripUrlWrappingAsterisks(mrkdwn.text);
45878
45976
  if (urlFixed.stripped) emitUrlAsteriskStripTelemetry("slack", urlFixed.count);
45879
45977
  const text = urlFixed.text;
45978
+ const recoveryMarkers = readSlackThreadMarkerState(payload.channel, payload.thread_ts);
45979
+ if (shouldSuppressDirectPost({
45980
+ channel: payload.channel,
45981
+ threadTs: payload.thread_ts,
45982
+ ingressOnly: ingressOnlyThreads,
45983
+ persistedIngressOnly: recoveryMarkers.allIngressOnly,
45984
+ persistedHumanOwed: recoveryMarkers.humanOwed
45985
+ })) {
45986
+ process.stderr.write(
45987
+ `slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery suppressed reason=ingress_only channel=${redactSlackId(payload.channel)} thread=${redactSlackId(payload.thread_ts)}
45988
+ `
45989
+ );
45990
+ if (payload.thread_ts) clearPendingMessage(payload.channel, payload.thread_ts);
45991
+ removeRecoveryLedgerEntry(SLACK_RECOVERY_LEDGER_DIR, payload.marker_name);
45992
+ try {
45993
+ unlinkSync7(fullPath);
45994
+ } catch {
45995
+ }
45996
+ return;
45997
+ }
45880
45998
  const ghostReplyMode = resolveGhostReplyMode();
45881
45999
  if (ghostReplyMode !== "off") {
45882
46000
  const decision = await decideRecovery(text, ghostReplyMode);
@@ -47119,8 +47237,7 @@ async function handleNotifyCommand(payload, codeName) {
47119
47237
  );
47120
47238
  return;
47121
47239
  }
47122
- const muteIsLive = NOTIFY_DISPATCH_MODE === "filter" || NOTIFY_DISPATCH_MODE === "addressed";
47123
- const inactiveNote = muteIsLive ? "" : " _(note: I\u2019m not in a per-channel dispatch mode right now, so this is saved but won\u2019t take effect until an operator enables it.)_";
47240
+ const inactiveNote = notifyAckNote(NOTIFY_DISPATCH_MODE);
47124
47241
  const { action } = parseNotifyCommand(payload.text, channelId);
47125
47242
  if (action === "help") {
47126
47243
  await postEphemeralViaResponseUrl(
@@ -49801,15 +49918,13 @@ function connectSocketModeSafely() {
49801
49918
  void startSocketModeAfterSeedingMutes();
49802
49919
  }
49803
49920
  async function startSocketModeAfterSeedingMutes() {
49804
- if (NOTIFY_DISPATCH_MODE === "filter" || NOTIFY_DISPATCH_MODE === "addressed") {
49805
- try {
49806
- await seedMutedChannelsFromApi();
49807
- } catch (err) {
49808
- process.stderr.write(
49809
- `slack-channel: notify-prefs seed threw, connecting anyway: ${err.message}
49921
+ try {
49922
+ await seedMutedChannelsFromApi();
49923
+ } catch (err) {
49924
+ process.stderr.write(
49925
+ `slack-channel: notify-prefs seed threw, connecting anyway: ${err.message}
49810
49926
  `
49811
- );
49812
- }
49927
+ );
49813
49928
  }
49814
49929
  if (isShuttingDown) return;
49815
49930
  void runOrRetry(connectSocketMode, {
@@ -50074,8 +50189,12 @@ async function connectSocketMode() {
50074
50189
  if (passedIdentity) recordActivity("inbound");
50075
50190
  const ingressThreadTs = evt.thread_ts ?? evt.ts;
50076
50191
  if (peerClassification?.kind === "bot-ingress" && access.kind !== "drop") {
50077
- ingressOnlyThreads.mark(evt.channel, ingressThreadTs);
50078
- } else if (!isBot) {
50192
+ if (evt.channel && readSlackThreadMarkerState(evt.channel, ingressThreadTs).humanOwed) {
50193
+ ingressOnlyThreads.clear(evt.channel, ingressThreadTs);
50194
+ } else {
50195
+ ingressOnlyThreads.mark(evt.channel, ingressThreadTs);
50196
+ }
50197
+ } else if (access.kind !== "drop" && (!isBot || peerClassification?.kind === "peer-ingress")) {
50079
50198
  ingressOnlyThreads.clear(evt.channel, ingressThreadTs);
50080
50199
  }
50081
50200
  if (access.kind === "drop") {
@@ -50277,17 +50396,25 @@ async function connectSocketMode() {
50277
50396
  return;
50278
50397
  }
50279
50398
  } else {
50280
- if (!channelMessageShouldWake(
50281
- evt.text ?? "",
50282
- CHANNEL_RESPONSE_MODE,
50283
- NOTIFY_DISPATCH_MODE,
50284
- evt.channel,
50285
- MUTED_CHANNELS,
50399
+ const wake = decideTopLevelChannelWake({
50400
+ text: evt.text ?? "",
50401
+ responseMode: CHANNEL_RESPONSE_MODE,
50402
+ notifyMode: NOTIFY_DISPATCH_MODE,
50403
+ channelId: evt.channel,
50404
+ mutedChannels: MUTED_CHANNELS,
50286
50405
  // ENG-8657: who this agent is, so `addressed` can tell a message
50287
50406
  // naming IT from one naming a different agent in the same channel.
50288
- { botUserId, codeName: AGENT_CODE_NAME }
50289
- ))
50407
+ agent: { botUserId, codeName: AGENT_CODE_NAME },
50408
+ thirdPartyBotGranted: peerClassification?.kind === "bot-ingress"
50409
+ });
50410
+ if (!wake.wake) {
50411
+ const channelHash = createHash3("sha256").update(evt.channel ?? "").digest("hex").slice(0, 8);
50412
+ process.stderr.write(
50413
+ `slack-channel: inbound drop reason=engagement:${wake.reason} notify=${NOTIFY_DISPATCH_MODE} response=${CHANNEL_RESPONSE_MODE} bot=${isBot} channel=${channelHash} ts=${redactSlackId(evt.ts)}
50414
+ `
50415
+ );
50290
50416
  return;
50417
+ }
50291
50418
  }
50292
50419
  }
50293
50420
  const composed = composeInboundText(evt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.28.1052",
3
+ "version": "0.28.1053",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {