ralph-hero-mcp-server 2.5.50 → 2.5.52
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/lib/helpers.js +4 -2
- package/dist/lib/lock-guard.js +40 -0
- package/dist/tools/issue-tools.js +16 -1
- package/package.json +1 -1
package/dist/lib/helpers.js
CHANGED
|
@@ -319,7 +319,8 @@ export function resolveConfig(client, args) {
|
|
|
319
319
|
const owner = args.owner || client.config.owner;
|
|
320
320
|
const repo = args.repo || client.config.repo;
|
|
321
321
|
if (!owner)
|
|
322
|
-
throw new Error("owner is required
|
|
322
|
+
throw new Error("owner is required. Set RALPH_GH_OWNER in ~/.claude/settings.json (user-scoped) " +
|
|
323
|
+
"or .claude/settings.local.json (project-scoped), or pass owner explicitly.");
|
|
323
324
|
if (!repo)
|
|
324
325
|
throw new Error("repo is required. Set RALPH_GH_REPO env var, pass repo explicitly, or link exactly one repo to your project.");
|
|
325
326
|
return { owner, repo };
|
|
@@ -334,7 +335,8 @@ export function resolveConfig(client, args) {
|
|
|
334
335
|
export function resolveConfigOptionalRepo(client, args) {
|
|
335
336
|
const owner = args.owner || client.config.owner;
|
|
336
337
|
if (!owner)
|
|
337
|
-
throw new Error("owner is required
|
|
338
|
+
throw new Error("owner is required. Set RALPH_GH_OWNER in ~/.claude/settings.json (user-scoped) " +
|
|
339
|
+
"or .claude/settings.local.json (project-scoped), or pass owner explicitly.");
|
|
338
340
|
const repo = args.repo || client.config.repo;
|
|
339
341
|
return { owner, repo };
|
|
340
342
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side lock guard for save_issue.
|
|
3
|
+
*
|
|
4
|
+
* Provides a pure, unit-testable function that determines whether a workflow
|
|
5
|
+
* state transition would result in a lock conflict — i.e., two agents trying
|
|
6
|
+
* to claim the same exclusive lock state simultaneously.
|
|
7
|
+
*/
|
|
8
|
+
import { LOCK_STATES } from "./workflow-states.js";
|
|
9
|
+
/**
|
|
10
|
+
* Returns true when the requested transition is a lock conflict.
|
|
11
|
+
*
|
|
12
|
+
* A conflict exists when:
|
|
13
|
+
* 1. The issue is already in a lock state (currentState ∈ LOCK_STATES), AND
|
|
14
|
+
* 2. The caller is trying to set another lock state (targetState ∈ LOCK_STATES)
|
|
15
|
+
*
|
|
16
|
+
* The guard is intentionally narrow:
|
|
17
|
+
* - If currentState is undefined or empty, the issue's state is unknown
|
|
18
|
+
* (e.g., no project item yet). Allow the claim — it cannot conflict.
|
|
19
|
+
* - If targetState is NOT a lock state (e.g., moving to Done or reverting to
|
|
20
|
+
* Backlog), the guard is bypassed entirely. Non-lock transitions are always
|
|
21
|
+
* safe and should not incur an extra API roundtrip in the caller.
|
|
22
|
+
*
|
|
23
|
+
* @param currentState - The issue's current workflow state from the live API,
|
|
24
|
+
* or undefined/empty if it could not be resolved.
|
|
25
|
+
* @param targetState - The workflow state the caller is trying to set.
|
|
26
|
+
* @returns true if the transition should be blocked, false if it should proceed.
|
|
27
|
+
*/
|
|
28
|
+
export function isLockConflict(currentState, targetState) {
|
|
29
|
+
if (!currentState) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (!LOCK_STATES.includes(targetState)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (currentState === targetState) {
|
|
36
|
+
return false; // idempotent re-claim: same agent re-locking is safe
|
|
37
|
+
}
|
|
38
|
+
return LOCK_STATES.includes(currentState);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=lock-guard.js.map
|
|
@@ -14,8 +14,9 @@ import { resolveState } from "../lib/state-resolution.js";
|
|
|
14
14
|
import { parseDateMath } from "../lib/date-math.js";
|
|
15
15
|
import { expandProfile } from "../lib/filter-profiles.js";
|
|
16
16
|
import { toolSuccess, toolError } from "../types.js";
|
|
17
|
-
import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, resolveConfig, resolveFullConfig, resolveFullConfigOptionalRepo, syncStatusField, autoAdvanceParent, resolveIterationId, } from "../lib/helpers.js";
|
|
17
|
+
import { ensureFieldCache, resolveIssueNodeId, resolveProjectItemId, updateProjectItemField, getCurrentFieldValue, resolveConfig, resolveFullConfig, resolveFullConfigOptionalRepo, syncStatusField, autoAdvanceParent, resolveIterationId, } from "../lib/helpers.js";
|
|
18
18
|
import { lookupRepo, mergeDefaults } from "../lib/repo-registry.js";
|
|
19
|
+
import { isLockConflict } from "../lib/lock-guard.js";
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Register issue tools
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
@@ -833,6 +834,8 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
833
834
|
.describe("Iteration/sprint title (e.g., 'Sprint 1'), @current, @next, or null to clear."),
|
|
834
835
|
command: z.string().optional()
|
|
835
836
|
.describe("Ralph command for semantic intent resolution (e.g., 'ralph_impl'). Required when workflowState is a semantic intent."),
|
|
837
|
+
force: z.boolean().optional()
|
|
838
|
+
.describe("Bypass lock guard. Use only for recovery when an agent crash left an issue stuck in a lock state."),
|
|
836
839
|
}, async (args) => {
|
|
837
840
|
try {
|
|
838
841
|
const { owner, repo } = resolveConfig(client, args);
|
|
@@ -989,6 +992,18 @@ export function registerIssueTools(server, client, fieldCache) {
|
|
|
989
992
|
if (!projectId) {
|
|
990
993
|
return toolError("Could not resolve project ID");
|
|
991
994
|
}
|
|
995
|
+
// Server-side lock guard: prevent two agents from claiming the same
|
|
996
|
+
// lock state simultaneously. Only fires when the caller is trying to
|
|
997
|
+
// SET a lock state — non-lock transitions skip this check entirely.
|
|
998
|
+
if (!args.force && resolvedWorkflowState && LOCK_STATES.includes(resolvedWorkflowState)) {
|
|
999
|
+
const currentWorkflowState = await getCurrentFieldValue(client, fieldCache, owner, repo, args.number, "Workflow State", projectNumber);
|
|
1000
|
+
if (isLockConflict(currentWorkflowState, resolvedWorkflowState)) {
|
|
1001
|
+
return toolError(`Issue #${args.number} is already in a lock state ("${currentWorkflowState}") ` +
|
|
1002
|
+
`and cannot be claimed as "${resolvedWorkflowState}". ` +
|
|
1003
|
+
`Another agent is actively working on this issue. ` +
|
|
1004
|
+
`Use save_issue with force=true to override, or wait for the lock holder to release.`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
992
1007
|
// Collect field updates for aliased batch mutation
|
|
993
1008
|
const updates = [];
|
|
994
1009
|
// Collect fields to clear (separate mutations)
|