forgedock 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/engine/outcome-codes.mjs +13 -0
- package/bin/engine/phases.mjs +54 -4
- package/bin/engine/runlog.mjs +10 -2
- package/bin/engine/telemetry-seam.mjs +3 -0
- package/bin/engine/telemetry.test.mjs +3 -2
- package/bin/engine-cli.mjs +32 -2
- package/bin/engine.mjs +187 -10
- package/bin/runner.mjs +89 -13
- package/bin/stats/aggregate.mjs +7 -3
- package/bin/stats/cache.mjs +7 -3
- package/bin/stats/github-source.mjs +136 -28
- package/bin/stats/index.mjs +46 -9
- package/bin/stats/render.mjs +1 -1
- package/bin/stats/stats.test.mjs +201 -40
- package/commands/orchestrate/phase-4-execution.md +1 -1
- package/commands/review-pr-agents/protocols.md +19 -0
- package/commands/review-pr-staging.md +4 -4
- package/commands/review-pr.md +31 -20
- package/commands/work-on/build/architect.md +54 -1
- package/commands/work-on/build/implement.md +28 -1
- package/commands/work-on/build/validate.md +321 -29
- package/commands/work-on/build.md +184 -17
- package/commands/work-on/close.md +9 -4
- package/commands/work-on/investigate.md +7 -0
- package/commands/work-on/remediate.md +43 -17
- package/commands/work-on/review.md +43 -22
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +93 -2
- package/docs/OUTCOME-CODES.md +5 -1
- package/docs/RELEASE-NOTES.md +15 -0
- package/docs/STATS.md +11 -4
- package/docs/TELEMETRY.md +5 -2
- package/docs/WORK-ON-RUNTIME.md +29 -3
- package/package.json +2 -2
- 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/architect-resume-match.test.sh +12 -3
- package/scripts/build-knowledge-index.mjs +8 -5
- package/scripts/builder-resume-match.test.sh +12 -3
- package/scripts/calibration.mjs +5 -2
- package/scripts/cap-findings.test.sh +2 -0
- 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-markers.sh +4 -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 +379 -0
- package/scripts/ci-local/plan.mjs +186 -0
- package/scripts/ci-local/run.mjs +451 -0
- package/scripts/ci-local/scoping.mjs +531 -0
- package/scripts/ci-local.sh +98 -0
- package/scripts/ci-local.test.sh +1108 -0
- package/scripts/classify-finding.sh +33 -7
- 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 +2 -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/file-deferred-followups.sh +91 -30
- package/scripts/file-deferred-followups.test.sh +101 -8
- 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 +1 -1
- package/scripts/lib/telemetry.sh +3 -0
- package/scripts/remediate-approved-head.test.sh +2 -0
- package/scripts/remediate-basesync.test.sh +1 -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 +294 -53
- package/scripts/review-finalize.test.sh +347 -9
- package/scripts/review-pr-delta-wiring.test.sh +13 -0
- package/scripts/review-prepare.sh +5 -3
- package/scripts/review-prepare.test.sh +18 -2
- package/scripts/review-roster.sh +18 -5
- package/scripts/review-roster.test.sh +57 -1
- package/scripts/run-shell-tests.sh +5 -0
- package/scripts/selfreview-plan.sh +86 -0
- package/scripts/selfreview-plan.test.sh +90 -0
- package/scripts/spawn-depth-check.sh +5 -4
- package/scripts/spawn-depth-check.test.sh +3 -0
- package/scripts/telemetry.test.sh +10 -0
- package/scripts/transition-label.sh +26 -0
- package/scripts/trusted-comments.test.sh +2 -1
- package/scripts/verify-phase-trail.test.sh +14 -0
- package/scripts/wait-ci-green.sh +61 -9
- package/scripts/wait-ci-green.test.sh +41 -3
- package/scripts/with-run-tmpdir.sh +20 -0
package/docs/RELEASE-NOTES.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Operator-facing notes for behavior changes that need action or awareness. Newest first.
|
|
4
4
|
|
|
5
|
+
## v1.19.1: build-time self-review, ci-local under concurrency, review roster gates
|
|
6
|
+
|
|
7
|
+
- **Build-time self-review (B5.6, #3971).** After implement, the risk-tier review roster's domain agents review the staged diff, and the builder fixes CONFIRMED/LIKELY findings at MEDIUM or above in one repair round before validate. Small or docs-only diffs skip. Each build that runs it spends extra reviewer tokens, which the official review should save back through fewer findings and remediation rounds. Off switch: `FORGE_SELFREVIEW=off`. Wait budget: `SELFREVIEW_WAIT_SECS` (default 600, max 1800). Telemetry: `build_selfreview`.
|
|
8
|
+
- **ci-local (#4057).** The SEC-3 "every changed source reaches a test" check uses the step's own ecosystem tests, not only the step's candidates, so unrelated single-test steps are skipped instead of widened. Identical checks in several jobs run once per invocation (`cache=dedup`). Machine slots are granted in FIFO ticket order. Job lines and `ci_local_check` carry `wait_ms` apart from execution `duration_ms`. Jobs run shortest first.
|
|
9
|
+
- **Review roster.** The Auth path gate also matches common auth file names (session, token, permission, login, API key) (#3847). The Infra gate covers deploy scripts, entrypoints and `compose.yaml` (#3851). The Security-only fix-PR roster applies only to fix PRs of at most 150 changed lines with a known size; larger or unknown-size fix PRs get the standard roster (#3848).
|
|
10
|
+
- **Merge gate.** `review-finalize.sh verdict` derives OTHER_BLOCKING itself and persists it for `merge-gate`, so a blocking result that a base conflict or purpose regression does not explain can no longer be merged on a missing flag (#3514).
|
|
11
|
+
|
|
12
|
+
Action: none required. Expect more reviewer agents per build (self-review) and per large fix PR (roster cap).
|
|
13
|
+
|
|
14
|
+
## Deferred follow-up lookup no longer duplicates issues (#3606)
|
|
15
|
+
|
|
16
|
+
`scripts/file-deferred-followups.sh` now finds an existing follow-up through the `FORGE:DEFERRED_ISSUE` claim marker it posts on the parent issue (REST comments, read-after-write consistent) before falling back to the lagging search index, searches closed issues as well as open ones (a closed follow-up is reopened), and after creating re-checks once: if a concurrent build filed a lower-numbered trusted follow-up, the items move to it and the newer issue is closed as a duplicate. Failed lookups still exit non-zero (`DEFERRED_LOOKUP=FAILED`).
|
|
17
|
+
|
|
18
|
+
Action: none. Output rows and the `DEFERRED_TELEMETRY` line are unchanged.
|
|
19
|
+
|
|
5
20
|
## Review specs call the prepare and finalize scripts (#3841)
|
|
6
21
|
|
|
7
22
|
`commands/review-pr.md` is now under 100 KB: Phases 1-3B and 6 run through `scripts/review-prepare.sh` and `scripts/review-finalize.sh`, and `commands/review-pr-staging.md` reuses both (`review-prepare.sh --no-delta` in 0C, then `review-finalize.sh extract|classify|file-prepare|file-verify` with `--scope-files` and `--prior-prs` in Phase 7) instead of carrying its own template resolution, slicing, classifier and cap copies. A staging bundle whose API diff is too large is read from `origin/<base>...origin/<head>`. CI now runs `--fence-funcs` over `commands/review-pr*.md` and `--fence-state` over the staging spec. Engine: `close.detectOutcome` reports `merged` only for a CLOSED issue that also carries `workflow:merged` (a stale label on an open issue no longer counts).
|
package/docs/STATS.md
CHANGED
|
@@ -14,6 +14,10 @@ npx forgedock stats [--since <version|date>] [--repo <owner/repo>] [--json] [--r
|
|
|
14
14
|
versions at or above it; `--since 2026-10-01` keeps activity on or after the date. `--compact` prints one row per
|
|
15
15
|
version (the table embedded in the staging review summary). `--json` is machine-readable.
|
|
16
16
|
|
|
17
|
+
`--since` also bounds the GitHub crawl: only issues and comments updated on or after the window start are read (a version
|
|
18
|
+
is resolved to its tag time minus one day; an unknown tag falls back to a full crawl and says so on stderr). Progress is
|
|
19
|
+
printed to stderr while crawling, so `--json` stdout stays parseable.
|
|
20
|
+
|
|
17
21
|
## Sources
|
|
18
22
|
|
|
19
23
|
1. **GitHub (primary).** Issues, PRs and labels of the target repo, plus trusted `<!-- FORGE:TELEMETRY v1 -->` comments
|
|
@@ -21,8 +25,9 @@ version (the table embedded in the staging review summary). `--json` is machine-
|
|
|
21
25
|
`scripts/trusted-comments.sh`); `part=N` comments are concatenated and records are deduped on `eid`. All `gh` calls
|
|
22
26
|
are argv-based. Needs an authenticated `gh`.
|
|
23
27
|
2. **Local cache (optional).** `~/.forge/stats-cache/<owner>_<repo>/cache.json` (`FORGE_STATS_CACHE_DIR`) holds what was
|
|
24
|
-
fetched
|
|
25
|
-
|
|
28
|
+
fetched, an `updated_at` watermark (the next run only re-reads items updated since) and a `coveredFrom` floor (the
|
|
29
|
+
oldest window start crawled, so a later, wider `--since` crawls the uncovered gap instead of under-reporting). It is
|
|
30
|
+
safe to delete (the cost is one full re-fetch); a corrupt cache is treated as empty. `--refresh` ignores it, `--no-cache` never touches it.
|
|
26
31
|
3. **Local run logs (optional).** `~/.forge/runs/*.jsonl` (`FORGE_RUNS_DIR`) for runs that predate the engine's
|
|
27
32
|
`phase_attempt` / `run_terminal` records. A run already present on GitHub is not counted twice. Only runs whose
|
|
28
33
|
`RUN_START.repo` equals the target repo are used. `--no-local` skips them.
|
|
@@ -59,5 +64,7 @@ instead of failing.
|
|
|
59
64
|
|
|
60
65
|
## Limits
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
Comments are read from the repo-wide `issues/comments?since=` endpoint (a few paginated calls); only issues with a trusted
|
|
68
|
+
telemetry comment in the window get their full comment list re-read (6 in parallel, backing off when the API rate limit is
|
|
69
|
+
low). A run with no `--since` and no cache still walks the whole history once. Telemetry comments are edited in place, which
|
|
70
|
+
the comment window catches; if a cached item still looks stale, run with `--refresh`.
|
package/docs/TELEMETRY.md
CHANGED
|
@@ -33,9 +33,12 @@ is dropped. Payload keys are `[A-Za-z0-9_]+` and cannot shadow the shared keys;
|
|
|
33
33
|
|-------|-----------|---------|
|
|
34
34
|
| `diff_size` | `scripts/diff-size.sh`; `work-on/build.md` B5.5 (final result) | `lines`, `excluded`, `ws_lines`, `review_weight`, `modules`, `result`; the script emits the legacy `threshold` and `OK` \| `OVER` \| `COHESIVE`, the B5.5 call site emits the final `result` = `OK` \| `EXTENDED_REVIEW` \| `SCOPE_FLAGGED` \| `OVERRIDDEN` plus `estimate` (plan), `actual` (whitespace-insensitive lines), `scope` = `ok` \| `out_of_plan` \| `no_baseline` and `review_depth` = `STANDARD` \| `EXTENDED` (calibration inputs, forge#3862) |
|
|
35
35
|
| `acceptance_gate` | `work-on/build.md` B6.5 (`phase=build`); `scripts/review-acceptance-gate.sh` (`phase=review`) | `phase`, `verified`, `failed`, `malformed`, `deferred` |
|
|
36
|
-
| `finding_disposition` | `scripts/classify-finding.sh`, one per finding | `severity`, `confidence`, `agent`, `disposition` = `ISSUE` \| `NOTE` \| `INPR_FIX
|
|
36
|
+
| `finding_disposition` | `scripts/classify-finding.sh`, one per finding | `severity`, `confidence`, `agent`, `scope` = `introduced` \| `pre-existing` \| `unknown` (from the diff; `unknown` when not supplied), `disposition` = `ISSUE` \| `NOTE` \| `INPR_FIX` (also MEDIUM LIKELY when `scope=introduced`), `reason` |
|
|
37
37
|
| `review_panel` | `review-pr.md` Phase 4 | `agents_selected`, `agents_completed`, `mode` = `FULL` \| `DELTA` \| `BASE_SYNC_ONLY` |
|
|
38
|
-
| `ci_gate` | `scripts/wait-ci-green.sh` | `result` = `PASS` \| `FAIL` \| `ERROR` \| `TIMEOUT
|
|
38
|
+
| `ci_gate` | `scripts/wait-ci-green.sh` | `result` = `PASS` \| `FAIL` \| `ERROR` \| `TIMEOUT` \| `RATE_LIMITED` (rc 4: GitHub throttled or briefly unavailable for the whole window; retryable, never a verdict), `rc`, `checks` |
|
|
39
|
+
| `ci_pending_wait` | the engine (`bin/engine/telemetry-seam.mjs`, one per finished CI-pending wait at the merge gate; `phase` = `review` \| `remediate`) | `pr`, `minutes_waited`, `result` = `green` \| `red` \| `error` \| `timeout`, `polls` (CI reads made), `rate_limit_sleeps` (polls that were rate-limited plus sleeps until the budget reset) |
|
|
40
|
+
| `ci_local` | `scripts/ci-local.sh run` (validate V4.8), one per run | `result` = `PASS` \| `FAIL` \| `DEGRADED`, `jobs_run`, `jobs_deferred`, `jobs_failed`, `duration_ms` (execution), `wait_ms` (time queued for a machine slot), `fix_iterations` (0 first run, 1 and 2 after fixes), `checks`, `cache_hits`, `narrowed` (checks run on a subset or skipped for no impact), `widened` (checks widened to the full command), `selected` / `total` (summed scope) |
|
|
41
|
+
| `ci_local_check` | `scripts/ci-local.sh run`, one per scoped, widened or executed check (at most 20 per run) | `job`, `selector`, `selected`, `total`, `cache` = `hit` \| `miss` \| `dedup` \| `na`, `duration_ms`, `wait_ms` (job slot wait), `widen` (reason or `none`). A remote-CI failure at merge on a check recorded here as narrowed is a selector bug |
|
|
39
42
|
| `liveness` | `scripts/remediate-liveness.sh` | `decision` = `PROCEED` \| `DEFER` \| `EXHAUSTED`, `why` |
|
|
40
43
|
| `phase_attempt` | the engine (`bin/engine/telemetry-seam.mjs`, one per phase attempt; `phase` = the phase id) | `attempt`, `model`, `turns`, `duration_ms`, `cost_usd`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_creation_tokens` (any value the runner did not report is omitted) |
|
|
41
44
|
| `run_terminal` | the engine (`terminate()`, one per run segment) | `reason` (terminal reason), `code` (see `docs/OUTCOME-CODES.md`), `wall_ms` (this engine process, start to terminal) |
|
package/docs/WORK-ON-RUNTIME.md
CHANGED
|
@@ -46,7 +46,8 @@ L3) works, which is why most PRs are reviewed normally.
|
|
|
46
46
|
## 3. Design rules
|
|
47
47
|
|
|
48
48
|
**R1 — Dispatching phases are invoked only by the router.** A *dispatching* phase is one that spawns
|
|
49
|
-
`Agent(...)` sub-agents directly or through `/review-pr`: `work-on:review
|
|
49
|
+
`Agent(...)` sub-agents directly or through `/review-pr`: `work-on:review`, `work-on:remediate` and, since forge#3971,
|
|
50
|
+
`work-on:build` (its B5.6 self-review dispatches the risk-tier roster's domain reviewers on the staged diff).
|
|
50
51
|
Only the router may invoke them, so they always run at router layer + 1 (L2 under `/orchestrate`,
|
|
51
52
|
L1 solo). When another phase needs one, it returns a handoff (`status: NEXT`, `next: <phase>`) and
|
|
52
53
|
the router invokes it. Concretely: review returns `next: remediate` instead of calling remediation.
|
|
@@ -59,7 +60,8 @@ re-runs the missing phases. `/review-pr` (Phase 8, via `scripts/review-finalize.
|
|
|
59
60
|
|
|
60
61
|
**R2 — Non-dispatching forks may nest.** Phases and children that never spawn sub-agents may invoke
|
|
61
62
|
further forks at any depth (F5): build → context/architect/implement/validate → quality-gate,
|
|
62
|
-
review → quality-gate / missing-phase re-dispatch, remediate → close.
|
|
63
|
+
review → quality-gate / missing-phase re-dispatch, remediate → close. The build children never spawn: the
|
|
64
|
+
self-review reviewers are spawned by `work-on:build` itself, and its repair round is an `implement` fork. Forking them keeps each
|
|
63
65
|
context small. If one of them ever needs to spawn sub-agents, it becomes a dispatching phase and
|
|
64
66
|
R1 applies.
|
|
65
67
|
|
|
@@ -99,17 +101,27 @@ phase hosting `/review-pr` cannot wait on a notification. Therefore:
|
|
|
99
101
|
| Skill | Fork | Dispatches sub-agents | Invoked by | Layer solo / orchestrated |
|
|
100
102
|
|-------|------|-----------------------|------------|---------------------------|
|
|
101
103
|
| `work-on` (router) | — | no | user / orchestrator worker | 0 / 1 |
|
|
102
|
-
| `work-on:investigate`, `work-on:decompose`, `work-on:
|
|
104
|
+
| `work-on:investigate`, `work-on:decompose`, `work-on:close` | yes | no | router | 1 / 2 |
|
|
105
|
+
| `work-on:build` (B5.6 self-review) | yes | **yes** (self-review reviewers; degrades to a recorded skip when `Agent` is withheld) | router only (R1) | 1 / 2 |
|
|
103
106
|
| `work-on:build:{context,architect,implement,validate}` | yes | no | `work-on:build` (R2) | 2 / 3 |
|
|
104
107
|
| `quality-gate` | yes | no | validate (R2) | 3 / 4 |
|
|
105
108
|
| `quality-gate` (stale-review re-gate) | yes | no | review (R2) | 2 / 3 |
|
|
106
109
|
| `work-on:review` → `/review-pr` | yes | **yes** | router only (R1) | 1 / 2 |
|
|
107
110
|
| `work-on:remediate` → `/review-pr` | yes | **yes** | router only (R1) | 1 / 2 |
|
|
108
111
|
| `/review-pr` domain reviewers | Agent | no (leaves) | `/review-pr` | 2 / 3 |
|
|
112
|
+
| build self-review reviewers (`FORGE:SELFREVIEW-AGENT:*`) | Agent | no (leaves) | `work-on:build` B5.6 | 2 / 3 |
|
|
109
113
|
|
|
110
114
|
`/orchestrate` Step 4B item 6.4 dispatches a worker that runs `work-on <pr> --remediate`: worker L1,
|
|
111
115
|
remediate L2, reviewers L3, same as the table.
|
|
112
116
|
|
|
117
|
+
### Pre-handoff base sync and union registries (forge#3969)
|
|
118
|
+
|
|
119
|
+
`work-on:review` merges `origin/<base>` into the branch before the first push, so a PR is never born stale and a conflict is resolved while the build context is warm. Hand-maintained append-only registries (test lists, cron targets) are the usual conflict source; `scripts/detect-append-registries.sh` ranks them from history and proves a `.gitattributes` `merge=union` entry safe (`--check-union`). Union is allowed only for line-independent files, never YAML/JSON maps or ordered lists (see `.gitattributes`). `scripts/review-delta.sh` still treats a hand-resolved merge as not `BASE_SYNC_ONLY`, but accepts a merge whose tree is exactly what a mechanical `git merge-tree` of the same parents produces (a clean or union merge), so union registries take the no-panel fast path.
|
|
120
|
+
|
|
121
|
+
### Build-time self-review (forge#3971)
|
|
122
|
+
|
|
123
|
+
`work-on:build` Phase B5.6 runs the risk-tier roster (`scripts/selfreview-plan.sh`, which calls `scripts/review-roster.sh` on the staged diff) before validate, with the same personas as `/review-pr` in SELF-REVIEW MODE (`commands/review-pr-agents/protocols.md`). It is not the official review: reviewers post `FORGE:SELFREVIEW-AGENT:{domain}` issue comments pinned to the staged tree (`Tree:`), never `FORGE:REVIEW-AGENT`, and `review-finalize.sh` / `verify-phase-trail.sh` ignore them. Like R6, completion is read from GitHub with a bounded wait, because reviewers spawned inside a fork report to the root session. A build at a layer where `Agent` is withheld, or any self-review failure, degrades to a recorded skip (`build_selfreview skipped=<reason>`) and never blocks the build.
|
|
124
|
+
|
|
113
125
|
## 5. Router routing
|
|
114
126
|
|
|
115
127
|
| Phase result | Router action |
|
|
@@ -136,3 +148,17 @@ remediate L2, reviewers L3, same as the table.
|
|
|
136
148
|
## 8. Outcome codes
|
|
137
149
|
|
|
138
150
|
Every `PHASE_FAILED`, blocked `PHASE_COMMIT` and `RUN_TERMINAL` run-log event carries a machine-readable `code` from a closed, versioned enum (`bin/engine/outcome-codes.mjs`), and `FORGE:STATE` records the causing code as `outcomeCode`. Codes come from structured evidence only, never from the prose `detail`; an unmapped outcome is `unclassified` with the raw reason kept. See `docs/OUTCOME-CODES.md`.
|
|
151
|
+
|
|
152
|
+
## Remediation re-gate outcomes and spawn retries (forge#4021)
|
|
153
|
+
|
|
154
|
+
The `**Re-gate outcome**` token in a `FORGE:REMEDIATION:COMPLETE` trail is a closed vocabulary the engine parses:
|
|
155
|
+
`AUTO-LANDED`, `HELD-AWAITING-MERGE`, `RE-ESCALATED`, `UNFIXABLE`, `REREVIEW-REQUIRED` and `INPR-FIX-REQUESTED`.
|
|
156
|
+
`INPR-FIX-REQUESTED` (with `**INPR round**: N`) means the re-review approved but posted a `FORGE:INPR_FIX` work
|
|
157
|
+
order. The engine commits `remediate` with `outputs.remediation = "inpr-fix"`, re-arms it once, and re-reviews. The
|
|
158
|
+
`INPR_REMEDIATION` marker count bounds it: a second request escalates to `needs-human`.
|
|
159
|
+
|
|
160
|
+
A CLI spawn failure (`ETIMEDOUT`, `EAGAIN`, `ENOMEM`, `EMFILE`) carries `code = "CLI_SPAWN_TRANSIENT"`. The engine
|
|
161
|
+
retries it after a 10s/30s/60s backoff on a separate counter (max 3) that does not consume a phase attempt; once
|
|
162
|
+
spent, normal attempt accounting applies. `ETIMEDOUT` (a `FORGEDOCK_CLI_TIMEOUT_MS` kill) is in this class by design,
|
|
163
|
+
so a phase that times out every time can run up to `maxAttempts + 3` full-length CLI sessions; lower the timeout or
|
|
164
|
+
max attempts if that cost is unacceptable.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"README.md"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"test": "node --test \"bin/tests/**/*.test.mjs\" \"bin/engine/**/*.test.mjs\" \"bin/stats/**/*.test.mjs\" \"packages/protocol/test/**/*.test.mjs\"",
|
|
51
|
+
"test": "bash scripts/with-run-tmpdir.sh node --test \"bin/tests/**/*.test.mjs\" \"bin/engine/**/*.test.mjs\" \"bin/stats/**/*.test.mjs\" \"packages/protocol/test/**/*.test.mjs\"",
|
|
52
52
|
"test:sh": "bash scripts/run-shell-tests.sh",
|
|
53
53
|
"docs:dev": "vitepress dev docs/site",
|
|
54
54
|
"docs:build": "vitepress build docs/site",
|
|
@@ -109,7 +109,7 @@ export const RESERVED_TYPES = {
|
|
|
109
109
|
partialSentinel: null,
|
|
110
110
|
inlineValue: false,
|
|
111
111
|
requiredFields: [],
|
|
112
|
-
reGateOutcomeValues: ['AUTO-LANDED', 'HELD-AWAITING-MERGE', 'RE-ESCALATED', 'UNFIXABLE'],
|
|
112
|
+
reGateOutcomeValues: ['AUTO-LANDED', 'HELD-AWAITING-MERGE', 'RE-ESCALATED', 'UNFIXABLE', 'INPR-FIX-REQUESTED', 'REREVIEW-REQUIRED'],
|
|
113
113
|
},
|
|
114
114
|
TRAJECTORY: {
|
|
115
115
|
type: 'TRAJECTORY',
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# acceptance-red-green.sh — red-green proof for one acceptance test: it must FAIL on the base commit and PASS on head.
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# Usage: acceptance-red-green.sh --repo-path <worktree> --base-ref <ref> --test-cmd "<cmd>" [--timeout <sec>]
|
|
7
|
+
# Runs <cmd> in <worktree> (head) first. Only when head RAN does it create a detached base worktree at
|
|
8
|
+
# `git merge-base HEAD <base-ref>` (under mktemp -d, never inside the repo), overlay head's changed test files
|
|
9
|
+
# onto it, and run <cmd> there. Each run is classified with classify-test-run.sh.
|
|
10
|
+
# Prints exactly one line on stdout: BASE=<FAILED|RAN|RAN_NOTHING|UNRUNNABLE|SKIPPED> HEAD=<FAILED|RAN|RAN_NOTHING|UNRUNNABLE>
|
|
11
|
+
# FAILED the run executed and failed
|
|
12
|
+
# RAN the run executed and passed
|
|
13
|
+
# RAN_NOTHING the runner exercised nothing
|
|
14
|
+
# UNRUNNABLE could not execute (timeout 124, not executable 126/127, worktree/overlay/classifier failure)
|
|
15
|
+
# SKIPPED base was not run because head did not PASS
|
|
16
|
+
# Never prints a verdict on success of cleanup: the base worktree is removed on every exit path (trap + explicit remove).
|
|
17
|
+
# Callers map: HEAD=RAN BASE=FAILED -> proof; BASE=RAN -> passes on base (gate FAIL); anything else that is not a proof -> DEGRADED/FAIL.
|
|
18
|
+
# Exit: 0 line printed | 2 usage error (stdout empty so callers fail closed)
|
|
19
|
+
# Bash 3.2 portable.
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
REPO=""; BASE_REF=""; CMD=""; TMO=300
|
|
23
|
+
while [ $# -gt 0 ]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--repo-path) REPO="${2-}"; shift 2 ;;
|
|
26
|
+
--base-ref) BASE_REF="${2-}"; shift 2 ;;
|
|
27
|
+
--test-cmd) CMD="${2-}"; shift 2 ;;
|
|
28
|
+
--timeout) TMO="${2-}"; shift 2 ;;
|
|
29
|
+
*) echo "usage: acceptance-red-green.sh --repo-path <dir> --base-ref <ref> --test-cmd <cmd> [--timeout <sec>]" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
[ -n "$REPO" ] && [ -d "$REPO" ] && [ -n "$BASE_REF" ] && [ -n "$CMD" ] || { echo "usage: acceptance-red-green.sh --repo-path <dir> --base-ref <ref> --test-cmd <cmd> [--timeout <sec>]" >&2; exit 2; }
|
|
33
|
+
case "$TMO" in ''|*[!0-9]*) TMO=300 ;; esac
|
|
34
|
+
CLS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/classify-test-run.sh"
|
|
35
|
+
|
|
36
|
+
WORK=$(mktemp -d "${TMPDIR:-/tmp}/forge-redgreen.XXXXXX") || { echo "BASE=UNRUNNABLE HEAD=UNRUNNABLE"; exit 0; }
|
|
37
|
+
BASE_WT="$WORK/base"; ADDED=false
|
|
38
|
+
cleanup() {
|
|
39
|
+
[ "$ADDED" = "true" ] && git -C "$REPO" worktree remove --force "$BASE_WT" >/dev/null 2>&1
|
|
40
|
+
ADDED=false
|
|
41
|
+
rm -rf "$WORK"
|
|
42
|
+
git -C "$REPO" worktree prune >/dev/null 2>&1 || true
|
|
43
|
+
}
|
|
44
|
+
trap cleanup EXIT
|
|
45
|
+
trap 'exit 130' INT TERM
|
|
46
|
+
|
|
47
|
+
TIMEOUT_BIN=$(command -v timeout || command -v gtimeout || true)
|
|
48
|
+
|
|
49
|
+
# run_in <dir> -> prints the classification word
|
|
50
|
+
run_in() {
|
|
51
|
+
local dir="$1" out="$WORK/out.$$.txt" rc cls
|
|
52
|
+
if [ -n "$TIMEOUT_BIN" ]; then ( cd "$dir" && "$TIMEOUT_BIN" "$TMO" bash -c "$CMD" ) >"$out" 2>&1; rc=$?
|
|
53
|
+
else ( cd "$dir" && bash -c "$CMD" ) >"$out" 2>&1; rc=$?; fi
|
|
54
|
+
case "$rc" in 124|126|127) echo UNRUNNABLE; return ;; esac
|
|
55
|
+
cls=$(bash "$CLS" "$rc" "$out" 2>/dev/null)
|
|
56
|
+
case "$cls" in FAILED|RAN|RAN_NOTHING) echo "$cls" ;; *) echo UNRUNNABLE ;; esac
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
HEAD_RES=$(run_in "$REPO")
|
|
60
|
+
if [ "$HEAD_RES" != "RAN" ]; then echo "BASE=SKIPPED HEAD=$HEAD_RES"; exit 0; fi
|
|
61
|
+
|
|
62
|
+
BASE_SHA=$(git -C "$REPO" merge-base HEAD "$BASE_REF" 2>/dev/null)
|
|
63
|
+
[ -n "$BASE_SHA" ] && git -C "$REPO" worktree add --detach "$BASE_WT" "$BASE_SHA" >/dev/null 2>&1 && ADDED=true
|
|
64
|
+
if [ "$ADDED" != "true" ]; then echo "BASE=UNRUNNABLE HEAD=$HEAD_RES"; exit 0; fi
|
|
65
|
+
|
|
66
|
+
# Overlay: head's changed test files onto base (a new test file does not exist at base; running without it is a false red).
|
|
67
|
+
is_test_path() {
|
|
68
|
+
case "$1" in
|
|
69
|
+
/*|..|../*|*/../*|*/..) return 1 ;;
|
|
70
|
+
esac
|
|
71
|
+
case "$1" in
|
|
72
|
+
test/*|tests/*|spec/*|__tests__/*|*/test/*|*/tests/*|*/spec/*|*/__tests__/*|*.test.*|*_test.*|*.spec.*|*_spec.*|*/test_*|test_*) return 0 ;;
|
|
73
|
+
esac
|
|
74
|
+
return 1
|
|
75
|
+
}
|
|
76
|
+
OVERLAY_OK=true
|
|
77
|
+
CHANGED=$( { git -C "$REPO" diff --name-only "$BASE_SHA" 2>/dev/null; git -C "$REPO" ls-files --others --exclude-standard 2>/dev/null; } | sort -u )
|
|
78
|
+
while IFS= read -r f; do
|
|
79
|
+
[ -n "$f" ] || continue
|
|
80
|
+
is_test_path "$f" || continue
|
|
81
|
+
[ -f "$REPO/$f" ] && [ ! -L "$REPO/$f" ] || continue # deleted at head, or a symlink: never copied
|
|
82
|
+
mkdir -p "$BASE_WT/$(dirname "$f")" && cp -p "$REPO/$f" "$BASE_WT/$f" 2>/dev/null && [ -f "$BASE_WT/$f" ] || OVERLAY_OK=false
|
|
83
|
+
done <<< "$CHANGED"
|
|
84
|
+
# Untracked dependency tree (never in git): link it so a missing install is not mistaken for a red run.
|
|
85
|
+
[ -d "$REPO/node_modules" ] && [ ! -e "$BASE_WT/node_modules" ] && ln -s "$REPO/node_modules" "$BASE_WT/node_modules" 2>/dev/null
|
|
86
|
+
|
|
87
|
+
if [ "$OVERLAY_OK" != "true" ]; then BASE_RES=UNRUNNABLE; else BASE_RES=$(run_in "$BASE_WT"); fi
|
|
88
|
+
cleanup # explicit; the EXIT trap is the backstop
|
|
89
|
+
echo "BASE=$BASE_RES HEAD=$HEAD_RES"
|
|
90
|
+
exit 0
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# acceptance-red-green.test.sh — tests for scripts/acceptance-red-green.sh (real temp git repos, no network)
|
|
3
|
+
# Usage: bash scripts/acceptance-red-green.test.sh
|
|
4
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
RG="$SCRIPT_DIR/acceptance-red-green.sh"
|
|
9
|
+
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
|
10
|
+
export TMPDIR="$TMP/tmpdir"; mkdir -p "$TMPDIR"
|
|
11
|
+
PASS=0; FAILN=0
|
|
12
|
+
ok() { PASS=$((PASS+1)); echo "ok - $1"; }
|
|
13
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
|
|
14
|
+
|
|
15
|
+
# make_repo <dir>: base commit on branch main with app.sh (defect: prints bad), then a feature branch is the caller's job
|
|
16
|
+
make_repo() {
|
|
17
|
+
git init -q -b main "$1" && git -C "$1" config user.email t@t && git -C "$1" config user.name t
|
|
18
|
+
printf 'echo bad\n' > "$1/app.sh"; git -C "$1" add -A; git -C "$1" commit -qm base
|
|
19
|
+
git -C "$1" update-ref refs/remotes/origin/main HEAD
|
|
20
|
+
}
|
|
21
|
+
fix_head() { printf 'echo good\n' > "$1/app.sh"; git -C "$1" add -A; git -C "$1" commit -qm fix; }
|
|
22
|
+
leaked() { git -C "$1" worktree list | wc -l | tr -d ' '; }
|
|
23
|
+
check() { # name want repo cmd [extra...]
|
|
24
|
+
local name="$1" want="$2" repo="$3" cmd="$4" got
|
|
25
|
+
got=$(bash "$RG" --repo-path "$repo" --base-ref origin/main --test-cmd "$cmd" 2>/dev/null)
|
|
26
|
+
if [ "$got" = "$want" ]; then ok "$name"; else bad "$name: want '$want' got '$got'"; fi
|
|
27
|
+
if [ "$(leaked "$repo")" = "1" ] && [ -z "$(ls "$TMPDIR")" ]; then ok "$name: base worktree and tmp dir cleaned"; else bad "$name: leaked worktree or tmp ($(leaked "$repo") worktrees)"; fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# 1. real red-green proof: test greps the fixed output, new test file only exists at head (overlay)
|
|
31
|
+
R1="$TMP/r1"; make_repo "$R1"; fix_head "$R1"
|
|
32
|
+
mkdir -p "$R1/tests"; printf 'test "$(bash app.sh)" = good && echo "1 passed"\n' > "$R1/tests/t.sh"; git -C "$R1" add -A; git -C "$R1" commit -qm test
|
|
33
|
+
check "red on base, green on head (new test file overlaid)" "BASE=FAILED HEAD=RAN" "$R1" "bash tests/t.sh"
|
|
34
|
+
|
|
35
|
+
# 2. passes on base: test never touches the defect
|
|
36
|
+
R2="$TMP/r2"; make_repo "$R2"; fix_head "$R2"
|
|
37
|
+
mkdir -p "$R2/tests"; printf 'echo "1 passed"\n' > "$R2/tests/t.sh"; git -C "$R2" add -A; git -C "$R2" commit -qm test
|
|
38
|
+
check "passes on base" "BASE=RAN HEAD=RAN" "$R2" "bash tests/t.sh"
|
|
39
|
+
|
|
40
|
+
# 3. head fails: base is not run
|
|
41
|
+
check "head failure skips base" "BASE=SKIPPED HEAD=FAILED" "$R1" "bash app.sh; exit 1"
|
|
42
|
+
|
|
43
|
+
# 4. RAN_NOTHING on head
|
|
44
|
+
check "runner that exercises nothing on head" "BASE=SKIPPED HEAD=RAN_NOTHING" "$R1" "echo 'no tests ran'"
|
|
45
|
+
|
|
46
|
+
# 5. command not found (127) on head
|
|
47
|
+
check "rc 127 is UNRUNNABLE" "BASE=SKIPPED HEAD=UNRUNNABLE" "$R1" "definitely-not-a-command-xyz"
|
|
48
|
+
|
|
49
|
+
# 6. base cannot execute: the command only exists at head (non-test file), so base yields 127
|
|
50
|
+
R3="$TMP/r3"; make_repo "$R3"; fix_head "$R3"
|
|
51
|
+
printf 'echo "1 passed"\n' > "$R3/runner.sh"; chmod +x "$R3/runner.sh"; git -C "$R3" add -A; git -C "$R3" commit -qm runner
|
|
52
|
+
check "base rc 127 (runner absent at base) is UNRUNNABLE" "BASE=UNRUNNABLE HEAD=RAN" "$R3" "./runner.sh"
|
|
53
|
+
|
|
54
|
+
# 7. base ref unresolvable: worktree cannot be created
|
|
55
|
+
got=$(bash "$RG" --repo-path "$R1" --base-ref origin/does-not-exist --test-cmd "bash tests/t.sh" 2>/dev/null)
|
|
56
|
+
[ "$got" = "BASE=UNRUNNABLE HEAD=RAN" ] && ok "unresolvable base ref is UNRUNNABLE" || bad "unresolvable base ref: got '$got'"
|
|
57
|
+
[ "$(leaked "$R1")" = "1" ] && ok "no worktree after failed setup" || bad "worktree leaked after failed setup"
|
|
58
|
+
|
|
59
|
+
# 8. non-test files are never overlaid (a changed source file stays at base)
|
|
60
|
+
R4="$TMP/r4"; make_repo "$R4"; fix_head "$R4"
|
|
61
|
+
check "source file stays at base (no overlay of non-test files)" "BASE=FAILED HEAD=RAN" "$R4" 'test "$(bash app.sh)" = good && echo "1 passed"'
|
|
62
|
+
|
|
63
|
+
# 9. timeout is UNRUNNABLE when a timeout binary exists
|
|
64
|
+
if command -v timeout >/dev/null 2>&1 || command -v gtimeout >/dev/null 2>&1; then
|
|
65
|
+
got=$(bash "$RG" --repo-path "$R1" --base-ref origin/main --test-cmd "sleep 5" --timeout 1 2>/dev/null)
|
|
66
|
+
[ "$got" = "BASE=SKIPPED HEAD=UNRUNNABLE" ] && ok "timeout is UNRUNNABLE" || bad "timeout: got '$got'"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# 10. usage errors print nothing and exit 2
|
|
70
|
+
out=$(bash "$RG" --repo-path "$R1" 2>/dev/null); rc=$?
|
|
71
|
+
{ [ "$rc" -eq 2 ] && [ -z "$out" ]; } && ok "missing args exit 2, empty stdout" || bad "usage: rc=$rc out='$out'"
|
|
72
|
+
|
|
73
|
+
echo "$PASS passed, $FAILN failed"
|
|
74
|
+
[ "$FAILN" -eq 0 ]
|
|
@@ -15,7 +15,7 @@ ok() { PASS=$((PASS+1)); echo "ok - $1"; }
|
|
|
15
15
|
bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
|
|
16
16
|
eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (got '$2', want '$3')"; fi; }
|
|
17
17
|
|
|
18
|
-
SELECT_JQ='[.[][] | select((.body // "") | startswith("<!-- FORGE:ARCHITECT -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:ARCHITECT:COMPLETE -->")), other: (.body | test("
|
|
18
|
+
SELECT_JQ='[.[][] | select((.body // "") | startswith("<!-- FORGE:ARCHITECT -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:ARCHITECT:COMPLETE -->")), other: (.body | test("\\n[ \\t]*<!-- FORGE:(INVESTIGATOR|CONTEXT|CONTRACT|BUILDER)"))}]'
|
|
19
19
|
DELETE_JQ='[.[] | select(.complete | not)] | if length == 1 and (.[0].other | not) then .[0].id else empty end'
|
|
20
20
|
|
|
21
21
|
# c <id> <assoc> <type> <body>
|
|
@@ -28,7 +28,7 @@ PLAN=$'<!-- FORGE:ARCHITECT -->\n## Plan\n\n<!-- FORGE:ARCHITECT:COMPLETE -->'
|
|
|
28
28
|
PARTIAL=$'<!-- FORGE:ARCHITECT -->\n## Plan (in progress)'
|
|
29
29
|
INVEST=$'<!-- FORGE:INVESTIGATOR -->\nThe architect phase posts FORGE:ARCHITECT and FORGE:ARCHITECT:COMPLETE later.'
|
|
30
30
|
CTX=$'<!-- FORGE:CONTEXT -->\nmentions <!-- FORGE:ARCHITECT -->'
|
|
31
|
-
MIXED=$'<!-- FORGE:ARCHITECT -->\nquoted
|
|
31
|
+
MIXED=$'<!-- FORGE:ARCHITECT -->\nquoted:\n<!-- FORGE:INVESTIGATOR -->'
|
|
32
32
|
|
|
33
33
|
eq "INVESTIGATOR mentioning FORGE:ARCHITECT is not selected" "$(arch "$(c 1 NONE Bot "$INVEST")" | jq length)" 0
|
|
34
34
|
eq "INVESTIGATOR mentioning FORGE:ARCHITECT is never deleted" "$(del_id "$(c 1 NONE Bot "$INVEST")")" ""
|
|
@@ -39,10 +39,19 @@ eq "single partial plan -> delete its id" "$(del_id "$(c 4 NONE Bot "$PARTIAL")"
|
|
|
39
39
|
eq "two partial plans -> delete nothing" "$(del_id "$(c 4 NONE Bot "$PARTIAL")"$'\n'"$(c 5 NONE Bot "$PARTIAL")")" ""
|
|
40
40
|
eq "untrusted author partial ignored" "$(del_id "$(c 6 NONE User "$PARTIAL")")" ""
|
|
41
41
|
eq "untrusted author complete ignored" "$(n_complete "$(c 6 NONE User "$PLAN")")" 0
|
|
42
|
-
eq "partial
|
|
42
|
+
eq "partial with another FORGE marker at a line start is not deleted" "$(del_id "$(c 7 NONE Bot "$MIXED")")" ""
|
|
43
43
|
eq "investigator + partial plan -> only the plan id" "$(del_id "$(c 1 NONE Bot "$INVEST")"$'\n'"$(c 8 NONE Bot "$PARTIAL")")" 8
|
|
44
44
|
eq "paginated concatenated arrays" "$(printf '[%s]\n[%s]' "$(c 1 NONE Bot "$INVEST")" "$(c 9 NONE Bot "$PLAN")" | jq -s "$SELECT_JQ" | jq '[.[] | select(.complete)] | length')" 1
|
|
45
45
|
|
|
46
|
+
INLINE=$'<!-- FORGE:ARCHITECT -->\n## Approach\nFollowed the plan in `<!-- FORGE:BUILDER -->` and <!-- FORGE:INVESTIGATOR --> notes.'
|
|
47
|
+
CRLF=$'<!-- FORGE:ARCHITECT -->\r\n<!-- FORGE:BUILDER -->\r\nbody'
|
|
48
|
+
eq "partial quoting a marker inline is picked and deleted e1_inline_picked" "$(del_id "$(c 11 NONE Bot "$INLINE")")" 11
|
|
49
|
+
eq "inline quoting partial is selected as non-other e1_inline_not_other" "$(arch "$(c 11 NONE Bot "$INLINE")" | jq -r '.[0].other')" false
|
|
50
|
+
eq "marker at the start of a later line is refused e2_line_start_refused" "$(arch "$(c 12 NONE Bot "$MIXED")" | jq -r '.[0].other')" true
|
|
51
|
+
eq "own first-line marker never counts as other e4_own_marker_not_other" "$(arch "$(c 13 NONE Bot "$PARTIAL")" | jq -r '.[0].other')" false
|
|
52
|
+
eq "two inline-quoting partials still delete nothing e3_two_partials_none" "$(del_id "$(c 11 NONE Bot "$INLINE")"$'\n'"$(c 14 NONE Bot "$INLINE")")" ""
|
|
53
|
+
eq "CRLF body with a marker line is still refused e5_crlf_refused" "$(del_id "$(c 15 NONE Bot "$CRLF")")" ""
|
|
54
|
+
|
|
46
55
|
# Spec drift guard: the spec carries this exact predicate and no unanchored selector.
|
|
47
56
|
! grep -qF 'contains("FORGE:ARCHITECT")' "$SPEC" && ok "architect.md has no unanchored contains(FORGE:ARCHITECT)" || bad "architect.md still uses unanchored contains"
|
|
48
57
|
grep -qF 'startswith("<!-- FORGE:ARCHITECT -->")' "$SPEC" && ok "architect.md anchors with startswith" || bad "architect.md lacks the startswith anchor"
|
|
@@ -173,12 +173,14 @@ function readForgeYaml(repoPath) {
|
|
|
173
173
|
const yamlPath = join(repoPath, 'forge.yaml');
|
|
174
174
|
if (!existsSync(yamlPath)) return null;
|
|
175
175
|
try {
|
|
176
|
-
const result = spawnSync('yq', ['-r', '.project.owner
|
|
176
|
+
const result = spawnSync('yq', ['-r', '.project.owner, .project.repo', yamlPath], {
|
|
177
177
|
encoding: 'utf8',
|
|
178
178
|
timeout: 5000,
|
|
179
179
|
});
|
|
180
|
-
if (result.status === 0
|
|
181
|
-
|
|
180
|
+
if (result.status === 0) {
|
|
181
|
+
const [owner, name] = result.stdout.split('\n').map(l => l.trim());
|
|
182
|
+
// Both halves must be real values: a partial forge.yaml must not yield "/" or "null/x"
|
|
183
|
+
if (owner && name && owner !== 'null' && name !== 'null') return `${owner}/${name}`;
|
|
182
184
|
}
|
|
183
185
|
} catch (_) {
|
|
184
186
|
// ignore
|
|
@@ -1576,7 +1578,7 @@ async function main() {
|
|
|
1576
1578
|
repo = readForgeYaml(repoPath);
|
|
1577
1579
|
}
|
|
1578
1580
|
if (!repo) {
|
|
1579
|
-
process.stderr.write('[ledger] ERROR: Cannot determine GitHub repo. Pass --repo owner/repo or ensure forge.yaml
|
|
1581
|
+
process.stderr.write('[ledger] ERROR: Cannot determine GitHub repo. Pass --repo owner/repo or ensure forge.yaml has project.owner and project.repo (yq required).\n');
|
|
1580
1582
|
process.exit(1);
|
|
1581
1583
|
}
|
|
1582
1584
|
|
|
@@ -1760,6 +1762,7 @@ async function main() {
|
|
|
1760
1762
|
}
|
|
1761
1763
|
|
|
1762
1764
|
main().catch(e => {
|
|
1763
|
-
process.
|
|
1765
|
+
const showStack = process.argv.includes('--verbose') || !!process.env.FORGE_DEBUG;
|
|
1766
|
+
process.stderr.write(`[ledger] FATAL: ${e && e.message ? e.message : e}\n${showStack && e && e.stack ? e.stack + '\n' : ''}`);
|
|
1764
1767
|
process.exit(1);
|
|
1765
1768
|
});
|
|
@@ -14,7 +14,7 @@ ok() { PASS=$((PASS+1)); echo "ok - $1"; }
|
|
|
14
14
|
bad() { FAILN=$((FAILN+1)); echo "FAIL - $1"; }
|
|
15
15
|
eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (got '$2', want '$3')"; fi; }
|
|
16
16
|
|
|
17
|
-
SELECT_JQ='[.[][] | select((.body // "") | startswith("<!-- FORGE:BUILDER -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:BUILDER:COMPLETE -->")), other: (.body | test("
|
|
17
|
+
SELECT_JQ='[.[][] | select((.body // "") | startswith("<!-- FORGE:BUILDER -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:BUILDER:COMPLETE -->")), other: (.body | test("\\n[ \\t]*<!-- FORGE:(INVESTIGATOR|CONTEXT|CONTRACT|ARCHITECT)"))}]'
|
|
18
18
|
DELETE_JQ='[.[] | select(.complete | not)] | if length == 1 and (.[0].other | not) then .[0].id else empty end'
|
|
19
19
|
|
|
20
20
|
# c <id> <assoc> <type> <body>
|
|
@@ -27,7 +27,7 @@ PLAN=$'<!-- FORGE:BUILDER -->\n## Implementation\n\n<!-- FORGE:BUILDER:COMPLETE
|
|
|
27
27
|
PARTIAL=$'<!-- FORGE:BUILDER -->\n## Implementation (in progress)'
|
|
28
28
|
INVEST=$'<!-- FORGE:INVESTIGATOR -->\nThe build phase later posts FORGE:BUILDER and FORGE:BUILDER:COMPLETE.'
|
|
29
29
|
CTX=$'<!-- FORGE:CONTEXT -->\nmentions <!-- FORGE:BUILDER -->'
|
|
30
|
-
MIXED=$'<!-- FORGE:BUILDER -->\nquoted
|
|
30
|
+
MIXED=$'<!-- FORGE:BUILDER -->\nquoted:\n<!-- FORGE:INVESTIGATOR -->'
|
|
31
31
|
|
|
32
32
|
eq "INVESTIGATOR mentioning FORGE:BUILDER is not selected" "$(arch "$(c 1 NONE Bot "$INVEST")" | jq length)" 0
|
|
33
33
|
eq "INVESTIGATOR mentioning FORGE:BUILDER is never deleted" "$(del_id "$(c 1 NONE Bot "$INVEST")")" ""
|
|
@@ -38,7 +38,7 @@ eq "single partial comment -> delete its id" "$(del_id "$(c 4 NONE Bot "$PARTIAL
|
|
|
38
38
|
eq "two partial comments -> delete nothing" "$(del_id "$(c 4 NONE Bot "$PARTIAL")"$'\n'"$(c 5 NONE Bot "$PARTIAL")")" ""
|
|
39
39
|
eq "untrusted author partial ignored" "$(del_id "$(c 6 NONE User "$PARTIAL")")" ""
|
|
40
40
|
eq "untrusted author complete ignored" "$(n_complete "$(c 6 NONE User "$PLAN")")" 0
|
|
41
|
-
eq "partial
|
|
41
|
+
eq "partial with another FORGE marker at a line start is not deleted" "$(del_id "$(c 7 NONE Bot "$MIXED")")" ""
|
|
42
42
|
eq "investigator + partial comment -> only the comment id" "$(del_id "$(c 1 NONE Bot "$INVEST")"$'\n'"$(c 8 NONE Bot "$PARTIAL")")" 8
|
|
43
43
|
eq "paginated concatenated arrays" "$(printf '[%s]\n[%s]' "$(c 1 NONE Bot "$INVEST")" "$(c 9 NONE Bot "$PLAN")" | jq -s "$SELECT_JQ" | jq '[.[] | select(.complete)] | length')" 1
|
|
44
44
|
|
|
@@ -46,6 +46,15 @@ ARCHC=$'<!-- FORGE:ARCHITECT -->\nplan; the builder will post <!-- FORGE:BUILDER
|
|
|
46
46
|
eq "ARCHITECT mentioning the marker is not selected" "$(arch "$(c 10 NONE Bot "$ARCHC")" | jq length)" 0
|
|
47
47
|
eq "unparsable input fails closed" "$(printf 'not json' | jq -s "$SELECT_JQ" 2>/dev/null | jq -r "$DELETE_JQ" 2>/dev/null)" ""
|
|
48
48
|
|
|
49
|
+
INLINE=$'<!-- FORGE:BUILDER -->\n## Approach\nFollowed the plan in `<!-- FORGE:ARCHITECT -->` and <!-- FORGE:INVESTIGATOR --> notes.'
|
|
50
|
+
CRLF=$'<!-- FORGE:BUILDER -->\r\n<!-- FORGE:ARCHITECT -->\r\nbody'
|
|
51
|
+
eq "partial quoting a marker inline is picked and deleted e1_inline_picked" "$(del_id "$(c 11 NONE Bot "$INLINE")")" 11
|
|
52
|
+
eq "inline quoting partial is selected as non-other e1_inline_not_other" "$(arch "$(c 11 NONE Bot "$INLINE")" | jq -r '.[0].other')" false
|
|
53
|
+
eq "marker at the start of a later line is refused e2_line_start_refused" "$(arch "$(c 12 NONE Bot "$MIXED")" | jq -r '.[0].other')" true
|
|
54
|
+
eq "own first-line marker never counts as other e4_own_marker_not_other" "$(arch "$(c 13 NONE Bot "$PARTIAL")" | jq -r '.[0].other')" false
|
|
55
|
+
eq "two inline-quoting partials still delete nothing e3_two_partials_none" "$(del_id "$(c 11 NONE Bot "$INLINE")"$'\n'"$(c 14 NONE Bot "$INLINE")")" ""
|
|
56
|
+
eq "CRLF body with a marker line is still refused e5_crlf_refused" "$(del_id "$(c 15 NONE Bot "$CRLF")")" ""
|
|
57
|
+
|
|
49
58
|
# Spec drift guard: every mutating site carries this exact predicate and no unanchored selector.
|
|
50
59
|
for f in commands/work-on/build/implement.md commands/work-on/build.md commands/work-on/build/validate.md; do
|
|
51
60
|
SPEC="$ROOT/$f"
|
package/scripts/calibration.mjs
CHANGED
|
@@ -143,10 +143,13 @@ export function readForgeYaml(repoPath) {
|
|
|
143
143
|
const configPath = join(repoPath, 'forge.yaml');
|
|
144
144
|
if (!existsSync(configPath)) return null;
|
|
145
145
|
try {
|
|
146
|
-
const result = spawnSync('yq', ['-r', '.project.owner
|
|
146
|
+
const result = spawnSync('yq', ['-r', '.project.owner, .project.repo', configPath], {
|
|
147
147
|
encoding: 'utf8', timeout: 5000,
|
|
148
148
|
});
|
|
149
|
-
if (result.status === 0
|
|
149
|
+
if (result.status === 0) {
|
|
150
|
+
const [owner, name] = result.stdout.split('\n').map(l => l.trim());
|
|
151
|
+
if (owner && name && owner !== 'null' && name !== 'null') return `${owner}/${name}`;
|
|
152
|
+
}
|
|
150
153
|
} catch (_) { /* ignore */ }
|
|
151
154
|
return null;
|
|
152
155
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
# Run: bash scripts/cap-findings.test.sh (bash 3.2 compatible)
|
|
7
7
|
|
|
8
8
|
set -u
|
|
9
|
+
# Every mktemp below lands in one private dir that the EXIT trap removes (forge#4016).
|
|
10
|
+
_SUITE_TMP=$(mktemp -d); export TMPDIR="$_SUITE_TMP"; trap 'rm -rf "$_SUITE_TMP"' EXIT
|
|
9
11
|
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
10
12
|
S="$DIR/cap-findings.sh"
|
|
11
13
|
pass=0; fail=0
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
# Run: bash scripts/check-contract-scope.test.sh (bash 3.2 compatible)
|
|
7
7
|
|
|
8
8
|
set -u
|
|
9
|
+
# Every mktemp below lands in one private dir that the EXIT trap removes (forge#4016).
|
|
10
|
+
_SUITE_TMP=$(mktemp -d); export TMPDIR="$_SUITE_TMP"; trap 'rm -rf "$_SUITE_TMP"' EXIT
|
|
9
11
|
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
10
12
|
S="$DIR/check-contract-scope.sh"
|
|
11
13
|
pass=0; fail=0
|