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.
Files changed (127) hide show
  1. package/README.md +4 -1
  2. package/bin/bootstrap.mjs +55 -0
  3. package/bin/engine/outcome-codes.mjs +10 -0
  4. package/bin/engine/telemetry-seam.mjs +37 -8
  5. package/bin/engine/telemetry-seam.test.mjs +31 -0
  6. package/bin/engine-cli.mjs +542 -38
  7. package/bin/engine.mjs +8 -4
  8. package/bin/forgedock.mjs +75 -9
  9. package/bin/gc.mjs +580 -0
  10. package/bin/lean/admission.mjs +33 -3
  11. package/bin/lean/batch.mjs +22 -4
  12. package/bin/lean/batch.test.mjs +5 -3
  13. package/bin/lean/checks.mjs +199 -0
  14. package/bin/lean/checks.test.mjs +183 -0
  15. package/bin/lean/conformance.mjs +190 -58
  16. package/bin/lean/conformance.test.mjs +116 -7
  17. package/bin/lean/credentials.mjs +29 -0
  18. package/bin/lean/depcache.mjs +312 -0
  19. package/bin/lean/depcache.test.mjs +298 -0
  20. package/bin/lean/drift.mjs +198 -0
  21. package/bin/lean/drift.test.mjs +223 -0
  22. package/bin/lean/effects.mjs +85 -8
  23. package/bin/lean/execution.e2e.test.mjs +81 -0
  24. package/bin/lean/execution.mjs +431 -0
  25. package/bin/lean/execution.test.mjs +583 -0
  26. package/bin/lean/heavy.mjs +289 -0
  27. package/bin/lean/heavy.test.mjs +207 -0
  28. package/bin/lean/lean.mjs +572 -139
  29. package/bin/lean/lean.test.mjs +1193 -31
  30. package/bin/lean/ledger.mjs +27 -2
  31. package/bin/lean/ledger.test.mjs +36 -1
  32. package/bin/lean/limits.mjs +58 -0
  33. package/bin/lean/limits.test.mjs +150 -0
  34. package/bin/lean/orchestrate.mjs +312 -19
  35. package/bin/lean/prompts.mjs +89 -15
  36. package/bin/lean/refactor.mjs +276 -0
  37. package/bin/lean/refactor.test.mjs +359 -0
  38. package/bin/lean/render.mjs +24 -5
  39. package/bin/lean/review-trigger.mjs +134 -0
  40. package/bin/lean/review-trigger.test.mjs +104 -0
  41. package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
  42. package/bin/lean/sandbox-remote.mjs +142 -0
  43. package/bin/lean/sched.mjs +221 -0
  44. package/bin/lean/sched.test.mjs +310 -0
  45. package/bin/lean/schedd.mjs +247 -0
  46. package/bin/lean/schedd.test.mjs +180 -0
  47. package/bin/lean/toolchain.mjs +228 -0
  48. package/bin/lean/toolchain.test.mjs +154 -0
  49. package/bin/lean/verify.mjs +20 -9
  50. package/bin/lean/verify.test.mjs +26 -3
  51. package/bin/lean/worktree.mjs +69 -0
  52. package/bin/lean/worktree.test.mjs +141 -0
  53. package/bin/runner.mjs +53 -10
  54. package/bin/stats/github-source.mjs +1 -1
  55. package/bin/stats/index.mjs +14 -2
  56. package/bin/stats/query.mjs +72 -0
  57. package/bin/stats/stats.test.mjs +54 -0
  58. package/commands/audit.md +2 -0
  59. package/commands/autopilot.md +2 -0
  60. package/commands/bootstrap.md +35 -0
  61. package/commands/cleanup.md +7 -22
  62. package/commands/forge-stats.md +2 -1
  63. package/commands/forgedock-init.md +7 -0
  64. package/commands/issue.md +89 -830
  65. package/commands/milestone.md +55 -6
  66. package/commands/orchestrate.md +10 -2
  67. package/commands/upgrade-deps.md +3 -3
  68. package/commands/work-on/build/validate.md +4 -0
  69. package/commands/work-on/close.md +2 -0
  70. package/commands/work-on/decompose.md +24 -6
  71. package/docs/CI.md +3 -3
  72. package/docs/CONFIG.md +177 -2
  73. package/docs/OUTCOME-CODES.md +8 -0
  74. package/docs/TELEMETRY.md +1 -1
  75. package/docs/site/command-reference.md +22 -0
  76. package/docs/site/getting-started.md +2 -0
  77. package/docs/site/start-a-new-project.md +327 -0
  78. package/package.json +1 -1
  79. package/scripts/affected-files-check.sh +91 -0
  80. package/scripts/affected-files-check.test.sh +80 -0
  81. package/scripts/bootstrap-create.mjs +393 -0
  82. package/scripts/bootstrap-validate.mjs +36 -2
  83. package/scripts/calibration.mjs +116 -0
  84. package/scripts/check-devdocs-freshness.sh +9 -4
  85. package/scripts/check-devdocs-freshness.test.sh +11 -1
  86. package/scripts/check-spec-markers.sh +3 -0
  87. package/scripts/check-spec-size.sh +29 -11
  88. package/scripts/check-spec-size.test.sh +27 -4
  89. package/scripts/ci-local/lib.mjs +8 -1
  90. package/scripts/ci-local/parallelism.test.mjs +27 -0
  91. package/scripts/ci-local/run.mjs +3 -3
  92. package/scripts/ci-local/scoping.mjs +82 -0
  93. package/scripts/ci-local.test.sh +1 -1
  94. package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
  95. package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
  96. package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
  97. package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
  98. package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
  99. package/scripts/fixtures/symbol-index/py/sample.py +41 -0
  100. package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
  101. package/scripts/fixtures/symbol-move/gen_big.py +201 -0
  102. package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
  103. package/scripts/forge-root.test.sh +1 -1
  104. package/scripts/hotspots.sh +100 -0
  105. package/scripts/hotspots.test.sh +101 -0
  106. package/scripts/issue-create.sh +197 -0
  107. package/scripts/issue-create.test.sh +166 -0
  108. package/scripts/maintenance-scan.sh +185 -0
  109. package/scripts/maintenance-scan.test.sh +89 -0
  110. package/scripts/milestone-drift.sh +206 -0
  111. package/scripts/milestone-drift.test.sh +120 -0
  112. package/scripts/refactor-verify.sh +368 -0
  113. package/scripts/refactor-verify.test.sh +202 -0
  114. package/scripts/structure-delta.sh +10 -5
  115. package/scripts/structure-delta.test.sh +3 -2
  116. package/scripts/symbol-index.mjs +253 -0
  117. package/scripts/symbol-index.py +354 -0
  118. package/scripts/symbol-index.test.sh +119 -0
  119. package/scripts/symbol-move-ts.test.sh +161 -0
  120. package/scripts/symbol-move.mjs +379 -0
  121. package/scripts/symbol-move.py +383 -0
  122. package/scripts/symbol-move.test.sh +168 -0
  123. package/templates/bootstrap/interview.md +34 -0
  124. package/templates/bootstrap/issue-body.md +42 -0
  125. package/templates/bootstrap/needs-human-issue.md +45 -0
  126. package/templates/bootstrap/stack-profiles.md +34 -0
  127. package/templates/bootstrap/technical-spike.md +75 -0
@@ -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
+ }
@@ -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
@@ -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.
@@ -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` | Prune worktrees and remote branches for merged PRs |
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: Identify worktrees with merged PRs
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
- # For each worktree (excluding the main one), check if its branch has a merged PR
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
- ### 3B: Remove stale worktrees and local branches
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
 
@@ -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
- - `review-pr.md`, `review-pr-staging.md`, `work-on/remediate.md`, `work-on/review.md`, `work-on.md` may not grow by more than 2% or 4 KB. Shrinking always passes.
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):