@retasc/cli 1.11.0 → 1.12.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.
@@ -277,7 +277,33 @@ export async function completeWorkspaceSetup(args) {
277
277
  catch {
278
278
  /* binding written; whoami confirmation is best-effort */
279
279
  }
280
+ // RTSC-519 — say that setup FINISHED, and what to do with it.
281
+ //
282
+ // Here rather than in either command, because it is true of both and this is the tail
283
+ // they share. `join` said it and `bind` did not, so the person who set up their own org
284
+ // was left reading a status line and guessing whether anything else was required — while
285
+ // the invited teammate, who arrived through the other command, was told. A line in one
286
+ // caller is a line the other has to remember; a line here is one neither can lose.
287
+ //
288
+ // AFTER the confirmation, and after everything that can throw, so it is only ever
289
+ // printed by a run that actually finished. Nothing below it can fail.
290
+ if (isInteractive())
291
+ console.log(`\n${NEXT_STEP}`);
280
292
  }
293
+ /**
294
+ * What to do with a workspace that is now set up.
295
+ *
296
+ * "or restart it if it's already open" is the load-bearing half. `.mcp.json` is read when
297
+ * an MCP client starts, so an agent already running on this folder will not see Retasc
298
+ * until it restarts — the same trap RTSC-496 records for the Dash's version of this
299
+ * handover, where the screen otherwise waits forever on an agent that can never call in.
300
+ * It bites harder here: `bind` is routinely run from inside a session already open on the
301
+ * folder being bound.
302
+ *
303
+ * Deliberately NOT "run `retasc next`". The agent pulls work over MCP; the CLI's claim
304
+ * commands are a human convenience, and naming one here would teach the wrong first move.
305
+ */
306
+ export const NEXT_STEP = "Start your agent in this folder, or restart it if it's already open, and it'll pull from the queue.";
281
307
  /**
282
308
  * Sign in first when there is no session at all (RTSC-481, extended to `join` by RTSC-492).
283
309
  *
@@ -50,15 +50,19 @@ function report(outcome, orgLabel) {
50
50
  console.log("Run this again after a migration — each import brings its own people across.");
51
51
  return;
52
52
  case "answered":
53
- // The permanent decline. Worth its own wording: from in here it is indistinguishable
54
- // from "none", and someone who declined during an earlier import and now has a real
55
- // placeholder from a NEW one would otherwise read "nothing waiting" as the truth.
56
- console.log(`You've already said none of the imported people in ${orgLabel} are you.`);
57
- console.log("That answer covers the whole org, including later migrations.");
58
- console.log("If a newer import did carry you across, an owner can link it from the Team page.");
53
+ // They have answered here before, and nothing is left from a tool they haven't
54
+ // answered about. It differs from "none" only in being able to say what already
55
+ // happened; the advice is identical, because it is now TRUE for both.
56
+ //
57
+ // RTSC-507 rewrote this. It used to read "that answer covers the whole org,
58
+ // including later migrations" and send them to an owner because one decline really
59
+ // did shut them out of every future import. That sentence existed only to describe a
60
+ // bug, and it goes in the same release the bug does.
61
+ console.log(`You've answered about every tool imported into ${orgLabel} so far.`);
62
+ console.log("Run this again after the next migration — each one is asked about separately.");
59
63
  return;
60
64
  case "declined":
61
- console.log("Noted you won't be asked again.");
65
+ console.log("Noted. That covers those tools only, so a later import will ask again.");
62
66
  return;
63
67
  case "left":
64
68
  console.log("Nothing linked. Run `retasc identity` again whenever you want to look.");
@@ -16,6 +16,24 @@ const UNKNOWN_SOURCE = "another tool";
16
16
  * number of migrated tools.
17
17
  */
18
18
  const MAX_IDENTITY_ROUNDS = 10;
19
+ /**
20
+ * The tools on screen, named the way they'd be read aloud (RTSC-507).
21
+ *
22
+ * The twin of `sourcePhrase` in `dash/src/lib/ghostPrompt.ts`, duplicated rather than
23
+ * shared because the two live in separate packages with no common build — the same reason
24
+ * `UNKNOWN_SOURCE` above is a second copy of the Dash's constant. Keep the two in step:
25
+ * they answer the same question in front of the same irreversible decision.
26
+ */
27
+ export function sourcePhrase(ghosts) {
28
+ const names = [...new Set(ghosts.map((g) => clean(g.sourceLabel ?? UNKNOWN_SOURCE)))].sort((a, b) =>
29
+ // Keep the vague fallback last, so a known tool always leads.
30
+ a === UNKNOWN_SOURCE ? 1 : b === UNKNOWN_SOURCE ? -1 : a.localeCompare(b));
31
+ if (names.length <= 1)
32
+ return names[0] ?? UNKNOWN_SOURCE;
33
+ if (names.length === 2)
34
+ return `${names[0]} and ${names[1]}`;
35
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
36
+ }
19
37
  /** How one row reads: the person, and the tool they were carried in from. */
20
38
  function ghostRow(g) {
21
39
  // BOTH fields are third-party text: the name was typed by someone in ClickUp/Jira/Asana
@@ -62,13 +80,16 @@ deps = {}) {
62
80
  let ghosts;
63
81
  try {
64
82
  const res = await d.claimableGhosts({ orgId });
65
- // Two different endings the server returns on one shape. "Answered" is this human's
66
- // permanent org-wide decline; "none" is simply an empty list, and they can be asked
67
- // again after the next migration. `join` treats both as silence; `identity` does not.
68
- if (res.asked)
69
- return claimedAny ? "claimed" : "answered";
70
- if (!res.ghosts.length)
71
- return claimedAny ? "claimed" : "none";
83
+ // An empty list is the only ending now (RTSC-507). `asked` no longer gates anything
84
+ // it used to mean "declined, permanently", and branching on it FIRST is exactly
85
+ // what hid a later migration's placeholders for good. It now only picks the wording:
86
+ // "you've answered everything so far" reads differently from "nothing was ever
87
+ // imported here", and both are empty. `join` treats them alike; `identity` does not.
88
+ if (!res.ghosts.length) {
89
+ if (claimedAny)
90
+ return "claimed";
91
+ return res.asked ? "answered" : "none";
92
+ }
72
93
  ghosts = res.ghosts;
73
94
  }
74
95
  catch (e) {
@@ -80,14 +101,18 @@ deps = {}) {
80
101
  ? `\nWhen ${orgLabel ?? "this org"} migrated, some people were carried across.\n` +
81
102
  "One of them may be you:"
82
103
  : "\nAnything else here you recognise?");
83
- // "Don't ask again" is not decoration. Declining writes `ghostPromptDismissedAt` on the
84
- // member row and `claimGhost` refuses for good afterwards, org-wide so a bare "none
85
- // of these are me" would read as "none of these ClickUp ones" and quietly close the
86
- // door on a second tool's placeholder too.
104
+ // RTSC-507 the row says what the answer COVERS, because that is what changed.
105
+ // Declining used to write one org-wide flag that shut the person out of every later
106
+ // migration, so the row had to carry the warning "don't ask again". It now records
107
+ // only the tools on screen, so a bare "none of these are me" is finally the literal
108
+ // truth, and naming the tools is what makes the answer safe to give.
87
109
  // Compared by REFERENCE below, not by a magic id value: a sentinel that is merely a
88
- // row with an empty id would let any future ghost with a falsy id read as a decline
89
- // and a decline is permanent.
90
- const NONE = { id: "", name: "None of these are me (don't ask again)", sourceLabel: null };
110
+ // row with an empty id would let any future ghost with a falsy id read as a decline.
111
+ const NONE = {
112
+ id: "",
113
+ name: `None of these are me (covers ${sourcePhrase(ghosts)} only)`,
114
+ sourceLabel: null,
115
+ };
91
116
  // No label: the heading above is the prompt, and `choose` would print a second one.
92
117
  const chosen = await pickExisting("", [...ghosts, NONE], (g) => (g === NONE ? g.name : ghostRow(g)), d.askFn);
93
118
  if (chosen === NONE) {
@@ -96,10 +121,10 @@ deps = {}) {
96
121
  }
97
122
  catch (e) {
98
123
  console.error(` ! ${formatError(e).message}`);
99
- // NOT "declined". A caller answers that with "you won't be asked again", and the
100
- // write is the only thing that makes it true — the flag never landed, so they WILL
101
- // be asked again. The error is already on stderr; `unavailable` adds no second
102
- // sentence on top of it rather than a reassuring false one.
124
+ // NOT "declined". A caller answers that with "noted", and the write is the only
125
+ // thing that makes it true — nothing landed, so these tools WILL be offered again.
126
+ // The error is already on stderr; `unavailable` adds no second sentence on top of
127
+ // it rather than a reassuring false one.
103
128
  return "unavailable";
104
129
  }
105
130
  return "declined";
@@ -212,7 +237,6 @@ export async function joinAction(link, opts) {
212
237
  console.error(` Resume with: ${resume}`);
213
238
  throw e;
214
239
  }
215
- if (isInteractive()) {
216
- console.log("\nStart your agent in this folder and it'll pull from the queue.");
217
- }
240
+ // RTSC-519 — the closing line moved into `completeWorkspaceSetup`, the tail both commands
241
+ // already share, so `bind` gets it too. Saying it here as well would say it twice.
218
242
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Retasc CLI — sign in with GitHub, 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": {