@nurix/nustack 0.10.0-dev.9 → 0.11.0

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.
Files changed (74) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/bootstrap.js +4 -4
  3. package/dist/commands/docs.js +13 -13
  4. package/dist/commands/doctor.js +51 -17
  5. package/dist/commands/handoff.js +8 -8
  6. package/dist/commands/init.js +43 -43
  7. package/dist/commands/login.js +19 -6
  8. package/dist/commands/logout.js +14 -6
  9. package/dist/commands/logscope.js +55 -0
  10. package/dist/commands/{workspace-sync.js → project-sync.js} +5 -5
  11. package/dist/commands/{workspace.js → project.js} +133 -133
  12. package/dist/commands/review.js +38 -15
  13. package/dist/commands/search.js +7 -7
  14. package/dist/commands/sessions.js +156 -5
  15. package/dist/commands/sync.js +10 -10
  16. package/dist/commands/token.js +102 -0
  17. package/dist/commands/toolkit.js +19 -19
  18. package/dist/index.js +88 -36
  19. package/dist/lib/auth_grant.js +32 -0
  20. package/dist/lib/auth_recovery.js +3 -1
  21. package/dist/lib/credential_store.js +106 -0
  22. package/dist/lib/device_identity.js +6 -6
  23. package/dist/lib/device_keychain.js +16 -37
  24. package/dist/lib/etna/profile.js +9 -9
  25. package/dist/lib/etna/reconcile.js +11 -11
  26. package/dist/lib/git-sync/core/engine.js +4 -4
  27. package/dist/lib/git-sync/inbound/apply_commit.js +3 -3
  28. package/dist/lib/git-sync/inbound/approval.js +1 -1
  29. package/dist/lib/git-sync/inbound/download.js +8 -8
  30. package/dist/lib/git-sync/inbound/event_sync.js +13 -13
  31. package/dist/lib/git-sync/inbound/notification.js +1 -1
  32. package/dist/lib/git-sync/outbound/extraction.js +4 -4
  33. package/dist/lib/git-sync/outbound/history_decision.js +2 -2
  34. package/dist/lib/git-sync/outbound/publisher.js +2 -2
  35. package/dist/lib/git-sync/outbound/queue.js +3 -3
  36. package/dist/lib/git-sync/outbound/upload_client.js +4 -4
  37. package/dist/lib/git-sync/whitelist/index.js +1 -1
  38. package/dist/lib/git-sync/whitelist/types.js +1 -1
  39. package/dist/lib/git-sync/whitelist/validation.js +5 -5
  40. package/dist/lib/libsecret_store.js +47 -0
  41. package/dist/lib/local_store/accepted_uploads.js +5 -5
  42. package/dist/lib/local_store/bindings.js +11 -11
  43. package/dist/lib/local_store/index.js +1 -1
  44. package/dist/lib/local_store/migrations.js +13 -0
  45. package/dist/lib/local_store/project_events_cache.js +54 -0
  46. package/dist/lib/logs/answer.js +99 -0
  47. package/dist/lib/logs/library.js +19 -0
  48. package/dist/lib/logs/lifecycle.js +159 -0
  49. package/dist/lib/nustack_client.js +88 -46
  50. package/dist/lib/onboarding/attach_existing_git.js +7 -7
  51. package/dist/lib/onboarding/binding.js +11 -11
  52. package/dist/lib/onboarding/folder_state.js +10 -10
  53. package/dist/lib/onboarding/handoff_apply.js +2 -2
  54. package/dist/lib/onboarding/init_fresh_folder.js +18 -18
  55. package/dist/lib/onboarding/manifest_writer.js +31 -31
  56. package/dist/lib/onboarding/web_first_attach.js +12 -12
  57. package/dist/lib/{workspace_context.js → project_context.js} +24 -24
  58. package/dist/lib/review/client.js +1 -1
  59. package/dist/lib/{lanes → session-engine}/claudeAdapter.js +3 -3
  60. package/dist/lib/{lanes → session-engine}/frames.js +36 -27
  61. package/dist/lib/session-engine/prune.js +233 -0
  62. package/dist/lib/session-engine/registry.js +83 -0
  63. package/dist/lib/session-engine/supervisor.js +321 -0
  64. package/dist/lib/session-engine/worktree.js +89 -0
  65. package/dist/lib/session_telemetry.js +2 -2
  66. package/dist/lib/sessions/client.js +8 -8
  67. package/dist/lib/sessions/outsideSessions.js +6 -6
  68. package/dist/lib/telemetry/config_renderer.js +3 -3
  69. package/dist/lib/telemetry/health_report.js +1 -1
  70. package/package.json +5 -4
  71. package/dist/commands/lane.js +0 -12
  72. package/dist/lib/lanes/supervisor.js +0 -247
  73. package/dist/lib/lanes/worktree.js +0 -33
  74. package/dist/lib/local_store/workspace_events_cache.js +0 -54
@@ -0,0 +1,321 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { createInterface } from "node:readline";
5
+ import { ClaudeSession } from "./claudeAdapter.js";
6
+ import { FRAME_VERSION, parseInboundFrame, serializeFrame, } from "./frames.js";
7
+ import { inspectSession, reclaimSession } from "./prune.js";
8
+ import { sessionRegistryPath, readSessionRegistry, updateSessionRegistry } from "./registry.js";
9
+ import { createSessionWorktree } from "./worktree.js";
10
+ function defaultProbeClaude(env) {
11
+ return new Promise((resolve) => {
12
+ execFile("claude", ["--version"], { env, timeout: 5_000 }, (error, stdout) => {
13
+ if (error)
14
+ resolve({ found: false, version: null });
15
+ else
16
+ resolve({ found: true, version: stdout.trim() });
17
+ });
18
+ });
19
+ }
20
+ export class SessionSupervisor {
21
+ options;
22
+ env;
23
+ sessions = new Map();
24
+ closingSessions = new Set();
25
+ shuttingDown = false;
26
+ resolveDone = null;
27
+ lines = null;
28
+ registryWrites = Promise.resolve();
29
+ constructor(options) {
30
+ this.options = options;
31
+ this.env = options.env ?? process.env;
32
+ }
33
+ emit(frame) {
34
+ this.options.output.write(serializeFrame(frame));
35
+ }
36
+ status(sessionId, status, message = null) {
37
+ this.emit({ v: FRAME_VERSION, type: "session.status", sessionId, status, message });
38
+ }
39
+ error(sessionId, code, message) {
40
+ this.emit({ v: FRAME_VERSION, type: "session.error", sessionId, code, message });
41
+ }
42
+ async boundProjectOf(projectPath) {
43
+ try {
44
+ const parsed = JSON.parse(await readFile(path.join(projectPath, ".nustack", "workspace.json"), "utf8"));
45
+ const id = parsed?.workspaceId;
46
+ return typeof id === "string" && id.trim() ? id.trim() : null;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ async moveSessionRow(record, to) {
53
+ const client = this.options.sessions;
54
+ if (!client || !record.projectId)
55
+ return;
56
+ try {
57
+ await client.transitionSession(record.projectId, record.sessionId, to);
58
+ }
59
+ catch {
60
+ }
61
+ }
62
+ async updateRegistry(mutate) {
63
+ const run = () => updateSessionRegistry(this.env, mutate);
64
+ this.registryWrites = this.registryWrites.then(run, run);
65
+ return this.registryWrites;
66
+ }
67
+ async run() {
68
+ const probe = this.options.probeClaude ?? (() => defaultProbeClaude(this.env));
69
+ const claude = await probe();
70
+ this.emit({ v: FRAME_VERSION, type: "engine.hello", engineVersion: this.options.engineVersion, claude });
71
+ const lines = createInterface({ input: this.options.input });
72
+ this.lines = lines;
73
+ lines.on("line", (line) => {
74
+ if (line.trim() === "")
75
+ return;
76
+ const parsed = parseInboundFrame(line);
77
+ if (!parsed.ok) {
78
+ this.error(null, parsed.error.code, parsed.error.message);
79
+ return;
80
+ }
81
+ void this.handle(parsed.frame);
82
+ });
83
+ await new Promise((resolve) => {
84
+ this.resolveDone = resolve;
85
+ lines.on("close", () => void this.shutdown());
86
+ });
87
+ }
88
+ async handle(frame) {
89
+ switch (frame.type) {
90
+ case "engine.shutdown":
91
+ await this.shutdown();
92
+ return;
93
+ case "session.open":
94
+ await this.openSession(frame.sessionId, frame.projectPath, frame.permissionMode, frame.isolated, frame.resumeSessionId, frame.commit);
95
+ return;
96
+ case "session.turn": {
97
+ const session = this.sessions.get(frame.sessionId);
98
+ if (!session)
99
+ return this.error(frame.sessionId, "SESSION_UNKNOWN", `no open session ${frame.sessionId}`);
100
+ session.adapter.sendTurn(frame.text);
101
+ this.status(frame.sessionId, "running");
102
+ return;
103
+ }
104
+ case "session.approval": {
105
+ const session = this.sessions.get(frame.sessionId);
106
+ if (!session)
107
+ return this.error(frame.sessionId, "SESSION_UNKNOWN", `no open session ${frame.sessionId}`);
108
+ session.adapter.respondApproval(frame.requestId, frame.verdict);
109
+ return;
110
+ }
111
+ case "session.interrupt": {
112
+ const session = this.sessions.get(frame.sessionId);
113
+ if (!session)
114
+ return this.error(frame.sessionId, "SESSION_UNKNOWN", `no open session ${frame.sessionId}`);
115
+ session.adapter.interrupt();
116
+ return;
117
+ }
118
+ case "session.close": {
119
+ const session = this.sessions.get(frame.sessionId);
120
+ if (!session)
121
+ return this.error(frame.sessionId, "SESSION_UNKNOWN", `no open session ${frame.sessionId}`);
122
+ this.closingSessions.add(frame.sessionId);
123
+ session.adapter.stop();
124
+ return;
125
+ }
126
+ case "session.list":
127
+ await this.sendRoster();
128
+ return;
129
+ case "session.reclaim":
130
+ await this.reclaimRow(frame.sessionId);
131
+ return;
132
+ }
133
+ }
134
+ async openSession(sessionId, projectPath, permissionMode, isolated, resumeSessionId, commit) {
135
+ if (this.sessions.has(sessionId))
136
+ return this.error(sessionId, "SESSION_ALREADY_OPEN", `session ${sessionId} is already open`);
137
+ this.status(sessionId, "starting");
138
+ const worktree = await createSessionWorktree(projectPath, sessionId, this.env, isolated === true, commit);
139
+ if (worktree.commitRefusal !== null) {
140
+ this.error(sessionId, worktree.commitRefusal.code, worktree.commitRefusal.detail);
141
+ this.status(sessionId, "error", `could not open ${String(commit)}`);
142
+ return;
143
+ }
144
+ const record = {
145
+ sessionId,
146
+ projectPath,
147
+ worktreePath: worktree.worktreePath,
148
+ branch: worktree.branch,
149
+ provider: "claude-code",
150
+ providerSessionId: null,
151
+ createdAt: new Date().toISOString(),
152
+ status: "open",
153
+ nustack: 1,
154
+ projectId: await this.boundProjectOf(projectPath),
155
+ permissionMode: permissionMode ?? null,
156
+ baseBranch: worktree.baseBranch,
157
+ baseCommit: worktree.baseCommit,
158
+ enginePid: process.pid,
159
+ closedAt: null,
160
+ };
161
+ const adapterOptions = {
162
+ cwd: worktree.worktreePath,
163
+ permissionMode,
164
+ resumeSessionId,
165
+ env: this.env,
166
+ onEvent: (event) => {
167
+ this.emit({ v: FRAME_VERSION, type: "session.event", sessionId, event });
168
+ if (typeof event === "object" && event !== null && event.type === "result") {
169
+ this.status(sessionId, "ready");
170
+ }
171
+ },
172
+ onApprovalRequest: (request) => {
173
+ this.emit({
174
+ v: FRAME_VERSION,
175
+ type: "session.approvalRequest",
176
+ sessionId,
177
+ requestId: request.requestId,
178
+ toolName: request.toolName,
179
+ displayName: request.displayName,
180
+ toolInput: request.toolInput,
181
+ description: request.description,
182
+ });
183
+ },
184
+ onProtocolError: (code, message) => this.error(sessionId, code, message),
185
+ onSessionId: (vendorSessionId) => {
186
+ record.providerSessionId = vendorSessionId;
187
+ void this.updateRegistry((sessions) => sessions.map((row) => (row.sessionId === sessionId ? { ...row, providerSessionId: vendorSessionId } : row)));
188
+ const client = this.options.sessions;
189
+ if (client && record.projectId) {
190
+ void client.setVendorSessionId(record.projectId, sessionId, vendorSessionId).catch(() => undefined);
191
+ }
192
+ },
193
+ onExit: (code) => {
194
+ const wasExpected = this.closingSessions.delete(sessionId) || this.shuttingDown;
195
+ this.sessions.delete(sessionId);
196
+ if (!wasExpected)
197
+ this.error(sessionId, "ADAPTER_FAILED", `claude exited unexpectedly (code ${String(code)})`);
198
+ this.emit({ v: FRAME_VERSION, type: "session.closed", sessionId });
199
+ void this.updateRegistry((sessions) => sessions.map((row) => row.sessionId === sessionId ? { ...row, status: "closed", closedAt: new Date().toISOString() } : row));
200
+ void this.moveSessionRow(record, wasExpected ? "done" : "failed");
201
+ },
202
+ };
203
+ const create = this.options.createAdapter ?? ((opts) => new ClaudeSession(opts));
204
+ let adapter;
205
+ try {
206
+ adapter = create(adapterOptions);
207
+ }
208
+ catch (error) {
209
+ return this.error(sessionId, "ADAPTER_FAILED", error instanceof Error ? error.message : String(error));
210
+ }
211
+ this.sessions.set(sessionId, { adapter, record });
212
+ await this.updateRegistry((sessions) => [...sessions.filter((row) => row.sessionId !== sessionId), record]);
213
+ if (this.options.sessions && record.projectId) {
214
+ try {
215
+ await this.options.sessions.createSession(record.projectId, {
216
+ id: sessionId,
217
+ startedBy: "person",
218
+ substrate: "device",
219
+ mode: "synchronous",
220
+ nustack: 1,
221
+ vendor: "claude_code",
222
+ renderer: "claude_transcript",
223
+ status: "created",
224
+ });
225
+ }
226
+ catch {
227
+ }
228
+ }
229
+ this.emit({
230
+ v: FRAME_VERSION,
231
+ type: "session.opened",
232
+ sessionId,
233
+ projectPath,
234
+ worktreePath: worktree.worktreePath,
235
+ branch: worktree.branch,
236
+ isolated: worktree.isolated,
237
+ commit: worktree.baseCommit,
238
+ });
239
+ this.status(sessionId, "ready");
240
+ }
241
+ async sendRoster() {
242
+ const { records, dropped } = await readSessionRegistry(this.env);
243
+ const rows = [];
244
+ for (const record of records) {
245
+ const attached = this.sessions.has(record.sessionId);
246
+ const facts = await inspectSession(record, this.env);
247
+ const reclaimable = !attached && facts.reclaimable;
248
+ const reason = attached ? "this engine is running it — close the session first" : facts.reason;
249
+ rows.push({
250
+ sessionId: record.sessionId,
251
+ projectPath: record.projectPath,
252
+ worktreePath: record.worktreePath,
253
+ branch: record.branch,
254
+ createdAt: record.createdAt,
255
+ status: record.status,
256
+ closedAt: record.closedAt ?? null,
257
+ permissionMode: record.permissionMode ?? null,
258
+ baseBranch: record.baseBranch ?? null,
259
+ attached,
260
+ liveElsewhere: !attached && record.status === "open" && typeof record.enginePid === "number" && !facts.stale,
261
+ isolated: facts.isolated,
262
+ checkoutPresent: facts.checkoutPresent,
263
+ worktreePresent: facts.worktreePresent,
264
+ branchExists: facts.branchExists,
265
+ reading: facts.reading,
266
+ dirty: facts.dirty,
267
+ stale: facts.stale,
268
+ reclaimable,
269
+ reason,
270
+ });
271
+ }
272
+ this.emit({
273
+ v: FRAME_VERSION,
274
+ type: "session.roster",
275
+ registryPath: sessionRegistryPath(this.env),
276
+ unreadable: dropped,
277
+ sessions: rows,
278
+ });
279
+ }
280
+ async reclaimRow(sessionId) {
281
+ if (this.sessions.has(sessionId)) {
282
+ return this.error(sessionId, "SESSION_ATTACHED", `session ${sessionId} is open in this engine — close it first`);
283
+ }
284
+ const { records } = await readSessionRegistry(this.env);
285
+ const record = records.find((row) => row.sessionId === sessionId);
286
+ if (!record)
287
+ return this.error(sessionId, "SESSION_UNKNOWN", `no session ${sessionId} in this machine's registry`);
288
+ const reclaim = this.registryWrites.then(() => reclaimSession(record, {}, this.env), () => reclaimSession(record, {}, this.env));
289
+ this.registryWrites = reclaim.then(() => undefined, () => undefined);
290
+ try {
291
+ const outcome = await reclaim;
292
+ this.emit({
293
+ v: FRAME_VERSION,
294
+ type: "session.reclaimed",
295
+ sessionId,
296
+ removedWorktree: outcome.removedWorktree,
297
+ deletedBranch: outcome.deletedBranch,
298
+ droppedRow: outcome.droppedRow,
299
+ reason: outcome.reason,
300
+ });
301
+ }
302
+ catch (error) {
303
+ this.error(sessionId, "RECLAIM_FAILED", error instanceof Error ? error.message : String(error));
304
+ }
305
+ }
306
+ async shutdown() {
307
+ if (this.shuttingDown)
308
+ return;
309
+ this.shuttingDown = true;
310
+ const held = [...this.sessions.keys()];
311
+ for (const [, session] of this.sessions)
312
+ session.adapter.stop();
313
+ this.sessions.clear();
314
+ if (held.length > 0) {
315
+ const closedAt = new Date().toISOString();
316
+ await this.updateRegistry((sessions) => sessions.map((row) => (held.includes(row.sessionId) && row.status === "open" ? { ...row, status: "closed", closedAt } : row)));
317
+ }
318
+ this.lines?.close();
319
+ this.resolveDone?.();
320
+ }
321
+ }
@@ -0,0 +1,89 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdir } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ export const ENGINE_SESSION_BRANCH_PREFIX = "nustack/session-";
6
+ export function sessionsHome(env = process.env) {
7
+ const override = env.NUSTACK_SESSIONS_HOME;
8
+ if (override && override !== "")
9
+ return override;
10
+ return path.join(os.homedir(), ".nustack", "sessions");
11
+ }
12
+ export const LEGACY_ENGINE_BRANCH_PREFIX = "nustack/lane-";
13
+ const FETCH_TIMEOUT_MS = 10 * 60_000;
14
+ export function legacyEngineHome(env = process.env) {
15
+ const override = env.NUSTACK_LEGACY_ENGINE_HOME;
16
+ if (override && override !== "")
17
+ return override;
18
+ return path.join(os.homedir(), ".nustack", "lanes");
19
+ }
20
+ function git(args, cwd) {
21
+ return gitOutcome(args, cwd).then((outcome) => (outcome.ok ? outcome.stdout : null));
22
+ }
23
+ export function gitOutcome(args, cwd, timeoutMs = 15_000) {
24
+ return new Promise((resolve) => {
25
+ const failed = (error) => {
26
+ resolve({ ok: false, code: null, stdout: "", stderr: error instanceof Error ? error.message : String(error) });
27
+ };
28
+ try {
29
+ execFile("git", args, { cwd, timeout: timeoutMs }, (error, stdout, stderr) => {
30
+ const raw = error?.code;
31
+ const code = error ? (typeof raw === "number" ? raw : null) : 0;
32
+ resolve({ ok: !error, code, stdout: stdout.trim(), stderr: stderr.trim() });
33
+ });
34
+ }
35
+ catch (error) {
36
+ failed(error);
37
+ }
38
+ });
39
+ }
40
+ export async function createSessionWorktree(projectPath, sessionId, env = process.env, isolate = false, commit) {
41
+ const inPlace = {
42
+ worktreePath: projectPath,
43
+ branch: null,
44
+ isolated: false,
45
+ baseBranch: null,
46
+ baseCommit: null,
47
+ commitRefusal: null,
48
+ };
49
+ if (!isolate && commit === undefined)
50
+ return inPlace;
51
+ const isRepo = await git(["rev-parse", "--is-inside-work-tree"], projectPath);
52
+ if (isRepo !== "true") {
53
+ return commit === undefined
54
+ ? inPlace
55
+ : { ...inPlace, commitRefusal: { code: "COMMIT_UNREACHABLE", detail: `${projectPath} is not a git checkout, so ${commit} cannot be opened here.` } };
56
+ }
57
+ if (commit !== undefined) {
58
+ const refusal = await fetchCommit(projectPath, commit);
59
+ if (refusal !== null)
60
+ return { ...inPlace, commitRefusal: refusal };
61
+ }
62
+ const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], projectPath);
63
+ const baseBranch = head === null || head === "HEAD" ? null : head;
64
+ const branch = `${ENGINE_SESSION_BRANCH_PREFIX}${sessionId}`;
65
+ const worktreePath = path.join(sessionsHome(env), "worktrees", sessionId);
66
+ await mkdir(path.dirname(worktreePath), { recursive: true });
67
+ const added = await git(commit === undefined
68
+ ? ["worktree", "add", worktreePath, "-b", branch]
69
+ : ["worktree", "add", worktreePath, "-b", branch, commit], projectPath);
70
+ if (added === null) {
71
+ return commit === undefined
72
+ ? inPlace
73
+ : { ...inPlace, commitRefusal: { code: "COMMIT_UNREACHABLE", detail: `git refused a worktree for ${commit} in ${projectPath}.` } };
74
+ }
75
+ const baseCommit = await git(["rev-parse", "HEAD"], worktreePath);
76
+ return { worktreePath, branch, isolated: true, baseBranch, baseCommit, commitRefusal: null };
77
+ }
78
+ async function fetchCommit(projectPath, commit) {
79
+ const has = () => gitOutcome(["cat-file", "-e", `${commit}^{commit}`], projectPath);
80
+ if ((await has()).ok)
81
+ return null;
82
+ let fetched = await gitOutcome(["fetch", "origin", commit], projectPath, FETCH_TIMEOUT_MS);
83
+ if (!fetched.ok)
84
+ fetched = await gitOutcome(["fetch", "origin"], projectPath, FETCH_TIMEOUT_MS);
85
+ if ((await has()).ok)
86
+ return null;
87
+ const said = `${fetched.stderr}\n${fetched.stdout}`.trim();
88
+ return { code: "COMMIT_UNREACHABLE", detail: said === "" ? `${commit} is not in this checkout and origin did not have it.` : said };
89
+ }
@@ -44,7 +44,7 @@ function toTelemetryBinding(binding, deviceRef, userId) {
44
44
  bindingId: binding.id,
45
45
  repoRoot: binding.repositoryRoot,
46
46
  organizationId: binding.organizationId,
47
- workspaceId: binding.workspaceId,
47
+ projectId: binding.projectId,
48
48
  deviceRef,
49
49
  userId,
50
50
  };
@@ -122,7 +122,7 @@ export async function ensureBindingTelemetry(opts) {
122
122
  }
123
123
  let brokerFailure = null;
124
124
  const credential = await fetchBrokeredCredential(baseUrl, auth.accessToken, {
125
- workspaceId: binding.workspaceId,
125
+ projectId: binding.projectId,
126
126
  service: TELEMETRY_SERVICE,
127
127
  }).catch((error) => {
128
128
  brokerFailure = error instanceof Error ? error.message : String(error);
@@ -23,19 +23,19 @@ export function makeSessionsClient(baseUrl, accessToken) {
23
23
  return { status: response.status, session: payload.data };
24
24
  }
25
25
  return {
26
- async createSession(workspaceId, input) {
27
- const safeId = assertSafeOpaqueId("The Workspace id", workspaceId);
28
- const answered = await post(`/api/v2/workspaces/${encodeURIComponent(safeId)}/sessions`, input, "session create failed");
26
+ async createSession(projectId, input) {
27
+ const safeId = assertSafeOpaqueId("The Project id", projectId);
28
+ const answered = await post(`/api/v2/projects/${encodeURIComponent(safeId)}/sessions`, input, "session create failed");
29
29
  return { created: answered.status === 201, session: answered.session };
30
30
  },
31
- async transitionSession(workspaceId, sessionId, to, patch = {}) {
32
- const safeWorkspace = assertSafeOpaqueId("The Workspace id", workspaceId);
31
+ async transitionSession(projectId, sessionId, to, patch = {}) {
32
+ const safeProject = assertSafeOpaqueId("The Project id", projectId);
33
33
  const safeSession = assertSafeOpaqueId("The session id", sessionId);
34
- const answered = await post(`/api/v2/workspaces/${encodeURIComponent(safeWorkspace)}/sessions/${encodeURIComponent(safeSession)}/transition`, { to, ...patch }, "session transition failed");
34
+ const answered = await post(`/api/v2/projects/${encodeURIComponent(safeProject)}/sessions/${encodeURIComponent(safeSession)}/transition`, { to, ...patch }, "session transition failed");
35
35
  return answered.session;
36
36
  },
37
- async setVendorSessionId(workspaceId, sessionId, vendorSessionId) {
38
- return this.transitionSession(workspaceId, sessionId, "running", { vendorSessionId });
37
+ async setVendorSessionId(projectId, sessionId, vendorSessionId) {
38
+ return this.transitionSession(projectId, sessionId, "running", { vendorSessionId });
39
39
  },
40
40
  };
41
41
  }
@@ -9,10 +9,10 @@ function vendorOf(provider) {
9
9
  const AT_REST = new Set(["done", "failed", "cancelled"]);
10
10
  export async function recordOutsideSessions(client, input) {
11
11
  const now = input.now ?? Date.now;
12
- const laneIds = new Set((input.laneSessionIds ?? []).filter((id) => Boolean(id)));
12
+ const engineIds = new Set((input.engineSessionIds ?? []).filter((id) => Boolean(id)));
13
13
  const report = { created: 0, finished: 0, skipped: 0, failed: 0 };
14
14
  for (const transcript of input.transcripts) {
15
- if (!transcript.workspaceId) {
15
+ if (!transcript.projectId) {
16
16
  report.skipped += 1;
17
17
  continue;
18
18
  }
@@ -23,14 +23,14 @@ export async function recordOutsideSessions(client, input) {
23
23
  }
24
24
  const stale = now() - transcript.modifiedMs >= STOPPED_GROWING_MS;
25
25
  const vendorSessionId = transcript.vendorSessionId?.trim() || null;
26
- const isLane = vendorSessionId !== null && laneIds.has(vendorSessionId);
26
+ const isEngineSession = vendorSessionId !== null && engineIds.has(vendorSessionId);
27
27
  try {
28
- const { created, session } = await client.createSession(transcript.workspaceId, {
28
+ const { created, session } = await client.createSession(transcript.projectId, {
29
29
  id: transcript.sessionKey,
30
30
  startedBy: "person",
31
31
  substrate: "device",
32
32
  mode: "synchronous",
33
- origin: isLane ? "inside" : "outside",
33
+ nustack: isEngineSession ? 1 : 0,
34
34
  vendor: kinds.vendor,
35
35
  renderer: kinds.renderer,
36
36
  status: stale ? "done" : "running",
@@ -39,7 +39,7 @@ export async function recordOutsideSessions(client, input) {
39
39
  if (created)
40
40
  report.created += 1;
41
41
  if (stale && !AT_REST.has(session.status)) {
42
- await client.transitionSession(transcript.workspaceId, transcript.sessionKey, "done");
42
+ await client.transitionSession(transcript.projectId, transcript.sessionKey, "done");
43
43
  report.finished += 1;
44
44
  }
45
45
  else if (!created) {
@@ -92,7 +92,7 @@ const BINDING_TEMPLATE = `# Managed by the nustack CLI — binding: {{binding_id
92
92
  nustack:
93
93
  schema_version: {{schema_version}}
94
94
  organization_id: {{organization_id}}
95
- workspace_id: {{workspace_id}}
95
+ project_id: {{project_id}}
96
96
  binding_id: {{binding_id}}
97
97
  device_id: {{device_id}}
98
98
  user_id: {{user_id}}
@@ -116,7 +116,7 @@ export function renderBindingInput(binding) {
116
116
  claude_glob: yamlScalar(`${claudeProjectDir(binding.repoRoot)}/**/*.jsonl`),
117
117
  schema_version: String(ENVELOPE_SCHEMA_VERSION),
118
118
  organization_id: yamlScalar(binding.organizationId),
119
- workspace_id: yamlScalar(binding.workspaceId),
119
+ project_id: yamlScalar(binding.projectId),
120
120
  binding_id: yamlScalar(binding.bindingId),
121
121
  device_id: yamlScalar(binding.deviceRef),
122
122
  user_id: yamlScalar(binding.userId),
@@ -142,7 +142,7 @@ export function renderCodexSharedInput(bindings) {
142
142
  target: nustack
143
143
  fields:
144
144
  organization_id: ${yamlScalar(binding.organizationId)}
145
- workspace_id: ${yamlScalar(binding.workspaceId)}
145
+ project_id: ${yamlScalar(binding.projectId)}
146
146
  binding_id: ${yamlScalar(binding.bindingId)}`)
147
147
  .join("\n");
148
148
  const dropList = ["cwd", ...providerDropFields("codex")].join(", ");
@@ -38,7 +38,7 @@ export async function collectTelemetryStatus(store, probes = defaultProbes(), op
38
38
  bindingId: binding.id,
39
39
  repoRoot: binding.repositoryRoot,
40
40
  organizationId: binding.organizationId,
41
- workspaceId: binding.workspaceId,
41
+ projectId: binding.projectId,
42
42
  deviceRef,
43
43
  userId,
44
44
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/nustack",
3
- "version": "0.10.0-dev.9",
3
+ "version": "0.11.0",
4
4
  "description": "The nustack CLI — bootstrap NuStack services into a repo via the NuStack discovery plane.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -26,15 +26,16 @@
26
26
  "commander": "^14.0.0"
27
27
  },
28
28
  "optionalDependencies": {
29
- "@nurix/codegraph": "0.2.1"
29
+ "@nurix/codegraph": "0.2.1",
30
+ "@nurix/logscope": "0.1.0"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@ngneat/falso": "^8.0.2",
33
34
  "@types/node": "^22.19.20",
34
35
  "typescript": "^5.7.2",
35
36
  "vitest": "^4.1.11",
36
- "@nurix/nustack.studio": "0.0.0",
37
- "@nurix/nustack.ui": "0.0.0"
37
+ "@nurix/nustack.ui": "0.0.0",
38
+ "@nurix/nustack.studio": "0.0.0"
38
39
  },
39
40
  "publishConfig": {
40
41
  "access": "public"
@@ -1,12 +0,0 @@
1
- import { LaneSupervisor } from "../lib/lanes/supervisor.js";
2
- import { sessionsClientFromEnv } from "../lib/sessions/client.js";
3
- export async function runLaneServe(engineVersion) {
4
- const supervisor = new LaneSupervisor({
5
- input: process.stdin,
6
- output: process.stdout,
7
- engineVersion,
8
- sessions: sessionsClientFromEnv(process.env),
9
- });
10
- await supervisor.run();
11
- process.exit(0);
12
- }