@jentrix/cli 0.5.20 → 0.5.21

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,5 +1,5 @@
1
1
  {
2
2
  "name": "jentrix",
3
3
  "description": "Connect this Claude Code session to a Jentrix project: /jentrix-connect binds the current session (trusted lifecycle hooks supply the session id), /jentrix-align runs the MVP alignment wizard, /jentrix-status and /jentrix-end wrap the same session operations. Business logic lives in the Jentrix CLI — removing this plugin leaves `jentrix session attach` as the universal fallback.",
4
- "version": "0.5.2"
4
+ "version": "0.5.3"
5
5
  }
@@ -49,6 +49,15 @@ the plan, when a long investigation resolves, or before handing the work over.
49
49
  `jentrix task move --task <id> --to-column-id <id>`. Only the columns the
50
50
  board actually has.
51
51
 
52
+ **Cadence duty (evidence floor).** Checkpoint after EVERY commit, not only at
53
+ the end: push the `decision` (with `--basis`) the commit rested on the moment
54
+ it lands, and run its gates through `jentrix push log --from-cmd "<command>"`
55
+ so the exit code and output tail are recorded as a LOG. Record quality must
56
+ track the work, not end-of-session diligence — `jentrix session end` enforces
57
+ this (it refuses on commits with no decision record and on sectioned reports
58
+ with no typed artifacts; a deliberate exception is an ordinary
59
+ `jentrix push gap`).
60
+
52
61
  Note for the operator: this records the distillation. On a session aligned
53
62
  with `--capture`, the raw pre-compaction range is preserved separately and
54
63
  automatically by the `PreCompact` hook, so the bytes survive even when this
@@ -77,3 +77,17 @@ pipeline), the typed artifacts ARE the record — push them before closing:
77
77
  step 1, or in the next session. Do not try to reopen it.
78
78
  8. Ending the Jentrix session does NOT terminate this Claude Code session —
79
79
  say so if the user seems to expect it.
80
+
81
+ **Evidence floor.** `jentrix session end` enforces fixed checks and will
82
+ REFUSE to close when they are unmet: E1 — HEAD moved but no attested DIFF
83
+ (the CLI pushes the real patch itself; just re-run `session end` from the
84
+ aligned checkout); E2 — commits with no `jentrix push decision` and no
85
+ declared-deviation `jentrix push gap`; E3 — a REPORT carrying `## Learnings`
86
+ / `## Gaps` / `## Decisions` sections while the session has zero
87
+ corresponding typed artifacts (split them into `push learning|gap|decision`,
88
+ never one blob). Comply by pushing the named evidence and retrying, or
89
+ deviate honestly with a `gap` — `--acknowledge-evidence-gaps` closes anyway
90
+ and stamps each unmet check MISSING into Review readiness. Run every gate
91
+ through `jentrix push log --from-cmd "<command>"` (it records the exit code
92
+ and output tail as a LOG and exits with the command's own code), so "tests
93
+ green" claims carry evidence instead of tripping the E4 advisory.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jentrix",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Connect Codex tasks to Jentrix with trusted lifecycle hooks, exact-rollout telemetry recovery, typed artifacts, and operator-confirmed alignment.",
5
5
  "author": {
6
6
  "name": "Jentrix",
@@ -20,3 +20,10 @@ status is stale, read the real columns and correct it.
20
20
 
21
21
  The compact hooks preserve only the approved evidence boundary; this skill performs
22
22
  the model-authored distillation.
23
+
24
+ **Cadence duty (evidence floor).** Checkpoint after EVERY commit, not only at the
25
+ end: push the `decision` (with `--basis`) the commit rested on the moment it lands,
26
+ and run its gates through `jentrix push log --from-cmd "<command>"` so the exit code
27
+ and output tail land as a LOG. `jentrix session end` enforces this — it refuses on
28
+ commits with no decision record and on sectioned reports with no typed artifacts;
29
+ a deliberate exception is an ordinary `jentrix push gap`.
@@ -29,3 +29,16 @@ description: End a connected Jentrix session after preserving approved outputs,
29
29
  the command and must not be claimed as captured.
30
30
  8. The Jentrix session is sealed after end; do not retry pushes against it. Ending it
31
31
  does not terminate the Codex task.
32
+
33
+ **Evidence floor.** `jentrix session end` REFUSES to close on unmet checks: E1 —
34
+ HEAD moved but no attested DIFF (the CLI pushes the real patch itself; re-run
35
+ `session end` from the aligned checkout); E2 — commits with no `jentrix push
36
+ decision` and no declared-deviation `jentrix push gap`; E3 — a REPORT carrying
37
+ `## Learnings` / `## Gaps` / `## Decisions` sections while the session has zero
38
+ corresponding typed artifacts (split them into `push learning|gap|decision`).
39
+ Comply by pushing the named evidence and retrying, or deviate honestly with a
40
+ `gap`; `--acknowledge-evidence-gaps` closes anyway and stamps each unmet check
41
+ MISSING into Review readiness. Run every gate through
42
+ `jentrix push log --from-cmd "<command>"` (records exit code + output tail as a
43
+ LOG, exits with the command's own code) so gate claims carry evidence instead of
44
+ tripping the E4 advisory.
package/dist/main.js CHANGED
@@ -10324,7 +10324,7 @@ var require_dist = __commonJS({
10324
10324
  });
10325
10325
 
10326
10326
  // src/main.ts
10327
- import { spawn as spawn3 } from "node:child_process";
10327
+ import { spawn as spawn4 } from "node:child_process";
10328
10328
  import { createInterface } from "node:readline";
10329
10329
  import {
10330
10330
  appendFileSync,
@@ -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.20";
20969
+ var CLI_VERSION = "0.5.21";
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) {
@@ -21302,6 +21302,7 @@ function safeStringify(value) {
21302
21302
  }
21303
21303
 
21304
21304
  // src/commands/push.ts
21305
+ import { spawn as spawn2 } from "node:child_process";
21305
21306
  import { readFileSync as readFileSync4 } from "node:fs";
21306
21307
 
21307
21308
  // src/align-questions.ts
@@ -22584,8 +22585,9 @@ async function runSessionStart(provider, flags, deps2) {
22584
22585
  // outlives the CLI holding a dead credential.
22585
22586
  bearer: deps2.resolveTarget().token,
22586
22587
  // …and the config file itself, so the host FOLLOWS later rotations
22587
- // instead of dying with a revoked snapshot (2026-08-08 finding).
22588
- configPath: deps2.configPath,
22588
+ // instead of dying with a revoked snapshot (2026-08-08 finding)
22589
+ // but ONLY when the token IS the config's (hostConfigPathOf).
22590
+ configPath: hostConfigPathOf(deps2),
22589
22591
  repoRoot: inspection.root,
22590
22592
  installationId,
22591
22593
  mode: "launch",
@@ -22944,8 +22946,9 @@ async function runSessionAttach(flags, deps2) {
22944
22946
  // outlives the CLI holding a dead credential.
22945
22947
  bearer: deps2.resolveTarget().token,
22946
22948
  // …and the config file itself, so the host FOLLOWS later rotations
22947
- // instead of dying with a revoked snapshot (2026-08-08 finding).
22948
- configPath: deps2.configPath,
22949
+ // instead of dying with a revoked snapshot (2026-08-08 finding)
22950
+ // but ONLY when the token IS the config's (hostConfigPathOf).
22951
+ configPath: hostConfigPathOf(deps2),
22949
22952
  repoRoot: inspection.root,
22950
22953
  installationId,
22951
22954
  mode: "watch",
@@ -23026,6 +23029,8 @@ async function runSessionStatus(sessionId, flags, deps2) {
23026
23029
  `Project: ${String(session.projectName)} (${String(session.projectId)})`,
23027
23030
  `Repository: ${String(session.repoOwnerName)}`,
23028
23031
  `Capture: ${capture}`,
23032
+ // Evidence floor (D3): the skeleton mode the alignment declared.
23033
+ `Skeleton: ${session.alignment?.skeleton === "off" ? "off (--no-skeleton at align)" : "on (content-free activity counts/timing)"}`,
23029
23034
  ...local,
23030
23035
  telemetry.line,
23031
23036
  `Summary artifact: ${session.summaryArtifactId ? String(session.summaryArtifactId) : "\u2014"}`
@@ -23122,6 +23127,87 @@ function telemetryVerdict(sessionId, usage, hostRan, provider = "claude") {
23122
23127
  warning: `NO TOKEN TELEMETRY: no local session host ever ran for ${sessionId}, so nothing could collect provider usage receipts. Align from inside the provider session (the plugin's hooks record the transcript), or pass --transcript-path.`
23123
23128
  };
23124
23129
  }
23130
+ function hostConfigPathOf(deps2) {
23131
+ const source = deps2.resolveTarget().tokenSource;
23132
+ return source === void 0 || source === "file" ? deps2.configPath : null;
23133
+ }
23134
+ var MAX_ATTESTED_DIFF_BYTES = 256 * 1024;
23135
+ async function buildAttestedDiffBody(git, root, startHead, endHead, dirty) {
23136
+ const counted = await git(
23137
+ ["rev-list", "--count", `${startHead}..${endHead}`],
23138
+ root
23139
+ );
23140
+ if (counted.code !== 0) return null;
23141
+ const commitCount = Number(counted.stdout.trim());
23142
+ if (!Number.isFinite(commitCount) || commitCount === 0) return null;
23143
+ const header = [
23144
+ "Attested delivery \u2014 generated by `jentrix session end` from the aligned checkout (source: cli).",
23145
+ `Range: ${startHead}..${endHead} \xB7 ${commitCount} commit(s)`,
23146
+ ""
23147
+ ].join("\n");
23148
+ const cap = MAX_ATTESTED_DIFF_BYTES - Buffer.byteLength(header, "utf8");
23149
+ const full = await git(
23150
+ ["log", "--patch", "--stat", `${startHead}..${endHead}`],
23151
+ root
23152
+ );
23153
+ let body;
23154
+ if (full.code === 0 && Buffer.byteLength(full.stdout, "utf8") <= cap) {
23155
+ body = full.stdout;
23156
+ } else {
23157
+ const stat = await git(["log", "--stat", `${startHead}..${endHead}`], root);
23158
+ const notice = "[truncated stat-first: the full --patch output exceeded the 256 KB attested-diff cap \u2014 commit list + per-file stat retained; the full patch lives in git]\n\n";
23159
+ let kept = stat.code === 0 ? stat.stdout : "(git log --stat failed)";
23160
+ const room = cap - Buffer.byteLength(notice, "utf8");
23161
+ if (Buffer.byteLength(kept, "utf8") > room) {
23162
+ kept = Buffer.from(kept, "utf8").subarray(0, Math.max(0, room - 64)).toString("utf8").replace(/�+$/, "") + "\n[stat tail truncated at the cap]";
23163
+ }
23164
+ body = notice + kept;
23165
+ }
23166
+ if (dirty) {
23167
+ 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)"}`;
23172
+ }
23173
+ return { body: header + body, commitCount };
23174
+ }
23175
+ async function postAttestedArtifact(deps2, sessionId, input) {
23176
+ const credentials = deps2.resolveTarget();
23177
+ try {
23178
+ const response = await (deps2.fetchImpl ?? fetch)(
23179
+ new URL(
23180
+ `/api/agent-sessions/${sessionId}/artifacts`,
23181
+ stacksBaseUrlOf(credentials.url)
23182
+ ),
23183
+ {
23184
+ method: "POST",
23185
+ headers: {
23186
+ authorization: `Bearer ${credentials.token}`,
23187
+ "content-type": "application/json"
23188
+ },
23189
+ body: JSON.stringify({
23190
+ kind: input.kind,
23191
+ title: input.title,
23192
+ body: redactLocal(input.body, deps2.env),
23193
+ attested: true
23194
+ })
23195
+ }
23196
+ );
23197
+ const payload = await response.json().catch(() => ({}));
23198
+ return response.ok ? { ok: true, artifactId: payload.artifactId ?? null, detail: null } : {
23199
+ ok: false,
23200
+ artifactId: null,
23201
+ detail: payload.detail ?? payload.error ?? `HTTP ${response.status}`
23202
+ };
23203
+ } catch (error2) {
23204
+ return {
23205
+ ok: false,
23206
+ artifactId: null,
23207
+ detail: error2 instanceof Error ? error2.message : "network failure"
23208
+ };
23209
+ }
23210
+ }
23125
23211
  async function runSessionEnd(sessionIdFlag, flags, deps2) {
23126
23212
  try {
23127
23213
  const inspection = await inspectRepository(deps2.cwd(), deps2.git);
@@ -23188,6 +23274,39 @@ async function runSessionEnd(sessionIdFlag, flags, deps2) {
23188
23274
  );
23189
23275
  }
23190
23276
  const sessionId = resolved;
23277
+ let commitCount = null;
23278
+ try {
23279
+ const row = await call(caller, "get_agent_session", { sessionId });
23280
+ const startHead = typeof row.startHead === "string" ? row.startHead : null;
23281
+ const attestRepo = inspection && inspection.repoOwnerName === row.repoOwnerName ? inspection : null;
23282
+ if (attestRepo?.head && startHead && attestRepo.head !== startHead && (row.status === "ACTIVE" || row.status === "STARTING")) {
23283
+ const built = await buildAttestedDiffBody(
23284
+ deps2.git ?? defaultGitRunner,
23285
+ attestRepo.root,
23286
+ startHead,
23287
+ attestRepo.head,
23288
+ attestRepo.dirty
23289
+ );
23290
+ if (built) {
23291
+ commitCount = built.commitCount;
23292
+ const pushed = await postAttestedArtifact(deps2, sessionId, {
23293
+ kind: "diff",
23294
+ title: `Attested diff ${startHead.slice(0, 10)}..${attestRepo.head.slice(0, 10)} (${built.commitCount} commit${built.commitCount === 1 ? "" : "s"})`,
23295
+ body: built.body
23296
+ });
23297
+ if (pushed.ok) {
23298
+ deps2.writeOut(
23299
+ `Attested diff pushed \u2192 artifact ${pushed.artifactId ?? "(unknown)"} (${built.commitCount} commit(s), source: cli)`
23300
+ );
23301
+ } else {
23302
+ deps2.writeErr(
23303
+ `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.`
23304
+ );
23305
+ }
23306
+ }
23307
+ }
23308
+ } catch {
23309
+ }
23191
23310
  const hostRan = hasLocalCaptureFootprint(deps2, sessionId);
23192
23311
  const reportTelemetry = (row) => {
23193
23312
  const verdict2 = telemetryVerdict(
@@ -23273,6 +23392,11 @@ async function runSessionEnd(sessionIdFlag, flags, deps2) {
23273
23392
  endHead: sameRepo?.head ?? null,
23274
23393
  endDirty: sameRepo?.dirty ?? null,
23275
23394
  ...spooledUsage ? { usage: spooledUsage } : {},
23395
+ // Evidence floor (§5/§6): the CLI-counted commits in the range feed
23396
+ // E2 and the summary's density line; the acknowledge flag closes over
23397
+ // unmet checks with each stamped MISSING into Review readiness.
23398
+ ...commitCount !== null ? { commitCount } : {},
23399
+ ...flags.acknowledgeEvidenceGaps ? { acknowledgeEvidenceGaps: true } : {},
23276
23400
  expectedUpdatedAt: session.updatedAt
23277
23401
  });
23278
23402
  if (inspection) {
@@ -23606,6 +23730,12 @@ function reportError(error2, deps2) {
23606
23730
  }
23607
23731
  const message = error2 instanceof Error ? error2.message : String(error2);
23608
23732
  deps2.writeErr(`error: ${message}`);
23733
+ if (message.includes("EVIDENCE_FLOOR")) {
23734
+ deps2.writeErr(
23735
+ "To close anyway with the unmet checks stamped MISSING: `jentrix session end --acknowledge-evidence-gaps` (a declared deviation is an ordinary `jentrix push gap`)."
23736
+ );
23737
+ return EXIT_CODES.CONFLICT;
23738
+ }
23609
23739
  if (message.includes("SESSION_ALREADY_BOUND")) return EXIT_CODES.CONFLICT;
23610
23740
  if (message.includes("FORBIDDEN") || message.includes("PROJECT_DISCOVERY_REQUIRES_UNPINNED_LOGIN")) {
23611
23741
  return EXIT_CODES.FORBIDDEN;
@@ -23661,7 +23791,12 @@ function registerSessionCommand(program3, deps2, onExit2) {
23661
23791
  ).option("--json", "stable JSON output").action(
23662
23792
  async (sessionId, flags) => onExit2(await runSessionStatus(sessionId, flags, deps2))
23663
23793
  );
23664
- session.command("end [sessionId]").description("Close a session, verify capture, and store the RUN_SUMMARY.").option("--json", "stable JSON output").action(
23794
+ session.command("end [sessionId]").description(
23795
+ "Close a session, verify capture, and store the RUN_SUMMARY. Pushes the attested delivery patch when HEAD moved, and enforces the evidence-floor checks E1\u2013E4 (refusal names each unmet check and its fix)."
23796
+ ).option("--json", "stable JSON output").option(
23797
+ "--acknowledge-evidence-gaps",
23798
+ "close even with unmet evidence-floor checks \u2014 each unmet check is stamped MISSING into the summary's Review readiness"
23799
+ ).action(
23665
23800
  async (sessionId, flags) => onExit2(await runSessionEnd(sessionId, flags, deps2))
23666
23801
  );
23667
23802
  return session;
@@ -24297,9 +24432,13 @@ ${JSON.stringify(session.alignment, null, 2)}`
24297
24432
  } : {},
24298
24433
  ...budget !== null ? { tokenBudget: budget } : {},
24299
24434
  capture: captureMode,
24435
+ // Evidence floor (D3): only an explicit --no-skeleton is sent; absent
24436
+ // lets the server keep the stored mode (default "on").
24437
+ ...flags.skeleton === false ? { skeleton: "off" } : {},
24300
24438
  expectedUpdatedAt: session.updatedAt
24301
24439
  });
24302
24440
  const alignment = aligned.alignment;
24441
+ const skeletonMode = alignment.skeleton === "off" ? "off" : "on";
24303
24442
  writeAlignmentMarker(
24304
24443
  deps2.configPath,
24305
24444
  inspection.root,
@@ -24347,8 +24486,9 @@ ${JSON.stringify(session.alignment, null, 2)}`
24347
24486
  mcpUrl: target.url,
24348
24487
  bearer: deps2.resolveTarget().token,
24349
24488
  // …and the config file itself, so the host FOLLOWS later rotations
24350
- // instead of dying with a revoked snapshot (2026-08-08 finding).
24351
- configPath: deps2.configPath,
24489
+ // instead of dying with a revoked snapshot (2026-08-08 finding)
24490
+ // but ONLY when the token IS the config's (hostConfigPathOf).
24491
+ configPath: hostConfigPathOf(deps2),
24352
24492
  repoRoot: inspection.root,
24353
24493
  installationId,
24354
24494
  mode: "watch",
@@ -24356,6 +24496,7 @@ ${JSON.stringify(session.alignment, null, 2)}`
24356
24496
  ...detected.transcriptPath ? { transcriptPath: detected.transcriptPath } : {},
24357
24497
  ...detected.hookDir ? { hookDir: detected.hookDir } : {},
24358
24498
  captureTrace: captureMode === "on",
24499
+ collectSkeleton: skeletonMode === "on",
24359
24500
  spoolRoot: deps2.spoolRoot
24360
24501
  });
24361
24502
  if (pid !== null) {
@@ -24830,6 +24971,7 @@ async function runAlign(flags, deps2) {
24830
24971
  ` Agent: ${a.agent?.provider ?? "\u2014"}`,
24831
24972
  ` Repo: ${a.repo?.ownerName ?? "\u2014"}@${a.repo?.branch ?? "detached"}`,
24832
24973
  ` Capture: ${a.capture ?? "off"}`,
24974
+ ` Skeleton: ${a.skeleton ?? "on"} (content-free activity counts/timing; --no-skeleton opts out)`,
24833
24975
  `Alignment snapshot (server-confirmed): ${JSON.stringify(result.alignment)}`
24834
24976
  ].join("\n")
24835
24977
  );
@@ -24894,6 +25036,9 @@ function registerAlignCommand(program3, deps2, onExit2) {
24894
25036
  ).option(
24895
25037
  "--no-capture",
24896
25038
  "keep TRACE capture OFF explicitly \u2014 also corrects a wrongly-on record on re-align"
25039
+ ).option(
25040
+ "--no-skeleton",
25041
+ "opt out of the content-free activity skeleton (event/tool counts, files-touched, timing buckets) for this session \u2014 default on, independent of capture"
24897
25042
  ).addOption(
24898
25043
  new Option(
24899
25044
  "--provider <provider>",
@@ -24931,7 +25076,8 @@ var PUSH_KINDS = [
24931
25076
  "prd",
24932
25077
  "gap",
24933
25078
  "issue",
24934
- "context"
25079
+ "context",
25080
+ "log"
24935
25081
  ];
24936
25082
  var ARTIFACT_TYPE_BY_PUSH_KIND = {
24937
25083
  plan: "PLAN",
@@ -24946,8 +25092,23 @@ var ARTIFACT_TYPE_BY_PUSH_KIND = {
24946
25092
  prd: "PRD",
24947
25093
  gap: "GAP",
24948
25094
  issue: "ISSUE",
24949
- context: "SOURCE_DIGEST"
25095
+ context: "SOURCE_DIGEST",
25096
+ log: "LOG"
24950
25097
  };
25098
+ var MAX_FROM_CMD_OUTPUT_BYTES = 64 * 1024;
25099
+ function fromCmdLogBody(command, exitCode, output) {
25100
+ const header = `$ ${command}
25101
+ exit code: ${exitCode}
25102
+
25103
+ `;
25104
+ if (Buffer.byteLength(output, "utf8") <= MAX_FROM_CMD_OUTPUT_BYTES) {
25105
+ return header + (output || "(no output)");
25106
+ }
25107
+ const notice = `[head truncated: output exceeded ${MAX_FROM_CMD_OUTPUT_BYTES / 1024} KB \u2014 tail retained]
25108
+ `;
25109
+ const tail = Buffer.from(output, "utf8").subarray(-MAX_FROM_CMD_OUTPUT_BYTES).toString("utf8").replace(/^�+/, "");
25110
+ return header + notice + tail;
25111
+ }
24951
25112
  function sessionTarget(sessionId) {
24952
25113
  return {
24953
25114
  path: `/api/agent-sessions/${sessionId}/artifacts`,
@@ -24957,6 +25118,32 @@ function sessionTarget(sessionId) {
24957
25118
  function taskTarget(taskId) {
24958
25119
  return { path: `/api/tasks/${taskId}/artifacts`, label: `task ${taskId}` };
24959
25120
  }
25121
+ function defaultRunCommand(command) {
25122
+ return new Promise((resolve5) => {
25123
+ const child = spawn2(command, { shell: true, windowsHide: true });
25124
+ let tail = Buffer.alloc(0);
25125
+ const keep = (chunk) => {
25126
+ tail = Buffer.concat([tail, chunk]);
25127
+ if (tail.byteLength > MAX_FROM_CMD_OUTPUT_BYTES * 2) {
25128
+ tail = tail.subarray(-MAX_FROM_CMD_OUTPUT_BYTES);
25129
+ }
25130
+ };
25131
+ child.stdout?.on("data", keep);
25132
+ child.stderr?.on("data", keep);
25133
+ child.once(
25134
+ "error",
25135
+ (error2) => resolve5({ code: 127, output: `${error2.message}
25136
+ ` })
25137
+ );
25138
+ child.once(
25139
+ "exit",
25140
+ (code, signal) => resolve5({
25141
+ code: code ?? (signal ? 130 : 1),
25142
+ output: tail.toString("utf8")
25143
+ })
25144
+ );
25145
+ });
25146
+ }
24960
25147
  async function drainStdin() {
24961
25148
  const chunks = [];
24962
25149
  for await (const chunk of process.stdin) {
@@ -24976,6 +25163,21 @@ async function runPush(kind, file, flags, deps2) {
24976
25163
  "--basis records what a decision rested on \u2014 it only applies to `jentrix push decision`"
24977
25164
  );
24978
25165
  }
25166
+ if (flags.fromCmd !== void 0) {
25167
+ if (kind !== "log") {
25168
+ throw new UsageError(
25169
+ '--from-cmd captures a command\'s exit code and output as a LOG \u2014 it only applies to `jentrix push log --from-cmd "<command>"`'
25170
+ );
25171
+ }
25172
+ if (file || flags.ref) {
25173
+ throw new UsageError(
25174
+ "--from-cmd generates the LOG body from the command run \u2014 drop the file/--ref"
25175
+ );
25176
+ }
25177
+ if (!flags.fromCmd.trim()) {
25178
+ throw new UsageError("--from-cmd needs a non-empty command");
25179
+ }
25180
+ }
24979
25181
  if (flags.ref) {
24980
25182
  if (flags.basis?.length) {
24981
25183
  throw new UsageError(
@@ -25028,7 +25230,17 @@ async function runPush(kind, file, flags, deps2) {
25028
25230
  correlatedSessionId = marker.sessionId;
25029
25231
  fallbackTaskId = marker.taskId;
25030
25232
  }
25031
- const raw = file ? readFileSync4(file, "utf8") : await (deps2.readStdin ?? drainStdin)();
25233
+ let commandExit = null;
25234
+ let raw;
25235
+ if (flags.fromCmd !== void 0) {
25236
+ const command = flags.fromCmd.trim();
25237
+ deps2.writeErr(`running: ${command}`);
25238
+ const ran = await (deps2.runCommand ?? defaultRunCommand)(command);
25239
+ commandExit = ran.code;
25240
+ raw = fromCmdLogBody(command, ran.code, ran.output);
25241
+ } else {
25242
+ raw = file ? readFileSync4(file, "utf8") : await (deps2.readStdin ?? drainStdin)();
25243
+ }
25032
25244
  if (!raw.trim()) {
25033
25245
  throw new UsageError(
25034
25246
  file ? `${file} is empty` : "stdin is empty \u2014 pipe the content or name a file"
@@ -25052,6 +25264,9 @@ ${raw}` : raw;
25052
25264
  ...flags.title ? { title: flags.title } : {},
25053
25265
  ...flags.agent ? { producerLabel: flags.agent } : {},
25054
25266
  ...flags.agentEmoji ? { producerEmoji: flags.agentEmoji } : {},
25267
+ // D4: only the CLI-generated capture is attested — never a piped
25268
+ // body, whatever its kind.
25269
+ ...commandExit !== null ? { attested: true } : {},
25055
25270
  body
25056
25271
  })
25057
25272
  }
@@ -25107,7 +25322,12 @@ ${raw}` : raw;
25107
25322
  if (isMintableKind(kind) && payload.taskId) {
25108
25323
  await offerCardMint(kind, payload, flags, deps2, correlatedSessionId);
25109
25324
  }
25110
- return 0;
25325
+ if (commandExit !== null && commandExit !== 0) {
25326
+ deps2.writeErr(
25327
+ `command exited ${commandExit} \u2014 recorded in the LOG; exiting with the command's code`
25328
+ );
25329
+ }
25330
+ return commandExit ?? 0;
25111
25331
  } catch (error2) {
25112
25332
  if (error2 instanceof UsageError) {
25113
25333
  deps2.writeErr(`error: ${error2.message}`);
@@ -25421,6 +25641,9 @@ function registerPushCommand(program3, deps2, onExit2) {
25421
25641
  ).option(
25422
25642
  "--blocks",
25423
25643
  "with --yes on a findings/gap/issue push: the minted card BLOCKS the aligned task (acceptance-blocking) instead of RELATES_TO"
25644
+ ).option(
25645
+ "--from-cmd <command>",
25646
+ `log pushes only: run the command locally, capture exit code + a 64 KB tail-biased output tail, and push an ATTESTED LOG opening with both \u2014 then exit with the command's own code ("tests green" claims carry evidence)`
25424
25647
  ).option("--json", "stable JSON output").action(
25425
25648
  async (kind, file, flags) => onExit2(await runPush(kind, file, flags, deps2))
25426
25649
  );
@@ -27775,7 +27998,7 @@ function registerPluginCommand(program3, deps2, onExit2) {
27775
27998
  }
27776
27999
 
27777
28000
  // src/commands/runner.ts
27778
- import { execFile as execFile2, spawn as spawn2 } from "node:child_process";
28001
+ import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
27779
28002
  import { constants, promises as fs } from "node:fs";
27780
28003
  import { homedir } from "node:os";
27781
28004
  import { delimiter, isAbsolute, resolve as resolve3 } from "node:path";
@@ -28209,7 +28432,7 @@ function runRunnerForeground(file, args) {
28209
28432
  }
28210
28433
  const target = execTarget(file, args);
28211
28434
  return new Promise((resolveRun, reject) => {
28212
- const child = spawn2(target.file, target.args, {
28435
+ const child = spawn3(target.file, target.args, {
28213
28436
  stdio: "inherit",
28214
28437
  ...target.options
28215
28438
  });
@@ -29223,7 +29446,11 @@ var sessionDeps = {
29223
29446
  configPath: CONFIG_PATH,
29224
29447
  resolveTarget: () => {
29225
29448
  const target = resolveConfig({ env: process.env, file: configFile() });
29226
- return { token: target.token, url: target.url };
29449
+ return {
29450
+ token: target.token,
29451
+ url: target.url,
29452
+ tokenSource: target.tokenSource
29453
+ };
29227
29454
  },
29228
29455
  ensureInstallationId: () => ensureInstallationId(HOME_CONFIG_PATH),
29229
29456
  connect: async (target) => {
@@ -29246,7 +29473,7 @@ var sessionDeps = {
29246
29473
  planPath
29247
29474
  ]);
29248
29475
  try {
29249
- const child = spawn3(target.file, target.args, {
29476
+ const child = spawn4(target.file, target.args, {
29250
29477
  detached: true,
29251
29478
  stdio: ["ignore", fd, fd],
29252
29479
  ...target.options
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jentrix/cli",
3
- "version": "0.5.20",
3
+ "version": "0.5.21",
4
4
  "description": "Command-line client for the Jentrix MCP surface (jentrix tool <name>, generated noun-verb commands).",
5
5
  "keywords": [
6
6
  "jentrix",
package/surface.json CHANGED
@@ -463,6 +463,14 @@
463
463
  "minLength": 1,
464
464
  "type": "string"
465
465
  },
466
+ "skeleton": {
467
+ "description": "Activity-skeleton mode the CLI declared for this session (--no-skeleton). Default on, independent of capture.",
468
+ "enum": [
469
+ "on",
470
+ "off"
471
+ ],
472
+ "type": "string"
473
+ },
466
474
  "taskId": {
467
475
  "anyOf": [
468
476
  {
@@ -1655,6 +1663,10 @@
1655
1663
  "description": "Close a session with an honest lifecycle outcome (COMPLETED normal end / INTERRUPTED crash / CANCELLED pre-first-turn abort). The server verifies the trace-part manifest against durable artifacts, stores the deterministic RUN_SUMMARY, and sets captureComplete ONLY when every expected part and the summary are acknowledged — lifecycle and capture integrity stay orthogonal. Requires expectedUpdatedAt (CAS).",
1656
1664
  "inputSchema": {
1657
1665
  "properties": {
1666
+ "acknowledgeEvidenceGaps": {
1667
+ "description": "Close even with unmet evidence-floor checks (E1–E3); each unmet check is stamped MISSING into the summary's Review readiness.",
1668
+ "type": "boolean"
1669
+ },
1658
1670
  "captureError": {
1659
1671
  "anyOf": [
1660
1672
  {
@@ -1666,6 +1678,19 @@
1666
1678
  }
1667
1679
  ]
1668
1680
  },
1681
+ "commitCount": {
1682
+ "anyOf": [
1683
+ {
1684
+ "maximum": 9007199254740991,
1685
+ "minimum": 0,
1686
+ "type": "integer"
1687
+ },
1688
+ {
1689
+ "type": "null"
1690
+ }
1691
+ ],
1692
+ "description": "CLI-counted commits in startHead..endHead; absent = not counted (HEAD movement is then the only signal)."
1693
+ },
1669
1694
  "endBranch": {
1670
1695
  "anyOf": [
1671
1696
  {