@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
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
+ }