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 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.0",
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) {