forgedock 1.16.1 → 1.17.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/bin/engine/outcome-codes.mjs +92 -0
- package/bin/engine/phases.mjs +105 -25
- package/bin/engine/projector.mjs +48 -1
- package/bin/engine/runlog.mjs +13 -5
- package/bin/engine/telemetry-seam.mjs +62 -0
- package/bin/engine/telemetry-seam.test.mjs +60 -0
- package/bin/engine/telemetry.mjs +148 -0
- package/bin/engine/telemetry.test.mjs +120 -0
- package/bin/engine-cli.mjs +12 -1
- package/bin/engine.mjs +151 -39
- package/bin/forgedock.mjs +7 -1
- package/bin/hooks/interactive-engine.mjs +10 -0
- package/bin/hooks/pre-tool-use.mjs +187 -0
- package/bin/runner.mjs +7 -0
- package/bin/stats/aggregate.mjs +231 -0
- package/bin/stats/cache.mjs +40 -0
- package/bin/stats/github-source.mjs +140 -0
- package/bin/stats/index.mjs +154 -0
- package/bin/stats/local-source.mjs +79 -0
- package/bin/stats/package.json +6 -0
- package/bin/stats/render.mjs +82 -0
- package/bin/stats/stats.test.mjs +224 -0
- package/commands/cleanup.md +83 -2
- package/commands/deploy-info.md +8 -4
- package/commands/orchestrate/phase-1-resolve.md +6 -3
- package/commands/orchestrate/phase-4-execution.md +25 -10
- package/commands/orchestrate/phase-5-cleanup.md +18 -2
- package/commands/pipeline-health/phase-1-context.md +2 -0
- package/commands/pipeline-health/phase-2-metrics.md +12 -5
- package/commands/pipeline-status.md +21 -1
- package/commands/quality-gate.md +4 -4
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents.md +10 -2
- package/commands/review-pr-staging.md +134 -57
- package/commands/review-pr.md +135 -119
- package/commands/work-on/build/implement.md +26 -5
- package/commands/work-on/build/validate.md +5 -4
- package/commands/work-on/build.md +143 -80
- package/commands/work-on/close.md +19 -8
- package/commands/work-on/decompose.md +50 -8
- package/commands/work-on/investigate.md +14 -7
- package/commands/work-on/remediate.md +40 -14
- package/commands/work-on/review.md +22 -5
- package/commands/work-on.md +6 -5
- package/docs/CONFIG.md +37 -2
- package/docs/OUTCOME-CODES.md +48 -0
- package/docs/RELEASE-NOTES.md +12 -0
- package/docs/STATS.md +62 -0
- package/docs/TELEMETRY.md +65 -0
- package/docs/WORK-ON-RUNTIME.md +14 -1
- package/docs/site/command-reference.md +1 -1
- package/docs/spec/forge-protocol-v1.md +1 -1
- package/docs/spec/label-state-machine.md +1 -1
- package/package.json +3 -2
- package/scripts/builder-resume-match.test.sh +58 -0
- package/scripts/cap-findings.sh +110 -0
- package/scripts/cap-findings.test.sh +130 -0
- package/scripts/check-spec-markers.sh +4 -0
- package/scripts/classify-finding.sh +65 -16
- package/scripts/classify-finding.test.sh +43 -1
- package/scripts/classify-grep-exit.sh +20 -0
- package/scripts/classify-grep-exit.test.sh +46 -0
- package/scripts/close-parent-c3.test.sh +54 -0
- package/scripts/cost-report.sh +146 -0
- package/scripts/cost-report.test.sh +80 -0
- package/scripts/diff-size.sh +41 -3
- package/scripts/diff-size.test.sh +47 -0
- package/scripts/doctor-pipeline-state.sh +43 -0
- package/scripts/file-deferred-followups.sh +159 -0
- package/scripts/file-deferred-followups.test.sh +106 -0
- package/scripts/fixtures/cost-report/a-merged1.jsonl +6 -0
- package/scripts/fixtures/cost-report/a-merged2-remediated.jsonl +7 -0
- package/scripts/fixtures/cost-report/a-needs-human.jsonl +4 -0
- package/scripts/fixtures/cost-report/b-merged.jsonl +3 -0
- package/scripts/fixtures/cost-report/c-merged-attempt-usage.jsonl +8 -0
- package/scripts/fixtures/cost-report/hydrated-no-usage.jsonl +3 -0
- package/scripts/fixtures/cost-report/old-log-no-version.jsonl +3 -0
- package/scripts/fixtures/cost-report/unfinished-no-terminal.jsonl +2 -0
- package/scripts/fixtures/cost-report/unfinished-reopened.jsonl +5 -0
- package/scripts/forge-root.test.sh +18 -5
- package/scripts/lib/fence-bootstrap.sh +4 -3
- package/scripts/lib/forge-resolve.sh +13 -4
- package/scripts/lib/pr-issue-number.sh +18 -0
- package/scripts/lib/review-template-resolve.sh +41 -0
- package/scripts/lib/telemetry.sh +168 -0
- package/scripts/remediate-liveness.sh +132 -0
- package/scripts/review-acceptance-gate.sh +141 -0
- package/scripts/review-acceptance-gate.test.sh +103 -0
- package/scripts/review-pr-domain-map.test.sh +8 -3
- package/scripts/review-pr-gate2.test.sh +15 -0
- package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
- package/scripts/review-pr-staging-scope.test.sh +65 -5
- package/scripts/review-prepare.sh +517 -0
- package/scripts/review-prepare.test.sh +270 -0
- package/scripts/review-roster.sh +172 -0
- package/scripts/review-roster.test.sh +163 -0
- package/scripts/run-shell-tests.sh +3 -0
- package/scripts/telemetry.test.sh +106 -0
- package/scripts/wait-ci-green.sh +8 -1
- package/templates/forge.yaml.minimal +9 -0
package/commands/cleanup.md
CHANGED
|
@@ -10,7 +10,7 @@ install: extras
|
|
|
10
10
|
|
|
11
11
|
**Input**: $ARGUMENTS
|
|
12
12
|
|
|
13
|
-
Scan the entire development environment for rot and fix it. This is a maintenance command — run periodically or after large orchestration batches. It covers 6 domains: stale labels, orphaned issues, worktree/branch pruning, milestone hygiene, and Project board sync.
|
|
13
|
+
Scan the entire development environment for rot and fix it. This is a maintenance command — run periodically or after large orchestration batches. It covers 6 domains: stale labels, orphaned issues and dead claims boards, worktree/branch pruning, milestone hygiene, and Project board sync.
|
|
14
14
|
|
|
15
15
|
**NEVER use plan mode (EnterPlanMode)** — it breaks execution context.
|
|
16
16
|
|
|
@@ -46,7 +46,7 @@ All `{GH_REPO}`, `{GH_FLAG}`, `{REPO_PATH}`, `{STAGING_BRANCH}`, `{PROJECT_BOARD
|
|
|
46
46
|
| Input | Action |
|
|
47
47
|
|-------|--------|
|
|
48
48
|
| `labels` or empty | Fix stale/missing workflow labels on closed issues; detect OPEN issues stuck in intermediate states (Phase 1D) |
|
|
49
|
-
| `orphans` | Close open issues whose PRs are already merged |
|
|
49
|
+
| `orphans` | Close open issues whose PRs are already merged (Phase 2A/2B), and close dead orchestrate claims boards (Phase 2C) |
|
|
50
50
|
| `branches` | Prune worktrees and remote branches for merged PRs |
|
|
51
51
|
| `milestones` | Report milestones with 0 open issues (advisory — never closes) |
|
|
52
52
|
| `board` | Sync closed issues to Project board with correct terminal state |
|
|
@@ -234,6 +234,82 @@ gh issue edit $NUM {GH_FLAG} --remove-label "workflow:in-review,workflow:remedia
|
|
|
234
234
|
|
|
235
235
|
Also check open issues with `workflow:building` — same pattern (search for merged PRs referencing them).
|
|
236
236
|
|
|
237
|
+
### 2C: Close dead orchestrate claims boards <!-- Added: forge#3699 -->
|
|
238
|
+
|
|
239
|
+
Each `/orchestrate` batch creates one claims board (`orchestrate: claims board for batch <id>`, label `automation`) with no PR of its own, so 2A/2B never match it. `/orchestrate` Phase 5 Step 5C closes the board on a clean finish; batches that end by compaction, freeze, crash or a fresh-context resume never reach it. This sweep is the durable safety net. Runs for `orphans` and `all`; respects `DRY_RUN`.
|
|
240
|
+
|
|
241
|
+
A board is closed only when **all** identity checks pass and **one** closing condition holds:
|
|
242
|
+
|
|
243
|
+
- Identity: title starts with `orchestrate: claims board for batch`, body contains `<!-- FORGE:COORD_ISSUE -->`, and the author is trusted: `author_association` `OWNER`, `MEMBER` or `COLLABORATOR`, or a Bot account (the pipeline's GitHub App, reported as `CONTRIBUTOR`). A look-alike issue from an outside account is never closed.
|
|
244
|
+
- Close when the last `FORGE:LEASE*` comment is a `FORGE:LEASE_RELEASED`.
|
|
245
|
+
- Close when the last `FORGE:LEASE*`/`FORGE:CLAIM*` comment is older than `BOARD_STALE_HOURS` (default 6, far above the 15 minute lease TTL).
|
|
246
|
+
- Close when there is no lease/claim comment and the issue itself is older than `BOARD_STALE_HOURS`.
|
|
247
|
+
- Never close a board whose last lease event is an unreleased `FORGE:LEASE` younger than `BOARD_STALE_HOURS`.
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
BOARD_STALE_HOURS=${BOARD_STALE_HOURS:-6}
|
|
251
|
+
BOARD_CUTOFF=$(date -u -d "${BOARD_STALE_HOURS} hours ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
|
|
252
|
+
|| date -u -v-${BOARD_STALE_HOURS}H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
|
|
253
|
+
|| echo "")
|
|
254
|
+
BOARDS_CLOSED=0
|
|
255
|
+
|
|
256
|
+
if [ -z "$BOARD_CUTOFF" ]; then
|
|
257
|
+
echo "WARNING: cannot compute BOARD_STALE_HOURS cutoff — only released boards will be closed"
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
# REST (not `gh issue list`): it exposes author_association and user.type. Boards are created by the
|
|
261
|
+
# pipeline's GitHub App, which GitHub reports as CONTRIBUTOR, so trust is the same predicate as
|
|
262
|
+
# scripts/trusted-comments.sh: OWNER/MEMBER/COLLABORATOR or a Bot account.
|
|
263
|
+
BOARDS=$(gh api --paginate "repos/{GH_REPO}/issues?state=open&labels=automation&per_page=100" 2>/dev/null \
|
|
264
|
+
| jq -rs '.[][] | select(.pull_request == null)
|
|
265
|
+
| select(((.title // "") | startswith("orchestrate: claims board for batch"))
|
|
266
|
+
and ((.body // "") | contains("<!-- FORGE:COORD_ISSUE -->"))
|
|
267
|
+
and (((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR")) or ((.user.type // "") == "Bot")))
|
|
268
|
+
| "\(.number)\t\(.created_at)"' 2>/dev/null || echo "")
|
|
269
|
+
|
|
270
|
+
while IFS=$'\t' read -r BNUM BCREATED; do
|
|
271
|
+
[ -z "$BNUM" ] && continue
|
|
272
|
+
|
|
273
|
+
# --paginate emits one array per page: slurp (jq -s) and flatten BEFORE taking `last`,
|
|
274
|
+
# otherwise a board with >30 comments reads a stale event from an earlier page.
|
|
275
|
+
LAST_LEASE=$(gh api --paginate "repos/{GH_REPO}/issues/${BNUM}/comments" 2>/dev/null \
|
|
276
|
+
| jq -s '[.[][] | select(.body | contains("FORGE:LEASE"))] | last // empty
|
|
277
|
+
| {released: (.body | contains("FORGE:LEASE_RELEASED")), at: .created_at}' 2>/dev/null || echo "")
|
|
278
|
+
LAST_ACTIVITY=$(gh api --paginate "repos/{GH_REPO}/issues/${BNUM}/comments" 2>/dev/null \
|
|
279
|
+
| jq -rs '[.[][] | select(.body | contains("FORGE:LEASE") or contains("FORGE:CLAIM"))] | last // empty | .created_at' 2>/dev/null || echo "")
|
|
280
|
+
|
|
281
|
+
REASON=""
|
|
282
|
+
if [ -n "$LAST_LEASE" ] && [ "$(echo "$LAST_LEASE" | jq -r '.released')" = "true" ]; then
|
|
283
|
+
REASON="last lease event is FORGE:LEASE_RELEASED"
|
|
284
|
+
elif [ -n "$BOARD_CUTOFF" ]; then
|
|
285
|
+
# ISO-8601 UTC timestamps compare correctly as strings
|
|
286
|
+
LAST_SEEN="${LAST_ACTIVITY:-$BCREATED}"
|
|
287
|
+
if [ -n "$LAST_SEEN" ] && [ "$LAST_SEEN" \< "$BOARD_CUTOFF" ]; then
|
|
288
|
+
REASON="no lease/claim activity since ${LAST_SEEN} (older than ${BOARD_STALE_HOURS}h)"
|
|
289
|
+
fi
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
if [ -z "$REASON" ]; then
|
|
293
|
+
echo "KEEP: board #$BNUM has a live or recent lease"
|
|
294
|
+
continue
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
298
|
+
echo "DRY-RUN: would close claims board #$BNUM — $REASON"
|
|
299
|
+
continue
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
# Closing an already-closed issue is a no-op; a gh failure is non-fatal.
|
|
303
|
+
gh issue close "$BNUM" {GH_FLAG} --comment "Closing dead claims board — ${REASON}.
|
|
304
|
+
|
|
305
|
+
Closed automatically by \`/cleanup\` Phase 2C." 2>/dev/null \
|
|
306
|
+
&& { echo "Closed claims board #$BNUM ($REASON)"; BOARDS_CLOSED=$((BOARDS_CLOSED + 1)); } \
|
|
307
|
+
|| echo "WARNING: failed to close claims board #$BNUM — non-fatal" # allowlist:check-command-side-effects
|
|
308
|
+
done <<< "$BOARDS"
|
|
309
|
+
|
|
310
|
+
echo "Claims boards closed: $BOARDS_CLOSED"
|
|
311
|
+
```
|
|
312
|
+
|
|
237
313
|
---
|
|
238
314
|
|
|
239
315
|
## Phase 3: Worktree & Branch Pruning
|
|
@@ -599,6 +675,11 @@ fi
|
|
|
599
675
|
|-------|-----------|--------|
|
|
600
676
|
| #{N} | PR #{M} | Closed |
|
|
601
677
|
|
|
678
|
+
### Claims Boards Closed
|
|
679
|
+
| Issue | Reason | Action |
|
|
680
|
+
|-------|--------|--------|
|
|
681
|
+
| #{N} | LEASE_RELEASED / stale > {BOARD_STALE_HOURS}h | Closed |
|
|
682
|
+
|
|
602
683
|
### Worktrees & Branches Pruned
|
|
603
684
|
| Type | Count |
|
|
604
685
|
|------|-------|
|
package/commands/deploy-info.md
CHANGED
|
@@ -56,13 +56,15 @@ else
|
|
|
56
56
|
# Step 2: Check for open review-finding issues referencing each PR
|
|
57
57
|
OPEN_FINDINGS_SUMMARY=""
|
|
58
58
|
for pr_num in $ALL_PR_NUMBERS; do
|
|
59
|
+
# Anchored post-filter (same as review-pr-staging.md): the search is a coarse candidate fetch only
|
|
60
|
+
FINDING_PR_FILTER='[.[] | select((.title | endswith("PR #" + $n + ")")) or ((.body // "") | test("(^|\\n)\\*\\*Source\\*\\*: PR #" + $n + "([^0-9]|$)")))] | .[] | " - #\(.number): \(.title)"' # FINDING_PR_FILTER
|
|
59
61
|
OPEN_FINDINGS=$(gh issue list -R {GH_REPO} \
|
|
60
62
|
--label "review-finding" \
|
|
61
63
|
--state open \
|
|
62
64
|
--search "PR #${pr_num}" \
|
|
63
65
|
--limit 20 \
|
|
64
|
-
--json number,title \
|
|
65
|
-
|
|
66
|
+
--json number,title,body 2>/dev/null \
|
|
67
|
+
| jq -r --arg n "${pr_num}" "$FINDING_PR_FILTER" 2>/dev/null) # uses FINDING_PR_FILTER
|
|
66
68
|
|
|
67
69
|
if [ -n "$OPEN_FINDINGS" ]; then
|
|
68
70
|
OPEN_FINDINGS_SUMMARY="${OPEN_FINDINGS_SUMMARY}
|
|
@@ -304,13 +306,15 @@ if [ -z "$TRAIN_PR_NUMBER" ]; then
|
|
|
304
306
|
TRAIN_STATUS="NO_TRAIN — no open staging→main PR. Run /review-pr-staging to open one."
|
|
305
307
|
else
|
|
306
308
|
# Step 2: Check whether the train is held (has blocking findings)
|
|
309
|
+
# Anchored post-filter (same as review-pr-staging.md): the search is a coarse candidate fetch only
|
|
310
|
+
FINDING_PR_FILTER='[.[] | select((.title | endswith("PR #" + $n + ")")) or ((.body // "") | test("(^|\\n)\\*\\*Source\\*\\*: PR #" + $n + "([^0-9]|$)")))] | .[] | " - #\(.number): \(.title)"' # FINDING_PR_FILTER
|
|
307
311
|
HELD_BY=$(gh issue list {GH_FLAG} \
|
|
308
312
|
--label "review-finding" \
|
|
309
313
|
--state open \
|
|
310
314
|
--search "PR #${TRAIN_PR_NUMBER}" \
|
|
311
315
|
--limit 20 \
|
|
312
|
-
--json number,title \
|
|
313
|
-
|
|
316
|
+
--json number,title,body 2>/dev/null \
|
|
317
|
+
| jq -r --arg n "${TRAIN_PR_NUMBER}" "$FINDING_PR_FILTER" 2>/dev/null | sed 's/^ - //' || true) # uses FINDING_PR_FILTER
|
|
314
318
|
|
|
315
319
|
if [ -n "$HELD_BY" ]; then
|
|
316
320
|
TRAIN_STATUS="HELD"
|
|
@@ -205,7 +205,7 @@ Classify `$ARGUMENTS` and persist that classification alongside the resolved iss
|
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
207
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
208
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
208
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
209
209
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
210
210
|
# Import ForgeDock's OWN resolve.mjs from FORGE_ROOT, never from the consumer {REPO_PATH}
|
|
211
211
|
# (inert on non-ForgeDock repos, and would execute repo-controlled JS). Unresolved/missing => no output,
|
|
@@ -260,7 +260,7 @@ gh issue list -R {SATELLITE_REPO} --state open --limit 500 --json number,title,l
|
|
|
260
260
|
### Filter out ineligible issues
|
|
261
261
|
|
|
262
262
|
Remove issues that should NOT be worked on:
|
|
263
|
-
- Already closed or has `workflow:merged` label
|
|
263
|
+
- Already closed or has `workflow:merged` label (exception: an explicitly requested closed `workflow:decomposed` parent is expanded first, see below)
|
|
264
264
|
- Has `workflow:invalid` label
|
|
265
265
|
- Has `needs-human` label (requires manual action)
|
|
266
266
|
- Has `workflow:decomposed` label (parent tracker — its sub-issues should be picked up instead)
|
|
@@ -269,6 +269,9 @@ Remove issues that should NOT be worked on:
|
|
|
269
269
|
|
|
270
270
|
If a `workflow:decomposed` issue is found, automatically expand it to its open sub-issues instead.
|
|
271
271
|
|
|
272
|
+
<!-- Added: forge#3736 -->
|
|
273
|
+
**Closed decomposed parents**: with `decomposition.close_parent` enabled, a decomposed parent is CLOSED at decomposition and keeps `workflow:decomposed`. Run this expansion BEFORE the "already closed" filter: when an explicitly requested issue is a closed `workflow:decomposed` parent, expand it to its open children (parse the `- [ ] #N` and `- [x] #N` lines of the Sub-Issue Tracker in its body, keep those whose state is OPEN) instead of dropping it as closed. A closed decomposed parent with no open children is reported as complete and skipped. A closed parent without `workflow:decomposed` is still dropped.
|
|
274
|
+
|
|
272
275
|
### Source-PR Triage Hint (pre-flight, non-binding)
|
|
273
276
|
|
|
274
277
|
<!-- Added: forge#2351 -->
|
|
@@ -378,7 +381,7 @@ After executing the plan, retain every ungrouped finding in the registry for the
|
|
|
378
381
|
# Run this after initial resolution and every later admission event; execute the
|
|
379
382
|
# returned `groups`/`extensions` member numbers against the repo each candidate came from.
|
|
380
383
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
381
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
384
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
382
385
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
383
386
|
# Import ForgeDock's OWN admission.mjs from FORGE_ROOT, never from the consumer {REPO_PATH}.
|
|
384
387
|
# Unresolved/missing => no plan: every candidate stays an ungrouped singleton (fail closed, loudly).
|
|
@@ -53,6 +53,8 @@ fi
|
|
|
53
53
|
# --- End lease gate ---
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
**Board closure ownership** <!-- Added: forge#3699 -->: lease takeover never reclaims or closes a claims board (each batch creates its own, Step 3D.1). Closing it is owned by Phase 5 Step 5C and, for batches that never reach it, `/cleanup` Phase 2C.
|
|
57
|
+
|
|
56
58
|
**Lease release**: `LEASE_RELEASED_THIS_SESSION` (declared just below, alongside `release_orchestrator_lease()`) guards against double-posting `FORGE:LEASE_RELEASED`. The release call is invoked from both the **Step 4A-pre.-0.5 "Stopping the orchestrator"** procedure (interrupted path) and the **Termination condition** at the end of Step 4B (normal clean/paused drain) — both exit paths route through the same idempotent function so the lease is never left dangling either way.
|
|
57
59
|
|
|
58
60
|
```bash
|
|
@@ -828,7 +830,7 @@ fi
|
|
|
828
830
|
|
|
829
831
|
```bash
|
|
830
832
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
831
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
833
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
832
834
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
833
835
|
recovery_claim_blocks() {
|
|
834
836
|
local NUM="$1" OUT RC
|
|
@@ -1185,7 +1187,7 @@ RENDERED_PROMPT=$(cat "$RENDERED_PROMPT_SRC"); rm -f "$RENDERED_PROMPT_SRC"
|
|
|
1185
1187
|
# Runs inside the per-issue dispatch loop (same loop as claim_conflicts_with_live_holder above),
|
|
1186
1188
|
# immediately before that issue's Agent()/task() call. LINT_REFUSED_ISSUES is initialised once, empty.
|
|
1187
1189
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
1188
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
1190
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
1189
1191
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
1190
1192
|
# Fail closed on unbound inputs (forge#3149): an empty rendered prompt or a missing resolved value would lint an empty file
|
|
1191
1193
|
# (rc=2) or compare against nothing; refuse with a distinct reason instead of a misleading lint verdict.
|
|
@@ -1609,6 +1611,12 @@ classify_predecessor_state() {
|
|
|
1609
1611
|
PRED_LABELS=$(echo "$PRED_INFO" | jq -r '.workflow[]?' 2>/dev/null)
|
|
1610
1612
|
fi
|
|
1611
1613
|
|
|
1614
|
+
# forge#3566: age bound for the remediating / engine-error IN_PROGRESS arms below. Resolved here, not once at
|
|
1615
|
+
# Step 4 setup, because wake reconstruction calls this helper from a fresh shell. Non-numeric falls back to 48.
|
|
1616
|
+
local IN_PROGRESS_MAX_HOURS
|
|
1617
|
+
IN_PROGRESS_MAX_HOURS=$(yq '.orchestration.in_progress_max_hours // 48' forge.yaml 2>/dev/null || echo 48)
|
|
1618
|
+
case "$IN_PROGRESS_MAX_HOURS" in ''|*[!0-9]*) IN_PROGRESS_MAX_HOURS=48 ;; esac
|
|
1619
|
+
|
|
1612
1620
|
# reverify_merged_trail: re-run the phase-trail verifier for a merged predecessor that has NO escalation
|
|
1613
1621
|
# record (forge#3156), so wake reconstruction does not trust the `workflow:merged` label alone. Prints OK
|
|
1614
1622
|
# when the trail verifies, UNVERIFIED otherwise. Fails closed: a wake runs in a fresh shell where FORGE_ROOT is
|
|
@@ -1618,7 +1626,7 @@ classify_predecessor_state() {
|
|
|
1618
1626
|
local P="$1" RC
|
|
1619
1627
|
if [ -z "${FORGE_ROOT:-}" ]; then
|
|
1620
1628
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
1621
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
1629
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
1622
1630
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
1623
1631
|
fi
|
|
1624
1632
|
if [ -z "${FORGE_ROOT:-}" ] || [ ! -f "$FORGE_ROOT/scripts/verify-phase-trail.sh" ]; then
|
|
@@ -1671,7 +1679,10 @@ classify_predecessor_state() {
|
|
|
1671
1679
|
# forge#3541: autonomous remediation (ci-gate / in-pr-fix / base-sync) is pending or running. No human decision is
|
|
1672
1680
|
# needed, so this is NOT GATED: dependents keep waiting exactly as for any mid-pipeline predecessor, and item 6.4
|
|
1673
1681
|
# (or the engine handoff) drives the fix. A concurrent `needs-human` is matched by the GATED arm above first.
|
|
1674
|
-
|
|
1682
|
+
# Time-boxed (forge#3566): past orchestration.in_progress_max_hours (default 48) [IN_PROGRESS_MAX_HOURS, resolved from forge.yaml] the state is stranded, not in flight.
|
|
1683
|
+
PRED_AGE_H=$(gh issue view "$PRED" -R {GH_REPO} --json updatedAt --jq '((now - (.updatedAt | fromdateiso8601)) / 3600 | floor)' 2>/dev/null || echo "")
|
|
1684
|
+
case "$PRED_AGE_H" in ''|*[!0-9]*) PRED_AGE_H=0 ;; esac # unreadable age keeps IN_PROGRESS (fail safe)
|
|
1685
|
+
if [ "$PRED_AGE_H" -ge "${IN_PROGRESS_MAX_HOURS:-48}" ]; then echo "GATED"; else echo "IN_PROGRESS"; fi
|
|
1675
1686
|
elif echo "$PRED_LABELS" | grep -qx "workflow:engine-error"; then
|
|
1676
1687
|
# forge#2261: the engine/tool itself broke on this run (e.g. a fail-fast
|
|
1677
1688
|
# CLI_BACKEND_FAILED, or an exhausted retry loop where the runner never
|
|
@@ -1688,7 +1699,11 @@ classify_predecessor_state() {
|
|
|
1688
1699
|
# ENGINE_DISPATCH_MAP-tracked issues (fresh Agent-spawn fallback on empty
|
|
1689
1700
|
# committed state) — 2a's Agent(resume=...) alone cannot reach them, since
|
|
1690
1701
|
# they were never in AGENT_ISSUE_MAP.
|
|
1691
|
-
|
|
1702
|
+
# Time-boxed (forge#3566): past orchestration.in_progress_max_hours (default 48) [IN_PROGRESS_MAX_HOURS, resolved from forge.yaml] auto-resume has evidently
|
|
1703
|
+
# not recovered it; classify GATED so a human is told instead of dependents waiting forever.
|
|
1704
|
+
PRED_AGE_H=$(gh issue view "$PRED" -R {GH_REPO} --json updatedAt --jq '((now - (.updatedAt | fromdateiso8601)) / 3600 | floor)' 2>/dev/null || echo "")
|
|
1705
|
+
case "$PRED_AGE_H" in ''|*[!0-9]*) PRED_AGE_H=0 ;; esac # unreadable age keeps IN_PROGRESS (fail safe)
|
|
1706
|
+
if [ "$PRED_AGE_H" -ge "${IN_PROGRESS_MAX_HOURS:-48}" ]; then echo "GATED"; else echo "IN_PROGRESS"; fi
|
|
1692
1707
|
elif [ "$PRED_STATE" = "CLOSED" ]; then
|
|
1693
1708
|
# Closed with no workflow:invalid/merged label (e.g. closed-not-planned) — treat as DONE,
|
|
1694
1709
|
# not a new deadlock state; there is no pending code for dependents to wait on.
|
|
@@ -2059,7 +2074,7 @@ done
|
|
|
2059
2074
|
0. **Phase-trail verification (MANDATORY on EVERY merge completion, before classifying DONE)** <!-- Added: forge#3062 --> — a `workflow:merged` label alone is not proof the pipeline ran. **Scope (forge#3102)**: run it only when the issue carries `workflow:merged` (a failed label read fails closed). Issues in other terminal-for-this-agent states (`workflow:invalid`, `workflow:decomposed`, `needs-human`, `workflow:awaiting-merge`, paused) legitimately lack a full trail; classify them by label per Predecessor Classification, skip this verifier, and do NOT flag them as bypass or re-dispatch them. Run the deterministic verifier (add `--docs-only` for docs-only diffs):
|
|
2060
2075
|
```bash
|
|
2061
2076
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
2062
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
2077
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
2063
2078
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
2064
2079
|
# forge#3102: only merged completions carry a full trail; invalid/decomposed/needs-human/awaiting-merge/paused do not.
|
|
2065
2080
|
# Fail closed: a failed label read is an ERROR, never a skip.
|
|
@@ -2801,7 +2816,7 @@ RERESOLVE_ENABLED=$(yq '.orchestration.reresolve.enabled // true' forge.yaml 2>/
|
|
|
2801
2816
|
RERESOLVE_MAX_ROUNDS=$(yq '.orchestration.reresolve.max_rounds // "unbounded"' forge.yaml 2>/dev/null || echo "unbounded")
|
|
2802
2817
|
|
|
2803
2818
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
2804
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
2819
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
2805
2820
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
2806
2821
|
# Run ForgeDock's OWN orchestrate-canary.mjs from FORGE_ROOT, never from the consumer {REPO_PATH}.
|
|
2807
2822
|
# Unresolved/missing => no result, so reResolve is treated as false for this cycle (fail closed, loudly).
|
|
@@ -2822,7 +2837,7 @@ If the result's `reResolve` is `false` (off switch, or `RERESOLVE_ROUNDS_SO_FAR`
|
|
|
2822
2837
|
|
|
2823
2838
|
```bash
|
|
2824
2839
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
2825
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
2840
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
2826
2841
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
2827
2842
|
# Import ForgeDock's OWN resolve.mjs from FORGE_ROOT, never from the consumer {REPO_PATH}.
|
|
2828
2843
|
# Unresolved/missing => no output, so no new matches are folded this cycle (fail closed, loudly).
|
|
@@ -3438,7 +3453,7 @@ Finding #${FINDING_NUM} has no **Code branch** annotation and its parent PR #${R
|
|
|
3438
3453
|
# non-routine so a security/billing finding is never paused. Import via {REPO_PATH} and pass
|
|
3439
3454
|
# the text through the environment (argv text beginning with "-" is parsed as a node option).
|
|
3440
3455
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
3441
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
3456
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
3442
3457
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
3443
3458
|
# Import ForgeDock's OWN admission.mjs from FORGE_ROOT, never from the consumer {REPO_PATH}
|
|
3444
3459
|
# (inert on non-ForgeDock repos, and would execute repo-controlled JS). Unresolved/missing => fail closed, loudly.
|
|
@@ -3609,7 +3624,7 @@ for FINDING_NUM in "${BATCHING_CANDIDATES[@]}"; do
|
|
|
3609
3624
|
[ -z "$FINDING_FILE" ] && continue
|
|
3610
3625
|
|
|
3611
3626
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
3612
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
3627
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
3613
3628
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
3614
3629
|
# Import from ForgeDock's own root, never the consumer cwd; fail closed (non-routine) when unresolved.
|
|
3615
3630
|
SAFETY_CLASS="unclassified"
|
|
@@ -44,13 +44,24 @@ Include the audit summary in the final report (Phase 6). Key metrics to surface:
|
|
|
44
44
|
|
|
45
45
|
### Step 5C: Close the coordination issue (claims board) <!-- Added: forge#2072 -->
|
|
46
46
|
|
|
47
|
-
**Why this step exists**: Step 3D.1 (`phase-3-dependency.md`) creates a per-batch coordination issue (the claims board) and exports `FORGE_COORD_ISSUE`/`COORD_ISSUE_NUMBER`. Step 5A's `/cleanup` sweep only closes
|
|
47
|
+
**Why this step exists**: Step 3D.1 (`phase-3-dependency.md`) creates a per-batch coordination issue (the claims board) and exports `FORGE_COORD_ISSUE`/`COORD_ISSUE_NUMBER`. Step 5A's `/cleanup` orphan sweep (Phase 2A/2B) only closes issues whose PR merged — the claims board has no PR of its own, so that heuristic never matches it; `/cleanup` Phase 2C is the separate safety net that closes boards this step missed. Without this step, one `automation`-labelled issue leaks per orchestration batch.
|
|
48
48
|
|
|
49
49
|
**Run after Step 5B, before Step 5D.** No-ops cleanly if the claims board was never created this run (`COORD_ISSUE_NUMBER` unset/empty — e.g. Step 3D.1's `gh issue create` failed, or this batch never reached DAG construction). Tolerates GitHub API failures without aborting the rest of Phase 5 — the same tolerant-failure convention (`2>/dev/null || echo ...`, `|| true`) used throughout `phase-3-dependency.md`/`phase-4-execution.md`.
|
|
50
50
|
|
|
51
51
|
**Orchestrator lease should already be released by this point** (forge#2627): `phase-4-execution.md`'s Termination condition (end of Step 4B) calls `release_orchestrator_lease()` before handing off to Phase 5 on a normal drain, and the "Stopping the orchestrator" procedure (Step 4A-pre.-0.5) does the same on an interrupted exit. Closing the coordination issue here does not depend on lease state either way — an unreleased lease on an issue about to be closed is harmless (a future batch creates its own new coordination issue per Step 3D.1), but if `ACTIVE_CLAIMS_NOTE` logic is ever extended to also surface lease state, prefer checking for a still-unreleased `FORGE:LEASE` the same way `ACTIVE_CLAIMS` is computed above.
|
|
52
52
|
|
|
53
|
+
**Recover the board when the variable is lost** <!-- Added: forge#3699 -->: `COORD_ISSUE_NUMBER` is a shell variable and does not survive compaction, `--retry` or a fresh-context resume. When it is unset but `BATCH_ID` is known, look the board up on GitHub by the exact `<!-- FORGE:BATCH_ID: ${BATCH_ID} -->` body marker (never a prefix, so another live batch's board is never matched). If `BATCH_ID` is also lost, fall through to the no-op and leave the board to `/cleanup` Phase 2C.
|
|
54
|
+
|
|
53
55
|
```bash
|
|
56
|
+
if [ -z "${COORD_ISSUE_NUMBER:-}" ] && [ -n "${BATCH_ID:-}" ]; then
|
|
57
|
+
COORD_ISSUE_NUMBER=$(gh issue list -R {GH_REPO} --state open --label automation --limit 100 \
|
|
58
|
+
--json number,body 2>/dev/null \
|
|
59
|
+
| jq -r --arg marker "<!-- FORGE:BATCH_ID: ${BATCH_ID} -->" \
|
|
60
|
+
'[.[] | select(.body | contains("<!-- FORGE:COORD_ISSUE -->") and contains($marker))] | first | .number // empty' \
|
|
61
|
+
2>/dev/null || echo "")
|
|
62
|
+
[ -n "$COORD_ISSUE_NUMBER" ] && echo "Recovered coordination issue #${COORD_ISSUE_NUMBER} from FORGE:BATCH_ID marker"
|
|
63
|
+
fi
|
|
64
|
+
|
|
54
65
|
if [ -n "${COORD_ISSUE_NUMBER:-}" ]; then
|
|
55
66
|
# Use Step 4A's read_active_claims() helper (re-declare it verbatim here when this
|
|
56
67
|
# phase runs in a fresh context). Releases are separate later comments, so checking
|
|
@@ -66,15 +77,20 @@ $(echo "$ACTIVE_CLAIMS" | jq -r '.[] | "- " + .' 2>/dev/null)"
|
|
|
66
77
|
ACTIVE_CLAIMS_NOTE="No active (unreleased) claims at close time."
|
|
67
78
|
fi
|
|
68
79
|
|
|
80
|
+
# Outcome counters come from Phase 4/6 state; tolerate unset values (default n/a when the orchestrator did not track them).
|
|
81
|
+
MERGED_COUNT="${MERGED_COUNT:-n/a}"; GATED_COUNT="${GATED_COUNT:-n/a}"; FAILED_COUNT="${FAILED_COUNT:-n/a}"
|
|
82
|
+
|
|
69
83
|
gh issue close "$COORD_ISSUE_NUMBER" -R {GH_REPO} --comment "Batch complete — closing claims board.
|
|
70
84
|
|
|
85
|
+
**Summary**: merged ${MERGED_COUNT} / gated ${GATED_COUNT} / failed ${FAILED_COUNT}
|
|
86
|
+
|
|
71
87
|
${ACTIVE_CLAIMS_NOTE}
|
|
72
88
|
|
|
73
89
|
Closed automatically by \`/orchestrate\` Phase 5 Step 5C." 2>/dev/null \
|
|
74
90
|
&& echo "Closed coordination issue #${COORD_ISSUE_NUMBER}" \
|
|
75
91
|
|| echo "WARNING: failed to close coordination issue #${COORD_ISSUE_NUMBER} — non-fatal, continuing Phase 5"
|
|
76
92
|
else
|
|
77
|
-
echo "No coordination issue for this batch (
|
|
93
|
+
echo "No coordination issue for this batch (unset and not recoverable via BATCH_ID) — Step 5C is a no-op; /cleanup Phase 2C will sweep it if it exists"
|
|
78
94
|
fi
|
|
79
95
|
```
|
|
80
96
|
|
|
@@ -36,6 +36,8 @@ else
|
|
|
36
36
|
fi
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
**Decomposition trackers** <!-- Added: forge#3736 -->: issues labelled `workflow:decomposed` are trackers (open, or closed at decomposition when `decomposition.close_parent` is true), not work. Phase 2 excludes `workflow:decomposed` from every open-work and issue-flow count and reports them separately as `trackers (N)`.
|
|
40
|
+
|
|
39
41
|
### 1C: Load Prior Health Report
|
|
40
42
|
|
|
41
43
|
Fetch the most recent `health-report` issue for the same repo and parse its metrics for trend computation. This must run before Phase 2 so that PRIOR_* variables are available when Phase 3A builds the trend column.
|
|
@@ -323,7 +323,7 @@ INVALID_ISSUES=$(gh issue list -R $REPO --state closed --label "workflow:invalid
|
|
|
323
323
|
--jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"review-finding\") | not))] | length")
|
|
324
324
|
|
|
325
325
|
TOTAL_INVESTIGATED=$(gh issue list -R $REPO --state all --limit 500 --json number,createdAt,labels \
|
|
326
|
-
--jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | any(. == \"workflow:investigating\" or . == \"workflow:building\" or . == \"workflow:in-review\" or . == \"workflow:merged\" or . == \"workflow:invalid\"))) ] | length")
|
|
326
|
+
--jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"workflow:decomposed\") | not) and (.labels | map(.name) | any(. == \"workflow:investigating\" or . == \"workflow:building\" or . == \"workflow:in-review\" or . == \"workflow:merged\" or . == \"workflow:invalid\"))) ] | length")
|
|
327
327
|
|
|
328
328
|
echo "Investigation invalidation rate: $INVALID_ISSUES / $TOTAL_INVESTIGATED"
|
|
329
329
|
# Target: < 15%
|
|
@@ -354,15 +354,22 @@ echo "Re-investigation count: $REINVESTIGATED"
|
|
|
354
354
|
# 4. Issue structure compliance (/issue adoption proxy)
|
|
355
355
|
# Issues created in window that have an "Affected Files" section in their body
|
|
356
356
|
TOTAL_ISSUES=$(gh issue list -R $REPO --state all --limit 500 --json number,createdAt,labels \
|
|
357
|
-
--jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"review-finding\") | not))] | length")
|
|
357
|
+
--jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"review-finding\") | not) and (.labels | map(.name) | index(\"workflow:decomposed\") | not))] | length")
|
|
358
358
|
|
|
359
359
|
STRUCTURED_ISSUES=$(gh issue list -R $REPO --state all --limit 500 --json number,createdAt,body,labels \
|
|
360
|
-
--jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"review-finding\") | not) and (.body | contains(\"Affected Files\")))] | length")
|
|
360
|
+
--jq "[.[] | select(.createdAt > \"$SINCE\" and (.labels | map(.name) | index(\"review-finding\") | not) and (.labels | map(.name) | index(\"workflow:decomposed\") | not) and (.body | contains(\"Affected Files\")))] | length")
|
|
361
361
|
|
|
362
362
|
echo "Issue structure compliance: $STRUCTURED_ISSUES / $TOTAL_ISSUES"
|
|
363
363
|
# Target: > 80%. Issues without Affected Files are harder to investigate and more likely to produce ISSUE_SPEC failures.
|
|
364
364
|
```
|
|
365
365
|
|
|
366
|
+
**Decomposition trackers** <!-- Added: forge#3736 -->: issues labelled `workflow:decomposed` are trackers, not work. `TOTAL_INVESTIGATED`, `TOTAL_ISSUES`, `STRUCTURED_ISSUES` and both stall-rate queries exclude them with an `index("workflow:decomposed") | not` filter, so a parent closed at decomposition (`decomposition.close_parent`) and an open one are treated alike. Report them separately as `trackers (N)`:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
TRACKER_COUNT=$(gh issue list -R $REPO --state all --label "workflow:decomposed" --limit 500 --json number --jq 'length' 2>/dev/null || echo 0)
|
|
370
|
+
echo "trackers ($TRACKER_COUNT) — decomposed parents, excluded from work counts"
|
|
371
|
+
```
|
|
372
|
+
|
|
366
373
|
**Issue quality score** = weighted average of sub-metrics:
|
|
367
374
|
- Invalidation rate: 40% weight (direct signal — these issues failed)
|
|
368
375
|
- ISSUE_SPEC audit findings: 30% weight (downstream cost signal)
|
|
@@ -496,14 +503,14 @@ Query issues that received a `<!-- FORGE:STALL_DETECTED -->` comment within the
|
|
|
496
503
|
BATCH_TOTAL=$(gh issue list -R $GH_REPO \
|
|
497
504
|
--state all --limit 500 \
|
|
498
505
|
--json number,createdAt,labels \
|
|
499
|
-
--jq "[.[] | select(.createdAt > \"$SINCE\") | select(.labels | map(.name) | any(startswith(\"workflow:\")))] | length" \
|
|
506
|
+
--jq "[.[] | select(.createdAt > \"$SINCE\") | select(.labels | map(.name) | any(startswith(\"workflow:\"))) | select(.labels | map(.name) | index(\"workflow:decomposed\") | not)] | length" \
|
|
500
507
|
2>/dev/null || echo 0)
|
|
501
508
|
|
|
502
509
|
# Enumerate pipeline-active issue numbers for per-issue comment queries
|
|
503
510
|
PIPELINE_ISSUE_NUMS=$(gh issue list -R $GH_REPO \
|
|
504
511
|
--state all --limit 500 \
|
|
505
512
|
--json number,createdAt,labels \
|
|
506
|
-
--jq "[.[] | select(.createdAt > \"$SINCE\") | select(.labels | map(.name) | any(startswith(\"workflow:\"))) | .number]" \
|
|
513
|
+
--jq "[.[] | select(.createdAt > \"$SINCE\") | select(.labels | map(.name) | any(startswith(\"workflow:\"))) | select(.labels | map(.name) | index(\"workflow:decomposed\") | not) | .number]" \
|
|
507
514
|
2>/dev/null || echo "[]")
|
|
508
515
|
|
|
509
516
|
# Count issues with at least one FORGE:STALL_DETECTED comment
|
|
@@ -100,6 +100,26 @@ if [ -n "$UNTRACKED" ] && [ "$UNTRACKED_COUNT" -gt 0 ]; then
|
|
|
100
100
|
done
|
|
101
101
|
echo ""
|
|
102
102
|
fi
|
|
103
|
+
|
|
104
|
+
# Decomposition trackers (workflow:decomposed) are NOT work: they are excluded from every
|
|
105
|
+
# work count in this report and listed separately. With decomposition.close_parent a tracker
|
|
106
|
+
# is CLOSED at decomposition, so query --state all and keep only trackers with unfinished
|
|
107
|
+
# `- [ ]` checklist items. <!-- Added: forge#3736 -->
|
|
108
|
+
TRACKERS=$(gh issue list $GH_FLAG \
|
|
109
|
+
--state all \
|
|
110
|
+
--label "workflow:decomposed" \
|
|
111
|
+
--limit 100 \
|
|
112
|
+
--json number,title,state,body \
|
|
113
|
+
--jq '.[] | select(.body | test("(?m)^[[:space:]]*[-*+] \\[ \\]")) | "#\(.number) \(.title[:60]) [\(.state | ascii_downcase)]"' \
|
|
114
|
+
2>/dev/null || echo "")
|
|
115
|
+
TRACKER_COUNT=$(printf '%s' "$TRACKERS" | grep -c '#' 2>/dev/null || true)
|
|
116
|
+
echo "[trackers] (${TRACKER_COUNT:-0} — decomposed parents awaiting children; not counted as work)"
|
|
117
|
+
if [ -n "$TRACKERS" ]; then
|
|
118
|
+
echo "$TRACKERS" | while IFS= read -r line; do
|
|
119
|
+
echo " $line"
|
|
120
|
+
done
|
|
121
|
+
fi
|
|
122
|
+
echo ""
|
|
103
123
|
```
|
|
104
124
|
|
|
105
125
|
---
|
|
@@ -295,7 +315,7 @@ echo ""
|
|
|
295
315
|
INVESTIGATING=$(gh issue list $GH_FLAG --state open --label "workflow:investigating" --limit 100 --json number --jq '. | length' 2>/dev/null || echo 0)
|
|
296
316
|
BUILDING=$(gh issue list $GH_FLAG --state open --label "workflow:building" --limit 100 --json number --jq '. | length' 2>/dev/null || echo 0)
|
|
297
317
|
IN_REVIEW=$(gh issue list $GH_FLAG --state open --label "workflow:in-review" --limit 100 --json number --jq '. | length' 2>/dev/null || echo 0)
|
|
298
|
-
BLOCKED=$(gh issue list $GH_FLAG --state open --label "needs-human" --limit 100 --json number --jq '. | length' 2>/dev/null || echo 0)
|
|
318
|
+
BLOCKED=$(gh issue list $GH_FLAG --state open --label "needs-human" --limit 100 --json number,labels --jq '[.[] | select(.labels | map(.name) | index("workflow:decomposed") | not)] | length' 2>/dev/null || echo 0) # workflow:decomposed trackers are excluded from work counts
|
|
299
319
|
READY=$(gh issue list $GH_FLAG --state open --label "workflow:ready-to-build" --limit 100 --json number --jq '. | length' 2>/dev/null || echo 0)
|
|
300
320
|
|
|
301
321
|
TOTAL_INFLIGHT=$((INVESTIGATING + BUILDING + IN_REVIEW + READY))
|
package/commands/quality-gate.md
CHANGED
|
@@ -590,7 +590,7 @@ Report any hit as **HIGH** — the DB connection will fail in any environment wh
|
|
|
590
590
|
# Run only when commands/*.md or scripts/* changed and the validator ships in this repo.
|
|
591
591
|
# Install-root-only resolution (no worktree/cwd fallback: the branch under review must not supply the script that gates it).
|
|
592
592
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
593
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
593
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
594
594
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
595
595
|
VALIDATOR="${FORGE_ROOT:+$FORGE_ROOT/scripts/validate-spec-graph.sh}"
|
|
596
596
|
if [ -z "$FORGE_ROOT" ]; then
|
|
@@ -623,7 +623,7 @@ Report each `[HARD]` line as **HIGH** — a dangling `Skill()` target or a label
|
|
|
623
623
|
# root-level native commands and are not scanned.
|
|
624
624
|
# Install-root-only resolution (no worktree/cwd fallback: the branch under review must not supply the script that gates it).
|
|
625
625
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
626
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
626
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
627
627
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
628
628
|
CONFLICT_CHECKER="${FORGE_ROOT:+$FORGE_ROOT/scripts/check-native-conflicts.sh}"
|
|
629
629
|
if [ -z "$FORGE_ROOT" ]; then
|
|
@@ -739,7 +739,7 @@ Do NOT suppress the finding by making the check less strict or adding fake no-op
|
|
|
739
739
|
```bash
|
|
740
740
|
# Install-root-only resolution (no worktree/cwd fallback: the branch under review must not supply the script that gates it).
|
|
741
741
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
742
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
742
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
743
743
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
744
744
|
SPEC_BASH_CHECKER="${FORGE_ROOT:+$FORGE_ROOT/scripts/check-spec-bash.sh}"
|
|
745
745
|
SPEC_MD_FILES=$(echo {CHANGED_FILES} | tr ' ' '\n' | grep -E '^commands/.*\.md$' || true)
|
|
@@ -1716,7 +1716,7 @@ For each failing test ID (or test command) surfaced in Step 2:
|
|
|
1716
1716
|
|
|
1717
1717
|
# Install-root-only resolution (no worktree/cwd fallback: the branch under review must not supply the script that gates it).
|
|
1718
1718
|
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => callers fail closed)
|
|
1719
|
-
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
1719
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
1720
1720
|
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
1721
1721
|
CLASSIFIER="${FORGE_ROOT:+$FORGE_ROOT/scripts/flaky-quarantine.sh}"
|
|
1722
1722
|
|
|
@@ -192,6 +192,7 @@ Append this block at the very end of your comment (after the `---` footer line,
|
|
|
192
192
|
9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the marker domain the orchestrator passes you (the canonical value from the roster-to-marker table in `review-pr.md`, e.g. `infra` and `scraper`; never a lowercased display name such as `infrastructure` or `scraping`).
|
|
193
193
|
10. **Reviewed SHA**: Include exactly one line `Reviewed-SHA: [REVIEW_SHA]` (the full 40-hex head you were pinned to) in the persisted body, outside the marker. `/review-pr` scopes its completion wait and idempotent re-entry to this line, so a comment without the current head does not count as a completed review. Do NOT put the SHA inside the `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker.
|
|
194
194
|
11. **Failure scenario and scope (provenance inputs)**: every finding's body section (the prose above the findings block) MUST carry a `**Failure scenario**` line — concrete inputs leading to a wrong output or behaviour — and a `**Scope**: changed` or `**Scope**: pre-existing` line (`changed` = the cited line is added or modified by this PR; `pre-existing` = it was already there). `/review-pr` §6B.4 reads these: a finding without a failure scenario is demoted to a note (unless CRITICAL/HIGH, any confidence), and a MEDIUM or lower finding whose cited line is not in the PR diff is routed as pre-existing (CRITICAL/HIGH never are; see §5). The `**Scope**` tag is informational and never gates routing. Speculative hardening with no failure scenario is a note, not a finding. The one-line `<!-- FINDING:... -->` grammar above is unchanged. <!-- Added: forge#3452 -->
|
|
195
|
+
12. **Fix efficacy (linked-issue contradiction)**: when a finding says the PR's linked issue is NOT fixed, or that the PR re-introduces the defect it claims to fix, its body section MUST carry a `**Fix efficacy**: fails` (the fix does not work) or `**Fix efficacy**: regresses` (the PR re-introduces the defect) line, and the prose may include the token on its own line (`FIX_EFFICACY: fails|regresses`; line-anchored, a mid-sentence mention does not count). Opt-in on the tag, never on keywords. `/review-pr` §6B.5 passes it to `scripts/classify-finding.sh --fix-efficacy`, which exempts a MEDIUM finding from the fix-PR damper. Use it only for the linked issue's own fix; never for unrelated defects. The one-line `<!-- FINDING:... -->` grammar is unchanged. <!-- Added: forge#3693 -->
|
|
195
196
|
|
|
196
197
|
### Domain Prefixes
|
|
197
198
|
|
|
@@ -35,6 +35,12 @@ post `FORGE:REVIEW_BLOCKED` and never replace the isolated review with inline wo
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
+
## Read-Only Git Rule (all reviewer agents)
|
|
39
|
+
|
|
40
|
+
Every reviewer agent is read-only with respect to the main checkout (the repo's shared primary working tree). It MUST NOT run `git checkout`, `git switch`, `git reset`, `git stash`, or `git merge` there: those race with the operator and other agents. Read PR code with `git show <sha>:<path>`, `gh pr diff`, `gh api`, or a per-PR worktree (`git worktree add --detach`) that the agent removes afterward. The orchestrator appends this rule to every dispatched prompt (review-pr.md Phase 3C). <!-- Added: forge#3650 -->
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
38
44
|
## How to Load Agents (Phase 3C)
|
|
39
45
|
|
|
40
46
|
For each agent selected in Phase 3B, read **only** the files relevant to that agent:
|
|
@@ -49,14 +55,16 @@ For each agent selected in Phase 3B, read **only** the files relevant to that ag
|
|
|
49
55
|
| Agent | Trigger | Marker domain | File |
|
|
50
56
|
|-------|---------|---------------|------|
|
|
51
57
|
| General Security & Quality Scan | ALWAYS RUNS | `security` | `review-pr-agents/security.md` |
|
|
52
|
-
| Auth Conventions Auditor |
|
|
58
|
+
| Auth Conventions Auditor | Auth/session/trust-filter files changed (path-gated) | `auth` | `review-pr-agents/auth.md` |
|
|
53
59
|
| Billing Integrity Auditor | BILLING domain | `billing` | `review-pr-agents/billing.md` |
|
|
54
60
|
| Concurrency & Race Condition Auditor | CONCURRENCY or BILLING domain | `concurrency` | `review-pr-agents/concurrency.md` |
|
|
55
61
|
| Domain Logic Auditor | SCRAPING domain (requires `review.domains.scraping` in forge.yaml) | `scraper` | `review-pr-agents/scraper.md` |
|
|
56
62
|
| Frontend Quality Auditor | WEB service touched | `frontend` | `review-pr-agents/frontend.md` |
|
|
57
63
|
| API Design & Consistency Auditor | New/modified routers or SDK/OpenAPI files | `api` | `review-pr-agents/api.md` |
|
|
58
64
|
| Database & Migration Auditor | DATABASE domain | `database` | `review-pr-agents/database.md` |
|
|
59
|
-
| Infrastructure & Deploy Safety Auditor |
|
|
65
|
+
| Infrastructure & Deploy Safety Auditor | Docker, CI, deploy or infra files changed (path-gated) | `infra` | `review-pr-agents/infra.md` |
|
|
66
|
+
|
|
67
|
+
The table is applied by `scripts/review-roster.sh` (Phase 3B Step 4), which tiers the roster by diff size and fix-PR lineage: small and fix PRs run Security only unless a path trigger fires, and Auth and Infra are selected by file path, never by diff-text score. `--thorough` and milestone-to-staging always run the full union. The tier and reason are recorded in `FORGE:REVIEW_TIER`.
|
|
60
68
|
|
|
61
69
|
## Domain Prefixes (for Structured Findings)
|
|
62
70
|
|