forgedock 1.10.3 → 1.11.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.
Files changed (46) hide show
  1. package/commands/ci-audit.md +3 -3
  2. package/commands/compat-audit.md +1 -1
  3. package/commands/deploy-pr.md +6 -6
  4. package/commands/explain.md +2 -2
  5. package/commands/fix-ci.md +2 -2
  6. package/commands/forge-stats.md +1 -1
  7. package/commands/forgedock-init.md +3 -3
  8. package/commands/optimize.md +5 -5
  9. package/commands/orchestrate/phase-1-resolve.md +2 -2
  10. package/commands/orchestrate/phase-4-execution.md +182 -41
  11. package/commands/quality-gate.md +1 -1
  12. package/commands/recover-orphans.md +6 -6
  13. package/commands/review-pr-agents/protocols.md +1 -0
  14. package/commands/review-pr-staging.md +28 -6
  15. package/commands/review-pr.md +434 -120
  16. package/commands/security-audit.md +1 -1
  17. package/commands/spec-doctor.md +2 -2
  18. package/commands/test-gate.md +7 -7
  19. package/commands/work-on/build/architect.md +21 -0
  20. package/commands/work-on/build/context.md +1 -1
  21. package/commands/work-on/build/implement.md +17 -0
  22. package/commands/work-on/build/validate.md +2 -2
  23. package/commands/work-on/build.md +98 -9
  24. package/commands/work-on/close.md +33 -11
  25. package/commands/work-on/decompose.md +19 -5
  26. package/commands/work-on/investigate.md +34 -4
  27. package/commands/work-on/remediate.md +30 -6
  28. package/commands/work-on/review.md +10 -8
  29. package/commands/work-on.md +20 -11
  30. package/docs/CONFIG.md +30 -0
  31. package/docs/RELEASE-NOTES.md +9 -0
  32. package/docs/WORK-ON-RUNTIME.md +22 -2
  33. package/docs/site/architecture.md +1 -1
  34. package/docs/spec/forge-protocol-v1.md +10 -0
  35. package/docs/spec/review-protocol.md +1 -0
  36. package/package.json +1 -1
  37. package/packages/protocol/src/types.js +17 -0
  38. package/scripts/check-spec-bash.sh +45 -2
  39. package/scripts/check-spec-bash.test.sh +24 -1
  40. package/scripts/check-spec-markers.sh +53 -0
  41. package/scripts/diff-size.sh +84 -0
  42. package/scripts/diff-size.test.sh +117 -0
  43. package/scripts/forge-root.test.sh +15 -0
  44. package/scripts/remediation-marker-match.test.sh +51 -0
  45. package/scripts/review-pr-arguments.test.sh +180 -0
  46. package/scripts/trusted-comments.test.sh +23 -0
@@ -12,7 +12,19 @@
12
12
  #
13
13
  # Usage:
14
14
  # check-spec-bash.sh [--base <git-ref>] [--shellcheck] <file.md>...
15
+ # check-spec-bash.sh --positional [<file.md>...]
16
+ # check-spec-bash.sh (no args: same as --positional over all Skill-loaded commands/**/*.md,
17
+ # excluding orchestrate/**, pipeline-health/**, review-pr-agents/**)
15
18
  #
19
+ # --positional Different check, opt-in: fail on any `$0`..`$9` inside a fenced code block.
20
+ # Claude Code rewrites those tokens with the invocation's arguments whenever a
21
+ # spec is loaded via Skill/slash command WITH args (awk `{print $2}` silently
22
+ # becomes `{print --issue}`). Use `${1}` in shell and `$(1)` in awk instead;
23
+ # `${N}`, `$(N)`, `$NF`, `$@`, `$#` and `$10`+ are not substituted. Suppress a
24
+ # deliberate hit by putting `allowlist:positional-arg` on the same line.
25
+ # Scope the file list to Skill-loaded specs (Read-loaded orchestrate/**,
26
+ # pipeline-health/** and the review-pr-agents catalog are not substituted).
27
+ # Output: `FAIL <file>:<line> positional arg ...` then `SPEC-POSITIONAL: ...`.
16
28
  # --base <ref> Only check blocks that contain a line changed relative to <ref>
17
29
  # (`git diff -U0 <ref> -- <file>`), so pre-existing blocks never fail
18
30
  # a new change. Without --base every block is checked.
@@ -33,20 +45,51 @@
33
45
 
34
46
  set -u
35
47
 
36
- BASE=""; SHELLCHECK=0; FILES=""
37
- usage() { echo "ERROR: Usage: check-spec-bash.sh [--base <git-ref>] [--shellcheck] <file.md>..." >&2; exit 2; }
48
+ BASE=""; SHELLCHECK=0; POSITIONAL=0; FILES=""
49
+ usage() { echo "ERROR: Usage: check-spec-bash.sh [--base <git-ref>] [--shellcheck] <file.md>... | --positional [<file.md>...]" >&2; exit 2; }
38
50
 
39
51
  while [ "$#" -gt 0 ]; do
40
52
  case "$1" in
41
53
  --base) [ "$#" -ge 2 ] || usage; BASE="$2"; shift 2 ;;
42
54
  --shellcheck) SHELLCHECK=1; shift ;;
55
+ --positional) POSITIONAL=1; shift ;;
43
56
  --*) usage ;;
44
57
  *) FILES="$FILES
45
58
  $1"; shift ;;
46
59
  esac
47
60
  done
61
+
62
+ # No file arguments: default to the --positional check over every Skill-loaded spec under commands/
63
+ # (orchestrate/**, pipeline-health/** and the review-pr-agents catalog are Read-loaded, not substituted).
64
+ # Resolved relative to this script, so it works from any cwd and from linked worktrees.
65
+ if [ -z "$FILES" ] && { [ "$#" -eq 0 ] && [ "$POSITIONAL" = 0 ] && [ -z "$BASE" ] && [ "$SHELLCHECK" = 0 ] || [ "$POSITIONAL" = 1 ]; }; then
66
+ POSITIONAL=1
67
+ cd "$(dirname "$0")/.." || exit 2
68
+ FILES=$(find commands -name '*.md' -not -path 'commands/orchestrate/*' \
69
+ -not -path 'commands/pipeline-health/*' -not -path 'commands/review-pr-agents/*' | LC_ALL=C sort)
70
+ fi
48
71
  [ -n "$FILES" ] || usage
49
72
 
73
+ if [ "$POSITIONAL" = 1 ]; then
74
+ pfail=0; pfiles=0
75
+ while IFS= read -r f; do
76
+ [ -n "$f" ] || continue
77
+ case "$f" in *.md) ;; *) continue ;; esac
78
+ [ -f "$f" ] || { echo "SKIP $f missing"; continue; }
79
+ pfiles=$((pfiles + 1))
80
+ hits=$(awk -v f="$f" '
81
+ /^[ \t]*```/ { inb = !inb; next }
82
+ inb && /\$[0-9]([^0-9]|$)/ && !/allowlist:positional-arg/ { print "FAIL " f ":" NR " positional arg ($0-$9) in fenced block: " $0 }
83
+ ' "$f")
84
+ if [ -n "$hits" ]; then printf '%s\n' "$hits"; pfail=$((pfail + $(printf '%s\n' "$hits" | wc -l))); fi
85
+ done <<EOP
86
+ $FILES
87
+ EOP
88
+ echo "SPEC-POSITIONAL: files=$pfiles violations=$pfail"
89
+ [ "$pfail" -eq 0 ]
90
+ exit $?
91
+ fi
92
+
50
93
  TMPD=$(mktemp -d "${TMPDIR:-/tmp}/check-spec-bash.XXXXXX") || exit 2
51
94
  trap 'rm -rf "$TMPD"' EXIT
52
95
 
@@ -70,7 +70,9 @@ check 0 "SKIP allow.md:2 allowlisted" "allowlisted fragment skipped" -
70
70
  check 0 "checked=0 failed=0" "non-bash fence ignored" -- notbash.md
71
71
  check 1 "FAIL prose-mention.md:2" "prose mention of the marker does not allowlist" -- prose-mention.md
72
72
  check 1 "checked=2 failed=1 skipped=1" "multi-file summary" -- good.md bad.md pseudo.md
73
- check 2 "" "no files is a usage error" --
73
+ check 0 "SPEC-POSITIONAL: files=" "no args defaults to --positional over commands/" --
74
+ check 0 "SPEC-POSITIONAL: files=" "--positional with no files uses the default set" -- --positional
75
+ check 2 "" "--shellcheck alone is a usage error" -- --shellcheck
74
76
  check 2 "" "unknown flag" -- --bogus good.md
75
77
 
76
78
  # --base: only blocks touching changed lines are checked, so pre-existing breakage never fails a change.
@@ -86,5 +88,26 @@ ${FENCE}
86
88
  EOF
87
89
  check 1 "FAIL good.md:" "--base: newly added broken block fails" -- --base HEAD good.md
88
90
 
91
+ # --positional: $0-$9 inside fences are flagged; ${N}, $(N), $NF, prose and allowlisted lines are not.
92
+ cat > "$T/pos-bad.md" <<EOF
93
+ ${FENCE}bash
94
+ X=\$(echo a b | awk '{print \$2}')
95
+ local a="\$1"
96
+ ${FENCE}
97
+ EOF
98
+ cat > "$T/pos-good.md" <<EOF
99
+ Prose mentions \$1 and \$2 freely.
100
+ ${FENCE}bash
101
+ X=\$(echo a b | awk '{print \$(2) \$NF}')
102
+ local a="\${1}" b="\$@" n="\$#"
103
+ echo \$10
104
+ y=\$1 # allowlist:positional-arg
105
+ ${FENCE}
106
+ EOF
107
+ check 1 "FAIL pos-bad.md:2" "--positional flags awk \$2" -- --positional pos-bad.md
108
+ check 1 "FAIL pos-bad.md:3" "--positional flags shell \$1" -- --positional pos-bad.md
109
+ check 1 "violations=2" "--positional counts violations" -- --positional pos-bad.md
110
+ check 0 "violations=0" "--positional passes safe forms, prose, allowlist" -- --positional pos-good.md
111
+
89
112
  echo "check-spec-bash.test.sh: passed=$pass failed=$fail"
90
113
  [ "$fail" -eq 0 ]
@@ -105,6 +105,7 @@ MARKER_REGISTRY="
105
105
  DECOMPOSED
106
106
  DECOMPOSE_BLOCKED
107
107
  DESIGN
108
+ DIFF_SIZE
108
109
  DISPATCH
109
110
  DISPATCHER
110
111
  DOSSIER_UPDATED
@@ -157,6 +158,7 @@ MARKER_REGISTRY="
157
158
  RECOVERY_CLAIM_RELEASED
158
159
  REMEDIATION
159
160
  REREVIEW_DISPATCHED
161
+ REREVIEW_RELEASED
160
162
  REREVIEW_SKIPPED
161
163
  REVIEW
162
164
  REVIEWER
@@ -165,6 +167,7 @@ MARKER_REGISTRY="
165
167
  SECURITY_AUDIT
166
168
  SIGNAL_RESOLVED
167
169
  SIGNAL_UNRESOLVED
170
+ SIZE_OVERRIDE
168
171
  SPAWN_POLICY
169
172
  SPEC_DOCTOR_COMPLETE
170
173
  SPEC_LOADED
@@ -361,6 +364,56 @@ if [ -f "$P4_SPEC" ]; then
361
364
  fi
362
365
  fi
363
366
 
367
+ # ---------------------------------------------------------------------------
368
+ # Router claim pre-check must classify the existing/winning claim's lease (forge#3431)
369
+ # ---------------------------------------------------------------------------
370
+
371
+ WORKON_SPEC="$COMMANDS_DIR/work-on.md"
372
+ if [ -f "$WORKON_SPEC" ]; then
373
+ # Scope: the "Claim pre-check" paragraph only, from its "**Claim pre-check" line up to the line before
374
+ # "**Terminal fallback". Full-line HTML comments are skipped, then each numbered step (N. ...) is collapsed
375
+ # to one whitespace-normalised line tagged "STEP<N>:" so tokens in other steps, the Terminal fallback
376
+ # paragraph, or comments cannot satisfy another step's assertion.
377
+ PRECHECK_STEPS=$(awk '
378
+ /^\*\*Terminal fallback/ { inpre = 0 }
379
+ /^\*\*Claim pre-check/ { inpre = 1; next }
380
+ inpre {
381
+ line = $0
382
+ if (line ~ /^[[:space:]]*<!--.*-->[[:space:]]*$/) next
383
+ gsub(/<!--[^>]*-->/, "", line)
384
+ if (match(line, /^[0-9]+\. /)) {
385
+ n = substr(line, 1, RLENGTH - 2)
386
+ cur = n
387
+ steps[cur] = "STEP" n ": " substr(line, RLENGTH + 1)
388
+ next
389
+ }
390
+ if (line ~ /^Only after the read-back/) { cur = ""; next }
391
+ if (cur != "") steps[cur] = steps[cur] " " line
392
+ }
393
+ END { for (k in steps) print steps[k] }
394
+ ' "$WORKON_SPEC" | tr -s '[:space:]' ' ' | sed 's/ STEP/\nSTEP/g' || true)
395
+ # WIRE:PROVEN: manual mutation in a temp copy of commands/work-on.md: removing `STALE` from step 4 only fired step 4, removing it from step 2 only fired step 2, tokens only in an HTML comment or only in the Terminal fallback paragraph still fired, a router-local REREVIEW_LEASE_SECS= or rereview_lease_state fired, clean tree passes
396
+ for STEP_N in 2 4; do
397
+ STEP_TXT=$(printf '%s\n' "$PRECHECK_STEPS" | grep "^STEP${STEP_N}:" || true)
398
+ if [ -z "$STEP_TXT" ] \
399
+ || [[ "$STEP_TXT" != *STALE* ]] \
400
+ || [[ "$STEP_TXT" != *phase-4-execution.md* ]] \
401
+ || [[ "$STEP_TXT" != *fallback* ]]; then
402
+ echo "HIGH | $WORKON_SPEC | claim pre-check step $STEP_N does not route STALE to the terminal fallback via the shared phase-4-execution.md Step 1 classification" >&2
403
+ VIOLATIONS=$((VIOLATIONS + 1))
404
+ fi
405
+ done
406
+ # The router must reference the orchestrator's single lease definition, never redefine it.
407
+ if grep -qE '^[[:space:]]*REREVIEW_LEASE_SECS=' "$WORKON_SPEC"; then
408
+ echo "HIGH | $WORKON_SPEC | router redefines REREVIEW_LEASE_SECS (single definition lives in phase-4-execution.md Step 1)" >&2
409
+ VIOLATIONS=$((VIOLATIONS + 1))
410
+ fi
411
+ if grep -rq 'rereview_lease_state' "$COMMANDS_DIR" 2>/dev/null; then
412
+ echo "HIGH | $COMMANDS_DIR | competing lease helper rereview_lease_state reintroduced (router must reuse the shared Step 1 classification)" >&2
413
+ VIOLATIONS=$((VIOLATIONS + 1))
414
+ fi
415
+ fi
416
+
364
417
  # ---------------------------------------------------------------------------
365
418
  # Summary
366
419
  # ---------------------------------------------------------------------------
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # diff-size.sh — the ONE measurement of a build's diff size for the build phase size gate (forge#3450).
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ #
6
+ # Usage: diff-size.sh --repo-path P --base B [--threshold N] [--exclude-glob G]...
7
+ # Measures lines changed (added + deleted) by the index plus any earlier commits against the MERGE BASE of
8
+ # HEAD and origin/B (git diff --cached --numstat --no-renames $(git merge-base HEAD origin/B)), so commits that
9
+ # landed on the base after the branch point are never counted as this build's changes. Exit 2 when the merge
10
+ # base cannot be computed. It must run AFTER implement staged its changes
11
+ # and BEFORE validate commits. Unstaged or untracked files are not measured. Binary files count 0.
12
+ # Files matching the built-in default globs or any --exclude-glob are excluded (additive).
13
+ # Glob rules: a configured (--exclude-glob) glob ending in "/" matches that directory at any depth; any other
14
+ # glob is a bash case pattern matched against the basename and the full path. The built-in directory defaults
15
+ # build/ dist/ generated/ fixtures/ vendor/ are anchored to the repo root (so first-party dirs such as
16
+ # commands/work-on/build/ are counted); __generated__/ and __snapshots__/ match at any depth.
17
+ # Globs are data, never evaluated.
18
+ # Default globs (mirrored in docs/CONFIG.md under build.diff_size.exclude_globs):
19
+ # lockfiles (package-lock.json yarn.lock pnpm-lock.yaml Cargo.lock poetry.lock uv.lock go.sum Gemfile.lock
20
+ # composer.lock), *.min.*, *.snap, root-anchored dist/ build/ generated/ fixtures/ vendor/, any-depth
21
+ # __generated__/ __snapshots__/
22
+ # Output (stdout, key=value lines): diff_lines=N excluded_lines=M threshold=T over=true|false, then up to 10
23
+ # "top=<lines> <path>" lines, largest first. threshold 0 disables the gate (over=false always).
24
+ # Exit: 0 measured (even when over); 2 usage or git failure with EMPTY stdout (fail closed: unknown is not zero).
25
+
26
+ set -euo pipefail
27
+
28
+ REPO_PATH=""; BASE=""; THRESHOLD="1000"; EXTRA=()
29
+ die() { echo "diff-size.sh: $*" >&2; exit 2; }
30
+ while [ $# -gt 0 ]; do
31
+ case "$1" in
32
+ --repo-path) [ $# -ge 2 ] || die "missing value for $1"; REPO_PATH="$2"; shift 2 ;;
33
+ --base) [ $# -ge 2 ] || die "missing value for $1"; BASE="$2"; shift 2 ;;
34
+ --threshold) [ $# -ge 2 ] || die "missing value for $1"; THRESHOLD="$2"; shift 2 ;;
35
+ --exclude-glob) [ $# -ge 2 ] || die "missing value for $1"; [ -z "$2" ] || EXTRA+=("$2"); shift 2 ;;
36
+ *) die "unknown argument: $1" ;;
37
+ esac
38
+ done
39
+ [ -n "$REPO_PATH" ] && [ -d "$REPO_PATH" ] || die "--repo-path must be an existing directory"
40
+ [ -n "$BASE" ] || die "--base is required"
41
+ case "$THRESHOLD" in ""|*[!0-9]*) die "--threshold must be a non-negative integer" ;; esac
42
+
43
+ DEFAULTS=(package-lock.json yarn.lock pnpm-lock.yaml Cargo.lock poetry.lock uv.lock go.sum Gemfile.lock composer.lock
44
+ '*.min.*' '*.snap' __generated__/ __snapshots__/)
45
+ ROOT_DIRS=(dist/ build/ generated/ fixtures/ vendor/)
46
+ GLOBS=("${DEFAULTS[@]}" ${EXTRA[@]+"${EXTRA[@]}"})
47
+
48
+ excluded() { # path -> 0 when excluded
49
+ local p="$1" b="${1##*/}" g
50
+ for g in "${ROOT_DIRS[@]}"; do case "$p" in "$g"*) return 0 ;; esac; done
51
+ for g in "${GLOBS[@]}"; do
52
+ case "$g" in
53
+ */) case "/$p" in *"/$g"*) return 0 ;; esac ;;
54
+ *) case "$b" in $g) return 0 ;; esac; case "$p" in $g) return 0 ;; esac ;;
55
+ esac
56
+ done
57
+ return 1
58
+ }
59
+
60
+ MB="$(git -C "$REPO_PATH" merge-base HEAD "origin/$BASE" 2>/dev/null)" && [ -n "$MB" ] \
61
+ || die "cannot compute merge base of HEAD and origin/$BASE"
62
+ RAW="$(git -C "$REPO_PATH" diff --cached --numstat --no-renames -z "$MB" -- | tr '\0' '\001')" \
63
+ || die "git diff against merge base $MB failed"
64
+
65
+ TOTAL=0; EXCL=0; LIST=""
66
+ # -z numstat records are "added<TAB>deleted<TAB>path" separated by NUL (translated to \001 above).
67
+ while IFS= read -r -d $'\001' rec; do
68
+ [ -n "$rec" ] || continue
69
+ a="${rec%%$'\t'*}"; rest="${rec#*$'\t'}"; d="${rest%%$'\t'*}"; path="${rest#*$'\t'}"
70
+ case "$a" in ''|*[!0-9]*) a=0 ;; esac
71
+ case "$d" in ''|*[!0-9]*) d=0 ;; esac
72
+ n=$((a + d))
73
+ if excluded "$path"; then EXCL=$((EXCL + n)); else TOTAL=$((TOTAL + n)); LIST="${LIST}${n} ${path}"$'\n'; fi
74
+ done <<< "$RAW"
75
+
76
+ OVER=false
77
+ if [ "$THRESHOLD" -gt 0 ] && [ "$TOTAL" -gt "$THRESHOLD" ]; then OVER=true; fi
78
+ echo "diff_lines=$TOTAL"
79
+ echo "excluded_lines=$EXCL"
80
+ echo "threshold=$THRESHOLD"
81
+ echo "over=$OVER"
82
+ if [ -n "$LIST" ]; then
83
+ printf '%s' "$LIST" | sort -t' ' -k1,1nr | head -n 10 | sed 's/^/top=/' || true
84
+ fi
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # diff-size.test.sh — cases for scripts/diff-size.sh (forge#3450).
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ set -uo pipefail
6
+ S="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/diff-size.sh"
7
+ PASS=0; FAILN=0
8
+ TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
9
+ ok() { PASS=$((PASS+1)); }
10
+ bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
11
+
12
+ # Fixture: a bare "origin" plus a clone on a feature branch.
13
+ git init -q --bare "$TMP/origin.git"
14
+ git clone -q "$TMP/origin.git" "$TMP/w" 2>/dev/null
15
+ cd "$TMP/w" || exit 1
16
+ git config user.email t@example.com; git config user.name t
17
+ echo base > README.md
18
+ git add README.md; git commit -qm base; git branch -M staging; git push -q origin staging 2>/dev/null
19
+ git checkout -q -b feat
20
+
21
+ gen() { awk -v n="$1" 'BEGIN{for(i=0;i<n;i++) print "line " i}'; }
22
+ reset() { git reset -q --hard origin/staging; git clean -qfd; }
23
+ run() { bash "$S" --repo-path "$TMP/w" --base staging "$@"; }
24
+ val() { printf '%s\n' "$1" | sed -n "s/^$2=//p" | head -n1; }
25
+ check() { # name out key want
26
+ local got; got="$(val "$2" "$3")"
27
+ if [ "$got" = "$4" ]; then ok; else bad "$1 ($3=$got want $4)"; fi
28
+ }
29
+
30
+ # empty diff
31
+ OUT="$(run)"; check "empty diff lines" "$OUT" diff_lines 0; check "empty diff over" "$OUT" over false
32
+
33
+ # under threshold
34
+ reset; gen 10 > a.txt; git add a.txt
35
+ OUT="$(run --threshold 100)"; check "under lines" "$OUT" diff_lines 10; check "under over" "$OUT" over false
36
+
37
+ # over threshold, exit 0 even when over
38
+ reset; gen 150 > a.txt; git add a.txt
39
+ OUT="$(run --threshold 100)"; rc=$?
40
+ [ "$rc" = 0 ] && ok || bad "over exits 0 (rc=$rc)"
41
+ check "over lines" "$OUT" diff_lines 150; check "over flag" "$OUT" over true
42
+
43
+ # exactly at threshold is not over
44
+ OUT="$(run --threshold 150)"; check "at threshold" "$OUT" over false
45
+
46
+ # threshold 0 disables
47
+ OUT="$(run --threshold 0)"; check "threshold 0 disables" "$OUT" over false
48
+
49
+ # default exclusions: lockfile, min, snap, root dist/, any-depth __snapshots__/
50
+ reset; gen 5 > a.txt
51
+ gen 500 > package-lock.json; gen 500 > app.min.js; gen 500 > x.snap
52
+ mkdir -p dist pkg/__snapshots__/deep; gen 500 > dist/out.js; gen 500 > pkg/__snapshots__/deep/f.json
53
+ git add -A
54
+ OUT="$(run --threshold 100)"
55
+ check "defaults diff_lines" "$OUT" diff_lines 5; check "defaults excluded" "$OUT" excluded_lines 2500; check "defaults over" "$OUT" over false
56
+
57
+ # SEC-2: built-in directory defaults are root-anchored; first-party nested dirs are counted
58
+ reset; mkdir -p commands/work-on/build pkg/vendor pkg/fixtures; gen 300 > commands/work-on/build/a.md
59
+ gen 200 > pkg/vendor/v.js; gen 100 > pkg/fixtures/f.json; mkdir -p build; gen 50 > build/root.js; git add -A
60
+ OUT="$(run --threshold 100)"
61
+ check "nested defaults counted" "$OUT" diff_lines 600; check "root build excluded" "$OUT" excluded_lines 50; check "nested defaults over" "$OUT" over true
62
+
63
+
64
+ # a directory glob is not a substring match (distribution/ is not dist/)
65
+ reset; mkdir -p distribution; gen 30 > distribution/a.txt; git add -A
66
+ OUT="$(run --threshold 100)"; check "dist is not substring" "$OUT" diff_lines 30
67
+
68
+ # extra glob is additive
69
+ reset; gen 40 > keep.txt; mkdir -p docs/gen; gen 400 > docs/gen/big.md; git add -A
70
+ OUT="$(run --threshold 100 --exclude-glob 'docs/gen/*')"
71
+ check "extra glob lines" "$OUT" diff_lines 40; check "extra glob excluded" "$OUT" excluded_lines 400
72
+ OUT="$(run --threshold 100 --exclude-glob 'docs/gen/*' --exclude-glob '*.txt')"
73
+ check "two extra globs" "$OUT" diff_lines 0
74
+
75
+ # glob is data, not evaluated
76
+ reset; gen 10 > a.txt; git add -A
77
+ OUT="$(run --exclude-glob '$(touch '"$TMP"'/pwned)' 2>/dev/null)"
78
+ [ ! -e "$TMP/pwned" ] && ok || bad "glob evaluated as code"
79
+
80
+ # binary file counts 0
81
+ reset; head -c 2000 /dev/urandom > b.bin; gen 7 > a.txt; git add -A
82
+ OUT="$(run)"; check "binary counts 0" "$OUT" diff_lines 7
83
+
84
+ # deletions and renames count both sides (--no-renames)
85
+ reset; gen 20 > r.txt; git add -A; git commit -qm r; git push -q origin feat:staging 2>/dev/null
86
+ git mv r.txt r2.txt; OUT="$(run)"; check "rename counts add+delete" "$OUT" diff_lines 40
87
+ git reset -q --hard origin/staging
88
+
89
+ # committed + staged are both measured
90
+ reset; gen 10 > c1.txt; git add -A; git commit -qm c1; gen 5 > c2.txt; git add -A
91
+ OUT="$(run)"; check "committed + staged" "$OUT" diff_lines 15
92
+
93
+ # top files, largest first
94
+ reset; gen 3 > s.txt; gen 9 > l.txt; git add -A
95
+ OUT="$(run)"; FIRST="$(printf '%s\n' "$OUT" | sed -n 's/^top=//p' | head -n1)"
96
+ [ "$FIRST" = "9 l.txt" ] && ok || bad "top ordering (got '$FIRST')"
97
+
98
+ # fail closed: bad repo path, bad base, bad threshold, unknown arg, missing base
99
+ OUT="$(bash "$S" --repo-path "$TMP/nope" --base staging 2>/dev/null)"; rc=$?
100
+ { [ "$rc" = 2 ] && [ -z "$OUT" ]; } && ok || bad "bad repo path (rc=$rc out='$OUT')"
101
+ OUT="$(run2() { bash "$S" --repo-path "$TMP/w" --base nonexistent 2>/dev/null; }; run2)"; rc=$?
102
+ { [ "$rc" = 2 ] && [ -z "$OUT" ]; } && ok || bad "bad base (rc=$rc out='$OUT')"
103
+ OUT="$(bash "$S" --repo-path "$TMP/w" --base staging --threshold abc 2>/dev/null)"; rc=$?
104
+ { [ "$rc" = 2 ] && [ -z "$OUT" ]; } && ok || bad "bad threshold (rc=$rc)"
105
+ OUT="$(bash "$S" --repo-path "$TMP/w" --bogus 2>/dev/null)"; rc=$?
106
+ { [ "$rc" = 2 ] && [ -z "$OUT" ]; } && ok || bad "unknown arg (rc=$rc)"
107
+ OUT="$(bash "$S" --repo-path "$TMP/w" 2>/dev/null)"; rc=$?
108
+ { [ "$rc" = 2 ] && [ -z "$OUT" ]; } && ok || bad "missing base (rc=$rc)"
109
+
110
+ # SEC-1: commits landing on the base after the branch point are not counted
111
+ reset; git checkout -q staging; gen 3000 > base-only.txt; git add base-only.txt; git commit -qm "base moves"
112
+ git push -q origin staging 2>/dev/null; git checkout -q feat; gen 10 > a.txt; git add a.txt
113
+ OUT="$(run --threshold 100)"
114
+ check "moving base lines" "$OUT" diff_lines 10; check "moving base over" "$OUT" over false
115
+
116
+ echo "diff-size.test.sh: $PASS passed, $FAILN failed"
117
+ [ "$FAILN" = 0 ]
@@ -229,5 +229,20 @@ if command -v node >/dev/null 2>&1; then
229
229
  done
230
230
  fi
231
231
 
232
+ # Guard (#3400): gate-helper (trusted-comments.sh) resolution loops must never list "$PWD" — the repo
233
+ # under review is author-controlled. Exact copy counts + normalized identity prevent drift.
234
+ TC_LOOPS="commands/review-pr.md:7 commands/work-on/review.md:1"
235
+ : > "$T/tc_all"
236
+ for e in $TC_LOOPS; do
237
+ f="${e%%:*}"; want="${e##*:}"
238
+ n=$(grep -c 'trusted-comments.sh" \]' "$ROOT/$f"); expect "trusted-comments.sh loop count in $f" "$want" "$n"
239
+ grep -n 'for _c in' "$ROOT/$f" | while IFS=: read -r ln _; do
240
+ sed -n "$((ln+1))p" "$ROOT/$f" | grep -q 'trusted-comments.sh' && sed -n "${ln}p" "$ROOT/$f" | sed 's/^[ ]*//' >> "$T/tc_all"
241
+ done
242
+ done
243
+ expect "trusted-comments.sh loops total" 8 "$(wc -l < "$T/tc_all" | tr -d ' ')"
244
+ expect "trusted-comments.sh loops byte-identical" 1 "$(sort -u "$T/tc_all" | wc -l | tr -d ' ')"
245
+ grep -qF '"$PWD"' "$T/tc_all" && bad 'trusted-comments.sh loop lists "$PWD"' || ok
246
+
232
247
  echo "forge-root tests: pass=$PASS fail=$FAILN skipped=$SKIPPED"
233
248
  [ "$FAILN" -eq 0 ]
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # remediation-marker-match.test.sh — the anchored, trusted FORGE:REMEDIATION trail match (forge#3412)
3
+ # Exercises the exact regexes used by commands/work-on/remediate.md Phase M0, commands/work-on.md Phase 0B
4
+ # and commands/orchestrate/phase-4-execution.md. No network. Usage: bash scripts/remediation-marker-match.test.sh
5
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
6
+ # SPDX-License-Identifier: AGPL-3.0-or-later
7
+
8
+ set -uo pipefail
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
11
+ TC="$SCRIPT_DIR/trusted-comments.sh"
12
+ unset FORGE_TRAIL_TRUSTED_ASSOCIATIONS FORGE_TRAIL_TRUSTED_LOGINS
13
+
14
+ PASS=0; FAILN=0
15
+ ok() { PASS=$((PASS+1)); echo "ok - $1"; }
16
+ bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
17
+ eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (got '$2', want '$3')"; fi; }
18
+
19
+ COMPLETE_RE='^<!-- FORGE:REMEDIATION -->[\s\S]*<!-- FORGE:REMEDIATION:COMPLETE -->'
20
+ TRAIL_RE='^<!-- FORGE:REMEDIATION -->'
21
+ # c <assoc> <type> <login> <body>
22
+ c() { jq -nc --arg a "$1" --arg t "$2" --arg l "$3" --arg b "$4" '{id:1,author_association:$a,user:{type:$t,login:$l},body:$b}'; }
23
+ complete_n() { printf '%s' "$1" | bash "$TC" count "$COMPLETE_RE"; }
24
+ trail_n() { printf '%s' "$1" | bash "$TC" count "$TRAIL_RE"; }
25
+
26
+ REAL=$'<!-- FORGE:REMEDIATION -->\n## Remediation\nVerdict: FIXED\n\n<!-- FORGE:REMEDIATION:COMPLETE -->'
27
+ INTERIM=$'<!-- FORGE:REMEDIATION -->\n## Remediation (in progress)'
28
+ QUOTED_REVIEW=$'## Review\nThe prior run posted FORGE:REMEDIATION:COMPLETE on this PR.\n<!-- FORGE:REMEDIATION:COMPLETE -->'
29
+ QUOTED_WORK_ORDER=$'<!-- FORGE:INPR_FIX: head=abc1234 -->\nquoting <!-- FORGE:REMEDIATION -->\n<!-- FORGE:REMEDIATION:COMPLETE -->'
30
+
31
+ eq "genuine M8 trail by bot matches COMPLETE" "$(complete_n "[$(c NONE Bot 'app[bot]' "$REAL")]")" 1
32
+ eq "review comment quoting COMPLETE mid-body does not match" "$(complete_n "[$(c OWNER User u "$QUOTED_REVIEW")]")" 0
33
+ eq "INPR_FIX work order quoting the markers does not match COMPLETE" "$(complete_n "[$(c NONE Bot b "$QUOTED_WORK_ORDER")]")" 0
34
+ eq "INPR_FIX work order quoting the markers is not a trail comment" "$(trail_n "[$(c NONE Bot b "$QUOTED_WORK_ORDER")]")" 0
35
+ eq "quoting comment posted AFTER the real trail does not hide it" "$(complete_n "[$(c NONE Bot b "$REAL"),$(c NONE Bot b "$QUOTED_WORK_ORDER")]")" 1
36
+ eq "untrusted-author trail does not match" "$(complete_n "[$(c NONE User mallory "$REAL")]")" 0
37
+ eq "trusted human (MEMBER) trail matches" "$(complete_n "[$(c MEMBER User dev "$REAL")]")" 1
38
+ eq "interim trail (no COMPLETE) is a trail but not complete" "$(complete_n "[$(c NONE Bot b "$INTERIM")]")/$(trail_n "[$(c NONE Bot b "$INTERIM")]")" "0/1"
39
+ eq "paginated concatenated arrays" "$(complete_n "[$(c NONE Bot b "$QUOTED_REVIEW")] [$(c NONE Bot b "$REAL")]")" 1
40
+ printf 'not json' | bash "$TC" count "$COMPLETE_RE" >/dev/null 2>&1; eq "unparsable comments fail closed (exit 2)" "$?" 2
41
+
42
+ # Spec wiring: no unanchored marker read remains, and each reader goes through the trust helper.
43
+ ! grep -qF 'contains("FORGE:REMEDIATION")' "$ROOT/commands/work-on/remediate.md" && ok "remediate.md has no unanchored contains(FORGE:REMEDIATION)" || bad "remediate.md still uses unanchored contains"
44
+ ! grep -qF 'contains("FORGE:REMEDIATION")' "$ROOT/commands/orchestrate/phase-4-execution.md" && ok "phase-4-execution.md has no unanchored contains(FORGE:REMEDIATION)" || bad "phase-4-execution.md still uses unanchored contains"
45
+ for f in commands/work-on/remediate.md commands/work-on.md commands/orchestrate/phase-4-execution.md; do
46
+ grep -qF 'trusted-comments.sh' "$ROOT/$f" && ok "$f references trusted-comments.sh" || bad "$f does not reference trusted-comments.sh"
47
+ done
48
+ grep -qF "count '$COMPLETE_RE'" "$ROOT/commands/work-on/remediate.md" && ok "remediate.md uses the tested COMPLETE regex" || bad "remediate.md COMPLETE regex drifted from this test"
49
+
50
+ echo "passed=$PASS failed=$FAILN"
51
+ [ "$FAILN" -eq 0 ]