@jentrix/cli 0.5.12 → 0.5.13

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.0"
4
+ "version": "0.5.1"
5
5
  }
@@ -59,7 +59,8 @@ and do NOT answer any question yourself.
59
59
  and move with
60
60
  `jentrix task move --task <taskId> --to-column-id <columnId>`. Use only
61
61
  the columns the board actually has — never invent a status. `/jentrix-end`
62
- moves it to a terminal column when the work is genuinely finished. A
62
+ moves it to the In review column when the work is genuinely finished
63
+ the terminal move is the operator's Accept, never yours. A
63
64
  board that never moves is a board that lies about what is in flight.
64
65
  11. If align wrote `./.mcp.json`, relay that line too: MCP servers load at
65
66
  session start, so the tools in THIS session still point wherever they
@@ -1,12 +1,12 @@
1
1
  {
2
- "description": "M20.1 \u00a715.1 \u2014 lifecycle bridge: hooks append their structured stdin payload (session_id, transcript_path, cwd) to the local session context file via `stacks-runner session-hook`. argv carries only the directory and event name \u2014 never credentials or transcript content. PreCompact/PostCompact additionally run `jentrix session snapshot`, which FOLLOWS THE CAPTURE CONSENT: capture ON preserves the transcript range about to leave the model's context; capture OFF (the MVP default) records the compaction boundary and NO content, because `capture: \"off\"` was disclosed as typed artifacts only, no transcript. Preservation is never distillation \u2014 a hook has no model turn; /jentrix-checkpoint does that. Compaction-path hooks carry an explicit 10s timeout (STA-51): they run inside the operator's /compact, and a slow artifact push must delay it by seconds, not the 60s default.",
2
+ "description": "M20.1 \u00a715.1 \u2014 lifecycle bridge: hooks append their structured stdin payload (session_id, transcript_path, cwd) to the local session context file via `stacks-runner session-hook`. argv carries only the provider and event name (the runner resolves the directory cross-platform \u2014 STA-131: a literal $HOME never expands in Windows hook commands) \u2014 never credentials or transcript content. PreCompact/PostCompact additionally run `jentrix session snapshot`, which FOLLOWS THE CAPTURE CONSENT: capture ON preserves the transcript range about to leave the model's context; capture OFF (the MVP default) records the compaction boundary and NO content, because `capture: \"off\"` was disclosed as typed artifacts only, no transcript. Preservation is never distillation \u2014 a hook has no model turn; /jentrix-checkpoint does that. Compaction-path hooks carry an explicit 10s timeout (STA-51): they run inside the operator's /compact, and a slow artifact push must delay it by seconds, not the 60s default.",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/claude-sessions\" --event SessionStart"
9
+ "command": "stacks-runner session-hook --provider claude --event SessionStart"
10
10
  }
11
11
  ]
12
12
  }
@@ -16,7 +16,7 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/claude-sessions\" --event SessionEnd"
19
+ "command": "stacks-runner session-hook --provider claude --event SessionEnd"
20
20
  }
21
21
  ]
22
22
  }
@@ -26,7 +26,7 @@
26
26
  "hooks": [
27
27
  {
28
28
  "type": "command",
29
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/claude-sessions\" --event PreCompact",
29
+ "command": "stacks-runner session-hook --provider claude --event PreCompact",
30
30
  "timeout": 10
31
31
  },
32
32
  {
@@ -42,7 +42,7 @@
42
42
  "hooks": [
43
43
  {
44
44
  "type": "command",
45
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/claude-sessions\" --event PostCompact",
45
+ "command": "stacks-runner session-hook --provider claude --event PostCompact",
46
46
  "timeout": 10
47
47
  },
48
48
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jentrix",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Connect Codex tasks to Jentrix projects with trusted lifecycle hooks, alignment, checkpoints, and honest session capture.",
5
5
  "author": {
6
6
  "name": "Jentrix",
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event SessionStart"
9
+ "command": "stacks-runner session-hook --provider codex --event SessionStart"
10
10
  }
11
11
  ]
12
12
  }
@@ -16,7 +16,7 @@
16
16
  "hooks": [
17
17
  {
18
18
  "type": "command",
19
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event UserPromptSubmit"
19
+ "command": "stacks-runner session-hook --provider codex --event UserPromptSubmit"
20
20
  }
21
21
  ]
22
22
  }
@@ -26,7 +26,7 @@
26
26
  "hooks": [
27
27
  {
28
28
  "type": "command",
29
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event PostToolUse"
29
+ "command": "stacks-runner session-hook --provider codex --event PostToolUse"
30
30
  }
31
31
  ]
32
32
  }
@@ -36,7 +36,7 @@
36
36
  "hooks": [
37
37
  {
38
38
  "type": "command",
39
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event Stop"
39
+ "command": "stacks-runner session-hook --provider codex --event Stop"
40
40
  }
41
41
  ]
42
42
  }
@@ -46,7 +46,7 @@
46
46
  "hooks": [
47
47
  {
48
48
  "type": "command",
49
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event PreCompact",
49
+ "command": "stacks-runner session-hook --provider codex --event PreCompact",
50
50
  "timeout": 10
51
51
  },
52
52
  {
@@ -62,7 +62,7 @@
62
62
  "hooks": [
63
63
  {
64
64
  "type": "command",
65
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event PostCompact",
65
+ "command": "stacks-runner session-hook --provider codex --event PostCompact",
66
66
  "timeout": 10
67
67
  },
68
68
  {
@@ -78,7 +78,7 @@
78
78
  "hooks": [
79
79
  {
80
80
  "type": "command",
81
- "command": "stacks-runner session-hook --dir \"$HOME/.config/stacks/codex-sessions\" --event SessionEnd"
81
+ "command": "stacks-runner session-hook --provider codex --event SessionEnd"
82
82
  }
83
83
  ]
84
84
  }
package/dist/main.js CHANGED
@@ -20962,7 +20962,7 @@ function refreshAccessToken(input) {
20962
20962
 
20963
20963
  // src/client.ts
20964
20964
  var CLI_NAME = "stacks-cli";
20965
- var CLI_VERSION = "0.5.12";
20965
+ var CLI_VERSION = "0.5.13";
20966
20966
  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).";
20967
20967
  var RELOGIN_MESSAGE = "OAuth session expired and could not be refreshed \u2014 run `jentrix login` to sign in again.";
20968
20968
  function originOf2(url2) {
@@ -21909,6 +21909,30 @@ import {
21909
21909
  writeFileSync as writeFileSync2
21910
21910
  } from "node:fs";
21911
21911
  import { dirname as dirname2, join as join2 } from "node:path";
21912
+
21913
+ // src/retry.ts
21914
+ async function withRateLimitRetry(fn, options) {
21915
+ const { maxRetries, maxWaitSeconds, sleep, now } = options;
21916
+ const start = now();
21917
+ let attempt = 0;
21918
+ for (; ; ) {
21919
+ const result = await fn();
21920
+ const envelope = envelopeOfResult(result);
21921
+ if (envelope?.error.code !== "RATE_LIMITED") return result;
21922
+ const retryAfter = envelope.error.retryAfterSeconds;
21923
+ if (typeof retryAfter !== "number" || !Number.isFinite(retryAfter)) {
21924
+ return result;
21925
+ }
21926
+ if (retryAfter < 0) return result;
21927
+ if (attempt >= maxRetries) return result;
21928
+ const elapsedMs = now() - start;
21929
+ if (elapsedMs + retryAfter * 1e3 > maxWaitSeconds * 1e3) return result;
21930
+ await sleep(retryAfter * 1e3);
21931
+ attempt += 1;
21932
+ }
21933
+ }
21934
+
21935
+ // src/commands/session.ts
21912
21936
  var LEGACY_MARKER_KEY = "__v1__";
21913
21937
  function isMarker(value) {
21914
21938
  return typeof value === "object" && value !== null && typeof value.sessionId === "string";
@@ -22029,9 +22053,28 @@ function isRecord5(value) {
22029
22053
  async function callStructured(caller, name, args) {
22030
22054
  return call(caller, name, args);
22031
22055
  }
22056
+ var ToolCallError = class extends Error {
22057
+ constructor(message, code = null) {
22058
+ super(message);
22059
+ this.code = code;
22060
+ }
22061
+ code;
22062
+ };
22032
22063
  async function call(caller, name, args) {
22033
- const result = await caller.callTool({ name, arguments: args });
22064
+ const result = await withRateLimitRetry(
22065
+ () => caller.callTool({ name, arguments: args }),
22066
+ {
22067
+ maxRetries: 2,
22068
+ maxWaitSeconds: 60,
22069
+ sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
22070
+ now: Date.now
22071
+ }
22072
+ );
22034
22073
  if (isRecord5(result) && result.isError === true) {
22074
+ const envelope = envelopeOfResult(result);
22075
+ if (envelope) {
22076
+ throw new ToolCallError(envelope.error.message, envelope.error.code);
22077
+ }
22035
22078
  let message = "tool call failed";
22036
22079
  if (Array.isArray(result.content)) {
22037
22080
  const text = result.content.find(
@@ -22046,7 +22089,7 @@ async function call(caller, name, args) {
22046
22089
  }
22047
22090
  }
22048
22091
  }
22049
- throw new Error(message);
22092
+ throw new ToolCallError(message);
22050
22093
  }
22051
22094
  return isRecord5(result) && isRecord5(result.structuredContent) ? result.structuredContent : {};
22052
22095
  }
@@ -23573,6 +23616,26 @@ function registerSessionCommand(program3, deps2, onExit2) {
23573
23616
  }
23574
23617
 
23575
23618
  // src/commands/align.ts
23619
+ async function resolveWorkspaceFlag(caller, wanted, pinned) {
23620
+ const listed = await callStructured(caller, "list_workspaces", {});
23621
+ const rows = listed.workspaces ?? [];
23622
+ const match = rows.find(
23623
+ (ws) => ws.id === wanted || ws.slug === wanted.toLowerCase()
23624
+ );
23625
+ if (!match) {
23626
+ throw new UsageError(
23627
+ `--workspace ${wanted} matches none of your workspaces (slugs: ${rows.map((ws) => ws.slug).filter(Boolean).join(", ") || "none visible"})`,
23628
+ EXIT_CODES.NOT_FOUND
23629
+ );
23630
+ }
23631
+ if (pinned && pinned.id !== match.id) {
23632
+ throw new UsageError(
23633
+ `--workspace ${wanted} conflicts with this token's pinned workspace (${pinned.id})`,
23634
+ EXIT_CODES.FORBIDDEN
23635
+ );
23636
+ }
23637
+ return match.id;
23638
+ }
23576
23639
  var MAX_WORK_ITEMS = 15;
23577
23640
  async function projectBoards(caller, projectId) {
23578
23641
  const project = await callStructured(caller, "get_project", { projectId });
@@ -24364,10 +24427,17 @@ async function runAlign(flags, deps2) {
24364
24427
  let pendingCreateProject = null;
24365
24428
  if (flags.newTask) workItem = `__new__:${flags.newTask}`;
24366
24429
  if (flags.newProject && flags.newProject.trim()) {
24367
- const workspaceId = catalog.chosenWorkspaceId ?? catalog.pinnedWorkspace?.id ?? null;
24430
+ let workspaceId = catalog.chosenWorkspaceId ?? catalog.pinnedWorkspace?.id ?? null;
24431
+ if (flags.workspace?.trim()) {
24432
+ workspaceId = await resolveWorkspaceFlag(
24433
+ caller,
24434
+ flags.workspace.trim(),
24435
+ catalog.pinnedWorkspace ?? null
24436
+ );
24437
+ }
24368
24438
  if (!workspaceId) {
24369
24439
  throw new UsageError(
24370
- "--new-project needs a resolved workspace (a workspace-pinned token or a repo-matched candidate) \u2014 none resolved here"
24440
+ "--new-project needs a resolved workspace (a workspace-pinned token, a repo-matched candidate, or --workspace <id-or-slug>) \u2014 none resolved here"
24371
24441
  );
24372
24442
  }
24373
24443
  pendingCreateProject = {
@@ -24694,6 +24764,9 @@ function registerAlignCommand(program3, deps2, onExit2) {
24694
24764
  ).option("--project <id-or-slug>", "project to align to").option(
24695
24765
  "--new-project <name>",
24696
24766
  "create a project in the resolved workspace (seeds a board + repo/board links), then align to it"
24767
+ ).option(
24768
+ "--workspace <id-or-slug>",
24769
+ "workspace for --new-project when neither a pinned token nor a repo match resolves one (fresh checkout, non-interactive)"
24697
24770
  ).option("--task <taskId>", "existing work item to align to").option(
24698
24771
  "--new-task <title>",
24699
24772
  "create a task (existing ops core) and align to it"
@@ -24944,7 +25017,20 @@ async function basisBlock(refs, deps2) {
24944
25017
  });
24945
25018
  const record3 = result.artifact ?? result;
24946
25019
  titles.set(id, String(record3.title ?? ""));
24947
- } catch {
25020
+ } catch (error2) {
25021
+ const code = error2 instanceof ToolCallError ? error2.code : null;
25022
+ if (code === "RATE_LIMITED") {
25023
+ throw new UsageError(
25024
+ `--basis ${id} could not be verified \u2014 the server is rate limiting reads; the ref may be fine. Retry the push shortly.`,
25025
+ EXIT_CODES.RATE_LIMITED
25026
+ );
25027
+ }
25028
+ if (code && code !== "NOT_FOUND" && code !== "FORBIDDEN") {
25029
+ throw new UsageError(
25030
+ `--basis ${id} could not be verified (${code}) \u2014 the ref may be fine. Retry the push.`,
25031
+ EXIT_CODES.INTERNAL
25032
+ );
25033
+ }
24948
25034
  throw new UsageError(
24949
25035
  `--basis ${id} does not resolve to an artifact you can read \u2014 push the context first (\`jentrix push context\`), or pass a URL`,
24950
25036
  EXIT_CODES.NOT_FOUND
@@ -25023,7 +25109,7 @@ async function offerCardMint(kind, payload, flags, deps2, sessionId = null) {
25023
25109
  sessionId
25024
25110
  });
25025
25111
  deps2.writeOut(
25026
- `Minted ${minted.key ?? minted.id} on ${board.name} \xB7 artifact linked \xB7 ${flags.blocks ? "BLOCKS" : "RELATES_TO"} ${payload.taskId}`
25112
+ `Minted ${minted.key ?? minted.id} on ${board.name} \xB7 ${minted.artifactLinked ? "artifact linked" : "artifact NOT linked (see note)"} \xB7 ${flags.blocks ? "BLOCKS" : "RELATES_TO"} ${payload.taskId}`
25027
25113
  );
25028
25114
  } catch (error2) {
25029
25115
  deps2.writeErr(
@@ -25068,7 +25154,7 @@ async function mintCard(deps2, input) {
25068
25154
  idempotencyKey: `mint-${input.artifactId}`
25069
25155
  });
25070
25156
  const issueId = String(created.id);
25071
- await (deps2.fetchImpl ?? fetch)(
25157
+ const linkResponse = await (deps2.fetchImpl ?? fetch)(
25072
25158
  new URL(taskTarget(issueId).path, stacksBaseUrlOf(target.url)),
25073
25159
  {
25074
25160
  method: "POST",
@@ -25080,16 +25166,29 @@ async function mintCard(deps2, input) {
25080
25166
  body: JSON.stringify({ refArtifactId: input.artifactId })
25081
25167
  }
25082
25168
  );
25169
+ const artifactLinked = linkResponse.ok;
25170
+ if (!artifactLinked) {
25171
+ deps2.writeErr(
25172
+ `note: minted the card but linking artifact ${input.artifactId} to it failed (HTTP ${linkResponse.status}) \u2014 link it by hand: jentrix push ${pushKindForNoun(input.noun)} --task ${issueId} --ref ${input.artifactId}`
25173
+ );
25174
+ }
25083
25175
  await callStructured(caller, "add_task_link", {
25084
25176
  fromTaskId: issueId,
25085
25177
  toTaskId: input.anchorTaskId,
25086
25178
  kind: input.blocks ? "BLOCKS" : "RELATES_TO"
25087
25179
  });
25088
- return { id: issueId, key: created.key };
25180
+ return {
25181
+ id: issueId,
25182
+ key: created.key,
25183
+ artifactLinked
25184
+ };
25089
25185
  },
25090
25186
  input.sessionId ? { sessionId: input.sessionId } : void 0
25091
25187
  );
25092
25188
  }
25189
+ function pushKindForNoun(noun) {
25190
+ return noun === "Gap" ? "gap" : noun === "Issue" ? "issue" : "findings";
25191
+ }
25093
25192
  async function runMintIssue(flags, deps2) {
25094
25193
  try {
25095
25194
  let sessionId = null;
@@ -25135,9 +25234,10 @@ async function runMintIssue(flags, deps2) {
25135
25234
  issueKey: minted.key ?? null,
25136
25235
  boardId: board.id,
25137
25236
  artifactId: flags.artifact,
25237
+ artifactLinked: minted.artifactLinked,
25138
25238
  relatedTaskId: flags.fromTask,
25139
25239
  linkKind: flags.blocks ? "BLOCKS" : "RELATES_TO"
25140
- }) : `Minted ${minted.key ?? minted.id} on ${board.name} \xB7 artifact linked \xB7 ${flags.blocks ? "BLOCKS" : "RELATES_TO"} ${flags.fromTask}`
25240
+ }) : `Minted ${minted.key ?? minted.id} on ${board.name} \xB7 ${minted.artifactLinked ? "artifact linked" : "artifact NOT linked (see note)"} \xB7 ${flags.blocks ? "BLOCKS" : "RELATES_TO"} ${flags.fromTask}`
25141
25241
  );
25142
25242
  return 0;
25143
25243
  } catch (error2) {
@@ -26020,28 +26120,6 @@ function cellText(value) {
26020
26120
  return text;
26021
26121
  }
26022
26122
 
26023
- // src/retry.ts
26024
- async function withRateLimitRetry(fn, options) {
26025
- const { maxRetries, maxWaitSeconds, sleep, now } = options;
26026
- const start = now();
26027
- let attempt = 0;
26028
- for (; ; ) {
26029
- const result = await fn();
26030
- const envelope = envelopeOfResult(result);
26031
- if (envelope?.error.code !== "RATE_LIMITED") return result;
26032
- const retryAfter = envelope.error.retryAfterSeconds;
26033
- if (typeof retryAfter !== "number" || !Number.isFinite(retryAfter)) {
26034
- return result;
26035
- }
26036
- if (retryAfter < 0) return result;
26037
- if (attempt >= maxRetries) return result;
26038
- const elapsedMs = now() - start;
26039
- if (elapsedMs + retryAfter * 1e3 > maxWaitSeconds * 1e3) return result;
26040
- await sleep(retryAfter * 1e3);
26041
- attempt += 1;
26042
- }
26043
- }
26044
-
26045
26123
  // src/call.ts
26046
26124
  async function callTool(client, name, args, options) {
26047
26125
  let result;
@@ -27577,7 +27655,10 @@ function renderOperatorIdentity(operator, workspaceName) {
27577
27655
  }
27578
27656
  async function executable(path) {
27579
27657
  try {
27580
- await fs.access(path, constants.X_OK);
27658
+ await fs.access(
27659
+ path,
27660
+ process.platform === "win32" ? constants.F_OK : constants.X_OK
27661
+ );
27581
27662
  return await fs.realpath(path);
27582
27663
  } catch {
27583
27664
  return null;
@@ -28302,8 +28383,15 @@ async function runSetupCommand(flags, deps2) {
28302
28383
  }
28303
28384
  const codex = await deps2.resolveCodex();
28304
28385
  if (codex) await registerCodexMcp(deps2, codex, pending);
28305
- if (flags.git !== false && deps2.isInteractive && deps2.cwd() !== deps2.homeDir()) {
28306
- await bootstrapRepository(deps2);
28386
+ if (flags.git !== false && deps2.cwd() !== deps2.homeDir()) {
28387
+ if (deps2.isInteractive) {
28388
+ await bootstrapRepository(deps2);
28389
+ } else if (!await deps2.resolveGit() || (await deps2.git(["rev-parse", "--is-inside-work-tree"], deps2.cwd())).code !== 0) {
28390
+ say(
28391
+ deps2,
28392
+ "note: skipped the repository offer (not an interactive terminal) \u2014 this folder is not a git work tree, and align requires one. Run `git init` here first."
28393
+ );
28394
+ }
28307
28395
  }
28308
28396
  say(deps2, "");
28309
28397
  if (pending.failedPlugins.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jentrix/cli",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
4
4
  "description": "Command-line client for the Jentrix MCP surface (jentrix tool <name>, generated noun-verb commands).",
5
5
  "keywords": [
6
6
  "jentrix",