ralph-hero-mcp-server 2.5.51 → 2.5.57

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/index.js CHANGED
@@ -218,6 +218,13 @@ function registerCoreTools(server, client) {
218
218
  detail: "RALPH_GH_PROJECT_NUMBER not set",
219
219
  };
220
220
  }
221
+ // Token source detection — re-derive which env vars resolved
222
+ const repoTokenSource = resolveEnv("RALPH_GH_REPO_TOKEN")
223
+ ? "RALPH_GH_REPO_TOKEN"
224
+ : "RALPH_HERO_GITHUB_TOKEN";
225
+ const projectTokenSource = resolveEnv("RALPH_GH_PROJECT_TOKEN")
226
+ ? "RALPH_GH_PROJECT_TOKEN"
227
+ : repoTokenSource;
221
228
  // Summary
222
229
  const allOk = Object.values(checks).every((c) => c.status === "ok" || c.status === "skip");
223
230
  return toolSuccess({
@@ -228,11 +235,17 @@ function registerCoreTools(server, client) {
228
235
  repo: client.config.repo || "(not set)",
229
236
  projectOwner: resolveProjectOwner(client.config) || "(not set)",
230
237
  projectNumber: client.config.projectNumber || "(not set)",
231
- tokenMode: client.config.projectToken &&
232
- client.config.projectToken !== client.config.token
238
+ tokenMode: projectTokenSource !== repoTokenSource
233
239
  ? "dual-token"
234
240
  : "single-token",
235
241
  },
242
+ tokenSources: {
243
+ repoToken: repoTokenSource,
244
+ projectToken: projectTokenSource,
245
+ note: projectTokenSource !== repoTokenSource
246
+ ? `Repo operations use ${repoTokenSource}, project operations use ${projectTokenSource}`
247
+ : `Both repo and project operations use ${repoTokenSource}`,
248
+ },
236
249
  });
237
250
  });
238
251
  }
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-hero-mcp-server",
3
- "version": "2.5.51",
3
+ "version": "2.5.57",
4
4
  "description": "MCP server for GitHub Projects V2 - Ralph workflow automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",