@essentialai/cogent-plugin 3.25.9 → 3.25.11

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogent",
3
- "version": "3.25.9",
3
+ "version": "3.25.11",
4
4
  "description": "Inter-session communication bridge for Claude Code with Slack integration. Enables CC agents and Slack team members to communicate in real time.",
5
5
  "author": {
6
6
  "name": "Essential AI Solutions",
@@ -26475,8 +26475,8 @@ var init_stdio2 = __esm({
26475
26475
  // src/constants.ts
26476
26476
  import { createRequire } from "node:module";
26477
26477
  function resolveVersion() {
26478
- if ("3.25.9") {
26479
- return "3.25.9";
26478
+ if ("3.25.11") {
26479
+ return "3.25.11";
26480
26480
  }
26481
26481
  try {
26482
26482
  const require2 = createRequire(import.meta.url);
@@ -26692,6 +26692,9 @@ ${issues}`);
26692
26692
  _config = Object.freeze(data);
26693
26693
  return _config;
26694
26694
  }
26695
+ function readOnlyWake(config2) {
26696
+ return config2.COGENT_READ_ONLY_WAKE || config2.COGENT_WAKE_TOOLS === "read";
26697
+ }
26695
26698
  function getConfig() {
26696
26699
  if (!_config) throw new Error("Config not loaded. Call loadConfig() first.");
26697
26700
  return _config;
@@ -26755,11 +26758,44 @@ var init_config = __esm({
26755
26758
  // launches via the daemon (see bin/cogent-codex) and sets this to `app-server`.
26756
26759
  // Only affects COGENT_PLATFORM=codex; claude/gemini/mail rails never read it.
26757
26760
  COGENT_CODEX_WAKE: import_zod2.z.enum(["auto", "exec-resume", "app-server"]).default("auto"),
26758
- // COG-86: what a Claude wake may do. `read` (default) = Read, Glob and Grep only: the wake answers but cannot edit,
26759
- // run commands or commit. A wake resumes the SAME session a human may have open in a terminal, and nothing can
26760
- // tell reliably that the terminal is there; on 2026-10-15 a full-tool wake edited a live session's files, then
26761
- // committed and pushed. `full` = every tool (the behaviour before 3.25.9), for an agent no human drives.
26762
- COGENT_WAKE_TOOLS: import_zod2.z.enum(["read", "full"]).default("read"),
26761
+ // COG-86 / COG-88: what a Claude wake may do. `full` (default) = every tool, as before 3.25.9. `read` = Read,
26762
+ // Glob and Grep only: the wake answers but cannot edit, run commands or commit. A wake resumes the SAME session
26763
+ // a human may have open in a terminal, and nothing can tell reliably that the terminal is there; on 2026-10-15
26764
+ // a full-tool wake edited a live session's files, then committed and pushed. 3.25.9 made `read` the default;
26765
+ // the operator reversed that on 2026-10-16 (COG-88): read-only is an opt-in, switched with the environment
26766
+ // variable COGENT_READ_ONLY_WAKE (below) or here. An EMPTY value counts as unset — a host that substitutes an empty
26767
+ // string for a missing setting must not take the bridge down. Case and surrounding spaces are ignored
26768
+ // ("Read", "read " = read), the same rule COGENT_READ_ONLY_WAKE uses — a user opting in must not stop the
26769
+ // bridge over a capital letter. Any other unknown value still fails loudly.
26770
+ // Read it through readOnlyWake(), never directly: either opt-in turns read-only on.
26771
+ COGENT_WAKE_TOOLS: import_zod2.z.preprocess(
26772
+ (v) => {
26773
+ if (typeof v !== "string") return v;
26774
+ const s = v.trim().toLowerCase();
26775
+ return s === "" ? void 0 : s;
26776
+ },
26777
+ import_zod2.z.enum(["read", "full"]).default("full")
26778
+ ),
26779
+ // COG-88: the read-only wake opt-in — an ENVIRONMENT VARIABLE only: no plugin setting, so no Claude Code
26780
+ // version floor. Plugin users set it in the "env" block of ~/.claude/settings.json or export it in the shell
26781
+ // (both reach the plugin's MCP server and its SessionStart hook — measured, Claude Code 2.1.74 and 2.1.302); a custom/Team config sets it
26782
+ // in its server env block. Applied at bridge start: /reload-plugins does NOT restart the server (measured).
26783
+ // The plugin's .mcp.json must NEVER set this key (nor COGENT_WAKE_TOOLS): a plugin env-block key wins over the
26784
+ // shell and settings.json, so any value there would silently block the user's opt-in (measured).
26785
+ //
26786
+ // FUTURE /config option (operator, 2026-10-16: may become the only switch; NOT shipped now). It would be a
26787
+ // plugin.json "userConfig": { "read_only_wake": { "type": "boolean", ... } } mapped by plugin/.mcp.json
26788
+ // "COGENT_READ_ONLY_WAKE": "${user_config.read_only_wake}" — Claude Code passes the STRING "true"/"false"
26789
+ // (measured, 2.1.301). userConfig needs Claude Code >= 2.1.75: 2.1.74 and older reject the manifest
26790
+ // ('Unrecognized key: "userConfig"') and load NONE of the plugin (measured 2026-10-16). Adding that env line
26791
+ // also ends the env-variable path for plugin users (the block wins), so the two cannot ship side by side, and
26792
+ // the guards in src/config.test.ts and cogent-plugin/scripts/sync-from-plugin.test.mjs must change with it.
26793
+ //
26794
+ // Only an explicit affirmative turns it on. Anything else is OFF and never an error — "", "false", a typo, or
26795
+ // an unsubstituted "${user_config.read_only_wake}": a config error here would stop the bridge, which is worse
26796
+ // than a wake that behaves as it did before 3.25.9. The plugin's SessionStart notice reads it with the same rule
26797
+ // (readOnlyWakeFromEnv in plugin/hooks/check-on-stop.lib.mjs, parity-tested).
26798
+ COGENT_READ_ONLY_WAKE: import_zod2.z.string().optional().transform((v) => v !== void 0 && ["true", "1", "yes", "on"].includes(v.trim().toLowerCase())),
26763
26799
  // Auto-recovery for a blocked Codex sandbox. On a host where the OS sandbox
26764
26800
  // can't initialize (unprivileged user namespaces blocked — Ubuntu 24.04's
26765
26801
  // apparmor_restrict_unprivileged_userns, containers, hardened kernels),
@@ -27424,15 +27460,17 @@ async function execClaude(sessionId, message, cwd, timeoutMs) {
27424
27460
  // (the Monitor tool, loaded through ToolSearch, ran a shell and wrote a file) and would miss every tool a later
27425
27461
  // claude adds. --tools is variadic, so it must be followed by another option, never by the message. A claude
27426
27462
  // too old to know --tools exits "unknown option" — the wake fails, it never runs with every tool.
27427
- ...config2.COGENT_WAKE_TOOLS === "read" ? ["--tools", READ_ONLY_WAKE_TOOLS, "--append-system-prompt", READ_ONLY_WAKE_NOTICE] : [],
27463
+ // COG-88: opt-in (COGENT_READ_ONLY_WAKE=true or COGENT_WAKE_TOOLS=read in the env); off by default.
27464
+ ...readOnlyWake(config2) ? ["--tools", READ_ONLY_WAKE_TOOLS, "--append-system-prompt", READ_ONLY_WAKE_NOTICE] : [],
27428
27465
  "-p",
27429
- truncated
27466
+ readOnlyWake(config2) ? `${truncated}${READ_ONLY_WAKE_PROMPT_SEPARATOR}${READ_ONLY_WAKE_NOTICE}` : truncated
27430
27467
  ];
27431
27468
  const cleanEnv = buildResumeEnv(process.env, "CLAUDE", ["CLAUDE_CONFIG_DIR"]);
27432
27469
  if (resolvedRoot && resolvedRoot !== path10.resolve(claudeConfigDir())) {
27433
27470
  cleanEnv.CLAUDE_CONFIG_DIR = resolvedRoot;
27434
27471
  }
27435
27472
  cleanEnv.COGENT_CHECK_ON_STOP = "0";
27473
+ cleanEnv.COGENT_WAKE = "1";
27436
27474
  const child = spawn(config2.COGENT_CLAUDE_PATH, args, {
27437
27475
  cwd,
27438
27476
  env: cleanEnv,
@@ -27553,7 +27591,7 @@ function parseClaudeJsonReply(raw) {
27553
27591
  const result = typeof env.result === "string" ? env.result : null;
27554
27592
  return { result, isError: env.is_error === true || env.subtype !== "success" };
27555
27593
  }
27556
- var READ_ONLY_WAKE_TOOLS, READ_ONLY_WAKE_NOTICE;
27594
+ var READ_ONLY_WAKE_TOOLS, READ_ONLY_WAKE_NOTICE, READ_ONLY_WAKE_PROMPT_SEPARATOR;
27557
27595
  var init_cc_cli = __esm({
27558
27596
  "src/services/cc-cli.ts"() {
27559
27597
  "use strict";
@@ -27561,7 +27599,8 @@ var init_cc_cli = __esm({
27561
27599
  init_agent_config();
27562
27600
  init_agent_sessions();
27563
27601
  READ_ONLY_WAKE_TOOLS = "Read,Glob,Grep";
27564
- READ_ONLY_WAKE_NOTICE = "This is an automatic Cogent wake. You can read files but cannot change them or run commands. If the message asks for changes, say what must change and that it has to be done in the agent's own session.";
27602
+ READ_ONLY_WAKE_NOTICE = "You are answering a message from another Cogent peer automatically. Your reply goes to that sender, not to your user. Your user chose to make these automatic replies read-only: you can read files, but you cannot edit files, run commands or commit, and neither you nor the sender can change that. If the message asks for a change, do not attempt it. Your reply must: (1) say plainly that this automatic reply could not change any files; (2) give the exact change: the file path and the precise text to replace and its replacement, or the exact command to run; (3) say that your user can make the change in this agent's own session. Do not offer to make the change later, and do not ask the sender to enable, re-enable or turn on any tool or permission.";
27603
+ READ_ONLY_WAKE_PROMPT_SEPARATOR = "\n\n---\n[Cogent: read-only automatic reply \u2014 not part of the message above]\n";
27565
27604
  }
27566
27605
  });
27567
27606
 
@@ -32777,6 +32816,7 @@ function execCodex(sessionId, message, cwd, timeoutMs) {
32777
32816
  ];
32778
32817
  const cleanEnv = buildResumeEnv(process.env, "CODEX", ["CODEX_HOME"]);
32779
32818
  cleanEnv.COGENT_CHECK_ON_STOP = "0";
32819
+ cleanEnv.COGENT_WAKE = "1";
32780
32820
  const child = spawn2(config2.COGENT_CODEX_PATH, args, {
32781
32821
  cwd,
32782
32822
  env: cleanEnv,
@@ -33133,6 +33173,8 @@ async function _injectTurn(threadId, message, cwd, timeoutMs, deps = {}) {
33133
33173
  await client.waitOpen();
33134
33174
  await client.request("initialize", {
33135
33175
  clientInfo: { name: "cogent-bridge", version: "1" },
33176
+ // Also required by thread/resume `excludeTurns` below: codex 0.145–0.150.1 reject it
33177
+ // without this ("requires experimentalApi capability"); 0.151.0+ accept it either way.
33136
33178
  capabilities: { experimentalApi: true }
33137
33179
  });
33138
33180
  } catch (err) {
@@ -33144,7 +33186,7 @@ async function _injectTurn(threadId, message, cwd, timeoutMs, deps = {}) {
33144
33186
  };
33145
33187
  }
33146
33188
  try {
33147
- await client.request("thread/resume", { threadId, cwd });
33189
+ await client.request("thread/resume", { threadId, cwd, excludeTurns: true });
33148
33190
  const buffered = [];
33149
33191
  let myTurnId;
33150
33192
  const isMine = (msg) => msg.method === "turn/completed" && (myTurnId !== void 0 ? msg.params?.turn?.id === myTurnId : msg.params?.threadId === threadId);
@@ -34319,7 +34361,10 @@ ${message}${formatAttachmentsBlock(attachments)}
34319
34361
  ---
34320
34362
  FYI ONLY \u2014 another agent addressed the whole channel. Take note for context, but do NOT reply and do NOT call any cogent_* tool in response. A human will drive any reply.`;
34321
34363
  }
34322
- function describeWakeFailure(exitCode, stderr, stdout) {
34364
+ function wakeAgentFor(platform) {
34365
+ return platform === "codex" ? "codex" : "claude";
34366
+ }
34367
+ function describeWakeFailure(agent, exitCode, stderr, stdout) {
34323
34368
  const raw = (stderr ?? "").trim();
34324
34369
  if (!raw && exitCode !== 0 && !stdout) return "";
34325
34370
  const firstLine = raw.split("\n").map((l) => l.trim()).find(Boolean) ?? "";
@@ -34331,7 +34376,7 @@ function describeWakeFailure(exitCode, stderr, stdout) {
34331
34376
  }
34332
34377
  if (/CLI_NOT_FOUND/.test(raw)) {
34333
34378
  return truncate(
34334
- `the claude executable was not found \u2014 install Claude Code or set COGENT_CLAUDE_PATH.`
34379
+ agent === "codex" ? `the codex executable was not found \u2014 install the Codex CLI or set COGENT_CODEX_PATH.` : `the claude executable was not found \u2014 install Claude Code or set COGENT_CLAUDE_PATH.`
34335
34380
  );
34336
34381
  }
34337
34382
  const timedOut = /CLI_TIMEOUT: CLI subprocess timed out after (\d+)ms/.exec(raw);
@@ -34353,15 +34398,15 @@ function describeWakeFailure(exitCode, stderr, stdout) {
34353
34398
  }
34354
34399
  if (env.result) bits.push(env.result);
34355
34400
  if (!bits.length && env.isError) bits.push("the run reported is_error with no detail");
34356
- if (bits.length) return truncate(`claude exited ${code}: ${bits.join(" \u2014 ")}`);
34401
+ if (bits.length) return truncate(`${agent} exited ${code}: ${bits.join(" \u2014 ")}`);
34357
34402
  }
34358
34403
  return truncate(
34359
- stdout?.trim() ? `claude exited ${code} with nothing on stderr; its stdout was not a readable result envelope` : `claude exited ${code} with no output at all on stdout or stderr`
34404
+ stdout?.trim() ? `${agent} exited ${code} with nothing on stderr; its stdout was not a readable result envelope` : `${agent} exited ${code} with no output at all on stdout or stderr`
34360
34405
  );
34361
34406
  }
34362
34407
  if (firstLine) {
34363
34408
  const code = exitCode === null || exitCode === void 0 ? "?" : exitCode;
34364
- return truncate(`claude exited ${code}: ${firstLine}`);
34409
+ return truncate(`${agent} exited ${code}: ${firstLine}`);
34365
34410
  }
34366
34411
  if (exitCode === 0) {
34367
34412
  return truncate(`the resume completed successfully but returned no text.`);
@@ -35267,7 +35312,7 @@ var init_auto_relay = __esm({
35267
35312
  */
35268
35313
  async _recordNoReplyFailure(msg, traceId, durationMs, opts) {
35269
35314
  const codexLive = opts?.codexLiveSession === true;
35270
- const message = codexLive ? `\u{1F4E8} Queued for "${this.localPeerId}": it's live in an interactive Codex session, so Cogent can't resume it in real time \u2014 it will see this at its next turn. For real-time replies, launch it via \`cogent-codex\` (COGENT_CODEX_WAKE=app-server). (trace ${traceId})` : `\u26A0\uFE0F Cogent could not capture a reply from "${this.localPeerId}". ${describeWakeFailure(opts?.exitCode, opts?.stderr, opts?.stdout) || "It may have been busy, or the resume produced no output."} Manual response required \u2014 the target agent must reply in its own session. (trace ${traceId})`;
35315
+ const message = codexLive ? `\u{1F4E8} Queued for "${this.localPeerId}": it's live in an interactive Codex session, so Cogent can't resume it in real time \u2014 it will see this at its next turn. For real-time replies, launch it via \`cogent-codex\` (COGENT_CODEX_WAKE=app-server). (trace ${traceId})` : `\u26A0\uFE0F Cogent could not capture a reply from "${this.localPeerId}". ${describeWakeFailure(wakeAgentFor(getConfig().COGENT_PLATFORM), opts?.exitCode, opts?.stderr, opts?.stdout) || "It may have been busy, or the resume produced no output."} Manual response required \u2014 the target agent must reply in its own session. (trace ${traceId})`;
35271
35316
  try {
35272
35317
  await getBackend().recordMessage({
35273
35318
  fromPeerId: this.localPeerId,
@@ -35828,14 +35873,18 @@ var init_heartbeat = __esm({
35828
35873
  // src/services/claude-preflight.ts
35829
35874
  import { execFile as execFile4 } from "node:child_process";
35830
35875
  import { promisify as promisify3 } from "node:util";
35831
- function analyzeClaudeHelp(helpText) {
35832
- const checked = EXPECTED_CLAUDE_FLAGS.map((f) => f.flag);
35833
- const missing = EXPECTED_CLAUDE_FLAGS.filter(
35876
+ function expectedClaudeFlags(readOnly) {
35877
+ return readOnly ? [...EXPECTED_CLAUDE_FLAGS, ...READ_ONLY_WAKE_CLAUDE_FLAGS] : EXPECTED_CLAUDE_FLAGS;
35878
+ }
35879
+ function analyzeClaudeHelp(helpText, readOnly = false) {
35880
+ const expected = expectedClaudeFlags(readOnly);
35881
+ const checked = expected.map((f) => f.flag);
35882
+ const missing = expected.filter(
35834
35883
  (f) => !helpText.includes(f.flag)
35835
35884
  ).map((f) => f.flag);
35836
35885
  return { missing, checked };
35837
35886
  }
35838
- async function preflightClaude(claudePath) {
35887
+ async function preflightClaude(claudePath, readOnly = false) {
35839
35888
  let helpText;
35840
35889
  try {
35841
35890
  const { stdout, stderr } = await execFileAsync2(claudePath, ["--help"], {
@@ -35849,9 +35898,9 @@ ${stderr}`;
35849
35898
  );
35850
35899
  return null;
35851
35900
  }
35852
- const analysis = analyzeClaudeHelp(helpText);
35901
+ const analysis = analyzeClaudeHelp(helpText, readOnly);
35853
35902
  if (analysis.missing.length > 0) {
35854
- const detail = EXPECTED_CLAUDE_FLAGS.filter(
35903
+ const detail = expectedClaudeFlags(readOnly).filter(
35855
35904
  (f) => analysis.missing.includes(f.flag)
35856
35905
  ).map((f) => `${f.flag} (${f.usedFor})`).join(", ");
35857
35906
  logger.error(
@@ -35864,7 +35913,7 @@ ${stderr}`;
35864
35913
  }
35865
35914
  return analysis;
35866
35915
  }
35867
- var execFileAsync2, EXPECTED_CLAUDE_FLAGS;
35916
+ var execFileAsync2, EXPECTED_CLAUDE_FLAGS, READ_ONLY_WAKE_CLAUDE_FLAGS;
35868
35917
  var init_claude_preflight = __esm({
35869
35918
  "src/services/claude-preflight.ts"() {
35870
35919
  "use strict";
@@ -35876,7 +35925,9 @@ var init_claude_preflight = __esm({
35876
35925
  { flag: "--dangerously-skip-permissions", usedFor: "unattended auto-relay execution" },
35877
35926
  { flag: "--no-session-persistence", usedFor: "don't pollute the resumed session transcript" },
35878
35927
  { flag: "--strict-mcp-config", usedFor: "isolate MCP config of the resumed process" },
35879
- { flag: "--output-format", usedFor: "structured JSON reply envelope (Slice 3 capture)" },
35928
+ { flag: "--output-format", usedFor: "structured JSON reply envelope (Slice 3 capture)" }
35929
+ ];
35930
+ READ_ONLY_WAKE_CLAUDE_FLAGS = [
35880
35931
  { flag: "--tools", usedFor: "read-only wake: Read, Glob and Grep only (COG-86)" },
35881
35932
  { flag: "--append-system-prompt", usedFor: "tell a read-only wake it cannot change anything (COG-86)" }
35882
35933
  ];
@@ -36400,7 +36451,7 @@ async function runPreflights() {
36400
36451
  setSandboxOverride("bypass");
36401
36452
  }
36402
36453
  } else {
36403
- await preflightClaude(config2.COGENT_CLAUDE_PATH);
36454
+ await preflightClaude(config2.COGENT_CLAUDE_PATH, readOnlyWake(config2));
36404
36455
  }
36405
36456
  }
36406
36457
  var UUID_V4_RE, execFileAsync3, PERSIST_PATH, legacyWarnEmitted, cloudWsClient, cloudInbox, cloudHttpClient, cloudModeActive, inboxNotifier, notifierBoundTo, reRegistrationInProgress, presenceRetry, ignoredHolders, presenceSupersedeBound, PRESENCE_SUPERSEDED, pendingLabelResolve, lastRestoreSkip;
@@ -21,6 +21,16 @@ export function parseScope(csv) {
21
21
  };
22
22
  }
23
23
 
24
+ /**
25
+ * COG-88: are this agent's Claude wakes read-only? MUST equal src/config.ts readOnlyWake(loadConfig(env)):
26
+ * COGENT_READ_ONLY_WAKE truthy ("true"/"1"/"yes"/"on") OR COGENT_WAKE_TOOLS = "read", each trimmed and
27
+ * case-folded. Parity is tested against the REAL loadConfig (plugin/hooks/record-session.runtime.test.mjs).
28
+ */
29
+ export function readOnlyWakeFromEnv(env) {
30
+ const v = (k) => String(env[k] ?? "").trim().toLowerCase();
31
+ return ["true", "1", "yes", "on"].includes(v("COGENT_READ_ONLY_WAKE")) || v("COGENT_WAKE_TOOLS") === "read";
32
+ }
33
+
24
34
  /** sha256(path.resolve(cwd))[..16] — MUST equal credential-store.defaultCredentialPath's hash. */
25
35
  export function credHashForCwd(cwd) {
26
36
  return crypto.createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 16);
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // Cogent record-session. Runs on SessionStart as a SEPARATE short-lived process
3
3
  // (the host spawns it; it is NOT the bridge). Writes one small file and exits 0.
4
- // ALWAYS silent — it prints nothing on success or failure, so it can never block,
5
- // delay, or annotate a turn.
4
+ // Silent, with ONE exception (COG-88, below): the once-only wake-permissions notice,
5
+ // printed as {"systemMessage": ...} — valid SessionStart hook output that the host
6
+ // shows to the human and never feeds to the model. It can never block or fail a turn.
6
7
  //
7
8
  // 🔴 WHY THIS EXISTS. Auto-wake has to find the agent's session transcript, and it
8
9
  // used to DERIVE the location:
@@ -23,10 +24,12 @@
23
24
  // Node built-ins only (fs, os, path). Byte-identical between plugin/hooks/ and
24
25
  // codex-plugin/hooks/ — enforced by scripts/wake-race-test.mjs assertContract().
25
26
  import fs from "node:fs/promises";
27
+ import os from "node:os";
26
28
  import path from "node:path";
27
29
  import {
28
30
  agentSessionRecordPath,
29
31
  buildAgentSessionRecord,
32
+ readOnlyWakeFromEnv,
30
33
  } from "./check-on-stop.lib.mjs";
31
34
 
32
35
  async function readStdin() {
@@ -55,8 +58,70 @@ export async function persistRecord(record) {
55
58
  return target;
56
59
  }
57
60
 
61
+ // ── COG-88: tell the human ONCE what a Cogent wake may do in this folder ──────────────
62
+ // 3.25.9 made wakes read-only by default; 3.25.10 turned that back into an opt-in: the environment variable
63
+ // COGENT_READ_ONLY_WAKE=true (or COGENT_WAKE_TOOLS=read). Nobody reads a changelog, so the person at the
64
+ // terminal is told once, at a session start, what wakes can do and how to switch it.
65
+ //
66
+ // The mode is read from this hook's own env with the bridge's rule (readOnlyWakeFromEnv = readOnlyWake()).
67
+ // Measured on Claude Code 2.1.302 and 2.1.74 (throwaway CLAUDE_CONFIG_DIR, probe plugin): a settings.json "env" value and
68
+ // a shell export reach BOTH this hook and the plugin's MCP server, so the hook sees what the bridge sees.
69
+ // Also measured: after a settings.json change, /reload-plugins does NOT restart the plugin's MCP server (it
70
+ // keeps the old value), so the notice says "restart Claude Code".
71
+ //
72
+ // Shown ONLY when all hold:
73
+ // CLAUDECODE set — Claude Code. Codex never sets it (and shows systemMessage only
74
+ // after the first prompt).
75
+ // CLAUDE_CODE_SESSION_ATTENDED === "1" — a human is at the TUI ("0" in headless -p).
76
+ // COGENT_WAKE !== "1" — not inside a wake: the resumed claude runs this
77
+ // hook too (source "resume"), with the wake's env,
78
+ // and a notice there is swallowed but would still
79
+ // burn the once-only marker.
80
+ // the marker does not exist — once per notice version.
81
+ // Measured: Claude Code 2.1.302 gives a TUI SessionStart hook both variables; 2.1.74 gives it NEITHER (clean env, real
82
+ // TUI), so on 2.1.74 the notice is never shown. Deliberate: nothing there tells a TUI from headless -p, and a
83
+ // notice nobody sees would burn the marker. The opt-in itself still works on 2.1.74 (env reaches the bridge).
84
+ // The marker is created with "wx" BEFORE printing, so two sessions starting together
85
+ // cannot both show it, and it is never written unless the notice is printed.
86
+ export const WAKE_NOTICE_MARKER = "wake-tools-notice-v1";
87
+
88
+ export function wakeNoticeText(env) {
89
+ // Claude Code reads <CLAUDE_CONFIG_DIR>/settings.json when that variable is set; naming ~/.claude there
90
+ // would send the user to a file with no effect.
91
+ const dir = (env.CLAUDE_CONFIG_DIR || "").trim();
92
+ const settings = dir ? path.join(dir, "settings.json") : "~/.claude/settings.json";
93
+ if (readOnlyWakeFromEnv(env)) {
94
+ const names = ["COGENT_READ_ONLY_WAKE", "COGENT_WAKE_TOOLS"].filter((k) => readOnlyWakeFromEnv({ [k]: env[k] }));
95
+ const setting = names.join(" and ");
96
+ return (
97
+ `Cogent: wakes are read-only (${setting} ${names.length > 1 ? "are" : "is"} set) — when a Cogent message ` +
98
+ "wakes this agent, the wake can read files and answer, but cannot edit files or run commands. To let wakes " +
99
+ `edit again, remove ${setting} from the "env" block of ${settings} (or from your shell), then restart ` +
100
+ "Claude Code. (Shown once.)"
101
+ );
102
+ }
103
+ return (
104
+ "Cogent: when a Cogent message wakes this agent, the wake can edit files and run commands in this folder. " +
105
+ `To make wakes read-only, add "COGENT_READ_ONLY_WAKE": "true" to the "env" block of ${settings} ` +
106
+ "(or export it in your shell), then restart Claude Code. (Shown once.)"
107
+ );
108
+ }
109
+
110
+ async function maybeShowWakeNotice(env) {
111
+ if (!env.CLAUDECODE || env.CLAUDE_CODE_SESSION_ATTENDED !== "1" || env.COGENT_WAKE === "1") return;
112
+ const dir = env.CLAUDE_PLUGIN_DATA || path.join(os.homedir(), ".cogent", "notices");
113
+ await fs.mkdir(dir, { recursive: true });
114
+ // EEXIST (already shown) and every other failure throw → swallowed by the caller: silent.
115
+ await fs.writeFile(path.join(dir, WAKE_NOTICE_MARKER), `${new Date().toISOString()}\n`, { flag: "wx" });
116
+ const out = JSON.stringify({ systemMessage: wakeNoticeText(env) });
117
+ // Wait for the write: a pipe can be asynchronous (macOS), and exit() would cut it off.
118
+ await new Promise((resolve) => process.stdout.write(out, () => resolve()));
119
+ }
120
+
58
121
  async function main() {
59
122
  const input = await readStdin();
123
+ // Independent of the record below, and never allowed to break it (or the session start).
124
+ await maybeShowWakeNotice(process.env).catch(() => {});
60
125
  const record = buildAgentSessionRecord(input, new Date().toISOString());
61
126
  // No transcript_path in this payload → nothing authoritative to record. The
62
127
  // resolver's discovery layer still covers this case; staying silent is correct.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@essentialai/cogent-plugin",
3
- "version": "3.25.9",
3
+ "version": "3.25.11",
4
4
  "description": "Cogent — Claude Code plugin (skills + slash-commands + MCP server) for the cross-agent comms fabric.",
5
5
  "author": { "name": "Essential AI Solutions Ltd.", "url": "https://essentialai.uk" },
6
6
  "homepage": "https://cogent.tools",
@@ -10,6 +10,7 @@
10
10
  "files": [".claude-plugin", "commands", "skills", ".mcp.json", "hooks", "bridge", "README.md", "LICENSE"],
11
11
  "scripts": {
12
12
  "sync": "node scripts/sync-from-plugin.mjs",
13
+ "test": "node --test scripts/sync-from-plugin.test.mjs",
13
14
  "prepack": "node scripts/sync-from-plugin.mjs"
14
15
  }
15
16
  }