forgedock 1.0.22 → 1.1.1

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 (46) hide show
  1. package/README.md +217 -218
  2. package/bin/cinema.mjs +320 -0
  3. package/bin/engine/phases.mjs +148 -0
  4. package/bin/engine/projector.mjs +32 -0
  5. package/bin/engine/reconcile.mjs +11 -0
  6. package/bin/engine/runlog.mjs +79 -0
  7. package/bin/engine/state.mjs +27 -0
  8. package/bin/engine-cli.mjs +50 -0
  9. package/bin/engine.mjs +138 -0
  10. package/bin/forgedock.mjs +464 -1491
  11. package/bin/journey.mjs +783 -0
  12. package/bin/runner.mjs +80 -13
  13. package/bin/settings-hook.mjs +112 -0
  14. package/bin/tui.mjs +6 -3
  15. package/commands/autopilot.md +4 -4
  16. package/commands/deploy-info.md +5 -3
  17. package/commands/design-bench.md +1 -0
  18. package/commands/design-render-critique-loop.md +1 -0
  19. package/commands/design.md +1 -0
  20. package/commands/forge-stats.md +1 -0
  21. package/commands/issue.md +1 -1
  22. package/commands/orchestrate.md +27 -9
  23. package/commands/quality-gate.md +80 -34
  24. package/commands/review-pr-agents.md +7 -7
  25. package/commands/review-pr.md +21 -4
  26. package/commands/work-on/build/implement.md +16 -4
  27. package/commands/work-on/build/validate.md +38 -1
  28. package/commands/work-on/build.md +27 -9
  29. package/commands/work-on/close.md +2 -2
  30. package/commands/work-on/review.md +24 -0
  31. package/commands/work-on-monolithic.md +1 -0
  32. package/commands/work-on.md +45 -3
  33. package/docs/BRAND.md +6 -2
  34. package/docs/CONFIG.md +39 -34
  35. package/docs/site/.vitepress/config.mjs +85 -0
  36. package/docs/site/getting-started.md +15 -8
  37. package/docs/site/index.md +1 -1
  38. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  39. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  40. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  41. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  42. package/package.json +10 -4
  43. package/scripts/verify-env-vars.sh +18 -2
  44. package/scripts/verify-host-headers.sh +4 -4
  45. package/scripts/verify-route-registration.sh +2 -2
  46. package/scripts/worktree-lifecycle.sh +202 -0
package/bin/engine.mjs ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Durable per-phase engine loop. Drives one pipeline phase at a time via an
3
+ * injected runner (runCommand-shaped), determining each phase's outcome from
4
+ * GitHub state (phase.detectOutcome). All effects are injected → fully testable.
5
+ */
6
+ import { fileURLToPath } from "node:url";
7
+ import { appendEvent, readLog, deriveState, rewriteLog } from "./engine/runlog.mjs";
8
+ import { pickPhase, TERMINAL_REASONS } from "./engine/phases.mjs";
9
+ import { reconcileState } from "./engine/reconcile.mjs";
10
+ import { makeProjector } from "./engine/projector.mjs";
11
+
12
+ const DEFAULT_MAX_ATTEMPTS = 3;
13
+
14
+ export async function runIssue(opts) {
15
+ const { issue, dir, agentId, lane = "staging", io, runner,
16
+ now = () => Date.now(), maxAttempts = DEFAULT_MAX_ATTEMPTS,
17
+ commandsDir = fileURLToPath(new URL("../commands", import.meta.url)) } = opts;
18
+ const projector = makeProjector(io);
19
+
20
+ // 1. Load + reconcile (GitHub wins).
21
+ const local = readLog(dir, issue).length ? deriveState(readLog(dir, issue)) : null;
22
+ const remote = await projector.readState(issue);
23
+ let { state, action } = reconcileState(local, remote);
24
+
25
+ // I3 (best-effort): GitHub issue-edit is not an atomic CAS, so this is a
26
+ // courtesy check, not a hard mutual-exclusion guarantee — a concurrent
27
+ // start can still race between this read and the next lease write.
28
+ // MUST come before any projector.writeState() — remirror/hydrate branches
29
+ // must not overwrite GitHub state when another agent holds a valid lease.
30
+ if (remote?.lease && remote.lease.until > now() && remote.lease.by !== agentId) {
31
+ return { terminalReason: "deferred", detail: `issue ${issue} leased by ${remote.lease.by}` };
32
+ }
33
+
34
+ if (!state) {
35
+ state = freshState(issue, lane);
36
+ appendEvent(dir, issue, { event: "RUN_START", issue, run: state.run, lane });
37
+ await projector.writeState(issue, state);
38
+ } else if (action === "remirror") {
39
+ await projector.writeState(issue, state);
40
+ } else if (action === "hydrate") {
41
+ // GitHub is ahead of (or the only source of) local state. Rebuild the
42
+ // local run-log to match the remote compact index so downstream
43
+ // `deriveState(readLog(...))` calls stay consistent — otherwise the
44
+ // post-commit re-derive below would fold over an empty/stale local log
45
+ // and regress committed/issue/v (C2).
46
+ rewriteLog(dir, issue, eventsFromIndex(state));
47
+ state = deriveState(readLog(dir, issue));
48
+ await projector.writeState(issue, state);
49
+ }
50
+
51
+ // 2. Drive phases until terminal.
52
+ let phase;
53
+ while ((phase = pickPhase(state))) {
54
+ // Every issue's build works on a deterministic branch; set it before build runs.
55
+ if (phase.id === "build" && !state.branch) state.branch = `fix/pipeline-${issue}`;
56
+
57
+ let reconciled;
58
+ try {
59
+ reconciled = phase.reconcile ? await phase.reconcile(state, io) : { satisfied: false };
60
+ } catch {
61
+ // A reconcile probe (gh/git) failing must not crash the run — degrade
62
+ // to "not satisfied" so the phase runs normally instead (C1).
63
+ reconciled = { satisfied: false };
64
+ }
65
+ let outcome;
66
+ if (reconciled.satisfied) {
67
+ outcome = { status: "committed", outputs: reconciled.outputs || {} };
68
+ } else {
69
+ if (reconciled.outputs?.pr) state.pr = reconciled.outputs.pr;
70
+ outcome = await runPhaseWithRetry(phase, state, { io, runner, dir, issue, commandsDir, maxAttempts });
71
+ }
72
+
73
+ if (outcome.status === "blocked") return await terminate(state, "needs-human", outcome.detail);
74
+
75
+ // committed
76
+ appendEvent(dir, issue, { event: "PHASE_COMMIT", phase: phase.id, outputs: outcome.outputs || {} });
77
+ state = deriveState(readLog(dir, issue));
78
+ if (outcome.terminalReason) state.terminalReason = outcome.terminalReason;
79
+ await projector.writeState(issue, { ...state, lease: { by: agentId, until: now() + 600000 } });
80
+
81
+ if (outcome.terminalReason && TERMINAL_REASONS.includes(outcome.terminalReason))
82
+ return await terminate(state, outcome.terminalReason);
83
+ if (phase.isTerminalAfter && phase.isTerminalAfter(state))
84
+ return await terminate(state, state.terminalReason || "merged");
85
+ }
86
+ return await terminate(state, state.terminalReason || "merged");
87
+
88
+ async function terminate(s, reason, detail) {
89
+ appendEvent(dir, issue, { event: "RUN_TERMINAL", reason });
90
+ const final = { ...deriveState(readLog(dir, issue)), terminal: true, terminalReason: reason, lease: null };
91
+ await projector.writeState(issue, final);
92
+ if (reason === "needs-human") await projector.setLabel(issue, "needs-human");
93
+ return { terminalReason: reason, detail };
94
+ }
95
+ }
96
+
97
+ async function runPhaseWithRetry(phase, state, ctx) {
98
+ const { io, runner, dir, issue, commandsDir, maxAttempts } = ctx;
99
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
100
+ appendEvent(dir, issue, { event: "PHASE_START", phase: phase.id, attempt });
101
+ try {
102
+ await runner({ commandsDir, commandName: phase.command, args: [String(issue)] });
103
+ } catch (e) {
104
+ // A missing API key / SDK is a config error, not a transient phase
105
+ // failure — surface it distinctly instead of burning retries and
106
+ // escalating to needs-human as if the LLM run itself misbehaved.
107
+ if (e.code === "NO_API_KEY" || e.code === "NO_SDK") throw e;
108
+ appendEvent(dir, issue, { event: "PHASE_FAILED", phase: phase.id, attempt, reason: e.message });
109
+ continue;
110
+ }
111
+ const outcome = await phase.detectOutcome(state, io);
112
+ if (outcome.status === "committed" || outcome.status === "blocked") return outcome;
113
+ appendEvent(dir, issue, { event: "PHASE_FAILED", phase: phase.id, attempt, reason: outcome.detail });
114
+ }
115
+ // Exhausted transient retries → escalate (spec §7).
116
+ return { status: "blocked", detail: `phase ${phase.id} failed after ${maxAttempts} attempts` };
117
+ }
118
+
119
+ /**
120
+ * Build a run-log event sequence that reproduces a compact FORGE:STATE index,
121
+ * used to reconstruct the local run-log on hydrate (C2).
122
+ */
123
+ function eventsFromIndex(idx) {
124
+ const events = [{ event: "RUN_START", issue: idx.issue, run: idx.run, lane: idx.lane }];
125
+ for (const phase of idx.committed) {
126
+ const outputs = {};
127
+ if (phase === "build" && idx.branch) outputs.branch = idx.branch;
128
+ if (phase === "review" && idx.pr != null) outputs.pr = idx.pr;
129
+ events.push({ event: "PHASE_COMMIT", phase, outputs });
130
+ }
131
+ if (idx.terminal) events.push({ event: "RUN_TERMINAL", reason: idx.terminalReason });
132
+ return events;
133
+ }
134
+
135
+ function freshState(issue, lane) {
136
+ return { v: 0, run: `r_${issue}_${lane}`, issue, lane, committed: [], phase: null,
137
+ branch: null, pr: null, terminal: false, terminalReason: null, lease: null };
138
+ }