@bugb-technologies/bravos 0.0.0-stage → 1.0.1

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 (85) hide show
  1. package/LICENSE +70 -0
  2. package/README.md +225 -2
  3. package/dist/account.js +140 -0
  4. package/dist/artifacts.js +450 -0
  5. package/dist/ci.js +52 -0
  6. package/dist/cli.js +963 -0
  7. package/dist/codegraph.js +268 -0
  8. package/dist/cursor.js +70 -0
  9. package/dist/detect.js +26 -0
  10. package/dist/doctor.js +99 -0
  11. package/dist/global.js +95 -0
  12. package/dist/graphbin.js +136 -0
  13. package/dist/guardlink.js +123 -0
  14. package/dist/guide.js +255 -0
  15. package/dist/help.js +334 -0
  16. package/dist/hooks.js +167 -0
  17. package/dist/install.js +209 -0
  18. package/dist/licence.js +41 -0
  19. package/dist/lock.js +50 -0
  20. package/dist/mcp.js +139 -0
  21. package/dist/migrate.js +275 -0
  22. package/dist/omp.js +46 -0
  23. package/dist/opencode.js +118 -0
  24. package/dist/paths.js +33 -0
  25. package/dist/plugin.js +101 -0
  26. package/dist/report/check.js +252 -0
  27. package/dist/report/data.js +401 -0
  28. package/dist/report/export.js +105 -0
  29. package/dist/report/markdown.js +310 -0
  30. package/dist/report/meta.js +48 -0
  31. package/dist/report/pdf.js +96 -0
  32. package/dist/review-server.js +7 -0
  33. package/dist/review.js +274 -0
  34. package/dist/setup.js +133 -0
  35. package/dist/skills.js +81 -0
  36. package/dist/types.js +9 -0
  37. package/dist/uninstall.js +192 -0
  38. package/dist/versions.js +8 -0
  39. package/dist/watch-frame.js +593 -0
  40. package/dist/watch.js +873 -0
  41. package/dist/workspace.js +316 -0
  42. package/docs/GUIDE.md +323 -0
  43. package/hooks/autopilot.mjs +45 -0
  44. package/hooks/lib/autopilot.mjs +191 -0
  45. package/hooks/lib/briefing.mjs +750 -0
  46. package/hooks/lib/licence.mjs +452 -0
  47. package/hooks/lib/pre-pr.mjs +209 -0
  48. package/hooks/lib/sast.mjs +207 -0
  49. package/hooks/lib/setup.mjs +107 -0
  50. package/hooks/lib/workspace.mjs +78 -0
  51. package/hooks/pre-pr.mjs +54 -0
  52. package/hooks/session-start.mjs +82 -0
  53. package/package.json +49 -4
  54. package/skills/bravos/SKILL.md +63 -0
  55. package/skills/bravos-annotate/SKILL.md +56 -0
  56. package/skills/bravos-annotate/scripts/lib/pick.mjs +127 -0
  57. package/skills/bravos-annotate/scripts/slice.mjs +141 -0
  58. package/skills/bravos-campaign/SKILL.md +49 -0
  59. package/skills/bravos-cicd/SKILL.md +48 -0
  60. package/skills/bravos-codegraph/SKILL.md +57 -0
  61. package/skills/bravos-context/SKILL.md +51 -0
  62. package/skills/bravos-context/intent.example.md +48 -0
  63. package/skills/bravos-env/SKILL.md +48 -0
  64. package/skills/bravos-env/scope.example.yaml +50 -0
  65. package/skills/bravos-prove/SKILL.md +52 -0
  66. package/skills/bravos-recheck/SKILL.md +47 -0
  67. package/skills/bravos-report/SKILL.md +53 -0
  68. package/skills/bravos-report/assessment.md +54 -0
  69. package/skills/bravos-report/brief.md +23 -0
  70. package/skills/bravos-report/finding.md +74 -0
  71. package/skills/bravos-report/retest.md +29 -0
  72. package/skills/bravos-report/style.md +44 -0
  73. package/skills/bravos-sast/SKILL.md +48 -0
  74. package/skills/bravos-schedule/SKILL.md +44 -0
  75. package/skills/bravos-schedule/loop.md +16 -0
  76. package/skills/bravos-ship/SKILL.md +58 -0
  77. package/skills/bravos-templatize/SKILL.md +48 -0
  78. package/skills/bravos-templatize/operator-guidance.md +20 -0
  79. package/templates/omp/bravos.js +62 -0
  80. package/templates/opencode/bravos.js +33 -0
  81. package/templates/report/report.html +88 -0
  82. package/templates/review/review.html +327 -0
  83. package/templates/workflows/bravos-nightly-claude.yml +173 -0
  84. package/templates/workflows/bravos-nightly-codex.yml +172 -0
  85. package/templates/workflows/bravos-pr-gate.yml +142 -0
@@ -0,0 +1,327 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src data:; base-uri 'none'; form-action 'none'">
7
+ <title>bravos review</title>
8
+ <!-- bravos (managed by bravos review; regenerated from .guardlink/<kind>-draft.json) -->
9
+ <style>
10
+ :root {
11
+ --green: #33d49d; --navy: #1f3943; --blue: #0360a2; --light: #f0f0f0; --steel: #55899e; --red: #ea1d1d; --slate: #3b6779;
12
+ --bg: #f6f8f9; --card: #ffffff; --ink: #10222a; --muted: #4c6b78; --line: #d8e2e6; --chip: #e9f2f4; --accent: #0360a2; --ok: #138a63;
13
+ }
14
+ @media (prefers-color-scheme: dark) {
15
+ :root:not([data-theme="light"]) { --bg: #0e1b21; --card: #15262e; --ink: #f0f0f0; --muted: #8fb0bd; --line: #24404b; --chip: #1f3943; --accent: #33d49d; --ok: #33d49d; }
16
+ }
17
+ :root[data-theme="dark"] { --bg: #0e1b21; --card: #15262e; --ink: #f0f0f0; --muted: #8fb0bd; --line: #24404b; --chip: #1f3943; --accent: #33d49d; --ok: #33d49d; }
18
+ * { box-sizing: border-box; }
19
+ body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
20
+ header { background: #0e1b21; color: var(--light); padding: 20px 16px; }
21
+ .brand { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; max-width: 980px; margin: 0 auto; }
22
+ pre.art { margin: 0; font: 9px/1.05 ui-monospace, "SF Mono", Menlo, monospace; white-space: pre; }
23
+ .brand h1 { margin: 0; font-size: 18px; font-weight: 600; }
24
+ .brand p { margin: 4px 0 0; color: #8fb0bd; font-size: 13px; }
25
+ main { max-width: 980px; margin: 0 auto; padding: 16px; }
26
+ .bar { position: sticky; top: 0; z-index: 2; background: var(--bg); padding: 12px 0; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; border-bottom: 1px solid var(--line); }
27
+ .bar label { font-size: 13px; color: var(--muted); }
28
+ .bar input { font: inherit; padding: 6px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--card); color: var(--ink); min-width: 180px; }
29
+ .progress { flex: 1; min-width: 140px; height: 8px; background: var(--chip); border-radius: 99px; overflow: hidden; }
30
+ .progress > div { height: 100%; width: 0; background: var(--green); transition: width .2s; }
31
+ button { font: inherit; border-radius: 8px; border: 1px solid var(--line); background: var(--card); color: var(--ink); padding: 6px 12px; cursor: pointer; }
32
+ button.primary { background: var(--blue); border-color: var(--blue); color: #fff; }
33
+ button.on { outline: 2px solid var(--accent); outline-offset: 1px; font-weight: 600; }
34
+ h2 { font-size: 13px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin: 24px 0 8px; }
35
+ .card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; margin: 10px 0; }
36
+ .card.done { border-left: 4px solid var(--green); }
37
+ .card .id { font: 12px ui-monospace, Menlo, monospace; color: var(--muted); }
38
+ .card .title { font-weight: 600; margin: 2px 0 4px; }
39
+ .card .why { font-size: 13px; color: var(--muted); }
40
+ .card .text { white-space: pre-wrap; margin: 6px 0; }
41
+ .sources { font: 12px ui-monospace, Menlo, monospace; color: var(--muted); overflow-wrap: anywhere; }
42
+ .choices, .verdicts { display: flex; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
43
+ textarea { width: 100%; font: inherit; border: 1px solid var(--line); border-radius: 8px; padding: 8px; background: var(--bg); color: var(--ink); min-height: 56px; }
44
+ .others { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
45
+ .chip { background: var(--chip); border-radius: 99px; padding: 2px 10px; font-size: 12px; }
46
+ .chip.no { color: var(--red); }
47
+ .chip.agree { color: var(--ok); }
48
+ .note { padding: 10px 14px; border-radius: 10px; background: var(--chip); margin: 12px 0; font-size: 14px; }
49
+ .hidden { display: none; }
50
+ .top { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
51
+ .badge { font-size: 11px; border-radius: 99px; padding: 1px 8px; border: 1px solid var(--line); }
52
+ .badge.sugg { color: var(--accent); }
53
+ .badge.must { background: #ea1d1d; color: #fff; border-color: #ea1d1d; }
54
+ .badge.edited { color: var(--blue); border-color: var(--blue); }
55
+ .badge.chosen { color: var(--ok); border-color: var(--ok); }
56
+ .badge.low { color: #b26b00; border-color: #b26b00; }
57
+ .card.must { border-left: 4px solid #ea1d1d; }
58
+ .reason { font-size: 13px; color: var(--muted); margin: 4px 0; }
59
+ button.keep { margin: 4px 0 8px; }
60
+ button:disabled { opacity: .55; cursor: not-allowed; }
61
+ textarea.comment { min-height: 36px; margin-top: 6px; }
62
+ details.facts { margin: 8px 0 16px; }
63
+ details.facts > summary { cursor: pointer; color: var(--muted); padding: 8px 0; }
64
+ .chip.edit { color: var(--blue); }
65
+ @media (max-width: 600px) { pre.art { font-size: 6px; } .bar input { min-width: 0; width: 100%; } }
66
+ </style>
67
+ </head>
68
+ <body>
69
+ <header>
70
+ <div class="brand">
71
+ <pre class="art" id="mascot" aria-hidden="true"></pre>
72
+ <div>
73
+ <pre class="art" id="wordmark" aria-label="bravos"></pre>
74
+ <h1 id="heading"></h1>
75
+ <p id="sub"></p>
76
+ </div>
77
+ </div>
78
+ </header>
79
+ <main>
80
+ <div class="bar">
81
+ <label for="who">Your name</label>
82
+ <input id="who" autocomplete="name" placeholder="e.g. Sam Lee">
83
+ <div class="progress" aria-label="answered"><div id="progress"></div></div>
84
+ <span id="count" class="chip"></span>
85
+ <button id="save" class="primary">Confirm</button>
86
+ <button id="done">Done</button>
87
+ </div>
88
+ <div id="status" class="note hidden" role="status"></div>
89
+ <div id="questions"></div>
90
+ </main>
91
+ <script type="application/json" id="bravos-data">__BRAVOS_DATA__</script>
92
+ <script>
93
+ (() => {
94
+ 'use strict';
95
+ const DATA = JSON.parse(document.getElementById('bravos-data').textContent);
96
+ const draft = DATA.draft;
97
+ const mode = DATA.mode; // { kind: 'serve' | 'file' | 'artifact', token? }
98
+ const $ = (id) => document.getElementById(id);
99
+ // Every piece of draft text is untrusted (it came from repo files): built with textContent only.
100
+ const el = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = String(text); return e; };
101
+
102
+ const C = { '▓': '#3b6779', '▄': '#55899e', '▀': '#55899e', '█': '#33d49d', '▐': '#33d49d', '▌': '#33d49d' };
103
+ const W = { '█': '#33d49d' };
104
+ const paint = (pre, lines, map, fallback) => {
105
+ for (const line of lines) {
106
+ for (const ch of line) {
107
+ const s = el('span', '', ch);
108
+ const c = map[ch] || (fallback && ch !== ' ' ? fallback : null);
109
+ if (c) s.style.color = c;
110
+ pre.appendChild(s);
111
+ }
112
+ pre.appendChild(document.createTextNode('\n'));
113
+ }
114
+ };
115
+ paint($('mascot'), DATA.mascot, C);
116
+ paint($('wordmark'), DATA.logo, W, '#0360a2');
117
+ $('heading').textContent = DATA.heading;
118
+ $('sub').textContent = `${draft.repo || ''} · drafted ${draft.drafted || ''} · ${draft.questions.length} questions`;
119
+
120
+ const store = { get: (k) => { try { return localStorage.getItem(k); } catch { return null; } }, set: (k, v) => { try { localStorage.setItem(k, v); } catch { /* private window */ } } };
121
+ $('who').value = store.get('bravos-review-name') || '';
122
+ $('who').addEventListener('change', () => store.set('bravos-review-name', $('who').value.trim()));
123
+
124
+ const qs = draft.questions;
125
+ const state = {}; // id -> { verdict, value, touched, comment }
126
+ const cards = {};
127
+ const sameValue = (a, b) => JSON.stringify(Array.isArray(a) ? [...a].sort() : a) === JSON.stringify(Array.isArray(b) ? [...b].sort() : b);
128
+ const status = (msg) => { const s = $('status'); s.textContent = msg; s.classList.remove('hidden'); };
129
+
130
+ for (const q of qs) {
131
+ const has = q.suggested !== undefined;
132
+ const initial = has ? q.suggested : q.type === 'text' ? (q.draft ?? '') : q.type === 'item' ? null : q.type === 'multi' ? [] : null;
133
+ state[q.id] = { verdict: has ? 'accepted' : null, value: Array.isArray(initial) ? [...initial] : initial, touched: false, comment: '' };
134
+ }
135
+
136
+ const needsCall = (q) => q.mustTouch && !state[q.id].touched;
137
+ const decide = (q, value, explicit) => {
138
+ const s = state[q.id];
139
+ s.value = value;
140
+ s.touched = s.touched || explicit;
141
+ if (value === 'no') s.verdict = 'no';
142
+ else if (q.suggested !== undefined && sameValue(value, q.suggested)) s.verdict = q.mustTouch ? (s.touched ? 'chosen' : 'accepted') : 'accepted';
143
+ else s.verdict = value === null || (Array.isArray(value) && value.length === 0 && q.type !== 'multi') ? null : 'edited';
144
+ render(q);
145
+ refresh();
146
+ };
147
+
148
+ const refresh = () => {
149
+ const left = qs.filter(needsCall);
150
+ const answered = qs.filter((q) => state[q.id].verdict).length;
151
+ $('progress').style.width = `${Math.round((answered / qs.length) * 100)}%`;
152
+ $('count').textContent = left.length ? `${left.length} decision${left.length === 1 ? '' : 's'} left` : `${answered} / ${qs.length} answered`;
153
+ $('save').disabled = left.length > 0;
154
+ $('save').textContent = left.length ? `Confirm (${left.length} left)` : 'Confirm';
155
+ };
156
+
157
+ // Each card is rebuilt from state on change; draft text is set with textContent only.
158
+ const render = (q) => {
159
+ const s = state[q.id];
160
+ const card = cards[q.id];
161
+ card.replaceChildren();
162
+ card.className = `card${s.verdict ? ' done' : ''}${needsCall(q) ? ' must' : ''}`;
163
+ const top = el('div', 'top');
164
+ top.appendChild(el('span', 'id', q.id));
165
+ if (needsCall(q)) top.appendChild(el('span', 'badge must', 'needs your call'));
166
+ else if (s.verdict === 'edited' || s.verdict === 'no') top.appendChild(el('span', 'badge edited', s.verdict === 'no' ? 'you said no' : 'edited'));
167
+ else if (s.verdict === 'chosen') top.appendChild(el('span', 'badge chosen', 'your call'));
168
+ else if (s.verdict === 'accepted') top.appendChild(el('span', 'badge sugg', 'suggested'));
169
+ if (q.confidence === 'low') top.appendChild(el('span', 'badge low', 'bravos is unsure'));
170
+ card.appendChild(top);
171
+ if (q.title) card.appendChild(el('div', 'title', q.title));
172
+ if (q.why) card.appendChild(el('div', 'why', q.why));
173
+ if (q.type === 'item' && q.text) card.appendChild(el('div', 'text', q.text));
174
+ if (q.reason) card.appendChild(el('div', 'reason', `bravos: ${q.reason}`));
175
+ if (q.sources && q.sources.length) card.appendChild(el('div', 'sources', `from ${q.sources.join(' · ')}`));
176
+
177
+ const row = el('div', 'choices');
178
+ const btn = (label, on, fn) => { const b = el('button', on ? 'on' : '', label); b.type = 'button'; b.addEventListener('click', fn); row.appendChild(b); return b; };
179
+ if (q.type === 'choice') {
180
+ for (const o of q.options) btn(`${o.id === q.suggested ? '★ ' : ''}${o.label}`, s.value === o.id, () => decide(q, o.id, true));
181
+ } else if (q.type === 'multi') {
182
+ for (const o of q.options) {
183
+ const on = Array.isArray(s.value) && s.value.includes(o.id);
184
+ btn(`${on ? '☑' : '☐'} ${o.label}`, on, () => {
185
+ const next = new Set(s.value || []);
186
+ next.has(o.id) ? next.delete(o.id) : next.add(o.id);
187
+ decide(q, q.options.map((x) => x.id).filter((id) => next.has(id)), true);
188
+ });
189
+ }
190
+ } else if (q.type === 'item') {
191
+ const isText = typeof s.value === 'string' && s.value !== 'agree' && s.value !== 'no';
192
+ btn('Agree', s.value === 'agree', () => decide(q, 'agree', true));
193
+ btn('Change', isText, () => decide(q, q.text || '', true));
194
+ btn('No', s.value === 'no', () => decide(q, 'no', true));
195
+ if (isText) {
196
+ const t = el('textarea');
197
+ t.value = s.value;
198
+ t.addEventListener('change', () => decide(q, t.value, true));
199
+ card.appendChild(row);
200
+ card.appendChild(t);
201
+ }
202
+ }
203
+ if (q.type === 'text') {
204
+ const t = el('textarea');
205
+ t.value = typeof s.value === 'string' ? s.value : '';
206
+ t.addEventListener('change', () => decide(q, t.value, true));
207
+ card.appendChild(t);
208
+ }
209
+ if (q.type !== 'text' && !card.contains(row)) card.appendChild(row);
210
+ if (needsCall(q) && q.suggested !== undefined) {
211
+ const keep = el('button', 'keep', "Keep bravos's answer");
212
+ keep.type = 'button';
213
+ keep.addEventListener('click', () => decide(q, Array.isArray(q.suggested) ? [...q.suggested] : q.suggested, true));
214
+ card.appendChild(keep);
215
+ }
216
+ const c = el('textarea', 'comment');
217
+ c.placeholder = 'Comment (optional)';
218
+ c.value = s.comment;
219
+ c.addEventListener('input', () => { s.comment = c.value; });
220
+ card.appendChild(c);
221
+ card.appendChild(card._others || (card._others = el('div', 'others')));
222
+ };
223
+
224
+ // Open questions first by section; facts (folded) collapse under one header per section.
225
+ const sections = [];
226
+ for (const q of qs) {
227
+ const name = q.section || 'Questions';
228
+ let sec = sections.find((x) => x.name === name);
229
+ if (!sec) sections.push((sec = { name, open: [], folded: [] }));
230
+ (q.folded ? sec.folded : sec.open).push(q);
231
+ }
232
+ const mount = (parent, q) => { const card = el('div', 'card'); cards[q.id] = card; parent.appendChild(card); render(q); };
233
+ for (const sec of sections) {
234
+ $('questions').appendChild(el('h2', '', sec.name));
235
+ for (const q of sec.open) mount($('questions'), q);
236
+ if (sec.folded.length) {
237
+ const d = el('details', 'facts');
238
+ d.appendChild(el('summary', '', `${sec.folded.length} fact${sec.folded.length === 1 ? '' : 's'} bravos drafted — accepted unless you open one to object`));
239
+ for (const q of sec.folded) mount(d, q);
240
+ $('questions').appendChild(d);
241
+ }
242
+ }
243
+ refresh();
244
+
245
+ const showOthers = (all) => {
246
+ for (const q of qs) {
247
+ const box = cards[q.id]._others;
248
+ box.replaceChildren();
249
+ for (const f of all) {
250
+ const a = f.answers && f.answers[q.id];
251
+ if (!a) continue;
252
+ const v = a.value !== undefined ? (Array.isArray(a.value) ? a.value.join(', ') : a.value) : a.choice || a.text || '';
253
+ box.appendChild(el('span', `chip ${a.verdict === 'no' ? 'no' : a.verdict === 'edited' ? 'edit' : 'agree'}`, `${f.by}: ${a.verdict}${v && a.verdict !== 'accepted' ? ` — ${v}` : ''}`));
254
+ }
255
+ }
256
+ };
257
+
258
+ const payload = () => {
259
+ const by = $('who').value.trim();
260
+ const answers = {};
261
+ for (const q of qs) {
262
+ const s = state[q.id];
263
+ if (!s.verdict && !s.comment) continue;
264
+ answers[q.id] = { verdict: s.verdict || 'comment', ...(s.verdict && s.verdict !== 'accepted' && s.value !== null ? { value: s.value } : {}), ...(s.comment ? { comment: s.comment } : {}) };
265
+ }
266
+ return { schema: 'bravos.review-answers/v1', kind: draft.kind, by, date: new Date().toISOString().slice(0, 10), answers };
267
+ };
268
+ const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
269
+
270
+ const api = (path, body) => fetch(`${path}?t=${encodeURIComponent(mode.token)}`, body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
271
+
272
+ // Artifact mode (opt-in sharing): the page saves through the artifact's shared database (claude.use('db')),
273
+ // which resolves null outside claude.ai; the page then falls back to downloading the answers file.
274
+ let dbPromise = null;
275
+ const artifactDb = () => {
276
+ if (mode.kind !== 'artifact' || !window.claude || typeof window.claude.use !== 'function') return Promise.resolve(null);
277
+ dbPromise = dbPromise || window.claude.use('db').catch(() => null);
278
+ return dbPromise;
279
+ };
280
+
281
+ const load = async () => {
282
+ try {
283
+ if (mode.kind === 'serve') { const r = await api('/answers'); if (r.ok) showOthers(await r.json()); }
284
+ else if (mode.kind === 'artifact') {
285
+ const db = await artifactDb();
286
+ if (db) { const snap = await db.collection('answers').get(); showOthers(snap.docs.map((d) => d.data()).filter(Boolean)); }
287
+ }
288
+ } catch { /* offline: nothing to show */ }
289
+ };
290
+ load();
291
+
292
+ $('save').addEventListener('click', async () => {
293
+ const p = payload();
294
+ if (!p.by) { status('Add your name first, so your answers are confirmed by you.'); $('who').focus(); return; }
295
+ store.set('bravos-review-name', p.by);
296
+ try {
297
+ if (mode.kind === 'serve') {
298
+ const r = await api('/answers', p);
299
+ if (r.status === 400) { status(await r.text()); return; }
300
+ if (!r.ok) throw new Error(await r.text());
301
+ status(`Confirmed and saved to .guardlink/${draft.kind}-answers/${slug(p.by)}.json. Teammates can run bravos review ${draft.kind} and add theirs; click Done when you are finished.`);
302
+ load();
303
+ return;
304
+ }
305
+ const db = await artifactDb();
306
+ if (db) {
307
+ await db.doc(`answers/${slug(p.by)}`).set(p);
308
+ status('Saved. Everyone with this link sees it; the agent will read it back into the repo.');
309
+ load();
310
+ return;
311
+ }
312
+ } catch (e) { status(`Could not save to the server (${e.message || e}); downloading the file instead.`); }
313
+ const a = document.createElement('a');
314
+ a.href = URL.createObjectURL(new Blob([JSON.stringify(p, null, 2) + '\n'], { type: 'application/json' }));
315
+ a.download = `${slug(p.by)}.json`;
316
+ a.click();
317
+ status(`Downloaded ${slug(p.by)}.json. Put it in .guardlink/${draft.kind}-answers/ and commit it.`);
318
+ });
319
+
320
+ $('done').addEventListener('click', async () => {
321
+ if (mode.kind === 'serve') { try { await api('/done', {}); } catch { /* already gone */ } }
322
+ status('Thanks. Tell the agent "done" and it will read everyone\'s answers. You can close this tab.');
323
+ });
324
+ })();
325
+ </script>
326
+ </body>
327
+ </html>
@@ -0,0 +1,173 @@
1
+ # bravos:generated — written by `bravos init --ci`; `bravos sync --ci` refreshes it while this line stays.
2
+ # The nightly "bulk bee": Claude Code covers the next slice of unannotated files with the
3
+ # bravos-annotate skill, the lint gate checks what it wrote, and the job opens one small PR.
4
+ # Optionally it then replays the committed retest kits against the target the reviewed
5
+ # .guardlink/scope.yaml authorizes (see the "Authorized scan (gated)" step); without that scope
6
+ # file's human-written attestation the scan step skips and the job annotates only.
7
+ # The nightly is on the Team plan: add the repo secret BRAVOS_LICENCE (the token from
8
+ # account.bugb.io/dashboard/license) so the agent's paid skills run; without it they say so and stop.
9
+ # Needs the repo secret ANTHROPIC_API_KEY, and Settings > Actions > General > "Allow GitHub
10
+ # Actions to create and approve pull requests". Pin the actions to commit SHAs if your
11
+ # organisation requires it.
12
+ name: bravos nightly (Claude Code)
13
+
14
+ on:
15
+ schedule:
16
+ - cron: '17 3 * * 1-5'
17
+ workflow_dispatch:
18
+
19
+ permissions:
20
+ contents: write
21
+ pull-requests: write
22
+ issues: write
23
+ id-token: write
24
+
25
+ concurrency:
26
+ group: bravos-nightly
27
+ cancel-in-progress: false
28
+
29
+ jobs:
30
+ annotate:
31
+ runs-on: ubuntu-latest
32
+ env:
33
+ BRAVOS_LICENCE: ${{ secrets.BRAVOS_LICENCE }}
34
+ timeout-minutes: 60
35
+ defaults:
36
+ run:
37
+ shell: bash
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ with:
41
+ fetch-depth: 0
42
+
43
+ - uses: actions/setup-node@v4
44
+ with:
45
+ node-version: 22
46
+
47
+ - name: Install guardlink
48
+ run: npm install -g guardlink
49
+
50
+ - name: Starting point
51
+ id: start
52
+ run: |
53
+ echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
54
+ guardlink ci -f json > "$RUNNER_TEMP/ci-before.json" || true
55
+ echo "coverage=$(jq -r '.summary.coverage.percent // "unknown"' "$RUNNER_TEMP/ci-before.json")" >> "$GITHUB_OUTPUT"
56
+
57
+ - name: Annotate the next slice
58
+ uses: anthropics/claude-code-action@v1
59
+ with:
60
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
61
+ prompt: |
62
+ Follow .claude/skills/bravos-annotate/SKILL.md in slice mode (the default) to cover the
63
+ next unannotated files that `guardlink unannotated --json` lists. You are the agent: do
64
+ the work in this session and never run guardlink annotate with an agent flag. Stop when
65
+ `guardlink lint . --since ${{ steps.start.outputs.sha }} --json` reports 0 errors and
66
+ `guardlink validate .` passes. Leave the changes uncommitted; a later step commits them
67
+ and opens the pull request.
68
+ claude_args: --allowedTools "Read,Edit,Write,Glob,Grep,Bash(guardlink:*),Bash(node:*),Bash(git status:*),Bash(git diff:*),Bash(git log:*),Bash(git rev-parse:*),Bash(git symbolic-ref:*)"
69
+
70
+ - name: Gate what the agent wrote
71
+ env:
72
+ START: ${{ steps.start.outputs.sha }}
73
+ run: |
74
+ guardlink validate .
75
+ guardlink lint . --since "$START" --json > "$RUNNER_TEMP/lint.json" || true
76
+ errors=$(jq -r '.summary.errors' "$RUNNER_TEMP/lint.json")
77
+ if [ "$errors" != "0" ]; then
78
+ echo "::error::guardlink lint reports $errors error(s) in what the agent wrote"
79
+ jq . "$RUNNER_TEMP/lint.json"
80
+ exit 1
81
+ fi
82
+
83
+ # Opt-in: replays the committed retest kits against the target the reviewed scope authorizes.
84
+ # It runs only when .guardlink/scope.yaml carries a human-written authorization_attestation and
85
+ # vars.BRAVOS_SCAN_TARGET is loopback (started by vars.BRAVOS_APP_START) or in the scope's
86
+ # host_allowlist. It replays kits only: no probe generation and no destructive classes.
87
+ - name: Authorized scan (gated)
88
+ id: scan
89
+ env:
90
+ TARGET: ${{ vars.BRAVOS_SCAN_TARGET }}
91
+ APP_START: ${{ vars.BRAVOS_APP_START }}
92
+ AUTH_PROFILE: ${{ vars.BRAVOS_SCAN_AUTH }}
93
+ AUTH_STATE: ${{ secrets.BRAVOS_AUTH_STATE }}
94
+ run: |
95
+ skip() { echo "::notice::nightly scan skipped: $1"; echo "ran=false" >> "$GITHUB_OUTPUT"; exit 0; }
96
+ scope=.guardlink/scope.yaml
97
+ [ -n "$TARGET" ] || skip "vars.BRAVOS_SCAN_TARGET is not set"
98
+ [ -f "$scope" ] || skip "$scope is not committed"
99
+ attest=$(sed -n 's/^authorization_attestation:[[:space:]]*//p' "$scope" | sed 's/^"\(.*\)"$/\1/')
100
+ [ -n "$attest" ] || skip "$scope has no authorization_attestation; a human must write it and get it reviewed"
101
+ ls .guardlink/cxg-replay/*.js >/dev/null 2>&1 || skip "no retest kits in .guardlink/cxg-replay/"
102
+ host=$(printf '%s' "$TARGET" | sed -E 's#^[a-z]+://##; s#[:/].*$##')
103
+ case "$host" in
104
+ 127.0.0.1|localhost|::1|'[::1]') loopback=yes ;;
105
+ *) loopback=no ;;
106
+ esac
107
+ if [ "$loopback" = no ] && ! grep -qF "$host" <(sed -n '/^host_allowlist:/,/^[a-z_]*:/p' "$scope"); then
108
+ skip "$host is neither loopback nor in the scope's host_allowlist"
109
+ fi
110
+ if [ "$loopback" = yes ]; then
111
+ [ -n "$APP_START" ] || skip "a loopback target needs vars.BRAVOS_APP_START to start the app"
112
+ bash -c "$APP_START"
113
+ for i in $(seq 1 60); do curl -fsS -o /dev/null "$TARGET" && break; sleep 2; done
114
+ fi
115
+ cargo install --locked cert-x-gen
116
+ cxg pentest install
117
+ auth=()
118
+ if [ -n "$AUTH_PROFILE" ]; then
119
+ [ -n "$AUTH_STATE" ] || skip "vars.BRAVOS_SCAN_AUTH is set but secrets.BRAVOS_AUTH_STATE is empty"
120
+ printf '%s' "$AUTH_STATE" | base64 -d > "$RUNNER_TEMP/state.json"
121
+ cxg pentest auth import --storage-state "$RUNNER_TEMP/state.json" --profile "$AUTH_PROFILE" --target "$TARGET"
122
+ auth=(--auth "$AUTH_PROFILE")
123
+ fi
124
+ guardlink sarif . --profile pentest -o whitebox/findings.sarif
125
+ run=".guardlink/pentest-findings/nightly-$(date -u +%Y%m%d)"
126
+ set +e
127
+ cxg pentest run --codebase . --target "$TARGET" "${auth[@]}" --template-dir .guardlink/cxg-replay/ --scope-file "$scope" --attestation "$attest" --session-dir "$run" --ci
128
+ code=$?
129
+ set -e
130
+ echo "exit=$code" >> "$GITHUB_OUTPUT"
131
+ case "$code" in
132
+ 0|2) guardlink hypothesis confirm --from-scan "$run/report.json" --write || true
133
+ echo "ran=true" >> "$GITHUB_OUTPUT" ;;
134
+ *) echo "::warning::cxg pentest run exited $code: nothing was proven"; echo "ran=false" >> "$GITHUB_OUTPUT" ;;
135
+ esac
136
+
137
+ - name: Open the pull request
138
+ env:
139
+ SCAN_RAN: ${{ steps.scan.outputs.ran }}
140
+ SCAN_EXIT: ${{ steps.scan.outputs.exit }}
141
+ SCAN_TARGET: ${{ vars.BRAVOS_SCAN_TARGET }}
142
+ GH_TOKEN: ${{ github.token }}
143
+ START: ${{ steps.start.outputs.sha }}
144
+ BEFORE: ${{ steps.start.outputs.coverage }}
145
+ run: |
146
+ if [ -z "$(git status --porcelain)" ]; then
147
+ echo "Nothing new to annotate."
148
+ exit 0
149
+ fi
150
+ guardlink ci -f json > "$RUNNER_TEMP/ci-after.json" || true
151
+ after=$(jq -r '.summary.coverage.percent // "unknown"' "$RUNNER_TEMP/ci-after.json")
152
+ base=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name)
153
+ branch="bravos/nightly-$(date -u +%Y%m%d-%H%M)"
154
+ git config user.name "bravos-nightly[bot]"
155
+ git config user.email "bravos-nightly@users.noreply.github.com"
156
+ git switch -c "$branch"
157
+ git add -A
158
+ git commit -m "annotate: nightly coverage slice (${BEFORE}% -> ${after}%)"
159
+ git push origin "$branch"
160
+ {
161
+ echo "Nightly coverage slice by bravos-annotate: ${BEFORE}% -> ${after}%."
162
+ echo
163
+ echo "## Security gate"
164
+ echo "validate ✔ · lint 0 errors · exposures recorded by this slice are listed below and stay open"
165
+ if [ "$SCAN_RAN" = "true" ]; then
166
+ echo "Tested: retest kits replayed against $SCAN_TARGET (exit code $SCAN_EXIT); outcomes recorded in .guardlink/hypotheses.json."
167
+ else
168
+ echo "Not tested: the gated scan did not run (see the job's notices)."
169
+ fi
170
+ echo
171
+ guardlink diff "$START" --markdown || true
172
+ } > "$RUNNER_TEMP/body.md"
173
+ gh pr create --base "$base" --head "$branch" --title "annotate: nightly coverage slice" --body-file "$RUNNER_TEMP/body.md"