forgedock 1.27.0 → 1.28.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/README.md +4 -1
- package/bin/bootstrap.mjs +55 -0
- package/bin/engine/outcome-codes.mjs +10 -0
- package/bin/engine/telemetry-seam.mjs +37 -8
- package/bin/engine/telemetry-seam.test.mjs +31 -0
- package/bin/engine-cli.mjs +542 -38
- package/bin/engine.mjs +8 -4
- package/bin/forgedock.mjs +75 -9
- package/bin/gc.mjs +580 -0
- package/bin/lean/admission.mjs +33 -3
- package/bin/lean/batch.mjs +22 -4
- package/bin/lean/batch.test.mjs +5 -3
- package/bin/lean/checks.mjs +199 -0
- package/bin/lean/checks.test.mjs +183 -0
- package/bin/lean/conformance.mjs +190 -58
- package/bin/lean/conformance.test.mjs +116 -7
- package/bin/lean/credentials.mjs +29 -0
- package/bin/lean/depcache.mjs +312 -0
- package/bin/lean/depcache.test.mjs +298 -0
- package/bin/lean/drift.mjs +198 -0
- package/bin/lean/drift.test.mjs +223 -0
- package/bin/lean/effects.mjs +85 -8
- package/bin/lean/execution.e2e.test.mjs +81 -0
- package/bin/lean/execution.mjs +431 -0
- package/bin/lean/execution.test.mjs +583 -0
- package/bin/lean/heavy.mjs +289 -0
- package/bin/lean/heavy.test.mjs +207 -0
- package/bin/lean/lean.mjs +572 -139
- package/bin/lean/lean.test.mjs +1193 -31
- package/bin/lean/ledger.mjs +27 -2
- package/bin/lean/ledger.test.mjs +36 -1
- package/bin/lean/limits.mjs +58 -0
- package/bin/lean/limits.test.mjs +150 -0
- package/bin/lean/orchestrate.mjs +312 -19
- package/bin/lean/prompts.mjs +89 -15
- package/bin/lean/refactor.mjs +276 -0
- package/bin/lean/refactor.test.mjs +359 -0
- package/bin/lean/render.mjs +24 -5
- package/bin/lean/review-trigger.mjs +134 -0
- package/bin/lean/review-trigger.test.mjs +104 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
- package/bin/lean/sandbox-remote.mjs +142 -0
- package/bin/lean/sched.mjs +221 -0
- package/bin/lean/sched.test.mjs +310 -0
- package/bin/lean/schedd.mjs +247 -0
- package/bin/lean/schedd.test.mjs +180 -0
- package/bin/lean/toolchain.mjs +228 -0
- package/bin/lean/toolchain.test.mjs +154 -0
- package/bin/lean/verify.mjs +20 -9
- package/bin/lean/verify.test.mjs +26 -3
- package/bin/lean/worktree.mjs +69 -0
- package/bin/lean/worktree.test.mjs +141 -0
- package/bin/runner.mjs +53 -10
- package/bin/stats/github-source.mjs +1 -1
- package/bin/stats/index.mjs +14 -2
- package/bin/stats/query.mjs +72 -0
- package/bin/stats/stats.test.mjs +54 -0
- package/commands/audit.md +2 -0
- package/commands/autopilot.md +2 -0
- package/commands/bootstrap.md +35 -0
- package/commands/cleanup.md +7 -22
- package/commands/forge-stats.md +2 -1
- package/commands/forgedock-init.md +7 -0
- package/commands/issue.md +89 -830
- package/commands/milestone.md +55 -6
- package/commands/orchestrate.md +10 -2
- package/commands/upgrade-deps.md +3 -3
- package/commands/work-on/build/validate.md +4 -0
- package/commands/work-on/close.md +2 -0
- package/commands/work-on/decompose.md +24 -6
- package/docs/CI.md +3 -3
- package/docs/CONFIG.md +177 -2
- package/docs/OUTCOME-CODES.md +8 -0
- package/docs/TELEMETRY.md +1 -1
- package/docs/site/command-reference.md +22 -0
- package/docs/site/getting-started.md +2 -0
- package/docs/site/start-a-new-project.md +327 -0
- package/package.json +1 -1
- package/scripts/affected-files-check.sh +91 -0
- package/scripts/affected-files-check.test.sh +80 -0
- package/scripts/bootstrap-create.mjs +393 -0
- package/scripts/bootstrap-validate.mjs +36 -2
- package/scripts/calibration.mjs +116 -0
- package/scripts/check-devdocs-freshness.sh +9 -4
- package/scripts/check-devdocs-freshness.test.sh +11 -1
- package/scripts/check-spec-markers.sh +3 -0
- package/scripts/check-spec-size.sh +29 -11
- package/scripts/check-spec-size.test.sh +27 -4
- package/scripts/ci-local/lib.mjs +8 -1
- package/scripts/ci-local/parallelism.test.mjs +27 -0
- package/scripts/ci-local/run.mjs +3 -3
- package/scripts/ci-local/scoping.mjs +82 -0
- package/scripts/ci-local.test.sh +1 -1
- package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
- package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
- package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
- package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
- package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
- package/scripts/fixtures/symbol-index/py/sample.py +41 -0
- package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
- package/scripts/fixtures/symbol-move/gen_big.py +201 -0
- package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/hotspots.sh +100 -0
- package/scripts/hotspots.test.sh +101 -0
- package/scripts/issue-create.sh +197 -0
- package/scripts/issue-create.test.sh +166 -0
- package/scripts/maintenance-scan.sh +185 -0
- package/scripts/maintenance-scan.test.sh +89 -0
- package/scripts/milestone-drift.sh +206 -0
- package/scripts/milestone-drift.test.sh +120 -0
- package/scripts/refactor-verify.sh +368 -0
- package/scripts/refactor-verify.test.sh +202 -0
- package/scripts/structure-delta.sh +10 -5
- package/scripts/structure-delta.test.sh +3 -2
- package/scripts/symbol-index.mjs +253 -0
- package/scripts/symbol-index.py +354 -0
- package/scripts/symbol-index.test.sh +119 -0
- package/scripts/symbol-move-ts.test.sh +161 -0
- package/scripts/symbol-move.mjs +379 -0
- package/scripts/symbol-move.py +383 -0
- package/scripts/symbol-move.test.sh +168 -0
- package/templates/bootstrap/interview.md +34 -0
- package/templates/bootstrap/issue-body.md +42 -0
- package/templates/bootstrap/needs-human-issue.md +45 -0
- package/templates/bootstrap/stack-profiles.md +34 -0
- package/templates/bootstrap/technical-spike.md +75 -0
package/bin/stats/index.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { aggregate, DEFAULT_MIN_N } from "./aggregate.mjs";
|
|
|
13
13
|
import { fetchGithubRecords, isValidRepo, toModel } from "./github-source.mjs";
|
|
14
14
|
import { readLocalRuns, runsDir } from "./local-source.mjs";
|
|
15
15
|
import { renderCompact, renderJson, renderMarkdown } from "./render.mjs";
|
|
16
|
+
import { edgeQuery, parseEdge, verdictMix } from "./query.mjs";
|
|
16
17
|
|
|
17
18
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
|
|
@@ -34,10 +35,14 @@ delta against the previous version and a low-n flag for samples under 10. Missin
|
|
|
34
35
|
--min-n N low-n threshold (default ${DEFAULT_MIN_N})
|
|
35
36
|
--release-times FILE "<version> <epoch-seconds>" lines used to estimate versions of unversioned records
|
|
36
37
|
(default: git tags v* of the ForgeDock checkout)
|
|
38
|
+
|
|
39
|
+
Audit queries (forge#4165; JSON, from structure_delta / run_terminal telemetry, no comment bodies read):
|
|
40
|
+
--edge FROM:TO merged PRs that added the dependency or import edge FROM -> TO (e.g. @s/web:@s/db)
|
|
41
|
+
--milestone M conformance verdict mix and per-day structure series for milestone M
|
|
37
42
|
`;
|
|
38
43
|
|
|
39
44
|
export function parseArgs(argv) {
|
|
40
|
-
const o = { since: null, repo: null, json: false, refresh: false, compact: false, local: true, cache: true, minN: DEFAULT_MIN_N, releaseTimes: null, help: false };
|
|
45
|
+
const o = { since: null, repo: null, edge: null, milestone: null, json: false, refresh: false, compact: false, local: true, cache: true, minN: DEFAULT_MIN_N, releaseTimes: null, help: false };
|
|
41
46
|
const val = (i, name) => {
|
|
42
47
|
const v = argv[i + 1];
|
|
43
48
|
if (v == null || v.startsWith("--")) throw new Error(`${name} needs a value`);
|
|
@@ -51,6 +56,8 @@ export function parseArgs(argv) {
|
|
|
51
56
|
if (name === "--since") o.since = take();
|
|
52
57
|
else if (name === "--repo") o.repo = take();
|
|
53
58
|
else if (name === "--release-times") o.releaseTimes = take();
|
|
59
|
+
else if (name === "--edge") { const v = take(); if (!parseEdge(v)) throw new Error("--edge must be FROM:TO"); o.edge = v; }
|
|
60
|
+
else if (name === "--milestone") o.milestone = take();
|
|
54
61
|
else if (name === "--min-n") { const v = take(); if (!/^\d+$/.test(v)) throw new Error("--min-n must be a non-negative integer"); o.minN = Number(v); }
|
|
55
62
|
else if (name === "--json") o.json = true;
|
|
56
63
|
else if (name === "--refresh") o.refresh = true;
|
|
@@ -153,6 +160,11 @@ export async function buildReport(opts, deps = {}) {
|
|
|
153
160
|
if (bound.note) (deps.warn ?? ((m) => process.stderr.write(`forgedock stats: ${m}\n`)))(bound.note);
|
|
154
161
|
const gh = await fetchGithubRecords({ repo, since: bound.since, refresh: opts.refresh, useCache: opts.cache, gh: deps.gh, env,
|
|
155
162
|
onProgress: deps.onProgress, sleep: deps.sleep, now: deps.now });
|
|
163
|
+
if (opts.edge || opts.milestone) { // forge#4165: audit query instead of the version report
|
|
164
|
+
const e = opts.edge ? parseEdge(opts.edge) : null;
|
|
165
|
+
const query = { ...(e ? { edge: { ...e, prs: edgeQuery(gh.records, e.from, e.to) } } : {}), ...(opts.milestone ? { mix: verdictMix(gh.records, gh.items, opts.milestone) } : {}) };
|
|
166
|
+
return { repo, sources: { github: { items: gh.items.length, records: gh.records.length, cached: gh.fromCache } }, query };
|
|
167
|
+
}
|
|
156
168
|
const model = toModel(repo, gh);
|
|
157
169
|
const ghKeys = new Set(model.runs.map((r) => `${r.repo}#${r.issue}`));
|
|
158
170
|
let local = [];
|
|
@@ -178,7 +190,7 @@ export async function runStats(argv, deps = {}) {
|
|
|
178
190
|
}
|
|
179
191
|
try {
|
|
180
192
|
const report = await buildReport(opts, { onProgress: makeProgressPrinter(), ...deps });
|
|
181
|
-
process.stdout.write(`${opts.json ? renderJson(report) : opts.compact ? renderCompact(report) : renderMarkdown(report)}\n`);
|
|
193
|
+
process.stdout.write(`${opts.json || report.query ? renderJson(report) : opts.compact ? renderCompact(report) : renderMarkdown(report)}\n`);
|
|
182
194
|
return 0;
|
|
183
195
|
} catch (err) {
|
|
184
196
|
process.stderr.write(`forgedock stats: ${err.message}\n`);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
/**
|
|
4
|
+
* forge#4165: deterministic audit queries over FORGE:TELEMETRY records, so `/audit` answers "which PRs added A→B?" and
|
|
5
|
+
* "what is the conformance verdict mix for milestone M?" from structured events instead of comment archaeology.
|
|
6
|
+
* Inputs are the records and issue metadata `fetchGithubRecords` already returns; no extra API call, no model.
|
|
7
|
+
* structure_delta (lean, merge-time): deps_added / new_edges / removed_edges as "from:to" comma lists, pr, milestone
|
|
8
|
+
* run_terminal: reason, pr, milestone, conformance_mode, shadow_would
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const list = (v) => (typeof v === "string" && v !== "none" ? v.split(",").filter(Boolean) : []);
|
|
12
|
+
const day = (ts) => (typeof ts === "string" ? ts.slice(0, 10) : "unknown");
|
|
13
|
+
|
|
14
|
+
/** "from:to" -> {from, to}; package names hold no ":" so the last ":" splits (a scoped name keeps its "/"). */
|
|
15
|
+
export function parseEdge(s) {
|
|
16
|
+
const i = String(s ?? "").lastIndexOf(":");
|
|
17
|
+
if (i <= 0 || i === String(s).length - 1) return null;
|
|
18
|
+
return { from: String(s).slice(0, i), to: String(s).slice(i + 1) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Milestone of a record: its issue's milestone from the crawled metadata (the exact title), else the record's own field
|
|
23
|
+
* (the emitter replaces characters outside [A-Za-z0-9 _.:/#@+=,-] with "_", so it may differ from the title).
|
|
24
|
+
*/
|
|
25
|
+
function milestoneOf(r, byIssue) {
|
|
26
|
+
return byIssue.get(r.issue)?.milestone ?? (typeof r.milestone === "string" && r.milestone ? r.milestone : null);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* PRs (and issues) whose merged change added the edge from→to: `kind` "package" (a package.json dependency) or
|
|
31
|
+
* "import" (value imports). Oldest first; one row per issue and kind (the latest record wins).
|
|
32
|
+
*/
|
|
33
|
+
export function edgeQuery(records, from, to) {
|
|
34
|
+
const key = `${from}:${to}`;
|
|
35
|
+
const rows = new Map();
|
|
36
|
+
for (const r of records) {
|
|
37
|
+
if (r?.event !== "structure_delta") continue;
|
|
38
|
+
for (const [field, kind] of [["deps_added", "package"], ["new_edges", "import"]]) {
|
|
39
|
+
if (!list(r[field]).includes(key)) continue;
|
|
40
|
+
const k = `${r.issue}\u0000${kind}`;
|
|
41
|
+
const prev = rows.get(k);
|
|
42
|
+
if (!prev || String(r.ts ?? "") >= String(prev.ts ?? "")) rows.set(k, { issue: r.issue ?? null, pr: r.pr ?? null, ts: r.ts ?? null, kind, milestone: r.milestone ?? null });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...rows.values()].sort((a, b) => String(a.ts ?? "").localeCompare(String(b.ts ?? "")) || (a.issue ?? 0) - (b.issue ?? 0));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Conformance verdict mix of finished runs (run_terminal) and the per-day structure series (structure_delta), for one
|
|
50
|
+
* milestone or, with `milestone` null, for the whole repo. `items` is the crawled issue metadata ({number, milestone}).
|
|
51
|
+
*/
|
|
52
|
+
export function verdictMix(records, items = [], milestone = null) {
|
|
53
|
+
const byIssue = new Map(items.map((i) => [i.number, i]));
|
|
54
|
+
const sel = (r) => milestone == null || milestoneOf(r, byIssue) === milestone;
|
|
55
|
+
const terms = records.filter((r) => r?.event === "run_terminal" && sel(r));
|
|
56
|
+
const count = (xs, f) => xs.reduce((m, x) => { const k = f(x); if (k != null) m[k] = (m[k] ?? 0) + 1; return m; }, {});
|
|
57
|
+
const series = new Map();
|
|
58
|
+
for (const r of records) {
|
|
59
|
+
if (r?.event !== "structure_delta" || r.phase !== "merge" || !sel(r)) continue;
|
|
60
|
+
const d = day(r.ts);
|
|
61
|
+
const s = series.get(d) ?? { date: d, merged: 0, deps_added: 0, new_edges: 0, removed_edges: 0 };
|
|
62
|
+
s.merged++; s.deps_added += list(r.deps_added).length; s.new_edges += list(r.new_edges).length; s.removed_edges += list(r.removed_edges).length;
|
|
63
|
+
series.set(d, s);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
milestone, runs: terms.length,
|
|
67
|
+
verdicts: count(terms, (r) => (typeof r.shadow_would === "string" ? r.shadow_would : null)),
|
|
68
|
+
modes: count(terms, (r) => (typeof r.conformance_mode === "string" ? r.conformance_mode : null)),
|
|
69
|
+
reasons: count(terms, (r) => (typeof r.reason === "string" ? r.reason : null)),
|
|
70
|
+
series: [...series.values()].sort((a, b) => a.date.localeCompare(b.date)),
|
|
71
|
+
};
|
|
72
|
+
}
|
package/bin/stats/stats.test.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { fetchGithubRecords, parseTelemetryComments, runsFromRecords, toModel }
|
|
|
14
14
|
import { buildReport, makeProgressPrinter, parseArgs, parseSince, runStats, sinceToTimestamp } from "./index.mjs";
|
|
15
15
|
import { readLocalRuns } from "./local-source.mjs";
|
|
16
16
|
import { renderCompact, renderJson, renderMarkdown } from "./render.mjs";
|
|
17
|
+
import { edgeQuery, parseEdge, verdictMix } from "./query.mjs";
|
|
17
18
|
|
|
18
19
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const fx = (f) => join(here, "fixtures", f);
|
|
@@ -471,3 +472,56 @@ describe("cli", () => {
|
|
|
471
472
|
try { assert.equal(await runStats(["--repo", "bad repo"]), 2); } finally { process.stderr.write = real; }
|
|
472
473
|
});
|
|
473
474
|
});
|
|
475
|
+
|
|
476
|
+
describe("audit queries (forge#4165)", () => {
|
|
477
|
+
const rec = (o) => ({ v: 1, repo: "o/r", run: null, forge_version: "1.20.0", ...o });
|
|
478
|
+
const RECS = [
|
|
479
|
+
rec({ eid: "s1", ts: "2026-10-10T10:00:00Z", issue: 11, pr: 21, phase: "merge", event: "structure_delta", deps_added: "@s/web:@s/db", new_edges: "none", removed_edges: "none", decisions: "K1:lint", milestone: "M One" }),
|
|
480
|
+
rec({ eid: "s2", ts: "2026-10-11T10:00:00Z", issue: 12, pr: 22, phase: "merge", event: "structure_delta", deps_added: "none", new_edges: "@s/web:@s/db,@s/web:@s/core", removed_edges: "@s/a:@s/b", milestone: "M One" }),
|
|
481
|
+
rec({ eid: "s3", ts: "2026-10-11T11:00:00Z", issue: 13, pr: 23, phase: "merge", event: "structure_delta", deps_added: "none", new_edges: "@s/x:@s/db" }),
|
|
482
|
+
rec({ eid: "s4", ts: "2026-10-09T10:00:00Z", issue: 14, phase: "implement", event: "structure_delta", edges_added: 1 }),
|
|
483
|
+
rec({ eid: "t1", ts: "2026-10-10T10:01:00Z", issue: 11, pr: 21, event: "run_terminal", reason: "merged", conformance_mode: "shadow", shadow_would: "conforms", milestone: "M One" }),
|
|
484
|
+
rec({ eid: "t2", ts: "2026-10-11T10:01:00Z", issue: 12, pr: 22, event: "run_terminal", reason: "merged", conformance_mode: "shadow", shadow_would: "conformance.scope" }),
|
|
485
|
+
rec({ eid: "t3", ts: "2026-10-11T11:01:00Z", issue: 13, pr: 23, event: "run_terminal", reason: "merged", conformance_mode: "enforce", milestone: "M Two" }),
|
|
486
|
+
];
|
|
487
|
+
const ITEMS = [{ number: 11, milestone: "M One" }, { number: 12, milestone: "M One" }, { number: 13, milestone: null }];
|
|
488
|
+
|
|
489
|
+
it("parseEdge splits on the last colon and rejects malformed input", () => {
|
|
490
|
+
assert.deepEqual(parseEdge("@s/web:@s/db"), { from: "@s/web", to: "@s/db" });
|
|
491
|
+
for (const bad of ["", "nocolon", ":b", "a:"]) assert.equal(parseEdge(bad), null, bad);
|
|
492
|
+
});
|
|
493
|
+
it("lists the PRs that added an edge, package or import, oldest first; exact element match only", () => {
|
|
494
|
+
assert.deepEqual(edgeQuery(RECS, "@s/web", "@s/db"), [
|
|
495
|
+
{ issue: 11, pr: 21, ts: "2026-10-10T10:00:00Z", kind: "package", milestone: "M One" },
|
|
496
|
+
{ issue: 12, pr: 22, ts: "2026-10-11T10:00:00Z", kind: "import", milestone: "M One" },
|
|
497
|
+
]);
|
|
498
|
+
assert.deepEqual(edgeQuery(RECS, "@s/web", "@s/d"), [], "a prefix is not the edge");
|
|
499
|
+
assert.deepEqual(edgeQuery(RECS, "@s/a", "@s/b"), [], "a removed edge is not an added one");
|
|
500
|
+
});
|
|
501
|
+
it("verdict mix and structure series for a milestone (issue metadata first, then the record's own field)", () => {
|
|
502
|
+
const m = verdictMix(RECS, ITEMS, "M One");
|
|
503
|
+
assert.equal(m.runs, 2);
|
|
504
|
+
assert.deepEqual(m.verdicts, { conforms: 1, "conformance.scope": 1 });
|
|
505
|
+
assert.deepEqual(m.modes, { shadow: 2 });
|
|
506
|
+
assert.deepEqual(m.series, [{ date: "2026-10-10", merged: 1, deps_added: 1, new_edges: 0, removed_edges: 0 }, { date: "2026-10-11", merged: 1, deps_added: 0, new_edges: 2, removed_edges: 1 }]);
|
|
507
|
+
assert.equal(verdictMix(RECS, [], "M Two").runs, 1, "falls back to the record's milestone");
|
|
508
|
+
assert.equal(verdictMix(RECS, ITEMS, null).runs, 3, "no milestone: the whole repo");
|
|
509
|
+
});
|
|
510
|
+
it("buildReport --edge/--milestone answers from telemetry records alone; the version report is unchanged without them", async () => {
|
|
511
|
+
const body = `<!-- FORGE:TELEMETRY v1 -->\n\`\`\`jsonl\n${RECS.filter((r) => r.issue === 11 || r.issue === 12).map((r) => JSON.stringify(r)).join("\n")}\n\`\`\``;
|
|
512
|
+
const issues = [11, 12].map((n) => ({ number: n, labels: [], created_at: "2026-10-09T00:00:00Z", closed_at: null, updated_at: "2026-10-11T12:00:00Z", user: { type: "Bot" }, milestone: { title: "M One" } }));
|
|
513
|
+
const gh = (args) => {
|
|
514
|
+
const path = args.find((a) => a.startsWith("repos/")) ?? "";
|
|
515
|
+
if (/issues\/(11|12)\/comments$/.test(path)) return JSON.stringify(path.includes("/11/") ? [{ body, user: { type: "Bot" } }] : []);
|
|
516
|
+
if (/issues\/comments\?/.test(path)) return JSON.stringify(/page=1(&|$)/.test(path) ? [{ body, user: { type: "Bot" }, issue_url: "https://api.github.com/repos/o/r/issues/11" }] : []);
|
|
517
|
+
return JSON.stringify(issues);
|
|
518
|
+
};
|
|
519
|
+
const opts = { repo: "o/r", local: false, cache: false, minN: 10, ...parseArgs(["--edge", "@s/web:@s/db", "--milestone", "M One"]) };
|
|
520
|
+
const report = await buildReport({ ...opts, repo: "o/r", local: false, cache: false }, { gh, env: ENV, releaseTimes: [] });
|
|
521
|
+
assert.deepEqual(report.query.edge.prs.map((p) => [p.pr, p.kind]), [[21, "package"], [22, "import"]]);
|
|
522
|
+
assert.deepEqual(report.query.mix.verdicts, { conforms: 1, "conformance.scope": 1 });
|
|
523
|
+
assert.ok(!("groups" in report));
|
|
524
|
+
assert.throws(() => parseArgs(["--edge", "nocolon"]), /--edge must be FROM:TO/);
|
|
525
|
+
assert.deepEqual([parseArgs([]).edge, parseArgs([]).milestone], [null, null]);
|
|
526
|
+
});
|
|
527
|
+
});
|
package/commands/audit.md
CHANGED
|
@@ -94,6 +94,8 @@ Read `$ARGUMENTS` carefully. The user is describing a failure — extract:
|
|
|
94
94
|
|
|
95
95
|
### 1B: Find related GitHub artifacts
|
|
96
96
|
|
|
97
|
+
Events first: `forgedock stats --repo {GH_REPO} --edge FROM:TO` (PRs that added an edge) or `--milestone M` (verdict mix, edge series) reads `structure_delta`/`run_terminal` telemetry. Read only the matched issues; parse comments below only when the events are missing.
|
|
98
|
+
|
|
97
99
|
Start from whatever the user provides (issue number, PR number, or description) and expand outward:
|
|
98
100
|
|
|
99
101
|
```bash
|
package/commands/autopilot.md
CHANGED
|
@@ -28,6 +28,8 @@ You are a fully autonomous deploy loop for this project. Your job is to **detect
|
|
|
28
28
|
|
|
29
29
|
**Issue selection and priority**: Phase 2 (Fast Lane Loop) dispatches all eligible open issues **concurrently** via the durable engine — ordering is irrelevant at dispatch time because all issues start simultaneously. For scenarios requiring sequential, priority-ordered dispatch (P0 before P1 before P2, unlabeled excluded), use `scripts/select-fix-targets.sh` as the entry point. This script will be the substitution point when #1743 (economic scheduling) implements value/cost-based ordering. <!-- Added: forge#1752 -->
|
|
30
30
|
|
|
31
|
+
**Maintenance budget** (#4197, only with forge.yaml `maintenance.enabled: true`): the batch runner admits P0/P1 features > maintenance-labelled issues within `maintenance.share` of lane starts (0.2 = 1 in 5) > P2+ features, and `when_idle` drains maintenance when no feature is pending. When `maintenance.scan: weekly` and the newest issue carrying a `FORGE:MAINT` marker (open or closed: `gh issue list --state all --search "FORGE:MAINT in:body" --limit 1 --json createdAt`) is older than 7 days, or none exists, run `scripts/maintenance-scan.sh --print-issue-args` once before Phase 2 and file each printed line through `/issue` programmatic mode, `Skill(skill="{FORGE_SKILL_PREFIX}issue", args=<line>)`; never raw `gh issue create`. The scan is deterministic, already deduplicated by marker, and prints nothing under threshold.
|
|
32
|
+
|
|
31
33
|
**This command overrides the standard "never merge to main" rule.** `/autopilot` IS the authorized deploy system. It ships staging→main and milestone→staging→main as part of normal operation.
|
|
32
34
|
|
|
33
35
|
**This command resumes from wherever the pipeline is stuck.** It always reads current GitHub state before taking any action.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Guided idea-to-backlog setup for a brand-new project — interview, defaults, doubts, spike doc, validated orchestrate-ready backlog
|
|
3
|
+
argument-hint: "<free text | path to docs> [--dry-run] [--spike <path>]"
|
|
4
|
+
---
|
|
5
|
+
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
|
+
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
7
|
+
|
|
8
|
+
# /bootstrap — idea to orchestrate-ready backlog
|
|
9
|
+
|
|
10
|
+
**Input**: $ARGUMENTS
|
|
11
|
+
|
|
12
|
+
This spec holds only judgment. All GitHub writes run in `scripts/bootstrap-create.mjs`. Templates live in `$(forge_root)/templates/bootstrap/`.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
16
|
+
BOOT_TPL="$(forge_root)/templates/bootstrap"; VALIDATE=$(forge_resolve bootstrap-validate.mjs); CREATE=$(forge_resolve bootstrap-create.mjs)
|
|
17
|
+
[ -n "$VALIDATE" ] && [ -n "$CREATE" ] || { echo "ERROR: bootstrap scripts not found in a trusted ForgeDock install" >&2; exit 3; }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
1. **Read the input.** Use the free text, or read each doc at the given path(s).
|
|
21
|
+
2. **Interview** from `interview.md`. Cover every aspect it lists, briefly and conversationally. Skip what the input already answers.
|
|
22
|
+
3. **Fill the blanks.** Give every unanswered aspect a sensible default with a one-line rationale tied to what the user said, using the closest `stack-profiles.md` preset. Show them all in ONE editable "Here's what I assumed" list and apply the user's edits.
|
|
23
|
+
4. **Ask only genuine doubts** (they change architecture or cost: native vs PWA, data residency, self-host vs managed DB, product name, repo owner). Ask with AskUserQuestion, at most 4 multiple-choice questions per batch, each with a recommended option. Everything else stays a default. Headless (no AskUserQuestion, e.g. `npx forgedock bootstrap`): take each recommended option and list it under "Here's what I assumed".
|
|
24
|
+
5. **Spike doc.** Write `technical-spike.md` to `--spike` (default `docs/engineering/technical-spike.md`); set the plan's `spikeDoc` to that path. Fill every section and give each decision an ADR-style `**Rationale**:` line.
|
|
25
|
+
6. **Plan JSON** in `$TMPDIR` (shape: header of `bootstrap-validate.mjs` and `bootstrap-create.mjs`):
|
|
26
|
+
- Milestones in build order, foundation first.
|
|
27
|
+
- A `SPIKE` parent issue.
|
|
28
|
+
- Sub-issues from `issue-body.md` with planned paths, `[type:*]` tags, `priority` and `component`.
|
|
29
|
+
- `"ui": true` issues carry the full design spec. `"hero": true` issues route to `/design`.
|
|
30
|
+
- One `needs-human-issue.md` issue per credential, account or external input. Code issues reference it as `#{{KEY}}` but never depend on it, and name the mock that keeps dev and CI green.
|
|
31
|
+
- Sub-issues point at the spike via `**Parent**` only, never `Depends on`.
|
|
32
|
+
- `guardrails` and `domainRules` from the interview.
|
|
33
|
+
7. **Validate.** Run `node "$VALIDATE" <plan>`. Fix the plan until it exits 0. Never create from an invalid plan.
|
|
34
|
+
8. **Create.** Run `node "$CREATE" <plan>`, adding `--dry-run` when the input has it (preview only, GitHub untouched). The script reuses existing issues by title and parent, so on an error re-run the same command to resume.
|
|
35
|
+
9. **Hand off.** Print the script's hand-off verbatim: what was created (links), what needs a human (links), and the `/orchestrate` command per milestone, in order.
|
package/commands/cleanup.md
CHANGED
|
@@ -12,6 +12,8 @@ install: extras
|
|
|
12
12
|
|
|
13
13
|
Scan the entire development environment for rot and fix it. This is a maintenance command — run periodically or after large orchestration batches. It covers 6 domains: stale labels, orphaned issues and dead claims boards, worktree/branch pruning, milestone hygiene, and Project board sync.
|
|
14
14
|
|
|
15
|
+
Issues this command or a `dead-code` finding produces carry the `maintenance` label, so `forgedock orchestrate` dispatches them within the maintenance budget (forge.yaml `maintenance.share`, #4197), not the feature queue.
|
|
16
|
+
|
|
15
17
|
**NEVER use plan mode (EnterPlanMode)** — it breaks execution context.
|
|
16
18
|
|
|
17
19
|
---
|
|
@@ -47,7 +49,7 @@ All `{GH_REPO}`, `{GH_FLAG}`, `{REPO_PATH}`, `{STAGING_BRANCH}`, `{PROJECT_BOARD
|
|
|
47
49
|
|-------|--------|
|
|
48
50
|
| `labels` or empty | Fix stale/missing workflow labels on closed issues; detect OPEN issues stuck in intermediate states (Phase 1D) |
|
|
49
51
|
| `orphans` | Close open issues whose PRs are already merged (Phase 2A/2B), and close dead orchestrate claims boards (Phase 2C) |
|
|
50
|
-
| `branches` |
|
|
52
|
+
| `branches` | Reclaim ForgeDock-owned worktrees, tmp, docker and orphans via `forgedock gc` (3A/3B); prune remote branches for merged PRs (3C) |
|
|
51
53
|
| `milestones` | Report milestones with 0 open issues (advisory — never closes) |
|
|
52
54
|
| `board` | Sync closed issues to Project board with correct terminal state |
|
|
53
55
|
| `batch-p3` | Sweep: fold stale unbatched P3 review findings into surface-area-grouped batch issues (same file, then leaf directory) |
|
|
@@ -321,32 +323,15 @@ echo "Claims boards closed: $BOARDS_CLOSED"
|
|
|
321
323
|
|
|
322
324
|
## Phase 3: Worktree & Branch Pruning
|
|
323
325
|
|
|
324
|
-
### 3A:
|
|
326
|
+
### 3A/3B: Reclaim owned worktrees, tmp, docker and orphans (the `forgedock gc` reaper, #4183)
|
|
325
327
|
|
|
326
328
|
```bash
|
|
327
329
|
cd {REPO_PATH}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
REPO_NAME=$(basename "{REPO_PATH}")
|
|
331
|
-
git worktree list --porcelain | grep "^worktree " | grep -v "/$REPO_NAME$" | sed 's/^worktree //' | while read wt; do
|
|
332
|
-
branch=$(git -C "$wt" branch --show-current 2>/dev/null)
|
|
333
|
-
if [ -n "$branch" ]; then
|
|
334
|
-
merged_pr=$(gh pr list --head "$branch" --state merged --json number --jq '.[0].number' 2>/dev/null)
|
|
335
|
-
if [ -n "$merged_pr" ]; then
|
|
336
|
-
echo "STALE_WT|$wt|$branch|PR#$merged_pr"
|
|
337
|
-
fi
|
|
338
|
-
fi
|
|
339
|
-
done
|
|
330
|
+
npx forgedock gc --worktrees --tmp --docker --processes # dry-run report
|
|
331
|
+
npx forgedock gc --worktrees --tmp --docker --processes --apply # reclaim
|
|
340
332
|
```
|
|
341
333
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
For each worktree with a merged PR:
|
|
345
|
-
```bash
|
|
346
|
-
git worktree remove "$WORKTREE_PATH" --force
|
|
347
|
-
git branch -D "$BRANCH_NAME" 2>/dev/null || true
|
|
348
|
-
echo "Removed: $WORKTREE_PATH ($BRANCH_NAME)"
|
|
349
|
-
```
|
|
334
|
+
It honours `forge.yaml` `hygiene:` TTLs and reclaims only what ForgeDock owns (stale lane ledgers, `lean-*`/`agent-*` worktrees that are clean and pushed, including unregistered dirs, `forgedock.owner=1` containers/volumes, `~/.forge/tmp` scratch, orphaned lane processes). Everything else is reported, never removed. Include its summary table in the report.
|
|
350
335
|
|
|
351
336
|
### 3C: Prune merged remote branches
|
|
352
337
|
|
package/commands/forge-stats.md
CHANGED
|
@@ -56,7 +56,8 @@ Commands are classified by role. Each role has a max line count — beyond this,
|
|
|
56
56
|
|
|
57
57
|
`scripts/check-spec-size.sh` (job `spec-size-budget` in `check-commands.yml`, PRs only) enforces a budget that is independent of the line-count `.baselines`; the baseline is refreshed on release via `/forge-stats baseline`. It compares bytes against the PR base:
|
|
58
58
|
|
|
59
|
-
-
|
|
59
|
+
- Every `commands/**/*.md` spec may not grow by more than 2% or 4 KB; a new spec may be at most 4 KB. Shrinking always passes.
|
|
60
|
+
- Per-file hard ceilings (`CEILINGS` in the script) cap a spec whatever its growth: `commands/issue.md` 15 KB (its procedure lives in `scripts/issue-create.sh`, #4164).
|
|
60
61
|
- No `commands/**` file may gain a copy of a canonical block (FORGE_ROOT bootstrap, TRUSTED_SCRIPT/HELPER_SCRIPT resolver, FENCE_BOOTSTRAP anchor); the per-file marker count may not rise. The shared helpers (`scripts/lib/forge-resolve.sh`, `scripts/lib/fence-bootstrap.sh`) are sourced through marker-free stubs, so a conversion reads as a net reduction.
|
|
61
62
|
- Exemption: the `spec-size-exempt` PR label only (needs triage rights; a PR-body marker is not honoured). The job summary lists the per-file delta and any waiver.
|
|
62
63
|
|
|
@@ -544,6 +544,13 @@ verification:
|
|
|
544
544
|
- "{SERVICE_PREFIX}"
|
|
545
545
|
```
|
|
546
546
|
|
|
547
|
+
**env** (optional, off by default; never auto-filled): lanes already read the repo's toolchain pins (`packageManager`, `.nvmrc`, `.node-version`, `.tool-versions`, `engines.node`) at lane setup. Write this section only when the user asks for it (`--interactive`). `toolchain` overrides the pinned package manager (or `off`); `setup` runs once per fresh lane worktree before the builder, e.g. a dev-DB reconcile script. See docs/CONFIG.md → `env`.
|
|
548
|
+
```yaml
|
|
549
|
+
env:
|
|
550
|
+
toolchain: "{PM}@{VERSION}"
|
|
551
|
+
setup: "{SETUP_COMMAND}"
|
|
552
|
+
```
|
|
553
|
+
|
|
547
554
|
### 5C: Section merge strategy
|
|
548
555
|
|
|
549
556
|
For each optional section that is ALREADY ACTIVE in forge.yaml (detected in Phase 1C):
|