Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/miner/soft-claim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// #2315 miner soft-claim spec builder. A miner can OPTIONALLY make its claim visible to a fleet by posting a public
// "soft claim" comment ("a miner is working on this") to reduce duplicate work. This module composes a
// deterministic, well-formatted claim comment BODY and hands it to the EXISTING `buildPostEligibilityCommentSpec`
// — it BUILDS the spec only and never executes anything, so the same spec-builder-not-actuator boundary documented
// by `src/mcp/local-write-tools.ts` (see `LOCAL_WRITE_BOUNDARY`, local-write-tools.ts:8) holds: the miner's OWN
// harness runs the command with its OWN GitHub credentials. Shell-safety is inherited from the reused builder's
// single-quote escaping — never re-implemented here.
import {
buildPostEligibilityCommentSpec,
type LocalWriteActionSpec,
} from "../mcp/local-write-tools";

/** Format the deterministic Markdown body of a soft-claim comment: which miner claimed the issue, when, and — when
* provided — when the claim lapses. Pure: the same inputs always produce the same string. The body's shell-safety
* is guaranteed downstream by {@link buildSoftClaimSpec} reusing `local-write-tools`' single-quote escaping. */
export function buildSoftClaimCommentBody(input: { minerId: string; claimedAt: string; expiresAt?: string }): string {
const lines = [
`🤖 **Soft claim** — a Gittensor miner (\`${input.minerId}\`) is working on this issue, claimed at ${input.claimedAt}.`,
];
if (input.expiresAt) {
lines.push(
`This claim expires at ${input.expiresAt}; if no PR has landed by then, the issue is open for others again.`,
);
}
lines.push("_Soft claim only — not an assignment. It signals intent so the fleet avoids duplicate work._");
return lines.join("\n\n");
}

/** Build the local-write action spec that posts a soft-claim comment on an issue. Composes
* {@link buildSoftClaimCommentBody} and delegates to the EXISTING `buildPostEligibilityCommentSpec` — reusing its
* shell-safe command construction/escaping rather than duplicating it — so the output stays deterministic and
* single-quote-shell-safe. Builds the spec only; the miner's own harness runs it (see `LOCAL_WRITE_BOUNDARY`). */
export function buildSoftClaimSpec(input: {
repoFullName: string;
number: number;
minerId: string;
claimedAt: string;
expiresAt?: string;
}): LocalWriteActionSpec {
const body = buildSoftClaimCommentBody(input);
return buildPostEligibilityCommentSpec({ repoFullName: input.repoFullName, number: input.number, body });
}
52 changes: 52 additions & 0 deletions test/unit/soft-claim.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import { LOCAL_WRITE_BOUNDARY } from "../../src/mcp/local-write-tools";
import { buildSoftClaimCommentBody, buildSoftClaimSpec } from "../../src/miner/soft-claim";

describe("soft-claim spec builder (#2315)", () => {
it("buildSoftClaimCommentBody: formats a deterministic body with miner id + claimed-at, no expiry line when absent", () => {
const body = buildSoftClaimCommentBody({ minerId: "miner-7", claimedAt: "2026-07-03T12:00:00Z" });
expect(body).toContain("`miner-7`");
expect(body).toContain("claimed at 2026-07-03T12:00:00Z");
expect(body).toContain("Soft claim only — not an assignment");
expect(body).not.toContain("expires at"); // no expiry note without expiresAt
// deterministic
expect(buildSoftClaimCommentBody({ minerId: "miner-7", claimedAt: "2026-07-03T12:00:00Z" })).toBe(body);
});

it("buildSoftClaimCommentBody: includes an expiry note when expiresAt is provided", () => {
const body = buildSoftClaimCommentBody({
minerId: "miner-7",
claimedAt: "2026-07-03T12:00:00Z",
expiresAt: "2026-07-04T12:00:00Z",
});
expect(body).toContain("This claim expires at 2026-07-04T12:00:00Z");
});

it("buildSoftClaimSpec: delegates to the eligibility-comment builder, carrying the local-write boundary", () => {
const spec = buildSoftClaimSpec({
repoFullName: "octo/repo",
number: 42,
minerId: "miner-7",
claimedAt: "2026-07-03T12:00:00Z",
});
expect(spec.action).toBe("post_eligibility_comment");
expect(spec.boundary).toBe(LOCAL_WRITE_BOUNDARY);
// the command is the reused builder's shell-safe `gh issue comment`, with the composed body as --body
expect(spec.command).toContain("gh issue comment 42 --repo 'octo/repo' --body '");
expect(spec.inputs.body).toBe(buildSoftClaimCommentBody({ minerId: "miner-7", claimedAt: "2026-07-03T12:00:00Z" }));
});

it("buildSoftClaimSpec: a single quote in minerId is POSIX-escaped by the reused builder (injection-safe)", () => {
const spec = buildSoftClaimSpec({
repoFullName: "octo/repo",
number: 42,
minerId: "evil'; rm -rf / #",
claimedAt: "2026-07-03T12:00:00Z",
});
// the embedded single quote is escaped as '\'' so the command stays a single, safe --body argument
expect(spec.command).toContain("evil'\\''; rm -rf / #");
// and the whole --body value remains wrapped in one pair of single quotes (no unescaped break-out quote)
const unescaped = spec.command.replace(/'\\''/g, "");
expect((unescaped.match(/'/g) ?? []).length % 2).toBe(0);
});
});
Loading