@jentrix/cli 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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/commands/jentrix-checkpoint.md +9 -0
- package/claude-plugin/commands/jentrix-end.md +14 -0
- package/codex-plugin/plugins/jentrix/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/plugins/jentrix/skills/jentrix-checkpoint/SKILL.md +7 -0
- package/codex-plugin/plugins/jentrix/skills/jentrix-end/SKILL.md +13 -0
- package/dist/main.js +309 -30
- package/package.json +1 -1
- package/surface.json +25 -0
|
@@ -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.
|
|
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.
|
|
@@ -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
|
|
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.
|
|
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) {
|
|
@@ -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
|
|
@@ -21387,6 +21388,9 @@ function buildAlignDisclosures(catalog) {
|
|
|
21387
21388
|
disclosures.push(
|
|
21388
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"
|
|
21389
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
|
+
);
|
|
21390
21394
|
disclosures.push("write the local alignment marker for this checkout");
|
|
21391
21395
|
const mcp = catalog.mcpConfig;
|
|
21392
21396
|
if (mcp && mcp.action !== "unchanged") {
|
|
@@ -22449,20 +22453,39 @@ function readLiveHostMarker(deps2, sessionId) {
|
|
|
22449
22453
|
return null;
|
|
22450
22454
|
}
|
|
22451
22455
|
}
|
|
22452
|
-
async function
|
|
22456
|
+
async function waitForHostEnd(deps2, sessionId, pid, timeoutMs) {
|
|
22453
22457
|
const sleep = deps2.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
22454
|
-
const
|
|
22458
|
+
const dir = join2(deps2.spoolRoot, sessionId);
|
|
22459
|
+
const path = join2(dir, "host.json");
|
|
22455
22460
|
const deadline = Date.now() + timeoutMs;
|
|
22456
22461
|
while (Date.now() < deadline) {
|
|
22457
22462
|
await sleep(500);
|
|
22463
|
+
const refusal = readEndRefusal(deps2, sessionId);
|
|
22464
|
+
if (refusal) return { kind: "refused", message: refusal };
|
|
22458
22465
|
try {
|
|
22459
22466
|
const marker = JSON.parse(readFileSync2(path, "utf8"));
|
|
22460
|
-
if (marker.exitedAt) return
|
|
22467
|
+
if (marker.exitedAt) return { kind: "exited" };
|
|
22461
22468
|
} catch {
|
|
22462
22469
|
}
|
|
22463
|
-
if (!(deps2.isPidAlive ?? defaultIsPidAlive)(pid)) return
|
|
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 {
|
|
22464
22488
|
}
|
|
22465
|
-
return false;
|
|
22466
22489
|
}
|
|
22467
22490
|
function localCaptureLines(deps2, sessionId, serverStatus) {
|
|
22468
22491
|
const dir = join2(deps2.spoolRoot, sessionId);
|
|
@@ -22584,8 +22607,9 @@ async function runSessionStart(provider, flags, deps2) {
|
|
|
22584
22607
|
// outlives the CLI holding a dead credential.
|
|
22585
22608
|
bearer: deps2.resolveTarget().token,
|
|
22586
22609
|
// …and the config file itself, so the host FOLLOWS later rotations
|
|
22587
|
-
// instead of dying with a revoked snapshot (2026-08-08 finding)
|
|
22588
|
-
|
|
22610
|
+
// instead of dying with a revoked snapshot (2026-08-08 finding) —
|
|
22611
|
+
// but ONLY when the token IS the config's (hostConfigPathOf).
|
|
22612
|
+
configPath: hostConfigPathOf(deps2),
|
|
22589
22613
|
repoRoot: inspection.root,
|
|
22590
22614
|
installationId,
|
|
22591
22615
|
mode: "launch",
|
|
@@ -22944,8 +22968,9 @@ async function runSessionAttach(flags, deps2) {
|
|
|
22944
22968
|
// outlives the CLI holding a dead credential.
|
|
22945
22969
|
bearer: deps2.resolveTarget().token,
|
|
22946
22970
|
// …and the config file itself, so the host FOLLOWS later rotations
|
|
22947
|
-
// instead of dying with a revoked snapshot (2026-08-08 finding)
|
|
22948
|
-
|
|
22971
|
+
// instead of dying with a revoked snapshot (2026-08-08 finding) —
|
|
22972
|
+
// but ONLY when the token IS the config's (hostConfigPathOf).
|
|
22973
|
+
configPath: hostConfigPathOf(deps2),
|
|
22949
22974
|
repoRoot: inspection.root,
|
|
22950
22975
|
installationId,
|
|
22951
22976
|
mode: "watch",
|
|
@@ -23026,6 +23051,8 @@ async function runSessionStatus(sessionId, flags, deps2) {
|
|
|
23026
23051
|
`Project: ${String(session.projectName)} (${String(session.projectId)})`,
|
|
23027
23052
|
`Repository: ${String(session.repoOwnerName)}`,
|
|
23028
23053
|
`Capture: ${capture}`,
|
|
23054
|
+
// Evidence floor (D3): the skeleton mode the alignment declared.
|
|
23055
|
+
`Skeleton: ${session.alignment?.skeleton === "off" ? "off (--no-skeleton at align)" : "on (content-free activity counts/timing)"}`,
|
|
23029
23056
|
...local,
|
|
23030
23057
|
telemetry.line,
|
|
23031
23058
|
`Summary artifact: ${session.summaryArtifactId ? String(session.summaryArtifactId) : "\u2014"}`
|
|
@@ -23122,6 +23149,85 @@ function telemetryVerdict(sessionId, usage, hostRan, provider = "claude") {
|
|
|
23122
23149
|
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
23150
|
};
|
|
23124
23151
|
}
|
|
23152
|
+
function hostConfigPathOf(deps2) {
|
|
23153
|
+
const source = deps2.resolveTarget().tokenSource;
|
|
23154
|
+
return source === void 0 || source === "file" ? deps2.configPath : null;
|
|
23155
|
+
}
|
|
23156
|
+
var MAX_ATTESTED_DIFF_BYTES = 256 * 1024;
|
|
23157
|
+
async function buildAttestedDiffBody(git, root, startHead, endHead, dirty) {
|
|
23158
|
+
const counted = await git(
|
|
23159
|
+
["rev-list", "--count", `${startHead}..${endHead}`],
|
|
23160
|
+
root
|
|
23161
|
+
);
|
|
23162
|
+
if (counted.code !== 0) return null;
|
|
23163
|
+
const commitCount = Number(counted.stdout.trim());
|
|
23164
|
+
if (!Number.isFinite(commitCount) || commitCount === 0) return null;
|
|
23165
|
+
const header = [
|
|
23166
|
+
"Attested delivery \u2014 generated by `jentrix session end` from the aligned checkout (source: cli).",
|
|
23167
|
+
`Range: ${startHead}..${endHead} \xB7 ${commitCount} commit(s)`,
|
|
23168
|
+
""
|
|
23169
|
+
].join("\n");
|
|
23170
|
+
const cap = MAX_ATTESTED_DIFF_BYTES - Buffer.byteLength(header, "utf8");
|
|
23171
|
+
const full = await git(
|
|
23172
|
+
["log", "--patch", "--stat", `${startHead}..${endHead}`],
|
|
23173
|
+
root
|
|
23174
|
+
);
|
|
23175
|
+
let body;
|
|
23176
|
+
if (full.code === 0 && Buffer.byteLength(full.stdout, "utf8") <= cap) {
|
|
23177
|
+
body = full.stdout;
|
|
23178
|
+
} else {
|
|
23179
|
+
const stat = await git(["log", "--stat", `${startHead}..${endHead}`], root);
|
|
23180
|
+
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";
|
|
23181
|
+
let kept = stat.code === 0 ? stat.stdout : "(git log --stat failed)";
|
|
23182
|
+
const room = cap - Buffer.byteLength(notice, "utf8");
|
|
23183
|
+
if (Buffer.byteLength(kept, "utf8") > room) {
|
|
23184
|
+
kept = Buffer.from(kept, "utf8").subarray(0, Math.max(0, room - 64)).toString("utf8").replace(/�+$/, "") + "\n[stat tail truncated at the cap]";
|
|
23185
|
+
}
|
|
23186
|
+
body = notice + kept;
|
|
23187
|
+
}
|
|
23188
|
+
let uncommitted = null;
|
|
23189
|
+
if (dirty) {
|
|
23190
|
+
const delta = await git(["diff", "--stat", "HEAD"], root);
|
|
23191
|
+
uncommitted = delta.code === 0 && delta.stdout.trim() ? delta.stdout.trim() : null;
|
|
23192
|
+
}
|
|
23193
|
+
return { body: header + body, commitCount, uncommitted };
|
|
23194
|
+
}
|
|
23195
|
+
async function postAttestedArtifact(deps2, sessionId, input) {
|
|
23196
|
+
const credentials = deps2.resolveTarget();
|
|
23197
|
+
try {
|
|
23198
|
+
const response = await (deps2.fetchImpl ?? fetch)(
|
|
23199
|
+
new URL(
|
|
23200
|
+
`/api/agent-sessions/${sessionId}/artifacts`,
|
|
23201
|
+
stacksBaseUrlOf(credentials.url)
|
|
23202
|
+
),
|
|
23203
|
+
{
|
|
23204
|
+
method: "POST",
|
|
23205
|
+
headers: {
|
|
23206
|
+
authorization: `Bearer ${credentials.token}`,
|
|
23207
|
+
"content-type": "application/json"
|
|
23208
|
+
},
|
|
23209
|
+
body: JSON.stringify({
|
|
23210
|
+
kind: input.kind,
|
|
23211
|
+
title: input.title,
|
|
23212
|
+
body: redactLocal(input.body, deps2.env),
|
|
23213
|
+
attested: true
|
|
23214
|
+
})
|
|
23215
|
+
}
|
|
23216
|
+
);
|
|
23217
|
+
const payload = await response.json().catch(() => ({}));
|
|
23218
|
+
return response.ok ? { ok: true, artifactId: payload.artifactId ?? null, detail: null } : {
|
|
23219
|
+
ok: false,
|
|
23220
|
+
artifactId: null,
|
|
23221
|
+
detail: payload.detail ?? payload.error ?? `HTTP ${response.status}`
|
|
23222
|
+
};
|
|
23223
|
+
} catch (error2) {
|
|
23224
|
+
return {
|
|
23225
|
+
ok: false,
|
|
23226
|
+
artifactId: null,
|
|
23227
|
+
detail: error2 instanceof Error ? error2.message : "network failure"
|
|
23228
|
+
};
|
|
23229
|
+
}
|
|
23230
|
+
}
|
|
23125
23231
|
async function runSessionEnd(sessionIdFlag, flags, deps2) {
|
|
23126
23232
|
try {
|
|
23127
23233
|
const inspection = await inspectRepository(deps2.cwd(), deps2.git);
|
|
@@ -23188,6 +23294,45 @@ async function runSessionEnd(sessionIdFlag, flags, deps2) {
|
|
|
23188
23294
|
);
|
|
23189
23295
|
}
|
|
23190
23296
|
const sessionId = resolved;
|
|
23297
|
+
let commitCount = null;
|
|
23298
|
+
try {
|
|
23299
|
+
const row = await call(caller, "get_agent_session", { sessionId });
|
|
23300
|
+
const startHead = typeof row.startHead === "string" ? row.startHead : null;
|
|
23301
|
+
const attestRepo = inspection && inspection.repoOwnerName === row.repoOwnerName ? inspection : null;
|
|
23302
|
+
if (attestRepo?.head && startHead && attestRepo.head !== startHead && (row.status === "ACTIVE" || row.status === "STARTING")) {
|
|
23303
|
+
const built = await buildAttestedDiffBody(
|
|
23304
|
+
deps2.git ?? defaultGitRunner,
|
|
23305
|
+
attestRepo.root,
|
|
23306
|
+
startHead,
|
|
23307
|
+
attestRepo.head,
|
|
23308
|
+
attestRepo.dirty
|
|
23309
|
+
);
|
|
23310
|
+
if (built) {
|
|
23311
|
+
commitCount = built.commitCount;
|
|
23312
|
+
const pushed = await postAttestedArtifact(deps2, sessionId, {
|
|
23313
|
+
kind: "diff",
|
|
23314
|
+
title: `Attested diff ${startHead.slice(0, 10)}..${attestRepo.head.slice(0, 10)} (${built.commitCount} commit${built.commitCount === 1 ? "" : "s"})`,
|
|
23315
|
+
body: built.body
|
|
23316
|
+
});
|
|
23317
|
+
if (pushed.ok) {
|
|
23318
|
+
deps2.writeOut(
|
|
23319
|
+
`Attested diff pushed \u2192 artifact ${pushed.artifactId ?? "(unknown)"} (${built.commitCount} commit(s), source: cli)`
|
|
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
|
+
}
|
|
23327
|
+
} else {
|
|
23328
|
+
deps2.writeErr(
|
|
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.`
|
|
23330
|
+
);
|
|
23331
|
+
}
|
|
23332
|
+
}
|
|
23333
|
+
}
|
|
23334
|
+
} catch {
|
|
23335
|
+
}
|
|
23191
23336
|
const hostRan = hasLocalCaptureFootprint(deps2, sessionId);
|
|
23192
23337
|
const reportTelemetry = (row) => {
|
|
23193
23338
|
const verdict2 = telemetryVerdict(
|
|
@@ -23201,21 +23346,32 @@ async function runSessionEnd(sessionIdFlag, flags, deps2) {
|
|
|
23201
23346
|
};
|
|
23202
23347
|
const live = readLiveHostMarker(deps2, sessionId);
|
|
23203
23348
|
if (live) {
|
|
23349
|
+
clearEndRefusal(deps2, sessionId);
|
|
23204
23350
|
writeFileSync2(
|
|
23205
23351
|
join2(deps2.spoolRoot, sessionId, "end-request.json"),
|
|
23206
|
-
JSON.stringify({
|
|
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
|
+
}),
|
|
23207
23362
|
{ mode: 384 }
|
|
23208
23363
|
);
|
|
23209
23364
|
deps2.writeOut(
|
|
23210
23365
|
`Local capture host (pid ${live.pid}) is finalizing capture\u2026`
|
|
23211
23366
|
);
|
|
23212
|
-
const
|
|
23213
|
-
|
|
23214
|
-
|
|
23215
|
-
|
|
23216
|
-
|
|
23217
|
-
|
|
23218
|
-
|
|
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") {
|
|
23219
23375
|
const final = await call(caller, "get_agent_session", { sessionId });
|
|
23220
23376
|
const stillOpen = final.status === "STARTING" || final.status === "ACTIVE";
|
|
23221
23377
|
if (!stillOpen) {
|
|
@@ -23273,6 +23429,11 @@ async function runSessionEnd(sessionIdFlag, flags, deps2) {
|
|
|
23273
23429
|
endHead: sameRepo?.head ?? null,
|
|
23274
23430
|
endDirty: sameRepo?.dirty ?? null,
|
|
23275
23431
|
...spooledUsage ? { usage: spooledUsage } : {},
|
|
23432
|
+
// Evidence floor (§5/§6): the CLI-counted commits in the range feed
|
|
23433
|
+
// E2 and the summary's density line; the acknowledge flag closes over
|
|
23434
|
+
// unmet checks with each stamped MISSING into Review readiness.
|
|
23435
|
+
...commitCount !== null ? { commitCount } : {},
|
|
23436
|
+
...flags.acknowledgeEvidenceGaps ? { acknowledgeEvidenceGaps: true } : {},
|
|
23276
23437
|
expectedUpdatedAt: session.updatedAt
|
|
23277
23438
|
});
|
|
23278
23439
|
if (inspection) {
|
|
@@ -23606,6 +23767,12 @@ function reportError(error2, deps2) {
|
|
|
23606
23767
|
}
|
|
23607
23768
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
23608
23769
|
deps2.writeErr(`error: ${message}`);
|
|
23770
|
+
if (message.includes("EVIDENCE_FLOOR")) {
|
|
23771
|
+
deps2.writeErr(
|
|
23772
|
+
"To close anyway with the unmet checks stamped MISSING: `jentrix session end --acknowledge-evidence-gaps` (a declared deviation is an ordinary `jentrix push gap`)."
|
|
23773
|
+
);
|
|
23774
|
+
return EXIT_CODES.CONFLICT;
|
|
23775
|
+
}
|
|
23609
23776
|
if (message.includes("SESSION_ALREADY_BOUND")) return EXIT_CODES.CONFLICT;
|
|
23610
23777
|
if (message.includes("FORBIDDEN") || message.includes("PROJECT_DISCOVERY_REQUIRES_UNPINNED_LOGIN")) {
|
|
23611
23778
|
return EXIT_CODES.FORBIDDEN;
|
|
@@ -23661,7 +23828,12 @@ function registerSessionCommand(program3, deps2, onExit2) {
|
|
|
23661
23828
|
).option("--json", "stable JSON output").action(
|
|
23662
23829
|
async (sessionId, flags) => onExit2(await runSessionStatus(sessionId, flags, deps2))
|
|
23663
23830
|
);
|
|
23664
|
-
session.command("end [sessionId]").description(
|
|
23831
|
+
session.command("end [sessionId]").description(
|
|
23832
|
+
"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)."
|
|
23833
|
+
).option("--json", "stable JSON output").option(
|
|
23834
|
+
"--acknowledge-evidence-gaps",
|
|
23835
|
+
"close even with unmet evidence-floor checks \u2014 each unmet check is stamped MISSING into the summary's Review readiness"
|
|
23836
|
+
).action(
|
|
23665
23837
|
async (sessionId, flags) => onExit2(await runSessionEnd(sessionId, flags, deps2))
|
|
23666
23838
|
);
|
|
23667
23839
|
return session;
|
|
@@ -24050,6 +24222,9 @@ async function buildCatalog(caller, flags, detection) {
|
|
|
24050
24222
|
// checkout root); defaults here are the no-live-host reading.
|
|
24051
24223
|
liveHostPid: null,
|
|
24052
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",
|
|
24053
24228
|
producer: producerFromFlags(flags),
|
|
24054
24229
|
tokenBudget: budgetFromFlags(flags),
|
|
24055
24230
|
preconditionNotices: [],
|
|
@@ -24297,9 +24472,13 @@ ${JSON.stringify(session.alignment, null, 2)}`
|
|
|
24297
24472
|
} : {},
|
|
24298
24473
|
...budget !== null ? { tokenBudget: budget } : {},
|
|
24299
24474
|
capture: captureMode,
|
|
24475
|
+
// Evidence floor (D3): only an explicit --no-skeleton is sent; absent
|
|
24476
|
+
// lets the server keep the stored mode (default "on").
|
|
24477
|
+
...flags.skeleton === false ? { skeleton: "off" } : {},
|
|
24300
24478
|
expectedUpdatedAt: session.updatedAt
|
|
24301
24479
|
});
|
|
24302
24480
|
const alignment = aligned.alignment;
|
|
24481
|
+
const skeletonMode = alignment.skeleton === "off" ? "off" : "on";
|
|
24303
24482
|
writeAlignmentMarker(
|
|
24304
24483
|
deps2.configPath,
|
|
24305
24484
|
inspection.root,
|
|
@@ -24347,8 +24526,9 @@ ${JSON.stringify(session.alignment, null, 2)}`
|
|
|
24347
24526
|
mcpUrl: target.url,
|
|
24348
24527
|
bearer: deps2.resolveTarget().token,
|
|
24349
24528
|
// …and the config file itself, so the host FOLLOWS later rotations
|
|
24350
|
-
// instead of dying with a revoked snapshot (2026-08-08 finding)
|
|
24351
|
-
|
|
24529
|
+
// instead of dying with a revoked snapshot (2026-08-08 finding) —
|
|
24530
|
+
// but ONLY when the token IS the config's (hostConfigPathOf).
|
|
24531
|
+
configPath: hostConfigPathOf(deps2),
|
|
24352
24532
|
repoRoot: inspection.root,
|
|
24353
24533
|
installationId,
|
|
24354
24534
|
mode: "watch",
|
|
@@ -24356,6 +24536,7 @@ ${JSON.stringify(session.alignment, null, 2)}`
|
|
|
24356
24536
|
...detected.transcriptPath ? { transcriptPath: detected.transcriptPath } : {},
|
|
24357
24537
|
...detected.hookDir ? { hookDir: detected.hookDir } : {},
|
|
24358
24538
|
captureTrace: captureMode === "on",
|
|
24539
|
+
collectSkeleton: skeletonMode === "on",
|
|
24359
24540
|
spoolRoot: deps2.spoolRoot
|
|
24360
24541
|
});
|
|
24361
24542
|
if (pid !== null) {
|
|
@@ -24830,6 +25011,7 @@ async function runAlign(flags, deps2) {
|
|
|
24830
25011
|
` Agent: ${a.agent?.provider ?? "\u2014"}`,
|
|
24831
25012
|
` Repo: ${a.repo?.ownerName ?? "\u2014"}@${a.repo?.branch ?? "detached"}`,
|
|
24832
25013
|
` Capture: ${a.capture ?? "off"}`,
|
|
25014
|
+
` Skeleton: ${a.skeleton ?? "on"} (content-free activity counts/timing; --no-skeleton opts out)`,
|
|
24833
25015
|
`Alignment snapshot (server-confirmed): ${JSON.stringify(result.alignment)}`
|
|
24834
25016
|
].join("\n")
|
|
24835
25017
|
);
|
|
@@ -24894,6 +25076,9 @@ function registerAlignCommand(program3, deps2, onExit2) {
|
|
|
24894
25076
|
).option(
|
|
24895
25077
|
"--no-capture",
|
|
24896
25078
|
"keep TRACE capture OFF explicitly \u2014 also corrects a wrongly-on record on re-align"
|
|
25079
|
+
).option(
|
|
25080
|
+
"--no-skeleton",
|
|
25081
|
+
"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
25082
|
).addOption(
|
|
24898
25083
|
new Option(
|
|
24899
25084
|
"--provider <provider>",
|
|
@@ -24931,7 +25116,8 @@ var PUSH_KINDS = [
|
|
|
24931
25116
|
"prd",
|
|
24932
25117
|
"gap",
|
|
24933
25118
|
"issue",
|
|
24934
|
-
"context"
|
|
25119
|
+
"context",
|
|
25120
|
+
"log"
|
|
24935
25121
|
];
|
|
24936
25122
|
var ARTIFACT_TYPE_BY_PUSH_KIND = {
|
|
24937
25123
|
plan: "PLAN",
|
|
@@ -24946,8 +25132,23 @@ var ARTIFACT_TYPE_BY_PUSH_KIND = {
|
|
|
24946
25132
|
prd: "PRD",
|
|
24947
25133
|
gap: "GAP",
|
|
24948
25134
|
issue: "ISSUE",
|
|
24949
|
-
context: "SOURCE_DIGEST"
|
|
25135
|
+
context: "SOURCE_DIGEST",
|
|
25136
|
+
log: "LOG"
|
|
24950
25137
|
};
|
|
25138
|
+
var MAX_FROM_CMD_OUTPUT_BYTES = 64 * 1024;
|
|
25139
|
+
function fromCmdLogBody(command, exitCode, output) {
|
|
25140
|
+
const header = `$ ${command}
|
|
25141
|
+
exit code: ${exitCode}
|
|
25142
|
+
|
|
25143
|
+
`;
|
|
25144
|
+
if (Buffer.byteLength(output, "utf8") <= MAX_FROM_CMD_OUTPUT_BYTES) {
|
|
25145
|
+
return header + (output || "(no output)");
|
|
25146
|
+
}
|
|
25147
|
+
const notice = `[head truncated: output exceeded ${MAX_FROM_CMD_OUTPUT_BYTES / 1024} KB \u2014 tail retained]
|
|
25148
|
+
`;
|
|
25149
|
+
const tail = Buffer.from(output, "utf8").subarray(-MAX_FROM_CMD_OUTPUT_BYTES).toString("utf8").replace(/^�+/, "");
|
|
25150
|
+
return header + notice + tail;
|
|
25151
|
+
}
|
|
24951
25152
|
function sessionTarget(sessionId) {
|
|
24952
25153
|
return {
|
|
24953
25154
|
path: `/api/agent-sessions/${sessionId}/artifacts`,
|
|
@@ -24957,6 +25158,32 @@ function sessionTarget(sessionId) {
|
|
|
24957
25158
|
function taskTarget(taskId) {
|
|
24958
25159
|
return { path: `/api/tasks/${taskId}/artifacts`, label: `task ${taskId}` };
|
|
24959
25160
|
}
|
|
25161
|
+
function defaultRunCommand(command) {
|
|
25162
|
+
return new Promise((resolve5) => {
|
|
25163
|
+
const child = spawn2(command, { shell: true, windowsHide: true });
|
|
25164
|
+
let tail = Buffer.alloc(0);
|
|
25165
|
+
const keep = (chunk) => {
|
|
25166
|
+
tail = Buffer.concat([tail, chunk]);
|
|
25167
|
+
if (tail.byteLength > MAX_FROM_CMD_OUTPUT_BYTES * 2) {
|
|
25168
|
+
tail = tail.subarray(-MAX_FROM_CMD_OUTPUT_BYTES);
|
|
25169
|
+
}
|
|
25170
|
+
};
|
|
25171
|
+
child.stdout?.on("data", keep);
|
|
25172
|
+
child.stderr?.on("data", keep);
|
|
25173
|
+
child.once(
|
|
25174
|
+
"error",
|
|
25175
|
+
(error2) => resolve5({ code: 127, output: `${error2.message}
|
|
25176
|
+
` })
|
|
25177
|
+
);
|
|
25178
|
+
child.once(
|
|
25179
|
+
"exit",
|
|
25180
|
+
(code, signal) => resolve5({
|
|
25181
|
+
code: code ?? (signal ? 130 : 1),
|
|
25182
|
+
output: tail.toString("utf8")
|
|
25183
|
+
})
|
|
25184
|
+
);
|
|
25185
|
+
});
|
|
25186
|
+
}
|
|
24960
25187
|
async function drainStdin() {
|
|
24961
25188
|
const chunks = [];
|
|
24962
25189
|
for await (const chunk of process.stdin) {
|
|
@@ -24976,6 +25203,21 @@ async function runPush(kind, file, flags, deps2) {
|
|
|
24976
25203
|
"--basis records what a decision rested on \u2014 it only applies to `jentrix push decision`"
|
|
24977
25204
|
);
|
|
24978
25205
|
}
|
|
25206
|
+
if (flags.fromCmd !== void 0) {
|
|
25207
|
+
if (kind !== "log") {
|
|
25208
|
+
throw new UsageError(
|
|
25209
|
+
'--from-cmd captures a command\'s exit code and output as a LOG \u2014 it only applies to `jentrix push log --from-cmd "<command>"`'
|
|
25210
|
+
);
|
|
25211
|
+
}
|
|
25212
|
+
if (file || flags.ref) {
|
|
25213
|
+
throw new UsageError(
|
|
25214
|
+
"--from-cmd generates the LOG body from the command run \u2014 drop the file/--ref"
|
|
25215
|
+
);
|
|
25216
|
+
}
|
|
25217
|
+
if (!flags.fromCmd.trim()) {
|
|
25218
|
+
throw new UsageError("--from-cmd needs a non-empty command");
|
|
25219
|
+
}
|
|
25220
|
+
}
|
|
24979
25221
|
if (flags.ref) {
|
|
24980
25222
|
if (flags.basis?.length) {
|
|
24981
25223
|
throw new UsageError(
|
|
@@ -25028,7 +25270,17 @@ async function runPush(kind, file, flags, deps2) {
|
|
|
25028
25270
|
correlatedSessionId = marker.sessionId;
|
|
25029
25271
|
fallbackTaskId = marker.taskId;
|
|
25030
25272
|
}
|
|
25031
|
-
|
|
25273
|
+
let commandExit = null;
|
|
25274
|
+
let raw;
|
|
25275
|
+
if (flags.fromCmd !== void 0) {
|
|
25276
|
+
const command = flags.fromCmd.trim();
|
|
25277
|
+
deps2.writeErr(`running: ${command}`);
|
|
25278
|
+
const ran = await (deps2.runCommand ?? defaultRunCommand)(command);
|
|
25279
|
+
commandExit = ran.code;
|
|
25280
|
+
raw = fromCmdLogBody(command, ran.code, ran.output);
|
|
25281
|
+
} else {
|
|
25282
|
+
raw = file ? readFileSync4(file, "utf8") : await (deps2.readStdin ?? drainStdin)();
|
|
25283
|
+
}
|
|
25032
25284
|
if (!raw.trim()) {
|
|
25033
25285
|
throw new UsageError(
|
|
25034
25286
|
file ? `${file} is empty` : "stdin is empty \u2014 pipe the content or name a file"
|
|
@@ -25052,6 +25304,9 @@ ${raw}` : raw;
|
|
|
25052
25304
|
...flags.title ? { title: flags.title } : {},
|
|
25053
25305
|
...flags.agent ? { producerLabel: flags.agent } : {},
|
|
25054
25306
|
...flags.agentEmoji ? { producerEmoji: flags.agentEmoji } : {},
|
|
25307
|
+
// D4: only the CLI-generated capture is attested — never a piped
|
|
25308
|
+
// body, whatever its kind.
|
|
25309
|
+
...commandExit !== null ? { attested: true } : {},
|
|
25055
25310
|
body
|
|
25056
25311
|
})
|
|
25057
25312
|
}
|
|
@@ -25107,7 +25362,12 @@ ${raw}` : raw;
|
|
|
25107
25362
|
if (isMintableKind(kind) && payload.taskId) {
|
|
25108
25363
|
await offerCardMint(kind, payload, flags, deps2, correlatedSessionId);
|
|
25109
25364
|
}
|
|
25110
|
-
|
|
25365
|
+
if (commandExit !== null && commandExit !== 0) {
|
|
25366
|
+
deps2.writeErr(
|
|
25367
|
+
`command exited ${commandExit} \u2014 recorded in the LOG; exiting with the command's code`
|
|
25368
|
+
);
|
|
25369
|
+
}
|
|
25370
|
+
return commandExit ?? 0;
|
|
25111
25371
|
} catch (error2) {
|
|
25112
25372
|
if (error2 instanceof UsageError) {
|
|
25113
25373
|
deps2.writeErr(`error: ${error2.message}`);
|
|
@@ -25421,6 +25681,9 @@ function registerPushCommand(program3, deps2, onExit2) {
|
|
|
25421
25681
|
).option(
|
|
25422
25682
|
"--blocks",
|
|
25423
25683
|
"with --yes on a findings/gap/issue push: the minted card BLOCKS the aligned task (acceptance-blocking) instead of RELATES_TO"
|
|
25684
|
+
).option(
|
|
25685
|
+
"--from-cmd <command>",
|
|
25686
|
+
`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
25687
|
).option("--json", "stable JSON output").action(
|
|
25425
25688
|
async (kind, file, flags) => onExit2(await runPush(kind, file, flags, deps2))
|
|
25426
25689
|
);
|
|
@@ -26351,6 +26614,10 @@ function parseArgsJson(raw, source) {
|
|
|
26351
26614
|
}
|
|
26352
26615
|
return { ok: true, value: parsed };
|
|
26353
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
|
+
}
|
|
26354
26621
|
async function runToolCommand(name, flags, deps2) {
|
|
26355
26622
|
const maxWaitSeconds = Number(flags.maxWait);
|
|
26356
26623
|
if (!Number.isFinite(maxWaitSeconds) || maxWaitSeconds < 0) {
|
|
@@ -26387,6 +26654,14 @@ async function runToolCommand(name, flags, deps2) {
|
|
|
26387
26654
|
deps2.writeErr(`error: ${parsed.error}`);
|
|
26388
26655
|
return EXIT_CODES.INVALID_INPUT;
|
|
26389
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
|
+
}
|
|
26390
26665
|
let config2;
|
|
26391
26666
|
try {
|
|
26392
26667
|
config2 = resolveConfig({
|
|
@@ -27775,7 +28050,7 @@ function registerPluginCommand(program3, deps2, onExit2) {
|
|
|
27775
28050
|
}
|
|
27776
28051
|
|
|
27777
28052
|
// src/commands/runner.ts
|
|
27778
|
-
import { execFile as execFile2, spawn as
|
|
28053
|
+
import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
|
|
27779
28054
|
import { constants, promises as fs } from "node:fs";
|
|
27780
28055
|
import { homedir } from "node:os";
|
|
27781
28056
|
import { delimiter, isAbsolute, resolve as resolve3 } from "node:path";
|
|
@@ -28209,7 +28484,7 @@ function runRunnerForeground(file, args) {
|
|
|
28209
28484
|
}
|
|
28210
28485
|
const target = execTarget(file, args);
|
|
28211
28486
|
return new Promise((resolveRun, reject) => {
|
|
28212
|
-
const child =
|
|
28487
|
+
const child = spawn3(target.file, target.args, {
|
|
28213
28488
|
stdio: "inherit",
|
|
28214
28489
|
...target.options
|
|
28215
28490
|
});
|
|
@@ -29223,7 +29498,11 @@ var sessionDeps = {
|
|
|
29223
29498
|
configPath: CONFIG_PATH,
|
|
29224
29499
|
resolveTarget: () => {
|
|
29225
29500
|
const target = resolveConfig({ env: process.env, file: configFile() });
|
|
29226
|
-
return {
|
|
29501
|
+
return {
|
|
29502
|
+
token: target.token,
|
|
29503
|
+
url: target.url,
|
|
29504
|
+
tokenSource: target.tokenSource
|
|
29505
|
+
};
|
|
29227
29506
|
},
|
|
29228
29507
|
ensureInstallationId: () => ensureInstallationId(HOME_CONFIG_PATH),
|
|
29229
29508
|
connect: async (target) => {
|
|
@@ -29246,7 +29525,7 @@ var sessionDeps = {
|
|
|
29246
29525
|
planPath
|
|
29247
29526
|
]);
|
|
29248
29527
|
try {
|
|
29249
|
-
const child =
|
|
29528
|
+
const child = spawn4(target.file, target.args, {
|
|
29250
29529
|
detached: true,
|
|
29251
29530
|
stdio: ["ignore", fd, fd],
|
|
29252
29531
|
...target.options
|
package/package.json
CHANGED
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
|
{
|