hex-audit 0.4.4 → 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 (102) hide show
  1. package/DEMO.md +14 -12
  2. package/README.md +66 -61
  3. package/dashboard/.next/BUILD_ID +1 -1
  4. package/dashboard/.next/app-build-manifest.json +18 -11
  5. package/dashboard/.next/app-path-routes-manifest.json +1 -1
  6. package/dashboard/.next/build-manifest.json +4 -4
  7. package/dashboard/.next/next-server.js.nft.json +1 -1
  8. package/dashboard/.next/prerender-manifest.js +1 -1
  9. package/dashboard/.next/prerender-manifest.json +1 -1
  10. package/dashboard/.next/required-server-files.json +1 -1
  11. package/dashboard/.next/routes-manifest.json +1 -1
  12. package/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/dashboard/.next/server/app/_not-found.html +1 -1
  14. package/dashboard/.next/server/app/_not-found.rsc +4 -4
  15. package/dashboard/.next/server/app/access/page_client-reference-manifest.js +1 -1
  16. package/dashboard/.next/server/app/api/flows/route.js +1 -1
  17. package/dashboard/.next/server/app/api/issues/[id]/move/route.js +1 -1
  18. package/dashboard/.next/server/app/api/issues/[id]/route.js +1 -1
  19. package/dashboard/.next/server/app/calls/page_client-reference-manifest.js +1 -1
  20. package/dashboard/.next/server/app/conformance/page_client-reference-manifest.js +1 -1
  21. package/dashboard/.next/server/app/diagram/page_client-reference-manifest.js +1 -1
  22. package/dashboard/.next/server/app/diagram.html +1 -1
  23. package/dashboard/.next/server/app/diagram.rsc +4 -4
  24. package/dashboard/.next/server/app/flows/page_client-reference-manifest.js +1 -1
  25. package/dashboard/.next/server/app/flows.html +1 -1
  26. package/dashboard/.next/server/app/flows.rsc +4 -4
  27. package/dashboard/.next/server/app/functions/page_client-reference-manifest.js +1 -1
  28. package/dashboard/.next/server/app/issues/page.js +12 -1
  29. package/dashboard/.next/server/app/issues/page_client-reference-manifest.js +1 -1
  30. package/dashboard/.next/server/app/overleaf/page.js +1 -0
  31. package/dashboard/.next/server/app/overleaf/page.js.nft.json +1 -0
  32. package/dashboard/.next/server/app/overleaf/page_client-reference-manifest.js +1 -0
  33. package/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  34. package/dashboard/.next/server/app/progress/page_client-reference-manifest.js +1 -1
  35. package/dashboard/.next/server/app/stats/page_client-reference-manifest.js +1 -1
  36. package/dashboard/.next/server/app-paths-manifest.json +9 -8
  37. package/dashboard/.next/server/chunks/431.js +1 -1
  38. package/dashboard/.next/server/functions-config-manifest.json +1 -1
  39. package/dashboard/.next/server/middleware-build-manifest.js +1 -1
  40. package/dashboard/.next/server/pages/404.html +1 -1
  41. package/dashboard/.next/server/pages/500.html +1 -1
  42. package/dashboard/.next/server/server-reference-manifest.js +1 -1
  43. package/dashboard/.next/server/server-reference-manifest.json +1 -1
  44. package/dashboard/.next/static/chunks/app/issues/page-2bb52a36f20497fe.js +1 -0
  45. package/dashboard/.next/static/chunks/app/{layout-527d8aa86e483fbd.js → layout-9c3c84993dd895f6.js} +1 -1
  46. package/dashboard/.next/static/chunks/app/overleaf/page-bcea12a80c97b92f.js +1 -0
  47. package/dashboard/.next/static/chunks/{main-70c6d841ad572657.js → main-8f1e837090aef5cf.js} +1 -1
  48. package/dashboard/.next/static/css/9c84313d0484b74f.css +3 -0
  49. package/dashboard/app/api/flows/route.ts +4 -0
  50. package/dashboard/app/api/issues/[id]/move/route.ts +29 -58
  51. package/dashboard/app/api/issues/[id]/route.ts +11 -102
  52. package/dashboard/app/issues/IssuesBoardClient.tsx +155 -50
  53. package/dashboard/app/issues/page.tsx +40 -3
  54. package/dashboard/app/overleaf/OverleafClient.tsx +97 -0
  55. package/dashboard/app/overleaf/page.tsx +32 -0
  56. package/dashboard/components/DashboardLayout.tsx +8 -2
  57. package/dashboard/lib/finding-markdown.ts +110 -0
  58. package/dashboard/next.config.mjs +4 -0
  59. package/dist/analysis/external-calls.d.ts +8 -1
  60. package/dist/analysis/external-calls.d.ts.map +1 -1
  61. package/dist/analysis/external-calls.js +57 -13
  62. package/dist/analysis/external-calls.js.map +1 -1
  63. package/dist/commands/calls.d.ts.map +1 -1
  64. package/dist/commands/calls.js +15 -6
  65. package/dist/commands/calls.js.map +1 -1
  66. package/dist/commands/dashboard.d.ts.map +1 -1
  67. package/dist/commands/dashboard.js +12 -3
  68. package/dist/commands/dashboard.js.map +1 -1
  69. package/dist/commands/init.js +19 -19
  70. package/dist/commands/init.js.map +1 -1
  71. package/dist/commands/issue.d.ts +3 -0
  72. package/dist/commands/issue.d.ts.map +1 -0
  73. package/dist/commands/issue.js +195 -0
  74. package/dist/commands/issue.js.map +1 -0
  75. package/dist/core/config.d.ts +1 -0
  76. package/dist/core/config.d.ts.map +1 -1
  77. package/dist/core/config.js +10 -0
  78. package/dist/core/config.js.map +1 -1
  79. package/dist/core/issues.d.ts +116 -0
  80. package/dist/core/issues.d.ts.map +1 -0
  81. package/dist/core/issues.js +403 -0
  82. package/dist/core/issues.js.map +1 -0
  83. package/dist/core/schema.d.ts +24 -8
  84. package/dist/core/schema.d.ts.map +1 -1
  85. package/dist/core/schema.js +12 -1
  86. package/dist/core/schema.js.map +1 -1
  87. package/dist/index.js +3 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/skills/generate-overleaf.md +18 -10
  90. package/dist/skills/ingest-aa-report.md +15 -19
  91. package/dist/skills/init-audit.md +13 -18
  92. package/dist/skills/sync-issues.md +86 -195
  93. package/dist/skills/validate-all-findings.md +50 -0
  94. package/dist/skills/validate-issue.md +20 -10
  95. package/dist/skills/write-finding.md +23 -28
  96. package/package.json +1 -1
  97. package/dashboard/.next/server/app/api/flows.body +0 -1
  98. package/dashboard/.next/server/app/api/flows.meta +0 -1
  99. package/dashboard/.next/static/chunks/app/issues/page-dc110d4cc0a5c398.js +0 -1
  100. package/dashboard/.next/static/css/7a38e5062ba6d403.css +0 -3
  101. /package/dashboard/.next/static/{ePObaFrrwft0BlvlllUMo → NrFPHIFHWQ5jNF62Px_X8}/_buildManifest.js +0 -0
  102. /package/dashboard/.next/static/{ePObaFrrwft0BlvlllUMo → NrFPHIFHWQ5jNF62Px_X8}/_ssgManifest.js +0 -0
@@ -1,266 +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
19
+ ## Context
31
20
 
32
- **0a. `gh` installed and authenticated?** Run `gh auth status`. If non-zero exit, print:
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).
33
24
 
34
- ```
35
- gh CLI is not authenticated. Run `gh auth login` once on this machine, then re-run /sync-issues.
36
- ```
37
-
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`.
42
+ **1b. Parse each issue.** Title is `[Severity] Title`. Body is the five-field template:
56
43
 
57
- For each issue whose `updatedAt` is newer than the prior `last_synced_at`, fetch comments:
58
-
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
-
147
- Persist updated `tracking.json` and `comparison.json` after the dedup pass.
148
-
149
- ## Phase 3 — Push
150
-
151
- Skip if `push_enabled = false`.
152
-
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).
154
73
 
155
- Within candidates:
156
- - Has `github.issue_number` → update via `gh issue edit`.
157
- - No `github.issue_number` → create via `gh issue create`.
74
+ Either way the card lands in the **Synced** column, locked. Reconcile strictly by `#N`; never scan the body for an identifier.
158
75
 
159
- **3b. Render the body** (write to a temp file, then `--body-file`):
76
+ ## Phase 1.5 — Dedup before push (GitHub is canonical)
160
77
 
161
- ```markdown
162
- **Severity:** <Severity>
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:
163
79
 
164
- <finding.description>
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 |
165
86
 
166
- ### Affected code
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`.
167
88
 
168
- <for each finding.root_cause.locations[]>
169
- **`<location.file>`**
89
+ On match — the synced (on-GitHub) finding is canonical, so the local verified copy becomes the duplicate. Do **not** push it:
170
90
 
171
- ```solidity
172
- <location.snippet>
91
+ ```bash
92
+ npx hex issue move <verified-H-id> --to duplicate --duplicate-of <synced-H-id>
173
93
  ```
174
- </for>
175
-
176
- ### Recommendation
177
-
178
- <finding.recommendation>
179
-
180
- ---
181
94
 
182
- *Synced from Hex audit `<config.project.name>` at commit `<config.project.commit>`.*
95
+ Then append to `<output_dir>/comparison.json` under `duplicates` (the board renders the chip + signals from this):
183
96
 
184
- <!-- hex-finding-id: <finding.id> -->
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>" }
185
101
  ```
186
102
 
187
- The footer is the round-trip identity key — never omit.
103
+ If `comparison.json` doesn't exist, create it with `{ "duplicates": [], "novel": [], "rejected": [] }`.
188
104
 
189
- **3c. Labels.**
105
+ ## Phase 2 — Push (verified local findings → GitHub)
190
106
 
191
- ```
192
- <default_labels...> severity:<lowercased> source:<finding.source or 'manual'> status:<tracking.status>
193
- ```
107
+ Skip if `push_enabled` is false.
194
108
 
195
- Apply via repeated `--label` flags.
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.)
196
110
 
197
- **3d. Create or update.**
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.**
198
112
 
199
- Create:
200
113
  ```bash
201
- gh issue create --repo <repo> --title "[<Severity>] <title>" --body-file <tmp> \
202
- --label "hex" --label "audit" --label "severity:..." --label "source:..." --label "status:..."
114
+ gh issue create --repo <repo> --title "[<Severity>] <title>" --body-file <tmp>
203
115
  ```
204
116
 
205
- 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:
206
118
 
207
- Update existing:
208
119
  ```bash
209
- gh issue edit <num> --repo <repo> --title "[<Severity>] <title>" --body-file <tmp> \
210
- --add-label "severity:..." --add-label "status:..."
120
+ npx hex issue sync-set <H-id> --issue-number N --issue-url <url> --state open
211
121
  ```
212
122
 
213
- Do NOT `--remove-label` — teammates may have added labels we shouldn't strip.
214
-
215
- 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.
123
+ From now on the finding is identified by `#N`; the next pull matches it by number, so it never duplicates.
216
124
 
217
- **3e. Persist** `findings.json` once after all pushes, atomically.
125
+ If a single `gh` call fails, log the error and continue with the next candidate — never abort the whole sync.
218
126
 
219
- ## Phase 4 — Status report
220
-
221
- Write `external/github/sync-status.json`:
222
-
223
- ```json
224
- {
225
- "repo": "<repo>",
226
- "last_synced_at": "<now>",
227
- "pushed": <new-created>,
228
- "updated": <existing-updated>,
229
- "pulled": <total-issues-seen>,
230
- "teammate_findings": <count-of-footer-less>,
231
- "duplicates_detected": <local-entries-flipped-to-duplicate>,
232
- "errors": [<one-line-per-error>]
233
- }
234
- ```
127
+ ## Phase 3 — Status report
235
128
 
236
129
  Print:
237
130
 
238
131
  ```
239
132
  GitHub sync complete (<repo>)
240
- ↑ Pushed N new, updated M
241
- ↓ Pulled K issues — J teammate findings, L matched local
242
- ⇄ Dedup flipped D local entries to Duplicate (GitHub is canonical)
243
- <if errors:>
244
- Errors:
245
- - <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>
246
136
  ```
247
137
 
248
- ## Conflict resolution defaults
138
+ ## Conflict resolution
249
139
 
250
- - **Body content** — local wins on push. Reviewer edits inside `### Affected code` will be overwritten on re-push. Reviewer comments are never touched.
251
- - **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.
252
- - **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.
253
143
 
254
144
  ## Error handling
255
145
 
256
- - Per-issue errors are isolated — log to `errors[]`, continue.
257
- - Rate limit (403 with "rate limit" in output): print "Rate-limited — sync resumes after <ISO time>." Stop the current phase.
258
- - 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.
259
149
 
260
150
  ## What this skill does NOT do
261
151
 
262
- - Does not authenticate (use `gh auth login` once).
263
- - Does not post comments (auditors discuss inside GitHub).
264
- - Does not create labels (manual one-time setup; `gh issue create` errors if a label is missing).
265
- - Does not delete remote issues.
266
- - 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.
@@ -77,22 +77,32 @@ For conformance items, the "claim" is `spec_text` + the finding paragraph — i.
77
77
  >
78
78
  > Reply **1** or **2**.
79
79
 
80
- 2. **If PoC chosen:** invoke the `generate-poc` skill flow (read the project test setup, write the PoC, run and iterate until it passes). Set `poc.status = "passing"`, `poc.file = "test/hex-pocs/<id>_<name>.t.sol"`.
80
+ 2. **If PoC chosen:** invoke the `generate-poc` skill flow (read the project test setup, write the PoC, run and iterate until it passes). Note the test path, e.g. `test/hex-pocs/<id>_<name>.t.sol`.
81
81
 
82
- 3. **If memo-only chosen:** leave `poc.status = "not_started"`, `poc.file = null`.
82
+ 3. **If memo-only chosen:** no PoC file.
83
83
 
84
- 4. If the issue source is **conformance**, **auditagent**, or **github** (no manual finding record yet), invoke the `write-finding` skill flow to materialize a structured finding into `<output_dir>/findings.json` — reusing the existing tracking id. If `source = "manual"`, the finding already exists; update its `poc` block in place.
84
+ 4. **Persist via the CLI — do not hand-edit `findings.json` or `tracking.json`.** The `hex issue` command is the single source of truth for board mutations; it materializes a `findings.json` entry from the source record (conformance / auditagent / github) when one does not exist yet, and routes fields correctly. Write the validation memo and the recommendation to temp files, then:
85
85
 
86
- 5. Update the tracking entry: `status = "verified"`, `poc_status` per choice above, `poc_file` per choice above.
86
+ ```bash
87
+ # Land the editable fields (materializes a finding for conformance/auditagent/github ids):
88
+ npx hex issue patch <id> \
89
+ --severity <Severity> \
90
+ --description-file <output_dir>/validations/<id>_memo.md \
91
+ --recommendation-file /tmp/<id>_reco.md
92
+ # Promote to Verified:
93
+ npx hex issue move <id> --to verified
94
+ ```
87
95
 
88
- 6. For source files in `root_cause.locations[]`, replace any `// @audit-issue F<NNN>` annotation with `// @audit-issue-verified F<NNN> <short title>` (matching indentation).
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.
89
97
 
90
- 7. Use **AskUserQuestion** to ask:
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
+
100
+ 5. Use **AskUserQuestion** to ask:
91
101
  > Issue verified as **\<id\>** with severity **\<current severity\>**.
92
102
  >
93
103
  > Would you like to change the severity? Reply with the new severity (Critical / High / Medium / Low / Info), or **no** to keep it.
94
104
 
95
- 8. If the user provides a new severity, update both `findings.json` and `tracking.json`.
105
+ 6. If the user provides a new severity, run `npx hex issue patch <id> --severity <NewSeverity>`.
96
106
 
97
107
  **Invalid:**
98
108
 
@@ -100,14 +110,14 @@ For conformance items, the "claim" is `spec_text` + the finding paragraph — i.
100
110
  - Is the attack path blocked? By what specifically (modifier, require, state check)?
101
111
  - Are the preconditions impossible? Why?
102
112
  - Is the impact assessment wrong? What actually happens?
103
- 2. Update tracking: `status = "rejected"`.
104
- 3. Do NOT write a finding to `findings.json`. The memo alone records the reasoning.
113
+ 2. Move it to the Invalid column: `npx hex issue move <id> --to invalid`.
114
+ 3. Do NOT write a finding body. The memo alone records the reasoning.
105
115
 
106
116
  **Uncertain:**
107
117
 
108
118
  1. List specific questions that need manual investigation.
109
119
  2. State what would confirm or deny the issue.
110
- 3. Leave tracking `status` as-is. Append findings to the `notes` field describing the open questions.
120
+ 3. Leave the column as-is (no `hex issue move`). Record the open questions on the card: `npx hex issue patch <id> --notes "<open questions>"`.
111
121
 
112
122
  ## Output: validation memo
113
123
 
@@ -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` with:
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.4",
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
- {"flows":[]}
@@ -1 +0,0 @@
1
- {"status":200,"headers":{"content-type":"application/json","x-next-cache-tags":"_N_T_/layout,_N_T_/api/layout,_N_T_/api/flows/layout,_N_T_/api/flows/route,_N_T_/api/flows"}}
@@ -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,6310))},6310:function(e,t,a){"use strict";a.d(t,{IssuesBoardClient:function(){return x}});var r=a(7821),n=a(8078),s=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 o(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}=e,[i,o]=(0,n.useState)(a),[d,u]=(0,n.useState)(null),[x,f]=(0,n.useState)("all");(0,n.useEffect)(()=>{o(a)},[a]);let b=(0,s.Dy)((0,s.VT)(s.we,{activationConstraint:{distance:4}})),v=(0,n.useMemo)(()=>"all"===x?i:i.filter(e=>e.source===x),[i,x]),g=(0,n.useMemo)(()=>{let e={potential:[],verified:[],invalid:[],duplicate:[]};for(let t of v)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},[v]),y=(0,n.useMemo)(()=>{let e={potential:0,verified:0,invalid:0,duplicate:0};for(let t of i)e[t.column]++;return e},[i]),j=(0,n.useCallback)(async(e,t)=>{o(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),o(t=>t.map(t=>{var r;return t.id===e&&null!==(r=a.find(t=>t.id===e))&&void 0!==r?r:t}))}},[a]),N=(0,n.useCallback)(async(e,t)=>{o(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)}},[]),w=null!==(t=i.find(e=>e.id===d))&&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:[i.length," total — ",y.potential," potential, ",y.verified," verified,"," ",y.invalid," invalid, ",y.duplicate," duplicate. Drag a card to change column; click to edit."]})]}),(0,r.jsx)(m,{value:x,onChange:f})]}),(0,r.jsx)(s.LB,{sensors:b,onDragEnd:e=>{let{active:t,over:a}=e;if(!a)return;let r=a.id,n=t.id,s=i.find(e=>e.id===n);s&&s.column!==r&&j(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:g[e.id],onOpen:u},e.id))})}),w&&(0,r.jsx)(h,{issue:w,onClose:()=>u(null),onSave:e=>N(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:o}=(0,s.Zj)({id:t.id});return(0,r.jsxs)("section",{ref:o,"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,s.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)(o,{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}),t.github_synced&&(0,r.jsxs)("a",{href:t.github_issue_url,target:"_blank",rel:"noopener noreferrer",onClick:e=>e.stopPropagation(),className:"rounded-md bg-surface-2 px-1.5 py-0.5 text-text-secondary hover:bg-surface-3 hover:text-text-primary",children:["GH ","closed"===t.github_state?"✕":"✓"]}),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,s;let{issue:i,onClose:o,onSave:l}=e,[c,x]=(0,n.useState)(i.title),[m,p]=(0,n.useState)(i.severity),[f,h]=(0,n.useState)(i.description),[g,y]=(0,n.useState)(i.recommendation),[j,N]=(0,n.useState)(null!==(t=i.resolution)&&void 0!==t?t:""),[w,_]=(0,n.useState)(null!==(a=i.update_from_client)&&void 0!==a?a:""),[C,k]=(0,n.useState)(!1),S=(0,n.useRef)(null);(0,n.useEffect)(()=>{var e;null===(e=S.current)||void 0===e||e.focus()},[]);let M=async()=>{k(!0),await l({title:c,severity:m,description:f,recommendation:g,resolution:j||void 0,update_from_client:w||void 0}),k(!1),o()};return(0,r.jsx)("div",{ref:S,tabIndex:-1,className:"fixed inset-0 z-50 flex items-center justify-center p-4 outline-none",style:{background:"var(--overlay-bg)"},onClick:o,onKeyDown:e=>{"Escape"===e.key&&o()},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",children:[(0,r.jsx)("span",{className:"font-mono text-body text-text-tertiary",children:i.id}),(0,r.jsx)(b,{source:i.source}),i.github_synced&&i.github_issue_url&&(0,r.jsx)("a",{href:i.github_issue_url,target:"_blank",rel:"noopener noreferrer",className:"rounded-md bg-surface-3 px-1.5 py-0.5 text-caption text-text-secondary hover:text-text-primary",children:"GitHub issue"})]}),(0,r.jsx)("button",{type:"button",onClick:o,"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)(v,{label:"Title",children:(0,r.jsx)("input",{type:"text",value:c,onChange:e=>x(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)(v,{label:"Severity",children:(0,r.jsx)("select",{value:m,onChange:e=>p(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)(v,{label:"Resolution (verified only)",children:(0,r.jsxs)("select",{value:j,onChange:e=>N(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)(v,{label:"Description",className:"mt-3",children:(0,r.jsx)("textarea",{value:f,onChange:e=>h(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)(v,{label:"Recommendation",className:"mt-3",children:(0,r.jsx)("textarea",{value:g,onChange:e=>y(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)(v,{label:"Update from the client",className:"mt-3",children:(0,r.jsx)("textarea",{value:w,onChange:e=>_(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!==(s=i.reasoning)&&void 0!==s?s:"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-end gap-2 border-t border-border-default bg-surface-2 px-4 py-3",children:[(0,r.jsx)("button",{type:"button",onClick:o,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:M,disabled:C,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:C?"Saving…":"Save"})]})]})})}function v(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()}]);