@darkhunt-security/endpoint-codex 0.9.3 → 0.9.5

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": "darkhunt-guard",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "hooks": "./hooks/hooks.json",
5
5
  "description": "Darkhunt endpoint plugin for Codex CLI \u2014 full session trace capture and PreToolUse guardrails.",
6
6
  "author": {
package/README.md CHANGED
@@ -60,7 +60,8 @@ the new version reports success and changes nothing.
60
60
 
61
61
  The session transcript, mapped to spans: user and assistant messages, thinking blocks,
62
62
  tool calls paired with their results, token usage, attachment metadata (never the bytes),
63
- and the subagent topology as linked child traces.
63
+ and every subagent's own rollout — Codex writes one per subagent, linked to its
64
+ parent through `parent_thread_id` and labelled with the agent.
64
65
 
65
66
  Reading the transcript rather than instrumenting the agent is what makes this complete:
66
67
  there is no code path in Codex CLI we could have missed.
@@ -18350,7 +18350,7 @@ var SessionEmitter = class {
18350
18350
  seeded(parts, create) {
18351
18351
  if (!this.options.ids)
18352
18352
  return create();
18353
- return this.options.ids.under(seed(this.options.sessionId, ...parts), create);
18353
+ return this.options.ids.under(seed(this.options.seedKey ?? this.options.sessionId, ...parts), create);
18354
18354
  }
18355
18355
  ingest(record) {
18356
18356
  if (record.kind === "session_meta") {
@@ -18358,6 +18358,9 @@ var SessionEmitter = class {
18358
18358
  this.title = record.meta.title;
18359
18359
  if (record.meta?.model)
18360
18360
  this.model = record.meta.model;
18361
+ if (record.meta?.parentSessionId && !this.trace) {
18362
+ this.grouping = record.meta.parentSessionId;
18363
+ }
18361
18364
  for (const [key, value] of Object.entries(record.meta ?? {})) {
18362
18365
  if (typeof value === "string" && key !== "title")
18363
18366
  this.meta[key] = value;
@@ -18368,7 +18371,7 @@ var SessionEmitter = class {
18368
18371
  const root = this.ensureTrace(record.ts);
18369
18372
  const parentTrace = record.parentUuid ? this.owners.get(record.parentUuid) : void 0;
18370
18373
  let owner = parentTrace ?? root;
18371
- if (record.isSidechain && owner === root) {
18374
+ if (record.isSidechain && owner === root && this.options.agentId === void 0) {
18372
18375
  owner = this.seeded(["subagent", record.uuid], () => this.client.trace({
18373
18376
  name: "subagent",
18374
18377
  sessionId: this.options.sessionId,
@@ -18471,14 +18474,31 @@ var SessionEmitter = class {
18471
18474
  }
18472
18475
  await this.client.flush();
18473
18476
  }
18477
+ defaultTraceName() {
18478
+ const agentType = this.options.agentType ?? this.meta["agentType"];
18479
+ if (agentType)
18480
+ return `subagent: ${agentType}`;
18481
+ if (this.options.agentId === void 0) {
18482
+ return this.grouping ? "subagent" : `${this.options.vendor} session`;
18483
+ }
18484
+ return this.options.agentType ? `subagent: ${this.options.agentType}` : `subagent ${this.options.agentId}`;
18485
+ }
18486
+ /** The session traces group under. Set from the path, or from `session_meta`. */
18487
+ grouping;
18474
18488
  ensureTrace(ts) {
18475
18489
  if (!this.trace) {
18476
18490
  this.trace = this.seeded(["trace", this.options.passKey ?? "0"], () => this.client.trace({
18477
- name: this.title ?? `${this.options.vendor} session`,
18478
- sessionId: this.options.sessionId,
18491
+ // A subagent's transcript has no title of its own, and "claude-code session"
18492
+ // beside its parent reads as a second session rather than part of one.
18493
+ name: this.title ?? this.defaultTraceName(),
18494
+ sessionId: this.grouping ?? this.options.sessionId,
18479
18495
  startTime: ts,
18480
- tags: [this.options.vendor],
18481
- metadata: this.meta,
18496
+ tags: [this.options.vendor, ...this.options.agentId ? ["subagent"] : []],
18497
+ metadata: {
18498
+ ...this.meta,
18499
+ ...this.options.agentId !== void 0 ? { agentId: this.options.agentId } : {},
18500
+ ...this.options.agentType !== void 0 ? { agentType: this.options.agentType } : {}
18501
+ },
18482
18502
  ...this.options.userId !== void 0 ? { userId: this.options.userId } : {},
18483
18503
  ...this.options.userEmail !== void 0 ? { userEmail: this.options.userEmail } : {},
18484
18504
  ...this.options.environment !== void 0 ? { environment: this.options.environment } : {}
@@ -21585,7 +21605,8 @@ async function runForwarder(mapper, options = {}) {
21585
21605
  }
21586
21606
  }
21587
21607
  const drain = options.paths ? { paths: options.paths } : drainSpool(mapper, checkpoints);
21588
- const paths2 = options.paths ? drain.paths : [.../* @__PURE__ */ new Set([...drain.paths, ...liveTranscripts(loadBeat(config.scope))])];
21608
+ const named = options.paths ? drain.paths : [.../* @__PURE__ */ new Set([...drain.paths, ...liveTranscripts(loadBeat(config.scope))])];
21609
+ const paths2 = [...new Set(named.flatMap((path) => [path, ...related(mapper, path)]))];
21589
21610
  if (paths2.length === 0) {
21590
21611
  if (drain.checkpoint)
21591
21612
  checkpoints[SPOOL_KEY] = drain.checkpoint;
@@ -21620,10 +21641,15 @@ async function runForwarder(mapper, options = {}) {
21620
21641
  try {
21621
21642
  const from = resumeOffset(checkpoints, path);
21622
21643
  const carried = resumeTurn(checkpoints, path, from);
21644
+ const own = mapper.sessionIdFor(path);
21645
+ const sub = subagentOf(mapper, path);
21623
21646
  const emitter = new SessionEmitter(client, {
21624
- sessionId: mapper.sessionIdFor(path),
21647
+ sessionId: sub?.sessionId ?? own,
21648
+ seedKey: own,
21625
21649
  vendor: mapper.vendor,
21626
21650
  passKey: String(from),
21651
+ ...sub?.agentId !== void 0 ? { agentId: sub.agentId } : {},
21652
+ ...sub?.agentType !== void 0 ? { agentType: sub.agentType } : {},
21627
21653
  ...ids !== void 0 ? { ids } : {},
21628
21654
  ...userId !== void 0 ? { userId } : {},
21629
21655
  ...carried !== void 0 ? { resumeTurn: carried } : {}
@@ -21707,6 +21733,20 @@ function withinDays(paths2, days) {
21707
21733
  }
21708
21734
  });
21709
21735
  }
21736
+ function related(mapper, path) {
21737
+ try {
21738
+ return mapper.relatedTranscripts?.(path) ?? [];
21739
+ } catch {
21740
+ return [];
21741
+ }
21742
+ }
21743
+ function subagentOf(mapper, path) {
21744
+ try {
21745
+ return mapper.subagentOf?.(path);
21746
+ } catch {
21747
+ return void 0;
21748
+ }
21749
+ }
21710
21750
  function discoverTranscripts(mapper) {
21711
21751
  const found = [];
21712
21752
  const walk = (dir) => {
@@ -21814,7 +21854,14 @@ var codexTranscript = {
21814
21854
  meta: {
21815
21855
  ...str(payload["cwd"]) !== void 0 ? { cwd: str(payload["cwd"]) } : {},
21816
21856
  ...str(payload["cli_version"]) !== void 0 ? { version: str(payload["cli_version"]) } : {},
21817
- ...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {}
21857
+ ...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {},
21858
+ // A Codex subagent is a rollout of its own, a peer of its parent in the
21859
+ // same date directory — nothing in the path says whose it is. These three
21860
+ // are the only link, and unlike Claude Code's layout they are the vendor's
21861
+ // own documented session metadata rather than a guess.
21862
+ ...str(payload["parent_thread_id"]) !== void 0 ? { parentSessionId: str(payload["parent_thread_id"]) } : {},
21863
+ ...str(payload["agent_nickname"]) !== void 0 ? { agentType: str(payload["agent_nickname"]) } : {},
21864
+ ...str(payload["agent_path"]) !== void 0 ? { agentPath: str(payload["agent_path"]) } : {}
21818
21865
  }
21819
21866
  }
21820
21867
  ];
@@ -18350,7 +18350,7 @@ var SessionEmitter = class {
18350
18350
  seeded(parts, create) {
18351
18351
  if (!this.options.ids)
18352
18352
  return create();
18353
- return this.options.ids.under(seed(this.options.sessionId, ...parts), create);
18353
+ return this.options.ids.under(seed(this.options.seedKey ?? this.options.sessionId, ...parts), create);
18354
18354
  }
18355
18355
  ingest(record) {
18356
18356
  if (record.kind === "session_meta") {
@@ -18358,6 +18358,9 @@ var SessionEmitter = class {
18358
18358
  this.title = record.meta.title;
18359
18359
  if (record.meta?.model)
18360
18360
  this.model = record.meta.model;
18361
+ if (record.meta?.parentSessionId && !this.trace) {
18362
+ this.grouping = record.meta.parentSessionId;
18363
+ }
18361
18364
  for (const [key, value] of Object.entries(record.meta ?? {})) {
18362
18365
  if (typeof value === "string" && key !== "title")
18363
18366
  this.meta[key] = value;
@@ -18368,7 +18371,7 @@ var SessionEmitter = class {
18368
18371
  const root = this.ensureTrace(record.ts);
18369
18372
  const parentTrace = record.parentUuid ? this.owners.get(record.parentUuid) : void 0;
18370
18373
  let owner = parentTrace ?? root;
18371
- if (record.isSidechain && owner === root) {
18374
+ if (record.isSidechain && owner === root && this.options.agentId === void 0) {
18372
18375
  owner = this.seeded(["subagent", record.uuid], () => this.client.trace({
18373
18376
  name: "subagent",
18374
18377
  sessionId: this.options.sessionId,
@@ -18471,14 +18474,31 @@ var SessionEmitter = class {
18471
18474
  }
18472
18475
  await this.client.flush();
18473
18476
  }
18477
+ defaultTraceName() {
18478
+ const agentType = this.options.agentType ?? this.meta["agentType"];
18479
+ if (agentType)
18480
+ return `subagent: ${agentType}`;
18481
+ if (this.options.agentId === void 0) {
18482
+ return this.grouping ? "subagent" : `${this.options.vendor} session`;
18483
+ }
18484
+ return this.options.agentType ? `subagent: ${this.options.agentType}` : `subagent ${this.options.agentId}`;
18485
+ }
18486
+ /** The session traces group under. Set from the path, or from `session_meta`. */
18487
+ grouping;
18474
18488
  ensureTrace(ts) {
18475
18489
  if (!this.trace) {
18476
18490
  this.trace = this.seeded(["trace", this.options.passKey ?? "0"], () => this.client.trace({
18477
- name: this.title ?? `${this.options.vendor} session`,
18478
- sessionId: this.options.sessionId,
18491
+ // A subagent's transcript has no title of its own, and "claude-code session"
18492
+ // beside its parent reads as a second session rather than part of one.
18493
+ name: this.title ?? this.defaultTraceName(),
18494
+ sessionId: this.grouping ?? this.options.sessionId,
18479
18495
  startTime: ts,
18480
- tags: [this.options.vendor],
18481
- metadata: this.meta,
18496
+ tags: [this.options.vendor, ...this.options.agentId ? ["subagent"] : []],
18497
+ metadata: {
18498
+ ...this.meta,
18499
+ ...this.options.agentId !== void 0 ? { agentId: this.options.agentId } : {},
18500
+ ...this.options.agentType !== void 0 ? { agentType: this.options.agentType } : {}
18501
+ },
18482
18502
  ...this.options.userId !== void 0 ? { userId: this.options.userId } : {},
18483
18503
  ...this.options.userEmail !== void 0 ? { userEmail: this.options.userEmail } : {},
18484
18504
  ...this.options.environment !== void 0 ? { environment: this.options.environment } : {}
@@ -21585,7 +21605,8 @@ async function runForwarder(mapper, options = {}) {
21585
21605
  }
21586
21606
  }
21587
21607
  const drain = options.paths ? { paths: options.paths } : drainSpool(mapper, checkpoints);
21588
- const paths = options.paths ? drain.paths : [.../* @__PURE__ */ new Set([...drain.paths, ...liveTranscripts(loadBeat(config.scope))])];
21608
+ const named = options.paths ? drain.paths : [.../* @__PURE__ */ new Set([...drain.paths, ...liveTranscripts(loadBeat(config.scope))])];
21609
+ const paths = [...new Set(named.flatMap((path) => [path, ...related(mapper, path)]))];
21589
21610
  if (paths.length === 0) {
21590
21611
  if (drain.checkpoint)
21591
21612
  checkpoints[SPOOL_KEY] = drain.checkpoint;
@@ -21620,10 +21641,15 @@ async function runForwarder(mapper, options = {}) {
21620
21641
  try {
21621
21642
  const from = resumeOffset(checkpoints, path);
21622
21643
  const carried = resumeTurn(checkpoints, path, from);
21644
+ const own = mapper.sessionIdFor(path);
21645
+ const sub = subagentOf(mapper, path);
21623
21646
  const emitter = new SessionEmitter(client, {
21624
- sessionId: mapper.sessionIdFor(path),
21647
+ sessionId: sub?.sessionId ?? own,
21648
+ seedKey: own,
21625
21649
  vendor: mapper.vendor,
21626
21650
  passKey: String(from),
21651
+ ...sub?.agentId !== void 0 ? { agentId: sub.agentId } : {},
21652
+ ...sub?.agentType !== void 0 ? { agentType: sub.agentType } : {},
21627
21653
  ...ids !== void 0 ? { ids } : {},
21628
21654
  ...userId !== void 0 ? { userId } : {},
21629
21655
  ...carried !== void 0 ? { resumeTurn: carried } : {}
@@ -21694,6 +21720,20 @@ async function runForwarder(mapper, options = {}) {
21694
21720
  release();
21695
21721
  }
21696
21722
  }
21723
+ function related(mapper, path) {
21724
+ try {
21725
+ return mapper.relatedTranscripts?.(path) ?? [];
21726
+ } catch {
21727
+ return [];
21728
+ }
21729
+ }
21730
+ function subagentOf(mapper, path) {
21731
+ try {
21732
+ return mapper.subagentOf?.(path);
21733
+ } catch {
21734
+ return void 0;
21735
+ }
21736
+ }
21697
21737
  function discoverTranscripts(mapper) {
21698
21738
  const found = [];
21699
21739
  const walk = (dir) => {
@@ -21801,7 +21841,14 @@ var codexTranscript = {
21801
21841
  meta: {
21802
21842
  ...str(payload["cwd"]) !== void 0 ? { cwd: str(payload["cwd"]) } : {},
21803
21843
  ...str(payload["cli_version"]) !== void 0 ? { version: str(payload["cli_version"]) } : {},
21804
- ...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {}
21844
+ ...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {},
21845
+ // A Codex subagent is a rollout of its own, a peer of its parent in the
21846
+ // same date directory — nothing in the path says whose it is. These three
21847
+ // are the only link, and unlike Claude Code's layout they are the vendor's
21848
+ // own documented session metadata rather than a guess.
21849
+ ...str(payload["parent_thread_id"]) !== void 0 ? { parentSessionId: str(payload["parent_thread_id"]) } : {},
21850
+ ...str(payload["agent_nickname"]) !== void 0 ? { agentType: str(payload["agent_nickname"]) } : {},
21851
+ ...str(payload["agent_path"]) !== void 0 ? { agentPath: str(payload["agent_path"]) } : {}
21805
21852
  }
21806
21853
  }
21807
21854
  ];
@@ -4,7 +4,7 @@ const require = __dhCreateRequire(import.meta.url);
4
4
 
5
5
  // adapters/codex/bin/status.mjs
6
6
  import { readFileSync as readFileSync7 } from "node:fs";
7
- import { dirname, join as join11 } from "node:path";
7
+ import { dirname, join as join12 } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
10
10
  // packages/core/dist/config/local.js
@@ -1124,8 +1124,40 @@ var VALIDATORS = Object.freeze({
1124
1124
  var LIB_VERSION = package_default.version;
1125
1125
 
1126
1126
  // packages/core/dist/cli/status.js
1127
- import { existsSync as existsSync2, readFileSync as readFileSync5, statSync as statSync3 } from "node:fs";
1127
+ import { existsSync as existsSync2, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
1128
+ import { join as join8 } from "node:path";
1128
1129
  var SESSION_HOOK_STALE_MS = 30 * 60 * 1e3;
1130
+ function countSubagents(mapper, checkpoints) {
1131
+ if (!mapper?.subagentOf || !mapper.sessionRoots)
1132
+ return void 0;
1133
+ let found = 0;
1134
+ let shipped = 0;
1135
+ const walk = (dir) => {
1136
+ let entries;
1137
+ try {
1138
+ entries = readdirSync2(dir, { withFileTypes: true });
1139
+ } catch {
1140
+ return;
1141
+ }
1142
+ for (const entry of entries) {
1143
+ const full = join8(dir, entry.name);
1144
+ if (entry.isDirectory()) {
1145
+ walk(full);
1146
+ continue;
1147
+ }
1148
+ if (!entry.name.endsWith(".jsonl"))
1149
+ continue;
1150
+ if (!mapper.subagentOf?.(full))
1151
+ continue;
1152
+ found++;
1153
+ if (checkpoints[full])
1154
+ shipped++;
1155
+ }
1156
+ };
1157
+ for (const root of mapper.sessionRoots())
1158
+ walk(root);
1159
+ return { found, shipped };
1160
+ }
1129
1161
  function status(vendor, mapper) {
1130
1162
  const config = loadLocalConfig(vendor);
1131
1163
  const checkpoints = loadCheckpoints(config.scope);
@@ -1136,6 +1168,7 @@ function status(vendor, mapper) {
1136
1168
  const size = existsSync2(path) ? statSync3(path).size : 0;
1137
1169
  transcripts.push({ path, shipped: cp.offset, size, lag: Math.max(0, size - cp.offset) });
1138
1170
  }
1171
+ const subagents = countSubagents(mapper, checkpoints);
1139
1172
  const lastPass = loadHealth(config.scope);
1140
1173
  const spool = spoolPath(vendor);
1141
1174
  let spoolEvents = 0;
@@ -1176,17 +1209,18 @@ function status(vendor, mapper) {
1176
1209
  transcripts,
1177
1210
  totalLag: transcripts.reduce((sum, t) => sum + t.lag, 0),
1178
1211
  ...lastPass !== void 0 ? { lastPass } : {},
1212
+ ...subagents !== void 0 ? { subagents } : {},
1179
1213
  lockHeld: existsSync2(spool.replace(/spool\/.*$/, `${config.scope}.forwarder.lock`))
1180
1214
  };
1181
1215
  }
1182
1216
 
1183
1217
  // packages/core/dist/cli/enroll.js
1184
1218
  import { homedir as homedir3 } from "node:os";
1185
- import { join as join8 } from "node:path";
1186
- var CLI_CREDENTIALS_PATH = join8(homedir3(), ".darkhunt", "credentials.json");
1219
+ import { join as join9 } from "node:path";
1220
+ var CLI_CREDENTIALS_PATH = join9(homedir3(), ".darkhunt", "credentials.json");
1187
1221
 
1188
1222
  // adapters/codex/dist/transcript.js
1189
- import { basename, join as join9 } from "node:path";
1223
+ import { basename, join as join10 } from "node:path";
1190
1224
  import { homedir as homedir4 } from "node:os";
1191
1225
  function str(value) {
1192
1226
  return typeof value === "string" ? value : void 0;
@@ -1225,7 +1259,7 @@ function textOf(content) {
1225
1259
  var codexTranscript = {
1226
1260
  vendor: "codex",
1227
1261
  sessionRoots() {
1228
- return [join9(homedir4(), ".codex", "sessions")];
1262
+ return [join10(homedir4(), ".codex", "sessions")];
1229
1263
  },
1230
1264
  // No `resolveUserId` yet, so Codex traces fall back to the configured `userId`.
1231
1265
  // The identity exists but not as a plain field: `~/.codex/auth.json` holds
@@ -1262,7 +1296,14 @@ var codexTranscript = {
1262
1296
  meta: {
1263
1297
  ...str(payload["cwd"]) !== void 0 ? { cwd: str(payload["cwd"]) } : {},
1264
1298
  ...str(payload["cli_version"]) !== void 0 ? { version: str(payload["cli_version"]) } : {},
1265
- ...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {}
1299
+ ...str(payload["originator"]) !== void 0 ? { entrypoint: str(payload["originator"]) } : {},
1300
+ // A Codex subagent is a rollout of its own, a peer of its parent in the
1301
+ // same date directory — nothing in the path says whose it is. These three
1302
+ // are the only link, and unlike Claude Code's layout they are the vendor's
1303
+ // own documented session metadata rather than a guess.
1304
+ ...str(payload["parent_thread_id"]) !== void 0 ? { parentSessionId: str(payload["parent_thread_id"]) } : {},
1305
+ ...str(payload["agent_nickname"]) !== void 0 ? { agentType: str(payload["agent_nickname"]) } : {},
1306
+ ...str(payload["agent_path"]) !== void 0 ? { agentPath: str(payload["agent_path"]) } : {}
1266
1307
  }
1267
1308
  }
1268
1309
  ];
@@ -1326,12 +1367,12 @@ var codexTranscript = {
1326
1367
  // adapters/codex/bin/lib/launchers.mjs
1327
1368
  import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
1328
1369
  import { homedir as homedir5 } from "node:os";
1329
- import { join as join10 } from "node:path";
1370
+ import { join as join11 } from "node:path";
1330
1371
  var LAUNCHERS = {
1331
1372
  "darkhunt-codex-spool": "spool.mjs",
1332
1373
  "darkhunt-codex-guard": "guard.mjs"
1333
1374
  };
1334
- var BIN_DIR = join10(homedir5(), ".local", "bin");
1375
+ var BIN_DIR = join11(homedir5(), ".local", "bin");
1335
1376
  var script = (target) => `#!/bin/sh
1336
1377
  # Installed by darkhunt endpoint-plugins. Resolves the newest installed
1337
1378
  # plugin version at run time; do not hard-code a version here.
@@ -1343,7 +1384,7 @@ function launcherState(binDir = BIN_DIR, path = process.env["PATH"] ?? "") {
1343
1384
  const missing = [];
1344
1385
  const stale = [];
1345
1386
  for (const [name, target] of Object.entries(LAUNCHERS)) {
1346
- const file = join10(binDir, name);
1387
+ const file = join11(binDir, name);
1347
1388
  if (!existsSync3(file)) {
1348
1389
  missing.push(name);
1349
1390
  continue;
@@ -1385,10 +1426,10 @@ try {
1385
1426
  const s = status("codex", codexTranscript);
1386
1427
  const n = (x) => x.toLocaleString();
1387
1428
  console.log(`darkhunt endpoint status \u2014 codex`);
1388
- const root = join11(dirname(fileURLToPath(import.meta.url)), "..", "..");
1429
+ const root = join12(dirname(fileURLToPath(import.meta.url)), "..", "..");
1389
1430
  let installed = "unknown";
1390
1431
  try {
1391
- installed = JSON.parse(readFileSync7(join11(root, "package.json"), "utf8")).version;
1432
+ installed = JSON.parse(readFileSync7(join12(root, "package.json"), "utf8")).version;
1392
1433
  } catch {
1393
1434
  }
1394
1435
  console.log(` plugin ${installed} ${root}`);
@@ -1398,6 +1439,12 @@ try {
1398
1439
  ` user ${s.userId ?? "(none \u2014 traces are unattributed)"}` + (s.userIdSource === "none" ? "" : ` [from ${s.userIdSource}]`)
1399
1440
  );
1400
1441
  console.log(` capture ${s.capture ? "ON" : "OFF"} enforce ${s.enforce}`);
1442
+ if (s.subagents && s.subagents.found > 0) {
1443
+ const { found, shipped } = s.subagents;
1444
+ console.log(
1445
+ ` subagents ${shipped} of ${found} shipped` + (shipped < found ? ` \u2014 ${found - shipped} not captured; run backfill.mjs` : "")
1446
+ );
1447
+ }
1401
1448
  const launchers = launcherState();
1402
1449
  const launcherProblems = launcherWarnings(launchers);
1403
1450
  console.log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darkhunt-security/endpoint-codex",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "type": "module",
5
5
  "description": "Darkhunt endpoint adapter for Codex CLI: hook codec, rollout transcript mapper, plugin manifest.",
6
6
  "bin": {