Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ or resume, it belongs in Pi/pi-subagents or the provider layer rather than Forge

## Model policy boundary

ForgeFlow registers stable logical roles as Pi virtual models. Their physical model mapping is read from operator policy rather than hard-coded into workflows or subagent definitions. New user/direct requests resolve the current mapping, while continuation/retry requests stay on the physical model already handling the turn to preserve prompt-cache and reasoning-signature continuity.
ForgeFlow registers stable logical roles as Pi virtual models. Their physical model mapping is read from operator policy rather than hard-coded into workflows or subagent definitions. For native Pi children, the parent session registers a required child-only extension that installs only those virtual-model definitions before child model resolution; it does not duplicate ForgeFlow's prompt policy or workflow resources in the child. New user/direct requests resolve the current mapping, while continuation/retry requests stay on the physical model already handling the turn to preserve prompt-cache and reasoning-signature continuity.

Project-local `.pi/forgeflow-models.json` is considered only when Pi reports the project trusted. User-level policy under `~/.pi/forgeflow-models.json` remains available for untrusted projects. `FORGEFLOW_MODEL_POLICY` is an explicit operator override.

Expand Down
2 changes: 1 addition & 1 deletion docs/model-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The pinned `pi-subagents@0.75.0` includes the child-runtime fixes required for P

The parent Pi session can select `forgeflow/planner` when the parent is acting as the planning/orchestration role. ForgeFlow's trusted `forgeflow.review` and `forgeflow.accept` workflows explicitly request `forgeflow/reviewer`, so the workflow owns the stable reviewer role while operator policy remains free to change the physical reviewer model without editing workflow code.

`pi-subagents@0.75.0` contains the upstream child-runtime fixes for queued virtual-model registration and logical-selection verification (#2636 and #2638). ForgeFlow therefore does not carry a compatibility shim for virtual child models.
`pi-subagents@0.75.0` contains the upstream child-runtime fixes for queued virtual-model registration and logical-selection verification (#2636 and #2638). ForgeFlow additionally registers a required child-only extension that does nothing except register the five `forgeflow/*` virtual models. Native Pi children therefore receive the logical model registry before model resolution without loading a second copy of ForgeFlow's prompt policy or trusted workflow resources. ForgeFlow does not carry a routing shim for virtual child models.

## Policy file

Expand Down
5 changes: 5 additions & 0 deletions extension/child-model-policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { registerForgeFlowVirtualModels } from "./model-policy.js";

export default function registerForgeFlowChildModelPolicy(pi) {
registerForgeFlowVirtualModels(pi);
}
20 changes: 20 additions & 0 deletions extension/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { fileURLToPath } from "node:url";

import { registerRequiredChildExtensions } from "pi-subagents/required-child-extensions";
import { registerWorkflowResource } from "pi-subagents/workflow-resources";
import { renderPreflight } from "./invariants.js";
import { createAcceptanceWorkflowDefinition } from "./acceptance-workflow.js";
import { createReviewWorkflowDefinition } from "./review-workflow.js";
import { registerForgeFlowVirtualModels } from "./model-policy.js";

const CHILD_MODEL_POLICY_EXTENSION = fileURLToPath(
new URL("./child-model-policy.js", import.meta.url)
);

const CORE_POLICY = [
"ForgeFlow is a thin engineering-governance extension for Pi; Pi and pi-subagents own agent execution, sessions, delegation, worktrees, missions, schedules, and resume.",
"ForgeFlow may define stable logical model roles, but Pi owns virtual-model dispatch and the provider layer owns channel, credential, quota, and transport routing.",
Expand All @@ -22,6 +29,7 @@ export default function registerForgeFlow(pi) {

let reviewRegistration;
let acceptanceRegistration;
let childModelPolicyRegistration;

pi.on("before_agent_start", (event) => {
event.systemPromptOptions.sections.forgeflow_policy = buildForgeFlowPromptSection(event.prompt);
Expand All @@ -30,6 +38,7 @@ export default function registerForgeFlow(pi) {
pi.on("session_start", (_event, ctx) => {
reviewRegistration?.dispose();
acceptanceRegistration?.dispose();
childModelPolicyRegistration?.dispose();

const sessionId = ctx.sessionManager.getSessionId();
const sessionCwd = ctx.cwd;
Expand All @@ -41,12 +50,23 @@ export default function registerForgeFlow(pi) {
sessionId,
definition: createAcceptanceWorkflowDefinition(sessionCwd)
});
childModelPolicyRegistration = registerRequiredChildExtensions({
sessionId,
extensions: [
{
id: "forgeflow-model-policy",
path: CHILD_MODEL_POLICY_EXTENSION
}
]
});
});

pi.on("session_shutdown", () => {
reviewRegistration?.dispose();
acceptanceRegistration?.dispose();
childModelPolicyRegistration?.dispose();
reviewRegistration = undefined;
acceptanceRegistration = undefined;
childModelPolicyRegistration = undefined;
});
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@
},
"scripts": {
"test": "node --test tests/*.test.js",
"check": "npm test && node --check extension/index.js && node --check extension/model-policy.js && node --check extension/invariants.js && node --check extension/review-workflow.js && node --check extension/review-verdict.js && node --check extension/github-acceptance.js && node --check extension/acceptance-workflow.js && node --check scripts/review-verdict.mjs && node --check scripts/github-acceptance.mjs"
"check": "npm test && node --check extension/index.js && node --check extension/child-model-policy.js && node --check extension/model-policy.js && node --check extension/invariants.js && node --check extension/review-workflow.js && node --check extension/review-verdict.js && node --check extension/github-acceptance.js && node --check extension/acceptance-workflow.js && node --check scripts/review-verdict.mjs && node --check scripts/github-acceptance.mjs"
}
}
49 changes: 48 additions & 1 deletion tests/extension.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import test from "node:test";

import { registerRequiredChildExtensions } from "pi-subagents/required-child-extensions";
import registerForgeFlowChildModelPolicy from "../extension/child-model-policy.js";
import registerForgeFlow from "../extension/index.js";

test("extension registers Pi lifecycle hooks and injects policy without a model call", () => {
Expand Down Expand Up @@ -40,14 +43,58 @@ test("extension registers Pi lifecycle hooks and injects policy without a model
assert.match(event.systemPromptOptions.sections.forgeflow_policy, /INV-CUTOVER-001/);
assert.match(event.systemPromptOptions.sections.forgeflow_policy, /INV-REPLAY-001/);

const sessionId = "forgeflow-pi-native-test";
assert.doesNotThrow(() => {
handlers.get("session_start")(
{},
{
cwd: process.cwd(),
sessionManager: { getSessionId: () => "forgeflow-pi-native-test" }
sessionManager: { getSessionId: () => sessionId }
}
);
});

const childExtensionPath = fileURLToPath(
new URL("../extension/child-model-policy.js", import.meta.url)
);
assert.throws(
() => registerRequiredChildExtensions({
sessionId,
extensions: [{ id: "duplicate-probe", path: childExtensionPath }]
}),
/already registered/
);

assert.doesNotThrow(() => handlers.get("session_shutdown")());

const afterShutdown = registerRequiredChildExtensions({
sessionId,
extensions: [{ id: "post-shutdown-probe", path: childExtensionPath }]
});
afterShutdown.dispose();
});

test("child-only model policy extension registers only ForgeFlow virtual models", () => {
const registrations = [];
const handlers = [];
registerForgeFlowChildModelPolicy({
registerVirtualModel(definition) {
registrations.push(definition);
},
on(...args) {
handlers.push(args);
}
});

assert.deepEqual(
registrations.map(({ provider, id }) => `${provider}/${id}`),
[
"forgeflow/planner",
"forgeflow/worker",
"forgeflow/reviewer",
"forgeflow/scout",
"forgeflow/oracle"
]
);
assert.deepEqual(handlers, []);
});
Loading