Skip to content
Merged
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-subagents children, ForgeFlow registers its own extension as a required child extension so those virtual roles are present even though local foreground children intentionally skip parent ambient-extension discovery. 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 also registers its own extension as a required native-child extension for each Pi session, because local foreground children intentionally do not load the parent's ambient extensions. This makes the `forgeflow/*` virtual models available in foreground, detached, nested, and recovery child sessions without hard-coding an installation path in operator profile settings. External CLI runners remain excluded by pi-subagents. ForgeFlow therefore does not carry a virtual-child compatibility shim.

## Policy file

Expand Down
13 changes: 13 additions & 0 deletions extension/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
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 FORGEFLOW_EXTENSION_PATH = fileURLToPath(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 @@ -20,6 +25,7 @@ export function buildForgeFlowPromptSection(prompt) {
export default function registerForgeFlow(pi) {
registerForgeFlowVirtualModels(pi);

let requiredChildRegistration;
let reviewRegistration;
let acceptanceRegistration;

Expand All @@ -28,11 +34,16 @@ export default function registerForgeFlow(pi) {
});

pi.on("session_start", (_event, ctx) => {
requiredChildRegistration?.dispose();
reviewRegistration?.dispose();
acceptanceRegistration?.dispose();

const sessionId = ctx.sessionManager.getSessionId();
const sessionCwd = ctx.cwd;
requiredChildRegistration = registerRequiredChildExtensions({
sessionId,
extensions: [{ id: "forgeflow", path: FORGEFLOW_EXTENSION_PATH }]
});
reviewRegistration = registerWorkflowResource({
sessionId,
definition: createReviewWorkflowDefinition(sessionCwd)
Expand All @@ -44,8 +55,10 @@ export default function registerForgeFlow(pi) {
});

pi.on("session_shutdown", () => {
requiredChildRegistration?.dispose();
reviewRegistration?.dispose();
acceptanceRegistration?.dispose();
requiredChildRegistration = undefined;
reviewRegistration = undefined;
acceptanceRegistration = undefined;
});
Expand Down
12 changes: 11 additions & 1 deletion tests/extension.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";

import { registerRequiredChildExtensions } from "pi-subagents/required-child-extensions";
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 +41,23 @@ 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 }
}
);
});

assert.throws(
() => registerRequiredChildExtensions({ sessionId, extensions: [] }),
/already registered/
);

assert.doesNotThrow(() => handlers.get("session_shutdown")());
const afterShutdown = registerRequiredChildExtensions({ sessionId, extensions: [] });
afterShutdown.dispose();
});
Loading