forgedock 1.38.0 → 1.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine-cli.mjs +7 -0
- package/bin/lean/effects.mjs +28 -10
- package/bin/lean/explore.test.mjs +1 -1
- package/docs/CONFIG.md +1 -1
- package/package.json +1 -1
package/bin/engine-cli.mjs
CHANGED
|
@@ -288,6 +288,13 @@ export async function readPostDeployMinAge(cwd = process.cwd()) {
|
|
|
288
288
|
export function readMilestoneCloseOn(cwd = process.cwd()) {
|
|
289
289
|
try { return String(parseForgeYaml(readFileSync(join(cwd, "forge.yaml"), "utf8"))?.milestones?.close_on ?? "").trim().toLowerCase() === "staging" ? "staging" : "default"; } catch { return "default"; }
|
|
290
290
|
}
|
|
291
|
+
/** forge.yaml `agents.subagent_model` (#4843): the default model of subagents in sessions ForgeDock launches; null when unset or invalid. */
|
|
292
|
+
export function readSubagentModel(cwd = process.cwd()) {
|
|
293
|
+
try {
|
|
294
|
+
const v = String(parseForgeYaml(readFileSync(join(cwd, "forge.yaml"), "utf8"))?.agents?.subagent_model ?? "").trim();
|
|
295
|
+
return /^[A-Za-z0-9._:[\]-]+$/.test(v) ? v : null;
|
|
296
|
+
} catch { return null; }
|
|
297
|
+
}
|
|
291
298
|
/** forge.yaml `lean.explore` (on|off) and `lean.explore_model` (#4391): the builder's explorer subagent; undefined when neither is set or valid. */
|
|
292
299
|
export function readExplore(cwd = process.cwd()) {
|
|
293
300
|
try {
|
package/bin/lean/effects.mjs
CHANGED
|
@@ -8,8 +8,8 @@ import { homedir, hostname, freemem, loadavg, cpus, tmpdir } from "node:os";
|
|
|
8
8
|
import { promises as fsp } from "node:fs";
|
|
9
9
|
import { dirname, join, resolve, sep } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { runCliBackendAsync, resolveClaudeCliBinary, getClaudeCliCapabilities, buildLeanSessionFlags, buildReadOnlyToolFlags } from "../runner.mjs";
|
|
12
|
-
import { makeIo } from "../engine-cli.mjs";
|
|
11
|
+
import { runCliBackendAsync, resolveClaudeCliBinary, getClaudeCliCapabilities, buildLeanSessionFlags, buildReadOnlyToolFlags, sanitizeCliModel } from "../runner.mjs";
|
|
12
|
+
import { makeIo, readSubagentModel } from "../engine-cli.mjs";
|
|
13
13
|
import { makeProjector } from "../engine/projector.mjs";
|
|
14
14
|
import { makeLedger, countLiveLanes, ledgerRoot } from "./ledger.mjs";
|
|
15
15
|
import { makeDepCache } from "./depcache.mjs";
|
|
@@ -76,21 +76,39 @@ export const noToolFlags = (caps) => (caps?.strictMcp ? ["--tools", ""]
|
|
|
76
76
|
|
|
77
77
|
/** #4456: `--settings` registering the re-read nudge (bin/hooks/reread-nudge.mjs) as a PostToolUse hook on Read and Bash. */
|
|
78
78
|
export const REREAD_HOOK = fileURLToPath(new URL("../hooks/reread-nudge.mjs", import.meta.url));
|
|
79
|
-
export const
|
|
79
|
+
export const rereadHooks = (node = process.execPath, hook = REREAD_HOOK) => {
|
|
80
80
|
const hooks = [{ type: "command", command: `${JSON.stringify(node)} ${JSON.stringify(hook)}` }];
|
|
81
81
|
// #4580: the same script denies a full-file Read of an unchanged path already read twice (PreToolUse)
|
|
82
|
-
return
|
|
82
|
+
return { PostToolUse: [{ matcher: "Read|Bash", hooks }], PreToolUse: [{ matcher: "Read", hooks }] };
|
|
83
83
|
};
|
|
84
|
+
export const rereadNudgeFlags = (node = process.execPath, hook = REREAD_HOOK) => ["--settings", JSON.stringify({ hooks: rereadHooks(node, hook) })];
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
/**
|
|
87
|
+
* #4843: ForgeDock owns the subagent model of the sessions it launches. An operator settings.json `env` such as
|
|
88
|
+
* CLAUDE_CODE_SUBAGENT_MODEL=sonnet + _FORCE=1 would otherwise override every subagent, the #4391 Haiku explorer
|
|
89
|
+
* included, and a child env var cannot undo it (settings env wins); a `--settings` flag does. FORCE=0 lets a
|
|
90
|
+
* ForgeDock-defined agent's own `model` win. CLAUDE_CODE_SUBAGENT_MODEL (the default for agents without one) is
|
|
91
|
+
* set only from forge.yaml `agents.subagent_model`; unset leaves the operator's own default alone (#4849).
|
|
92
|
+
*/
|
|
93
|
+
export const ownedModelEnv = (subagentModel = null) => {
|
|
94
|
+
const m = sanitizeCliModel(subagentModel);
|
|
95
|
+
return { ...(m ? { CLAUDE_CODE_SUBAGENT_MODEL: m } : {}), CLAUDE_CODE_SUBAGENT_MODEL_FORCE: "0" };
|
|
96
|
+
};
|
|
97
|
+
/** The one `--settings` of a lean session: the owned model env, plus the re-read hooks for builder sessions (#4456). */
|
|
98
|
+
export const leanSettingsFlags = ({ subagentModel = null, reread = false } = {}) =>
|
|
99
|
+
["--settings", JSON.stringify({ env: ownedModelEnv(subagentModel), ...(reread ? { hooks: rereadHooks() } : {}) })];
|
|
100
|
+
|
|
101
|
+
export function makeLeanRunner({ runCli = runCliBackendAsync, probe = systemdRunAvailable, subagentModel = null } = {}) {
|
|
86
102
|
return async ({ role, issue, cwd, model, resume, system, prompt, readOnly = false, noTools = false, rereadNudge = false, maxTurns = null, agents = null, env = null, idleMs = 0, maxMs = 0, laneLimits = null, control = null, onSpawn = null, contextMax = 0, turnMax = 0, busy = null, busyMs = 0, onIdleExtend = null }) => {
|
|
87
103
|
const bin = resolveClaudeCliBinary(cwd) || "claude";
|
|
88
104
|
const caps = getClaudeCliCapabilities(cwd);
|
|
89
105
|
// #4391: custom subagents (the explorer) for the builder session; null leaves argv unchanged
|
|
90
|
-
const flags = [...buildLeanSessionFlags(caps), ...(noTools ? noToolFlags(caps) : readOnly ? buildReadOnlyToolFlags(caps) : []), ...(agents ? ["--agents", JSON.stringify(agents)] : [])
|
|
91
|
-
|
|
106
|
+
const flags = [...buildLeanSessionFlags(caps), ...(noTools ? noToolFlags(caps) : readOnly ? buildReadOnlyToolFlags(caps) : []), ...(agents ? ["--agents", JSON.stringify(agents)] : [])];
|
|
107
|
+
// #4843 owned models, in extraArgs so FORGEDOCK_FULL_SESSION=1 (which drops sessionFlags) keeps them (#4848 BUG-1);
|
|
108
|
+
// #4456 re-read hooks: builder sessions only, and like the other session flags not in a full session
|
|
109
|
+
const settings = leanSettingsFlags({ subagentModel, reread: rereadNudge && !readOnly && !noTools && process.env.FORGEDOCK_FULL_SESSION !== "1" });
|
|
92
110
|
const r = await runCli({ spec: { name: `core-${role}` }, userMessage: prompt, systemPrompt: resume ? "" : system,
|
|
93
|
-
args: [String(issue)], cwd, bin, model, maxTurns: caps?.maxTurns ? maxTurns : null, env, sessionFlags: flags, extraArgs: resume ? ["--resume", resume] : [],
|
|
111
|
+
args: [String(issue)], cwd, bin, model, maxTurns: caps?.maxTurns ? maxTurns : null, env, sessionFlags: flags, extraArgs: [...(resume ? ["--resume", resume] : []), ...settings],
|
|
94
112
|
logger: { log() {} },
|
|
95
113
|
idleMs, maxMs, stream: idleMs > 0, control, onSpawn, busy, busyMs, onIdleExtend, ...(contextMax > 0 ? { contextMax } : {}), ...(turnMax > 0 ? { turnMax } : {}), wrap: probe() ? (b, a) => wrapLaneLimits(b, a, laneLimits) : null });
|
|
96
114
|
// #4395: a resumed session's first turn carries the prior history, so only a fresh session reports a baseline.
|
|
@@ -275,12 +293,12 @@ export function makeGhCache({ root = null, tmp = tmpdir() } = {}) {
|
|
|
275
293
|
};
|
|
276
294
|
}
|
|
277
295
|
|
|
278
|
-
export function makeLeanIo({ repo }) {
|
|
296
|
+
export function makeLeanIo({ repo, subagentModel = readSubagentModel() }) {
|
|
279
297
|
const base = makeIo();
|
|
280
298
|
const run = (bin) => async (args, opts = {}) => (await pexec(bin, args, { cwd: opts.cwd, maxBuffer: 100 * 1024 * 1024, timeout: 120000, ...(opts.env ? { env: { ...process.env, ...opts.env } } : {}) })).stdout;
|
|
281
299
|
return {
|
|
282
300
|
ghCache: (root) => makeGhCache({ root }),
|
|
283
|
-
gh: run("gh"), git: run("git"), docker: run("docker"), killGroup: async (pid, marker) => killLedgerPid(pid, marker), ledger: (opts) => makeLedger(opts), runTmp: makeRunTmp(), host: makeHostProbe(), heavy: makeHeavyIo({ host: makeHostProbe() }), projector: makeProjector({ gh: base.gh }), runner: makeLeanRunner(), waitCiGreen: base.waitCiGreen,
|
|
301
|
+
gh: run("gh"), git: run("git"), docker: run("docker"), killGroup: async (pid, marker) => killLedgerPid(pid, marker), ledger: (opts) => makeLedger(opts), runTmp: makeRunTmp(), host: makeHostProbe(), heavy: makeHeavyIo({ host: makeHostProbe() }), projector: makeProjector({ gh: base.gh }), runner: makeLeanRunner({ subagentModel }), waitCiGreen: base.waitCiGreen,
|
|
284
302
|
/** Shared dependency cache (#4220): the repo's own package manager, argv only, bounded by the cache's timeout. */
|
|
285
303
|
deps: makeDepCache({ exec: async (cmd, args, { cwd, env, timeoutMs } = {}) => String((await pexec(cmd, args, { cwd, env: { ...process.env, ...(env ?? {}) }, timeout: timeoutMs ?? 0, maxBuffer: 100 * 1024 * 1024 })).stdout) }),
|
|
286
304
|
/** Names of the checks the base branch requires: classic protection plus rulesets (#4158). [] when neither has any. */
|
|
@@ -42,7 +42,7 @@ describe("explorer subagent config (#4391)", () => {
|
|
|
42
42
|
const [plain, withAgents] = seen;
|
|
43
43
|
assert.ok(!plain.includes("--agents"));
|
|
44
44
|
const i = withAgents.indexOf("--agents");
|
|
45
|
-
assert.deepEqual(withAgents.slice(0, i), plain);
|
|
45
|
+
assert.deepEqual([...withAgents.slice(0, i), ...withAgents.slice(i + 2)], plain, "only the --agents pair is added");
|
|
46
46
|
assert.deepEqual(JSON.parse(withAgents[i + 1]), agents);
|
|
47
47
|
});
|
|
48
48
|
|
package/docs/CONFIG.md
CHANGED
|
@@ -148,7 +148,7 @@ agents:
|
|
|
148
148
|
| Field | Type | Required | Default | Description |
|
|
149
149
|
|-------|------|----------|---------|-------------|
|
|
150
150
|
| `default_model` | string | No | `"sonnet"` | Short alias — `"sonnet"`, `"opus"`, or `"haiku"` — the same values the Agent/Task tool's `model` parameter accepts. This is the only form that works for interactive command-spec sub-agent spawning. |
|
|
151
|
-
| `subagent_model` | string | No | `default_model`, else `"sonnet"` | Short alias, same accepted values as `default_model`. Overrides the model used for child agents spawned internally by a top-level command (see above). Omit to inherit `default_model`. |
|
|
151
|
+
| `subagent_model` | string | No | `default_model`, else `"sonnet"` | Short alias, same accepted values as `default_model`. Overrides the model used for child agents spawned internally by a top-level command (see above). Omit to inherit `default_model`. In engine lanes (core work-on), only this field is read, with no `default_model` fallback. When set, it is the default model of subagents that have no model of their own. When unset, the operator's own `CLAUDE_CODE_SUBAGENT_MODEL` (if any) stays in effect. In every lane session, ForgeDock-defined agents such as the explorer keep their own model: an ambient `CLAUDE_CODE_SUBAGENT_MODEL_FORCE` in the operator's settings is turned off (#4843, #4849). **Upgrade note (1.38.1):** a `_FORCE=1` cap in your settings.json no longer applies to ForgeDock-defined agents in lanes. To pin model-less subagents, set this field. |
|
|
152
152
|
|
|
153
153
|
**Resolution order** (highest precedence first):
|
|
154
154
|
1. `--model <id>` — CLI flag, `npx forgedock run` only (headless; main agent only).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.38.
|
|
3
|
+
"version": "1.38.1",
|
|
4
4
|
"description": "Autonomous development pipeline for coding agents. GitHub as institutional memory, with native Claude Code and OpenCode entrypoints.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|