@retasc/cli 1.45.0 → 1.47.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 +48 -0
- package/dist/commands/doctor.js +11 -1
- package/dist/commands/mcp.js +44 -0
- package/dist/commands/setup.js +8 -0
- package/dist/index.js +8 -2
- package/dist/lib/harness.js +130 -4
- package/dist/lib/skill.js +110 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,54 @@ 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.47.0 (2026-09-08)
|
|
10
|
+
|
|
11
|
+
- **RTSC-864** — `retasc key mint` now prints every remote-key config block a hosted
|
|
12
|
+
agent can paste, including the TOML one that did not exist.
|
|
13
|
+
|
|
14
|
+
A key is shown once, and until now that was all `key mint` printed. Anyone wiring a
|
|
15
|
+
host that cannot run our proxy (a claude.ai connector, Codex Cloud, CI) had to invent
|
|
16
|
+
the config, and for two of the six harnesses it was not inventable: `tomlBlock` emitted
|
|
17
|
+
only the stdio proxy shape, so Codex and Grok users had nothing correct to paste at all.
|
|
18
|
+
The new `tomlHttpBlock` fills that in, and all three blocks now print next to the key
|
|
19
|
+
while it is still on the screen.
|
|
20
|
+
|
|
21
|
+
The two TOML dialects are separate on purpose, and the reason is the dangerous part.
|
|
22
|
+
Verified against both real binaries: Codex reads `[mcp_servers.retasc.http_headers]`
|
|
23
|
+
and Grok reads `[mcp_servers.retasc.headers]`, and each loads the other's file without
|
|
24
|
+
a warning while ignoring the header. The result is a server that reports itself
|
|
25
|
+
enabled and configured, and returns UNAUTHORIZED on the first tool call.
|
|
26
|
+
|
|
27
|
+
The blocks also say, next to themselves, that a key belongs in a user-scope config or
|
|
28
|
+
a platform secret store and never in a tracked `.mcp.json`, which the repo's pre-commit
|
|
29
|
+
hook refuses anyway. Where a container DOES have an environment, the proxy with
|
|
30
|
+
`RETASC_MCP_KEY` set is still the better door: it renews leases for you.
|
|
31
|
+
|
|
32
|
+
## 1.46.0 (2026-09-08)
|
|
33
|
+
|
|
34
|
+
- **RTSC-855** — `retasc doctor` and `retasc setup` now say whether the **Retasc Agent
|
|
35
|
+
Skill** is installed on this machine, and print the one command that installs it.
|
|
36
|
+
|
|
37
|
+
Wiring the MCP server gives an agent the ability to claim work. The skill is what tells
|
|
38
|
+
it what a lease is, that a claim needs its own worktree, what a checkpoint is for, and
|
|
39
|
+
how a handoff into review works. Nothing connected the two, so an agent could be fully
|
|
40
|
+
wired and still have the tools without the manual, and no surface anywhere would
|
|
41
|
+
mention it. The server cannot fill the gap: MCP carries a bearer token and no
|
|
42
|
+
filesystem, so only something running on the machine can look.
|
|
43
|
+
|
|
44
|
+
The hard part is not finding the file, it is not being wrong about it. A false "not
|
|
45
|
+
installed" shown to somebody who followed our own instructions teaches them to distrust
|
|
46
|
+
everything else `doctor` says, so the check knows all three routes a skill really
|
|
47
|
+
arrives by: global (`~/.claude/skills` and each harness's equivalent), project (the
|
|
48
|
+
`skills` installer's DEFAULT scope, which lands in the working directory), and, for
|
|
49
|
+
Claude Code, an installed plugin's own folder, since `/plugin install retasc@retasc` is
|
|
50
|
+
one of the routes our public README offers and it writes none of the above. It honours
|
|
51
|
+
`CLAUDE_CONFIG_DIR`, `CODEX_HOME` and `GROK_HOME` for the same reason. The printed
|
|
52
|
+
command carries `-g` so it installs where the check looks. A harness whose convention
|
|
53
|
+
we cannot verify is skipped rather than accused, one harness holding the skill silences
|
|
54
|
+
the nudge for the rest, absence is reported with `ℹ` rather than `✗`, and neither
|
|
55
|
+
command changes its exit code over any of it.
|
|
56
|
+
|
|
9
57
|
## 1.45.0 (2026-09-08)
|
|
10
58
|
|
|
11
59
|
- **RTSC-862** — a binding now covers every **git worktree** of the repo it was made in.
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,9 @@ import { loadConfig } from "../config.js";
|
|
|
2
2
|
import { claudeConfigPath, isNetworkError, readGlobalBinding, readLocalBinding, readShadowedBinding, resolveBinding, sameIdentity, } from "../lib/binding.js";
|
|
3
3
|
import { getBinding } from "../lib/keystore.js";
|
|
4
4
|
import { gitCommonDir } from "../lib/gitRepo.js";
|
|
5
|
+
import { detectHarnesses } from "../lib/harness.js";
|
|
5
6
|
import { runsOk } from "../lib/launcher.js";
|
|
7
|
+
import { skillLines, skillReport } from "../lib/skill.js";
|
|
6
8
|
import { clean } from "../lib/text.js";
|
|
7
9
|
// RTSC-91 (DESIGN §13): `retasc doctor` — confirm THIS folder is correctly and
|
|
8
10
|
// safely bound. The question a human actually has is "which org/project does
|
|
@@ -217,7 +219,15 @@ export async function doctorAction() {
|
|
|
217
219
|
` own, so issues can land in the wrong project.\n` +
|
|
218
220
|
` Fix: claude mcp remove -s user retasc`);
|
|
219
221
|
}
|
|
220
|
-
// 3)
|
|
222
|
+
// 3) Does the agent that will use this binding have the manual? (RTSC-855)
|
|
223
|
+
//
|
|
224
|
+
// A machine fact rather than a folder fact, so it comes after both folder checks.
|
|
225
|
+
// It is still a check, not a footnote: everything above proves an agent can REACH
|
|
226
|
+
// Retasc, and this is the one that says whether it will know what to do when it
|
|
227
|
+
// gets there. Absence never touches the exit code, and `doctorAction` sets none.
|
|
228
|
+
for (const line of skillLines(skillReport(detectHarnesses())))
|
|
229
|
+
console.log(line);
|
|
230
|
+
// 4) Ambient context, not a verdict on this folder — so it trails the checks
|
|
221
231
|
// rather than framing them, and on macOS it prints nothing at all.
|
|
222
232
|
const note = platformNote();
|
|
223
233
|
if (note)
|
package/dist/commands/mcp.js
CHANGED
|
@@ -4,6 +4,9 @@ import { join } from "node:path";
|
|
|
4
4
|
import { resolveLauncher, launcherNote, portableLauncher, } from "../lib/launcher.js";
|
|
5
5
|
import { VERSION } from "../version.js";
|
|
6
6
|
import { hasClaudeLocalPlaceholder } from "../lib/binding.js";
|
|
7
|
+
// The TOML dialects live with the rest of the TOML knowledge (harness.ts), not here:
|
|
8
|
+
// the same file has to keep Codex's and Grok's config shapes straight for the proxy form.
|
|
9
|
+
import { tomlHttpBlock } from "../lib/harness.js";
|
|
7
10
|
export const SERVER_NAME = "retasc";
|
|
8
11
|
/** Normalize a user-supplied scope string. `user` (global) is refused and
|
|
9
12
|
* downgraded to `local`, loudly — per-folder binding is the only right way. */
|
|
@@ -29,6 +32,47 @@ export function mcpServerEntry(url, key) {
|
|
|
29
32
|
export function mcpConfigBlock(url, key) {
|
|
30
33
|
return JSON.stringify({ mcpServers: { [SERVER_NAME]: mcpServerEntry(url, key) } }, null, 2);
|
|
31
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Every direct-form block, labelled with the harnesses that read it (RTSC-864).
|
|
37
|
+
*
|
|
38
|
+
* The direct form is for a host that cannot spawn our proxy at all, such as a claude.ai
|
|
39
|
+
* connector, Codex Cloud or a hosted OpenCode, where an HTTP URL and a bearer key is the
|
|
40
|
+
* only door. Two config languages and three dialects, because `tomlHttpBlock` documents
|
|
41
|
+
* that Codex and Grok silently ignore each other's header key.
|
|
42
|
+
*
|
|
43
|
+
* Printed by `key mint` in the same breath as the key itself, and NOT only when some
|
|
44
|
+
* install failed: the key is shown once, so every form a person might have to paste it
|
|
45
|
+
* into has to be on the screen while it is still readable. Sending them to a doc after
|
|
46
|
+
* the one credential they were given has scrolled away is how a key gets re-minted.
|
|
47
|
+
*/
|
|
48
|
+
export function directFormBlocks(url, key) {
|
|
49
|
+
return [
|
|
50
|
+
"Remote-key config, for a host that cannot run a local process",
|
|
51
|
+
"(claude.ai connectors, Codex Cloud, CI, any hosted agent):",
|
|
52
|
+
"",
|
|
53
|
+
" JSON, for Claude Code, Cursor, OpenCode and Gemini CLI:",
|
|
54
|
+
"",
|
|
55
|
+
mcpConfigBlock(url, key),
|
|
56
|
+
"",
|
|
57
|
+
" TOML, for Codex (~/.codex/config.toml):",
|
|
58
|
+
"",
|
|
59
|
+
tomlHttpBlock(url, key, "codex"),
|
|
60
|
+
"",
|
|
61
|
+
" TOML, for Grok (~/.grok/config.toml):",
|
|
62
|
+
"",
|
|
63
|
+
tomlHttpBlock(url, key, "grok"),
|
|
64
|
+
"",
|
|
65
|
+
// The hook is not a style preference and the reason belongs next to the block that
|
|
66
|
+
// would trip it: a committed key is readable by everyone who ever clones the repo,
|
|
67
|
+
// and stays readable in git history after it is deleted.
|
|
68
|
+
"These blocks carry a live credential. Put them in the harness's own user-scope",
|
|
69
|
+
"config or the platform's secret store, never in a tracked `.mcp.json`. Retasc's own",
|
|
70
|
+
"repo refuses one at pre-commit, because a key committed once stays readable in git",
|
|
71
|
+
"history long after it is deleted.",
|
|
72
|
+
"Where the host DOES give you an environment, prefer the proxy with `RETASC_MCP_KEY`",
|
|
73
|
+
"set as a secret: it keeps your leases alive for you.",
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
32
76
|
/**
|
|
33
77
|
* The stdio server entry that runs the liveness watchdog proxy (RTSC-44). The
|
|
34
78
|
* harness spawns `retasc mcp-proxy`, which forwards to the remote MCP and keeps
|
package/dist/commands/setup.js
CHANGED
|
@@ -18,6 +18,7 @@ import { wireClaudeHook, hookLauncher } from "../lib/sessionHook.js";
|
|
|
18
18
|
import { AUTO_WORKSPACE } from "../lib/keystore.js";
|
|
19
19
|
import { HARNESSES, detectHarnesses, tildePath } from "../lib/harness.js";
|
|
20
20
|
import { resolveLauncher, launcherNote } from "../lib/launcher.js";
|
|
21
|
+
import { skillLines, skillReport } from "../lib/skill.js";
|
|
21
22
|
import { VERSION } from "../version.js";
|
|
22
23
|
import { card } from "../lib/card.js";
|
|
23
24
|
/** The one entry every harness gets: the watchdog proxy, resolving its own folder. */
|
|
@@ -49,6 +50,7 @@ export function runSetup(opts) {
|
|
|
49
50
|
wired: [],
|
|
50
51
|
failed: [],
|
|
51
52
|
absent: known.filter((h) => !presentIds.has(h.id)).map((h) => h.label),
|
|
53
|
+
skill: skillReport(present),
|
|
52
54
|
};
|
|
53
55
|
for (const h of present) {
|
|
54
56
|
const outcome = h.install(entry);
|
|
@@ -98,6 +100,12 @@ export function printSetup(r) {
|
|
|
98
100
|
}
|
|
99
101
|
if (r.absent.length)
|
|
100
102
|
console.log(` Not on this machine: ${r.absent.join(", ")}.`);
|
|
103
|
+
// RTSC-855 — next to the harness rows, because it is the same kind of fact: what this
|
|
104
|
+
// machine has, and what it is still missing. `setup` wires the tools; the skill is the
|
|
105
|
+
// document that tells an agent what to do with them, and nothing else on this receipt
|
|
106
|
+
// would ever mention that it is absent. Never a failure, and never installed for you.
|
|
107
|
+
for (const line of skillLines(r.skill))
|
|
108
|
+
console.log(line);
|
|
101
109
|
console.log("\nThese entries carry no key and name no project, so they are correct in every folder.\n" +
|
|
102
110
|
"Run `retasc bind` in a project to say which org it belongs to, then start your agent\n" +
|
|
103
111
|
"there (or restart it if it's already open).");
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
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
|
-
import { installMcp, noteRuntimeIsALabel, normalizeScope } from "./commands/mcp.js";
|
|
8
|
+
import { installMcp, noteRuntimeIsALabel, normalizeScope, directFormBlocks } from "./commands/mcp.js";
|
|
9
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";
|
|
@@ -379,8 +379,14 @@ key
|
|
|
379
379
|
}));
|
|
380
380
|
console.log(`✓ Minted key: ${res.key}`);
|
|
381
381
|
console.log(" (Shown once — store it now.)");
|
|
382
|
+
// RTSC-864: every paste form, now, while the key is still on the screen. A hosted
|
|
383
|
+
// agent or a container is the reason most people run this command at all, and
|
|
384
|
+
// until now it printed a bare key and left them to invent the config, which for
|
|
385
|
+
// Codex and Grok was not inventable, since no direct-form TOML block existed.
|
|
386
|
+
const cfg = loadConfig();
|
|
387
|
+
console.log("");
|
|
388
|
+
console.log(directFormBlocks(cfg.mcpUrl, res.key));
|
|
382
389
|
if (opts.install) {
|
|
383
|
-
const cfg = loadConfig();
|
|
384
390
|
console.log("");
|
|
385
391
|
noteRuntimeIsALabel(opts.runtime);
|
|
386
392
|
installMcp({ url: cfg.mcpUrl, key: res.key, scope: normalizeScope(opts.scope), watchdog: true });
|
package/dist/lib/harness.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { spawnSync } from "node:child_process";
|
|
27
27
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
28
|
import { homedir } from "node:os";
|
|
29
|
-
import { dirname, join } from "node:path";
|
|
29
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
30
30
|
/**
|
|
31
31
|
* The home directory harness configs are resolved under.
|
|
32
32
|
*
|
|
@@ -48,6 +48,38 @@ export function tildePath(p) {
|
|
|
48
48
|
const h = home();
|
|
49
49
|
return p.startsWith(h + "/") ? `~${p.slice(h.length)}` : p;
|
|
50
50
|
}
|
|
51
|
+
// --- skills directories -----------------------------------------------------
|
|
52
|
+
//
|
|
53
|
+
// Not derived from theory, same rule as the rest of this registry: every path below is a
|
|
54
|
+
// directory the `skills` installer (agentskills.io, the tool `SKILL_INSTALL_CMD` runs)
|
|
55
|
+
// reads for that harness, taken from its published agent table rather than guessed at. If
|
|
56
|
+
// a harness ever grew a convention we could not verify, the honest entry would be an empty
|
|
57
|
+
// list. "We do not know where yours lives" is a finding a human can act on, a made-up path
|
|
58
|
+
// is not.
|
|
59
|
+
//
|
|
60
|
+
// TWO scopes, because the installer has two and defaults to the one a global-only check
|
|
61
|
+
// would never see (`installGlobally = options.global ?? false`, and its interactive prompt
|
|
62
|
+
// offers Project first). A check that read only the home directory would call the skill
|
|
63
|
+
// missing for everybody who ran the install command exactly as printed, forever.
|
|
64
|
+
//
|
|
65
|
+
// The harness's own home variable is honoured here even though `configPath()` does not
|
|
66
|
+
// honour it. That asymmetry is deliberate: this path only READS, so resolving the same
|
|
67
|
+
// home the harness itself resolves can at worst find the skill somewhere true, whereas
|
|
68
|
+
// `configPath()` WRITES and shares its answer with subprocesses. RETASC_HOME still
|
|
69
|
+
// outranks everything, exactly as in `opencodeConfig()`, or the test override would stop
|
|
70
|
+
// protecting the developer's real home the moment one of these variables is set.
|
|
71
|
+
/** A harness's own config home: the test override first, then its documented variable,
|
|
72
|
+
* then the default under `~`. */
|
|
73
|
+
function harnessHome(envVar, dir) {
|
|
74
|
+
if (process.env.RETASC_HOME)
|
|
75
|
+
return join(home(), dir);
|
|
76
|
+
return process.env[envVar]?.trim() || join(home(), dir);
|
|
77
|
+
}
|
|
78
|
+
/** `<global skills dir>` plus the project-scope folder this harness reads in the working
|
|
79
|
+
* directory. Both are real install targets, so both answer "is the skill here". */
|
|
80
|
+
function skillDirs(globalDir, projectDir) {
|
|
81
|
+
return [globalDir, join(process.cwd(), ...projectDir.split("/"))];
|
|
82
|
+
}
|
|
51
83
|
export const SERVER_NAME = "retasc";
|
|
52
84
|
// --- shared helpers ---------------------------------------------------------
|
|
53
85
|
/**
|
|
@@ -134,11 +166,42 @@ export function tomlBlock(entry) {
|
|
|
134
166
|
env,
|
|
135
167
|
].join("\n");
|
|
136
168
|
}
|
|
137
|
-
|
|
169
|
+
/** The header table key each dialect actually reads. */
|
|
170
|
+
const HTTP_HEADER_TABLE = {
|
|
171
|
+
codex: "http_headers",
|
|
172
|
+
grok: "headers",
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* The DIRECT-form `[mcp_servers.retasc]` block: talk to the remote MCP over HTTP with a
|
|
176
|
+
* bearer key, no local process (RTSC-864).
|
|
177
|
+
*
|
|
178
|
+
* The TOML counterpart of `mcpConfigBlock`, and the block that did not exist: `tomlBlock`
|
|
179
|
+
* above emits only the stdio proxy shape, so a Codex or Grok user on a host that cannot
|
|
180
|
+
* spawn a process (claude.ai connectors, Codex Cloud, a hosted OpenCode) had nothing
|
|
181
|
+
* correct to paste and no way to tell that from the JSON form.
|
|
182
|
+
*
|
|
183
|
+
* The key is written literally, exactly as the JSON block writes it, because the host
|
|
184
|
+
* this is pasted into is the one that has no environment for us to read. Both tools can
|
|
185
|
+
* take the key from the environment instead where there IS one (Codex:
|
|
186
|
+
* `bearer_token_env_var = "RETASC_MCP_KEY"`; Grok expands `${RETASC_MCP_KEY}` inside a
|
|
187
|
+
* header value), so prefer that, and never paste either form into a file the repo tracks.
|
|
188
|
+
*/
|
|
189
|
+
export function tomlHttpBlock(url, key, dialect) {
|
|
190
|
+
return [
|
|
191
|
+
`[mcp_servers.${SERVER_NAME}]`,
|
|
192
|
+
`url = ${JSON.stringify(url)}`,
|
|
193
|
+
`enabled = true`,
|
|
194
|
+
``,
|
|
195
|
+
`[mcp_servers.${SERVER_NAME}.${HTTP_HEADER_TABLE[dialect]}]`,
|
|
196
|
+
`Authorization = ${JSON.stringify(`Bearer ${key}`)}`,
|
|
197
|
+
].join("\n");
|
|
198
|
+
}
|
|
199
|
+
function tomlHarness(id, label, path, bin, skills) {
|
|
138
200
|
return {
|
|
139
201
|
id,
|
|
140
202
|
label,
|
|
141
203
|
configPath: path,
|
|
204
|
+
skillDirs: () => skillDirs(join(harnessHome(skills.home, skills.dir), "skills"), skills.project),
|
|
142
205
|
detect: () => existsSync(path()) || onPath(bin),
|
|
143
206
|
install(entry) {
|
|
144
207
|
const p = path();
|
|
@@ -335,6 +398,53 @@ export function jsonServerValue(entry) {
|
|
|
335
398
|
return JSON.stringify({ command: entry.command, args: entry.args, env: entry.env }, null, 2);
|
|
336
399
|
}
|
|
337
400
|
// --- Claude Code ------------------------------------------------------------
|
|
401
|
+
/**
|
|
402
|
+
* The `skills/` folder of every plugin Claude Code has installed (RTSC-855).
|
|
403
|
+
*
|
|
404
|
+
* `/plugin install retasc@retasc` is one of the three install routes our public skills
|
|
405
|
+
* README offers, and it does NOT write into `~/.claude/skills`. The plugin is unpacked
|
|
406
|
+
* under `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/` and its skills are
|
|
407
|
+
* read from there. So a check that only looked in the skills folder would tell exactly
|
|
408
|
+
* the people who followed our own instructions that they had not.
|
|
409
|
+
*
|
|
410
|
+
* Read from the manifest rather than globbed, because the manifest is what Claude Code
|
|
411
|
+
* itself acts on: a stale directory left behind by an uninstall is not an installed
|
|
412
|
+
* skill, and enumerating the cache would count it as one. Best effort throughout, because
|
|
413
|
+
* this feeds a nudge and a manifest we cannot parse must never become a hard failure in
|
|
414
|
+
* `doctor`.
|
|
415
|
+
*/
|
|
416
|
+
function claudePluginSkillDirs() {
|
|
417
|
+
try {
|
|
418
|
+
const manifest = join(harnessHome("CLAUDE_CONFIG_DIR", ".claude"), "plugins", "installed_plugins.json");
|
|
419
|
+
const parsed = JSON.parse(readIfExists(manifest) || "{}");
|
|
420
|
+
const plugins = parsed?.plugins;
|
|
421
|
+
if (!plugins || typeof plugins !== "object")
|
|
422
|
+
return [];
|
|
423
|
+
const dirs = [];
|
|
424
|
+
for (const installs of Object.values(plugins)) {
|
|
425
|
+
for (const i of Array.isArray(installs) ? installs : []) {
|
|
426
|
+
// Absolute only. A relative `installPath` would resolve against the CURRENT
|
|
427
|
+
// DIRECTORY, so running `retasc doctor` inside any repo that happens to contain
|
|
428
|
+
// `skills/retasc/SKILL.md` (this one does) would report the skill installed.
|
|
429
|
+
// Claude Code writes absolute paths, so nothing legitimate is dropped.
|
|
430
|
+
// User scope only. A plugin installed for one project would otherwise make
|
|
431
|
+
// `doctor` report the skill present in every OTHER folder, where the agent
|
|
432
|
+
// cannot load it. A false "installed" is the worse error of the two: it
|
|
433
|
+
// produces silence instead of the nudge this check exists to give.
|
|
434
|
+
const scope = i?.scope;
|
|
435
|
+
if (scope !== undefined && scope !== "user")
|
|
436
|
+
continue;
|
|
437
|
+
const p = i?.installPath;
|
|
438
|
+
if (typeof p === "string" && isAbsolute(p))
|
|
439
|
+
dirs.push(join(p, "skills"));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return dirs;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
338
448
|
/**
|
|
339
449
|
* Claude Code is the one harness with a real CLI for this, and `claude mcp add` is
|
|
340
450
|
* the ONLY sanctioned way to edit `~/.claude.json`: a running Claude Code rewrites
|
|
@@ -351,6 +461,10 @@ const claudeCode = {
|
|
|
351
461
|
id: "claude-code",
|
|
352
462
|
label: "Claude Code",
|
|
353
463
|
configPath: () => join(home(), ".claude.json"),
|
|
464
|
+
skillDirs: () => [
|
|
465
|
+
...skillDirs(join(harnessHome("CLAUDE_CONFIG_DIR", ".claude"), "skills"), ".claude/skills"),
|
|
466
|
+
...claudePluginSkillDirs(),
|
|
467
|
+
],
|
|
354
468
|
// A test home means "do not touch this machine's real config". `claude mcp add`
|
|
355
469
|
// writes ~/.claude.json wherever HOME points, so the override has to gate DETECTION,
|
|
356
470
|
// not just the path: an undetected harness is never installed into.
|
|
@@ -404,6 +518,7 @@ const cursor = {
|
|
|
404
518
|
id: "cursor",
|
|
405
519
|
label: "Cursor",
|
|
406
520
|
configPath: () => join(home(), ".cursor", "mcp.json"),
|
|
521
|
+
skillDirs: () => skillDirs(join(home(), ".cursor", "skills"), ".agents/skills"),
|
|
407
522
|
detect: () => existsSync(join(home(), ".cursor")) || onPath("cursor-agent"),
|
|
408
523
|
install(entry) {
|
|
409
524
|
const p = join(home(), ".cursor", "mcp.json");
|
|
@@ -438,6 +553,7 @@ function addCommandHarness(opts) {
|
|
|
438
553
|
id: opts.id,
|
|
439
554
|
label: opts.label,
|
|
440
555
|
configPath: opts.configPath,
|
|
556
|
+
skillDirs: () => skillDirs(join(dirname(opts.configPath()), "skills"), opts.projectSkillDir),
|
|
441
557
|
// Gated on RETASC_HOME for the same reason Claude Code is: this shells out to a
|
|
442
558
|
// command that resolves the REAL home, so a test home has to stop it at DETECTION.
|
|
443
559
|
// An undetected harness is never installed into.
|
|
@@ -488,6 +604,7 @@ const opencode = addCommandHarness({
|
|
|
488
604
|
label: "OpenCode",
|
|
489
605
|
bin: "opencode",
|
|
490
606
|
configPath: opencodeConfig,
|
|
607
|
+
projectSkillDir: ".agents/skills",
|
|
491
608
|
args: (entry) => [
|
|
492
609
|
"mcp", "add", SERVER_NAME,
|
|
493
610
|
...Object.entries(entry.env).flatMap(([k, v]) => ["--env", `${k}=${v}`]),
|
|
@@ -513,6 +630,7 @@ const gemini = addCommandHarness({
|
|
|
513
630
|
label: "Gemini CLI",
|
|
514
631
|
bin: "gemini",
|
|
515
632
|
configPath: () => join(home(), ".gemini", "settings.json"),
|
|
633
|
+
projectSkillDir: ".agents/skills",
|
|
516
634
|
args: (entry) => [
|
|
517
635
|
"mcp", "add",
|
|
518
636
|
"-s", "user",
|
|
@@ -524,8 +642,16 @@ const gemini = addCommandHarness({
|
|
|
524
642
|
// --- the registry -----------------------------------------------------------
|
|
525
643
|
export const HARNESSES = [
|
|
526
644
|
claudeCode,
|
|
527
|
-
tomlHarness("codex", "Codex", () => join(home(), ".codex", "config.toml"), "codex"
|
|
528
|
-
|
|
645
|
+
tomlHarness("codex", "Codex", () => join(home(), ".codex", "config.toml"), "codex", {
|
|
646
|
+
home: "CODEX_HOME",
|
|
647
|
+
dir: ".codex",
|
|
648
|
+
project: ".agents/skills",
|
|
649
|
+
}),
|
|
650
|
+
tomlHarness("grok", "Grok", () => join(home(), ".grok", "config.toml"), "grok", {
|
|
651
|
+
home: "GROK_HOME",
|
|
652
|
+
dir: ".grok",
|
|
653
|
+
project: ".grok/skills",
|
|
654
|
+
}),
|
|
529
655
|
cursor,
|
|
530
656
|
opencode,
|
|
531
657
|
gemini,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// RTSC-855: is the Retasc Agent Skill on this machine?
|
|
2
|
+
//
|
|
3
|
+
// This check can only live here. The server sees a bearer token and nothing else, no
|
|
4
|
+
// filesystem and no working directory, so it cannot know whether `skills/retasc/SKILL.md`
|
|
5
|
+
// was ever installed and must not pretend to infer it. The CLI runs on the machine, and
|
|
6
|
+
// `doctor` and `setup` already report local facts, so they are the honest place to ask.
|
|
7
|
+
//
|
|
8
|
+
// What is at stake is not cosmetic. The MCP tools give an agent the ability to claim
|
|
9
|
+
// work; the skill is what tells it what a lease is, that a claim needs its own worktree,
|
|
10
|
+
// what a checkpoint is for, and how a handoff into review works. An agent with the tools
|
|
11
|
+
// and not the manual looks like it is working and quietly gets the protocol wrong.
|
|
12
|
+
//
|
|
13
|
+
// Absence is a FINDING, not a failure. Nothing here touches an exit code, and nothing
|
|
14
|
+
// here writes: installing the skill is a decision for the human, made with one command
|
|
15
|
+
// they can read before they run it.
|
|
16
|
+
//
|
|
17
|
+
// Explicitly NOT in scope: deciding whether an installed copy is STALE. That needs a
|
|
18
|
+
// version stamp on the published skill, which does not exist yet, and a guess at it
|
|
19
|
+
// would produce the one output worse than silence, a confident wrong answer about a
|
|
20
|
+
// file the human can see is right there.
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { clean } from "./text.js";
|
|
24
|
+
/** The skill's folder name, and the name it is published under. */
|
|
25
|
+
export const SKILL_NAME = "retasc";
|
|
26
|
+
/**
|
|
27
|
+
* The one command to print.
|
|
28
|
+
*
|
|
29
|
+
* Harness-agnostic on purpose: `npx skills add Retasc/skills` is the first of the three
|
|
30
|
+
* routes our public README lists and the only one that is correct for all six harnesses
|
|
31
|
+
* at once, so a machine with Codex and Cursor on it gets a single line rather than a
|
|
32
|
+
* decision tree. The Claude Code plugin route and the git-clone route stay in the README
|
|
33
|
+
* where somebody choosing between them is already reading.
|
|
34
|
+
*
|
|
35
|
+
* `-g` is load-bearing and was missing here first time round. The installer defaults to
|
|
36
|
+
* PROJECT scope (`installGlobally = options.global ?? false`, and its interactive prompt
|
|
37
|
+
* offers Project first), which installs into the current folder. Without the flag this
|
|
38
|
+
* command would install the skill somewhere real, the agent would load it in that one
|
|
39
|
+
* folder, and every other folder would keep being told to run the same command again.
|
|
40
|
+
* Global is what "install this on my machine" means, and it is what a nudge printed by a
|
|
41
|
+
* machine-wide check should hand you.
|
|
42
|
+
*/
|
|
43
|
+
export const SKILL_INSTALL_CMD = "npx skills add Retasc/skills -g";
|
|
44
|
+
/** A directory is "holding the skill" when the skill's own file is readable in it. A
|
|
45
|
+
* bare `retasc/` folder is not an installed skill, and `existsSync` follows symlinks,
|
|
46
|
+
* which is how a checkout linked into `~/.claude/skills` counts (see skills/README.md). */
|
|
47
|
+
function holdsSkill(dir) {
|
|
48
|
+
try {
|
|
49
|
+
return existsSync(join(dir, SKILL_NAME, "SKILL.md"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Which of these harnesses can read the skill, and which cannot.
|
|
57
|
+
*
|
|
58
|
+
* Takes the DETECTED harnesses rather than detecting for itself, so the answer is about
|
|
59
|
+
* agents that actually run here: telling someone the skill is missing from Gemini CLI on
|
|
60
|
+
* a machine with no Gemini CLI is noise dressed as a finding, and both callers have the
|
|
61
|
+
* detected list in hand already.
|
|
62
|
+
*/
|
|
63
|
+
export function skillReport(harnesses) {
|
|
64
|
+
const report = { installed: [], missing: [] };
|
|
65
|
+
for (const h of harnesses) {
|
|
66
|
+
let dirs = [];
|
|
67
|
+
try {
|
|
68
|
+
dirs = h.skillDirs();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
dirs = [];
|
|
72
|
+
}
|
|
73
|
+
// No known skills directory means we cannot answer for this harness, and the honest
|
|
74
|
+
// move is to say nothing rather than to invent a path and report a miss against it.
|
|
75
|
+
if (!dirs.length)
|
|
76
|
+
continue;
|
|
77
|
+
const entry = { label: h.label };
|
|
78
|
+
(dirs.some(holdsSkill) ? report.installed : report.missing).push(entry);
|
|
79
|
+
}
|
|
80
|
+
return report;
|
|
81
|
+
}
|
|
82
|
+
const names = (list) => clean(list.map((p) => p.label).join(", "));
|
|
83
|
+
/**
|
|
84
|
+
* The lines to print, prefixes included, shared verbatim by `doctor` and `setup`.
|
|
85
|
+
*
|
|
86
|
+
* One renderer because the issue asks both surfaces to say the same thing, and two
|
|
87
|
+
* copies of a sentence are two sentences the moment one of them is edited.
|
|
88
|
+
*
|
|
89
|
+
* `ℹ` and not `✗`: this is context about the machine, not a fault in the folder being
|
|
90
|
+
* checked, and it sits in a report whose ✓/✗ marks people read as pass and fail.
|
|
91
|
+
*/
|
|
92
|
+
export function skillLines(report) {
|
|
93
|
+
// One harness with the skill is enough to say so and stop talking. The nudge exists for
|
|
94
|
+
// the machine that has the manual NOWHERE; on a developer laptop that has Cursor and
|
|
95
|
+
// Gemini installed but points neither at Retasc, listing them as missing on every run
|
|
96
|
+
// is an unresolvable nag about work nobody intends to do, and a check people learn to
|
|
97
|
+
// scroll past has stopped being a check. Which harnesses DO have it is named in the ✓
|
|
98
|
+
// line, so nothing true is being hidden.
|
|
99
|
+
if (report.installed.length) {
|
|
100
|
+
return [` ✓ the Retasc Agent Skill is installed (${names(report.installed)}).`];
|
|
101
|
+
}
|
|
102
|
+
if (!report.missing.length)
|
|
103
|
+
return [];
|
|
104
|
+
return [
|
|
105
|
+
` ℹ the Retasc Agent Skill is not installed for ${names(report.missing)}.\n` +
|
|
106
|
+
` It is what teaches an agent to work the queue: dispatch, leases, checkpoints,\n` +
|
|
107
|
+
` handing off through review. Without it an agent has the tools and not the manual.\n` +
|
|
108
|
+
` Install: ${SKILL_INSTALL_CMD}`,
|
|
109
|
+
];
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retasc/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.47.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": {
|