forgedock 1.17.0 → 1.19.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 +37 -2
- package/bin/engine/phases.mjs +207 -106
- package/bin/engine/runlog.mjs +17 -2
- package/bin/engine/telemetry-seam.mjs +3 -0
- package/bin/engine/telemetry.test.mjs +3 -2
- package/bin/engine-cli.mjs +37 -2
- package/bin/engine.mjs +221 -15
- package/bin/forgedock.mjs +1 -1
- package/bin/hooks/pre-tool-use.mjs +65 -32
- package/bin/runner.mjs +125 -6
- package/bin/stats/aggregate.mjs +7 -3
- package/bin/stats/cache.mjs +7 -3
- package/bin/stats/github-source.mjs +203 -24
- package/bin/stats/index.mjs +46 -9
- package/bin/stats/render.mjs +1 -1
- package/bin/stats/stats.test.mjs +264 -15
- package/commands/cleanup.md +10 -3
- package/commands/deploy-pr.md +64 -0
- package/commands/fix-ci.md +20 -6
- package/commands/milestone.md +34 -17
- package/commands/orchestrate/phase-4-execution.md +11 -4
- package/commands/review-pr-staging.md +145 -522
- package/commands/review-pr.md +496 -3124
- package/commands/test-gate.md +25 -10
- package/commands/work-on/build/architect.md +66 -1
- package/commands/work-on/build/implement.md +21 -1
- package/commands/work-on/build/validate.md +322 -28
- package/commands/work-on/build.md +173 -95
- package/commands/work-on/close.md +50 -15
- package/commands/work-on/decompose.md +72 -34
- package/commands/work-on/investigate.md +16 -12
- package/commands/work-on/remediate.md +58 -25
- package/commands/work-on/review.md +36 -21
- package/commands/work-on.md +1 -2
- package/docs/CONFIG.md +132 -26
- package/docs/OUTCOME-CODES.md +12 -3
- package/docs/RELEASE-NOTES.md +12 -0
- package/docs/STATS.md +13 -5
- package/docs/TELEMETRY.md +7 -4
- package/docs/WORK-ON-RUNTIME.md +24 -0
- package/docs/spec/forge-protocol-v1.md +2 -2
- package/package.json +2 -2
- package/packages/protocol/src/phases.js +5 -5
- package/packages/protocol/src/types.js +1 -1
- package/scripts/acceptance-red-green.sh +90 -0
- package/scripts/acceptance-red-green.test.sh +74 -0
- package/scripts/cap-findings.sh +1 -1
- package/scripts/cap-findings.test.sh +3 -1
- package/scripts/check-contract-scope.test.sh +2 -0
- package/scripts/check-edge-cases.sh +143 -0
- package/scripts/check-edge-cases.test.sh +175 -0
- package/scripts/check-spec-bash.sh +5 -1
- package/scripts/check-spec-bash.test.sh +20 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/check-test-tmp-cleanup.sh +56 -0
- package/scripts/check-test-tmp-cleanup.test.sh +69 -0
- package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
- package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
- package/scripts/ci-local/adapters/buildkite.mjs +58 -0
- package/scripts/ci-local/adapters/circleci.mjs +101 -0
- package/scripts/ci-local/adapters/common.mjs +115 -0
- package/scripts/ci-local/adapters/explicit.mjs +21 -0
- package/scripts/ci-local/adapters/github-actions.mjs +349 -0
- package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
- package/scripts/ci-local/adapters/portability.mjs +104 -0
- package/scripts/ci-local/adapters/task-runners.mjs +67 -0
- package/scripts/ci-local/cli.mjs +50 -0
- package/scripts/ci-local/lib.mjs +371 -0
- package/scripts/ci-local/plan.mjs +186 -0
- package/scripts/ci-local/run.mjs +345 -0
- package/scripts/ci-local/scoping.mjs +508 -0
- package/scripts/ci-local.sh +97 -0
- package/scripts/ci-local.test.sh +919 -0
- package/scripts/classify-finding.sh +32 -6
- package/scripts/classify-finding.test.sh +23 -0
- package/scripts/classify-test-run.sh +69 -0
- package/scripts/classify-test-run.test.sh +90 -0
- package/scripts/close-parent-c3.test.sh +71 -0
- package/scripts/consumer-sweep.sh +569 -0
- package/scripts/consumer-sweep.test.sh +357 -0
- package/scripts/detect-append-registries.sh +106 -0
- package/scripts/detect-append-registries.test.sh +64 -0
- package/scripts/diff-size.sh +93 -4
- package/scripts/diff-size.test.sh +59 -2
- package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
- package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
- package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
- package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
- package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
- package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
- package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
- package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
- package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
- package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
- package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
- package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
- package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
- package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
- package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
- package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
- package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
- package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
- package/scripts/forge-root.test.sh +14 -12
- package/scripts/lib/telemetry.sh +1 -0
- package/scripts/publish-staging-sync.test.sh +48 -0
- package/scripts/remediate-approved-head.test.sh +2 -0
- package/scripts/remediate-basesync.test.sh +1 -1
- package/scripts/resolve-deploy-gate.sh +32 -6
- package/scripts/resolve-deploy-gate.test.sh +42 -1
- package/scripts/review-acceptance-gate.sh +2 -2
- package/scripts/review-acceptance-gate.test.sh +20 -0
- package/scripts/review-delta.sh +8 -1
- package/scripts/review-delta.test.sh +19 -0
- package/scripts/review-finalize.sh +1579 -0
- package/scripts/review-finalize.test.sh +1040 -0
- package/scripts/review-pr-delta-wiring.test.sh +32 -72
- package/scripts/review-pr-domain-map.test.sh +18 -13
- package/scripts/review-pr-gate2.test.sh +9 -65
- package/scripts/review-pr-staging-scope.test.sh +14 -98
- package/scripts/review-prepare.sh +53 -15
- package/scripts/review-prepare.test.sh +37 -3
- package/scripts/review-roster.sh +16 -2
- package/scripts/review-roster.test.sh +22 -0
- package/scripts/run-shell-tests.sh +5 -0
- package/scripts/telemetry.test.sh +2 -0
- package/scripts/trusted-comments.test.sh +11 -3
- package/scripts/verify-phase-trail.test.sh +8 -0
- package/scripts/wait-ci-green.sh +61 -9
- package/scripts/wait-ci-green.test.sh +44 -6
- package/scripts/with-run-tmpdir.sh +20 -0
package/commands/test-gate.md
CHANGED
|
@@ -439,23 +439,38 @@ BASELINE_RESULTS=""
|
|
|
439
439
|
|
|
440
440
|
# Run the same tests against the base branch state to establish baseline
|
|
441
441
|
# If base-branch tests also fail, those are pre-existing failures — not batch-introduced
|
|
442
|
-
|
|
443
|
-
|
|
442
|
+
# Use a detached worktree at the base branch — never stash/checkout in the main checkout
|
|
443
|
+
BASE_WT=$(mktemp -d)
|
|
444
|
+
git -C "$REPO_PATH" worktree add --detach "$BASE_WT" "origin/${BASE_BRANCH}" 2>/dev/null \
|
|
445
|
+
|| { echo "WARNING: could not create baseline worktree — baseline unavailable (failures will count as batch-introduced)"; rmdir "$BASE_WT" 2>/dev/null; BASE_WT=""; }
|
|
446
|
+
# A fresh worktree has no installed deps or local env files: link the main checkout's (read-only use), at the repo
|
|
447
|
+
# root and in every cluster working_dir, so a baseline cluster fails only for code reasons, never for a missing install.
|
|
448
|
+
if [ -n "$BASE_WT" ]; then
|
|
449
|
+
{ echo .; yq '.verification.integration_tests[].working_dir // "."' "$CONFIG_FILE" 2>/dev/null; } | sort -u | while IFS= read -r _wd; do
|
|
450
|
+
for _dep in node_modules .venv venv .env .env.local .env.test; do
|
|
451
|
+
[ -e "${REPO_PATH}/${_wd}/${_dep}" ] && [ ! -e "${BASE_WT}/${_wd}/${_dep}" ] && [ -d "${BASE_WT}/${_wd}" ] \
|
|
452
|
+
&& ln -s "${REPO_PATH}/${_wd}/${_dep}" "${BASE_WT}/${_wd}/${_dep}"
|
|
453
|
+
done
|
|
454
|
+
done # one working_dir per line: a path with spaces is never word-split
|
|
455
|
+
fi
|
|
444
456
|
|
|
445
457
|
for i in $(seq 0 $((CLUSTER_COUNT - 1))); do
|
|
446
458
|
CLUSTER=$(yq ".verification.integration_tests[${i}].cluster" "$CONFIG_FILE")
|
|
447
459
|
CMD=$(yq ".verification.integration_tests[${i}].command" "$CONFIG_FILE")
|
|
448
460
|
WORKING_DIR=$(yq ".verification.integration_tests[${i}].working_dir // \".\"" "$CONFIG_FILE")
|
|
449
461
|
|
|
450
|
-
if
|
|
462
|
+
if [ -z "$BASE_WT" ]; then
|
|
463
|
+
# No baseline: never claim pre-existing. A bundle failure in this cluster counts as batch-introduced (fail closed).
|
|
464
|
+
BASELINE_RESULTS="${BASELINE_RESULTS}\n- ${CLUSTER}: UNAVAILABLE"
|
|
465
|
+
elif (cd "${BASE_WT}/${WORKING_DIR}" && eval "$CMD") 2>&1; then
|
|
451
466
|
BASELINE_RESULTS="${BASELINE_RESULTS}\n- ${CLUSTER}: PASS"
|
|
452
467
|
else
|
|
453
468
|
BASELINE_RESULTS="${BASELINE_RESULTS}\n- ${CLUSTER}: FAIL (pre-existing)"
|
|
454
469
|
fi
|
|
455
470
|
done
|
|
456
471
|
|
|
457
|
-
#
|
|
458
|
-
git
|
|
472
|
+
# Remove the baseline worktree (the main checkout was never touched)
|
|
473
|
+
[ -n "$BASE_WT" ] && git -C "$REPO_PATH" worktree remove --force "$BASE_WT" 2>/dev/null || true
|
|
459
474
|
|
|
460
475
|
echo "Baseline results:"
|
|
461
476
|
echo -e "$BASELINE_RESULTS"
|
|
@@ -475,10 +490,10 @@ while IFS= read -r result_line; do
|
|
|
475
490
|
if echo "$STATUS" | grep -q "^FAIL"; then
|
|
476
491
|
# Check if this cluster also failed in baseline
|
|
477
492
|
BASELINE_STATUS=$(echo -e "$BASELINE_RESULTS" | grep "^- ${CLUSTER}:" | grep -oP ':\s+\K.+' || echo "PASS")
|
|
478
|
-
if echo "$BASELINE_STATUS" | grep -q "^
|
|
479
|
-
#
|
|
493
|
+
if ! echo "$BASELINE_STATUS" | grep -q "^FAIL"; then
|
|
494
|
+
# Baseline PASS (or UNAVAILABLE: no proof it was pre-existing) but fails in the bundle — batch-introduced failure
|
|
480
495
|
BATCH_FAILURES="${BATCH_FAILURES}\n${CLUSTER}"
|
|
481
|
-
echo "BATCH-INTRODUCED FAILURE: cluster '${CLUSTER}' (baseline:
|
|
496
|
+
echo "BATCH-INTRODUCED FAILURE: cluster '${CLUSTER}' (baseline: ${BASELINE_STATUS} → bundle: FAIL)"
|
|
482
497
|
else
|
|
483
498
|
echo "PRE-EXISTING FAILURE: cluster '${CLUSTER}' (baseline: FAIL → bundle: FAIL — not this batch)"
|
|
484
499
|
fi
|
|
@@ -534,7 +549,7 @@ else
|
|
|
534
549
|
cat <<ISSUE_EOF > "$TESTGATE_FAIL_BODY_FILE"
|
|
535
550
|
## Problem
|
|
536
551
|
|
|
537
|
-
The \`/test-gate\` command detected a runtime failure in cluster \`${cluster}\` that was introduced by the staging→${DEFAULT_BRANCH} bundle.
|
|
552
|
+
The \`/test-gate\` command detected a runtime failure in cluster \`${cluster}\` that was introduced by the staging→${DEFAULT_BRANCH} bundle. It was not observed failing on \`${BASE_BRANCH}\` (baseline: PASS, or UNAVAILABLE when the baseline could not run — then treated as batch-introduced, fail closed).
|
|
538
553
|
|
|
539
554
|
## Root Cause (if known)
|
|
540
555
|
|
|
@@ -574,7 +589,7 @@ $(echo -e "$SOURCE_ISSUES_LIST")
|
|
|
574
589
|
\`\`\`
|
|
575
590
|
3. Observe failure. Compare against \`${BASE_BRANCH}\` baseline:
|
|
576
591
|
\`\`\`bash
|
|
577
|
-
git
|
|
592
|
+
git worktree add --detach "$BASE_WT" origin/${BASE_BRANCH} # run the command inside $BASE_WT
|
|
578
593
|
$(yq ".verification.integration_tests[] | select(.cluster == \"${cluster}\") | .command" "$CONFIG_FILE")
|
|
579
594
|
\`\`\`
|
|
580
595
|
ISSUE_EOF
|
|
@@ -604,6 +604,39 @@ The block is delimited by `<!-- FORGE:BLAST_RADIUS:BEGIN -->` and `<!-- FORGE:BL
|
|
|
604
604
|
|
|
605
605
|
---
|
|
606
606
|
|
|
607
|
+
## Phase A2.9: Append-Only Registry Check *(skip when no planned file is a hand-maintained list)* <!-- Added: forge#3969 -->
|
|
608
|
+
|
|
609
|
+
Two PRs appending at the same spot of a shared list (test paths, cron targets, workflow steps) conflict textually although every line is independent, and each conflict costs a base-sync remediation round. For each planned file that is an append-only registry (one independent entry per line, edited by many PRs):
|
|
610
|
+
|
|
611
|
+
1. Confirm with `bash <scripts>/detect-append-registries.sh --detect` (history ranking) and `--check-union <path>`. Resolve `<scripts>` through the trusted script resolution (plugin root, `FORGE_ROOT`, `FORGEDOCK_HOME`), never `$PWD`. The helper rejects JSON, YAML, TOML, lockfiles and Markdown.
|
|
612
|
+
2. Plan one of: (a) a `.gitattributes` `merge=union` entry, only when `--check-union` prints `UNION SAFE` and every line is independent (no ordering, uniqueness or structure); or (b) converting the list to glob discovery where its consumer supports it.
|
|
613
|
+
3. Name the registry and the chosen option in `### Affected Paths`. Never plan union for a file with ordering, uniqueness or structure: that file is edited at the end of the list, and the pre-handoff base sync resolves its conflicts.
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Phase A2.10: Consumer Sweep Intake *(advisory: skip silently when no existing identifier changes; never blocks)* <!-- Added: forge#3970 -->
|
|
618
|
+
|
|
619
|
+
No diff exists yet, so sweep by name. List the existing identifiers the plan will change, rename or re-type (functions, classes, constants, config or context keys, CLI flags, env vars, paths, literal hashes or versions), take them from the investigation's affected files, and run the script in identifier mode. Validate (Phase V4.6) later re-runs the same sweep on the final diff and gates it.
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => the sweep is skipped, never guessed)
|
|
623
|
+
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() { :; }; }
|
|
624
|
+
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
625
|
+
CS_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/consumer-sweep.sh}"; [ -f "$CS_SCRIPT" ] || CS_SCRIPT=""
|
|
626
|
+
EXCLUDES=()
|
|
627
|
+
while IFS= read -r _g; do [ -n "$_g" ] && [ "$_g" != "null" ] && EXCLUDES+=(--exclude-glob "$_g"); done \
|
|
628
|
+
< <(yq '(.build.scope.exclude_globs // []) + (.build.diff_size.exclude_globs // []) | .[]' forge.yaml 2>/dev/null || true)
|
|
629
|
+
if [ -z "$CS_SCRIPT" ]; then echo "CONSUMER_SWEEP: unavailable (script not found under FORGE_ROOT)"; else
|
|
630
|
+
bash "$CS_SCRIPT" --ident {IDENTIFIER_1} --ident {IDENTIFIER_2} ${EXCLUDES[@]+"${EXCLUDES[@]}"}; echo "CONSUMER_SWEEP_RC=$?"
|
|
631
|
+
fi
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Each identifier must match `^[A-Za-z0-9_.:/-]{4,80}$` (flags without leading dashes). Output is TSV `identifier<TAB>kind<TAB>file:line`; a `truncated` row means more sites exist than the cap shows (advisory). Exit `2` or an unavailable script is not "no sites": write "consumer sweep unavailable" in `### Risk Assessment` and continue; never block the plan.
|
|
635
|
+
|
|
636
|
+
Fold the result into Phase A2.8, with no new record grammar: each swept identifier becomes a `SYMBOL:` (`query` = the identifier) and each referencing file a `HIT:` (`disposition=change` when the plan edits it, else `verified-unaffected` with a reason after reading the site). The builder must update or justify each site (implement I6 `CONSUMERS:` block).
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
607
640
|
## Phase A3: Consistency Rules
|
|
608
641
|
|
|
609
642
|
Identify invariants that ALL affected paths must satisfy. These become the builder's consistency checklist.
|
|
@@ -636,6 +669,14 @@ Rules for ordering:
|
|
|
636
669
|
|
|
637
670
|
---
|
|
638
671
|
|
|
672
|
+
## Phase A4.5: Edge-Case Enumeration *(skip on the skip-marker path; an empty block is valid)* <!-- Added: forge#3631 -->
|
|
673
|
+
|
|
674
|
+
List the error, edge and concurrency cases the change must survive, so validate (Phase V4.7, `scripts/check-edge-cases.sh`) can require a test for each. Cover every error class the code path can raise (not only the one in the report), boundary and empty inputs, and concurrent or repeated invocation. Skip cases that only restate a normal-path acceptance criterion.
|
|
675
|
+
|
|
676
|
+
Record one single-line record per case between `<!-- FORGE:EDGE_CASES:BEGIN -->` and `<!-- FORGE:EDGE_CASES:END -->`: `EDGE: id=e1 kind=error|edge|concurrency description=<free text>`. Ids are `e1`, `e2`, ... and are unique and stable. Only `id=` and `kind=` are parsed; the description is free text. Emit an empty block (or omit the section) when the change has no such cases. The builder maps each id to a test or an `untestable=` reason in FORGE:BUILDER (implement I6). Never use the architect marker strings as delimiters.
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
639
680
|
## Phase A5: Risk Assessment
|
|
640
681
|
|
|
641
682
|
For each non-obvious interaction or potential failure mode:
|
|
@@ -719,6 +760,15 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ARCHITECT -->
|
|
|
719
760
|
|---|------|----------------|-----------------|-----|
|
|
720
761
|
| 1 | {FILE} | {FUNCTION} | {CHANGE} | {REASON} |
|
|
721
762
|
|
|
763
|
+
### Scope Baseline
|
|
764
|
+
<!-- Machine-readable scope baseline read by build B5.5's post-build scope check (forge#3862). Every
|
|
765
|
+
path in ### Affected Paths (including tests, docs and config the plan changes), one per line as
|
|
766
|
+
- path (repo-relative; a trailing slash means every file under that directory). The keys sit alone
|
|
767
|
+
on their lines. ESTIMATED_DIFF_LINES is the plan's own estimate (code plus tests, a whole number). -->
|
|
768
|
+
PLANNED_FILES:
|
|
769
|
+
- {FILE}
|
|
770
|
+
ESTIMATED_DIFF_LINES: {N}
|
|
771
|
+
|
|
722
772
|
### Pattern Sweep
|
|
723
773
|
<!-- Pass-through from the FORGE:INVESTIGATOR '### Pattern Sweep' table (Phase A2.7).
|
|
724
774
|
Omit this section when the investigation report had none. -->
|
|
@@ -735,6 +785,16 @@ HIT: symbol=s1 file={FILE} role={producer|consumer|sibling} disposition=change
|
|
|
735
785
|
HIT: symbol=s1 file={FILE} role={producer|consumer|sibling} disposition=verified-unaffected reason={one-hyphenated-token}
|
|
736
786
|
<!-- FORGE:BLAST_RADIUS:END -->
|
|
737
787
|
|
|
788
|
+
### Edge Cases
|
|
789
|
+
<!-- Phase A4.5. Human table plus the machine block. Omit when skipped; an empty block is valid. -->
|
|
790
|
+
| id | kind | description |
|
|
791
|
+
|----|------|-------------|
|
|
792
|
+
| e1 | {error\|edge\|concurrency} | {DESCRIPTION} |
|
|
793
|
+
|
|
794
|
+
<!-- FORGE:EDGE_CASES:BEGIN -->
|
|
795
|
+
EDGE: id=e1 kind={error|edge|concurrency} description={ONE_LINE_DESCRIPTION}
|
|
796
|
+
<!-- FORGE:EDGE_CASES:END -->
|
|
797
|
+
|
|
738
798
|
### Implementation Order
|
|
739
799
|
1. {FIRST_CHANGE} — {WHY_FIRST}
|
|
740
800
|
2. {SECOND_CHANGE} — {WHY_SECOND}
|
|
@@ -799,13 +859,16 @@ SKIP_BODY=$(cat <<SKIP_EOF
|
|
|
799
859
|
|
|
800
860
|
**Skipped**: {SKIP_REASON}. No cross-path consistency risk — the builder proceeds with the investigation report + context briefing.
|
|
801
861
|
|
|
862
|
+
### Scope Baseline
|
|
863
|
+
PLANNED_FILES:
|
|
864
|
+
|
|
802
865
|
<!-- FORGE:ARCHITECT:COMPLETE -->
|
|
803
866
|
SKIP_EOF
|
|
804
867
|
)
|
|
805
868
|
gh issue comment {NUMBER} {GH_FLAG} --body "$SKIP_BODY" # <!-- allowlist:check-command-side-effects -->
|
|
806
869
|
```
|
|
807
870
|
|
|
808
|
-
The empty-plan-with-marker changes no downstream behavior — the builder already falls back to the investigation report + contract when the plan is empty (see Integration Point below). The marker only makes the legitimate skip **observable** to the marker-only engine gate. This intentionally does **not** weaken crash detection: a genuinely interrupted architect run still posts no marker at all, so `detectOutcome` still fails-and-escalates it correctly.
|
|
871
|
+
The empty `PLANNED_FILES:` list means "no plan baseline": build B5.5 then skips the out-of-plan check (proceed, never block) and falls back to the investigation's `ESTIMATED_DIFF_LINES`. The empty-plan-with-marker changes no downstream behavior — the builder already falls back to the investigation report + contract when the plan is empty (see Integration Point below). The marker only makes the legitimate skip **observable** to the marker-only engine gate. This intentionally does **not** weaken crash detection: a genuinely interrupted architect run still posts no marker at all, so `detectOutcome` still fails-and-escalates it correctly.
|
|
809
872
|
|
|
810
873
|
---
|
|
811
874
|
|
|
@@ -840,8 +903,10 @@ B4 → [THIS MODULE] Architecture Planning (FORGE:ARCHITECT comment)
|
|
|
840
903
|
Phase A2.5: Pipeline Phase-Dependency Check
|
|
841
904
|
Phase A2.7: Pattern Sweep Intake (review-finding issues only)
|
|
842
905
|
Phase A2.8: Blast-Radius Manifest (enforced by validate V4.5, passed to review-pr)
|
|
906
|
+
Phase A2.10: Consumer Sweep Intake (consumer-sweep.sh --ident; gated by validate V4.6)
|
|
843
907
|
Phase A3: Consistency Rules
|
|
844
908
|
Phase A4: Sequence the Implementation
|
|
909
|
+
Phase A4.5: Edge-Case Enumeration (EDGE: ids; gated by validate V4.7)
|
|
845
910
|
Phase A5: Risk Assessment
|
|
846
911
|
B5 → Implement (builder reads plan, implements ALL affected paths)
|
|
847
912
|
```
|
|
@@ -42,6 +42,7 @@ Parse from $ARGUMENTS:
|
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
+
- `--fix-consumers "<UNADDRESSED lines>"` — optional. Set only by build B6 after validate returned `consumer_unaddressed`. Skip I1–I2 planning: for each listed `UNADDRESSED: <identifier> <file:line>` site, either update that file so it no longer depends on the old identifier (the file then joins the change set) or add a `- <path>[:<line>] unaffected: <reason>` line to the `### Consumers` block of the FORGE:BUILDER comment (the match is by path, split on the literal ` unaffected: `, so any path characters work). Never edit the checker (`scripts/consumer-sweep.sh`) or its stop-lists, and never add `unaffected:` lines without reading the site. Stage the changes, return `IMPLEMENT_RESULT: status: COMPLETE`; if a site cannot be addressed, leave it: build records it as `CONSUMERS: unresolved`. This is the single repair round; there is no second one. <!-- Added: forge#4017 -->
|
|
45
46
|
- `--fix-acceptance "<failed checks>"` — optional. Set only by build B6.5 after a failed acceptance gate. In this mode, skip I1–I2 planning: read the listed failing `ACCEPTANCE_CHECK` ids from the FORGE:INVESTIGATOR comment, change the code in `{WORKTREE_PATH}` until each check's target/matcher holds (do not edit the checks), update the BUILDER comment's `### Changes` list, and return `IMPLEMENT_RESULT: status: COMPLETE`. If a check is impossible as written (contradicts the contract), return `status: BLOCKED` with that reason. **Decomposition refusal** <!-- Added: forge#3543 -->: never implement work that the FORGE:INVESTIGATOR `### Decomposition Assessment` (`**YES**`) assigns to a separate sub-issue, even if a listed check targets it; return `status: BLOCKED` with blocker "check <id> belongs to decomposed item <N>" instead.
|
|
46
47
|
|
|
47
48
|
## Phase I1: Load Context from GitHub
|
|
@@ -165,6 +166,7 @@ Work in `{WORKTREE_PATH}`. Follow the contract deliverables table exactly — im
|
|
|
165
166
|
- For each acceptance criterion in the contract: implement it, then mentally verify it's met
|
|
166
167
|
- Do NOT add unrequested scope — contract out-of-scope items stay out of scope
|
|
167
168
|
- **Library callback verification**: When writing a lambda or callable that will be passed to a library/framework parameter (e.g., `prepared_statement_name_func=lambda: ""`, `key=lambda x: ...`), you MUST verify the expected calling convention BEFORE writing it. Check the library's default value for that parameter, its documentation, or its source code. A lambda with wrong arity causes `TypeError` at runtime — this is invisible to static analysis and linting. The P0 incident from PR #14391 was caused by `lambda _: ""` (1 arg) passed where SQLAlchemy expects 0 args.
|
|
169
|
+
- **Scratch placement (venvs, caches)**: create virtualenvs, package caches and other large scratch data under `"$TMPDIR/forge-venv-{NUMBER}"` (the engine exports a per-run, disk-backed `TMPDIR`) or inside the worktree. Never use a literal `/tmp/<name>` path: a size-limited `/tmp` fails concurrent lanes with EDQUOT/ENOSPC. Remove what you created when the run finishes. <!-- Added: forge#3572 -->
|
|
168
170
|
- **Worktree-aware path derivation**: When writing shell code that derives repository paths, ALWAYS use `git rev-parse --show-toplevel` for the repo root in regular checkouts, or `git rev-parse --git-common-dir` (then `dirname`) to get the shared `.git` directory when the context may be a linked worktree. For worktree cleanup code specifically, ALWAYS use `--git-common-dir` — `--show-toplevel` returns the worktree path itself, NOT the main repo root. NEVER use `pwd`, relative paths, or `dirname` chains on `--show-toplevel` output for repo root derivation. Test path logic for both regular checkouts and linked worktrees. (Ref: review-findings #104, #105 — 4 PATH_DERIVATION defects, 15% of all review findings)
|
|
169
171
|
- **State machine completeness verification**: When implementing routing logic, state transition tables, or phase dispatch code (e.g., adding a `Skill()` call in a routing loop, adding a new phase to a state machine, creating a new subcommand file), you MUST run three checks BEFORE staging: (1) **Routing target existence** — for each `Skill("subcommand", ...)` call or file reference in routing logic, verify the target file exists at `commands/{subcommand-path}.md`; (2) **State reachability** — for each declared state or phase, verify at least one prior state or entry condition in the router routes to it; (3) **Subcommand invocation wiring** — for each `commands/work-on/*.md` file that declares its invocation condition (e.g., `Invoked by work-on.md routing loop, when X`), verify that condition is actually handled in the router. A missing target file, an unreachable state, or a declared-but-unwired subcommand will not surface until review. (Ref: review-findings #85, #116, #137 — 4 ROUTING defects, 15% of all review findings)
|
|
170
172
|
- **Migration safety checklist** *(trigger: diff includes `*.sql` files or files under a `migrations/` path)*: When any migration file is in the diff, verify ALL of the following BEFORE staging. Fix each violation inline — do not defer to review or the quality gate: (a) **Rollback file**: a corresponding down/rollback migration file exists (e.g. `0042_down_*.sql`, `rollback_*.sql`) or the migration is explicitly self-reversing (DROP of a previously-added column); (b) **NOT NULL safety**: any `ADD COLUMN ... NOT NULL` either includes a `DEFAULT` clause or is preceded by a backfill step — a NOT NULL column without DEFAULT locks the table and fails on existing rows; (c) **Constraint name consistency**: constraint names in the migration match ORM model declarations — a mismatch causes `alembic stamp` and FK introspection to silently diverge; (d) **CREATE TRIGGER idempotency**: every `CREATE TRIGGER` uses `CREATE OR REPLACE TRIGGER` or is preceded by `DROP TRIGGER IF EXISTS` — a bare `CREATE TRIGGER` fails on re-run in test and CI environments; (e) **Migration prefix uniqueness**: confirm the new file's numeric prefix does not already exist in the `infra/migrations/` tree (`ls infra/migrations/*.sql | xargs -n1 basename | grep -oE '^[0-9]+' | sort | uniq -d` prints duplicates) — a duplicate prefix hard-fails the deploy gate regardless of file content. <!-- Added: forge#373 -->
|
|
@@ -172,6 +174,8 @@ Work in `{WORKTREE_PATH}`. Follow the contract deliverables table exactly — im
|
|
|
172
174
|
- **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 -->
|
|
173
175
|
- **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.
|
|
174
176
|
- **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 -->
|
|
177
|
+
- **Consumer sweep — address every referencing site**: when the FORGE:ARCHITECT plan lists consumer-sweep sites (`HIT:` rows from `scripts/consumer-sweep.sh`, Phase A2.10), update each referencing file or justify it as unaffected. Validate (V4.6) re-runs the sweep on the final diff; in `blocking` mode it returns `consumer_unaddressed` for every referencing site outside the change set that has no justification, and build runs one repair round (`--fix-consumers`). Sites still unaddressed after that round are recorded as `CONSUMERS: unresolved` and passed to review as known risk; they never block. Record the justifications in the `CONSUMERS:` block of the FORGE:BUILDER comment (Phase I6). <!-- Added: forge#3970 -->
|
|
178
|
+
- **Edge cases — one test per id**: when the FORGE:ARCHITECT plan carries a `FORGE:EDGE_CASES` block (`### Edge Cases`, Phase A4.5), write one test per `EDGE:` id (error, edge and concurrency cases, not only the happy path) and keep the test in the diff's added lines. Record each id in the `### Edge Case Coverage` section of the FORGE:BUILDER comment (Phase I6). Validate (V4.7) fails with `edge-cases: <ids>` for an id with no `EDGE_TEST:` line, a mapped test name absent from the added lines of its file, or an empty `untestable=` reason. Use `untestable=<reason>` only when no test can exercise the case, and give the real reason. An empty or absent block is a no-op. <!-- Added: forge#3631 -->
|
|
175
179
|
- **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 -->
|
|
176
180
|
- 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
|
|
177
181
|
|
|
@@ -305,7 +309,7 @@ git add {CHANGED_FILES}
|
|
|
305
309
|
|
|
306
310
|
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`.
|
|
307
311
|
|
|
308
|
-
**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
|
|
312
|
+
**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 scope check lives in build B5.5, which re-measures authoritatively against the plan (it needs only the `diff_lines=` key here). Leave `DIFF_LINES` empty when the script is unavailable or fails.
|
|
309
313
|
|
|
310
314
|
```bash
|
|
311
315
|
# <Script resolution block from work-on/build.md, verbatim>
|
|
@@ -354,6 +358,22 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILDER -->
|
|
|
354
358
|
### Acceptance Criteria Status
|
|
355
359
|
{Checklist of each criterion from the contract, marked ✅ or ❌}
|
|
356
360
|
|
|
361
|
+
### Consumers
|
|
362
|
+
CONSUMERS:
|
|
363
|
+
- {path}[:{line}] unaffected: {one-line reason}
|
|
364
|
+
- {path} updated
|
|
365
|
+
|
|
366
|
+
CONSUMERS: unresolved
|
|
367
|
+
- {path}:{line} unresolved: {identifier}
|
|
368
|
+
|
|
369
|
+
> **Consumers block**: one line per referencing site of a changed or removed identifier (the architect's A2.10 sweep and the V4.6 `UNADDRESSED:` output). Only `unaffected: <reason>` lines justify a site outside the change set; the match is by file path (the line number is optional, lines shift). A file you changed needs no line. `unresolved` lines are written by validate V5 for sites left unaddressed after the one repair round (a recorded known risk for review): they never justify a site and you do not write them by hand. Omit the block when there are no sites.
|
|
370
|
+
|
|
371
|
+
### Edge Case Coverage
|
|
372
|
+
EDGE_TEST: id=e1 test={path}::{test_name}
|
|
373
|
+
EDGE_TEST: id=e2 untestable={reason}
|
|
374
|
+
|
|
375
|
+
> **Edge case block**: one line per `EDGE:` id of the architect's `FORGE:EDGE_CASES` block, each line starting at the line start (no prose before `EDGE_TEST:`). `test=` names a file and a test name (a single token with no whitespace; name the test with underscores or hyphens) that occurs in a line this change adds to that file; a pre-existing test does not count. `untestable=` needs a non-empty reason. Omit the section when the plan has no edge cases.
|
|
376
|
+
|
|
357
377
|
### Testing Checklist
|
|
358
378
|
- [ ] {Test scenario 1} [type:api]
|
|
359
379
|
- [ ] {Test scenario 2} [type:unit]
|