forgedock 1.16.2 → 1.18.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 (114) hide show
  1. package/bin/engine/outcome-codes.mjs +114 -0
  2. package/bin/engine/phases.mjs +211 -100
  3. package/bin/engine/projector.mjs +48 -1
  4. package/bin/engine/runlog.mjs +20 -5
  5. package/bin/engine/telemetry-seam.mjs +62 -0
  6. package/bin/engine/telemetry-seam.test.mjs +60 -0
  7. package/bin/engine/telemetry.mjs +148 -0
  8. package/bin/engine/telemetry.test.mjs +120 -0
  9. package/bin/engine-cli.mjs +17 -1
  10. package/bin/engine.mjs +227 -48
  11. package/bin/forgedock.mjs +8 -2
  12. package/bin/hooks/interactive-engine.mjs +10 -0
  13. package/bin/hooks/pre-tool-use.mjs +220 -0
  14. package/bin/runner.mjs +70 -4
  15. package/bin/stats/aggregate.mjs +231 -0
  16. package/bin/stats/cache.mjs +40 -0
  17. package/bin/stats/github-source.mjs +211 -0
  18. package/bin/stats/index.mjs +154 -0
  19. package/bin/stats/local-source.mjs +79 -0
  20. package/bin/stats/package.json +6 -0
  21. package/bin/stats/render.mjs +82 -0
  22. package/bin/stats/stats.test.mjs +312 -0
  23. package/commands/cleanup.md +90 -2
  24. package/commands/deploy-info.md +8 -4
  25. package/commands/deploy-pr.md +64 -0
  26. package/commands/fix-ci.md +20 -6
  27. package/commands/milestone.md +34 -17
  28. package/commands/orchestrate/phase-1-resolve.md +4 -1
  29. package/commands/orchestrate/phase-4-execution.md +28 -6
  30. package/commands/orchestrate/phase-5-cleanup.md +18 -2
  31. package/commands/pipeline-health/phase-1-context.md +2 -0
  32. package/commands/pipeline-health/phase-2-metrics.md +12 -5
  33. package/commands/pipeline-status.md +21 -1
  34. package/commands/review-pr-agents/protocols.md +1 -0
  35. package/commands/review-pr-agents.md +10 -2
  36. package/commands/review-pr-staging.md +196 -496
  37. package/commands/review-pr.md +512 -3134
  38. package/commands/test-gate.md +25 -10
  39. package/commands/work-on/build/architect.md +13 -1
  40. package/commands/work-on/build/implement.md +28 -6
  41. package/commands/work-on/build/validate.md +5 -2
  42. package/commands/work-on/build.md +197 -145
  43. package/commands/work-on/close.md +66 -20
  44. package/commands/work-on/decompose.md +113 -34
  45. package/commands/work-on/investigate.md +15 -14
  46. package/commands/work-on/remediate.md +49 -19
  47. package/commands/work-on/review.md +7 -5
  48. package/commands/work-on.md +1 -2
  49. package/docs/CONFIG.md +76 -24
  50. package/docs/OUTCOME-CODES.md +53 -0
  51. package/docs/RELEASE-NOTES.md +24 -0
  52. package/docs/STATS.md +63 -0
  53. package/docs/TELEMETRY.md +65 -0
  54. package/docs/WORK-ON-RUNTIME.md +19 -0
  55. package/docs/site/command-reference.md +1 -1
  56. package/docs/spec/forge-protocol-v1.md +2 -2
  57. package/docs/spec/label-state-machine.md +1 -1
  58. package/package.json +3 -2
  59. package/packages/protocol/src/phases.js +5 -5
  60. package/scripts/builder-resume-match.test.sh +58 -0
  61. package/scripts/cap-findings.sh +110 -0
  62. package/scripts/cap-findings.test.sh +130 -0
  63. package/scripts/check-spec-bash.sh +5 -1
  64. package/scripts/check-spec-bash.test.sh +20 -0
  65. package/scripts/check-spec-markers.sh +2 -0
  66. package/scripts/classify-finding.sh +65 -16
  67. package/scripts/classify-finding.test.sh +43 -1
  68. package/scripts/classify-grep-exit.sh +20 -0
  69. package/scripts/classify-grep-exit.test.sh +46 -0
  70. package/scripts/close-parent-c3.test.sh +123 -0
  71. package/scripts/cost-report.sh +146 -0
  72. package/scripts/cost-report.test.sh +80 -0
  73. package/scripts/diff-size.sh +97 -3
  74. package/scripts/diff-size.test.sh +59 -2
  75. package/scripts/doctor-pipeline-state.sh +43 -0
  76. package/scripts/file-deferred-followups.sh +159 -0
  77. package/scripts/file-deferred-followups.test.sh +106 -0
  78. package/scripts/fixtures/cost-report/a-merged1.jsonl +6 -0
  79. package/scripts/fixtures/cost-report/a-merged2-remediated.jsonl +7 -0
  80. package/scripts/fixtures/cost-report/a-needs-human.jsonl +4 -0
  81. package/scripts/fixtures/cost-report/b-merged.jsonl +3 -0
  82. package/scripts/fixtures/cost-report/c-merged-attempt-usage.jsonl +8 -0
  83. package/scripts/fixtures/cost-report/hydrated-no-usage.jsonl +3 -0
  84. package/scripts/fixtures/cost-report/old-log-no-version.jsonl +3 -0
  85. package/scripts/fixtures/cost-report/unfinished-no-terminal.jsonl +2 -0
  86. package/scripts/fixtures/cost-report/unfinished-reopened.jsonl +5 -0
  87. package/scripts/forge-root.test.sh +18 -11
  88. package/scripts/lib/fence-bootstrap.sh +4 -3
  89. package/scripts/lib/pr-issue-number.sh +18 -0
  90. package/scripts/lib/review-template-resolve.sh +41 -0
  91. package/scripts/lib/telemetry.sh +168 -0
  92. package/scripts/publish-staging-sync.test.sh +48 -0
  93. package/scripts/remediate-liveness.sh +42 -6
  94. package/scripts/resolve-deploy-gate.sh +32 -6
  95. package/scripts/resolve-deploy-gate.test.sh +42 -1
  96. package/scripts/review-acceptance-gate.sh +16 -7
  97. package/scripts/review-acceptance-gate.test.sh +13 -0
  98. package/scripts/review-finalize.sh +1427 -0
  99. package/scripts/review-finalize.test.sh +833 -0
  100. package/scripts/review-pr-delta-wiring.test.sh +19 -72
  101. package/scripts/review-pr-domain-map.test.sh +23 -13
  102. package/scripts/review-pr-gate2.test.sh +24 -65
  103. package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
  104. package/scripts/review-pr-staging-scope.test.sh +56 -80
  105. package/scripts/review-prepare.sh +553 -0
  106. package/scripts/review-prepare.test.sh +288 -0
  107. package/scripts/review-roster.sh +186 -0
  108. package/scripts/review-roster.test.sh +185 -0
  109. package/scripts/run-shell-tests.sh +3 -0
  110. package/scripts/telemetry.test.sh +106 -0
  111. package/scripts/trusted-comments.test.sh +9 -2
  112. package/scripts/wait-ci-green.sh +8 -1
  113. package/scripts/wait-ci-green.test.sh +3 -3
  114. package/templates/forge.yaml.minimal +9 -0
@@ -11,6 +11,7 @@
11
11
  # classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>]
12
12
  # [--lineage none|review-finding] [--text <T> | --text-file <F>]
13
13
  # [--inpr-diff <F> --file <P>] [--contract-scope <F> --contract-open <F> [--pr-files <F>] [--merge-issue <N>]]
14
+ # [--fix-efficacy fails|regresses] [--fixes-issue <N>]
14
15
  # [--prior-findings <F> [--title <T>] [--line-start <N> [--line-end <M>]] [--diff-map <F>]]
15
16
  #
16
17
  # --severity CRITICAL | HIGH | MEDIUM | LOW (case-insensitive). Missing or
@@ -21,6 +22,14 @@
21
22
  # --lineage `review-finding` when the PR's linked issue (MERGE_ISSUE) carries
22
23
  # the `review-finding` label — i.e. this PR is itself a fix for a
23
24
  # review finding. Default `none`.
25
+ # --fix-efficacy fails|regresses The finding says the PR's linked issue is not fixed (`fails`) or
26
+ # that the PR re-introduces its defect (`regresses`). A MEDIUM finding so marked is
27
+ # `ISSUE fix-efficacy-<value>` under any lineage and confidence (the cascade damper must
28
+ # not merge a known-broken fix). Without the flag the tag is read from `--text`
29
+ # (a line consisting only of `FIX_EFFICACY: fails|regresses` or `**Fix efficacy**: fails|regresses`;
30
+ # a mid-sentence mention never counts). Opt-in only: never keyword-based. Runs after the INPR_FIX and re-flag gates and
31
+ # before the contract-scope and lineage gates; LOW stays NOTE; HIGH/CRITICAL unchanged.
32
+ # --fixes-issue <N> The PR's linked issue; alias of `--merge-issue` (same variable).
24
33
  # --text/--text-file Finding title + body + affected file paths, used for the
25
34
  # content-based safety exemption.
26
35
  # --inpr-diff <F> --file <P> In-PR fix gate (#3387, narrowed): F is a file listing the
@@ -100,6 +109,7 @@ CONTRACT_SCOPE=""
100
109
  CONTRACT_OPEN=""
101
110
  PR_FILES=""
102
111
  MERGE_ISSUE=""
112
+ FIX_EFF=""
103
113
  PRIOR_FINDINGS=""
104
114
  TITLE=""
105
115
  LINE_START=""
@@ -107,7 +117,7 @@ LINE_END=""
107
117
  DIFF_MAP=""
108
118
 
109
119
  usage() {
110
- echo "ERROR: Usage: classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>] [--lineage none|review-finding] [--text <T> | --text-file <F>] [--inpr-diff <F> --file <P>] [--contract-scope <F> --contract-open <F> [--pr-files <F>] [--merge-issue <N>]] [--prior-findings <F> [--title <T>] [--line-start <N> [--line-end <M>]] [--diff-map <F>]]" >&2
120
+ echo "ERROR: Usage: classify-finding.sh --severity <S> [--confidence <C>] [--agent <A>] [--lineage none|review-finding] [--text <T> | --text-file <F>] [--inpr-diff <F> --file <P>] [--contract-scope <F> --contract-open <F> [--pr-files <F>] [--merge-issue <N>]] [--fix-efficacy fails|regresses] [--fixes-issue <N>] [--prior-findings <F> [--title <T>] [--line-start <N> [--line-end <M>]] [--diff-map <F>]]" >&2
111
121
  exit 2
112
122
  }
113
123
 
@@ -124,6 +134,8 @@ while [ "$#" -gt 0 ]; do
124
134
  --contract-open) [ "$#" -ge 2 ] || usage; CONTRACT_OPEN="$2"; shift 2 ;;
125
135
  --pr-files) [ "$#" -ge 2 ] || usage; PR_FILES="$2"; shift 2 ;;
126
136
  --merge-issue) [ "$#" -ge 2 ] || usage; MERGE_ISSUE="${2#\#}"; shift 2 ;;
137
+ --fixes-issue) [ "$#" -ge 2 ] || usage; MERGE_ISSUE="${2#\#}"; shift 2 ;;
138
+ --fix-efficacy) [ "$#" -ge 2 ] || usage; FIX_EFF=$(printf '%s' "$2" | tr '[:upper:]' '[:lower:]'); shift 2 ;;
127
139
  --prior-findings) [ "$#" -ge 2 ] || usage; PRIOR_FINDINGS="$2"; shift 2 ;;
128
140
  --title) [ "$#" -ge 2 ] || usage; TITLE="$2"; shift 2 ;;
129
141
  --line-start) [ "$#" -ge 2 ] || usage; LINE_START="$2"; shift 2 ;;
@@ -142,6 +154,11 @@ case "$LINEAGE" in
142
154
  *) echo "ERROR: --lineage must be 'none' or 'review-finding' (got '$LINEAGE')" >&2; exit 2 ;;
143
155
  esac
144
156
 
157
+ case "$FIX_EFF" in
158
+ ''|fails|regresses) ;;
159
+ *) echo "ERROR: --fix-efficacy must be 'fails' or 'regresses' (got '$FIX_EFF')" >&2; exit 2 ;;
160
+ esac
161
+
145
162
  if [ -n "$INPR_DIFF" ] && [ ! -r "$INPR_DIFF" ]; then
146
163
  echo "ERROR: --inpr-diff not readable: $INPR_DIFF" >&2; exit 2
147
164
  fi
@@ -165,10 +182,32 @@ upper() { printf '%s' "$1" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]'; }
165
182
  SEV=$(upper "$SEVERITY")
166
183
  CONF=$(upper "$CONFIDENCE")
167
184
 
185
+ # Telemetry (forge#3834): one best-effort record per finding, then the unchanged stdout line.
186
+ _TEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/telemetry.sh"; [ -f "$_TEL" ] && . "$_TEL" 2>/dev/null
187
+ type forge_emit >/dev/null 2>&1 || forge_emit() { :; }
188
+ disp() { # "<DISPOSITION> <reason>"
189
+ forge_emit finding_disposition severity="$SEV" confidence="${CONF:-UNKNOWN}" agent="${AGENT:-none}" disposition="${1%% *}" reason="${1#* }"
190
+ echo "$1"
191
+ }
192
+
193
+ # In-PR fix gate predicate (#3387, widened to HIGH/CRITICAL in #3592): CONFIRMED, linked to a
194
+ # --inpr-diff file list, and the finding's file is one of those files. Fails toward ISSUE.
195
+ inpr_match() {
196
+ [ -n "$INPR_DIFF" ] && [ -n "$FINDING_FILE" ] && [ "$CONF" = "CONFIRMED" ] || return 1
197
+ _path="${FINDING_FILE#./}"; _path="${_path%%:*}"
198
+ grep -Fxq -- "$_path" "$INPR_DIFF"
199
+ }
200
+
168
201
  case "$SEV" in
169
- CRITICAL|HIGH) echo "ISSUE severity-$SEV"; exit 0 ;;
202
+ CRITICAL|HIGH)
203
+ # HIGH/CRITICAL skip the re-flag and safety gates below, so the in-PR check runs here, before
204
+ # the exit. Everything else stays ISSUE: a HIGH is never demoted to NOTE.
205
+ if inpr_match; then
206
+ disp "INPR_FIX $(printf '%s' "$SEV" | tr '[:upper:]' '[:lower:]')-confirmed-in-diff"; exit 0
207
+ fi
208
+ disp "ISSUE severity-$SEV"; exit 0 ;;
170
209
  MEDIUM|LOW) ;;
171
- *) echo "ISSUE unparseable-severity"; exit 0 ;;
210
+ *) disp "ISSUE unparseable-severity"; exit 0 ;;
172
211
  esac
173
212
 
174
213
  # Cross-head / cross-PR re-flag gate (#3641). Runs after the severity parse so HIGH/CRITICAL and
@@ -236,17 +275,27 @@ if [ -n "$PRIOR_FINDINGS" ] && [ -n "$FINDING_FILE" ]; then
236
275
  }
237
276
  END { if (!found && note) print note }
238
277
  ' "$PRIOR_FINDINGS" 2>/dev/null) || _reflag=""
239
- if [ -n "$_reflag" ]; then echo "$_reflag"; exit 0; fi
278
+ if [ -n "$_reflag" ]; then disp "$_reflag"; exit 0; fi
240
279
  fi
241
280
 
242
281
  # In-PR fix gate (#3387, narrowed). MEDIUM CONFIRMED is an ISSUE under every rule below
243
282
  # (lineage included), so checking here changes only where it is handled, never whether.
244
- if [ -n "$INPR_DIFF" ] && [ -n "$FINDING_FILE" ] && [ "$SEV" = "MEDIUM" ] && [ "$CONF" = "CONFIRMED" ]; then
245
- _path="${FINDING_FILE#./}"; _path="${_path%%:*}"
246
- if grep -Fxq -- "$_path" "$INPR_DIFF"; then
247
- echo "INPR_FIX medium-confirmed-in-diff"
248
- exit 0
283
+ # HIGH/CRITICAL were handled in the severity case above.
284
+ if [ "$SEV" = "MEDIUM" ] && inpr_match; then
285
+ disp "INPR_FIX medium-confirmed-in-diff"
286
+ exit 0
287
+ fi
288
+
289
+ # Fix-efficacy exemption: a MEDIUM finding saying the PR's linked issue is not fixed, or that the PR
290
+ # re-introduces its defect, is never damped (any lineage, any confidence, no contract demotion).
291
+ # Opt-in on an explicit flag or a line-anchored tag only; never keyword-based (a bare "regression test for #N" must not match).
292
+ if [ "$SEV" = "MEDIUM" ]; then
293
+ if [ -z "$FIX_EFF" ]; then
294
+ FIX_EFF=$(printf '%s' "$TEXT" | tr '[:upper:]' '[:lower:]' \
295
+ | grep -Eo '^[[:space:]]*(fix_efficacy:|\*\*fix efficacy\*\*:)[[:space:]]*(fails|regresses)[[:space:]]*$' | head -n 1 \
296
+ | grep -Eo '(fails|regresses)$') || FIX_EFF=""
249
297
  fi
298
+ if [ -n "$FIX_EFF" ]; then disp "ISSUE fix-efficacy-$FIX_EFF"; exit 0; fi
250
299
  fi
251
300
 
252
301
  # Safety exemption. Keywords match whole words, where `_`, `-`, `/` and other
@@ -282,10 +331,10 @@ if [ -n "$CONTRACT_SCOPE" ] && [ -n "$FINDING_FILE" ]; then
282
331
  deferred)
283
332
  if [ -z "$SAFETY" ] && [ -n "$_inum" ] && [ "$_inum" != "$MERGE_ISSUE" ] \
284
333
  && [ -n "$CONTRACT_OPEN" ] && grep -Fxq -- "$_inum" "$CONTRACT_OPEN"; then
285
- echo "NOTE contract-deferred #$_inum"; exit 0
334
+ disp "NOTE contract-deferred #$_inum"; exit 0
286
335
  fi ;;
287
336
  accepted-risk)
288
- if [ -z "$SAFETY" ]; then echo "NOTE contract-accepted-risk"; exit 0; fi ;;
337
+ if [ -z "$SAFETY" ]; then disp "NOTE contract-accepted-risk"; exit 0; fi ;;
289
338
  esac
290
339
  done < "$CONTRACT_SCOPE"
291
340
  fi
@@ -293,21 +342,21 @@ fi
293
342
 
294
343
  if [ "$LINEAGE" = "review-finding" ]; then
295
344
  if [ "$SEV" = "MEDIUM" ] && { [ "$CONF" = "CONFIRMED" ] || { [ -n "$SAFETY" ] && [ "$CONF" = "LIKELY" ]; }; }; then
296
- echo "ISSUE lineage-medium-${CONF:-UNKNOWN}"
345
+ disp "ISSUE lineage-medium-${CONF:-UNKNOWN}"
297
346
  else
298
- echo "NOTE lineage-${SEV}-${CONF:-UNKNOWN}"
347
+ disp "NOTE lineage-${SEV}-${CONF:-UNKNOWN}"
299
348
  fi
300
349
  exit 0
301
350
  fi
302
351
 
303
352
  if [ "$SEV" = "LOW" ] || [ "$CONF" = "POSSIBLE" ]; then
304
353
  if [ -n "$SAFETY" ]; then
305
- echo "ISSUE safety-exemption-$SAFETY"
354
+ disp "ISSUE safety-exemption-$SAFETY"
306
355
  else
307
- echo "NOTE ${SEV}-${CONF:-UNKNOWN}"
356
+ disp "NOTE ${SEV}-${CONF:-UNKNOWN}"
308
357
  fi
309
358
  exit 0
310
359
  fi
311
360
 
312
- echo "ISSUE ${SEV}-${CONF:-UNKNOWN}"
361
+ disp "ISSUE ${SEV}-${CONF:-UNKNOWN}"
313
362
  exit 0
@@ -68,6 +68,35 @@ expect NOTE "lineage MEDIUM LIKELY no-safety note" -- --severity MEDIUM --confi
68
68
  expect ISSUE "lineage MEDIUM LIKELY with keyword files" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "token leaks"
69
69
  expect NOTE "lineage MEDIUM POSSIBLE with keyword note" -- --severity MEDIUM --confidence POSSIBLE --lineage review-finding --text "token leaks"
70
70
 
71
+ # --- Fix-efficacy exemption (#3693): #3683 SEC-1 shape ---
72
+ SEC1="enqueue_breaker_decision helper never declared in the fences that call it; fresh shell never queues the breaker decision"
73
+ LN=(--severity MEDIUM --confidence LIKELY --lineage review-finding --agent Security --text "$SEC1")
74
+ expect NOTE "SEC-1 untagged stays damped" -- "${LN[@]}"
75
+ expect ISSUE "SEC-1 --fix-efficacy fails" -- "${LN[@]}" --fix-efficacy fails --fixes-issue 3621
76
+ expect ISSUE "SEC-1 --fix-efficacy regresses" -- "${LN[@]}" --fix-efficacy regresses
77
+ expect ISSUE "SEC-1 POSSIBLE with fix-efficacy" -- --severity MEDIUM --confidence POSSIBLE --lineage review-finding --text "x" --fix-efficacy fails
78
+ expect ISSUE "text tag FIX_EFFICACY fallback" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "$(printf 'helper missing.\nFIX_EFFICACY: fails')"
79
+ expect ISSUE "bold Fix efficacy line fallback" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "**Fix efficacy**: regresses"
80
+ expect NOTE "mid-line FIX_EFFICACY token stays NOTE" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "helper missing. FIX_EFFICACY: fails"
81
+ expect NOTE "#N + still-fails phrase no longer opts in" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "this still fails to fix #3621 in a fresh shell" --fixes-issue 3621
82
+ expect NOTE "regression test for #N stays NOTE" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "Missing regression test for #3621" --fixes-issue 3621
83
+ expect NOTE "no regression #N stays NOTE" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "no regression #3621, but wording is off" --fixes-issue 3621
84
+ expect NOTE "reintroduce-style phrase stays NOTE" -- --severity MEDIUM --confidence LIKELY --lineage review-finding --text "could reintroduce #3621 naming" --fixes-issue 3621
85
+ expect NOTE "LOW + fix-efficacy stays NOTE" -- --severity LOW --confidence CONFIRMED --lineage review-finding --text "x" --fix-efficacy fails
86
+ expect ISSUE "non-lineage MEDIUM POSSIBLE + efficacy" -- --severity MEDIUM --confidence POSSIBLE --text "x" --fix-efficacy fails
87
+ expect ISSUE "HIGH unchanged" -- --severity HIGH --confidence LIKELY --lineage review-finding --fix-efficacy fails
88
+ expect_exit 2 "bad --fix-efficacy value" -- --severity MEDIUM --fix-efficacy broken
89
+ FX_DIFF=$(mktemp "${TMPDIR:-/tmp}/classify-finding-fx.XXXXXX"); printf 'src/a.sh\n' > "$FX_DIFF"
90
+ expect INPR_FIX "CONFIRMED in-PR efficacy stays INPR_FIX" -- --severity MEDIUM --confidence CONFIRMED --lineage review-finding --fix-efficacy fails --inpr-diff "$FX_DIFF" --file src/a.sh
91
+ rm -f "$FX_DIFF"
92
+ FX_SCOPE=$(mktemp "${TMPDIR:-/tmp}/classify-finding-fxs.XXXXXX"); FX_OPEN=$(mktemp "${TMPDIR:-/tmp}/classify-finding-fxo.XXXXXX")
93
+ printf 'deferred\tsrc/b.sh\t3446\n' > "$FX_SCOPE"; printf '3446\n' > "$FX_OPEN"
94
+ expect NOTE "contract-deferred untagged demotes" -- --severity MEDIUM --confidence LIKELY --text "x" --file src/b.sh --contract-scope "$FX_SCOPE" --contract-open "$FX_OPEN"
95
+ expect ISSUE "contract-deferred + efficacy files" -- --severity MEDIUM --confidence LIKELY --text "x" --file src/b.sh --contract-scope "$FX_SCOPE" --contract-open "$FX_OPEN" --fix-efficacy fails
96
+ rm -f "$FX_SCOPE" "$FX_OPEN"
97
+ _a=$(bash "$S" --severity MEDIUM --lineage review-finding --text "x" --fix-efficacy fails --fixes-issue 5); _b=$(bash "$S" --severity MEDIUM --lineage review-finding --text "x" --fix-efficacy fails --merge-issue 5)
98
+ if [ "$_a" = "$_b" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: --fixes-issue/--merge-issue equivalence"; fi
99
+
71
100
  # --- text-file input ---
72
101
  TMPF=$(mktemp "${TMPDIR:-/tmp}/classify-finding-test.XXXXXX")
73
102
  printf 'Title: unchecked xss sink\n' > "$TMPF"
@@ -83,7 +112,20 @@ expect INPR_FIX "./ prefix matches" -- --severity MEDIUM --con
83
112
  expect INPR_FIX "lineage PR still fixes in-PR" -- --severity MEDIUM --confidence CONFIRMED --lineage review-finding --inpr-diff "$DIFF" --file src/api/jobs.py
84
113
  expect ISSUE "MEDIUM CONFIRMED outside diff files" -- --severity MEDIUM --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/other.py
85
114
  expect ISSUE "MEDIUM LIKELY in diff is not gated" -- --severity MEDIUM --confidence LIKELY --inpr-diff "$DIFF" --file src/api/jobs.py
86
- expect ISSUE "HIGH in diff stays ISSUE (7B blocks it)" -- --severity HIGH --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/jobs.py
115
+ expect INPR_FIX "HIGH CONFIRMED in diff (#3592)" -- --severity HIGH --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/jobs.py
116
+ expect INPR_FIX "CRITICAL CONFIRMED in diff (#3592)" -- --severity CRITICAL --confidence CONFIRMED --inpr-diff "$DIFF" --file "src/api/jobs.py:9"
117
+ expect INPR_FIX "HIGH in diff with safety keyword" -- --severity HIGH --confidence CONFIRMED --title "billing refund leg unreachable" --inpr-diff "$DIFF" --file src/api/jobs.py
118
+ expect ISSUE "HIGH LIKELY in diff stays ISSUE" -- --severity HIGH --confidence LIKELY --inpr-diff "$DIFF" --file src/api/jobs.py
119
+ expect ISSUE "CRITICAL LIKELY in diff stays ISSUE" -- --severity CRITICAL --confidence LIKELY --inpr-diff "$DIFF" --file src/api/jobs.py
120
+ expect ISSUE "HIGH CONFIRMED outside diff stays ISSUE" -- --severity HIGH --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/other.py
121
+ expect ISSUE "CRITICAL CONFIRMED outside diff" -- --severity CRITICAL --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/other.py
122
+ expect ISSUE "HIGH CONFIRMED without --inpr-diff" -- --severity HIGH --confidence CONFIRMED --file src/api/jobs.py
123
+ expect ISSUE "HIGH CONFIRMED without --file" -- --severity HIGH --confidence CONFIRMED --inpr-diff "$DIFF"
124
+ for _s in HIGH CRITICAL MEDIUM; do
125
+ _want="INPR_FIX $(printf '%s' "$_s" | tr '[:upper:]' '[:lower:]')-confirmed-in-diff"
126
+ _got=$(bash "$S" --severity "$_s" --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/jobs.py 2>/dev/null)
127
+ if [ "$_got" = "$_want" ]; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL: reason carries severity $_s (want '$_want', got '$_got')"; fi
128
+ done
87
129
  expect NOTE "LOW in diff stays NOTE" -- --severity LOW --confidence CONFIRMED --inpr-diff "$DIFF" --file src/api/jobs.py
88
130
  expect ISSUE "no --inpr-diff: unchanged behaviour" -- --severity MEDIUM --confidence CONFIRMED --file src/api/jobs.py
89
131
  expect ISSUE "no --file: unchanged behaviour" -- --severity MEDIUM --confidence CONFIRMED --inpr-diff "$DIFF"
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # classify-grep-exit.sh <rc> <stderr> <target> — is a grep exit >= 2 a malformed check, or a real failure?
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ #
6
+ # Exit 0 + reason on stdout = malformed (a usage/regex error, a check defect, never a verdict on the code).
7
+ # Exit 1 = real verdict (exit 1 and exit 2 both fail the check). Never executes the target; it only reads its text.
8
+ # Wording covers GNU grep and BSD/macOS regcomp (`brackets ([ ]) not balanced`, `repetition-operator operand invalid`, ...).
9
+ # Fail closed <!-- Added: forge#3802 -->: malformed only when stderr POSITIVELY names a regex/option usage error.
10
+ # A target that suppresses stderr (`2>/dev/null`, `-s`) leaves it empty, so a missing/unreadable/looping operand
11
+ # file can never hide as malformed. Unknown or empty stderr is a real failure.
12
+ RC="${1:-0}"; ERR="${2:-}"; TARGET="${3:-}"
13
+ case "$RC" in ''|*[!0-9]*) exit 1 ;; esac
14
+ [ "$RC" -ge 2 ] || exit 1
15
+ case "$TARGET" in grep\ *|egrep\ *|fgrep\ *) ;; *) exit 1 ;; esac
16
+ if printf '%s' "$ERR" | LC_ALL=C grep -qiE 'Unmatched|Invalid (regular expression|preceding|back reference|range|character class|content of)|unrecognized option|invalid option|option requires an argument|Usage:|trailing backslash|character class syntax|conflicting matchers|error at position|mismatched|invalid escape|not balanced|operand invalid|empty \(sub\)expression|invalid repetition count|empty \(sub\)|illegal repetition|repetition-operator'; then
17
+ echo "grep usage error (exit $RC)"
18
+ exit 0
19
+ fi
20
+ exit 1
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ # classify-grep-exit.test.sh — verdicts of scripts/classify-grep-exit.sh (forge#3802). Runs real grep, no network.
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ set -uo pipefail
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ CLS="$SCRIPT_DIR/classify-grep-exit.sh"
8
+ PASS=0; FAILN=0
9
+ ok() { PASS=$((PASS+1)); echo "ok - $1"; }
10
+ bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
11
+ TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
12
+ cd "$TMP" || exit 1
13
+ echo hello > present.txt
14
+ ln -s loop1 loop2; ln -s loop2 loop1
15
+
16
+ # run_target <target>: execute like B6.5 does, then classify. Prints "malformed" or "real".
17
+ verdict() {
18
+ local t="$1" err rc
19
+ err=$(eval "$t" 2>&1 >/dev/null); rc=$?
20
+ if bash "$CLS" "$rc" "$err" "$t" >/dev/null; then echo malformed; else echo real; fi
21
+ }
22
+ expect() { local got; got=$(verdict "$2"); if [ "$got" = "$3" ]; then ok "$1"; else bad "$1 (got $got, want $3)"; fi; }
23
+
24
+ expect "missing file, plain" "grep -qF -e x nofile.md" real
25
+ expect "missing file, 2>/dev/null" "grep -qF -e x nofile.md 2>/dev/null" real
26
+ expect "missing file, -s" "grep -sqF -e x nofile.md" real
27
+ expect "missing file, --no-messages" "grep --no-messages -qF -e x nofile.md" real
28
+ expect "symlink loop, stderr suppressed" "grep -qF -e x loop1 2>/dev/null" real
29
+ expect "directory operand, stderr suppressed" "grep -qF -e x . 2>/dev/null" real
30
+ expect "unmatched bracket regex" "grep -qE '[abc' present.txt" malformed
31
+ expect "unmatched paren regex" "grep -qE '(abc' present.txt" malformed
32
+ expect "pattern read as option" "grep -qE '--no-such-flag' present.txt" malformed
33
+ expect "option-looking pattern" "grep -qE '-z foo' present.txt" malformed
34
+ expect "non-grep target" "bash -c 'exit 2'" real
35
+ for _w in "grep: brackets ([ ]) not balanced" "grep: parentheses not balanced" "grep: braces not balanced" "grep: repetition-operator operand invalid" "grep: empty (sub)expression" "grep: invalid repetition count(s)"; do
36
+ bash "$CLS" 2 "$_w" "grep -E x f" >/dev/null && ok "BSD wording malformed: $_w" || bad "BSD wording malformed: $_w"
37
+ done
38
+ [ "$(bash "$CLS" 2 "grep: Unmatched [" "grep -q x f")" = "grep usage error (exit 2)" ] && ok "reason text" || bad "reason text"
39
+ bash "$CLS" 2 "grep: Unmatched [" "ls -l" >/dev/null && bad "non-grep with usage stderr is not malformed" || ok "non-grep with usage stderr is not malformed"
40
+ bash "$CLS" 1 "grep: Unmatched [" "grep -q x f" >/dev/null && bad "rc 1 is not malformed" || ok "rc 1 is not malformed"
41
+ bash "$CLS" 0 "" "grep -q x f" >/dev/null && bad "rc 0 is not malformed" || ok "rc 0 is not malformed"
42
+ bash "$CLS" 2 "" "grep -q x f" >/dev/null && bad "rc 2 empty stderr is a real failure" || ok "rc 2 empty stderr is a real failure"
43
+ bash "$CLS" 2 "" >/dev/null && bad "missing args fail closed" || ok "missing args fail closed"
44
+
45
+ echo "passed=$PASS failed=$FAILN"
46
+ [ "$FAILN" -eq 0 ]
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ # close-parent-c3.test.sh — Phase C3 (close.md) behaviour for decomposition.close_parent:
3
+ # the last child of a CLOSED parent posts "All sub-issues complete" and sets merged
4
+ # without closing or reopening; an OPEN parent is auto-closed as before.
5
+ #
6
+ # Usage: bash scripts/close-parent-c3.test.sh
7
+
8
+ set -euo pipefail
9
+
10
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
11
+ CLOSE="$ROOT/commands/work-on/close.md"
12
+
13
+ # The all-children-checked-off branch of C3.
14
+ BLOCK=$(awk '/^if \[ "\$OPEN_SUBS" -eq 0 \]; then/{p=1} p{print} p&&/^fi$/{exit}' "$CLOSE")
15
+ [ -n "$BLOCK" ] || { printf 'FAIL: close.md C3 completion block not found\n' >&2; exit 1; }
16
+ grep -q 'close_parent' "$CLOSE" || { printf 'FAIL: close.md does not mention close_parent\n' >&2; exit 1; }
17
+
18
+ run_case() {
19
+ local parent_state="$1" already_posted="$2" log
20
+ log=$(mktemp)
21
+ gh() {
22
+ case "$1 $2" in
23
+ "issue view") echo "$parent_state" ;;
24
+ "api repos/"*) echo "$already_posted" ;;
25
+ *) echo "gh $*" >>"$log" ;;
26
+ esac
27
+ }
28
+ set_workflow_state() { echo "set_workflow_state $*" >>"$log"; }
29
+ # Render the spec placeholders.
30
+ local rendered
31
+ rendered=$(printf '%s\n' "$BLOCK" | sed -e 's/{GH_FLAG}/-R o\/r/g' -e 's/{GH_REPO}/o\/r/g' -e 's/{NUMBER}/7/g')
32
+ OPEN_SUBS=0 PARENT_REF=5 PR_NUMBER=9 PARENT_CLOSED=false
33
+ eval "$rendered"
34
+ cat "$log"; rm -f "$log"
35
+ }
36
+
37
+ fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
38
+
39
+ OUT=$(run_case CLOSED 0)
40
+ printf '%s\n' "$OUT" | grep -q 'gh issue comment 5 .*All sub-issues complete' || fail "closed parent: completion comment missing"
41
+ printf '%s\n' "$OUT" | grep -q 'set_workflow_state 5 merged' || fail "closed parent: merged not set"
42
+ printf '%s\n' "$OUT" | grep -q 'gh issue close' && fail "closed parent: must not be closed again"
43
+ printf '%s\n' "$OUT" | grep -qE 'reopen|--state open' && fail "closed parent: must not be reopened"
44
+
45
+ OUT=$(run_case CLOSED 1)
46
+ printf '%s\n' "$OUT" | grep -q 'gh issue comment' && fail "closed parent: completion comment must not repeat"
47
+ printf '%s\n' "$OUT" | grep -q 'set_workflow_state 5 merged' || fail "closed parent resume: merged not set"
48
+
49
+ OUT=$(run_case OPEN 0)
50
+ printf '%s\n' "$OUT" | grep -q 'gh issue close 5 .*All sub-issues complete' || fail "open parent: must be auto-closed"
51
+ printf '%s\n' "$OUT" | grep -q 'set_workflow_state 5 merged' || fail "open parent: merged not set"
52
+ printf '%s\n' "$OUT" | grep -q 'gh issue comment' && fail "open parent: no separate comment expected"
53
+
54
+ # ---- Verified-retry checkbox write + state-derived OPEN_SUBS (concurrent sibling close race) ----
55
+ WRITE_BLOCK=$(awk '/^PARENT_UPDATED=false$/{p=1} p{print} p&&/^\[ "\$PARENT_UPDATED" = true \]/{exit}' "$CLOSE")
56
+ DERIVE_BLOCK=$(awk '/^PARENT_FRESH=/{p=1} p{print} p&&/^OPEN_SUBS=\$\{OPEN_SUBS:-0\}$/{exit}' "$CLOSE")
57
+ [ -n "$WRITE_BLOCK" ] || fail "close.md C3 verified-retry write block not found"
58
+ [ -n "$DERIVE_BLOCK" ] || fail "close.md C3 state-derivation block not found"
59
+
60
+ # run_derive <parent body> <states "N=STATE ..."> [lost_edits]
61
+ # gh stub: parent body lives in a file; the first <lost_edits> edits are clobbered by a stale sibling write.
62
+ run_derive() {
63
+ local body="$1" states="$2" lost="${3:-0}" dir rendered
64
+ dir=$(mktemp -d)
65
+ printf '%s\n' "$body" >"$dir/body"; printf '%s\n' "$body" >"$dir/stale"
66
+ echo 0 >"$dir/edits"
67
+ gh() {
68
+ case "$1 $2" in
69
+ "issue view")
70
+ if [ "$6" = "--json" ] && [ "$7" = "body" ]; then cat "$dir/body"; return 0; fi
71
+ if [ "$6" = "--json" ] && [ "$7" = "state" ]; then
72
+ local kv; for kv in $states; do [ "${kv%%=*}" = "$3" ] && { echo "${kv#*=}"; return 0; }; done
73
+ return 1
74
+ fi ;;
75
+ "issue edit")
76
+ local n; n=$(( $(cat "$dir/edits") + 1 )); echo "$n" >"$dir/edits"
77
+ if [ "$n" -le "$lost" ]; then cp "$dir/stale" "$dir/body"; else printf '%s\n' "$7" >"$dir/body"; fi ;;
78
+ esac
79
+ }
80
+ sleep() { :; }
81
+ rendered=$(printf '%s\n%s\n' "$WRITE_BLOCK" "$DERIVE_BLOCK" | sed -e 's/{GH_FLAG}/-R o\/r/g' -e 's/{GH_REPO}/o\/r/g' -e 's/{NUMBER}/7/g')
82
+ PARENT_REF=5
83
+ eval "$rendered" >/dev/null 2>&1 || true
84
+ printf 'OPEN_SUBS=%s UPDATED=%s EDITS=%s CHECKED7=%s\n' "$OPEN_SUBS" "$PARENT_UPDATED" "$(cat "$dir/edits")" \
85
+ "$(grep -cE '^- \[x\] #7( |$)' "$dir/body" || true)"
86
+ rm -rf "$dir"; unset -f gh sleep
87
+ }
88
+
89
+ TWO=$'- [ ] #7 first\n- [ ] #8 second'
90
+
91
+ # Lost checkbox is re-applied by the retry loop (first write clobbered by a sibling).
92
+ OUT=$(run_derive "$TWO" "7=CLOSED 8=CLOSED" 1)
93
+ printf '%s\n' "$OUT" | grep -q 'CHECKED7=1' || fail "retry: checkbox not restored after a lost write ($OUT)"
94
+ printf '%s\n' "$OUT" | grep -q 'UPDATED=true' || fail "retry: PARENT_UPDATED not set ($OUT)"
95
+ printf '%s\n' "$OUT" | grep -q 'EDITS=2' || fail "retry: expected exactly 2 writes ($OUT)"
96
+
97
+ # Race: every write is lost, boxes stay unchecked, but all subs are CLOSED => 0 open (parent completes).
98
+ OUT=$(run_derive "$TWO" "7=CLOSED 8=CLOSED" 9)
99
+ printf '%s\n' "$OUT" | grep -q 'OPEN_SUBS=0' || fail "race: stale checklist with all subs CLOSED must derive 0 open ($OUT)"
100
+ printf '%s\n' "$OUT" | grep -q 'EDITS=3' || fail "race: retries must be bounded at 3 writes ($OUT)"
101
+
102
+ # A still-OPEN sibling keeps the parent open.
103
+ OUT=$(run_derive "$TWO" "7=CLOSED 8=OPEN")
104
+ printf '%s\n' "$OUT" | grep -q 'OPEN_SUBS=1' || fail "one open sibling must derive 1 open ($OUT)"
105
+
106
+ # State lookup failure falls back to the checklist count (fail closed, never 0 from a failed lookup).
107
+ OUT=$(run_derive "$TWO" "7=CLOSED")
108
+ printf '%s\n' "$OUT" | grep -q 'OPEN_SUBS=1' || fail "lookup failure must fall back to checklist count ($OUT)"
109
+
110
+ # Already-checked box: no write needed.
111
+ OUT=$(run_derive $'- [x] #7 first\n- [ ] #8 second' "7=CLOSED 8=CLOSED")
112
+ printf '%s\n' "$OUT" | grep -q 'EDITS=0' || fail "already checked: must not rewrite the parent ($OUT)"
113
+
114
+ # #7 must not match #70.
115
+ OUT=$(run_derive $'- [ ] #70 other\n- [ ] #7 first' "7=CLOSED 70=OPEN")
116
+ printf '%s\n' "$OUT" | grep -q 'OPEN_SUBS=1' || fail "#70 must stay open and not be confused with #7 ($OUT)"
117
+ printf '%s\n' "$OUT" | grep -q 'CHECKED7=1' || fail "#7 checkbox must be checked without touching #70 ($OUT)"
118
+
119
+ # A non-leading #N mention on an unchecked line is an open item, not a sub-issue ref (no fail-open close).
120
+ OUT=$(run_derive $'- [ ] #7 first\n- [ ] #8 second\n- [ ] Write docs (see #9)' "7=CLOSED 8=CLOSED")
121
+ printf '%s\n' "$OUT" | grep -q 'OPEN_SUBS=1' || fail "non-leading ref line must count as open ($OUT)"
122
+
123
+ printf 'PASS: close-parent-c3\n'
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # cost-report.sh — pipeline token cost per ForgeDock version and repo (forge#3824).
6
+ #
7
+ # Reads the engine run logs (<runs-dir>/*.jsonl) and prints a markdown table, one row per
8
+ # (version, repo): finished-run count n, mean/median cache-read tokens per issue, mean per
9
+ # phase, remediated %, merged %, and cache-read tokens per merged issue.
10
+ #
11
+ # Rules:
12
+ # - A run counts only when finished: the last RUN_START..end slice of a log has a
13
+ # RUN_TERMINAL after any RUN_REOPEN / RUN_NEXT_CYCLE. A log is counted once.
14
+ # - Runs with no usage events, or with a hydrated (reconstructed) RUN_START, are excluded:
15
+ # they carry no usage and would drag the means down.
16
+ # - Tokens = usage.cache_read_input_tokens summed per phase over PHASE_ATTEMPT_USAGE when the
17
+ # phase has any (PHASE_COMMIT.usage is then cumulative, so it is not added again), else over
18
+ # PHASE_COMMIT and PHASE_FAILED (older logs).
19
+ # Per-phase means divide by n (a run that skipped a phase counts 0), so the phase
20
+ # columns add up to the per-issue mean. "merged" is RUN_TERMINAL.reason == "merged".
21
+ # - Version/repo come from RUN_START. Older logs lack them: repo becomes "unknown", and the
22
+ # version is estimated from the log's mtime against release times (marked with "~").
23
+ # - Groups with n < --min-n (default 10) are flagged low-n.
24
+ #
25
+ # Usage: bash scripts/cost-report.sh [--runs-dir DIR] [--last N] [--versions a,b] [--min-n N]
26
+ # [--release-times FILE]
27
+ # --runs-dir default $FORGE_RUNS_DIR, else ~/.forge/runs
28
+ # --last N only the N highest versions (unknown excluded)
29
+ # --versions comma list of versions (a trailing "~" in the data is ignored)
30
+ # --release-times file of "<version> <epoch-seconds>" lines; default: git tags v* of this repo
31
+ # Exit: 0 report printed (also when there are no finished runs) | 2 bad usage / missing runs dir
32
+ # Portable: bash 3.2, jq, POSIX stat/git.
33
+
34
+ set -u
35
+
36
+ RUNS_DIR="${FORGE_RUNS_DIR:-$HOME/.forge/runs}"
37
+ LAST=""
38
+ VERSIONS=""
39
+ MIN_N=10
40
+ REL_FILE=""
41
+
42
+ die() { echo "cost-report: $1" >&2; exit 2; }
43
+ is_uint() { case "$1" in ''|*[!0-9]*) return 1 ;; *) return 0 ;; esac; }
44
+
45
+ while [ $# -gt 0 ]; do
46
+ case "$1" in
47
+ --runs-dir) [ $# -ge 2 ] || die "--runs-dir needs a value"; RUNS_DIR="$2"; shift 2 ;;
48
+ --last) [ $# -ge 2 ] || die "--last needs a value"; LAST="$2"; shift 2 ;;
49
+ --versions) [ $# -ge 2 ] || die "--versions needs a value"; VERSIONS="$2"; shift 2 ;;
50
+ --min-n) [ $# -ge 2 ] || die "--min-n needs a value"; MIN_N="$2"; shift 2 ;;
51
+ --release-times) [ $# -ge 2 ] || die "--release-times needs a value"; REL_FILE="$2"; shift 2 ;;
52
+ -h|--help) sed -n '2,32p' "$0"; exit 0 ;;
53
+ *) die "unknown argument: $1" ;;
54
+ esac
55
+ done
56
+
57
+ [ -d "$RUNS_DIR" ] || die "runs dir not found: $RUNS_DIR"
58
+ command -v jq >/dev/null 2>&1 || die "jq is required"
59
+ is_uint "$MIN_N" || die "--min-n must be a non-negative integer"
60
+ if [ -n "$LAST" ]; then is_uint "$LAST" || die "--last must be a non-negative integer"; fi
61
+ if [ -n "$REL_FILE" ] && [ ! -f "$REL_FILE" ]; then die "release-times file not found: $REL_FILE"; fi
62
+
63
+ # Release times: "<version> <epoch>" lines, from the file or this repo's v* tags (fail-soft: none).
64
+ if [ -n "$REL_FILE" ]; then
65
+ REL_RAW=$(cat "$REL_FILE")
66
+ else
67
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
68
+ REL_RAW=$(git -C "$ROOT" for-each-ref refs/tags --format='%(refname:short) %(creatordate:unix)' 2>/dev/null \
69
+ | sed -n 's/^v\([0-9][0-9.]*\) /\1 /p')
70
+ fi
71
+ REL_JSON=$(printf '%s\n' "$REL_RAW" | jq -R -s -c '
72
+ split("\n") | map(split(" ") | select(length == 2 and (.[1] | test("^[0-9]+$"))) | {v: .[0], t: (.[1] | tonumber)})
73
+ | sort_by(.t)')
74
+ [ -n "$REL_JSON" ] || REL_JSON='[]'
75
+
76
+ mtime_of() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0; }
77
+
78
+ # Pass 1: one summary object per log (empty for logs that cannot be parsed or have no RUN_START).
79
+ ROWS=""
80
+ for f in "$RUNS_DIR"/*.jsonl; do
81
+ [ -f "$f" ] || continue
82
+ mt=$(mtime_of "$f")
83
+ row=$(jq -c -s --argjson mt "$mt" '
84
+ . as $all
85
+ | ([range(0; length) | select($all[.].event == "RUN_START")] | last) as $s
86
+ | if $s == null then empty else
87
+ $all[$s:] as $ev
88
+ | $ev[0] as $start
89
+ | ([range(0; $ev | length) | select($ev[.].event == "RUN_TERMINAL")] | last) as $t
90
+ | ([range(0; $ev | length) | select($ev[.].event == "RUN_REOPEN" or $ev[.].event == "RUN_NEXT_CYCLE")] | last) as $r
91
+ | ([$ev[] | select(.event == "PHASE_ATTEMPT_USAGE") | .phase] | unique) as $ap
92
+ | [$ev[] | select(((.event == "PHASE_ATTEMPT_USAGE")
93
+ or ((.event == "PHASE_COMMIT" or .event == "PHASE_FAILED") and ((.phase as $p | $ap | index($p)) == null)))
94
+ and (.usage | type) == "object")] as $u
95
+ | {
96
+ version: ($start.version // null), repo: ($start.repo // null), mt: $mt,
97
+ hydrated: ($start.hydrated == true),
98
+ finished: ($t != null and ($r == null or $t > $r)),
99
+ reason: (if $t == null then null else $ev[$t].reason end),
100
+ usage_events: ($u | length),
101
+ remediated: ([$ev[] | select(.event == "PHASE_COMMIT" and .phase == "remediate")] | length > 0),
102
+ cr: ([$u[] | .usage.cache_read_input_tokens // 0] | add // 0),
103
+ out: ([$u[] | .usage.output_tokens // 0] | add // 0),
104
+ phase: ($u | group_by(.phase) | map({key: (.[0].phase // "other"), value: ([.[] | .usage.cache_read_input_tokens // 0] | add)}) | from_entries)
105
+ }
106
+ end' "$f" 2>/dev/null) || row=""
107
+ [ -n "$row" ] && ROWS="$ROWS$row
108
+ "
109
+ done
110
+
111
+ printf '%s' "$ROWS" | jq -s -r \
112
+ --argjson rel "$REL_JSON" --argjson minn "$MIN_N" \
113
+ --arg last "$LAST" --arg versions "$VERSIONS" '
114
+ def vkey: (sub("~$"; "") | split(".") | map(tonumber? // 0));
115
+ def mean: if length == 0 then 0 else add / length end;
116
+ def median: sort | if length == 0 then 0 elif length % 2 == 1 then .[length / 2 | floor] else (.[length / 2 - 1] + .[length / 2]) / 2 end;
117
+ def k: ((. / 1000) | round | tostring) + "k";
118
+ def pct: ((. * 100) | round | tostring) + "%";
119
+ def ver($mt): if .version != null then .version
120
+ else ([$rel[] | select(.t <= $mt)] | last | if . == null then "unknown" else .v + "~" end) end;
121
+
122
+ . as $rows
123
+ | ($rows | map(select(.finished | not)) | length) as $unfinished
124
+ | ($rows | map(select(.finished and (.hydrated or .usage_events == 0))) | length) as $nousage
125
+ | [ $rows[] | select(.finished and (.hydrated | not) and .usage_events > 0)
126
+ | . as $r | $r + {ver: ($r | ver($r.mt)), repo: ($r.repo // "unknown")} ] as $runs
127
+ | ($versions | split(",") | map(select(length > 0))) as $vf
128
+ | ($runs | map(.ver | select(. != "unknown")) | unique | sort_by(vkey)) as $vers
129
+ | (if $last == "" then $vers else ($vers | .[(length - ($last | tonumber) | if . < 0 then 0 else . end):]) end) as $keep
130
+ | [ $runs[] | select((.ver | IN($keep[])) or ($last == "" and .ver == "unknown"))
131
+ | select($vf == [] or ((.ver | sub("~$"; "")) | IN($vf[]))) ]
132
+ | group_by([.ver, .repo]) | sort_by(.[0].ver | vkey) as $groups
133
+ | "| Version | Repo | n | Cache-read/issue mean | median | investigate | build | review | remediate | close | Remediated | Merged | Per merged issue | Confidence |",
134
+ "|---|---|---|---|---|---|---|---|---|---|---|---|---|---|",
135
+ ( $groups[] | . as $g | length as $n
136
+ | ([.[] | select(.reason == "merged")] | length) as $m
137
+ | (map(.cr) | add) as $tot
138
+ | def pm($p): (map(.phase[$p] // 0) | add) / $n;
139
+ "| \($g[0].ver) | \($g[0].repo) | \($n) | \((map(.cr) | mean) | k) | \((map(.cr) | median) | k)"
140
+ + " | \(pm("investigate") | k) | \(pm("build") | k) | \(pm("review") | k) | \(pm("remediate") | k) | \(pm("close") | k)"
141
+ + " | \(([.[] | select(.remediated)] | length) / $n | pct) | \($m / $n | pct)"
142
+ + " | \(if $m == 0 then "n/a" else ($tot / $m | k) end)"
143
+ + " | \(if $n < $minn then "low-n (<\($minn))" else "ok" end) |" ),
144
+ "",
145
+ "Finished runs only; tokens are cache-read per issue. Excluded: \($unfinished) unfinished, \($nousage) without usage data (hydrated or empty). \"~\" = version estimated from log mtime; \"unknown\" = no version available."
146
+ '