@jentrix/cli 0.5.15 → 0.5.16
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-align.md +9 -0
- package/codex-plugin/plugins/jentrix/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/plugins/jentrix/skills/jentrix-align/SKILL.md +7 -0
- package/dist/main.js +267 -127
- package/package.json +1 -1
|
@@ -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.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
|
|
@@ -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.
|
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
|
|
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.
|
|
20969
|
+
var CLI_VERSION = "0.5.16";
|
|
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
|
|
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
|
|
23851
|
-
const match = (
|
|
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 (
|
|
23970
|
+
if (resolvedWorkspaceId) {
|
|
23867
23971
|
try {
|
|
23868
|
-
|
|
23869
|
-
|
|
23870
|
-
|
|
23871
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}`;
|
|
@@ -28356,21 +28447,21 @@ async function bootstrapRepository(deps2) {
|
|
|
28356
28447
|
deps2,
|
|
28357
28448
|
" Jentrix aligns sessions by repository identity, so install git (https://git-scm.com/downloads) and re-run."
|
|
28358
28449
|
);
|
|
28359
|
-
return;
|
|
28450
|
+
return false;
|
|
28360
28451
|
}
|
|
28361
28452
|
const inRepo = async () => (await deps2.git(["rev-parse", "--is-inside-work-tree"], here)).code === 0;
|
|
28362
28453
|
if (!await inRepo()) {
|
|
28363
28454
|
const reply = await deps2.readLine(
|
|
28364
28455
|
`Initialize a git repository in ${here} so agent sessions can align here? [y/N] `
|
|
28365
28456
|
);
|
|
28366
|
-
if (!/^y/i.test(reply.trim())) return;
|
|
28457
|
+
if (!/^y/i.test(reply.trim())) return false;
|
|
28367
28458
|
if ((await deps2.git(["init"], here)).code !== 0) {
|
|
28368
28459
|
say(deps2, "note: `git init` failed \u2014 skipped the repository step.");
|
|
28369
|
-
return;
|
|
28460
|
+
return false;
|
|
28370
28461
|
}
|
|
28371
28462
|
}
|
|
28372
28463
|
if ((await deps2.git(["remote", "get-url", "origin"], here)).code === 0)
|
|
28373
|
-
return;
|
|
28464
|
+
return true;
|
|
28374
28465
|
const fallback = here.split(/[/\\]/).filter(Boolean).pop().toLowerCase().replace(/ /g, "-");
|
|
28375
28466
|
say(
|
|
28376
28467
|
deps2,
|
|
@@ -28383,15 +28474,55 @@ async function bootstrapRepository(deps2) {
|
|
|
28383
28474
|
const answer = (await deps2.readLine(
|
|
28384
28475
|
"GitHub repo for origin (owner/name or full URL, blank to skip): "
|
|
28385
28476
|
)).trim();
|
|
28386
|
-
if (!answer) return;
|
|
28477
|
+
if (!answer) return true;
|
|
28387
28478
|
const url2 = /:\/\/|^[^/]+@[^:]+:/.test(answer) ? answer : /^[^/]+\/[^/]+$/.test(answer) ? `https://github.com/${answer}.git` : null;
|
|
28388
28479
|
if (!url2) {
|
|
28389
28480
|
say(deps2, `skipped \u2014 "${answer}" is neither owner/name nor a URL`);
|
|
28390
|
-
return;
|
|
28481
|
+
return true;
|
|
28391
28482
|
}
|
|
28392
28483
|
if ((await deps2.git(["remote", "add", "origin", url2], here)).code === 0) {
|
|
28393
28484
|
say(deps2, `origin \u2192 ${url2}`);
|
|
28394
28485
|
}
|
|
28486
|
+
return true;
|
|
28487
|
+
}
|
|
28488
|
+
function writeCheckoutMcpConfig(deps2, url2) {
|
|
28489
|
+
const path = join5(deps2.cwd(), ".mcp.json");
|
|
28490
|
+
if (!url2) {
|
|
28491
|
+
say(
|
|
28492
|
+
deps2,
|
|
28493
|
+
"note: skipped ./.mcp.json \u2014 no endpoint is known yet (not signed in, no --url); the first `jentrix align` here writes it."
|
|
28494
|
+
);
|
|
28495
|
+
return;
|
|
28496
|
+
}
|
|
28497
|
+
const raw = deps2.readTextFile(path);
|
|
28498
|
+
let existing = null;
|
|
28499
|
+
if (raw !== null) {
|
|
28500
|
+
try {
|
|
28501
|
+
existing = JSON.parse(raw);
|
|
28502
|
+
} catch {
|
|
28503
|
+
say(
|
|
28504
|
+
deps2,
|
|
28505
|
+
`note: ${path} is not valid JSON \u2014 left untouched (fix or remove it, then re-run; align refuses it the same way).`
|
|
28506
|
+
);
|
|
28507
|
+
return;
|
|
28508
|
+
}
|
|
28509
|
+
}
|
|
28510
|
+
const plan = planMcpServerEntry(existing, url2, null);
|
|
28511
|
+
if (plan.action === "unchanged") {
|
|
28512
|
+
say(deps2, `./.mcp.json already names ${url2}.`);
|
|
28513
|
+
return;
|
|
28514
|
+
}
|
|
28515
|
+
deps2.writeTextFile(path, renderMcpConfig(plan.next));
|
|
28516
|
+
say(
|
|
28517
|
+
deps2,
|
|
28518
|
+
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).`
|
|
28519
|
+
);
|
|
28520
|
+
if (plan.removedAuthorization) {
|
|
28521
|
+
say(
|
|
28522
|
+
deps2,
|
|
28523
|
+
" Removed its Authorization header: a header suppresses the client's OAuth discovery. `jentrix align --pat` puts it back for a headless checkout."
|
|
28524
|
+
);
|
|
28525
|
+
}
|
|
28395
28526
|
}
|
|
28396
28527
|
async function runSetupCommand(flags, deps2) {
|
|
28397
28528
|
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
@@ -28425,13 +28556,20 @@ async function runSetupCommand(flags, deps2) {
|
|
|
28425
28556
|
const codex = await deps2.resolveCodex();
|
|
28426
28557
|
if (codex) await registerCodexMcp(deps2, codex, pending);
|
|
28427
28558
|
if (flags.git !== false && deps2.cwd() !== deps2.homeDir()) {
|
|
28559
|
+
let inWorkTree;
|
|
28428
28560
|
if (deps2.isInteractive) {
|
|
28429
|
-
await bootstrapRepository(deps2);
|
|
28430
|
-
} else
|
|
28431
|
-
|
|
28432
|
-
|
|
28433
|
-
|
|
28434
|
-
|
|
28561
|
+
inWorkTree = await bootstrapRepository(deps2);
|
|
28562
|
+
} else {
|
|
28563
|
+
inWorkTree = await deps2.resolveGit() !== null && (await deps2.git(["rev-parse", "--is-inside-work-tree"], deps2.cwd())).code === 0;
|
|
28564
|
+
if (!inWorkTree) {
|
|
28565
|
+
say(
|
|
28566
|
+
deps2,
|
|
28567
|
+
"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."
|
|
28568
|
+
);
|
|
28569
|
+
}
|
|
28570
|
+
}
|
|
28571
|
+
if (inWorkTree) {
|
|
28572
|
+
writeCheckoutMcpConfig(deps2, deps2.signedInUrl() ?? flags.url ?? null);
|
|
28435
28573
|
}
|
|
28436
28574
|
}
|
|
28437
28575
|
say(deps2, "");
|
|
@@ -28517,7 +28655,7 @@ import {
|
|
|
28517
28655
|
statSync as statSync2,
|
|
28518
28656
|
writeFileSync as writeFileSync4
|
|
28519
28657
|
} from "node:fs";
|
|
28520
|
-
import { join as
|
|
28658
|
+
import { join as join6 } from "node:path";
|
|
28521
28659
|
var MAX_SLICE_BYTES = 1e6;
|
|
28522
28660
|
function captureConsented(host, marker) {
|
|
28523
28661
|
if (host && typeof host.captureTrace === "boolean") return host.captureTrace;
|
|
@@ -28552,7 +28690,7 @@ function resolveSnapshotSession(payload, spoolRoot, configPath, repoRoot, readHo
|
|
|
28552
28690
|
return null;
|
|
28553
28691
|
}
|
|
28554
28692
|
function offsetPath(spoolRoot, sessionId) {
|
|
28555
|
-
return
|
|
28693
|
+
return join6(spoolRoot, sessionId, "snapshot.json");
|
|
28556
28694
|
}
|
|
28557
28695
|
function readOffset(spoolRoot, sessionId) {
|
|
28558
28696
|
try {
|
|
@@ -29075,7 +29213,7 @@ var sessionDeps = {
|
|
|
29075
29213
|
closeSync3(fd);
|
|
29076
29214
|
}
|
|
29077
29215
|
},
|
|
29078
|
-
spoolRoot:
|
|
29216
|
+
spoolRoot: join7(homedir2(), ".config", "stacks", "session-spool")
|
|
29079
29217
|
};
|
|
29080
29218
|
var sessionCommand = registerSessionCommand(program2, sessionDeps, onExit);
|
|
29081
29219
|
registerSnapshotCommand(sessionCommand, sessionDeps, onExit);
|
|
@@ -29133,17 +29271,17 @@ registerSetupCommand(
|
|
|
29133
29271
|
};
|
|
29134
29272
|
const root = await ask(["root", "-g"]);
|
|
29135
29273
|
const prefix = await ask(["prefix", "-g"]);
|
|
29136
|
-
const binDir = prefix ? process.platform === "win32" ? prefix :
|
|
29274
|
+
const binDir = prefix ? process.platform === "win32" ? prefix : join7(prefix, "bin") : null;
|
|
29137
29275
|
const dir = (name) => {
|
|
29138
29276
|
if (!root) return null;
|
|
29139
|
-
const candidate =
|
|
29140
|
-
return existsSync6(
|
|
29277
|
+
const candidate = join7(root, "@jentrix", name);
|
|
29278
|
+
return existsSync6(join7(candidate, "package.json")) ? candidate : null;
|
|
29141
29279
|
};
|
|
29142
29280
|
const cliDir = dir("cli");
|
|
29143
29281
|
const versionOf = (packageDir) => {
|
|
29144
29282
|
if (!packageDir) return null;
|
|
29145
29283
|
try {
|
|
29146
|
-
const raw = readFileSync7(
|
|
29284
|
+
const raw = readFileSync7(join7(packageDir, "package.json"), "utf8");
|
|
29147
29285
|
const parsed = JSON.parse(raw);
|
|
29148
29286
|
const value = typeof parsed === "object" && parsed !== null ? parsed.version : void 0;
|
|
29149
29287
|
return typeof value === "string" ? value : null;
|
|
@@ -29178,14 +29316,14 @@ registerSetupCommand(
|
|
|
29178
29316
|
const packageRoot = persistentPluginRoot(
|
|
29179
29317
|
root,
|
|
29180
29318
|
PACKAGE_ROOT,
|
|
29181
|
-
(dir) => isPluginMarketplaceDir(
|
|
29319
|
+
(dir) => isPluginMarketplaceDir(join7(dir, "claude-plugin"))
|
|
29182
29320
|
);
|
|
29183
29321
|
return runPluginInstall(
|
|
29184
29322
|
packageRoot === PACKAGE_ROOT ? pluginDeps : {
|
|
29185
29323
|
...pluginDeps,
|
|
29186
|
-
resolvePluginDir: () =>
|
|
29324
|
+
resolvePluginDir: () => join7(packageRoot, "claude-plugin"),
|
|
29187
29325
|
resolveCodexPluginDir: () => {
|
|
29188
|
-
const dir =
|
|
29326
|
+
const dir = join7(packageRoot, "codex-plugin");
|
|
29189
29327
|
return isCodexPluginMarketplaceDir(dir) ? dir : null;
|
|
29190
29328
|
}
|
|
29191
29329
|
},
|
|
@@ -29202,12 +29340,14 @@ registerSetupCommand(
|
|
|
29202
29340
|
}
|
|
29203
29341
|
},
|
|
29204
29342
|
login: (flags) => runLoginCommand(flags, loginDeps),
|
|
29205
|
-
codexConfigPath: () =>
|
|
29343
|
+
codexConfigPath: () => join7(process.env.CODEX_HOME ?? join7(homedir2(), ".codex"), "config.toml"),
|
|
29206
29344
|
readTextFile: (path) => existsSync6(path) ? readFileSync7(path, "utf8") : null,
|
|
29207
29345
|
appendTextFile: (path, text) => {
|
|
29208
29346
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
29209
29347
|
appendFileSync(path, text);
|
|
29210
29348
|
},
|
|
29349
|
+
// D6 — the checkout's ./.mcp.json (OAuth mode, no credential inside).
|
|
29350
|
+
writeTextFile: (path, text) => writeFileSync5(path, text, { mode: 420 }),
|
|
29211
29351
|
copyFile: (from, to) => copyFileSync(from, to),
|
|
29212
29352
|
now: () => /* @__PURE__ */ new Date(),
|
|
29213
29353
|
cwd: () => process.cwd(),
|