repo-agent-brief 0.1.0 → 0.2.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/README.md +18 -1
- package/package.json +3 -2
- package/skills/repo-agent-brief/SKILL.md +53 -0
- package/src/cli.js +6 -2
- package/src/index.js +101 -0
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ npx repo-agent-brief
|
|
|
15
15
|
- Finds high-signal files: `AGENTS.md`, `CLAUDE.md`, `README.md`, `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, etc.
|
|
16
16
|
- Infers stack and common commands.
|
|
17
17
|
- Builds a compact repo map.
|
|
18
|
+
- Optionally summarizes the current git diff so agents can start from “what changed?” instead of rereading the whole repo.
|
|
18
19
|
- Scans context files for obvious secrets and risky operational instructions.
|
|
19
20
|
- Emits Markdown for humans/agents or JSON for automation.
|
|
20
21
|
|
|
@@ -42,6 +43,7 @@ Options:
|
|
|
42
43
|
- `--format markdown|json` / `-f` — output format. Default: `markdown`.
|
|
43
44
|
- `--max-file-bytes N` — max bytes to read per context file. Default: `12000`.
|
|
44
45
|
- `--no-snippets` — omit source snippets.
|
|
46
|
+
- `--diff [ref]` — include changed files, insertions/deletions, and high-impact path warnings versus a git ref. Defaults to `HEAD` when no ref is provided.
|
|
45
47
|
- `--fail-on-high-risk` — exit `2` if high-severity risk patterns are found.
|
|
46
48
|
|
|
47
49
|
Examples:
|
|
@@ -49,9 +51,20 @@ Examples:
|
|
|
49
51
|
```bash
|
|
50
52
|
agent-brief . > AGENT_BRIEF.md
|
|
51
53
|
agent-brief ~/dev/my-app --format json
|
|
54
|
+
agent-brief . --diff origin/main
|
|
52
55
|
agent-brief . --fail-on-high-risk
|
|
53
56
|
```
|
|
54
57
|
|
|
58
|
+
## Diff-aware handoffs
|
|
59
|
+
|
|
60
|
+
When you are handing an in-progress branch to an agent, run:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
agent-brief . --diff origin/main > AGENT_HANDOFF.md
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The brief adds a `Git diff` section with changed paths, line counts, and warnings for high-impact files such as GitHub Actions workflows, deploy scripts, migrations, Docker Compose files, and lockfiles. This keeps the first agent turn grounded in the actual patch instead of a vague repo overview.
|
|
67
|
+
|
|
55
68
|
## Why this exists
|
|
56
69
|
|
|
57
70
|
The current agent tooling boom has plenty of orchestration, MCP servers, and observability dashboards. The missing small thing is a cheap, local preflight that gives any agent the same crisp project orientation before it spends tokens or touches files.
|
|
@@ -63,7 +76,7 @@ This is intentionally zero-dependency and boring. It should be safe to run in al
|
|
|
63
76
|
```js
|
|
64
77
|
import { generateBrief, formatMarkdown } from 'repo-agent-brief';
|
|
65
78
|
|
|
66
|
-
const brief = generateBrief(process.cwd());
|
|
79
|
+
const brief = generateBrief(process.cwd(), { diffRef: 'origin/main' });
|
|
67
80
|
console.log(formatMarkdown(brief));
|
|
68
81
|
```
|
|
69
82
|
|
|
@@ -74,3 +87,7 @@ This is not a full secret scanner. It catches common token/private-key/secret-as
|
|
|
74
87
|
## License
|
|
75
88
|
|
|
76
89
|
MIT
|
|
90
|
+
|
|
91
|
+
## Agent Skill
|
|
92
|
+
|
|
93
|
+
This package includes an OpenClaw/Claude-style skill at `skills/repo-agent-brief` that teaches agents to run repo preflight and diff-aware handoff briefs before editing or reviewing code.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repo-agent-brief",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Generate concise, safety-aware project briefs for coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"src",
|
|
14
14
|
"README.md",
|
|
15
|
-
"LICENSE"
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"skills/"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
19
|
"test": "node --test",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: repo-agent-brief
|
|
3
|
+
description: Generate concise, safety-aware repository orientation briefs with repo-agent-brief/agent-brief before coding-agent work, reviews, handoffs, PR analysis, unfamiliar repo edits, diff-aware branch handoffs, or when an agent needs stack/commands/context/risk signals before changing files.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Repo Agent Brief Skill
|
|
7
|
+
|
|
8
|
+
Use `repo-agent-brief` to orient an agent before it edits or reviews a repository. It finds high-signal context files, infers stack/commands, builds a compact repo map, and flags obvious secret/risky-instruction patterns.
|
|
9
|
+
|
|
10
|
+
## Default workflow
|
|
11
|
+
|
|
12
|
+
From the repository root:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx repo-agent-brief . > AGENT_BRIEF.md
|
|
16
|
+
sed -n '1,220p' AGENT_BRIEF.md
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For in-progress branches:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx repo-agent-brief . --diff origin/main > AGENT_HANDOFF.md
|
|
23
|
+
sed -n '1,260p' AGENT_HANDOFF.md
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For machine-readable automation:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx repo-agent-brief . --format json > agent-brief.json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## When to use
|
|
33
|
+
|
|
34
|
+
- First pass in an unfamiliar repo.
|
|
35
|
+
- Before delegating to a coding agent.
|
|
36
|
+
- PR/branch handoff where changed files matter.
|
|
37
|
+
- Safety preflight before touching CI, migrations, deploy scripts, auth, or config.
|
|
38
|
+
|
|
39
|
+
## Safety
|
|
40
|
+
|
|
41
|
+
- This is not a full secret scanner. Use Gitleaks/TruffleHog for full audits.
|
|
42
|
+
- If high-risk patterns are found, inspect before proceeding.
|
|
43
|
+
- Use `--fail-on-high-risk` in CI or strict agent workflows.
|
|
44
|
+
- Generated briefs may include snippets from repo context files; avoid posting publicly without review.
|
|
45
|
+
|
|
46
|
+
## Useful commands
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx repo-agent-brief .
|
|
50
|
+
npx repo-agent-brief . --diff HEAD
|
|
51
|
+
npx repo-agent-brief . --diff origin/main --fail-on-high-risk
|
|
52
|
+
npx repo-agent-brief . --no-snippets
|
|
53
|
+
```
|
package/src/cli.js
CHANGED
|
@@ -3,13 +3,14 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { generateBrief, formatJson, formatMarkdown } from './index.js';
|
|
4
4
|
|
|
5
5
|
function parseArgs(argv) {
|
|
6
|
-
const args = { path: '.', format: 'markdown', maxFileBytes: 12000, noSnippets: false, failOnHighRisk: false };
|
|
6
|
+
const args = { path: '.', format: 'markdown', maxFileBytes: 12000, noSnippets: false, failOnHighRisk: false, diffRef: null };
|
|
7
7
|
for (let i = 0; i < argv.length; i++) {
|
|
8
8
|
const arg = argv[i];
|
|
9
9
|
if (arg === '--format' || arg === '-f') args.format = argv[++i];
|
|
10
10
|
else if (arg === '--max-file-bytes') args.maxFileBytes = Number(argv[++i]);
|
|
11
11
|
else if (arg === '--no-snippets') args.noSnippets = true;
|
|
12
12
|
else if (arg === '--fail-on-high-risk') args.failOnHighRisk = true;
|
|
13
|
+
else if (arg === '--diff') args.diffRef = argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[++i] : 'HEAD';
|
|
13
14
|
else if (arg === '--help' || arg === '-h') args.help = true;
|
|
14
15
|
else if (!arg.startsWith('-')) args.path = arg;
|
|
15
16
|
else throw new Error(`Unknown option: ${arg}`);
|
|
@@ -27,12 +28,14 @@ Options:
|
|
|
27
28
|
-f, --format markdown|json Output format (default: markdown)
|
|
28
29
|
--max-file-bytes N Max bytes to read per context file (default: 12000)
|
|
29
30
|
--no-snippets Omit context snippets from output
|
|
31
|
+
--diff [ref] Include changed files vs ref (default: HEAD)
|
|
30
32
|
--fail-on-high-risk Exit 2 if high-severity risk patterns are found
|
|
31
33
|
-h, --help Show help
|
|
32
34
|
|
|
33
35
|
Examples:
|
|
34
36
|
npx @builtbyecho/agent-brief
|
|
35
37
|
agent-brief ~/dev/my-app --format json
|
|
38
|
+
agent-brief . --diff origin/main
|
|
36
39
|
agent-brief . --fail-on-high-risk > AGENT_BRIEF.md
|
|
37
40
|
`;
|
|
38
41
|
}
|
|
@@ -46,7 +49,8 @@ try {
|
|
|
46
49
|
if (!['markdown', 'json'].includes(args.format)) throw new Error('--format must be markdown or json');
|
|
47
50
|
const brief = generateBrief(resolve(args.path), {
|
|
48
51
|
maxFileBytes: args.maxFileBytes,
|
|
49
|
-
includeSnippets: !args.noSnippets
|
|
52
|
+
includeSnippets: !args.noSnippets,
|
|
53
|
+
diffRef: args.diffRef
|
|
50
54
|
});
|
|
51
55
|
console.log(args.format === 'json' ? formatJson(brief) : formatMarkdown(brief));
|
|
52
56
|
if (args.failOnHighRisk && brief.risks.some(r => r.severity === 'high')) process.exit(2);
|
package/src/index.js
CHANGED
|
@@ -40,6 +40,7 @@ export function generateBrief(root = process.cwd(), options = {}) {
|
|
|
40
40
|
const tree = collectTree(absRoot, opts.maxTreeEntries);
|
|
41
41
|
const packageInfo = readPackage(absRoot);
|
|
42
42
|
const git = readGit(absRoot);
|
|
43
|
+
const diff = opts.diffRef ? collectDiff(absRoot, opts.diffRef) : null;
|
|
43
44
|
const risks = scanRisks(absRoot, context.files);
|
|
44
45
|
const commands = inferCommands(absRoot, packageInfo);
|
|
45
46
|
const stack = inferStack(absRoot, packageInfo);
|
|
@@ -51,6 +52,7 @@ export function generateBrief(root = process.cwd(), options = {}) {
|
|
|
51
52
|
root: absRoot,
|
|
52
53
|
score,
|
|
53
54
|
git,
|
|
55
|
+
diff,
|
|
54
56
|
stack,
|
|
55
57
|
commands,
|
|
56
58
|
contextFiles: context.files,
|
|
@@ -81,6 +83,25 @@ export function formatMarkdown(brief) {
|
|
|
81
83
|
}
|
|
82
84
|
lines.push('');
|
|
83
85
|
|
|
86
|
+
if (brief.diff) {
|
|
87
|
+
lines.push(`## Git diff vs ${brief.diff.ref}`);
|
|
88
|
+
if (brief.diff.available) {
|
|
89
|
+
lines.push(`- ${brief.diff.files.length} changed file(s), +${brief.diff.insertions} -${brief.diff.deletions}`);
|
|
90
|
+
if (brief.diff.files.length) {
|
|
91
|
+
for (const file of brief.diff.files) {
|
|
92
|
+
lines.push(`- ${file.status} ${file.path}${file.additions || file.deletions ? ` (+${file.additions}/-${file.deletions})` : ''}${file.risky ? ' ⚠️' : ''}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (brief.diff.riskNotes.length) {
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push(...brief.diff.riskNotes.map(note => `- ⚠️ ${note}`));
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
lines.push(`- Diff unavailable: ${brief.diff.error}`);
|
|
101
|
+
}
|
|
102
|
+
lines.push('');
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
lines.push('## Agent context files');
|
|
85
106
|
if (brief.contextFiles.length) {
|
|
86
107
|
for (const file of brief.contextFiles) {
|
|
@@ -180,6 +201,86 @@ function readGit(root) {
|
|
|
180
201
|
return git;
|
|
181
202
|
}
|
|
182
203
|
|
|
204
|
+
function collectDiff(root, ref) {
|
|
205
|
+
const diff = { ref, available: false, files: [], insertions: 0, deletions: 0, riskNotes: [], error: '' };
|
|
206
|
+
try {
|
|
207
|
+
const numstat = execFileSync('git', ['-C', root, 'diff', '--numstat', ref, '--'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
208
|
+
const nameStatus = execFileSync('git', ['-C', root, 'diff', '--name-status', ref, '--'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
209
|
+
const status = execFileSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
210
|
+
diff.available = true;
|
|
211
|
+
const byPath = new Map();
|
|
212
|
+
for (const line of numstat ? numstat.split('\n') : []) {
|
|
213
|
+
const parts = line.split('\t');
|
|
214
|
+
if (parts.length >= 3 && /^(?:\d+|-)$/.test(parts[0]) && /^(?:\d+|-)$/.test(parts[1])) {
|
|
215
|
+
const additions = parts[0] === '-' ? 0 : Number(parts[0]);
|
|
216
|
+
const deletions = parts[1] === '-' ? 0 : Number(parts[1]);
|
|
217
|
+
const path = parts.slice(2).join('\t');
|
|
218
|
+
const entry = byPath.get(path) || { path, status: 'M', additions: 0, deletions: 0, risky: false };
|
|
219
|
+
entry.additions += additions;
|
|
220
|
+
entry.deletions += deletions;
|
|
221
|
+
byPath.set(path, entry);
|
|
222
|
+
diff.insertions += additions;
|
|
223
|
+
diff.deletions += deletions;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const line of nameStatus ? nameStatus.split('\n') : []) {
|
|
227
|
+
const parts = line.split('\t');
|
|
228
|
+
if (parts.length >= 2 && /^[A-Z]/.test(parts[0])) {
|
|
229
|
+
const status = parts[0];
|
|
230
|
+
const path = parts[parts.length - 1];
|
|
231
|
+
const entry = byPath.get(path) || { path, status, additions: 0, deletions: 0, risky: false };
|
|
232
|
+
entry.status = status;
|
|
233
|
+
byPath.set(path, entry);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (const line of status ? status.split('\n') : []) {
|
|
237
|
+
if (!line.startsWith('?? ')) continue;
|
|
238
|
+
const path = line.slice(3).trim();
|
|
239
|
+
for (const filePath of expandUntracked(root, path)) {
|
|
240
|
+
if (!byPath.has(filePath)) byPath.set(filePath, { path: filePath, status: '??', additions: 0, deletions: 0, risky: false });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
diff.files = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
244
|
+
for (const file of diff.files) {
|
|
245
|
+
file.risky = isRiskyChangedPath(file.path);
|
|
246
|
+
if (file.risky) diff.riskNotes.push(`${file.path} is a high-impact path; inspect carefully before handing changes to an agent.`);
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
diff.error = error.message;
|
|
250
|
+
}
|
|
251
|
+
return diff;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function expandUntracked(root, path) {
|
|
255
|
+
const fullPath = join(root, path);
|
|
256
|
+
try {
|
|
257
|
+
const stat = statSync(fullPath);
|
|
258
|
+
if (!stat.isDirectory()) return [path];
|
|
259
|
+
const out = [];
|
|
260
|
+
walkUntracked(fullPath, path, out);
|
|
261
|
+
return out;
|
|
262
|
+
} catch {
|
|
263
|
+
return [path];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function walkUntracked(base, prefix, out) {
|
|
268
|
+
let entries = [];
|
|
269
|
+
try { entries = readdirSync(base, { withFileTypes: true }); } catch { return; }
|
|
270
|
+
for (const entry of entries) {
|
|
271
|
+
if (DEFAULT_IGNORES.has(entry.name)) continue;
|
|
272
|
+
const rel = `${prefix.replace(/\/$/, '')}/${entry.name}`;
|
|
273
|
+
if (entry.isDirectory()) walkUntracked(join(base, entry.name), rel, out);
|
|
274
|
+
else out.push(rel);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isRiskyChangedPath(path) {
|
|
279
|
+
return /(^|\/)(\.env|\.npmrc|\.pypirc|Dockerfile|docker-compose\.ya?ml|compose\.ya?ml|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$/i.test(path)
|
|
280
|
+
|| /(^|\/)\.github\/workflows\//i.test(path)
|
|
281
|
+
|| /(^|\/)(migrations?|supabase|terraform|infra|deploy|scripts?)\//i.test(path);
|
|
282
|
+
}
|
|
283
|
+
|
|
183
284
|
function inferCommands(root, pkg) {
|
|
184
285
|
const commands = [];
|
|
185
286
|
if (pkg?.scripts) {
|