Autoresearch is an autonomous research system. research.sh orchestrates Claude Code (claude -p) to research GitHub Issues, open PRs, and build a verified research document. A NestJS backend provides REST API access and hourly cron scheduling.
Repo: https://github.com/danlex/autoresearch Owner: Alexandru DAN (danlex, dan_lex@yahoo.com)
- Always
set -euo pipefailat the top of every script - Never embed user-derived variables in regex — use
grep -F(fixed-string) or exact string comparison ([[ "$a" == "$b" ]]) - Never pass large strings as shell arguments — write to temp file, pipe via stdin to avoid
ARG_MAXlimits - Always quote variables:
"$var"not$var— prevents word splitting and glob expansion - Use
${var:-default}for optional env vars, fail explicitly withexit 1for required ones - Never use
evalor backtick command substitution — use$(...)instead - Sanitize any input used in file paths — prevent path traversal
- Clean up temp files in a trap:
trap 'rm -f "$tmpfile"' EXIT - Never store secrets in scripts or commit them — use env vars or
.envfiles (gitignored)
- Always handle command failures:
cmd || { log "failed"; return 1; } - Check if files exist before reading:
[[ -f "$file" ]] || { log "missing"; exit 1; } - Use
2>/dev/null || trueonly when you intentionally want to ignore errors — add a comment explaining why - Separate
localdeclaration from assignment to avoid masking return values:# Bad — masks the return value of cmd local x="$(cmd)" # Good local x x=$(cmd)
- Validate numeric values before arithmetic:
[[ "$val" =~ ^[0-9]+$ ]] || val=0
- Functions:
snake_case - Local variables:
snake_case - Constants/env vars:
UPPER_SNAKE_CASE - Use
localfor all function variables — no globals except top-level state - One function per concern — keep functions under 40 lines
- Add a header comment block for each script
- Indent with 2 spaces
- Use
[[ ]]not[ ]for conditionals - Use
$(( ))for arithmetic, notexpr
- All
.shfiles must passshellcheckwith zero warnings - Run
shellcheck *.shbefore every commit - Address warnings properly — don't just add
# shellcheck disableunless truly necessary with an explanation
- Always handle
ghfailures — network/rate limits are common - Pipe JSON through
jq— never parse structured data with grep/sed/awk - Use
--json+--jqflags instead of separatejqcalls where possible - Use
--state openexplicitly — don't rely on defaults - Be aware of pagination —
ghreturns max 30 items by default, use--limitfor more
- Cache values that don't change within a loop iteration (e.g., subject parsed from goal.md)
- Minimize subprocess spawning in hot paths — prefer bash builtins over external commands
- Use
$(<file)instead of$(cat file)for reading files (bash builtin, no fork)
- Follow NestJS conventions: Module → Controller → Service
- Controllers handle HTTP concerns (routing, validation, response shaping)
- Services contain business logic and file I/O
- One module per domain (e.g.,
ResearchModule)
- Wrap all
JSON.parsecalls in try/catch — files may be partially written by concurrent processes - Use configurable paths via env vars (e.g.,
ROOT_DIR) — never rely on__dirnametraversal alone - Validate all user input at the controller level with DTOs and
class-validator - Enable global
ValidationPipe— already done inmain.ts - Use
@MaxLength()on string inputs to prevent abuse - Return consistent error shapes:
{ error: string }
- Use TypeScript strict mode where practical
- Prefer
readonlyfor injected services and config - Use
Loggerfrom@nestjs/common, notconsole.log - DTOs: one class per request body, decorators for validation
- Name files:
feature.controller.ts,feature.service.ts,feature.module.ts - Use
async/awaitover raw Promises
- Services should throw
HttpExceptionsubclasses for expected errors - Unexpected errors bubble to NestJS global exception filter
- Log errors with context:
this.logger.error(message, stack) - Never expose internal paths or stack traces in API responses
- Pin major versions in
package.json - Run
npm auditbefore adding new dependencies - Prefer NestJS ecosystem packages (
@nestjs/schedule,@nestjs/config) over generic alternatives
- Tests exist to catch regressions, not to achieve coverage numbers
- Test behavior, not implementation — test what functions return, not how they compute it
- Mock external boundaries (GitHub API, Claude CLI, filesystem) — not internal functions
- Every bug fix should come with a test that would have caught it
shellcheck *.sh— zero warningsbash tests/test_functions.sh— all bash unit tests passcd backend && npm test— all NestJS tests pass
| Category | Location | Runner | What it tests |
|---|---|---|---|
| Bash unit | tests/test_functions.sh |
bash | Pure functions: slugify, section parsing, score calc |
| Bash integration | tests/test_research_loop.sh |
bash | Full iteration with mocked gh/claude |
| NestJS unit | backend/src/**/*.spec.ts |
jest | Service methods, file I/O, process spawning |
| NestJS e2e | backend/test/app.e2e-spec.ts |
jest + supertest | API endpoints end-to-end |
- GitHub CLI: Create mock
ghscript that returns fixture JSON - Claude CLI: Create mock
claudescript that modifies document.md predictably - Filesystem: Use temp directories for isolation
- Time: Use fixed timestamps in test assertions
git -c user.name="Alexandru DAN" -c user.email="dan_lex@yahoo.com" commitFormat: type: short description
Types: feat, fix, docs, test, refactor, chore
Always include:
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mainis protected — all changes via PRs (when judges are active)- Task branches:
task/{issue_number}-{slug} - Clean up local branches after merge
- Never force-push to main
goal.md→ research intent (human-edited)document.md→ research findings (Claude-written, judge-verified)- GitHub Issues → all tasks (created, tracked, closed there)
autoresearch.sh→ scoring formulastatus.json→ runtime loop state
research.shis the only file that callsclaude -p- NestJS backend spawns
research.sh— never calls Claude directly - Score is computed by
autoresearch.sh— research.sh doesn't hardcode scoring logic - GitHub is the task store — no task state in local files
goal.md: human-only writes (system reads)document.md: Claude writes, judges verify, humans readstatus.json,research.log: research.sh writes, backend readsfeedback.md: human/backend writes, research.sh reads and deletespause.flag: human/backend creates/removes, research.sh polls
These are ephemeral and must never be committed:
status.json— may be partially writtenresearch.log— grows unboundedfeedback.md— consumed and deleted each iterationpause.flag— presence/absence is the signal.prompt-*.txt— temp prompt files.env— secrets
- Never commit API keys, tokens, or secrets
.envfiles are gitignored with chmod 600deploy.shprompts for keys with hidden input (read -rsp)- Validate and sanitize all inputs at system boundaries (API endpoints, Issue body parsing)
- Don't trust Issue body content — treat it as untrusted input when building shell commands
- Use
jqfor JSON handling — never construct JSON with string concatenation
Before approving any change:
-
shellcheck *.shpasses - All tests pass
- No secrets in diff
- No hardcoded paths or values that should be configurable
- Error paths handled (not just happy path)
- New functions have corresponding tests
- Commit message follows convention