forgedock 1.29.0 → 1.29.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/bin/engine-cli.mjs +12 -6
- package/bin/lean/context.mjs +27 -4
- package/bin/lean/context.test.mjs +40 -12
- package/bin/lean/lean.mjs +23 -18
- package/bin/lean/lean.test.mjs +31 -3
- package/bin/lean/orchestrate.mjs +41 -11
- package/bin/lean/prefilter.mjs +79 -14
- package/bin/lean/prefilter.test.mjs +88 -2
- package/bin/lean/recovery.test.mjs +196 -7
- package/bin/lean/render.mjs +13 -0
- package/bin/lean/testmap.mjs +17 -0
- package/bin/lean/testmap.test.mjs +16 -1
- package/commands/deploy-pr.md +5 -4
- package/commands/orchestrate.md +4 -3
- package/commands/review-pr-staging.md +6 -1
- package/docs/OUTCOME-CODES.md +1 -1
- package/package.json +1 -1
- package/scripts/code-index.sh +33 -33
- package/scripts/code-index.test.sh +21 -2
- package/scripts/fixtures/test-map/go/.github/workflows/ci.yml +2 -0
- package/scripts/repo-facts.test.sh +5 -5
- package/scripts/review-prepare.sh +28 -6
- package/scripts/review-prepare.test.sh +22 -2
- package/scripts/test-map.sh +139 -33
- package/scripts/test-map.test.sh +195 -7
- package/scripts/verify-phase-trail.test.sh +44 -44
- package/scripts/wait-ci-green.sh +1 -1
- package/scripts/wait-ci-green.test.sh +4 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
import { describe, it } from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
|
-
import { parseTestMap, readTestMap, selectCommands, factsPath, TEST_MAP_FILE, REPO_FACTS_FILE } from "./testmap.mjs";
|
|
5
|
+
import { parseTestMap, readTestMap, selectCommands, mergeChecks, factsPath, TEST_MAP_FILE, REPO_FACTS_FILE } from "./testmap.mjs";
|
|
6
6
|
|
|
7
7
|
const MAP = {
|
|
8
8
|
version: 1,
|
|
@@ -56,3 +56,18 @@ describe("test map (#4272)", () => {
|
|
|
56
56
|
assert.equal(await factsPath(["/wt"], async () => { throw new Error("EACCES"); }), null);
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
describe("mergeChecks (#4315): the map can only add checks", () => {
|
|
61
|
+
it("a partial map keeps every learned command and adds the ones it selects that learned lacks", () => {
|
|
62
|
+
assert.deepEqual(mergeChecks(["npm test", "cd pkg && npm test"], ["make lint", "npm test"]),
|
|
63
|
+
{ commands: ["make lint", "npm test", "cd pkg && npm test"], added: 1, source: "test-map+learned" });
|
|
64
|
+
});
|
|
65
|
+
it("duplicates, whitespace variants included, appear once", () => {
|
|
66
|
+
assert.deepEqual(mergeChecks(["npm run lint", " npm run lint", "npm test"], ["npm run lint ", "npm run lint"]).commands, ["npm run lint", "npm test"]);
|
|
67
|
+
});
|
|
68
|
+
it("an empty selection keeps the learned commands; no learned commands keep the map's selection", () => {
|
|
69
|
+
assert.deepEqual(mergeChecks([], ["make lint"]), { commands: ["make lint"], added: 0, source: "learned" });
|
|
70
|
+
assert.deepEqual(mergeChecks(["npm test"], []), { commands: ["npm test"], added: 1, source: "test-map" });
|
|
71
|
+
assert.deepEqual(mergeChecks(undefined, undefined), { commands: [], added: 0, source: "learned" });
|
|
72
|
+
});
|
|
73
|
+
});
|
package/commands/deploy-pr.md
CHANGED
|
@@ -357,8 +357,9 @@ else
|
|
|
357
357
|
# Check current CI status
|
|
358
358
|
# Fail closed: valid fields only (`status`/`conclusion` do not exist on gh pr checks --json; the
|
|
359
359
|
# old query errored and defaulted to 0 = "no failures"). An unreadable result counts as failing.
|
|
360
|
+
# forgedock/verify (the lean engine's report-only gate summary) is not a code failure: ignored, as wait-ci-green.sh does.
|
|
360
361
|
CURRENT_CI_STATUS=$(gh pr checks "$PR_NUMBER" $GH_FLAG --json name,bucket \
|
|
361
|
-
--jq '[.[] | select(.bucket == "fail" or .bucket == "cancel")] | length' 2>/dev/null || echo "1")
|
|
362
|
+
--jq '[.[] | select(.name != "forgedock/verify") | select(.bucket == "fail" or .bucket == "cancel")] | length' 2>/dev/null || echo "1")
|
|
362
363
|
|
|
363
364
|
if [ "$CURRENT_CI_STATUS" -eq 0 ]; then
|
|
364
365
|
echo "CI gate: PASSED (no failing checks)"
|
|
@@ -378,9 +379,9 @@ else
|
|
|
378
379
|
|
|
379
380
|
CI_FIXES=$((CI_FIXES + 1))
|
|
380
381
|
|
|
381
|
-
# Re-check CI status after fix attempt
|
|
382
|
-
FAILING_AFTER=$(gh pr checks "$PR_NUMBER" $GH_FLAG --json name,
|
|
383
|
-
--jq '[.[] | select(.
|
|
382
|
+
# Re-check CI status after fix attempt (same fail-closed query as above)
|
|
383
|
+
FAILING_AFTER=$(gh pr checks "$PR_NUMBER" $GH_FLAG --json name,bucket \
|
|
384
|
+
--jq '[.[] | select(.name != "forgedock/verify") | select(.bucket == "fail" or .bucket == "cancel")] | length' 2>/dev/null || echo "1")
|
|
384
385
|
|
|
385
386
|
if [ "$FAILING_AFTER" -eq 0 ]; then
|
|
386
387
|
echo "CI gate: PASSED after $CI_ITER iteration(s)"
|
package/commands/orchestrate.md
CHANGED
|
@@ -38,7 +38,8 @@ Do this yourself from issue text and the code. Spawn no sub-agents here.
|
|
|
38
38
|
|
|
39
39
|
It closes, with the evidence on the issue and no lane, every issue that a merged PR already fixed (one
|
|
40
40
|
short read-only validation on a small model) or an auto-filed CI failure whose workflow is green again
|
|
41
|
-
(the green run link)
|
|
41
|
+
(the green run link), unless it is flaky (wording or `flaky` label), was reopened after a pre-filter close, or
|
|
42
|
+
the workflow failed and passed at the same SHA. It checks K issues at a time. Its last line is `lanes: #A #B ...`: continue with only
|
|
42
43
|
those issues. This is the only pre-filter in this command: lanes run `forgedock run-issue`, which does not
|
|
43
44
|
pre-filter again. Keep its
|
|
44
45
|
`pre-filter:` line for the report.
|
|
@@ -144,12 +145,12 @@ When a lane finishes (agent lanes; the batch runner applies the same rules itsel
|
|
|
144
145
|
These are mechanical. **Do not ask for confirmation on them.** The batch runner recovers each one itself
|
|
145
146
|
(`orchestration.recovery.max_attempts`, default 1 per issue). It launches one recovery lane under the same lease, lane
|
|
146
147
|
and admission rules as any lane, with `run-issue --recover <code> [--recover-attempt n]` (core path only:
|
|
147
|
-
`--path full` rejects `--recover`). That lane:
|
|
148
|
+
`--path full` rejects `--recover`; recovery is off when `engine.path: full`). That lane:
|
|
148
149
|
- removes `needs-human`;
|
|
149
150
|
- resumes the open PR: merges the base, fixes the failed required checks from their logs, and pushes new commits only;
|
|
150
151
|
- re-queues the issue, keeping its dependents queued in the same batch.
|
|
151
152
|
|
|
152
|
-
Report what was recovered afterwards. A failed recovery stays `needs-human` with its evidence, exactly once (failed = `needs-human`, `ci-pending`, or a final `engine-error`/`deferred`; `invalid`, `decomposed` and `dependency-*` are not failures and never re-label),
|
|
153
|
+
Report what was recovered afterwards. A failed recovery stays `needs-human` with its evidence, re-labelled exactly once per issue, by the final attempt only (failed = `needs-human`, `ci-pending`, or a final `engine-error`/`deferred`; `invalid`, `decomposed` and `dependency-*` are not failures and never re-label). With `max_attempts > 1` a non-final attempt adds no label or comment: the runner either launches the next attempt or, when none follows, re-labels it once itself, so the label does not flap between attempts. There
|
|
153
154
|
is no second attempt beyond `max_attempts`.
|
|
154
155
|
- `needs-human` with a **human code** (`builder.blocked`: product-decision, spend, credentials, promotion, customer): an
|
|
155
156
|
external action agents cannot take, or a product decision too critical for agents. It is never auto-recovered. Read
|
|
@@ -660,8 +660,13 @@ git diff origin/$DEFAULT_BRANCH...origin/$STAGING_BRANCH | grep -iE "(api[_-]?ke
|
|
|
660
660
|
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
661
661
|
. "$(forge_resolve lib/fence-bootstrap.sh)" || { echo "BLOCKED: ForgeDock helper lib/fence-bootstrap.sh not resolvable"; exit 1; } # FENCE_BOOTSTRAP
|
|
662
662
|
gh pr checks "$PR_NUMBER" -R "$GH_REPO" 2>&1
|
|
663
|
+
# forgedock/verify (the lean engine's report-only gate summary) is not a code failure: ignored, as wait-ci-green.sh does.
|
|
664
|
+
# Fail closed: an unreadable result counts as failing.
|
|
665
|
+
CI_FAILING=$(gh pr checks "$PR_NUMBER" -R "$GH_REPO" --json name,bucket \
|
|
666
|
+
--jq '[.[] | select(.name != "forgedock/verify") | select(.bucket == "fail" or .bucket == "cancel")] | length' 2>/dev/null || echo "1")
|
|
667
|
+
echo "CI_FAILING: $CI_FAILING"
|
|
663
668
|
```
|
|
664
|
-
|
|
669
|
+
`CI_FAILING` > 0 → BLOCK DEPLOY (unless autofixed in Phase 1F). A red `forgedock/verify` alone does not block.
|
|
665
670
|
|
|
666
671
|
### Phase 1F: CI Autofix
|
|
667
672
|
|
package/docs/OUTCOME-CODES.md
CHANGED
|
@@ -97,7 +97,7 @@ An absent or unknown code is recorded as `unclassified`, with the raw reason kep
|
|
|
97
97
|
- `contract.invalid` when the issue was edited mid-run (`issueEdited`);
|
|
98
98
|
- `ci-pending` on a conflicting PR, recovered as `base.conflict`.
|
|
99
99
|
|
|
100
|
-
The runner relaunches the issue once per `orchestration.recovery.max_attempts` (default 1) as a recovery lane (`run-issue --recover <code>`), under the normal admission rules. That lane removes the `needs-human` label, posts `FORGE:DECISION`, and resumes the open PR: base-sync, then CI fix rounds from the failed-check logs. Dependents stay queued. A merged recovery is reported as `recovered`. A failed one is final, keeps the recovery evidence on the row, and skips its dependents. A failed recovery stays `needs-human`: the lane re-adds the label (with a `FORGE:NEEDS_HUMAN` comment carrying the evidence) only for a final `needs-human` or `ci-pending
|
|
100
|
+
The runner relaunches the issue once per `orchestration.recovery.max_attempts` (default 1) as a recovery lane (`run-issue --recover <code>`), under the normal admission rules. That lane removes the `needs-human` label, posts `FORGE:DECISION`, and resumes the open PR: base-sync, then CI fix rounds from the failed-check logs. Dependents stay queued. A merged recovery is reported as `recovered`. A failed one is final, keeps the recovery evidence on the row, and skips its dependents. A failed recovery stays `needs-human`: the lane re-adds the label (with a `FORGE:NEEDS_HUMAN` comment carrying the evidence) only for a final `needs-human` or `ci-pending`, and only when it is the last attempt (`attempt == max_attempts`); a non-final attempt that ends `needs-human` or `ci-pending` without another attempt following is re-labelled once by the batch runner; the batch runner re-adds it, once, for a final `engine-error` or `deferred` after the recovery pass (those may be requeued, so the lane cannot know). `invalid`, `decomposed`, `dependency-*` and `merged` never re-label.
|
|
101
101
|
- **human**: `builder.blocked`, which is only reached by a typed human block: credentials, spend, promotion, customer or product-decision. It is never recovered, it skips dependents, and it is the only class in the report's escalation list.
|
|
102
102
|
- **terminal**: every other code. No recovery runs, dependents are skipped, and the row is reported as `unrecovered`.
|
|
103
103
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.29.
|
|
3
|
+
"version": "1.29.1",
|
|
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
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
# Import graph: language-specific grep patterns (Python, JS/TS, Go, Rust, shell)
|
|
25
25
|
#
|
|
26
26
|
# Cache key: HEAD SHA — cache is a hit on unchanged HEAD, miss on new commit.
|
|
27
|
-
# Works on Windows (Git Bash / WSL) and
|
|
27
|
+
# Works on Windows (Git Bash / WSL), Linux and macOS. Bash 3.2 portable: no associative arrays, mapfile, grep -P.
|
|
28
28
|
#
|
|
29
29
|
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
30
30
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
@@ -181,7 +181,7 @@ src_files() {
|
|
|
181
181
|
# Step 1: Symbol extraction
|
|
182
182
|
# ---------------------------------------------------------------------------
|
|
183
183
|
SYMBOLS_FILE="${CACHE_DIR}/symbols.tsv"
|
|
184
|
-
> "$SYMBOLS_FILE"
|
|
184
|
+
: > "$SYMBOLS_FILE"
|
|
185
185
|
|
|
186
186
|
echo "INFO: Extracting symbols..."
|
|
187
187
|
|
|
@@ -239,7 +239,7 @@ if [[ ! -s "$SYMBOLS_FILE" ]] || ! command -v ctags >/dev/null 2>&1; then
|
|
|
239
239
|
name=""
|
|
240
240
|
fi
|
|
241
241
|
kind=$(echo "$content" | grep -q "^def \| def " && echo "function" || echo "class")
|
|
242
|
-
rel_path="${f
|
|
242
|
+
rel_path="${f#"$REPO_PATH"/}"
|
|
243
243
|
[[ -n "$name" ]] && echo -e "${name}\t${rel_path}\t${lnum}\t${kind}"
|
|
244
244
|
done || true
|
|
245
245
|
done >> "$SYMBOLS_FILE" 2>/dev/null || true
|
|
@@ -257,7 +257,7 @@ if [[ ! -s "$SYMBOLS_FILE" ]] || ! command -v ctags >/dev/null 2>&1; then
|
|
|
257
257
|
else
|
|
258
258
|
name=""
|
|
259
259
|
fi
|
|
260
|
-
rel_path="${f
|
|
260
|
+
rel_path="${f#"$REPO_PATH"/}"
|
|
261
261
|
[[ -n "$name" ]] && echo -e "${name}\t${rel_path}\t${lnum}\tfunction"
|
|
262
262
|
done || true
|
|
263
263
|
done >> "$SYMBOLS_FILE" 2>/dev/null || true
|
|
@@ -267,13 +267,13 @@ fi
|
|
|
267
267
|
# Step 2: Import graph extraction
|
|
268
268
|
# ---------------------------------------------------------------------------
|
|
269
269
|
IMPORTS_FILE="${CACHE_DIR}/imports.tsv"
|
|
270
|
-
> "$IMPORTS_FILE"
|
|
270
|
+
: > "$IMPORTS_FILE"
|
|
271
271
|
|
|
272
272
|
echo "INFO: Building import graph..."
|
|
273
273
|
|
|
274
274
|
# Python imports
|
|
275
275
|
src_files py | while read -r f; do
|
|
276
|
-
rel_path="${f
|
|
276
|
+
rel_path="${f#"$REPO_PATH"/}"
|
|
277
277
|
grep -n "^import \|^from " "$f" 2>/dev/null | while IFS=: read -r lnum content; do
|
|
278
278
|
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
279
279
|
# grep build on Windows, so use a bash regex instead of lookbehind.
|
|
@@ -293,7 +293,7 @@ done >> "$IMPORTS_FILE" 2>/dev/null || true
|
|
|
293
293
|
|
|
294
294
|
# JavaScript/TypeScript imports
|
|
295
295
|
src_files js ts tsx jsx | while read -r f; do
|
|
296
|
-
rel_path="${f
|
|
296
|
+
rel_path="${f#"$REPO_PATH"/}"
|
|
297
297
|
grep -n "^import \|^} from \|require(" "$f" 2>/dev/null | while IFS=: read -r lnum content; do
|
|
298
298
|
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
299
299
|
# grep build on Windows, so use a bash regex instead of lookbehind/lookahead.
|
|
@@ -310,7 +310,7 @@ done >> "$IMPORTS_FILE" 2>/dev/null || true
|
|
|
310
310
|
|
|
311
311
|
# Go imports
|
|
312
312
|
src_files go | while read -r f; do
|
|
313
|
-
rel_path="${f
|
|
313
|
+
rel_path="${f#"$REPO_PATH"/}"
|
|
314
314
|
grep -n '"[^"]*"' "$f" 2>/dev/null | grep -v "//\|fmt.Print\|errors.New" | while IFS=: read -r lnum content; do
|
|
315
315
|
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
316
316
|
# grep build on Windows, so use a bash regex instead of lookbehind/lookahead.
|
|
@@ -327,7 +327,7 @@ done >> "$IMPORTS_FILE" 2>/dev/null || true
|
|
|
327
327
|
# Step 3: Caller graph (callee → callers mapping)
|
|
328
328
|
# ---------------------------------------------------------------------------
|
|
329
329
|
CALLERS_FILE="${CACHE_DIR}/callers.tsv"
|
|
330
|
-
> "$CALLERS_FILE"
|
|
330
|
+
: > "$CALLERS_FILE"
|
|
331
331
|
|
|
332
332
|
echo "INFO: Building caller graph..."
|
|
333
333
|
|
|
@@ -363,50 +363,50 @@ fi
|
|
|
363
363
|
# Step 4: File-to-domain mapping
|
|
364
364
|
# ---------------------------------------------------------------------------
|
|
365
365
|
DOMAIN_FILE="${CACHE_DIR}/file_domain.tsv"
|
|
366
|
-
> "$DOMAIN_FILE"
|
|
366
|
+
: > "$DOMAIN_FILE"
|
|
367
367
|
|
|
368
368
|
echo "INFO: Building file-domain map..."
|
|
369
369
|
|
|
370
370
|
# Read forge.yaml domain hints if available
|
|
371
371
|
FORGE_YAML="${REPO_PATH}/forge.yaml"
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
["services/api/db"]="database"
|
|
383
|
-
["migrations"]="database"
|
|
384
|
-
[".github/workflows"]="ci-cd"
|
|
385
|
-
["infra"]="infrastructure"
|
|
386
|
-
["web/src"]="frontend"
|
|
372
|
+
# Ordered prefix -> domain table, most specific first; the first matching prefix wins (forge#4235).
|
|
373
|
+
# Parallel indexed arrays, not an associative array: bash 3.2 (stock macOS) has no `declare -A`,
|
|
374
|
+
# and hash iteration order made services/api vs services/api/auth unspecified.
|
|
375
|
+
DOMAIN_PREFIXES=(
|
|
376
|
+
"web/src/app/billing" "web/src/app/auth" "services/api/auth" "services/api/billing" "services/api/db"
|
|
377
|
+
"services/api" "services/worker" "migrations" ".github/workflows" "infra" "web/src" "commands" "scripts"
|
|
378
|
+
)
|
|
379
|
+
DOMAIN_LABELS=(
|
|
380
|
+
"billing" "auth" "auth" "billing" "database"
|
|
381
|
+
"api" "worker" "database" "ci-cd" "infrastructure" "frontend" "pipeline-commands" "pipeline-scripts"
|
|
387
382
|
)
|
|
388
383
|
|
|
389
|
-
# Read key_paths from forge.yaml if present
|
|
384
|
+
# Read key_paths from forge.yaml if present: prepended so they override the defaults
|
|
390
385
|
if [[ -f "$FORGE_YAML" ]]; then
|
|
391
386
|
# Simple key_paths extraction — reads "domain: [patterns]" blocks under review.key_paths
|
|
392
387
|
while IFS=: read -r key val; do
|
|
393
388
|
key=$(echo "$key" | tr -d ' ')
|
|
394
389
|
val=$(echo "$val" | tr -d ' "')
|
|
395
390
|
if [[ -n "$key" && -n "$val" ]]; then
|
|
396
|
-
|
|
391
|
+
DOMAIN_PREFIXES=("$val" "${DOMAIN_PREFIXES[@]}")
|
|
392
|
+
DOMAIN_LABELS=("$key" "${DOMAIN_LABELS[@]}")
|
|
397
393
|
fi
|
|
398
394
|
done < <(grep -A2 'key_paths:' "$FORGE_YAML" 2>/dev/null | grep '^\s*\w\+:' | head -20 || true)
|
|
399
395
|
fi
|
|
396
|
+
DOMAIN_COUNT=${#DOMAIN_PREFIXES[@]}
|
|
400
397
|
|
|
401
398
|
# Map each file to its domain
|
|
402
399
|
while IFS= read -r rel_path; do
|
|
403
400
|
case "$rel_path" in .forge/*|*/node_modules/*|node_modules/*|*/__pycache__/*) continue ;; esac
|
|
404
401
|
domain="unclassified"
|
|
405
|
-
|
|
402
|
+
i=0
|
|
403
|
+
while [[ "$i" -lt "$DOMAIN_COUNT" ]]; do
|
|
404
|
+
pattern="${DOMAIN_PREFIXES[$i]}"
|
|
406
405
|
if [[ "$rel_path" == $pattern* ]]; then
|
|
407
|
-
domain="${
|
|
406
|
+
domain="${DOMAIN_LABELS[$i]}"
|
|
408
407
|
break
|
|
409
408
|
fi
|
|
409
|
+
i=$((i + 1))
|
|
410
410
|
done
|
|
411
411
|
echo -e "${rel_path}\t${domain}"
|
|
412
412
|
done < "$FILE_LIST" >> "$DOMAIN_FILE" 2>/dev/null || true
|
|
@@ -415,10 +415,10 @@ done < "$FILE_LIST" >> "$DOMAIN_FILE" 2>/dev/null || true
|
|
|
415
415
|
# Step 5: Write index manifest
|
|
416
416
|
# ---------------------------------------------------------------------------
|
|
417
417
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ")
|
|
418
|
-
SYMBOL_COUNT=$(wc -l < "$SYMBOLS_FILE" 2>/dev/null || echo 0)
|
|
419
|
-
IMPORT_COUNT=$(wc -l < "$IMPORTS_FILE" 2>/dev/null || echo 0)
|
|
420
|
-
CALLER_COUNT=$(wc -l < "$CALLERS_FILE" 2>/dev/null || echo 0)
|
|
421
|
-
FILE_COUNT=$(wc -l < "$DOMAIN_FILE" 2>/dev/null || echo 0)
|
|
418
|
+
SYMBOL_COUNT=$( (wc -l < "$SYMBOLS_FILE" 2>/dev/null || echo 0) | tr -d " ")
|
|
419
|
+
IMPORT_COUNT=$( (wc -l < "$IMPORTS_FILE" 2>/dev/null || echo 0) | tr -d " ")
|
|
420
|
+
CALLER_COUNT=$( (wc -l < "$CALLERS_FILE" 2>/dev/null || echo 0) | tr -d " ")
|
|
421
|
+
FILE_COUNT=$( (wc -l < "$DOMAIN_FILE" 2>/dev/null || echo 0) | tr -d " ")
|
|
422
422
|
|
|
423
423
|
# Sanity check: if source files that should have produced symbols/imports were
|
|
424
424
|
# found, but extraction came back empty, a loop was truncated somewhere above
|
|
@@ -38,7 +38,7 @@ mkdir -p "$REPO/.venv/lib"; printf 'def venv_only_fn():\n helper_fn()\n' > "$
|
|
|
38
38
|
OUT="$WORK/out"
|
|
39
39
|
bash "$SCRIPT" --repo-path "$REPO" --output "$OUT" > "$WORK/log" 2>&1; rc=$?
|
|
40
40
|
if [ "$rc" -eq 0 ]; then ok; else bad "build exit $rc: $(head -c 400 "$WORK/log")"; fi
|
|
41
|
-
C=$(
|
|
41
|
+
C=$(find "$OUT" -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
|
42
42
|
for f in symbols imports callers file_domain; do
|
|
43
43
|
if [ -s "$C/$f.tsv" ]; then ok; else bad "$f.tsv is empty"; fi
|
|
44
44
|
if grep -qE '\.claude/|\.forge/|\.venv/|worktree_only_fn|forge_wt_only_fn|venv_only_fn' "$C/$f.tsv" 2>/dev/null; then
|
|
@@ -65,9 +65,28 @@ printf 'def venv_only_fn():\n pass\n' > "$PLAIN/.venv/lib/site.py"
|
|
|
65
65
|
printf 'def worktree_only_fn():\n pass\n' > "$PLAIN/.claude/worktrees/x/src/w.py"
|
|
66
66
|
bash "$SCRIPT" --repo-path "$PLAIN" --output "$WORK/out2" > "$WORK/log3" 2>&1; rc=$?
|
|
67
67
|
if [ "$rc" -eq 0 ]; then ok; else bad "non-git build exit $rc"; fi
|
|
68
|
-
C2=$(
|
|
68
|
+
C2=$(find "$WORK/out2" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | head -n 1)
|
|
69
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
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
71
|
|
|
72
|
+
# --- 5. bash 3.2 portable (forge#4235): stock macOS bash has no associative arrays or mapfile ---
|
|
73
|
+
if grep -vE '^[[:space:]]*#' "$SCRIPT" | grep -qE '(declare|local|typeset) -[a-zA-Z]*A|mapfile|readarray|grep -[a-zA-Z]*P'; then
|
|
74
|
+
bad "code-index.sh stays bash 3.2 portable (no associative arrays / mapfile): $(grep -nE '(declare|local|typeset) -[a-zA-Z]*A|mapfile|readarray' "$SCRIPT" | head -n 3 | tr '\n' ' ')"
|
|
75
|
+
else ok; fi
|
|
76
|
+
|
|
77
|
+
# --- 6. file-domain map: the most specific prefix wins, deterministically ---
|
|
78
|
+
DOM="$WORK/dom"; mkdir -p "$DOM/services/api/auth" "$DOM/web/src/app/billing" "$DOM/scripts"
|
|
79
|
+
printf 'x = 1\n' > "$DOM/services/api/auth/login.py"; printf 'y = 1\n' > "$DOM/services/api/routes.py"
|
|
80
|
+
printf 'export const p = 1\n' > "$DOM/web/src/app/billing/page.tsx"; printf 'echo hi\n' > "$DOM/scripts/x.sh"
|
|
81
|
+
git -C "$DOM" init -q -b main; git -C "$DOM" add -A; git -C "$DOM" commit -qm init
|
|
82
|
+
bash "$SCRIPT" --repo-path "$DOM" --output "$WORK/out3" > "$WORK/log4" 2>&1; rc=$?
|
|
83
|
+
if [ "$rc" -eq 0 ]; then ok; else bad "domain fixture build exit $rc: $(head -c 400 "$WORK/log4")"; fi
|
|
84
|
+
C3=$(find "$WORK/out3" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | head -n 1)
|
|
85
|
+
for want in "services/api/auth/login.py auth" "services/api/routes.py api" "web/src/app/billing/page.tsx billing" "scripts/x.sh pipeline-scripts"; do
|
|
86
|
+
if grep -qxF "$want" "$C3/file_domain.tsv" 2>/dev/null; then ok; else bad "file_domain.tsv missing '$want': $(tr '\n' ' ' < "$C3/file_domain.tsv" 2>/dev/null)"; fi
|
|
87
|
+
done
|
|
88
|
+
q=$(bash "$SCRIPT" query --domain auth --repo-path "$DOM" --output "$WORK/out3" 2>/dev/null)
|
|
89
|
+
if [ "$q" = "services/api/auth/login.py auth" ]; then ok; else bad "query --domain auth returned '$q'"; fi
|
|
90
|
+
|
|
72
91
|
echo "code-index tests: $pass passed, $fail failed"
|
|
73
92
|
[ "$fail" -eq 0 ]
|
|
@@ -12,7 +12,7 @@ eq() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got: $2 want: $3)"; fi }
|
|
|
12
12
|
q() { printf '%s' "$OUT" | jq -c "$1"; }
|
|
13
13
|
|
|
14
14
|
new_repo() {
|
|
15
|
-
rm -rf "$TMP/$1"; mkdir -p "$TMP/$1"; cd "$TMP/$1" || exit 1
|
|
15
|
+
rm -rf "${TMP:?}/$1"; mkdir -p "$TMP/$1"; cd "$TMP/$1" || exit 1
|
|
16
16
|
git init -q -b main; git config user.email t@example.com; git config user.name t; git config commit.gpgsign false
|
|
17
17
|
}
|
|
18
18
|
commit() { git add -A; git commit -qm "$1"; }
|
|
@@ -44,12 +44,12 @@ jobs:
|
|
|
44
44
|
runs-on: ubuntu-latest
|
|
45
45
|
timeout-minutes: 45
|
|
46
46
|
steps:
|
|
47
|
-
- run:
|
|
47
|
+
- run: pytest tests/slow
|
|
48
48
|
quick:
|
|
49
49
|
runs-on: ubuntu-latest
|
|
50
50
|
timeout-minutes: 5
|
|
51
51
|
steps:
|
|
52
|
-
- run:
|
|
52
|
+
- run: ruff check .
|
|
53
53
|
EOF
|
|
54
54
|
lines src/big.ts 2100; commit c1
|
|
55
55
|
echo x >> src/big.ts; lines src/small.ts 10; commit c2
|
|
@@ -62,8 +62,8 @@ eq "packages" "$(q '.layer_rules.packages')" '[{"name":"@x/a","dir":"packages/a"
|
|
|
62
62
|
eq "edge" "$(q '.layer_rules.edges')" '[{"from":"@x/a","to":"@x/b","kind":"dependencies","manifest":"packages/a/package.json"}]'
|
|
63
63
|
eq "boundary configs" "$(q '.layer_rules.boundary_configs')" '[".eslintrc.js"]'
|
|
64
64
|
eq "services job" "$(q '[.slow_suites[] | select(.command == "pnpm run test") | .why]')" '["CI job .github/workflows/ci.yml#jobs.db uses services"]'
|
|
65
|
-
eq "timeout job" "$(q '[.slow_suites[] | select(.command == "
|
|
66
|
-
eq "quick job not slow" "$(q '[.slow_suites[] | select(.command == "
|
|
65
|
+
eq "timeout job" "$(q '[.slow_suites[] | select(.command == "pytest tests/slow") | .why]')" '["CI job .github/workflows/ci.yml#jobs.long sets timeout-minutes 45"]'
|
|
66
|
+
eq "quick job not slow" "$(q '[.slow_suites[] | select(.command == "ruff check .")] | length')" 0
|
|
67
67
|
eq "hotspot" "$(q '.hotspots')" '[{"path":"src/big.ts","lines":2101,"commits_90d":2,"hotspot":4202}]'
|
|
68
68
|
|
|
69
69
|
# ---- 2. a cached test map is read: its e2e command is a slow suite ----
|
|
@@ -87,15 +87,23 @@ case "$MERGE_ISSUE" in *[!0-9]*) MERGE_ISSUE="" ;; esac
|
|
|
87
87
|
[ "$THOROUGH" = "true" ] || THOROUGH="false"
|
|
88
88
|
|
|
89
89
|
# --- trusted helper resolution (install roots only; never cwd) ---
|
|
90
|
+
# shellcheck source=/dev/null # the resolver lib is found at runtime in the trusted install root
|
|
90
91
|
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; _SELF_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd -P)"; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" "${CLAUDE_PLUGIN_ROOT:-}" "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/* "$_SELF_ROOT"; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
91
92
|
|
|
92
93
|
ROSTER_SCRIPT=$(forge_resolve review-roster.sh)
|
|
93
94
|
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
95
|
+
# shellcheck disable=SC2034 # read by lib/review-delta-mode.sh (sourced below)
|
|
94
96
|
REVIEW_DELTA_SCRIPT=$(forge_resolve review-delta.sh)
|
|
95
97
|
CODE_INDEX_SCRIPT=$(forge_resolve code-index.sh)
|
|
96
|
-
_l=$(forge_resolve lib/pr-issue-number.sh)
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
_l=$(forge_resolve lib/pr-issue-number.sh)
|
|
99
|
+
# shellcheck source=/dev/null # resolved at runtime from the trusted install root
|
|
100
|
+
if [ -n "$_l" ]; then . "$_l"; else pr_issue_number() { return 0; }; fi
|
|
101
|
+
_l=$(forge_resolve lib/review-template-resolve.sh)
|
|
102
|
+
# shellcheck source=/dev/null # resolved at runtime from the trusted install root
|
|
103
|
+
if [ -n "$_l" ]; then . "$_l"; else review_template_resolve() { TEMPLATE_BASE=""; TEMPLATE_SOURCE="none"; MONOLITHIC_CATALOG=""; }; fi
|
|
104
|
+
_l=$(forge_resolve lib/review-delta-mode.sh)
|
|
105
|
+
# shellcheck source=/dev/null disable=SC2329 # resolved at runtime; the fallback stubs mirror the lib's API
|
|
106
|
+
if [ -n "$_l" ]; then . "$_l"; else review_delta_mode() { echo "FULL - 0"; }; carry_verified_from() { :; }; fi
|
|
99
107
|
|
|
100
108
|
# --- state, scratch and the ref-restore guard ---
|
|
101
109
|
BLOCKERS=""
|
|
@@ -105,12 +113,14 @@ mkdir -p "$OUT_DIR" 2>/dev/null || { echo "review-prepare: cannot create $OUT_DI
|
|
|
105
113
|
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/review-prepare.XXXXXX") || exit 2
|
|
106
114
|
ORIG_REF=""; CHECKED_OUT=""
|
|
107
115
|
# restore_ref: put the checkout back exactly where it was. -f so a dirty review checkout can never wedge the restore.
|
|
116
|
+
# shellcheck disable=SC2329 # called from cleanup (EXIT trap) and the checkout paths
|
|
108
117
|
restore_ref() {
|
|
109
118
|
if [ -n "$CHECKED_OUT" ] && [ -n "$ORIG_REF" ]; then
|
|
110
119
|
git checkout -f "$ORIG_REF" >/dev/null 2>&1 || echo "review-prepare: WARNING could not restore $ORIG_REF" >&2
|
|
111
120
|
fi
|
|
112
121
|
CHECKED_OUT=""
|
|
113
122
|
}
|
|
123
|
+
# shellcheck disable=SC2329 # invoked by the EXIT trap
|
|
114
124
|
cleanup() { restore_ref; rm -rf "$TMP_DIR"; }
|
|
115
125
|
trap cleanup EXIT
|
|
116
126
|
|
|
@@ -144,6 +154,7 @@ if [ -z "$BLOCKERS" ]; then
|
|
|
144
154
|
# Pinned (forge#3935): same-repo PRs only (a fork's branch name resolves to origin's own branch), and the fetched tip must equal REVIEW_SHA; the diff is taken against REVIEW_SHA, never the branch name.
|
|
145
155
|
if { [ "$NO_DELTA" = "true" ] || [ "$IS_MILESTONE_TO_STAGING" = "true" ]; } && [ -n "$BASE" ] && [ -n "$HEAD" ] && [ "$(printf '%s' "$PR_INFO" | jq -r .isCrossRepository 2>/dev/null)" = "false" ] && printf '%s' "$REVIEW_SHA" | grep -qE '^[0-9a-f]{40}$' \
|
|
146
156
|
&& git fetch -q origin "$BASE" "$HEAD" 2>/dev/null && [ "$(git rev-parse -q --verify "refs/remotes/origin/${HEAD}^{commit}" 2>/dev/null)" = "$REVIEW_SHA" ]; then
|
|
157
|
+
# shellcheck disable=SC2015 # either assignment failing resets both, by design
|
|
147
158
|
FILES=$(git diff --name-only "origin/${BASE}...${REVIEW_SHA}" 2>/dev/null) && DIFF=$(git diff "origin/${BASE}...${REVIEW_SHA}" 2>/dev/null) || { FILES=""; DIFF=""; }
|
|
148
159
|
fi
|
|
149
160
|
[ -n "$FILES" ] || block "gh pr diff failed"
|
|
@@ -420,7 +431,9 @@ canonical_roster() { local out="" a d; for a in $1; do d=$(agent_domain "$a"); c
|
|
|
420
431
|
SELECTED_AGENTS=$(canonical_roster "$SELECTED_AGENTS")
|
|
421
432
|
|
|
422
433
|
# --- Phase 3C: review mode (FULL | DELTA | BASE_SYNC_ONLY) ---
|
|
434
|
+
# shellcheck disable=SC2034 # both read by lib/review-delta-mode.sh
|
|
423
435
|
BASE_BRANCH="$BASE"
|
|
436
|
+
# shellcheck disable=SC2034
|
|
424
437
|
COMMENTS_JSON=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" 2>/dev/null || echo "")
|
|
425
438
|
delta_roster() {
|
|
426
439
|
local rounds="${1}" full="${2}" files="${3}" out="security" root
|
|
@@ -473,11 +486,20 @@ if [ "$REVIEW_MODE" != "BASE_SYNC_ONLY" ]; then
|
|
|
473
486
|
_ito="${REVIEW_PREPARE_INDEX_TIMEOUT:-120}"; case "$_ito" in ''|*[!0-9]*|0) _ito=120 ;; esac
|
|
474
487
|
_tbin=$(command -v timeout || command -v gtimeout || true)
|
|
475
488
|
echo "review-prepare: building code index (limit ${_ito}s)" >&2
|
|
476
|
-
|
|
477
|
-
|
|
489
|
+
# forge#4235: keep the build's stderr so a failure is diagnosable (its tail is echoed below); stdout stays discarded.
|
|
490
|
+
_ierr=$(mktemp "$OUT_DIR/.code-index-err-XXXXXX" 2>/dev/null) || _ierr=/dev/null
|
|
491
|
+
if [ -n "$_tbin" ]; then "$_tbin" "$_ito" bash "$CODE_INDEX_SCRIPT" --repo-path "$REPO_PATH" >/dev/null 2>"$_ierr"; _irc=$?
|
|
492
|
+
else bash "$CODE_INDEX_SCRIPT" --repo-path "$REPO_PATH" >/dev/null 2>"$_ierr"; _irc=$?; fi
|
|
478
493
|
if [ "$_irc" -eq 0 ]; then INDEX_OK=1
|
|
479
494
|
elif [ "$_irc" -eq 124 ]; then echo "review-prepare: code index timed out after ${_ito}s — skipping index slices (agents grep instead)" >&2
|
|
480
|
-
else
|
|
495
|
+
else
|
|
496
|
+
echo "review-prepare: code index failed (exit $_irc) — skipping index slices (agents grep instead)" >&2
|
|
497
|
+
# forge#4316: the tail can name repo files; show the repo root as <repo>, strip control chars, cap each line
|
|
498
|
+
[ -s "$_ierr" ] && tail -n 20 "$_ierr" | tr -d '\000-\010\013-\037\177' \
|
|
499
|
+
| awk -v r="$REPO_PATH" '{ l = $0; if (r != "") { o = ""; while ((i = index(l, r)) > 0) { o = o substr(l, 1, i - 1) "<repo>"; l = substr(l, i + length(r)) } l = o l } print substr(l, 1, 200) }' \
|
|
500
|
+
| sed 's/^/review-prepare: code-index: /' >&2
|
|
501
|
+
fi
|
|
502
|
+
[ "$_ierr" = /dev/null ] || rm -f "$_ierr"
|
|
481
503
|
fi
|
|
482
504
|
build_index_slice() {
|
|
483
505
|
local domain="${1}" files
|
|
@@ -109,7 +109,9 @@ setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'CONFLICTING|DIRTY\n' > "
|
|
|
109
109
|
prep "$ROOT"; eq "conflicting" .merge_health.mergeable CONFLICTING; eq "conflicting state" .merge_health.state DIRTY; eq "no retry on CONFLICTING" .merge_health.retries 0
|
|
110
110
|
setup feat/x staging "README.md" "$DIFF_PLAIN"; printf 'UNKNOWN|UNKNOWN\n' > "$FIX/merge.seq"
|
|
111
111
|
prep "$ROOT"; eq "unknown stays unknown after 3 retries" .merge_health.retries 3; eq "still UNKNOWN" .merge_health.mergeable UNKNOWN
|
|
112
|
-
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
112
|
+
setup feat/x staging "README.md" "$DIFF_PLAIN"
|
|
113
|
+
# shellcheck disable=SC2016 # the literal $(rm -rf) is the hostile payload under test
|
|
114
|
+
printf 'garbage|$(rm -rf)\n' > "$FIX/merge.seq"
|
|
113
115
|
prep "$ROOT"; eq "garbage state normalized" .merge_health.state UNKNOWN
|
|
114
116
|
setup feat/x staging "README.md" "$DIFF_PLAIN"; echo 0 > "$FIX/prior"
|
|
115
117
|
prep "$ROOT"; eq "first-time contributor" .pr.first_time_contributor true
|
|
@@ -234,6 +236,7 @@ CFG_OVERRIDE="$WORK/emptyroot.yaml" prep "$WORK/root-notpl"
|
|
|
234
236
|
rc_is "no template source" 3; eq "templates none" .templates.source none
|
|
235
237
|
|
|
236
238
|
# --- 8. issue number extraction and trusted contract ---
|
|
239
|
+
# shellcheck source=/dev/null
|
|
237
240
|
. "$ROOT/scripts/lib/pr-issue-number.sh"
|
|
238
241
|
chk() { local got; got=$(pr_issue_number_of "$2"); if [ "$got" = "$3" ]; then ok; else bad "$1: got '$got', want '$3'"; fi; }
|
|
239
242
|
chk "Closes" "Closes #12" 12
|
|
@@ -271,6 +274,7 @@ setup feat/x staging "README.md" "$DIFF_PLAIN" "00000000000000000000000000000000
|
|
|
271
274
|
prep "$ROOT" --checkout-pr
|
|
272
275
|
rc_is "unknown sha cannot be checked out" 3
|
|
273
276
|
if [ "$(git -C "$REPO_DIR" symbolic-ref --short HEAD 2>/dev/null)" = "main" ]; then ok; else bad "failed checkout moved HEAD"; fi
|
|
277
|
+
# shellcheck disable=SC2016 # matches the literal $ORIG_REF in the script source
|
|
274
278
|
if grep -q 'checkout -f "\$ORIG_REF"' "$ROOT/scripts/review-prepare.sh"; then ok; else bad "restore_ref must use git checkout -f"; fi
|
|
275
279
|
|
|
276
280
|
# --- 10. fail closed on gh and bad arguments ---
|
|
@@ -317,8 +321,24 @@ eq "a hostile cwd roster script is never used" '.roster.agents | index("evil") !
|
|
|
317
321
|
# --- code index build is bounded and visible (forge#4126) ---
|
|
318
322
|
setup feat/x staging "services/api/auth.py" "$DIFF_AUTH_BILL"
|
|
319
323
|
prep "$ROOT"
|
|
320
|
-
eq "real index fills the auth slice" '.index_slices.auth | startswith("Domain files (auth):")' true
|
|
324
|
+
_f0=$fail; eq "real index fills the auth slice" '.index_slices.auth | startswith("Domain files (auth):")' true
|
|
325
|
+
# forge#4235: on failure show review-prepare's stderr (it carries the index build's exit code and stderr tail)
|
|
326
|
+
[ "$fail" -eq "$_f0" ] || sed 's/^/ stderr: /' "$WORK/stderr"
|
|
321
327
|
if grep -q "review-prepare: building code index" "$WORK/stderr"; then ok; else bad "index build must print a progress line on stderr"; fi
|
|
328
|
+
# a failing build reports its exit code and stderr tail, and the slices fall back to grep
|
|
329
|
+
mkroot "$WORK/root-failindex" "" "" templates
|
|
330
|
+
# forge#4316: a line naming a repo file, carrying an ESC and over 200 chars is echoed sanitised and capped
|
|
331
|
+
# shellcheck disable=SC2016 # $2 is the fake index script's own argument
|
|
332
|
+
printf '#!/usr/bin/env bash\necho "index-boom-marker" >&2\nprintf "parse error in %%s/secret/file.py \\033[31m%%0300d\\n" "$2" 0 >&2\nexit 3\n' > "$WORK/root-failindex/scripts/code-index.sh"
|
|
333
|
+
prep "$WORK/root-failindex"
|
|
334
|
+
rc_is "failing index build" 0
|
|
335
|
+
if grep -q "code index failed (exit 3)" "$WORK/stderr"; then ok; else bad "index failure must report the build's exit code on stderr"; fi
|
|
336
|
+
if grep -q "review-prepare: code-index: index-boom-marker" "$WORK/stderr"; then ok; else bad "index failure must echo the build's stderr tail: $(head -c 300 "$WORK/stderr")"; fi
|
|
337
|
+
_ix=$(grep "review-prepare: code-index: parse error" "$WORK/stderr")
|
|
338
|
+
case "$_ix" in "review-prepare: code-index: parse error in <repo>/secret/file.py [31m0"*) ok ;; *) bad "index stderr must show the repo root as <repo> without control chars: $(printf '%s' "$_ix" | head -c 120 | od -c | head -3)" ;; esac
|
|
339
|
+
if [ "${#_ix}" -le $((28 + 200)) ]; then ok; else bad "index stderr line must be capped at 200 chars, got ${#_ix}"; fi
|
|
340
|
+
eq "failed index falls back to grep slices" .index_slices.auth "Code index not available — agent will use grep exploration"
|
|
341
|
+
if ls "$WORK/out"/.code-index-err-* >/dev/null 2>&1; then bad "index stderr temp file must be removed"; else ok; fi
|
|
322
342
|
if command -v timeout >/dev/null 2>&1 || command -v gtimeout >/dev/null 2>&1; then
|
|
323
343
|
mkroot "$WORK/root-slowindex" "" "" templates
|
|
324
344
|
printf '#!/usr/bin/env bash\nsleep 30\n' > "$WORK/root-slowindex/scripts/code-index.sh"
|