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.
- package/README.md +217 -218
- package/bin/cinema.mjs +320 -0
- package/bin/engine/phases.mjs +148 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +27 -0
- package/bin/engine-cli.mjs +50 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +464 -1491
- package/bin/journey.mjs +783 -0
- package/bin/runner.mjs +80 -13
- package/bin/settings-hook.mjs +112 -0
- package/bin/tui.mjs +6 -3
- package/commands/autopilot.md +4 -4
- package/commands/deploy-info.md +5 -3
- package/commands/design-bench.md +1 -0
- package/commands/design-render-critique-loop.md +1 -0
- package/commands/design.md +1 -0
- package/commands/forge-stats.md +1 -0
- package/commands/issue.md +1 -1
- package/commands/orchestrate.md +27 -9
- package/commands/quality-gate.md +80 -34
- package/commands/review-pr-agents.md +7 -7
- package/commands/review-pr.md +21 -4
- package/commands/work-on/build/implement.md +16 -4
- package/commands/work-on/build/validate.md +38 -1
- package/commands/work-on/build.md +27 -9
- package/commands/work-on/close.md +2 -2
- package/commands/work-on/review.md +24 -0
- package/commands/work-on-monolithic.md +1 -0
- package/commands/work-on.md +45 -3
- package/docs/BRAND.md +6 -2
- package/docs/CONFIG.md +39 -34
- package/docs/site/.vitepress/config.mjs +85 -0
- package/docs/site/getting-started.md +15 -8
- package/docs/site/index.md +1 -1
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/package.json +10 -4
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- 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
|
+
}
|