@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.
- 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 +244 -17
- 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.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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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
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
|
{
|