@jentrix/runner 0.5.20 → 0.5.22

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/README.md CHANGED
@@ -132,10 +132,10 @@ Setup:
132
132
 
133
133
  | Context loaded up front | Bytes | ≈ Tokens |
134
134
  | --- | --- | --- |
135
- | MCP agent — all 242 tool definitions | 374 KB | ~101k |
135
+ | MCP agent — all 242 tool definitions | 375 KB | ~101k |
136
136
  | CLI agent — `CLI_STANDUP_SYSTEM_PROMPT` only | 2 KB | ~510 |
137
137
 
138
- That is a **~198× smaller** Jentrix-specific up-front context.
138
+ That is a **~199× smaller** Jentrix-specific up-front context.
139
139
  <!-- END GENERATED: mcp-tool-count:agent-token-cost -->
140
140
 
141
141
  Reproduce the two numbers directly:
@@ -0,0 +1,7 @@
1
+ // lib/version.ts
2
+ var RUNNER_VERSION = "0.5.22";
3
+
4
+ export {
5
+ RUNNER_VERSION
6
+ };
7
+ //# sourceMappingURL=chunk-2YTE5PGH.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../lib/version.ts"],
4
- "sourcesContent": ["export const RUNNER_VERSION = \"0.5.20\";\n"],
4
+ "sourcesContent": ["export const RUNNER_VERSION = \"0.5.22\";\n"],
5
5
  "mappings": ";AAAO,IAAM,iBAAiB;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  RUNNER_VERSION
4
- } from "./chunk-FWAHAHAM.js";
4
+ } from "./chunk-2YTE5PGH.js";
5
5
 
6
6
  // runner-cli.ts
7
7
  import { fileURLToPath } from "node:url";
@@ -219,7 +219,7 @@ async function main(args = process.argv.slice(2)) {
219
219
  return runConfiguredRunner(args.includes("--once"));
220
220
  }
221
221
  if (command === "session-run" && (args.includes("--plan-stdin") || args.includes("--plan-file"))) {
222
- const { runSessionHost } = await import("./session-host-P6W3PH3S.js");
222
+ const { runSessionHost } = await import("./session-host-6WPHLGSS.js");
223
223
  let raw;
224
224
  const planFile = valueAfter(args, "--plan-file");
225
225
  if (planFile) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RUNNER_VERSION
3
- } from "./chunk-FWAHAHAM.js";
3
+ } from "./chunk-2YTE5PGH.js";
4
4
  import {
5
5
  appendHookEvent,
6
6
  readHookLines,
@@ -180,6 +180,126 @@ function serializeSessionEvent(event) {
180
180
  `;
181
181
  }
182
182
 
183
+ // lib/session-skeleton.ts
184
+ var SKELETON_VERSION = 1;
185
+ var MAX_SKELETON_BYTES = 32 * 1024;
186
+ var MAX_DISTINCT_TOOLS = 100;
187
+ var MAX_TRACKED_FILES = 300;
188
+ var MAX_HOURLY_BUCKETS = 500;
189
+ var MAX_PATH_CHARS = 300;
190
+ var PATH_KEYS = ["file_path", "path", "notebook_path", "filePath"];
191
+ function pathOf(value) {
192
+ if (typeof value !== "string") return null;
193
+ const trimmed = value.trim();
194
+ if (!trimmed || /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return null;
195
+ return trimmed.slice(0, MAX_PATH_CHARS);
196
+ }
197
+ var SessionSkeleton = class {
198
+ constructor(provider) {
199
+ this.provider = provider;
200
+ }
201
+ provider;
202
+ turns = 0;
203
+ firstEventAt = null;
204
+ lastEventAt = null;
205
+ eventCounts = /* @__PURE__ */ new Map();
206
+ toolCounts = /* @__PURE__ */ new Map();
207
+ files = /* @__PURE__ */ new Set();
208
+ filesOverflow = 0;
209
+ hourly = /* @__PURE__ */ new Map();
210
+ coalesced = false;
211
+ get observedAnything() {
212
+ return this.firstEventAt !== null;
213
+ }
214
+ /** Feed one REDACTED observed event. Pure accumulation, never throws. */
215
+ observe(event) {
216
+ this.eventCounts.set(
217
+ event.kind,
218
+ (this.eventCounts.get(event.kind) ?? 0) + 1
219
+ );
220
+ if (event.kind === "user_message") this.turns += 1;
221
+ if (this.firstEventAt === null) this.firstEventAt = event.at;
222
+ this.lastEventAt = event.at;
223
+ const hour = event.at.slice(0, 13);
224
+ if (this.hourly.has(hour) || this.hourly.size < MAX_HOURLY_BUCKETS) {
225
+ this.hourly.set(hour, (this.hourly.get(hour) ?? 0) + 1);
226
+ } else {
227
+ this.coalesced = true;
228
+ }
229
+ const payload = event.payload ?? {};
230
+ if (event.kind === "tool_call") {
231
+ const name = typeof payload.name === "string" && payload.name.trim() ? payload.name.trim().slice(0, 120) : "(unnamed)";
232
+ if (this.toolCounts.has(name) || this.toolCounts.size < MAX_DISTINCT_TOOLS) {
233
+ this.toolCounts.set(name, (this.toolCounts.get(name) ?? 0) + 1);
234
+ } else {
235
+ this.coalesced = true;
236
+ this.toolCounts.set(
237
+ "(other)",
238
+ (this.toolCounts.get("(other)") ?? 0) + 1
239
+ );
240
+ }
241
+ const input = payload.input;
242
+ if (input && typeof input === "object" && !Array.isArray(input)) {
243
+ for (const key of PATH_KEYS) {
244
+ const path = pathOf(input[key]);
245
+ if (path) this.touch(path);
246
+ }
247
+ }
248
+ }
249
+ if (event.kind === "file_change") {
250
+ const changes = payload.changes;
251
+ if (Array.isArray(changes)) {
252
+ for (const change of changes) {
253
+ const path = pathOf(change?.path);
254
+ if (path) this.touch(path);
255
+ }
256
+ } else if (changes && typeof changes === "object") {
257
+ for (const key of Object.keys(changes)) {
258
+ const path = pathOf(key);
259
+ if (path) this.touch(path);
260
+ }
261
+ }
262
+ }
263
+ }
264
+ touch(path) {
265
+ if (this.files.has(path)) return;
266
+ if (this.files.size >= MAX_TRACKED_FILES) {
267
+ this.filesOverflow += 1;
268
+ this.coalesced = true;
269
+ return;
270
+ }
271
+ this.files.add(path);
272
+ }
273
+ /** The bounded v1 JSON. ≤ 32 KB serialized — file-list tail drops first. */
274
+ snapshot() {
275
+ const build = (paths2, listTruncated2) => ({
276
+ version: SKELETON_VERSION,
277
+ provider: this.provider,
278
+ turns: this.turns,
279
+ firstEventAt: this.firstEventAt,
280
+ lastEventAt: this.lastEventAt,
281
+ eventCounts: Object.fromEntries(this.eventCounts),
282
+ toolCounts: Object.fromEntries(this.toolCounts),
283
+ filesTouched: {
284
+ total: this.files.size + this.filesOverflow,
285
+ paths: paths2,
286
+ ...listTruncated2 ? { listTruncated: true } : {}
287
+ },
288
+ hourlyBuckets: Object.fromEntries(this.hourly),
289
+ ...this.coalesced || listTruncated2 ? { truncated: true } : {}
290
+ });
291
+ let paths = [...this.files];
292
+ let listTruncated = this.filesOverflow > 0;
293
+ let skeleton = build(paths, listTruncated);
294
+ while (Buffer.byteLength(JSON.stringify(skeleton), "utf8") > MAX_SKELETON_BYTES && paths.length > 0) {
295
+ paths = paths.slice(0, Math.max(0, Math.floor(paths.length / 2)));
296
+ listTruncated = true;
297
+ skeleton = build(paths, listTruncated);
298
+ }
299
+ return skeleton;
300
+ }
301
+ };
302
+
183
303
  // lib/session-usage.ts
184
304
  function insideOneRange(ranges, from, to) {
185
305
  return ranges.some((r) => r.from <= from && to <= r.to);
@@ -362,6 +482,7 @@ var HEARTBEAT_MIN_INTERVAL_MS = 3e4;
362
482
  var SessionBridge = class {
363
483
  constructor(deps) {
364
484
  this.deps = deps;
485
+ this.skeleton = deps.collectSkeleton === false ? null : new SessionSkeleton(deps.provider);
365
486
  }
366
487
  deps;
367
488
  sequence = 0;
@@ -393,6 +514,13 @@ var SessionBridge = class {
393
514
  * session keeps its telemetry and loses what it actually concluded).
394
515
  */
395
516
  lastAssistantMessage = null;
517
+ /**
518
+ * Session evidence floor (PRD §4): the v1 activity skeleton, accumulated
519
+ * from every REDACTED event this bridge records — capture-off included —
520
+ * and submitted on the existing heartbeats plus the forced beat at close.
521
+ * Null when the operator opted out at align (D3).
522
+ */
523
+ skeleton;
396
524
  /**
397
525
  * True after a heartbeat came back 409 SESSION_NOT_ACTIVE — the session is
398
526
  * terminal server-side. The watch host uses this as its end signal when no
@@ -463,6 +591,7 @@ var SessionBridge = class {
463
591
  this.deps.redactor.text(serializeSessionEvent(full))
464
592
  );
465
593
  }
594
+ this.skeleton?.observe(full);
466
595
  if (full.kind === "assistant_message") {
467
596
  const text2 = full.payload?.text;
468
597
  if (typeof text2 === "string" && text2.trim().length > 0) {
@@ -601,7 +730,12 @@ var SessionBridge = class {
601
730
  // Sent only once a receipt has actually been observed — an empty
602
731
  // rollup would overwrite the session's totals with nulls and
603
732
  // report UNAVAILABLE for a session that had already reported.
604
- ...this.receipts.length > 0 ? { usage: this.usageRollup() } : {}
733
+ ...this.receipts.length > 0 ? { usage: this.usageRollup() } : {},
734
+ // Evidence floor (PRD §4/D2): the bounded activity skeleton rides
735
+ // the same beat (tolerant server parse — an old server strips the
736
+ // unknown key). Sent only once something was observed: null column
737
+ // means "never observed", never an empty object.
738
+ ...this.skeleton?.observedAnything ? { activitySkeleton: this.skeleton.snapshot() } : {}
605
739
  })
606
740
  }
607
741
  );
@@ -801,6 +935,7 @@ var SessionBridge = class {
801
935
  */
802
936
  async complete(opts) {
803
937
  const { pending } = await this.flushParts();
938
+ await this.flushUsageNow().catch(() => false);
804
939
  const finalResponseArtifactId = await this.pushFinalResponse();
805
940
  const rollup = this.usageRollup();
806
941
  const current = await this.deps.callTool("get_agent_session", {
@@ -818,6 +953,8 @@ var SessionBridge = class {
818
953
  endHead: opts.end.head,
819
954
  endDirty: opts.end.dirty,
820
955
  captureError,
956
+ ...opts.acknowledgeEvidenceGaps ? { acknowledgeEvidenceGaps: true } : {},
957
+ ...typeof opts.commitCount === "number" ? { commitCount: opts.commitCount } : {},
821
958
  ...manifest ? { manifest } : {},
822
959
  usage: {
823
960
  inputTokens: rollup.inputTokens,
@@ -1230,6 +1367,13 @@ function mapCodexRolloutLine(rawLine) {
1230
1367
  return { event: null, modelId: null, turn: null };
1231
1368
  }
1232
1369
  }
1370
+ function lastCodexRolloutModelOf(rollout) {
1371
+ let modelId = null;
1372
+ for (const rawLine of rollout.split("\n")) {
1373
+ modelId = mapCodexRolloutLine(rawLine).modelId ?? modelId;
1374
+ }
1375
+ return modelId;
1376
+ }
1233
1377
 
1234
1378
  // lib/session-codex-hooks.ts
1235
1379
  function text(value) {
@@ -1595,6 +1739,22 @@ function sessionCallTool(mcpUrl, bearerSource, sessionId) {
1595
1739
  }
1596
1740
  };
1597
1741
  }
1742
+ function evidenceFloorRefusalOf(error) {
1743
+ const raw = error instanceof Error ? error.message : String(error ?? "");
1744
+ if (!raw.includes("EVIDENCE_FLOOR")) return null;
1745
+ const start = raw.indexOf("{");
1746
+ if (start >= 0) {
1747
+ try {
1748
+ const envelope = JSON.parse(raw.slice(start));
1749
+ const message = envelope.error?.message;
1750
+ if (typeof message === "string" && message.includes("EVIDENCE_FLOOR")) {
1751
+ return message;
1752
+ }
1753
+ } catch {
1754
+ }
1755
+ }
1756
+ return raw;
1757
+ }
1598
1758
  function claudeHookSettings(runnerBin, sessionDir) {
1599
1759
  const hook = (event) => [
1600
1760
  {
@@ -1664,6 +1824,7 @@ async function runClaudeSessionHost(plan, deps = {}) {
1664
1824
  callTool: deps.callTool ?? sessionCallTool(plan.mcpUrl, bearerSource, plan.sessionId),
1665
1825
  fetchImpl: deps.fetchImpl,
1666
1826
  traceCapture,
1827
+ collectSkeleton: plan.collectSkeleton !== false,
1667
1828
  log
1668
1829
  });
1669
1830
  bridge.recordCapabilities(
@@ -1724,6 +1885,9 @@ async function runClaudeSessionHost(plan, deps = {}) {
1724
1885
  }
1725
1886
  let transcriptPath = watch ? plan.transcriptPath ?? null : null;
1726
1887
  let transcriptOffset = 0;
1888
+ let codexTurnId = null;
1889
+ let codexModelId = null;
1890
+ const codexRolloutTurnStarts = /* @__PURE__ */ new Map();
1727
1891
  const timing = new ClaudeTimingTracker();
1728
1892
  let transcriptSeen = false;
1729
1893
  let transcriptWarned = false;
@@ -1734,18 +1898,29 @@ async function runClaudeSessionHost(plan, deps = {}) {
1734
1898
  markHostTranscript(sessionDir, true, transcriptPath ?? void 0);
1735
1899
  }
1736
1900
  };
1901
+ const observePriorCodexModel = (path) => {
1902
+ if (plan.provider !== "codex" || plan.importHistory) return;
1903
+ try {
1904
+ const modelId = lastCodexRolloutModelOf(readFileSync3(path, "utf8"));
1905
+ if (modelId) {
1906
+ codexModelId = modelId;
1907
+ bridge.observeModel(modelId);
1908
+ }
1909
+ } catch {
1910
+ }
1911
+ };
1737
1912
  if (watch && transcriptPath && !plan.importHistory) {
1738
1913
  try {
1739
1914
  transcriptOffset = statSync2(transcriptPath).size;
1915
+ observePriorCodexModel(transcriptPath);
1740
1916
  noteTranscriptSeen();
1741
1917
  } catch {
1742
1918
  }
1743
1919
  }
1744
1920
  let bound = watch;
1745
- let codexTurnId = null;
1746
- let codexModelId = null;
1747
- const codexRolloutTurnStarts = /* @__PURE__ */ new Map();
1748
1921
  let sessionEnded = false;
1922
+ let endAcknowledgeGaps = false;
1923
+ let endCommitCount = null;
1749
1924
  let lastPeriodicFlushAt = 0;
1750
1925
  let lastPromptAttemptAt = 0;
1751
1926
  const promptRedactor = createSessionRedactor({ homedir: homedir() });
@@ -1823,6 +1998,14 @@ async function runClaudeSessionHost(plan, deps = {}) {
1823
1998
  const poll = async () => {
1824
1999
  const endRequestPath = join3(sessionDir, "end-request.json");
1825
2000
  if (!sessionEnded && existsSync(endRequestPath)) {
2001
+ try {
2002
+ const request = JSON.parse(readFileSync3(endRequestPath, "utf8"));
2003
+ endAcknowledgeGaps = request.acknowledgeEvidenceGaps === true;
2004
+ endCommitCount = typeof request.commitCount === "number" ? request.commitCount : null;
2005
+ } catch {
2006
+ endAcknowledgeGaps = false;
2007
+ endCommitCount = null;
2008
+ }
1826
2009
  try {
1827
2010
  unlinkSync3(endRequestPath);
1828
2011
  } catch {
@@ -1841,6 +2024,7 @@ async function runClaudeSessionHost(plan, deps = {}) {
1841
2024
  transcriptPath = hookTranscript;
1842
2025
  try {
1843
2026
  transcriptOffset = plan.importHistory ? 0 : statSync2(transcriptPath).size;
2027
+ observePriorCodexModel(transcriptPath);
1844
2028
  noteTranscriptSeen();
1845
2029
  } catch {
1846
2030
  transcriptOffset = 0;
@@ -1988,52 +2172,77 @@ async function runClaudeSessionHost(plan, deps = {}) {
1988
2172
  }
1989
2173
  await bridge.maybeHeartbeat();
1990
2174
  };
1991
- const timer = setInterval(() => {
2175
+ let timer = setInterval(() => {
1992
2176
  void poll();
1993
2177
  }, 2e3);
1994
- const exitCode = await (watch ? (
1995
- // Watch mode: live capture beside the operator's own provider process.
1996
- // End signals: the SessionEnd lifecycle hook, an end request from
1997
- // `jentrix session end`, or a heartbeat 409 (session terminal
1998
- // server-side the out-of-band case the hooks can never deliver).
1999
- new Promise((resolve) => {
2000
- const check = setInterval(() => {
2001
- if (sessionEnded || bridge.sessionInactive) {
2002
- clearInterval(check);
2003
- resolve(0);
2004
- }
2005
- }, 1e3);
2006
- })
2007
- ) : new Promise((resolve) => {
2008
- child.once("error", () => resolve(1));
2009
- child.once(
2010
- "exit",
2011
- (code, signal) => resolve(code ?? (signal ? 130 : 0))
2012
- );
2013
- }));
2014
- clearInterval(timer);
2015
- await poll().catch(() => void 0);
2016
- await bridge.flushParts().catch(() => void 0);
2017
- for (const id of timing.unclosedToolIds()) {
2018
- bridge.recordUnclosedInterval("tool", id);
2019
- }
2020
- for (const id of codexRolloutTurnStarts.keys()) {
2021
- bridge.recordUnclosedInterval("turn", id);
2022
- }
2023
- if (plan.provider === "codex") {
2024
- await bridge.flushUsageNow().catch(() => false);
2025
- }
2026
- const end = await endRepoState(plan.repoRoot);
2027
- const result = await bridge.complete({
2028
- outcome: exitCode === 0 ? "COMPLETED" : "INTERRUPTED",
2029
- end
2030
- }).catch((error) => {
2178
+ let exitCode = 0;
2179
+ let result = null;
2180
+ for (; ; ) {
2181
+ exitCode = await (watch ? (
2182
+ // Watch mode: live capture beside the operator's own provider process.
2183
+ // End signals: the SessionEnd lifecycle hook, an end request from
2184
+ // `jentrix session end`, or a heartbeat 409 (session terminal
2185
+ // server-side the out-of-band case the hooks can never deliver).
2186
+ new Promise((resolve) => {
2187
+ const check = setInterval(() => {
2188
+ if (sessionEnded || bridge.sessionInactive) {
2189
+ clearInterval(check);
2190
+ resolve(0);
2191
+ }
2192
+ }, 1e3);
2193
+ })
2194
+ ) : new Promise((resolve) => {
2195
+ child.once("error", () => resolve(1));
2196
+ child.once(
2197
+ "exit",
2198
+ (code, signal) => resolve(code ?? (signal ? 130 : 0))
2199
+ );
2200
+ }));
2201
+ clearInterval(timer);
2202
+ await poll().catch(() => void 0);
2203
+ await bridge.flushParts().catch(() => void 0);
2204
+ for (const id of timing.unclosedToolIds()) {
2205
+ bridge.recordUnclosedInterval("tool", id);
2206
+ }
2207
+ for (const id of codexRolloutTurnStarts.keys()) {
2208
+ bridge.recordUnclosedInterval("turn", id);
2209
+ }
2210
+ const end = await endRepoState(plan.repoRoot);
2211
+ let failure = null;
2212
+ result = await bridge.complete({
2213
+ outcome: exitCode === 0 ? "COMPLETED" : "INTERRUPTED",
2214
+ end,
2215
+ acknowledgeEvidenceGaps: endAcknowledgeGaps,
2216
+ commitCount: endCommitCount
2217
+ }).catch((error) => {
2218
+ failure = error;
2219
+ return null;
2220
+ });
2221
+ if (result) break;
2222
+ const refusal = watch ? evidenceFloorRefusalOf(failure) : null;
2223
+ if (refusal) {
2224
+ writeFileSync4(
2225
+ join3(sessionDir, "end-refusal.json"),
2226
+ JSON.stringify({
2227
+ refusedAt: (/* @__PURE__ */ new Date()).toISOString(),
2228
+ message: refusal
2229
+ }),
2230
+ { mode: 384 }
2231
+ );
2232
+ log(
2233
+ `capture: the close was refused by the evidence floor \u2014 this host (pid ${process.pid}) stays up; push the named evidence and re-run \`jentrix session end ${plan.sessionId}\``
2234
+ );
2235
+ sessionEnded = false;
2236
+ endAcknowledgeGaps = false;
2237
+ endCommitCount = null;
2238
+ timer = setInterval(() => {
2239
+ void poll();
2240
+ }, 2e3);
2241
+ continue;
2242
+ }
2031
2243
  log(
2032
- `capture: completion failed (${error instanceof Error ? error.message : "unknown"}) \u2014 spool retained for retry`
2244
+ `capture: completion failed (${failure instanceof Error ? failure.message : "unknown"}) \u2014 spool retained for retry`
2033
2245
  );
2034
- return null;
2035
- });
2036
- if (!result) {
2037
2246
  markHostExited(sessionDir, 1);
2038
2247
  return 1;
2039
2248
  }
@@ -2066,6 +2275,7 @@ async function runCodexSessionHost(plan, deps = {}) {
2066
2275
  redactor: createSessionRedactor({ homedir: homedir() }),
2067
2276
  callTool: sessionCallTool(plan.mcpUrl, codexBearerSource, plan.sessionId),
2068
2277
  fetchImpl: deps.fetchImpl,
2278
+ collectSkeleton: plan.collectSkeleton !== false,
2069
2279
  log
2070
2280
  });
2071
2281
  bridge.recordCapabilities({
@@ -2196,6 +2406,7 @@ export {
2196
2406
  appendHookEvent,
2197
2407
  claudeHookSettings,
2198
2408
  defaultSpoolRoot,
2409
+ evidenceFloorRefusalOf,
2199
2410
  readHookLines,
2200
2411
  readTranscriptTail,
2201
2412
  runClaudeSessionHost,
@@ -2204,4 +2415,4 @@ export {
2204
2415
  safeParse,
2205
2416
  sessionCallTool
2206
2417
  };
2207
- //# sourceMappingURL=session-host-P6W3PH3S.js.map
2418
+ //# sourceMappingURL=session-host-6WPHLGSS.js.map