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.
- 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/state.mjs +20 -28
- package/bin/engine-cli.mjs +80 -5
- package/bin/engine.mjs +390 -18
- package/bin/forgedock.mjs +336 -59
- package/bin/hooks/interactive-engine.mjs +50 -24
- package/bin/hooks/pre-tool-use.mjs +169 -13
- package/bin/journey.mjs +8 -2
- package/bin/labels.json +5 -0
- package/bin/runner.mjs +321 -10
- package/bin/watch-utils.mjs +255 -0
- package/commands/cleanup.md +7 -3
- package/commands/orchestrate/config.md +41 -1
- package/commands/orchestrate/phase-1-resolve.md +234 -8
- package/commands/orchestrate/phase-3-dependency.md +27 -18
- package/commands/orchestrate/phase-4-execution.md +221 -22
- package/commands/quality-gate.md +6 -6
- package/commands/review-pr.md +23 -7
- 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 +2 -0
- 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/scripts/check-admission-parity.mjs +167 -0
- package/scripts/check-command-side-effects.sh +181 -66
- package/scripts/check-phase-registry-drift.mjs +96 -0
- package/scripts/check-spec-markers.sh +1 -0
- 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();
|
package/scripts/forge-run.sh
CHANGED
|
@@ -72,17 +72,48 @@ die() {
|
|
|
72
72
|
local message="$2"
|
|
73
73
|
local ts_val
|
|
74
74
|
ts_val=$(ts)
|
|
75
|
-
#
|
|
76
|
-
|
|
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
|
|
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
|
|
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
|
|