@bugb-technologies/bravos 0.0.0-stage → 1.0.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.
- package/LICENSE +70 -0
- package/README.md +225 -2
- package/dist/account.js +140 -0
- package/dist/artifacts.js +450 -0
- package/dist/ci.js +52 -0
- package/dist/cli.js +963 -0
- package/dist/codegraph.js +268 -0
- package/dist/cursor.js +70 -0
- package/dist/detect.js +26 -0
- package/dist/doctor.js +99 -0
- package/dist/global.js +95 -0
- package/dist/graphbin.js +136 -0
- package/dist/guardlink.js +123 -0
- package/dist/guide.js +255 -0
- package/dist/help.js +334 -0
- package/dist/hooks.js +167 -0
- package/dist/install.js +209 -0
- package/dist/licence.js +41 -0
- package/dist/lock.js +50 -0
- package/dist/mcp.js +139 -0
- package/dist/migrate.js +275 -0
- package/dist/omp.js +46 -0
- package/dist/opencode.js +118 -0
- package/dist/paths.js +33 -0
- package/dist/plugin.js +101 -0
- package/dist/report/check.js +252 -0
- package/dist/report/data.js +401 -0
- package/dist/report/export.js +105 -0
- package/dist/report/markdown.js +310 -0
- package/dist/report/meta.js +48 -0
- package/dist/report/pdf.js +96 -0
- package/dist/review-server.js +7 -0
- package/dist/review.js +274 -0
- package/dist/setup.js +121 -0
- package/dist/skills.js +81 -0
- package/dist/types.js +9 -0
- package/dist/uninstall.js +192 -0
- package/dist/versions.js +8 -0
- package/dist/watch-frame.js +593 -0
- package/dist/watch.js +873 -0
- package/dist/workspace.js +316 -0
- package/docs/GUIDE.md +323 -0
- package/hooks/autopilot.mjs +45 -0
- package/hooks/lib/autopilot.mjs +191 -0
- package/hooks/lib/briefing.mjs +750 -0
- package/hooks/lib/licence.mjs +452 -0
- package/hooks/lib/pre-pr.mjs +209 -0
- package/hooks/lib/sast.mjs +207 -0
- package/hooks/lib/setup.mjs +107 -0
- package/hooks/lib/workspace.mjs +78 -0
- package/hooks/pre-pr.mjs +54 -0
- package/hooks/session-start.mjs +82 -0
- package/package.json +49 -4
- package/skills/bravos/SKILL.md +63 -0
- package/skills/bravos-annotate/SKILL.md +56 -0
- package/skills/bravos-annotate/scripts/lib/pick.mjs +127 -0
- package/skills/bravos-annotate/scripts/slice.mjs +141 -0
- package/skills/bravos-campaign/SKILL.md +49 -0
- package/skills/bravos-cicd/SKILL.md +48 -0
- package/skills/bravos-codegraph/SKILL.md +57 -0
- package/skills/bravos-context/SKILL.md +51 -0
- package/skills/bravos-context/intent.example.md +48 -0
- package/skills/bravos-env/SKILL.md +48 -0
- package/skills/bravos-env/scope.example.yaml +50 -0
- package/skills/bravos-prove/SKILL.md +52 -0
- package/skills/bravos-recheck/SKILL.md +47 -0
- package/skills/bravos-report/SKILL.md +53 -0
- package/skills/bravos-report/assessment.md +54 -0
- package/skills/bravos-report/brief.md +23 -0
- package/skills/bravos-report/finding.md +74 -0
- package/skills/bravos-report/retest.md +29 -0
- package/skills/bravos-report/style.md +44 -0
- package/skills/bravos-sast/SKILL.md +48 -0
- package/skills/bravos-schedule/SKILL.md +44 -0
- package/skills/bravos-schedule/loop.md +16 -0
- package/skills/bravos-ship/SKILL.md +58 -0
- package/skills/bravos-templatize/SKILL.md +48 -0
- package/skills/bravos-templatize/operator-guidance.md +20 -0
- package/templates/omp/bravos.js +62 -0
- package/templates/opencode/bravos.js +33 -0
- package/templates/report/report.html +88 -0
- package/templates/review/review.html +327 -0
- package/templates/workflows/bravos-nightly-claude.yml +173 -0
- package/templates/workflows/bravos-nightly-codex.yml +172 -0
- 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"
|