forgedock 1.13.1 → 1.15.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/phases-args.test.mjs +11 -0
- package/bin/engine/phases.mjs +268 -25
- package/bin/engine/phases.test.mjs +24 -0
- package/bin/engine-cli.mjs +18 -5
- package/bin/engine.mjs +149 -12
- package/bin/forgedock.mjs +4 -1
- package/commands/issue.md +17 -2
- package/commands/orchestrate/phase-3-dependency.md +5 -2
- package/commands/orchestrate/phase-4-execution.md +154 -27
- package/commands/pipeline-health/phase-2-metrics.md +5 -3
- package/commands/review-pr-staging.md +554 -54
- package/commands/review-pr.md +662 -51
- package/commands/validate.md +11 -3
- package/commands/work-on/build/architect.md +11 -8
- package/commands/work-on/build/implement.md +15 -1
- package/commands/work-on/build.md +34 -10
- package/commands/work-on/close.md +5 -4
- package/commands/work-on/decompose.md +7 -2
- package/commands/work-on/investigate.md +13 -0
- package/commands/work-on/remediate.md +332 -91
- package/commands/work-on/review.md +3 -3
- package/docs/CONFIG.md +14 -1
- package/docs/RELEASE-NOTES.md +20 -0
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +6 -0
- package/scripts/architect-resume-match.test.sh +53 -0
- package/scripts/check-spec-bash.sh +73 -3
- package/scripts/check-spec-bash.test.sh +50 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/checkbox-sections.test.sh +19 -0
- package/scripts/classify-finding.sh +103 -1
- package/scripts/classify-finding.test.sh +56 -0
- package/scripts/forge-root.test.sh +86 -2
- package/scripts/orchestrate-soft-edge.test.sh +58 -0
- package/scripts/remediate-approved-head.test.sh +43 -0
- package/scripts/remediate-basesync.test.sh +62 -3
- package/scripts/remediation-marker-match.test.sh +6 -0
- package/scripts/resolve-deploy-gate.sh +51 -0
- package/scripts/resolve-deploy-gate.test.sh +59 -0
- package/scripts/review-delta.sh +3 -2
- package/scripts/review-delta.test.sh +1 -1
- package/scripts/review-pr-delta-wiring.test.sh +274 -0
- package/scripts/review-pr-domain-map.test.sh +10 -0
- package/scripts/review-pr-staging-scope.test.sh +159 -0
- package/scripts/trusted-comments.test.sh +11 -0
package/commands/validate.md
CHANGED
|
@@ -184,9 +184,17 @@ if [ -n "$INPUT_ISSUE" ] && [ -n "$GH_FLAG" ]; then
|
|
|
184
184
|
esac
|
|
185
185
|
|
|
186
186
|
# Resolve and call transition-label.sh --validate
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
# Trusted-install resolution only; never the working tree / REPO_PATH (#3400/#3483, #3596)
|
|
188
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
189
|
+
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
190
|
+
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
191
|
+
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(find -L "$_cfg/plugins/cache/forgedock/forgedock" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | awk -F/ '$NF ~ /^[0-9]+\.[0-9]+\.[0-9]+$/{split($NF,a,".");printf "%d %d %d %s\n",a[1],a[2],a[3],$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr | cut -d' ' -f4- || true)"
|
|
192
|
+
done
|
|
193
|
+
SCRIPT=""
|
|
194
|
+
while IFS= read -r _c; do
|
|
195
|
+
case "$_c" in /*) [ -z "$SCRIPT" ] && [ -f "$_c/scripts/transition-label.sh" ] && SCRIPT="$_c/scripts/transition-label.sh" ;; esac
|
|
196
|
+
done <<< "$_tc"
|
|
197
|
+
if [ -n "$SCRIPT" ]; then
|
|
190
198
|
bash "$SCRIPT" --validate "$FORGE_VERDICT" "$INPUT_ISSUE" "$GH_FLAG" || true
|
|
191
199
|
else
|
|
192
200
|
# Prose fallback: apply directly via gh
|
|
@@ -80,19 +80,22 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
|
80
80
|
|
|
81
81
|
## Resume Check
|
|
82
82
|
|
|
83
|
-
Before doing any work:
|
|
83
|
+
Before doing any work, select the architect comments with ONE anchored, trusted selector (body must start with the marker; author must be a Bot or OWNER/MEMBER/COLLABORATOR; mirrors `scripts/trusted-comments.sh`). A comment that merely mentions the marker (an INVESTIGATOR report, a CONTEXT briefing) never matches. <!-- Added: forge#3648 -->
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
gh api repos/{GH_REPO}/issues/{NUMBER}/comments
|
|
87
|
-
--jq '.[] | select(.body | contains("FORGE:ARCHITECT")) | .body'
|
|
86
|
+
ARCH=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments | jq -s '[.[][] | select((.body // "") | startswith("<!-- FORGE:ARCHITECT -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:ARCHITECT:COMPLETE -->")), other: (.body | test("<!-- FORGE:(INVESTIGATOR|CONTEXT|CONTRACT|BUILDER)"))}]') || ARCH=""
|
|
88
87
|
```
|
|
89
88
|
|
|
90
|
-
- If
|
|
91
|
-
- If
|
|
89
|
+
- If `ARCH` is empty (unparsable) → fail closed: delete nothing and treat as no plan.
|
|
90
|
+
- If any element of `ARCH` has `complete == true` → plan already complete: print `ARCHITECT_RESULT:` with `status: COMPLETE` and the existing comment's `comment_url`, EXIT.
|
|
91
|
+
- If anchored trusted comments exist but none is complete → plan was interrupted. Delete the partial comment ONLY when exactly one such comment exists and it carries no other FORGE phase marker (`other == false`); zero or multiple matches delete nothing (log and continue):
|
|
92
92
|
```bash
|
|
93
|
-
COMMENT_ID=$(
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
COMMENT_ID=$(printf '%s' "$ARCH" | jq -r '[.[] | select(.complete | not)] | if length == 1 and (.[0].other | not) then .[0].id else empty end')
|
|
94
|
+
if [ -n "$COMMENT_ID" ]; then
|
|
95
|
+
gh api repos/{GH_REPO}/issues/comments/$COMMENT_ID -X DELETE
|
|
96
|
+
else
|
|
97
|
+
echo "architect resume: no single safe partial plan comment — deleting nothing"
|
|
98
|
+
fi
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
---
|
|
@@ -116,7 +116,21 @@ Extract from contract:
|
|
|
116
116
|
| Refactor / Maintenance | Implement directly following contract deliverables |
|
|
117
117
|
| Investigation | Spawn research agents, create GitHub issues for findings, skip to I5 |
|
|
118
118
|
|
|
119
|
-
**Investigation task special case**: Research deeply, create GitHub issues for each finding using the Pipeline Issue Template (see `commands/issue.md` § "Pipeline Issue Template"). Each issue MUST include `## Problem`, `## Affected Files`, and `## Acceptance Criteria`. Create each issue via the `/issue` create-hook's programmatic invocation contract (see `commands/issue.md` § "Programmatic Invocation Contract") — `Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"...\" --body-file <path> --label ...")` — instead of calling the raw issue-creation command directly; this gets dedup and body validation for free. Post
|
|
119
|
+
**Investigation task special case**: Research deeply, create GitHub issues for each finding using the Pipeline Issue Template (see `commands/issue.md` § "Pipeline Issue Template"). Each issue MUST include `## Problem`, `## Affected Files`, and `## Acceptance Criteria`. Create each issue via the `/issue` create-hook's programmatic invocation contract (see `commands/issue.md` § "Programmatic Invocation Contract") — `Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"...\" --body-file <path> --label ...")` — instead of calling the raw issue-creation command directly; this gets dedup and body validation for free. Post the durable deliverables comment, remove the stale `workflow:building` label, close the original issue, and print `IMPLEMENT_RESULT: status: INVESTIGATION_COMPLETE` as the final reply (the build skill passes this up and the router skips review). <!-- Added: forge#2090 -->
|
|
120
|
+
|
|
121
|
+
The deliverables comment MUST be posted BEFORE the issue is closed, MUST start with the marker line below as its first line, and lists each created issue. The engine reads this marker (not the free-text result) to tell a legitimate investigation exit from a crashed build, since an investigation posts no `FORGE:BUILDER:COMPLETE` and has no commits. <!-- Added: forge#3534 -->
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
DELIVERABLES_BODY="<!-- FORGE:INVESTIGATION:DELIVERABLES -->
|
|
125
|
+
## Investigation Deliverables
|
|
126
|
+
|
|
127
|
+
- #{CREATED_ISSUE_1}: {title}
|
|
128
|
+
- #{CREATED_ISSUE_2}: {title}"
|
|
129
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "$DELIVERABLES_BODY" # allowlist:check-command-side-effects
|
|
130
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:building" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Resume: if a `<!-- FORGE:INVESTIGATION:DELIVERABLES -->` comment already exists on the issue, do not research or create issues again; ensure the label cleanup and close are done and print `INVESTIGATION_COMPLETE`.
|
|
120
134
|
|
|
121
135
|
---
|
|
122
136
|
|
|
@@ -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 (including B5.5) in order (the
|
|
26
|
+
**CRITICAL: You MUST execute ALL phases B0–B6.5 (including B5.5) in order (the exceptions are the B0 size-gate resume branch, which enters at B5.5, and the B0 gate-resume branch, which enters at B6.5, each with its state verified). 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
|
|
|
@@ -172,7 +172,10 @@ EXISTING_FAST_PATH=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments --paginate
|
|
|
172
172
|
```
|
|
173
173
|
|
|
174
174
|
**Resume check**:
|
|
175
|
-
- If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment →
|
|
175
|
+
- If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment → the builder finished, but COMPLETE is posted at validate V5, BEFORE the B6.5 acceptance gate, so it is not proof the build passed. **Gate resume** (checked first; first match wins) <!-- Added: forge#3725 -->: read the newest trusted, current-cycle (after the newest `FORGE:CYCLE`) comment that starts with `FORGE:ACCEPTANCE_GATE` or `FORGE:BUILD_BLOCKED`, through the same anchored, trust-filtered lookup as the size-gate resume below (script resolution block verbatim; use `bodies '^<!-- FORGE:(ACCEPTANCE_GATE|BUILD_BLOCKED)'`, and compare by comment order, never by presence):
|
|
176
|
+
- Lookup failed (`trusted-comments.sh` unresolved or `gh` error) → Blocked exit with blocker `gate-lookup-unavailable: could not read FORGE:ACCEPTANCE_GATE` (a transient GitHub failure uses the `github-unavailable:` prefix). Never read a failed lookup as "no gate marker".
|
|
177
|
+
- Newest is `FORGE:ACCEPTANCE_GATE:FAILED`, `FORGE:ACCEPTANCE_GATE:BLOCKED`, or a `FORGE:BUILD_BLOCKED` comment → the gate is blocked and the engine fails closed on it, so returning ALREADY_DONE would deadlock every re-run. Derive BRANCH/WORKTREE_PATH/PR_BASE as in B1A/B1C (without creating anything), verify the branch and worktree exist and that `git -C "$WORKTREE_PATH" branch --show-current` equals BRANCH (missing or mismatched → Blocked exit `worktree-missing: cannot re-enter B6.5`; never recreate silently), recover `COMPLEXITY_BAND` from `EXISTING_FAST_PATH` with the B0.5 resume extraction, then skip B0.5-B6 and re-enter at **Phase B6.5**, which posts a fresh PASSED/FAILED/BLOCKED verdict (the one-repair `FORGE:ACCEPTANCE_REPAIR` bound still applies).
|
|
178
|
+
- Newest is `FORGE:ACCEPTANCE_GATE:PASSED`, or no gate marker exists → build already complete. Derive BRANCH/WORKTREE_PATH as in B1A/B1C (without creating anything), then print `BUILD_RESULT: status: ALREADY_DONE`.
|
|
176
179
|
- **Size-gate resume** (checked BEFORE the partial-BUILDER rule below; first match wins) <!-- Added: forge#3463 -->: if a `<!-- FORGE:BUILDER -->` comment exists without `<!-- FORGE:BUILDER:COMPLETE -->` AND the latest trusted `FORGE:DIFF_SIZE` comment has `result: OVER`, the build stopped at a deliberate B5.5 exit (`NEEDS_DECOMPOSE` or the decompose-loop-guard Blocked exit), not in a crash. Keep the BUILDER comment, do not re-run B2-B5, and re-enter at **Phase B5.5**. Read the marker through the same anchored, trust-filtered lookup B5.5 uses (script resolution block verbatim):
|
|
177
180
|
```bash
|
|
178
181
|
# <Script resolution block from above goes here, verbatim>
|
|
@@ -384,15 +387,18 @@ Every item in the contract's `### Out of Scope` section must carry exactly one d
|
|
|
384
387
|
```bash
|
|
385
388
|
# <Script resolution block from work-on/build.md, verbatim>
|
|
386
389
|
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
390
|
+
# Trusted-author filter (OWNER/MEMBER/COLLABORATOR or Bot, same predicate as trusted-comments.sh) is applied at
|
|
391
|
+
# the source in the LOOKUP --jq, before the path match: an untrusted match is never reused. The REST search API is used because it exposes
|
|
392
|
+
# author_association and user.type (`gh issue list --json` has no authorAssociation field).
|
|
387
393
|
# For each deferred item (DEFERRED_PATH = the backticked path, DEFERRED_WHY = one line), repeat:
|
|
388
394
|
DEFERRED_MARKER="<!-- FORGE:DEFERRED_FROM: #{NUMBER} -->"
|
|
389
|
-
LOOKUP=$(gh
|
|
390
|
-
|
|
395
|
+
LOOKUP=$(gh api -X GET search/issues -f per_page=50 \
|
|
396
|
+
-f q="\"FORGE:DEFERRED_FROM: #{NUMBER}\" in:body repo:{GH_REPO} is:issue is:open" --jq '[.items[] | select(((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR")) or ((.user.type // "") == "Bot"))]' 2>/dev/null) || LOOKUP="FAILED"
|
|
391
397
|
if [ "$LOOKUP" = "FAILED" ]; then
|
|
392
398
|
echo "DEFERRED_LOOKUP=FAILED" # a failed lookup is not "none found": Blocked exit, never re-file blind
|
|
393
399
|
else
|
|
394
400
|
DEFERRED_NUM=$(printf '%s' "$LOOKUP" | jq -r --arg p "$DEFERRED_PATH" \
|
|
395
|
-
'[.[] | select(.body | contains($p))] | first | .number // empty' 2>/dev/null)
|
|
401
|
+
'[.[] | select((.body // "") | contains($p))] | first | .number // empty' 2>/dev/null)
|
|
396
402
|
if [ -z "$DEFERRED_NUM" ]; then
|
|
397
403
|
DEFERRED_URL=$(run gh issue create {GH_FLAG} --title "follow-up: ${DEFERRED_PATH} (deferred from #{NUMBER})" \
|
|
398
404
|
--body "## Problem
|
|
@@ -417,6 +423,7 @@ ${DEFERRED_MARKER}")
|
|
|
417
423
|
fi
|
|
418
424
|
```
|
|
419
425
|
|
|
426
|
+
- Only follow-up issues authored by a trusted author (OWNER/MEMBER/COLLABORATOR or Bot) are reused; an untrusted match carrying the marker is ignored and a fresh follow-up is filed.
|
|
420
427
|
- `DEFERRED_LOOKUP=FAILED`, or an empty `DEFERRED_NUM` after a real (non-`DRY_RUN`) create → Blocked exit with blocker `github-unavailable: could not file or look up deferred follow-up for <path>` (a transient failure follows the retry rule above; no `needs-human` for `github-unavailable:`).
|
|
421
428
|
- On resume the lookup reuses the open issue that carries the marker and the same path, so a retried build never files a duplicate.
|
|
422
429
|
- Prefer filing through the `/issue` create-hook's programmatic contract (`Skill(skill="{FORGE_SKILL_PREFIX}issue", ...)`) when it is available; the raw create above is the fallback and must keep the `FORGE:DEFERRED_FROM` marker in the body.
|
|
@@ -603,7 +610,7 @@ Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo
|
|
|
603
610
|
**After subcommand returns** (read its `IMPLEMENT_RESULT:` block):
|
|
604
611
|
- `status: COMPLETE` → continue to B5.5
|
|
605
612
|
- `status: ALREADY_DONE` → continue to B5.5 (size-check what's already there)
|
|
606
|
-
- `status: INVESTIGATION_COMPLETE` → issues were created as deliverables and the original closed; print `BUILD_RESULT: status: INVESTIGATION_COMPLETE` (skip B5.5/B6/B6.5)
|
|
613
|
+
- `status: INVESTIGATION_COMPLETE` → issues were created as deliverables and the original closed; verify a `<!-- FORGE:INVESTIGATION:DELIVERABLES -->` comment exists on the issue (the implement child posts it and removes `workflow:building`; if it is missing, post it listing the created issues and remove the label), then print `BUILD_RESULT: status: INVESTIGATION_COMPLETE` (skip B5.5/B6/B6.5). The engine keys off that marker, not the result text. <!-- Added: forge#3534 -->
|
|
607
614
|
- `status: BLOCKED` → Blocked exit with the child's `blocker`
|
|
608
615
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:implement"
|
|
609
616
|
- 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`)
|
|
@@ -688,9 +695,26 @@ else run gh issue comment {NUMBER} {GH_FLAG} --body "$BODY"; fi
|
|
|
688
695
|
| `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` |
|
|
689
696
|
| `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) |
|
|
690
697
|
|
|
691
|
-
**`### 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).
|
|
698
|
+
**`### 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} — wip: {WIP_REF}@{WIP_SHA} (git checkout {WIP_SHA} -- {files})` line each (this is the plan `work-on:decompose` consumes in D0/D2). The `wip:` part comes from the persist step below; when persistence fell back to a patch file write `patch: {PATCH_PATH}` instead, and when both failed write `wip: unavailable`. Lines without the suffix stay valid.
|
|
692
699
|
|
|
693
|
-
|
|
700
|
+
**Persist the staged hunks** (before the `NEEDS_DECOMPOSE` exit; runs once the `FORGE:DIFF_SIZE` result is `OVER` and the decompose-loop guard did not fire) <!-- Added: forge#3573 -->: commit the staged index to a standalone commit object WITHOUT moving the build branch or the index, and push it to a dedicated ref named only from the issue number (never interpolate a title or file name into a ref). Re-running overwrites the same ref, so resume is idempotent. Never block `NEEDS_DECOMPOSE` on a persistence failure.
|
|
701
|
+
|
|
702
|
+
```bash
|
|
703
|
+
WIP_REF="forge/wip/{NUMBER}"
|
|
704
|
+
WIP_SHA=""; PATCH_PATH=""
|
|
705
|
+
TREE=$(git -C "{WORKTREE_PATH}" write-tree) \
|
|
706
|
+
&& WIP_SHA=$(git -C "{WORKTREE_PATH}" commit-tree "$TREE" -p HEAD -m "wip: staged hunks for #{NUMBER} (diff-size OVER)") || WIP_SHA=""
|
|
707
|
+
if [ -n "$WIP_SHA" ] && git -C "{WORKTREE_PATH}" push --force origin "${WIP_SHA}:refs/heads/${WIP_REF}" 2>/dev/null; then
|
|
708
|
+
echo "WIP_REF=${WIP_REF}@${WIP_SHA}"
|
|
709
|
+
else
|
|
710
|
+
PATCH_PATH="$(git -C "{WORKTREE_PATH}" rev-parse --git-common-dir)/forge-wip-{NUMBER}.patch"
|
|
711
|
+
git -C "{WORKTREE_PATH}" diff --cached --binary > "$PATCH_PATH" 2>/dev/null && echo "PATCH_PATH=${PATCH_PATH}" || echo "WIP=unavailable"
|
|
712
|
+
fi
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
The checkout command in each line restores that seam's files from the persisted commit (`git fetch origin {WIP_REF}` first in a fresh clone); `git diff {WIP_SHA}^ {WIP_SHA} -- {files}` shows them. Post `FORGE:DIFF_SIZE` (Step 3) after this step so the proposal carries the references.
|
|
716
|
+
|
|
717
|
+
**`NEEDS_DECOMPOSE` exit**: leave the staged worktree and branch as they are (the persisted WIP ref or patch is an additional durable copy, not a replacement); 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:
|
|
694
718
|
|
|
695
719
|
```
|
|
696
720
|
BUILD_RESULT:
|
|
@@ -700,7 +724,7 @@ BUILD_RESULT:
|
|
|
700
724
|
blocker: diff-size-over-threshold: {diff_lines} > {threshold}; split proposal posted in FORGE:DIFF_SIZE
|
|
701
725
|
```
|
|
702
726
|
|
|
703
|
-
**Resume**: a re-invoked build re-enters here via the B0 size-gate resume branch (partial `FORGE:BUILDER` plus latest trusted `FORGE:DIFF_SIZE` `result: OVER` plus a staged diff in the kept worktree), so it does not re-run implement; this covers both exits that leave BUILDER incomplete, `NEEDS_DECOMPOSE` and the decompose-loop-guard Blocked exit. B5.5 re-measures: if still over with no override it re-emits `NEEDS_DECOMPOSE` without a duplicate comment (the loop-guard case re-emits the same Blocked exit); when a valid `FORGE:SIZE_OVERRIDE` has appeared since, it continues via `OVERRIDDEN`.
|
|
727
|
+
**Resume**: a re-invoked build re-enters here via the B0 size-gate resume branch (partial `FORGE:BUILDER` plus latest trusted `FORGE:DIFF_SIZE` `result: OVER` plus a staged diff in the kept worktree), so it does not re-run implement; this covers both exits that leave BUILDER incomplete, `NEEDS_DECOMPOSE` and the decompose-loop-guard Blocked exit. B5.5 re-measures: if still over with no override it re-pushes the same `forge/wip/{NUMBER}` ref (idempotent) and re-emits `NEEDS_DECOMPOSE` without a duplicate comment (the loop-guard case re-emits the same Blocked exit); when a valid `FORGE:SIZE_OVERRIDE` has appeared since, it continues via `OVERRIDDEN`.
|
|
704
728
|
# MUST CONTINUE to Phase B6 — the size gate result is intermediate, NOT terminal (validation still required), except the NEEDS_DECOMPOSE and Blocked exits above.
|
|
705
729
|
|
|
706
730
|
---
|
|
@@ -729,7 +753,7 @@ CHANGED_FILES=$(git -C "{WORKTREE_PATH}" diff --name-only "origin/{PR_BASE}...HE
|
|
|
729
753
|
|
|
730
754
|
## Phase B6.5: Acceptance Gate (MANDATORY — cannot be silently skipped) <!-- Added: forge#1315 -->
|
|
731
755
|
|
|
732
|
-
**Goal**: Execute the machine-checkable acceptance spec emitted by investigate Phase 1C and block merge if any check fails. This is a hard gate — not advisory. Run the checks from the worktree (`cd "{WORKTREE_PATH}"`) so relative targets resolve against the built code.
|
|
756
|
+
**Goal**: Execute the machine-checkable acceptance spec emitted by investigate Phase 1C and block merge if any check fails. This is a hard gate — not advisory. Run the checks from the worktree (`cd "{WORKTREE_PATH}"`) so relative targets resolve against the built code. This phase may also be entered directly from the B0 gate resume (BRANCH/WORKTREE_PATH/PR_BASE re-derived there) when a prior run left a blocked gate.
|
|
733
757
|
|
|
734
758
|
**Read acceptance spec from FORGE:INVESTIGATOR comment**:
|
|
735
759
|
|
|
@@ -355,7 +355,9 @@ The `INVARIANT_ANOMALIES` variable is read in Phase C5 (Step 1) and rendered in
|
|
|
355
355
|
BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
|
|
356
356
|
|
|
357
357
|
# Count remaining unchecked items BEFORE any edit
|
|
358
|
-
|
|
358
|
+
# The investigate-appended placeholder box ("Fix confirmed during investigation.") is
|
|
359
|
+
# pipeline scaffolding, not a phase item: never count it (forge#3545).
|
|
360
|
+
REMAINING_BEFORE=$(printf '%s\n' "$BODY" | grep -vE '^[-*+] \[ \] Fix confirmed during investigation\.?[[:space:]]*$' | grep -cE '^[-*+] \[ \]' || true)
|
|
359
361
|
```
|
|
360
362
|
|
|
361
363
|
**If `REMAINING_BEFORE == 0`** (no unchecked items): skip body edit — all items already checked, proceed to add PR reference only:
|
|
@@ -390,6 +392,7 @@ fi
|
|
|
390
392
|
CHECKBOX_SECTIONS=$(printf '%s\n' "$BODY_STRIPPED" | awk '
|
|
391
393
|
/^#+ / { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
392
394
|
/^(=+|-+)$/ && previous != "" { if (in_section && has) n++; in_section=1; has=0; previous=""; next }
|
|
395
|
+
/^[-*+] \[[ xX]\] Fix confirmed during investigation\.?[ \t]*$/ { previous=$(0); next }
|
|
393
396
|
{ if (in_section && /^[-*+] \[[ xX]\]/) has=1; previous=$(0) }
|
|
394
397
|
END { if (in_section && has) n++; print n+0 }
|
|
395
398
|
')
|
|
@@ -1231,7 +1234,6 @@ Where:
|
|
|
1231
1234
|
# Resolve from the ForgeDock install root (FORGE_ROOT, from the script-resolution block) — never from ${0}, which is meaningless inside a skill
|
|
1232
1235
|
INDEXER_PATH=""
|
|
1233
1236
|
[ -n "${FORGE_ROOT:-}" ] && INDEXER_PATH="$FORGE_ROOT/scripts/build-knowledge-index.mjs"
|
|
1234
|
-
[ -f "$INDEXER_PATH" ] || INDEXER_PATH="$REPO_PATH/scripts/build-knowledge-index.mjs"
|
|
1235
1237
|
if node --version >/dev/null 2>&1 && [ -f "$INDEXER_PATH" ]; then
|
|
1236
1238
|
echo "[cost-prior] Re-indexing issue #${NUMBER} and regenerating cost priors..."
|
|
1237
1239
|
node "$INDEXER_PATH" --issue {NUMBER} --no-mirror 2>&1 | tail -5 \
|
|
@@ -1391,12 +1393,11 @@ LEDGER_INDEXED=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
|
1391
1393
|
|
|
1392
1394
|
### Step 2: Run incremental indexer
|
|
1393
1395
|
|
|
1394
|
-
Resolve the indexer script path from the ForgeDock install root (`FORGE_ROOT`),
|
|
1396
|
+
Resolve the indexer script path from the ForgeDock install root (`FORGE_ROOT`), never from the repository root (`REPO_PATH` holds the PR checkout):
|
|
1395
1397
|
|
|
1396
1398
|
```bash
|
|
1397
1399
|
INDEXER_PATH=""
|
|
1398
1400
|
[ -n "${FORGE_ROOT:-}" ] && INDEXER_PATH="$FORGE_ROOT/scripts/build-knowledge-index.mjs"
|
|
1399
|
-
[ -f "$INDEXER_PATH" ] || INDEXER_PATH="$REPO_PATH/scripts/build-knowledge-index.mjs"
|
|
1400
1401
|
|
|
1401
1402
|
if [ ! -f "$INDEXER_PATH" ]; then
|
|
1402
1403
|
echo "[LEDGER] scripts/build-knowledge-index.mjs not found — skipping Phase C5.3"
|
|
@@ -190,7 +190,7 @@ if [ -n "$TRUSTED_SCRIPT" ] && [ -f "$TRUSTED_SCRIPT" ]; then
|
|
|
190
190
|
else echo "SIZE_PLAN_READ=FAILED"; fi
|
|
191
191
|
```
|
|
192
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.
|
|
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. <!-- Added: forge#3573 --> A line may end with `— wip: {REF}@{SHA} (git checkout {SHA} -- {files})` (the durable staged hunks build B5.5 pushed to `forge/wip/{NUMBER}`), or `— patch: {PATH}` when the push failed and a patch file was written. Parse that per-seam reference as `WIP_REF_{N}` for Phase D2/D3. A line without one (or `wip: unavailable`) means no hunks were persisted; behaviour is unchanged.
|
|
194
194
|
- Milestone (from issue metadata)
|
|
195
195
|
- Priority label (P0/P1/P2) from issue labels
|
|
196
196
|
|
|
@@ -267,7 +267,7 @@ From the Decomposition Assessment in the investigation report (adjusted for any
|
|
|
267
267
|
|
|
268
268
|
For each sub-issue, prepare:
|
|
269
269
|
- **Title**: from investigation report's proposed sub-issue title (re-derived from the owner override when one was detected), including the final `{fix|feat|refactor}: ` prefix used in Phase D3
|
|
270
|
-
- **Body**: brief description of scope + `**Parent**: #{NUMBER}` + dependency note if applicable
|
|
270
|
+
- **Body**: brief description of scope + `**Parent**: #{NUMBER}` + dependency note if applicable + a `## Staged Hunks` section when the Split Proposal line carries a wip ref or patch (see D3)
|
|
271
271
|
- **Labels**: inherit priority label (P0/P1/P2) from parent; do NOT copy workflow labels
|
|
272
272
|
- **Milestone**: same milestone title as parent (if parent has one)
|
|
273
273
|
|
|
@@ -355,6 +355,11 @@ Files that need changes:
|
|
|
355
355
|
|
|
356
356
|
**Parent**: #{NUMBER}
|
|
357
357
|
{If depends on another sub-issue: "**Depends on**: #{SUB_ISSUE_N} — {reason}"}
|
|
358
|
+
|
|
359
|
+
{Only when this seam's Split Proposal line carries a wip ref or patch (D0); otherwise omit this section entirely:}
|
|
360
|
+
## Staged Hunks
|
|
361
|
+
|
|
362
|
+
The parent build's staged hunks for this seam are preserved outside the worktree: `{REF}@{SHA}`. Reuse them with `git fetch origin {REF} && git checkout {SHA} -- {files}` (wip ref), or `git apply {PATCH_PATH}` (patch file). They survive worktree cleanup.
|
|
358
363
|
SUB_BODY_EOF
|
|
359
364
|
```
|
|
360
365
|
|
|
@@ -601,6 +601,13 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
601
601
|
- **Scope cap**: if more than 25 `fix` rows result, do not grow one PR to fit them. Record the cited instance plus the rows in the same subsystem as `fix`, mark the rest `fix-deferred`, and recommend decomposition in the report (set `decompose: YES` if the remaining scope is too large for one PR).
|
|
602
602
|
- **Degrade, never block**: if any query fails (`RC` other than 0 or 1), write `Pattern sweep skipped: {reason}` in the `### Pattern Sweep` section, fall back to step 7.5, and continue. A failed query must never be recorded as 0 hits. The sweep is advisory evidence, not a gate.
|
|
603
603
|
- Every `fix` row MUST appear in `### Affected Files`, and the acceptance spec MUST include a class-wide coverage check plus a check that the added test exercises more than the reviewer's single repro (see Phase 1C).
|
|
604
|
+
7.7. **Size estimate** *(always, after the affected-file list is settled)*: estimate the changed lines the build will produce BEFORE anything is built, so an oversized issue is decomposed up front instead of discarded at the post-build size gate (build B5.5). <!-- Added: forge#3573 -->
|
|
605
|
+
- Per affected file: a new file counts about its full size; an edit counts about its hunks (added plus deleted lines). Add roughly 1:1 test lines for changed behavior. Skip lockfiles, generated files and fixtures (the gate excludes them).
|
|
606
|
+
- `INDEPENDENT_SEAMS`: the number of independently buildable pieces. For a batch issue (several review findings or checklist members) this is the member count; otherwise count distinct directory/phase seams. Minimum 1.
|
|
607
|
+
- Resolve config (defaults when absent): `THRESHOLD=$(yq '.build.diff_size.threshold // 1000' forge.yaml 2>/dev/null || echo 1000)`, `FACTOR=$(yq '.build.diff_size.estimate_factor // 1.2' forge.yaml 2>/dev/null || echo 1.2)`, `SEAM_MIN=$(yq '.build.diff_size.batch_seam_min // 3' forge.yaml 2>/dev/null || echo 3)`.
|
|
608
|
+
- Rule: when `build.diff_size.threshold` is `0` the rule is skipped (emit `GATE_THRESHOLD: 0`). Otherwise the Decomposition Assessment MUST be `**YES**` when `ESTIMATED_DIFF_LINES` > threshold x `FACTOR` (`estimate_factor`, default 1.2), OR when `INDEPENDENT_SEAMS` >= `batch_seam_min` and `ESTIMATED_DIFF_LINES` > threshold / 2 (not well under the gate). Propose one sub-issue per seam. A batch that is well under the gate (estimate at or under half the threshold) may stay `**NO**`.
|
|
609
|
+
- **Exemption** <!-- Added: forge#3676 -->: emit `**NO**` (state the guard in the reason; still emit the numeric fields) instead of `**YES**` when the issue is already a sub-issue (its body has a `**Parent**: #N` line), a `FORGE:DECOMPOSED` comment exists, or a justified `FORGE:SIZE_OVERRIDE` exists. Decompose Guard 1 refuses these issues, so a YES would stall the run; the over-threshold YES rule applies only otherwise. The engine applies the same guard when it reads the verdict.
|
|
610
|
+
- Emit the numbers in the comment as the own-line fields in Phase 1C. A pure estimate is a heuristic; state it in the reason, do not pad it.
|
|
604
611
|
8. **Fix-approach validation** — if the issue proposes a fix, don't adopt it as spec. Trace through the target system's middleware, auth, routing, config. Cross-domain: if fix in domain A interacts with domain B, read domain B's files too.
|
|
605
612
|
|
|
606
613
|
---
|
|
@@ -703,6 +710,10 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
|
703
710
|
### Decomposition Assessment
|
|
704
711
|
**{YES|NO}** — {reason}
|
|
705
712
|
{if YES: proposed sub-issues with titles and dependencies}
|
|
713
|
+
ESTIMATED_DIFF_LINES: {N}
|
|
714
|
+
INDEPENDENT_SEAMS: {N}
|
|
715
|
+
GATE_THRESHOLD: {N — build.diff_size.threshold; 0 when the gate is disabled}
|
|
716
|
+
ESTIMATE_FACTOR: {FACTOR — resolved build.diff_size.estimate_factor, positive number, decimal allowed; 1.2 by default}
|
|
706
717
|
<!-- DECOMPOSE:{YES|NO} -->
|
|
707
718
|
|
|
708
719
|
### Acceptance Spec <!-- Added: forge#1829 -->
|
|
@@ -739,6 +750,8 @@ ${INVESTIGATION_SENTINEL}"
|
|
|
739
750
|
|
|
740
751
|
**Decomposition marker (MANDATORY, both output paths — template and codec)**: emit exactly one `<!-- DECOMPOSE:YES -->` or `<!-- DECOMPOSE:NO -->` line per investigator comment, matching the `**YES**`/`**NO**` verdict, on its own line (nothing else on that line) after the verdict line and any sub-issue list (the `**YES**`/`**NO**` line must stay directly under the heading — the resume parser reads it). The headless engine routes to `work-on/decompose` on this marker (or on the `**YES**` heading in older comments). Never quote or mention the opposite marker anywhere in the comment. Never emit it when Verdict=INVALID. <!-- Added: forge#3543 -->
|
|
741
752
|
|
|
753
|
+
**Size-estimate fields (MANDATORY when Verdict is not INVALID)** <!-- Added: forge#3573 -->: `ESTIMATED_DIFF_LINES: N`, `INDEPENDENT_SEAMS: N` and `GATE_THRESHOLD: N` are each alone on their own line (whole numbers, no prose or backticks on the line), as is `ESTIMATE_FACTOR: F` (the resolved `FACTOR`, a positive number, decimal allowed), INSIDE `### Decomposition Assessment` after the verdict line and sub-issue list and before the `<!-- DECOMPOSE:.. -->` marker. The engine reads them with full-line anchors as a safety net: an estimate over `GATE_THRESHOLD` x `ESTIMATE_FACTOR` (1.2 when the field is absent or invalid) routes to decompose even when the verdict says NO, so the verdict and the numbers must agree. Never quote these field lines elsewhere in the comment.
|
|
754
|
+
|
|
742
755
|
**Decomposition scopes the Acceptance Spec**: when the assessment is YES, the Acceptance Spec MUST cover only the first, in-scope item — or tag each check with the sub-item it belongs to (append `# item-N` to the description). Never emit checks for work the assessment assigns to a separate sub-issue: build's acceptance gate would otherwise fail on them and its repair loop would pull that work into this branch. <!-- Added: forge#3543 -->
|
|
743
756
|
|
|
744
757
|
**Do not hardcode `<!-- INVESTIGATION:COMPLETE -->` as the closing line.** The closing line MUST be the `${INVESTIGATION_SENTINEL}` variable computed above — it resolves to `<!-- INVESTIGATION:INVALID -->` for an INVALID verdict and `<!-- INVESTIGATION:COMPLETE -->` otherwise. `INVESTIGATION:COMPLETE` and `INVESTIGATION:INVALID` are mutually exclusive within a single posted comment — never emit both.
|