@retasc/cli 1.2.1 → 1.2.3
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/dist/commands/claim.js +51 -6
- package/dist/index.js +9 -2
- package/dist/lib/claim.js +30 -0
- package/dist/lib/watchdog.js +26 -3
- package/package.json +1 -1
package/dist/commands/claim.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, resolve, sep } from "node:path";
|
|
4
|
-
import { resolveMcpConn, readMcpJson, mcpCall, parseClaimResult, planWorktree, isValidIssueId, } from "../lib/claim.js";
|
|
4
|
+
import { resolveMcpConn, readMcpJson, mcpCall, parseClaimResult, planWorktree, isValidIssueId, normalizeIssueId, workspacePrefix, } from "../lib/claim.js";
|
|
5
|
+
import { loadConfig } from "../config.js";
|
|
5
6
|
function git(args, cwd) {
|
|
6
7
|
return spawnSync("git", args, { encoding: "utf8", cwd });
|
|
7
8
|
}
|
|
@@ -28,6 +29,50 @@ export async function claimAction(opts) {
|
|
|
28
29
|
note(" (Looked at $RETASC_MCP_KEY and ./.mcp.json.)");
|
|
29
30
|
process.exit(1);
|
|
30
31
|
}
|
|
32
|
+
// RTSC-148: resolve the requested issue id up front — a positional (`claim 143`,
|
|
33
|
+
// `claim RTSC-143`) or --id, normalized to PREFIX-NN. An unresolvable reference
|
|
34
|
+
// fails HERE; it must never fall through to "claim whatever is next".
|
|
35
|
+
// A bare number expands against the WORKSPACE's prefix (whoami over this
|
|
36
|
+
// workspace's key — the key decides the org/project), falling back to the
|
|
37
|
+
// global config only if the server can't be asked.
|
|
38
|
+
let prefix;
|
|
39
|
+
let prefixResolved = false;
|
|
40
|
+
let requestedId;
|
|
41
|
+
for (const [label, raw] of [
|
|
42
|
+
["issue", opts.issueArg],
|
|
43
|
+
["--id", opts.id],
|
|
44
|
+
]) {
|
|
45
|
+
// Only ABSENT skips — an empty string (e.g. `claim "$UNSET_VAR"`) must hit
|
|
46
|
+
// the loud-failure path below, not fall through to next_issue.
|
|
47
|
+
if (raw === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
if (/^\d+$/.test(raw.trim()) && !prefixResolved) {
|
|
50
|
+
prefix = (await workspacePrefix(conn)) ?? loadConfig().defaultProjectPrefix;
|
|
51
|
+
prefixResolved = true;
|
|
52
|
+
}
|
|
53
|
+
const norm = normalizeIssueId(raw, prefix);
|
|
54
|
+
if (!norm) {
|
|
55
|
+
if (/^\d+$/.test(raw.trim()) && !prefix) {
|
|
56
|
+
note(`✗ "${raw}" is a bare number and the project prefix couldn't be resolved from this workspace.`);
|
|
57
|
+
note(" Use the full id (e.g. RTSC-" + raw.trim() + ").");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
note(`✗ "${raw}" doesn't look like an issue id (expected RTSC-NN or a bare number).`);
|
|
61
|
+
}
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (requestedId && requestedId !== norm) {
|
|
65
|
+
note(`✗ Conflicting issue ids: positional "${requestedId}" vs ${label} "${norm}". Pass just one.`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
requestedId = norm;
|
|
69
|
+
}
|
|
70
|
+
// --mine filters the next_issue pull; on a targeted claim it would be
|
|
71
|
+
// silently ignored — reject instead of letting the intent evaporate.
|
|
72
|
+
if (requestedId && opts.mine) {
|
|
73
|
+
note(`✗ --mine only applies when pulling the next issue; drop it to claim ${requestedId} directly.`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
31
76
|
const makeWorktree = opts.worktree !== false;
|
|
32
77
|
// Must be in a git repo to place the worktree. Resolve the *main* checkout so
|
|
33
78
|
// worktrees are always siblings of it, even when claiming from another worktree.
|
|
@@ -52,8 +97,8 @@ export async function claimAction(opts) {
|
|
|
52
97
|
// --- claim over MCP ------------------------------------------------------
|
|
53
98
|
let claim;
|
|
54
99
|
try {
|
|
55
|
-
const result =
|
|
56
|
-
? await mcpCall(conn, "claim_issue", { identifier:
|
|
100
|
+
const result = requestedId
|
|
101
|
+
? await mcpCall(conn, "claim_issue", { identifier: requestedId })
|
|
57
102
|
: await mcpCall(conn, "next_issue", opts.mine ? { mine: true } : {});
|
|
58
103
|
claim = parseClaimResult(result);
|
|
59
104
|
}
|
|
@@ -62,9 +107,9 @@ export async function claimAction(opts) {
|
|
|
62
107
|
note(`✗ ${msg}`);
|
|
63
108
|
// Re-claiming an issue you already hold is the "resume my own work" case —
|
|
64
109
|
// point at the worktree instead of leaving the user stuck on the error.
|
|
65
|
-
if (
|
|
66
|
-
note(` ${
|
|
67
|
-
note(` or \`retasc release ${
|
|
110
|
+
if (requestedId && /ALREADY_CLAIMED/.test(msg) && makeWorktree) {
|
|
111
|
+
note(` ${requestedId} is held. If it's your session, resume in ${parentDir}/${repoName}-${requestedId.toLowerCase()}`);
|
|
112
|
+
note(` or \`retasc release ${requestedId}\` first.`);
|
|
68
113
|
}
|
|
69
114
|
process.exit(1);
|
|
70
115
|
}
|
package/dist/index.js
CHANGED
|
@@ -414,12 +414,19 @@ function addClaimFlags(cmd) {
|
|
|
414
414
|
.option("--print-path", "Print only the worktree path on stdout (for `cd \"$(…)\"`)")
|
|
415
415
|
.option("--json", "Emit the claim + worktree as JSON");
|
|
416
416
|
}
|
|
417
|
+
// RTSC-148: `claim` takes the issue as a positional too (`retasc claim 143` /
|
|
418
|
+
// `retasc claim RTSC-143`). Excess arguments are rejected loudly on both
|
|
419
|
+
// commands — Commander otherwise silently drops an undeclared positional, which
|
|
420
|
+
// turned "claim this specific issue" into "claim whatever is next".
|
|
417
421
|
addClaimFlags(program
|
|
418
422
|
.command("claim")
|
|
419
|
-
.description("Claim an issue (--id, or the next unblocked) and drop into a fresh worktree.")
|
|
423
|
+
.description("Claim an issue (RTSC-NN, a bare number, --id, or the next unblocked) and drop into a fresh worktree.")
|
|
424
|
+
.argument("[issue]", "Issue to claim, e.g. RTSC-143 or 143 (defaults to the next unblocked)")
|
|
425
|
+
.allowExcessArguments(false)).action((issue, opts) => claimAction({ ...opts, issueArg: issue }).catch(fail));
|
|
420
426
|
addClaimFlags(program
|
|
421
427
|
.command("next")
|
|
422
|
-
.description("Claim the next unblocked issue and drop into a fresh worktree (alias of `claim`).")
|
|
428
|
+
.description("Claim the next unblocked issue and drop into a fresh worktree (alias of `claim`).")
|
|
429
|
+
.allowExcessArguments(false)).action((opts) => claimAction(opts).catch(fail));
|
|
423
430
|
// --- branch hygiene (close the worktree+branch claim opened) ----------------
|
|
424
431
|
program
|
|
425
432
|
.command("tidy")
|
package/dist/lib/claim.js
CHANGED
|
@@ -60,6 +60,36 @@ export function slugFromTitle(title, max = 50) {
|
|
|
60
60
|
export function isValidIssueId(id) {
|
|
61
61
|
return typeof id === "string" && /^[A-Za-z][A-Za-z0-9]*-\d+$/.test(id);
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Normalize a user-typed issue reference (`143`, `rtsc-143`, `RTSC-143`) to the
|
|
65
|
+
* canonical `PREFIX-NN` form. A bare number needs `defaultPrefix` (from the
|
|
66
|
+
* logged-in project) to be resolvable. Returns null when the input can't be an
|
|
67
|
+
* issue id — the caller decides how to fail loudly (RTSC-148: a typo must never
|
|
68
|
+
* silently fall through to `next_issue`).
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* The workspace's project prefix, asked of the server over the workspace's own
|
|
72
|
+
* key (`whoami`). Org/project routing IS the key — the global config's prefix
|
|
73
|
+
* may belong to a different workspace, so this is the authoritative source for
|
|
74
|
+
* expanding a bare issue number. Best-effort: undefined on any failure.
|
|
75
|
+
*/
|
|
76
|
+
export async function workspacePrefix(conn, fetchImpl = fetch) {
|
|
77
|
+
try {
|
|
78
|
+
const who = (await mcpCall(conn, "whoami", {}, fetchImpl));
|
|
79
|
+
const p = who?.project?.prefix;
|
|
80
|
+
return typeof p === "string" && p ? p : undefined;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function normalizeIssueId(input, defaultPrefix) {
|
|
87
|
+
const s = input.trim();
|
|
88
|
+
if (/^\d+$/.test(s)) {
|
|
89
|
+
return defaultPrefix ? `${defaultPrefix.toUpperCase()}-${s}` : null;
|
|
90
|
+
}
|
|
91
|
+
return isValidIssueId(s) ? s.toUpperCase() : null;
|
|
92
|
+
}
|
|
63
93
|
/**
|
|
64
94
|
* Derive the branch + sibling worktree path from an issue, matching the CLAUDE.md
|
|
65
95
|
* convention: branch `rtsc-NN/<slug>`, worktree dir `../<repo>-rtsc-NN`.
|
package/dist/lib/watchdog.js
CHANGED
|
@@ -57,10 +57,33 @@ export function heartbeatRequest(rpcId, issueId, claimToken) {
|
|
|
57
57
|
params: { name: "heartbeat", arguments: { identifier: issueId, claimToken } },
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* A heartbeat CLAIM_LOST means the lease is gone — stop tracking it.
|
|
62
|
+
*
|
|
63
|
+
* The server throws `Error("CLAIM_LOST: …")` (convex/lib/claims.ts), and http.ts
|
|
64
|
+
* surfaces that as an MCP tool ERROR: `{ isError: true, content:[{text:"CLAIM_LOST: …"}] }`.
|
|
65
|
+
* Match ONLY that signal (or a top-level `error`/`code` field, for a future
|
|
66
|
+
* structured shape) — NEVER `JSON.stringify` the whole result and regex it, or a
|
|
67
|
+
* benign payload field that merely CONTAINS "CLAIM_LOST" (an issue title, a
|
|
68
|
+
* checkpoint note quoting an error, a comment body) would silently drop a live
|
|
69
|
+
* lease and let the reclaimer hand our work to someone else (RTSC-150).
|
|
70
|
+
*/
|
|
61
71
|
export function isClaimLost(result) {
|
|
62
72
|
if (!result || typeof result !== "object")
|
|
63
73
|
return false;
|
|
64
|
-
const
|
|
65
|
-
|
|
74
|
+
const r = result;
|
|
75
|
+
// A structured error signal on a top-level field (belt-and-suspenders; also the
|
|
76
|
+
// shape the older unit test asserts). Bounded to top-level scalars, not nested.
|
|
77
|
+
if (r.code === "CLAIM_LOST")
|
|
78
|
+
return true;
|
|
79
|
+
if (typeof r.error === "string" && /CLAIM_LOST/.test(r.error))
|
|
80
|
+
return true;
|
|
81
|
+
// The live shape: only an MCP error envelope, and only its text blocks — success
|
|
82
|
+
// payloads (isError absent/false) are never treated as a lost claim.
|
|
83
|
+
if (r.isError !== true || !Array.isArray(r.content))
|
|
84
|
+
return false;
|
|
85
|
+
return r.content.some((c) => c !== null &&
|
|
86
|
+
typeof c === "object" &&
|
|
87
|
+
typeof c.text === "string" &&
|
|
88
|
+
/CLAIM_LOST/.test(c.text));
|
|
66
89
|
}
|
package/package.json
CHANGED