Compare commits

..

No commits in common. "master" and "feat/per-job-log-symlink" have entirely different histories.

7 changed files with 39 additions and 73 deletions

View file

@ -22,29 +22,27 @@ CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-vervang-mij
# Als deze ge-revoked wordt: rebuild + redeploy (zie README). # Als deze ge-revoked wordt: rebuild + redeploy (zie README).
SCRUM4ME_TOKEN=vervang-mij SCRUM4ME_TOKEN=vervang-mij
# ----- Forgejo credentials (PBI-86 hybride model) ----------- # ----- GitHub credentials -----------------------------------
# Personal Access Token van je Forgejo-account met scope read+write # Personal Access Token (fine-grained) met:
# op de Scrum4Me-repos. Variabele heet historisch nog `GH_TOKEN`; # - Repository access op madhura68/Scrum4Me + madhura68/scrum4me-mcp
# in het hybride model bevat 'ie een Forgejo-PAT. # - Permissions: Contents (read/write), Pull requests (read/write),
# Metadata (read)
# #
# Gebruikt voor: # Gebruikt voor:
# 1. Pre-clone van de repos in /home/agent/Projects/<name>/ bij # 1. Pre-clone van de repos in /home/agent/Projects/<name>/ bij
# container-start (entrypoint.sh) # container-start (entrypoint.sh)
# 2. `git push` van agent feature-branches naar Forgejo via HTTPS # 2. `git push` van agent feature-branches via HTTPS
# 3. `gh pr create` (auto_pr=true) — gh CLI leest GH_TOKEN uit env
# #
# `gh pr create` is uit de worker-flow verwijderd (PBI-86, T-1005); # Genereer op github.com → Settings → Developer settings →
# de GitHub-PR ontstaat via de handmatig getriggerde promote-Action # Personal access tokens → Fine-grained tokens.
# in Forgejo. GH_TOKEN=ghp_vervang-mij
#
# Genereer in Forgejo: avatar → Settings → Applications →
# Generate New Token; scope minimaal `write:repository`.
GH_TOKEN=vervang-mij
# Lijst (komma-gescheiden) van repos om vooraf te clonen naar # Lijst (komma-gescheiden) van repos om vooraf te clonen naar
# ~agent/Projects/<name>. resolveRepoRoot in scrum4me-mcp valt # ~agent/Projects/<name>. resolveRepoRoot in scrum4me-mcp valt
# automatisch terug op die conventie. `<owner>/<repo>` zoals 'ie op # automatisch terug op die conventie. Voeg meer toe als je nieuwe
# Forgejo staat. Voeg meer toe als je nieuwe producten/repos toevoegt. # producten/repos toevoegt aan Scrum4Me.
GH_PRECLONE_REPOS=janpeter/Scrum4Me,janpeter/scrum4me-mcp GH_PRECLONE_REPOS=madhura68/Scrum4Me,madhura68/scrum4me-mcp
# ----- Git commit-author ------------------------------------- # ----- Git commit-author -------------------------------------
# Verplicht — Vercel weigert deploys waarvan de commit-author email # Verplicht — Vercel weigert deploys waarvan de commit-author email
@ -110,9 +108,3 @@ AGENT_BACKOFF_MAX=300
AGENT_LOG_GZIP_AFTER_HOURS=24 AGENT_LOG_GZIP_AFTER_HOURS=24
# Hoeveel dagen ge-gzipte logs bewaren voor we ze verwijderen. # Hoeveel dagen ge-gzipte logs bewaren voor we ze verwijderen.
AGENT_LOG_DELETE_AFTER_DAYS=30 AGENT_LOG_DELETE_AFTER_DAYS=30
# Claude CLI --output-format. Default 'stream-json' streamt de volledige
# event-stream (tool-calls, berichten) live naar de run-log; 'text' geeft
# alleen Claude's eind-samenvatting (terser, maar geen live-meekijken).
# stream-json maakt de run-log JSONL — gebruik jq of een viewer.
AGENT_CLAUDE_OUTPUT_FORMAT=stream-json

View file

@ -53,7 +53,7 @@ RUN curl -fsSL https://claude.ai/install.sh | bash -s ${CLAUDE_CODE_VERSION} \
# Clone zonder submodules — de Prisma-schema zit al gecommit in het repo. # Clone zonder submodules — de Prisma-schema zit al gecommit in het repo.
# De vendor/scrum4me submodule is alleen nodig om het schema te updaten, # De vendor/scrum4me submodule is alleen nodig om het schema te updaten,
# niet om te builden. Pin via build-arg; default = main. # niet om te builden. Pin via build-arg; default = main.
ARG MCP_GIT_REPO=https://git.jp-visser.nl/janpeter/scrum4me-mcp.git ARG MCP_GIT_REPO=https://github.com/madhura68/scrum4me-mcp.git
ARG MCP_GIT_REF=main ARG MCP_GIT_REF=main
# Cache-bust voor de clone-laag: hetzelfde MCP_GIT_REF kan tussen rebuilds # Cache-bust voor de clone-laag: hetzelfde MCP_GIT_REF kan tussen rebuilds
# een ander commit aanwijzen (bv. main na een merge). Geef als build-arg # een ander commit aanwijzen (bv. main na een merge). Geef als build-arg

View file

@ -66,7 +66,7 @@ fouten.
`/share/Agent → /share/CACHEDEV1_DATA/Agent`. `/share/Agent → /share/CACHEDEV1_DATA/Agent`.
- Drie subdirs onder die share: `/share/Agent/cache`, `/share/Agent/logs`, - Drie subdirs onder die share: `/share/Agent/cache`, `/share/Agent/logs`,
`/share/Agent/state`. Aanmaken via File Station of via SSH na share-creatie. `/share/Agent/state`. Aanmaken via File Station of via SSH na share-creatie.
- Internet-uitgang naar `api.anthropic.com`, `git.jp-visser.nl` (Forgejo HTTPS-clone/push), `cli.github.com` (build-time voor de gh CLI), je Neon-host, `registry.npmjs.org`. - Internet-uitgang naar `api.anthropic.com`, `github.com`, je Neon-host, `registry.npmjs.org`.
> **Verifieer** vóór je deployt dat `/share/Agent` echt op disk staat: > **Verifieer** vóór je deployt dat `/share/Agent` echt op disk staat:
> ```bash > ```bash
@ -85,21 +85,18 @@ fouten.
# b. SCRUM4ME_TOKEN → log in als de dedicated agent-user in # b. SCRUM4ME_TOKEN → log in als de dedicated agent-user in
# Scrum4Me, /settings/tokens, label "NAS-runner" # Scrum4Me, /settings/tokens, label "NAS-runner"
# c. DATABASE_URL/DIRECT_URL → Neon dashboard # c. DATABASE_URL/DIRECT_URL → Neon dashboard
# d. GH_TOKEN → Forgejo → avatar → Settings → # d. GH_TOKEN → github.com → Settings → Developer settings →
# Applications → Generate New Token; scope # Personal access tokens → Fine-grained.
# minimaal `write:repository` op de twee # Repository access op madhura68/Scrum4Me +
# repos (janpeter/Scrum4Me + janpeter/ # madhura68/scrum4me-mcp; Permissions:
# scrum4me-mcp). Wordt gebruikt voor clone # Contents (RW), Pull requests (RW),
# en push naar Forgejo. PBI-86 (hybride # Metadata (R). Wordt gebruikt voor clone,
# model): `gh pr create` is uit de # push en `gh pr create` (auto_pr).
# worker-flow verwijderd — de GitHub-PR
# komt via de handmatige promote-Action
# in Forgejo.
# 2. Repo op de NAS plaatsen # 2. Repo op de NAS plaatsen
ssh admin@nas ssh admin@nas
cd /share/Agent cd /share/Agent
git clone https://git.jp-visser.nl/<jij>/scrum4me-agent-runner.git git clone https://github.com/<jij>/scrum4me-agent-runner.git
cd scrum4me-agent-runner cd scrum4me-agent-runner
# 3. Env aanmaken # 3. Env aanmaken
@ -329,8 +326,7 @@ Bij elke container-start runt `bin/repo-bootstrap.sh` (als de
`agent`-user, ná drop-privileges) en zet zo'n setup neer: `agent`-user, ná drop-privileges) en zet zo'n setup neer:
1. Configureert git's credential-helper met `GH_TOKEN` zodat 1. Configureert git's credential-helper met `GH_TOKEN` zodat
`git clone`/`push` naar `https://git.jp-visser.nl/...` (Forgejo) zonder `git clone`/`push` naar `https://github.com/...` zonder prompt werkt.
prompt werkt.
2. Voor elke repo in `GH_PRECLONE_REPOS` (komma-gescheiden owner/name): 2. Voor elke repo in `GH_PRECLONE_REPOS` (komma-gescheiden owner/name):
- Bestaat `~/Projects/<name>/.git` al? → `git fetch origin --prune` - Bestaat `~/Projects/<name>/.git` al? → `git fetch origin --prune`
- Anders → fresh `git clone` - Anders → fresh `git clone`
@ -341,10 +337,8 @@ voor jobs landen vervolgens onder `~/.scrum4me-agent-worktrees/<jobId>/`
zodat de hoofd-clone niet wordt aangeraakt. zodat de hoofd-clone niet wordt aangeraakt.
Push gaat over dezelfde token: `git push -u origin feat/story-<id>` Push gaat over dezelfde token: `git push -u origin feat/story-<id>`
slaagt zonder prompt. **`gh pr create` is in PBI-86 (T-1005) verwijderd slaagt zonder prompt. `gh pr create` (voor producten met `auto_pr=true`)
uit de worker-flow** — de GitHub-PR ontstaat via een handmatig gebruikt dezelfde `GH_TOKEN` via de `gh` CLI's standaard env-detect.
getriggerde promote-Action in Forgejo (zie de Scrum4Me-repo
`docs/runbooks/forgejo-hybrid-flow.md`).
## Veelvoorkomende issues ## Veelvoorkomende issues

View file

@ -28,7 +28,7 @@ if [[ -z "$JOB_ID" || -z "$REPO_URL" ]]; then
exit 2 exit 2
fi fi
# Slug uit repo_url voor de cache-naam: "git.jp-visser.nl/foo/bar.git" → "foo_bar" # Slug uit repo_url voor de cache-naam: "github.com/foo/bar.git" → "foo_bar"
SLUG=$(echo "$REPO_URL" \ SLUG=$(echo "$REPO_URL" \
| sed -E 's#^.*[:/]([^/]+/[^/]+?)(\.git)?/?$#\1#' \ | sed -E 's#^.*[:/]([^/]+/[^/]+?)(\.git)?/?$#\1#' \
| tr '/' '_') | tr '/' '_')

View file

@ -32,8 +32,8 @@ fi
mkdir -p "$HOME" mkdir -p "$HOME"
git config --global credential.helper store git config --global credential.helper store
CREDS_FILE="$HOME/.git-credentials" CREDS_FILE="$HOME/.git-credentials"
if [[ ! -f "$CREDS_FILE" ]] || ! grep -q "oauth2:${GH_TOKEN}@git.jp-visser.nl" "$CREDS_FILE" 2>/dev/null; then if [[ ! -f "$CREDS_FILE" ]] || ! grep -q "oauth2:${GH_TOKEN}@github.com" "$CREDS_FILE" 2>/dev/null; then
printf 'https://oauth2:%s@git.jp-visser.nl\n' "$GH_TOKEN" > "$CREDS_FILE" printf 'https://oauth2:%s@github.com\n' "$GH_TOKEN" > "$CREDS_FILE"
chmod 600 "$CREDS_FILE" chmod 600 "$CREDS_FILE"
log "git credentials helper configured at ${CREDS_FILE}" log "git credentials helper configured at ${CREDS_FILE}"
fi fi
@ -71,7 +71,7 @@ for repo in "${REPOS[@]}"; do
else else
log "cloning ${repo} into ${target}" log "cloning ${repo} into ${target}"
rm -rf "$target" rm -rf "$target"
git clone --quiet "https://git.jp-visser.nl/${repo}.git" "$target" \ git clone --quiet "https://github.com/${repo}.git" "$target" \
|| { log "ERROR: clone failed for ${repo}"; continue; } || { log "ERROR: clone failed for ${repo}"; continue; }
fi fi
done done

View file

@ -80,12 +80,8 @@ while true; do
# Token-expiry detectie: run-one-job.ts retourneert exit 3 wanneer het # Token-expiry detectie: run-one-job.ts retourneert exit 3 wanneer het
# bekende auth-error-strings in Claude's output ziet. We checken óók de # bekende auth-error-strings in Claude's output ziet. We checken óók de
# log-tekst voor het geval een ander pad het patroon raakt (bv. Prisma- # log-tekst voor het geval een ander pad het patroon raakt (bv. Prisma-
# connection-error met OAuth-expired in error-body) — maar alléén bij een # connection-error met OAuth-expired in error-body).
# niet-nul exit. Het run-log bevat de volledige stream-json output (incl. if [[ "$exit_code" -eq 3 ]] || grep -qE '(invalid_api_key|authentication.*failed|401.*unauthor|OAuth.*expired)' "${run_log}"; then
# tool-results én run-one-job's eigen "TOKEN_EXPIRED detected"-logregel),
# dus een geslaagde job die toevallig "401 unauthorized" in z'n output
# heeft mag de grep-fallback niet triggeren.
if [[ "$exit_code" -eq 3 ]] || { [[ "$exit_code" -ne 0 ]] && grep -qE '(invalid_api_key|authentication.*failed|401.*unauthor|OAuth.*expired)' "${run_log}"; }; then
log "AUTH FAILURE detected (exit=$exit_code or pattern in log) — marking TOKEN_EXPIRED" log "AUTH FAILURE detected (exit=$exit_code or pattern in log) — marking TOKEN_EXPIRED"
touch "${AGENT_STATE_DIR}/TOKEN_EXPIRED" touch "${AGENT_STATE_DIR}/TOKEN_EXPIRED"
write_state "$(jq -n \ write_state "$(jq -n \

View file

@ -292,13 +292,6 @@ async function main(): Promise<number> {
// 7. Build CLI args. // 7. Build CLI args.
const promptText = getKindPromptText(ctx.kind).replace('$PAYLOAD_PATH', payloadPath) const promptText = getKindPromptText(ctx.kind).replace('$PAYLOAD_PATH', payloadPath)
// --output-format is configureerbaar via env. Default 'stream-json' geeft
// de volledige event-stream (elke tool-call, elk bericht) live in de
// run-log, i.p.v. alleen Claude's eind-samenvatting. stream-json vereist
// --verbose in print-mode. Zet AGENT_CLAUDE_OUTPUT_FORMAT=text terug voor
// de oude terse output. TOKEN_EXPIRED-detectie werkt ongewijzigd: de
// auth-error-strings staan ook binnen de JSON-events.
const outputFormat = process.env.AGENT_CLAUDE_OUTPUT_FORMAT ?? 'stream-json'
const args: string[] = [ const args: string[] = [
'-p', '-p',
promptText, promptText,
@ -313,9 +306,8 @@ async function main(): Promise<number> {
'--add-dir', '--add-dir',
'/opt/agent', '/opt/agent',
'--output-format', '--output-format',
outputFormat, 'text',
] ]
if (outputFormat === 'stream-json') args.push('--verbose')
if (effort) args.push('--effort', effort) if (effort) args.push('--effort', effort)
const cwd = worktreePath ?? '/opt/agent' const cwd = worktreePath ?? '/opt/agent'
@ -383,21 +375,13 @@ async function main(): Promise<number> {
`duration_ms=${durationMs} wall_clock_seconds=${Math.round(durationMs / 1000)}`, `duration_ms=${durationMs} wall_clock_seconds=${Math.round(durationMs / 1000)}`,
) )
// 10. Token-expiry detection — alleen als Claude zelf non-zero eindigde. // 10. Token-expiry detection.
// stdoutBuf bevat de volledige stream-json output incl. álle tool-results,
// dus de auth-error-strings kunnen ook agent-werk-content zijn (een doc
// over 401-handling gelezen, een endpoint getest). Een echte credential-
// fout laat 'claude' non-zero exiten; een geslaagde run (exit 0) is per
// definitie geen token-expiry. Zonder deze gate legt zulke content de
// worker onterecht plat (run-agent.sh → TOKEN_EXPIRED marker + sleep).
let tokenExpired = false let tokenExpired = false
if (exitCode !== 0) { for (const pat of TOKEN_EXPIRY_PATTERNS) {
for (const pat of TOKEN_EXPIRY_PATTERNS) { if (pat.test(stdoutBuf)) {
if (pat.test(stdoutBuf)) { tokenExpired = true
tokenExpired = true log(`TOKEN_EXPIRED detected pattern="${pat.source}" exiting code=3`)
log(`TOKEN_EXPIRED detected pattern="${pat.source}" exiting code=3`) break
break
}
} }
} }