forgedock 1.19.0 → 1.20.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.
- package/README.md +2 -0
- package/bin/engine.mjs +48 -3
- package/bin/runner.mjs +20 -4
- package/commands/orchestrate/phase-4-execution.md +1 -1
- package/commands/review-pr-agents/protocols.md +19 -0
- package/commands/review-pr.md +7 -6
- package/commands/work-on/build/architect.md +1 -1
- package/commands/work-on/build/implement.md +9 -1
- package/commands/work-on/build/validate.md +1 -1
- package/commands/work-on/build.md +118 -8
- package/commands/work-on/close.md +9 -4
- package/commands/work-on/remediate.md +11 -4
- package/commands/work-on/review.md +11 -3
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +5 -1
- package/docs/RELEASE-NOTES.md +15 -0
- package/docs/TELEMETRY.md +2 -2
- package/docs/WORK-ON-RUNTIME.md +11 -3
- package/package.json +1 -1
- package/scripts/architect-resume-match.test.sh +12 -3
- package/scripts/build-knowledge-index.mjs +8 -5
- package/scripts/builder-resume-match.test.sh +12 -3
- package/scripts/calibration.mjs +5 -2
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/ci-local/lib.mjs +11 -3
- package/scripts/ci-local/run.mjs +155 -49
- package/scripts/ci-local/scoping.mjs +27 -4
- package/scripts/ci-local.sh +3 -2
- package/scripts/ci-local.test.sh +189 -0
- package/scripts/classify-finding.sh +1 -1
- package/scripts/file-deferred-followups.sh +91 -30
- package/scripts/file-deferred-followups.test.sh +101 -8
- package/scripts/lib/telemetry.sh +2 -0
- package/scripts/review-finalize.sh +101 -12
- package/scripts/review-finalize.test.sh +136 -5
- package/scripts/review-roster.sh +18 -5
- package/scripts/review-roster.test.sh +57 -1
- package/scripts/selfreview-plan.sh +86 -0
- package/scripts/selfreview-plan.test.sh +90 -0
- package/scripts/spawn-depth-check.sh +5 -4
- package/scripts/spawn-depth-check.test.sh +3 -0
- package/scripts/telemetry.test.sh +8 -0
- package/scripts/transition-label.sh +26 -0
- package/scripts/verify-phase-trail.test.sh +6 -0
|
@@ -85,7 +85,10 @@ ISSUE_LABELS=$(echo "$ISSUE_STATE" | jq -r '[.labels[].name] | join(",")')
|
|
|
85
85
|
- **Stranded-state rule**: `workflow:remediating` must never outlive this run. Every exit that returns `BLOCKED` or `UNFIXABLE` (including exits before Phase M1; except a `RESUME_AT_M7` run, which leaves the issue at `workflow:awaiting-merge` untouched, and except a `deferred-live-run:` exit, which leaves every label untouched: a live run owns the issue) removes `workflow:remediating` and adds `needs-human`, because at that point a human really is needed:
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
|
|
88
|
+
# forge#3567: ensure the label, add first, then remove (a missing label cancels a whole edit)
|
|
89
|
+
gh label create "needs-human" --color "E4E669" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
90
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
91
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --remove-label "workflow:remediating" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
89
92
|
```
|
|
90
93
|
|
|
91
94
|
**Idempotency / resume check** — the paper trail lives on **both** the PR (primary — checked by the orchestrator's item 6.4 dispatch guard) and the linked issue (mirror — keeps `/work-on`'s standard FORGE-annotation trajectory and resume logic consistent with every other phase):
|
|
@@ -212,9 +215,11 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
212
215
|
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
213
216
|
echo "DRY_RUN: would replace needs-human / workflow:remediating with workflow:in-review on issue #{ISSUE_NUMBER}"
|
|
214
217
|
else
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
# forge#3567: ensure label, add first, single-label removes
|
|
219
|
+
gh label create "workflow:in-review" --color "5319E7" --force {GH_FLAG} 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
220
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "workflow:in-review" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
221
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
222
|
+
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --remove-label "workflow:remediating" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
218
223
|
fi
|
|
219
224
|
```
|
|
220
225
|
|
|
@@ -566,6 +571,7 @@ if [ "$(printf '%s' "$PR_LIVE" | jq -r '.state // ""' 2>/dev/null)" = "MERGED" ]
|
|
|
566
571
|
case "$TIER" in
|
|
567
572
|
universal) bash "$TRANSITION_SCRIPT" {ISSUE_NUMBER} {GH_FLAG} merged ;;
|
|
568
573
|
prose|*)
|
|
574
|
+
gh label create "workflow:remediating" --color "C5DEF5" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
569
575
|
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "workflow:merged" --remove-label "workflow:awaiting-merge,needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
570
576
|
;;
|
|
571
577
|
esac
|
|
@@ -730,6 +736,7 @@ if [ "$MERGE_STATE" = "MERGED" ]; then
|
|
|
730
736
|
case "$TIER" in
|
|
731
737
|
universal) bash "$TRANSITION_SCRIPT" {ISSUE_NUMBER} {GH_FLAG} merged ;;
|
|
732
738
|
prose|*)
|
|
739
|
+
gh label create "workflow:remediating" --color "C5DEF5" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
733
740
|
gh issue edit {ISSUE_NUMBER} {GH_FLAG} --add-label "workflow:merged" --remove-label "workflow:awaiting-merge,needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:remediating,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
734
741
|
;;
|
|
735
742
|
esac
|
|
@@ -459,7 +459,9 @@ Use the existing PR number and continue.
|
|
|
459
459
|
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
460
460
|
case "$TIER" in
|
|
461
461
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} in-review ;;
|
|
462
|
-
prose) gh
|
|
462
|
+
prose) gh label create "workflow:remediating" --color "C5DEF5" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
463
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
464
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:investigating" --remove-label "workflow:ready-to-build" --remove-label "workflow:building" --remove-label "workflow:awaiting-merge" --remove-label "workflow:remediating" --remove-label "workflow:merged" --remove-label "workflow:invalid" --remove-label "workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects ;;
|
|
463
465
|
esac
|
|
464
466
|
```
|
|
465
467
|
|
|
@@ -570,10 +572,13 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
570
572
|
|
|
571
573
|
- **Remediation-pending handoff (shared by the ci-gate, in-pr-fix and base-conflict cases below)** <!-- Added: forge#3541 -->: these three cases are autonomous pipeline work, not a human decision, so the issue must NOT carry `needs-human` while remediation is pending. `needs-human` is a human-gate signal (operators alert on it, and orchestrate holds DAG dependents as GATED on it). Mark the handoff with the non-human state `workflow:remediating` instead, and clear any `needs-human` that review-pr Phase 8 already set for the ci-gate refusal. `work-on:remediate` accepts either label (a bare `needs-human` from an older run still works), clears `workflow:remediating` on every exit, and re-asserts `needs-human` only when it cannot fix the PR. Run this block, then return `REVIEW_RESULT: status: NEXT`:
|
|
572
574
|
```bash
|
|
575
|
+
# forge#3567: ensure the label first, add before remove
|
|
576
|
+
gh label create "workflow:remediating" --color "C5DEF5" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
573
577
|
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
574
578
|
case "$TIER" in
|
|
575
579
|
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} remediating ;;
|
|
576
|
-
prose) gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:remediating"
|
|
580
|
+
prose) gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:remediating" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
581
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true ;; # allowlist:check-command-side-effects
|
|
577
582
|
esac
|
|
578
583
|
gh issue edit {NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
579
584
|
gh pr edit {PR_NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
@@ -590,7 +595,10 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
590
595
|
- **Bound already used** (including the re-invocation after a remediation that did not land): first read `max_severity=` from the latest trusted `FORGE:INPR_FIX` marker on the PR (`gh pr view {PR_NUMBER} {GH_FLAG} --json comments`, author-trusted as elsewhere in this phase; absent means MEDIUM). **HIGH/CRITICAL are never waived**: remove `workflow:remediating`, label the issue `needs-human`, leave the PR unmerged and return `REVIEW_RESULT: status: BLOCKED` with blocker "in-pr fix not landed (HIGH/CRITICAL, no waiver)". The `FORGE:INPR_FIX` work order stays on the PR for the human. For MEDIUM, waive the gate for the current head. Remove `needs-human` and `workflow:remediating` (restoring `workflow:in-review`), post `<!-- FORGE:INPR_FIX_WAIVED: head=<current head SHA> -->` on the **PR**, then re-invoke Phase R3 once under the `STALE_REREVIEW` bound. That review files the remaining findings as issues and merges as it would have before this gate. Only if `STALE_REREVIEW` is also exhausted, label the issue `needs-human`.
|
|
591
596
|
```bash
|
|
592
597
|
INPR_WAIVE_HEAD=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq .headRefOid)
|
|
593
|
-
gh
|
|
598
|
+
gh label create "workflow:in-review" --color "5319E7" --force {GH_FLAG} 2>/dev/null || true # allowlist:check-command-side-effects
|
|
599
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:in-review" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
600
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
601
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:remediating" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
594
602
|
gh pr comment {PR_NUMBER} {GH_FLAG} --body "<!-- FORGE:INPR_FIX_WAIVED: head=${INPR_WAIVE_HEAD} -->
|
|
595
603
|
In-PR fix round did not land; the remaining CONFIRMED MEDIUM findings are filed as issues on the next review instead of blocking the merge." # allowlist:check-command-side-effects
|
|
596
604
|
```
|
package/commands/work-on.md
CHANGED
|
@@ -21,7 +21,7 @@ Orchestrator for the full issue lifecycle: investigate → decompose (if needed)
|
|
|
21
21
|
|
|
22
22
|
1. **This file is a router. Every phase runs as a forked sub-skill via `Skill(...)`; the router never executes phase logic itself.** Each phase sub-skill (`work-on:investigate`, `work-on:decompose`, `work-on:build`, `work-on:review`, `work-on:close`, `work-on:remediate`) declares `context: fork`, so Claude Code runs it in an isolated sub-agent context that holds only that phase's spec and the args you pass. The phase re-reads everything else from GitHub/git and returns exactly one `*_RESULT:` block — that block is all the router sees. Do not read phase spec files, do not re-implement a phase step here, and do not "help" a phase by posting its markers or labels. If a skill name does not resolve, STOP with "skill not found" (see Skill Name Resolution) — never run a phase inline.
|
|
23
23
|
|
|
24
|
-
1a. **Dispatching phases are invoked only by this router.** `work-on:review` and `work-on:remediate` spawn sub-agents (the `/review-pr` domain reviewers), and Claude Code only grants the `Agent` tool down to a fixed depth (see Depth Budget). Invoking them from here keeps them one level below the router, where `Agent` works. No phase may invoke them; a phase that needs one returns `status: NEXT` with `next: <phase>`, and the router runs it (Phase 4R). Non-dispatching phases may nest forks freely. <!-- forge#3398 -->
|
|
24
|
+
1a. **Dispatching phases are invoked only by this router.** `work-on:review` and `work-on:remediate` spawn sub-agents (the `/review-pr` domain reviewers), and `work-on:build` spawns the build-time self-review reviewers (B5.6, forge#3971; it degrades to a recorded skip when `Agent` is withheld), and Claude Code only grants the `Agent` tool down to a fixed depth (see Depth Budget). Invoking them from here keeps them one level below the router, where `Agent` works. No phase may invoke them; a phase that needs one returns `status: NEXT` with `next: <phase>`, and the router runs it (Phase 4R). Non-dispatching phases may nest forks freely. <!-- forge#3398 -->
|
|
25
25
|
|
|
26
26
|
2. **Route only on `*_RESULT` blocks and GitHub state.** After each phase returns, apply the routing table for that phase (below). Never infer success from a phase's prose; never continue past a `BLOCKED`/`FAILED` result.
|
|
27
27
|
|
|
@@ -103,11 +103,11 @@ Layers are counted below the main conversation (layer 0). Claude Code grants the
|
|
|
103
103
|
|-----------------------------|-----------|-------------------|
|
|
104
104
|
| 0 / 0 | solo `/work-on` router / `/orchestrate` | orchestrate: one worker per issue |
|
|
105
105
|
| — / 1 | `/work-on` router (orchestrated worker) | no (`Skill` only) |
|
|
106
|
-
| 1 / 2 | every phase invoked by the router, including `work-on:review` and `work-on:remediate` | review/remediate: `/review-pr` domain reviewers |
|
|
106
|
+
| 1 / 2 | every phase invoked by the router, including `work-on:build`, `work-on:review` and `work-on:remediate` | build: self-review reviewers (B5.6); review/remediate: `/review-pr` domain reviewers |
|
|
107
107
|
| 2 / 3 | domain reviewers (leaves); build children (context, architect, implement, validate) | no |
|
|
108
108
|
| 3 / 4 | quality gate (inside validate or review) | no |
|
|
109
109
|
|
|
110
|
-
The only layer constraint that matters is the spawner: `work-on:review` and `work-on:remediate` must sit at layer ≤ 2, which Hard Rule 1a guarantees. The Phase 0A spawn-depth preflight checks the effective depth before any work starts.
|
|
110
|
+
The only layer constraint that matters is the spawner: `work-on:build`, `work-on:review` and `work-on:remediate` must sit at layer ≤ 2, which Hard Rule 1a guarantees. The Phase 0A spawn-depth preflight checks the effective depth before any work starts.
|
|
111
111
|
|
|
112
112
|
### Model and Effort Tiering — What Actually Applies
|
|
113
113
|
|
|
@@ -277,7 +277,7 @@ If detected, dispatch immediately and STOP — do NOT fall through to Phase 0B's
|
|
|
277
277
|
Skill(skill="{FORGE_SKILL_PREFIX}work-on:remediate", args="${REMEDIATE_PR_NUMBER} ${REMEDIATE_ISSUE_FLAG} --repo {GH_REPO} --gh-flag {GH_FLAG}")
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
-
**If `REMEDIATE_RESULT: status: REREVIEW_REQUIRED`** (forge#3240, fallback only since forge#3398: fix pushed, but remediation had no sub-agent dispatch tool, which cannot happen when the Phase 0A spawn-depth preflight passed and remediation is invoked from this router): run the re-review from this top-level session, which has dispatch, after the **claim pre-check** below — `Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="${REMEDIATE_PR_NUMBER} --auto-merge --issue ${ISSUE} --base ${PR_BASE} --gh-flag {GH_FLAG}")` (`ISSUE` and `PR_BASE` from the result and the PR) — never review inline. If it returns `REVIEW_RESULT: status: COMPLETE` (merged), invoke `Skill("{FORGE_SKILL_PREFIX}work-on:close", ...)` as remediate M8 does for `AUTO-LANDED`. If the claim cannot be recorded, the `review-pr` skill does not resolve, cannot dispatch, or returns anything other than `REVIEW_RESULT: status: COMPLETE` while the issue is still at `workflow:in-review` with no `needs-human` (no state anything will advance), apply the **terminal fallback**. Single attempt: no retry loop, no polling. Then STOP. <!-- Added: forge#3406; claim pre-check forge#3413 -->
|
|
280
|
+
**If `REMEDIATE_RESULT: status: REREVIEW_REQUIRED`** (forge#3240, fallback only since forge#3398: fix pushed, but remediation had no sub-agent dispatch tool, which cannot happen when the Phase 0A spawn-depth preflight passed and remediation is invoked from this router): run the re-review from this top-level session, which has dispatch, after the **claim pre-check** below — `Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="${REMEDIATE_PR_NUMBER} --auto-merge --issue ${ISSUE} --base ${PR_BASE} --gh-flag {GH_FLAG}")` (`ISSUE` and `PR_BASE` from the result and the PR) — never review inline. If it returns `REVIEW_RESULT: status: COMPLETE` (merged), invoke `Skill("{FORGE_SKILL_PREFIX}work-on:close", ...)` as remediate M8 does for `AUTO-LANDED`. If the claim cannot be recorded, the `review-pr` skill does not resolve, cannot dispatch, or returns anything other than `REVIEW_RESULT: status: COMPLETE` while the issue is still at `workflow:in-review` with no `needs-human` (no state anything will advance), apply the **terminal fallback**. **Base-conflict mapping (forge#3516)**: if `review-pr` returns `REVIEW_RESULT: status: BLOCKED` with a blocker containing `base-conflict`, it added no label (the base sync is pipeline work), so this router maps it before the claim release: re-read the labels first (closed, already `needs-human`, or no longer `workflow:in-review` means skip and report that state; idempotent); unless `DRY_RUN=true` (then record "would set workflow:remediating"), run `transition-label.sh ${ISSUE} {GH_FLAG} remediating` and re-read the labels to verify `workflow:remediating`. The next router pass (Phase 4R) runs the bounded `FORGE:BASESYNC_REMEDIATION` round via `work-on:remediate`. If the transition fails or is not verified, apply the **terminal fallback** (`needs-human`) and do not release the claim as settled. Never fall through to a manual merge. A `base-conflict` combined with other blockers is reported by the script as a `needs-human` outcome, so this mapping does not fire for it. Single attempt: no retry loop, no polling. Then STOP. <!-- Added: forge#3406; claim pre-check forge#3413 -->
|
|
281
281
|
|
|
282
282
|
**Claim pre-check (forge#3413; one per-head marker shared with the orchestrator)**: the orchestrator's re-review handoff (`commands/orchestrate/phase-4-execution.md` Step 1/2) dedupes on `<!-- FORGE:REREVIEW_DISPATCHED -->`, so the router MUST write the SAME marker before `review-pr`, or the orchestrator re-dispatches after the router already re-reviewed. In order, failing closed (any failure = no dispatch, terminal fallback):
|
|
283
283
|
1. **Labels first**: read the issue's state and labels (`gh issue view ${ISSUE} {GH_FLAG} --json labels,state`). If the issue is closed, already has `needs-human`, or lacks `workflow:in-review`, do NOT dispatch and do NOT apply the fallback (something else already advanced it); STOP.
|
package/docs/CONFIG.md
CHANGED
|
@@ -823,7 +823,11 @@ import forms. `${{ github.* }}` expressions in `run:` scripts are passed to the
|
|
|
823
823
|
substituted into the script text. CircleCI secondary containers run on a per-job docker network and publish only declared
|
|
824
824
|
(or well-known default) ports on 127.0.0.1, never `--network host`.
|
|
825
825
|
Passing results are cached by input hash under `~/.forge/ci-local/results` and reused across build iterations, lanes and
|
|
826
|
-
issues on the same base.
|
|
826
|
+
issues on the same base. An identical check (command, cwd, shell, env, input hash) that appears in several jobs of one invocation
|
|
827
|
+
runs once; the later jobs reuse its result (`cache=dedup`, a failure is shared). Machine slots are granted in FIFO ticket order
|
|
828
|
+
across invocations (`~/.forge/ci-local.lock/queue`; tickets of dead processes are reaped), and a slot is taken only right before a
|
|
829
|
+
step really executes. Job lines, `CI_LOCAL_CHECK`, `CI_LOCAL_RESULT` and the `ci_local` telemetry report `wait_ms` (time queued
|
|
830
|
+
for a slot) separately from `duration_ms` (execution). Ready jobs run narrow/scoped first, then cheapest first (cached durations).
|
|
827
831
|
|
|
828
832
|
**Environment** (all optional): `FORGE_CI_LOCAL_SLOTS` (machine-wide concurrent jobs across all runs, default
|
|
829
833
|
`max(2, min(8, cpus/2))`; slots live under `~/.forge/ci-local.lock` and stale ones are reclaimed), `FORGE_CI_LOCAL_BUDGET_MINUTES`,
|
package/docs/RELEASE-NOTES.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Operator-facing notes for behavior changes that need action or awareness. Newest first.
|
|
4
4
|
|
|
5
|
+
## v1.19.1: build-time self-review, ci-local under concurrency, review roster gates
|
|
6
|
+
|
|
7
|
+
- **Build-time self-review (B5.6, #3971).** After implement, the risk-tier review roster's domain agents review the staged diff, and the builder fixes CONFIRMED/LIKELY findings at MEDIUM or above in one repair round before validate. Small or docs-only diffs skip. Each build that runs it spends extra reviewer tokens, which the official review should save back through fewer findings and remediation rounds. Off switch: `FORGE_SELFREVIEW=off`. Wait budget: `SELFREVIEW_WAIT_SECS` (default 600, max 1800). Telemetry: `build_selfreview`.
|
|
8
|
+
- **ci-local (#4057).** The SEC-3 "every changed source reaches a test" check uses the step's own ecosystem tests, not only the step's candidates, so unrelated single-test steps are skipped instead of widened. Identical checks in several jobs run once per invocation (`cache=dedup`). Machine slots are granted in FIFO ticket order. Job lines and `ci_local_check` carry `wait_ms` apart from execution `duration_ms`. Jobs run shortest first.
|
|
9
|
+
- **Review roster.** The Auth path gate also matches common auth file names (session, token, permission, login, API key) (#3847). The Infra gate covers deploy scripts, entrypoints and `compose.yaml` (#3851). The Security-only fix-PR roster applies only to fix PRs of at most 150 changed lines with a known size; larger or unknown-size fix PRs get the standard roster (#3848).
|
|
10
|
+
- **Merge gate.** `review-finalize.sh verdict` derives OTHER_BLOCKING itself and persists it for `merge-gate`, so a blocking result that a base conflict or purpose regression does not explain can no longer be merged on a missing flag (#3514).
|
|
11
|
+
|
|
12
|
+
Action: none required. Expect more reviewer agents per build (self-review) and per large fix PR (roster cap).
|
|
13
|
+
|
|
14
|
+
## Deferred follow-up lookup no longer duplicates issues (#3606)
|
|
15
|
+
|
|
16
|
+
`scripts/file-deferred-followups.sh` now finds an existing follow-up through the `FORGE:DEFERRED_ISSUE` claim marker it posts on the parent issue (REST comments, read-after-write consistent) before falling back to the lagging search index, searches closed issues as well as open ones (a closed follow-up is reopened), and after creating re-checks once: if a concurrent build filed a lower-numbered trusted follow-up, the items move to it and the newer issue is closed as a duplicate. Failed lookups still exit non-zero (`DEFERRED_LOOKUP=FAILED`).
|
|
17
|
+
|
|
18
|
+
Action: none. Output rows and the `DEFERRED_TELEMETRY` line are unchanged.
|
|
19
|
+
|
|
5
20
|
## Review specs call the prepare and finalize scripts (#3841)
|
|
6
21
|
|
|
7
22
|
`commands/review-pr.md` is now under 100 KB: Phases 1-3B and 6 run through `scripts/review-prepare.sh` and `scripts/review-finalize.sh`, and `commands/review-pr-staging.md` reuses both (`review-prepare.sh --no-delta` in 0C, then `review-finalize.sh extract|classify|file-prepare|file-verify` with `--scope-files` and `--prior-prs` in Phase 7) instead of carrying its own template resolution, slicing, classifier and cap copies. A staging bundle whose API diff is too large is read from `origin/<base>...origin/<head>`. CI now runs `--fence-funcs` over `commands/review-pr*.md` and `--fence-state` over the staging spec. Engine: `close.detectOutcome` reports `merged` only for a CLOSED issue that also carries `workflow:merged` (a stale label on an open issue no longer counts).
|
package/docs/TELEMETRY.md
CHANGED
|
@@ -37,8 +37,8 @@ is dropped. Payload keys are `[A-Za-z0-9_]+` and cannot shadow the shared keys;
|
|
|
37
37
|
| `review_panel` | `review-pr.md` Phase 4 | `agents_selected`, `agents_completed`, `mode` = `FULL` \| `DELTA` \| `BASE_SYNC_ONLY` |
|
|
38
38
|
| `ci_gate` | `scripts/wait-ci-green.sh` | `result` = `PASS` \| `FAIL` \| `ERROR` \| `TIMEOUT` \| `RATE_LIMITED` (rc 4: GitHub throttled or briefly unavailable for the whole window; retryable, never a verdict), `rc`, `checks` |
|
|
39
39
|
| `ci_pending_wait` | the engine (`bin/engine/telemetry-seam.mjs`, one per finished CI-pending wait at the merge gate; `phase` = `review` \| `remediate`) | `pr`, `minutes_waited`, `result` = `green` \| `red` \| `error` \| `timeout`, `polls` (CI reads made), `rate_limit_sleeps` (polls that were rate-limited plus sleeps until the budget reset) |
|
|
40
|
-
| `ci_local` | `scripts/ci-local.sh run` (validate V4.8), one per run | `result` = `PASS` \| `FAIL` \| `DEGRADED`, `jobs_run`, `jobs_deferred`, `jobs_failed`, `duration_ms
|
|
41
|
-
| `ci_local_check` | `scripts/ci-local.sh run`, one per scoped, widened or executed check (at most 20 per run) | `job`, `selector`, `selected`, `total`, `cache` = `hit` \| `miss` \| `na`, `duration_ms`, `widen` (reason or `none`). A remote-CI failure at merge on a check recorded here as narrowed is a selector bug |
|
|
40
|
+
| `ci_local` | `scripts/ci-local.sh run` (validate V4.8), one per run | `result` = `PASS` \| `FAIL` \| `DEGRADED`, `jobs_run`, `jobs_deferred`, `jobs_failed`, `duration_ms` (execution), `wait_ms` (time queued for a machine slot), `fix_iterations` (0 first run, 1 and 2 after fixes), `checks`, `cache_hits`, `narrowed` (checks run on a subset or skipped for no impact), `widened` (checks widened to the full command), `selected` / `total` (summed scope) |
|
|
41
|
+
| `ci_local_check` | `scripts/ci-local.sh run`, one per scoped, widened or executed check (at most 20 per run) | `job`, `selector`, `selected`, `total`, `cache` = `hit` \| `miss` \| `dedup` \| `na`, `duration_ms`, `wait_ms` (job slot wait), `widen` (reason or `none`). A remote-CI failure at merge on a check recorded here as narrowed is a selector bug |
|
|
42
42
|
| `liveness` | `scripts/remediate-liveness.sh` | `decision` = `PROCEED` \| `DEFER` \| `EXHAUSTED`, `why` |
|
|
43
43
|
| `phase_attempt` | the engine (`bin/engine/telemetry-seam.mjs`, one per phase attempt; `phase` = the phase id) | `attempt`, `model`, `turns`, `duration_ms`, `cost_usd`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_creation_tokens` (any value the runner did not report is omitted) |
|
|
44
44
|
| `run_terminal` | the engine (`terminate()`, one per run segment) | `reason` (terminal reason), `code` (see `docs/OUTCOME-CODES.md`), `wall_ms` (this engine process, start to terminal) |
|
package/docs/WORK-ON-RUNTIME.md
CHANGED
|
@@ -46,7 +46,8 @@ L3) works, which is why most PRs are reviewed normally.
|
|
|
46
46
|
## 3. Design rules
|
|
47
47
|
|
|
48
48
|
**R1 — Dispatching phases are invoked only by the router.** A *dispatching* phase is one that spawns
|
|
49
|
-
`Agent(...)` sub-agents directly or through `/review-pr`: `work-on:review
|
|
49
|
+
`Agent(...)` sub-agents directly or through `/review-pr`: `work-on:review`, `work-on:remediate` and, since forge#3971,
|
|
50
|
+
`work-on:build` (its B5.6 self-review dispatches the risk-tier roster's domain reviewers on the staged diff).
|
|
50
51
|
Only the router may invoke them, so they always run at router layer + 1 (L2 under `/orchestrate`,
|
|
51
52
|
L1 solo). When another phase needs one, it returns a handoff (`status: NEXT`, `next: <phase>`) and
|
|
52
53
|
the router invokes it. Concretely: review returns `next: remediate` instead of calling remediation.
|
|
@@ -59,7 +60,8 @@ re-runs the missing phases. `/review-pr` (Phase 8, via `scripts/review-finalize.
|
|
|
59
60
|
|
|
60
61
|
**R2 — Non-dispatching forks may nest.** Phases and children that never spawn sub-agents may invoke
|
|
61
62
|
further forks at any depth (F5): build → context/architect/implement/validate → quality-gate,
|
|
62
|
-
review → quality-gate / missing-phase re-dispatch, remediate → close.
|
|
63
|
+
review → quality-gate / missing-phase re-dispatch, remediate → close. The build children never spawn: the
|
|
64
|
+
self-review reviewers are spawned by `work-on:build` itself, and its repair round is an `implement` fork. Forking them keeps each
|
|
63
65
|
context small. If one of them ever needs to spawn sub-agents, it becomes a dispatching phase and
|
|
64
66
|
R1 applies.
|
|
65
67
|
|
|
@@ -99,13 +101,15 @@ phase hosting `/review-pr` cannot wait on a notification. Therefore:
|
|
|
99
101
|
| Skill | Fork | Dispatches sub-agents | Invoked by | Layer solo / orchestrated |
|
|
100
102
|
|-------|------|-----------------------|------------|---------------------------|
|
|
101
103
|
| `work-on` (router) | — | no | user / orchestrator worker | 0 / 1 |
|
|
102
|
-
| `work-on:investigate`, `work-on:decompose`, `work-on:
|
|
104
|
+
| `work-on:investigate`, `work-on:decompose`, `work-on:close` | yes | no | router | 1 / 2 |
|
|
105
|
+
| `work-on:build` (B5.6 self-review) | yes | **yes** (self-review reviewers; degrades to a recorded skip when `Agent` is withheld) | router only (R1) | 1 / 2 |
|
|
103
106
|
| `work-on:build:{context,architect,implement,validate}` | yes | no | `work-on:build` (R2) | 2 / 3 |
|
|
104
107
|
| `quality-gate` | yes | no | validate (R2) | 3 / 4 |
|
|
105
108
|
| `quality-gate` (stale-review re-gate) | yes | no | review (R2) | 2 / 3 |
|
|
106
109
|
| `work-on:review` → `/review-pr` | yes | **yes** | router only (R1) | 1 / 2 |
|
|
107
110
|
| `work-on:remediate` → `/review-pr` | yes | **yes** | router only (R1) | 1 / 2 |
|
|
108
111
|
| `/review-pr` domain reviewers | Agent | no (leaves) | `/review-pr` | 2 / 3 |
|
|
112
|
+
| build self-review reviewers (`FORGE:SELFREVIEW-AGENT:*`) | Agent | no (leaves) | `work-on:build` B5.6 | 2 / 3 |
|
|
109
113
|
|
|
110
114
|
`/orchestrate` Step 4B item 6.4 dispatches a worker that runs `work-on <pr> --remediate`: worker L1,
|
|
111
115
|
remediate L2, reviewers L3, same as the table.
|
|
@@ -114,6 +118,10 @@ remediate L2, reviewers L3, same as the table.
|
|
|
114
118
|
|
|
115
119
|
`work-on:review` merges `origin/<base>` into the branch before the first push, so a PR is never born stale and a conflict is resolved while the build context is warm. Hand-maintained append-only registries (test lists, cron targets) are the usual conflict source; `scripts/detect-append-registries.sh` ranks them from history and proves a `.gitattributes` `merge=union` entry safe (`--check-union`). Union is allowed only for line-independent files, never YAML/JSON maps or ordered lists (see `.gitattributes`). `scripts/review-delta.sh` still treats a hand-resolved merge as not `BASE_SYNC_ONLY`, but accepts a merge whose tree is exactly what a mechanical `git merge-tree` of the same parents produces (a clean or union merge), so union registries take the no-panel fast path.
|
|
116
120
|
|
|
121
|
+
### Build-time self-review (forge#3971)
|
|
122
|
+
|
|
123
|
+
`work-on:build` Phase B5.6 runs the risk-tier roster (`scripts/selfreview-plan.sh`, which calls `scripts/review-roster.sh` on the staged diff) before validate, with the same personas as `/review-pr` in SELF-REVIEW MODE (`commands/review-pr-agents/protocols.md`). It is not the official review: reviewers post `FORGE:SELFREVIEW-AGENT:{domain}` issue comments pinned to the staged tree (`Tree:`), never `FORGE:REVIEW-AGENT`, and `review-finalize.sh` / `verify-phase-trail.sh` ignore them. Like R6, completion is read from GitHub with a bounded wait, because reviewers spawned inside a fork report to the root session. A build at a layer where `Agent` is withheld, or any self-review failure, degrades to a recorded skip (`build_selfreview skipped=<reason>`) and never blocks the build.
|
|
124
|
+
|
|
117
125
|
## 5. Router routing
|
|
118
126
|
|
|
119
127
|
| Phase result | Router action |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|
|
@@ -15,7 +15,7 @@ ok() { PASS=$((PASS+1)); echo "ok - $1"; }
|
|
|
15
15
|
bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
|
|
16
16
|
eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (got '$2', want '$3')"; fi; }
|
|
17
17
|
|
|
18
|
-
SELECT_JQ='[.[][] | select((.body // "") | startswith("<!-- FORGE:ARCHITECT -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:ARCHITECT:COMPLETE -->")), other: (.body | test("
|
|
18
|
+
SELECT_JQ='[.[][] | select((.body // "") | startswith("<!-- FORGE:ARCHITECT -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:ARCHITECT:COMPLETE -->")), other: (.body | test("\\n[ \\t]*<!-- FORGE:(INVESTIGATOR|CONTEXT|CONTRACT|BUILDER)"))}]'
|
|
19
19
|
DELETE_JQ='[.[] | select(.complete | not)] | if length == 1 and (.[0].other | not) then .[0].id else empty end'
|
|
20
20
|
|
|
21
21
|
# c <id> <assoc> <type> <body>
|
|
@@ -28,7 +28,7 @@ PLAN=$'<!-- FORGE:ARCHITECT -->\n## Plan\n\n<!-- FORGE:ARCHITECT:COMPLETE -->'
|
|
|
28
28
|
PARTIAL=$'<!-- FORGE:ARCHITECT -->\n## Plan (in progress)'
|
|
29
29
|
INVEST=$'<!-- FORGE:INVESTIGATOR -->\nThe architect phase posts FORGE:ARCHITECT and FORGE:ARCHITECT:COMPLETE later.'
|
|
30
30
|
CTX=$'<!-- FORGE:CONTEXT -->\nmentions <!-- FORGE:ARCHITECT -->'
|
|
31
|
-
MIXED=$'<!-- FORGE:ARCHITECT -->\nquoted
|
|
31
|
+
MIXED=$'<!-- FORGE:ARCHITECT -->\nquoted:\n<!-- FORGE:INVESTIGATOR -->'
|
|
32
32
|
|
|
33
33
|
eq "INVESTIGATOR mentioning FORGE:ARCHITECT is not selected" "$(arch "$(c 1 NONE Bot "$INVEST")" | jq length)" 0
|
|
34
34
|
eq "INVESTIGATOR mentioning FORGE:ARCHITECT is never deleted" "$(del_id "$(c 1 NONE Bot "$INVEST")")" ""
|
|
@@ -39,10 +39,19 @@ eq "single partial plan -> delete its id" "$(del_id "$(c 4 NONE Bot "$PARTIAL")"
|
|
|
39
39
|
eq "two partial plans -> delete nothing" "$(del_id "$(c 4 NONE Bot "$PARTIAL")"$'\n'"$(c 5 NONE Bot "$PARTIAL")")" ""
|
|
40
40
|
eq "untrusted author partial ignored" "$(del_id "$(c 6 NONE User "$PARTIAL")")" ""
|
|
41
41
|
eq "untrusted author complete ignored" "$(n_complete "$(c 6 NONE User "$PLAN")")" 0
|
|
42
|
-
eq "partial
|
|
42
|
+
eq "partial with another FORGE marker at a line start is not deleted" "$(del_id "$(c 7 NONE Bot "$MIXED")")" ""
|
|
43
43
|
eq "investigator + partial plan -> only the plan id" "$(del_id "$(c 1 NONE Bot "$INVEST")"$'\n'"$(c 8 NONE Bot "$PARTIAL")")" 8
|
|
44
44
|
eq "paginated concatenated arrays" "$(printf '[%s]\n[%s]' "$(c 1 NONE Bot "$INVEST")" "$(c 9 NONE Bot "$PLAN")" | jq -s "$SELECT_JQ" | jq '[.[] | select(.complete)] | length')" 1
|
|
45
45
|
|
|
46
|
+
INLINE=$'<!-- FORGE:ARCHITECT -->\n## Approach\nFollowed the plan in `<!-- FORGE:BUILDER -->` and <!-- FORGE:INVESTIGATOR --> notes.'
|
|
47
|
+
CRLF=$'<!-- FORGE:ARCHITECT -->\r\n<!-- FORGE:BUILDER -->\r\nbody'
|
|
48
|
+
eq "partial quoting a marker inline is picked and deleted e1_inline_picked" "$(del_id "$(c 11 NONE Bot "$INLINE")")" 11
|
|
49
|
+
eq "inline quoting partial is selected as non-other e1_inline_not_other" "$(arch "$(c 11 NONE Bot "$INLINE")" | jq -r '.[0].other')" false
|
|
50
|
+
eq "marker at the start of a later line is refused e2_line_start_refused" "$(arch "$(c 12 NONE Bot "$MIXED")" | jq -r '.[0].other')" true
|
|
51
|
+
eq "own first-line marker never counts as other e4_own_marker_not_other" "$(arch "$(c 13 NONE Bot "$PARTIAL")" | jq -r '.[0].other')" false
|
|
52
|
+
eq "two inline-quoting partials still delete nothing e3_two_partials_none" "$(del_id "$(c 11 NONE Bot "$INLINE")"$'\n'"$(c 14 NONE Bot "$INLINE")")" ""
|
|
53
|
+
eq "CRLF body with a marker line is still refused e5_crlf_refused" "$(del_id "$(c 15 NONE Bot "$CRLF")")" ""
|
|
54
|
+
|
|
46
55
|
# Spec drift guard: the spec carries this exact predicate and no unanchored selector.
|
|
47
56
|
! grep -qF 'contains("FORGE:ARCHITECT")' "$SPEC" && ok "architect.md has no unanchored contains(FORGE:ARCHITECT)" || bad "architect.md still uses unanchored contains"
|
|
48
57
|
grep -qF 'startswith("<!-- FORGE:ARCHITECT -->")' "$SPEC" && ok "architect.md anchors with startswith" || bad "architect.md lacks the startswith anchor"
|
|
@@ -173,12 +173,14 @@ function readForgeYaml(repoPath) {
|
|
|
173
173
|
const yamlPath = join(repoPath, 'forge.yaml');
|
|
174
174
|
if (!existsSync(yamlPath)) return null;
|
|
175
175
|
try {
|
|
176
|
-
const result = spawnSync('yq', ['-r', '.project.owner
|
|
176
|
+
const result = spawnSync('yq', ['-r', '.project.owner, .project.repo', yamlPath], {
|
|
177
177
|
encoding: 'utf8',
|
|
178
178
|
timeout: 5000,
|
|
179
179
|
});
|
|
180
|
-
if (result.status === 0
|
|
181
|
-
|
|
180
|
+
if (result.status === 0) {
|
|
181
|
+
const [owner, name] = result.stdout.split('\n').map(l => l.trim());
|
|
182
|
+
// Both halves must be real values: a partial forge.yaml must not yield "/" or "null/x"
|
|
183
|
+
if (owner && name && owner !== 'null' && name !== 'null') return `${owner}/${name}`;
|
|
182
184
|
}
|
|
183
185
|
} catch (_) {
|
|
184
186
|
// ignore
|
|
@@ -1576,7 +1578,7 @@ async function main() {
|
|
|
1576
1578
|
repo = readForgeYaml(repoPath);
|
|
1577
1579
|
}
|
|
1578
1580
|
if (!repo) {
|
|
1579
|
-
process.stderr.write('[ledger] ERROR: Cannot determine GitHub repo. Pass --repo owner/repo or ensure forge.yaml
|
|
1581
|
+
process.stderr.write('[ledger] ERROR: Cannot determine GitHub repo. Pass --repo owner/repo or ensure forge.yaml has project.owner and project.repo (yq required).\n');
|
|
1580
1582
|
process.exit(1);
|
|
1581
1583
|
}
|
|
1582
1584
|
|
|
@@ -1760,6 +1762,7 @@ async function main() {
|
|
|
1760
1762
|
}
|
|
1761
1763
|
|
|
1762
1764
|
main().catch(e => {
|
|
1763
|
-
process.
|
|
1765
|
+
const showStack = process.argv.includes('--verbose') || !!process.env.FORGE_DEBUG;
|
|
1766
|
+
process.stderr.write(`[ledger] FATAL: ${e && e.message ? e.message : e}\n${showStack && e && e.stack ? e.stack + '\n' : ''}`);
|
|
1764
1767
|
process.exit(1);
|
|
1765
1768
|
});
|
|
@@ -14,7 +14,7 @@ ok() { PASS=$((PASS+1)); echo "ok - $1"; }
|
|
|
14
14
|
bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
|
|
15
15
|
eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (got '$2', want '$3')"; fi; }
|
|
16
16
|
|
|
17
|
-
SELECT_JQ='[.[][] | select((.body // "") | startswith("<!-- FORGE:BUILDER -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:BUILDER:COMPLETE -->")), other: (.body | test("
|
|
17
|
+
SELECT_JQ='[.[][] | select((.body // "") | startswith("<!-- FORGE:BUILDER -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:BUILDER:COMPLETE -->")), other: (.body | test("\\n[ \\t]*<!-- FORGE:(INVESTIGATOR|CONTEXT|CONTRACT|ARCHITECT)"))}]'
|
|
18
18
|
DELETE_JQ='[.[] | select(.complete | not)] | if length == 1 and (.[0].other | not) then .[0].id else empty end'
|
|
19
19
|
|
|
20
20
|
# c <id> <assoc> <type> <body>
|
|
@@ -27,7 +27,7 @@ PLAN=$'<!-- FORGE:BUILDER -->\n## Implementation\n\n<!-- FORGE:BUILDER:COMPLETE
|
|
|
27
27
|
PARTIAL=$'<!-- FORGE:BUILDER -->\n## Implementation (in progress)'
|
|
28
28
|
INVEST=$'<!-- FORGE:INVESTIGATOR -->\nThe build phase later posts FORGE:BUILDER and FORGE:BUILDER:COMPLETE.'
|
|
29
29
|
CTX=$'<!-- FORGE:CONTEXT -->\nmentions <!-- FORGE:BUILDER -->'
|
|
30
|
-
MIXED=$'<!-- FORGE:BUILDER -->\nquoted
|
|
30
|
+
MIXED=$'<!-- FORGE:BUILDER -->\nquoted:\n<!-- FORGE:INVESTIGATOR -->'
|
|
31
31
|
|
|
32
32
|
eq "INVESTIGATOR mentioning FORGE:BUILDER is not selected" "$(arch "$(c 1 NONE Bot "$INVEST")" | jq length)" 0
|
|
33
33
|
eq "INVESTIGATOR mentioning FORGE:BUILDER is never deleted" "$(del_id "$(c 1 NONE Bot "$INVEST")")" ""
|
|
@@ -38,7 +38,7 @@ eq "single partial comment -> delete its id" "$(del_id "$(c 4 NONE Bot "$PARTIAL
|
|
|
38
38
|
eq "two partial comments -> delete nothing" "$(del_id "$(c 4 NONE Bot "$PARTIAL")"$'\n'"$(c 5 NONE Bot "$PARTIAL")")" ""
|
|
39
39
|
eq "untrusted author partial ignored" "$(del_id "$(c 6 NONE User "$PARTIAL")")" ""
|
|
40
40
|
eq "untrusted author complete ignored" "$(n_complete "$(c 6 NONE User "$PLAN")")" 0
|
|
41
|
-
eq "partial
|
|
41
|
+
eq "partial with another FORGE marker at a line start is not deleted" "$(del_id "$(c 7 NONE Bot "$MIXED")")" ""
|
|
42
42
|
eq "investigator + partial comment -> only the comment id" "$(del_id "$(c 1 NONE Bot "$INVEST")"$'\n'"$(c 8 NONE Bot "$PARTIAL")")" 8
|
|
43
43
|
eq "paginated concatenated arrays" "$(printf '[%s]\n[%s]' "$(c 1 NONE Bot "$INVEST")" "$(c 9 NONE Bot "$PLAN")" | jq -s "$SELECT_JQ" | jq '[.[] | select(.complete)] | length')" 1
|
|
44
44
|
|
|
@@ -46,6 +46,15 @@ ARCHC=$'<!-- FORGE:ARCHITECT -->\nplan; the builder will post <!-- FORGE:BUILDER
|
|
|
46
46
|
eq "ARCHITECT mentioning the marker is not selected" "$(arch "$(c 10 NONE Bot "$ARCHC")" | jq length)" 0
|
|
47
47
|
eq "unparsable input fails closed" "$(printf 'not json' | jq -s "$SELECT_JQ" 2>/dev/null | jq -r "$DELETE_JQ" 2>/dev/null)" ""
|
|
48
48
|
|
|
49
|
+
INLINE=$'<!-- FORGE:BUILDER -->\n## Approach\nFollowed the plan in `<!-- FORGE:ARCHITECT -->` and <!-- FORGE:INVESTIGATOR --> notes.'
|
|
50
|
+
CRLF=$'<!-- FORGE:BUILDER -->\r\n<!-- FORGE:ARCHITECT -->\r\nbody'
|
|
51
|
+
eq "partial quoting a marker inline is picked and deleted e1_inline_picked" "$(del_id "$(c 11 NONE Bot "$INLINE")")" 11
|
|
52
|
+
eq "inline quoting partial is selected as non-other e1_inline_not_other" "$(arch "$(c 11 NONE Bot "$INLINE")" | jq -r '.[0].other')" false
|
|
53
|
+
eq "marker at the start of a later line is refused e2_line_start_refused" "$(arch "$(c 12 NONE Bot "$MIXED")" | jq -r '.[0].other')" true
|
|
54
|
+
eq "own first-line marker never counts as other e4_own_marker_not_other" "$(arch "$(c 13 NONE Bot "$PARTIAL")" | jq -r '.[0].other')" false
|
|
55
|
+
eq "two inline-quoting partials still delete nothing e3_two_partials_none" "$(del_id "$(c 11 NONE Bot "$INLINE")"$'\n'"$(c 14 NONE Bot "$INLINE")")" ""
|
|
56
|
+
eq "CRLF body with a marker line is still refused e5_crlf_refused" "$(del_id "$(c 15 NONE Bot "$CRLF")")" ""
|
|
57
|
+
|
|
49
58
|
# Spec drift guard: every mutating site carries this exact predicate and no unanchored selector.
|
|
50
59
|
for f in commands/work-on/build/implement.md commands/work-on/build.md commands/work-on/build/validate.md; do
|
|
51
60
|
SPEC="$ROOT/$f"
|
package/scripts/calibration.mjs
CHANGED
|
@@ -143,10 +143,13 @@ export function readForgeYaml(repoPath) {
|
|
|
143
143
|
const configPath = join(repoPath, 'forge.yaml');
|
|
144
144
|
if (!existsSync(configPath)) return null;
|
|
145
145
|
try {
|
|
146
|
-
const result = spawnSync('yq', ['-r', '.project.owner
|
|
146
|
+
const result = spawnSync('yq', ['-r', '.project.owner, .project.repo', configPath], {
|
|
147
147
|
encoding: 'utf8', timeout: 5000,
|
|
148
148
|
});
|
|
149
|
-
if (result.status === 0
|
|
149
|
+
if (result.status === 0) {
|
|
150
|
+
const [owner, name] = result.stdout.split('\n').map(l => l.trim());
|
|
151
|
+
if (owner && name && owner !== 'null' && name !== 'null') return `${owner}/${name}`;
|
|
152
|
+
}
|
|
150
153
|
} catch (_) { /* ignore */ }
|
|
151
154
|
return null;
|
|
152
155
|
}
|
package/scripts/ci-local/lib.mjs
CHANGED
|
@@ -29,10 +29,18 @@ export function commandExists(name) {
|
|
|
29
29
|
return r.code === 0;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// Memoized per process: stepKey runs for every step (twice: cost ordering and execution), and an unmemoized
|
|
33
|
+
// `<tool> --version` is a synchronous spawn that blocks the scheduler and burns the budget (#4068 review CONC-1).
|
|
34
|
+
const TOOL_VERSIONS = new Map();
|
|
32
35
|
export function toolVersion(name) {
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
if (TOOL_VERSIONS.has(name)) return TOOL_VERSIONS.get(name);
|
|
37
|
+
let v = '';
|
|
38
|
+
if (/^[A-Za-z0-9._+-]+$/.test(name) && commandExists(name)) {
|
|
39
|
+
const r = run(name, ['--version'], { timeout: 5000 });
|
|
40
|
+
v = (r.out || r.err).split('\n')[0].slice(0, 120);
|
|
41
|
+
}
|
|
42
|
+
TOOL_VERSIONS.set(name, v);
|
|
43
|
+
return v;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
export function hostClass() {
|