@fro.bot/systematic 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/research/learnings-researcher.md +27 -26
- package/agents/review/api-contract-reviewer.md +1 -1
- package/agents/review/correctness-reviewer.md +1 -1
- package/agents/review/data-migrations-reviewer.md +1 -1
- package/agents/review/dhh-rails-reviewer.md +31 -52
- package/agents/review/julik-frontend-races-reviewer.md +27 -200
- package/agents/review/kieran-python-reviewer.md +29 -116
- package/agents/review/kieran-rails-reviewer.md +29 -98
- package/agents/review/kieran-typescript-reviewer.md +29 -107
- package/agents/review/maintainability-reviewer.md +1 -1
- package/agents/review/performance-reviewer.md +1 -1
- package/agents/review/reliability-reviewer.md +1 -1
- package/agents/review/security-reviewer.md +1 -1
- package/agents/review/testing-reviewer.md +1 -1
- package/agents/workflow/pr-comment-resolver.md +99 -50
- package/dist/index.js +9 -0
- package/dist/lib/config-handler.d.ts +2 -0
- package/package.json +1 -1
- package/skills/ce-compound/SKILL.md +100 -27
- package/skills/ce-compound-refresh/SKILL.md +172 -74
- package/skills/ce-review/SKILL.md +379 -418
- package/skills/ce-work/SKILL.md +5 -4
- package/skills/ce-work-beta/SKILL.md +6 -5
- package/skills/claude-permissions-optimizer/scripts/extract-commands.mjs +9 -159
- package/skills/claude-permissions-optimizer/scripts/normalize.mjs +151 -0
- package/skills/git-worktree/scripts/worktree-manager.sh +163 -0
- package/skills/lfg/SKILL.md +2 -2
- package/skills/orchestrating-swarms/SKILL.md +1 -1
- package/skills/setup/SKILL.md +8 -137
- package/skills/slfg/SKILL.md +8 -4
- package/skills/test-browser/SKILL.md +2 -2
- package/skills/test-xcode/SKILL.md +2 -2
package/skills/ce-work/SKILL.md
CHANGED
|
@@ -150,6 +150,7 @@ This command takes a work document (plan, specification, or todo file) and execu
|
|
|
150
150
|
|
|
151
151
|
**When this matters most:** Any change that touches models with callbacks, error handling with fallback/retry, or functionality exposed through multiple interfaces.
|
|
152
152
|
|
|
153
|
+
|
|
153
154
|
2. **Incremental Commits**
|
|
154
155
|
|
|
155
156
|
After completing each task, evaluate whether to create an incremental commit:
|
|
@@ -234,11 +235,9 @@ This command takes a work document (plan, specification, or todo file) and execu
|
|
|
234
235
|
# Use linting-agent before pushing to origin
|
|
235
236
|
```
|
|
236
237
|
|
|
237
|
-
2. **Consider
|
|
238
|
-
|
|
239
|
-
Use for complex, risky, or large changes. Read agents from `systematic.local.md` frontmatter (`review_agents`). If no settings file, invoke the `setup` skill to create one.
|
|
238
|
+
2. **Consider Code Review** (Optional)
|
|
240
239
|
|
|
241
|
-
|
|
240
|
+
Use for complex, risky, or large changes. Load the `ce:review` skill with `mode:autofix` to fix safe issues and flag the rest before shipping.
|
|
242
241
|
|
|
243
242
|
3. **Final Validation**
|
|
244
243
|
- All tasks marked completed
|
|
@@ -370,6 +369,7 @@ This command takes a work document (plan, specification, or todo file) and execu
|
|
|
370
369
|
|
|
371
370
|
---
|
|
372
371
|
|
|
372
|
+
[![Systematic v[VERSION]](https://img.shields.io/badge/Systematic-v[VERSION]-6366f1)](https://github.com/marcusrbrown/systematic)
|
|
373
373
|
🤖 Generated with [MODEL] ([CONTEXT] context, [THINKING]) via [HARNESS](HARNESS_URL)
|
|
374
374
|
EOF
|
|
375
375
|
)"
|
|
@@ -487,3 +487,4 @@ For most features: tests + linting + following patterns is sufficient.
|
|
|
487
487
|
- **Forgetting to track progress** - Update task status as you go or lose track of what's done
|
|
488
488
|
- **80% done syndrome** - Finish the feature, don't move on early
|
|
489
489
|
- **Over-reviewing simple changes** - Save reviewer agents for complex work
|
|
490
|
+
|
|
@@ -151,6 +151,7 @@ This command takes a work document (plan, specification, or todo file) and execu
|
|
|
151
151
|
|
|
152
152
|
**When this matters most:** Any change that touches models with callbacks, error handling with fallback/retry, or functionality exposed through multiple interfaces.
|
|
153
153
|
|
|
154
|
+
|
|
154
155
|
2. **Incremental Commits**
|
|
155
156
|
|
|
156
157
|
After completing each task, evaluate whether to create an incremental commit:
|
|
@@ -243,11 +244,9 @@ This command takes a work document (plan, specification, or todo file) and execu
|
|
|
243
244
|
# Use linting-agent before pushing to origin
|
|
244
245
|
```
|
|
245
246
|
|
|
246
|
-
2. **Consider
|
|
247
|
-
|
|
248
|
-
Use for complex, risky, or large changes. Read agents from `systematic.local.md` frontmatter (`review_agents`). If no settings file, invoke the `setup` skill to create one.
|
|
247
|
+
2. **Consider Code Review** (Optional)
|
|
249
248
|
|
|
250
|
-
|
|
249
|
+
Use for complex, risky, or large changes. Load the `ce:review` skill with `mode:autofix` to fix safe issues and flag the rest before shipping.
|
|
251
250
|
|
|
252
251
|
3. **Final Validation**
|
|
253
252
|
- All tasks marked completed
|
|
@@ -379,6 +378,7 @@ This command takes a work document (plan, specification, or todo file) and execu
|
|
|
379
378
|
|
|
380
379
|
---
|
|
381
380
|
|
|
381
|
+
[![Systematic v[VERSION]](https://img.shields.io/badge/Systematic-v[VERSION]-6366f1)](https://github.com/marcusrbrown/systematic)
|
|
382
382
|
🤖 Generated with [MODEL] ([CONTEXT] context, [THINKING]) via [HARNESS](HARNESS_URL)
|
|
383
383
|
EOF
|
|
384
384
|
)"
|
|
@@ -468,7 +468,7 @@ When external delegation is active, follow this workflow for each tagged task. D
|
|
|
468
468
|
|
|
469
469
|
Verify the delegate CLI is installed. If not found, print "Delegate CLI not installed - continuing with standard mode." and proceed normally.
|
|
470
470
|
|
|
471
|
-
2. **Build prompt** — For each task, assemble a prompt from the plan's implementation unit (Goal, Files, Approach, Conventions from
|
|
471
|
+
2. **Build prompt** — For each task, assemble a prompt from the plan's implementation unit (Goal, Files, Approach, Conventions from project AGENTS.md/AGENTS.md). Include rules: no git commits, no PRs, run `git status` and `git diff --stat` when done. Never embed credentials or tokens in the prompt - pass auth through environment variables.
|
|
472
472
|
|
|
473
473
|
3. **Write prompt to file** — Save the assembled prompt to a unique temporary file to avoid shell quoting issues and cross-task races. Use a unique filename per task.
|
|
474
474
|
|
|
@@ -560,3 +560,4 @@ For most features: tests + linting + following patterns is sufficient.
|
|
|
560
560
|
- **Forgetting to track progress** - Update task status as you go or lose track of what's done
|
|
561
561
|
- **80% done syndrome** - Finish the feature, don't move on early
|
|
562
562
|
- **Over-reviewing simple changes** - Save reviewer agents for complex work
|
|
563
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// Extracts, normalizes, and pre-classifies Bash commands from
|
|
3
|
+
// Extracts, normalizes, and pre-classifies Bash commands from Claude Code sessions.
|
|
4
4
|
// Filters against the current allowlist, groups by normalized pattern, and classifies
|
|
5
5
|
// each pattern as green/yellow/red so the model can review rather than classify from scratch.
|
|
6
6
|
//
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
16
16
|
import { homedir } from 'node:os'
|
|
17
17
|
import { join } from 'node:path'
|
|
18
|
+
import { normalize } from './normalize.mjs'
|
|
18
19
|
|
|
19
20
|
const args = process.argv.slice(2)
|
|
20
21
|
|
|
@@ -42,9 +43,8 @@ const maxSessions = parseInt(flag('max-sessions', '500'), 10)
|
|
|
42
43
|
const minCount = parseInt(flag('min-count', '5'), 10)
|
|
43
44
|
const projectSlugFilter = flag('project-slug', null)
|
|
44
45
|
const settingsPaths = flagAll('settings')
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
const projectsDir = join(opencodeDir, 'projects')
|
|
46
|
+
const claudeDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude')
|
|
47
|
+
const projectsDir = join(claudeDir, 'projects')
|
|
48
48
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000
|
|
49
49
|
|
|
50
50
|
// ── Allowlist loading ──────────────────────────────────────────────────────
|
|
@@ -70,9 +70,9 @@ async function loadAllowlist(filePath) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
if (settingsPaths.length === 0) {
|
|
73
|
-
settingsPaths.push(join(
|
|
74
|
-
settingsPaths.push(join(process.cwd(), '.
|
|
75
|
-
settingsPaths.push(join(process.cwd(), '.
|
|
73
|
+
settingsPaths.push(join(claudeDir, 'settings.json'))
|
|
74
|
+
settingsPaths.push(join(process.cwd(), '.claude', 'settings.json'))
|
|
75
|
+
settingsPaths.push(join(process.cwd(), '.claude', 'settings.local.json'))
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
for (const p of settingsPaths) {
|
|
@@ -320,7 +320,7 @@ const GREEN_COMPOUND = [
|
|
|
320
320
|
/\b--dry-run\b/,
|
|
321
321
|
/^git\s+clean\s+.*(-[a-z]*n|--dry-run)\b/, // git clean dry run
|
|
322
322
|
// NOTE: find is intentionally NOT green. Bash(find *) would also match
|
|
323
|
-
// find -delete and find -exec rm in
|
|
323
|
+
// find -delete and find -exec rm in Claude Code's allowlist glob matching.
|
|
324
324
|
// Commands with mode-switching flags: only green when the normalized pattern
|
|
325
325
|
// is narrow enough that the allowlist glob can't match the destructive form.
|
|
326
326
|
// Bash(sed -n *) is safe; Bash(sed *) would also match sed -i.
|
|
@@ -410,156 +410,7 @@ function classify(command) {
|
|
|
410
410
|
return { tier: 'unknown' }
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
// ── Normalization
|
|
414
|
-
|
|
415
|
-
// Risk-modifying flags that must NOT be collapsed into wildcards.
|
|
416
|
-
// Global flags are always preserved; context-specific flags only matter
|
|
417
|
-
// for certain base commands.
|
|
418
|
-
const GLOBAL_RISK_FLAGS = new Set([
|
|
419
|
-
'--force',
|
|
420
|
-
'--hard',
|
|
421
|
-
'-rf',
|
|
422
|
-
'--privileged',
|
|
423
|
-
'--no-verify',
|
|
424
|
-
'--system',
|
|
425
|
-
'--force-with-lease',
|
|
426
|
-
'-D',
|
|
427
|
-
'--force-if-includes',
|
|
428
|
-
'--volumes',
|
|
429
|
-
'--rmi',
|
|
430
|
-
'--rewrite',
|
|
431
|
-
'--delete',
|
|
432
|
-
])
|
|
433
|
-
|
|
434
|
-
// Flags that are only risky for specific base commands.
|
|
435
|
-
// -f means force-push in git, force-remove in docker, but pattern-file in grep.
|
|
436
|
-
// -v means remove-volumes in docker-compose, but verbose everywhere else.
|
|
437
|
-
const CONTEXTUAL_RISK_FLAGS = {
|
|
438
|
-
'-f': new Set(['git', 'docker', 'rm']),
|
|
439
|
-
'-v': new Set(['docker', 'docker-compose']),
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function isRiskFlag(token, base) {
|
|
443
|
-
if (GLOBAL_RISK_FLAGS.has(token)) return true
|
|
444
|
-
// Check context-specific flags
|
|
445
|
-
const contexts = CONTEXTUAL_RISK_FLAGS[token]
|
|
446
|
-
if (contexts && base && contexts.has(base)) return true
|
|
447
|
-
// Combined short flags containing risk chars: -rf, -fr, -fR, etc.
|
|
448
|
-
if (/^-[a-zA-Z]*[rf][a-zA-Z]*$/.test(token) && token.length <= 4) return true
|
|
449
|
-
return false
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: command normalization intentionally centralizes risk checks and pattern shaping.
|
|
453
|
-
function normalize(command) {
|
|
454
|
-
// Don't normalize shell injection patterns
|
|
455
|
-
if (/\|\s*(sh|bash|zsh)\b/.test(command)) return command
|
|
456
|
-
// Don't normalize sudo -- keep as-is
|
|
457
|
-
if (/^sudo\s/.test(command)) return 'sudo *'
|
|
458
|
-
|
|
459
|
-
// Handle pnpm --filter <pkg> <subcommand> specially
|
|
460
|
-
const pnpmFilter = command.match(/^pnpm\s+--filter\s+\S+\s+(\S+)/)
|
|
461
|
-
if (pnpmFilter) return `pnpm --filter * ${pnpmFilter[1]} *`
|
|
462
|
-
|
|
463
|
-
// Handle sed specially -- preserve the mode flag to keep safe patterns narrow.
|
|
464
|
-
// sed -i (in-place) is destructive; sed -n, sed -e, bare sed are read-only.
|
|
465
|
-
if (/^sed\s/.test(command)) {
|
|
466
|
-
if (/\s-i\b/.test(command)) return 'sed -i *'
|
|
467
|
-
const sedFlag = command.match(/^sed\s+(-[a-zA-Z])\s/)
|
|
468
|
-
return sedFlag ? `sed ${sedFlag[1]} *` : 'sed *'
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Handle ast-grep specially -- preserve --rewrite flag.
|
|
472
|
-
if (/^(ast-grep|sg)\s/.test(command)) {
|
|
473
|
-
const base = command.startsWith('sg') ? 'sg' : 'ast-grep'
|
|
474
|
-
return /\s--rewrite\b/.test(command) ? `${base} --rewrite *` : `${base} *`
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Handle find specially -- preserve key action flags.
|
|
478
|
-
// find -delete and find -exec rm are destructive; find -name/-type are safe.
|
|
479
|
-
if (/^find\s/.test(command)) {
|
|
480
|
-
if (/\s-delete\b/.test(command)) return 'find -delete *'
|
|
481
|
-
if (/\s-exec\s/.test(command)) return 'find -exec *'
|
|
482
|
-
// Extract the first predicate flag for a narrower safe pattern
|
|
483
|
-
const findFlag = command.match(/\s(-(?:name|type|path|iname))\s/)
|
|
484
|
-
return findFlag ? `find ${findFlag[1]} *` : 'find *'
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Handle git -C <dir> <subcommand> -- strip the -C <dir> and normalize the git subcommand
|
|
488
|
-
const gitC = command.match(/^git\s+-C\s+\S+\s+(.+)$/)
|
|
489
|
-
if (gitC) return normalize(`git ${gitC[1]}`)
|
|
490
|
-
|
|
491
|
-
// Split on compound operators -- normalize the first command only
|
|
492
|
-
const compoundMatch = command.match(/^(.+?)\s*(&&|\|\||;)\s*(.+)$/)
|
|
493
|
-
if (compoundMatch) {
|
|
494
|
-
return normalize(compoundMatch[1].trim())
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Strip trailing pipe chains for normalization (e.g., `cmd | tail -5`)
|
|
498
|
-
// but preserve pipe-to-shell (already handled by shell injection check above)
|
|
499
|
-
const pipeMatch = command.match(/^(.+?)\s*\|\s*(.+)$/)
|
|
500
|
-
if (pipeMatch) {
|
|
501
|
-
return normalize(pipeMatch[1].trim())
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Strip trailing redirections (2>&1, > file, >> file)
|
|
505
|
-
const cleaned = command
|
|
506
|
-
.replace(/\s*[12]?>>?\s*\S+\s*$/, '')
|
|
507
|
-
.replace(/\s*2>&1\s*$/, '')
|
|
508
|
-
.trim()
|
|
509
|
-
|
|
510
|
-
const parts = cleaned.split(/\s+/)
|
|
511
|
-
if (parts.length === 0) return command
|
|
512
|
-
|
|
513
|
-
const base = parts[0]
|
|
514
|
-
|
|
515
|
-
// For git/docker/gh/npm etc, include the subcommand
|
|
516
|
-
const multiWordBases = [
|
|
517
|
-
'git',
|
|
518
|
-
'docker',
|
|
519
|
-
'docker-compose',
|
|
520
|
-
'gh',
|
|
521
|
-
'npm',
|
|
522
|
-
'bun',
|
|
523
|
-
'pnpm',
|
|
524
|
-
'yarn',
|
|
525
|
-
'cargo',
|
|
526
|
-
'pip',
|
|
527
|
-
'pip3',
|
|
528
|
-
'bundle',
|
|
529
|
-
'systemctl',
|
|
530
|
-
'kubectl',
|
|
531
|
-
]
|
|
532
|
-
|
|
533
|
-
let prefix = base
|
|
534
|
-
let argStart = 1
|
|
535
|
-
|
|
536
|
-
if (multiWordBases.includes(base) && parts.length > 1) {
|
|
537
|
-
prefix = `${base} ${parts[1]}`
|
|
538
|
-
argStart = 2
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Preserve risk-modifying flags in the remaining args
|
|
542
|
-
const preservedFlags = []
|
|
543
|
-
for (let i = argStart; i < parts.length; i++) {
|
|
544
|
-
if (isRiskFlag(parts[i], base)) {
|
|
545
|
-
preservedFlags.push(parts[i])
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Build the normalized pattern
|
|
550
|
-
if (parts.length <= argStart && preservedFlags.length === 0) {
|
|
551
|
-
return prefix // no args, no flags: e.g., "git status"
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const flagStr =
|
|
555
|
-
preservedFlags.length > 0 ? ` ${preservedFlags.join(' ')}` : ''
|
|
556
|
-
const hasVaryingArgs = parts.length > argStart + preservedFlags.length
|
|
557
|
-
|
|
558
|
-
if (hasVaryingArgs) {
|
|
559
|
-
return `${prefix + flagStr} *`
|
|
560
|
-
}
|
|
561
|
-
return prefix + flagStr
|
|
562
|
-
}
|
|
413
|
+
// ── Normalization (see ./normalize.mjs) ────────────────────────────────────
|
|
563
414
|
|
|
564
415
|
// ── Session file scanning ──────────────────────────────────────────────────
|
|
565
416
|
|
|
@@ -587,7 +438,6 @@ async function listJsonlFiles(dir) {
|
|
|
587
438
|
}
|
|
588
439
|
}
|
|
589
440
|
|
|
590
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: transcript parsing requires defensive guards for heterogeneous session data.
|
|
591
441
|
async function processFile(filePath, sessionId) {
|
|
592
442
|
try {
|
|
593
443
|
filesScanned++
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Normalization helpers extracted from extract-commands.mjs for testability.
|
|
2
|
+
|
|
3
|
+
// Risk-modifying flags that must NOT be collapsed into wildcards.
|
|
4
|
+
// Global flags are always preserved; context-specific flags only matter
|
|
5
|
+
// for certain base commands.
|
|
6
|
+
const GLOBAL_RISK_FLAGS = new Set([
|
|
7
|
+
'--force',
|
|
8
|
+
'--hard',
|
|
9
|
+
'-rf',
|
|
10
|
+
'--privileged',
|
|
11
|
+
'--no-verify',
|
|
12
|
+
'--system',
|
|
13
|
+
'--force-with-lease',
|
|
14
|
+
'-D',
|
|
15
|
+
'--force-if-includes',
|
|
16
|
+
'--volumes',
|
|
17
|
+
'--rmi',
|
|
18
|
+
'--rewrite',
|
|
19
|
+
'--delete',
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
// Flags that are only risky for specific base commands.
|
|
23
|
+
// -f means force-push in git, force-remove in docker, but pattern-file in grep.
|
|
24
|
+
// -v means remove-volumes in docker-compose, but verbose everywhere else.
|
|
25
|
+
const CONTEXTUAL_RISK_FLAGS = {
|
|
26
|
+
'-f': new Set(['git', 'docker', 'rm']),
|
|
27
|
+
'-v': new Set(['docker', 'docker-compose']),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isRiskFlag(token, base) {
|
|
31
|
+
if (GLOBAL_RISK_FLAGS.has(token)) return true
|
|
32
|
+
// Check context-specific flags
|
|
33
|
+
const contexts = Object.hasOwn(CONTEXTUAL_RISK_FLAGS, token)
|
|
34
|
+
? CONTEXTUAL_RISK_FLAGS[token]
|
|
35
|
+
: undefined
|
|
36
|
+
if (contexts && base && contexts.has(base)) return true
|
|
37
|
+
// Combined short flags containing risk chars: -rf, -fr, -fR, etc.
|
|
38
|
+
if (/^-[a-zA-Z]*[rf][a-zA-Z]*$/.test(token) && token.length <= 4) return true
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalize(command) {
|
|
43
|
+
// Don't normalize shell injection patterns
|
|
44
|
+
if (/\|\s*(sh|bash|zsh)\b/.test(command)) return command
|
|
45
|
+
// Don't normalize sudo -- keep as-is
|
|
46
|
+
if (/^sudo\s/.test(command)) return 'sudo *'
|
|
47
|
+
|
|
48
|
+
// Handle pnpm --filter <pkg> <subcommand> specially
|
|
49
|
+
const pnpmFilter = command.match(/^pnpm\s+--filter\s+\S+\s+(\S+)/)
|
|
50
|
+
if (pnpmFilter) return `pnpm --filter * ${pnpmFilter[1]} *`
|
|
51
|
+
|
|
52
|
+
// Handle sed specially -- preserve the mode flag to keep safe patterns narrow.
|
|
53
|
+
// sed -i (in-place) is destructive; sed -n, sed -e, bare sed are read-only.
|
|
54
|
+
if (/^sed\s/.test(command)) {
|
|
55
|
+
if (/\s-i\b/.test(command)) return 'sed -i *'
|
|
56
|
+
const sedFlag = command.match(/^sed\s+(-[a-zA-Z])\s/)
|
|
57
|
+
return sedFlag ? `sed ${sedFlag[1]} *` : 'sed *'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle ast-grep specially -- preserve --rewrite flag.
|
|
61
|
+
if (/^(ast-grep|sg)\s/.test(command)) {
|
|
62
|
+
const base = command.startsWith('sg') ? 'sg' : 'ast-grep'
|
|
63
|
+
return /\s--rewrite\b/.test(command) ? `${base} --rewrite *` : `${base} *`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle find specially -- preserve key action flags.
|
|
67
|
+
// find -delete and find -exec rm are destructive; find -name/-type are safe.
|
|
68
|
+
if (/^find\s/.test(command)) {
|
|
69
|
+
if (/\s-delete\b/.test(command)) return 'find -delete *'
|
|
70
|
+
if (/\s-exec\s/.test(command)) return 'find -exec *'
|
|
71
|
+
// Extract the first predicate flag for a narrower safe pattern
|
|
72
|
+
const findFlag = command.match(/\s(-(?:name|type|path|iname))\s/)
|
|
73
|
+
return findFlag ? `find ${findFlag[1]} *` : 'find *'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle git -C <dir> <subcommand> -- strip the -C <dir> and normalize the git subcommand
|
|
77
|
+
const gitC = command.match(/^git\s+-C\s+\S+\s+(.+)$/)
|
|
78
|
+
if (gitC) return normalize(`git ${gitC[1]}`)
|
|
79
|
+
|
|
80
|
+
// Split on compound operators -- normalize the first command only
|
|
81
|
+
const compoundMatch = command.match(/^(.+?)\s*(&&|\|\||;)\s*(.+)$/)
|
|
82
|
+
if (compoundMatch) {
|
|
83
|
+
return normalize(compoundMatch[1].trim())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Strip trailing pipe chains for normalization (e.g., `cmd | tail -5`)
|
|
87
|
+
// but preserve pipe-to-shell (already handled by shell injection check above)
|
|
88
|
+
const pipeMatch = command.match(/^(.+?)\s*\|\s*(.+)$/)
|
|
89
|
+
if (pipeMatch) {
|
|
90
|
+
return normalize(pipeMatch[1].trim())
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Strip trailing redirections (2>&1, > file, >> file)
|
|
94
|
+
const cleaned = command
|
|
95
|
+
.replace(/\s*[12]?>>?\s*\S+\s*$/, '')
|
|
96
|
+
.replace(/\s*2>&1\s*$/, '')
|
|
97
|
+
.trim()
|
|
98
|
+
|
|
99
|
+
const parts = cleaned.split(/\s+/)
|
|
100
|
+
if (parts.length === 0) return command
|
|
101
|
+
|
|
102
|
+
const base = parts[0]
|
|
103
|
+
|
|
104
|
+
// For git/docker/gh/npm etc, include the subcommand
|
|
105
|
+
const multiWordBases = [
|
|
106
|
+
'git',
|
|
107
|
+
'docker',
|
|
108
|
+
'docker-compose',
|
|
109
|
+
'gh',
|
|
110
|
+
'npm',
|
|
111
|
+
'bun',
|
|
112
|
+
'pnpm',
|
|
113
|
+
'yarn',
|
|
114
|
+
'cargo',
|
|
115
|
+
'pip',
|
|
116
|
+
'pip3',
|
|
117
|
+
'bundle',
|
|
118
|
+
'systemctl',
|
|
119
|
+
'kubectl',
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
let prefix = base
|
|
123
|
+
let argStart = 1
|
|
124
|
+
|
|
125
|
+
if (multiWordBases.includes(base) && parts.length > 1) {
|
|
126
|
+
prefix = `${base} ${parts[1]}`
|
|
127
|
+
argStart = 2
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Preserve risk-modifying flags in the remaining args
|
|
131
|
+
const preservedFlags = []
|
|
132
|
+
for (let i = argStart; i < parts.length; i++) {
|
|
133
|
+
if (isRiskFlag(parts[i], base)) {
|
|
134
|
+
preservedFlags.push(parts[i])
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Build the normalized pattern
|
|
139
|
+
if (parts.length <= argStart && preservedFlags.length === 0) {
|
|
140
|
+
return prefix // no args, no flags: e.g., "git status"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const flagStr =
|
|
144
|
+
preservedFlags.length > 0 ? ` ${preservedFlags.join(' ')}` : ''
|
|
145
|
+
const hasVaryingArgs = parts.length > argStart + preservedFlags.length
|
|
146
|
+
|
|
147
|
+
if (hasVaryingArgs) {
|
|
148
|
+
return `${prefix + flagStr} *`
|
|
149
|
+
}
|
|
150
|
+
return prefix + flagStr
|
|
151
|
+
}
|
|
@@ -65,6 +65,137 @@ copy_env_files() {
|
|
|
65
65
|
echo -e " ${GREEN}✓ Copied $copied environment file(s)${NC}"
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
# Resolve the repository default branch, falling back to main when origin/HEAD
|
|
69
|
+
# is unavailable (for example in single-branch clones).
|
|
70
|
+
get_default_branch() {
|
|
71
|
+
local head_ref
|
|
72
|
+
head_ref=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || true)
|
|
73
|
+
|
|
74
|
+
if [[ -n "$head_ref" ]]; then
|
|
75
|
+
echo "${head_ref#refs/remotes/origin/}"
|
|
76
|
+
else
|
|
77
|
+
echo "main"
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Auto-trust is only safe when the worktree is created from a long-lived branch
|
|
82
|
+
# the developer already controls. Review/PR branches should fall back to the
|
|
83
|
+
# default branch baseline and require manual direnv approval.
|
|
84
|
+
is_trusted_base_branch() {
|
|
85
|
+
local branch="$1"
|
|
86
|
+
local default_branch="$2"
|
|
87
|
+
|
|
88
|
+
[[ "$branch" == "$default_branch" ]] && return 0
|
|
89
|
+
|
|
90
|
+
case "$branch" in
|
|
91
|
+
develop|dev|trunk|staging|release/*)
|
|
92
|
+
return 0
|
|
93
|
+
;;
|
|
94
|
+
*)
|
|
95
|
+
return 1
|
|
96
|
+
;;
|
|
97
|
+
esac
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Trust development tool configs in a new worktree.
|
|
101
|
+
# Worktrees get a new filesystem path that tools like mise and direnv
|
|
102
|
+
# have never seen. Without trusting, these tools block with interactive
|
|
103
|
+
# prompts or refuse to load configs, which breaks hooks and scripts.
|
|
104
|
+
#
|
|
105
|
+
# Safety: auto-trusts only configs unchanged from a trusted baseline branch.
|
|
106
|
+
# Review/PR branches fall back to the default-branch baseline, and direnv
|
|
107
|
+
# auto-allow is limited to trusted base branches because .envrc can source
|
|
108
|
+
# additional files that direnv does not validate.
|
|
109
|
+
#
|
|
110
|
+
# TOCTOU between hash-check and trust is acceptable for local dev use.
|
|
111
|
+
trust_dev_tools() {
|
|
112
|
+
local worktree_path="$1"
|
|
113
|
+
local base_ref="$2"
|
|
114
|
+
local allow_direnv_auto="$3"
|
|
115
|
+
local trusted=0
|
|
116
|
+
local skipped_messages=()
|
|
117
|
+
local manual_commands=()
|
|
118
|
+
|
|
119
|
+
# mise: trust the specific config file if present and unchanged
|
|
120
|
+
if command -v mise &>/dev/null; then
|
|
121
|
+
for f in .mise.toml mise.toml .tool-versions; do
|
|
122
|
+
if [[ -f "$worktree_path/$f" ]]; then
|
|
123
|
+
if _config_unchanged "$f" "$base_ref" "$worktree_path"; then
|
|
124
|
+
if (cd "$worktree_path" && mise trust "$f" --quiet); then
|
|
125
|
+
trusted=$((trusted + 1))
|
|
126
|
+
else
|
|
127
|
+
echo -e " ${YELLOW}Warning: 'mise trust $f' failed -- run manually in $worktree_path${NC}"
|
|
128
|
+
fi
|
|
129
|
+
else
|
|
130
|
+
skipped_messages+=("mise trust $f (config differs from $base_ref)")
|
|
131
|
+
manual_commands+=("mise trust $f")
|
|
132
|
+
fi
|
|
133
|
+
break
|
|
134
|
+
fi
|
|
135
|
+
done
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# direnv: allow .envrc
|
|
139
|
+
if command -v direnv &>/dev/null; then
|
|
140
|
+
if [[ -f "$worktree_path/.envrc" ]]; then
|
|
141
|
+
if [[ "$allow_direnv_auto" != "true" ]]; then
|
|
142
|
+
skipped_messages+=("direnv allow (.envrc auto-allow is disabled for non-trusted base branches)")
|
|
143
|
+
manual_commands+=("direnv allow")
|
|
144
|
+
elif _config_unchanged ".envrc" "$base_ref" "$worktree_path"; then
|
|
145
|
+
if (cd "$worktree_path" && direnv allow); then
|
|
146
|
+
trusted=$((trusted + 1))
|
|
147
|
+
else
|
|
148
|
+
echo -e " ${YELLOW}Warning: 'direnv allow' failed -- run manually in $worktree_path${NC}"
|
|
149
|
+
fi
|
|
150
|
+
else
|
|
151
|
+
skipped_messages+=("direnv allow (.envrc differs from $base_ref)")
|
|
152
|
+
manual_commands+=("direnv allow")
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
if [[ $trusted -gt 0 ]]; then
|
|
158
|
+
echo -e " ${GREEN}✓ Trusted $trusted dev tool config(s)${NC}"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
if [[ ${#skipped_messages[@]} -gt 0 ]]; then
|
|
162
|
+
echo -e " ${YELLOW}Skipped auto-trust for config(s) requiring manual review:${NC}"
|
|
163
|
+
for item in "${skipped_messages[@]}"; do
|
|
164
|
+
echo -e " - $item"
|
|
165
|
+
done
|
|
166
|
+
if [[ ${#manual_commands[@]} -gt 0 ]]; then
|
|
167
|
+
local joined
|
|
168
|
+
joined=$(printf ' && %s' "${manual_commands[@]}")
|
|
169
|
+
echo -e " ${BLUE}Review the diff, then run manually: cd $worktree_path${joined}${NC}"
|
|
170
|
+
fi
|
|
171
|
+
fi
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Check if a config file is unchanged from the base branch.
|
|
175
|
+
# Returns 0 (true) if the file is identical to the base branch version.
|
|
176
|
+
# Returns 1 (false) if the file was added or modified by this branch.
|
|
177
|
+
#
|
|
178
|
+
# Note: rev-parse returns the stored blob hash; hash-object on a path applies
|
|
179
|
+
# gitattributes filters. A mismatch causes a false negative (trust skipped),
|
|
180
|
+
# which is the safe direction.
|
|
181
|
+
_config_unchanged() {
|
|
182
|
+
local file="$1"
|
|
183
|
+
local base_ref="$2"
|
|
184
|
+
local worktree_path="$3"
|
|
185
|
+
|
|
186
|
+
# Reject symlinks -- trust only regular files with verifiable content
|
|
187
|
+
[[ -L "$worktree_path/$file" ]] && return 1
|
|
188
|
+
|
|
189
|
+
# Get the blob hash directly from git's object database via rev-parse
|
|
190
|
+
local base_hash
|
|
191
|
+
base_hash=$(git rev-parse "$base_ref:$file" 2>/dev/null) || return 1
|
|
192
|
+
|
|
193
|
+
local worktree_hash
|
|
194
|
+
worktree_hash=$(git hash-object "$worktree_path/$file") || return 1
|
|
195
|
+
|
|
196
|
+
[[ "$base_hash" == "$worktree_hash" ]]
|
|
197
|
+
}
|
|
198
|
+
|
|
68
199
|
# Create a new worktree
|
|
69
200
|
create_worktree() {
|
|
70
201
|
local branch_name="$1"
|
|
@@ -107,6 +238,29 @@ create_worktree() {
|
|
|
107
238
|
# Copy environment files
|
|
108
239
|
copy_env_files "$worktree_path"
|
|
109
240
|
|
|
241
|
+
# Trust dev tool configs (mise, direnv) so hooks and scripts work immediately.
|
|
242
|
+
# Long-lived integration branches can use themselves as the trust baseline,
|
|
243
|
+
# while review/PR branches fall back to the default branch and require manual
|
|
244
|
+
# direnv approval.
|
|
245
|
+
local default_branch
|
|
246
|
+
default_branch=$(get_default_branch)
|
|
247
|
+
local trust_branch="$default_branch"
|
|
248
|
+
local allow_direnv_auto="false"
|
|
249
|
+
if is_trusted_base_branch "$from_branch" "$default_branch"; then
|
|
250
|
+
trust_branch="$from_branch"
|
|
251
|
+
allow_direnv_auto="true"
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
if ! git fetch origin "$trust_branch" --quiet; then
|
|
255
|
+
echo -e " ${YELLOW}Warning: could not fetch origin/$trust_branch -- trust check may use stale data${NC}"
|
|
256
|
+
fi
|
|
257
|
+
# Skip trust entirely if the baseline ref doesn't exist locally.
|
|
258
|
+
if git rev-parse --verify "origin/$trust_branch" &>/dev/null; then
|
|
259
|
+
trust_dev_tools "$worktree_path" "origin/$trust_branch" "$allow_direnv_auto"
|
|
260
|
+
else
|
|
261
|
+
echo -e " ${YELLOW}Skipping dev tool trust -- origin/$trust_branch not found locally${NC}"
|
|
262
|
+
fi
|
|
263
|
+
|
|
110
264
|
echo -e "${GREEN}✓ Worktree created successfully!${NC}"
|
|
111
265
|
echo ""
|
|
112
266
|
echo "To switch to this worktree:"
|
|
@@ -321,6 +475,15 @@ Environment Files:
|
|
|
321
475
|
- Creates .backup files if destination already exists
|
|
322
476
|
- Use 'copy-env' to refresh env files after main repo changes
|
|
323
477
|
|
|
478
|
+
Dev Tool Trust:
|
|
479
|
+
- Trusts mise config (.mise.toml, mise.toml, .tool-versions) and direnv (.envrc)
|
|
480
|
+
- Uses trusted base branches directly (main, develop, dev, trunk, staging, release/*)
|
|
481
|
+
- Other branches fall back to the default branch as the trust baseline
|
|
482
|
+
- direnv auto-allow is skipped on non-trusted base branches; review manually first
|
|
483
|
+
- Modified configs are flagged for manual review
|
|
484
|
+
- Only runs if the tool is installed and config exists
|
|
485
|
+
- Prevents hooks/scripts from hanging on interactive trust prompts
|
|
486
|
+
|
|
324
487
|
Examples:
|
|
325
488
|
worktree-manager.sh create feature-login
|
|
326
489
|
worktree-manager.sh create feature-auth develop
|
package/skills/lfg/SKILL.md
CHANGED
|
@@ -23,9 +23,9 @@ CRITICAL: You MUST execute every step below IN ORDER. Do NOT skip any required s
|
|
|
23
23
|
|
|
24
24
|
GATE: STOP. Verify that implementation work was performed - files were created or modified beyond the plan. Do NOT proceed to step 5 if no code changes were made.
|
|
25
25
|
|
|
26
|
-
5. `/ce:review`
|
|
26
|
+
5. `/ce:review mode:autofix`
|
|
27
27
|
|
|
28
|
-
6. `/systematic:
|
|
28
|
+
6. `/systematic:todo-resolve`
|
|
29
29
|
|
|
30
30
|
7. `/systematic:test-browser`
|
|
31
31
|
|