@retasc/cli 1.40.0 → 1.41.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ release commits and the issues they reference.
6
6
 
7
7
  Dates are the npm publish date. Each entry names the RTSC issue behind it.
8
8
 
9
+ ## 1.41.0 (2026-09-04)
10
+
11
+ - **RTSC-825** — every workspace now carries the name of the folder it is bound to. The
12
+ server never receives a path, so the name can only come from the machine that has one:
13
+ `retasc bind` sends it when the key is minted, and the watchdog proxy re-sends it at
14
+ every session start, so a folder bound before this existed is named the next time an
15
+ agent runs there. Only the folder's own name travels, never the path above it, and it
16
+ is the folder the BINDING names — a session opened in a subdirectory still reports the
17
+ bound folder rather than renaming the row to whatever directory it started in. The
18
+ Dash shows it as `workspace` on the Agents page and on the Connect page, and shows
19
+ nothing at all where a workspace has not reported one; a key retired before this
20
+ shipped can never report one, so it stays blank.
21
+
9
22
  ## 1.40.0 (2026-09-04)
10
23
 
11
24
  - **RTSC-820** — every session now tells Retasc which conversation it is. `retasc setup`
@@ -521,6 +521,9 @@ export async function completeWorkspaceSetup(args) {
521
521
  // The server cleans the name and falls back to the project's own when the
522
522
  // leaf is empty.
523
523
  keyName: basename(cwd),
524
+ // RTSC-825 — and as the WORKSPACE, which is the field the Dash reads. `keyName`
525
+ // stays for the legacy label; nothing reads it as a folder any more.
526
+ workspace: basename(cwd),
524
527
  }));
525
528
  // The key is named in the receipt card below, not here — one mention, in the place
526
529
  // that says where it went (RTSC-673).
@@ -646,7 +649,7 @@ export async function setupFromToken(token, opts, deps = {}) {
646
649
  const cwd = process.cwd();
647
650
  const guard = deps.guard ?? rebindGuard;
648
651
  const redeem = deps.redeem ??
649
- ((t, label) => api.redeemSetupToken({ token: t, label }));
652
+ ((t, label, workspace) => api.redeemSetupToken({ token: t, label, workspace }));
650
653
  // BEFORE the redeem. `rebindGuard` prints its own explanation and, with no TTY,
651
654
  // refuses with exit 1 unless `--yes` — which is exactly the required behaviour, so it
652
655
  // is reused rather than restated. No org/project passed: its idempotent-converge
@@ -664,7 +667,12 @@ export async function setupFromToken(token, opts, deps = {}) {
664
667
  // RTSC-532 — the LEAF name, never the full path. It names the key in the Dash, so it
665
668
  // has to be recognisable ("client-a → ENG") without publishing where on her disk it
666
669
  // sits. `basename` of the cwd is exactly that.
667
- const redeemed = await redeem(token.trim(), basename(cwd));
670
+ //
671
+ // RTSC-825 — sent TWICE, on purpose: once as the legacy `label` and once as the
672
+ // `workspace`. They happen to be the same string here and nowhere is that a rule —
673
+ // the label is a human field a future caller may set to anything, so the folder
674
+ // travels in its own argument rather than being read back out of it.
675
+ const redeemed = await redeem(token.trim(), basename(cwd), basename(cwd));
668
676
  const workspaceId = existing?.workspaceId ?? newWorkspaceId();
669
677
  (deps.bind ?? setBinding)(workspaceId, {
670
678
  orgId: redeemed.orgId,
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { VERSION } from "./version.js";
6
6
  import { selfCommand, versionStamp } from "./lib/launcher.js";
7
7
  import { loadConfig, patchConfig, saveConfig, configPath, isLoggedIn } from "./config.js";
8
8
  import { installMcp, noteRuntimeIsALabel, normalizeScope } from "./commands/mcp.js";
9
- import { runSetup } from "./commands/setup.js";
9
+ import { runSetup, printSetup } from "./commands/setup.js";
10
10
  import { installGate, resolveGatePrefix } from "./commands/gate.js";
11
11
  import { claimAction, releaseAction } from "./commands/claim.js";
12
12
  import { bindAction, completeWorkspaceSetup, rebindGuard, setupFromToken } from "./commands/bind.js";
@@ -576,7 +576,7 @@ program
576
576
  .option("--no-install", "Don't install `retasc` on this machine; wire the pinned npx launcher instead")
577
577
  .allowExcessArguments(false)
578
578
  .action((opts) => {
579
- runSetup({ install: opts.install });
579
+ printSetup(runSetup({ install: opts.install, quiet: true }));
580
580
  });
581
581
  const mcp = program.command("mcp").description("Wire the Retasc MCP server into your agent.");
582
582
  mcp
@@ -202,6 +202,7 @@ export function resolveConn(opts) {
202
202
  // came from the keystore, so it travels only to the keystore's url. That is what
203
203
  // lets `auto` be written into a GLOBAL harness config: the entry is identical on
204
204
  // every machine and in every folder, and carries nothing worth stealing.
205
+ let boundPath = "";
205
206
  if (!key) {
206
207
  const wsId = env.RETASC_WORKSPACE || entry?.env?.RETASC_WORKSPACE;
207
208
  const b = wsId === AUTO_WORKSPACE
@@ -217,7 +218,8 @@ export function resolveConn(opts) {
217
218
  // url" is stated here outright rather than left resting on that invariant, so
218
219
  // restoring an unpaired url above cannot quietly resurrect the RTSC-800 redirect.
219
220
  url = b.url;
221
+ boundPath = b.boundPath ?? "";
220
222
  }
221
223
  }
222
- return { key, url: url || opts.defaultUrl || "" };
224
+ return { key, url: url || opts.defaultUrl || "", boundPath };
223
225
  }
@@ -5,9 +5,17 @@
5
5
  // sessions collapse into one server-side identity and the per-session claim fence
6
6
  // (RTSC-49/50) is silently inert.
7
7
  import { toolResult } from "./toolresult.js";
8
- // Out-of-band ids are negative so they never collide with the harness's (the
9
- // same convention as the proxy's heartbeat ids; announceBinding uses -1001).
10
- const MINT_RPC_ID = -1000;
8
+ // Out-of-band ids are negative so they never collide with the harness's (the same
9
+ // convention as the proxy's heartbeat ids). One id per out-of-band call, named rather
10
+ // than counted: the offsets these replaced had drifted into a collision, with
11
+ // `announceBinding` and `record_session` both sending -1001.
12
+ export const RPC_ID = {
13
+ mint: -1000,
14
+ announce: -1001,
15
+ recordSession: -1002,
16
+ nameWorkspace: -1003,
17
+ };
18
+ const MINT_RPC_ID = RPC_ID.mint;
11
19
  // The mint runs BEFORE the proxy serves `initialize`, so a hung server must not
12
20
  // stall startup past the harness's MCP timeout — that would turn "degraded
13
21
  // identity" into "no Retasc tools at all", the exact failure fail-soft exists
@@ -92,11 +100,17 @@ export function appendFallbackNotice(toolName, resp, degraded) {
92
100
  }
93
101
  }
94
102
  /**
95
- * Record the harness's transcript id on THIS session's key (RTSC-820). Best effort and
96
- * quiet: the worst case is the Dash saying "none recorded", which is what it said
97
- * before this existed. Returns true once the server has it (already-recorded counts).
103
+ * One out-of-band `tools/call` (RTSC-825). The proxy makes several of these around a
104
+ * session's start mint the session key, announce the binding, record the transcript,
105
+ * name the folder and they had been written out longhand each time, so a change to
106
+ * the transport (the auth header, the timeout, how a failure is surfaced) had to be
107
+ * made in three places and would have been made in one.
108
+ *
109
+ * Every caller here is best-effort: the session must serve MCP traffic whatever the
110
+ * server says, so this NEVER throws. `expect` is the field the tool returns on success;
111
+ * its presence is what separates a real result from an error envelope carrying prose.
98
112
  */
99
- export async function recordSession(opts) {
113
+ async function callTool(opts) {
100
114
  const fetchImpl = opts.fetchImpl ?? fetch;
101
115
  const warn = opts.warn ?? ((msg) => process.stderr.write(`[retasc] ${msg}\n`));
102
116
  try {
@@ -109,25 +123,58 @@ export async function recordSession(opts) {
109
123
  },
110
124
  body: JSON.stringify({
111
125
  jsonrpc: "2.0",
112
- id: MINT_RPC_ID - 1,
126
+ id: opts.id,
113
127
  method: "tools/call",
114
- params: {
115
- name: "record_session",
116
- arguments: { transcriptId: opts.transcriptId, ...(opts.harness ? { harness: opts.harness } : {}) },
117
- },
128
+ params: { name: opts.name, arguments: opts.args },
118
129
  }),
119
130
  signal: AbortSignal.timeout(MINT_TIMEOUT_MS),
120
131
  });
121
132
  const text = await res.text();
122
133
  if (!res.ok) {
123
- warn(`record_session failed (HTTP ${res.status})`);
124
- return false;
134
+ warn(`${opts.name} failed (HTTP ${res.status})`);
135
+ // 5xx is the server having a bad minute; anything else is it saying no.
136
+ return res.status >= 500 ? "unreachable" : "refused";
125
137
  }
126
138
  const r = toolResult(text ? JSON.parse(text) : null, warn);
127
- return !!(r && typeof r === "object" && "transcriptId" in r);
139
+ if (r && typeof r === "object" && opts.expect in r)
140
+ return "ok";
141
+ // A 200 carrying an error result: an unknown tool on an older deployment, a
142
+ // revoked key, the wrong kind of key. All of them are answers, not outages.
143
+ return "refused";
128
144
  }
129
145
  catch (e) {
130
- warn(`record_session failed: ${String(e?.message ?? e)}`);
131
- return false;
146
+ warn(`${opts.name} failed: ${String(e?.message ?? e)}`);
147
+ return "unreachable";
132
148
  }
133
149
  }
150
+ /**
151
+ * Record the harness's transcript id on THIS session's key (RTSC-820). Best effort and
152
+ * quiet: the worst case is the Dash saying "none recorded", which is what it said
153
+ * before this existed. Returns true once the server has it (already-recorded counts).
154
+ */
155
+ export async function recordSession(opts) {
156
+ const out = await callTool({
157
+ ...opts,
158
+ id: RPC_ID.recordSession,
159
+ name: "record_session",
160
+ args: { transcriptId: opts.transcriptId, ...(opts.harness ? { harness: opts.harness } : {}) },
161
+ expect: "transcriptId",
162
+ });
163
+ return out === "ok";
164
+ }
165
+ /**
166
+ * Tell Retasc which folder a workspace key is bound to (RTSC-825). Best effort and
167
+ * quiet, like `recordSession`: the worst case is the Dash omitting the workspace, which
168
+ * is what it did before this existed. "named" once the server holds this folder
169
+ * (already-correct counts).
170
+ */
171
+ export async function nameWorkspace(opts) {
172
+ const out = await callTool({
173
+ ...opts,
174
+ id: RPC_ID.nameWorkspace,
175
+ name: "name_workspace",
176
+ args: { workspace: opts.workspace },
177
+ expect: "workspace",
178
+ });
179
+ return out === "ok" ? "named" : out;
180
+ }
package/dist/proxy.js CHANGED
@@ -8,12 +8,12 @@
8
8
  import { createInterface } from "node:readline";
9
9
  import { hostname } from "node:os";
10
10
  import { spawn, spawnSync } from "node:child_process";
11
- import { dirname, resolve } from "node:path";
11
+ import { basename, dirname, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { applyObservation, heartbeatRequest, isClaimLost, isUnauthorized, shouldReapOnClose, untrackedLeaseWarning, } from "./lib/watchdog.js";
14
14
  import { AUTO_WORKSPACE, resolveConn } from "./lib/keystore.js";
15
15
  import { toolResult as parseTool } from "./lib/toolresult.js";
16
- import { mintSessionKey, appendFallbackNotice, recordSession } from "./lib/session.js";
16
+ import { mintSessionKey, appendFallbackNotice, recordSession, nameWorkspace, RPC_ID } from "./lib/session.js";
17
17
  import { readHookRecord, clearHookRecord } from "./lib/sessionHook.js";
18
18
  import { attachRoot, isLocalAttachCall, mergeAttachTool, readAttachFile, resolveAttachPath, uploadFailureMessage, uploadUrlWith, } from "./lib/attachFile.js";
19
19
  import { MAX_FETCH_BYTES, downloadFailureMessage, existingDownload, isLocalFetchCall, mergeFetchTool, resolveDownloadTarget, writeDownloadedFile, } from "./lib/fetchFile.js";
@@ -251,7 +251,7 @@ async function announceBinding() {
251
251
  try {
252
252
  const resp = await postRemote({
253
253
  jsonrpc: "2.0",
254
- id: -1001,
254
+ id: RPC_ID.announce,
255
255
  method: "tools/call",
256
256
  params: { name: "whoami", arguments: {} },
257
257
  });
@@ -627,6 +627,20 @@ export async function runProxy() {
627
627
  // attributed to this session. stdin buffers in the OS pipe meanwhile.
628
628
  await adoptSessionKey();
629
629
  await announceBinding();
630
+ // RTSC-825 — the folder this session runs in IS its workspace, and this process is
631
+ // the only thing that knows it. Sent on the WORKSPACE key (not the session child just
632
+ // adopted), so a folder bound before the CLI reported one, or renamed since, corrects
633
+ // itself the next time an agent starts. A no-op server-side when it already matches.
634
+ //
635
+ // The BOUND folder, not the cwd. The `auto` marker resolves this key by walking up
636
+ // from wherever the harness was started, so a session opened in `retasc/dash` runs on
637
+ // `retasc`'s key — naming it `dash` would be wrong, and the next session from the root
638
+ // would rename it back, flapping the row on every start. Empty for a key that came
639
+ // from the environment rather than the keystore: that one names no folder.
640
+ const BOUND_FOLDER = resolved.boundPath ? basename(resolved.boundPath) : "";
641
+ if (KEY && BOUND_FOLDER) {
642
+ nameWorkspace({ url: MCP_URL, key: KEY, workspace: BOUND_FOLDER, warn: log }).catch((e) => log(`name_workspace: ${String(e?.message ?? e)}`));
643
+ }
630
644
  reportTranscript().catch((e) => log(`record_session: ${String(e?.message ?? e)}`));
631
645
  const timer = setInterval(() => void heartbeatAll(), HEARTBEAT_MS);
632
646
  timer.unref?.(); // the timer alone must not keep the process alive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "Retasc CLI — the issue tracker AI agents pull work from. Sign in with GitHub or Google, create projects, mint agent API keys, and wire your agent to the Retasc MCP server in one command.",
5
5
  "type": "module",
6
6
  "bin": {