forgedock 1.3.0 → 1.4.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.
Files changed (45) hide show
  1. package/bin/batch-runner.mjs +31 -2
  2. package/bin/engine/admission.mjs +294 -0
  3. package/bin/engine/admission.test.mjs +265 -0
  4. package/bin/engine/invariants.mjs +17 -4
  5. package/bin/engine/phases.mjs +201 -35
  6. package/bin/engine/resolve.mjs +256 -0
  7. package/bin/engine/resolve.test.mjs +149 -0
  8. package/bin/engine/state.mjs +20 -28
  9. package/bin/engine-cli.mjs +80 -5
  10. package/bin/engine.mjs +390 -18
  11. package/bin/forgedock.mjs +336 -59
  12. package/bin/hooks/interactive-engine.mjs +50 -24
  13. package/bin/hooks/pre-tool-use.mjs +169 -13
  14. package/bin/journey.mjs +8 -2
  15. package/bin/labels.json +5 -0
  16. package/bin/runner.mjs +321 -10
  17. package/bin/watch-utils.mjs +255 -0
  18. package/commands/cleanup.md +7 -3
  19. package/commands/orchestrate/config.md +41 -1
  20. package/commands/orchestrate/phase-1-resolve.md +234 -8
  21. package/commands/orchestrate/phase-3-dependency.md +27 -18
  22. package/commands/orchestrate/phase-4-execution.md +221 -22
  23. package/commands/quality-gate.md +6 -6
  24. package/commands/review-pr.md +23 -7
  25. package/commands/upgrade-deps.md +7 -2
  26. package/commands/work-on/decompose.md +2 -0
  27. package/commands/work-on/investigate.md +22 -4
  28. package/commands/work-on/remediate.md +2 -0
  29. package/commands/work-on/review.md +40 -1
  30. package/commands/work-on.md +19 -4
  31. package/docs/CONFIG.md +120 -0
  32. package/docs/spec/eval-run-result.md +8 -4
  33. package/docs/spec/label-state-machine.md +9 -0
  34. package/package.json +2 -2
  35. package/packages/protocol/src/emit.js +5 -22
  36. package/packages/protocol/src/html-comment-escape.js +68 -0
  37. package/packages/protocol/src/index.js +1 -0
  38. package/packages/protocol/src/parse.js +5 -6
  39. package/packages/protocol/src/phases.js +105 -0
  40. package/packages/protocol/src/types.js +17 -0
  41. package/scripts/check-admission-parity.mjs +167 -0
  42. package/scripts/check-command-side-effects.sh +181 -66
  43. package/scripts/check-phase-registry-drift.mjs +96 -0
  44. package/scripts/check-spec-markers.sh +1 -0
  45. package/scripts/forge-run.sh +35 -4
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CI drift check: bin/engine/phases.mjs's `PHASES` array (the authoritative
4
+ * engine phase table) must have exactly the same phase-id list, in the same
5
+ * order, as packages/protocol/src/phases.js's `PHASE_IDS` — the single-source
6
+ * registry both bin/engine/phases.mjs and bin/hooks/interactive-engine.mjs
7
+ * import their marker strings from (forge#2378).
8
+ *
9
+ * This does NOT re-validate the marker *strings* themselves — both consumer
10
+ * files import the registry directly, so a string-level mismatch between them
11
+ * is now structurally impossible (there is exactly one literal declaration of
12
+ * each marker, in packages/protocol/src/phases.js / types.js). What CAN still
13
+ * drift is the *phase-id list itself*: someone adds/removes/reorders a phase
14
+ * in bin/engine/phases.mjs's PHASES array without updating the registry's
15
+ * PHASE_IDS (or vice versa). This script catches that.
16
+ *
17
+ * Usage: node scripts/check-phase-registry-drift.mjs
18
+ * Exit codes: 0 = no drift, 1 = drift detected, 2 = usage/import error.
19
+ */
20
+
21
+ import { fileURLToPath, pathToFileURL } from "node:url";
22
+ import path from "node:path";
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+ const REPO_ROOT = path.resolve(__dirname, "..");
26
+
27
+ async function main() {
28
+ let registryModule;
29
+ let phasesModule;
30
+ try {
31
+ registryModule = await import(
32
+ pathToFileURL(path.join(REPO_ROOT, "packages/protocol/src/phases.js")).href
33
+ );
34
+ } catch (err) {
35
+ console.error(`ERROR: failed to import packages/protocol/src/phases.js: ${err.message}`);
36
+ process.exit(2);
37
+ }
38
+ try {
39
+ phasesModule = await import(
40
+ pathToFileURL(path.join(REPO_ROOT, "bin/engine/phases.mjs")).href
41
+ );
42
+ } catch (err) {
43
+ console.error(`ERROR: failed to import bin/engine/phases.mjs: ${err.message}`);
44
+ process.exit(2);
45
+ }
46
+
47
+ const registryIds = registryModule.PHASE_IDS;
48
+ const enginePhaseIds = (phasesModule.PHASES || []).map((p) => p.id);
49
+
50
+ if (!Array.isArray(registryIds)) {
51
+ console.error("ERROR: packages/protocol/src/phases.js did not export a PHASE_IDS array.");
52
+ process.exit(2);
53
+ }
54
+ if (!Array.isArray(enginePhaseIds) || enginePhaseIds.length === 0) {
55
+ console.error("ERROR: bin/engine/phases.mjs did not export a non-empty PHASES array.");
56
+ process.exit(2);
57
+ }
58
+
59
+ const drifted =
60
+ registryIds.length !== enginePhaseIds.length ||
61
+ registryIds.some((id, i) => id !== enginePhaseIds[i]);
62
+
63
+ if (drifted) {
64
+ console.error("ERROR: phase-id list drift detected between the registry and the engine.");
65
+ console.error(` packages/protocol/src/phases.js PHASE_IDS: ${JSON.stringify(registryIds)}`);
66
+ console.error(` bin/engine/phases.mjs PHASES ids: ${JSON.stringify(enginePhaseIds)}`);
67
+ console.error(" Update packages/protocol/src/phases.js's PHASE_IDS to match bin/engine/phases.mjs's");
68
+ console.error(" PHASES array (or vice versa) so the two stay single-sourced. See forge#2378.");
69
+ process.exit(1);
70
+ }
71
+
72
+ // Also verify every registry marker entry that has a `completionMarker` or
73
+ // `completionLabel` field is a non-empty string — catches an accidental
74
+ // undefined/null slipping in (e.g. a typo'd RESERVED_TYPES key whose
75
+ // completionSentinel is null) which would otherwise fail silently at
76
+ // has(blob, undefined) → substring "undefined" never matches, masking a
77
+ // real bug as "phase never completes".
78
+ const emptyMarkers = [];
79
+ for (const [phaseId, entry] of Object.entries(registryModule.PHASE_MARKERS || {})) {
80
+ const marker = entry.completionMarker ?? entry.completionLabel;
81
+ if (typeof marker !== "string" || marker.length === 0) {
82
+ emptyMarkers.push(phaseId);
83
+ }
84
+ }
85
+ if (emptyMarkers.length > 0) {
86
+ console.error(
87
+ `ERROR: PHASE_MARKERS entries with no usable completionMarker/completionLabel: ${emptyMarkers.join(", ")}`
88
+ );
89
+ process.exit(1);
90
+ }
91
+
92
+ console.log("OK: phase-id list and marker registry are consistent (no drift detected).");
93
+ process.exit(0);
94
+ }
95
+
96
+ main();
@@ -135,6 +135,7 @@ MARKER_REGISTRY="
135
135
  PUSH_BLOCKED_EMPTY_BRANCH
136
136
  PUSH_FAILED
137
137
  REMEDIATION
138
+ REREVIEW_SKIPPED
138
139
  REVIEW
139
140
  REVIEWER
140
141
  REVIEW_ROUTE
@@ -72,17 +72,48 @@ die() {
72
72
  local message="$2"
73
73
  local ts_val
74
74
  ts_val=$(ts)
75
- # Escape double quotes in message for JSON embedding
76
- message="${message//\"/\\\"}"
75
+ # Route through json_str for full JSON-string escaping (backslash, quote, and
76
+ # control characters) — die() is fed raw multi-line `gh` CLI stderr, which may
77
+ # contain backslashes and newlines that a quote-only escape would not handle.
78
+ message=$(json_str "$message")
77
79
  emit "{\"event\":\"error\",\"code\":\"${code}\",\"message\":\"${message}\",\"ts\":\"${ts_val}\"}"
78
80
  exit 1
79
81
  }
80
82
 
81
- # JSON-escape a string (double-quote characters only — sufficient for ASCII label/branch values)
83
+ # JSON-escape a string: backslash, double-quote, and the full C0 control range
84
+ # (U+0000-U+001F), per RFC 8259 section 7. Order matters:
85
+ # 1. Backslash escaping runs first so a literal backslash becomes \\ before
86
+ # any other pass runs, avoiding double-escaping ambiguity.
87
+ # 2. Double-quote next.
88
+ # 3. The three named control escapes (\n, \r, \t) next, so they get their
89
+ # short two-character form rather than falling through to \u00XX.
90
+ # 4. Every remaining C0 control byte (U+0000-U+001F, excluding \n/\r/\t
91
+ # already handled above) is escaped as \u00XX — bash's ${s//pat/rep}
92
+ # cannot loop over an arbitrary byte range in one substitution, so this
93
+ # is a bounded loop over hex 00-1F.
82
94
  json_str() {
83
95
  local s="$1"
84
96
  s="${s//\\/\\\\}"
85
97
  s="${s//\"/\\\"}"
98
+ s="${s//$'\n'/\\n}"
99
+ s="${s//$'\r'/\\r}"
100
+ s="${s//$'\t'/\\t}"
101
+ local i
102
+ for ((i = 0; i <= 31; i++)); do
103
+ # \n (10=0x0a), \r (13=0x0d), \t (9=0x09) already escaped above — skip them
104
+ # here so they keep their short named form instead of becoming \u00XX.
105
+ if [ "$i" -eq 9 ] || [ "$i" -eq 10 ] || [ "$i" -eq 13 ]; then
106
+ continue
107
+ fi
108
+ local hex
109
+ hex=$(printf '%02x' "$i")
110
+ local ctrl_char
111
+ printf -v ctrl_char '\\x%02x' "$i"
112
+ ctrl_char=$(printf "$ctrl_char")
113
+ if [[ "$s" == *"$ctrl_char"* ]]; then
114
+ s="${s//$ctrl_char/\\u00$hex}"
115
+ fi
116
+ done
86
117
  printf '%s' "$s"
87
118
  }
88
119
 
@@ -315,7 +346,7 @@ emit "{\"event\":\"phase_detail\",\"phase\":\"$(json_str "${PHASE}")\",\"lane\":
315
346
  # Event 3: action_required (only for non-terminal active phases)
316
347
  if [ -n "$ACTION" ]; then
317
348
  ts_val=$(ts)
318
- ACTION_ESCAPED="${ACTION//\"/\\\"}"
349
+ ACTION_ESCAPED=$(json_str "${ACTION}")
319
350
  emit "{\"event\":\"action_required\",\"phase\":\"$(json_str "${PHASE}")\",\"action\":\"${ACTION_ESCAPED}\",\"ts\":\"${ts_val}\"}"
320
351
  fi
321
352