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.
@@ -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 {
@@ -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 rereadNudgeFlags = (node = process.execPath, hook = REREAD_HOOK) => {
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 ["--settings", JSON.stringify({ hooks: { PostToolUse: [{ matcher: "Read|Bash", hooks }], PreToolUse: [{ matcher: "Read", hooks }] } })];
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
- export function makeLeanRunner({ runCli = runCliBackendAsync, probe = systemdRunAvailable } = {}) {
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
- ...(rereadNudge && !readOnly && !noTools ? rereadNudgeFlags() : [])]; // #4456: builder sessions only
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.0",
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"