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
32 changes: 20 additions & 12 deletions .claude/skills/swamp-issue/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
---
name: swamp-issue
description: Submit issues to the swamp Lab or route them to the publisher's repository — file bug reports, feature requests, and security vulnerability reports against swamp itself or against a specific extension, and post follow-up ripples (comments) on existing Lab issues. Use when the user wants to report a bug, request a feature, disclose a vulnerability, comment on an existing issue, or provide feedback about swamp. Triggers on "bug report", "feature request", "security report", "vulnerability", "report bug", "request feature", "file bug", "submit bug", "swamp bug", "swamp feature", "feedback", "report issue", "file issue", "report against extension", "extension bug", "ripple", "comment on issue", "reply to issue", "follow up on issue", "add comment to issue".
description: Fetch issue details and submit issues to the swamp Lab or route them to the publisher's repository — fetch issue details, file bug reports, feature requests, and security vulnerability reports against swamp itself or against a specific extension, and post follow-up ripples (comments) on existing Lab issues. Use when the user wants to view an issue, report a bug, request a feature, disclose a vulnerability, comment on an existing issue, or provide feedback about swamp. Triggers on "bug report", "feature request", "security report", "vulnerability", "report bug", "request feature", "file bug", "submit bug", "swamp bug", "swamp feature", "feedback", "report issue", "file issue", "report against extension", "extension bug", "ripple", "comment on issue", "reply to issue", "follow up on issue", "add comment to issue", "get issue", "view issue", "fetch issue", "issue details", "show issue".
---

# Swamp Issue Submission Skill
# Swamp Issue Skill

Submit bug reports, feature requests, and security vulnerability disclosures
through the swamp CLI. When logged in (`swamp auth login`), issues are submitted
directly to the swamp.club Lab. When not logged in, the user is prompted to log
in or send via email. The `--email` flag skips straight to a pre-filled email.
Fetch issue details, submit bug reports, feature requests, and security
vulnerability disclosures through the swamp CLI. When logged in
(`swamp auth login`), issues are submitted directly to the swamp.club Lab. When
not logged in, the user is prompted to log in or send via email. The `--email`
flag skips straight to a pre-filled email.

To view an existing issue, use `swamp issue get <number>` — this fetches and
displays the issue's title, type, status, author, body, assignees, and comment
count.

With `--extension <name>`, reports are routed to the extension's publisher
instead — either as a tagged swamp.club Lab issue (for `@swamp/*` extensions) or
Expand All @@ -28,16 +33,19 @@ a template) and non-interactive mode with `--title` and `--body` flags. `ripple`
takes a positional issue number and either opens the editor or accepts `--body`
directly.

| Command | Template sections |
| ----------------------------- | ------------------------------------------------------------- |
| `swamp issue bug` | Title, description, steps to reproduce, environment |
| `swamp issue feature` | Title, problem statement, proposed solution, alternatives |
| `swamp issue security` | Title, description, reproduction, affected components, impact |
| `swamp issue ripple <number>` | Free-form markdown body (no title) |
| Command | Purpose |
| ----------------------------- | --------------------------------------------------------------------------------------------- |
| `swamp issue get <number>` | Fetch and display issue details (title, type, status, author, body, assignees, comment count) |
| `swamp issue bug` | Title, description, steps to reproduce, environment |
| `swamp issue feature` | Title, problem statement, proposed solution, alternatives |
| `swamp issue security` | Title, description, reproduction, affected components, impact |
| `swamp issue ripple <number>` | Free-form markdown body (no title) |

**Basic non-interactive examples:**

```bash
swamp issue get 42
swamp issue get 42 --json
swamp issue bug --title "CLI crashes on empty input" --body "When running..." --json
swamp issue feature --title "Add dark mode" --body "I'd like..." --json
swamp issue security --title "..." --body "..." --json
Expand Down
16 changes: 16 additions & 0 deletions .claude/skills/swamp-issue/references/output_shapes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@
JSON output shapes for `swamp issue *` commands invoked with `--json`. Use these
to assert on results programmatically.

## Issue Get (`swamp issue get <number> --json`)

```json
{
"number": 42,
"title": "Add swamp issue get CLI command",
"type": "feature",
"status": "open",
"author": "stack72",
"body": "## Problem\n\nThe swamp CLI can submit issues...",
"assignees": ["alice", "bob"],
"commentCount": 3,
"serverUrl": "https://swamp.club"
}
```

## Plain Submission (no `--extension`)

**Lab submission** (user logged in):
Expand Down
4 changes: 3 additions & 1 deletion src/cli/commands/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@
import { Command } from "@cliffy/command";
import { issueBugCommand } from "./issue_bug.ts";
import { issueFeatureCommand } from "./issue_feature.ts";
import { issueGetCommand } from "./issue_get.ts";
import { issueRippleCommand } from "./issue_ripple.ts";
import { issueSecurityCommand } from "./issue_security.ts";

export const issueCommand = new Command()
.name("issue")
.description(
"Submit bug reports, feature requests, security reports, and ripples",
"Fetch issue details, submit bug reports, feature requests, security reports, and ripples",
)
.action(function () {
this.showHelp();
})
.command("get", issueGetCommand)
.command("bug", issueBugCommand)
.command("feature", issueFeatureCommand)
.command("security", issueSecurityCommand)
Expand Down
71 changes: 71 additions & 0 deletions src/cli/commands/issue_get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Swamp, an Automation Framework
// Copyright (C) 2026 System Initiative, Inc.
//
// This file is part of Swamp.
//
// Swamp is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation, with the Swamp
// Extension and Definition Exception (found in the "COPYING-EXCEPTION"
// file).
//
// Swamp is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Swamp. If not, see <https://www.gnu.org/licenses/>.

import { Command } from "@cliffy/command";
import { createContext, type GlobalOptions } from "../context.ts";
import {
consumeStream,
createLibSwampContext,
issueGet,
type IssueGetDeps,
} from "../../libswamp/mod.ts";
import { createIssueGetRenderer } from "../../presentation/renderers/issue_get.ts";
import { AuthRepository } from "../../infrastructure/persistence/auth_repository.ts";
import { SwampClubClient } from "../../infrastructure/http/swamp_club_client.ts";
import { UserError } from "../../domain/errors.ts";

// deno-lint-ignore no-explicit-any
type AnyOptions = any;

export const issueGetCommand = new Command()
.name("get")
.description("Fetch and display details of a swamp-club Lab issue")
.example("View issue details", "swamp issue get 42")
.example("Get issue as JSON", "swamp issue get 42 --json")
.arguments("<number:integer>")
.action(async function (options: AnyOptions, issueNumber: number) {
const ctx = createContext(options as GlobalOptions, ["issue", "get"]);

if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
throw new UserError("Issue number must be a positive integer.");
}

const credentials = await new AuthRepository().load();
if (!credentials) {
throw new UserError(
'Not logged in. Run "swamp auth login" first.',
);
}

const client = new SwampClubClient(credentials.serverUrl);
const deps: IssueGetDeps = {
fetchIssue: async (num) => {
const issue = await client.fetchIssue(credentials.apiKey, num);
return { ...issue, serverUrl: credentials.serverUrl };
},
};

const libCtx = createLibSwampContext({ logger: ctx.logger });
const renderer = createIssueGetRenderer(ctx.outputMode);

await consumeStream(
issueGet(libCtx, deps, { issueNumber }),
renderer.handlers(),
);
});
29 changes: 29 additions & 0 deletions src/cli/commands/issue_get_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Swamp, an Automation Framework
// Copyright (C) 2026 System Initiative, Inc.
//
// This file is part of Swamp.
//
// Swamp is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation, with the Swamp
// Extension and Definition Exception (found in the "COPYING-EXCEPTION"
// file).
//
// Swamp is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Swamp. If not, see <https://www.gnu.org/licenses/>.

import { assertEquals } from "@std/assert";
import { issueGetCommand } from "./issue_get.ts";

Deno.test("issueGetCommand: has correct name and description", () => {
assertEquals(issueGetCommand.getName(), "get");
assertEquals(
issueGetCommand.getDescription(),
"Fetch and display details of a swamp-club Lab issue",
);
});
53 changes: 53 additions & 0 deletions src/infrastructure/http/swamp_club_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ export function getCollectives(
return whoami.organizations.map((org) => org.slug);
}

/** Response from fetching a Lab issue by number. */
export interface FetchIssueResponse {
number: number;
title: string;
type: string;
status: string;
author: string;
body: string;
assignees: string[];
commentCount: number;
}

/**
* HTTP client for swamp-club API interactions.
* Used by auth commands to sign in, create API keys, and verify identity.
Expand Down Expand Up @@ -252,6 +264,47 @@ export class SwampClubClient {
return { id: data.id };
}

/**
* Fetch an existing Lab issue by number.
* Authenticates using the x-api-key header.
*/
async fetchIssue(
apiKey: string,
issueNumber: number,
): Promise<FetchIssueResponse> {
const res = await this.fetch(`/api/v1/lab/issues/${issueNumber}`, {
method: "GET",
headers: { "x-api-key": apiKey },
});

if (!res.ok) {
const text = await res.text();
if (res.status === 404) {
throw new UserError(`Issue #${issueNumber} not found.`);
}
throw new UserError(
`Failed to fetch issue #${issueNumber} (HTTP ${res.status}): ${text}`,
);
}

const data = await res.json();
const issue = data.issue;
return {
number: issue.number,
title: issue.title ?? "",
type: issue.type ?? "feature",
status: issue.status ?? "open",
author: issue.authorUsername ?? "unknown",
body: issue.body ?? "",
assignees: (issue.assignees ?? [])
.filter(
(a: Record<string, unknown>) => typeof a.username === "string",
)
.map((a: Record<string, string>) => a.username),
commentCount: (issue.comments ?? []).length,
};
}

private async fetch(
path: string,
init: RequestInit,
Expand Down
67 changes: 67 additions & 0 deletions src/libswamp/issues/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Swamp, an Automation Framework
// Copyright (C) 2026 System Initiative, Inc.
//
// This file is part of Swamp.
//
// Swamp is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation, with the Swamp
// Extension and Definition Exception (found in the "COPYING-EXCEPTION"
// file).
//
// Swamp is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Swamp. If not, see <https://www.gnu.org/licenses/>.

import type { LibSwampContext } from "../context.ts";
import type { SwampError } from "../errors.ts";
import { withGeneratorSpan } from "../../infrastructure/tracing/mod.ts";

export interface IssueGetData {
number: number;
title: string;
type: string;
status: string;
author: string;
body: string;
assignees: string[];
commentCount: number;
serverUrl: string;
}

export type IssueGetEvent =
| { kind: "completed"; data: IssueGetData }
| { kind: "error"; error: SwampError };

export interface IssueGetInput {
issueNumber: number;
}

export interface IssueGetDeps {
fetchIssue: (issueNumber: number) => Promise<IssueGetData>;
}

export async function* issueGet(
ctx: LibSwampContext,
deps: IssueGetDeps,
input: IssueGetInput,
): AsyncIterable<IssueGetEvent> {
yield* withGeneratorSpan(
"swamp.issue.get",
{},
(async function* () {
ctx.logger.debug`Fetching issue #${input.issueNumber}`;

const result = await deps.fetchIssue(input.issueNumber);

yield {
kind: "completed",
data: result,
};
})(),
);
}
Loading
Loading