Skip to content

Commit d9e37d8

Browse files
authored
Merge pull request #12 from nullclaw/feat/report
Add GitHub issue reporting flow to nullhub
2 parents dfd6d71 + ef44bd1 commit d9e37d8

18 files changed

Lines changed: 5228 additions & 9 deletions

File tree

docs/superpowers/plans/2026-03-18-report-command.md

Lines changed: 1902 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
# Report Command Design
2+
3+
Create GitHub issues with pre-filled system data from CLI and Web UI.
4+
5+
## Target repositories
6+
7+
| # | Component | GitHub repo |
8+
|---|-----------|-------------|
9+
| 1 | nullhub | nullclaw/nullhub |
10+
| 2 | nullclaw | nullclaw/nullclaw |
11+
| 3 | nullboiler | nullclaw/NullBoiler |
12+
| 4 | nulltickets | nullclaw/nulltickets |
13+
| 5 | nullwatch | nullclaw/nullwatch |
14+
15+
Order is fixed (as listed above) in both CLI and Web UI selectors.
16+
17+
The target list is hardcoded in `report.zig`, independent of `known_components` in `registry.zig`. nullwatch exists as a repo but is not yet in the component registry.
18+
19+
## Report types and labels
20+
21+
| Report type | CLI value | Label(s) |
22+
|---|---|---|
23+
| Bug: crash (process exits or hangs) | `bug:crash` | `bug`, `bug:crash` |
24+
| Bug: behavior (incorrect output/state) | `bug:behavior` | `bug`, `bug:behavior` |
25+
| Bug: regression (worked before, now fails) | `regression` | `bug`, `regression` |
26+
| Feature request | `feature` | `enhancement` |
27+
28+
Labels `regression`, `bug:behavior`, `bug:crash` must be created in all 5 repos before launch (one-time script, not committed).
29+
30+
## System data collected automatically
31+
32+
| Field | Source | Notes |
33+
|---|---|---|
34+
| nullhub version | `version.zig` (CalVer, e.g. `2026.3.13`) | Always available (compile-time) |
35+
| Platform | `platform.zig` (e.g. `aarch64-macos`) | Always available (compile-time) |
36+
| OS version | `uname -s -r` at runtime | Child process call |
37+
| Installed components + versions | `state.json` via `state.zig` | Versions only, no runtime status — CLI runs without the server |
38+
39+
Feature requests include only nullhub version and platform (intentionally lightweight — system details are less relevant for feature requests).
40+
41+
## Issue body format
42+
43+
### Bug report
44+
45+
```markdown
46+
### Bug type
47+
48+
<bug type text>
49+
50+
### Description
51+
52+
<user description>
53+
54+
### System information
55+
56+
| Field | Value |
57+
|---|---|
58+
| nullhub version | 2026.3.13 |
59+
| Platform | aarch64-macos |
60+
| OS version | Darwin 25.1.0 |
61+
62+
### Installed components
63+
64+
| Component | Version |
65+
|---|---|
66+
| nullclaw | 2026.3.14 |
67+
| nullboiler | 2026.3.10 |
68+
```
69+
70+
Title: `[Bug]: <description>`
71+
72+
### Feature request
73+
74+
```markdown
75+
### Summary
76+
77+
<user description>
78+
79+
### System information
80+
81+
| Field | Value |
82+
|---|---|
83+
| nullhub version | 2026.3.13 |
84+
| Platform | aarch64-macos |
85+
```
86+
87+
Title: `[Feature]: <description>`
88+
89+
## CLI flow
90+
91+
### Interactive mode
92+
93+
```
94+
$ nullhub report
95+
96+
Where is the problem?
97+
1. nullhub
98+
2. nullclaw
99+
3. nullboiler
100+
4. nulltickets
101+
5. nullwatch
102+
> 1
103+
104+
Report type?
105+
1. Bug: crash (process exits or hangs)
106+
2. Bug: behavior (incorrect output/state)
107+
3. Bug: regression (worked before, now fails)
108+
4. Feature request
109+
> 2
110+
111+
Description: Dashboard shows stale status after restart
112+
113+
Preview:
114+
──────────────────────────
115+
Title: [Bug]: Dashboard shows stale status after restart
116+
117+
### Bug type
118+
...
119+
──────────────────────────
120+
Submit? [Y/n/e]
121+
```
122+
123+
- `Y` / Enter — submit
124+
- `n` — cancel
125+
- `e` — open `$EDITOR` to edit the full markdown before submitting. If `$EDITOR` is unset, falls back to `vi`. Writes markdown to a temp file, invokes editor, reads back on exit, deletes temp file.
126+
127+
### Non-interactive mode
128+
129+
```
130+
$ nullhub report --repo nullhub --type bug:behavior --message "Dashboard shows stale status"
131+
```
132+
133+
Flags:
134+
- `--repo <name>` — one of: nullhub, nullclaw, nullboiler, nulltickets, nullwatch
135+
- `--type <type>` — one of: bug:crash, bug:behavior, regression, feature
136+
- `--message <text>` — one-line description
137+
- `--yes` — skip confirmation (for scripting)
138+
- `--dry-run` — show preview and exit without submitting
139+
140+
Non-interactive mode still shows preview and asks for confirmation unless `--yes` is passed.
141+
142+
### TTY detection
143+
144+
If stdin is not a TTY (piped input), all three flags (`--repo`, `--type`, `--message`) are required. If any is missing, print error and exit. No interactive prompts in non-TTY mode.
145+
146+
## Web UI flow
147+
148+
Page at `/report`:
149+
150+
1. Select: repository (dropdown, same order as CLI)
151+
2. Select: report type (dropdown)
152+
3. Input: description (single-line text input)
153+
4. Click "Next" → calls `POST /api/report/preview` → shows preview step with full markdown in an editable textarea
154+
5. Click "Submit" (calls `POST /api/report` with final markdown) or "Back" to edit inputs
155+
156+
On success: show link to created issue.
157+
On fallback: show copyable markdown block + hint to install gh.
158+
159+
System data collected on server side — user does not fill system fields manually.
160+
161+
## Submission fallback chain
162+
163+
Each step falls through to the next on failure (non-zero exit, network error, missing tool, etc.):
164+
165+
```
166+
1. `gh` in PATH + `gh auth status` succeeds?
167+
→ gh issue create --repo <repo> --title <title> --body <body> --label <labels>
168+
→ Success: return issue URL
169+
170+
2. Token from `gh auth token`?
171+
→ curl POST https://api.github.com/repos/<repo>/issues
172+
-H "Authorization: Bearer <token>"
173+
→ Success: return issue URL
174+
175+
3. $GITHUB_TOKEN env var set?
176+
→ curl POST (same as above with env token)
177+
→ Success: return issue URL
178+
179+
4. No auth available:
180+
→ Output formatted markdown + hint:
181+
"Install and authenticate gh CLI to submit automatically:
182+
https://cli.github.com/"
183+
```
184+
185+
Web UI uses the same chain server-side. If all fail, API returns the fallback response and UI renders a copyable block.
186+
187+
## Architecture
188+
189+
### New files
190+
191+
| File | Purpose |
192+
|---|---|
193+
| `src/report.zig` | Core module: repo/type enums, system data collection, issue body formatting, submission via fallback chain |
194+
| `src/report_cli.zig` | Interactive CLI flow: stdin prompts, flag parsing, preview, editor support |
195+
| `src/api/report.zig` | API endpoint handlers for `POST /api/report` and `POST /api/report/preview` |
196+
| `ui/src/routes/report/+page.svelte` | Report form page |
197+
198+
### Modified files
199+
200+
| File | Change |
201+
|---|---|
202+
| `src/cli.zig` | Add `ReportOptions` struct, `ReportRepo` enum, `ReportType` enum, `report: ReportOptions` to `Command` union, `parseReport()` sub-parser, update `printUsage()` |
203+
| `src/main.zig` | Add `.report` case to command dispatch switch |
204+
| `src/server.zig` | Add `POST /api/report` and `POST /api/report/preview` routes |
205+
206+
### Enums and options
207+
208+
```zig
209+
pub const ReportRepo = enum {
210+
nullhub,
211+
nullclaw,
212+
nullboiler,
213+
nulltickets,
214+
nullwatch,
215+
216+
pub fn fromStr(s: []const u8) ?ReportRepo { ... }
217+
pub fn toGithubRepo(self: ReportRepo) []const u8 { ... }
218+
pub fn displayName(self: ReportRepo) []const u8 { ... }
219+
};
220+
221+
pub const ReportType = enum {
222+
bug_crash,
223+
bug_behavior,
224+
regression,
225+
feature,
226+
227+
pub fn fromStr(s: []const u8) ?ReportType { ... }
228+
pub fn toLabels(self: ReportType) []const []const u8 { ... }
229+
pub fn displayName(self: ReportType) []const u8 { ... }
230+
};
231+
232+
pub const ReportOptions = struct {
233+
repo: ?ReportRepo = null,
234+
report_type: ?ReportType = null,
235+
message: ?[]const u8 = null,
236+
yes: bool = false,
237+
dry_run: bool = false,
238+
};
239+
```
240+
241+
### API endpoints
242+
243+
#### `POST /api/report/preview`
244+
245+
Generates preview without submitting.
246+
247+
Request:
248+
```json
249+
{
250+
"repo": "nullhub",
251+
"type": "bug:behavior",
252+
"message": "Dashboard shows stale status"
253+
}
254+
```
255+
256+
Response:
257+
```json
258+
{
259+
"title": "[Bug]: Dashboard shows stale status",
260+
"markdown": "### Bug type\n...",
261+
"labels": ["bug", "bug:behavior"],
262+
"repo": "nullclaw/nullhub"
263+
}
264+
```
265+
266+
#### `POST /api/report`
267+
268+
Submits issue (with optional edited markdown from preview).
269+
270+
Request:
271+
```json
272+
{
273+
"repo": "nullhub",
274+
"type": "bug:behavior",
275+
"message": "Dashboard shows stale status",
276+
"markdown": "### Bug type\n..."
277+
}
278+
```
279+
280+
The `markdown` field is optional. If provided (edited by user in preview), it replaces the auto-generated body. If omitted, the server generates it.
281+
282+
Success response:
283+
```json
284+
{
285+
"status": "created",
286+
"url": "https://github.com/nullclaw/nullhub/issues/42"
287+
}
288+
```
289+
290+
Fallback response (no auth):
291+
```json
292+
{
293+
"status": "no_auth",
294+
"title": "[Bug]: Dashboard shows stale status",
295+
"markdown": "### Bug type\n...",
296+
"labels": ["bug", "bug:behavior"],
297+
"repo": "nullclaw/nullhub",
298+
"hint": "Install and authenticate gh CLI to submit automatically: https://cli.github.com/"
299+
}
300+
```
301+
302+
Error response (invalid input):
303+
```json
304+
{
305+
"status": "error",
306+
"error": "invalid repo: foo"
307+
}
308+
```
309+
310+
Returns HTTP 400 for invalid/missing fields (repo, type, message).
311+
312+
### Auth on report endpoint
313+
314+
`POST /api/report` and `POST /api/report/preview` follow the existing auth pattern in `server.zig` — if a bearer token is configured, these endpoints require it. Same as all other `/api/` routes.

src/api/channels.zig

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,7 @@ fn probeChannel(
343343
config_json: []const u8,
344344
) ProbeResult {
345345
// Create temp dir for minimal config
346-
const timestamp = @abs(std.time.milliTimestamp());
347-
const tmp_dir = std.fmt.allocPrint(allocator, "/tmp/nullhub-channel-validate-{d}", .{timestamp}) catch
346+
const tmp_dir = paths_mod.uniqueTempPathAlloc(allocator, "nullhub-channel-validate", "") catch
348347
return .{ .live_ok = false, .reason = "tmp_dir_failed" };
349348
defer {
350349
std.fs.deleteTreeAbsolute(tmp_dir) catch {};

src/api/providers.zig

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,7 @@ fn probeProvider(
261261
base_url: []const u8,
262262
) wizard_api.ProviderProbeResult {
263263
// Create temp dir for minimal config
264-
const timestamp = @abs(std.time.milliTimestamp());
265-
const tmp_dir = std.fmt.allocPrint(allocator, "/tmp/nullhub-provider-validate-{d}", .{timestamp}) catch
264+
const tmp_dir = paths_mod.uniqueTempPathAlloc(allocator, "nullhub-provider-validate", "") catch
266265
return .{ .live_ok = false, .reason = "tmp_dir_failed" };
267266
defer {
268267
std.fs.deleteTreeAbsolute(tmp_dir) catch {};

0 commit comments

Comments
 (0)