forgedock 1.26.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 +13 -0
- package/bin/engine/phases.mjs +38 -2
- package/bin/engine/telemetry-seam.mjs +41 -6
- package/bin/engine/telemetry-seam.test.mjs +35 -0
- package/bin/engine-cli.mjs +616 -54
- package/bin/engine.mjs +13 -5
- package/bin/forgedock.mjs +169 -39
- package/bin/gc.mjs +580 -0
- package/bin/init-detect.mjs +68 -3
- package/bin/init-enrich.mjs +3 -0
- package/bin/init-repo.mjs +213 -0
- package/bin/journey.mjs +278 -43
- package/bin/lean/admission.mjs +33 -3
- package/bin/lean/batch.mjs +24 -5
- package/bin/lean/batch.test.mjs +54 -4
- package/bin/lean/checks.mjs +199 -0
- package/bin/lean/checks.test.mjs +183 -0
- package/bin/lean/conformance.mjs +235 -37
- package/bin/lean/conformance.test.mjs +143 -5
- 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 +178 -16
- 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 +877 -151
- package/bin/lean/lean.test.mjs +1798 -28
- package/bin/lean/ledger.mjs +92 -0
- package/bin/lean/ledger.test.mjs +100 -0
- package/bin/lean/limits.mjs +58 -0
- package/bin/lean/limits.test.mjs +150 -0
- package/bin/lean/orchestrate.mjs +321 -19
- package/bin/lean/prompts.mjs +125 -17
- package/bin/lean/refactor.mjs +276 -0
- package/bin/lean/refactor.test.mjs +359 -0
- package/bin/lean/render.mjs +25 -6
- 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 +124 -0
- package/bin/lean/verify.test.mjs +118 -0
- 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/adopt.md +3 -0
- package/commands/audit.md +5 -8
- 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 +125 -6
- package/commands/issue.md +89 -830
- package/commands/milestone.md +55 -6
- package/commands/orchestrate.md +10 -2
- package/commands/review-pr.md +2 -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 +227 -3
- package/docs/OUTCOME-CODES.md +11 -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 +321 -0
- package/scripts/calibration.mjs +116 -0
- package/scripts/check-devdocs-freshness.sh +125 -0
- package/scripts/check-devdocs-freshness.test.sh +130 -0
- 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 +16 -5
- package/scripts/ci-local/scoping.mjs +82 -0
- package/scripts/ci-local.test.sh +14 -2
- 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/northstar-docs.test.sh +68 -0
- package/scripts/refactor-verify.sh +368 -0
- package/scripts/refactor-verify.test.sh +202 -0
- package/scripts/review-prepare.sh +11 -9
- package/scripts/review-prepare.test.sh +14 -0
- package/scripts/structure-delta.sh +155 -0
- package/scripts/structure-delta.test.sh +136 -0
- 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/runner.mjs
CHANGED
|
@@ -359,6 +359,37 @@ export function resolveConfiguredDefaultModel(cwd) {
|
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Whether the owner approved the paid API backend as the `auto` fallback (#4257).
|
|
364
|
+
* Needs both forge.yaml `execution.agent_backend: api` in `cwd` (repo owner) and
|
|
365
|
+
* env `FORGEDOCK_ALLOW_API_BACKEND=1` (host owner), so a committed config edit
|
|
366
|
+
* alone cannot start billing an API key. Never throws.
|
|
367
|
+
*
|
|
368
|
+
* @param {{cwd?: string, env?: object}} [opts]
|
|
369
|
+
* @returns {boolean}
|
|
370
|
+
*/
|
|
371
|
+
export function isApiBackendApproved({ cwd = process.cwd(), env = process.env } = {}) {
|
|
372
|
+
if (String(env?.FORGEDOCK_ALLOW_API_BACKEND ?? "").trim() !== "1") return false;
|
|
373
|
+
try {
|
|
374
|
+
const forgeYamlPath = join(cwd, "forge.yaml");
|
|
375
|
+
if (!existsSync(forgeYamlPath)) return false;
|
|
376
|
+
return parseForgeYaml(readFileSync(forgeYamlPath, "utf-8"))?.execution?.agent_backend === "api";
|
|
377
|
+
} catch {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** #4257: typed error when `auto` finds no usable CLI and the API backend is not approved. */
|
|
383
|
+
function cliUnavailableError(detail = "The claude CLI is not available") {
|
|
384
|
+
const err = new Error(
|
|
385
|
+
`${detail}, and the API backend is never used implicitly. Install and authenticate the \`claude\` CLI, ` +
|
|
386
|
+
"or opt in to the API backend (forge.yaml `execution.agent_backend: api` plus FORGEDOCK_ALLOW_API_BACKEND=1), " +
|
|
387
|
+
"or pass --backend api.",
|
|
388
|
+
);
|
|
389
|
+
err.code = "CLI_BACKEND_UNAVAILABLE";
|
|
390
|
+
return err;
|
|
391
|
+
}
|
|
392
|
+
|
|
362
393
|
/**
|
|
363
394
|
* Resolve the CLI backend timeout from forge.yaml's
|
|
364
395
|
* `pipeline.cli_timeout_minutes` field, if it is a positive integer.
|
|
@@ -854,6 +885,7 @@ export function checkExecutionBackend({
|
|
|
854
885
|
apiKey = env.ANTHROPIC_API_KEY,
|
|
855
886
|
resolveCliFn = resolveClaudeCliBinary,
|
|
856
887
|
spawnImpl = spawnSync,
|
|
888
|
+
apiApproved = isApiBackendApproved({ cwd, env }),
|
|
857
889
|
sdkAvailableFn = () => {
|
|
858
890
|
try {
|
|
859
891
|
createRequire(import.meta.url).resolve("@anthropic-ai/sdk");
|
|
@@ -884,7 +916,8 @@ export function checkExecutionBackend({
|
|
|
884
916
|
return { ready: true, backend: "cli", reason: "resolved-cli" };
|
|
885
917
|
}
|
|
886
918
|
}
|
|
887
|
-
|
|
919
|
+
// #4257: auto never crosses to the paid API backend without the owner's opt-in.
|
|
920
|
+
if (requested === "cli" || !apiApproved) {
|
|
888
921
|
return { ready: false, backend: "cli", reason: "cli-unavailable" };
|
|
889
922
|
}
|
|
890
923
|
}
|
|
@@ -905,7 +938,8 @@ export function checkExecutionBackend({
|
|
|
905
938
|
* CLI and prefer it when present — but diverge on (a) which override
|
|
906
939
|
* values are valid and whether an invalid one throws or is silently
|
|
907
940
|
* ignored, and (b) what to return when the CLI probe fails (`resolveBackend`
|
|
908
|
-
*
|
|
941
|
+
* falls back to `"api"` only with the owner's opt-in, else `"cli"` (#4257);
|
|
942
|
+
* `resolveEnrichBackend` falls back to `"api"`
|
|
909
943
|
* only if `ANTHROPIC_API_KEY` is set, else `"none"`). Rather than force
|
|
910
944
|
* those into one shape, this shared helper implements exactly the common
|
|
911
945
|
* two steps and defers to a `cliFallback` callback for the diverging tail,
|
|
@@ -954,24 +988,27 @@ export function resolveBackendLadder({
|
|
|
954
988
|
* 2. `"auto"` (the default) probes `isClaudeCliAvailable()`: prefers the
|
|
955
989
|
* CLI backend when a working `claude` install is detected (reuses
|
|
956
990
|
* whatever credentials the CLI already has — Pro/Max OAuth or a
|
|
957
|
-
* CLI-managed key — with no ANTHROPIC_API_KEY required)
|
|
958
|
-
* falls back to the API backend
|
|
959
|
-
*
|
|
991
|
+
* CLI-managed key — with no ANTHROPIC_API_KEY required). Without a CLI it
|
|
992
|
+
* falls back to the API backend ONLY when the owner opted in
|
|
993
|
+
* (`isApiBackendApproved()`, #4257); otherwise it returns `"cli"` and
|
|
994
|
+
* `runCommand()` fails with a typed `CLI_BACKEND_UNAVAILABLE`. The API
|
|
995
|
+
* backend is never the implicit default.
|
|
960
996
|
*
|
|
961
997
|
* Delegates its shared "override-or-probe-CLI" shape to
|
|
962
998
|
* `resolveBackendLadder()` (issue #2026) — this function retains its own
|
|
963
999
|
* `VALID_BACKENDS` throw-on-invalid check (NOT shared; see that helper's
|
|
964
|
-
* doc comment) and its own
|
|
965
|
-
*
|
|
1000
|
+
* doc comment) and its own opt-in-gated fallback (`"api"` only when
|
|
1001
|
+
* approved — the API key itself is validated lazily downstream, only
|
|
966
1002
|
* if/when the api backend is actually selected).
|
|
967
1003
|
*
|
|
968
1004
|
* @param {object} [opts]
|
|
969
1005
|
* @param {string} [opts.requested] - "cli" | "api" | "auto" (default "auto").
|
|
970
1006
|
* @param {string} [opts.cwd] - Working directory for the CLI probe.
|
|
971
1007
|
* @param {object} [opts.env] - Runtime environment used for capability checks.
|
|
1008
|
+
* @param {Function} [opts.isCliAvailableFn] - Injectable CLI-presence probe (test seam).
|
|
972
1009
|
* @returns {"cli"|"api"}
|
|
973
1010
|
*/
|
|
974
|
-
export function resolveBackend({ requested = "auto", cwd = process.cwd(), env = process.env } = {}) {
|
|
1011
|
+
export function resolveBackend({ requested = "auto", cwd = process.cwd(), env = process.env, isCliAvailableFn = isClaudeCliAvailable } = {}) {
|
|
975
1012
|
if (!VALID_BACKENDS.has(requested)) {
|
|
976
1013
|
throw new Error(
|
|
977
1014
|
`Invalid backend "${requested}". Must be one of: ${[...VALID_BACKENDS].join(", ")}.`,
|
|
@@ -982,7 +1019,8 @@ export function resolveBackend({ requested = "auto", cwd = process.cwd(), env =
|
|
|
982
1019
|
override: requested === "auto" ? undefined : requested,
|
|
983
1020
|
validOverrides: new Set(["cli", "api"]),
|
|
984
1021
|
cwd,
|
|
985
|
-
|
|
1022
|
+
isCliAvailableFn,
|
|
1023
|
+
cliFallback: () => (isApiBackendApproved({ cwd, env }) ? "api" : "cli"),
|
|
986
1024
|
});
|
|
987
1025
|
}
|
|
988
1026
|
|
|
@@ -2789,7 +2827,9 @@ export function renderSummaryCard(ctx) {
|
|
|
2789
2827
|
* @param {string} [opts.backend] - "cli" | "api" | "auto" (default
|
|
2790
2828
|
* "auto"). Resolution order when omitted: $FORGEDOCK_BACKEND env > "auto".
|
|
2791
2829
|
* "auto" prefers the local Claude Code CLI when detected (no
|
|
2792
|
-
* ANTHROPIC_API_KEY needed)
|
|
2830
|
+
* ANTHROPIC_API_KEY needed); without a CLI it uses the API backend only
|
|
2831
|
+
* with the owner's opt-in (`isApiBackendApproved()`, #4257), else it fails
|
|
2832
|
+
* with a typed CLI_BACKEND_UNAVAILABLE — api is never the default. This
|
|
2793
2833
|
* precedence is intentional/shipped (issue #2003's accepted acceptance
|
|
2794
2834
|
* criteria) and is NOT changed by issue #2020 — that issue only adds a
|
|
2795
2835
|
* discoverability notice (below) for the case where the ladder silently
|
|
@@ -2840,6 +2880,7 @@ export async function runCommand(opts = {}) {
|
|
|
2840
2880
|
|
|
2841
2881
|
if (resolvedBackend === "cli" && backend === "auto") {
|
|
2842
2882
|
const readiness = checkExecutionBackend({ requested: "auto", cwd, apiKey, env: runtimeEnv });
|
|
2883
|
+
// #4257: readiness only reports api when the owner approved it.
|
|
2843
2884
|
if (readiness.backend === "api") resolvedBackend = "api";
|
|
2844
2885
|
}
|
|
2845
2886
|
|
|
@@ -2938,6 +2979,8 @@ export async function runCommand(opts = {}) {
|
|
|
2938
2979
|
sessionFlags = buildMinimalSessionFlags(commandName, caps);
|
|
2939
2980
|
}
|
|
2940
2981
|
if (!resolvedBin && backend === "auto") {
|
|
2982
|
+
// #4257: never cross to the paid API backend without the owner's opt-in.
|
|
2983
|
+
if (!isApiBackendApproved({ cwd, env: runtimeEnv })) throw cliUnavailableError();
|
|
2941
2984
|
resolvedBackend = "api";
|
|
2942
2985
|
} else if (!resolvedBin) {
|
|
2943
2986
|
const err = new Error("The configured claude CLI path disappeared before invocation.");
|
|
@@ -163,7 +163,7 @@ async function* listIssuePages(gh, repo, startSince) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
const meta = (it) => ({ number: it.number, isPr: Boolean(it.pull_request), createdAt: it.created_at ?? null, closedAt: it.closed_at ?? null,
|
|
166
|
-
updatedAt: it.updated_at ?? null, labels: (it.labels ?? []).map((l) => (typeof l === "string" ? l : l.name)).filter(Boolean), authorType: it.user?.type ?? null });
|
|
166
|
+
updatedAt: it.updated_at ?? null, milestone: it.milestone?.title ?? null, labels: (it.labels ?? []).map((l) => (typeof l === "string" ? l : l.name)).filter(Boolean), authorType: it.user?.type ?? null });
|
|
167
167
|
|
|
168
168
|
const CHECKPOINT_EVERY = 1000;
|
|
169
169
|
const RATE_CHECK_EVERY = 50;
|
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/adopt.md
CHANGED
|
@@ -421,6 +421,9 @@ Where `{reason}` is a brief signal: "confirmed bug + file refs", "P1 + acceptanc
|
|
|
421
421
|
|
|
422
422
|
4. To re-run adoption on newly added issues:
|
|
423
423
|
\`/adopt\` (already-labeled issues are skipped automatically)
|
|
424
|
+
|
|
425
|
+
{If the repo's boundary config contains an allow-all rule (check as in `/forgedock-init` 2G: eslint-plugin-boundaries `from: [X], allow: ['*']`, dependency-cruiser with no `forbidden` rules, Nx `onlyDependOnLibsWithTags: ['*']`), add once:}
|
|
426
|
+
5. Your boundary config allows every package to import every package ({FILE}: {RULE}). Consider tightening it per package; core work-on only reports import growth along allowed edges.
|
|
424
427
|
```
|
|
425
428
|
|
|
426
429
|
---
|
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
|
|
@@ -261,14 +263,9 @@ Classify where the breakdown occurred. Use these categories:
|
|
|
261
263
|
- **Root cause**: The single most impactful failure point. If THIS had worked, the problem wouldn't have reached production.
|
|
262
264
|
- **Contributing factors**: Other pipeline links that could have caught it as a safety net but didn't.
|
|
263
265
|
|
|
264
|
-
### 3D: Cross-reference with
|
|
265
|
-
|
|
266
|
-
Before proposing a fix, check which layer it belongs to (from CLAUDE.md):
|
|
266
|
+
### 3D: Cross-reference with the hardening layers
|
|
267
267
|
|
|
268
|
-
|
|
269
|
-
- **Review agents** → semantic reasoning, library API contracts, cross-service interactions
|
|
270
|
-
- **Domain detection** → broad category matching, not per-bug-instance
|
|
271
|
-
- **Builder rules** → prevention at write time, simple memorable rules
|
|
268
|
+
Before proposing a fix, pick its layer from `NORTHSTAR.md` § Where a fix belongs: the earliest, cheapest layer that would have stopped it (issue spec/plan rule → builder rule → deterministic check → domain detection → review prompt line → new agent, last resort).
|
|
272
269
|
|
|
273
270
|
**Do NOT propose a quality-gate grep check for a reasoning failure. Do NOT propose a review-agent prompt change for a pattern grep could catch.**
|
|
274
271
|
|
|
@@ -368,7 +365,7 @@ Files that need changes:
|
|
|
368
365
|
- If tightening a review agent: describe the reasoning step to add
|
|
369
366
|
- If adding context/architect guidance: describe the lookup or trace step}
|
|
370
367
|
|
|
371
|
-
**Hardening layer justification** (per
|
|
368
|
+
**Hardening layer justification** (per `NORTHSTAR.md` § Where a fix belongs):
|
|
372
369
|
{Explain why this fix belongs in the proposed layer and not another}
|
|
373
370
|
|
|
374
371
|
### Related Prior Work
|
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
|
|