@retasc/cli 1.38.0 → 1.38.2

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,37 @@ 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.38.2 (2026-08-30)
10
+
11
+ - **RTSC-789** — `bind` keeps this folder's key instead of minting a new one on every run.
12
+ The guard meant to prevent that (RTSC-262) read the `--org-id`/`--project-id` flags, so it
13
+ only ever fired for a provisioning script. The canonical `npx @retasc/cli@latest bind`
14
+ leaves both undefined and could never reach it: pick the same org and the same project
15
+ from the menu and it minted anyway, leaving the old key live. The check now runs where
16
+ the target is known, after the pick, so how you got there stops mattering. A different
17
+ org or project still mints, as it must.
18
+ A kept key is resolved against the server before it is kept. `bind` clears a binding only
19
+ when the server answers `UNAUTHORIZED`, so a folder can reach the reuse branch holding a
20
+ key whose health is unknown: a 5xx, a proxy sign-in page, a refusal whose wording drifts.
21
+ Keeping one of those would have stranded the folder for good, since `doctor` sends that
22
+ exact state back to `bind`. It falls back to minting instead, which is the self-healing
23
+ the old mint-every-run was providing by accident.
24
+
25
+ ## 1.38.1 (2026-08-28)
26
+
27
+ - **RTSC-780 follow-up** — `retasc init` now sets a folder up the same way every other
28
+ door does. 1.38.0 taught `bind`, `join` and `bind --setup` to wire every harness on the
29
+ machine, but `init` kept its own copy of that work: it minted a key and wrote a
30
+ key-bearing entry for Claude Code and nothing else. So the one command named onboarding
31
+ was the one that still left Codex with no Retasc tools, which is the exact failure
32
+ 1.38.0 exists to end.
33
+ Fixed by deleting the duplicate rather than teaching it the same trick. Everything after
34
+ the org is now `bind`'s job, which also means `init` writes the keystore binding an
35
+ `auto` marker resolves against, guards a folder that is already bound instead of
36
+ overwriting it, and prints the same receipt.
37
+ `--scope` and `--no-watchdog` are gone from `init`: both described the shape of an entry
38
+ it no longer writes. `-y/--yes` and `--no-install` are there instead, matching `bind`.
39
+
9
40
  ## 1.38.0 (2026-08-28)
10
41
 
11
42
  - **RTSC-780** — `retasc setup` wires Retasc into every MCP harness on the machine, once,
@@ -99,6 +99,55 @@ export function strandRecoveryHint(args) {
99
99
  `but has no project or key yet — re-run to finish:\n` +
100
100
  ` retasc bind --org-id ${orgId}`);
101
101
  }
102
+ /**
103
+ * The key this folder should KEEP rather than replace (RTSC-789).
104
+ *
105
+ * `bind` used to mint on every run. The guard that was supposed to stop that
106
+ * (RTSC-262) sat in `preflight` and read `args.orgId && args.projectId`, so it
107
+ * only fired for `retasc bind --org-id X --project-id Y` — the provisioning-script
108
+ * path it was written for. The canonical command is a bare `bind`, which leaves
109
+ * both undefined, so the branch was unreachable for almost every real run: pick
110
+ * the same org and the same project from the menu and it minted anyway. Fourteen
111
+ * keys in one org, three of them the same agent on the same project, and the old
112
+ * ones stay live — two were carrying 89 and 283 sessions.
113
+ *
114
+ * So the check moves to where the target is actually KNOWN, after the interactive
115
+ * pick, and asks the only question that matters: does this folder already hold a
116
+ * key for this exact (org, project)? Flags are irrelevant to that.
117
+ *
118
+ * A DIFFERENT org or project still mints.
119
+ *
120
+ * This names a CANDIDATE, not a decision. It reads the keystore and nothing else,
121
+ * so it cannot know whether the key still works — and "`preflight` already cleared
122
+ * anything the server refused" is not true enough to lean on: that clearing fires
123
+ * only on a literal UNAUTHORIZED, and every other failure walks straight past it.
124
+ * The caller resolves the candidate before keeping it.
125
+ */
126
+ export function reusableKey(existing, target, read = getBinding) {
127
+ if (!existing?.workspaceId)
128
+ return null;
129
+ const cur = read(existing.workspaceId);
130
+ if (!cur?.key)
131
+ return null;
132
+ return cur.orgId === target.orgId && cur.projectId === target.projectId ? cur.key : null;
133
+ }
134
+ /**
135
+ * The binding as the AGENT will see it, or null when the server won't confirm it.
136
+ *
137
+ * One place, because two callers need the same answer for different reasons: the
138
+ * receipt prints it, and the reuse decision above depends on it. Every failure is
139
+ * the same answer here — "the server did not confirm this key" — so the caller
140
+ * decides what that means rather than parsing an error a second time.
141
+ */
142
+ async function describeBinding(url, key) {
143
+ try {
144
+ const b = await resolveBinding(url, key);
145
+ return { org: clean(b.org.name), prefix: clean(b.project.prefix), name: b.project.name };
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
102
151
  /**
103
152
  * "This folder is already bound — replace it?" (RTSC-262/RTSC-91)
104
153
  *
@@ -423,14 +472,37 @@ export async function completeWorkspaceSetup(args) {
423
472
  const note = launcherNote(launcher);
424
473
  if (note)
425
474
  console.log(note);
426
- // --- mint a key for THIS (org, project) and wire the watchdog into THIS folder
427
- const minted = (await api.mintKey({
428
- orgId,
429
- projectId: projectId,
430
- agentName: opts.agent,
431
- runtime: opts.runtime ?? "claude-code",
432
- keyName: prefix ? `${prefix} key` : undefined,
433
- }));
475
+ // --- keep this folder's key if it already names this (org, project), else mint
476
+ //
477
+ // A key is only kept once the SERVER agrees it works. `preflight` clears a binding
478
+ // only on a literal UNAUTHORIZED, so a folder reaches here holding a key of unknown
479
+ // health whenever that check failed some other way — a 5xx, a proxy page, a refusal
480
+ // whose wording drifts off the regex. Keeping such a key silently would strand the
481
+ // folder permanently: `doctor` tells people in exactly that state to re-run `bind`,
482
+ // and `bind` is this path, so it would answer "✓ bound" forever and never heal. The
483
+ // mint-on-every-run this issue removes was doing that healing by accident; it stays,
484
+ // on purpose, as the fallback.
485
+ //
486
+ // The check is not extra work — it IS the receipt's confirmation call, hoisted, and
487
+ // `bound` carries the answer down to the card. The mint path resolves once below,
488
+ // exactly as before.
489
+ const candidate = reusableKey(existing, { orgId, projectId: projectId });
490
+ let bound = candidate ? await describeBinding(cfg.mcpUrl, candidate) : null;
491
+ // A key that answers for a DIFFERENT project is not this folder's key, whatever the
492
+ // keystore says its ids were. Compared only when the target's prefix is known (a
493
+ // bare --project-id never listed one), because a guess would reject good keys.
494
+ if (bound && prefix && bound.prefix !== clean(prefix))
495
+ bound = null;
496
+ const reused = bound !== null;
497
+ const minted = reused
498
+ ? { key: candidate }
499
+ : (await api.mintKey({
500
+ orgId,
501
+ projectId: projectId,
502
+ agentName: opts.agent,
503
+ runtime: opts.runtime ?? "claude-code",
504
+ keyName: prefix ? `${prefix} key` : undefined,
505
+ }));
434
506
  // The key is named in the receipt card below, not here — one mention, in the place
435
507
  // that says where it went (RTSC-673).
436
508
  // RTSC-92: the secret stays OUT of the repo. Store it in the home keystore
@@ -460,21 +532,24 @@ export async function completeWorkspaceSetup(args) {
460
532
  // (RTSC-673). Resolved rather than echoed from what we just sent: this is the last
461
533
  // chance to notice a folder bound to something other than what was asked for, and a
462
534
  // card built from our own inputs could never show that.
463
- let bound = null;
464
- try {
465
- const b = await resolveBinding(cfg.mcpUrl, minted.key);
466
- bound = { org: clean(b.org.name), prefix: clean(b.project.prefix), name: b.project.name };
467
- }
468
- catch {
469
- /* binding written; whoami confirmation is best-effort — fall back to what we sent */
470
- }
535
+ //
536
+ // A reused key was already resolved above, and that answer is the one that decided to
537
+ // keep it so only a freshly minted key is left to confirm. Still best-effort here:
538
+ // the binding is written and a key we just minted is live by construction, so a
539
+ // network fault at this point is about the confirmation, not about the key.
540
+ if (!bound)
541
+ bound = await describeBinding(cfg.mcpUrl, minted.key);
471
542
  const orgLabel = bound?.org ?? clean(args.orgLabel ?? "");
472
543
  const pfx = bound?.prefix ?? clean(prefix ?? "");
473
544
  console.log("\n" +
474
545
  card(`✓ This folder is bound to ${orgLabel} / ${pfx}`, [
475
546
  { label: "Org", value: orgLabel },
476
547
  { label: "Project", value: bound?.name ? `${pfx} ${DOT} ${bound.name}` : pfx },
477
- { label: "Agent key", value: `${minted.key.slice(0, 14)}…`, note: "never leaves ~/.retasc" },
548
+ {
549
+ label: "Agent key",
550
+ value: `${minted.key.slice(0, 14)}…`,
551
+ note: reused ? "kept, not replaced" : "never leaves ~/.retasc",
552
+ },
478
553
  { label: "MCP wired", value: marker.where, note: "secret-free, safe to commit" },
479
554
  ...(wired.wired.length
480
555
  ? [
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { installMcp, noteRuntimeIsALabel, normalizeScope } from "./commands/mcp.
7
7
  import { runSetup } from "./commands/setup.js";
8
8
  import { installGate, resolveGatePrefix } from "./commands/gate.js";
9
9
  import { claimAction, releaseAction } from "./commands/claim.js";
10
- import { bindAction, setupFromToken } from "./commands/bind.js";
10
+ import { bindAction, completeWorkspaceSetup, rebindGuard, setupFromToken } from "./commands/bind.js";
11
11
  import { unbindAction } from "./commands/unbind.js";
12
12
  import { joinAction } from "./commands/join.js";
13
13
  import { chooseInviteOrg, chooseInviteProjects } from "./commands/invite.js";
@@ -146,8 +146,8 @@ program
146
146
  .requiredOption("--prefix <PREFIX>", "Project prefix, e.g. XEN")
147
147
  .option("--agent <name>", "Agent member name (default: auto, \"{you}'s {runtime}\")")
148
148
  .option("--runtime <runtime>", "Label for this agent in the Dash (claude-code | codex | grok | …). Does NOT choose where MCP config is written — `retasc setup` wires every harness on the machine.", "claude-code")
149
- .option("--scope <scope>", "MCP install scope: local | project (per-folder only)", "local")
150
- .option("--no-watchdog", "Wire a plain direct connection instead of the liveness watchdog")
149
+ .option("-y, --yes", "Don't prompt to confirm replacing an existing binding")
150
+ .option("--no-install", "Don't install `retasc` on this machine; wire the pinned npx launcher instead")
151
151
  .action(async (opts) => {
152
152
  requireLogin();
153
153
  try {
@@ -159,22 +159,43 @@ program
159
159
  orgId = org.orgId;
160
160
  console.log(`✓ Created org (${org.slug}).`);
161
161
  }
162
- const project = (await api.createProject({ orgId: orgId, name: opts.project, prefix: opts.prefix }));
163
- console.log(`✓ Created project ${project.prefix}.`);
164
- const minted = (await api.mintKey({
162
+ // RTSC-780 follow-up: everything after the org is `bind`'s job now.
163
+ //
164
+ // This command used to mint a key and call `installMcp` itself, which wrote a
165
+ // key-bearing entry for Claude Code and nothing else. When RTSC-780 taught the
166
+ // other three doors (`bind`, `join`, `bind --setup`) to wire every harness on the
167
+ // machine, this one was left behind — so the one command named "onboarding" was
168
+ // the one that still left Codex with no Retasc tools, the exact failure RTSC-780
169
+ // exists to end.
170
+ //
171
+ // Fixed by DELETING the duplicate rather than teaching it the same trick.
172
+ // `completeWorkspaceSetup` already creates the project, mints, writes the keystore
173
+ // binding (which is what an `auto` marker resolves against — without it a marker
174
+ // has nothing to find), installs the folder marker, runs setup and prints the
175
+ // receipt. Two implementations of "set this folder up" is how they drift apart,
176
+ // and this is the second time they did.
177
+ //
178
+ // `--scope` and `--no-watchdog` are gone with it: both described the shape of an
179
+ // entry that is no longer written here. The marker is per-folder and secret-free,
180
+ // and the watchdog is not optional in it.
181
+ const cfg = patchConfig({ defaultProjectPrefix: opts.prefix });
182
+ const guard = await rebindGuard({ cwd: process.cwd(), mcpUrl: cfg.mcpUrl, yes: opts.yes });
183
+ if (!guard.proceed)
184
+ return;
185
+ await completeWorkspaceSetup({
165
186
  orgId: orgId,
166
- projectId: project.projectId,
167
- agentName: opts.agent, // undefined → backend auto-names "{you}'s {runtime}"
168
- runtime: opts.runtime,
169
- keyName: `${project.prefix} key`,
170
- }));
171
- console.log(`✓ Minted API key (${minted.key.slice(0, 14)}…).`);
172
- // RTSC-91 (§13, override #2): never persist a global default org — binding
173
- // is a per-folder act. (The prefix is kept only as a gate convenience.)
174
- const cfg = patchConfig({ defaultProjectPrefix: project.prefix });
175
- console.log("");
176
- noteRuntimeIsALabel(opts.runtime);
177
- installMcp({ url: cfg.mcpUrl, key: minted.key, scope: normalizeScope(opts.scope), watchdog: opts.watchdog });
187
+ orgLabel: opts.org,
188
+ opts: {
189
+ project: opts.project,
190
+ prefix: opts.prefix,
191
+ agent: opts.agent,
192
+ runtime: opts.runtime,
193
+ install: opts.install,
194
+ yes: opts.yes,
195
+ },
196
+ existing: guard.existing,
197
+ canCreateProject: true,
198
+ });
178
199
  }
179
200
  catch (e) {
180
201
  fail(e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.38.0",
3
+ "version": "1.38.2",
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": {