@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.
- 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 +133 -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/graphbin.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { whichBin } from './guardlink.js';
|
|
4
|
+
import { packageRoot } from './paths.js';
|
|
5
|
+
/** The names the Python bravos launcher may have on PATH: its own, or `bravos-py` once it is renamed so `bravos` means this CLI. */
|
|
6
|
+
export const PYTHON_BRAVOS_NAMES = ['bravos', 'bravos-py'];
|
|
7
|
+
// @comment -- "The Python bravos CLI (siete) on PATH, under its own name or renamed to bravos-py, and never this package's own `bravos` bin: every candidate is resolved to its real file and skipped when it lives inside this package or is an npm shim onto dist/cli.js, so the fallbacks to the Python install's code graph and analyzer cannot spawn this CLI back into itself. `bravos setup` staging the bundles makes this lookup unnecessary."
|
|
8
|
+
export function pythonBravos(env = process.env) {
|
|
9
|
+
const self = packageRoot();
|
|
10
|
+
for (const name of PYTHON_BRAVOS_NAMES) {
|
|
11
|
+
for (const dir of (env.PATH ?? '').split(':')) {
|
|
12
|
+
if (!dir)
|
|
13
|
+
continue;
|
|
14
|
+
const file = join(dir, name);
|
|
15
|
+
try {
|
|
16
|
+
if (!statSync(file).isFile())
|
|
17
|
+
continue;
|
|
18
|
+
accessSync(file, constants.X_OK);
|
|
19
|
+
const real = realpathSync(file);
|
|
20
|
+
if (real.startsWith(self + '/'))
|
|
21
|
+
continue;
|
|
22
|
+
// An npm shim (a shell script that execs node on this package's dist/cli.js) is not the Python CLI either.
|
|
23
|
+
if (readFileSync(real, 'utf8').includes('dist/cli.js'))
|
|
24
|
+
continue;
|
|
25
|
+
return file;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// not here
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
// @comment -- "Finds a code-graph binary (codegraph-mcp, codegraph-build) the way bravos resolves it: on PATH first, else inside the bravos install's own wheel payload (<venv>/lib/python*/site-packages/bravos/_graph/bin/<name>), found from the real path of the bravos launcher. Read-only; null when neither has it."
|
|
35
|
+
export function graphBin(name, env = process.env) {
|
|
36
|
+
const onPath = whichBin(name, env);
|
|
37
|
+
if (onPath)
|
|
38
|
+
return onPath;
|
|
39
|
+
const launcher = pythonBravos(env);
|
|
40
|
+
if (!launcher)
|
|
41
|
+
return null;
|
|
42
|
+
let real;
|
|
43
|
+
try {
|
|
44
|
+
real = realpathSync(launcher);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
// <install>/bin/bravos → <install>, then the venv layouts siete's installer and pipx use.
|
|
50
|
+
const install = dirname(dirname(real));
|
|
51
|
+
for (const lib of [join(install, 'venv', 'lib'), join(install, 'lib')]) {
|
|
52
|
+
let pythons = [];
|
|
53
|
+
try {
|
|
54
|
+
pythons = readdirSync(lib).filter((d) => d.startsWith('python'));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
for (const py of pythons.sort().reverse()) {
|
|
60
|
+
const file = join(lib, py, 'site-packages', 'bravos', '_graph', 'bin', name);
|
|
61
|
+
try {
|
|
62
|
+
if (existsSync(file) && statSync(file).isFile())
|
|
63
|
+
return file;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* keep looking */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// @comment -- "Finds bravos-cli the way the bravos launcher puts it on its own PATH: on PATH first, else <install>/analyzer/bravos-cli beside the real path of the bravos launcher (<install>/bin/bravos). Read-only; null when neither has it. hooks/lib/sast.mjs carries the same lookup, dependency-free, for the hooks."
|
|
73
|
+
export function findAnalyzer(env = process.env) {
|
|
74
|
+
// The bundle bravos setup staged wins; then PATH; then the Python install's own copy.
|
|
75
|
+
const home = env.BRAVOS_HOME || (env.HOME ? join(env.HOME, '.bravos') : null);
|
|
76
|
+
if (home) {
|
|
77
|
+
const stagedBin = join(home, 'analyzer', 'current', 'bravos-cli');
|
|
78
|
+
try {
|
|
79
|
+
accessSync(stagedBin, constants.X_OK);
|
|
80
|
+
return realpathSync(stagedBin);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* not staged */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const direct = whichBin('bravos-cli', env);
|
|
87
|
+
if (direct)
|
|
88
|
+
return direct;
|
|
89
|
+
const launcher = pythonBravos(env);
|
|
90
|
+
if (!launcher)
|
|
91
|
+
return null;
|
|
92
|
+
try {
|
|
93
|
+
const file = join(dirname(dirname(realpathSync(launcher))), 'analyzer', 'bravos-cli');
|
|
94
|
+
accessSync(file, constants.X_OK);
|
|
95
|
+
return file;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// @comment -- "Where a Bravos licence is, read-only: BRAVOS_LICENCE / BRAVOS_LICENSE, else licence.jwt or license.jwt in $BRAVOS_HOME, ~/.bravos, or ~/.bugb (where bravos still keeps its state until ~/.bravos holds some). Returns a short label for the source, or null. Same rule as hooks/lib/sast.mjs findLicence."
|
|
102
|
+
export function findLicence(env = process.env) {
|
|
103
|
+
if (env.BRAVOS_LICENCE)
|
|
104
|
+
return 'BRAVOS_LICENCE';
|
|
105
|
+
if (env.BRAVOS_LICENSE)
|
|
106
|
+
return 'BRAVOS_LICENSE';
|
|
107
|
+
const home = env.HOME;
|
|
108
|
+
const homes = [env.BRAVOS_HOME, home && join(home, '.bravos'), home && join(home, '.bugb')].filter((h) => !!h);
|
|
109
|
+
for (const dir of homes) {
|
|
110
|
+
for (const name of ['licence.jwt', 'license.jwt']) {
|
|
111
|
+
if (existsSync(join(dir, name)))
|
|
112
|
+
return dir === env.BRAVOS_HOME ? `$BRAVOS_HOME/${name}` : `${home ? dir.replace(home, '~') : dir}/${name}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
// @comment -- "The graph store bravos reads and writes, for the codegraph-mcp entry bravos registers: BRAVOS_CODEGRAPH_DIR when bravos's environment sets it, else the default a bravos launcher script assigns (`: \"${BRAVOS_CODEGRAPH_DIR:=<dir>}\"`, read from the launcher's real path, first 8 KiB, never executed). Null when neither says, which leaves codegraph-mcp on its own default."
|
|
118
|
+
export function graphStoreDir(env = process.env) {
|
|
119
|
+
if (env.BRAVOS_CODEGRAPH_DIR)
|
|
120
|
+
return env.BRAVOS_CODEGRAPH_DIR;
|
|
121
|
+
const launcher = pythonBravos(env);
|
|
122
|
+
if (!launcher)
|
|
123
|
+
return null;
|
|
124
|
+
try {
|
|
125
|
+
const head = readFileSync(realpathSync(launcher)).subarray(0, 8192).toString('utf8');
|
|
126
|
+
if (!head.startsWith('#!'))
|
|
127
|
+
return null;
|
|
128
|
+
const m = /\$\{BRAVOS_CODEGRAPH_DIR:=([^}"'\n]+)\}/.exec(head);
|
|
129
|
+
return m ? m[1].trim() : null;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** True for a codegraph-mcp entry bravos registers by full path into a bravos install, which goes stale when that install is replaced. */
|
|
136
|
+
export const isInstallGraphCommand = (command) => typeof command === 'string' && /[\\/]bravos[\\/]_graph[\\/]bin[\\/]codegraph-mcp$/.test(command);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { accessSync, constants, existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { delimiter, join } from 'node:path';
|
|
4
|
+
import { readLock, sha256, writeLock } from './lock.js';
|
|
5
|
+
import { BravosError } from './types.js';
|
|
6
|
+
const AGENT_ID = { claude: 'claude', agents: 'codex' };
|
|
7
|
+
/** Full path of the first executable regular file `name` on `env.PATH`, or null. */
|
|
8
|
+
// @comment -- "Resolves a command name to the first executable regular file on env.PATH without spawning anything; null when absent."
|
|
9
|
+
export function whichBin(name, env = process.env) {
|
|
10
|
+
for (const dir of (env.PATH ?? '').split(delimiter)) {
|
|
11
|
+
if (!dir)
|
|
12
|
+
continue;
|
|
13
|
+
const file = join(dir, name);
|
|
14
|
+
try {
|
|
15
|
+
if (!statSync(file).isFile())
|
|
16
|
+
continue;
|
|
17
|
+
accessSync(file, constants.X_OK);
|
|
18
|
+
return file;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// not here
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/** Runs `guardlink init` (argv array, no shell) only when `<root>/.guardlink` is absent. */
|
|
27
|
+
// @comment -- "Runs guardlink init (argv array, no shell) only when <root>/.guardlink is absent, and reports what it did or would do with the argv it used."
|
|
28
|
+
export function ensureGuardlink(root, harnesses, opts = {}) {
|
|
29
|
+
if (existsSync(join(root, '.guardlink')))
|
|
30
|
+
return { status: 'present' };
|
|
31
|
+
if (opts.skip)
|
|
32
|
+
return { status: 'skipped' };
|
|
33
|
+
const env = opts.env ?? process.env;
|
|
34
|
+
const command = [
|
|
35
|
+
'guardlink', 'init',
|
|
36
|
+
'--agent', harnesses.map((h) => AGENT_ID[h]).join(','),
|
|
37
|
+
'--mode', opts.mode ?? 'inline',
|
|
38
|
+
];
|
|
39
|
+
if (opts.dryRun)
|
|
40
|
+
return { status: 'dry-run', command };
|
|
41
|
+
if (!whichBin('guardlink', env))
|
|
42
|
+
return { status: 'guardlink-missing', command };
|
|
43
|
+
try {
|
|
44
|
+
execFileSync(command[0], command.slice(1), { cwd: root, stdio: 'inherit', env });
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const code = err.status;
|
|
48
|
+
throw new BravosError(`\`${command.join(' ')}\` failed with exit code ${code ?? 'unknown'}`);
|
|
49
|
+
}
|
|
50
|
+
return { status: 'initialised', command };
|
|
51
|
+
}
|
|
52
|
+
/** Sorted guardlink-annotate-* / guardlink-report-* / guardlink-translate skill folders under `<root>/<skillsRoot>` that hold a SKILL.md. */
|
|
53
|
+
// @comment -- "Lists the guardlink-annotate-*, guardlink-report-* and guardlink-translate skill folders (with a SKILL.md) under a skills root, sorted; read-only. guardlink-translate is included so Codex, which reads .agents/skills, gets the translate method bravos-templatize follows first."
|
|
54
|
+
export function installedGuardlinkSkills(root, skillsRoot) {
|
|
55
|
+
const dir = join(root, skillsRoot);
|
|
56
|
+
if (!existsSync(dir))
|
|
57
|
+
return [];
|
|
58
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
59
|
+
.filter((e) => e.isDirectory() && /^guardlink-(?:(?:annotate|report)-|translate$)/.test(e.name))
|
|
60
|
+
.filter((e) => existsSync(join(dir, e.name, 'SKILL.md')))
|
|
61
|
+
.map((e) => e.name)
|
|
62
|
+
.sort();
|
|
63
|
+
}
|
|
64
|
+
// @comment -- "Repo-relative path of the cxg scope file bravos's testing-loop skills read, and the bundled starter it is copied from."
|
|
65
|
+
export const SCOPE_PATH = '.guardlink/scope.yaml';
|
|
66
|
+
export const SCOPE_SOURCE = 'bravos-env/scope.example.yaml';
|
|
67
|
+
/** Copies the bundled loopback-only scope starter to `.guardlink/scope.yaml` when that file is absent; never overwrites. */
|
|
68
|
+
// @comment -- "Writes the cxg scope starter only when .guardlink/scope.yaml is absent and .guardlink/ exists (creating .guardlink/ here would make a later bravos init skip guardlink init); never overwrites a scope the team may have reviewed, and records the hash in the lock."
|
|
69
|
+
export function writeScope(root, skillsDir, opts = {}) {
|
|
70
|
+
const abs = join(root, SCOPE_PATH);
|
|
71
|
+
if (existsSync(abs))
|
|
72
|
+
return { status: 'present', path: SCOPE_PATH };
|
|
73
|
+
if (!existsSync(join(root, '.guardlink')) && !(opts.dryRun && opts.guardlinkPending))
|
|
74
|
+
return { status: 'no-guardlink', path: SCOPE_PATH };
|
|
75
|
+
const src = join(skillsDir, SCOPE_SOURCE);
|
|
76
|
+
if (!existsSync(src))
|
|
77
|
+
return { status: 'no-source', path: SCOPE_PATH };
|
|
78
|
+
if (!opts.dryRun) {
|
|
79
|
+
const content = readFileSync(src);
|
|
80
|
+
writeFileSync(abs, content);
|
|
81
|
+
const lock = readLock(root);
|
|
82
|
+
writeLock(root, { ...lock, files: { ...lock.files, [SCOPE_PATH]: sha256(content) } });
|
|
83
|
+
}
|
|
84
|
+
return { status: 'written', path: SCOPE_PATH };
|
|
85
|
+
}
|
|
86
|
+
// @comment -- "The pre_pr entry bravos adds to .guardlink/config.json; guardlink renders up to five such entries into its stamped agent block and refuses backticks, line breaks and HTML comment delimiters, which these strings avoid."
|
|
87
|
+
export const PRE_PR_ENTRY = Object.freeze({
|
|
88
|
+
command: 'node .bravos/hooks/pre-pr.mjs --check',
|
|
89
|
+
why: 'bravos pre-PR gate: blocks on lint errors and confirmed claims on changed files, lists new exposures to disclose in the PR body',
|
|
90
|
+
});
|
|
91
|
+
const CONFIG_PATH = '.guardlink/config.json';
|
|
92
|
+
/** Adds bravos's gate to `pre_pr` in `.guardlink/config.json`, creating the array if absent and never duplicating it. */
|
|
93
|
+
// @comment -- "Read-validate-write of .guardlink/config.json: an unparsable file, a non-object or a non-array pre_pr is refused with nothing written; an entry with the same command counts as registered, and the pre---check entry is replaced; a missing config is reported, not created, since guardlink init owns it."
|
|
94
|
+
export function registerPrePr(root, opts = {}) {
|
|
95
|
+
const abs = join(root, CONFIG_PATH);
|
|
96
|
+
if (!existsSync(abs))
|
|
97
|
+
return { status: 'no-config', path: CONFIG_PATH };
|
|
98
|
+
let doc;
|
|
99
|
+
try {
|
|
100
|
+
doc = JSON.parse(readFileSync(abs, 'utf8'));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw new BravosError(`${CONFIG_PATH} is not valid JSON; fix it or remove it, nothing was written`);
|
|
104
|
+
}
|
|
105
|
+
if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
|
|
106
|
+
throw new BravosError(`${CONFIG_PATH} is not a JSON object; fix it or remove it, nothing was written`);
|
|
107
|
+
}
|
|
108
|
+
const config = doc;
|
|
109
|
+
if (config.pre_pr !== undefined && !Array.isArray(config.pre_pr)) {
|
|
110
|
+
throw new BravosError(`${CONFIG_PATH} has a pre_pr value that is not an array; fix it or remove it, nothing was written`);
|
|
111
|
+
}
|
|
112
|
+
const entries = (config.pre_pr ?? []);
|
|
113
|
+
// The entry bravos registered before --check existed ran the PreToolUse entry with no payload, a silent no-op: it is replaced.
|
|
114
|
+
const LEGACY = 'node .bravos/hooks/pre-pr.mjs';
|
|
115
|
+
const commandOf = (e) => (typeof e === 'object' && e !== null ? e.command : undefined);
|
|
116
|
+
const hasLegacy = entries.some((e) => commandOf(e) === LEGACY);
|
|
117
|
+
if (!hasLegacy && entries.some((e) => commandOf(e) === PRE_PR_ENTRY.command))
|
|
118
|
+
return { status: 'already-registered', path: CONFIG_PATH };
|
|
119
|
+
config.pre_pr = [...entries.filter((e) => commandOf(e) !== LEGACY && commandOf(e) !== PRE_PR_ENTRY.command), { ...PRE_PR_ENTRY }];
|
|
120
|
+
if (!opts.dryRun)
|
|
121
|
+
writeFileSync(abs, JSON.stringify(config, null, 2) + '\n');
|
|
122
|
+
return { status: 'registered', path: CONFIG_PATH };
|
|
123
|
+
}
|
package/dist/guide.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// @comment -- "The bravos guide: what bravos does, what to do by role and by task, every skill, the three claim states, the safety rules, and how each harness starts it. One source rendered as terminal text (bravos guide) or Markdown (bravos guide --markdown, committed as docs/GUIDE.md). Pure data and string formatting; reads nothing."
|
|
2
|
+
export const GUIDE_TOPICS = [
|
|
3
|
+
{
|
|
4
|
+
id: 'start',
|
|
5
|
+
title: 'What bravos is and how a session starts',
|
|
6
|
+
summary: 'what bravos does, what you see at start, how to run a skill in each harness',
|
|
7
|
+
intro: [
|
|
8
|
+
'bravos is a skills pack for coding agents. It makes the agent in your session a power user of guardlink (the threat model, written as annotations beside the code), cxg (pentests as code, against targets you own), the code graph and licensed SAST. There is no bravos server and no bravos database: the threat model lives in your repo, the evidence comes from the real tools, and your team shares it through git.',
|
|
9
|
+
'Every claim moves through one loop: an annotation says code is exposed (or mitigated) → it becomes a cxg template → an authorized run tests it against a target you own → the result is recorded in guardlink → it is rechecked when the code changes.',
|
|
10
|
+
],
|
|
11
|
+
items: [
|
|
12
|
+
{
|
|
13
|
+
name: 'At session start',
|
|
14
|
+
body: [
|
|
15
|
+
'A hook gathers the repo\'s security picture (open exposures, confirmed exploits, what to test next, stale claims, tickets, teammates\' open PRs, intent) and hands it to the model, so you never need a command to load it. On a fresh start you also see the banner; the model does not.',
|
|
16
|
+
'Claude Code shows the banner above the first prompt. Codex lists a two-line summary as a hook warning (F2 to view). omp shows the banner above the editor until your first prompt. opencode gets the briefing for the model only. Antigravity (agy) gets the briefing for the model only. Anywhere else, the bravos skill loads the briefing when you run it.',
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'Running a skill',
|
|
21
|
+
body: [
|
|
22
|
+
'Claude Code and opencode: /bravos-ship. Codex: $bravos-ship. omp: /skill:bravos-ship. Elsewhere, ask for it by name ("use the bravos-ship skill"). You can also just say what you want ("is this PR safe to open?") and the agent picks the skill.',
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'Plans',
|
|
27
|
+
body: [
|
|
28
|
+
'Free (researcher) needs no licence: context, annotate, templatize, env, prove, recheck, report and watch, in one repository. Team, licensed per developer (Solo is one seat), adds the pre-PR gate and autopilot, the CI and nightly workflows, scheduled upkeep, multi-repository workspaces, campaigns, SAST and the code graph. The briefing opens every session with a Plan: line; bravos licence shows the same in a shell. bravos account activate signs you in once in the browser and writes the licence (a 30-day Team trial, no card, when the account has no plan); bravos account import <file> installs a downloaded one. Without a licence a paid command explains what it would do and how to start the trial, a paid hook stands down with one line and never blocks, and a paid skill stops at its first step and says so.',
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Effort',
|
|
33
|
+
body: [
|
|
34
|
+
'Every skill reads one dial: low, medium (default), high or max. Say it ("annotate at high") or put a line `bravos effort: high` in AGENTS.md or CLAUDE.md. These are starting budgets, not limits: the agent goes further when the work needs it and says why. Only the safety rules are hard.',
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'Installing',
|
|
39
|
+
body: [
|
|
40
|
+
'npm install -g @bugb-technologies/bravos (Node 20 or newer). Then bravos account activate signs you in once in the browser and writes the licence — a 30-day Team trial when the account has no plan; skip it to stay on Free — bravos init in a repository writes the skills, hooks and MCP wiring, and bravos setup puts the analyzer and the code graph on the machine (Team plan). guardlink and cxg come from bravos setup --tools -y, or npm and cargo by hand. If you used the Python bravos, rename its command so bravos means this one: mv ~/.local/bin/bravos ~/.local/bin/bravos-py (this CLI still finds its code graph and analyzer), or remove it with pipx or uv.',
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Your first day',
|
|
45
|
+
body: [
|
|
46
|
+
'Run bravos next in a shell (free): it shows which of the nine steps are done here, in the order they build on each other — the code graph first (a machine step every later skill reads), then what the app intends, annotation coverage, SAST (which reads the annotations), how findings are proven and a test target, then the gates (autopilot, the pre-PR gate, CI, cheap to switch on any time) — what each unlocks, and the exact command for the next one. The banner shows the next step too, and every harness gives it to the agent, which offers a few starts the first time and suggests the next step at natural moments.',
|
|
47
|
+
'Or just ask the agent "help me get started" (or run /bravos, $bravos in Codex): it gives a short tour fitted to this repo. BRAVOS_COACH=off stops the coaching; the Setup line stays.',
|
|
48
|
+
'Keep bravos watch running in a second terminal pane beside your agent (free, read-only, no model): the setup score, open and confirmed exposures, what to test next, stale claims and autopilot nudges, redrawn within seconds of a change. It also shows the hypothesis ledger, boundaries, the model\'s size, the code graph\'s call resolution and entry points, and the branch\'s commits. Keys: n copies the next command, r opens the intent review, d guardlink\'s dashboard, g asks the code graph, t shows test-next, i shows the recorded intent, u refreshes now, j and k scroll, q quits. Three columns from 150 wide, two from 110, one in an 80-column split.',
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Hands-off: autopilot',
|
|
53
|
+
body: [
|
|
54
|
+
'bravos init --autopilot (or sync --autopilot) makes upkeep happen without anyone asking. At the end of each turn a check sends the agent back once if the changes left code with no annotation, touched code under a confirmed or stale claim, or put open exposures on changed files. The pre-PR gate runs on every gh pr create.',
|
|
55
|
+
'Proving follows your choice, not a fixed rule: the first time it comes up the agent asks you once — prove automatically on your loopback target, ask each time, on a schedule, or not now — and records your words in .guardlink/proving.md. Say "ask me first" or "stop auto-proving" at any time to change it. Non-loopback targets still need a reviewed scope and your typed authorization every time.',
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'Free views (no tokens)',
|
|
60
|
+
body: [
|
|
61
|
+
'In a shell: `bravos guide` (this guide), `bravos doctor` (which tools are installed), `guardlink status .`, `guardlink ci -f json`, `guardlink hypothesis next`. None of these runs a model.',
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'roles',
|
|
68
|
+
title: 'What to do, by role',
|
|
69
|
+
summary: 'developer, security engineer, team lead, new to the repo, CI owner',
|
|
70
|
+
items: [
|
|
71
|
+
{
|
|
72
|
+
name: 'Developer',
|
|
73
|
+
skills: ['bravos', 'bravos-annotate', 'bravos-ship'],
|
|
74
|
+
body: [
|
|
75
|
+
'Work as usual; the briefing tells the agent what is open around your code. When you write or change an endpoint, an auth check, a query, file or process handling, crypto or config parsing, ask the agent to annotate it (bravos-annotate) so the threat model keeps up. Before you open a pull request, run bravos-ship: it validates the model, lints your new claims, lists new exposure the PR must disclose, and refuses to ship a confirmed exploit on a changed path. After you fix a finding, run bravos-recheck.',
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'Security engineer',
|
|
80
|
+
skills: ['bravos-context', 'bravos-templatize', 'bravos-env', 'bravos-prove', 'bravos-recheck'],
|
|
81
|
+
body: [
|
|
82
|
+
'Start by recording what the product intends (bravos-context), so severities reflect impact on this product, not code patterns. Then turn the top untested exposures into cxg templates (bravos-templatize), bring up a target you own with a reviewed scope and your typed authorization (bravos-env), and run the test (bravos-prove). Outcomes go back into guardlink\'s hypothesis ledger as confirmed or refuted, with the evidence. After a fix, bravos-recheck replays the retest kit. bravos-sast adds licensed static analysis joined to the threat model.',
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'Team lead or reviewer',
|
|
87
|
+
skills: ['bravos-campaign', 'bravos-schedule', 'bravos-cicd'],
|
|
88
|
+
body: [
|
|
89
|
+
'Turn on bravos init --autopilot so the threat model keeps up as people work, with no commands to remember, and settle once with the team how findings should be proven (it is recorded in .guardlink/proving.md and shows in every briefing).',
|
|
90
|
+
'Coverage and findings are visible to everyone from the repo itself. To raise coverage or clear the backlog, run bravos-campaign with a goal ("coverage 100%", "test every open exposure"): it plans disjoint batches, runs one subagent per batch, gates each, rests, and reports before-and-after numbers. Use bravos-schedule for recurring upkeep (an in-session loop or the nightly workflow) and bravos-cicd for the PR gate and code-scanning upload. Acceptances (@accepts) and entitlements are yours to write; agents never write them.',
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'New to the repo',
|
|
95
|
+
skills: ['bravos', 'bravos-context', 'bravos-codegraph'],
|
|
96
|
+
body: [
|
|
97
|
+
'Run bravos first: it reads the briefing, explains the loop in five lines and routes you. If the banner says "intent none", run bravos-context: a short interview about purpose, users and roles, the data that matters, what must never happen, and what looks dangerous but is intended. bravos-codegraph answers "where are the routes", "what does this handler reach", "is this code live" without reading the whole tree.',
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'CI owner',
|
|
102
|
+
skills: ['bravos-cicd', 'bravos-schedule'],
|
|
103
|
+
body: [
|
|
104
|
+
'bravos init --ci writes the PR gate and nightly workflows. bravos-cicd audits existing workflows: broken guardlink calls, script injection from pull-request text, missing SARIF upload. The nightly can replay retest kits only against a target the committed, reviewed scope authorizes.',
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'tasks',
|
|
111
|
+
title: 'What to do, by task',
|
|
112
|
+
summary: 'the skill (or skills, in order) for each common job',
|
|
113
|
+
items: [
|
|
114
|
+
{ name: 'Where do I start? What is open?', skills: ['bravos'], body: ['Reads the briefing and names the one next step, with the reason.'] },
|
|
115
|
+
{ name: 'Teach bravos what the app is for', skills: ['bravos-context'], body: ['Writes .guardlink/intent.md: invariants (INV-n) that become threats, and intended-by-design items (INT-n) that get an "intended?" audit instead of a high rating.'] },
|
|
116
|
+
{ name: 'Threat-model someone else\'s project', skills: ['bravos-context', 'bravos-annotate'], body: ['bravos-context assumes you are researching software you do not own (an open-source project, a vendor\'s code): it drafts the intent from upstream\'s own docs with links and asks only the scope decisions — which configuration to rate against (shipped defaults or the hardened setup upstream recommends), single organisation or multi-tenant, which attackers are in scope, and the intended-looking behaviours upstream does not document. intent.md records them as Assumptions, and severities are rated against them. Say "this is our product" for the owner interview instead.'] },
|
|
117
|
+
{ name: 'Answer bravos\'s questions as a team', skills: ['bravos-context'], body: ['bravos-context opens its questions as a bravos page (bravos review intent): pre-filled with bravos\'s suggestions and their sources; change what is wrong, make the calls marked \'needs your call\' (Confirm stays off until you do), and the facts bravos drafted stay folded unless you open one to object. Each person\'s answers save to .guardlink/intent-answers/<name>.json; teammates run bravos review intent after pulling and commit theirs. bravos review intent --summary shows where people agree and disagree; only what a named person agreed to is marked confirmed. On Claude Code, --share puts the same page on a private claude.ai link instead.'] },
|
|
118
|
+
{ name: 'I wrote security-relevant code', skills: ['bravos-annotate'], body: ['Annotates what you changed to guardlink\'s evidence bar and runs the lint gate until clean.'] },
|
|
119
|
+
{ name: 'Cover the whole repo', skills: ['bravos-campaign'], body: ['Rounds of batches with subagents and rests, until the coverage goal or until you stop it.'] },
|
|
120
|
+
{ name: 'Is this exposure real?', skills: ['bravos-templatize', 'bravos-env', 'bravos-prove'], body: ['Template → authorized target → run. Only a reproduced run confirms; reading code never does.'] },
|
|
121
|
+
{ name: 'I fixed a finding', skills: ['bravos-recheck'], body: ['Re-verifies stale claims, replays the retest kit, and checks the fix added no new exposure.'] },
|
|
122
|
+
{ name: 'I am about to open a PR', skills: ['bravos-ship'], body: ['The no-mistakes gate: validate, lint since the merge base, disclose new exposure, block confirmed exploits on changed paths.'] },
|
|
123
|
+
{ name: 'Find the routes, callers or blast radius', skills: ['bravos-codegraph'], body: ['One bounded question to the code graph instead of grepping. Needs codegraph-mcp (pip install bravos; licence-free). With it, other skills get sharper on their own: bravos-templatize targets the route and auth that actually reach each sink, bravos-recheck follows a fix\'s callers and blast radius into other files, bravos-context drafts the real public surface, and bravos-prove tests the most reachable exposures first. Without it they read the code and say so.'] },
|
|
124
|
+
{ name: 'Scan for dependency CVEs, secrets, IaC', skills: ['bravos-sast'], body: ['Licensed bravos-cli: Semgrep rules, request-to-sink taint over the code graph, dependency CVEs with whether the vulnerable function is reached, secrets (git history too) and IaC. It reads the GuardLink threat model as input, so annotate first. Says plainly when it is unavailable.'] },
|
|
125
|
+
{ name: 'What did SAST find, and what happens to it', skills: ['bravos-sast', 'bravos-annotate', 'bravos-templatize', 'bravos-prove', 'bravos-recheck'], body: ['bravos sast candidates (free, no model) reads the newest run in .bravos/sast/: reachable findings joined to a claim feed bravos-templatize and bravos-prove (the route and taint path to target, tested first); reachable findings not in the threat model are leads for bravos-annotate; bravos-recheck rescans the changed files after a fix. A static finding is a lead, never a confirmation; only a run confirms. The briefing\'s SAST line shows the latest run, or that SAST is unavailable.'] },
|
|
126
|
+
{ name: 'Keep it fresh while I work, or nightly', skills: ['bravos-schedule'], body: ['An in-session /loop from .claude/loop.md, the nightly workflow, or a cloud task you create. Loops never send test traffic.'] },
|
|
127
|
+
{ name: 'Add or fix security CI', skills: ['bravos-cicd'], body: ['PR gate, nightly annotation, SARIF to code scanning, script-injection fixes.'] },
|
|
128
|
+
{ name: 'An app made of several repositories (microservices)', skills: ['bravos-annotate', 'bravos-codegraph', 'bravos-prove'], body: ['Clone the services side by side in one folder, then run bravos workspace create NAME --member ./gateway=gateway@http://127.0.0.1:8080 --member ./orders=orders@http://127.0.0.1:8081 (bravos workspace plus guardlink link-project plus bravos in each repo and in the shared folder). Work on a service from its own repo; ask questions that cross services from the shared folder, where the briefing and bravos watch show every service. Declare each call to another service as @flows #<this>-<x> -> #<other>-<y> via http (plain ids, not #repo.tag). Build the code graph over the shared folder so it links one service\'s client calls to another\'s handlers. bravos report data --workspace writes one report across the services.'] },
|
|
129
|
+
{ name: 'Write up the assessment, a finding or a retest', skills: ['bravos-report'], body: ['Ask "write the assessment report" (or a finding, a retest, an executive brief). bravos report data gathers the evidence; the agent writes report.md in the eight-section reading order, with every proof claim citing its evidence; bravos report check refuses a finding labelled reproduced without a run or ledger record; bravos report export writes md, txt, html and pdf. bravos watch shows the newest report (p to read it, o to open the pdf or html).'] },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'skills',
|
|
134
|
+
title: 'Every skill',
|
|
135
|
+
summary: 'all fourteen skills, what each does and leaves for the next',
|
|
136
|
+
items: [
|
|
137
|
+
{ name: 'bravos', body: ['Front door: reads the security picture, explains the evidence loop, routes to the right skill.'] },
|
|
138
|
+
{ name: 'bravos-context', body: ['Learns what the product intends from the repo and a short interview; records invariants, intended behaviour, actors, assets and boundaries.'] },
|
|
139
|
+
{ name: 'bravos-annotate', body: ['Picks a budgeted slice (entry points and trust code first), follows guardlink\'s own playbook, writes annotations to the evidence bar, lints until clean, reports the coverage delta.'] },
|
|
140
|
+
{ name: 'bravos-codegraph', body: ['Bounded questions to codegraph-mcp: routes, attack surface, sinks a symbol reaches, liveness, blast radius, annotation gaps in a change.'] },
|
|
141
|
+
{ name: 'bravos-ship', body: ['The pre-PR gate. Optional PreToolUse hook (bravos init --pre-pr-hook) runs it on every gh pr create / gh pr ready.'] },
|
|
142
|
+
{ name: 'bravos-templatize', body: ['Turns open exposures into validated cxg templates following guardlink\'s translate method; keeps replay kits apart from scan templates; records classes with no oracle as gaps.'] },
|
|
143
|
+
{ name: 'bravos-env', body: ['Brings up a target you own: loopback by default, a committed reviewed scope.yaml, your typed authorization, a real session captured or imported.'] },
|
|
144
|
+
{ name: 'bravos-prove', body: ['Runs the authorized test, reads cxg\'s report and exit code, writes outcomes only through guardlink\'s hypothesis ledger.'] },
|
|
145
|
+
{ name: 'bravos-recheck', body: ['After a fix: stale claims, retest-kit replay, no new exposure, what to test next.'] },
|
|
146
|
+
{ name: 'bravos-sast', body: ['Licensed static analysis joined to guardlink claims and reachability; gates on findings new since the base.'] },
|
|
147
|
+
{ name: 'bravos-campaign', body: ['A goal over many rounds: disjoint batches, one subagent each, a gate per batch, rests, a final before-and-after.'] },
|
|
148
|
+
{ name: 'bravos-schedule', body: ['Recurring upkeep with each harness\'s own mechanism; never live tests from a loop.'] },
|
|
149
|
+
{ name: 'bravos-cicd', body: ['Audits and writes the security workflows.'] },
|
|
150
|
+
{ name: 'bravos-report', body: ['Writes the assessment, a single finding, a retest or an executive brief from the evidence bravos report data gathers; each finding\'s evidence level is computed from the ledger and runs, bravos report check gates the report, and bravos report export renders md, txt, html and pdf. Reports stay in .bravos/reports/, not committed.'] },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'states',
|
|
155
|
+
title: 'Reading a finding',
|
|
156
|
+
summary: 'the three separate states every claim has, and what each word means',
|
|
157
|
+
items: [
|
|
158
|
+
{ name: 'open / mitigated / accepted', body: ['Does a control cover it? Open means no control is recorded; mitigated names the control; accepted is a human\'s written decision to live with it, never an agent\'s.'] },
|
|
159
|
+
{ name: 'untested / confirmed / refuted', body: ['Has an authorized run tested it? Confirmed means a run reproduced it, with evidence; refuted means a run showed it does not hold. Untested is neither cleared nor proven.'] },
|
|
160
|
+
{ name: 'verified / stale', body: ['Is the claim still true of today\'s code? Stale means the code under it changed since it was last checked; bravos-recheck clears it.'] },
|
|
161
|
+
{ name: 'Severity', body: ['Rated by impact on this product × reach × ease, with the reason written down. Without .guardlink/intent.md, severities are defaults, not calibrated.'] },
|
|
162
|
+
{ name: 'unknown is not zero', body: ['When a tool is unavailable the briefing says "unknown", never 0. Nothing is called clean unless a gate exited 0.'] },
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'safety',
|
|
167
|
+
title: 'The hard rules',
|
|
168
|
+
summary: 'what bravos never does, whatever the effort level',
|
|
169
|
+
items: [
|
|
170
|
+
{ name: 'Authorization', body: ['No request goes to a host other than loopback without a committed, reviewed scope.yaml and your own typed authorization naming the target.'] },
|
|
171
|
+
{ name: 'Destructive tests', body: ['DoS and other destructive classes run only against a disposable loopback target.'] },
|
|
172
|
+
{ name: 'Credentials', body: ['Never invented. Test users and sessions are seeded or captured for real, or the run waits for you.'] },
|
|
173
|
+
{ name: 'No hidden agents', body: ['The agent in your session does the work; bravos never spawns another agent CLI. Model-API probe generation in cxg runs only after you agree to the spend.'] },
|
|
174
|
+
{ name: 'Governance', body: ['Agents never write @accepts or @entitles (a human\'s decision) and never write @confirmed without reproduction evidence in hand.'] },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'harnesses',
|
|
179
|
+
title: 'Harnesses',
|
|
180
|
+
summary: 'what bravos installs for each agent and how it starts there',
|
|
181
|
+
items: [
|
|
182
|
+
{ name: 'Claude Code', body: ['Skills in .claude/skills, SessionStart hook in .claude/settings.json (banner + briefing), MCP in .mcp.json, optional pre-PR PreToolUse hook. Also as a plugin.'] },
|
|
183
|
+
{ name: 'Codex', body: ['Skills in .agents/skills, hook in .codex/hooks.json (trust it once in /hooks), MCP in .codex/config.toml. Also as a plugin.'] },
|
|
184
|
+
{ name: 'omp', body: ['Reads the Claude Code skills and MCP; .omp/extensions/bravos.js shows the banner and hands over the briefing.'] },
|
|
185
|
+
{ name: 'opencode', body: ['Finds the skills in .claude/skills and .agents/skills through its skill tool; bravos adds /bravos… commands in .opencode/commands that load them, a plugin in .opencode/plugins (opencode 2.x) that adds the briefing to the system prompt, and guardlink MCP in opencode.json.'] },
|
|
186
|
+
{ name: 'Antigravity (agy)', body: ['Reads .agents/skills; .agents/hooks.json carries a bravos SessionStart group that injects the briefing.'] },
|
|
187
|
+
{ name: 'Cursor', body: ['.cursor/rules/bravos.mdc points at the skills when the repo has a .cursor/ folder.'] },
|
|
188
|
+
{ name: 'VS Code (Copilot agent)', body: ['Reads hooks from .claude/settings.json (preview), so the briefing can load there too.'] },
|
|
189
|
+
{ name: 'Every repo, or one copy of each skill', body: ['bravos init --global installs for your user: skills in ~/.claude/skills (linked from ~/.agents/skills) and a user hook that briefs repos with a threat model but no bravos install. bravos sync --link-skills makes a repo\'s .agents/skills a link to .claude/skills, so tools that read both (Warp) list each skill once.'] },
|
|
190
|
+
{ name: 'Banner switches', body: ['BRAVOS_BANNER=off hides it; BRAVOS_BANNER_COLOR=off or NO_COLOR prints it plain. The briefing for the model is unchanged either way.'] },
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
const WIDTH = 80;
|
|
195
|
+
function wrap(text, width, indent) {
|
|
196
|
+
const out = [];
|
|
197
|
+
let line = '';
|
|
198
|
+
for (const word of text.split(/\s+/).filter(Boolean)) {
|
|
199
|
+
if (line && (indent + line + ' ' + word).length > width) {
|
|
200
|
+
out.push(indent + line);
|
|
201
|
+
line = word;
|
|
202
|
+
}
|
|
203
|
+
else
|
|
204
|
+
line = line ? `${line} ${word}` : word;
|
|
205
|
+
}
|
|
206
|
+
if (line)
|
|
207
|
+
out.push(indent + line);
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
const skillList = (skills, sigil) => (skills?.length ? skills.map((s) => sigil + s).join(' · ') : '');
|
|
211
|
+
// @comment -- "Renders one topic, or every topic, as wrapped terminal text with skills shown in the given harness syntax."
|
|
212
|
+
export function renderGuide(topicId, opts = {}) {
|
|
213
|
+
const sigil = opts.sigil ?? '/';
|
|
214
|
+
const topics = topicId === 'all' ? GUIDE_TOPICS : GUIDE_TOPICS.filter((t) => t.id === (topicId ?? 'start'));
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const t of topics) {
|
|
217
|
+
out.push(t.title.toUpperCase(), '');
|
|
218
|
+
for (const p of t.intro ?? [])
|
|
219
|
+
out.push(...wrap(p, WIDTH, ' '), '');
|
|
220
|
+
for (const it of t.items) {
|
|
221
|
+
const skills = skillList(it.skills, sigil);
|
|
222
|
+
out.push(` ${it.name}`);
|
|
223
|
+
if (skills)
|
|
224
|
+
out.push(...wrap(`skills: ${skills}`, WIDTH, ' '));
|
|
225
|
+
for (const p of it.body)
|
|
226
|
+
out.push(...wrap(p, WIDTH, ' '));
|
|
227
|
+
out.push('');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (topicId === undefined || topicId === 'start') {
|
|
231
|
+
out.push('MORE TOPICS (bravos guide <topic>, or bravos guide all)', '');
|
|
232
|
+
for (const t of GUIDE_TOPICS.filter((x) => x.id !== 'start'))
|
|
233
|
+
out.push(` ${t.id.padEnd(10)} ${t.summary}`);
|
|
234
|
+
out.push('', 'Skills are shown in Claude Code syntax; Codex uses $bravos-…, omp /skill:bravos-….');
|
|
235
|
+
}
|
|
236
|
+
return out.join('\n').replace(/\n+$/, '');
|
|
237
|
+
}
|
|
238
|
+
// @comment -- "Renders the whole guide as Markdown for docs/GUIDE.md; a test keeps the committed file in step."
|
|
239
|
+
export function renderGuideMarkdown() {
|
|
240
|
+
const out = ['# bravos guide', '', '_Generated from `src/guide.ts` by `bravos guide --markdown`; edit the source, not this file._', ''];
|
|
241
|
+
out.push('Skills are written in Claude Code syntax (`/bravos-ship`); in Codex use `$bravos-ship`, in omp `/skill:bravos-ship`.', '');
|
|
242
|
+
for (const t of GUIDE_TOPICS) {
|
|
243
|
+
out.push(`## ${t.title}`, '');
|
|
244
|
+
for (const p of t.intro ?? [])
|
|
245
|
+
out.push(p, '');
|
|
246
|
+
for (const it of t.items) {
|
|
247
|
+
out.push(`### ${it.name}`, '');
|
|
248
|
+
if (it.skills?.length)
|
|
249
|
+
out.push(`Skills: ${it.skills.map((s) => `\`/${s}\``).join(' · ')}`, '');
|
|
250
|
+
out.push(...it.body.flatMap((p) => [p, '']));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return out.join('\n').replace(/\n+$/, '') + '\n';
|
|
254
|
+
}
|
|
255
|
+
export const GUIDE_TOPIC_IDS = GUIDE_TOPICS.map((t) => t.id);
|