forgedock 1.5.0 → 1.6.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.
@@ -29,7 +29,7 @@ COMPLEXITY_BAND=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
29
29
  COMPLEXITY_BAND="${COMPLEXITY_BAND:-STANDARD}"
30
30
  ```
31
31
 
32
- **If COMPLEXITY_BAND: TRIVIAL** → skip all phases (A0 through A5), post NO comment, return empty plan to caller immediately. Do not read any files. This is not an error — trivial single-file changes have no cross-path consistency risk. <!-- Added: forge#679 -->
32
+ **If COMPLEXITY_BAND: TRIVIAL** → skip all phases (A0 through A5), **post the minimal skip marker comment described in "Skip Marker" under Skip Conditions below** (reason: `TRIVIAL complexity band`), then return an empty plan to caller immediately. Do not read any files. This is not an error — trivial single-file changes have no cross-path consistency risk. The skip marker (ending in `<!-- FORGE:ARCHITECT:COMPLETE -->`) is what makes a legitimate skip observable to the headless engine's marker-only gate, so the issue advances to build instead of being stranded at `needs-human`. <!-- Added: forge#679; skip-marker on skip: forge#2689 -->
33
33
 
34
34
  **If COMPLEXITY_BAND: STANDARD or COMPLEX** → proceed to Phase A0 below.
35
35
 
@@ -668,14 +668,39 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ARCHITECT -->
668
668
 
669
669
  ## Skip Conditions
670
670
 
671
- Skip this entire step (post nothing, return empty plan to caller) if:
671
+ Skip the planning work (Phases A0–A5) and return an empty plan to caller — **but always post the Skip Marker below first** — if:
672
672
  - **COMPLEXITY_BAND: TRIVIAL** — checked via FORGE:FAST_PATH comment at entry (see guard above) <!-- Primary skip path: forge#679 -->
673
673
  - Issue creates only **new files** with no callers to find (e.g. a new command file with no existing integration point yet)
674
674
  - Issue is a 1-file config or docs edit with no code logic
675
675
  - Issue title starts with "docs:" or "chore:"
676
676
  - `{AFFECTED_FILES}` is empty
677
677
 
678
- When skipped, the builder proceeds with investigation report + context briefing only.
678
+ When skipped, the builder proceeds with investigation report + context briefing only (see Integration Point below — the builder falls back gracefully on an empty plan).
679
+
680
+ ### Skip Marker (MANDATORY on every skip — do NOT post nothing) <!-- forge#2689 -->
681
+
682
+ **Do not post nothing.** The headless engine gate (`bin/engine/phases.mjs` → `architect.detectOutcome`) treats a **missing** `FORGE:ARCHITECT:COMPLETE` sentinel as a phase **failure**, retries to `maxAttempts` (3), then strands the issue at `needs-human`. It reads GitHub comment markers only — it cannot see this subcommand's in-process "empty plan" return value. So a skip that posts no marker is indistinguishable from a crashed architect run and deterministically strands **every** `chore:`/`docs:`/trivial issue.
683
+
684
+ On **every** skip branch above, post exactly this minimal comment (substitute the concrete `{SKIP_REASON}`, e.g. `chore:/docs: title`, `TRIVIAL complexity band`, `new files only — no callers`, or `AFFECTED_FILES empty`) **before** returning the empty plan:
685
+
686
+ ```bash
687
+ # The skip marker is mandatory and un-gated by design — posting it IS the point
688
+ # (a missing marker strands the issue at needs-human; see above). Same report-only
689
+ # risk class as the unguarded plan post in "Output Format" above, so it carries the
690
+ # same allowlist rather than a DRY_RUN guard.
691
+ SKIP_BODY=$(cat <<SKIP_EOF
692
+ <!-- FORGE:ARCHITECT -->
693
+ ## Architecture Plan — Skipped
694
+
695
+ **Skipped**: {SKIP_REASON}. No cross-path consistency risk — the builder proceeds with the investigation report + context briefing.
696
+
697
+ <!-- FORGE:ARCHITECT:COMPLETE -->
698
+ SKIP_EOF
699
+ )
700
+ gh issue comment {NUMBER} {GH_FLAG} --body "$SKIP_BODY" # <!-- allowlist:check-command-side-effects -->
701
+ ```
702
+
703
+ The empty-plan-with-marker changes no downstream behavior — the builder already falls back to the investigation report + contract when the plan is empty (see Integration Point below). The marker only makes the legitimate skip **observable** to the marker-only engine gate. This intentionally does **not** weaken crash detection: a genuinely interrupted architect run still posts no marker at all, so `detectOutcome` still fails-and-escalates it correctly.
679
704
 
680
705
  ---
681
706
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.5.0",
3
+ "version": "1.6.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"
@@ -48,7 +48,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, rea
48
48
  import { execSync, spawnSync } from 'child_process';
49
49
  import { homedir } from 'os';
50
50
  import { dirname, join, resolve } from 'path';
51
- import { fileURLToPath } from 'url';
51
+ import { fileURLToPath, pathToFileURL } from 'url';
52
52
 
53
53
  const __filename = fileURLToPath(import.meta.url);
54
54
  const __dirname = dirname(__filename);
@@ -309,7 +309,11 @@ async function getParser() {
309
309
  const parsePath = join(repoRoot, 'packages', 'protocol', 'src', 'parse.js');
310
310
 
311
311
  try {
312
- const mod = await import(parsePath);
312
+ // On win32 a bare absolute path (C:\...) makes Node parse the drive letter as a
313
+ // URL scheme (ERR_UNSUPPORTED_ESM_URL_SCHEME). Convert to a file:// URL first —
314
+ // valid on POSIX too, and matches the import(pathToFileURL(...).href) idiom used
315
+ // at check-admission-parity.mjs / validate-annotation-node.mjs / check-phase-registry-drift.mjs.
316
+ const mod = await import(pathToFileURL(parsePath).href);
313
317
  _parseAnnotation = mod.parse;
314
318
  return _parseAnnotation;
315
319
  } catch (e) {
@@ -102,6 +102,7 @@ MARKER_REGISTRY="
102
102
  DESIGN
103
103
  DISPATCHER
104
104
  DOSSIER_UPDATED
105
+ ENGINE_FALLBACK
105
106
  FAST_PATH
106
107
  FINDING_SOURCE
107
108
  FIX_CI
@@ -126,11 +126,15 @@ fi
126
126
  # Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
127
127
  # grep build on Windows, so use a bash regex + BASH_REMATCH instead of a
128
128
  # lookbehind (same convention as scripts/code-index.sh).
129
+ # The leading (^|[^a-z]) group anchors the alternation to a word start so a
130
+ # close-verb embedded in a larger word (e.g. "encloses #1234") cannot match;
131
+ # the body is already lowercased, so [^a-z] is the correct complement class.
132
+ # The extra group shifts the issue-number capture to BASH_REMATCH[3].
129
133
  PR_BODY=$(echo "$PR_JSON" | jq -r '.body // empty')
130
134
  SOURCE_ISSUE=""
131
135
  PR_BODY_LOWER=$(echo "$PR_BODY" | tr '[:upper:]' '[:lower:]')
132
- if [[ "$PR_BODY_LOWER" =~ (closes|fixes|resolves)[[:space:]]*#([0-9]+) ]]; then
133
- SOURCE_ISSUE="${BASH_REMATCH[2]}"
136
+ if [[ "$PR_BODY_LOWER" =~ (^|[^a-z])(closes|fixes|resolves)[[:space:]]*#([0-9]+) ]]; then
137
+ SOURCE_ISSUE="${BASH_REMATCH[3]}"
134
138
  fi
135
139
 
136
140
  if [ -n "$SOURCE_ISSUE" ]; then
@@ -167,12 +171,18 @@ if [ -n "$MILESTONE_BRANCH" ]; then
167
171
  ALL_MILESTONES=$(gh api "repos/${MILESTONES_REPO}/milestones" 2>/dev/null || echo "")
168
172
 
169
173
  if [ -n "$ALL_MILESTONES" ] && [ "$ALL_MILESTONES" != "[]" ]; then
170
- MATCH=$(echo "$ALL_MILESTONES" | jq -r --arg slug "$BRANCH_SLUG" \
171
- '.[] | select((.title | ascii_downcase | gsub("[^a-z0-9]+"; "-")) == $slug) | .title' | head -1)
172
- if [ -z "$MATCH" ]; then
173
- MATCH=$(echo "$ALL_MILESTONES" | jq -r --arg slug "$BRANCH_SLUG" \
174
- '.[] | select((.title | ascii_downcase | gsub("[^a-z0-9]+"; "-")) | test($slug)) | .title' | head -1)
175
- fi
174
+ # Single jq invocation: shared `def slugify` replaces the duplicated
175
+ # downcase/gsub slug transform that previously appeared once per tier
176
+ # (exact-match, then substring-fallback). Exact match is tried first
177
+ # (array 1), substring match second (array 2); `first` picks the winner
178
+ # from the concatenation, preserving the exact-match-wins-over-substring
179
+ # ordering and the regex semantics of `test($slug)` exactly as before.
180
+ # <!-- Fixes: forge#2670 -->
181
+ MATCH=$(echo "$ALL_MILESTONES" | jq -r --arg slug "$BRANCH_SLUG" '
182
+ def slugify: ascii_downcase | gsub("[^a-z0-9]+"; "-");
183
+ ([.[] | select((.title | slugify) == $slug)]
184
+ + [.[] | select((.title | slugify) | test($slug))])
185
+ | (first.title // empty)')
176
186
  if [ -n "$MATCH" ]; then
177
187
  echo "$MATCH"
178
188
  exit 0