forgedock 1.24.4 → 1.25.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/bin/engine/outcome-codes.mjs +11 -0
- package/bin/engine/phases.mjs +22 -6
- package/bin/engine/runlog.mjs +1 -1
- package/bin/engine-cli.mjs +186 -22
- package/bin/engine.mjs +3 -1
- package/bin/forgedock.mjs +17 -5
- package/bin/lean/batch.mjs +147 -0
- package/bin/lean/batch.test.mjs +184 -0
- package/bin/lean/lane.mjs +85 -0
- package/bin/lean/lane.test.mjs +91 -0
- package/bin/lean/lean.mjs +139 -11
- package/bin/lean/lean.test.mjs +390 -12
- package/bin/lean/orchestrate.mjs +229 -29
- package/bin/lean/prefilter.mjs +148 -0
- package/bin/lean/prefilter.test.mjs +210 -0
- package/bin/lean/prompts.mjs +80 -3
- package/bin/lean/retry.mjs +2 -2
- package/bin/runner.mjs +25 -3
- package/commands/orchestrate.md +62 -21
- package/commands/review-pr-staging.md +28 -3
- package/docs/CONFIG.md +2 -0
- package/docs/OUTCOME-CODES.md +11 -4
- package/package.json +1 -1
- package/scripts/code-index.sh +61 -40
- package/scripts/code-index.test.sh +73 -0
- package/scripts/review-pr-staging-scope.test.sh +36 -1
- package/scripts/review-prepare.sh +16 -3
- package/scripts/review-prepare.test.sh +16 -0
package/docs/OUTCOME-CODES.md
CHANGED
|
@@ -43,12 +43,19 @@ An absent or unknown code is recorded as `unclassified`, with the raw reason kep
|
|
|
43
43
|
| `infra.disk_quota` | Disk full or quota exhausted (EDQUOT/ENOSPC, errno -28/-122); retryable infra failure, never `needs-human` | engine | Runner error text |
|
|
44
44
|
| `infra.cli_spawn_transient` | CLI spawn failed transiently before the child ran (ETIMEDOUT, EAGAIN, ENOMEM, EMFILE); retried with backoff on a separate bounded counter that does not consume a phase attempt | engine | `err.code` `CLI_SPAWN_TRANSIENT` |
|
|
45
45
|
| `cli.timeout` | CLI child ran and was killed at the wall-clock limit; a normal attempt (consumes one of `maxAttempts`), usage recorded when the CLI emitted any | engine | `err.code` `CLI_TIMEOUT` |
|
|
46
|
+
| `cli.spawn_failed` | The CLI could not be started for a reason that repeats on every rerun (`E2BIG` argv too large, `ENOENT`/`EACCES`/`ENOEXEC` binary missing or not executable, `ENAMETOOLONG`). Core work-on ends `needs-human` with this code instead of `engine-error`, so `forgedock orchestrate` does not spend its relaunch on it | lean, engine | `err.code` `CLI_SPAWN_FAILED` (E2BIG) or `err.spawnDeterministic` (other errnos keep their `err.code`); `err.spawnErrno` holds the errno |
|
|
46
47
|
| `lease.held` | Another live agent holds the issue lease | engine | Foreign live lease on acquire |
|
|
47
|
-
| `dependency.open` | A trusted `**Depends on**: #N` dependency is
|
|
48
|
-
| `dependency.unreadable` | The issue body or a declared dependency could not be read, so the run was refused (fail closed) | engine | Failed or unparsable `gh api` read |
|
|
48
|
+
| `dependency.open` | A dependency is still open: a trusted `**Depends on**: #N` line refused the run before any state write, or the core builder reported `block.kind: "dependency"` naming an issue that is open. Carries `dependencies`; never `needs-human`. `forgedock orchestrate` adds the edge and requeues the issue behind it, without skipping dependents | engine, lean | Trusted `**Depends on**` line, or `result.json` `block.issues`, + the dependency issue's live open state |
|
|
49
|
+
| `dependency.unreadable` | The issue body or a declared dependency could not be read, so the run was refused (fail closed) | engine, lean | Failed or unparsable `gh api` / `gh issue view` read |
|
|
50
|
+
| `dependency.blocked` | Core work-on: the builder's dependency is open and itself labelled `needs-human`. Terminal reason `dependency-blocked` (blocked by #X); this issue is not labelled `needs-human` | lean | `result.json` `block.kind: "dependency"` + the dependency's `needs-human` label |
|
|
51
|
+
| `lane.mismatch` | Core work-on: `--lane` differs from the lane worked out from the issue (milestone → `branches.feature_pattern`, else `branches.staging`); refused before any lease, comment or worktree. `--lane-override` forces it and is recorded in the trajectory. A resumed PR whose base is not the lane is refused the same way before merge | lean | Issue milestone + forge.yaml `branches`, or the open PR's `baseRefName` |
|
|
52
|
+
| `lane.default_branch` | Core work-on: the requested or resolved lane is the default (production) branch (forge.yaml `branches.default` or the GitHub default branch); never a lane, refused at every entry point and before PR create and merge | lean, cli | forge.yaml `branches.default`, `gh repo view` `defaultBranchRef` |
|
|
53
|
+
| `lane.branch_missing` | Core work-on: the lane branch (e.g. `milestone/<slug>`) does not exist on origin; fail closed | lean | `git ls-remote --heads origin` |
|
|
54
|
+
| `lane.unresolved` | Core work-on: the lane could not be worked out (milestone or default branch unreadable, empty milestone slug, bad `feature_pattern`); fail closed | lean | Failed `gh` read or config |
|
|
49
55
|
| `invalid` | Issue is invalid or in a divergent closed state | investigate, engine | `INVESTIGATION:INVALID` marker, or live snapshot |
|
|
50
|
-
| `decomposed` | Issue was decomposed into sub-issues | investigate, decompose | `FORGE:DECOMPOSED` or the decomposition assessment |
|
|
51
|
-
| `builder.blocked` | Core work-on: the builder reported
|
|
56
|
+
| `decomposed` | Issue was decomposed into sub-issues. In core work-on, the builder reported `block.kind: "split-required"`; the engine files the sub-issues (`**Parent**` and `**Depends on**` lines) and returns them as `subissues` for `forgedock orchestrate` to run | investigate, decompose, lean | `FORGE:DECOMPOSED` or the decomposition assessment; `result.json` `block.subissues` |
|
|
57
|
+
| `builder.blocked` | Core work-on: the builder reported an external action agents cannot take, or a product decision too critical for agents: `block.kind: "human"` with `why` one of `credentials`, `spend`, `promotion`, `customer`, `product-decision`. An upstream issue, a needed split, or prod data that monitoring access can read never count | lean | `result.json` status `blocked` + typed `block` |
|
|
58
|
+
| `builder.block_untyped` | Core work-on: the builder stayed `blocked` without a valid typed reason (missing `block`, a non-human `why` such as prod data, or a dependency that is not open) after one corrective resume | lean | `result.json` status `blocked`, twice |
|
|
52
59
|
| `builder.no_evidence` | Core work-on: the builder reported invalid/already-fixed without file:line evidence, so the issue was not closed | lean | `result.json` with empty `evidence` |
|
|
53
60
|
| `builder.no_commit` | Core work-on: the builder made no commit for a fix | lean | No changed files after the builder session |
|
|
54
61
|
| `review.failed` | Core work-on: the fix/regression review failed twice (or could not resume the builder) | lean | Reviewer verdict FAIL |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|
package/scripts/code-index.sh
CHANGED
|
@@ -154,6 +154,29 @@ fi
|
|
|
154
154
|
echo "INFO: Building code index for HEAD ${HEAD_SHA:0:8}..."
|
|
155
155
|
mkdir -p "$CACHE_DIR"
|
|
156
156
|
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Step 0: File list (forge#4126)
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Enumerate files ONCE, from the git index when available: a filesystem walk also descends into nested
|
|
161
|
+
# worktrees (.forge/worktrees, .claude/worktrees), virtualenvs and build output inside the checkout —
|
|
162
|
+
# ~30x the tracked tree on a real repo. Outside git, fall back to a find that prunes those directories.
|
|
163
|
+
# Every step below reads this list (paths relative to REPO_PATH); none walks the tree itself.
|
|
164
|
+
FILE_LIST="${CACHE_DIR}/.files"
|
|
165
|
+
if command -v git >/dev/null 2>&1 && git -C "$REPO_PATH" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
166
|
+
git -C "$REPO_PATH" -c core.quotepath=off ls-files > "$FILE_LIST" 2>/dev/null || true
|
|
167
|
+
else
|
|
168
|
+
(cd "$REPO_PATH" && find . \( -name .git -o -name node_modules -o -name __pycache__ -o -name .forge \
|
|
169
|
+
-o -name .claude -o -name .venv -o -name venv \) -prune -o -type f -print 2>/dev/null | sed 's|^\./||') \
|
|
170
|
+
> "$FILE_LIST" || true
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
# src_files <ext>... — absolute paths of listed files with one of the given extensions.
|
|
174
|
+
src_files() {
|
|
175
|
+
local re
|
|
176
|
+
re="\\.($(IFS='|'; echo "$*"))\$"
|
|
177
|
+
grep -E "$re" "$FILE_LIST" 2>/dev/null | awk -v p="$REPO_PATH" '{print p "/" $0}' || true
|
|
178
|
+
}
|
|
179
|
+
|
|
157
180
|
# ---------------------------------------------------------------------------
|
|
158
181
|
# Step 1: Symbol extraction
|
|
159
182
|
# ---------------------------------------------------------------------------
|
|
@@ -163,12 +186,13 @@ SYMBOLS_FILE="${CACHE_DIR}/symbols.tsv"
|
|
|
163
186
|
echo "INFO: Extracting symbols..."
|
|
164
187
|
|
|
165
188
|
if command -v ctags >/dev/null 2>&1; then
|
|
166
|
-
# Use ctags for precise symbol extraction
|
|
167
189
|
# --fields=+ne adds line number and end-of-scope — portable across universal-ctags
|
|
168
|
-
ctags -
|
|
190
|
+
# Use ctags for precise symbol extraction over the listed files only (-L, never -R: forge#4126).
|
|
191
|
+
# Paths are relative to REPO_PATH, matching the grep fallback and the caller graph.
|
|
192
|
+
(cd "$REPO_PATH" && ctags --output-format=json \
|
|
169
193
|
--fields='nkz' \
|
|
170
194
|
--languages=Python,JavaScript,TypeScript,Go,Rust,Java,C,C++ \
|
|
171
|
-
-
|
|
195
|
+
-L "$FILE_LIST" -f - 2>/dev/null) \
|
|
172
196
|
| grep -v '^!' \
|
|
173
197
|
| while IFS= read -r line; do
|
|
174
198
|
# Parse JSON output from universal-ctags. Portable (non-PCRE) extraction —
|
|
@@ -205,8 +229,7 @@ if [[ ! -s "$SYMBOLS_FILE" ]] || ! command -v ctags >/dev/null 2>&1; then
|
|
|
205
229
|
echo "INFO: Falling back to grep-based symbol extraction"
|
|
206
230
|
|
|
207
231
|
# Python: functions and classes
|
|
208
|
-
|
|
209
|
-
-not -path "*/__pycache__/*" 2>/dev/null | while read -r f; do
|
|
232
|
+
src_files py | while read -r f; do
|
|
210
233
|
grep -n "^def \|^ def \|^class " "$f" 2>/dev/null | while IFS=: read -r lnum content; do
|
|
211
234
|
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
212
235
|
# grep build on Windows, so use a bash regex instead of a lookbehind.
|
|
@@ -222,8 +245,7 @@ if [[ ! -s "$SYMBOLS_FILE" ]] || ! command -v ctags >/dev/null 2>&1; then
|
|
|
222
245
|
done >> "$SYMBOLS_FILE" 2>/dev/null || true
|
|
223
246
|
|
|
224
247
|
# JavaScript/TypeScript: functions and classes
|
|
225
|
-
|
|
226
|
-
-not -path "*/.git/*" -not -path "*/node_modules/*" 2>/dev/null | while read -r f; do
|
|
248
|
+
src_files js ts tsx jsx | while read -r f; do
|
|
227
249
|
grep -n "^export function \|^function \|^export class \|^class \|^const .* = \(function\|async function\|() =>\)" \
|
|
228
250
|
"$f" 2>/dev/null | while IFS=: read -r lnum content; do
|
|
229
251
|
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
@@ -250,8 +272,7 @@ IMPORTS_FILE="${CACHE_DIR}/imports.tsv"
|
|
|
250
272
|
echo "INFO: Building import graph..."
|
|
251
273
|
|
|
252
274
|
# Python imports
|
|
253
|
-
|
|
254
|
-
2>/dev/null | while read -r f; do
|
|
275
|
+
src_files py | while read -r f; do
|
|
255
276
|
rel_path="${f#$REPO_PATH/}"
|
|
256
277
|
grep -n "^import \|^from " "$f" 2>/dev/null | while IFS=: read -r lnum content; do
|
|
257
278
|
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
@@ -271,8 +292,7 @@ find "$REPO_PATH" -name "*.py" -not -path "*/.git/*" -not -path "*/__pycache__/*
|
|
|
271
292
|
done >> "$IMPORTS_FILE" 2>/dev/null || true
|
|
272
293
|
|
|
273
294
|
# JavaScript/TypeScript imports
|
|
274
|
-
|
|
275
|
-
-not -path "*/.git/*" -not -path "*/node_modules/*" 2>/dev/null | while read -r f; do
|
|
295
|
+
src_files js ts tsx jsx | while read -r f; do
|
|
276
296
|
rel_path="${f#$REPO_PATH/}"
|
|
277
297
|
grep -n "^import \|^} from \|require(" "$f" 2>/dev/null | while IFS=: read -r lnum content; do
|
|
278
298
|
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
@@ -289,7 +309,7 @@ find "$REPO_PATH" \( -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.j
|
|
|
289
309
|
done >> "$IMPORTS_FILE" 2>/dev/null || true
|
|
290
310
|
|
|
291
311
|
# Go imports
|
|
292
|
-
|
|
312
|
+
src_files go | while read -r f; do
|
|
293
313
|
rel_path="${f#$REPO_PATH/}"
|
|
294
314
|
grep -n '"[^"]*"' "$f" 2>/dev/null | grep -v "//\|fmt.Print\|errors.New" | while IFS=: read -r lnum content; do
|
|
295
315
|
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
@@ -311,28 +331,32 @@ CALLERS_FILE="${CACHE_DIR}/callers.tsv"
|
|
|
311
331
|
|
|
312
332
|
echo "INFO: Building caller graph..."
|
|
313
333
|
|
|
314
|
-
#
|
|
334
|
+
# One pass (forge#4126): collect every `name(` occurrence across the listed sources with a single grep, then
|
|
335
|
+
# join it in awk against the first 500 symbols. The old loop ran one full find|grep tree walk per symbol.
|
|
336
|
+
# One row per (symbol, caller file) at the first call line; files defining the symbol are skipped.
|
|
315
337
|
if [[ -s "$SYMBOLS_FILE" ]]; then
|
|
316
|
-
|
|
317
|
-
head -500 "$SYMBOLS_FILE"
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
338
|
+
CALLEE_FILE="${CACHE_DIR}/.callees"
|
|
339
|
+
head -500 "$SYMBOLS_FILE" > "$CALLEE_FILE"
|
|
340
|
+
(cd "$REPO_PATH" && grep -E '\.(py|js|ts|go|rs|java)$' "$FILE_LIST" | tr '\n' '\0' \
|
|
341
|
+
| xargs -0 grep -n -o -H -E '[A-Za-z_][A-Za-z0-9_]*\(' -- 2>/dev/null) \
|
|
342
|
+
| awk -v syms="$CALLEE_FILE" '
|
|
343
|
+
BEGIN {
|
|
344
|
+
while ((getline l < syms) > 0) {
|
|
345
|
+
split(l, s, "\t")
|
|
346
|
+
# Skip very short or generic names to avoid false positives
|
|
347
|
+
if (length(s[1]) >= 3) { want[s[1]] = 1; def[s[1] SUBSEP s[2]] = 1 }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
{
|
|
351
|
+
# file:line:name( — split from the right so a ":" in the file name survives.
|
|
352
|
+
n = split($0, a, ":"); id = a[n]; sub(/\($/, "", id); ln = a[n-1]
|
|
353
|
+
f = substr($0, 1, length($0) - length(a[n]) - length(ln) - 2)
|
|
354
|
+
k = id SUBSEP f
|
|
355
|
+
if (!(id in want) || (k in def) || (k in seen)) next
|
|
356
|
+
seen[k] = 1
|
|
357
|
+
print id "\t" f "\t" ln
|
|
358
|
+
}' >> "$CALLERS_FILE" 2>/dev/null || true
|
|
359
|
+
# ^ grep exits 1 when nothing matches; guard so `set -e`/`pipefail` never aborts the build here.
|
|
336
360
|
fi
|
|
337
361
|
|
|
338
362
|
# ---------------------------------------------------------------------------
|
|
@@ -375,9 +399,8 @@ if [[ -f "$FORGE_YAML" ]]; then
|
|
|
375
399
|
fi
|
|
376
400
|
|
|
377
401
|
# Map each file to its domain
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
rel_path="${f#$REPO_PATH/}"
|
|
402
|
+
while IFS= read -r rel_path; do
|
|
403
|
+
case "$rel_path" in .forge/*|*/node_modules/*|node_modules/*|*/__pycache__/*) continue ;; esac
|
|
381
404
|
domain="unclassified"
|
|
382
405
|
for pattern in "${!DOMAIN_PATTERNS[@]}"; do
|
|
383
406
|
if [[ "$rel_path" == $pattern* ]]; then
|
|
@@ -386,7 +409,7 @@ find "$REPO_PATH" -type f -not -path "*/.git/*" -not -path "*/node_modules/*" \
|
|
|
386
409
|
fi
|
|
387
410
|
done
|
|
388
411
|
echo -e "${rel_path}\t${domain}"
|
|
389
|
-
done >> "$DOMAIN_FILE" 2>/dev/null || true
|
|
412
|
+
done < "$FILE_LIST" >> "$DOMAIN_FILE" 2>/dev/null || true
|
|
390
413
|
|
|
391
414
|
# ---------------------------------------------------------------------------
|
|
392
415
|
# Step 5: Write index manifest
|
|
@@ -401,9 +424,7 @@ FILE_COUNT=$(wc -l < "$DOMAIN_FILE" 2>/dev/null || echo 0)
|
|
|
401
424
|
# found, but extraction came back empty, a loop was truncated somewhere above
|
|
402
425
|
# (or a tool like grep -oP is unavailable/incompatible on this platform) — flag
|
|
403
426
|
# it instead of letting "Index built successfully" silently paper over data loss.
|
|
404
|
-
SRC_FILE_COUNT=$(
|
|
405
|
-
-o -name "*.tsx" -o -name "*.jsx" -o -name "*.go" \) -not -path "*/.git/*" \
|
|
406
|
-
-not -path "*/node_modules/*" -not -path "*/__pycache__/*" 2>/dev/null | wc -l | tr -d ' ')
|
|
427
|
+
SRC_FILE_COUNT=$(src_files py js ts tsx jsx go | wc -l | tr -d ' ')
|
|
407
428
|
INDEX_TRUNCATED=0
|
|
408
429
|
if [[ "$SRC_FILE_COUNT" -gt 0 && "$SYMBOL_COUNT" -eq 0 ]]; then
|
|
409
430
|
INDEX_TRUNCATED=1
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Tests for scripts/code-index.sh: the index covers the tracked tree only — nested worktrees (.forge/worktrees,
|
|
6
|
+
# .claude/worktrees) and virtualenvs inside the checkout are never walked (forge#4126) — and the one-pass caller
|
|
7
|
+
# graph still maps callees to their callers. Run: bash scripts/code-index.test.sh (bash 3.2 compatible)
|
|
8
|
+
|
|
9
|
+
set -u
|
|
10
|
+
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
11
|
+
ROOT=$(cd "$DIR/.." && pwd)
|
|
12
|
+
SCRIPT="${CODE_INDEX_UNDER_TEST:-$ROOT/scripts/code-index.sh}" # override to run the suite against another copy
|
|
13
|
+
pass=0; fail=0
|
|
14
|
+
ok() { pass=$((pass+1)); }
|
|
15
|
+
bad() { fail=$((fail+1)); echo "FAIL: $1"; }
|
|
16
|
+
WORK=$(mktemp -d "${TMPDIR:-/tmp}/code-index-test.XXXXXX")
|
|
17
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
18
|
+
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
|
19
|
+
|
|
20
|
+
# mksrc <dir> — a small Python + JS source tree; helper_fn is defined in app.py and called from use.py.
|
|
21
|
+
mksrc() {
|
|
22
|
+
mkdir -p "$1/src" "$1/web"
|
|
23
|
+
printf 'def helper_fn():\n return helper_fn()\n\nclass Widget:\n pass\n' > "$1/src/app.py"
|
|
24
|
+
printf 'from src.app import helper_fn\n\n\ndef run_it():\n return helper_fn()\n' > "$1/src/use.py"
|
|
25
|
+
printf "import x from './y'\nexport function renderPage() {}\n" > "$1/web/page.js"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# --- 1. git checkout with nested worktrees and a venv inside it ---
|
|
29
|
+
REPO="$WORK/repo"; mksrc "$REPO"
|
|
30
|
+
git -C "$REPO" init -q -b main
|
|
31
|
+
git -C "$REPO" add -A; git -C "$REPO" commit -qm init
|
|
32
|
+
git -C "$REPO" worktree add -q -b wt1 "$REPO/.claude/worktrees/one" 2>/dev/null
|
|
33
|
+
git -C "$REPO" worktree add -q -b wt2 "$REPO/.forge/worktrees/two" 2>/dev/null
|
|
34
|
+
printf 'def worktree_only_fn():\n helper_fn()\n' > "$REPO/.claude/worktrees/one/src/wt_only.py"
|
|
35
|
+
printf 'def forge_wt_only_fn():\n helper_fn()\n' > "$REPO/.forge/worktrees/two/src/fwt_only.py"
|
|
36
|
+
mkdir -p "$REPO/.venv/lib"; printf 'def venv_only_fn():\n helper_fn()\n' > "$REPO/.venv/lib/site.py"
|
|
37
|
+
|
|
38
|
+
OUT="$WORK/out"
|
|
39
|
+
bash "$SCRIPT" --repo-path "$REPO" --output "$OUT" > "$WORK/log" 2>&1; rc=$?
|
|
40
|
+
if [ "$rc" -eq 0 ]; then ok; else bad "build exit $rc: $(head -c 400 "$WORK/log")"; fi
|
|
41
|
+
C=$(ls -d "$OUT"/*/ 2>/dev/null | head -n 1)
|
|
42
|
+
for f in symbols imports callers file_domain; do
|
|
43
|
+
if [ -s "$C/$f.tsv" ]; then ok; else bad "$f.tsv is empty"; fi
|
|
44
|
+
if grep -qE '\.claude/|\.forge/|\.venv/|worktree_only_fn|forge_wt_only_fn|venv_only_fn' "$C/$f.tsv" 2>/dev/null; then
|
|
45
|
+
bad "$f.tsv indexed a nested worktree or venv: $(grep -E '\.claude/|\.forge/|\.venv/|_only_fn' "$C/$f.tsv" | head -n 3 | tr '\n' ' ')"
|
|
46
|
+
else ok; fi
|
|
47
|
+
done
|
|
48
|
+
if grep -q '^helper_fn src/app.py 1 ' "$C/symbols.tsv"; then ok; else bad "helper_fn symbol missing or path not relative: $(cat "$C/symbols.tsv")"; fi
|
|
49
|
+
if grep -q '"files": 3' "$C/index.json"; then ok; else bad "file count should be the 3 tracked files: $(grep '"files":' "$C/index.json" | head -n 1)"; fi
|
|
50
|
+
|
|
51
|
+
# --- 2. one-pass caller graph ---
|
|
52
|
+
if grep -q '^helper_fn src/use.py 5$' "$C/callers.tsv"; then ok; else bad "helper_fn caller src/use.py:5 missing: $(cat "$C/callers.tsv")"; fi
|
|
53
|
+
if grep -q '^helper_fn src/app.py ' "$C/callers.tsv"; then bad "the defining file must not be listed as a caller"; else ok; fi
|
|
54
|
+
n=$(grep -c '^helper_fn src/use.py ' "$C/callers.tsv")
|
|
55
|
+
if [ "$n" -eq 1 ]; then ok; else bad "one caller row per (symbol, file), got $n"; fi
|
|
56
|
+
|
|
57
|
+
# --- 3. cache hit on unchanged HEAD ---
|
|
58
|
+
bash "$SCRIPT" --repo-path "$REPO" --output "$OUT" > "$WORK/log2" 2>&1
|
|
59
|
+
if grep -q "Cache hit" "$WORK/log2"; then ok; else bad "second build should be a cache hit"; fi
|
|
60
|
+
|
|
61
|
+
# --- 4. outside git: pruned find fallback ---
|
|
62
|
+
PLAIN="$WORK/plain"; mksrc "$PLAIN"
|
|
63
|
+
mkdir -p "$PLAIN/.venv/lib" "$PLAIN/.claude/worktrees/x/src"
|
|
64
|
+
printf 'def venv_only_fn():\n pass\n' > "$PLAIN/.venv/lib/site.py"
|
|
65
|
+
printf 'def worktree_only_fn():\n pass\n' > "$PLAIN/.claude/worktrees/x/src/w.py"
|
|
66
|
+
bash "$SCRIPT" --repo-path "$PLAIN" --output "$WORK/out2" > "$WORK/log3" 2>&1; rc=$?
|
|
67
|
+
if [ "$rc" -eq 0 ]; then ok; else bad "non-git build exit $rc"; fi
|
|
68
|
+
C2=$(ls -d "$WORK"/out2/*/ 2>/dev/null | head -n 1)
|
|
69
|
+
if grep -q '^helper_fn src/app.py ' "$C2/symbols.tsv" 2>/dev/null; then ok; else bad "non-git build missed helper_fn"; fi
|
|
70
|
+
if grep -qE 'venv_only_fn|worktree_only_fn' "$C2/symbols.tsv" 2>/dev/null; then bad "non-git build walked .venv/.claude"; else ok; fi
|
|
71
|
+
|
|
72
|
+
echo "code-index tests: $pass passed, $fail failed"
|
|
73
|
+
[ "$fail" -eq 0 ]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# review-pr-staging-scope.test.sh — cases for the review-pr-staging scoped-review gates (forge#3451):
|
|
3
3
|
# (1) the 7B out-of-scope rule may only demote MEDIUM/LOW/INFO, never HIGH/CRITICAL or unknown severity;
|
|
4
|
-
# (2) the 0B.1 "reviewed" test needs the full-length Reviewed-SHA line, not a 7-char substring
|
|
4
|
+
# (2) the 0B.1 "reviewed" test needs the full-length Reviewed-SHA line, not a 7-char substring;
|
|
5
|
+
# (11) issue numbers cited in commit subjects never make 0B.1 fall open (forge#4127).
|
|
5
6
|
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
6
7
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
7
8
|
set -uo pipefail
|
|
@@ -264,6 +265,40 @@ vh10 blocking "PASS"
|
|
|
264
265
|
case "$VH10_OUT" in *rc=0*) [ "$VH10_V" = "PASS" ] && ok || bad "a valid persisted PASS must stay PASS" ;; *) bad "a valid persisted PASS must not block" ;; esac
|
|
265
266
|
vh10 blocking "SKIP"
|
|
266
267
|
case "$VH10_OUT" in *rc=0*) [ "$VH10_V" = "SKIP" ] && ok || bad "a valid persisted SKIP must stay SKIP" ;; *) bad "a valid persisted SKIP must not block" ;; esac
|
|
268
|
+
# (11) bundle PR set vs issue numbers (forge#4127): conventional subjects cite issues ("(#28668)"); an issue is skipped, never a reason to fall open.
|
|
269
|
+
# Stub gh: PRs 5 (reviewed at head) and 6; 28668 is an issue; 77 fails every read.
|
|
270
|
+
H11=0123456789abcdef0123456789abcdef01234567
|
|
271
|
+
gh11() {
|
|
272
|
+
case "$*" in
|
|
273
|
+
"pr view 5 "*|"pr view 6 "*) jq -n --arg h "$H11" '{headRefOid:$h,mergeCommit:{oid:"m"},commits:[{oid:"c"}],state:"MERGED",baseRefName:"staging",labels:[]}' ;;
|
|
274
|
+
"pr view "*) return 1 ;;
|
|
275
|
+
"api repos/o/r/issues/5 "*|"api repos/o/r/issues/6 "*) echo pr ;;
|
|
276
|
+
"api repos/o/r/issues/28668 "*) echo issue ;;
|
|
277
|
+
"api --paginate repos/o/r/issues/5/comments") printf '%s\n' "$(cm9 "<!-- FORGE:REVIEW_ROUTE mode=full spec=review-pr.md sha=${H11:0:7} -->")" "$(cm9 "$(agent9 security "$H11")")" | jq -s . ;;
|
|
278
|
+
"api --paginate repos/o/r/issues/"*/comments) echo '[]' ;;
|
|
279
|
+
"pr diff "*) echo "f$3.txt" ;;
|
|
280
|
+
*) return 1 ;;
|
|
281
|
+
esac
|
|
282
|
+
}
|
|
283
|
+
[ "$(grep -c '^# BEGIN bundle-pr-kind$' "$SPEC")" -eq 2 ] && [ "$(awk '/^# BEGIN bundle-pr-kind$/{f=1;n++} f{print > ("'"$TMP"'/kind" n ".sh")} /^# END bundle-pr-kind$/{f=0}' "$SPEC"; cmp -s "$TMP/kind1.sh" "$TMP/kind2.sh" && echo same)" = same ] && ok || bad "0A and 0B.1 must carry identical bundle_pr_kind blocks"
|
|
284
|
+
awk '/^# BEGIN bundle-pr-set$/{f=1;next} /^# END bundle-pr-set$/{f=0} f' "$SPEC" | sed 's/{GH_REPO}/o\/r/g' > "$TMP/prset.sh"
|
|
285
|
+
[ -s "$TMP/prset.sh" ] && ok || bad "bundle-pr-set block not found in Phase 0A"
|
|
286
|
+
OUT11=$( gh() { gh11 "$@"; }; BUNDLE_PRS="5 6 28668 77 90"; MERGE_PRS="90"; . "$TMP/prset.sh"; echo $ALL_PR_NUMBERS "|$SKIPPED_ISSUES" )
|
|
287
|
+
[ "$OUT11" = "5 6 77 90 | 28668" ] && ok || bad "0A must drop the confirmed issue and keep PRs, merge PRs and unclassifiable numbers (got '$OUT11')"
|
|
288
|
+
awk '/^# BEGIN bundle-scope-loop$/{f=1;next} /^# END bundle-scope-loop$/{f=0} f' "$SPEC" > "$TMP/loop.sh"
|
|
289
|
+
[ -s "$TMP/loop.sh" ] && ok || bad "bundle-scope-loop block not found in 0B.1"
|
|
290
|
+
scope11() { # $1 = ALL_PR_NUMBERS; prints "<SCOPE_MODE> <REVIEWED_PR_COUNT>" then the loop output
|
|
291
|
+
( gh() { gh11 "$@"; }; bundle_delta_credit() { DELTA_COMMITS=""; DELTA_FULL=0; }; . "$TMP/kind1.sh"
|
|
292
|
+
SCOPE_MODE=full; REVIEWED_COMMITS=""; REVIEWED_MERGES=""; REVIEWED_PR_COUNT=0; _PF="$TMP/pf11"; : > "$_PF"
|
|
293
|
+
TRUSTED_SCRIPT="$TC" ALL_PR_NUMBERS="$1" PR_NUMBER=9 GH_REPO=o/r _SD="$TMP" REVIEW_DELTA_SCRIPT=""
|
|
294
|
+
LOG11=$(. "$TMP/loop.sh"; echo "@@$SCOPE_MODE $REVIEWED_PR_COUNT"); echo "${LOG11##*@@}"; echo "${LOG11%@@*}" )
|
|
295
|
+
}
|
|
296
|
+
OUT11=$(scope11 "28668 5 6")
|
|
297
|
+
[ "$(printf '%s\n' "$OUT11" | head -1)" = "scoped 1" ] && ok || bad "an issue number in the bundle list must be skipped, not fall open (got '$OUT11')"
|
|
298
|
+
case "$OUT11" in *"#28668 is an issue"*) ok ;; *) bad "0B.1 must announce the skipped issue (got '$OUT11')" ;; esac
|
|
299
|
+
OUT11=$(scope11 "5 77")
|
|
300
|
+
[ "$(printf '%s\n' "$OUT11" | head -1 | cut -d' ' -f1)" = "full" ] && ok || bad "a genuine read failure must still fall open (got '$OUT11')"
|
|
301
|
+
case "$OUT11" in *"fell open at bundle PR #77"*"not a confirmed issue"*) ok ;; *) bad "a fall-open must print the PR and reason (got '$OUT11')" ;; esac
|
|
267
302
|
else echo "review-pr-staging-scope.test.sh: git missing, delta section skipped"; fi
|
|
268
303
|
echo "review-pr-staging-scope.test.sh: passed=$PASS failed=$FAILN"
|
|
269
304
|
[ "$FAILN" -eq 0 ]
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
# --no-delta skip the FULL/DELTA/BASE_SYNC_ONLY resolver: the review mode is always FULL (a staging bundle review always reviews the
|
|
24
24
|
# whole bundle), without widening the roster to the full union the way --thorough does
|
|
25
25
|
# Env: FORGE_CONFIG (forge.yaml path; default: the main checkout's forge.yaml, then the git top-level one),
|
|
26
|
-
# REVIEW_PREPARE_SLEEP (seconds between mergeability retries, default 5)
|
|
26
|
+
# REVIEW_PREPARE_SLEEP (seconds between mergeability retries, default 5),
|
|
27
|
+
# REVIEW_PREPARE_INDEX_TIMEOUT (seconds the code-index build may take, default 120; on timeout or failure the slices say
|
|
28
|
+
# "Code index not available" and agents grep instead — forge#4126).
|
|
27
29
|
# Exit: 0 context emitted (status OK) | 3 context emitted with status BLOCKED (the caller must stop, never skip) | 2 usage error
|
|
28
30
|
#
|
|
29
31
|
# Moved from the spec and still in the spec (the caller keeps running them): Phase 2 lint/typecheck/test/build EXECUTION (long-running,
|
|
@@ -463,10 +465,21 @@ if [ "$REVIEW_MODE" != "BASE_SYNC_ONLY" ]; then
|
|
|
463
465
|
gh pr comment "$PR_NUMBER" -R "$REPO" --body "$_msg" >/dev/null 2>&1 || true # allowlist:check-command-side-effects
|
|
464
466
|
[ -z "$MERGE_ISSUE" ] || gh issue edit "$MERGE_ISSUE" -R "$REPO" --add-label needs-human >/dev/null 2>&1 || true # allowlist:check-command-side-effects
|
|
465
467
|
fi
|
|
466
|
-
|
|
468
|
+
# forge#4126: the index build is bounded and visible. A hung build used to block review prep silently (output was discarded).
|
|
469
|
+
INDEX_OK=0
|
|
470
|
+
if [ -n "$CODE_INDEX_SCRIPT" ]; then
|
|
471
|
+
_ito="${REVIEW_PREPARE_INDEX_TIMEOUT:-120}"; case "$_ito" in ''|*[!0-9]*|0) _ito=120 ;; esac
|
|
472
|
+
_tbin=$(command -v timeout || command -v gtimeout || true)
|
|
473
|
+
echo "review-prepare: building code index (limit ${_ito}s)" >&2
|
|
474
|
+
if [ -n "$_tbin" ]; then "$_tbin" "$_ito" bash "$CODE_INDEX_SCRIPT" --repo-path "$REPO_PATH" >/dev/null 2>&1; _irc=$?
|
|
475
|
+
else bash "$CODE_INDEX_SCRIPT" --repo-path "$REPO_PATH" >/dev/null 2>&1; _irc=$?; fi
|
|
476
|
+
if [ "$_irc" -eq 0 ]; then INDEX_OK=1
|
|
477
|
+
elif [ "$_irc" -eq 124 ]; then echo "review-prepare: code index timed out after ${_ito}s — skipping index slices (agents grep instead)" >&2
|
|
478
|
+
else echo "review-prepare: code index failed (exit $_irc) — skipping index slices (agents grep instead)" >&2; fi
|
|
479
|
+
fi
|
|
467
480
|
build_index_slice() {
|
|
468
481
|
local domain="${1}" files
|
|
469
|
-
if [
|
|
482
|
+
if [ "$INDEX_OK" -eq 1 ]; then
|
|
470
483
|
files=$(bash "$CODE_INDEX_SCRIPT" query --domain "$domain" --repo-path "$REPO_PATH" 2>/dev/null | awk -F'\t' '{print $1}' | head -30 | tr '\n' ' ')
|
|
471
484
|
echo "Domain files (${domain}): ${files:-none}"
|
|
472
485
|
else echo "Code index not available — agent will use grep exploration"; fi
|
|
@@ -300,5 +300,21 @@ setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
|
300
300
|
CTX=$(cd "$WORK/hostile" && FORGEDOCK_HOME="$ROOT" FORGE_CONFIG="$CFG" bash "$ROOT/scripts/review-prepare.sh" --pr 7 --repo o/r --out-dir "$WORK/out" 2>/dev/null)
|
|
301
301
|
eq "a hostile cwd roster script is never used" '.roster.agents | index("evil") != null' false
|
|
302
302
|
|
|
303
|
+
# --- code index build is bounded and visible (forge#4126) ---
|
|
304
|
+
setup feat/x staging "services/api/auth.py" "$DIFF_AUTH_BILL"
|
|
305
|
+
prep "$ROOT"
|
|
306
|
+
eq "real index fills the auth slice" '.index_slices.auth | startswith("Domain files (auth):")' true
|
|
307
|
+
if grep -q "review-prepare: building code index" "$WORK/stderr"; then ok; else bad "index build must print a progress line on stderr"; fi
|
|
308
|
+
if command -v timeout >/dev/null 2>&1 || command -v gtimeout >/dev/null 2>&1; then
|
|
309
|
+
mkroot "$WORK/root-slowindex" "" "" templates
|
|
310
|
+
printf '#!/usr/bin/env bash\nsleep 30\n' > "$WORK/root-slowindex/scripts/code-index.sh"
|
|
311
|
+
t0=$(date +%s)
|
|
312
|
+
REVIEW_PREPARE_INDEX_TIMEOUT=1 prep "$WORK/root-slowindex"; t1=$(date +%s)
|
|
313
|
+
rc_is "hung index build" 0
|
|
314
|
+
if [ $((t1 - t0)) -lt 20 ]; then ok; else bad "hung index build was not bounded ($((t1 - t0))s)"; fi
|
|
315
|
+
eq "timed-out index falls back to grep slices" .index_slices.auth "Code index not available — agent will use grep exploration"
|
|
316
|
+
if grep -q "code index timed out after 1s" "$WORK/stderr"; then ok; else bad "index timeout must be reported on stderr"; fi
|
|
317
|
+
fi
|
|
318
|
+
|
|
303
319
|
echo "review-prepare tests: $pass passed, $fail failed"
|
|
304
320
|
[ "$fail" -eq 0 ]
|