@integrity-labs/agt-cli 0.28.1110 → 0.28.1112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/dist/bin/agt.js +5 -5
  2. package/dist/{chunk-4PTXGOVO.js → chunk-5EYINOIW.js} +2 -2
  3. package/dist/{chunk-TZM6YVE7.js → chunk-NO57I56V.js} +32 -9
  4. package/dist/{chunk-TZM6YVE7.js.map → chunk-NO57I56V.js.map} +1 -1
  5. package/dist/{chunk-RTYZQMSK.js → chunk-TJUDOE53.js} +4 -4
  6. package/dist/{claude-pair-runtime-L7KXG3DD.js → claude-pair-runtime-CKWLQMAC.js} +2 -2
  7. package/dist/lib/manager-worker.js +16 -16
  8. package/dist/mcp/direct-chat-channel.js +259 -14
  9. package/dist/mcp/index.js +267 -38
  10. package/dist/mcp/origami.js +31 -8
  11. package/dist/mcp/slack-channel.js +267 -14
  12. package/dist/mcp/telegram-channel.js +399 -70
  13. package/dist/mcp/xero.js +2 -2
  14. package/dist/{persistent-session-HSYN3SQ2.js → persistent-session-SDYY75X7.js} +3 -3
  15. package/dist/{responsiveness-probe-MO4L5O6F.js → responsiveness-probe-NEBHDSMI.js} +3 -3
  16. package/dist/{session-auth-dead-ZKI4XCSI.js → session-auth-dead-CNV4O7RF.js} +2 -2
  17. package/package.json +1 -1
  18. /package/dist/{chunk-4PTXGOVO.js.map → chunk-5EYINOIW.js.map} +0 -0
  19. /package/dist/{chunk-RTYZQMSK.js.map → chunk-TJUDOE53.js.map} +0 -0
  20. /package/dist/{claude-pair-runtime-L7KXG3DD.js.map → claude-pair-runtime-CKWLQMAC.js.map} +0 -0
  21. /package/dist/{persistent-session-HSYN3SQ2.js.map → persistent-session-SDYY75X7.js.map} +0 -0
  22. /package/dist/{responsiveness-probe-MO4L5O6F.js.map → responsiveness-probe-NEBHDSMI.js.map} +0 -0
  23. /package/dist/{session-auth-dead-ZKI4XCSI.js.map → session-auth-dead-CNV4O7RF.js.map} +0 -0
@@ -16,7 +16,7 @@ import {
16
16
  parseEnvFileEntries,
17
17
  parseEnvIntegrations,
18
18
  shellQuote
19
- } from "./chunk-4PTXGOVO.js";
19
+ } from "./chunk-5EYINOIW.js";
20
20
  import {
21
21
  BIND_FAILURE_QUARANTINE_THRESHOLD,
22
22
  INTEGRATIONS_SECTION_END,
@@ -79,7 +79,7 @@ import {
79
79
  sessionTranscriptDir,
80
80
  worseConnectivityOutcome,
81
81
  wrapScheduledTaskPrompt
82
- } from "./chunk-TZM6YVE7.js";
82
+ } from "./chunk-NO57I56V.js";
83
83
  import {
84
84
  classifyUnanswerablePane,
85
85
  isConsumerTermsDialogVisible,
@@ -7518,7 +7518,7 @@ function exchangeFailureKind(err) {
7518
7518
  }
7519
7519
 
7520
7520
  // src/lib/api-client.ts
7521
- var agtCliVersion = true ? "0.28.1110" : "dev";
7521
+ var agtCliVersion = true ? "0.28.1112" : "dev";
7522
7522
  var lastConfigHash = null;
7523
7523
  function setConfigHash(hash) {
7524
7524
  lastConfigHash = hash && hash.length > 0 ? hash : null;
@@ -12492,4 +12492,4 @@ export {
12492
12492
  managerInstallSystemUnitCommand,
12493
12493
  managerUninstallSystemUnitCommand
12494
12494
  };
12495
- //# sourceMappingURL=chunk-RTYZQMSK.js.map
12495
+ //# sourceMappingURL=chunk-TJUDOE53.js.map
@@ -266,7 +266,7 @@ async function spawnPairSession(session) {
266
266
  return { ok: true };
267
267
  } catch {
268
268
  }
269
- const { resolveClaudeBinary } = await import("./persistent-session-HSYN3SQ2.js");
269
+ const { resolveClaudeBinary } = await import("./persistent-session-SDYY75X7.js");
270
270
  const claudeBin = resolveClaudeBinary();
271
271
  const pairEnv = {
272
272
  ...process.env,
@@ -734,4 +734,4 @@ export {
734
734
  startClaudePair,
735
735
  submitClaudePairCode
736
736
  };
737
- //# sourceMappingURL=claude-pair-runtime-L7KXG3DD.js.map
737
+ //# sourceMappingURL=claude-pair-runtime-CKWLQMAC.js.map
@@ -68,7 +68,7 @@ import {
68
68
  safeWriteJsonAtomic,
69
69
  setConfigHash,
70
70
  tripClass
71
- } from "../chunk-RTYZQMSK.js";
71
+ } from "../chunk-TJUDOE53.js";
72
72
  import {
73
73
  getProjectDir as getProjectDir2,
74
74
  getReadyTasks,
@@ -155,7 +155,7 @@ import {
155
155
  webFetchBaselineValue,
156
156
  webFetchRestartNeeded,
157
157
  writeEgressAllowlist
158
- } from "../chunk-4PTXGOVO.js";
158
+ } from "../chunk-5EYINOIW.js";
159
159
  import {
160
160
  ACCOUNT_ENFORCEMENT_MARKER_FILENAME,
161
161
  AnchorSessionClient,
@@ -265,7 +265,7 @@ import {
265
265
  subagentActivityAgeSeconds,
266
266
  sumTranscriptUsageInWindow,
267
267
  transcriptActivityAgeSeconds
268
- } from "../chunk-TZM6YVE7.js";
268
+ } from "../chunk-NO57I56V.js";
269
269
  import {
270
270
  classifyUnanswerablePane,
271
271
  paneRemedy
@@ -14143,7 +14143,7 @@ var pendingDayRolloverReset = /* @__PURE__ */ new Set();
14143
14143
  var dayRolloverInboundHold = /* @__PURE__ */ new Map();
14144
14144
  var INBOUND_HOLD_EPISODE_GAP_MS = 12e4;
14145
14145
  async function channelInboundActivityAgeSecondsFor(codeName) {
14146
- const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-MO4L5O6F.js");
14146
+ const { newestPendingInboundActivityMtimeMs } = await import("../responsiveness-probe-NEBHDSMI.js");
14147
14147
  const newest = newestPendingInboundActivityMtimeMs(dirname11(paneLogPath(codeName)));
14148
14148
  if (newest === null) return null;
14149
14149
  return Math.max(0, Math.floor((Date.now() - newest) / 1e3));
@@ -14901,7 +14901,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
14901
14901
  var lastVersionCheckAt = 0;
14902
14902
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
14903
14903
  var lastResponsivenessProbeAt = 0;
14904
- var agtCliVersion = true ? "0.28.1110" : "dev";
14904
+ var agtCliVersion = true ? "0.28.1112" : "dev";
14905
14905
  function resolveBrewPath(execFileSync3) {
14906
14906
  try {
14907
14907
  const out = execFileSync3("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -16518,7 +16518,7 @@ function flushRestartedAgentDiagnostics(hostId, codeNames) {
16518
16518
  if (codeNames.length === 0) return;
16519
16519
  void (async () => {
16520
16520
  try {
16521
- const { collectDiagnostics } = await import("../persistent-session-HSYN3SQ2.js");
16521
+ const { collectDiagnostics } = await import("../persistent-session-SDYY75X7.js");
16522
16522
  await api.post("/host/heartbeat", {
16523
16523
  host_id: hostId,
16524
16524
  agent_diagnostics: collectDiagnostics(codeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
@@ -16660,7 +16660,7 @@ async function pollCycleInner() {
16660
16660
  }
16661
16661
  try {
16662
16662
  const { detectHostSecurity } = await import("../host-security-KQYYZZVW.js");
16663
- const { collectDiagnostics } = await import("../persistent-session-HSYN3SQ2.js");
16663
+ const { collectDiagnostics } = await import("../persistent-session-SDYY75X7.js");
16664
16664
  const diagCodeNames = [...agentState.persistentSessionAgents];
16665
16665
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames, quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor) : void 0;
16666
16666
  let tailscaleHostname;
@@ -16868,7 +16868,7 @@ async function pollCycleInner() {
16868
16868
  collectPanelessActivityProbes,
16869
16869
  getResponsivenessIntervalMs,
16870
16870
  occupancyQualificationClassifications
16871
- } = await import("../responsiveness-probe-MO4L5O6F.js");
16871
+ } = await import("../responsiveness-probe-NEBHDSMI.js");
16872
16872
  const probeIntervalMs = getResponsivenessIntervalMs();
16873
16873
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
16874
16874
  const probeCodeNames = [...agentState.persistentSessionAgents];
@@ -16985,7 +16985,7 @@ async function pollCycleInner() {
16985
16985
  collectResponsivenessProbes,
16986
16986
  livePendingInboundOldestAgeSeconds,
16987
16987
  parkPendingInbound
16988
- } = await import("../responsiveness-probe-MO4L5O6F.js");
16988
+ } = await import("../responsiveness-probe-NEBHDSMI.js");
16989
16989
  const { getProjectDir: wedgeProjectDir } = await import("../scheduler-engine-M6AFZWNU.js");
16990
16990
  const wedgeNow = /* @__PURE__ */ new Date();
16991
16991
  const liveAgents = agentState.persistentSessionAgents;
@@ -17271,7 +17271,7 @@ async function pollCycleInner() {
17271
17271
  }
17272
17272
  }
17273
17273
  );
17274
- const { parkPendingInbound } = await import("../responsiveness-probe-MO4L5O6F.js");
17274
+ const { parkPendingInbound } = await import("../responsiveness-probe-NEBHDSMI.js");
17275
17275
  const { parked, deadLettered } = parkPendingInbound(codeName, new Date(fatalNowMs));
17276
17276
  paneFatalStreak.delete(codeName);
17277
17277
  paneFatalRestarts.set(codeName, [...history, fatalNowMs]);
@@ -17302,7 +17302,7 @@ async function pollCycleInner() {
17302
17302
  TRANSCRIPT_MOUNT_ESCALATION_REFRESH_MS,
17303
17303
  bindSourceEscalations
17304
17304
  } = await import("../transcript-mount-health-OHE6IVJV.js");
17305
- const { transcriptBindSourcesFor, ensureTranscriptBindSources } = await import("../persistent-session-HSYN3SQ2.js");
17305
+ const { transcriptBindSourcesFor, ensureTranscriptBindSources } = await import("../persistent-session-SDYY75X7.js");
17306
17306
  const mountConfig = resolveTranscriptMountConfig();
17307
17307
  const liveAgents = agentState.persistentSessionAgents;
17308
17308
  for (const tracked of [
@@ -17467,7 +17467,7 @@ async function pollCycleInner() {
17467
17467
  }
17468
17468
  try {
17469
17469
  const { scrapeMcpFailedBannerCount } = await import("../pane-mcp-banner-scraper-VR5QGP3J.js");
17470
- const { probeSessionAuth } = await import("../session-auth-dead-ZKI4XCSI.js");
17470
+ const { probeSessionAuth } = await import("../session-auth-dead-CNV4O7RF.js");
17471
17471
  const observations = [];
17472
17472
  const pendingCacheCommits = [];
17473
17473
  const modelApiErrorReportingOn = hostFlagStore().getBoolean("model-api-error-reporting");
@@ -22054,7 +22054,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
22054
22054
  try {
22055
22055
  const freshId = rotateSessionForWedge(codeName);
22056
22056
  consecutiveWedgeCycles.delete(codeName);
22057
- const { parkPendingInbound: parkForRestart } = await import("../responsiveness-probe-MO4L5O6F.js");
22057
+ const { parkPendingInbound: parkForRestart } = await import("../responsiveness-probe-NEBHDSMI.js");
22058
22058
  const { parked, deadLettered } = parkForRestart(codeName, /* @__PURE__ */ new Date());
22059
22059
  const parkNote = parked > 0 ? `, ${parked} inbound parked` : "";
22060
22060
  const deadNote = deadLettered > 0 ? `, ${deadLettered} undeliverable dead-lettered` : "";
@@ -22097,7 +22097,7 @@ async function handleRestartDoorbell(agentId, requestedAt, restartReason) {
22097
22097
  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}`));
22098
22098
  void (async () => {
22099
22099
  try {
22100
- const { collectDiagnostics } = await import("../persistent-session-HSYN3SQ2.js");
22100
+ const { collectDiagnostics } = await import("../persistent-session-SDYY75X7.js");
22101
22101
  await api.post("/host/heartbeat", {
22102
22102
  host_id: hostId,
22103
22103
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
@@ -22148,7 +22148,7 @@ async function respawnAgentAfterMcpStop(codeName, reason) {
22148
22148
  }
22149
22149
  try {
22150
22150
  const hostId = await getHostId();
22151
- const { collectDiagnostics } = await import("../persistent-session-HSYN3SQ2.js");
22151
+ const { collectDiagnostics } = await import("../persistent-session-SDYY75X7.js");
22152
22152
  await api.post("/host/heartbeat", {
22153
22153
  host_id: hostId,
22154
22154
  agent_diagnostics: collectDiagnostics([codeName], quarantineEntriesFor, claudeMdSizeFor, spawnOutcomeForDiagnostics, pidPressureFor, forwardedToolsFor, runtimeVersionFor)
@@ -22821,7 +22821,7 @@ async function processClaudePairSessions(agents) {
22821
22821
  // cannot take it for itself — by the time it runs, the write it is looking
22822
22822
  // for may already have happened.
22823
22823
  snapshotPairConfig
22824
- } = await import("../claude-pair-runtime-L7KXG3DD.js");
22824
+ } = await import("../claude-pair-runtime-CKWLQMAC.js");
22825
22825
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
22826
22826
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
22827
22827
  const killed = await killPairSession(pairTmuxSession(pairId));
@@ -38455,14 +38455,37 @@ var FLAG_REGISTRY = [
38455
38455
  },
38456
38456
  {
38457
38457
  key: "platform-maintenance-mode",
38458
- description: 'Platform-wide maintenance mode (ENG-6506): when on, every agent across every team/org auto-replies to admitted human inbound with a "we are offline for maintenance" notice instead of dispatching to the agent \u2014 for whole-platform downtime such as DB upgrades. Read host-side per inbound from the flags cache (no MCP restart). Boolean gate; ships dark \u2014 when off, channels behave exactly as today. (Proactive "back online" follow-up is the ENG-6508 fast-follow, not this flag.)',
38459
- flagType: "boolean",
38460
- // Declared safe value is `false` (normal operation). Fail-safe direction: a
38461
- // flag-DB read error or absent flag map must never silence the fleet on its
38462
- // own — agents stay responsive unless an operator deliberately flips this on
38463
- // from the admin Feature Flags page. No envVar: net-new control, no
38464
- // pre-flags env gate to migrate.
38465
- defaultValue: false,
38458
+ description: 'Platform-wide maintenance mode (ENG-6506; widened to modes by ENG-11436). off = normal operation (ships dark; channels behave exactly as today). tools-offline = every agent KEEPS conversing but every Augmented tool that WRITES to the control plane refuses with a reason, so nothing is created during a window whose database restore would discard it; reads are untouched. fleet-offline = the previous boolean-true behaviour AND the write gate: admitted human inbound gets a "we are offline for maintenance" notice instead of being dispatched, for whole-platform downtime such as DB upgrades. The modes are CUMULATIVE \u2014 fleet-offline gates writes too, because silencing channel inbound does not stop a /loop or a scheduled task filing work. Read host-side per inbound and per tool call from the flags cache (no MCP restart). (Proactive "back online" follow-up is the ENG-6508 fast-follow, not this flag.)',
38459
+ flagType: "enum",
38460
+ allowedValues: ["off", "tools-offline", "fleet-offline"],
38461
+ // Declared safe value is `off` (normal operation). Fail-safe direction: a
38462
+ // flag-DB read error or absent flag map must never silence the fleet OR take
38463
+ // its write tools away on its own — agents stay fully functional unless an
38464
+ // operator deliberately sets a mode from the admin Feature Flags page. No
38465
+ // envVar: net-new control, no pre-flags env gate to migrate.
38466
+ //
38467
+ // ENG-11436 widened this from a boolean rather than adding a second flag, so
38468
+ // there is ONE operator control whose modes are mutually exclusive by type
38469
+ // instead of two similar booleans with four combinations.
38470
+ //
38471
+ // The host resolver GRANDFATHERS a boolean-shaped cache value
38472
+ // (true -> fleet-offline, false -> off), and the DIRECTION it protects is
38473
+ // narrower than an earlier version of this comment claimed (Obi Wan
38474
+ // `e3f0d2c154b9` on #6903). It said "an older manager still writing a boolean
38475
+ // must not make a central flip a silent no-op". That situation cannot arise:
38476
+ // once this registry change deploys, normalizeFlagValue only ever yields one
38477
+ // of the three strings, feature-flags-host.ts writes the heartbeat map
38478
+ // verbatim, so no manager writes a boolean again. What grandfathering
38479
+ // actually covers is a NEW MCP reading a STALE PRE-DEPLOY cache that still
38480
+ // holds one.
38481
+ //
38482
+ // The direction that DOES happen is not handled and cannot be: an OLD MCP
38483
+ // reads the new string with resolveHostBooleanFlag, gets undefined, and keeps
38484
+ // dispatching. That is a real regression against ENG-6506 until the fleet
38485
+ // converges, and it is why the runbook makes convergence a deploy-order
38486
+ // PRECONDITION rather than offering a boolean fallback — there is none, since
38487
+ // an enum flag rejects `{value: true}` with a 400.
38488
+ defaultValue: "off",
38466
38489
  // Flipping this takes the ENTIRE fleet offline to end users; a mis-flip is a
38467
38490
  // platform-wide availability incident, so mutations require explicit
38468
38491
  // confirmation (the admin flip-reach modal).
@@ -39786,6 +39809,37 @@ function resolveHostBooleanFlag(opts) {
39786
39809
  if (cached2 !== void 0) return cached2;
39787
39810
  return opts.defaultValue;
39788
39811
  }
39812
+ var MAINTENANCE_FLAG_KEY = "platform-maintenance-mode";
39813
+ function parseMaintenanceMode(raw) {
39814
+ if (raw === void 0) return void 0;
39815
+ const v = raw.trim().toLowerCase();
39816
+ if (v === "") return void 0;
39817
+ if (v === "off" || v === "tools-offline" || v === "fleet-offline") return v;
39818
+ const b = envBoolean(raw);
39819
+ if (b === true) return "fleet-offline";
39820
+ if (b === false) return "off";
39821
+ return void 0;
39822
+ }
39823
+ function cachedMaintenanceMode(path) {
39824
+ try {
39825
+ if (!existsSync4(path)) return void 0;
39826
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
39827
+ if (!parsed || typeof parsed !== "object") return void 0;
39828
+ const flags = parsed.flags;
39829
+ if (!flags || typeof flags !== "object") return void 0;
39830
+ const value = flags[MAINTENANCE_FLAG_KEY];
39831
+ if (typeof value === "string") return parseMaintenanceMode(value);
39832
+ if (typeof value === "boolean") return value ? "fleet-offline" : "off";
39833
+ return void 0;
39834
+ } catch {
39835
+ return void 0;
39836
+ }
39837
+ }
39838
+ function resolveMaintenanceMode(opts) {
39839
+ const cached2 = cachedMaintenanceMode(opts?.cachePath ?? defaultFlagsCachePath());
39840
+ if (cached2 !== void 0) return cached2;
39841
+ return "off";
39842
+ }
39789
39843
 
39790
39844
  // ../core/dist/channels/deflection-causes.js
39791
39845
  var DEFLECTION_CAUSES = [
@@ -41033,17 +41087,157 @@ function createKanbanCardActiveClient(args) {
41033
41087
  }
41034
41088
 
41035
41089
  // src/maintenance-mode.ts
41036
- var FLAG_KEY = "platform-maintenance-mode";
41037
41090
  var MAINTENANCE_OFFLINE_MESSAGE = "The Ninjafy platform is offline for scheduled maintenance right now, so I can't pick this up \u2014 please try again shortly.";
41038
41091
  function isMaintenanceModeActive(opts) {
41039
- return resolveHostBooleanFlag({
41040
- key: FLAG_KEY,
41041
- envVar: "",
41042
- defaultValue: false,
41092
+ return resolveMaintenanceMode({
41043
41093
  cachePath: opts?.cachePath,
41044
41094
  env: opts?.env
41045
- });
41095
+ }) === "fleet-offline";
41096
+ }
41097
+
41098
+ // src/maintenance-tools-offline.ts
41099
+ function isToolsOfflineActive(opts) {
41100
+ const mode = resolveMaintenanceMode({ cachePath: opts?.cachePath, env: opts?.env });
41101
+ return mode === "tools-offline" || mode === "fleet-offline";
41102
+ }
41103
+ var MAINTENANCE_GATED_TOOLS = /* @__PURE__ */ new Set([
41104
+ // Tasks.
41105
+ "kanban_add",
41106
+ "kanban_update",
41107
+ "kanban_move",
41108
+ "kanban_done",
41109
+ "kanban_reopen",
41110
+ "kanban_assign",
41111
+ "kanban_reassign",
41112
+ "kanban_recall",
41113
+ "kanban_respond",
41114
+ "kanban_progress",
41115
+ "kanban_log",
41116
+ "kanban_assign_project",
41117
+ "assign_kanban_to_human",
41118
+ // Canvases. `dashboards_request_refresh` is gated because it enqueues
41119
+ // server-side work, not because it renders anything itself.
41120
+ "dashboards_upsert",
41121
+ "dashboards_persist_widget",
41122
+ "dashboards_request_refresh",
41123
+ // ───────────────────────────────────────────────────────────────────────
41124
+ // FORWARDED writes (ENG-11436 / CodeRabbit on #6903). These reach the agent
41125
+ // through `registerForwardedApiTools()` -> `server.registerTool`, from
41126
+ // `packages/api/src/routes/mcp-tools.ts`, with names resolved at RUNTIME. They
41127
+ // are gated only because index.ts now wraps that method too; before that, this
41128
+ // whole block would have been a list the gate could not see.
41129
+ //
41130
+ // CodeRabbit named five (workflow_update, workflow_run_{start,phase,end},
41131
+ // memory_delete). Enumerating the surface to check them found 37 of the 50
41132
+ // forwarded tools unclassified, so the five were a subset — these are all of
41133
+ // the writes among them. The test now classifies that surface exhaustively, so
41134
+ // the next addition fails CI rather than slipping through.
41135
+ "composio_upload_file",
41136
+ "memory_delete",
41137
+ "memory_save",
41138
+ "onboarding_advance",
41139
+ "onboarding_provision_skills",
41140
+ "onboarding_provision_tasks",
41141
+ "onboarding_record_manager_contact",
41142
+ "request_batch_approval",
41143
+ "request_integration_access",
41144
+ "skill_contribute_fragment",
41145
+ "skill_create",
41146
+ "skill_delete",
41147
+ "skill_improve",
41148
+ "skill_propose_revision",
41149
+ "skill_publish_to_marketplace",
41150
+ "skill_update",
41151
+ "submit_performance_review",
41152
+ "workflow_run_end",
41153
+ "workflow_run_phase",
41154
+ "workflow_run_start",
41155
+ "workflow_update",
41156
+ // CHANNEL-SERVER write (Obi Wan `b3537c1e8495` on #6903). Dispatched by NAME in
41157
+ // slack-channel.ts / telegram-channel.ts / direct-chat-channel.ts, which are
41158
+ // separate processes that never call server.tool — so this one is enforced by
41159
+ // `refuseIfToolsOffline` at each dispatch point, not by the registration
41160
+ // wrapper. It inserts a `pending_interaction` row, the same durable
41161
+ // human-actionable record that gates request_buttons / request_form.
41162
+ "channel_request_input",
41163
+ // `slack.ask_user` is the SAME write under a dotted name, and it was missed
41164
+ // because the test's enumerator could not see dotted names at all (Obi Wan
41165
+ // `122ac55ff278` / `d7245eddd77f` on #6903). handleAskUser calls
41166
+ // runtime.createPendingInteraction -> POST /host/pending-interactions ->
41167
+ // inserts `pending_interaction`. Ungated, an agent on a /loop files the row,
41168
+ // the restore discards it, and the agent blocks forever on a resolution that
41169
+ // no longer exists — the exact failure the entry above is gated for.
41170
+ //
41171
+ // Verified it is the ONLY dotted channel tool that writes: createPendingInteraction
41172
+ // is CALLED once in the three channel servers (slack-channel.ts:8053);
41173
+ // slack-block-kit-runtime.ts only re-exports it.
41174
+ "slack.ask_user",
41175
+ // Workflows. FORWARDED, not local — it reaches the agent through
41176
+ // `server.registerTool` in registerForwardedApiTools, so it is gated only
41177
+ // because that method is now wrapped too (CodeRabbit on #6903).
41178
+ "workflow_create",
41179
+ // Knowledge.
41180
+ "knowledge_add",
41181
+ "knowledge_update",
41182
+ "knowledge_delete",
41183
+ // Projects and project tagging.
41184
+ "projects_create",
41185
+ "projects_update",
41186
+ "artefacts_assign_project",
41187
+ // Routines.
41188
+ "routine_propose",
41189
+ "routine_revise",
41190
+ "routine_retire",
41191
+ "submit_routine_output",
41192
+ // Standing rules.
41193
+ "rule_add",
41194
+ "rule_remove",
41195
+ // Scheduled work.
41196
+ "schedule_create",
41197
+ "schedule_update",
41198
+ "schedule_delete",
41199
+ // Requests that file a durable record a human is expected to act on. Filing
41200
+ // one mid-window is worse than refusing it: the record is discarded by the
41201
+ // restore, so the agent would have promised a human a follow-up that no
41202
+ // longer exists anywhere.
41203
+ "request_approval",
41204
+ "request_buttons",
41205
+ "request_channel_access",
41206
+ "request_form",
41207
+ "request_reconnect",
41208
+ "request_feature",
41209
+ "request_feature_withdraw",
41210
+ // ADR-0025 approval tools, registered by `approval-tools.ts` against the same
41211
+ // server object, so the wrapper in index.ts covers them too.
41212
+ "cancel_approval",
41213
+ // Status / identity.
41214
+ "status_standup",
41215
+ "status_update",
41216
+ "update_identity",
41217
+ "update_timezone",
41218
+ // Fleet changes.
41219
+ "recruit_agent",
41220
+ "integration_improve"
41221
+ ]);
41222
+ function toolsOfflineRefusalText(toolName) {
41223
+ return `Ninjafy is in a control-plane maintenance window, so \`${toolName}\` is unavailable right now. Nothing was created, changed or saved. Do NOT retry this call, and do not try a different Ninjafy tool to work around it \u2014 every tool that writes to the control plane is refusing for the same reason, and the window is short. Tell the person plainly that you cannot save this at the moment and ask them to bring it back afterwards; carry on with anything that does not need saving.`;
41224
+ }
41225
+ function refuseIfToolsOffline(name, opts) {
41226
+ if (!MAINTENANCE_GATED_TOOLS.has(name)) return null;
41227
+ const isActive = opts?.isActive ?? (() => isToolsOfflineActive());
41228
+ if (!isActive()) return null;
41229
+ return { content: [{ type: "text", text: toolsOfflineRefusalText(name) }] };
41230
+ }
41231
+ function shouldSendToolsOfflineNotice(opts) {
41232
+ const active = opts.isActive ?? (() => isToolsOfflineActive());
41233
+ if (!active()) return false;
41234
+ const now = opts.now ?? Date.now();
41235
+ const last = opts.cache.get(opts.key);
41236
+ if (last !== void 0 && now - last < opts.cooldownMs) return false;
41237
+ opts.cache.set(opts.key, now);
41238
+ return true;
41046
41239
  }
41240
+ var TOOLS_OFFLINE_USER_NOTICE = "Heads up \u2014 Ninjafy is in a maintenance window right now, so I can't save anything: no tasks, canvases, notes or scheduled items. I can still talk things through, and you can ask me to set it up once maintenance is done.";
41047
41241
 
41048
41242
  // src/account-enforcement-notice.ts
41049
41243
  import { readFileSync as readFileSync11 } from "fs";
@@ -41460,6 +41654,49 @@ async function apiPost(path, body) {
41460
41654
  }
41461
41655
  return res;
41462
41656
  }
41657
+ var DIRECT_CHAT_TOOLS_OFFLINE_CACHE = /* @__PURE__ */ new Map();
41658
+ var DIRECT_CHAT_TOOLS_OFFLINE_COOLDOWN_MS = (() => {
41659
+ const raw = process.env["DIRECT_CHAT_TOOLS_OFFLINE_COOLDOWN_SEC"];
41660
+ const n = raw ? Number(raw) : NaN;
41661
+ return Number.isFinite(n) && n >= 0 ? n * 1e3 : 60 * 60 * 1e3;
41662
+ })();
41663
+ async function maybeSendDirectChatToolsOfflineNotice(sessionId) {
41664
+ const key = `${sessionId}|tools_offline`;
41665
+ try {
41666
+ if (!shouldSendToolsOfflineNotice({
41667
+ cache: DIRECT_CHAT_TOOLS_OFFLINE_CACHE,
41668
+ key,
41669
+ cooldownMs: DIRECT_CHAT_TOOLS_OFFLINE_COOLDOWN_MS
41670
+ })) {
41671
+ return;
41672
+ }
41673
+ const res = await apiPost("/host/direct-chat/reply", {
41674
+ agent_id: AGT_AGENT_ID,
41675
+ session_id: sessionId,
41676
+ content: TOOLS_OFFLINE_USER_NOTICE,
41677
+ // An extra message alongside the agent's real reply, which has already
41678
+ // advanced the cursor.
41679
+ // cursor-advance-allow: deliberately EMPTY — nothing to advance, no count to read.
41680
+ message_ids: [],
41681
+ // kind:'notice' — the system speaking, not the agent, and owes no reply.
41682
+ kind: "notice"
41683
+ });
41684
+ const data = await res.json().catch(() => ({}));
41685
+ if (!res.ok || data.error) {
41686
+ DIRECT_CHAT_TOOLS_OFFLINE_CACHE.delete(key);
41687
+ process.stderr.write(
41688
+ `direct-chat-channel: tools-offline notice failed: ${data.error ?? res.status}
41689
+ `
41690
+ );
41691
+ }
41692
+ } catch (err) {
41693
+ DIRECT_CHAT_TOOLS_OFFLINE_CACHE.delete(key);
41694
+ process.stderr.write(
41695
+ `direct-chat-channel: tools-offline notice error: ${err.message}
41696
+ `
41697
+ );
41698
+ }
41699
+ }
41463
41700
  async function maybeSendDirectChatAccountWarn(sessionId) {
41464
41701
  try {
41465
41702
  if (!shouldSendAccountWarn({
@@ -41657,6 +41894,10 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
41657
41894
  ]
41658
41895
  }));
41659
41896
  mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
41897
+ {
41898
+ const offline = refuseIfToolsOffline(req.params.name);
41899
+ if (offline) return offline;
41900
+ }
41660
41901
  const { name, arguments: args } = req.params;
41661
41902
  const impersonationRefusal = shouldRefuseImpersonatedEgress(
41662
41903
  name,
@@ -41758,6 +41999,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
41758
41999
  }
41759
42000
  }
41760
42001
  await maybeSendDirectChatAccountWarn(session_id);
42002
+ void maybeSendDirectChatToolsOfflineNotice(session_id).catch(() => {
42003
+ });
41761
42004
  return { content: [{ type: "text", text: "sent" }] };
41762
42005
  } catch (err) {
41763
42006
  return {
@@ -41799,6 +42042,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
41799
42042
  clearDirectChatPendingMarkersForSession(DIRECT_CHAT_PENDING_INBOUND_DIR, session_id);
41800
42043
  recordDirectChatDelivery(session_id, message_ids);
41801
42044
  await maybeSendDirectChatAccountWarn(session_id);
42045
+ void maybeSendDirectChatToolsOfflineNotice(session_id).catch(() => {
42046
+ });
41802
42047
  return { content: [{ type: "text", text: "sent" }] };
41803
42048
  } catch (err) {
41804
42049
  return {