forgedock 1.7.1 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/admission.mjs +242 -7
- package/bin/engine/admission.test.mjs +183 -2
- package/bin/engine/phases.mjs +31 -22
- package/bin/engine/resolve.mjs +8 -1
- package/bin/engine/resolve.test.mjs +5 -0
- package/bin/engine.mjs +26 -17
- package/bin/forgedock.mjs +15 -2
- package/bin/hooks/pre-tool-use.mjs +573 -2
- package/bin/journey.mjs +11 -7
- package/bin/labels.json +5 -0
- package/bin/opencode-adapter.mjs +47 -13
- package/bin/orchestrate-preflight.mjs +434 -0
- package/bin/runner.mjs +30 -4
- package/commands/audit-agents.md +75 -26
- package/commands/autopilot.md +4 -4
- package/commands/cleanup.md +10 -5
- package/commands/issue.md +95 -35
- package/commands/orchestrate/phase-1-resolve.md +70 -32
- package/commands/orchestrate/phase-3-dependency.md +141 -13
- package/commands/orchestrate/phase-4-execution.md +660 -70
- package/commands/orchestrate/phase-5-cleanup.md +5 -11
- package/commands/orchestrate/phase-6-report.md +27 -1
- package/commands/orchestrate/safety.md +1 -1
- package/commands/orchestrate.md +39 -1
- package/commands/quality-gate.md +30 -10
- package/commands/review-pr-agents/protocols.md +13 -0
- package/commands/review-pr-agents.md +1 -1
- package/commands/review-pr-staging.md +100 -18
- package/commands/review-pr.md +78 -24
- package/commands/upgrade-deps.md +1 -1
- package/commands/work-on/build/validate.md +25 -4
- package/commands/work-on/close.md +40 -10
- package/commands/work-on/decompose.md +33 -1
- package/commands/work-on/investigate.md +33 -0
- package/commands/work-on/remediate.md +40 -5
- package/commands/work-on/review.md +27 -0
- package/commands/work-on.md +48 -5
- package/docs/CONFIG.md +45 -10
- package/docs/OPENCODE.md +21 -6
- package/docs/spec/review-protocol.md +7 -0
- package/package.json +1 -1
- package/scripts/check-admission-parity.mjs +1 -0
- package/scripts/check-command-side-effects.sh +14 -13
- package/scripts/check-shell-portability.sh +36 -0
- package/scripts/check-spec-markers.sh +21 -11
- package/scripts/checkbox-sections.test.sh +67 -0
- package/scripts/extract-affected-files.sh +85 -5
- package/scripts/extract-affected-files.test.sh +154 -2
package/commands/work-on.md
CHANGED
|
@@ -273,7 +273,7 @@ If detected, dispatch immediately and STOP — do NOT fall through to Phase 0B's
|
|
|
273
273
|
Skill(skill="work-on/remediate", args="${REMEDIATE_PR_NUMBER} ${REMEDIATE_ISSUE_FLAG} --repo {GH_REPO} --gh-flag {GH_FLAG}")
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
-
**After `REMEDIATE_RESULT` returns, STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained:
|
|
276
|
+
**After `REMEDIATE_RESULT` returns, STOP unconditionally** — do not run any further Phase 0–7 logic in this file. `work-on/remediate.md` is self-contained: a FIXABLE remediation replaces `needs-human` with the active `workflow:in-review` state only while it is running, then ends at `workflow:merged`, `workflow:awaiting-merge`, or a newly asserted `needs-human` label. When `re_gate_outcome: AUTO-LANDED`, it drives its own close phase internally (Phase M8 invokes `Skill("work-on:close", ...)` directly) before returning. For every other outcome (`HELD-AWAITING-MERGE`, `RE-ESCALATED`, `UNFIXABLE`, `BLOCKED`, `ALREADY_DONE`), the issue is already at a terminal state (`workflow:awaiting-merge` or `needs-human`, or already closed) per the Universal Phase Dispatcher — nothing further to do.
|
|
277
277
|
|
|
278
278
|
This mode is reachable both standalone (a human or script running `/work-on <pr> --remediate` directly) and via the orchestrator (`commands/orchestrate/phase-4-execution.md` item 6.4 auto-dispatches the identical `Skill(skill='work-on', args='{PR} --remediate --issue {N} ...')` invocation against a `needs-human`-gated predecessor's own PR).
|
|
279
279
|
|
|
@@ -358,9 +358,21 @@ fi
|
|
|
358
358
|
|
|
359
359
|
**Batch issue pipeline rules** (when `IS_BATCH=true`):
|
|
360
360
|
- Build phases execute exactly as normal (the batch issue body IS the spec for what to fix)
|
|
361
|
-
-
|
|
361
|
+
- Batch members are referenced in the PR body with `Refs #N`, never `Closes #N`; the batch issue is the only issue the PR may close.
|
|
362
|
+
- After successful merge, re-read every member's live state and labels before closing it. A member that is `needs-human`, `blocked`, or `operator-only` remains open and is reported as a split outcome:
|
|
362
363
|
```bash
|
|
363
364
|
for MEMBER in "${BATCH_MEMBERS[@]}"; do
|
|
365
|
+
MEMBER_SNAPSHOT=$(gh issue view "$MEMBER" {GH_FLAG} --json state,labels \
|
|
366
|
+
--jq '{state: .state, labels: [.labels[].name]}' 2>/dev/null) || {
|
|
367
|
+
echo "WARNING: could not verify batch member #${MEMBER}; leaving it open"
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
MEMBER_GATED=$(echo "$MEMBER_SNAPSHOT" | jq -r \
|
|
371
|
+
'(.state != "OPEN") or ([.labels[] | select(. == "needs-human" or . == "blocked" or . == "operator-only")] | length > 0)')
|
|
372
|
+
if [ "$MEMBER_GATED" = "true" ]; then
|
|
373
|
+
echo "SPLIT OUTCOME: #${MEMBER} remains open because it requires a human or operator action."
|
|
374
|
+
continue
|
|
375
|
+
fi
|
|
364
376
|
gh issue close "$MEMBER" {GH_FLAG} \
|
|
365
377
|
--comment "Resolved as part of batch PR #{PR_NUMBER} (#{ISSUE_NUMBER}). See batch issue for details."
|
|
366
378
|
gh issue edit "$MEMBER" {GH_FLAG} --add-label "workflow:merged" 2>/dev/null || true
|
|
@@ -2081,11 +2093,42 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:CHECKPOINT -->
|
|
|
2081
2093
|
|
|
2082
2094
|
```bash
|
|
2083
2095
|
BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
|
|
2084
|
-
REMAINING_BEFORE=$(
|
|
2085
|
-
|
|
2096
|
+
REMAINING_BEFORE=$(printf '%s\n' "$BODY" | grep -cE '^[-*+] \[ \]' || true)
|
|
2097
|
+
|
|
2098
|
+
# Structural test: count heading-delimited sections that contain checkbox items.
|
|
2099
|
+
# Multi-phase == 2+ checkbox-bearing sections. A single checkbox group is
|
|
2100
|
+
# single-phase regardless of how many prose headings surround it.
|
|
2101
|
+
#
|
|
2102
|
+
# - Fenced code blocks are stripped first: issue bodies routinely embed fenced
|
|
2103
|
+
# blocks whose lines start with '#' or contain a literal '- [ ]'. An
|
|
2104
|
+
# unterminated fence keeps the original body so later work is never hidden.
|
|
2105
|
+
# - ATX headings and setext underlines both delimit sections. The ATX pattern
|
|
2106
|
+
# avoids awk interval-quantifier variance across awk implementations.
|
|
2107
|
+
# - grep -E / awk only — no PCRE. '^#+ ' needs none.
|
|
2108
|
+
FENCE_COUNT=$(printf '%s\n' "$BODY" | grep -cE '^(```+|~~~+)' || true)
|
|
2109
|
+
if [ $(( ${FENCE_COUNT:-0} % 2 )) -ne 0 ]; then
|
|
2110
|
+
BODY_STRIPPED="$BODY"
|
|
2111
|
+
else
|
|
2112
|
+
BODY_STRIPPED=$(printf '%s\n' "$BODY" | awk '/^(```+|~~~+)/{f=!f; next} !f')
|
|
2113
|
+
fi
|
|
2114
|
+
|
|
2115
|
+
CHECKBOX_SECTIONS=$(printf '%s\n' "$BODY_STRIPPED" | awk '
|
|
2116
|
+
/^#+ / { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
2117
|
+
/^(=+|-+)$/ && previous != "" { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
2118
|
+
{ if (in_section && /^[-*+] \[[ xX]\]/) has=1; previous=$0 }
|
|
2119
|
+
END { if (in_section && has) n++; print n+0 }
|
|
2120
|
+
')
|
|
2121
|
+
|
|
2122
|
+
# Sub-issue-tracker guard: a decompose parent whose only checkbox group is
|
|
2123
|
+
# '## Sub-Issue Tracker' counts 1 section. Checking those off would mark open
|
|
2124
|
+
# sub-issues done and close the tracker, so any unchecked GFM task item for an
|
|
2125
|
+
# issue forces multi-phase.
|
|
2126
|
+
SUBISSUE_ITEMS=$(printf '%s\n' "$BODY_STRIPPED" | grep -cE '^[-*+] \[ \] #[0-9]+' || true)
|
|
2086
2127
|
```
|
|
2087
2128
|
|
|
2088
|
-
|
|
2129
|
+
**Sync invariant:** Keep the structural computation above and its consuming multi-phase guard in `commands/work-on/close.md` Phase C1 synchronized. The guard is `CHECKBOX_SECTIONS >= 2 OR SUBISSUE_ITEMS > 0`; this Phase 6A path is reached only when `REMAINING_BEFORE > 0`. <!-- Fixed: forge#2840, #2874 -->
|
|
2130
|
+
|
|
2131
|
+
If multi-phase (`CHECKBOX_SECTIONS >= 2` OR a `- [ ] #NNN` sub-issue item is present): do NOT check off future phase items. Add PR reference only. A heading count is **not** the test — every templated issue carries `## Problem`/`## Evidence`/`## Context`, so it is `> 0` universally.
|
|
2089
2132
|
|
|
2090
2133
|
If single-phase or final phase: check off all `[ ]` items, add PR reference.
|
|
2091
2134
|
|
package/docs/CONFIG.md
CHANGED
|
@@ -298,9 +298,13 @@ Concurrency limits and cascade admission policy for `/orchestrate`'s batch engin
|
|
|
298
298
|
|
|
299
299
|
```yaml
|
|
300
300
|
orchestration:
|
|
301
|
-
# Maximum number of /work-on
|
|
302
|
-
#
|
|
303
|
-
#
|
|
301
|
+
# Maximum number of top-level /work-on agents dispatched concurrently. Each
|
|
302
|
+
# worker normally fans out to about 8 total subagent spawns (build, quality
|
|
303
|
+
# gate, and review included), so set this to no more than session_budget / 8.
|
|
304
|
+
# Default: 12; use 25 or fewer for a 200-spawn session budget. This limits
|
|
305
|
+
# in-flight workers only: under cascade.policy: all, raising it increases
|
|
306
|
+
# both completion and finding-spawn throughput and does not itself bound or
|
|
307
|
+
# shrink cascade queue growth.
|
|
304
308
|
max_concurrent: 12
|
|
305
309
|
|
|
306
310
|
cascade:
|
|
@@ -324,13 +328,26 @@ orchestration:
|
|
|
324
328
|
# spawned from a review-finding. Accepts a positive integer or the literal
|
|
325
329
|
# string "unlimited". Governs Phase 1 resolve-time admission only (the
|
|
326
330
|
# `cascade`/`review-findings`/`findings` resolution in phase-1-resolve.md)
|
|
327
|
-
# — it does NOT
|
|
328
|
-
#
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
+
# — it does NOT individually dispatch deferred generation-2+ findings in
|
|
332
|
+
# Step 4C. Bounded P3 batching is the sole automated exception: it may
|
|
333
|
+
# aggregate a finite generation-2 member set under batch_max_generation,
|
|
334
|
+
# as implemented by #2849. This is what makes "admit gen-2, stop at gen-3"
|
|
335
|
+
# expressible, which the pre-#2234 --allow-gen2 flag could not say.
|
|
331
336
|
# Default: 1 (from the "balanced" preset); "unlimited" under "all".
|
|
332
337
|
max_generation: 1
|
|
333
338
|
|
|
339
|
+
# Maximum generation a P3 batch may aggregate. Batching is a sanctioned,
|
|
340
|
+
# auditable exception to Step 4C's autonomous cascade cap: it can fold
|
|
341
|
+
# several deferred low-priority findings into one reviewed unit, but it
|
|
342
|
+
# must never make recursion unbounded. Every batch records the maximum
|
|
343
|
+
# member generation in FORGE:BATCH_MAX_GENERATION. This value is always
|
|
344
|
+
# finite, even under policy: all. Default: 2.
|
|
345
|
+
batch_max_generation: 2
|
|
346
|
+
|
|
347
|
+
# Permit closure only of mechanically identical bot/app alerts: same
|
|
348
|
+
# normalized title, generator, and trigger condition, while retaining one
|
|
349
|
+
# canonical issue. Human reports are never included. Default: false.
|
|
350
|
+
dedup_automated: false
|
|
334
351
|
# Per-batch token ceiling for Step 4C's review-finding cascade dispatch.
|
|
335
352
|
# New home for the same lever previously read only as
|
|
336
353
|
# `pipeline.token_budget_per_batch` (kept working as a deprecated alias —
|
|
@@ -353,17 +370,30 @@ orchestration:
|
|
|
353
370
|
# (avoids serializing agents on same-file predecessor edges). Default:
|
|
354
371
|
# true (from "balanced"); false under "all".
|
|
355
372
|
p3_same_file_defer: true
|
|
373
|
+
|
|
374
|
+
# Running findings-spawned / merged-units ceiling. "off" (the default)
|
|
375
|
+
# only reports amplification; a positive decimal defers further
|
|
376
|
+
# same-lineage refinements to the completion sweep once exceeded.
|
|
377
|
+
max_amplification: "off"
|
|
378
|
+
|
|
379
|
+
# Number of consecutive merged-unit observations at ratio >= 1.0 before
|
|
380
|
+
# printing a convergence warning. Default: 3.
|
|
381
|
+
convergence_window: 3
|
|
356
382
|
```
|
|
357
383
|
|
|
358
384
|
| Field | Type | Required | Description |
|
|
359
385
|
|-------|------|----------|-------------|
|
|
360
|
-
| `max_concurrent` | integer | No | Max concurrent `/work-on`
|
|
386
|
+
| `max_concurrent` | integer | No | Max concurrent top-level `/work-on` dispatches. It does not bound cascade growth; under `policy: all` it raises drain and spawn throughput together. Default: 12. |
|
|
361
387
|
| `cascade.policy` | string | No | `all` \| `balanced` \| `conservative`. Default: `balanced` |
|
|
362
388
|
| `cascade.max_generation` | integer or `"unlimited"` | No | Max cascade generation admitted at Phase 1 resolve time. Default: 1 |
|
|
389
|
+
| `cascade.batch_max_generation` | integer | No | Finite maximum member generation that an automated P3 batch may aggregate. Default: 2 |
|
|
390
|
+
| `cascade.dedup_automated` | boolean | No | Allow only mechanically identical bot/app alerts to be closed with a retained canonical issue. Default: `false` |
|
|
363
391
|
| `cascade.token_budget` | integer or `"unlimited"` | No | Per-batch token ceiling for Step 4C cascade dispatch. Default: 900000 (deprecated alias: `pipeline.token_budget_per_batch`) |
|
|
364
392
|
| `cascade.defer_on_batch_gated` | boolean | No | Suppress cascade dispatch when the original batch is fully human-gated. Default: `true` |
|
|
365
393
|
| `cascade.keyword_heuristic` | boolean | No | Defer P3-and-below comment/typo-titled findings. Default: `true` |
|
|
366
394
|
| `cascade.p3_same_file_defer` | boolean | No | Defer P3 findings sharing a file with the active batch. Default: `true` |
|
|
395
|
+
| `cascade.max_amplification` | positive number or `"off"` | No | Opt-in ceiling for same-lineage refinement dispatch. Default: `"off"` |
|
|
396
|
+
| `cascade.convergence_window` | positive integer | No | Consecutive ratio observations at or above 1.0 before warning. Default: 3 |
|
|
367
397
|
|
|
368
398
|
**Hard invariant — not configurable**: safety exclusions (findings whose `## Problem` section indicates security/billing/anti-bot/auth concerns) are never batched and never auto-admitted by any `cascade.policy`, including `all`. That exclusion is enforced upstream of this section (the P3 batching eligibility check) and has no corresponding key here by design.
|
|
369
399
|
|
|
@@ -375,10 +405,14 @@ orchestration:
|
|
|
375
405
|
|
|
376
406
|
## `pipeline` (OPTIONAL)
|
|
377
407
|
|
|
378
|
-
Tuning knobs for `/orchestrate`'s batch engine — stall
|
|
408
|
+
Tuning knobs for the CLI backend and `/orchestrate`'s batch engine — invocation and stall timeouts, narration verbosity, and (deprecated alias only, see `orchestration.cascade` above) the review-finding cascade token budget. All keys optional; sane defaults apply when omitted.
|
|
379
409
|
|
|
380
410
|
```yaml
|
|
381
411
|
pipeline:
|
|
412
|
+
# Wall-clock limit for one CLI backend invocation. The environment variable
|
|
413
|
+
# FORGEDOCK_CLI_TIMEOUT_MS (milliseconds) takes precedence. Default: 60.
|
|
414
|
+
cli_timeout_minutes: 60
|
|
415
|
+
|
|
382
416
|
# Minutes an agent may sit idle (no FORGE:HEARTBEAT / label change) before
|
|
383
417
|
# /orchestrate's stall detector treats it as stuck and attempts an
|
|
384
418
|
# auto-resume. Default: 15.
|
|
@@ -401,12 +435,13 @@ pipeline:
|
|
|
401
435
|
|
|
402
436
|
| Field | Type | Required | Description |
|
|
403
437
|
|-------|------|----------|-------------|
|
|
438
|
+
| `cli_timeout_minutes` | integer | No | Wall-clock minutes for one CLI backend invocation. Default: 60. `FORGEDOCK_CLI_TIMEOUT_MS` (milliseconds) takes precedence. |
|
|
404
439
|
| `stall_timeout_minutes` | integer | No | Idle minutes before the stall detector attempts auto-resume. Default: 15 |
|
|
405
440
|
| `token_budget_per_batch` | integer | No | **Deprecated** — use `orchestration.cascade.token_budget`. Default: 900000 |
|
|
406
441
|
| `token_estimate_per_finding` | integer | No | Estimated tokens per P3-or-lower cascade finding. Default: 150000 |
|
|
407
442
|
| `narration` | string | No | `terse` \| `verbose`. Default: `terse` |
|
|
408
443
|
|
|
409
|
-
**Commands that use this section**: `orchestrate`
|
|
444
|
+
**Commands that use this section**: CLI backend (`cli_timeout_minutes`); `orchestrate` (remaining keys)
|
|
410
445
|
|
|
411
446
|
---
|
|
412
447
|
|
package/docs/OPENCODE.md
CHANGED
|
@@ -64,8 +64,8 @@ The generated plugin has no prompt text. It:
|
|
|
64
64
|
preserving explicit lower limits;
|
|
65
65
|
- grants the built-in `general` subagent permission to invoke native `task`
|
|
66
66
|
unless the user explicitly configured a task permission;
|
|
67
|
-
- opts into OpenCode background subagents
|
|
68
|
-
wake the parent
|
|
67
|
+
- opts into OpenCode background subagents for the orchestrator's explicit
|
|
68
|
+
ready-issue dispatches so each completed issue can wake the parent independently; set
|
|
69
69
|
`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` to opt out;
|
|
70
70
|
- selects Git Bash on Windows only when the user has not explicitly configured
|
|
71
71
|
an OpenCode shell, because the shared workflows and helper scripts use Bash.
|
|
@@ -104,6 +104,14 @@ The adapter follows these rules:
|
|
|
104
104
|
- Orchestration dispatches independent issues with `task(background=true)` and
|
|
105
105
|
processes each injected task-result event immediately; it does not wait for a
|
|
106
106
|
wave or the slowest sibling.
|
|
107
|
+
- The `/orchestrate` entrypoint reads only runtime config, then runs
|
|
108
|
+
`bin/orchestrate-preflight.mjs` before loading the phase specs. The helper batches the initial GitHub
|
|
109
|
+
snapshot and emits only the compact ready queue needed for the first native task.
|
|
110
|
+
Interactive runs keep the confirmation gate; `--auto` or `--confirm` is the
|
|
111
|
+
explicit headless authorization.
|
|
112
|
+
- The full Phase 3/4 prose is reserved for investigations, unsupported or multi-repo
|
|
113
|
+
queries, explicit deep planning, and recovery after a task-result event. It remains
|
|
114
|
+
authoritative for cascade handling, leases, cleanup, and reporting.
|
|
107
115
|
- GitHub state and the durable engine remain the recovery source instead of
|
|
108
116
|
replaying prior prompt context.
|
|
109
117
|
|
|
@@ -115,12 +123,12 @@ specs evaluate Claude's literal `Task`/`Agent` names, an OpenCode runtime marker
|
|
|
115
123
|
selects `DISPATCH_TOOL=task`; the absence of those Claude names must not produce
|
|
116
124
|
a false `FORGE:REVIEW_BLOCKED` result.
|
|
117
125
|
|
|
118
|
-
Every isolated review dispatch uses
|
|
126
|
+
Every isolated review dispatch uses a foreground native task. Every
|
|
119
127
|
native task call must include the schema-required `subagent_type` at the top
|
|
120
128
|
level alongside its `description` and `prompt`:
|
|
121
129
|
|
|
122
130
|
```js
|
|
123
|
-
{ description: "...", prompt: "...", subagent_type: "general", background:
|
|
131
|
+
{ description: "...", prompt: "...", subagent_type: "general", background: false }
|
|
124
132
|
```
|
|
125
133
|
|
|
126
134
|
Implementation and review work uses `general`; read-only discovery uses
|
|
@@ -131,8 +139,9 @@ generated adapter safely defaults it to `general`; unsupported types stop with
|
|
|
131
139
|
If the native `task` capability itself is unavailable, the workflow posts
|
|
132
140
|
`FORGE:REVIEW_BLOCKED` and stops rather than falling back to inline review or
|
|
133
141
|
another pipeline controller.
|
|
134
|
-
The generated plugin
|
|
135
|
-
`
|
|
142
|
+
The generated plugin defaults omitted `background` to `false`. Only the
|
|
143
|
+
orchestrator's ready-issue dispatcher sets `background: true`; setting
|
|
144
|
+
`OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=false` forces foreground execution.
|
|
136
145
|
|
|
137
146
|
`commands/work-on.md` is still a large entry dispatcher and is loaded in full,
|
|
138
147
|
matching the Claude Code path. The adapter prevents additional eager loading,
|
|
@@ -218,6 +227,12 @@ Commands that inspect Claude-specific transcripts or Claude installation state
|
|
|
218
227
|
remain runtime-specific and should not be represented as portable until they
|
|
219
228
|
receive dedicated implementations.
|
|
220
229
|
|
|
230
|
+
The preflight is intentionally bounded. It handles explicit issue sets and the
|
|
231
|
+
common single-repository `fast-lane`, `milestone`, `next`, `priority`, and
|
|
232
|
+
`no:milestone` queries, plus explicit dependencies and scoped issue-body file
|
|
233
|
+
overlap. Unsupported or complex inputs fall back to the shared phase specs rather
|
|
234
|
+
than silently weakening their safety rules.
|
|
235
|
+
|
|
221
236
|
## Orchestration Runtime
|
|
222
237
|
|
|
223
238
|
OpenCode orchestration uses the native OpenCode `task` tool for isolated issue
|
|
@@ -122,6 +122,12 @@ Every finding must include:
|
|
|
122
122
|
|
|
123
123
|
**All review agents MUST include a machine-readable findings block at the end of their PR comment.** This is NON-OPTIONAL. Without structured findings, the review system cannot create GitHub issues, and findings die as unread PR comments. Every finding that doesn't become a GitHub issue is a finding that will never be addressed.
|
|
124
124
|
|
|
125
|
+
### Persist Before Post
|
|
126
|
+
|
|
127
|
+
GitHub is a delivery channel, not the sole record of a review. Before the first `gh pr comment` attempt, write the finalized complete review body, including its `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker and structured findings block, to a uniquely named durable file such as `${TMPDIR:-/tmp}/forge-review-${PR_NUMBER}-${DOMAIN}-$$.md`. Use `gh pr comment --body-file "$REVIEW_BODY_PATH"`; do not construct a retry loop around a failed write.
|
|
128
|
+
|
|
129
|
+
Every agent MUST return its verdict, finding count, and one line per finding to the orchestrator even when posting succeeds. If the post fails, return the durable body path and the same finding summary, then stop. A 403 or other write failure is a failed delivery, not a clean review and not a reason to retry in the background.
|
|
130
|
+
|
|
125
131
|
### Format
|
|
126
132
|
|
|
127
133
|
Append this block at the very end of your comment (after the `---` footer line, still inside the EOF heredoc). It uses HTML comments so it's invisible in rendered markdown:
|
|
@@ -140,6 +146,7 @@ Append this block at the very end of your comment (after the `---` footer line,
|
|
|
140
146
|
6. **Summary**: Concise one-line description (no pipe `|` characters in summary)
|
|
141
147
|
7. **Empty block**: If no findings at all, include just the START/END markers
|
|
142
148
|
8. **HTML comments**: The block is invisible in rendered markdown but parseable by the review system
|
|
149
|
+
9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the lowercase dispatched domain.
|
|
143
150
|
|
|
144
151
|
### Domain Prefixes
|
|
145
152
|
|
package/package.json
CHANGED
|
@@ -47,6 +47,7 @@ function parseArmBody(body) {
|
|
|
47
47
|
};
|
|
48
48
|
return {
|
|
49
49
|
maxGeneration: get("PRESET_MAX_GEN"),
|
|
50
|
+
batchMaxGeneration: get("PRESET_BATCH_MAX_GEN"),
|
|
50
51
|
tokenBudget: get("PRESET_TOKEN_BUDGET"),
|
|
51
52
|
deferOnBatchGated: get("PRESET_DEFER_GATED"),
|
|
52
53
|
keywordHeuristic: get("PRESET_KEYWORD"),
|
|
@@ -120,7 +120,7 @@ class_a_scan() {
|
|
|
120
120
|
# accumulated violation silently. (forge#2289 added the equivalent
|
|
121
121
|
# end-of-file flush for Class B only; this closes the same gap in Class A.)
|
|
122
122
|
flush_a_block() {
|
|
123
|
-
if [ "$HAS_GIST" -eq 1 ] &&
|
|
123
|
+
if [ "$HAS_GIST" -eq 1 ] && [[ "$BLOCK_LINES" =~ (^|$'\n')[[:space:]]*--public([[:space:]]|$) ]]; then
|
|
124
124
|
# Find the line number of --public within the block
|
|
125
125
|
PUBLIC_LN=$(echo "$BLOCK_LINES" | grep -n '^[[:space:]]*--public' | head -1 | cut -d: -f1)
|
|
126
126
|
ACTUAL_LINE=$((BLOCK_START + PUBLIC_LN))
|
|
@@ -149,9 +149,9 @@ class_a_scan() {
|
|
|
149
149
|
# content; forge#2288 adds true depth so 2+ nesting levels close correctly.)
|
|
150
150
|
IS_FENCE=0
|
|
151
151
|
IS_BARE=0
|
|
152
|
-
if
|
|
152
|
+
if [[ "$line" =~ ^[[:space:]]*${FENCE} ]]; then
|
|
153
153
|
IS_FENCE=1
|
|
154
|
-
if
|
|
154
|
+
if [[ "$line" =~ ^[[:space:]]*${FENCE}[[:space:]]*$ ]]; then
|
|
155
155
|
IS_BARE=1
|
|
156
156
|
fi
|
|
157
157
|
fi
|
|
@@ -188,7 +188,7 @@ class_a_scan() {
|
|
|
188
188
|
BLOCK_LINES="${BLOCK_LINES}${line}
|
|
189
189
|
"
|
|
190
190
|
# Check if this line has gh gist create/edit (even with line continuation \)
|
|
191
|
-
if
|
|
191
|
+
if [[ "$line" =~ gh[[:space:]]+gist[[:space:]]+(create|edit) ]]; then
|
|
192
192
|
HAS_GIST=1
|
|
193
193
|
fi
|
|
194
194
|
done < "$file"
|
|
@@ -296,9 +296,9 @@ else
|
|
|
296
296
|
# 2+ nesting levels close correctly.)
|
|
297
297
|
IS_FENCE=0
|
|
298
298
|
IS_BARE=0
|
|
299
|
-
if
|
|
299
|
+
if [[ "$line" =~ ^[[:space:]]*${FENCE} ]]; then
|
|
300
300
|
IS_FENCE=1
|
|
301
|
-
if
|
|
301
|
+
if [[ "$line" =~ ^[[:space:]]*${FENCE}[[:space:]]*$ ]]; then
|
|
302
302
|
IS_BARE=1
|
|
303
303
|
fi
|
|
304
304
|
fi
|
|
@@ -314,8 +314,9 @@ else
|
|
|
314
314
|
# (forge#2289) and is always already at rest here, since this branch
|
|
315
315
|
# is only reached when IN_CB is 0 — i.e. any block that was open has
|
|
316
316
|
# already been closed and flushed at its own fence-close below.
|
|
317
|
-
if
|
|
318
|
-
heading
|
|
317
|
+
if [[ "$line" =~ ^#{1,6}[[:space:]]+(.*)$ ]]; then
|
|
318
|
+
heading="${BASH_REMATCH[1]}"
|
|
319
|
+
heading="${heading%"${heading##*[![:space:]]}"}"
|
|
319
320
|
SECTION="$heading"
|
|
320
321
|
continue
|
|
321
322
|
fi
|
|
@@ -338,18 +339,18 @@ else
|
|
|
338
339
|
fi
|
|
339
340
|
|
|
340
341
|
# Content line while inside (at any depth).
|
|
341
|
-
if
|
|
342
|
+
if [[ "$line" == *"$ALLOWLIST_TOKEN"* ]]; then continue; fi
|
|
342
343
|
|
|
343
344
|
# Check for guard — scoped to THIS code block only (forge#2289)
|
|
344
|
-
|
|
345
|
+
[[ "$line" =~ $GUARD_PATTERN ]] && BLOCK_HAS_GUARD=1
|
|
345
346
|
|
|
346
347
|
# Check if this line is in the diff's added lines AND has a side-effect verb
|
|
347
|
-
if [ "$BLOCK_HAS_ADDED_SE" -eq 0 ] &&
|
|
348
|
+
if [ "$BLOCK_HAS_ADDED_SE" -eq 0 ] && [[ "$line" =~ $SIDE_EFFECT_PATTERN ]]; then
|
|
348
349
|
# Is this specific line in the added content?
|
|
349
|
-
if
|
|
350
|
+
if [[ "$ADDED_CONTENT" == *"${line:0:80}"* ]]; then
|
|
350
351
|
BLOCK_HAS_ADDED_SE=1
|
|
351
352
|
BLOCK_SE_LINE=$LN
|
|
352
|
-
BLOCK_SE_VERB
|
|
353
|
+
BLOCK_SE_VERB="${BASH_REMATCH[0]:-side-effect}"
|
|
353
354
|
fi
|
|
354
355
|
fi
|
|
355
356
|
done < "$file"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# check-shell-portability.sh — Verify shipped shell scripts have an explicit
|
|
6
|
+
# Bash interpreter and parse successfully before a quality gate executes them.
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
SCRIPTS_DIR="${1:-./scripts}"
|
|
11
|
+
|
|
12
|
+
if [ ! -d "$SCRIPTS_DIR" ]; then
|
|
13
|
+
echo "ERROR: scripts directory not found: $SCRIPTS_DIR" >&2
|
|
14
|
+
exit 2
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
violations=0
|
|
18
|
+
while IFS= read -r -d '' script; do
|
|
19
|
+
first_line=$(IFS= read -r line < "$script" || true; printf '%s' "$line")
|
|
20
|
+
if [ "$first_line" != "#!/usr/bin/env bash" ]; then
|
|
21
|
+
echo "HIGH | $script | shell scripts must declare #!/usr/bin/env bash" >&2
|
|
22
|
+
violations=$((violations + 1))
|
|
23
|
+
continue
|
|
24
|
+
fi
|
|
25
|
+
if ! bash -n "$script"; then
|
|
26
|
+
echo "HIGH | $script | shell syntax validation failed" >&2
|
|
27
|
+
violations=$((violations + 1))
|
|
28
|
+
fi
|
|
29
|
+
done < <(find "$SCRIPTS_DIR" -type f -name '*.sh' -print0 | sort -z)
|
|
30
|
+
|
|
31
|
+
if [ "$violations" -gt 0 ]; then
|
|
32
|
+
echo "check-shell-portability: $violations violation(s) found. See stderr for details." >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
echo "OK: Shell scripts declare Bash and pass syntax validation"
|
|
@@ -87,9 +87,11 @@ MARKER_REGISTRY="
|
|
|
87
87
|
BATCH_MEMBERS
|
|
88
88
|
BENCH_SCORECARD
|
|
89
89
|
BLOCKED_ON_HUMAN_MERGE
|
|
90
|
+
BODY
|
|
90
91
|
BUILDER
|
|
91
92
|
CALIBRATION_CHECK
|
|
92
93
|
CARD
|
|
94
|
+
CLASS
|
|
93
95
|
CHECKPOINT
|
|
94
96
|
CLAIM
|
|
95
97
|
CLAIM_RELEASED
|
|
@@ -100,6 +102,7 @@ MARKER_REGISTRY="
|
|
|
100
102
|
DECISION_RECORD
|
|
101
103
|
DECOMPOSED
|
|
102
104
|
DESIGN
|
|
105
|
+
DISPATCH
|
|
103
106
|
DISPATCHER
|
|
104
107
|
DOSSIER_UPDATED
|
|
105
108
|
ENGINE_FALLBACK
|
|
@@ -187,28 +190,29 @@ while IFS= read -r file; do
|
|
|
187
190
|
content="${markerline#*:}"
|
|
188
191
|
|
|
189
192
|
# Skip allowlisted lines
|
|
190
|
-
if
|
|
193
|
+
if [[ "$content" == *"$ALLOWLIST_TOKEN"* ]]; then
|
|
191
194
|
continue
|
|
192
195
|
fi
|
|
193
196
|
|
|
194
197
|
# Skip markers that are inside code spans (backtick-quoted) — those are docs examples
|
|
195
198
|
# Heuristic: if the FORGE:TYPE appears between backticks on the same line, skip it
|
|
196
199
|
# We check for backtick on either side of the FORGE: marker
|
|
197
|
-
if
|
|
200
|
+
if [[ "$content" =~ \`[^\`]*FORGE:[A-Z_] ]]; then
|
|
198
201
|
continue
|
|
199
202
|
fi
|
|
200
203
|
|
|
201
204
|
# Extract the full marker string after FORGE: — everything up to a space, -->, or end
|
|
202
205
|
# This captures compound markers like REVIEW-AGENT:material-change
|
|
203
|
-
|
|
206
|
+
[[ "$content" =~ FORGE:([A-Z][A-Z0-9_:-]*) ]] || continue
|
|
207
|
+
full_marker="${BASH_REMATCH[1]}"
|
|
204
208
|
|
|
205
209
|
[ -z "$full_marker" ] && continue
|
|
206
210
|
|
|
207
211
|
# Extract PRIMARY type: part before the first - or : separator
|
|
208
|
-
primary_type
|
|
212
|
+
primary_type="${full_marker%%[-:]*}"
|
|
209
213
|
|
|
210
214
|
# Validate primary type against registry
|
|
211
|
-
if !
|
|
215
|
+
if ! [[ "$primary_type" =~ $REGISTRY_PATTERN ]]; then
|
|
212
216
|
echo "HIGH | $file:$lineno | unknown FORGE: marker 'FORGE:${full_marker}' (primary type '${primary_type}') — not in registry" >&2
|
|
213
217
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
214
218
|
fi
|
|
@@ -242,21 +246,27 @@ if [ -d "$WORKFLOWS_DIR" ]; then
|
|
|
242
246
|
LINENO=$((LINENO + 1))
|
|
243
247
|
|
|
244
248
|
# Skip allowlisted lines
|
|
245
|
-
if
|
|
249
|
+
if [[ "$line" == *"$ALLOWLIST_TOKEN"* ]]; then
|
|
246
250
|
continue
|
|
247
251
|
fi
|
|
248
252
|
|
|
249
253
|
# Skip comment lines
|
|
250
|
-
if
|
|
254
|
+
if [[ "$line" =~ ^[[:space:]]*# ]]; then
|
|
251
255
|
continue
|
|
252
256
|
fi
|
|
253
257
|
|
|
254
258
|
# Strip bash ${VAR} and $VAR patterns before checking for template placeholders
|
|
255
259
|
# so that ELAPSED in "${ELAPSED}s" doesn't false-positive as {ELAPSED}
|
|
256
|
-
stripped
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
stripped="$line"
|
|
261
|
+
while [[ "$stripped" =~ \$\{[^}]*\} ]]; do
|
|
262
|
+
stripped="${stripped/"${BASH_REMATCH[0]}"/BASH_VAR}"
|
|
263
|
+
done
|
|
264
|
+
while [[ "$stripped" =~ \$[A-Za-z_][A-Za-z0-9_]* ]]; do
|
|
265
|
+
stripped="${stripped/"${BASH_REMATCH[0]}"/BASH_VAR}"
|
|
266
|
+
done
|
|
267
|
+
|
|
268
|
+
if [[ "$stripped" =~ $PLACEHOLDER_PATTERN ]]; then
|
|
269
|
+
placeholder="${BASH_REMATCH[0]}"
|
|
260
270
|
echo "HIGH | $wf_file:$LINENO | unsubstituted placeholder '$placeholder' in workflow" >&2
|
|
261
271
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
262
272
|
fi
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# checkbox-sections.test.sh — Regression tests for the close-path classifier.
|
|
3
|
+
#
|
|
4
|
+
# Usage: bash scripts/checkbox-sections.test.sh
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
9
|
+
|
|
10
|
+
extract_block() {
|
|
11
|
+
awk '/^FENCE_COUNT=/{printing=1} printing{print} /^SUBISSUE_ITEMS=/{exit}' "$1"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
extract_classifier() {
|
|
15
|
+
awk '
|
|
16
|
+
/^CHECKBOX_SECTIONS=.*awk .$/ { printing=1; next }
|
|
17
|
+
printing && /^\047\)$/ { exit }
|
|
18
|
+
printing { print }
|
|
19
|
+
' "$1"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
CLOSE_BLOCK=$(extract_block "$ROOT/commands/work-on/close.md")
|
|
23
|
+
INLINE_BLOCK=$(extract_block "$ROOT/commands/work-on.md")
|
|
24
|
+
[[ "$CLOSE_BLOCK" == "$INLINE_BLOCK" ]] || {
|
|
25
|
+
printf 'FAIL: mirrored classifier blocks differ\n' >&2
|
|
26
|
+
exit 1
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
CLOSE_CLASSIFIER=$(extract_classifier "$ROOT/commands/work-on/close.md")
|
|
30
|
+
INLINE_CLASSIFIER=$(extract_classifier "$ROOT/commands/work-on.md")
|
|
31
|
+
[[ "$CLOSE_CLASSIFIER" == "$INLINE_CLASSIFIER" ]] || {
|
|
32
|
+
printf 'FAIL: mirrored classifier implementations differ\n' >&2
|
|
33
|
+
exit 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if grep -q 'echo "\$BODY\|echo "\$BODY_STRIPPED' \
|
|
37
|
+
"$ROOT/commands/work-on/close.md" "$ROOT/commands/work-on.md"; then
|
|
38
|
+
printf 'FAIL: arbitrary issue bodies must be piped with printf, not echo\n' >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
grep -Fq 'Keep the structural computation above and its consuming multi-phase guard' "$ROOT/commands/work-on.md" || {
|
|
43
|
+
printf 'FAIL: Phase 6A sync invariant must cover the consuming guard\n' >&2
|
|
44
|
+
exit 1
|
|
45
|
+
}
|
|
46
|
+
grep -Fq 'The guard is `CHECKBOX_SECTIONS >= 2 OR SUBISSUE_ITEMS > 0`' "$ROOT/commands/work-on.md" || {
|
|
47
|
+
printf 'FAIL: Phase 6A must document the Phase C1 guard\n' >&2
|
|
48
|
+
exit 1
|
|
49
|
+
}
|
|
50
|
+
grep -Fq 'if [ "${CHECKBOX_SECTIONS:-0}" -ge 2 ] || [ "${SUBISSUE_ITEMS:-0}" -gt 0 ]; then' "$ROOT/commands/work-on/close.md" || {
|
|
51
|
+
printf 'FAIL: Phase C1 multi-phase guard changed unexpectedly\n' >&2
|
|
52
|
+
exit 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
SETEXT_BODY=$'Phase One\n=========\n- [x] complete\n\nPhase Two\n---------\n- [ ] remaining'
|
|
56
|
+
[[ "$(printf '%s\n' "$SETEXT_BODY" | awk "$CLOSE_CLASSIFIER")" == "2" ]] || {
|
|
57
|
+
printf 'FAIL: setext phases must count as two sections\n' >&2
|
|
58
|
+
exit 1
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
SINGLE_BODY=$'## Acceptance Criteria\n- [ ] complete this work'
|
|
62
|
+
[[ "$(printf '%s\n' "$SINGLE_BODY" | awk "$CLOSE_CLASSIFIER")" == "1" ]] || {
|
|
63
|
+
printf 'FAIL: one checkbox-bearing section must remain single-phase\n' >&2
|
|
64
|
+
exit 1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
printf 'PASS: mirrored classifier supports setext section boundaries\n'
|