hex-audit 0.4.5 → 0.5.0

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.
Files changed (77) hide show
  1. package/DEMO.md +12 -11
  2. package/README.md +38 -37
  3. package/dashboard/.next/BUILD_ID +1 -1
  4. package/dashboard/.next/app-build-manifest.json +13 -13
  5. package/dashboard/.next/app-path-routes-manifest.json +1 -1
  6. package/dashboard/.next/build-manifest.json +2 -2
  7. package/dashboard/.next/next-server.js.nft.json +1 -1
  8. package/dashboard/.next/prerender-manifest.json +1 -1
  9. package/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  10. package/dashboard/.next/server/app/_not-found.html +1 -1
  11. package/dashboard/.next/server/app/_not-found.rsc +4 -4
  12. package/dashboard/.next/server/app/access/page_client-reference-manifest.js +1 -1
  13. package/dashboard/.next/server/app/api/issues/[id]/move/route.js +1 -1
  14. package/dashboard/.next/server/app/api/issues/[id]/route.js +1 -1
  15. package/dashboard/.next/server/app/calls/page_client-reference-manifest.js +1 -1
  16. package/dashboard/.next/server/app/conformance/page_client-reference-manifest.js +1 -1
  17. package/dashboard/.next/server/app/diagram/page_client-reference-manifest.js +1 -1
  18. package/dashboard/.next/server/app/diagram.html +1 -1
  19. package/dashboard/.next/server/app/diagram.rsc +4 -4
  20. package/dashboard/.next/server/app/flows/page_client-reference-manifest.js +1 -1
  21. package/dashboard/.next/server/app/flows.html +1 -1
  22. package/dashboard/.next/server/app/flows.rsc +4 -4
  23. package/dashboard/.next/server/app/functions/page_client-reference-manifest.js +1 -1
  24. package/dashboard/.next/server/app/issues/page.js +2 -2
  25. package/dashboard/.next/server/app/issues/page_client-reference-manifest.js +1 -1
  26. package/dashboard/.next/server/app/overleaf/page_client-reference-manifest.js +1 -1
  27. package/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  28. package/dashboard/.next/server/app/progress/page_client-reference-manifest.js +1 -1
  29. package/dashboard/.next/server/app/stats/page_client-reference-manifest.js +1 -1
  30. package/dashboard/.next/server/app-paths-manifest.json +7 -7
  31. package/dashboard/.next/server/chunks/431.js +1 -1
  32. package/dashboard/.next/server/functions-config-manifest.json +1 -1
  33. package/dashboard/.next/server/middleware-build-manifest.js +1 -1
  34. package/dashboard/.next/server/pages/404.html +1 -1
  35. package/dashboard/.next/server/pages/500.html +1 -1
  36. package/dashboard/.next/server/server-reference-manifest.js +1 -1
  37. package/dashboard/.next/server/server-reference-manifest.json +1 -1
  38. package/dashboard/.next/static/chunks/app/issues/page-2bb52a36f20497fe.js +1 -0
  39. package/dashboard/.next/static/chunks/app/{layout-cde7d75ea6df4f81.js → layout-9c3c84993dd895f6.js} +1 -1
  40. package/dashboard/.next/static/css/{36c6bb859d4545fd.css → 9c84313d0484b74f.css} +1 -1
  41. package/dashboard/app/issues/IssuesBoardClient.tsx +55 -24
  42. package/dashboard/app/issues/page.tsx +6 -3
  43. package/dashboard/components/DashboardLayout.tsx +1 -1
  44. package/dashboard/lib/finding-markdown.ts +53 -2
  45. package/dist/commands/dashboard.d.ts.map +1 -1
  46. package/dist/commands/dashboard.js +12 -3
  47. package/dist/commands/dashboard.js.map +1 -1
  48. package/dist/commands/init.d.ts.map +1 -1
  49. package/dist/commands/init.js +2 -0
  50. package/dist/commands/init.js.map +1 -1
  51. package/dist/commands/issue.d.ts.map +1 -1
  52. package/dist/commands/issue.js +85 -5
  53. package/dist/commands/issue.js.map +1 -1
  54. package/dist/core/config.d.ts +1 -0
  55. package/dist/core/config.d.ts.map +1 -1
  56. package/dist/core/config.js +10 -0
  57. package/dist/core/config.js.map +1 -1
  58. package/dist/core/issues.d.ts +49 -12
  59. package/dist/core/issues.d.ts.map +1 -1
  60. package/dist/core/issues.js +174 -24
  61. package/dist/core/issues.js.map +1 -1
  62. package/dist/core/schema.d.ts +24 -8
  63. package/dist/core/schema.d.ts.map +1 -1
  64. package/dist/core/schema.js +12 -1
  65. package/dist/core/schema.js.map +1 -1
  66. package/dist/index.js +1 -1
  67. package/dist/skills/generate-overleaf.md +18 -10
  68. package/dist/skills/ingest-aa-report.md +15 -19
  69. package/dist/skills/init-audit.md +13 -20
  70. package/dist/skills/sync-issues.md +87 -179
  71. package/dist/skills/validate-all-findings.md +50 -0
  72. package/dist/skills/validate-issue.md +3 -3
  73. package/dist/skills/write-finding.md +23 -28
  74. package/package.json +1 -1
  75. package/dashboard/.next/static/chunks/app/issues/page-30f7bcde0f4f6ddb.js +0 -1
  76. /package/dashboard/.next/static/{4HWoeGmBSZi6iGcKL3VWA → NrFPHIFHWQ5jNF62Px_X8}/_buildManifest.js +0 -0
  77. /package/dashboard/.next/static/{4HWoeGmBSZi6iGcKL3VWA → NrFPHIFHWQ5jNF62Px_X8}/_ssgManifest.js +0 -0
@@ -1,249 +1,157 @@
1
1
  ---
2
- description: 'Two-way sync between local Hex findings and GitHub Issues — pulls teammate issues into the board (canonical), pushes verified findings out, runs inline dedup'
2
+ description: 'Two-way sync between local Hex findings and GitHub Issues — GitHub is the source of truth; identity is the issue number'
3
3
  ---
4
4
 
5
5
  # Skill: Sync Issues
6
6
 
7
7
  **Recommended model:** Sonnet
8
8
 
9
- This skill is Hex's GitHub Issues integration. Hex never authenticates with GitHub — you authenticate once via `gh auth login` and this skill drives the `gh` CLI via bash. A single invocation runs both directions plus inline dedup.
9
+ Hex's GitHub Issues integration. Hex never authenticates with GitHub — you authenticate once via `gh auth login` and this skill drives the `gh` CLI. One invocation runs both directions.
10
10
 
11
- **Dedup direction note.** GitHub is the **source of truth**. When a teammate's pulled GitHub issue matches a local potential issue (same root cause, same contract/function), the **local** entry is marked Duplicate — not the GitHub one. The rationale is multi-auditor coordination: if a teammate already filed an issue, your in-progress local card is redundant. This is opposite of the auditagent dedup direction in `/ingest-aa-report`.
11
+ ## The model: GitHub is the source of truth, identity is the issue number
12
12
 
13
- ## Context Assembly
13
+ Once an issue is on GitHub it is **read-only in Hex** (status `synced`, locked on the board). Edits — severity, description, recommendation, status — happen on GitHub; this skill pulls them back into the local finding. `/generate-overleaf` reports **only** from synced issues.
14
14
 
15
- Read:
16
- - `<output_dir>/config.json` — needs `settings.github.repo` set.
17
- - `<output_dir>/findings.json` — local canonical findings.
18
- - `<output_dir>/tracking.json` — for status filtering on push (default: only `verified` is pushed).
19
- - `<output_dir>/external/github/findings.json` if it exists — last pulled state.
20
- - `<output_dir>/external/github/sync-status.json` for `last_synced_at`.
15
+ **Identity is the GitHub issue number `#N`.** There is no hidden footer in the issue body (auditors create issues by hand, so a footer can't be relied on). Reconciliation matches a local finding to the issue number it has stored in `github.issue_number`. The body is exactly the five-field template, nothing hidden.
21
16
 
22
- Writes to:
23
- - `<output_dir>/findings.json` — updates `github.*` blocks on local findings.
24
- - `<output_dir>/external/github/findings.json` — teammate findings in `AiResultFile` shape.
25
- - `<output_dir>/external/github/sync-status.json` — counters + timestamp.
26
- - `<output_dir>/external/github/raw-issues.json` — debug cache of the gh JSON.
27
- - `<output_dir>/tracking.json` — teammate findings as new entries; local entries flipped to `duplicate` on match.
28
- - `<output_dir>/comparison.json` — appended with `match_signals` for each dedup hit.
17
+ All board mutations go through `hex issue` — never hand-edit `tracking.json` / `findings.json`.
29
18
 
30
- ## Phase 0 — Preflight
31
-
32
- **0a. `gh` installed and authenticated?** Run `gh auth status`. If non-zero exit, print:
19
+ ## Context
33
20
 
34
- ```
35
- gh CLI is not authenticated. Run `gh auth login` once on this machine, then re-run /sync-issues.
36
- ```
21
+ - `<output_dir>/config.json` — needs `settings.github.repo`.
22
+ - `<output_dir>/findings.json` — local findings (some already carry a `github.issue_number`).
23
+ - `<output_dir>/tracking.json` — status (`verified` are push candidates).
37
24
 
38
- …and stop. Hex never stores GitHub credentials.
25
+ ## Phase 0 — Preflight
39
26
 
40
- If `gh` itself is missing: instruct install from `https://cli.github.com/` then `gh auth login`.
27
+ **0a.** `gh auth status`. If non-zero, print "gh CLI is not authenticated. Run `gh auth login` once, then re-run /sync-issues." and stop. If `gh` is missing, point to `https://cli.github.com/`.
41
28
 
42
- **0b. Resolve `settings.github.repo`.** If missing, use **AskUserQuestion**: "Which GitHub repo should this audit sync to? (e.g. `nethermind/audit-vaultx`)". Validate it has an `owner/repo` shape. Persist back to `config.json`.
29
+ **0b.** Resolve `settings.github.repo`. If missing, **AskUserQuestion** for `owner/repo`, validate the shape, persist to `config.json`.
43
30
 
44
- **0c. Permission check.** Run `gh repo view <repo> --json viewerPermission`. If `READ` or null, **AskUserQuestion**: "Read-only access to <repo>. Push will fail. Continue with pull-only? (yes/no)". Track `push_enabled`.
31
+ **0c.** `gh repo view <repo> --json viewerPermission`. If `READ`/null, **AskUserQuestion**: "Read-only access — push will fail. Continue pull-only? (yes/no)". Track `push_enabled`.
45
32
 
46
- ## Phase 1 — Pull
33
+ ## Phase 1 — Pull (all issues, GitHub → local)
47
34
 
48
- **1a. Fetch issues.**
35
+ **1a. Fetch every issue — no label filter.** Audit repos contain only audit findings, so pull them all:
49
36
 
50
37
  ```bash
51
- gh issue list --repo <repo> --label "hex" --state all --limit 1000 \
52
- --json number,title,body,labels,state,author,createdAt,updatedAt,url
38
+ gh issue list --repo <repo> --state all --limit 1000 \
39
+ --json number,title,body,state,author,createdAt,updatedAt,url
53
40
  ```
54
41
 
55
- Save raw JSON to `external/github/raw-issues.json`.
56
-
57
- For each issue whose `updatedAt` is newer than the prior `last_synced_at`, fetch comments:
42
+ **1b. Parse each issue.** Title is `[Severity] Title`. Body is the five-field template:
58
43
 
59
- ```bash
60
- gh issue view <number> --repo <repo> --json comments
61
- ```
44
+ ```markdown
45
+ **File(s)**: [`<file>`](<url>)
62
46
 
63
- (First sync: fetch for everything.)
47
+ **Description**: <text>
64
48
 
65
- **1b. Classify each issue.**
49
+ **Recommendation(s)**: <text>
66
50
 
67
- Find the round-trip footer in the body: regex `<!--\s*hex-finding-id:\s*(F\d+|AA-\d+|SC-\d+)\s*-->`. Then:
51
+ **Status**: <Fixed|Mitigated|Acknowledged|Not Fixed|Unresolved>
68
52
 
69
- - **Footer present, ID matches a local `findings.json` entry** → known local finding. Update its `github` block in place:
70
- ```json
71
- {
72
- "issue_number": <num>,
73
- "issue_url": "<url>",
74
- "state": "open|closed",
75
- "last_synced_at": "<now>",
76
- "sync_status": "in_sync",
77
- "comments": [{ "author": "<login>", "body": "<text>", "created_at": "<iso>", "url": "<url>" }]
78
- }
79
- ```
53
+ **Update from the client**: <text>
54
+ ```
80
55
 
81
- - **Footer present, no matching local finding** → an issue we previously pushed that's since been deleted locally. Append a tracking entry restoring the link (status `pending_validation`, source `manual`, notes pointing at the URL). Do not touch `findings.json` — the body may have been edited remotely.
82
-
83
- - **No footer** → teammate finding. Two writes:
84
- 1. Add to the in-memory `external/github/findings.json` AiResultFile (`tool: "github"`, severity parsed from `severity:*` labels, default Medium):
85
- ```json
86
- {
87
- "id": "github-<issue_number>",
88
- "tool": "github",
89
- "title": "<issue title>",
90
- "severity": "Critical|High|Medium|Low|Info",
91
- "description": "<body without footer>",
92
- "affected_code": [<best-effort file refs extracted from body>],
93
- "confidence": "medium",
94
- "category": "<from label if present>",
95
- "raw_category": "github-issue"
96
- }
97
- ```
98
- 2. Add to `tracking.json`:
99
- ```json
100
- {
101
- "id": "github-<issue_number>",
102
- "title": "<title>",
103
- "severity": "<severity>",
104
- "source": "github",
105
- "status": "pending_validation",
106
- "poc_status": "not_started",
107
- "poc_file": null,
108
- "duplicates": [],
109
- "notes": "issue=<url> author=<author>"
110
- }
111
- ```
112
-
113
- Write `external/github/findings.json` atomically after the pass.
114
-
115
- ## Phase 2 — Inline dedup (GitHub-canonical)
116
-
117
- For each **new github entry** added in Phase 1 step 1b's third branch, run a semantic comparison against every **existing** non-github tracking entry. Match signals:
56
+ Extract severity from the title, and the four body fields. If a body is **blatantly malformed** (no `**Description**:` marker), warn (`Skipping #N: not in the Hex finding format`) and skip it — do not fail the whole sync. Otherwise treat it as a valid finding (most will be).
118
57
 
119
- | Signal | Compare |
120
- |---|---|
121
- | **contract** (boolean) | Affected contract name match? |
122
- | **function** (boolean) | Affected function name match? |
123
- | **root_cause** (`same` / `overlapping` / `different`) | Technical mechanism |
124
- | **attack_vector** (`same` / `overlapping` / `different`) | What an attacker actually does |
125
-
126
- A match requires at least one of:
127
- - `root_cause = same` AND `function = true`, or
128
- - `root_cause = same` AND `contract = true` AND `attack_vector = same`, or
129
- - `root_cause = overlapping` AND `function = true` AND `attack_vector = same`.
130
-
131
- **On match (GitHub is canonical):**
132
-
133
- - Flip the **local** matching entry to `status: "duplicate"`, set `duplicate_of: "github-<issue_number>"` on it.
134
- - The github entry stays `pending_validation` (it's the canonical the team is working from).
135
- - Append to `comparison.json` under `duplicates`:
136
- ```json
137
- {
138
- "ai_finding": "<local-entry-id>",
139
- "matches": "github-<issue_number>",
140
- "confidence": "high|medium|low",
141
- "match_signals": { "contract": true, "function": true, "root_cause": "same", "attack_vector": "same" },
142
- "reasoning": "<one short sentence>"
143
- }
58
+ **1c. Reconcile by issue number `#N`:**
59
+ - **A local finding already records `github.issue_number === N`** → it's the same issue; pull GitHub's content into it:
60
+ ```bash
61
+ npx hex issue sync-set <local-H-id> --issue-number N --issue-url <url> --state <open|closed> \
62
+ --title "<title-without-[Severity]>" --severity <Severity> \
63
+ --description-file /tmp/N_desc.md --recommendation-file /tmp/N_reco.md \
64
+ --resolution "<Status>" --update-from-client-file /tmp/N_update.md
65
+ ```
66
+ - **No local finding has `#N`** (a teammate's hand-added issue, or one created outside Hex) → create a local finding for it, then sync-set:
67
+ ```bash
68
+ ID=$(npx hex issue new --source github --source-ref N --title "<title>" --severity <Severity>)
69
+ npx hex issue sync-set "$ID" --issue-number N --issue-url <url> --state <open|closed> \
70
+ --description-file /tmp/N_desc.md --recommendation-file /tmp/N_reco.md \
71
+ --resolution "<Status>" --update-from-client-file /tmp/N_update.md
144
72
  ```
145
- (The schema field is named `ai_finding` for historical reasons but is repurposed here as "the local duplicate.")
146
73
 
147
- Persist updated `tracking.json` and `comparison.json` after the dedup pass.
74
+ Either way the card lands in the **Synced** column, locked. Reconcile strictly by `#N`; never scan the body for an identifier.
148
75
 
149
- ## Phase 3 — Push
76
+ ## Phase 1.5 — Dedup before push (GitHub is canonical)
150
77
 
151
- Skip if `push_enabled = false`.
78
+ Before pushing, guard against filing a finding that is already on GitHub (e.g. a teammate created it by hand, and you pulled it into Synced in Phase 1). For each **verified** local finding that has no `github.issue_number` yet, semantically compare it against every **synced** finding on these axes:
152
79
 
153
- **3a. Candidates.** From `findings.json`, select each finding whose tracking entry has `status` in `settings.github.publish_status` (default `["verified"]`). Skip findings already marked `status: "duplicate"` (no point publishing a known duplicate).
80
+ | Signal | Compare |
81
+ |---|---|
82
+ | **contract** (boolean) | affected contract name |
83
+ | **function** (boolean) | affected function name |
84
+ | **root_cause** (`same`/`overlapping`/`different`) | the technical mechanism |
85
+ | **attack_vector** (`same`/`overlapping`/`different`) | what an attacker actually does |
154
86
 
155
- Within candidates:
156
- - Has `github.issue_number` → update via `gh issue edit`.
157
- - No `github.issue_number` → create via `gh issue create`.
87
+ A match is at least one of: `root_cause = same` AND `function = true`; or `root_cause = same` AND `contract = true` AND `attack_vector = same`; or `root_cause = overlapping` AND `function = true` AND `attack_vector = same`.
158
88
 
159
- **3b. Render the body** (write to a temp file, then `--body-file`). Use this exact template — nothing more, nothing less in the visible body. It mirrors `dashboard/lib/finding-markdown.ts::findingToGithubBody`, so the board's "Copy as HackMD" output and the pushed issue stay identical:
89
+ On match — the synced (on-GitHub) finding is canonical, so the local verified copy becomes the duplicate. Do **not** push it:
160
90
 
161
- ```markdown
162
- **File(s)**: [`<file>`](<github-blob-url-or-empty>)
91
+ ```bash
92
+ npx hex issue move <verified-H-id> --to duplicate --duplicate-of <synced-H-id>
93
+ ```
163
94
 
164
- **Description**: <finding.description>
95
+ Then append to `<output_dir>/comparison.json` under `duplicates` (the board renders the chip + signals from this):
165
96
 
166
- **Recommendation(s)**: <finding.recommendation>
97
+ ```json
98
+ { "ai_finding": "<verified-H-id>", "matches": "<synced-H-id>", "confidence": "high|medium|low",
99
+ "match_signals": { "contract": true, "function": true, "root_cause": "same", "attack_vector": "same" },
100
+ "reasoning": "<one sentence>" }
101
+ ```
167
102
 
168
- **Status**: <finding.resolution or "Unresolved">
103
+ If `comparison.json` doesn't exist, create it with `{ "duplicates": [], "novel": [], "rejected": [] }`.
169
104
 
170
- **Update from the client**: <finding.update_from_client or empty>
105
+ ## Phase 2 — Push (verified local findings → GitHub)
171
106
 
172
- <!-- hex-finding-id: <finding.id> -->
173
- ```
107
+ Skip if `push_enabled` is false.
174
108
 
175
- Rules:
176
- - **File(s)** — one markdown link per affected file in `root_cause.locations[]`, comma-separated when there is more than one: `` [`src/Vault.sol`](url1), [`src/MockToken.sol`](url2) ``. Build each URL as `<settings.report.repository_url>/blob/<settings.report.initial_commit>/<file>`. If either `repository_url` or `initial_commit` is missing from `config.json`, use an empty link target: `` [`<file>`]() ``.
177
- - **Status** — the finding's `resolution` field if set, otherwise the literal `Unresolved`.
178
- - **Update from the client** — the finding's `update_from_client` field if set, otherwise leave it blank after the colon.
179
- - Do NOT add a `**Severity:**` line (severity is already in the title), an `### Affected code` section, code blocks, or a "Synced from Hex" attribution line.
180
- - The `<!-- hex-finding-id: <finding.id> -->` HTML comment at the very bottom is **mandatory and load-bearing** — it is the round-trip dedup key, invisible in GitHub's rendered view. Never omit it.
109
+ Candidates: findings whose tracking status is in `settings.github.publish_status` (default `["verified"]`) **and** which have no `github.issue_number` yet, **and** which Phase 1.5 did not flag as duplicates. (Synced findings are already on GitHub; duplicates/rejected/unverified are never pushed.)
181
110
 
182
- **3c. Create or update — no labels.** Hex does not apply, add, or remove labels. The target repo's label set is the repo owner's concern; assuming labels exist (e.g. `severity:critical`) makes `gh issue create` fail when they don't.
111
+ For each candidate, render the body with the exact five-field template (see Phase 1b — `dashboard/lib/finding-markdown.ts::findingToGithubBody` is the canonical renderer). Write it to a temp file. **No labels, no footer.**
183
112
 
184
- Create:
185
113
  ```bash
186
114
  gh issue create --repo <repo> --title "[<Severity>] <title>" --body-file <tmp>
187
115
  ```
188
116
 
189
- Parse the returned URL to extract the issue number; update the local finding's `github` block.
117
+ Parse the returned URL for `#N`, then lock it into Synced and record the number:
190
118
 
191
- Update existing:
192
119
  ```bash
193
- gh issue edit <num> --repo <repo> --title "[<Severity>] <title>" --body-file <tmp>
120
+ npx hex issue sync-set <H-id> --issue-number N --issue-url <url> --state open
194
121
  ```
195
122
 
196
- (`settings.github.default_labels` in config is retained for backwards compatibility but is no longer applied.)
197
-
198
- If push fails for any single finding: capture stderr, set its `github.sync_status` to `conflict`, append a one-line error, continue with the next finding. Never abort the whole sync on one failure.
199
-
200
- **3e. Persist** `findings.json` once after all pushes, atomically.
123
+ From now on the finding is identified by `#N`; the next pull matches it by number, so it never duplicates.
201
124
 
202
- ## Phase 4 — Status report
125
+ If a single `gh` call fails, log the error and continue with the next candidate — never abort the whole sync.
203
126
 
204
- Write `external/github/sync-status.json`:
205
-
206
- ```json
207
- {
208
- "repo": "<repo>",
209
- "last_synced_at": "<now>",
210
- "pushed": <new-created>,
211
- "updated": <existing-updated>,
212
- "pulled": <total-issues-seen>,
213
- "teammate_findings": <count-of-footer-less>,
214
- "duplicates_detected": <local-entries-flipped-to-duplicate>,
215
- "errors": [<one-line-per-error>]
216
- }
217
- ```
127
+ ## Phase 3 — Status report
218
128
 
219
129
  Print:
220
130
 
221
131
  ```
222
132
  GitHub sync complete (<repo>)
223
- ↑ Pushed N new, updated M
224
- ↓ Pulled K issues — J teammate findings, L matched local
225
- ⇄ Dedup flipped D local entries to Duplicate (GitHub is canonical)
226
- <if errors:>
227
- Errors:
228
- - <error 1>
133
+ ↓ Pulled K issues (S synced into the board, M skipped as malformed)
134
+ ↑ Pushed P verified findings as new issues
135
+ Errors: <n>
229
136
  ```
230
137
 
231
- ## Conflict resolution defaults
138
+ ## Conflict resolution
232
139
 
233
- - **Body content** — local wins on push. Reviewer edits inside `### Affected code` will be overwritten on re-push. Reviewer comments are never touched.
234
- - **State (open/closed)** — remote wins. Updates the local finding's `github.state`. Surface in the dashboard so the auditor can decide whether to also reject the finding.
235
- - **Comments** — pull-only. Hex never posts.
140
+ - **Field content** — GitHub wins for synced issues. Every pull overwrites the local finding's title/severity/description/recommendation/status/update from the issue.
141
+ - **State (open/closed)** — from GitHub, stored on `github.state`.
142
+ - **Comments** — Hex never reads or posts comments.
236
143
 
237
144
  ## Error handling
238
145
 
239
- - Per-issue errors are isolated — log to `errors[]`, continue.
240
- - Rate limit (403 with "rate limit" in output): print "Rate-limited — sync resumes after <ISO time>." Stop the current phase.
241
- - Network failures: print the gh error and stop the current phase. Already-completed phases stay persisted.
146
+ - Per-issue errors are isolated — log and continue.
147
+ - Rate limit (403 / "rate limit"): print "Rate-limited — try again later." and stop the current phase.
148
+ - Network failures: print the `gh` error and stop the current phase; completed work stays persisted.
242
149
 
243
150
  ## What this skill does NOT do
244
151
 
245
- - Does not authenticate (use `gh auth login` once).
246
- - Does not post comments (auditors discuss inside GitHub).
247
- - Does not apply, create, or remove labels (the repo owner manages its own labels).
248
- - Does not delete remote issues.
249
- - Does not push `rejected`, `unverified`, or `duplicate` findings.
152
+ - Authenticate (use `gh auth login`).
153
+ - Post or read comments.
154
+ - Apply, create, or remove labels (the repo owner manages labels).
155
+ - Embed any hidden footer or marker in the issue body.
156
+ - Delete remote issues.
157
+ - Push `rejected`, `unverified`, `duplicate`, or already-`synced` findings.
@@ -0,0 +1,50 @@
1
+ ---
2
+ description: "Walk every Potential issue on the board and validate them one by one (interactive)"
3
+ ---
4
+
5
+ # Skill: Validate All Findings
6
+
7
+ **Recommended model:** Opus
8
+
9
+ Runs `/validate-issue` over every issue currently in the **Potential** column, one at a time, so the auditor can clear the backlog in a single pass without naming each id.
10
+
11
+ ## Step 1 — Build the work list
12
+
13
+ Read `<output_dir>/tracking.json`. The Potential column is every entry whose `status` is `pending_validation` or `unverified`. Collect their ids in a stable order: severity (Critical → High → Medium → Low → Info), then id. Call this list `POTENTIAL` with size `N`.
14
+
15
+ If `N` is 0, report "No Potential issues to validate." and stop.
16
+
17
+ ## Step 2 — Loop
18
+
19
+ For each issue `i` of `N` (1-based), print a header:
20
+
21
+ ```
22
+ ── Validating i/N: <id> [<severity>] <title> ──
23
+ ```
24
+
25
+ Then run the full **`/validate-issue`** flow for that `<id>` exactly as that skill defines it:
26
+ 1. Assemble context (the tracking entry + its source record + the affected source).
27
+ 2. Trace the claim in the code.
28
+ 3. Reach a verdict:
29
+ - **Valid** → ask PoC-vs-memo (AskUserQuestion), write the validation memo, optionally generate the PoC, then `hex issue patch <id> ...` + `hex issue move <id> --to verified`; offer the severity-adjustment prompt.
30
+ - **Invalid** → `hex issue move <id> --to invalid`, memo records why.
31
+ - **Uncertain** → leave in place, `hex issue patch <id> --notes "<open questions>"`.
32
+
33
+ Do not batch or skip the interactive prompts — this skill is the same per-issue UX as `/validate-issue`, just iterated. The auditor stays in control of each verdict.
34
+
35
+ ## Step 3 — Resume safety
36
+
37
+ Re-fetch the entry's current status before validating it (an earlier loop iteration, or a previous run, may have already moved it). Skip any id that is no longer in the Potential column and note it: `Skipping <id> (already <status>)`. This makes the skill safe to re-run after an interruption.
38
+
39
+ ## Step 4 — Summary
40
+
41
+ After the loop, print a tally:
42
+
43
+ ```
44
+ Validated N Potential issues:
45
+ → Verified: V
46
+ → Invalid: X
47
+ → Left for review (uncertain): U
48
+ ```
49
+
50
+ Remind the auditor of the next steps: `/sync-issues` to push the Verified findings to GitHub, then `/generate-overleaf` for the report.
@@ -95,14 +95,14 @@ For conformance items, the "claim" is `spec_text` + the finding paragraph — i.
95
95
 
96
96
  For a memo-only verdict, the same two commands apply (the PoC fields just stay unset). For `--description-file`, you may pass either the memo or a dedicated description file; prefer a clean description file if the memo is long.
97
97
 
98
- 5. **Annotate source files** (this is the one mutation `hex issue` deliberately does NOT do, so it stays explicit). For each location in the finding's `root_cause.locations[]`, replace any `// @audit-issue <id> ...` comment with `// @audit-issue-verified <id> <short title>` (matching indentation), or insert it if absent.
98
+ Hex does not write `@audit-issue` comments into the client's source files — the board and finding record are the only place the issue lives.
99
99
 
100
- 6. Use **AskUserQuestion** to ask:
100
+ 5. Use **AskUserQuestion** to ask:
101
101
  > Issue verified as **\<id\>** with severity **\<current severity\>**.
102
102
  >
103
103
  > Would you like to change the severity? Reply with the new severity (Critical / High / Medium / Low / Info), or **no** to keep it.
104
104
 
105
- 7. If the user provides a new severity, run `npx hex issue patch <id> --severity <NewSeverity>`.
105
+ 6. If the user provides a new severity, run `npx hex issue patch <id> --severity <NewSeverity>`.
106
106
 
107
107
  **Invalid:**
108
108
 
@@ -13,13 +13,13 @@ This skill records a manual issue that the auditor has spotted during review. Th
13
13
  Read:
14
14
  - The issue description provided by the auditor.
15
15
  - The relevant source code.
16
- - `<output_dir>/findings.json` and `<output_dir>/tracking.json` — to determine the next finding ID. Use the highest existing `F<NNN>` ID across both files and increment by 1. Rejected and invalid findings keep their IDs reserved.
16
+ - `<output_dir>/findings.json` and `<output_dir>/tracking.json` — to check for duplicates (ids are allocated by `hex issue new`, never hand-assigned).
17
17
 
18
18
  ## Step 0: Duplicate gate
19
19
 
20
20
  Scan `findings.json` and `tracking.json` for any existing entry that covers the same vulnerability (same affected contract/function, same or overlapping root cause). If a match is found, warn the auditor:
21
21
 
22
- > "This appears to duplicate `F<NNN>`: \<title\>. Proceed anyway?"
22
+ > "This appears to duplicate `<H-NNN>`: \<title\>. Proceed anyway?"
23
23
 
24
24
  Only continue if confirmed.
25
25
 
@@ -51,9 +51,11 @@ Cross-reference both before assigning a severity.
51
51
 
52
52
  Write the finding following this structure:
53
53
 
54
+ The `id`, `poc`, and `created_at` fields are managed by `hex issue new` (see Actions) — you supply the rest via `hex issue patch`. The full record shape, for reference:
55
+
54
56
  ```json
55
57
  {
56
- "id": "F<NNN>",
58
+ "id": "H-NNN (allocated by hex issue new)",
57
59
  "title": "<concise, descriptive title>",
58
60
  "severity": "Critical|High|Medium|Low|Info",
59
61
  "category": "<e.g., Math / Rounding, Access Control, Reentrancy, Oracle Manipulation>",
@@ -108,35 +110,28 @@ All inserted audit comments must be full sentences starting with a capital lette
108
110
 
109
111
  ## Actions
110
112
 
111
- 1. **Append the finding** to the `findings` array inside `<output_dir>/findings.json`. File structure is `{ "findings": [...] }` — create with this wrapper if missing.
112
- 2. **Add a tracking entry** to `<output_dir>/tracking.json`. The file MUST be wrapped as `{ "findings": [...] }` (the wrapper key is `findings`, not `issues`); see `init-audit.md` Phase 6 for the canonical schema. Append this entry:
113
- ```json
114
- {
115
- "id": "F<NNN>",
116
- "title": "<same as finding>",
117
- "severity": "<same as finding>",
118
- "source": "manual",
119
- "status": "pending_validation",
120
- "poc_status": "not_started",
121
- "poc_file": null,
122
- "duplicates": [],
123
- "notes": ""
124
- }
113
+ Persist through the `hex issue` CLI — never hand-edit `findings.json` or `tracking.json`. The CLI allocates the uniform `H-NNN` id, writes the tracking entry (`status: pending_validation`, `source: manual`), and creates the findings.json skeleton.
114
+
115
+ 1. **Create the issue:**
116
+ ```bash
117
+ npx hex issue new --source manual --title "<concise title>" --severity <Severity>
118
+ ```
119
+ This prints the allocated id (e.g. `H-007`). Capture it.
120
+
121
+ 2. **Fill the fields** (write the description and recommendation to temp files first, to preserve newlines):
122
+ ```bash
123
+ npx hex issue patch <id> --description-file /tmp/<id>_desc.md --recommendation-file /tmp/<id>_reco.md
125
124
  ```
126
- 3. **Annotate source files.** For each entry in `root_cause.locations[]`:
127
- - Open the source file at `file`.
128
- - Find the first significant line of the `snippet` (skip blank lines and `// ....`).
129
- - If an existing `// @audit-issue ...` comment is directly above, replace it with `// @audit-issue F<NNN> <short title>`.
130
- - Otherwise insert `// @audit-issue F<NNN> <short title>` directly above, matching indentation.
125
+ The description must be self-contained (what the issue is, why it exists, the impact). The recommendation is prose only, hedged (`Consider...`), no code.
131
126
 
132
- (The `@audit-issue-verified` annotation comes later, via `/validate-issue` once the issue is promoted to Verified.)
127
+ There is **no source-file annotation** — Hex does not write `@audit-issue` comments into the client's `.sol` files. The board and the finding record are the single place the issue lives.
133
128
 
134
129
  ## After writing
135
130
 
136
- The finding now appears on the dashboard's `/issues` board as a Potential card. The auditor can:
131
+ The finding appears on the dashboard's `/issues` board as a Potential card (`H-NNN`). The auditor can:
137
132
 
138
- - Run `/validate-issue F<NNN>` to write a validation memo and optionally generate a PoC; on success the card moves to Verified.
139
- - Drag the card directly from Potential → Verified on the board if they're confident no PoC is needed (e.g., trivial Info-level issues).
140
- - Run `/sync-issues` if `settings.github.repo` is configured, to push verified findings to the team's GitHub repo.
133
+ - Run `/validate-issue <id>` to write a validation memo and optionally generate a PoC; on success the card moves to Verified.
134
+ - Drag the card from Potential → Verified on the board if no PoC is needed (e.g., trivial Info-level issues).
135
+ - Run `/sync-issues` (once findings are Verified and `settings.github.repo` is set) to push them to GitHub.
141
136
 
142
- Do NOT run `/validate-issue` or `/sync-issues` automatically — those are auditor-driven decisions.
137
+ Do NOT run `/validate-issue` or `/sync-issues` automatically — those are auditor-driven.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hex-audit",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "CLI toolkit for Solidity smart contract auditors",
6
6
  "bin": {
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[4521],{4260:function(e,t,a){Promise.resolve().then(a.bind(a,7063))},7063:function(e,t,a){"use strict";a.d(t,{IssuesBoardClient:function(){return x}});var r=a(7821),n=a(8078),o=a(3924);let i={Critical:"bg-[var(--critical)]/18 text-[var(--critical)]",High:"bg-[var(--high)]/18 text-[var(--high)]",Medium:"bg-[var(--medium)]/18 text-[var(--medium)]",Low:"bg-[var(--low)]/18 text-[var(--low)]",Info:"bg-[var(--info)]/18 text-[var(--info)]"};function s(e){var t;let{severity:a}=e,n=null!==(t=i[a])&&void 0!==t?t:i.Info;return(0,r.jsx)("span",{className:"inline-flex items-center rounded-md px-2 py-0.5 text-caption font-medium ".concat(n),children:a})}let l=[{id:"potential",label:"Potential",description:"Awaiting validation"},{id:"verified",label:"Verified",description:"Confirmed issues ready to report"},{id:"invalid",label:"Invalid",description:"Rejected after review"},{id:"duplicate",label:"Duplicate",description:"Covered by another entry"}],c={Critical:0,High:1,Medium:2,Low:3,Info:4},d=["Critical","High","Medium","Low","Info"],u=["Fixed","Mitigated","Acknowledged","Not Fixed","Unresolved"];function x(e){var t;let{issues:a,formatCtx:i={}}=e,[s,d]=(0,n.useState)(a),[u,x]=(0,n.useState)(null),[f,b]=(0,n.useState)("all");(0,n.useEffect)(()=>{d(a)},[a]);let h=(0,o.Dy)((0,o.VT)(o.we,{activationConstraint:{distance:4}})),g=(0,n.useMemo)(()=>"all"===f?s:s.filter(e=>e.source===f),[s,f]),y=(0,n.useMemo)(()=>{let e={potential:[],verified:[],invalid:[],duplicate:[]};for(let t of g)e[t.column].push(t);for(let t of Object.values(e))t.sort((e,t)=>{var a,r;return(null!==(a=c[e.severity])&&void 0!==a?a:5)-(null!==(r=c[t.severity])&&void 0!==r?r:5)||e.id.localeCompare(t.id)});return e},[g]),j=(0,n.useMemo)(()=>{let e={potential:0,verified:0,invalid:0,duplicate:0};for(let t of s)e[t.column]++;return e},[s]),N=(0,n.useCallback)(async(e,t)=>{d(a=>a.map(a=>a.id===e?{...a,column:t,status:function(e){switch(e){case"potential":return"pending_validation";case"verified":return"verified";case"invalid":return"rejected";case"duplicate":return"duplicate"}}(t),duplicate_of:"duplicate"===t?a.duplicate_of:null}:a));try{let a=await fetch("/api/issues/".concat(encodeURIComponent(e),"/move"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({column:t})});if(!a.ok){var r;let e=await a.json().catch(()=>({}));throw Error(null!==(r=e.error)&&void 0!==r?r:"move failed")}}catch(t){console.error(t),d(t=>t.map(t=>{var r;return t.id===e&&null!==(r=a.find(t=>t.id===e))&&void 0!==r?r:t}))}},[a]),w=(0,n.useCallback)(async(e,t)=>{d(a=>a.map(a=>a.id===e?{...a,...t}:a));try{let r=await fetch("/api/issues/".concat(encodeURIComponent(e)),{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){var a;let e=await r.json().catch(()=>({}));throw Error(null!==(a=e.error)&&void 0!==a?a:"update failed")}}catch(e){console.error(e)}},[]),_=null!==(t=s.find(e=>e.id===u))&&void 0!==t?t:null;return(0,r.jsxs)("div",{children:[(0,r.jsxs)("header",{className:"mb-6 flex flex-wrap items-end justify-between gap-3",children:[(0,r.jsxs)("div",{children:[(0,r.jsx)("h1",{className:"text-title font-semibold text-text-primary",children:"Issues"}),(0,r.jsxs)("p",{className:"mt-1 text-body text-text-secondary",children:[s.length," total — ",j.potential," potential, ",j.verified," verified,"," ",j.invalid," invalid, ",j.duplicate," duplicate. Drag a card to change column; click to edit."]})]}),(0,r.jsx)(m,{value:f,onChange:b})]}),(0,r.jsx)(o.LB,{sensors:h,onDragEnd:e=>{let{active:t,over:a}=e;if(!a)return;let r=a.id,n=t.id,o=s.find(e=>e.id===n);o&&o.column!==r&&N(n,r)},children:(0,r.jsx)("div",{className:"grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4",children:l.map(e=>(0,r.jsx)(p,{column:e,issues:y[e.id],onOpen:x},e.id))})}),_&&(0,r.jsx)(v,{issue:_,formatCtx:i,onClose:()=>x(null),onSave:e=>w(_.id,e)})]})}function m(e){let{value:t,onChange:a}=e;return(0,r.jsx)("div",{className:"flex flex-wrap gap-1.5",children:[{id:"all",label:"All sources"},{id:"manual",label:"Manual"},{id:"auditagent",label:"AuditAgent"},{id:"conformance",label:"Conformance"},{id:"github",label:"GitHub"}].map(e=>{let n=t===e.id;return(0,r.jsx)("button",{type:"button",onClick:()=>a(e.id),className:"rounded-md px-2.5 py-1 text-caption font-medium ".concat(n?"bg-accent text-surface-0":"bg-surface-2 text-text-secondary hover:text-text-primary"),children:e.label},e.id)})})}function p(e){let{column:t,issues:a,onOpen:n}=e,{isOver:i,setNodeRef:s}=(0,o.Zj)({id:t.id});return(0,r.jsxs)("section",{ref:s,"aria-label":t.label,className:"flex flex-col gap-2 rounded-lg border bg-surface-1 p-3 transition-colors ".concat(i?"border-accent bg-surface-2":"border-border-subtle"),children:[(0,r.jsxs)("header",{className:"flex items-baseline justify-between",children:[(0,r.jsx)("h2",{className:"text-heading font-medium text-text-primary",children:t.label}),(0,r.jsx)("span",{className:"text-caption text-text-tertiary",children:a.length})]}),(0,r.jsx)("p",{className:"text-caption text-text-tertiary",children:t.description}),(0,r.jsxs)("div",{className:"mt-1 flex flex-col gap-2",children:[a.map(e=>(0,r.jsx)(f,{issue:e,onOpen:n},e.id)),0===a.length&&(0,r.jsx)("div",{className:"rounded-md border border-dashed border-border-subtle py-6 text-center text-caption text-text-tertiary",children:"Empty"})]})]})}function f(e){let{issue:t,onOpen:a}=e,{attributes:n,listeners:i,setNodeRef:l,isDragging:c,transform:d}=(0,o.O1)({id:t.id}),u=d?{transform:"translate3d(".concat(d.x,"px, ").concat(d.y,"px, 0)"),zIndex:30}:{};return(0,r.jsxs)("article",{ref:l,style:u,...n,...i,onClick:e=>{d||(e.stopPropagation(),a(t.id))},className:"group cursor-grab rounded-md border bg-surface-0 p-3 text-left active:cursor-grabbing ".concat(c?"border-accent shadow-lg":"border-border-subtle hover:border-border-default"),children:[(0,r.jsxs)("div",{className:"flex items-start justify-between gap-2",children:[(0,r.jsx)("span",{className:"font-mono text-caption text-text-tertiary",children:t.id}),(0,r.jsx)(s,{severity:t.severity})]}),(0,r.jsx)("h3",{className:"mt-1 text-body font-medium text-text-primary line-clamp-2",children:t.title}),(0,r.jsxs)("div",{className:"mt-2 flex flex-wrap items-center gap-1.5 text-caption",children:[(0,r.jsx)(b,{source:t.source}),(0,r.jsx)(h,{issue:t}),t.duplicate_of&&(0,r.jsxs)("span",{className:"rounded-md bg-surface-2 px-1.5 py-0.5 text-text-secondary",children:["dup of ",t.duplicate_of]}),"passing"===t.poc_status&&(0,r.jsx)("span",{className:"rounded-md bg-[var(--low)]/15 px-1.5 py-0.5 text-[var(--low)]",children:"PoC ✓"}),"failing"===t.poc_status&&(0,r.jsx)("span",{className:"rounded-md bg-[var(--critical)]/15 px-1.5 py-0.5 text-[var(--critical)]",children:"PoC ✕"}),t.resolution&&(0,r.jsx)("span",{className:"rounded-md bg-surface-2 px-1.5 py-0.5 text-text-secondary",children:t.resolution})]}),t.match_signals&&(0,r.jsx)("div",{className:"mt-2 text-caption text-text-tertiary",children:(0,r.jsx)("span",{className:"font-mono",children:[t.match_signals.contract?"contract":null,t.match_signals.function?"function":null,"different"!==t.match_signals.root_cause?"rc:".concat(t.match_signals.root_cause):null,"different"!==t.match_signals.attack_vector?"av:".concat(t.match_signals.attack_vector):null].filter(Boolean).join(" \xb7 ")})})]})}function b(e){var t;let{source:a}=e;return(0,r.jsx)("span",{className:"rounded-md bg-surface-2 px-1.5 py-0.5 text-text-tertiary",children:null!==(t=({manual:"Manual",auditagent:"AuditAgent",conformance:"Conformance",github:"GitHub"})[a])&&void 0!==t?t:a})}function h(e){var t,a;let{issue:n}=e,o=null!==(t=n.sync_state)&&void 0!==t?t:"none";if("none"===o)return null;if("unsynced"===o)return(0,r.jsx)("span",{title:"Run /sync-issues to push verified findings to GitHub.",className:"rounded-md border border-dashed border-border-default px-1.5 py-0.5 text-text-tertiary",children:"GH unsynced"});if("conflict"===o)return(0,r.jsx)("span",{title:"Local and remote diverged. Re-run /sync-issues to reconcile.",className:"rounded-md bg-[var(--critical)]/15 px-1.5 py-0.5 text-[var(--critical)]",children:"GH conflict"});let i="synced_closed"===o,s="GH #".concat(null!==(a=n.github_issue_number)&&void 0!==a?a:"?"," ").concat(i?"closed":"open"),l=i?"bg-surface-2 text-text-tertiary":"bg-[var(--low)]/15 text-[var(--low)]";return n.github_issue_url?(0,r.jsx)("a",{href:n.github_issue_url,target:"_blank",rel:"noopener noreferrer",onClick:e=>e.stopPropagation(),className:"rounded-md px-1.5 py-0.5 hover:underline ".concat(l),children:s}):(0,r.jsx)("span",{className:"rounded-md px-1.5 py-0.5 ".concat(l),children:s})}function v(e){var t,a,o;let{issue:i,formatCtx:s,onClose:l,onSave:c}=e,[x,m]=(0,n.useState)(i.title),[p,f]=(0,n.useState)(i.severity),[v,y]=(0,n.useState)(i.description),[j,N]=(0,n.useState)(i.recommendation),[w,_]=(0,n.useState)(null!==(t=i.resolution)&&void 0!==t?t:""),[C,k]=(0,n.useState)(null!==(a=i.update_from_client)&&void 0!==a?a:""),[S,M]=(0,n.useState)(!1),[H,E]=(0,n.useState)(!1),I=(0,n.useRef)(null);(0,n.useEffect)(()=>{var e;null===(e=I.current)||void 0===e||e.focus()},[]);let R=async()=>{let e=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return"## [".concat(e.severity,"] ").concat(e.title,"\n\n").concat(function(e,t){var a;let r=new Set,n=e.code_locations.map(e=>e.file).filter(e=>e&&!r.has(e)&&r.add(e)).map(e=>(function(e,t){if(t.repositoryUrl&&t.commit){let a=t.repositoryUrl.replace(/\/+$/,"");return"[`".concat(e,"`](").concat(a,"/blob/").concat(t.commit,"/").concat(e,")")}return"[`".concat(e,"`]()")})(e,t)).join(", "),o=e.resolution||"Unresolved",i=null!==(a=e.update_from_client)&&void 0!==a?a:"";return["**File(s)**: ".concat(n),"","**Description**: ".concat(e.description),"","**Recommendation(s)**: ".concat(e.recommendation),"","**Status**: ".concat(o),"","**Update from the client**: ".concat(i)].join("\n")}(e,t),"\n")}({...i,title:x,severity:p,description:v,recommendation:j,resolution:w||void 0,update_from_client:C||void 0},s);try{await navigator.clipboard.writeText(e),E(!0),setTimeout(()=>E(!1),1500)}catch(e){}},D=async()=>{M(!0),await c({title:x,severity:p,description:v,recommendation:j,resolution:w||void 0,update_from_client:C||void 0}),M(!1),l()};return(0,r.jsx)("div",{ref:I,tabIndex:-1,className:"fixed inset-0 z-50 flex items-center justify-center p-4 outline-none",style:{background:"var(--overlay-bg)"},onClick:l,onKeyDown:e=>{"Escape"===e.key&&l()},children:(0,r.jsxs)("div",{role:"dialog","aria-modal":"true","aria-label":"Edit issue ".concat(i.id),className:"relative flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden border border-border-emphasis bg-surface-1 shadow-2xl",style:{borderRadius:"var(--radius-md)",boxShadow:"var(--shadow-accent)"},onClick:e=>e.stopPropagation(),children:[(0,r.jsxs)("div",{className:"flex items-center justify-between border-b border-border-default bg-surface-2 px-4 py-3",children:[(0,r.jsxs)("div",{className:"flex items-center gap-2 text-caption",children:[(0,r.jsx)("span",{className:"font-mono text-body text-text-tertiary",children:i.id}),(0,r.jsx)(b,{source:i.source}),(0,r.jsx)(h,{issue:i})]}),(0,r.jsx)("button",{type:"button",onClick:l,"aria-label":"Close",className:"rounded p-1 text-text-tertiary hover:bg-surface-3 hover:text-text-primary",children:(0,r.jsx)("svg",{className:"h-5 w-5","aria-hidden":"true",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:2,children:(0,r.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M6 18L18 6M6 6l12 12"})})})]}),(0,r.jsxs)("div",{className:"flex-1 overflow-y-auto p-4",children:[(0,r.jsx)(g,{label:"Title",children:(0,r.jsx)("input",{type:"text",value:x,onChange:e=>m(e.target.value),className:"w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 text-body text-text-primary focus:border-accent focus:outline-none"})}),(0,r.jsxs)("div",{className:"mt-3 grid grid-cols-2 gap-3",children:[(0,r.jsx)(g,{label:"Severity",children:(0,r.jsx)("select",{value:p,onChange:e=>f(e.target.value),className:"w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 text-body text-text-primary focus:border-accent focus:outline-none",children:d.map(e=>(0,r.jsx)("option",{value:e,children:e},e))})}),(0,r.jsx)(g,{label:"Resolution (verified only)",children:(0,r.jsxs)("select",{value:w,onChange:e=>_(e.target.value),className:"w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 text-body text-text-primary focus:border-accent focus:outline-none",children:[(0,r.jsx)("option",{value:"",children:"— none —"}),u.map(e=>(0,r.jsx)("option",{value:e,children:e},e))]})})]}),(0,r.jsx)(g,{label:"Description",className:"mt-3",children:(0,r.jsx)("textarea",{value:v,onChange:e=>y(e.target.value),rows:8,className:"w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 font-mono text-caption text-text-primary focus:border-accent focus:outline-none"})}),(0,r.jsx)(g,{label:"Recommendation",className:"mt-3",children:(0,r.jsx)("textarea",{value:j,onChange:e=>N(e.target.value),rows:4,className:"w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 font-mono text-caption text-text-primary focus:border-accent focus:outline-none"})}),(0,r.jsx)(g,{label:"Update from the client",className:"mt-3",children:(0,r.jsx)("textarea",{value:C,onChange:e=>k(e.target.value),rows:4,placeholder:"How the client responded — used by /generate-overleaf.",className:"w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 font-mono text-caption text-text-primary focus:border-accent focus:outline-none"})}),i.code_locations.length>0&&(0,r.jsxs)("div",{className:"mt-4",children:[(0,r.jsx)("div",{className:"mb-1 text-caption font-medium text-text-tertiary",children:"Code locations"}),(0,r.jsx)("ul",{className:"space-y-1 font-mono text-caption text-text-secondary",children:i.code_locations.map((e,t)=>(0,r.jsx)("li",{children:e.file},"".concat(e.file,"-").concat(t)))})]}),i.match_signals&&(0,r.jsxs)("div",{className:"mt-4",children:[(0,r.jsx)("div",{className:"mb-1 text-caption font-medium text-text-tertiary",children:"Duplicate signals"}),(0,r.jsx)("p",{className:"text-caption text-text-secondary",children:null!==(o=i.reasoning)&&void 0!==o?o:"Matched against another entry."}),(0,r.jsxs)("div",{className:"mt-1 flex flex-wrap gap-1.5 font-mono text-caption text-text-tertiary",children:[(0,r.jsxs)("span",{children:["contract: ",i.match_signals.contract?"✓":"—"]}),(0,r.jsxs)("span",{children:["function: ",i.match_signals.function?"✓":"—"]}),(0,r.jsxs)("span",{children:["root_cause: ",i.match_signals.root_cause]}),(0,r.jsxs)("span",{children:["attack_vector: ",i.match_signals.attack_vector]})]})]})]}),(0,r.jsxs)("div",{className:"flex items-center justify-between gap-2 border-t border-border-default bg-surface-2 px-4 py-3",children:[(0,r.jsx)("button",{type:"button",onClick:R,className:"rounded-md border border-border-default px-3 py-1.5 text-body text-text-secondary hover:bg-surface-3 hover:text-text-primary",children:H?"Copied!":"Copy as HackMD"}),(0,r.jsxs)("div",{className:"flex items-center gap-2",children:[(0,r.jsx)("button",{type:"button",onClick:l,className:"rounded-md px-3 py-1.5 text-body text-text-secondary hover:text-text-primary",children:"Cancel"}),(0,r.jsx)("button",{type:"button",onClick:D,disabled:S,className:"rounded-md bg-accent px-3 py-1.5 text-body font-medium text-surface-0 hover:bg-accent-secondary disabled:opacity-50",children:S?"Saving…":"Save"})]})]})]})})}function g(e){let{label:t,className:a,children:n}=e;return(0,r.jsxs)("label",{className:"block ".concat(null!=a?a:""),children:[(0,r.jsx)("span",{className:"mb-1 block text-caption font-medium text-text-tertiary",children:t}),n]})}}},function(e){e.O(0,[3924,7115,3205,1744],function(){return e(e.s=4260)}),_N_E=e.O()}]);