forgedock 1.19.0 → 1.21.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 (53) hide show
  1. package/README.md +2 -0
  2. package/bin/engine/phases.mjs +4 -2
  3. package/bin/engine/telemetry-seam.mjs +1 -1
  4. package/bin/engine-cli.mjs +40 -1
  5. package/bin/engine.mjs +48 -3
  6. package/bin/forgedock.mjs +1 -1
  7. package/bin/lean/effects.mjs +51 -0
  8. package/bin/lean/lean.mjs +352 -0
  9. package/bin/lean/lean.test.mjs +525 -0
  10. package/bin/lean/prompts.mjs +62 -0
  11. package/bin/lean/risk.mjs +49 -0
  12. package/bin/runner.mjs +99 -6
  13. package/commands/orchestrate/phase-4-execution.md +1 -1
  14. package/commands/review-pr-agents/protocols.md +19 -0
  15. package/commands/review-pr.md +7 -6
  16. package/commands/work-on/build/architect.md +1 -1
  17. package/commands/work-on/build/implement.md +9 -1
  18. package/commands/work-on/build/validate.md +14 -7
  19. package/commands/work-on/build.md +118 -8
  20. package/commands/work-on/close.md +9 -4
  21. package/commands/work-on/remediate.md +11 -4
  22. package/commands/work-on/review.md +12 -4
  23. package/commands/work-on.md +4 -4
  24. package/docs/CONFIG.md +8 -1
  25. package/docs/RELEASE-NOTES.md +21 -0
  26. package/docs/TELEMETRY.md +2 -2
  27. package/docs/WORK-ON-RUNTIME.md +11 -3
  28. package/package.json +2 -2
  29. package/scripts/architect-resume-match.test.sh +12 -3
  30. package/scripts/build-knowledge-index.mjs +8 -5
  31. package/scripts/builder-resume-match.test.sh +12 -3
  32. package/scripts/calibration.mjs +5 -2
  33. package/scripts/check-spec-markers.sh +2 -0
  34. package/scripts/ci-local/lib.mjs +11 -3
  35. package/scripts/ci-local/run.mjs +155 -49
  36. package/scripts/ci-local/scoping.mjs +27 -4
  37. package/scripts/ci-local.sh +3 -2
  38. package/scripts/ci-local.test.sh +189 -0
  39. package/scripts/classify-finding.sh +1 -1
  40. package/scripts/file-deferred-followups.sh +91 -30
  41. package/scripts/file-deferred-followups.test.sh +101 -8
  42. package/scripts/lib/telemetry.sh +2 -0
  43. package/scripts/review-finalize.sh +101 -12
  44. package/scripts/review-finalize.test.sh +136 -5
  45. package/scripts/review-roster.sh +18 -5
  46. package/scripts/review-roster.test.sh +57 -1
  47. package/scripts/selfreview-plan.sh +86 -0
  48. package/scripts/selfreview-plan.test.sh +90 -0
  49. package/scripts/spawn-depth-check.sh +5 -4
  50. package/scripts/spawn-depth-check.test.sh +3 -0
  51. package/scripts/telemetry.test.sh +8 -0
  52. package/scripts/transition-label.sh +26 -0
  53. package/scripts/verify-phase-trail.test.sh +6 -0
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ # selfreview-plan.sh — build-time self-review plan: run or skip, and which roster (forge#3971).
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ #
6
+ # Pure decision script: flags in, key=value lines out. No network, no git, no eval; every flag value is data.
7
+ # It calls scripts/review-roster.sh (resolved through forge_resolve, never from the repo under build) on the staged
8
+ # diff's file list so the self-review roster equals the roster /review-pr would pick for the same diff.
9
+ #
10
+ # Usage: selfreview-plan.sh --files-file PATH|- [--changed-lines N] [--changed-files N] [--contract-flags "HIGH_RISK ..."]
11
+ # [--spec-cli true|false] [--review-depth STANDARD|EXTENDED] [--scores "AUTH=n ..."]
12
+ # [--agent-available true|false] [--tree HASH] [--done-trees "HASH HASH ..."]
13
+ # Env: FORGE_SELFREVIEW=off|0|false|disabled operator off switch
14
+ # Output (stdout, always all four lines):
15
+ # SELFREVIEW=run|skip SKIP_REASON=<reason or empty> TIER=<roster tier or empty> SELECTED_AGENTS=<domains>
16
+ # Skip reasons (first match wins): disabled, empty-diff, docs-only, no-agent-tool, already-done, roster-unavailable,
17
+ # small-security-only (small tier, Security-only roster, no path or contract trigger fired).
18
+ # Exit: 0 for every parseable call, including every degrade-to-skip path (self-review never blocks build); 2 on an unknown flag.
19
+ # Bash 3.2 portable (no mapfile, associative arrays, grep -P).
20
+ set -u
21
+
22
+ FILES_FILE=""; LINES=""; NFILES=""; CFLAGS=""; SPEC_CLI=false; DEPTH=STANDARD; SCORES=""
23
+ AGENT_AVAILABLE=true; TREE=""; DONE_TREES=""
24
+
25
+ while [ $# -gt 0 ]; do
26
+ case "$1" in
27
+ --files-file) FILES_FILE="${2:-}"; shift 2 ;;
28
+ --changed-lines) LINES="${2:-}"; shift 2 ;;
29
+ --changed-files) NFILES="${2:-}"; shift 2 ;;
30
+ --contract-flags) CFLAGS="${2:-}"; shift 2 ;;
31
+ --spec-cli) SPEC_CLI="${2:-false}"; shift 2 ;;
32
+ --review-depth) DEPTH="${2:-STANDARD}"; shift 2 ;;
33
+ --scores) SCORES="${2:-}"; shift 2 ;;
34
+ --agent-available) AGENT_AVAILABLE="${2:-true}"; shift 2 ;;
35
+ --tree) TREE="${2:-}"; shift 2 ;;
36
+ --done-trees) DONE_TREES="${2:-}"; shift 2 ;;
37
+ *) echo "selfreview-plan: unknown argument: $1" >&2; exit 2 ;;
38
+ esac
39
+ done
40
+
41
+ emit() { # <run|skip> <skip reason> <tier> <agents>
42
+ printf 'SELFREVIEW=%s\nSKIP_REASON=%s\nTIER=%s\nSELECTED_AGENTS=%s\n' "$1" "$2" "$3" "$4"
43
+ exit 0
44
+ }
45
+
46
+ case "$(printf '%s' "${FORGE_SELFREVIEW:-}" | tr '[:upper:]' '[:lower:]')" in
47
+ off|0|false|disabled|no) emit skip disabled "" "" ;;
48
+ esac
49
+
50
+ FILES=""
51
+ if [ "$FILES_FILE" = "-" ]; then FILES=$(cat)
52
+ elif [ -n "$FILES_FILE" ] && [ -f "$FILES_FILE" ]; then FILES=$(cat "$FILES_FILE"); fi
53
+ FILES=$(printf '%s\n' "$FILES" | grep -v '^[[:space:]]*$' || true)
54
+
55
+ [ -n "$FILES" ] || emit skip empty-diff "" ""
56
+
57
+ # Docs-only: every file is documentation. commands/**, .agents/** and skills/** are executable spec, never docs.
58
+ CODE_FILES=$(printf '%s\n' "$FILES" | grep -vE '^(docs/|README|CHANGELOG|LICENSE|NOTICE)|\.(md|txt|rst)$' || true)
59
+ SPEC_FILES=$(printf '%s\n' "$FILES" | grep -E '^(commands|\.agents|skills)/.*\.md$' || true)
60
+ [ -n "$CODE_FILES" ] || [ -n "$SPEC_FILES" ] || emit skip docs-only "" ""
61
+
62
+ [ "$AGENT_AVAILABLE" = "true" ] || emit skip no-agent-tool "" ""
63
+
64
+ if [ -n "$TREE" ]; then
65
+ for _t in $DONE_TREES; do
66
+ [ "$_t" = "$TREE" ] && emit skip already-done "" ""
67
+ done
68
+ fi
69
+
70
+ 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() { :; }; } # allowlist:portability — canonical resolver block; readlink -f is guarded (2>/dev/null) and optional
71
+ ROSTER_SCRIPT=$(forge_resolve review-roster.sh)
72
+ [ -n "$ROSTER_SCRIPT" ] && [ -f "$ROSTER_SCRIPT" ] || emit skip roster-unavailable "" ""
73
+
74
+ set -- --files-file - --contract-flags "$CFLAGS" --spec-cli "$SPEC_CLI" --review-depth "$DEPTH"
75
+ [ -n "$LINES" ] && set -- "$@" --changed-lines "$LINES"
76
+ [ -n "$NFILES" ] && set -- "$@" --changed-files "$NFILES"
77
+ [ -n "$SCORES" ] && set -- "$@" --scores "$SCORES"
78
+ OUT=$(printf '%s\n' "$FILES" | bash "$ROSTER_SCRIPT" "$@" 2>/dev/null) || emit skip roster-unavailable "" ""
79
+
80
+ SEL=$(printf '%s\n' "$OUT" | sed -n 's/^SELECTED_AGENTS=//p' | head -n 1)
81
+ TIER=$(printf '%s\n' "$OUT" | sed -n 's/^TIER=//p' | head -n 1)
82
+ [ -n "$SEL" ] && [ -n "$TIER" ] || emit skip roster-unavailable "" ""
83
+
84
+ # Cost guard: the small tier runs only when a path or contract trigger widened the roster past Security.
85
+ if [ "$TIER" = "small" ] && [ "$SEL" = "security" ]; then emit skip small-security-only "$TIER" "$SEL"; fi
86
+ emit run "" "$TIER" "$SEL"
@@ -0,0 +1,90 @@
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/selfreview-plan.sh (build-time self-review plan, forge#3971). Run: bash scripts/selfreview-plan.test.sh
6
+ # (bash 3.2 compatible)
7
+ set -u
8
+ DIR=$(cd "$(dirname "$0")" && pwd)
9
+ ROOT=$(cd "$DIR/.." && pwd)
10
+ SCRIPT="$DIR/selfreview-plan.sh"
11
+ pass=0; fail=0
12
+ ok() { pass=$((pass+1)); }
13
+ bad() { fail=$((fail+1)); echo "FAIL: $1"; }
14
+ WORK=$(mktemp -d "${TMPDIR:-/tmp}/selfreview-plan.XXXXXX")
15
+ trap 'rm -rf "$WORK"' EXIT
16
+ export FORGEDOCK_HOME="$ROOT" # the resolver must not pick up an ambient install
17
+ unset FORGE_SELFREVIEW
18
+
19
+ PLAN=""; RC=0
20
+ # run <files, newline separated> <flags...>
21
+ run() {
22
+ printf '%s\n' "$1" > "$WORK/files"; shift
23
+ PLAN=$(bash "$SCRIPT" --files-file "$WORK/files" "$@" 2>/dev/null); RC=$?
24
+ }
25
+ field() { printf '%s\n' "$PLAN" | sed -n "s/^$1=//p"; }
26
+ expect() { # name key want
27
+ local got; got=$(field "$2")
28
+ if [ "$got" = "$3" ]; then ok; else bad "$1: $2='$got', want '$3'"; fi
29
+ }
30
+ has_agent() { case " $(field SELECTED_AGENTS) " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
31
+
32
+ # e1: docs-only and empty diffs skip with a recorded reason
33
+ run "docs/GUIDE.md
34
+ README.md" --changed-lines 40
35
+ expect "docs-only skips" SELFREVIEW skip; expect "docs-only reason" SKIP_REASON docs-only
36
+ run "" --changed-lines 0
37
+ expect "empty diff skips" SELFREVIEW skip; expect "empty reason" SKIP_REASON empty-diff
38
+ run "commands/work-on/build.md" --changed-lines 40 --changed-files 1
39
+ expect "spec markdown is not docs" SELFREVIEW skip
40
+ if [ "$(field SKIP_REASON)" = "docs-only" ]; then bad "commands/*.md treated as docs"; else ok; fi
41
+
42
+ # e2: small tier, Security-only roster, no trigger: skipped; the same size with an auth path runs
43
+ run "scripts/foo.sh" --changed-lines 30 --changed-files 1
44
+ expect "small security-only skips" SELFREVIEW skip; expect "small reason" SKIP_REASON small-security-only; expect "small tier" TIER small
45
+ run "src/auth/session.py
46
+ src/util.py" --changed-lines 30 --changed-files 2
47
+ expect "small + auth path runs" SELFREVIEW run; expect "small + auth tier" TIER small
48
+ has_agent auth && ok || bad "auth path did not add auth"
49
+ has_agent security && ok || bad "security floor missing"
50
+ run "scripts/foo.sh
51
+ scripts/bar.sh" --changed-lines 30 --changed-files 2 --contract-flags "HIGH_RISK"
52
+ expect "small + HIGH_RISK runs" SELFREVIEW run; expect "HIGH_RISK lifts tier" TIER standard
53
+
54
+ # standard tier runs
55
+ run "scripts/foo.sh
56
+ scripts/bar.sh" --changed-lines 400 --changed-files 2
57
+ expect "standard runs" SELFREVIEW run; expect "standard tier" TIER standard; expect "standard no reason" SKIP_REASON ""
58
+ has_agent security && ok || bad "standard roster lacks security"
59
+
60
+ # EXTENDED depth lifts a small roster
61
+ run "scripts/foo.sh" --changed-lines 20 --changed-files 1 --review-depth EXTENDED
62
+ expect "extended depth runs" SELFREVIEW run; expect "extended tier" TIER standard
63
+
64
+ # e3: off switch, no Agent tool, unresolvable roster degrade to skip and exit 0
65
+ FORGE_SELFREVIEW=off run "scripts/a.sh
66
+ scripts/b.sh" --changed-lines 400
67
+ [ "$RC" = 0 ] && ok || bad "off exit code $RC"
68
+ expect "off skips" SELFREVIEW skip; expect "off reason" SKIP_REASON disabled
69
+ run "scripts/a.sh
70
+ scripts/b.sh" --changed-lines 400 --agent-available false
71
+ [ "$RC" = 0 ] && ok || bad "no-agent exit code $RC"
72
+ expect "no agent skips" SELFREVIEW skip; expect "no agent reason" SKIP_REASON no-agent-tool
73
+ FORGEDOCK_HOME=/nonexistent-forge-home run "scripts/a.sh
74
+ scripts/b.sh" --changed-lines 400
75
+ [ "$RC" = 0 ] && ok || bad "unresolvable exit code $RC"
76
+ expect "unresolvable roster skips" SELFREVIEW skip; expect "unresolvable reason" SKIP_REASON roster-unavailable
77
+
78
+ # e4: a tree that already has a completed self-review is not dispatched again
79
+ run "scripts/a.sh
80
+ scripts/b.sh" --changed-lines 400 --tree aaa --done-trees "bbb aaa"
81
+ expect "same tree skips" SELFREVIEW skip; expect "same tree reason" SKIP_REASON already-done
82
+ run "scripts/a.sh
83
+ scripts/b.sh" --changed-lines 400 --tree ccc --done-trees "bbb aaa"
84
+ expect "new tree runs" SELFREVIEW run
85
+
86
+ # usage: unknown flag exits 2
87
+ bash "$SCRIPT" --nope >/dev/null 2>&1; [ "$?" = 2 ] && ok || bad "unknown flag should exit 2"
88
+
89
+ echo "selfreview-plan: ${pass} passed, ${fail} failed"
90
+ [ "$fail" -eq 0 ]
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env bash
2
- # spawn-depth-check.sh — will a /work-on dispatching phase (review, remediate) have the Agent tool?
2
+ # spawn-depth-check.sh — will a /work-on dispatching phase (build self-review, review, remediate) have the Agent tool?
3
3
  # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
4
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  #
6
6
  # Claude Code grants the Agent tool to sub-agents down to CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH layers
7
7
  # below the main conversation (default 3 since 2.1.219; 5 for 2.1.172-2.1.216; 1 for 2.1.217-2.1.218).
8
- # A sub-agent at layer L can spawn only when L < depth. The router invokes review/remediate at
8
+ # A sub-agent at layer L can spawn only when L < depth. The router invokes build/review/remediate at
9
9
  # router layer + 1, so they can spawn reviewers only when router layer + 2 <= depth (forge#3398,
10
- # docs/WORK-ON-RUNTIME.md).
10
+ # docs/WORK-ON-RUNTIME.md). build (B5.6 self-review, forge#3971) uses the same arithmetic but degrades to a recorded
11
+ # skip on FAIL instead of stopping the run.
11
12
  #
12
13
  # Usage: spawn-depth-check.sh [--router-layer N] (N = 0 solo, 1 under /orchestrate; default 0)
13
14
  # Env: CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH explicit limit (wins over the version default)
@@ -69,5 +70,5 @@ if [ "$DEPTH" -ge "$REQUIRED" ]; then
69
70
  echo "SPAWN_DEPTH: OK depth=${DEPTH} required=${REQUIRED} (${SOURCE})"
70
71
  exit 0
71
72
  fi
72
- echo "SPAWN_DEPTH: FAIL depth=${DEPTH} required=${REQUIRED} (${SOURCE}) — review/remediate would run without the Agent tool and could not spawn /review-pr reviewers. Set CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH>=${REQUIRED}."
73
+ echo "SPAWN_DEPTH: FAIL depth=${DEPTH} required=${REQUIRED} (${SOURCE}) — build/review/remediate would run without the Agent tool and could not spawn self-review or /review-pr reviewers. Set CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH>=${REQUIRED}."
73
74
  exit 1
@@ -43,5 +43,8 @@ t "env depth 09 decimal" 0 "SPAWN_DEPTH: OK" CLAUDE_CODE_MAX_SUBA
43
43
  # Usage errors.
44
44
  t "bad router layer" 2 "spawn-depth-check:" -- --router-layer x
45
45
  t "unknown flag" 2 "usage:" -- --nope
46
+ # forge#3971: build is a dispatching phase too, and the FAIL text names it (exit codes unchanged)
47
+ out=$(env -u CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH -u FORGE_RUNTIME FORGE_CLAUDE_VERSION=2.1.217 bash "$S" 2>&1)
48
+ case "$out" in *build/review/remediate*) PASS=$((PASS+1)) ;; *) FAILN=$((FAILN+1)); echo "FAIL: FAIL text does not name build ($out)" ;; esac
46
49
  echo "spawn-depth-check: ${PASS} passed, ${FAILN} failed"
47
50
  [ "$FAILN" -eq 0 ]
@@ -61,6 +61,14 @@ check "finding_disposition carries scope" "$(grep '^{' "$TD/posted" | jq -r '.sc
61
61
  reset; FORGE_PR=9 forge_emit review_panel mode=FULL; grep -q 'issues/9/comments' "$TD/calls" && ok || bad "review_panel targets PR"
62
62
  reset; FORGE_PR=9 forge_emit diff_size lines=1; grep -q 'issues/5/comments' "$TD/calls" && ok || bad "diff_size targets issue"
63
63
 
64
+ # build_selfreview (forge#3971) is a build-side event: posted to the issue even when a PR number is set; ids and counts only
65
+ reset; FORGE_PR=9 forge_emit build_selfreview tier=standard agents=3 findings=4 fixed=2 declined=1 skipped=
66
+ grep -q 'issues/5/comments' "$TD/calls" && ok || bad "build_selfreview targets issue"
67
+ REC=$(grep '^{' "$TD/posted")
68
+ check "build_selfreview event" "$(printf '%s' "$REC" | jq -r '.event')" "build_selfreview"
69
+ check "build_selfreview counts typed" "$(printf '%s' "$REC" | jq -r '.agents + .findings + .fixed + .declined')" "10"
70
+ check "build_selfreview skip reason" "$(reset; forge_emit build_selfreview skipped=small-security-only tier=small; grep '^{' "$TD/posted" | jq -r '.skipped')" "small-security-only"
71
+
64
72
  # 2. Sanitising: reserved keys, bad keys, backticks and newlines never reach the record.
65
73
  reset
66
74
  forge_emit x repo=evil 'a b=1' ok='a`b
@@ -278,6 +278,32 @@ fi
278
278
  # ---------------------------------------------------------------------------
279
279
  # Add target label
280
280
  # ---------------------------------------------------------------------------
281
+ # forge#3567: `gh issue edit --add-label` fails when the label is absent from the repo (a
282
+ # consumer repo not re-synced via `forgedock labels setup`), aborting the transition under
283
+ # `set -e`. Best-effort ensure first: `--force` is a create-or-update, a failure (permissions,
284
+ # rate limit, concurrent create) is ignored so the add below surfaces its own error. Only
285
+ # ForgeDock-managed names are created (fixed table mirroring bin/labels.json, kept in sync by
286
+ # bin/tests/label-bootstrap.test.mjs); a custom FORGE_LABEL_MAP name is never created.
287
+ ensure_label() {
288
+ local name="$1" LBL_COLOR="" LBL_DESC=""
289
+ case "$name" in
290
+ "workflow:investigating") LBL_COLOR="1D76DB"; LBL_DESC="Pipeline: investigation phase in progress. Managed by ForgeDock." ;;
291
+ "workflow:ready-to-build") LBL_COLOR="0075CA"; LBL_DESC="Pipeline: investigation complete, ready for build. Managed by ForgeDock." ;;
292
+ "workflow:building") LBL_COLOR="0052CC"; LBL_DESC="Pipeline: implementation in progress. Managed by ForgeDock." ;;
293
+ "workflow:in-review") LBL_COLOR="5319E7"; LBL_DESC="Pipeline: PR created, under review. Managed by ForgeDock." ;;
294
+ "workflow:remediating") LBL_COLOR="C5DEF5"; LBL_DESC="Pipeline: autonomous remediation pending (no human action needed). Managed by ForgeDock." ;;
295
+ "workflow:awaiting-merge") LBL_COLOR="FF8C00"; LBL_DESC="Pipeline: remediated + re-reviewed, awaiting a human merge decision. Managed by ForgeDock." ;;
296
+ "workflow:merged") LBL_COLOR="0E8A16"; LBL_DESC="Pipeline: PR merged, issue closed. Managed by ForgeDock." ;;
297
+ "workflow:decomposed") LBL_COLOR="BFD4F2"; LBL_DESC="Pipeline: decomposed into sub-issues. Managed by ForgeDock." ;;
298
+ "workflow:invalid") LBL_COLOR="CCCCCC"; LBL_DESC="Pipeline: issue closed as invalid after investigation. Managed by ForgeDock." ;;
299
+ "needs-human") LBL_COLOR="E4E669"; LBL_DESC="Pipeline stalled — requires human intervention. Managed by ForgeDock." ;;
300
+ "workflow:engine-error") LBL_COLOR="B60205"; LBL_DESC="Pipeline stalled — engine/tool failure (not a genuine human-judgment block). Managed by ForgeDock." ;;
301
+ *) return 0 ;;
302
+ esac
303
+ gh label create "$name" --color "$LBL_COLOR" --description "$LBL_DESC" --force "${GH_ARGS[@]}" >/dev/null 2>&1 || true
304
+ }
305
+ ensure_label "$EFFECTIVE_LABEL"
306
+
281
307
  echo "Adding $EFFECTIVE_LABEL to issue #$ISSUE_NUMBER..."
282
308
  gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --add-label "$EFFECTIVE_LABEL"
283
309
 
@@ -58,6 +58,7 @@ mk() {
58
58
  FP_*) items+=("{\"body\":\"<!-- FORGE:FAST_PATH -->\\n**COMPLEXITY_BAND**: ${k#FP_}\"}") ;;
59
59
  QUOTED) items+=('{"body":"see `<!-- FORGE:CONTRACT -->` `<!-- FORGE:CONTEXT -->` `<!-- FORGE:ARCHITECT -->` `<!-- FORGE:QUALITY_GATE -->` **Result**: PASS"}') ;;
60
60
  INV_INVALID) items+=('{"body":"<!-- FORGE:INVESTIGATOR -->\nInvalid\n<!-- INVESTIGATION:INVALID -->"}') ;;
61
+ SR) items+=('{"body":"<!-- FORGE:SELFREVIEW-AGENT:security -->\nTree: abc\n<!-- REVIEW-FINDINGS-START -->\n<!-- REVIEW-FINDINGS-END -->"}' '{"body":"<!-- FORGE:SELFREVIEW -->\n## Self-review\nTree: abc"}') ;;
61
62
  PROSE) items+=('{"body":"I ran the architect and context inline; FORGE:CONTEXT FORGE:ARCHITECT FORGE:QUALITY_GATE done"}') ;;
62
63
  esac
63
64
  done
@@ -82,6 +83,11 @@ expect_pass "lowercase band is accepted" "$(mk lower INV CONTRACT FP_trivial QG_
82
83
  expect_pass "Investigation band passes with only INVESTIGATOR+FAST_PATH" "$(mk invest INV FP_INVESTIGATION)"
83
84
  expect_pass "docs-only waives QUALITY_GATE" "$(mk docs INV CONTRACT FP_TRIVIAL)" --docs-only
84
85
 
86
+ # forge#3971: build-time self-review comments are tolerated but never satisfy a required phase marker
87
+ expect_pass "self-review comments do not disturb a full trail" "$(mk sr1 INV CONTRACT FP_COMPLEX CONTEXT ARCH QG_PASS SR)"
88
+ expect_fail "self-review comments do not stand in for QUALITY_GATE" "$(mk sr2 INV CONTRACT FP_COMPLEX CONTEXT ARCH SR)" QUALITY_GATE
89
+ expect_fail "self-review comments do not stand in for CONTEXT" "$(mk sr3 INV CONTRACT FP_STANDARD ARCH QG_PASS SR)" CONTEXT
90
+
85
91
  expect_fail "missing INVESTIGATOR" "$(mk a CONTRACT FP_COMPLEX CONTEXT ARCH QG_PASS)" INVESTIGATOR
86
92
  expect_fail "partial investigation (no COMPLETE) fails" "$(mk b INV_PARTIAL CONTRACT FP_COMPLEX CONTEXT ARCH QG_PASS)" INVESTIGATOR
87
93
  expect_fail "missing CONTRACT" "$(mk c INV FP_COMPLEX CONTEXT ARCH QG_PASS)" CONTRACT