forgedock 1.10.2 → 1.11.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/forgedock.mjs +9 -0
- package/commands/ci-audit.md +3 -3
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-pr.md +6 -6
- package/commands/explain.md +2 -2
- package/commands/fix-ci.md +2 -2
- package/commands/forge-stats.md +1 -1
- package/commands/forgedock-init.md +3 -3
- package/commands/optimize.md +5 -5
- package/commands/orchestrate/phase-1-resolve.md +2 -2
- package/commands/orchestrate/phase-4-execution.md +232 -28
- package/commands/quality-gate.md +1 -1
- package/commands/recover-orphans.md +6 -6
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-staging.md +28 -6
- package/commands/review-pr.md +434 -120
- package/commands/security-audit.md +1 -1
- package/commands/spec-doctor.md +2 -2
- package/commands/test-gate.md +7 -7
- package/commands/work-on/build/architect.md +21 -0
- package/commands/work-on/build/context.md +1 -1
- package/commands/work-on/build/implement.md +17 -0
- package/commands/work-on/build/validate.md +2 -2
- package/commands/work-on/build.md +98 -9
- package/commands/work-on/close.md +33 -11
- package/commands/work-on/decompose.md +19 -5
- package/commands/work-on/investigate.md +34 -4
- package/commands/work-on/remediate.md +32 -8
- package/commands/work-on/review.md +10 -8
- package/commands/work-on.md +21 -10
- package/docs/CONFIG.md +30 -0
- package/docs/RELEASE-NOTES.md +9 -0
- package/docs/WORK-ON-RUNTIME.md +22 -2
- package/docs/site/architecture.md +1 -1
- package/docs/spec/forge-protocol-v1.md +10 -0
- package/docs/spec/review-protocol.md +1 -0
- package/package.json +1 -1
- package/packages/protocol/src/types.js +17 -0
- package/scripts/check-spec-bash.sh +45 -2
- package/scripts/check-spec-bash.test.sh +24 -1
- package/scripts/check-spec-markers.sh +53 -0
- package/scripts/diff-size.sh +84 -0
- package/scripts/diff-size.test.sh +117 -0
- package/scripts/forge-root.test.sh +15 -0
- package/scripts/remediation-marker-match.test.sh +51 -0
- package/scripts/review-pr-arguments.test.sh +180 -0
- package/scripts/trusted-comments.test.sh +23 -0
|
@@ -196,7 +196,7 @@ while IFS= read -r f; do
|
|
|
196
196
|
awk '
|
|
197
197
|
/trivy|snyk|owasp|dependency.check|gitleaks|semgrep|gosec|bandit/ {
|
|
198
198
|
scanner_line = NR
|
|
199
|
-
scanner_name = $0
|
|
199
|
+
scanner_name = $(0)
|
|
200
200
|
}
|
|
201
201
|
/continue-on-error:\s*true/ && NR - scanner_line <= 5 && scanner_line > 0 {
|
|
202
202
|
print FILENAME ":" NR ": continue-on-error: true near security scanner at line " scanner_line
|
package/commands/spec-doctor.md
CHANGED
|
@@ -133,12 +133,12 @@ cat /tmp/spec_ranked.txt | head -10
|
|
|
133
133
|
```bash
|
|
134
134
|
if [ "$SELECT_MODE" = "auto" ]; then
|
|
135
135
|
# Pick the top-ranked spec file with ≥ 5 findings
|
|
136
|
-
TARGET_SPEC=$(awk '$1 >= 5 {print $2; exit}' /tmp/spec_ranked.txt || echo "")
|
|
136
|
+
TARGET_SPEC=$(awk '$(1) >= 5 {print $(2); exit}' /tmp/spec_ranked.txt || echo "")
|
|
137
137
|
if [ -z "$TARGET_SPEC" ]; then
|
|
138
138
|
echo "No spec file has ≥5 findings in the last 30 days. Top offender:"
|
|
139
139
|
head -1 /tmp/spec_ranked.txt
|
|
140
140
|
# Use top offender even if below threshold for explicit runs
|
|
141
|
-
TARGET_SPEC=$(awk 'NR==1{print $2}' /tmp/spec_ranked.txt || echo "")
|
|
141
|
+
TARGET_SPEC=$(awk 'NR==1{print $(2)}' /tmp/spec_ranked.txt || echo "")
|
|
142
142
|
if [ -z "$TARGET_SPEC" ]; then
|
|
143
143
|
echo "ERROR: No finding data available. Run pipeline-health first to accumulate findings."
|
|
144
144
|
exit 1
|
package/commands/test-gate.md
CHANGED
|
@@ -65,9 +65,9 @@ BASE_BRANCH="${DEFAULT_BRANCH}"
|
|
|
65
65
|
|
|
66
66
|
# Parse --prs and --base from $ARGUMENTS
|
|
67
67
|
while [[ "$@" ]]; do
|
|
68
|
-
case "$1" in
|
|
69
|
-
--prs) shift; BUNDLE_PRS="$1";;
|
|
70
|
-
--base) shift; BASE_BRANCH="$1";;
|
|
68
|
+
case "${1}" in
|
|
69
|
+
--prs) shift; BUNDLE_PRS="${1}";;
|
|
70
|
+
--base) shift; BASE_BRANCH="${1}";;
|
|
71
71
|
esac
|
|
72
72
|
shift
|
|
73
73
|
done
|
|
@@ -630,11 +630,11 @@ echo ""
|
|
|
630
630
|
|
|
631
631
|
# Helper: classify a single criterion line into one of four buckets:
|
|
632
632
|
# covered, uncovered, untestable, manual
|
|
633
|
-
# Arguments: $1 = criterion text, $2 = source_issue, $3 = source_pr
|
|
633
|
+
# Arguments: ${1} = criterion text, ${2} = source_issue, ${3} = source_pr
|
|
634
634
|
classify_criterion() {
|
|
635
|
-
local criterion="$1"
|
|
636
|
-
local source_issue="$2"
|
|
637
|
-
local source_pr="$3"
|
|
635
|
+
local criterion="${1}"
|
|
636
|
+
local source_issue="${2}"
|
|
637
|
+
local source_pr="${3}"
|
|
638
638
|
|
|
639
639
|
# --- Step 1: Detect explicitly manual criteria ---
|
|
640
640
|
if echo "$criterion" | grep -qP '\[type:manual\]'; then
|
|
@@ -562,6 +562,19 @@ If no downstream phases are found (the change is additive only — new sections,
|
|
|
562
562
|
|
|
563
563
|
---
|
|
564
564
|
|
|
565
|
+
## Phase A2.7: Pattern Sweep Intake *(conditional — skip silently when the investigation report has no `### Pattern Sweep` section)* <!-- Added: forge#3449 -->
|
|
566
|
+
|
|
567
|
+
For `review-finding` issues the investigator records a `### Pattern Sweep` table (`Query | Hit | Disposition`, one row per hit) listing every instance of the finding's defect class. Treat it as the seed for the plan:
|
|
568
|
+
|
|
569
|
+
1. Extract the section from `{INVESTIGATION_REPORT}` (the text between `### Pattern Sweep` and the next `###` heading). If it says `Pattern sweep skipped`, note that in the plan and continue.
|
|
570
|
+
2. Every hit with disposition `fix` becomes a **required row** in `### Affected Paths` — the plan must cover the whole class, not only the cited instance. A `truncated at 30` row means the sweep was capped: re-run the query yourself with a higher cap before sequencing.
|
|
571
|
+
3. Every `not-affected` hit needs its justification carried into the plan; if the reason does not hold after you trace the code, promote it to `fix`.
|
|
572
|
+
4. Add a Consistency Check: "all `fix` rows in the Pattern Sweep are changed and the test exercises a representative set of instances, not only the reviewer's repro".
|
|
573
|
+
|
|
574
|
+
The comment below passes the table through in a `### Pattern Sweep` section so the builder reads it from the plan. Absent table means this phase is a no-op.
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
565
578
|
## Phase A3: Consistency Rules
|
|
566
579
|
|
|
567
580
|
Identify invariants that ALL affected paths must satisfy. These become the builder's consistency checklist.
|
|
@@ -677,6 +690,13 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ARCHITECT -->
|
|
|
677
690
|
|---|------|----------------|-----------------|-----|
|
|
678
691
|
| 1 | {FILE} | {FUNCTION} | {CHANGE} | {REASON} |
|
|
679
692
|
|
|
693
|
+
### Pattern Sweep
|
|
694
|
+
<!-- Pass-through from the FORGE:INVESTIGATOR '### Pattern Sweep' table (Phase A2.7).
|
|
695
|
+
Omit this section when the investigation report had none. -->
|
|
696
|
+
| Query | Hit | Disposition |
|
|
697
|
+
|-------|-----|-------------|
|
|
698
|
+
| {QUERY} | {FILE:LINE} | {fix \| not-affected — reason} |
|
|
699
|
+
|
|
680
700
|
### Implementation Order
|
|
681
701
|
1. {FIRST_CHANGE} — {WHY_FIRST}
|
|
682
702
|
2. {SECOND_CHANGE} — {WHY_SECOND}
|
|
@@ -780,6 +800,7 @@ B4 → [THIS MODULE] Architecture Planning (FORGE:ARCHITECT comment)
|
|
|
780
800
|
Phase A1: Read Entry Points (A1.5 Prior Decision Injection, A1.6 Route-Tree Classification)
|
|
781
801
|
Phase A2: Trace the Data Flow (A2.1 Runtime UID x Volume Ownership, A2.2 Gate-Condition Caller Sweep)
|
|
782
802
|
Phase A2.5: Pipeline Phase-Dependency Check
|
|
803
|
+
Phase A2.7: Pattern Sweep Intake (review-finding issues only)
|
|
783
804
|
Phase A3: Consistency Rules
|
|
784
805
|
Phase A4: Sequence the Implementation
|
|
785
806
|
Phase A5: Risk Assessment
|
|
@@ -970,7 +970,7 @@ EXISTING_JSON=$(set -o pipefail; gh api "repos/{GH_REPO}/issues/{NUMBER}/comment
|
|
|
970
970
|
or ((.user.type // "") == "Bot")
|
|
971
971
|
or ((.user.login // "") as $x | $L | index($x) != null)
|
|
972
972
|
)] | length') || LOOKUP_FAILED=1
|
|
973
|
-
EXISTING_CONTEXT=$(printf '%s\n' "$EXISTING_JSON" | awk '{s+=$1} END {print s+0}')
|
|
973
|
+
EXISTING_CONTEXT=$(printf '%s\n' "$EXISTING_JSON" | awk '{s+=$(1)} END {print s+0}')
|
|
974
974
|
if [ "${DRY_RUN:-false}" = "true" ]; then
|
|
975
975
|
echo "[DRY_RUN] would post minimal FORGE:CONTEXT marker on #{NUMBER}"
|
|
976
976
|
elif [ "$LOOKUP_FAILED" -eq 1 ]; then
|
|
@@ -136,6 +136,7 @@ Work in `{WORKTREE_PATH}`. Follow the contract deliverables table exactly — im
|
|
|
136
136
|
- **Cross-lane import guard**: Before adding any `import` or `from X import Y` statement for a service-internal module (`app.*`), verify the module exists on the PR's base branch (`{PR_BASE}`) — NOT just on your local disk or a milestone branch. Run `git show origin/{PR_BASE}:{module_path}.py` (replacing dots with slashes) to confirm. If the module only exists on a milestone branch, do NOT import it — find an alternative implementation or make the import conditional with a `try/except ImportError` fallback. A milestone-only import on a fast-lane PR will crash production on every request with `ModuleNotFoundError`. <!-- Added: forge#277 -->
|
|
137
137
|
- **Deliverable-type consistency check**: Before committing, compare the actual output against the CONTRACT's deliverable list. If the CONTRACT explicitly states "no code changes required", "docs only", or "configuration update only" AND the diff introduces new executable files (`.py`, `.js`, `.ts`, `.sh` — not test, config, or documentation files), STOP. Do NOT commit. Re-read the CONTRACT, the investigation recommendation, and the ARCHITECT plan. If all three agree that code is needed, update the CONTRACT comment to reflect the new deliverable type before proceeding. If only the builder decided to add code without contract support, discard the code change and implement the contracted deliverable instead. <!-- Added: forge#279 -->
|
|
138
138
|
- **Pipeline check documentation — generalization rule**: When writing or updating pipeline check documentation (in `commands/*.md`), describe the **bug class**, not a specific incident. Do NOT embed: PR numbers, issue numbers, run IDs, timestamps, function names, dollar amounts, or multi-sentence incident timelines in check prose or `**Evidence**:` blocks. One brief HTML comment `<!-- Added: forge#NNN -->` is acceptable per check for traceability. `**Evidence**:` blocks must describe the vulnerability pattern (what the class of bug looks like, why it's dangerous) — not narrate a single historical occurrence. CHANGELOG entries may reference originating issues; command prompt text must not.
|
|
139
|
+
- **Pattern sweep — class-wide fix and test**: When the FORGE:ARCHITECT plan or FORGE:INVESTIGATOR report contains a `### Pattern Sweep` table (`Query | Hit | Disposition`), fix EVERY hit with disposition `fix`, not only the instance the reviewer cited. Write the test against a representative set of instances from the table, not the reviewer's single repro, so it fails for any sibling still carrying the defect. Do not tune a rule, matcher, or fixture to the cited example. If a `fix` row cannot be changed, or a `not-affected` row turns out to be affected, say so in the `### Approach` of the FORGE:BUILDER comment instead of dropping it silently. Absent table = no-op. <!-- Added: forge#3449 -->
|
|
139
140
|
- **Endpoint response contract consumer tracing**: When changing an endpoint's response body shape or status field values (e.g., changing `"status": "healthy"` to `"status": "ok"`, renaming response keys, removing fields), grep the full repo for ALL consumers of that response body before committing. Consumers are not limited to the service being changed — they include deploy scripts, CI health checks, monitoring configs, docker-compose healthcheck definitions, Traefik probes, and any script that parses or pattern-matches on the response body. Run: `grep -rn "{old_value}\|{endpoint_path}" scripts/ infra/ .github/ docker-compose*.yml traefik/ 2>/dev/null`. All consumers whose behavior depends on the old response format MUST be updated in the same PR — a response contract change that updates only one consumer while leaving others on the old format is a deploy-time breakage. <!-- Added: forge#321 -->
|
|
140
141
|
- If you discover the contract is wrong (e.g. a file doesn't exist, a function has a different signature): STOP, post a comment on the issue explaining the discrepancy, add label `needs-human`, and EXIT with `IMPLEMENT_RESULT: status: BLOCKED` and a `blocker` describing the discrepancy
|
|
141
142
|
|
|
@@ -269,6 +270,20 @@ git add {CHANGED_FILES}
|
|
|
269
270
|
|
|
270
271
|
Do NOT run `git commit` here. The commit (with conventional commit message and issue reference) is made by validate.md Phase V5 after `GATE_PASSED=true`.
|
|
271
272
|
|
|
273
|
+
**Informational diff size** <!-- Added: forge#3450 -->: after staging, read the changed-line count so the builder comment and result carry it. This is a hint only. Implement never gates or decides: the diff-size gate lives in build B5.5, which re-measures authoritatively. Leave `DIFF_LINES` empty when the script is unavailable or fails.
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# <Script resolution block from work-on/build.md, verbatim>
|
|
277
|
+
SCRIPT_REF=$(resolve_script 'diff-size')
|
|
278
|
+
DIFF_LINES=""
|
|
279
|
+
case "$SCRIPT_REF" in
|
|
280
|
+
prose:*) ;;
|
|
281
|
+
*) DIFF_LINES=$(bash "${SCRIPT_REF#*:}" --repo-path "{WORKTREE_PATH}" --base "{PR_BASE}" 2>/dev/null | sed -n 's/^diff_lines=//p' | head -n 1) || DIFF_LINES="" ;;
|
|
282
|
+
esac
|
|
283
|
+
case "$DIFF_LINES" in ''|*[!0-9]*) DIFF_LINES="" ;; esac
|
|
284
|
+
echo "DIFF_LINES=${DIFF_LINES}"
|
|
285
|
+
```
|
|
286
|
+
|
|
272
287
|
---
|
|
273
288
|
|
|
274
289
|
## Phase I5: Update Issue Body
|
|
@@ -293,6 +308,7 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILDER -->
|
|
|
293
308
|
**Branch**: \`{BRANCH}\`
|
|
294
309
|
**Commits**: {COMMIT_SHA(S)}
|
|
295
310
|
**Files changed**: {COUNT}
|
|
311
|
+
**Diff lines**: {DIFF_LINES or —}
|
|
296
312
|
|
|
297
313
|
### Approach
|
|
298
314
|
{One paragraph: what was built, key decisions, why this approach over alternatives}
|
|
@@ -325,6 +341,7 @@ IMPLEMENT_RESULT:
|
|
|
325
341
|
branch: {BRANCH}
|
|
326
342
|
commits: [{SHA}, ...]
|
|
327
343
|
files_changed: [{file}, ...]
|
|
344
|
+
diff_lines: {N — informational, from scripts/diff-size.sh after I4; empty when unavailable}
|
|
328
345
|
comment_url: {url of FORGE:BUILDER comment}
|
|
329
346
|
blocker: {description if status=BLOCKED}
|
|
330
347
|
```
|
|
@@ -161,7 +161,7 @@ SKIPPED_CHECKS=""
|
|
|
161
161
|
# formatter, typechecker, or build cannot block validation indefinitely.
|
|
162
162
|
# Set FORGEDOCK_VERIFICATION_TIMEOUT_SECONDS to override the 120-second default.
|
|
163
163
|
run_verification_command() {
|
|
164
|
-
local label="$1" command="$2"
|
|
164
|
+
local label="${1}" command="${2}"
|
|
165
165
|
local timeout_seconds="${FORGEDOCK_VERIFICATION_TIMEOUT_SECONDS:-120}"
|
|
166
166
|
if ! [[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]]; then
|
|
167
167
|
echo "VERIFY-CONFIG | HIGH | timeout | FORGEDOCK_VERIFICATION_TIMEOUT_SECONDS must be a positive integer (got '$timeout_seconds')"
|
|
@@ -245,7 +245,7 @@ KNOWN_SLOW_TESTS=$(yq -o=json -I=0 '.verification.known_slow_tests // []' "$FORG
|
|
|
245
245
|
# skip it entirely. Matching is substring match of `pattern` against the full
|
|
246
246
|
# command text. Exactly one of skip/subset is expected per matched entry.
|
|
247
247
|
apply_known_slow_filter() {
|
|
248
|
-
local cmd="$1"
|
|
248
|
+
local cmd="${1}"
|
|
249
249
|
local out="$cmd"
|
|
250
250
|
if [ -n "$KNOWN_SLOW_TESTS" ] && [ "$KNOWN_SLOW_TESTS" != "[]" ] && [ "$KNOWN_SLOW_TESTS" != "null" ]; then
|
|
251
251
|
while IFS= read -r entry; do
|
|
@@ -23,7 +23,7 @@ argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --base {PR_BAS
|
|
|
23
23
|
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (classification, label transitions) stay at this tier because they are interleaved with the reasoning-heavy child phases in the same run. <!-- Added: forge#1827 -->
|
|
24
24
|
**NEVER use plan mode (EnterPlanMode).**
|
|
25
25
|
|
|
26
|
-
**CRITICAL: You MUST execute ALL phases B0–B6.5 in order. Every child (B3 context, B4 architect, B5 implement, B6 validate) is invoked via `Skill(...)` — each is a forked sub-skill with its own isolated context. B3 and B4 are invoked for every complexity band except where the band is TRIVIAL or INVESTIGATION (B2/B2.1/B2.5 are also skipped for INVESTIGATION); the children post their own skip markers, so a skipped child is still visible on the issue. Skipping a child without the band justification degrades build quality and fails the phase-trail check.**
|
|
26
|
+
**CRITICAL: You MUST execute ALL phases B0–B6.5 (including B5.5) in order. Every child (B3 context, B4 architect, B5 implement, B6 validate) is invoked via `Skill(...)` — each is a forked sub-skill with its own isolated context. B3 and B4 are invoked for every complexity band except where the band is TRIVIAL or INVESTIGATION (B2/B2.1/B2.5 are also skipped for INVESTIGATION); the children post their own skip markers, so a skipped child is still visible on the issue. Skipping a child without the band justification degrades build quality and fails the phase-trail check.**
|
|
27
27
|
|
|
28
28
|
**Synchronous child consumption**: each child `Skill(...)` call (B3-B6) runs to completion in this phase's own turn, and you consume its result in that same turn. Never end or yield your turn to wait for a child, and never wait for a completion notification: notifications for forked children go to the root session, not to this phase, so a turn that yields is never resumed. A child return with no `*_RESULT:` block (running, backgrounded, empty) is not a result — re-read the GitHub markers for that child (`FORGE:CONTEXT`, `FORGE:ARCHITECT`, `FORGE:BUILDER`, `FORGE:VALIDATE`/`FORGE:QUALITY_GATE`) and apply the **bounded re-invoke rule**: re-invoke the same child with the same args at most 2 times per child on running/backgrounded/empty returns (the counter is per child and is not reset by a re-invoke). Before re-invoking, check state: if the child's marker is present, consume it instead of re-invoking; for implement and validate, also check the worktree (`git status --short` and `git log origin/{PR_BASE}..HEAD` in `{WORKTREE_PATH}`) — uncommitted in-flight edits or a fresh commit mean the original child may still be live, so wait for it by re-reading state rather than launching a second writer on the same worktree, and count that as a re-invoke attempt. After the cap is exhausted, take the Blocked exit (see Result emission and Blocked exit) with `blocker: child-stalled: <child>` (child = context|architect|implement|validate); it is terminal and is not re-invoked by the router.
|
|
29
29
|
|
|
@@ -82,7 +82,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
82
82
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
83
83
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
84
84
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
85
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
85
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
86
86
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
87
87
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
88
88
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -101,7 +101,7 @@ UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped,
|
|
|
101
101
|
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
102
102
|
|
|
103
103
|
resolve_script() {
|
|
104
|
-
local operation="$1"
|
|
104
|
+
local operation="${1}"
|
|
105
105
|
# Tier 2: per-repo adaptive (skip if disabled)
|
|
106
106
|
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
107
107
|
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
@@ -125,7 +125,7 @@ Every exit path of this skill — COMPLETE, ALREADY_DONE, INVESTIGATION_COMPLETE
|
|
|
125
125
|
|
|
126
126
|
```
|
|
127
127
|
BUILD_RESULT:
|
|
128
|
-
status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE | BLOCKED
|
|
128
|
+
status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE | NEEDS_DECOMPOSE | BLOCKED
|
|
129
129
|
branch: {BRANCH}
|
|
130
130
|
worktree: {WORKTREE_PATH}
|
|
131
131
|
blocker: {description if status=BLOCKED, else empty}
|
|
@@ -509,13 +509,101 @@ Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo
|
|
|
509
509
|
```
|
|
510
510
|
|
|
511
511
|
**After subcommand returns** (read its `IMPLEMENT_RESULT:` block):
|
|
512
|
-
- `status: COMPLETE` → continue to
|
|
513
|
-
- `status: ALREADY_DONE` → continue to
|
|
514
|
-
- `status: INVESTIGATION_COMPLETE` → issues were created as deliverables and the original closed; print `BUILD_RESULT: status: INVESTIGATION_COMPLETE` (skip B6/B6.5)
|
|
512
|
+
- `status: COMPLETE` → continue to B5.5
|
|
513
|
+
- `status: ALREADY_DONE` → continue to B5.5 (size-check what's already there)
|
|
514
|
+
- `status: INVESTIGATION_COMPLETE` → issues were created as deliverables and the original closed; print `BUILD_RESULT: status: INVESTIGATION_COMPLETE` (skip B5.5/B6/B6.5)
|
|
515
515
|
- `status: BLOCKED` → Blocked exit with the child's `blocker`
|
|
516
516
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:implement"
|
|
517
517
|
- Returned running/backgrounded/empty (no `IMPLEMENT_RESULT:`) → do not end the turn to wait; re-read the `FORGE:BUILDER` comment and the worktree state; if `FORGE:BUILDER:COMPLETE` is present consume it, and if the worktree shows in-flight edits or a fresh commit treat the child as possibly live; before re-invoking, apply the bounded re-invoke rule (at most 2, then `child-stalled: implement`)
|
|
518
|
-
# MUST CONTINUE to Phase
|
|
518
|
+
# MUST CONTINUE to Phase B5.5 — implement result is intermediate, NOT terminal (the size gate and validation still follow).
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Phase B5.5: Diff-Size Gate
|
|
523
|
+
|
|
524
|
+
Review findings per PR rise steeply with diff size, so an oversized diff is split before it is validated. This phase measures the staged diff (implement staged everything in I4; nothing is committed until validate V5) and either continues, routes to decompose, or requires a justified override. It never edits code. <!-- Added: forge#3450 -->
|
|
525
|
+
|
|
526
|
+
**Skip** (continue to B6) when the task type is Investigation (B5 already exited), or when `build.diff_size.threshold` resolves to `0`. Never skip because a lookup failed: unknown is not under threshold.
|
|
527
|
+
|
|
528
|
+
**Step 1 — measure.** Paste the Script resolution block first (shell state does not persist); quote every interpolation:
|
|
529
|
+
|
|
530
|
+
```bash
|
|
531
|
+
# <Script resolution block from above goes here, verbatim>
|
|
532
|
+
git -C "{WORKTREE_PATH}" fetch origin "{PR_BASE}" 2>/dev/null || true # fresh origin/base for the merge-base measurement
|
|
533
|
+
THRESHOLD=$(yq '.build.diff_size.threshold // 1000' forge.yaml 2>/dev/null || echo 1000)
|
|
534
|
+
EXCLUDES=()
|
|
535
|
+
while IFS= read -r _g; do [ -n "$_g" ] && [ "$_g" != "null" ] && EXCLUDES+=(--exclude-glob "$_g"); done \
|
|
536
|
+
< <(yq '.build.diff_size.exclude_globs // [] | .[]' forge.yaml 2>/dev/null || true)
|
|
537
|
+
SCRIPT_REF=$(resolve_script 'diff-size')
|
|
538
|
+
case "$SCRIPT_REF" in
|
|
539
|
+
prose:*) echo "SIZE_GATE: scripts/diff-size.sh unresolved (prose tier) — gate skipped with a note in the FORGE:BUILDER comment" ;;
|
|
540
|
+
*)
|
|
541
|
+
SIZE_OUT=$(bash "${SCRIPT_REF#*:}" --repo-path "{WORKTREE_PATH}" --base "{PR_BASE}" --threshold "$THRESHOLD" ${EXCLUDES[@]+"${EXCLUDES[@]}"}) && SIZE_RC=0 || SIZE_RC=$?
|
|
542
|
+
echo "SIZE_RC=${SIZE_RC}"; printf '%s\n' "$SIZE_OUT" ;;
|
|
543
|
+
esac
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
- Prose tier (script unresolvable) → skip the gate, continue to B6 (the script is part of the install; a missing install must not strand every build).
|
|
547
|
+
- Exit 2, empty output, or output without `diff_lines=`, `threshold=` and `over=` → Blocked exit with blocker `size-gate-unavailable: diff-size.sh failed (rc=<N>)`. Do NOT treat an unmeasurable diff as under threshold.
|
|
548
|
+
- Otherwise parse `diff_lines`, `excluded_lines`, `threshold`, `over` and the `top=` lines.
|
|
549
|
+
|
|
550
|
+
**Step 2 — look up an override** (only when `over=true`). Both lookups are anchored, trust-filtered through `scripts/trusted-comments.sh` (trusted association, Bot, or `FORGE_TRAIL_TRUSTED_LOGINS`), and fail closed:
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
# <Script resolution block from above goes here, verbatim>
|
|
554
|
+
set -o pipefail
|
|
555
|
+
TRUSTED_SCRIPT="${UNIVERSAL_DIR:+$UNIVERSAL_DIR/trusted-comments.sh}"
|
|
556
|
+
[ -n "$TRUSTED_SCRIPT" ] && [ -f "$TRUSTED_SCRIPT" ] || { echo "OVERRIDE_LOOKUP=FAILED (trusted-comments.sh unresolved)"; exit 0; }
|
|
557
|
+
OVR=$(gh api --paginate "repos/{GH_REPO}/issues/{NUMBER}/comments" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:SIZE_OVERRIDE') \
|
|
558
|
+
|| { echo "OVERRIDE_LOOKUP=FAILED"; exit 0; }
|
|
559
|
+
# A valid override has a non-empty justification: the first non-blank, non-marker line after the marker line.
|
|
560
|
+
JUSTIFICATION=$(printf '%s\n' "$OVR" | jq -r 'select(type == "string") | split("\n") | .[1:] | map(select(test("\\S") and (test("^\\s*<!--") | not))) | first // empty' | tail -n 1)
|
|
561
|
+
if [ -n "$JUSTIFICATION" ]; then echo "OVERRIDE=VALID"; else echo "OVERRIDE=NONE"; fi
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
- `OVERRIDE_LOOKUP=FAILED` → Blocked exit with blocker `size-gate-unavailable: could not read FORGE:SIZE_OVERRIDE` (a transient GitHub failure uses the `github-unavailable:` prefix per the retry rule above). Never read a failed lookup as "no override".
|
|
565
|
+
- `OVERRIDE=VALID` → result `OVERRIDDEN`. A marker with an empty justification is not valid.
|
|
566
|
+
|
|
567
|
+
**Step 3 — record.** Set `DIFF_LINES`, `EXCLUDED_LINES`, `THRESHOLD`, `RESULT` (`OK`, `OVERRIDDEN` or `OVER`) and `SPLIT_PROPOSAL` from the steps above, then post one `<!-- FORGE:DIFF_SIZE -->` comment, idempotently: find the latest trusted comment starting with the marker and edit it, otherwise create it (on resume never stack duplicates; readers use the latest trusted one):
|
|
568
|
+
|
|
569
|
+
```bash
|
|
570
|
+
BODY="<!-- FORGE:DIFF_SIZE -->
|
|
571
|
+
## Diff Size
|
|
572
|
+
|
|
573
|
+
diff_lines: ${DIFF_LINES}
|
|
574
|
+
excluded_lines: ${EXCLUDED_LINES}
|
|
575
|
+
threshold: ${THRESHOLD}
|
|
576
|
+
result: ${RESULT}
|
|
577
|
+
${SPLIT_PROPOSAL:-}"
|
|
578
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
579
|
+
CID=$(gh api --paginate "repos/{GH_REPO}/issues/{NUMBER}/comments" --jq '[.[] | select(.body | startswith("<!-- FORGE:DIFF_SIZE")) | select((.author_association | IN("OWNER","MEMBER","COLLABORATOR")) or (.user.type == "Bot"))] | last | .id // ""' 2>/dev/null || true)
|
|
580
|
+
if [ -n "$CID" ]; then run gh api "repos/{GH_REPO}/issues/comments/${CID}" -X PATCH -f body="$BODY" >/dev/null 2>&1 || run gh issue comment {NUMBER} {GH_FLAG} --body "$BODY"
|
|
581
|
+
else run gh issue comment {NUMBER} {GH_FLAG} --body "$BODY"; fi
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**Step 4 — decide.**
|
|
585
|
+
|
|
586
|
+
| Condition | Result | Action |
|
|
587
|
+
|-----------|--------|--------|
|
|
588
|
+
| `over=false` | `OK` | Post/refresh `FORGE:DIFF_SIZE`, continue to B6 |
|
|
589
|
+
| `over=true`, valid override | `OVERRIDDEN` | Post/refresh `FORGE:DIFF_SIZE` (quote the justification), continue to B6 |
|
|
590
|
+
| `over=true`, no override, issue body has a `**Parent**: #N` line OR a trusted `FORGE:DECOMPOSED` comment exists | `OVER` | Decompose-loop guard: do NOT route to decompose. Post/refresh `FORGE:DIFF_SIZE`, then Blocked exit with blocker `diff-size-over-threshold: {diff_lines} > {threshold}; post a FORGE:SIZE_OVERRIDE with a justification to proceed` |
|
|
591
|
+
| `over=true`, no override, otherwise | `OVER` | Put a `### Split Proposal` in the `FORGE:DIFF_SIZE` comment, then print `BUILD_RESULT: status: NEEDS_DECOMPOSE` (see below) |
|
|
592
|
+
|
|
593
|
+
**`### Split Proposal`** (written into the `FORGE:DIFF_SIZE` comment): list the top files by changed lines from the `top=` output, then propose 2-4 sub-issues split along directory or phase seams, each independently buildable and no more than about half the threshold, in dependency order, one `- **{title}** — {files}` line each (this is the plan `work-on:decompose` consumes in D0/D2).
|
|
594
|
+
|
|
595
|
+
**`NEEDS_DECOMPOSE` exit**: leave the staged worktree and branch as they are; do not add `needs-human`; do not post `FORGE:BUILDER:COMPLETE`; do NOT call `work-on:decompose` here (the router is the sole dispatcher). The labels stay `workflow:building`; decompose owns the `workflow:decomposed` transition. Print:
|
|
596
|
+
|
|
597
|
+
```
|
|
598
|
+
BUILD_RESULT:
|
|
599
|
+
status: NEEDS_DECOMPOSE
|
|
600
|
+
branch: {BRANCH}
|
|
601
|
+
worktree: {WORKTREE_PATH}
|
|
602
|
+
blocker: diff-size-over-threshold: {diff_lines} > {threshold}; split proposal posted in FORGE:DIFF_SIZE
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
**Resume**: a re-invoked build whose latest trusted `FORGE:DIFF_SIZE` says `result: OVER` re-measures; if still over with no override it re-emits `NEEDS_DECOMPOSE` without a duplicate comment. When a valid `FORGE:SIZE_OVERRIDE` has appeared since, B5.5 re-measures and continues via `OVERRIDDEN`.
|
|
606
|
+
# MUST CONTINUE to Phase B6 — the size gate result is intermediate, NOT terminal (validation still required), except the NEEDS_DECOMPOSE and Blocked exits above.
|
|
519
607
|
|
|
520
608
|
---
|
|
521
609
|
|
|
@@ -725,7 +813,7 @@ Print this structured block as your final reply (and nothing after it) — the r
|
|
|
725
813
|
|
|
726
814
|
```
|
|
727
815
|
BUILD_RESULT:
|
|
728
|
-
status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE | BLOCKED
|
|
816
|
+
status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE | NEEDS_DECOMPOSE | BLOCKED
|
|
729
817
|
branch: {BRANCH}
|
|
730
818
|
worktree: {WORKTREE_PATH}
|
|
731
819
|
blocker: {description if status=BLOCKED}
|
|
@@ -734,6 +822,7 @@ BUILD_RESULT:
|
|
|
734
822
|
- `COMPLETE` — B0–B6.5 all passed; `FORGE:BUILDER:COMPLETE` is on the issue.
|
|
735
823
|
- `ALREADY_DONE` — B0 found `FORGE:BUILDER:COMPLETE`.
|
|
736
824
|
- `INVESTIGATION_COMPLETE` — the implement child created the deliverable issues (B5); no review follows.
|
|
825
|
+
- `NEEDS_DECOMPOSE` — B5.5 measured the staged diff over `build.diff_size.threshold` with no valid `FORGE:SIZE_OVERRIDE`; a split proposal is in `FORGE:DIFF_SIZE`. The router runs `work-on:decompose`. Not a failure: no `needs-human`.
|
|
737
826
|
- `BLOCKED` — any guard or failure; `needs-human` is set and the blocker is posted.
|
|
738
827
|
|
|
739
828
|
---
|
|
@@ -112,7 +112,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
112
112
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
113
113
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
114
114
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
115
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
115
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
116
116
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
117
117
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
118
118
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -131,7 +131,7 @@ UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped,
|
|
|
131
131
|
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
132
132
|
|
|
133
133
|
resolve_script() {
|
|
134
|
-
local operation="$1"
|
|
134
|
+
local operation="${1}"
|
|
135
135
|
# Tier 2: per-repo adaptive (skip if disabled)
|
|
136
136
|
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
137
137
|
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
@@ -151,7 +151,7 @@ Workflow-state transitions use the tiered `transition-label` script (adaptive
|
|
|
151
151
|
|
|
152
152
|
```bash
|
|
153
153
|
set_workflow_state() {
|
|
154
|
-
local _issue="$1" _state="$2" _res _tier _path _s _rm=""
|
|
154
|
+
local _issue="${1}" _state="${2}" _res _tier _path _s _rm=""
|
|
155
155
|
_res=$(resolve_script 'transition-label'); _tier="${_res%%:*}"; _path="${_res#*:}"
|
|
156
156
|
case "$_tier" in
|
|
157
157
|
adaptive|universal) bash "$_path" "$_issue" $GH_FLAG "$_state" ;;
|
|
@@ -189,8 +189,8 @@ fi
|
|
|
189
189
|
# All agent comments (to reconstruct pipeline results) — one paginated read, reused below
|
|
190
190
|
COMMENTS_JSON=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments 2>/dev/null | jq -s 'add // []' 2>/dev/null || echo '[]')
|
|
191
191
|
[ -n "$COMMENTS_JSON" ] || COMMENTS_JSON='[]'
|
|
192
|
-
last_comment_body() { # $1 = marker substring; prints the newest matching comment body (or empty)
|
|
193
|
-
printf '%s' "$COMMENTS_JSON" | jq -r --arg m "$1" '[.[] | select(.body | contains($m)) | .body] | last // ""' 2>/dev/null
|
|
192
|
+
last_comment_body() { # ${1} = marker substring; prints the newest matching comment body (or empty)
|
|
193
|
+
printf '%s' "$COMMENTS_JSON" | jq -r --arg m "${1}" '[.[] | select(.body | contains($m)) | .body] | last // ""' 2>/dev/null
|
|
194
194
|
}
|
|
195
195
|
INVESTIGATOR_BODY=$(last_comment_body "FORGE:INVESTIGATOR")
|
|
196
196
|
BUILDER_BODY=$(last_comment_body "FORGE:BUILDER")
|
|
@@ -207,7 +207,7 @@ FAST_PATH_BODY=$(last_comment_body "FORGE:FAST_PATH")
|
|
|
207
207
|
|
|
208
208
|
```bash
|
|
209
209
|
# --- Lane: --base wins (staging -> fast, milestone/* -> feature); otherwise the PR's baseRefName; otherwise unknown ---
|
|
210
|
-
lane_from_base() { case "$1" in staging) echo fast ;; milestone/*) echo feature ;; *) echo "" ;; esac; }
|
|
210
|
+
lane_from_base() { case "${1}" in staging) echo fast ;; milestone/*) echo feature ;; *) echo "" ;; esac; }
|
|
211
211
|
LANE=$(lane_from_base "$PR_BASE")
|
|
212
212
|
[ -n "$LANE" ] || LANE=$(lane_from_base "$PR_BASE_REF")
|
|
213
213
|
if [ -z "$LANE" ]; then
|
|
@@ -390,7 +390,7 @@ fi
|
|
|
390
390
|
CHECKBOX_SECTIONS=$(printf '%s\n' "$BODY_STRIPPED" | awk '
|
|
391
391
|
/^#+ / { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
392
392
|
/^(=+|-+)$/ && previous != "" { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
393
|
-
{ if (in_section && /^[-*+] \[[ xX]\]/) has=1; previous=$0 }
|
|
393
|
+
{ if (in_section && /^[-*+] \[[ xX]\]/) has=1; previous=$(0) }
|
|
394
394
|
END { if (in_section && has) n++; print n+0 }
|
|
395
395
|
')
|
|
396
396
|
|
|
@@ -967,6 +967,23 @@ exceeds the field; pad shorter lines with spaces so the right border `║` stays
|
|
|
967
967
|
|
|
968
968
|
### C4.5c: Build the machine-readable twin
|
|
969
969
|
|
|
970
|
+
First read the build's diff size (forge#3450). `FORGE:DIFF_SIZE` is posted by `work-on:build` B5.5; use the latest TRUSTED one (anchored, through `scripts/trusted-comments.sh`; the Script resolution block applies). Absent data, or any lookup failure, renders `—` and is never `0`:
|
|
971
|
+
|
|
972
|
+
```bash
|
|
973
|
+
# <Script resolution block, verbatim>
|
|
974
|
+
DIFF_LINES="—"; DIFF_THRESHOLD="—"; SIZE_OVERRIDE_USED="no"
|
|
975
|
+
TRUSTED_SCRIPT="${UNIVERSAL_DIR:+$UNIVERSAL_DIR/trusted-comments.sh}"
|
|
976
|
+
if [ -n "$TRUSTED_SCRIPT" ] && [ -f "$TRUSTED_SCRIPT" ]; then
|
|
977
|
+
_ds=$(gh api --paginate "repos/{GH_REPO}/issues/{NUMBER}/comments" 2>/dev/null \
|
|
978
|
+
| bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:DIFF_SIZE' 2>/dev/null | tail -n 1 | jq -r 'select(type == "string")' 2>/dev/null) || _ds=""
|
|
979
|
+
_n=$(printf '%s\n' "$_ds" | sed -n 's/^diff_lines: *\([0-9][0-9]*\).*/\1/p' | head -n 1)
|
|
980
|
+
_t=$(printf '%s\n' "$_ds" | sed -n 's/^threshold: *\([0-9][0-9]*\).*/\1/p' | head -n 1)
|
|
981
|
+
[ -n "$_n" ] && DIFF_LINES="$_n"
|
|
982
|
+
[ -n "$_t" ] && DIFF_THRESHOLD="$_t"
|
|
983
|
+
printf '%s\n' "$_ds" | grep -q '^result: *OVERRIDDEN' && SIZE_OVERRIDE_USED="yes"
|
|
984
|
+
fi
|
|
985
|
+
```
|
|
986
|
+
|
|
970
987
|
Assemble the JSON object below. Its field set is exactly what Phase C5 passes to the codec `emit CARD --b64` call (including `title` and `blockers`); the JSON itself is for local use/debugging and is not embedded. Numeric stats that were `—`
|
|
971
988
|
become `null` in JSON; never emit `"—"` as a number.
|
|
972
989
|
|
|
@@ -981,12 +998,14 @@ CARD_JSON=$(jq -nc \
|
|
|
981
998
|
--arg commits "$COMMITS" --arg adds "$ADDITIONS" --arg dels "$DELETIONS" \
|
|
982
999
|
--arg review "$REVIEW_SUMMARY" --argjson blockers "${BLOCKERS:-0}" \
|
|
983
1000
|
--argjson elapsed "${ELAPSED_SECS:-0}" \
|
|
1001
|
+
--arg diff_lines "$DIFF_LINES" \
|
|
984
1002
|
'{issue:$issue, title:$title, status:$status, pipeline:$pipeline,
|
|
985
1003
|
pr:($pr|tonumber? // null), pr_target:$target,
|
|
986
1004
|
commits:($commits|tonumber? // null),
|
|
987
1005
|
additions:($adds|tonumber? // null),
|
|
988
1006
|
deletions:($dels|tonumber? // null),
|
|
989
|
-
review:$review, blockers:$blockers, elapsed_seconds:$elapsed
|
|
1007
|
+
review:$review, blockers:$blockers, elapsed_seconds:$elapsed,
|
|
1008
|
+
diff_lines:($diff_lines|tonumber? // null)}')
|
|
990
1009
|
```
|
|
991
1010
|
|
|
992
1011
|
---
|
|
@@ -1135,7 +1154,8 @@ if [ -n "$CODEC_CLI" ]; then
|
|
|
1135
1154
|
--field deletions="${DELETIONS}" \
|
|
1136
1155
|
--field review="${REVIEW_SUMMARY}" \
|
|
1137
1156
|
--field blockers="${BLOCKERS:-0}" \
|
|
1138
|
-
--field elapsed="${ELAPSED_SECS:-0}"
|
|
1157
|
+
--field elapsed="${ELAPSED_SECS:-0}" \
|
|
1158
|
+
--field diff_lines="${DIFF_LINES:-—}" 2>/dev/null) || CARD_LINE=""
|
|
1139
1159
|
fi
|
|
1140
1160
|
|
|
1141
1161
|
TRAJ_FILE=$(mktemp)
|
|
@@ -1163,6 +1183,8 @@ ${DECISIONS_BLOCK}
|
|
|
1163
1183
|
|
|
1164
1184
|
**Cost (economic scheduling)**: ${COST_DELTA_NOTE}
|
|
1165
1185
|
|
|
1186
|
+
**Diff size**: ${DIFF_LINES:-—} lines (threshold ${DIFF_THRESHOLD:-—}, override: ${SIZE_OVERRIDE_USED:-no})
|
|
1187
|
+
|
|
1166
1188
|
**Pipeline completed**: ${TIMESTAMP}
|
|
1167
1189
|
|
|
1168
1190
|
${CARD_LINE}
|
|
@@ -1184,7 +1206,7 @@ If `CLOSE_FAILED` is set, STOP: print `CLOSE_RESULT: status: FAILED` with `block
|
|
|
1184
1206
|
|
|
1185
1207
|
The `**Decisions**:` block MUST stay a bullet list and `**Decisions**:` must precede `**Anomalies**:` — the Phase C5.4 ADR extractor reads the lines between those two markers.
|
|
1186
1208
|
|
|
1187
|
-
The `<!-- FORGE:CARD: v1 sha:... b64:... -->` line carries the machine-readable summary (the Phase C4.5c fields plus `title` and `blockers`), encoded as Base64url (design decision 2026-07-08: encoding beats escaping — the Base64url alphabet cannot contain HTML comment delimiters by construction). It is wrapped in the inline-value annotation form `<!-- FORGE:CARD: ... -->` so `parse()` extracts the encoded payload. Platform consumers (e.g. `/orchestrate`) decode via `node "$CODEC_CLI" parse --type CARD [--field <key>]`. This block is **additive**: all existing `FORGE:TRAJECTORY` consumers select via `contains("FORGE:TRAJECTORY")` and parse the markdown table, so the embedded CARD line does not affect them.
|
|
1209
|
+
The `<!-- FORGE:CARD: v1 sha:... b64:... -->` line carries the machine-readable summary (the Phase C4.5c fields plus `title` and `blockers`, including `diff_lines`), encoded as Base64url (design decision 2026-07-08: encoding beats escaping — the Base64url alphabet cannot contain HTML comment delimiters by construction). It is wrapped in the inline-value annotation form `<!-- FORGE:CARD: ... -->` so `parse()` extracts the encoded payload. Platform consumers (e.g. `/orchestrate`) decode via `node "$CODEC_CLI" parse --type CARD [--field <key>]`. This block is **additive**: all existing `FORGE:TRAJECTORY` consumers select via `contains("FORGE:TRAJECTORY")` and parse the markdown table, so the embedded CARD line does not affect them.
|
|
1188
1210
|
|
|
1189
1211
|
**CODEC PATH (forge#1727)**: the `emit CARD --b64` call replaces the previous `<!-- FORGE:CARD ${CARD_JSON} -->` inline-JSON form. The Base64url form is safe against all HTML comment injection vectors and includes a sha8 integrity prefix for truncation detection. Consumers that parsed the old inline-JSON form must migrate to the codec parse path: `echo '...' | node "$CODEC_CLI" parse --type CARD --field <key>`.
|
|
1190
1212
|
|
|
@@ -1206,7 +1228,7 @@ Where:
|
|
|
1206
1228
|
|
|
1207
1229
|
```bash
|
|
1208
1230
|
# Re-index this issue and regenerate cost priors — non-blocking
|
|
1209
|
-
# Resolve from the ForgeDock install root (FORGE_ROOT, from the script-resolution block) — never from $0, which is meaningless inside a skill
|
|
1231
|
+
# Resolve from the ForgeDock install root (FORGE_ROOT, from the script-resolution block) — never from ${0}, which is meaningless inside a skill
|
|
1210
1232
|
INDEXER_PATH=""
|
|
1211
1233
|
[ -n "${FORGE_ROOT:-}" ] && INDEXER_PATH="$FORGE_ROOT/scripts/build-knowledge-index.mjs"
|
|
1212
1234
|
[ -f "$INDEXER_PATH" ] || INDEXER_PATH="$REPO_PATH/scripts/build-knowledge-index.mjs"
|
|
@@ -76,7 +76,7 @@ if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$_h" in /*) FORGE_ROOT="$_h" ;; esac
|
|
|
76
76
|
# Codex: install-codex.sh records the clone path in $CODEX_HOME/forge-home (one absolute path); skills are generated files, not symlinks.
|
|
77
77
|
_cx="${CODEX_HOME:-$HOME/.codex}"; case "$_cx" in /*) _x="$(head -n 1 "$_cx/forge-home" 2>/dev/null || true)" ;; *) _x="" ;; esac
|
|
78
78
|
# newest cached version first: numeric major.minor.patch of the version dir name only (non-semver names such as commit SHAs are skipped); a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
79
|
-
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
79
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ -v mk="$_mk" '$(NF-2)==mk && $(NF-1)=="forgedock" && $NF ~ /^[0-9]+\.[0-9]+\.[0-9]+(-.*)?$/{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5- || true)"
|
|
80
80
|
_m="$HOME/.claude/plugins/marketplaces/$_mk"
|
|
81
81
|
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
82
82
|
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_x" "$_v" "$_m")"
|
|
@@ -95,7 +95,7 @@ UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped,
|
|
|
95
95
|
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
96
96
|
|
|
97
97
|
resolve_script() {
|
|
98
|
-
local operation="$1"
|
|
98
|
+
local operation="${1}"
|
|
99
99
|
# Tier 2: per-repo adaptive (skip if disabled)
|
|
100
100
|
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
101
101
|
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
@@ -164,7 +164,7 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
|
164
164
|
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR") | not) | {author: .user.login, body: .body}'
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
Scan non-agent comments for override signals — phrases like "do not", "do NOT", "instead", "revert", "remove this", "override", "actually", or explicit disagreement with the investigation's recommendation. If an override comment is found from a repo owner or admin (not a bot):
|
|
167
|
+
Ignore machine-posted marker comments (`FORGE:DIFF_SIZE`, `FORGE:SIZE_OVERRIDE`) in this scan: a `FORGE:DIFF_SIZE` comment is a plan source (below), never an owner override direction. Scan non-agent comments for override signals — phrases like "do not", "do NOT", "instead", "revert", "remove this", "override", "actually", or explicit disagreement with the investigation's recommendation. If an override comment is found from a repo owner or admin (not a bot):
|
|
168
168
|
|
|
169
169
|
1. **Document the override**: Note which direction the owner is steering (e.g., "remove the feature" vs. investigation's "keep with warnings")
|
|
170
170
|
2. **Re-derive sub-issue scopes**: Derive sub-issue titles, bodies, and file scope from the override direction — NOT from the original investigation recommendation. The investigation's Decomposition Assessment may list sub-issues that are now stale or contradictory with the override.
|
|
@@ -177,6 +177,20 @@ Scan non-agent comments for override signals — phrases like "do not", "do NOT"
|
|
|
177
177
|
|
|
178
178
|
Extract from investigation report:
|
|
179
179
|
- Decomposition Assessment section: list of proposed sub-issues with titles and dependencies
|
|
180
|
+
|
|
181
|
+
**Size-gate plan source** <!-- Added: forge#3450 -->: when the build's diff-size gate routed here, the investigation said no decomposition, so its assessment lists no sub-issues. Read the latest TRUSTED `FORGE:DIFF_SIZE` comment (anchored, through `scripts/trusted-comments.sh`; the Script resolution block applies) and use it when it has `result: OVER`:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# <Script resolution block, verbatim>
|
|
185
|
+
set -o pipefail
|
|
186
|
+
TRUSTED_SCRIPT="${UNIVERSAL_DIR:+$UNIVERSAL_DIR/trusted-comments.sh}"
|
|
187
|
+
if [ -n "$TRUSTED_SCRIPT" ] && [ -f "$TRUSTED_SCRIPT" ]; then
|
|
188
|
+
SIZE_PLAN=$(gh api --paginate "repos/{GH_REPO}/issues/{NUMBER}/comments" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:DIFF_SIZE' | jq -r 'select(type == "string")' ) \
|
|
189
|
+
&& echo "SIZE_PLAN_READ=ok" || echo "SIZE_PLAN_READ=FAILED"
|
|
190
|
+
else echo "SIZE_PLAN_READ=FAILED"; fi
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
A `FAILED` read or a comment without `result: OVER` means there is no size-gate plan (the assessment rules below then apply unchanged). Its `### Split Proposal` lines (`- **{title}** — {files}`) are the proposed sub-issues, in dependency order, for Phase D2.
|
|
180
194
|
- Milestone (from issue metadata)
|
|
181
195
|
- Priority label (P0/P1/P2) from issue labels
|
|
182
196
|
|
|
@@ -203,7 +217,7 @@ If `PARENT_REF` is non-empty → BLOCKED, blocker: "Issue #{NUMBER} is a sub-iss
|
|
|
203
217
|
|
|
204
218
|
**Guard 2 — investigation report present**: If the FORGE:INVESTIGATOR comment is absent → BLOCKED, blocker: "No investigation report found — run investigate first".
|
|
205
219
|
|
|
206
|
-
**Guard 3 — decomposition plan present**: If the investigation report has no Decomposition Assessment section, OR the assessment does not list any sub-issues → BLOCKED, blocker: "Investigation report has no decomposition plan — re-run investigate with explicit decomposition scope".
|
|
220
|
+
**Guard 3 — decomposition plan present**: If the investigation report has no Decomposition Assessment section, OR the assessment does not list any sub-issues, AND there is no trusted `FORGE:DIFF_SIZE` comment with `result: OVER` and a `### Split Proposal` (the size-gate plan source from D0) → BLOCKED, blocker: "Investigation report has no decomposition plan — re-run investigate with explicit decomposition scope".
|
|
207
221
|
|
|
208
222
|
**Resume check**:
|
|
209
223
|
|
|
@@ -246,7 +260,7 @@ If `GIST_URLS` is non-empty, a `## Prior Investigation` section will be appended
|
|
|
246
260
|
|
|
247
261
|
## Phase D2: Design Sub-Issues
|
|
248
262
|
|
|
249
|
-
From the Decomposition Assessment in the investigation report (adjusted for any owner override detected in Phase D0), extract:
|
|
263
|
+
From the Decomposition Assessment in the investigation report (adjusted for any owner override detected in Phase D0), or, when the assessment lists none, from the size-gate `### Split Proposal` in the trusted `FORGE:DIFF_SIZE` comment (Phase D0), extract:
|
|
250
264
|
1. Sub-issue titles (in dependency order — independent issues first)
|
|
251
265
|
2. Dependencies between sub-issues (if issue B depends on issue A, A is created first)
|
|
252
266
|
3. Brief description for each sub-issue body
|