@nurix/nustack 0.10.0-dev.18 → 0.10.0-dev.19

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.
@@ -0,0 +1,55 @@
1
+ import { answerLogscope, listLogscopeFunctions } from "../lib/logs/answer.js";
2
+ import { loadLogscope } from "../lib/logs/library.js";
3
+ import { allowSources, forgetCorpusCommand, indexLogs, listAllowed, listCorpora, pruneCorpora, revokeSources, } from "../lib/logs/lifecycle.js";
4
+ export function logscopeArgv(rawArgv) {
5
+ const args = rawArgv.slice(2);
6
+ const at = args.indexOf("logscope");
7
+ return at === -1 ? [] : args.slice(at + 1);
8
+ }
9
+ const VALUED = ["--root", "--corpus", "--budget", "--retain", "--since", "--until", "--revision", "--note"];
10
+ const BARE = ["--json", "--examples", "--no-redact", "--dry-run"];
11
+ export function functionArgv(argv, fn) {
12
+ const valued = fn !== undefined && fn.args["limit"] !== undefined ? VALUED : [...VALUED, "--limit"];
13
+ const out = [];
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const token = argv[i] ?? "";
16
+ if (valued.includes(token)) {
17
+ i++;
18
+ continue;
19
+ }
20
+ if (valued.some((flag) => token.startsWith(`${flag}=`)))
21
+ continue;
22
+ if (BARE.includes(token))
23
+ continue;
24
+ out.push(token);
25
+ }
26
+ return out;
27
+ }
28
+ function positionals(argv) {
29
+ return functionArgv(argv)
30
+ .slice(1)
31
+ .filter((token) => !token.startsWith("--"));
32
+ }
33
+ export async function runLogscope(rawArgv, options, cwd = process.cwd()) {
34
+ const argv = logscopeArgv(rawArgv);
35
+ const fn = argv.find((token) => !token.startsWith("--"));
36
+ if (fn === undefined || fn === "functions")
37
+ return listLogscopeFunctions(options);
38
+ if (fn === "index")
39
+ return indexLogs(positionals(argv), options, cwd);
40
+ if (fn === "corpora")
41
+ return listCorpora(options);
42
+ if (fn === "allow")
43
+ return allowSources(positionals(argv), options, cwd);
44
+ if (fn === "allowed")
45
+ return listAllowed(options, cwd);
46
+ if (fn === "revoke")
47
+ return revokeSources(positionals(argv), options, cwd);
48
+ if (fn === "prune")
49
+ return pruneCorpora(options, cwd);
50
+ if (fn === "forget")
51
+ return forgetCorpusCommand(positionals(argv)[0], options);
52
+ const library = await loadLogscope();
53
+ const rest = functionArgv(argv, library.findFn(fn));
54
+ return answerLogscope(fn, rest.slice(1), options, cwd);
55
+ }
@@ -77,7 +77,16 @@ function readSubject(raw) {
77
77
  type: "tool_permission",
78
78
  toolName,
79
79
  displayName,
80
- toolInput: row.toolInput ?? null,
80
+ ...(typeof row.target === "string" || row.target === null ? { target: row.target } : {}),
81
+ ...(typeof row.inputBytes === "number" && Number.isFinite(row.inputBytes)
82
+ ? { inputBytes: row.inputBytes }
83
+ : {}),
84
+ ...(Array.isArray(row.inputFields) && row.inputFields.every((field) => typeof field === "string")
85
+ ? { inputFields: row.inputFields }
86
+ : {}),
87
+ ...(typeof row.redactionVersion === "number" && Number.isFinite(row.redactionVersion)
88
+ ? { redactionVersion: row.redactionVersion }
89
+ : {}),
81
90
  ...(typeof row.description === "string" ? { description: row.description } : {}),
82
91
  };
83
92
  }
@@ -140,9 +149,9 @@ export function registerReviewCommands(reviewCommand, getGlobalOptions) {
140
149
  .action(async (options) => runReviewCheck(options, getGlobalOptions()));
141
150
  reviewCommand
142
151
  .command("mirror")
143
- .description("Reflect a lane's permission prompt, or its answer, into the Review pane")
144
- .option("--session <laneId>", "the lane's session id")
145
- .option("--project <id>", "the Project the lane's folder is bound to (required with --state open)")
152
+ .description("Reflect a session's permission prompt, or its answer, into the Review pane")
153
+ .option("--session <sessionId>", "the session's id")
154
+ .option("--project <id>", "the Project the session's folder is bound to (required with --state open)")
146
155
  .option("--state <state>", "open or answered")
147
156
  .option("--subject <json>", "the tool-permission subject, as JSON (required with --state open)")
148
157
  .option("--verdict <verdict>", "allow or deny (required with --state answered)")
@@ -1,9 +1,20 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { ensureAccessToken } from "../lib/auth_grant.js";
3
3
  import { resolveNuStackUrl } from "../lib/nustack_client.js";
4
- import { makeSessionsClient } from "../lib/sessions/client.js";
4
+ import { makeSessionsClient, sessionsClientFromEnv } from "../lib/sessions/client.js";
5
5
  import { recordOutsideSessions, } from "../lib/sessions/outsideSessions.js";
6
+ import { SessionSupervisor } from "../lib/session-engine/supervisor.js";
6
7
  import { printJsonSuccess } from "../lib/json_output.js";
8
+ export async function runSessionsServe(engineVersion) {
9
+ const supervisor = new SessionSupervisor({
10
+ input: process.stdin,
11
+ output: process.stdout,
12
+ engineVersion,
13
+ sessions: sessionsClientFromEnv(process.env),
14
+ });
15
+ await supervisor.run();
16
+ process.exit(0);
17
+ }
7
18
  async function readScanArgument(raw) {
8
19
  const body = raw.startsWith("@") ? await readFile(raw.slice(1), "utf8") : raw;
9
20
  return JSON.parse(body);
@@ -46,6 +57,10 @@ export async function runSessionsRecord(options, globals) {
46
57
  if (!options.scan)
47
58
  return report("sessions record", EMPTY, json, "no scan payload");
48
59
  const payload = (await readScanArgument(options.scan).catch(() => null));
60
+ if (payload && "laneSessionIds" in payload) {
61
+ process.stderr.write("sessions record: this NuStack desktop is older than this CLI: it sent laneSessionIds, which is now engineSessionIds. Update both.\n");
62
+ process.exit(1);
63
+ }
49
64
  const transcripts = readTranscripts(payload?.sessions);
50
65
  if (transcripts.length === 0)
51
66
  return report("sessions record", EMPTY, json, "nothing to record");
@@ -54,8 +69,8 @@ export async function runSessionsRecord(options, globals) {
54
69
  return report("sessions record", EMPTY, json, "signed out");
55
70
  const summary = await recordOutsideSessions(makeSessionsClient(baseUrl, deviceAuth.accessToken), {
56
71
  transcripts,
57
- laneSessionIds: Array.isArray(payload?.laneSessionIds)
58
- ? payload.laneSessionIds.filter((id) => typeof id === "string")
72
+ engineSessionIds: Array.isArray(payload?.engineSessionIds)
73
+ ? payload.engineSessionIds.filter((id) => typeof id === "string")
59
74
  : [],
60
75
  });
61
76
  report("sessions record", summary, json);
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { runSync } from "./commands/sync.js";
9
9
  import { runDescribe } from "./commands/describe.js";
10
10
  import { runValidate } from "./commands/validate.js";
11
11
  import { runGraph } from "./commands/graph.js";
12
+ import { runLogscope } from "./commands/logscope.js";
12
13
  import { runDoctor } from "./commands/doctor.js";
13
14
  import { runTokenCreate, runTokenList, runTokenRevoke } from "./commands/token.js";
14
15
  import { runOpen } from "./commands/open.js";
@@ -23,8 +24,7 @@ import { runOrganizationList, runOrganizationCreate, runOrganizationUse } from "
23
24
  import { runDocsList, runDocsShow, runDocsStatus } from "./commands/docs.js";
24
25
  import { runHandoffList, runHandoffShow } from "./commands/handoff.js";
25
26
  import { runToolkitStatus, runToolkitReconcile } from "./commands/toolkit.js";
26
- import { runLaneServe } from "./commands/lane.js";
27
- import { runSessionsRecord } from "./commands/sessions.js";
27
+ import { runSessionsRecord, runSessionsServe } from "./commands/sessions.js";
28
28
  import { registerReviewCommands } from "./commands/review.js";
29
29
  import { runAgentContext, runAgentHookInstall, runAgentHookRemove } from "./commands/agent.js";
30
30
  import { printJsonError } from "./lib/json_output.js";
@@ -96,6 +96,25 @@ program
96
96
  .allowUnknownOption()
97
97
  .allowExcessArguments()
98
98
  .action(async (_fn, _args, options) => runGraph(process.argv, options));
99
+ program
100
+ .command("logscope [fn] [args...]")
101
+ .description("Log tracking: ask this machine's log corpus a question, or `index` / `corpora` / `allow` / `prune`")
102
+ .option("--root <dir>", "the project this command is about (default: the working tree's root)")
103
+ .option("--corpus <ref>", "a corpus id, or a path belonging to one")
104
+ .option("--budget <n>", "token ceiling for the answer; 0 means unbounded")
105
+ .option("--limit <n>", "rows requested — beats a default budget, never an explicit one")
106
+ .option("--retain <days>", "retention for `index`; 0 keeps the corpus until it is forgotten")
107
+ .option("--examples", "keep up to three exemplar log lines per template (off by default)")
108
+ .option("--no-redact", "store exemplars unredacted (loud, recorded on the corpus)")
109
+ .option("--since <ts>", "window start for container sources")
110
+ .option("--until <ts>", "window end for container sources")
111
+ .option("--revision <sha>", "the source revision these logs were written by")
112
+ .option("--note <text>", "a note recorded with an `allow` grant")
113
+ .option("--dry-run", "for `prune`: report what would be dropped and delete nothing")
114
+ .option("--json", "emit the versioned machine envelope with the answer as data")
115
+ .allowUnknownOption()
116
+ .allowExcessArguments()
117
+ .action(async (_fn, _args, options) => runLogscope(process.argv, options));
99
118
  program
100
119
  .command("doctor")
101
120
  .description("Run the full local diagnostics matrix (identity, repository, manifests, toolkit, telemetry, credentials)")
@@ -230,12 +249,13 @@ handoff
230
249
  .option("--targets", "list each target's path, operation, and base semantics")
231
250
  .option("--json", "emit the versioned machine envelope")
232
251
  .action(async (id, options) => runHandoffShow(id, options, program.opts()));
233
- const lane = program.command("lane").description("The desktop build room's lane engine");
234
- lane
252
+ const sessions = program
253
+ .command("sessions")
254
+ .description("Sessions on this machine — the engine the desktop drives, and the record of what ran");
255
+ sessions
235
256
  .command("serve")
236
- .description("Run the long-lived lane supervisor: JSONL frames on stdin/stdout (used by the NuStack desktop)")
237
- .action(async () => runLaneServe(version));
238
- const sessions = program.command("sessions").description("The session record over this machine's agent work");
257
+ .description("Run the long-lived session supervisor: JSONL frames on stdin/stdout (used by the NuStack desktop)")
258
+ .action(async () => runSessionsServe(version));
239
259
  sessions
240
260
  .command("record")
241
261
  .description("Record the sessions one desktop scan found in a bound folder (used by the NuStack desktop)")
@@ -0,0 +1,99 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { printJsonSuccess } from "../json_output.js";
5
+ import { loadLogscope } from "./library.js";
6
+ export class LogsNoRepository extends Error {
7
+ code = "LOGS_NO_REPOSITORY";
8
+ constructor(where) {
9
+ super(`not a git repository: ${where} — pass --root <path> or run inside a checkout`);
10
+ this.name = "LogsNoRepository";
11
+ }
12
+ }
13
+ export class LogsUnavailable extends Error {
14
+ code = "LOGS_UNAVAILABLE";
15
+ constructor(reason) {
16
+ super(reason);
17
+ this.name = "LogsUnavailable";
18
+ }
19
+ }
20
+ export class LogsRefused extends Error {
21
+ code = "LOGS_REFUSED";
22
+ constructor(reason) {
23
+ super(reason);
24
+ this.name = "LogsRefused";
25
+ }
26
+ }
27
+ export class LogsNotFound extends Error {
28
+ code = "LOGS_NOT_FOUND";
29
+ constructor(reason) {
30
+ super(reason);
31
+ this.name = "LogsNotFound";
32
+ }
33
+ }
34
+ export function resolveRoot(root, cwd) {
35
+ if (root !== undefined) {
36
+ const path = resolve(cwd, root);
37
+ if (!existsSync(path))
38
+ throw new LogsNoRepository(root);
39
+ return path;
40
+ }
41
+ return gitRoot(cwd);
42
+ }
43
+ function gitRoot(from) {
44
+ try {
45
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: from, stdio: ["ignore", "pipe", "ignore"] })
46
+ .toString("utf8")
47
+ .trim();
48
+ }
49
+ catch {
50
+ throw new LogsNoRepository(from);
51
+ }
52
+ }
53
+ export const progress = (line) => {
54
+ process.stderr.write(`${line}\n`);
55
+ };
56
+ export async function answerLogscope(fn, argv, options, cwd) {
57
+ const library = await loadLogscope();
58
+ const known = library.findFn(fn);
59
+ if (known === undefined) {
60
+ throw new LogsUnavailable(`unknown logscope function '${fn}' — run \`nustack logscope functions\` for the list`);
61
+ }
62
+ const args = library.parseArgs(known, argv);
63
+ if (known.args["root"] !== undefined && args["root"] === undefined) {
64
+ args["root"] = resolveRoot(options.root, cwd);
65
+ }
66
+ if (args["limit"] === undefined && options.limit !== undefined)
67
+ args["limit"] = Number(options.limit);
68
+ const limit = args["limit"];
69
+ const budget = options.budget !== undefined ? Number(options.budget) : limit !== undefined ? 0 : library.DEFAULT_BUDGET;
70
+ const result = library.dispatch({ fn: known.name, args, corpus: options.corpus, budget });
71
+ if (result.answer.unavailable !== undefined)
72
+ throw new LogsUnavailable(result.answer.unavailable);
73
+ if (options.json === true) {
74
+ printJsonSuccess(`logscope ${known.name}`, result.answer);
75
+ return;
76
+ }
77
+ console.log(result.rendered);
78
+ }
79
+ export async function listLogscopeFunctions(options) {
80
+ const library = await loadLogscope();
81
+ const budget = options.budget !== undefined ? Number(options.budget) : 0;
82
+ const result = library.dispatch({ fn: "functions", args: {}, budget });
83
+ if (options.json === true) {
84
+ printJsonSuccess("logscope functions", result.answer);
85
+ return;
86
+ }
87
+ console.log(result.rendered);
88
+ console.log([
89
+ "",
90
+ "lifecycle — `nustack logscope <verb>`",
91
+ " allow <targets...> grant a log source; nothing is read before this",
92
+ " allowed the grants covering this project",
93
+ " revoke <targets...> withdraw grants",
94
+ " index <sources...> ingest or refresh a corpus from granted sources",
95
+ " corpora every corpus indexed on this machine",
96
+ " prune [--dry-run] drop every corpus past its retention",
97
+ " forget <id> drop one corpus and its store",
98
+ ].join("\n"));
99
+ }
@@ -0,0 +1,19 @@
1
+ export class LogsLibraryMissing extends Error {
2
+ code = "LOGS_LIBRARY_MISSING";
3
+ constructor() {
4
+ super("the log-tracking library is not installed with this nustack — install it beside the CLI (`npm i -g @nurix/logscope`) or run nustack from a checkout of the monorepo.");
5
+ this.name = "LogsLibraryMissing";
6
+ }
7
+ }
8
+ let cached = null;
9
+ export async function loadLogscope() {
10
+ if (cached !== null)
11
+ return cached;
12
+ try {
13
+ cached = (await import("@nurix/logscope"));
14
+ return cached;
15
+ }
16
+ catch {
17
+ throw new LogsLibraryMissing();
18
+ }
19
+ }
@@ -0,0 +1,159 @@
1
+ import { printJsonSuccess } from "../json_output.js";
2
+ import { loadLogscope } from "./library.js";
3
+ import { LogsNotFound, LogsRefused, progress, resolveRoot } from "./answer.js";
4
+ function reportPruneToStderr(library) {
5
+ for (const record of library.prune()) {
6
+ progress(`pruned ${record.id} (retention ${library.retentionOf(record)} days, indexed ${record.indexedAt ?? "unknown"})`);
7
+ }
8
+ }
9
+ export async function indexLogs(targets, options, cwd) {
10
+ const library = await loadLogscope();
11
+ const root = resolveRoot(options.root, cwd);
12
+ const retentionDays = Number(options.retain ?? String(library.DEFAULT_RETENTION_DAYS));
13
+ if (!Number.isInteger(retentionDays) || retentionDays < 0) {
14
+ throw new LogsRefused("--retain takes a non-negative whole number of days");
15
+ }
16
+ reportPruneToStderr(library);
17
+ const outcome = await library.indexCorpus({
18
+ targets,
19
+ root,
20
+ since: options.since,
21
+ until: options.until,
22
+ revision: options.revision,
23
+ redact: options.redact !== false,
24
+ keepExamples: options.examples === true,
25
+ retentionDays,
26
+ });
27
+ if (!outcome.ok)
28
+ throw new LogsRefused(outcome.message);
29
+ if (options.json === true) {
30
+ printJsonSuccess("logscope index", {
31
+ id: outcome.id,
32
+ sources: outcome.sources,
33
+ events: outcome.events,
34
+ templates: outcome.templates,
35
+ ratio: outcome.ratio,
36
+ resumes: outcome.resumes,
37
+ retentionDays: outcome.retentionDays,
38
+ hasExamples: outcome.hasExamples,
39
+ expiresAt: outcome.expiresAt ?? null,
40
+ warning: outcome.warning ?? null,
41
+ });
42
+ return;
43
+ }
44
+ console.log([
45
+ `indexed ${outcome.id}`,
46
+ ` ${outcome.events} events -> ${outcome.templates} templates (ratio ${outcome.ratio.toFixed(4)})`,
47
+ ` sources ${Object.entries(outcome.resumes).map(([path, resume]) => `${path} [${resume}]`).join(", ")}`,
48
+ ` retention ${outcome.retentionDays === 0 ? "off (kept until forgotten)" : `${outcome.retentionDays} days (expires ${outcome.expiresAt ?? "unknown"})`}`,
49
+ ` examples ${outcome.hasExamples ? "kept" : "not kept — drill and trace <id> cannot answer on this corpus"}`,
50
+ ...(outcome.warning === undefined ? [] : [` WARNING: ${outcome.warning}`]),
51
+ ].join("\n"));
52
+ }
53
+ export async function listCorpora(options) {
54
+ const library = await loadLogscope();
55
+ reportPruneToStderr(library);
56
+ const corpora = library.readRegistry().map((record) => ({
57
+ id: record.id,
58
+ sources: record.sources,
59
+ events: record.events ?? 0,
60
+ templates: record.templates ?? 0,
61
+ indexedAt: record.indexedAt ?? null,
62
+ retentionDays: library.retentionOf(record),
63
+ expiresAt: library.expiresAt(record) ?? null,
64
+ hasExamples: record.hasExamples === true,
65
+ }));
66
+ if (options.json === true) {
67
+ printJsonSuccess("logscope corpora", { corpora });
68
+ return;
69
+ }
70
+ if (corpora.length === 0) {
71
+ console.log("no corpus has been indexed on this machine — run: nustack logscope index <sources...>");
72
+ return;
73
+ }
74
+ console.log(corpora
75
+ .map((corpus) => `${corpus.id} ${corpus.events} events, ${corpus.templates} templates (${corpus.sources.length} sources) ` +
76
+ (corpus.retentionDays === 0
77
+ ? "retention off"
78
+ : `retention ${corpus.retentionDays} days, expires ${corpus.expiresAt ?? "unknown"}`))
79
+ .join("\n"));
80
+ }
81
+ export async function allowSources(targets, options, cwd) {
82
+ const library = await loadLogscope();
83
+ const root = resolveRoot(options.root, cwd);
84
+ const allowed = library.allow(root, targets, options.note).map((grant) => grant.path);
85
+ if (options.json === true) {
86
+ printJsonSuccess("logscope allow", { root, allowed });
87
+ return;
88
+ }
89
+ console.log(allowed.map((path) => `allowed ${path}`).join("\n"));
90
+ }
91
+ export async function listAllowed(options, cwd) {
92
+ const library = await loadLogscope();
93
+ const root = resolveRoot(options.root, cwd);
94
+ const grants = library.allowedFor(root);
95
+ if (options.json === true) {
96
+ printJsonSuccess("logscope allowed", { root, grants });
97
+ return;
98
+ }
99
+ if (grants.length === 0) {
100
+ console.log(`no source is allowed for ${root} — run: nustack logscope allow <path...>`);
101
+ return;
102
+ }
103
+ console.log(grants
104
+ .map((grant) => `${grant.path} granted ${grant.grantedAt}${grant.note === undefined ? "" : ` ${grant.note}`}`)
105
+ .join("\n"));
106
+ }
107
+ export async function revokeSources(targets, options, cwd) {
108
+ const library = await loadLogscope();
109
+ const root = resolveRoot(options.root, cwd);
110
+ const revoked = library.revoke(root, targets);
111
+ if (revoked === 0)
112
+ throw new LogsNotFound(`no grant under ${root} names ${targets.join(", ")}`);
113
+ if (options.json === true) {
114
+ printJsonSuccess("logscope revoke", { root, revoked });
115
+ return;
116
+ }
117
+ console.log(`revoked ${revoked} grant(s)`);
118
+ }
119
+ export async function pruneCorpora(options, cwd) {
120
+ const library = await loadLogscope();
121
+ resolveRoot(options.root, cwd);
122
+ const dryRun = options.dryRun === true;
123
+ const now = new Date();
124
+ const candidates = dryRun
125
+ ? library.readRegistry().filter((record) => {
126
+ const at = library.expiresAt(record);
127
+ return at !== null && at !== undefined && new Date(at).getTime() <= now.getTime();
128
+ })
129
+ : library.prune();
130
+ const pruned = candidates.map((record) => ({
131
+ id: record.id,
132
+ retentionDays: library.retentionOf(record),
133
+ indexedAt: record.indexedAt ?? null,
134
+ }));
135
+ if (options.json === true) {
136
+ printJsonSuccess("logscope prune", { dryRun, pruned });
137
+ return;
138
+ }
139
+ if (pruned.length === 0) {
140
+ console.log("nothing to prune");
141
+ return;
142
+ }
143
+ console.log(pruned
144
+ .map((record) => `${dryRun ? "would prune" : "pruned"} ${record.id} (retention ${record.retentionDays} days, indexed ${record.indexedAt ?? "unknown"})`)
145
+ .join("\n"));
146
+ }
147
+ export async function forgetCorpusCommand(ref, options) {
148
+ const library = await loadLogscope();
149
+ if (ref === undefined)
150
+ throw new LogsNotFound("name the corpus to forget — run: nustack logscope corpora");
151
+ const forgot = library.forgetCorpus(ref);
152
+ if (forgot === undefined)
153
+ throw new LogsNotFound(`no corpus matches ${ref}`);
154
+ if (options.json === true) {
155
+ printJsonSuccess("logscope forget", { forgot });
156
+ return;
157
+ }
158
+ console.log(`forgot ${forgot}`);
159
+ }
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
- export function claudeLaneArgs(permissionMode, resumeSessionId) {
3
+ export function claudeSessionArgs(permissionMode, resumeSessionId) {
4
4
  const args = [
5
5
  "-p",
6
6
  "--input-format",
@@ -20,7 +20,7 @@ export function claudeLaneArgs(permissionMode, resumeSessionId) {
20
20
  }
21
21
  const DENY_MESSAGE = "Denied by the user in NuStack desktop.";
22
22
  const STOP_KILL_GRACE_MS = 3_000;
23
- export class ClaudeLane {
23
+ export class ClaudeSession {
24
24
  child;
25
25
  pendingApprovals = new Map();
26
26
  interruptSeq = 0;
@@ -30,7 +30,7 @@ export class ClaudeLane {
30
30
  constructor(options) {
31
31
  this.options = options;
32
32
  const spawnImpl = options.spawnImpl ?? spawn;
33
- this.child = spawnImpl(options.claudeBin ?? "claude", claudeLaneArgs(options.permissionMode, options.resumeSessionId), {
33
+ this.child = spawnImpl(options.claudeBin ?? "claude", claudeSessionArgs(options.permissionMode, options.resumeSessionId), {
34
34
  cwd: options.cwd,
35
35
  env: options.env ?? process.env,
36
36
  stdio: ["pipe", "pipe", "pipe"],
@@ -1,10 +1,10 @@
1
- export const FRAME_VERSION = 1;
1
+ export const FRAME_VERSION = 2;
2
2
  const INBOUND_TYPES = new Set([
3
- "lane.open",
4
- "lane.turn",
5
- "lane.approval",
6
- "lane.interrupt",
7
- "lane.close",
3
+ "session.open",
4
+ "session.turn",
5
+ "session.approval",
6
+ "session.interrupt",
7
+ "session.close",
8
8
  "engine.shutdown",
9
9
  ]);
10
10
  export function parseInboundFrame(line) {
@@ -32,37 +32,37 @@ export function parseInboundFrame(line) {
32
32
  switch (type) {
33
33
  case "engine.shutdown":
34
34
  return { ok: true, frame: { v: FRAME_VERSION, type } };
35
- case "lane.open": {
36
- const laneId = requireString("laneId");
35
+ case "session.open": {
36
+ const sessionId = requireString("sessionId");
37
37
  const projectPath = requireString("projectPath");
38
- if (!laneId || !projectPath)
39
- return { ok: false, error: { code: "BAD_FRAME", name: type, message: "lane.open requires laneId and projectPath" } };
38
+ if (!sessionId || !projectPath)
39
+ return { ok: false, error: { code: "BAD_FRAME", name: type, message: "session.open requires sessionId and projectPath" } };
40
40
  const permissionMode = typeof frame.permissionMode === "string" ? frame.permissionMode : undefined;
41
41
  const isolated = frame.isolated === true ? true : undefined;
42
42
  const resumeSessionId = typeof frame.resumeSessionId === "string" && frame.resumeSessionId !== "" ? frame.resumeSessionId : undefined;
43
- return { ok: true, frame: { v: FRAME_VERSION, type, laneId, projectPath, permissionMode, isolated, resumeSessionId } };
43
+ return { ok: true, frame: { v: FRAME_VERSION, type, sessionId, projectPath, permissionMode, isolated, resumeSessionId } };
44
44
  }
45
- case "lane.turn": {
46
- const laneId = requireString("laneId");
45
+ case "session.turn": {
46
+ const sessionId = requireString("sessionId");
47
47
  const text = typeof frame.text === "string" ? frame.text : null;
48
- if (!laneId || text === null)
49
- return { ok: false, error: { code: "BAD_FRAME", name: type, message: "lane.turn requires laneId and text" } };
50
- return { ok: true, frame: { v: FRAME_VERSION, type, laneId, text } };
48
+ if (!sessionId || text === null)
49
+ return { ok: false, error: { code: "BAD_FRAME", name: type, message: "session.turn requires sessionId and text" } };
50
+ return { ok: true, frame: { v: FRAME_VERSION, type, sessionId, text } };
51
51
  }
52
- case "lane.approval": {
53
- const laneId = requireString("laneId");
52
+ case "session.approval": {
53
+ const sessionId = requireString("sessionId");
54
54
  const requestId = requireString("requestId");
55
55
  const verdict = frame.verdict === "allow" || frame.verdict === "deny" ? frame.verdict : null;
56
- if (!laneId || !requestId || !verdict)
57
- return { ok: false, error: { code: "BAD_FRAME", name: type, message: "lane.approval requires laneId, requestId, verdict" } };
58
- return { ok: true, frame: { v: FRAME_VERSION, type, laneId, requestId, verdict } };
56
+ if (!sessionId || !requestId || !verdict)
57
+ return { ok: false, error: { code: "BAD_FRAME", name: type, message: "session.approval requires sessionId, requestId, verdict" } };
58
+ return { ok: true, frame: { v: FRAME_VERSION, type, sessionId, requestId, verdict } };
59
59
  }
60
- case "lane.interrupt":
61
- case "lane.close": {
62
- const laneId = requireString("laneId");
63
- if (!laneId)
64
- return { ok: false, error: { code: "BAD_FRAME", name: type, message: `${type} requires laneId` } };
65
- return { ok: true, frame: { v: FRAME_VERSION, type, laneId } };
60
+ case "session.interrupt":
61
+ case "session.close": {
62
+ const sessionId = requireString("sessionId");
63
+ if (!sessionId)
64
+ return { ok: false, error: { code: "BAD_FRAME", name: type, message: `${type} requires sessionId` } };
65
+ return { ok: true, frame: { v: FRAME_VERSION, type, sessionId } };
66
66
  }
67
67
  default:
68
68
  return { ok: false, error: { code: "UNKNOWN_FRAME", name: type, message: `unknown frame type: ${type}` } };
@@ -2,9 +2,9 @@ import { execFile } from "node:child_process";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { createInterface } from "node:readline";
5
- import { ClaudeLane } from "./claudeAdapter.js";
5
+ import { ClaudeSession } from "./claudeAdapter.js";
6
6
  import { FRAME_VERSION, parseInboundFrame, serializeFrame, } from "./frames.js";
7
- import { createLaneWorktree, lanesHome } from "./worktree.js";
7
+ import { createSessionWorktree, sessionsHome } from "./worktree.js";
8
8
  function defaultProbeClaude(env) {
9
9
  return new Promise((resolve) => {
10
10
  execFile("claude", ["--version"], { env, timeout: 5_000 }, (error, stdout) => {
@@ -15,11 +15,11 @@ function defaultProbeClaude(env) {
15
15
  });
16
16
  });
17
17
  }
18
- export class LaneSupervisor {
18
+ export class SessionSupervisor {
19
19
  options;
20
20
  env;
21
- lanes = new Map();
22
- closingLanes = new Set();
21
+ sessions = new Map();
22
+ closingSessions = new Set();
23
23
  shuttingDown = false;
24
24
  resolveDone = null;
25
25
  lines = null;
@@ -31,14 +31,14 @@ export class LaneSupervisor {
31
31
  emit(frame) {
32
32
  this.options.output.write(serializeFrame(frame));
33
33
  }
34
- status(laneId, status, message = null) {
35
- this.emit({ v: FRAME_VERSION, type: "lane.status", laneId, status, message });
34
+ status(sessionId, status, message = null) {
35
+ this.emit({ v: FRAME_VERSION, type: "session.status", sessionId, status, message });
36
36
  }
37
- error(laneId, code, message) {
38
- this.emit({ v: FRAME_VERSION, type: "lane.error", laneId, code, message });
37
+ error(sessionId, code, message) {
38
+ this.emit({ v: FRAME_VERSION, type: "session.error", sessionId, code, message });
39
39
  }
40
40
  registryPath() {
41
- return path.join(lanesHome(this.env), "registry.json");
41
+ return path.join(sessionsHome(this.env), "registry.json");
42
42
  }
43
43
  async boundProjectOf(projectPath) {
44
44
  try {
@@ -50,12 +50,12 @@ export class LaneSupervisor {
50
50
  return null;
51
51
  }
52
52
  }
53
- async moveLaneSession(record, to) {
53
+ async moveSessionRow(record, to) {
54
54
  const client = this.options.sessions;
55
55
  if (!client || !record.projectId)
56
56
  return;
57
57
  try {
58
- await client.transitionSession(record.projectId, record.laneId, to);
58
+ await client.transitionSession(record.projectId, record.sessionId, to);
59
59
  }
60
60
  catch {
61
61
  }
@@ -63,17 +63,17 @@ export class LaneSupervisor {
63
63
  async updateRegistry(mutate) {
64
64
  const run = async () => {
65
65
  const file = this.registryPath();
66
- let lanes = [];
66
+ let sessions = [];
67
67
  try {
68
68
  const parsed = JSON.parse(await readFile(file, "utf8"));
69
- if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.lanes)) {
70
- lanes = parsed.lanes;
69
+ if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.sessions)) {
70
+ sessions = parsed.sessions;
71
71
  }
72
72
  }
73
73
  catch {
74
74
  }
75
75
  await mkdir(path.dirname(file), { recursive: true });
76
- await writeFile(file, JSON.stringify({ v: 1, lanes: mutate(lanes) }, null, 2) + "\n", "utf8");
76
+ await writeFile(file, JSON.stringify({ v: 2, sessions: mutate(sessions) }, null, 2) + "\n", "utf8");
77
77
  };
78
78
  this.registryWrites = this.registryWrites.then(run, run);
79
79
  return this.registryWrites;
@@ -104,48 +104,48 @@ export class LaneSupervisor {
104
104
  case "engine.shutdown":
105
105
  await this.shutdown();
106
106
  return;
107
- case "lane.open":
108
- await this.openLane(frame.laneId, frame.projectPath, frame.permissionMode, frame.isolated, frame.resumeSessionId);
107
+ case "session.open":
108
+ await this.openSession(frame.sessionId, frame.projectPath, frame.permissionMode, frame.isolated, frame.resumeSessionId);
109
109
  return;
110
- case "lane.turn": {
111
- const lane = this.lanes.get(frame.laneId);
112
- if (!lane)
113
- return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
114
- lane.adapter.sendTurn(frame.text);
115
- this.status(frame.laneId, "running");
110
+ case "session.turn": {
111
+ const session = this.sessions.get(frame.sessionId);
112
+ if (!session)
113
+ return this.error(frame.sessionId, "SESSION_UNKNOWN", `no open session ${frame.sessionId}`);
114
+ session.adapter.sendTurn(frame.text);
115
+ this.status(frame.sessionId, "running");
116
116
  return;
117
117
  }
118
- case "lane.approval": {
119
- const lane = this.lanes.get(frame.laneId);
120
- if (!lane)
121
- return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
122
- lane.adapter.respondApproval(frame.requestId, frame.verdict);
118
+ case "session.approval": {
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
+ session.adapter.respondApproval(frame.requestId, frame.verdict);
123
123
  return;
124
124
  }
125
- case "lane.interrupt": {
126
- const lane = this.lanes.get(frame.laneId);
127
- if (!lane)
128
- return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
129
- lane.adapter.interrupt();
125
+ case "session.interrupt": {
126
+ const session = this.sessions.get(frame.sessionId);
127
+ if (!session)
128
+ return this.error(frame.sessionId, "SESSION_UNKNOWN", `no open session ${frame.sessionId}`);
129
+ session.adapter.interrupt();
130
130
  return;
131
131
  }
132
- case "lane.close": {
133
- const lane = this.lanes.get(frame.laneId);
134
- if (!lane)
135
- return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
136
- this.closingLanes.add(frame.laneId);
137
- lane.adapter.stop();
132
+ case "session.close": {
133
+ const session = this.sessions.get(frame.sessionId);
134
+ if (!session)
135
+ return this.error(frame.sessionId, "SESSION_UNKNOWN", `no open session ${frame.sessionId}`);
136
+ this.closingSessions.add(frame.sessionId);
137
+ session.adapter.stop();
138
138
  return;
139
139
  }
140
140
  }
141
141
  }
142
- async openLane(laneId, projectPath, permissionMode, isolated, resumeSessionId) {
143
- if (this.lanes.has(laneId))
144
- return this.error(laneId, "LANE_ALREADY_OPEN", `lane ${laneId} is already open`);
145
- this.status(laneId, "starting");
146
- const worktree = await createLaneWorktree(projectPath, laneId, this.env, isolated === true);
142
+ async openSession(sessionId, projectPath, permissionMode, isolated, resumeSessionId) {
143
+ if (this.sessions.has(sessionId))
144
+ return this.error(sessionId, "SESSION_ALREADY_OPEN", `session ${sessionId} is already open`);
145
+ this.status(sessionId, "starting");
146
+ const worktree = await createSessionWorktree(projectPath, sessionId, this.env, isolated === true);
147
147
  const record = {
148
- laneId,
148
+ sessionId,
149
149
  projectPath,
150
150
  worktreePath: worktree.worktreePath,
151
151
  branch: worktree.branch,
@@ -153,6 +153,7 @@ export class LaneSupervisor {
153
153
  providerSessionId: null,
154
154
  createdAt: new Date().toISOString(),
155
155
  status: "open",
156
+ nustack: 1,
156
157
  projectId: await this.boundProjectOf(projectPath),
157
158
  };
158
159
  const adapterOptions = {
@@ -161,16 +162,16 @@ export class LaneSupervisor {
161
162
  resumeSessionId,
162
163
  env: this.env,
163
164
  onEvent: (event) => {
164
- this.emit({ v: FRAME_VERSION, type: "lane.event", laneId, event });
165
+ this.emit({ v: FRAME_VERSION, type: "session.event", sessionId, event });
165
166
  if (typeof event === "object" && event !== null && event.type === "result") {
166
- this.status(laneId, "ready");
167
+ this.status(sessionId, "ready");
167
168
  }
168
169
  },
169
170
  onApprovalRequest: (request) => {
170
171
  this.emit({
171
172
  v: FRAME_VERSION,
172
- type: "lane.approvalRequest",
173
- laneId,
173
+ type: "session.approvalRequest",
174
+ sessionId,
174
175
  requestId: request.requestId,
175
176
  toolName: request.toolName,
176
177
  displayName: request.displayName,
@@ -178,43 +179,43 @@ export class LaneSupervisor {
178
179
  description: request.description,
179
180
  });
180
181
  },
181
- onProtocolError: (code, message) => this.error(laneId, code, message),
182
- onSessionId: (sessionId) => {
183
- record.providerSessionId = sessionId;
184
- void this.updateRegistry((lanes) => lanes.map((row) => (row.laneId === laneId ? { ...row, providerSessionId: sessionId } : row)));
182
+ onProtocolError: (code, message) => this.error(sessionId, code, message),
183
+ onSessionId: (vendorSessionId) => {
184
+ record.providerSessionId = vendorSessionId;
185
+ void this.updateRegistry((sessions) => sessions.map((row) => (row.sessionId === sessionId ? { ...row, providerSessionId: vendorSessionId } : row)));
185
186
  const client = this.options.sessions;
186
187
  if (client && record.projectId) {
187
- void client.setVendorSessionId(record.projectId, laneId, sessionId).catch(() => undefined);
188
+ void client.setVendorSessionId(record.projectId, sessionId, vendorSessionId).catch(() => undefined);
188
189
  }
189
190
  },
190
191
  onExit: (code) => {
191
- const wasExpected = this.closingLanes.delete(laneId) || this.shuttingDown;
192
- this.lanes.delete(laneId);
192
+ const wasExpected = this.closingSessions.delete(sessionId) || this.shuttingDown;
193
+ this.sessions.delete(sessionId);
193
194
  if (!wasExpected)
194
- this.error(laneId, "ADAPTER_FAILED", `claude exited unexpectedly (code ${String(code)})`);
195
- this.emit({ v: FRAME_VERSION, type: "lane.closed", laneId });
196
- void this.updateRegistry((lanes) => lanes.map((row) => (row.laneId === laneId ? { ...row, status: "closed" } : row)));
197
- void this.moveLaneSession(record, wasExpected ? "done" : "failed");
195
+ this.error(sessionId, "ADAPTER_FAILED", `claude exited unexpectedly (code ${String(code)})`);
196
+ this.emit({ v: FRAME_VERSION, type: "session.closed", sessionId });
197
+ void this.updateRegistry((sessions) => sessions.map((row) => (row.sessionId === sessionId ? { ...row, status: "closed" } : row)));
198
+ void this.moveSessionRow(record, wasExpected ? "done" : "failed");
198
199
  },
199
200
  };
200
- const create = this.options.createAdapter ?? ((opts) => new ClaudeLane(opts));
201
+ const create = this.options.createAdapter ?? ((opts) => new ClaudeSession(opts));
201
202
  let adapter;
202
203
  try {
203
204
  adapter = create(adapterOptions);
204
205
  }
205
206
  catch (error) {
206
- return this.error(laneId, "ADAPTER_FAILED", error instanceof Error ? error.message : String(error));
207
+ return this.error(sessionId, "ADAPTER_FAILED", error instanceof Error ? error.message : String(error));
207
208
  }
208
- this.lanes.set(laneId, { adapter, record });
209
- await this.updateRegistry((lanes) => [...lanes.filter((row) => row.laneId !== laneId), record]);
209
+ this.sessions.set(sessionId, { adapter, record });
210
+ await this.updateRegistry((sessions) => [...sessions.filter((row) => row.sessionId !== sessionId), record]);
210
211
  if (this.options.sessions && record.projectId) {
211
212
  try {
212
213
  await this.options.sessions.createSession(record.projectId, {
213
- id: laneId,
214
+ id: sessionId,
214
215
  startedBy: "person",
215
216
  substrate: "device",
216
217
  mode: "synchronous",
217
- origin: "inside",
218
+ nustack: 1,
218
219
  vendor: "claude_code",
219
220
  renderer: "claude_transcript",
220
221
  status: "created",
@@ -225,22 +226,22 @@ export class LaneSupervisor {
225
226
  }
226
227
  this.emit({
227
228
  v: FRAME_VERSION,
228
- type: "lane.opened",
229
- laneId,
229
+ type: "session.opened",
230
+ sessionId,
230
231
  projectPath,
231
232
  worktreePath: worktree.worktreePath,
232
233
  branch: worktree.branch,
233
234
  isolated: worktree.isolated,
234
235
  });
235
- this.status(laneId, "ready");
236
+ this.status(sessionId, "ready");
236
237
  }
237
238
  async shutdown() {
238
239
  if (this.shuttingDown)
239
240
  return;
240
241
  this.shuttingDown = true;
241
- for (const [, lane] of this.lanes)
242
- lane.adapter.stop();
243
- this.lanes.clear();
242
+ for (const [, session] of this.sessions)
243
+ session.adapter.stop();
244
+ this.sessions.clear();
244
245
  this.lines?.close();
245
246
  this.resolveDone?.();
246
247
  }
@@ -2,12 +2,12 @@ import { execFile } from "node:child_process";
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- export const LANE_BRANCH_PREFIX = "nustack/lane-";
6
- export function lanesHome(env = process.env) {
7
- const override = env.NUSTACK_LANES_HOME;
5
+ export const ENGINE_SESSION_BRANCH_PREFIX = "nustack/session-";
6
+ export function sessionsHome(env = process.env) {
7
+ const override = env.NUSTACK_SESSIONS_HOME;
8
8
  if (override && override !== "")
9
9
  return override;
10
- return path.join(os.homedir(), ".nustack", "lanes");
10
+ return path.join(os.homedir(), ".nustack", "sessions");
11
11
  }
12
12
  function git(args, cwd) {
13
13
  return new Promise((resolve) => {
@@ -16,15 +16,15 @@ function git(args, cwd) {
16
16
  });
17
17
  });
18
18
  }
19
- export async function createLaneWorktree(projectPath, laneId, env = process.env, isolate = false) {
19
+ export async function createSessionWorktree(projectPath, sessionId, env = process.env, isolate = false) {
20
20
  const inPlace = { worktreePath: projectPath, branch: null, isolated: false };
21
21
  if (!isolate)
22
22
  return inPlace;
23
23
  const isRepo = await git(["rev-parse", "--is-inside-work-tree"], projectPath);
24
24
  if (isRepo !== "true")
25
25
  return inPlace;
26
- const branch = `${LANE_BRANCH_PREFIX}${laneId}`;
27
- const worktreePath = path.join(lanesHome(env), "worktrees", laneId);
26
+ const branch = `${ENGINE_SESSION_BRANCH_PREFIX}${sessionId}`;
27
+ const worktreePath = path.join(sessionsHome(env), "worktrees", sessionId);
28
28
  await mkdir(path.dirname(worktreePath), { recursive: true });
29
29
  const added = await git(["worktree", "add", worktreePath, "-b", branch], projectPath);
30
30
  if (added === null)
@@ -9,7 +9,7 @@ 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
15
  if (!transcript.projectId) {
@@ -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
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/nustack",
3
- "version": "0.10.0-dev.18",
3
+ "version": "0.10.0-dev.19",
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
- }