@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
package/dist/help.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// @comment -- "Long-form help text appended to `bravos --help` and each command's --help; pure strings, no logic."
|
|
2
|
+
export const PROGRAM_HELP = `
|
|
3
|
+
What bravos is
|
|
4
|
+
bravos makes the coding agents you already use (Claude Code, Codex, and Cursor through a
|
|
5
|
+
pointer) power users of guardlink (threat model as code), cert-x-gen / cxg (authorized
|
|
6
|
+
pentest as code), the code graph (codegraph-mcp) and licensed SAST (bravos-cli).
|
|
7
|
+
It installs skills, a session-start briefing, MCP wiring and optional CI into a repository.
|
|
8
|
+
The agents do the work with the real CLIs. bravos has no orchestrator and keeps no state:
|
|
9
|
+
.bravos/ holds only its hooks and a lock file.
|
|
10
|
+
|
|
11
|
+
Commands
|
|
12
|
+
init [dir] First install in a repository. Runs guardlink init if .guardlink/ is absent.
|
|
13
|
+
sync [dir] Re-apply bravos to a repository that already has it (after upgrading bravos,
|
|
14
|
+
or to add --ci / --pre-pr-hook / --scope / --autopilot later). Never runs
|
|
15
|
+
guardlink init. init and sync take --global for a user-level install.
|
|
16
|
+
doctor [dir] Read-only check of tools, licence, installed skills and hooks.
|
|
17
|
+
uninstall [dir]
|
|
18
|
+
Remove bravos from a repository (--global: from your user), keeping
|
|
19
|
+
anything you edited unless --force; --dry-run lists it first.
|
|
20
|
+
review <kind> Answer bravos's questions in a branded page (kind 'intent' from bravos-context),
|
|
21
|
+
pre-filled with bravos's suggestions: change what is wrong, make the calls marked
|
|
22
|
+
'needs your call', Confirm. Answers save to .guardlink/<kind>-answers/<name>.json;
|
|
23
|
+
--summary for the agent, --share for a claude.ai link (Claude Code).
|
|
24
|
+
next [dir] The setup score: what is switched on, what each step unlocks, the exact
|
|
25
|
+
command for the next one (also in the banner and the agent's briefing).
|
|
26
|
+
watch [dir] A live view for a second terminal pane beside any agent: setup, open and
|
|
27
|
+
confirmed exposures, the hypothesis ledger, what to test next, stale claims, the
|
|
28
|
+
code graph's resolution and entry points, SAST, the branch, PRs and autopilot
|
|
29
|
+
nudges, redrawn as files change. Read-only, no model. --once prints one frame,
|
|
30
|
+
--json one snapshot (bravos.watch/v1).
|
|
31
|
+
workspace <create|add|remove|target|show|sync>
|
|
32
|
+
One app made of several repos: create NAME --member PATH[=service[@url]]...
|
|
33
|
+
makes a bravos workspace, links the members with guardlink link-project,
|
|
34
|
+
installs bravos in each member and in the app folder they share, and keeps a
|
|
35
|
+
snapshot the briefing, watch (SERVICES panel, key s) and reports read. Calls
|
|
36
|
+
between services are written @flows #<this>-<x> -> #<other>-<y> via http.
|
|
37
|
+
report <data|check|export|list>
|
|
38
|
+
Evidence-bound write-ups for bravos-report: data gathers the facts into
|
|
39
|
+
.bravos/reports/<id>/evidence.json (not committed), check gates report.md,
|
|
40
|
+
export renders md, txt, html and pdf (pdf through an installed Chrome, Edge,
|
|
41
|
+
Brave or Chromium). No model.
|
|
42
|
+
sast <run|status|candidates>
|
|
43
|
+
run analyzes the repository with the staged bravos-cli into .bravos/sast/<run>/
|
|
44
|
+
(Team plan; not committed); status is one line about the newest run; candidates
|
|
45
|
+
lists its reachable findings joined to a claim, not yet in the threat model,
|
|
46
|
+
dependencies and secrets. No model.
|
|
47
|
+
setup Put the binaries bravos drives on this machine: the analyzer (SAST) and the
|
|
48
|
+
code graph are signed bundles fetched through the account portal (Team plan),
|
|
49
|
+
verified and staged under $BRAVOS_HOME; --tools adds guardlink and cxg (-y).
|
|
50
|
+
--check reports, --dry-run says what would happen.
|
|
51
|
+
graph The code graph (Team plan): graph build [DIR] [--scip MODE] indexes a repo;
|
|
52
|
+
graph <question> [args] --json asks it (the codegraph_ tools' answers);
|
|
53
|
+
graph --json lists every question this machine's binary answers.
|
|
54
|
+
licence Which plan this machine is on, where the licence is, what it unlocks
|
|
55
|
+
(--json: bravos.licence/v1). 'license' works too.
|
|
56
|
+
account <activate|import FILE|status>
|
|
57
|
+
activate: sign in once in the browser and get this machine its licence — a
|
|
58
|
+
30-day Team trial when the account has no plan, no card. import: install a
|
|
59
|
+
licence downloaded from account.bugb.io/dashboard/license.
|
|
60
|
+
guide [topic] How to use bravos: by role, by task, every skill, the safety rules.
|
|
61
|
+
Topics: start, roles, tasks, skills, states, safety, harnesses, all.
|
|
62
|
+
Run 'bravos <command> --help' for every flag and what it writes.
|
|
63
|
+
|
|
64
|
+
Plans
|
|
65
|
+
Free (researcher): the research loop in one repository — context, annotate, templatize,
|
|
66
|
+
env, prove, recheck, report, watch — with no licence at all.
|
|
67
|
+
Team (per developer; Solo is one seat): the pre-PR gate and autopilot, the CI and nightly
|
|
68
|
+
workflows, scheduled upkeep, multi-repository workspaces, campaigns, SAST and the code graph.
|
|
69
|
+
A paid command or flag without a licence says what it would do and how to start the trial;
|
|
70
|
+
a paid hook stands down with one line and never blocks. The licence is an offline-verified
|
|
71
|
+
token: BRAVOS_LICENCE, or licence.jwt in $BRAVOS_HOME (default ~/.bravos). Grace after
|
|
72
|
+
expiry is min(14 days, a quarter of the term). Seats are counted in the account portal.
|
|
73
|
+
|
|
74
|
+
The skills it installs (one folder each in .claude/skills/ and .agents/skills/)
|
|
75
|
+
bravos Start here: reads the briefing, explains the loop, routes to the rest.
|
|
76
|
+
bravos-annotate Extends the threat model in a budgeted slice (5 files) or deep rounds,
|
|
77
|
+
skipping files teammates have in open PRs; lint gate until 0 errors.
|
|
78
|
+
bravos-codegraph Answers reachability, attack-surface and blast-radius questions.
|
|
79
|
+
bravos-ship The pre-PR security gate; opens a PR only when you ask for one.
|
|
80
|
+
bravos-templatize Turns open exposures into validated cxg templates and retest kits.
|
|
81
|
+
bravos-env Brings up a target you own: loopback by default, reviewed scope.yaml,
|
|
82
|
+
your typed authorization, test accounts created on loopback only.
|
|
83
|
+
bravos-prove Runs the authorized test, reads cxg's verdicts, writes outcomes back
|
|
84
|
+
only through guardlink's hypothesis ledger.
|
|
85
|
+
bravos-recheck After a fix: re-verifies stale claims, replays retest kits.
|
|
86
|
+
bravos-sast Licensed static analysis; says plainly when it is unavailable.
|
|
87
|
+
bravos-schedule /loop while you work, the nightly workflow, or a cloud task you create.
|
|
88
|
+
bravos-cicd Writes and audits the security GitHub Actions workflows.
|
|
89
|
+
bravos-context Learns what the product intends: drafts answers from the code, asks you
|
|
90
|
+
a short interview, writes .guardlink/intent.md with invariants (INV-n)
|
|
91
|
+
and intended-by-design behaviour (INT-n) that every annotation is judged by.
|
|
92
|
+
bravos-campaign Reaches a goal in rounds ("coverage 100%", "test every exposure"):
|
|
93
|
+
disjoint batches, one subagent each, gate per batch, rest, repeat, stop.
|
|
94
|
+
bravos-report Writes the assessment, one finding, a retest or an executive brief from
|
|
95
|
+
bravos report data's evidence; every proof claim cites its evidence and its
|
|
96
|
+
level (reproduced / held / code review / suspected) is computed, not
|
|
97
|
+
asserted; bravos report check gates it; export to md, txt, html, pdf.
|
|
98
|
+
|
|
99
|
+
Intent-aware annotation
|
|
100
|
+
Threats are judged against .guardlink/intent.md, not only against code patterns. Business
|
|
101
|
+
invariants ("a user never reads another tenant's data") become threats beside the code that
|
|
102
|
+
should enforce them; each exposure is rated by impact x reach x ease with the reason written
|
|
103
|
+
down; something that looks dangerous but is intended gets an "intended?" audit for you to
|
|
104
|
+
answer, never an automatic acceptance. The briefing's Intent line says whether intent is
|
|
105
|
+
recorded, still draft, or missing.
|
|
106
|
+
|
|
107
|
+
Session start
|
|
108
|
+
One hook run returns two things. The briefing (counts, FIX NOW, what to test next, stale
|
|
109
|
+
claims, tickets, intent) goes to the model on every start, resume, clear and compact. On a
|
|
110
|
+
fresh start only, a banner goes to you and not to the model: the bravos logo, repo and
|
|
111
|
+
branch, counts, the top threats (or FIX NOW) beside a quick start chosen from the repo's
|
|
112
|
+
state, and a short introduction while .guardlink/intent.md is missing. Claude Code shows
|
|
113
|
+
it above the first prompt; Codex shows it as a hook message. BRAVOS_BANNER=off hides it;
|
|
114
|
+
the briefing still reaches the model. The banner uses bravos's brand colours (truecolor);
|
|
115
|
+
NO_COLOR or BRAVOS_BANNER_COLOR=off prints it plain. Codex lists a hook message as a
|
|
116
|
+
warning, so there it is a two-line summary instead of the art. Since 0.5.0 the hook is registered with
|
|
117
|
+
--harness claude or --harness codex so the banner prints /skill or $skill; sync migrates
|
|
118
|
+
the older entry, and Codex asks you to trust the changed hook once (/hooks).
|
|
119
|
+
omp runs no hooks, so when omp is in use (.omp/ exists or omp is on PATH) bravos writes
|
|
120
|
+
.omp/extensions/bravos.js: it shows the banner above the editor and hands the briefing to
|
|
121
|
+
the model with your first prompt. No command is needed in any of the three; /bravos is
|
|
122
|
+
for "what next". A harness with neither hooks nor extensions gets the briefing when the
|
|
123
|
+
bravos skill runs 'node .bravos/hooks/session-start.mjs --briefing'.
|
|
124
|
+
Antigravity (agy) reads .agents/hooks.json: bravos adds a 'bravos' SessionStart group
|
|
125
|
+
there that hands the briefing to the model (it documents no way to show a banner).
|
|
126
|
+
|
|
127
|
+
Autopilot (hands-off): bravos init --autopilot
|
|
128
|
+
For teams who would rather not ask. At the end of every turn a check (a Stop hook in
|
|
129
|
+
Claude Code and Codex, a few seconds) sends the agent back once when the turn's changes
|
|
130
|
+
leave a file with no GuardLink annotation, code under a confirmed or stale claim, or open
|
|
131
|
+
exposures on changed files, naming the skill to use; the pre-PR gate runs on every
|
|
132
|
+
gh pr create / gh pr ready. Each finding is raised once per session; a missing tool or a
|
|
133
|
+
timeout lets the turn finish; BRAVOS_AUTOPILOT=off pauses it.
|
|
134
|
+
Proving is the team's choice, not a rule: the first time it comes up the agent asks once
|
|
135
|
+
(prove automatically on your loopback target, ask each time, on a schedule, or not now;
|
|
136
|
+
model-API probes or not) and records your words in .guardlink/proving.md. The briefing
|
|
137
|
+
shows it; say "ask me first" to change it. Non-loopback targets still need a reviewed
|
|
138
|
+
scope and typed authorization every time.
|
|
139
|
+
|
|
140
|
+
Harnesses
|
|
141
|
+
Claude Code, Codex, Antigravity (agy), opencode and omp; --agent all installs for every
|
|
142
|
+
one. bravos init --global installs for your user (skills in ~/.claude/skills, a user hook
|
|
143
|
+
that briefs repos with a threat model but no bravos). bravos sync --link-skills keeps one
|
|
144
|
+
copy of each skill when a tool reads both .claude/skills and .agents/skills (Warp).
|
|
145
|
+
bravos uninstall [--global] removes it again.
|
|
146
|
+
|
|
147
|
+
Effort: low | medium | high | max
|
|
148
|
+
One dial every skill reads: from your words ("annotate at high"), else a line
|
|
149
|
+
'bravos effort: <level>' in AGENTS.md or CLAUDE.md, else medium (campaign: high).
|
|
150
|
+
annotate changed files | +5 from the worklist | 25 per round + chains | until done
|
|
151
|
+
templatize changed exposures | top 5 | top 20 | every open exposure
|
|
152
|
+
prove replay kits | +generation, 8 templates | 32 + chaining | every class, spend pre-approved
|
|
153
|
+
recheck stale claims | +confirmed on changed files | every confirmed | everything + gaps
|
|
154
|
+
ship deterministic gates | +code-graph gaps | +SAST | +recheck
|
|
155
|
+
These are starting budgets, not limits: the agent goes beyond one when the work needs it and
|
|
156
|
+
says why. bravos adds method and memory to the model; it never caps it. Only the safety rules
|
|
157
|
+
below are hard.
|
|
158
|
+
|
|
159
|
+
Campaign examples
|
|
160
|
+
Claude Code: /bravos-campaign coverage 100% at max, rest 10m
|
|
161
|
+
Codex: $bravos-campaign test every open exposure on loopback
|
|
162
|
+
Claude Code rests between rounds with its own wakeup or /loop; Codex has no in-session timer,
|
|
163
|
+
so rounds run back to back there, or the nightly workflow carries them.
|
|
164
|
+
|
|
165
|
+
The evidence loop
|
|
166
|
+
claim (guardlink annotation) -> testable template (cxg) -> authorized run -> verified
|
|
167
|
+
outcome (hypothesis ledger) -> recheck after every change. Three independent states per
|
|
168
|
+
claim: open / mitigated / accepted; untested / confirmed / refuted; verified / stale.
|
|
169
|
+
|
|
170
|
+
Rules every skill follows
|
|
171
|
+
Never spawns a second coding agent (no guardlink annotate/translate agent flags; cxg probe
|
|
172
|
+
generation only through its API providers). Never writes @accepts, @entitles or @confirmed
|
|
173
|
+
by hand. Never sends a request to a non-loopback host without a committed, reviewed
|
|
174
|
+
scope.yaml and the human's typed authorization. Destructive probe classes only on a
|
|
175
|
+
disposable loopback target. Never invents credentials. Never calls anything clean unless
|
|
176
|
+
a gate exited 0.
|
|
177
|
+
|
|
178
|
+
Typical first run
|
|
179
|
+
bravos doctor # what is installed, what is missing
|
|
180
|
+
bravos init --dry-run # preview every write
|
|
181
|
+
bravos init # install; then open a Claude Code or Codex session
|
|
182
|
+
bravos sync --ci --pre-pr-hook # later: add CI and the pre-PR gate
|
|
183
|
+
In Codex, run /hooks once to trust .codex/hooks.json.
|
|
184
|
+
|
|
185
|
+
Exit codes
|
|
186
|
+
0 success (files skipped to protect your edits are not failures)
|
|
187
|
+
1 refused (an unparsable config file; nothing in that file was written)
|
|
188
|
+
2 usage error
|
|
189
|
+
`;
|
|
190
|
+
const SHARED_WRITES = `
|
|
191
|
+
What it writes (repository-relative)
|
|
192
|
+
.claude/skills/bravos*/ Claude Code skills (when Claude Code is a harness)
|
|
193
|
+
.agents/skills/bravos*/ Codex and shared-agent skills (when codex is a harness)
|
|
194
|
+
.agents/skills/guardlink-*/ guardlink's playbook skills mirrored for Codex, if
|
|
195
|
+
guardlink wrote them under .claude/skills/
|
|
196
|
+
.bravos/hooks/ session-start briefing (and pre-PR gate) scripts
|
|
197
|
+
.bravos/skills.lock sha256 of every file bravos wrote
|
|
198
|
+
.claude/settings.json SessionStart hook, --harness claude (+ PreToolUse gate
|
|
199
|
+
with --pre-pr-hook)
|
|
200
|
+
.codex/hooks.json the same for Codex, --harness codex
|
|
201
|
+
.mcp.json, .codex/config.toml MCP servers: guardlink always; cxg when on PATH; codegraph-mcp
|
|
202
|
+
when on PATH or inside the bravos install (by full path, with
|
|
203
|
+
the launcher's graph store as BRAVOS_CODEGRAPH_DIR; sync
|
|
204
|
+
refreshes that entry when bravos is replaced)
|
|
205
|
+
.cursor/rules/bravos.mdc pointer to the skills, only if .cursor/ exists
|
|
206
|
+
.omp/extensions/bravos.js omp session start, only if .omp/ exists or omp is on PATH
|
|
207
|
+
Every merge keeps entries bravos did not write. A file it cannot parse is refused with
|
|
208
|
+
its path, and nothing is written to it.
|
|
209
|
+
|
|
210
|
+
Your edits are safe
|
|
211
|
+
A bravos file you edited (hash differs from the lock) or a file bravos did not write is
|
|
212
|
+
skipped and reported, with a 'git diff --no-index' hint. --force overwrites it.
|
|
213
|
+
If .agents/skills is a link to .claude/skills, skills are written once.
|
|
214
|
+
A written path your .gitignore ignores is reported, since it will not reach teammates.
|
|
215
|
+
|
|
216
|
+
Agents
|
|
217
|
+
Detected from the repository: CLAUDE.md or .claude/ means Claude Code; AGENTS.md,
|
|
218
|
+
GEMINI.md, .agents/ or .codex/ means Codex. No evidence means both. --agent overrides.
|
|
219
|
+
`;
|
|
220
|
+
const FLAG_DETAIL = `
|
|
221
|
+
Flags in depth
|
|
222
|
+
--agent claude,codex,agy,opencode,omp (or --agent all)
|
|
223
|
+
Install for these harnesses only, instead of detecting them. 'all' installs for
|
|
224
|
+
every one: Claude Code, Codex and Antigravity (both skill roots), opencode and omp. claude and codex
|
|
225
|
+
pick the skill roots (agy shares codex's .agents/ root). opencode and omp add
|
|
226
|
+
their own files on top (.opencode/ and opencode.json; .omp/extensions/), which
|
|
227
|
+
are otherwise written only when that tool is detected; once written, sync keeps
|
|
228
|
+
them up to date without the flag.
|
|
229
|
+
--autopilot
|
|
230
|
+
Upkeep without being asked. Registers an end-of-turn check (a Stop hook, Claude Code
|
|
231
|
+
and Codex) that sends the agent back once when the turn's changes leave code with no
|
|
232
|
+
GuardLink annotation, a confirmed or stale claim on a changed file, or open exposures
|
|
233
|
+
on changed files; and the pre-PR gate (implies --pre-pr-hook), which also lints. New
|
|
234
|
+
exposures are handled as you choose: the first time proving comes up the agent asks
|
|
235
|
+
you once (prove automatically on your loopback target, ask each time, on a schedule,
|
|
236
|
+
or not now; and whether cxg may generate probes through a model API) and records your
|
|
237
|
+
words in .guardlink/proving.md, which a PR that changes it must mention. It uses only
|
|
238
|
+
guardlink's fast calls (a few seconds); a missing tool or timeout lets the turn finish.
|
|
239
|
+
BRAVOS_AUTOPILOT=off pauses the check.
|
|
240
|
+
--link-skills
|
|
241
|
+
Make .agents/skills a link to .claude/skills, so each skill exists once. Tools that
|
|
242
|
+
read both folders (Warp, for one) otherwise list every skill twice. bravos removes
|
|
243
|
+
only its own unchanged copies; anything else in .agents/skills stops it, and nothing
|
|
244
|
+
is removed. Symlinks need core.symlinks on Windows checkouts.
|
|
245
|
+
--global
|
|
246
|
+
Install for your user instead of a repository: skills once in ~/.claude/skills
|
|
247
|
+
(absolute paths), a link per skill from ~/.agents/skills, the hook in ~/.bravos/hooks
|
|
248
|
+
registered in ~/.claude/settings.json and ~/.codex/hooks.json, guardlink MCP in
|
|
249
|
+
~/.codex/config.toml. The user hook stays silent in a repo that has its own bravos
|
|
250
|
+
hook or no .guardlink/, so it briefs only repos with a threat model and no bravos.
|
|
251
|
+
Personal skills are not in git: bravos init in a repo is still how a team shares it.
|
|
252
|
+
Claude Code prefers personal skills over a repo's, so once a user-level install
|
|
253
|
+
exists, bravos sync in any repo also brings it to the same version (it never creates
|
|
254
|
+
one). BRAVOS_SYNC_GLOBAL=off keeps them separate and only warns.
|
|
255
|
+
--force
|
|
256
|
+
Overwrite bravos files you edited, and same-named files bravos did not write.
|
|
257
|
+
--dry-run
|
|
258
|
+
Print every line of the summary without writing anything.
|
|
259
|
+
--ci
|
|
260
|
+
Copies three GitHub Actions workflows into .github/workflows/:
|
|
261
|
+
bravos-pr-gate.yml On every PR (opened, synchronize, reopened, edited):
|
|
262
|
+
guardlink validate, lint since the merge base, new
|
|
263
|
+
unmitigated exposure must be named in the PR body,
|
|
264
|
+
confirmed exploits on changed files block unless a
|
|
265
|
+
@ticket covers them, SARIF uploaded to code scanning.
|
|
266
|
+
A gate input that fails reads as unknown and blocks.
|
|
267
|
+
bravos-nightly-claude.yml Weekdays 03:17 UTC: Claude Code annotates the next slice
|
|
268
|
+
bravos-nightly-codex.yml (or Codex does), the lint gate checks it, one PR opens.
|
|
269
|
+
Keep the one for your harness and delete the other.
|
|
270
|
+
Opt-in scan step: replays retest kits only when
|
|
271
|
+
.guardlink/scope.yaml has a human-written
|
|
272
|
+
authorization_attestation and vars.BRAVOS_SCAN_TARGET is
|
|
273
|
+
loopback (started by vars.BRAVOS_APP_START) or in the
|
|
274
|
+
scope's host_allowlist; optional vars.BRAVOS_SCAN_AUTH +
|
|
275
|
+
secrets.BRAVOS_AUTH_STATE import a login. Otherwise it
|
|
276
|
+
skips with a notice.
|
|
277
|
+
Needs repository secret ANTHROPIC_API_KEY or OPENAI_API_KEY, and Settings > Actions >
|
|
278
|
+
"Allow GitHub Actions to create and approve pull requests". Workflows you wrote or
|
|
279
|
+
edited are skipped unless --force.
|
|
280
|
+
--pre-pr-hook
|
|
281
|
+
Registers the pre-PR gate as a PreToolUse hook on Bash for 'gh pr create' and
|
|
282
|
+
'gh pr ready' in .claude/settings.json and .codex/hooks.json, and lists
|
|
283
|
+
'node .bravos/hooks/pre-pr.mjs --check' in .guardlink/config.json pre_pr so guardlink's
|
|
284
|
+
agent block tells every agent to run it. Exit 2 blocks the PR on lint errors, undisclosed
|
|
285
|
+
new exposure, or an unticketed confirmed claim on a changed file. It allows (with a note)
|
|
286
|
+
only when guardlink is not installed or there is no merge base. BRAVOS_PRE_PR=off
|
|
287
|
+
disables it for one shell.
|
|
288
|
+
--scope
|
|
289
|
+
Copies the loopback-only cxg scope starter to .guardlink/scope.yaml if none exists:
|
|
290
|
+
GET/HEAD/OPTIONS/POST only, empty host_allowlist, cxg's nine destructive-path patterns,
|
|
291
|
+
and an empty authorization_attestation for you to write and get reviewed in a PR.
|
|
292
|
+
`;
|
|
293
|
+
export const INIT_HELP = `
|
|
294
|
+
What init does, in order
|
|
295
|
+
1. Detects the harnesses (see Agents below).
|
|
296
|
+
2. Runs 'guardlink init --agent <ids> --mode <inline|external>' if .guardlink/ is absent
|
|
297
|
+
(skipped with --no-guardlink-init; reported if guardlink is not installed).
|
|
298
|
+
3. Installs the skills, the hooks and the MCP wiring.
|
|
299
|
+
4. With --ci / --pre-pr-hook / --scope, adds those too.
|
|
300
|
+
5. Prints a summary, gitignore warnings, and the next step.
|
|
301
|
+
${SHARED_WRITES}${FLAG_DETAIL}
|
|
302
|
+
--guardlink-mode inline|external
|
|
303
|
+
Where guardlink keeps annotations: in source comments (default) or .gal sidecars.
|
|
304
|
+
--no-guardlink-init
|
|
305
|
+
Do not run guardlink init even if .guardlink/ is absent.
|
|
306
|
+
|
|
307
|
+
Examples
|
|
308
|
+
bravos init --dry-run
|
|
309
|
+
bravos init --agent claude
|
|
310
|
+
bravos init --ci --pre-pr-hook --scope
|
|
311
|
+
`;
|
|
312
|
+
export const SYNC_HELP = `
|
|
313
|
+
What sync does
|
|
314
|
+
Re-applies the installed bravos version: refreshes skills, hooks, MCP wiring and the
|
|
315
|
+
Cursor pointer, and (with the flags) workflows, the pre-PR gate and the scope starter.
|
|
316
|
+
It never runs guardlink init. Run it after upgrading bravos, or to add a flag later.
|
|
317
|
+
${SHARED_WRITES}${FLAG_DETAIL}
|
|
318
|
+
Examples
|
|
319
|
+
bravos sync
|
|
320
|
+
bravos sync --ci # add the PR gate and nightly workflows
|
|
321
|
+
bravos sync --agent codex --dry-run
|
|
322
|
+
`;
|
|
323
|
+
export const DOCTOR_HELP = `
|
|
324
|
+
What doctor reports (read-only; writes nothing, starts nothing)
|
|
325
|
+
Tools on PATH, with an install hint for each one missing:
|
|
326
|
+
guardlink npm install -g guardlink
|
|
327
|
+
cxg cargo install cert-x-gen (or brew tap bugb-technologies/cxg && brew install cxg)
|
|
328
|
+
codegraph-mcp bravos setup --codegraph (Team plan)
|
|
329
|
+
bravos-cli bravos setup --analyzer (Team plan); the licence is BRAVOS_LICENCE or
|
|
330
|
+
licence.jwt in $BRAVOS_HOME, ~/.bravos or ~/.bugb
|
|
331
|
+
The plan line (Free, Team trial, Team, expired or in grace), whether guardlink initialised the repo, bravos skills installed
|
|
332
|
+
per harness, guardlink playbook skills per harness, session-start hooks per harness,
|
|
333
|
+
and whether the pre-PR gate is registered.
|
|
334
|
+
`;
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
import { hooksDir as packageHooksDir } from './paths.js';
|
|
4
|
+
import { OLD_HOOK_COMMANDS, OLD_NAME, oldGlobalHookCommand } from './migrate.js';
|
|
5
|
+
import { BravosError } from './types.js';
|
|
6
|
+
// @comment -- "The SessionStart command before 0.5.0; init and sync replace it with the per-harness form, and doctor still accepts it."
|
|
7
|
+
export const LEGACY_HOOK_COMMAND = 'node .bravos/hooks/session-start.mjs';
|
|
8
|
+
// @comment -- "The SessionStart command bravos registers per harness; --harness tells the banner which skill syntax to print. doctor matches on the same constants."
|
|
9
|
+
export const HOOK_COMMANDS = {
|
|
10
|
+
claude: `${LEGACY_HOOK_COMMAND} --harness claude`,
|
|
11
|
+
agents: `${LEGACY_HOOK_COMMAND} --harness codex`,
|
|
12
|
+
};
|
|
13
|
+
// @comment -- "The command bravos registers as its opt-in PreToolUse pre-PR gate; doctor matches on the same constant."
|
|
14
|
+
export const PRE_PR_COMMAND = 'node .bravos/hooks/pre-pr.mjs';
|
|
15
|
+
// @comment -- "The SessionStart registration bravos writes into .claude/settings.json and .codex/hooks.json, per harness; the Codex plugin build reuses its shape."
|
|
16
|
+
// @comment -- "The user-level SessionStart command (bravos init --global): an absolute path into ~/.bravos/hooks, and --global so the hook stays silent in repos that have their own bravos hook or no threat model."
|
|
17
|
+
export const globalHookCommand = (home, harness) => `node "${home}/.bravos/hooks/session-start.mjs" --harness ${harness === 'claude' ? 'claude' : 'codex'} --global`;
|
|
18
|
+
export function sessionStart(harness, home) {
|
|
19
|
+
const command = home ? globalHookCommand(home, harness) : HOOK_COMMANDS[harness];
|
|
20
|
+
return {
|
|
21
|
+
event: 'SessionStart',
|
|
22
|
+
command,
|
|
23
|
+
// The pre-0.5.0 form, and every form the old name (joseph) registered, repo-level and user-level.
|
|
24
|
+
replaces: [
|
|
25
|
+
LEGACY_HOOK_COMMAND,
|
|
26
|
+
OLD_HOOK_COMMANDS.sessionStart,
|
|
27
|
+
...['claude', 'codex', 'agy'].map((h) => `${OLD_HOOK_COMMANDS.sessionStart} --harness ${h}`),
|
|
28
|
+
...(home ? ['claude', 'codex'].map((h) => oldGlobalHookCommand(home, h)) : []),
|
|
29
|
+
],
|
|
30
|
+
group: { matcher: 'startup|resume|clear|compact', hooks: [{ type: 'command', command, timeout: 30 }] },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// @comment -- "The command bravos registers as its opt-in autopilot Stop hook (Claude Code and Codex); doctor and uninstall match on it."
|
|
34
|
+
export const AUTOPILOT_COMMAND = 'node .bravos/hooks/autopilot.mjs';
|
|
35
|
+
const AUTOPILOT = {
|
|
36
|
+
event: 'Stop',
|
|
37
|
+
command: AUTOPILOT_COMMAND,
|
|
38
|
+
replaces: [OLD_HOOK_COMMANDS.autopilot],
|
|
39
|
+
group: { hooks: [{ type: 'command', command: AUTOPILOT_COMMAND, timeout: 30 }] },
|
|
40
|
+
};
|
|
41
|
+
// The hook script itself only acts on commands that run gh pr create or gh pr ready.
|
|
42
|
+
const PRE_PR = {
|
|
43
|
+
event: 'PreToolUse',
|
|
44
|
+
command: PRE_PR_COMMAND,
|
|
45
|
+
replaces: [OLD_HOOK_COMMANDS.prePr],
|
|
46
|
+
group: { matcher: 'Bash', hooks: [{ type: 'command', command: PRE_PR_COMMAND, timeout: 180 }] },
|
|
47
|
+
};
|
|
48
|
+
const SETTINGS_FILE = {
|
|
49
|
+
claude: '.claude/settings.json',
|
|
50
|
+
agents: '.codex/hooks.json',
|
|
51
|
+
};
|
|
52
|
+
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
53
|
+
function listFiles(dir) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
56
|
+
const abs = join(dir, e.name);
|
|
57
|
+
if (e.isDirectory())
|
|
58
|
+
out.push(...listFiles(abs));
|
|
59
|
+
else if (e.isFile())
|
|
60
|
+
out.push(abs);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
// @comment -- "Reads and validates one harness settings file and decides which of bravos's hook groups must be appended and which superseded bravos commands removed; writes nothing so every file can be validated before any write."
|
|
65
|
+
function planRegistration(root, rel, regs) {
|
|
66
|
+
const abs = join(root, rel);
|
|
67
|
+
let doc = {};
|
|
68
|
+
if (existsSync(abs)) {
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(readFileSync(abs, 'utf8'));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new BravosError(`${rel} is not valid JSON; fix it or remove it, nothing was written`);
|
|
75
|
+
}
|
|
76
|
+
if (!isObject(parsed)) {
|
|
77
|
+
throw new BravosError(`${rel} is not a JSON object; fix it or remove it, nothing was written`);
|
|
78
|
+
}
|
|
79
|
+
doc = parsed;
|
|
80
|
+
}
|
|
81
|
+
if (doc.hooks !== undefined && !isObject(doc.hooks)) {
|
|
82
|
+
throw new BravosError(`${rel} has a hooks value that is not an object; fix it or remove it, nothing was written`);
|
|
83
|
+
}
|
|
84
|
+
const hooks = (doc.hooks ?? {});
|
|
85
|
+
let changed = false;
|
|
86
|
+
for (const reg of regs) {
|
|
87
|
+
if (hooks[reg.event] !== undefined && !Array.isArray(hooks[reg.event])) {
|
|
88
|
+
throw new BravosError(`${rel} has a hooks.${reg.event} value that is not an array; fix it or remove it, nothing was written`);
|
|
89
|
+
}
|
|
90
|
+
let groups = (hooks[reg.event] ?? []);
|
|
91
|
+
const old = new Set(reg.replaces ?? []);
|
|
92
|
+
if (old.size > 0) {
|
|
93
|
+
// Drop superseded bravos commands from every group, and groups left empty by that.
|
|
94
|
+
const before = JSON.stringify(groups);
|
|
95
|
+
groups = groups
|
|
96
|
+
.map((g) => (isObject(g) && Array.isArray(g.hooks) ? { ...g, hooks: g.hooks.filter((h) => !(isObject(h) && typeof h.command === 'string' && old.has(h.command))) } : g))
|
|
97
|
+
.filter((g) => !(isObject(g) && Array.isArray(g.hooks) && g.hooks.length === 0));
|
|
98
|
+
if (JSON.stringify(groups) !== before) {
|
|
99
|
+
hooks[reg.event] = groups;
|
|
100
|
+
changed = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const present = groups.some((g) => isObject(g) && Array.isArray(g.hooks) && g.hooks.some((h) => isObject(h) && h.command === reg.command));
|
|
104
|
+
if (present)
|
|
105
|
+
continue;
|
|
106
|
+
hooks[reg.event] = [...groups, reg.group];
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
if (changed)
|
|
110
|
+
doc.hooks = hooks;
|
|
111
|
+
return { file: rel, changed, doc };
|
|
112
|
+
}
|
|
113
|
+
// @comment -- "The command Antigravity runs from .agents/hooks.json; --harness agy makes the hook print Antigravity's injectSteps."
|
|
114
|
+
export const AGY_HOOK_COMMAND = `${LEGACY_HOOK_COMMAND} --harness agy`;
|
|
115
|
+
export const AGY_HOOKS_FILE = '.agents/hooks.json';
|
|
116
|
+
const AGY_GROUP = { SessionStart: [{ type: 'command', command: AGY_HOOK_COMMAND, timeout: 30 }] };
|
|
117
|
+
// @comment -- "Plans bravos's entry in Antigravity's workspace hooks file, whose top-level keys are named hook groups; only the 'bravos' key is bravos's, every other key is kept, and a malformed file is refused before anything is written."
|
|
118
|
+
function planAgy(root) {
|
|
119
|
+
const abs = join(root, AGY_HOOKS_FILE);
|
|
120
|
+
let doc = {};
|
|
121
|
+
if (existsSync(abs)) {
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(readFileSync(abs, 'utf8'));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new BravosError(`${AGY_HOOKS_FILE} is not valid JSON; fix it or remove it, nothing was written`);
|
|
128
|
+
}
|
|
129
|
+
if (!isObject(parsed))
|
|
130
|
+
throw new BravosError(`${AGY_HOOKS_FILE} is not a JSON object; fix it or remove it, nothing was written`);
|
|
131
|
+
doc = parsed;
|
|
132
|
+
}
|
|
133
|
+
const hadOld = OLD_NAME in doc;
|
|
134
|
+
if (!hadOld && JSON.stringify(doc.bravos) === JSON.stringify(AGY_GROUP))
|
|
135
|
+
return { file: AGY_HOOKS_FILE, changed: false, doc };
|
|
136
|
+
const { [OLD_NAME]: _old, ...rest } = doc;
|
|
137
|
+
return { file: AGY_HOOKS_FILE, changed: true, doc: { ...rest, bravos: AGY_GROUP } };
|
|
138
|
+
}
|
|
139
|
+
// @comment -- "Copies the hook scripts into <root>/.bravos/hooks and registers the SessionStart briefing, plus the PreToolUse pre-PR gate when prePr is set, for Claude Code (.claude/settings.json), Codex (.codex/hooks.json) and Antigravity (.agents/hooks.json); settings files are validated before anything is written and existing keys and groups are preserved."
|
|
140
|
+
export function installHook(root, harnesses, opts = {}) {
|
|
141
|
+
const src = opts.hooksDir ?? packageHooksDir();
|
|
142
|
+
const files = listFiles(src).map((abs) => relative(src, abs).split('\\').join('/')).sort();
|
|
143
|
+
const copied = files.map((f) => `.bravos/hooks/${f}`);
|
|
144
|
+
const plans = [...new Set(harnesses)].map((h) => planRegistration(root, SETTINGS_FILE[h], [
|
|
145
|
+
sessionStart(h, opts.globalHome),
|
|
146
|
+
...((opts.prePr || opts.autopilot) && !opts.globalHome ? [PRE_PR] : []),
|
|
147
|
+
...(opts.autopilot && !opts.globalHome ? [AUTOPILOT] : []),
|
|
148
|
+
]));
|
|
149
|
+
// Antigravity reads .agents/skills and .agents/hooks.json, so it rides on the shared-agents harness (per repo only).
|
|
150
|
+
if (harnesses.includes('agents') && !opts.globalHome)
|
|
151
|
+
plans.push(planAgy(root));
|
|
152
|
+
if (!opts.dryRun) {
|
|
153
|
+
for (const f of files) {
|
|
154
|
+
const dest = join(root, '.bravos/hooks', f);
|
|
155
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
156
|
+
copyFileSync(join(src, f), dest);
|
|
157
|
+
}
|
|
158
|
+
for (const p of plans) {
|
|
159
|
+
if (!p.changed)
|
|
160
|
+
continue;
|
|
161
|
+
const abs = join(root, p.file);
|
|
162
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
163
|
+
writeFileSync(abs, JSON.stringify(p.doc, null, 2) + '\n');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { copied, registered: plans.map((p) => ({ file: p.file, changed: p.changed })) };
|
|
167
|
+
}
|