forgedock 1.7.1 → 1.8.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 (47) hide show
  1. package/bin/engine/admission.mjs +242 -7
  2. package/bin/engine/admission.test.mjs +183 -2
  3. package/bin/engine/phases.mjs +31 -22
  4. package/bin/engine/resolve.mjs +8 -1
  5. package/bin/engine/resolve.test.mjs +5 -0
  6. package/bin/engine.mjs +26 -17
  7. package/bin/forgedock.mjs +15 -2
  8. package/bin/hooks/pre-tool-use.mjs +573 -2
  9. package/bin/journey.mjs +11 -7
  10. package/bin/labels.json +5 -0
  11. package/bin/opencode-adapter.mjs +47 -13
  12. package/bin/orchestrate-preflight.mjs +434 -0
  13. package/bin/runner.mjs +30 -4
  14. package/commands/audit-agents.md +75 -26
  15. package/commands/autopilot.md +4 -4
  16. package/commands/cleanup.md +10 -5
  17. package/commands/issue.md +95 -35
  18. package/commands/orchestrate/phase-1-resolve.md +70 -32
  19. package/commands/orchestrate/phase-3-dependency.md +141 -13
  20. package/commands/orchestrate/phase-4-execution.md +660 -70
  21. package/commands/orchestrate/phase-5-cleanup.md +5 -11
  22. package/commands/orchestrate/phase-6-report.md +27 -1
  23. package/commands/orchestrate/safety.md +1 -1
  24. package/commands/orchestrate.md +39 -1
  25. package/commands/quality-gate.md +30 -10
  26. package/commands/review-pr-agents/protocols.md +13 -0
  27. package/commands/review-pr-agents.md +1 -1
  28. package/commands/review-pr-staging.md +100 -18
  29. package/commands/review-pr.md +78 -24
  30. package/commands/work-on/build/validate.md +25 -4
  31. package/commands/work-on/close.md +40 -10
  32. package/commands/work-on/decompose.md +33 -1
  33. package/commands/work-on/investigate.md +33 -0
  34. package/commands/work-on/remediate.md +40 -5
  35. package/commands/work-on/review.md +27 -0
  36. package/commands/work-on.md +48 -5
  37. package/docs/CONFIG.md +45 -10
  38. package/docs/OPENCODE.md +21 -6
  39. package/docs/spec/review-protocol.md +7 -0
  40. package/package.json +1 -1
  41. package/scripts/check-admission-parity.mjs +1 -0
  42. package/scripts/check-command-side-effects.sh +14 -13
  43. package/scripts/check-shell-portability.sh +36 -0
  44. package/scripts/check-spec-markers.sh +21 -11
  45. package/scripts/checkbox-sections.test.sh +67 -0
  46. package/scripts/extract-affected-files.sh +85 -5
  47. package/scripts/extract-affected-files.test.sh +154 -2
@@ -7,7 +7,7 @@
7
7
  # extract-affected-files.sh <issue_number> "-R <owner/repo>" (single pre-joined token also accepted)
8
8
  #
9
9
  # Output (stdout):
10
- # Line 1: PROVENANCE=affected-files-section | body-fallback | none | error
10
+ # Line 1: PROVENANCE=contract-deliverables | affected-files-section | body-fallback | none | error
11
11
  # Line 2+: one extracted file path per line (zero lines when PROVENANCE=none or error)
12
12
  #
13
13
  # Exit codes:
@@ -16,12 +16,39 @@
16
16
  # that files were found).
17
17
  # 2 — Usage error (missing issue number, malformed -R value).
18
18
  #
19
- # Extraction rules (forge#2436):
19
+ # Extraction rules (forge#2436, extended forge#2848):
20
+ # 0. Highest-confidence path — the issue's FORGE:CONTRACT comment (if one exists),
21
+ # scoped to ONLY its own "### Deliverables" section. A contract deliverables table
22
+ # states *intent to change* ("I will edit this file"), whereas an investigation's
23
+ # Affected Files list and especially a raw issue body routinely name files as
24
+ # *context* ("this interacts with X", "similar to the check in Y"). Preferring the
25
+ # contract is what stops a cohort of issues that merely cite the same file from
26
+ # being serialized against each other (forge#2848).
27
+ # -> PROVENANCE=contract-deliverables
28
+ #
29
+ # TEMPORAL CAVEAT — do not over-claim this source: FORGE:CONTRACT is posted at
30
+ # *build* time (work-on/build.md Phase B2), which is AFTER /orchestrate Phase 3
31
+ # builds the DAG. On a cold first-pass plan no contract exists yet, so this path is
32
+ # inert by construction. It pays off only on the paths that re-extract later: wake /
33
+ # re-plan after compaction, mid-batch re-derivation (phase-4-execution.md's DONE-arm
34
+ # DROP handling), and IN_PROGRESS predecessors built in an earlier wave or session.
35
+ #
36
+ # FALL-THROUGH RULE (load-bearing — see forge#2848 risk table): a contract that
37
+ # exists but whose Deliverables section yields ZERO paths MUST fall through to the
38
+ # INVESTIGATOR path below. A contract is an *upgrade* over the investigator source,
39
+ # never a replacement for it — blackholing to `none` here would fire Layer 4's
40
+ # conservative serialization for exactly the mid-batch issues this change exists to
41
+ # un-serialize, i.e. the bug being fixed, inverted. Note this is deliberately NOT
42
+ # the same shape as the INVESTIGATOR->body relationship in rule 2, which does NOT
43
+ # fall through on zero files (see rule 2's own note, forge#2382).
20
44
  # 1. Primary path — the issue's FORGE:INVESTIGATOR comment (if one exists), scoped to
21
45
  # ONLY its own "### Affected Files" section. Capture stops at the next markdown
22
46
  # heading of any level, so paths mentioned in "### Evidence", "### Root Cause",
23
47
  # "### Related Issues", etc. are never collected.
24
48
  # -> PROVENANCE=affected-files-section
49
+ # NOTE (forge#2382): unlike rule 0, an INVESTIGATOR comment that EXISTS but yields
50
+ # zero paths does NOT fall through to rule 2 — an investigation that scoped its own
51
+ # Affected Files section to nothing is a confirmed-empty result, not a missing one.
25
52
  # 2. Fallback path — used ONLY when no FORGE:INVESTIGATOR comment exists at all.
26
53
  # Scoped to a deliverables-shaped heading in the raw issue body:
27
54
  # "## Affected Files", "## Deliverables", or "### Files to change". Capture stops
@@ -126,6 +153,20 @@ fi
126
153
  # deeper sub-heading inside a deliverables section still closes capture
127
154
  # rather than leaking into unrelated prose.
128
155
  # --------------------------------------------------------------------------- #
156
+ extract_contract_section() {
157
+ # forge#2848 — same sentinel shape as the two helpers below, targeting the
158
+ # FORGE:CONTRACT comment's "### Deliverables" table. The `/^### Deliverables/`
159
+ # heading and the awk sentinel are the ones already proven in-tree at
160
+ # commands/work-on/build.md:218 and :258; only the path-extraction step differs
161
+ # (this script uses grep -oE via extract_paths, never build.md's grep -oP —
162
+ # forge#2436 removed the PCRE dependency on purpose, see extract_paths below).
163
+ awk '
164
+ /^### Deliverables/ { p=1; next }
165
+ /^#/ { p=0 }
166
+ p { print }
167
+ ' <<< "$1"
168
+ }
169
+
129
170
  extract_investigator_section() {
130
171
  awk '
131
172
  /^### Affected Files/ { p=1; next }
@@ -152,13 +193,49 @@ extract_paths() {
152
193
  grep -oE "$EXT_REGEX" <<< "$1" 2>/dev/null | tr -d '`' | sort -u || true
153
194
  }
154
195
 
155
- # --------------------------------------------------------------------------- #
156
- # Primary path: FORGE:INVESTIGATOR comment, scoped to its own Affected Files section
157
- # --------------------------------------------------------------------------- #
158
196
  PROVENANCE="none"
159
197
  FILES=""
160
198
  GH_CALL_FAILED=0
161
199
 
200
+ # --------------------------------------------------------------------------- #
201
+ # Highest-confidence path: FORGE:CONTRACT comment, scoped to its own Deliverables
202
+ # section (forge#2848). Runs AHEAD of the INVESTIGATOR path because a deliverables
203
+ # table states intent to change, while an Affected Files list may name files that
204
+ # are merely relevant. See rule 0 in the header for the temporal caveat (a contract
205
+ # only exists post-build, so this is inert on a cold first-pass plan).
206
+ #
207
+ # Uses the same `if ! VAR=$(cmd); then GH_CALL_FAILED=1; fi` idiom as the calls
208
+ # below — never `|| true` — so a failed contract fetch stays distinguishable from
209
+ # "no contract exists" and can still escalate to PROVENANCE=error if nothing else
210
+ # recovers real data (forge#2504).
211
+ # --------------------------------------------------------------------------- #
212
+ if ! CONTRACT_BODY=$(gh api "repos/${REPO}/issues/${NUM}/comments" \
213
+ --jq '[.[] | select(.body | contains("FORGE:CONTRACT"))] | last | .body // ""' 2>/dev/null); then
214
+ GH_CALL_FAILED=1
215
+ echo "WARNING: gh api call failed while fetching comments for issue #$NUM (repo: $REPO) — cannot confirm whether a FORGE:CONTRACT comment exists; treating as inconclusive, not empty" >&2
216
+ CONTRACT_BODY=""
217
+ fi
218
+
219
+ if [[ -n "$CONTRACT_BODY" ]]; then
220
+ SCOPED=$(extract_contract_section "$CONTRACT_BODY")
221
+ FILES=$(extract_paths "$SCOPED")
222
+ if [[ -n "$FILES" ]]; then
223
+ PROVENANCE="contract-deliverables"
224
+ fi
225
+ fi
226
+
227
+ # --------------------------------------------------------------------------- #
228
+ # Primary path: FORGE:INVESTIGATOR comment, scoped to its own Affected Files section
229
+ #
230
+ # Guarded on `-z "$FILES"` so the contract path above FALLS THROUGH when it yielded
231
+ # zero paths (no contract, no Deliverables section, or a deliverables table naming no
232
+ # recognized file extension). The guard wraps the whole INVESTIGATOR/body block from
233
+ # the outside, leaving the INVESTIGATOR->body-fallback relationship inside it
234
+ # byte-identical — that inner relationship deliberately does NOT fall through on zero
235
+ # files (forge#2382), and this change must not alter it.
236
+ # --------------------------------------------------------------------------- #
237
+ if [[ -z "$FILES" ]]; then
238
+
162
239
  # NOTE: deliberately no `| tail -1` here — `gh api --jq`'s raw-string output
163
240
  # for a multi-line `.body` field embeds literal newlines, so isolating "the
164
241
  # last comment" by taking the last output LINE would instead truncate to
@@ -215,6 +292,9 @@ else
215
292
  fi
216
293
  fi
217
294
  fi
295
+ fi # end contract fall-through guard (forge#2848) — the INVESTIGATOR/body block
296
+ # above is intentionally left un-indented so it stays byte-identical to its
297
+ # pre-forge#2848 form; only the surrounding guard is new.
218
298
 
219
299
  # forge#2504: only escalate to "error" when neither path yielded real data AND
220
300
  # at least one gh call genuinely failed along the way. A failed primary call
@@ -28,18 +28,38 @@ trap cleanup EXIT
28
28
 
29
29
  # --------------------------------------------------------------------------- #
30
30
  # Mock `gh` — intercepts:
31
- # gh api repos/<repo>/issues/<num>/comments --jq '...' -> cat $MOCK_GH_COMMENTS (if set+exists, else empty)
32
- # gh issue view <num> -R <repo> --json body --jq '...' -> cat $MOCK_GH_BODY (if set+exists, else empty)
31
+ # gh api .../comments --jq '...FORGE:CONTRACT...' -> cat $MOCK_GH_CONTRACT (if set+exists, else empty)
32
+ # gh api .../comments --jq '...FORGE:INVESTIGATOR...' -> cat $MOCK_GH_COMMENTS (if set+exists, else empty)
33
+ # gh issue view <num> -R <repo> --json body --jq '...' -> cat $MOCK_GH_BODY (if set+exists, else empty)
33
34
  # Any other invocation is a test setup error.
34
35
  #
36
+ # forge#2848: the two `gh api` call sites hit the same endpoint and differ only in
37
+ # their --jq selector, so the mock dispatches on which FORGE marker the selector
38
+ # names. This keeps the contract source and the investigator source independently
39
+ # fixturable — required to assert the contract-beats-investigator ordering rule.
40
+ #
35
41
  # forge#2504: MOCK_GH_COMMENTS_FAIL=1 / MOCK_GH_BODY_FAIL=1 force the respective
36
42
  # call site to simulate a genuine `gh` failure (non-zero exit, nothing on
37
43
  # stdout) instead of a "call succeeded, empty/no-match result" — this is the
38
44
  # distinction extract-affected-files.sh must now surface as PROVENANCE=error.
45
+ # MOCK_GH_CONTRACT_FAIL=1 does the same for the contract fetch alone; note that
46
+ # MOCK_GH_COMMENTS_FAIL=1 fails BOTH `gh api` call sites, which is the faithful
47
+ # simulation of a real network/auth/rate-limit outage (a real outage does not
48
+ # selectively spare one of two calls to the same endpoint).
39
49
  # --------------------------------------------------------------------------- #
40
50
  cat > "$TMP_BIN/gh" <<'MOCK'
41
51
  #!/usr/bin/env bash
42
52
  if [[ "$1" == "api" ]]; then
53
+ if [[ "$*" == *"FORGE:CONTRACT"* ]]; then
54
+ if [[ "${MOCK_GH_CONTRACT_FAIL:-}" == "1" || "${MOCK_GH_COMMENTS_FAIL:-}" == "1" ]]; then
55
+ echo "mock gh api: simulated failure (network error / rate limit)" >&2
56
+ exit 1
57
+ fi
58
+ if [[ -n "${MOCK_GH_CONTRACT:-}" && -f "${MOCK_GH_CONTRACT:-}" ]]; then
59
+ cat "$MOCK_GH_CONTRACT"
60
+ fi
61
+ exit 0
62
+ fi
43
63
  if [[ "${MOCK_GH_COMMENTS_FAIL:-}" == "1" ]]; then
44
64
  echo "mock gh api: simulated failure (network error / rate limit)" >&2
45
65
  exit 1
@@ -320,6 +340,138 @@ MOCK_GH_COMMENTS_FAIL=1 MOCK_GH_BODY="$BODY_SCOPED" assert_output \
320
340
  "forge#2504: primary gh api call fails but fallback succeeds with real files -> body-fallback, not error" \
321
341
  "body-fallback" "bin/engine.mjs,bin/engine/phases.mjs" 2506 -R test/repo
322
342
 
343
+ # --------------------------------------------------------------------------- #
344
+ # Scenario 12 (forge#2848): FORGE:CONTRACT comment present with a real
345
+ # "### Deliverables" table -> PROVENANCE=contract-deliverables. Paths named in
346
+ # the contract's OTHER sections ("### Proposed Approach", "### Out of Scope")
347
+ # must be ignored — same heading-scoping guarantee the investigator path has.
348
+ # --------------------------------------------------------------------------- #
349
+ CONTRACT_FULL="$TMP_FIXTURES/contract_full.txt"
350
+ cat > "$CONTRACT_FULL" <<'EOF'
351
+ <!-- FORGE:CONTRACT -->
352
+ ## Builder Contract
353
+
354
+ **Task type**: Bug Fix
355
+
356
+ ### Proposed Approach
357
+
358
+ Mirrors the approach already taken in `bin/approach-only-context.mjs`.
359
+
360
+ ### Deliverables
361
+
362
+ | File | Change | Why |
363
+ |------|--------|-----|
364
+ | `scripts/real-target.sh` | rewrite the parser | the actual defect |
365
+ | `bin/real-target.mjs` | update the caller | keeps the pair in sync |
366
+
367
+ ### Out of Scope
368
+
369
+ Anything under `bin/out-of-scope-file.mjs`.
370
+ EOF
371
+
372
+ unset MOCK_GH_COMMENTS MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
373
+ MOCK_GH_CONTRACT="$CONTRACT_FULL" assert_output \
374
+ "forge#2848: FORGE:CONTRACT ### Deliverables -> contract-deliverables, ignores Approach/Out-of-Scope paths" \
375
+ "contract-deliverables" "bin/real-target.mjs,scripts/real-target.sh" 2848 -R test/repo
376
+
377
+ # --------------------------------------------------------------------------- #
378
+ # Scenario 13 (forge#2848 ORDERING rule): both a FORGE:CONTRACT and a
379
+ # FORGE:INVESTIGATOR comment exist, and they name DIFFERENT files. The contract
380
+ # must win — a deliverables table states intent to change, while an Affected
381
+ # Files list may name files that are merely relevant. This is the assertion that
382
+ # pins the provenance precedence order; without it the two sources could silently
383
+ # swap with no test failure.
384
+ # --------------------------------------------------------------------------- #
385
+ INV_DIFFERENT="$TMP_FIXTURES/inv_different.txt"
386
+ cat > "$INV_DIFFERENT" <<'EOF'
387
+ <!-- FORGE:INVESTIGATOR -->
388
+ ## Investigation Report
389
+
390
+ ### Affected Files
391
+
392
+ 1. `scripts/investigator-guess.sh` — suspected
393
+ 2. `bin/investigator-guess.mjs` — suspected
394
+ EOF
395
+
396
+ unset MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
397
+ MOCK_GH_CONTRACT="$CONTRACT_FULL" MOCK_GH_COMMENTS="$INV_DIFFERENT" assert_output \
398
+ "forge#2848 ordering: contract beats investigator when both exist and disagree" \
399
+ "contract-deliverables" "bin/real-target.mjs,scripts/real-target.sh" 2849 -R test/repo
400
+
401
+ # --------------------------------------------------------------------------- #
402
+ # Scenario 14 (forge#2848 FALL-THROUGH rule — load-bearing): a FORGE:CONTRACT
403
+ # exists but yields ZERO paths. It must fall through to the INVESTIGATOR path,
404
+ # NOT blackhole to PROVENANCE=none. Blackholing here would fire Layer 4's
405
+ # conservative serialization for exactly the mid-batch issues this change exists
406
+ # to un-serialize (the bug being fixed, inverted — see the forge#2848 risk table).
407
+ # Two zero-yield shapes are covered: no "### Deliverables" heading at all, and a
408
+ # Deliverables section that names no recognized file path.
409
+ # --------------------------------------------------------------------------- #
410
+ CONTRACT_NO_DELIV="$TMP_FIXTURES/contract_no_deliv.txt"
411
+ cat > "$CONTRACT_NO_DELIV" <<'EOF'
412
+ <!-- FORGE:CONTRACT -->
413
+ ## Builder Contract
414
+
415
+ ### Proposed Approach
416
+
417
+ Prose only, mentioning `bin/should-not-be-extracted.mjs` as context.
418
+
419
+ ### Out of Scope
420
+
421
+ Everything else.
422
+ EOF
423
+
424
+ unset MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
425
+ MOCK_GH_CONTRACT="$CONTRACT_NO_DELIV" MOCK_GH_COMMENTS="$INV_DIFFERENT" assert_output \
426
+ "forge#2848 fall-through: contract with no ### Deliverables section -> falls through to affected-files-section" \
427
+ "affected-files-section" "bin/investigator-guess.mjs,scripts/investigator-guess.sh" 2850 -R test/repo
428
+
429
+ CONTRACT_EMPTY_DELIV="$TMP_FIXTURES/contract_empty_deliv.txt"
430
+ cat > "$CONTRACT_EMPTY_DELIV" <<'EOF'
431
+ <!-- FORGE:CONTRACT -->
432
+ ## Builder Contract
433
+
434
+ ### Deliverables
435
+
436
+ | File | Change | Why |
437
+ |------|--------|-----|
438
+ | (documentation-only, no code paths) | n/a | n/a |
439
+
440
+ ### Out of Scope
441
+
442
+ Everything else.
443
+ EOF
444
+
445
+ unset MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
446
+ MOCK_GH_CONTRACT="$CONTRACT_EMPTY_DELIV" MOCK_GH_COMMENTS="$INV_DIFFERENT" assert_output \
447
+ "forge#2848 fall-through: contract Deliverables naming no recognized path -> falls through to affected-files-section" \
448
+ "affected-files-section" "bin/investigator-guess.mjs,scripts/investigator-guess.sh" 2851 -R test/repo
449
+
450
+ # --------------------------------------------------------------------------- #
451
+ # Scenario 15 (forge#2848 x forge#2504): the CONTRACT fetch specifically fails
452
+ # while every other source is legitimately empty. Must yield PROVENANCE=error,
453
+ # not none — the new call site doubles the gh failure surface, so its own
454
+ # GH_CALL_FAILED wiring needs its own assertion rather than riding on the
455
+ # investigator call's coverage.
456
+ # --------------------------------------------------------------------------- #
457
+ unset MOCK_GH_CONTRACT MOCK_GH_COMMENTS MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
458
+ MOCK_GH_CONTRACT_FAIL=1 assert_output \
459
+ "forge#2848: contract gh api call fails, no other usable source -> PROVENANCE=error, not none" \
460
+ "error" "" 2852 -R test/repo
461
+
462
+ # --------------------------------------------------------------------------- #
463
+ # Scenario 16 (forge#2848 x forge#2504): the CONTRACT fetch fails but the
464
+ # investigator path succeeds with real files. A gh failure must never suppress
465
+ # or overwrite data actually recovered by a subsequent successful call — same
466
+ # invariant Scenario 11 asserts for the primary/fallback pair.
467
+ # --------------------------------------------------------------------------- #
468
+ unset MOCK_GH_CONTRACT MOCK_GH_BODY MOCK_GH_COMMENTS_FAIL MOCK_GH_BODY_FAIL
469
+ MOCK_GH_CONTRACT_FAIL=1 MOCK_GH_COMMENTS="$INV_DIFFERENT" assert_output \
470
+ "forge#2848: contract fetch fails but investigator succeeds with real files -> affected-files-section, not error" \
471
+ "affected-files-section" "bin/investigator-guess.mjs,scripts/investigator-guess.sh" 2853 -R test/repo
472
+
473
+ unset MOCK_GH_CONTRACT_FAIL
474
+
323
475
  # --------------------------------------------------------------------------- #
324
476
  # Summary
325
477
  # --------------------------------------------------------------------------- #