forgedock 1.3.0 → 1.5.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/batch-runner.mjs +31 -2
- package/bin/engine/admission.mjs +294 -0
- package/bin/engine/admission.test.mjs +265 -0
- package/bin/engine/invariants.mjs +17 -4
- package/bin/engine/phases.mjs +201 -35
- package/bin/engine/resolve.mjs +256 -0
- package/bin/engine/resolve.test.mjs +149 -0
- package/bin/engine/runlog.mjs +11 -1
- package/bin/engine/state.mjs +20 -28
- package/bin/engine-cli.mjs +86 -5
- package/bin/engine.mjs +543 -36
- package/bin/forgedock.mjs +397 -59
- package/bin/hooks/interactive-engine.mjs +50 -24
- package/bin/hooks/pre-tool-use.mjs +191 -13
- package/bin/journey.mjs +586 -44
- package/bin/labels.json +5 -0
- package/bin/runner.mjs +478 -10
- package/bin/watch-utils.mjs +258 -0
- package/commands/cleanup.md +34 -5
- package/commands/issue.md +14 -4
- package/commands/orchestrate/config.md +43 -1
- package/commands/orchestrate/phase-1-resolve.md +348 -11
- package/commands/orchestrate/phase-3-dependency.md +272 -49
- package/commands/orchestrate/phase-4-execution.md +622 -60
- package/commands/orchestrate/phase-5-cleanup.md +2 -0
- package/commands/quality-gate.md +6 -6
- package/commands/review-pr-staging.md +51 -8
- package/commands/review-pr.md +120 -39
- package/commands/upgrade-deps.md +7 -2
- package/commands/work-on/decompose.md +2 -0
- package/commands/work-on/investigate.md +22 -4
- package/commands/work-on/remediate.md +55 -4
- package/commands/work-on/review.md +40 -1
- package/commands/work-on.md +19 -4
- package/docs/CONFIG.md +120 -0
- package/docs/spec/eval-run-result.md +8 -4
- package/docs/spec/label-state-machine.md +9 -0
- package/package.json +2 -2
- package/packages/protocol/src/emit.js +5 -22
- package/packages/protocol/src/html-comment-escape.js +68 -0
- package/packages/protocol/src/index.js +1 -0
- package/packages/protocol/src/parse.js +5 -6
- package/packages/protocol/src/phases.js +105 -0
- package/packages/protocol/src/types.js +17 -0
- package/packages/protocol/src/validate.js +24 -1
- package/scripts/check-admission-parity.mjs +167 -0
- package/scripts/check-command-side-effects.sh +181 -66
- package/scripts/check-phase-registry-drift.mjs +118 -0
- package/scripts/check-spec-markers.sh +3 -0
- package/scripts/classify-lane.sh +29 -3
- package/scripts/derive-finding-milestone.sh +184 -0
- package/scripts/doctor-pipeline-state.sh +1 -0
- package/scripts/extract-affected-files.sh +232 -0
- package/scripts/extract-affected-files.test.sh +331 -0
- package/scripts/forge-run.sh +39 -4
- package/scripts/issue-dedup.sh +69 -8
- package/scripts/issue-dedup.test.sh +142 -0
- package/scripts/severity-to-priority.sh +81 -0
|
@@ -47,10 +47,49 @@ gh pr list {GH_FLAG} --head {BRANCH} --json number,state,url 2>/dev/null
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
**Resume check**:
|
|
50
|
-
- If PR already exists AND is OPEN →
|
|
50
|
+
- If PR already exists AND is OPEN → run the **HEAD-unchanged re-review guard** below before proceeding to Phase R3
|
|
51
51
|
- If PR already exists AND is MERGED → return `REVIEW_RESULT: status: ALREADY_MERGED`
|
|
52
52
|
- If no `<!-- FORGE:BUILDER -->` comment exists → EXIT with `REVIEW_RESULT: status: BLOCKED`, blocker: "FORGE:BUILDER comment not found — implement phase may not have completed"
|
|
53
53
|
|
|
54
|
+
**HEAD-unchanged re-review guard** (MANDATORY when a PR already exists and is OPEN) <!-- Added: forge#2243 --> — a PR whose most recent review verdict was CHANGES REQUESTED must not be resubmitted for a full domain-agent review fan-out if nothing has changed since that verdict. `/review-pr` records the exact commit it reviewed in its verdict comment (`CHANGES REQUESTED: commit {sha} — ...`, see `commands/review-pr.md` Phase 8/9); compare that recorded sha against the PR's current `headRefOid`:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# NOTE: the CHANGES REQUESTED verdict is posted via `gh pr review --comment` (see
|
|
58
|
+
# commands/review-pr.md Phase 7B) — that creates a PullRequestReview, which surfaces under
|
|
59
|
+
# the `reviews` field, NOT `comments` (issue/PR comments are a separate GraphQL object).
|
|
60
|
+
# Query both --json reviews and --json comments (same combined-read pattern already used in
|
|
61
|
+
# work-on.md's REVIEW_BODIES/REVIEW_PRESENT checks) so this guard works regardless of which
|
|
62
|
+
# GitHub object type carries the verdict text.
|
|
63
|
+
LAST_VERDICT=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
|
|
64
|
+
--jq '([.reviews[] | {body, created_at: (.submittedAt // "")}] + [.comments[] | {body, created_at: (.createdAt // "")}]) | map(select(.body | test("CHANGES REQUESTED: commit "))) | sort_by(.created_at) | last | .body // ""' 2>/dev/null)
|
|
65
|
+
LAST_VERDICT_SHA=$(echo "$LAST_VERDICT" | grep -oE 'CHANGES REQUESTED: commit [0-9a-f]+' | grep -oE '[0-9a-f]+$' | head -1)
|
|
66
|
+
CURRENT_SHA_SHORT=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null | cut -c1-7)
|
|
67
|
+
|
|
68
|
+
if [ -n "$LAST_VERDICT_SHA" ] && [ "$LAST_VERDICT_SHA" = "$CURRENT_SHA_SHORT" ]; then
|
|
69
|
+
echo "HEAD unchanged since last CHANGES REQUESTED verdict ($LAST_VERDICT_SHA) — skipping re-review."
|
|
70
|
+
# No DRY_RUN/governor guard here — consistent with every other gh issue comment/edit call
|
|
71
|
+
# already in this file (e.g. the Push Failed / Push Blocked sections below), none of which
|
|
72
|
+
# are gated either. This is a report-and-stop action (blocks re-review, does not merge or
|
|
73
|
+
# delete anything), same risk class as those pre-existing calls.
|
|
74
|
+
REREVIEW_SKIP_BODY=$(cat <<SKIP_EOF
|
|
75
|
+
## Re-Review Skipped — HEAD Unchanged
|
|
76
|
+
|
|
77
|
+
PR #{PR_NUMBER}'s HEAD (${CURRENT_SHA_SHORT}) has not changed since the last CHANGES REQUESTED verdict. Re-running /review-pr would re-review byte-identical code and reproduce the same verdict — this is a pure waste of a full domain-agent fan-out.
|
|
78
|
+
|
|
79
|
+
The PR is already needs-human (or will be shortly, if this is the first time this guard has fired for it). Progress here now depends on remediation (fix the findings, push a new commit, re-review) rather than another raw review submission. If running under /orchestrate, item 6.4 in phase-4-execution.md auto-dispatches remediation for any needs-human-gated issue, including this one (see forge#2243) — no manual action should be needed. If running standalone, invoke /work-on {PR_NUMBER} --remediate --issue {NUMBER} directly.
|
|
80
|
+
|
|
81
|
+
<!-- FORGE:REREVIEW_SKIPPED -->
|
|
82
|
+
SKIP_EOF
|
|
83
|
+
)
|
|
84
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "$REREVIEW_SKIP_BODY" # <!-- allowlist:check-command-side-effects -->
|
|
85
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label needs-human 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
86
|
+
# Return REVIEW_RESULT: status: BLOCKED — do not invoke /review-pr again on unchanged HEAD
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If `LAST_VERDICT_SHA` is empty (no prior CHANGES REQUESTED verdict found) or differs from the current HEAD (a new commit was pushed since the last verdict — e.g. by remediation), proceed normally to Phase R3.
|
|
92
|
+
|
|
54
93
|
---
|
|
55
94
|
|
|
56
95
|
## Phase R1: Pre-Push Ancestry Guard
|
package/commands/work-on.md
CHANGED
|
@@ -247,6 +247,8 @@ Extract project prefix and issue number. If `next`/`pick`: list open issues sort
|
|
|
247
247
|
|
|
248
248
|
### 0A.1: Remediation Mode Detection (`--remediate`) <!-- Added: forge#1813 -->
|
|
249
249
|
|
|
250
|
+
**Engine coverage** (forge#2379): `remediate` is now a registered phase in the headless engine's phase table (`packages/protocol/src/phases.js`, `bin/engine/phases.mjs`) — see `commands/work-on/remediate.md`'s own "Engine coverage" note for the current, documented limitation (a single continuous headless `runIssue()` walk cannot yet reach it; this prose-layer standalone-invocation path below remains the only way `remediate` actually runs today).
|
|
251
|
+
|
|
250
252
|
**Check first, before any other Phase 0 routing** — if `$ARGUMENTS` contains `--remediate`, this is NOT a normal issue-pipeline invocation. The first positional argument is a **PR number**, not an issue number:
|
|
251
253
|
|
|
252
254
|
```bash
|
|
@@ -524,9 +526,9 @@ a hard dependency.
|
|
|
524
526
|
|
|
525
527
|
## Phase 1: Investigation
|
|
526
528
|
|
|
527
|
-
**Skip if**: `<!-- FORGE:INVESTIGATOR -->` exists with `<!-- INVESTIGATION:COMPLETE -->` in the SAME comment.
|
|
529
|
+
**Skip if**: `<!-- FORGE:INVESTIGATOR -->` exists with `<!-- INVESTIGATION:COMPLETE -->` OR `<!-- INVESTIGATION:INVALID -->` in the SAME comment. `INVESTIGATION:INVALID` is the terminal sentinel Phase 1C emits for an INVALID verdict (see 1C below) — it signals investigation completion just as much as `INVESTIGATION:COMPLETE` does, not an interrupted state.
|
|
528
530
|
|
|
529
|
-
**Partial investigation**: If investigator comment exists BUT `<!-- INVESTIGATION:COMPLETE -->` is
|
|
531
|
+
**Partial investigation**: If investigator comment exists BUT NEITHER `<!-- INVESTIGATION:COMPLETE -->` NOR `<!-- INVESTIGATION:INVALID -->` is present → investigation was interrupted. Delete the partial comment and restart:
|
|
530
532
|
```bash
|
|
531
533
|
COMMENT_ID=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
532
534
|
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .id')
|
|
@@ -686,9 +688,18 @@ Use `forge.yaml → review.tech_stack` and the issue domain labels to identify e
|
|
|
686
688
|
|
|
687
689
|
### 1C: Post investigation comment
|
|
688
690
|
|
|
689
|
-
The comment MUST include
|
|
691
|
+
The comment MUST include a terminal sentinel at the very end. **The sentinel is conditional on the resolved Verdict — it is NOT always `<!-- INVESTIGATION:COMPLETE -->`:**
|
|
692
|
+
|
|
693
|
+
- **Verdict is INVALID** → close with `<!-- INVESTIGATION:INVALID -->`. This is a distinct, already-wired-up terminal marker: `bin/engine/phases.mjs`'s `detectOutcome` for the `investigate` phase checks for it explicitly (ahead of `INVESTIGATION:COMPLETE`) and routes to `terminalReason: "invalid"`; `bin/hooks/interactive-engine.mjs`'s `PHASE_MARKERS` table also already treats it as terminal. Emitting `INVESTIGATION:COMPLETE` for an INVALID verdict is what previously caused every completed investigation to read as `{verdict: "CONFIRMED"}` regardless of actual outcome — do NOT regress this (forge#2350).
|
|
694
|
+
- **Verdict is CONFIRMED or PARTIAL** → close with `<!-- INVESTIGATION:COMPLETE -->` as before (PARTIAL still routes to `ready-to-build` below — only INVALID gets the distinct terminal sentinel).
|
|
690
695
|
|
|
691
696
|
```bash
|
|
697
|
+
if [ "{VERDICT}" = "INVALID" ]; then
|
|
698
|
+
INVESTIGATION_SENTINEL="<!-- INVESTIGATION:INVALID -->"
|
|
699
|
+
else
|
|
700
|
+
INVESTIGATION_SENTINEL="<!-- INVESTIGATION:COMPLETE -->"
|
|
701
|
+
fi
|
|
702
|
+
|
|
692
703
|
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
693
704
|
## Investigation Report
|
|
694
705
|
|
|
@@ -728,9 +739,11 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:INVESTIGATOR -->
|
|
|
728
739
|
**{YES|NO}** — {reason}
|
|
729
740
|
{if YES: proposed sub-issues with titles and dependencies}
|
|
730
741
|
|
|
731
|
-
|
|
742
|
+
${INVESTIGATION_SENTINEL}"
|
|
732
743
|
```
|
|
733
744
|
|
|
745
|
+
**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.
|
|
746
|
+
|
|
734
747
|
### 1D: Correction capture (MANDATORY — run before label update)
|
|
735
748
|
|
|
736
749
|
Before routing, scan all non-agent comments for correction signals from the repository owner. Correction signals are owner comments that contain phrases like "no, use", "actually use", "use X instead", "not X, use Y", or "wrong branch". If found, write the correction to `forge.yaml → learned:` and emit a `FORGE:LEARNED` annotation.
|
|
@@ -890,6 +903,8 @@ gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from in
|
|
|
890
903
|
|
|
891
904
|
## Phase 2: Decomposition (Conditional)
|
|
892
905
|
|
|
906
|
+
**Engine coverage** (forge#2379): this phase's dispatch target (`work-on/decompose`) is now a real phase in the headless engine's phase table — `decompose` in `packages/protocol/src/phases.js`/`bin/engine/phases.mjs`, live-wired so `investigate`'s `DECOMPOSE:YES` outcome hands off to it instead of terminating the run in place. This prose Phase 2 remains the interactive/prose dispatch path; the engine path is the headless equivalent, both invoking the same `commands/work-on/decompose.md` subcommand.
|
|
907
|
+
|
|
893
908
|
**Skip if**: Already decomposed, is a sub-issue, or investigation says decompose: NO.
|
|
894
909
|
|
|
895
910
|
**Trigger if**: Investigation assessment says YES — 2+ signals match (at least 1 Strong): multiple task types, 3+ service groups, phased requirements, 6+ files across directories.
|
package/docs/CONFIG.md
CHANGED
|
@@ -48,6 +48,8 @@ If you have both a local `claude` CLI and `ANTHROPIC_API_KEY` set and want to pi
|
|
|
48
48
|
| [`agents`](#agents-optional) | No | Default model for pipeline agents |
|
|
49
49
|
| [`repos`](#repos-optional) | No | Multi-repository routing |
|
|
50
50
|
| [`project_board`](#project_board-optional) | No | GitHub Projects v2 integration |
|
|
51
|
+
| [`orchestration`](#orchestration-optional) | No | `/orchestrate` concurrency and cascade admission policy |
|
|
52
|
+
| [`pipeline`](#pipeline-optional) | No | `/orchestrate` batch-engine tuning (stall detection, token budget, narration) |
|
|
51
53
|
| [`services`](#services-optional) | No | External service URLs and IDs |
|
|
52
54
|
| [`review`](#review-optional) | No | Context injected into review agents |
|
|
53
55
|
| [`devdocs`](#devdocs-optional) | No | Devdocs knowledge tree path |
|
|
@@ -290,6 +292,124 @@ gh project field-list <project_number> --owner <owner> --format json \
|
|
|
290
292
|
|
|
291
293
|
---
|
|
292
294
|
|
|
295
|
+
## `orchestration` (OPTIONAL)
|
|
296
|
+
|
|
297
|
+
Concurrency limits and cascade admission policy for `/orchestrate`'s batch engine. Previously read at runtime (`orchestration.max_concurrent`) but undocumented; this section is the first place it — and the newer `cascade` sub-section (forge#2234) — are formally specified.
|
|
298
|
+
|
|
299
|
+
```yaml
|
|
300
|
+
orchestration:
|
|
301
|
+
# Maximum number of /work-on sub-agents dispatched concurrently. Guards against
|
|
302
|
+
# saturating the Anthropic API rate limit in one burst on a large ready set.
|
|
303
|
+
# Default: 12.
|
|
304
|
+
max_concurrent: 12
|
|
305
|
+
|
|
306
|
+
cascade:
|
|
307
|
+
# Named preset expanding to the granular keys below. Any granular key set
|
|
308
|
+
# explicitly overrides just that field — the preset supplies defaults, not
|
|
309
|
+
# hard values. Default: "balanced".
|
|
310
|
+
# all — admit everything: no generation cap, no token cap, every
|
|
311
|
+
# heuristic-based defer disabled. For a maintainer draining
|
|
312
|
+
# a backlog who wants /orchestrate to "pick up everything."
|
|
313
|
+
# balanced — the pre-#2234 hardcoded behavior, unchanged: generation
|
|
314
|
+
# cap at 1 (i.e. cascade-spawned findings deferred),
|
|
315
|
+
# 900000-token per-batch ceiling, idle/keyword/same-file
|
|
316
|
+
# heuristics all active.
|
|
317
|
+
# conservative — same admission shape as balanced, with a lower
|
|
318
|
+
# (450000) token ceiling for cost-sensitive or noisy repos.
|
|
319
|
+
policy: "balanced"
|
|
320
|
+
|
|
321
|
+
# Maximum cascade generation depth admitted. Generation 1 = an original
|
|
322
|
+
# issue (not spawned from a review-finding). Generation 2 = spawned from a
|
|
323
|
+
# review-finding. Generation 3 = spawned from a finding that was itself
|
|
324
|
+
# spawned from a review-finding. Accepts a positive integer or the literal
|
|
325
|
+
# string "unlimited". Governs Phase 1 resolve-time admission only (the
|
|
326
|
+
# `cascade`/`review-findings`/`findings` resolution in phase-1-resolve.md)
|
|
327
|
+
# — it does NOT relax Step 4C's absolute autonomous-cascade defer, which
|
|
328
|
+
# stays hardcoded regardless of this value (see phase-4-execution.md Step
|
|
329
|
+
# 4C rule 1). This is what makes "admit gen-2, stop at gen-3" expressible,
|
|
330
|
+
# which the pre-#2234 --allow-gen2 flag (all-or-nothing) could not say.
|
|
331
|
+
# Default: 1 (from the "balanced" preset); "unlimited" under "all".
|
|
332
|
+
max_generation: 1
|
|
333
|
+
|
|
334
|
+
# Per-batch token ceiling for Step 4C's review-finding cascade dispatch.
|
|
335
|
+
# New home for the same lever previously read only as
|
|
336
|
+
# `pipeline.token_budget_per_batch` (kept working as a deprecated alias —
|
|
337
|
+
# see the `pipeline` section below). Accepts a positive integer or
|
|
338
|
+
# "unlimited". Default: 900000 (from "balanced"); 450000 under
|
|
339
|
+
# "conservative"; "unlimited" under "all".
|
|
340
|
+
token_budget: 900000
|
|
341
|
+
|
|
342
|
+
# Whether a fully human-gated original batch (forge#1814's
|
|
343
|
+
# BATCH_FULLY_GATED — every original-batch issue DONE/FAILED/GATED with at
|
|
344
|
+
# least one GATED) suppresses further cascade-finding dispatch. Default:
|
|
345
|
+
# true (from "balanced"); false under "all".
|
|
346
|
+
defer_on_batch_gated: true
|
|
347
|
+
|
|
348
|
+
# Whether the comment/typo title-keyword heuristic defers P3-and-below
|
|
349
|
+
# cascade findings. Default: true (from "balanced"); false under "all".
|
|
350
|
+
keyword_heuristic: true
|
|
351
|
+
|
|
352
|
+
# Whether a P3 finding sharing a file with the active batch is deferred
|
|
353
|
+
# (avoids serializing agents on same-file predecessor edges). Default:
|
|
354
|
+
# true (from "balanced"); false under "all".
|
|
355
|
+
p3_same_file_defer: true
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
| Field | Type | Required | Description |
|
|
359
|
+
|-------|------|----------|-------------|
|
|
360
|
+
| `max_concurrent` | integer | No | Max concurrent `/work-on` sub-agent dispatch. Default: 12 |
|
|
361
|
+
| `cascade.policy` | string | No | `all` \| `balanced` \| `conservative`. Default: `balanced` |
|
|
362
|
+
| `cascade.max_generation` | integer or `"unlimited"` | No | Max cascade generation admitted at Phase 1 resolve time. Default: 1 |
|
|
363
|
+
| `cascade.token_budget` | integer or `"unlimited"` | No | Per-batch token ceiling for Step 4C cascade dispatch. Default: 900000 (deprecated alias: `pipeline.token_budget_per_batch`) |
|
|
364
|
+
| `cascade.defer_on_batch_gated` | boolean | No | Suppress cascade dispatch when the original batch is fully human-gated. Default: `true` |
|
|
365
|
+
| `cascade.keyword_heuristic` | boolean | No | Defer P3-and-below comment/typo-titled findings. Default: `true` |
|
|
366
|
+
| `cascade.p3_same_file_defer` | boolean | No | Defer P3 findings sharing a file with the active batch. Default: `true` |
|
|
367
|
+
|
|
368
|
+
**Hard invariant — not configurable**: safety exclusions (findings whose `## Problem` section indicates security/billing/anti-bot/auth concerns) are never batched and never auto-admitted by any `cascade.policy`, including `all`. That exclusion is enforced upstream of this section (the P3 batching eligibility check) and has no corresponding key here by design.
|
|
369
|
+
|
|
370
|
+
**CLI overrides**: `--policy`, `--max-generation`, `--token-budget` on `/orchestrate` take precedence over these keys for a single invocation — see `commands/orchestrate/config.md` → "Cascade Policy CLI Flags".
|
|
371
|
+
|
|
372
|
+
**Commands that use this section**: `orchestrate`
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## `pipeline` (OPTIONAL)
|
|
377
|
+
|
|
378
|
+
Tuning knobs for `/orchestrate`'s batch engine — stall detection, narration verbosity, and (deprecated alias only, see `orchestration.cascade` above) the review-finding cascade token budget. All keys optional; sane defaults apply when omitted.
|
|
379
|
+
|
|
380
|
+
```yaml
|
|
381
|
+
pipeline:
|
|
382
|
+
# Minutes an agent may sit idle (no FORGE:HEARTBEAT / label change) before
|
|
383
|
+
# /orchestrate's stall detector treats it as stuck and attempts an
|
|
384
|
+
# auto-resume. Default: 15.
|
|
385
|
+
stall_timeout_minutes: 15
|
|
386
|
+
|
|
387
|
+
# Deprecated alias for orchestration.cascade.token_budget (forge#2234) — kept
|
|
388
|
+
# working so existing configs are unaffected. When both are set,
|
|
389
|
+
# orchestration.cascade.token_budget wins. Default: 900000.
|
|
390
|
+
token_budget_per_batch: 900000
|
|
391
|
+
|
|
392
|
+
# Estimated token cost of a single P3-or-lower review-finding /work-on
|
|
393
|
+
# pipeline, used to decrement the token budget above. Default: 150000.
|
|
394
|
+
token_estimate_per_finding: 150000
|
|
395
|
+
|
|
396
|
+
# Narration verbosity for /orchestrate's per-completion status updates.
|
|
397
|
+
# "terse" (default): one line per completion. "verbose": adds a running
|
|
398
|
+
# per-completion recap table. Cosmetic only — never changes which phases run.
|
|
399
|
+
narration: "terse"
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
| Field | Type | Required | Description |
|
|
403
|
+
|-------|------|----------|-------------|
|
|
404
|
+
| `stall_timeout_minutes` | integer | No | Idle minutes before the stall detector attempts auto-resume. Default: 15 |
|
|
405
|
+
| `token_budget_per_batch` | integer | No | **Deprecated** — use `orchestration.cascade.token_budget`. Default: 900000 |
|
|
406
|
+
| `token_estimate_per_finding` | integer | No | Estimated tokens per P3-or-lower cascade finding. Default: 150000 |
|
|
407
|
+
| `narration` | string | No | `terse` \| `verbose`. Default: `terse` |
|
|
408
|
+
|
|
409
|
+
**Commands that use this section**: `orchestrate`
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
293
413
|
## `services` (OPTIONAL)
|
|
294
414
|
|
|
295
415
|
External service endpoints and identifiers. Used by analytics and monitoring commands.
|
|
@@ -27,7 +27,9 @@ Each headless run of a single corpus issue produces one JSON object:
|
|
|
27
27
|
"interventionCount": 0, // integer — number of human interventions (see below)
|
|
28
28
|
|
|
29
29
|
// Optional fields (nullable — do NOT block on missing upstream data)
|
|
30
|
-
"cost": null, // null | number —
|
|
30
|
+
"cost": null, // null | number — total billed tokens (input + output); null when
|
|
31
|
+
// the run reports no usage data (e.g. CLI backend, or an errored
|
|
32
|
+
// run). NOT a USD amount — no per-model pricing table exists yet.
|
|
31
33
|
"iterations": 23, // integer — tool-use loop iteration count
|
|
32
34
|
"stopReason": "end_turn", // string — Anthropic stop_reason or "max_iterations"
|
|
33
35
|
"error": null, // null | string — error message when status === "error"
|
|
@@ -109,7 +111,8 @@ a scorecard in the format consumed by `scripts/eval-gate-scorecard.mjs` (#1286):
|
|
|
109
111
|
"mean_wall_clock_ms": 38200, // number | null — arithmetic mean wall-clock
|
|
110
112
|
"median_wall_clock_ms": 35000, // number | null — median wall-clock
|
|
111
113
|
"total_intervention_count": 1, // integer — sum of interventionCount across all runs
|
|
112
|
-
"cost": null, // null | number — sum of per-run
|
|
114
|
+
"cost": null, // null | number — sum of per-run billed-token counts (not USD);
|
|
115
|
+
// null if any run has cost: null
|
|
113
116
|
"run_mode": "full", // "full" | "subset"
|
|
114
117
|
"spec_sha": "abc123" // string | null — git SHA of commands/ at run time
|
|
115
118
|
}
|
|
@@ -128,8 +131,9 @@ hangs and crashes, not just logic failures.
|
|
|
128
131
|
`wallClockMs`, `interventionCount`) or non-integer/non-string values cause
|
|
129
132
|
exit code 1 (validation error).
|
|
130
133
|
3. **Cost is nullable**: if any run has `cost: null`, the scorecard's
|
|
131
|
-
`cost` is `null`. The field becomes numeric
|
|
132
|
-
|
|
134
|
+
`cost` is `null`. The field becomes numeric once every run reports a
|
|
135
|
+
billed-token count (see the per-run `cost` field above — this is a
|
|
136
|
+
token-count sum, not a USD amount).
|
|
133
137
|
4. **Rate consistency**: `success_rate_pct` is always computed as
|
|
134
138
|
`round(successful_runs / issues_run * 100, 2)`. The CI gate (#1286) validates
|
|
135
139
|
this computed value matches the reported value.
|
|
@@ -19,9 +19,12 @@ See also: [FORGE Annotation Protocol §6](forge-protocol-v1.md#6-label-state-mac
|
|
|
19
19
|
| `workflow:invalid` | Issue closed as invalid | Investigator agent |
|
|
20
20
|
| `workflow:decomposed` | Issue decomposed into sub-issues | Decomposer agent |
|
|
21
21
|
| `needs-human` | Pipeline blocked, human intervention required | Any agent on error |
|
|
22
|
+
| `workflow:engine-error` | Pipeline blocked by the engine/tool itself breaking (CLI crash, missing SDK/API key, exhausted retries with no attempt ever reaching a real outcome) — not a genuine content-level judgment call | `terminate()` in `bin/engine.mjs` |
|
|
22
23
|
|
|
23
24
|
`workflow:awaiting-merge` vs `needs-human`: both are terminal (the pipeline stops advancing the issue automatically), but they mean different things. `needs-human` means the pipeline hit a condition it cannot resolve on its own (genuinely blocked — conflicting PR, failed verdict, calibration/trust escalation, etc.) and a human must diagnose and act. `workflow:awaiting-merge` means the opposite: the PR was previously escalated to `needs-human`, has since been remediated and re-reviewed to a clean `APPROVED` verdict with no mergeability blockers, but does not yet meet the automated auto-land bar (see forge#1809 Q1) — a human only needs to click merge, not diagnose a problem. `scripts/transition-label.sh` only clears `needs-human` (best-effort) when the target state is `workflow:awaiting-merge` — every other forward transition leaves a pre-existing `needs-human` label untouched, preserving its sticky/terminal semantics.
|
|
24
25
|
|
|
26
|
+
`workflow:engine-error` vs `needs-human` (taxonomy split — commit `90376f5` / #2261): before this commit, `terminate()` wrote `needs-human` for *every* exhausted-retry blocked outcome, including cases where the engine/CLI tool itself was breaking (crash, missing API key, no SDK) rather than a genuine human-judgment call. As of `90376f5`, `runPhaseWithRetry()` tags an outcome `engine-error` when every exhausted attempt failed via a thrown runner exception (never reached `detectOutcome()`) — the engine/tool is broken, not the issue content — and `terminate()` writes `workflow:engine-error` instead of `needs-human` for that case. A genuine content-level block (unmerged PR, unresolved branch, a fixed-point zero-commits case) still writes `needs-human` unchanged. **Consequence for downstream consumers**: any dashboard, query, or health metric that counts `needs-human` issues computed a different, larger population before `90376f5` than after — a subset of what used to be `needs-human` now shows up as `workflow:engine-error` instead. This is a one-time taxonomy discontinuity, not a real change in pipeline reliability; consumers tracking `needs-human` counts over time should also track `workflow:engine-error` from this commit forward to get a like-for-like comparison. `bin/labels.json` already includes `workflow:engine-error` in this repo; syncing it to consumer/satellite repos is tracked separately (see forge#2346).
|
|
27
|
+
|
|
25
28
|
## Transitions
|
|
26
29
|
|
|
27
30
|
```
|
|
@@ -35,8 +38,13 @@ See also: [FORGE Annotation Protocol §6](forge-protocol-v1.md#6-label-state-mac
|
|
|
35
38
|
→ workflow:invalid [TERMINAL] [Any phase: closed as invalid]
|
|
36
39
|
→ workflow:decomposed [TERMINAL] [Phase 2: split into sub-issues]
|
|
37
40
|
→ needs-human [TERMINAL] [Any phase: pipeline blocked]
|
|
41
|
+
→ workflow:engine-error [TERMINAL] [Any phase: the engine/tool itself broke — CLI crash, missing API key, no SDK — not a content-level judgment call]
|
|
38
42
|
```
|
|
39
43
|
|
|
44
|
+
**`workflow:invalid` after `ready-to-build`/`building`/`in-review`** (#2326): the enforcement hook (`bin/hooks/pre-tool-use.mjs`) allows `workflow:invalid` as a successor of these three states — not only of `workflow:investigating` — because invalidity is sometimes only discovered during architecture planning or build, once the actual code/tests are read (see #2312). This is gated, not unconditional: the hook requires a posted reversal comment already on the issue — a second `FORGE:INVESTIGATOR` annotation carrying `**Verdict**: INVALID` — before it allows the transition through. A bare relabel with no evidence trail is still blocked. `workflow:investigating → workflow:invalid` remains evidence-free (Phase 1D's normal path).
|
|
45
|
+
|
|
46
|
+
The reversal comment must also carry a trusted `authorAssociation` — `OWNER`, `MEMBER`, or `COLLABORATOR` (#2332). Marker+verdict text alone is forgeable by any commenter on a public issue with no repo write access; the hook additionally requires GitHub's own `authorAssociation` classification on that comment (returned by `gh issue view --json comments` at no extra cost) to be one of those three tiers. This check is deliberately identity-agnostic rather than a hardcoded bot-login allowlist, so it survives the pipeline's `gh` identity rotating between accounts (#1722) without needing to be updated.
|
|
47
|
+
|
|
40
48
|
## Terminal Labels
|
|
41
49
|
|
|
42
50
|
Processing stops when any of these labels is set:
|
|
@@ -46,6 +54,7 @@ Processing stops when any of these labels is set:
|
|
|
46
54
|
- `workflow:decomposed`
|
|
47
55
|
- `workflow:awaiting-merge`
|
|
48
56
|
- `needs-human`
|
|
57
|
+
- `workflow:engine-error`
|
|
49
58
|
|
|
50
59
|
## Label Exclusivity
|
|
51
60
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Autonomous dev pipeline for Claude Code. GitHub as institutional memory — every issue, PR, and annotation survives context resets and makes the next agent smarter.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"README.md"
|
|
47
47
|
],
|
|
48
48
|
"scripts": {
|
|
49
|
-
"test": "node --test \"bin/tests/**/*.test.mjs\" \"packages/protocol/test/**/*.test.mjs\"",
|
|
49
|
+
"test": "node --test \"bin/tests/**/*.test.mjs\" \"bin/engine/**/*.test.mjs\" \"packages/protocol/test/**/*.test.mjs\"",
|
|
50
50
|
"docs:dev": "vitepress dev docs/site",
|
|
51
51
|
"docs:build": "vitepress build docs/site",
|
|
52
52
|
"docs:preview": "vitepress preview docs/site"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { RESERVED_TYPES, RESERVED_TYPE_NAMES } from './types.js';
|
|
15
|
+
import { escapeHtmlCommentDelimiters } from './html-comment-escape.js';
|
|
15
16
|
|
|
16
17
|
// A folded field/inline value that is itself (whole, trimmed) shaped like a FORGE
|
|
17
18
|
// opening tag or a generic sentinel line has no legitimate reason to look that way —
|
|
@@ -40,7 +41,9 @@ const NEWLINE_RE = /\r\n|\r|\n/g;
|
|
|
40
41
|
* subsequent lines including the completion sentinel, and treats a literal
|
|
41
42
|
* `-->` as ending the enclosing comment early, leaking the remainder as
|
|
42
43
|
* visible rendered text. Both directions of the delimiter pair must be
|
|
43
|
-
* escaped. (forge#1594, forge#1638)
|
|
44
|
+
* escaped. (forge#1594, forge#1638) Delegated to the shared
|
|
45
|
+
* `escapeHtmlCommentDelimiters()` helper (forge#2225) so this scheme has a
|
|
46
|
+
* single implementation shared with `bin/engine/state.mjs`.
|
|
44
47
|
*
|
|
45
48
|
* As defense in depth, if a value still folds down to a line that is itself a
|
|
46
49
|
* bare FORGE tag/sentinel shape, emit() rejects it outright rather than silently
|
|
@@ -56,27 +59,7 @@ function sanitizeFieldValue(rawValue) {
|
|
|
56
59
|
`emit(): field value resolves to a bare FORGE tag/sentinel line, which is not permitted: ${JSON.stringify(rawValue)}`,
|
|
57
60
|
);
|
|
58
61
|
}
|
|
59
|
-
|
|
60
|
-
// a value that already contains literal entity-like text (e.g. "<!-->"
|
|
61
|
-
// or "<!--") collides with the encoded form of an unrelated real
|
|
62
|
-
// delimiter — sanitizeFieldValue() would not be injective, and the
|
|
63
|
-
// collision is unrecoverable by unescapeFieldValue() on decode
|
|
64
|
-
// (forge#2137). Escaping "&" first guarantees every "<!--"/"-->"/
|
|
65
|
-
// "--!>" sequence in the output was produced by *this* pass, never by
|
|
66
|
-
// pre-existing entity text (which is now doubly-escaped, e.g.
|
|
67
|
-
// "&" -> "&", left untouched by the delimiter passes that only match
|
|
68
|
-
// literal "<!--"/"--"+">" character sequences).
|
|
69
|
-
const withAmpEscaped = folded.replace(/&/g, '&');
|
|
70
|
-
// Escape the HTML comment opener ("<!--") so a value cannot start a new
|
|
71
|
-
// unterminated HTML comment in GitHub's renderer, which would visually swallow
|
|
72
|
-
// subsequent lines — the same rendering-leak the closer escaping below prevents
|
|
73
|
-
// from the opposite direction. (forge#1638)
|
|
74
|
-
const withOpenerEscaped = withAmpEscaped.replace(/<!--/g, '<!--');
|
|
75
|
-
// Escape both HTML comment-close forms ("-->" and "--!>") — only replacing the
|
|
76
|
-
// literal character sequence (not just documenting the risk) prevents the
|
|
77
|
-
// enclosing comment from terminating early. (forge#1594; the FORGE:STATE codec
|
|
78
|
-
// in bin/engine/state.mjs now mirrors this full three-form scheme — forge#2119)
|
|
79
|
-
return withOpenerEscaped.replace(/--(!)?>/g, (_, bang) => `--${bang || ''}>`);
|
|
62
|
+
return escapeHtmlCommentDelimiters(folded);
|
|
80
63
|
}
|
|
81
64
|
|
|
82
65
|
/**
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTML-comment-delimiter escape/unescape pair for the FORGE annotation
|
|
3
|
+
* wire format.
|
|
4
|
+
*
|
|
5
|
+
* This scheme is used by two independent callers that must agree byte-for-byte:
|
|
6
|
+
* - `packages/protocol/src/emit.js` / `parse.js` (per-field-value escaping)
|
|
7
|
+
* - `bin/engine/state.mjs` (whole-JSON-payload escaping for the FORGE:STATE
|
|
8
|
+
* compact-index block)
|
|
9
|
+
*
|
|
10
|
+
* Extracting it here removes the drift vector between those two call sites —
|
|
11
|
+
* they previously hand-duplicated this exact regex chain, kept in sync only by
|
|
12
|
+
* cross-referencing code comments, and had already drifted once (forge#1638's
|
|
13
|
+
* fix was applied to `emit.js` before `state.mjs` caught up in forge#2119).
|
|
14
|
+
*
|
|
15
|
+
* @license MIT
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Escape the three HTML comment delimiter forms so a payload/value can never
|
|
20
|
+
* open or close an HTML comment in GitHub's renderer:
|
|
21
|
+
* - `<!--` the comment opener — an unescaped opener would start a new
|
|
22
|
+
* (nested-looking) comment, visually swallowing subsequent content.
|
|
23
|
+
* - `-->` and `--!>` the two comment-close forms browsers/HTML parsers
|
|
24
|
+
* honour. An unescaped closer could end the enclosing comment early,
|
|
25
|
+
* leaking the remainder as visible rendered text (CodeQL js/bad-tag-filter).
|
|
26
|
+
*
|
|
27
|
+
* The `&` escape MUST run first, before either delimiter-escape pass. Without
|
|
28
|
+
* it, a value that already contains literal entity-like text (e.g. `<!-->`
|
|
29
|
+
* or `<!--`) collides with the encoded form of an unrelated real delimiter —
|
|
30
|
+
* the encoder would not be injective, and the collision is unrecoverable on
|
|
31
|
+
* decode (forge#2137). Escaping `&` first guarantees every `<!--`/`-->`/
|
|
32
|
+
* `--!>` sequence in the output was produced by *this* pass, never by
|
|
33
|
+
* pre-existing entity text (which is now doubly-escaped, e.g.
|
|
34
|
+
* `&` -> `&`, left untouched by the delimiter passes that only match the
|
|
35
|
+
* literal `<!--`/`--`+`>` character sequences).
|
|
36
|
+
*
|
|
37
|
+
* @param {string} value
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function escapeHtmlCommentDelimiters(value) {
|
|
41
|
+
return value
|
|
42
|
+
.replace(/&/g, '&')
|
|
43
|
+
.replace(/<!--/g, '<!--')
|
|
44
|
+
.replace(/--(!)?>/g, (_, bang) => `--${bang || ''}>`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Inverse of {@link escapeHtmlCommentDelimiters}. Restores `-->`, `--!>`,
|
|
49
|
+
* `<!--`, then `&` — in that order.
|
|
50
|
+
*
|
|
51
|
+
* Unescape order matters: the closer forms are restored first (most-specific
|
|
52
|
+
* `--!>` before `-->`) so a dangling `!` is never left behind, then the
|
|
53
|
+
* opener is restored. The `&` -> `&` unescape MUST run LAST (mirroring the
|
|
54
|
+
* encode side running it FIRST) so it never touches the `&` that is part of a
|
|
55
|
+
* just-restored `<!--`/`-->`/`--!>` sequence (forge#2137; forge#1662 — parse()
|
|
56
|
+
* previously failed to apply the exact inverse of emit()'s escape, corrupting
|
|
57
|
+
* round-trips).
|
|
58
|
+
*
|
|
59
|
+
* @param {string} raw
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function unescapeHtmlCommentDelimiters(raw) {
|
|
63
|
+
return raw
|
|
64
|
+
.replace(/--!>/g, '--!>')
|
|
65
|
+
.replace(/-->/g, '-->')
|
|
66
|
+
.replace(/<!--/g, '<!--')
|
|
67
|
+
.replace(/&/g, '&');
|
|
68
|
+
}
|
|
@@ -30,6 +30,7 @@ export { validate } from './validate.js';
|
|
|
30
30
|
export { emit, emitPartial, isKnownType } from './emit.js';
|
|
31
31
|
export { RESERVED_TYPES, RESERVED_TYPE_NAMES, Category, SentinelState } from './types.js';
|
|
32
32
|
export { canonicalJson, toBase64url, encodeCard, decodeCardInlineValue } from './card.js';
|
|
33
|
+
export { PHASE_IDS, PHASE_MARKERS } from './phases.js';
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* @typedef {Object} ParsedAnnotation
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { RESERVED_TYPES, RESERVED_TYPE_NAMES, SentinelState } from './types.js';
|
|
20
|
+
import { unescapeHtmlCommentDelimiters } from './html-comment-escape.js';
|
|
20
21
|
|
|
21
22
|
// Matches: <!-- FORGE:TYPE --> or <!-- FORGE:TYPE: value -->
|
|
22
23
|
// Anchored to require the tag to be the ENTIRE (trimmed) line — a FORGE-tag-shaped
|
|
@@ -119,17 +120,15 @@ function escapeRegExp(str) {
|
|
|
119
120
|
* on decode is what makes the scheme injective: any pre-existing entity-like
|
|
120
121
|
* text in the original value (e.g. `<!--` or `<!-->`) is doubly-escaped
|
|
121
122
|
* at encode time and can never collide with the single-escaped form produced
|
|
122
|
-
* by a real delimiter (forge#2137; forge#1662).
|
|
123
|
+
* by a real delimiter (forge#2137; forge#1662). Delegated to the shared
|
|
124
|
+
* `unescapeHtmlCommentDelimiters()` helper (forge#2225) so this scheme has a
|
|
125
|
+
* single implementation shared with `bin/engine/state.mjs`.
|
|
123
126
|
*
|
|
124
127
|
* @param {string} raw
|
|
125
128
|
* @returns {string}
|
|
126
129
|
*/
|
|
127
130
|
function unescapeFieldValue(raw) {
|
|
128
|
-
return raw
|
|
129
|
-
.replace(/--!>/g, '--!>')
|
|
130
|
-
.replace(/-->/g, '-->')
|
|
131
|
-
.replace(/<!--/g, '<!--')
|
|
132
|
-
.replace(/&/g, '&');
|
|
131
|
+
return unescapeHtmlCommentDelimiters(raw);
|
|
133
132
|
}
|
|
134
133
|
|
|
135
134
|
/**
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical FORGE pipeline phase table — single source of truth for phase ids
|
|
3
|
+
* (in dispatch order) and the exact marker/label strings that indicate each
|
|
4
|
+
* phase has committed.
|
|
5
|
+
*
|
|
6
|
+
* Consumers: `bin/engine/phases.mjs` (the authoritative engine gate) and
|
|
7
|
+
* `bin/hooks/interactive-engine.mjs` (the interactive-session SubagentStop
|
|
8
|
+
* hook) both import this module instead of hand-declaring their own copies
|
|
9
|
+
* of the marker strings. Before this module existed, the two files carried
|
|
10
|
+
* independent literal copies of the same strings and drifted out of sync at
|
|
11
|
+
* least twice (forge#1669 established the strict `:COMPLETE` gate for
|
|
12
|
+
* `context`/`architect`; forge#2375/PR#2395 then had to hand-patch the hook's
|
|
13
|
+
* copy back into agreement with the engine's). See forge#2378.
|
|
14
|
+
*
|
|
15
|
+
* `completionMarker` values are sourced from `RESERVED_TYPES[*].completionSentinel`
|
|
16
|
+
* wherever that reserved annotation type exists (INVESTIGATOR, CONTEXT,
|
|
17
|
+
* ARCHITECT, BUILDER) — so the reserved-annotation registry in `types.js` and
|
|
18
|
+
* this phase registry cannot drift apart for those four phases; there is
|
|
19
|
+
* exactly one literal declaration of each of those marker strings in the
|
|
20
|
+
* entire codebase, in `types.js`.
|
|
21
|
+
*
|
|
22
|
+
* `review` and `close` have no corresponding `RESERVED_TYPES` entry with a
|
|
23
|
+
* matching sentinel today (`REVIEWER` has no `completionSentinel` defined in
|
|
24
|
+
* spec §4.1; `close` is gated on a GitHub *label*, not a comment annotation),
|
|
25
|
+
* so those two are declared directly in this file — still single-sourced
|
|
26
|
+
* across the two consumers, just not additionally derived from `types.js`.
|
|
27
|
+
*
|
|
28
|
+
* @license MIT
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { RESERVED_TYPES } from './types.js';
|
|
32
|
+
|
|
33
|
+
/** Phase ids, in canonical dispatch order (mirrors `commands/work-on.md`'s
|
|
34
|
+
* "Universal Phase Dispatcher" table and `bin/engine/phases.mjs`'s `PHASES`
|
|
35
|
+
* array).
|
|
36
|
+
*
|
|
37
|
+
* `decompose` and `remediate` (forge#2379) are branch phases, not part of the
|
|
38
|
+
* six-phase linear happy path — `decompose` is reached only when `investigate`
|
|
39
|
+
* signals `DECOMPOSE:YES`, and `remediate` only when `review` escalates to
|
|
40
|
+
* `needs-human`. They are still declared here, in the exact same array
|
|
41
|
+
* position `bin/engine/phases.mjs`'s `PHASES` array declares them, because
|
|
42
|
+
* `scripts/check-phase-registry-drift.mjs` requires the two lists to match
|
|
43
|
+
* index-for-index — see that file's own doc comment. */
|
|
44
|
+
export const PHASE_IDS = ['investigate', 'decompose', 'context', 'architect', 'build', 'review', 'remediate', 'close'];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} PhaseMarkerEntry
|
|
48
|
+
* @property {string} [completionMarker] - marker/sentinel string (or label, for `close`)
|
|
49
|
+
* whose presence means this phase has committed.
|
|
50
|
+
* @property {string} [invalidMarker] - (investigate only) marks the issue INVALID.
|
|
51
|
+
* @property {string} [decomposedMarker] - (investigate only) marks the issue DECOMPOSED.
|
|
52
|
+
* @property {string} [partialMarker] - sentinel for an interrupted/partial annotation.
|
|
53
|
+
* @property {string} [presenceMarker] - bare annotation-opener substring, used only by
|
|
54
|
+
* phases whose completion is non-critical (see `context` below) — presence alone
|
|
55
|
+
* (without `:COMPLETE`) is enough to report a soft/visible skip rather than a hard fail.
|
|
56
|
+
* @property {string} [completionLabel] - a GitHub issue *label* (not a comment marker)
|
|
57
|
+
* whose presence means this phase has committed. Only `close` uses this form.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/** @type {Object.<string, PhaseMarkerEntry>} */
|
|
61
|
+
export const PHASE_MARKERS = {
|
|
62
|
+
investigate: {
|
|
63
|
+
completionMarker: RESERVED_TYPES.INVESTIGATOR.completionSentinel, // 'INVESTIGATION:COMPLETE'
|
|
64
|
+
invalidMarker: 'INVESTIGATION:INVALID',
|
|
65
|
+
decomposedMarker: 'DECOMPOSE:YES',
|
|
66
|
+
},
|
|
67
|
+
// forge#2379: `decompose` actually runs work-on/decompose (sub-issue
|
|
68
|
+
// fan-out) once `investigate` hands off on `DECOMPOSE:YES` — see
|
|
69
|
+
// bin/engine/phases.mjs's `decompose` phase entry for the handoff mechanics.
|
|
70
|
+
decompose: {
|
|
71
|
+
completionMarker: RESERVED_TYPES.DECOMPOSED.completionSentinel, // 'FORGE:DECOMPOSED:COMPLETE'
|
|
72
|
+
},
|
|
73
|
+
context: {
|
|
74
|
+
completionMarker: RESERVED_TYPES.CONTEXT.completionSentinel, // 'FORGE:CONTEXT:COMPLETE'
|
|
75
|
+
partialMarker: RESERVED_TYPES.CONTEXT.partialSentinel, // 'FORGE:CONTEXT:PARTIAL'
|
|
76
|
+
// Bare presence marker: context is documented as non-critical (spec §7) — a
|
|
77
|
+
// missing completion marker is a visible skip, not a hard fail. Only
|
|
78
|
+
// bin/engine/phases.mjs's `context.detectOutcome` uses this bare form;
|
|
79
|
+
// `context.reconcile` (and everything in interactive-engine.mjs) uses the
|
|
80
|
+
// strict `completionMarker` above. Do not collapse these two fields.
|
|
81
|
+
presenceMarker: `FORGE:${RESERVED_TYPES.CONTEXT.type}`, // 'FORGE:CONTEXT'
|
|
82
|
+
},
|
|
83
|
+
architect: {
|
|
84
|
+
completionMarker: RESERVED_TYPES.ARCHITECT.completionSentinel, // 'FORGE:ARCHITECT:COMPLETE'
|
|
85
|
+
partialMarker: RESERVED_TYPES.ARCHITECT.partialSentinel, // 'FORGE:ARCHITECT:PARTIAL'
|
|
86
|
+
},
|
|
87
|
+
build: {
|
|
88
|
+
completionMarker: RESERVED_TYPES.BUILDER.completionSentinel, // 'FORGE:BUILDER:COMPLETE'
|
|
89
|
+
},
|
|
90
|
+
review: {
|
|
91
|
+
// No RESERVED_TYPES entry defines this sentinel (REVIEWER has no
|
|
92
|
+
// completionSentinel in spec §4.1) — declared directly here.
|
|
93
|
+
completionMarker: 'FORGE:REVIEWER:MERGED',
|
|
94
|
+
},
|
|
95
|
+
// forge#2379: `remediate` re-drives a needs-human PR (commands/work-on/remediate.md).
|
|
96
|
+
// Posted to BOTH the PR and the linked issue (Phase M8) — bin/engine/phases.mjs's
|
|
97
|
+
// `remediate` phase reads it off the issue, consistent with every other phase here.
|
|
98
|
+
remediate: {
|
|
99
|
+
completionMarker: RESERVED_TYPES.REMEDIATION.completionSentinel, // 'FORGE:REMEDIATION:COMPLETE'
|
|
100
|
+
},
|
|
101
|
+
close: {
|
|
102
|
+
// A GitHub label, not a comment marker — see `completionLabel` above.
|
|
103
|
+
completionLabel: 'workflow:merged',
|
|
104
|
+
},
|
|
105
|
+
};
|
|
@@ -86,6 +86,23 @@ export const RESERVED_TYPES = {
|
|
|
86
86
|
requiredFields: ['Verdict'],
|
|
87
87
|
verdictValues: ['APPROVED', 'CHANGES_REQUESTED', 'COMMENTED'],
|
|
88
88
|
},
|
|
89
|
+
/**
|
|
90
|
+
* REMEDIATION — paper trail for a needs-human PR re-driven by
|
|
91
|
+
* commands/work-on/remediate.md. Posted to BOTH the PR and the linked
|
|
92
|
+
* issue (Phase M8) once the re-gate outcome (AUTO-LANDED /
|
|
93
|
+
* HELD-AWAITING-MERGE / RE-ESCALATED / UNFIXABLE) is known. Added in
|
|
94
|
+
* forge#2379 to single-source the completion sentinel that
|
|
95
|
+
* bin/engine/phases.mjs's `remediate` phase gates on. <!-- Added: forge#2379 -->
|
|
96
|
+
*/
|
|
97
|
+
REMEDIATION: {
|
|
98
|
+
type: 'REMEDIATION',
|
|
99
|
+
category: Category.LIFECYCLE,
|
|
100
|
+
completionSentinel: 'FORGE:REMEDIATION:COMPLETE',
|
|
101
|
+
partialSentinel: null,
|
|
102
|
+
inlineValue: false,
|
|
103
|
+
requiredFields: [],
|
|
104
|
+
reGateOutcomeValues: ['AUTO-LANDED', 'HELD-AWAITING-MERGE', 'RE-ESCALATED', 'UNFIXABLE'],
|
|
105
|
+
},
|
|
89
106
|
TRAJECTORY: {
|
|
90
107
|
type: 'TRAJECTORY',
|
|
91
108
|
category: Category.LIFECYCLE,
|