@jentrix/cli 0.5.15 → 0.5.17

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.1"
4
+ "version": "0.5.2"
5
5
  }
@@ -21,6 +21,15 @@ and do NOT answer any question yourself.
21
21
  3. If `currentAlignment` is non-null: show it to the operator and ask —
22
22
  using the native choice UI (AskUserQuestion) — whether to keep it or
23
23
  re-align. Keep → relay the current snapshot and stop.
24
+ **Fast path.** If the payload's only `ask` question is `confirmation`
25
+ (project / work item / owner were pre-filled from this checkout's previous
26
+ alignment and the confirmation prompt says so — "continuing this
27
+ checkout's previous alignment …"), present that prompt VERBATIM through
28
+ the choice UI. Confirm ⇒ submit with the answered values exactly as the
29
+ payload shows them (`--project <id> --task <id>|--session-level --owner
30
+ <id>`). Re-answer ⇒ re-run `jentrix align --questions --json --fresh` and
31
+ continue with the full flow below, passing `--fresh` on the final submit
32
+ too.
24
33
  4. For each question whose `status` is `"ask"`, present its `choices`
25
34
  verbatim through the native choice UI (AskUserQuestion), preserving the
26
35
  labels. If the question carries a `notice`, relay it to the operator
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jentrix",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: stacks-align
2
+ name: jentrix-align
3
3
  description: Align the current Codex task to a Jentrix workspace, project, work item, and owner with an operator-confirmed server snapshot.
4
4
  ---
5
5
  <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
@@ -10,6 +10,13 @@ description: Align the current Codex task to a Jentrix workspace, project, work
10
10
  2. Relay every failed or warning precondition before asking anything.
11
11
  3. If `currentAlignment` exists, ask whether to keep it or re-align. Keep means
12
12
  show the current snapshot and stop.
13
+ Fast path: if the payload's only `ask` question is `confirmation` (project /
14
+ work item / owner were pre-filled from this checkout's previous alignment and
15
+ the confirmation prompt says so), present that prompt verbatim. Confirm ⇒
16
+ submit with the answered values exactly as the payload shows them
17
+ (`--project <id> --task <id>|--session-level --owner <id>`). Re-answer ⇒
18
+ re-run `jentrix align --provider codex --questions --json --fresh` and
19
+ continue with the full flow below, passing `--fresh` on the final submit too.
13
20
  4. Present each `status:"ask"` question and its choices verbatim using Codex's
14
21
  structured input UI when available, otherwise ask in chat. Never answer for the
15
22
  operator. Preserve notices and the `Other…` free-text escape.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: stacks-checkpoint
2
+ name: jentrix-checkpoint
3
3
  description: Distill verified session state into a typed Jentrix checkpoint that survives compaction or handoff.
4
4
  ---
5
5
  <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: stacks-connect
2
+ name: jentrix-connect
3
3
  description: Connect the current Codex task to a Jentrix project using trusted lifecycle identity and start the session capture host.
4
4
  ---
5
5
  <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: stacks-end
2
+ name: jentrix-end
3
3
  description: End a connected Jentrix session after preserving approved outputs, parking completed work in review, and reporting capture honestly.
4
4
  ---
5
5
  <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: stacks-plan
2
+ name: jentrix-plan
3
3
  description: Turn the opening request into an operator-approved goal and small Jentrix task set, then store the prompt and goal as typed artifacts.
4
4
  ---
5
5
  <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: stacks-review
2
+ name: jentrix-review
3
3
  description: Review a Jentrix session's typed artifacts by category, verify the readiness contract, and post the verdict as a comment on the aligned task.
4
4
  ---
5
5
  <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: stacks-status
2
+ name: jentrix-status
3
3
  description: Show the connected Jentrix session, alignment, capture health, and honest telemetry availability for this Codex task.
4
4
  ---
5
5
  <!-- Source of truth. The cli/{claude,codex}-plugin copy is GENERATED by cli/scripts/build.mjs (rm + cp) on every build — edits made there are deleted. Edit this file. -->
package/dist/main.js CHANGED
@@ -10334,10 +10334,11 @@ import {
10334
10334
  mkdirSync as mkdirSync4,
10335
10335
  openSync as openSync3,
10336
10336
  readFileSync as readFileSync7,
10337
- statSync as statSync3
10337
+ statSync as statSync3,
10338
+ writeFileSync as writeFileSync5
10338
10339
  } from "node:fs";
10339
10340
  import { homedir as homedir2 } from "node:os";
10340
- import { basename as basename2, delimiter as delimiter2, dirname as dirname4, join as join6, resolve as resolve4 } from "node:path";
10341
+ import { basename as basename2, delimiter as delimiter2, dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
10341
10342
  import { fileURLToPath } from "node:url";
10342
10343
 
10343
10344
  // node_modules/.pnpm/commander@14.0.3/node_modules/commander/esm.mjs
@@ -20965,7 +20966,7 @@ function refreshAccessToken(input) {
20965
20966
 
20966
20967
  // src/client.ts
20967
20968
  var CLI_NAME = "stacks-cli";
20968
- var CLI_VERSION = "0.5.15";
20969
+ var CLI_VERSION = "0.5.17";
20969
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).";
20970
20971
  var RELOGIN_MESSAGE = "OAuth session expired and could not be refreshed \u2014 run `jentrix login` to sign in again.";
20971
20972
  function originOf2(url2) {
@@ -21316,6 +21317,15 @@ var PROJECT_NAME_PROMPT = "Name for the new project \u2014 your own words, exact
21316
21317
  function buildAlignDisclosures(catalog) {
21317
21318
  const disclosures = [];
21318
21319
  const repo = catalog.detection.repoOwnerName;
21320
+ const previous = catalog.previous;
21321
+ if (previous && previous.usedFor.length > 0) {
21322
+ const parts = previous.usedFor.map(
21323
+ (kind) => kind === "project" ? `project ${previous.projectName}` : kind === "work_item" ? `task ${previous.taskKey ?? "session-level"}` : `owner ${previous.ownerLabel}`
21324
+ );
21325
+ disclosures.push(
21326
+ `continuing this checkout's previous alignment \u2014 ${parts.join(" \xB7 ")} (choose Re-answer, or run with --fresh, to change)`
21327
+ );
21328
+ }
21319
21329
  if (catalog.chosenProjectId === "__pending__") {
21320
21330
  const name = catalog.newProjectName ?? "the new project";
21321
21331
  disclosures.push(
@@ -21420,7 +21430,10 @@ function buildAlignQuestions(catalog) {
21420
21430
  ...new Map(
21421
21431
  [
21422
21432
  ...catalog.candidates.map((candidate) => candidate.workspace),
21423
- ...catalog.pinnedWorkspace ? [catalog.pinnedWorkspace] : []
21433
+ ...catalog.pinnedWorkspace ? [catalog.pinnedWorkspace] : [],
21434
+ // D4: a repo that matches nothing offers the caller's own workspaces
21435
+ // instead of a lone "Other…" — the same skip/ask rules apply to them.
21436
+ ...catalog.candidates.length === 0 ? catalog.workspaces ?? [] : []
21424
21437
  ].map((ws) => [ws.id, ws])
21425
21438
  ).values()
21426
21439
  ];
@@ -21648,6 +21661,14 @@ function buildAlignQuestions(catalog) {
21648
21661
  };
21649
21662
  } else if (project.status !== "answered" || catalog.members === null) {
21650
21663
  owner = { id: "owner", prompt: "Owner", status: "pending", choices: [] };
21664
+ } else if (catalog.members.length === 1) {
21665
+ owner = {
21666
+ id: "owner",
21667
+ prompt: "Owner",
21668
+ status: "skipped",
21669
+ answer: catalog.members[0].id,
21670
+ choices: []
21671
+ };
21651
21672
  } else {
21652
21673
  owner = {
21653
21674
  id: "owner",
@@ -21666,7 +21687,8 @@ function buildAlignQuestions(catalog) {
21666
21687
  };
21667
21688
  }
21668
21689
  const targetKnown = project.status === "answered" && (projectName.status === "answered" || projectName.status === "skipped");
21669
- const ready = targetKnown && workItem.status === "answered" && (taskTitle.status === "answered" || taskTitle.status === "skipped") && owner.status === "answered";
21690
+ const ready = targetKnown && workItem.status === "answered" && (taskTitle.status === "answered" || taskTitle.status === "skipped") && // D5: a sole-member owner is resolved by detection, like the workspace.
21691
+ (owner.status === "answered" || owner.status === "skipped");
21670
21692
  const disclosures = targetKnown ? buildAlignDisclosures(catalog) : [];
21671
21693
  const confirmation = {
21672
21694
  id: "confirmation",
@@ -23682,6 +23704,78 @@ async function openTasksForProject(caller, boardIds) {
23682
23704
  tasks.sort((a, b) => b.number - a.number);
23683
23705
  return tasks.slice(0, MAX_WORK_ITEMS).map(({ id, key, title }) => ({ id, key, title }));
23684
23706
  }
23707
+ async function workspaceProjects(caller, workspaceId) {
23708
+ const listed = await callStructured(caller, "list_projects", { workspaceId });
23709
+ return (listed.projects ?? []).filter((project) => project.archivedAt === null).map(({ id, name, slug }) => ({ id, name, slug }));
23710
+ }
23711
+ async function workspaceMembers(caller, workspaceId) {
23712
+ const result = await callStructured(caller, "list_members", { workspaceId });
23713
+ return (result.members ?? []).map((member) => ({
23714
+ id: member.userId ?? member.id ?? "",
23715
+ name: member.name,
23716
+ email: member.email
23717
+ }));
23718
+ }
23719
+ async function loadProjectContext(caller, catalog, projectId) {
23720
+ try {
23721
+ const project = await projectBoards(caller, projectId);
23722
+ catalog.repoLinked = repoLinkedOf(catalog, projectId, project.repoOwners);
23723
+ catalog.projectHasBoard = project.boardIds.length > 0;
23724
+ catalog.openTasks = await openTasksForProject(caller, project.boardIds);
23725
+ catalog.members = await workspaceMembers(caller, project.workspaceId);
23726
+ return true;
23727
+ } catch {
23728
+ return false;
23729
+ }
23730
+ }
23731
+ async function previousAlignmentOf(caller, newest, prefetched = null) {
23732
+ try {
23733
+ const session = prefetched ?? await callStructured(caller, "get_agent_session", {
23734
+ sessionId: newest.sessionId
23735
+ });
23736
+ const a = session.alignment;
23737
+ if (!a?.project?.id || !a.owner?.id) return null;
23738
+ return {
23739
+ sessionId: newest.sessionId,
23740
+ workspaceId: a.workspace?.id ?? newest.workspaceId,
23741
+ project: { id: a.project.id, name: a.project.name ?? a.project.id },
23742
+ task: a.task?.id ? { id: a.task.id, key: a.task.key ?? a.task.id } : null,
23743
+ owner: {
23744
+ id: a.owner.id,
23745
+ label: a.owner.name ?? a.owner.email ?? a.owner.id
23746
+ }
23747
+ };
23748
+ } catch {
23749
+ return null;
23750
+ }
23751
+ }
23752
+ function prefillFromPrevious(previous, catalog, projectPrefilled) {
23753
+ const sameProject = projectPrefilled || catalog.chosenProjectId === previous.project.id;
23754
+ if (!sameProject) return null;
23755
+ const usedFor = [];
23756
+ if (projectPrefilled) usedFor.push("project");
23757
+ if (catalog.chosenWorkItem === null && catalog.openTasks !== null) {
23758
+ if (previous.task === null) {
23759
+ catalog.chosenWorkItem = "__none__";
23760
+ usedFor.push("work_item");
23761
+ } else if (catalog.openTasks.some((t) => t.id === previous.task.id)) {
23762
+ catalog.chosenWorkItem = previous.task.id;
23763
+ usedFor.push("work_item");
23764
+ }
23765
+ }
23766
+ if (catalog.chosenOwnerId === null && catalog.members?.some((m) => m.id === previous.owner.id)) {
23767
+ catalog.chosenOwnerId = previous.owner.id;
23768
+ usedFor.push("owner");
23769
+ }
23770
+ if (usedFor.length === 0) return null;
23771
+ return {
23772
+ sessionId: previous.sessionId,
23773
+ projectName: previous.project.name,
23774
+ taskKey: previous.task?.key ?? null,
23775
+ ownerLabel: previous.owner.label,
23776
+ usedFor
23777
+ };
23778
+ }
23685
23779
  async function resolveCandidates(caller, repoOwnerName) {
23686
23780
  try {
23687
23781
  const result = await callStructured(caller, "resolve_projects_for_repo", {
@@ -23847,8 +23941,8 @@ async function buildCatalog(caller, flags, detection) {
23847
23941
  const context = await callStructured(caller, "get_token_context", {});
23848
23942
  let pinnedWorkspace = null;
23849
23943
  if (context.workspacePinned === true && typeof context.workspaceId === "string") {
23850
- const workspaces = await callStructured(caller, "list_workspaces", {});
23851
- const match = (workspaces.workspaces ?? []).find((ws) => ws.id === context.workspaceId);
23944
+ const workspaces2 = await callStructured(caller, "list_workspaces", {});
23945
+ const match = (workspaces2.workspaces ?? []).find((ws) => ws.id === context.workspaceId);
23852
23946
  pinnedWorkspace = {
23853
23947
  id: context.workspaceId,
23854
23948
  name: match?.name ?? "pinned"
@@ -23861,17 +23955,28 @@ async function buildCatalog(caller, flags, detection) {
23861
23955
  ) : null;
23862
23956
  const { candidates: matched, pinned: discoveryPinned } = await resolveCandidates(caller, detection.repoOwnerName);
23863
23957
  const candidates = flagWorkspaceId ? matched.filter((candidate) => candidate.workspace.id === flagWorkspaceId) : matched;
23958
+ let workspaces = null;
23959
+ if (matched.length === 0 && !pinnedWorkspace && !flagWorkspaceId) {
23960
+ const listed = await callStructured(caller, "list_workspaces", {});
23961
+ workspaces = (listed.workspaces ?? []).map(({ id, name, slug }) => ({ id, name, slug }));
23962
+ }
23963
+ const sole = (rows) => rows.length === 1 && !isArchiveNamed(rows[0].name) ? rows[0] : null;
23964
+ const candidateWorkspaces = [
23965
+ ...new Map(candidates.map((c) => [c.workspace.id, c.workspace])).values()
23966
+ ];
23967
+ const resolvedWorkspaceId = flagWorkspaceId ?? pinnedWorkspace?.id ?? sole(workspaces ?? [])?.id ?? sole(candidateWorkspaces)?.id ?? null;
23864
23968
  let otherProjects = [];
23865
23969
  let discoveryNotice = null;
23866
- if (discoveryPinned && pinnedWorkspace) {
23970
+ if (resolvedWorkspaceId) {
23867
23971
  try {
23868
- const listed = await callStructured(caller, "list_projects", {
23869
- workspaceId: pinnedWorkspace.id
23870
- });
23871
- otherProjects = (listed.projects ?? []).filter((project) => project.archivedAt === null).map(({ id, name, slug }) => ({ id, name, slug }));
23872
- discoveryNotice = "repo discovery is unavailable for a workspace-pinned token \u2014 listing every project in the pinned workspace instead (repo match unverified)";
23972
+ otherProjects = await workspaceProjects(caller, resolvedWorkspaceId);
23973
+ if (discoveryPinned) {
23974
+ discoveryNotice = "repo discovery is unavailable for a workspace-pinned token \u2014 listing every project in the pinned workspace instead (repo match unverified)";
23975
+ }
23873
23976
  } catch {
23874
- discoveryNotice = "repo discovery is unavailable for a workspace-pinned token and the workspace project list could not be loaded \u2014 pass --project <id-or-slug>, or --new-project <name> to create one";
23977
+ if (discoveryPinned) {
23978
+ discoveryNotice = "repo discovery is unavailable for a workspace-pinned token and the workspace project list could not be loaded \u2014 pass --project <id-or-slug>, or --new-project <name> to create one";
23979
+ }
23875
23980
  }
23876
23981
  }
23877
23982
  const wantsCreate = Boolean(flags.newProject && flags.newProject.trim()) || flags.project === "__create__";
@@ -23888,68 +23993,28 @@ async function buildCatalog(caller, flags, detection) {
23888
23993
  } else if (candidates.length === 1 && matched.length === 1) {
23889
23994
  chosenProjectId = candidates[0].id;
23890
23995
  }
23891
- let openTasks = null;
23892
- let members = null;
23893
- let repoLinked = null;
23894
- let projectHasBoard = null;
23895
- if (wantsCreate) {
23896
- openTasks = [];
23897
- const workspaceId = pinnedWorkspace?.id ?? null;
23898
- if (workspaceId) {
23899
- try {
23900
- const memberResult = await callStructured(caller, "list_members", {
23901
- workspaceId
23902
- });
23903
- members = memberResult.members?.map((member) => ({
23904
- id: member.userId ?? member.id ?? "",
23905
- name: member.name,
23906
- email: member.email
23907
- })) ?? [];
23908
- } catch {
23909
- members = [];
23910
- }
23911
- } else {
23912
- members = [];
23913
- }
23914
- } else if (chosenProjectId) {
23915
- try {
23916
- const project = await projectBoards(caller, chosenProjectId);
23917
- repoLinked = repoLinkedOf(
23918
- { candidates, detection },
23919
- chosenProjectId,
23920
- project.repoOwners
23921
- );
23922
- projectHasBoard = project.boardIds.length > 0;
23923
- openTasks = await openTasksForProject(caller, project.boardIds);
23924
- const memberResult = await callStructured(caller, "list_members", {
23925
- workspaceId: project.workspaceId
23926
- });
23927
- members = memberResult.members?.map((member) => ({
23928
- id: member.userId ?? member.id ?? "",
23929
- name: member.name,
23930
- email: member.email
23931
- })) ?? null;
23932
- } catch {
23933
- }
23934
- }
23935
23996
  const chosenWorkspaceId = candidates.find((candidate) => candidate.id === chosenProjectId)?.workspace.id ?? flagWorkspaceId ?? pinnedWorkspace?.id ?? null;
23936
- return {
23997
+ const catalog = {
23937
23998
  detection,
23938
23999
  pinnedWorkspace,
23939
24000
  candidates,
23940
24001
  otherProjects,
23941
24002
  discoveryNotice,
24003
+ workspaces,
23942
24004
  chosenWorkspaceId,
23943
24005
  chosenProjectId,
23944
- openTasks,
23945
- members,
23946
- operatorUserId: null,
24006
+ openTasks: null,
24007
+ members: null,
24008
+ // D5 (STA-168): the server now says who the caller is — the owner
24009
+ // question's default, and what `--yes` falls back to. Null from an older
24010
+ // server, which keeps the explicit-owner refusal.
24011
+ operatorUserId: typeof context.userId === "string" ? context.userId : null,
23947
24012
  chosenWorkItem: flags.task ? flags.task : flags.newTask ? "__new__" : flags.sessionLevel ? "__none__" : null,
23948
24013
  chosenOwnerId: flags.owner ?? null,
23949
24014
  newTaskTitle: flags.newTask?.trim() || null,
23950
24015
  newProjectName: flags.newProject?.trim() || null,
23951
- repoLinked,
23952
- projectHasBoard,
24016
+ repoLinked: null,
24017
+ projectHasBoard: null,
23953
24018
  // STA-11: filled by runAlign (it needs the checkout root) — the ordering
23954
24019
  // preference for the workspace question.
23955
24020
  lastAlignedWorkspaceId: null,
@@ -23965,8 +24030,24 @@ async function buildCatalog(caller, flags, detection) {
23965
24030
  binding: null,
23966
24031
  mcpConfig: null,
23967
24032
  // Filled by runAlign — it needs the checkout's marker session.
23968
- priorArtifacts: null
24033
+ priorArtifacts: null,
24034
+ // D7: filled by runAlign (it needs the checkout's newest marker).
24035
+ previous: null
23969
24036
  };
24037
+ if (wantsCreate) {
24038
+ catalog.openTasks = [];
24039
+ catalog.members = [];
24040
+ if (resolvedWorkspaceId) {
24041
+ try {
24042
+ catalog.members = await workspaceMembers(caller, resolvedWorkspaceId);
24043
+ } catch {
24044
+ catalog.members = [];
24045
+ }
24046
+ }
24047
+ } else if (chosenProjectId) {
24048
+ await loadProjectContext(caller, catalog, chosenProjectId);
24049
+ }
24050
+ return catalog;
23970
24051
  }
23971
24052
  async function priorArtifactsOf(caller, markerSession) {
23972
24053
  const current = markerSession?.alignment?.task;
@@ -24323,7 +24404,8 @@ async function runAlign(flags, deps2) {
24323
24404
  flags.capture,
24324
24405
  marker !== null && markerHost !== null && isHostCapturing(deps2, marker.sessionId, markerHost)
24325
24406
  );
24326
- catalog.lastAlignedWorkspaceId = newestAlignmentMarker(deps2.configPath, inspection.root)?.workspaceId ?? null;
24407
+ const newest = newestAlignmentMarker(deps2.configPath, inspection.root);
24408
+ catalog.lastAlignedWorkspaceId = newest?.workspaceId ?? null;
24327
24409
  catalog.binding = {
24328
24410
  providerSessionId: detected.providerSessionId,
24329
24411
  transcriptPath: detected.transcriptPath,
@@ -24349,6 +24431,31 @@ async function runAlign(flags, deps2) {
24349
24431
  }
24350
24432
  }
24351
24433
  const markerActive = markerSession !== null && (markerSession.status === "ACTIVE" || markerSession.status === "STARTING");
24434
+ let workspaceFromPrevious = false;
24435
+ if (!flags.fresh && !markerActive && newest) {
24436
+ const previous = await previousAlignmentOf(
24437
+ caller,
24438
+ newest,
24439
+ // This provider session's own (ended) marker is the newest: reuse
24440
+ // the read above rather than paying for it twice.
24441
+ marker?.sessionId === newest.sessionId ? markerSession : null
24442
+ );
24443
+ if (previous) {
24444
+ const projectPrefilled = catalog.chosenProjectId === null && await loadProjectContext(caller, catalog, previous.project.id);
24445
+ if (projectPrefilled) {
24446
+ catalog.chosenProjectId = previous.project.id;
24447
+ if (catalog.chosenWorkspaceId === null) {
24448
+ catalog.chosenWorkspaceId = previous.workspaceId;
24449
+ workspaceFromPrevious = true;
24450
+ }
24451
+ }
24452
+ catalog.previous = prefillFromPrevious(
24453
+ previous,
24454
+ catalog,
24455
+ projectPrefilled
24456
+ );
24457
+ }
24458
+ }
24352
24459
  catalog.priorArtifacts = await priorArtifactsOf(caller, markerSession);
24353
24460
  const preconditions = await doctorLocalChecks(deps2);
24354
24461
  preconditions.push(sessionBindingCheck(detected));
@@ -24433,7 +24540,7 @@ async function runAlign(flags, deps2) {
24433
24540
  let pendingCreateProject = null;
24434
24541
  if (flags.newTask) workItem = `__new__:${flags.newTask}`;
24435
24542
  if (flags.newProject && flags.newProject.trim()) {
24436
- const workspaceId = catalog.chosenWorkspaceId ?? catalog.pinnedWorkspace?.id ?? null;
24543
+ const workspaceId = buildAlignQuestions(catalog)[0]?.answer ?? null;
24437
24544
  if (!workspaceId) {
24438
24545
  throw new UsageError(
24439
24546
  "--new-project needs a resolved workspace (a workspace-pinned token, a repo-matched candidate, or --workspace <id-or-slug>) \u2014 none resolved here"
@@ -24494,11 +24601,21 @@ async function runAlign(flags, deps2) {
24494
24601
  catalog.newProjectName = null;
24495
24602
  pendingCreateProject = null;
24496
24603
  createWorkspaceId = null;
24604
+ catalog.previous = null;
24605
+ if (workspaceFromPrevious) {
24606
+ catalog.chosenWorkspaceId = null;
24607
+ workspaceFromPrevious = false;
24608
+ }
24497
24609
  continue;
24498
24610
  }
24499
24611
  const answer = await askQuestion(deps2, next);
24500
24612
  if (next.id === "workspace") {
24501
24613
  catalog.chosenWorkspaceId = answer;
24614
+ try {
24615
+ catalog.otherProjects = await workspaceProjects(caller, answer);
24616
+ } catch {
24617
+ catalog.otherProjects = [];
24618
+ }
24502
24619
  } else if (next.id === "task_title") {
24503
24620
  newTaskTitle = answer;
24504
24621
  } else if (next.id === "project_name") {
@@ -24514,7 +24631,7 @@ async function runAlign(flags, deps2) {
24514
24631
  };
24515
24632
  } else if (next.id === "project") {
24516
24633
  if (answer === "__create__") {
24517
- const workspaceId = catalog.chosenWorkspaceId ?? catalog.pinnedWorkspace?.id;
24634
+ const workspaceId = questions[0]?.answer ?? null;
24518
24635
  if (!workspaceId) {
24519
24636
  throw new UsageError(
24520
24637
  "cannot create a project without a resolved workspace \u2014 pick a workspace first"
@@ -24526,16 +24643,7 @@ async function runAlign(flags, deps2) {
24526
24643
  catalog.projectHasBoard = null;
24527
24644
  catalog.openTasks = [];
24528
24645
  try {
24529
- const memberResult = await callStructured(
24530
- caller,
24531
- "list_members",
24532
- { workspaceId }
24533
- );
24534
- catalog.members = memberResult.members?.map((member) => ({
24535
- id: member.userId ?? member.id ?? "",
24536
- name: member.name,
24537
- email: member.email
24538
- })) ?? [];
24646
+ catalog.members = await workspaceMembers(caller, workspaceId);
24539
24647
  } catch {
24540
24648
  catalog.members = [];
24541
24649
  }
@@ -24543,31 +24651,7 @@ async function runAlign(flags, deps2) {
24543
24651
  } else {
24544
24652
  projectId = answer;
24545
24653
  }
24546
- try {
24547
- const project = await projectBoards(caller, projectId);
24548
- catalog.repoLinked = repoLinkedOf(
24549
- catalog,
24550
- projectId,
24551
- project.repoOwners
24552
- );
24553
- catalog.projectHasBoard = project.boardIds.length > 0;
24554
- catalog.openTasks = await openTasksForProject(
24555
- caller,
24556
- project.boardIds
24557
- );
24558
- const memberResult = await callStructured(
24559
- caller,
24560
- "list_members",
24561
- {
24562
- workspaceId: project.workspaceId
24563
- }
24564
- );
24565
- catalog.members = memberResult.members?.map((member) => ({
24566
- id: member.userId ?? member.id ?? "",
24567
- name: member.name,
24568
- email: member.email
24569
- })) ?? [];
24570
- } catch {
24654
+ if (!await loadProjectContext(caller, catalog, projectId)) {
24571
24655
  catalog.openTasks = [];
24572
24656
  catalog.members = [];
24573
24657
  catalog.repoLinked = null;
@@ -24580,10 +24664,11 @@ async function runAlign(flags, deps2) {
24580
24664
  }
24581
24665
  }
24582
24666
  }
24667
+ ownerUserId ??= (catalog.members?.length === 1 ? catalog.members[0].id : null) ?? catalog.operatorUserId;
24583
24668
  if (pendingCreateProject) {
24584
24669
  if (!ownerUserId) {
24585
24670
  throw new UsageError(
24586
- "creating a project needs an owner \u2014 answer the owner question (or pass --owner <userId>)"
24671
+ `creating a project needs an owner \u2014 answer the owner question, or pass --owner <userId> (this server did not identify you; discover members with \`jentrix tool list_members --args '{"workspaceId":"${pendingCreateProject.workspaceId}"}'\`)`
24587
24672
  );
24588
24673
  }
24589
24674
  const created = await callStructured(caller, "create_project", {
@@ -24798,6 +24883,9 @@ function registerAlignCommand(program3, deps2, onExit2) {
24798
24883
  ).option("--json", "stable JSON output").option(
24799
24884
  "--yes",
24800
24885
  "skip the re-align confirmation on an already-aligned session"
24886
+ ).option(
24887
+ "--fresh",
24888
+ "ignore this checkout's previous alignment and ask every question"
24801
24889
  ).action(async (flags) => onExit2(await runAlign(flags, deps2)));
24802
24890
  }
24803
24891
 
@@ -26871,6 +26959,7 @@ function renderHuman(report) {
26871
26959
  lines.push(
26872
26960
  ` workspace: ${tc.workspacePinned ? `pinned to ${tc.workspaceId ?? "?"}` : "not pinned (all your workspaces)"}`
26873
26961
  );
26962
+ if (tc.userId) lines.push(` user: ${tc.userId}`);
26874
26963
  if (tc.displayName) {
26875
26964
  lines.push(
26876
26965
  ` identity: ${tc.emoji ? `${tc.emoji} ` : ""}${tc.displayName}`
@@ -27031,6 +27120,7 @@ function parseTokenContext(rendered) {
27031
27120
  grandfathered: v.grandfathered,
27032
27121
  workspacePinned: v.workspacePinned,
27033
27122
  workspaceId: v.workspaceId ?? null,
27123
+ userId: typeof v.userId === "string" ? v.userId : null,
27034
27124
  displayName: v.displayName ?? null,
27035
27125
  emoji: v.emoji ?? null,
27036
27126
  rateLimit: {
@@ -28134,6 +28224,7 @@ function registerRunnerCommand(program3, deps2, onExit2) {
28134
28224
  }
28135
28225
 
28136
28226
  // src/commands/setup.ts
28227
+ import { join as join5 } from "node:path";
28137
28228
  var NPM_INSTALL_TIMEOUT_MS2 = 3e5;
28138
28229
  var SUPERSEDED = ["@jentrix/stacks-cli", "@jentrix/stacks-runner"];
28139
28230
  var CLI_SPEC = `@jentrix/cli@${CLI_VERSION}`;
@@ -28278,7 +28369,7 @@ async function installRuntimes(deps2, pending) {
28278
28369
  }
28279
28370
  }
28280
28371
  }
28281
- async function registerCodexMcp(deps2, codex, pending) {
28372
+ async function registerCodexMcp(deps2, codex, pending, url2) {
28282
28373
  const configPath = deps2.codexConfigPath();
28283
28374
  const config2 = deps2.readTextFile(configPath);
28284
28375
  const existingKey = config2 ? /^\[mcp_servers\.jentrix\]/m.test(config2) ? "jentrix" : /^\[mcp_servers\.stacks\]/m.test(config2) ? "stacks" : null : null;
@@ -28301,7 +28392,6 @@ async function registerCodexMcp(deps2, codex, pending) {
28301
28392
  );
28302
28393
  return;
28303
28394
  }
28304
- const url2 = deps2.signedInUrl();
28305
28395
  if (!url2) {
28306
28396
  say(
28307
28397
  deps2,
@@ -28356,21 +28446,21 @@ async function bootstrapRepository(deps2) {
28356
28446
  deps2,
28357
28447
  " Jentrix aligns sessions by repository identity, so install git (https://git-scm.com/downloads) and re-run."
28358
28448
  );
28359
- return;
28449
+ return false;
28360
28450
  }
28361
28451
  const inRepo = async () => (await deps2.git(["rev-parse", "--is-inside-work-tree"], here)).code === 0;
28362
28452
  if (!await inRepo()) {
28363
28453
  const reply = await deps2.readLine(
28364
28454
  `Initialize a git repository in ${here} so agent sessions can align here? [y/N] `
28365
28455
  );
28366
- if (!/^y/i.test(reply.trim())) return;
28456
+ if (!/^y/i.test(reply.trim())) return false;
28367
28457
  if ((await deps2.git(["init"], here)).code !== 0) {
28368
28458
  say(deps2, "note: `git init` failed \u2014 skipped the repository step.");
28369
- return;
28459
+ return false;
28370
28460
  }
28371
28461
  }
28372
28462
  if ((await deps2.git(["remote", "get-url", "origin"], here)).code === 0)
28373
- return;
28463
+ return true;
28374
28464
  const fallback = here.split(/[/\\]/).filter(Boolean).pop().toLowerCase().replace(/ /g, "-");
28375
28465
  say(
28376
28466
  deps2,
@@ -28383,15 +28473,55 @@ async function bootstrapRepository(deps2) {
28383
28473
  const answer = (await deps2.readLine(
28384
28474
  "GitHub repo for origin (owner/name or full URL, blank to skip): "
28385
28475
  )).trim();
28386
- if (!answer) return;
28476
+ if (!answer) return true;
28387
28477
  const url2 = /:\/\/|^[^/]+@[^:]+:/.test(answer) ? answer : /^[^/]+\/[^/]+$/.test(answer) ? `https://github.com/${answer}.git` : null;
28388
28478
  if (!url2) {
28389
28479
  say(deps2, `skipped \u2014 "${answer}" is neither owner/name nor a URL`);
28390
- return;
28480
+ return true;
28391
28481
  }
28392
28482
  if ((await deps2.git(["remote", "add", "origin", url2], here)).code === 0) {
28393
28483
  say(deps2, `origin \u2192 ${url2}`);
28394
28484
  }
28485
+ return true;
28486
+ }
28487
+ function writeCheckoutMcpConfig(deps2, url2) {
28488
+ const path = join5(deps2.cwd(), ".mcp.json");
28489
+ if (!url2) {
28490
+ say(
28491
+ deps2,
28492
+ "note: skipped ./.mcp.json \u2014 no endpoint is known yet (not signed in, no --url); the first `jentrix align` here writes it."
28493
+ );
28494
+ return;
28495
+ }
28496
+ const raw = deps2.readTextFile(path);
28497
+ let existing = null;
28498
+ if (raw !== null) {
28499
+ try {
28500
+ existing = JSON.parse(raw);
28501
+ } catch {
28502
+ say(
28503
+ deps2,
28504
+ `note: ${path} is not valid JSON \u2014 left untouched (fix or remove it, then re-run; align refuses it the same way).`
28505
+ );
28506
+ return;
28507
+ }
28508
+ }
28509
+ const plan = planMcpServerEntry(existing, url2, null);
28510
+ if (plan.action === "unchanged") {
28511
+ say(deps2, `./.mcp.json already names ${url2}.`);
28512
+ return;
28513
+ }
28514
+ deps2.writeTextFile(path, renderMcpConfig(plan.next));
28515
+ say(
28516
+ deps2,
28517
+ plan.action === "repoint" ? `Repointed the "jentrix" MCP server in ./.mcp.json: ${plan.previousUrl} \u2192 ${url2} (Claude Code reads it at session start; no credential inside).` : `Wrote ./.mcp.json \u2192 ${url2} (Claude Code reads it at session start; no credential inside).`
28518
+ );
28519
+ if (plan.removedAuthorization) {
28520
+ say(
28521
+ deps2,
28522
+ " Removed its Authorization header: a header suppresses the client's OAuth discovery. `jentrix align --pat` puts it back for a headless checkout."
28523
+ );
28524
+ }
28395
28525
  }
28396
28526
  async function runSetupCommand(flags, deps2) {
28397
28527
  const nodeMajor = Number(process.versions.node.split(".")[0]);
@@ -28401,6 +28531,14 @@ async function runSetupCommand(flags, deps2) {
28401
28531
  );
28402
28532
  return EXIT_CODES.INVALID_INPUT;
28403
28533
  }
28534
+ if (flags.url) {
28535
+ try {
28536
+ new URL(flags.url);
28537
+ } catch {
28538
+ deps2.writeErr(`INVALID_URL: ${JSON.stringify(flags.url)}`);
28539
+ return EXIT_CODES.INVALID_INPUT;
28540
+ }
28541
+ }
28404
28542
  const toolchain = await installToolchain(deps2);
28405
28543
  if (toolchain !== EXIT_CODES.OK) return toolchain;
28406
28544
  const pending = { failedPlugins: [] };
@@ -28422,16 +28560,24 @@ async function runSetupCommand(flags, deps2) {
28422
28560
  );
28423
28561
  }
28424
28562
  }
28563
+ const targetUrl = flags.url ?? deps2.signedInUrl();
28425
28564
  const codex = await deps2.resolveCodex();
28426
- if (codex) await registerCodexMcp(deps2, codex, pending);
28565
+ if (codex) await registerCodexMcp(deps2, codex, pending, targetUrl);
28427
28566
  if (flags.git !== false && deps2.cwd() !== deps2.homeDir()) {
28567
+ let inWorkTree;
28428
28568
  if (deps2.isInteractive) {
28429
- await bootstrapRepository(deps2);
28430
- } else if (!await deps2.resolveGit() || (await deps2.git(["rev-parse", "--is-inside-work-tree"], deps2.cwd())).code !== 0) {
28431
- say(
28432
- deps2,
28433
- "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."
28434
- );
28569
+ inWorkTree = await bootstrapRepository(deps2);
28570
+ } else {
28571
+ inWorkTree = await deps2.resolveGit() !== null && (await deps2.git(["rev-parse", "--is-inside-work-tree"], deps2.cwd())).code === 0;
28572
+ if (!inWorkTree) {
28573
+ say(
28574
+ deps2,
28575
+ "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."
28576
+ );
28577
+ }
28578
+ }
28579
+ if (inWorkTree) {
28580
+ writeCheckoutMcpConfig(deps2, targetUrl);
28435
28581
  }
28436
28582
  }
28437
28583
  say(deps2, "");
@@ -28517,7 +28663,7 @@ import {
28517
28663
  statSync as statSync2,
28518
28664
  writeFileSync as writeFileSync4
28519
28665
  } from "node:fs";
28520
- import { join as join5 } from "node:path";
28666
+ import { join as join6 } from "node:path";
28521
28667
  var MAX_SLICE_BYTES = 1e6;
28522
28668
  function captureConsented(host, marker) {
28523
28669
  if (host && typeof host.captureTrace === "boolean") return host.captureTrace;
@@ -28552,7 +28698,7 @@ function resolveSnapshotSession(payload, spoolRoot, configPath, repoRoot, readHo
28552
28698
  return null;
28553
28699
  }
28554
28700
  function offsetPath(spoolRoot, sessionId) {
28555
- return join5(spoolRoot, sessionId, "snapshot.json");
28701
+ return join6(spoolRoot, sessionId, "snapshot.json");
28556
28702
  }
28557
28703
  function readOffset(spoolRoot, sessionId) {
28558
28704
  try {
@@ -29075,7 +29221,7 @@ var sessionDeps = {
29075
29221
  closeSync3(fd);
29076
29222
  }
29077
29223
  },
29078
- spoolRoot: join6(homedir2(), ".config", "stacks", "session-spool")
29224
+ spoolRoot: join7(homedir2(), ".config", "stacks", "session-spool")
29079
29225
  };
29080
29226
  var sessionCommand = registerSessionCommand(program2, sessionDeps, onExit);
29081
29227
  registerSnapshotCommand(sessionCommand, sessionDeps, onExit);
@@ -29133,17 +29279,17 @@ registerSetupCommand(
29133
29279
  };
29134
29280
  const root = await ask(["root", "-g"]);
29135
29281
  const prefix = await ask(["prefix", "-g"]);
29136
- const binDir = prefix ? process.platform === "win32" ? prefix : join6(prefix, "bin") : null;
29282
+ const binDir = prefix ? process.platform === "win32" ? prefix : join7(prefix, "bin") : null;
29137
29283
  const dir = (name) => {
29138
29284
  if (!root) return null;
29139
- const candidate = join6(root, "@jentrix", name);
29140
- return existsSync6(join6(candidate, "package.json")) ? candidate : null;
29285
+ const candidate = join7(root, "@jentrix", name);
29286
+ return existsSync6(join7(candidate, "package.json")) ? candidate : null;
29141
29287
  };
29142
29288
  const cliDir = dir("cli");
29143
29289
  const versionOf = (packageDir) => {
29144
29290
  if (!packageDir) return null;
29145
29291
  try {
29146
- const raw = readFileSync7(join6(packageDir, "package.json"), "utf8");
29292
+ const raw = readFileSync7(join7(packageDir, "package.json"), "utf8");
29147
29293
  const parsed = JSON.parse(raw);
29148
29294
  const value = typeof parsed === "object" && parsed !== null ? parsed.version : void 0;
29149
29295
  return typeof value === "string" ? value : null;
@@ -29178,14 +29324,14 @@ registerSetupCommand(
29178
29324
  const packageRoot = persistentPluginRoot(
29179
29325
  root,
29180
29326
  PACKAGE_ROOT,
29181
- (dir) => isPluginMarketplaceDir(join6(dir, "claude-plugin"))
29327
+ (dir) => isPluginMarketplaceDir(join7(dir, "claude-plugin"))
29182
29328
  );
29183
29329
  return runPluginInstall(
29184
29330
  packageRoot === PACKAGE_ROOT ? pluginDeps : {
29185
29331
  ...pluginDeps,
29186
- resolvePluginDir: () => join6(packageRoot, "claude-plugin"),
29332
+ resolvePluginDir: () => join7(packageRoot, "claude-plugin"),
29187
29333
  resolveCodexPluginDir: () => {
29188
- const dir = join6(packageRoot, "codex-plugin");
29334
+ const dir = join7(packageRoot, "codex-plugin");
29189
29335
  return isCodexPluginMarketplaceDir(dir) ? dir : null;
29190
29336
  }
29191
29337
  },
@@ -29202,12 +29348,14 @@ registerSetupCommand(
29202
29348
  }
29203
29349
  },
29204
29350
  login: (flags) => runLoginCommand(flags, loginDeps),
29205
- codexConfigPath: () => join6(process.env.CODEX_HOME ?? join6(homedir2(), ".codex"), "config.toml"),
29351
+ codexConfigPath: () => join7(process.env.CODEX_HOME ?? join7(homedir2(), ".codex"), "config.toml"),
29206
29352
  readTextFile: (path) => existsSync6(path) ? readFileSync7(path, "utf8") : null,
29207
29353
  appendTextFile: (path, text) => {
29208
29354
  mkdirSync4(dirname4(path), { recursive: true });
29209
29355
  appendFileSync(path, text);
29210
29356
  },
29357
+ // D6 — the checkout's ./.mcp.json (OAuth mode, no credential inside).
29358
+ writeTextFile: (path, text) => writeFileSync5(path, text, { mode: 420 }),
29211
29359
  copyFile: (from, to) => copyFileSync(from, to),
29212
29360
  now: () => /* @__PURE__ */ new Date(),
29213
29361
  cwd: () => process.cwd(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jentrix/cli",
3
- "version": "0.5.15",
3
+ "version": "0.5.17",
4
4
  "description": "Command-line client for the Jentrix MCP surface (jentrix tool <name>, generated noun-verb commands).",
5
5
  "keywords": [
6
6
  "jentrix",