@jentrix/cli 0.5.21 → 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.
Files changed (2) hide show
  1. package/dist/main.js +71 -19
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -20966,7 +20966,7 @@ function refreshAccessToken(input) {
20966
20966
 
20967
20967
  // src/client.ts
20968
20968
  var CLI_NAME = "stacks-cli";
20969
- var CLI_VERSION = "0.5.21";
20969
+ var CLI_VERSION = "0.5.22";
20970
20970
  var DEAD_TOKEN_MESSAGE = "authentication failed (HTTP 401): the token is dead or expired \u2014 mint a new PAT at /account/tokens on your Jentrix server and update STACKS_TOKEN (or --token / the config file).";
20971
20971
  var RELOGIN_MESSAGE = "OAuth session expired and could not be refreshed \u2014 run `jentrix login` to sign in again.";
20972
20972
  function originOf2(url2) {
@@ -21388,6 +21388,9 @@ function buildAlignDisclosures(catalog) {
21388
21388
  disclosures.push(
21389
21389
  catalog.captureMode === "on" ? "TRACE capture: ON \u2014 the full transcript will be recorded" : "TRACE capture: OFF (MVP default) \u2014 typed artifacts only, no transcript"
21390
21390
  );
21391
+ disclosures.push(
21392
+ catalog.skeletonMode === "on" ? "activity skeleton: ON (default) \u2014 content-free counts/timing (turns, tool names, files-touched), independent of capture; --no-skeleton opts out" : "activity skeleton: OFF \u2014 no counts, no timing, no files-touched; the session's Session activity section will read as not observed"
21393
+ );
21391
21394
  disclosures.push("write the local alignment marker for this checkout");
21392
21395
  const mcp = catalog.mcpConfig;
21393
21396
  if (mcp && mcp.action !== "unchanged") {
@@ -22450,20 +22453,39 @@ function readLiveHostMarker(deps2, sessionId) {
22450
22453
  return null;
22451
22454
  }
22452
22455
  }
22453
- async function waitForHostExit(deps2, sessionId, pid, timeoutMs) {
22456
+ async function waitForHostEnd(deps2, sessionId, pid, timeoutMs) {
22454
22457
  const sleep = deps2.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
22455
- const path = join2(deps2.spoolRoot, sessionId, "host.json");
22458
+ const dir = join2(deps2.spoolRoot, sessionId);
22459
+ const path = join2(dir, "host.json");
22456
22460
  const deadline = Date.now() + timeoutMs;
22457
22461
  while (Date.now() < deadline) {
22458
22462
  await sleep(500);
22463
+ const refusal = readEndRefusal(deps2, sessionId);
22464
+ if (refusal) return { kind: "refused", message: refusal };
22459
22465
  try {
22460
22466
  const marker = JSON.parse(readFileSync2(path, "utf8"));
22461
- if (marker.exitedAt) return true;
22467
+ if (marker.exitedAt) return { kind: "exited" };
22462
22468
  } catch {
22463
22469
  }
22464
- if (!(deps2.isPidAlive ?? defaultIsPidAlive)(pid)) return false;
22470
+ if (!(deps2.isPidAlive ?? defaultIsPidAlive)(pid)) return { kind: "gone" };
22471
+ }
22472
+ return { kind: "gone" };
22473
+ }
22474
+ function readEndRefusal(deps2, sessionId) {
22475
+ try {
22476
+ const marker = JSON.parse(
22477
+ readFileSync2(join2(deps2.spoolRoot, sessionId, "end-refusal.json"), "utf8")
22478
+ );
22479
+ return typeof marker.message === "string" && marker.message.trim() ? marker.message : null;
22480
+ } catch {
22481
+ return null;
22482
+ }
22483
+ }
22484
+ function clearEndRefusal(deps2, sessionId) {
22485
+ try {
22486
+ unlinkSync(join2(deps2.spoolRoot, sessionId, "end-refusal.json"));
22487
+ } catch {
22465
22488
  }
22466
- return false;
22467
22489
  }
22468
22490
  function localCaptureLines(deps2, sessionId, serverStatus) {
22469
22491
  const dir = join2(deps2.spoolRoot, sessionId);
@@ -23163,14 +23185,12 @@ async function buildAttestedDiffBody(git, root, startHead, endHead, dirty) {
23163
23185
  }
23164
23186
  body = notice + kept;
23165
23187
  }
23188
+ let uncommitted = null;
23166
23189
  if (dirty) {
23167
23190
  const delta = await git(["diff", "--stat", "HEAD"], root);
23168
- body += `
23169
-
23170
- Uncommitted delta at end (stat only \u2014 not part of the attested range):
23171
- ${delta.code === 0 && delta.stdout.trim() ? delta.stdout : "(unreadable or empty)"}`;
23191
+ uncommitted = delta.code === 0 && delta.stdout.trim() ? delta.stdout.trim() : null;
23172
23192
  }
23173
- return { body: header + body, commitCount };
23193
+ return { body: header + body, commitCount, uncommitted };
23174
23194
  }
23175
23195
  async function postAttestedArtifact(deps2, sessionId, input) {
23176
23196
  const credentials = deps2.resolveTarget();
@@ -23298,6 +23318,12 @@ async function runSessionEnd(sessionIdFlag, flags, deps2) {
23298
23318
  deps2.writeOut(
23299
23319
  `Attested diff pushed \u2192 artifact ${pushed.artifactId ?? "(unknown)"} (${built.commitCount} commit(s), source: cli)`
23300
23320
  );
23321
+ if (built.uncommitted) {
23322
+ deps2.writeOut(
23323
+ `note: uncommitted delta at end (stat only \u2014 not part of the attested range):
23324
+ ${built.uncommitted}`
23325
+ );
23326
+ }
23301
23327
  } else {
23302
23328
  deps2.writeErr(
23303
23329
  `note: attested diff push failed (${pushed.detail}) \u2014 the E1 delivery check will refuse the close; retry \`jentrix session end\`, or acknowledge with --acknowledge-evidence-gaps.`
@@ -23320,21 +23346,32 @@ async function runSessionEnd(sessionIdFlag, flags, deps2) {
23320
23346
  };
23321
23347
  const live = readLiveHostMarker(deps2, sessionId);
23322
23348
  if (live) {
23349
+ clearEndRefusal(deps2, sessionId);
23323
23350
  writeFileSync2(
23324
23351
  join2(deps2.spoolRoot, sessionId, "end-request.json"),
23325
- JSON.stringify({ requestedAt: (/* @__PURE__ */ new Date()).toISOString() }),
23352
+ JSON.stringify({
23353
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
23354
+ // JEN-167: the host closes the session, so the operator's
23355
+ // acknowledgement has to travel with the request — otherwise the
23356
+ // floor refuses a close they explicitly asked to force. The
23357
+ // CLI-counted commits ride along for the same reason: only the
23358
+ // CLI can count them, and the host's close is the one that lands.
23359
+ ...flags.acknowledgeEvidenceGaps ? { acknowledgeEvidenceGaps: true } : {},
23360
+ ...commitCount !== null ? { commitCount } : {}
23361
+ }),
23326
23362
  { mode: 384 }
23327
23363
  );
23328
23364
  deps2.writeOut(
23329
23365
  `Local capture host (pid ${live.pid}) is finalizing capture\u2026`
23330
23366
  );
23331
- const finished = await waitForHostExit(
23332
- deps2,
23333
- sessionId,
23334
- live.pid,
23335
- 9e4
23336
- );
23337
- if (finished) {
23367
+ const outcome = await waitForHostEnd(deps2, sessionId, live.pid, 9e4);
23368
+ if (outcome.kind === "refused") {
23369
+ deps2.writeErr(
23370
+ `local capture host (pid ${live.pid}) is still running \u2014 the skeleton, heartbeats and telemetry keep accumulating through the comply work; re-run \`jentrix session end ${sessionId}\` once the evidence is pushed`
23371
+ );
23372
+ throw new Error(outcome.message);
23373
+ }
23374
+ if (outcome.kind === "exited") {
23338
23375
  const final = await call(caller, "get_agent_session", { sessionId });
23339
23376
  const stillOpen = final.status === "STARTING" || final.status === "ACTIVE";
23340
23377
  if (!stillOpen) {
@@ -24185,6 +24222,9 @@ async function buildCatalog(caller, flags, detection) {
24185
24222
  // checkout root); defaults here are the no-live-host reading.
24186
24223
  liveHostPid: null,
24187
24224
  captureMode: decideCaptureMode(flags.capture, false),
24225
+ // JEN-169: only `--no-skeleton` acts (the same rule submit applies at
24226
+ // line ~1273), so the disclosure needs no server round trip.
24227
+ skeletonMode: flags.skeleton === false ? "off" : "on",
24188
24228
  producer: producerFromFlags(flags),
24189
24229
  tokenBudget: budgetFromFlags(flags),
24190
24230
  preconditionNotices: [],
@@ -26574,6 +26614,10 @@ function parseArgsJson(raw, source) {
26574
26614
  }
26575
26615
  return { ok: true, value: parsed };
26576
26616
  }
26617
+ function workspaceIdShapeError(value) {
26618
+ if (/^[a-z0-9]{20,}$/.test(value)) return null;
26619
+ return `workspaceId "${value}" is not a workspace id (ids look like "cmt2cuhyo000004l38ddsim1y"). Find yours with \`jentrix workspace list\`, set it once as \`defaults.workspace\` in the config file, or use \`jentrix align --workspace <id-or-slug>\` \u2014 align is the command that accepts a slug.`;
26620
+ }
26577
26621
  async function runToolCommand(name, flags, deps2) {
26578
26622
  const maxWaitSeconds = Number(flags.maxWait);
26579
26623
  if (!Number.isFinite(maxWaitSeconds) || maxWaitSeconds < 0) {
@@ -26610,6 +26654,14 @@ async function runToolCommand(name, flags, deps2) {
26610
26654
  deps2.writeErr(`error: ${parsed.error}`);
26611
26655
  return EXIT_CODES.INVALID_INPUT;
26612
26656
  }
26657
+ const workspaceId = parsed.value.workspaceId;
26658
+ if (typeof workspaceId === "string") {
26659
+ const shapeError = workspaceIdShapeError(workspaceId);
26660
+ if (shapeError) {
26661
+ deps2.writeErr(`error: ${shapeError}`);
26662
+ return EXIT_CODES.INVALID_INPUT;
26663
+ }
26664
+ }
26613
26665
  let config2;
26614
26666
  try {
26615
26667
  config2 = resolveConfig({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jentrix/cli",
3
- "version": "0.5.21",
3
+ "version": "0.5.22",
4
4
  "description": "Command-line client for the Jentrix MCP surface (jentrix tool <name>, generated noun-verb commands).",
5
5
  "keywords": [
6
6
  "jentrix",