Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b8f4f27
docs(spec): design planning phase integration
rochecompaan Sep 9, 2026
35478b7
docs(plan): integrate planning phase workflow
rochecompaan Sep 10, 2026
d792461
feat(workflow): checkpoint implementation pull requests
rochecompaan Sep 10, 2026
1931417
feat(run-once): report planning review stops
rochecompaan Sep 10, 2026
c117117
feat(run-once): require implementation pull request markers
rochecompaan Sep 10, 2026
6733621
fix(run-once): harden planning implementation checkpoints
rochecompaan Sep 10, 2026
dc5ba41
fix(workflow): harden planning checkpoints
rochecompaan Sep 10, 2026
62a66d6
refactor(run-once): extract development environment agent
rochecompaan Sep 10, 2026
6d1def2
feat(run-once): validate implementation pull requests
rochecompaan Sep 10, 2026
77f7e87
feat(run-once): checkpoint implementation pull request flow
rochecompaan Sep 10, 2026
92b8d8a
feat(run-once): coordinate planning pull request phases
rochecompaan Sep 10, 2026
16517c3
feat(run-once): select planning workflows
rochecompaan Sep 10, 2026
679e937
feat(run-once): lock planning issue runs
rochecompaan Sep 10, 2026
cfd5341
feat(run-once): route planning pull request workflows
rochecompaan Sep 10, 2026
fc3adf5
feat(run-once): run planning workflow phases
rochecompaan Sep 10, 2026
472cc22
refactor(run-once): extract implementation agent core
rochecompaan Sep 10, 2026
82209a9
feat(run-once): construct planning runtime
rochecompaan Sep 10, 2026
18efd72
feat(run-once): route planning pull request workflows
rochecompaan Sep 10, 2026
ce02e42
test(run-once): cover planning workflow integration
rochecompaan Sep 10, 2026
b3bf508
fix(run-once): harden planning phase execution
rochecompaan Sep 10, 2026
e65dbd8
fix(run-once): harden planning workflow routing
rochecompaan Sep 10, 2026
41dac93
fix(run-once): classify planning recovery failures
rochecompaan Sep 10, 2026
bed42a7
fix(run-once): make planning finish resumable
rochecompaan Sep 10, 2026
0cf1148
test(run-once): cover planning finish recovery
rochecompaan Sep 10, 2026
33c46c9
feat(run-once): preserve planning run evidence
rochecompaan Sep 10, 2026
9db206d
feat(run-once): persist planning run cost
rochecompaan Sep 10, 2026
ca10d9b
fix(run-once): preserve planning session paths
rochecompaan Sep 10, 2026
0788a7e
fix(run-once): complete planning recovery boundaries
rochecompaan Sep 10, 2026
dd1153b
fix(run-once): revalidate planning workflow identity
rochecompaan Sep 10, 2026
17d272c
fix(run-once): report planning implementation progress
rochecompaan Sep 10, 2026
42ae895
test(run-once): cover planning workflow safety
rochecompaan Sep 10, 2026
0abadf3
test(run-once): prove planning pipeline safety
rochecompaan Sep 10, 2026
a4f10aa
test(run-once): expand planning safety scenarios
rochecompaan Sep 10, 2026
aa0599d
refactor(workflow): centralize planning checkpoints
rochecompaan Sep 10, 2026
96099aa
refactor(workflow): share planning phase replacement
rochecompaan Sep 10, 2026
8e13700
refactor(run-once): split planning phase orchestration
rochecompaan Sep 10, 2026
3f12e29
refactor(run-once): split planning runtime adapters
rochecompaan Sep 10, 2026
8f24ab6
test(run-once): validate strict planning finish
rochecompaan Sep 10, 2026
128ca53
refactor(run-once): share planning runtime context
rochecompaan Sep 10, 2026
165c528
fix(run-once): resume terminal planning results
rochecompaan Sep 10, 2026
b3b05ea
refactor(run-once): share durable implementation result
rochecompaan Sep 10, 2026
dd79dcb
fix(run-once): preserve fresh planning workspace context
rochecompaan Sep 10, 2026
1b2f356
fix(workflow): support current crypto hash input types
rochecompaan Sep 10, 2026
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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions src/cli/commands/run-once/development-environment-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { resolve } from "node:path";
import {
profileExtensionArgs,
runOnceDevelopmentEnvironmentPiProfile,
} from "../../../pi/resource-profiles.ts";
import { parseDevelopmentEnvironmentResult, runPiPrompt } from "./pi.ts";
import { buildDevelopmentEnvironmentPrompt } from "./prompts.ts";
import type {
AgentIssueConfig,
AgentIssueDevelopmentEnvironmentHandoff,
AgentIssueDevelopmentEnvironmentNotReadyResult,
AgentIssueProgressEvent,
CommandRunner,
IssueSummary,
ProgressReporter,
} from "./types.ts";

export type DevelopmentEnvironmentAgentOutcome =
| { kind: "ready"; handoff: AgentIssueDevelopmentEnvironmentHandoff }
| {
kind: "not-ready";
result: AgentIssueDevelopmentEnvironmentNotReadyResult;
};

export type DevelopmentEnvironmentAgentInput = {
runner: CommandRunner;
config: AgentIssueConfig;
issue: IssueSummary;
labels: string[];
planPath: string;
branch: string;
worktreePath: string;
completedAt: string;
piAgentDir: string;
tokenUsageState: { total: number };
progressReporter?: ProgressReporter | undefined;
streamPiOutput?: ((chunk: string) => void) | undefined;
verbosePiOutput?: boolean | undefined;
heartbeatMs?: number | undefined;
piSessionPath?: string | undefined;
progress: (
level: AgentIssueProgressEvent["level"],
stage: string,
message: string,
extras?: Partial<Pick<AgentIssueProgressEvent, "issueNumber">>,
) => Promise<void>;
runStep: <T>(label: string, fn: () => Promise<T>) => Promise<T>;
observePi: (
stage: "pi-development-environment",
) => (observation: AgentIssueProgressEvent["observation"]) => Promise<void>;
};

export async function runDevelopmentEnvironmentAgent(
input: DevelopmentEnvironmentAgentInput,
): Promise<DevelopmentEnvironmentAgentOutcome> {
const developmentEnvironmentSkill =
input.config.skills.developmentEnvironment;
if (!developmentEnvironmentSkill)
throw new Error(
"Development environment stage requires skills.developmentEnvironment",
);
const worktreeRoot = resolve(input.config.repoRoot, input.worktreePath);
const profile = runOnceDevelopmentEnvironmentPiProfile(
input.config.skills,
input.config.repoRoot,
);
const result = await input.runStep("development environment", async () => {
await input.progress(
"info",
"development-environment",
"running development environment with pi",
{ issueNumber: input.issue.number },
);
return runPiPrompt(
input.runner,
worktreeRoot,
buildDevelopmentEnvironmentPrompt({
issue: { ...input.issue, labels: input.labels },
planPath: input.planPath,
branch: input.branch,
worktreePath: input.worktreePath,
projectPolicy: input.config.projectPolicy,
skills: input.config.skills,
}),
{
progress: input.progressReporter,
stage: "pi-development-environment",
parseResult: parseDevelopmentEnvironmentResult,
skillPaths: profile.additionalSkillPaths,
extensionArgs: profileExtensionArgs(profile),
streamOutput: input.streamPiOutput,
issueNumber: input.issue.number,
repoRoot: worktreeRoot,
heartbeatMs: input.heartbeatMs,
tokenUsageState: input.tokenUsageState,
observeSession: true,
sessionRoot: input.piSessionPath,
verbosePiOutput: input.verbosePiOutput,
onObservation: input.observePi("pi-development-environment"),
taskContract: input.config.projectPolicy.pi.taskContract,
piAgentDir: input.piAgentDir,
},
);
});
return result.status === "not-ready"
? { kind: "not-ready", result }
: { kind: "ready", handoff: { ...result, completedAt: input.completedAt } };
}
162 changes: 57 additions & 105 deletions src/cli/commands/run-once/development-environment-stage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { join } from "node:path";
import {
profileExtensionArgs,
runOnceDevelopmentEnvironmentPiProfile,
} from "../../../pi/resource-profiles.ts";
import type { IssueHostProvider } from "../../../host/types.ts";
import { planLabelChange } from "../triage/labels.ts";
import { parseDevelopmentEnvironmentResult, runPiPrompt } from "./pi.ts";
import { buildDevelopmentEnvironmentPrompt } from "./prompts.ts";
import { runDevelopmentEnvironmentAgent } from "./development-environment-agent.ts";
import { writeRunState } from "./run-state.ts";
import { retryableLabelsAfterDevelopmentEnvironmentFailure } from "./workflow-state.ts";
import type {
Expand All @@ -33,37 +27,38 @@ type DevelopmentEnvironmentDetails = {
worktreePath: string;
};

type DevelopmentEnvironmentStageOptions = DevelopmentEnvironmentDetails & {
runner: CommandRunner;
host: IssueHostProvider;
config: AgentIssueConfig;
issue: IssueSummary;
labels: string[];
readyLabel: string;
inProgressLabel: string;
timestamp: string;
logPath?: string;
piSessionPath?: string;
streamPiOutput?: (chunk: string) => void;
verbosePiOutput?: boolean;
heartbeatMs?: number;
piAgentDir: string;
tokenUsageState: { total: number };
progressReporter?: ProgressReporter | undefined;
progress: (
level: AgentIssueProgressEvent["level"],
stage: string,
message: string,
extras?: Partial<
Pick<AgentIssueProgressEvent, "issueNumber" | "elapsedSeconds" | "data">
>,
) => Promise<void>;
runStep: <T>(label: string, fn: () => Promise<T>) => Promise<T>;
observePi: (
stage: "pi-development-environment",
) => (observation: AgentIssueProgressEvent["observation"]) => Promise<void>;
emitSimpleStep: (issueNumber: number, label: string) => Promise<void>;
};
export type DevelopmentEnvironmentStageOptions =
DevelopmentEnvironmentDetails & {
runner: CommandRunner;
host: IssueHostProvider;
config: AgentIssueConfig;
issue: IssueSummary;
labels: string[];
readyLabel: string;
inProgressLabel: string;
timestamp: string;
logPath?: string;
piSessionPath?: string;
streamPiOutput?: (chunk: string) => void;
verbosePiOutput?: boolean;
heartbeatMs?: number;
piAgentDir: string;
tokenUsageState: { total: number };
progressReporter?: ProgressReporter | undefined;
progress: (
level: AgentIssueProgressEvent["level"],
stage: string,
message: string,
extras?: Partial<
Pick<AgentIssueProgressEvent, "issueNumber" | "elapsedSeconds" | "data">
>,
) => Promise<void>;
runStep: <T>(label: string, fn: () => Promise<T>) => Promise<T>;
observePi: (
stage: "pi-development-environment",
) => (observation: AgentIssueProgressEvent["observation"]) => Promise<void>;
emitSimpleStep: (issueNumber: number, label: string) => Promise<void>;
};

function withLogPath<T extends AgentIssuePipelineResult>(
result: T,
Expand All @@ -79,7 +74,7 @@ function withLogPath<T extends AgentIssuePipelineResult>(
};
}

async function developmentEnvironmentNotReady(
export async function developmentEnvironmentNotReady(
options: DevelopmentEnvironmentStageOptions,
result: Extract<
AgentIssueDevelopmentEnvironmentResult,
Expand Down Expand Up @@ -149,73 +144,30 @@ async function developmentEnvironmentNotReady(
export async function runDevelopmentEnvironmentStage(
options: DevelopmentEnvironmentStageOptions,
): Promise<DevelopmentEnvironmentStageResult> {
const developmentEnvironmentSkill =
options.config.skills.developmentEnvironment;
if (!developmentEnvironmentSkill) {
throw new Error(
"Development environment stage requires skills.developmentEnvironment",
);
}

const worktreeRoot = join(options.config.repoRoot, options.worktreePath);
const profile = runOnceDevelopmentEnvironmentPiProfile(
options.config.skills,
options.config.repoRoot,
);
const result = await options.runStep(
"development environment",
async (): Promise<AgentIssueDevelopmentEnvironmentResult> => {
await options.progress(
"info",
"development-environment",
"running development environment with pi",
{ issueNumber: options.issue.number },
);
return await runPiPrompt(
options.runner,
worktreeRoot,
buildDevelopmentEnvironmentPrompt({
issue: { ...options.issue, labels: options.labels },
planPath: options.planPath,
branch: options.branch,
worktreePath: options.worktreePath,
projectPolicy: options.config.projectPolicy,
skills: options.config.skills,
}),
{
progress: options.progressReporter,
stage: "pi-development-environment",
parseResult: parseDevelopmentEnvironmentResult,
skillPaths: profile.additionalSkillPaths,
extensionArgs: profileExtensionArgs(profile),
streamOutput: options.streamPiOutput,
issueNumber: options.issue.number,
repoRoot: worktreeRoot,
heartbeatMs: options.heartbeatMs,
tokenUsageState: options.tokenUsageState,
observeSession: true,
sessionRoot: options.piSessionPath,
verbosePiOutput: options.verbosePiOutput,
onObservation: options.observePi("pi-development-environment"),
taskContract: options.config.projectPolicy.pi.taskContract,
piAgentDir: options.piAgentDir,
},
);
},
);

if (result.status === "not-ready") {
const result = await runDevelopmentEnvironmentAgent({
runner: options.runner,
config: options.config,
issue: options.issue,
labels: options.labels,
planPath: options.planPath,
branch: options.branch,
worktreePath: options.worktreePath,
completedAt: options.timestamp,
piAgentDir: options.piAgentDir,
tokenUsageState: options.tokenUsageState,
progressReporter: options.progressReporter,
streamPiOutput: options.streamPiOutput,
verbosePiOutput: options.verbosePiOutput,
heartbeatMs: options.heartbeatMs,
piSessionPath: options.piSessionPath,
progress: options.progress,
runStep: options.runStep,
observePi: options.observePi,
});
if (result.kind === "not-ready")
return {
kind: "not-ready",
result: await developmentEnvironmentNotReady(options, result),
result: await developmentEnvironmentNotReady(options, result.result),
};
}

return {
kind: "ready",
handoff: {
...result,
completedAt: options.timestamp,
},
};
return result;
}
57 changes: 57 additions & 0 deletions src/cli/commands/run-once/implementation-agent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import test from "node:test";
import { createMockRunner } from "../../../../test-support/run-once/mock-runner.ts";
import { makeConfig } from "../../../../test-support/run-once/pipeline-fixtures.ts";
import { runImplementationAgent } from "./implementation-agent.ts";

test("returns a merged Pi result without applying direct-land policy", async () => {
const config = await makeConfig({ dryRun: false, execute: true });
const worktreePath = ".worktrees/issue-189";
await mkdir(join(config.repoRoot, worktreePath), { recursive: true });
const planPath = "docs/plans/issue-189.md";
await mkdir(join(config.repoRoot, "docs/plans"), { recursive: true });
await writeFile(join(config.repoRoot, planPath), "# plan\n", "utf8");
const runner = createMockRunner(async (call) => {
if (call.command === "pi")
return {
code: 0,
stdout: JSON.stringify({
status: "merged",
mergeCommit: "a".repeat(40),
branch: "agent/issue-189",
commits: ["a".repeat(40)],
validation: ["npm test"],
}),
stderr: "",
};
throw new Error(`unexpected ${call.command}`);
});
const outcome = await runImplementationAgent({
runner,
config: {
...config,
skills: { ...config.skills, developmentEnvironment: undefined },
},
issue: { number: 189, title: "Core", body: "", labels: [], state: "open" },
labels: [],
planPath,
branch: "agent/issue-189",
worktreePath,
worktree: { created: true, existingCommits: [] },
git: { baseBranch: "main", remote: "origin", allowDirectLand: false },
resume: { resumed: false },
piAgentDir: join(config.repoRoot, ".patchmill/pi-agent"),
tokenUsageState: { total: 0 },
completedAt: "2026-09-10T00:00:00.000Z",
progress: async () => {},
runStep: async (_label, fn) => fn(),
stepStart: async () => {},
stepComplete: async () => {},
observePi: () => async () => {},
});
assert.equal(outcome.kind, "implemented");
if (outcome.kind === "implemented")
assert.equal(outcome.result.status, "merged");
});
Loading
Loading