forgedock 1.11.0 → 1.12.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/bin/engine/phases-args.test.mjs +113 -0
- package/bin/engine/phases.mjs +433 -109
- package/bin/engine/runlog.mjs +21 -1
- package/bin/engine-cli.mjs +14 -2
- package/bin/engine.mjs +150 -19
- package/bin/forgedock.mjs +2 -2
- package/bin/hooks/interactive-engine.mjs +3 -0
- package/bin/labels.json +5 -0
- package/bin/orchestrate-preflight.mjs +1 -1
- package/bin/runner.mjs +111 -12
- package/bin/watch-utils.mjs +1 -0
- package/commands/cleanup.md +1 -1
- package/commands/orchestrate/phase-4-execution.md +58 -24
- package/commands/orchestrate/phase-6-report.md +29 -0
- package/commands/quality-gate.md +96 -12
- package/commands/recover-orphans.md +5 -5
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-agents.md +11 -11
- package/commands/review-pr.md +227 -33
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build/validate.md +58 -7
- package/commands/work-on/build.md +11 -4
- package/commands/work-on/decompose.md +2 -2
- package/commands/work-on/investigate.md +14 -4
- package/commands/work-on/remediate.md +109 -19
- package/commands/work-on/review.md +79 -25
- package/commands/work-on.md +23 -15
- package/docs/WORK-ON-RUNTIME.md +2 -2
- package/docs/spec/label-state-machine.md +6 -1
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/packages/protocol/src/phases.js +27 -1
- package/scripts/check-branch-ancestry.sh +82 -0
- package/scripts/check-branch-ancestry.test.sh +78 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/forge-root.test.sh +36 -15
- package/scripts/review-pr-domain-map.test.sh +87 -0
- package/scripts/spec-no-worktree-script.test.sh +46 -0
- package/scripts/transition-label.sh +3 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Contract test (forge#3499): the arguments each engine phase passes to its
|
|
2
|
+
// work-on sub-skill must satisfy that spec's `argument-hint`. Fails CI when a
|
|
3
|
+
// phase's `buildArgs` and `commands/<command>.md` drift apart — the failure
|
|
4
|
+
// mode that parked every orchestrate run at architect.
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { PHASES, PhaseArgsError } from "./phases.mjs";
|
|
11
|
+
|
|
12
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
13
|
+
|
|
14
|
+
function argumentHint(command) {
|
|
15
|
+
const file = path.join(ROOT, "commands", `${command}.md`);
|
|
16
|
+
assert.ok(existsSync(file), `engine phase command has no spec: ${file}`);
|
|
17
|
+
const m = readFileSync(file, "utf-8").match(/^---\n([\s\S]*?)\n---/);
|
|
18
|
+
assert.ok(m, `${command}.md has no frontmatter`);
|
|
19
|
+
const h = m[1].match(/^argument-hint:\s*(.*)$/m);
|
|
20
|
+
assert.ok(h, `${command}.md has no argument-hint`);
|
|
21
|
+
let v = h[1].trim();
|
|
22
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
23
|
+
return v.replace(/\\"/g, '"');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Required = flags/positional outside [...] groups; optional = inside. */
|
|
27
|
+
function parseHint(hint) {
|
|
28
|
+
const required = hint.replace(/\[[^\]]*\]/g, " ");
|
|
29
|
+
const flagsIn = (t) => [...t.matchAll(/(--[a-z][a-z-]*)/g)].map((x) => x[1]);
|
|
30
|
+
return {
|
|
31
|
+
requiredFlags: flagsIn(required),
|
|
32
|
+
allFlags: flagsIn(hint),
|
|
33
|
+
requiredPositional: /^\s*\{[A-Z_]+\}/.test(required),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const state = { v: 0, run: "r", issue: 3499, lane: "staging", committed: ["investigate", "build", "review"],
|
|
38
|
+
phase: null, branch: "fix/example-3499", pr: 77, terminal: false, terminalReason: null, lease: null };
|
|
39
|
+
const ctx = { repo: "acme/widgets" };
|
|
40
|
+
const io = {
|
|
41
|
+
gh: async () => "",
|
|
42
|
+
git: async () =>
|
|
43
|
+
"worktree /repo/.claude/worktrees/fix-example-3499\nHEAD 1111111\nbranch refs/heads/fix/example-3499\n",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe("engine phase args satisfy each target spec's argument-hint (forge#3499)", () => {
|
|
47
|
+
it("context/architect are not engine phases (work-on/build owns them)", () => {
|
|
48
|
+
const ids = PHASES.map((p) => p.id);
|
|
49
|
+
assert.ok(!ids.includes("context") && !ids.includes("architect"), ids.join(","));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
for (const phase of PHASES) {
|
|
53
|
+
it(`${phase.id} (${phase.command})`, async () => {
|
|
54
|
+
assert.equal(typeof phase.buildArgs, "function", `${phase.id} must define buildArgs`);
|
|
55
|
+
const hint = parseHint(argumentHint(phase.command));
|
|
56
|
+
const args = await phase.buildArgs({ ...state, terminalReason: phase.id === "remediate" ? "needs-human" : null, remediationKind: phase.id === "remediate" ? "inpr-fix" : null }, ctx, io);
|
|
57
|
+
|
|
58
|
+
assert.ok(Array.isArray(args) && args.every((a) => typeof a === "string"), "args must be an array of strings");
|
|
59
|
+
assert.ok(args.length > 1, `${phase.id} must not be invoked with only a positional`);
|
|
60
|
+
for (const a of args) assert.ok(!/[\n\r\t]/.test(a), `control char in arg ${JSON.stringify(a)}`);
|
|
61
|
+
|
|
62
|
+
// positional: issue number (or PR number for remediate) comes first
|
|
63
|
+
const expectedPositional = phase.id === "remediate" ? String(state.pr) : String(state.issue);
|
|
64
|
+
assert.equal(args[0], expectedPositional);
|
|
65
|
+
if (hint.requiredPositional) assert.match(args[0], /^[0-9]+$/);
|
|
66
|
+
|
|
67
|
+
const emitted = args.filter((a) => a.startsWith("--"));
|
|
68
|
+
for (const flag of hint.requiredFlags) {
|
|
69
|
+
const i = args.indexOf(flag);
|
|
70
|
+
assert.ok(i >= 0, `${phase.id}: required ${flag} missing from [${args.join(" ")}]`);
|
|
71
|
+
assert.ok(args[i + 1] !== undefined && args[i + 1] !== "" && !args[i + 1].startsWith("--"),
|
|
72
|
+
`${phase.id}: ${flag} has no value`);
|
|
73
|
+
}
|
|
74
|
+
for (const flag of emitted) {
|
|
75
|
+
assert.ok(hint.allFlags.includes(flag), `${phase.id}: emits ${flag}, which ${phase.command}.md does not declare`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it("--gh-flag is a single quoted token, as in the specs' argument-hint", async () => {
|
|
81
|
+
const args = await PHASES.find((p) => p.id === "investigate").buildArgs(state, ctx, io);
|
|
82
|
+
assert.equal(args[args.indexOf("--gh-flag") + 1], '"-R acme/widgets"');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("fails closed (PhaseArgsError) on a missing or injectable repo/branch/worktree rather than sending a partial arg set", async () => {
|
|
86
|
+
const inv = PHASES.find((p) => p.id === "investigate");
|
|
87
|
+
const review = PHASES.find((p) => p.id === "review");
|
|
88
|
+
for (const repo of [null, "", "no-slash", "a/b --base main", 'a/b"c', "a/b\nc"]) {
|
|
89
|
+
await assert.rejects(() => inv.buildArgs(state, { repo }, io), PhaseArgsError, `repo=${JSON.stringify(repo)}`);
|
|
90
|
+
}
|
|
91
|
+
await assert.rejects(() => review.buildArgs({ ...state, branch: "x y" }, ctx, io), PhaseArgsError);
|
|
92
|
+
await assert.rejects(() => review.buildArgs({ ...state, branch: null }, ctx, io), PhaseArgsError);
|
|
93
|
+
await assert.rejects(() => review.buildArgs(state, ctx, { git: async () => "worktree /repo\nbranch refs/heads/main\n" }), PhaseArgsError);
|
|
94
|
+
await assert.rejects(() => review.buildArgs(state, ctx, { git: async () => { throw new Error("git down"); } }), PhaseArgsError);
|
|
95
|
+
await assert.rejects(() => PHASES.find((p) => p.id === "remediate").buildArgs({ ...state, pr: null }, ctx, io), PhaseArgsError);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("close refuses a merged terminal state without a PR (forge#3504)", async () => {
|
|
99
|
+
const close = PHASES.find((p) => p.id === "close");
|
|
100
|
+
await assert.rejects(() => close.buildArgs({ ...state, pr: null, terminalReason: "merged" }, ctx, io), PhaseArgsError);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("close derives --terminal-state from the run's terminal reason", async () => {
|
|
104
|
+
const close = PHASES.find((p) => p.id === "close");
|
|
105
|
+
for (const [reason, expected] of [[null, "merged"], ["invalid", "invalid"], ["decomposed", "decomposed"]]) {
|
|
106
|
+
const args = await close.buildArgs({ ...state, terminalReason: reason }, ctx, io);
|
|
107
|
+
assert.equal(args[args.indexOf("--terminal-state") + 1], expected);
|
|
108
|
+
}
|
|
109
|
+
// worktree is optional for close: absent worktree must not fail it
|
|
110
|
+
const args = await close.buildArgs(state, ctx, { git: async () => "" });
|
|
111
|
+
assert.ok(!args.includes("--worktree"));
|
|
112
|
+
});
|
|
113
|
+
});
|