From 8f7147c406d5f1def1c6b15567cfb4c107b36fe0 Mon Sep 17 00:00:00 2001 From: C1oud <120496180@qq.com> Date: Fri, 29 May 2026 14:20:16 +0800 Subject: [PATCH] Refine Threadsmith orchestration hygiene --- .../performance-and-redundancy-hygiene-v1.md | 40 +++ packages/domain/src/phaseRuns.ts | 5 + packages/orchestrator/src/phaseEvidence.ts | 45 ++- packages/orchestrator/src/phaseRunner.test.ts | 5 + packages/orchestrator/src/phaseRunner.ts | 21 ++ .../src/roleOrchestratorDefaults.ts | 47 +++ .../src/rolePacketBuildContext.ts | 53 +++ packages/orchestrator/src/rolePacketSpecs.ts | 161 +++++++++ packages/orchestrator/src/rolePackets.test.ts | 24 +- packages/orchestrator/src/rolePackets.ts | 310 ++---------------- .../orchestrator/src/rolePromptContracts.ts | 63 ++++ 11 files changed, 487 insertions(+), 287 deletions(-) create mode 100644 docs/plans/performance-and-redundancy-hygiene-v1.md create mode 100644 packages/orchestrator/src/roleOrchestratorDefaults.ts create mode 100644 packages/orchestrator/src/rolePacketBuildContext.ts create mode 100644 packages/orchestrator/src/rolePacketSpecs.ts create mode 100644 packages/orchestrator/src/rolePromptContracts.ts diff --git a/docs/plans/performance-and-redundancy-hygiene-v1.md b/docs/plans/performance-and-redundancy-hygiene-v1.md new file mode 100644 index 0000000..dde7cc1 --- /dev/null +++ b/docs/plans/performance-and-redundancy-hygiene-v1.md @@ -0,0 +1,40 @@ +# Performance And Redundancy Hygiene v1 Implementation Plan + +Goal: reduce Threadsmith's perceived slowness and structural redundancy without changing workflow behavior. + +Scope: role packet construction, phase runner timing evidence, shared context reads, and small responsibility seams in orchestrator/fs-bridge code. + +Non-goals: frontend changes, release work, global skill sync, multi-provider routing, prompt rewrites without tests, or parallel writes to committed truth. + +Assumptions: reads and pure derivations can be parallelized safely; writes to committed truth, phase-run records, events, and history must remain ordered. + +Verification: `npm run test --workspace @threadsmith/domain`, `npm run test --workspace @threadsmith/orchestrator`, `npm run test --workspace @threadsmith/fs-bridge`, `npm run verify:project-truth`, and `git diff --check`. + +## Files + +- Modify: `packages/domain/src/phaseRuns.ts` +- Modify: `packages/orchestrator/src/phaseRunner.ts` +- Modify: `packages/orchestrator/src/phaseRunner.test.ts` +- Modify: `packages/orchestrator/src/rolePackets.ts` +- Modify: `packages/orchestrator/src/phaseEvidence.ts` +- Create or modify tests near the changed orchestrator/fs-bridge seams as needed. + +## Steps + +1. Add a timing baseline to phase-run role runtime records so each role records packet build time, launch wait time, result apply time, result read time, and observed bridge overhead. +2. Use the timing baseline to identify duplicated reads in packet construction, especially repeated project state and latest run reads between role packet building and evidence bundle generation. +3. Introduce a shared role packet build context so independent reads can happen concurrently and repeated state/latest-run reads are reused. +4. Keep committed truth writes serial and preserve existing phase-run event ordering. +5. Clean up redundancy only where responsibility boundaries are clear; avoid generic utility modules or behavior-sensitive prompt rewrites. + +## Risks + +- Parallelizing writes could corrupt event order or produce confusing committed truth, so v1 only parallelizes reads and pure derivations. +- Timing fields can become noisy if they are treated as precise benchmarks; they are diagnostic hints, not acceptance criteria. +- Role packet prompt behavior is sensitive, so any prompt-adjacent cleanup needs tests or snapshots. + +## Done When + +- Phase-run role runtime artifacts expose enough timing detail to tell whether slowness comes from packet construction, CLI execution, result apply, or result readback. +- Tests prove the new runtime fields are recorded without changing the success path. +- A follow-up optimization slice can be selected from evidence rather than intuition. diff --git a/packages/domain/src/phaseRuns.ts b/packages/domain/src/phaseRuns.ts index 2b1d052..b35ce85 100644 --- a/packages/domain/src/phaseRuns.ts +++ b/packages/domain/src/phaseRuns.ts @@ -76,6 +76,11 @@ export const phaseRunRoleRuntimeRecordSchema = z.object({ startedAt: z.string().min(1), finishedAt: z.string().min(1), durationMs: z.number().int().min(0), + packetBuildDurationMs: z.number().int().min(0).optional(), + launchWaitDurationMs: z.number().int().min(0).optional(), + resultApplyDurationMs: z.number().int().min(0).optional(), + resultReadDurationMs: z.number().int().min(0).optional(), + observedBridgeOverheadMs: z.number().int().min(0).optional(), contextRefCount: z.number().int().min(0), packetEstimatedChars: z.number().int().min(0), packetEstimatedTokens: z.number().int().min(0), diff --git a/packages/orchestrator/src/phaseEvidence.ts b/packages/orchestrator/src/phaseEvidence.ts index 5fb51ca..a4314ce 100644 --- a/packages/orchestrator/src/phaseEvidence.ts +++ b/packages/orchestrator/src/phaseEvidence.ts @@ -1,9 +1,11 @@ import { execFile } from "node:child_process"; +import { relative } from "node:path"; import { promisify } from "node:util"; import type { ContextReference, PhaseRunEvidenceBundle, - PhaseRunRecord + PhaseRunRecord, + ProjectState } from "@threadsmith/domain"; import { loadProjectState, @@ -32,8 +34,29 @@ async function readGitSummary(projectRoot: string) { const command = "git status --short"; try { - const { stdout } = await execFileAsync("git", ["-C", projectRoot, "status", "--short"]); - const changedFiles = parseChangedFiles(stdout); + const { stdout: topLevelStdout } = await execFileAsync("git", [ + "-C", + projectRoot, + "rev-parse", + "--show-toplevel" + ]); + const gitTopLevel = topLevelStdout.trim(); + const projectPrefix = relative(gitTopLevel, projectRoot).replace(/\\/g, "/"); + const { stdout } = await execFileAsync("git", [ + "-C", + gitTopLevel, + "status", + "--short" + ]); + const changedFiles = parseChangedFiles(stdout) + .filter((filePath) => + projectPrefix === "" || + filePath.startsWith(`${projectPrefix}/`) + ) + .map((filePath) => + projectPrefix === "" ? filePath : filePath.slice(projectPrefix.length + 1) + ) + .filter(Boolean); return { status: changedFiles.length > 0 ? "dirty" as const : "clean" as const, @@ -74,9 +97,15 @@ function staleTruthWarnings(phaseRun: PhaseRunRecord, activeOwners: string[]) { return warnings; } +export interface PhaseEvidenceBundleContext { + state: ProjectState; + latestRuns: Awaited>; +} + export async function buildAndWritePhaseEvidenceBundle( projectRoot: string, - generatedAt = new Date().toISOString() + generatedAt = new Date().toISOString(), + context?: PhaseEvidenceBundleContext ): Promise<{ bundle: PhaseRunEvidenceBundle; ref: ContextReference } | null> { const phaseRun = await readLatestPhaseRun(projectRoot); @@ -84,9 +113,11 @@ export async function buildAndWritePhaseEvidenceBundle( return null; } - const state = await loadProjectState(projectRoot); - const latestRuns = await readLatestAgentRuns(projectRoot, 4); - const git = await readGitSummary(projectRoot); + const [state, latestRuns, git] = await Promise.all([ + context?.state ?? loadProjectState(projectRoot), + context?.latestRuns ?? readLatestAgentRuns(projectRoot, 4), + readGitSummary(projectRoot) + ]); const verification = decideVerificationPolicy({ phase: state.currentPhase, acceptance: state.acceptanceState, diff --git a/packages/orchestrator/src/phaseRunner.test.ts b/packages/orchestrator/src/phaseRunner.test.ts index 72fe426..816b222 100644 --- a/packages/orchestrator/src/phaseRunner.test.ts +++ b/packages/orchestrator/src/phaseRunner.test.ts @@ -271,6 +271,11 @@ describe("PhaseRunner", () => { phaseRunId: phaseRun.phaseRunId, role: "planner", contextRefCount: expect.any(Number), + packetBuildDurationMs: expect.any(Number), + launchWaitDurationMs: expect.any(Number), + resultApplyDurationMs: expect.any(Number), + resultReadDurationMs: expect.any(Number), + observedBridgeOverheadMs: expect.any(Number), packetEstimatedChars: expect.any(Number), outputSummaryEstimatedChars: expect.any(Number), verificationCommandCount: 0, diff --git a/packages/orchestrator/src/phaseRunner.ts b/packages/orchestrator/src/phaseRunner.ts index dc83e1c..72f0814 100644 --- a/packages/orchestrator/src/phaseRunner.ts +++ b/packages/orchestrator/src/phaseRunner.ts @@ -66,6 +66,10 @@ function durationMs(startedAt: string, finishedAt: string) { return Math.max(0, finished - started); } +function elapsedMs(startedAt: number) { + return Math.max(0, Math.round(performance.now() - startedAt)); +} + function nextSuccessfulRole( current: PhaseRunRecord, result: ExecutionResult @@ -380,17 +384,21 @@ export class PhaseRunner { while (phaseRun.status === "running") { const role = phaseRun.currentRole ?? "planner"; const runId = crypto.randomUUID(); + const packetBuildStartedAt = performance.now(); const packet = await buildPacketForRole({ projectRoot: input.projectRoot, role, provider: input.provider, runId }); + const packetBuildDurationMs = elapsedMs(packetBuildStartedAt); await createAgentRun(input.projectRoot, packet, this.now()); const roleStartedAt = this.now(); + const launchStartedAt = performance.now(); const launch = await this.roleLauncher(packet, { startedAt: roleStartedAt }); + const launchWaitDurationMs = elapsedMs(launchStartedAt); const launchEventPhaseRun = await appendRunEvent(input.projectRoot, phaseRun, { title: `phase-run ${phaseRun.phaseRunId} launched ${role}`, detail: `当前角色已启动,runId=${runId}`, @@ -405,14 +413,18 @@ export class PhaseRunner { }); await launch.completion; + const resultApplyStartedAt = performance.now(); if (!launch.resultAppliedByLauncher) { await applyAgentRunResult(input.projectRoot, runId); } + const resultApplyDurationMs = elapsedMs(resultApplyStartedAt); + const resultReadStartedAt = performance.now(); const [result, record] = await Promise.all([ readAgentRunResult(input.projectRoot, runId), readAgentRunRecord(input.projectRoot, runId) ]); + const resultReadDurationMs = elapsedMs(resultReadStartedAt); const latestRunRef = preferredRunArtifact(record) ?? packet.output.resultPath; const latestSuccessfulRole = nextSuccessfulRole(phaseRun, result); let currentSliceId = phaseRun.currentSliceId; @@ -430,6 +442,15 @@ export class PhaseRunner { startedAt: roleStartedAt, finishedAt: roleFinishedAt, durationMs: durationMs(roleStartedAt, roleFinishedAt), + packetBuildDurationMs, + launchWaitDurationMs, + resultApplyDurationMs, + resultReadDurationMs, + observedBridgeOverheadMs: + packetBuildDurationMs + + launchWaitDurationMs + + resultApplyDurationMs + + resultReadDurationMs, contextRefCount: packet.contextRefs.length, packetEstimatedChars: estimateChars(packet), packetEstimatedTokens: estimateTokens(estimateChars(packet)), diff --git a/packages/orchestrator/src/roleOrchestratorDefaults.ts b/packages/orchestrator/src/roleOrchestratorDefaults.ts new file mode 100644 index 0000000..94223c8 --- /dev/null +++ b/packages/orchestrator/src/roleOrchestratorDefaults.ts @@ -0,0 +1,47 @@ +import type { + PhaseOwner, + SkillCapability, + SkillOrchestratorConfig +} from "@threadsmith/domain"; + +export function builtInOnlyOrchestratorConfig(): SkillOrchestratorConfig { + return { + version: 1, + builtInProtocols: [ + "brief", + "plan", + "debug", + "review", + "verify", + "closeout", + "handoff", + "recover", + "research" + ], + adapters: [], + routePreferences: [], + defaultFallback: "plan", + selfHosting: { + activeController: "installed-skill", + repositorySkillPath: "codex/skills/threadsmith/SKILL.md", + installedSkillPath: "~/.codex/skills/threadsmith/SKILL.md", + allowGlobalSkillMutation: false + } + }; +} + +export function protocolCapabilityForRole(role: PhaseOwner): SkillCapability { + switch (role) { + case "planner": + case "executor": + return "plan"; + case "reviewer": + return "review"; + case "verifier": + return "verify"; + case "closeout": + return "closeout"; + case "hygiene": + return "recover"; + } +} diff --git a/packages/orchestrator/src/rolePacketBuildContext.ts b/packages/orchestrator/src/rolePacketBuildContext.ts new file mode 100644 index 0000000..0af57c1 --- /dev/null +++ b/packages/orchestrator/src/rolePacketBuildContext.ts @@ -0,0 +1,53 @@ +import { + loadProjectState, + readLatestAgentRuns, + readRecentEvents +} from "@threadsmith/fs-bridge"; +import { buildAndWritePhaseEvidenceBundle } from "./phaseEvidence.ts"; +import { latestPhaseRunRefs } from "./rolePacketContextRefs.ts"; + +export interface RolePacketBuildContext { + state: Awaited>; + recentEvents: Awaited>; + latestRuns: Awaited>; + phaseRefs: Awaited>; + evidenceBundle: Awaited>; +} + +export async function buildRolePacketContext( + projectRoot: string +): Promise { + const statePromise = loadProjectState(projectRoot); + const latestRunsPromise = readLatestAgentRuns(projectRoot, 4); + const evidenceBundlePromise = Promise.all([ + statePromise, + latestRunsPromise + ]).then(([state, latestRuns]) => + buildAndWritePhaseEvidenceBundle(projectRoot, new Date().toISOString(), { + state, + latestRuns + }) + ); + + const [ + state, + recentEvents, + latestRuns, + phaseRefs, + evidenceBundle + ] = await Promise.all([ + statePromise, + readRecentEvents(projectRoot, 4), + latestRunsPromise, + latestPhaseRunRefs(projectRoot), + evidenceBundlePromise + ]); + + return { + state, + recentEvents, + latestRuns, + phaseRefs, + evidenceBundle + }; +} diff --git a/packages/orchestrator/src/rolePacketSpecs.ts b/packages/orchestrator/src/rolePacketSpecs.ts new file mode 100644 index 0000000..f354a55 --- /dev/null +++ b/packages/orchestrator/src/rolePacketSpecs.ts @@ -0,0 +1,161 @@ +import type { + CurrentPhase, + PhaseOwner, + ProjectState +} from "@threadsmith/domain"; +import { decidePlannerMode, type PlannerMode } from "./plannerMode.ts"; + +function phaseScope(phase: CurrentPhase) { + return phase.inScope.length > 0 ? phase.inScope : [phase.deliverable]; +} + +function acceptanceDoneWhen( + phase: CurrentPhase, + acceptance: ProjectState["acceptanceState"] +) { + return acceptance.doneWhenChecklist.length > 0 + ? acceptance.doneWhenChecklist.map((item) => item.label) + : [phase.stopCondition]; +} + +function plannerSliceKindForMode(mode: PlannerMode) { + return mode === "planner-reset" ? "repair" : "primary"; +} + +function plannerDoneWhen(mode: PlannerMode) { + const sliceKind = plannerSliceKindForMode(mode); + + if (mode === "planning-phase") { + return [ + "产出当前 planning phase 要求的计划、边界或 brief", + "明确下一步是否可以进入 executor,还是需要 operator 审核", + "如果当前真相不足以安全继续,则改为给出 pause recommendation" + ]; + } + + if (mode === "planner-lite") { + return [ + "只收窄下一条最小 slice,不重新规划整个项目", + "明确 slice 的 scope、done when 与 verification", + "如果当前真相不足以安全继续,则改为给出 pause recommendation" + ]; + } + + return [ + `产出一条不超出当前 phase 的 ${sliceKind} slice 建议`, + "明确 slice 的 scope、done when 与 verification", + "如果当前真相不足以安全继续,则改为给出 pause recommendation" + ]; +} + +function buildPlannerObjective( + phaseGoal: string, + mode: PlannerMode, + reason: string +) { + switch (mode) { + case "planning-phase": + return `Planner mode: planning-phase。当前 phase 的交付物就是计划/边界/brief;请完成该 planning deliverable,而不是直接写实现。原因:${reason} 当前 phase goal:${phaseGoal}`; + case "planner-reset": + return `Planner mode: planner-reset。为当前锁定 phase 收束下一条 repair slice,并基于失败或阻塞信号缩小范围。原因:${reason} 当前 phase goal:${phaseGoal}`; + case "planner-lite": + return `Planner mode: planner-lite。只为当前锁定 phase 收窄下一条最小 slice,不重新规划整个项目。原因:${reason} 当前 phase goal:${phaseGoal}`; + case "direct-executor": + return `Planner mode: direct-executor。当前 truth 已指向后续执行角色;只有在被显式调用为 planner 时才重新校准。原因:${reason} 当前 phase goal:${phaseGoal}`; + } +} + +export interface RolePacketSpec { + role: PhaseOwner; + objective: string; + scope: string[]; + doneWhen: string[]; + verification: string[]; +} + +export function buildRolePacketSpec(input: { + role: PhaseOwner; + state: ProjectState; +}): RolePacketSpec { + const phase = input.state.currentPhase; + const acceptance = input.state.acceptanceState; + + switch (input.role) { + case "planner": { + const mode = decidePlannerMode(input.state); + + return { + role: "planner", + objective: buildPlannerObjective( + phase.phaseGoal, + mode.mode, + mode.reason + ), + scope: phaseScope(phase), + doneWhen: plannerDoneWhen(mode.mode), + verification: [] + }; + } + case "executor": + return { + role: "executor", + objective: phase.phaseGoal, + scope: phaseScope(phase), + doneWhen: acceptanceDoneWhen(phase, acceptance), + verification: phase.verificationForThisPhase + }; + case "reviewer": + return { + role: "reviewer", + objective: "复核当前 slice 输出是否符合 Project Brief、当前 phase 与当前 claim。", + scope: [ + "检查当前输出是否仍在 locked phase 范围内", + "判断当前结果是否可以进入 verification" + ], + doneWhen: [ + "只给出 ready-for-verification 或 review-blocked", + "如果阻塞,明确 blocker 与关键发现" + ], + verification: [] + }; + case "verifier": + return { + role: "verifier", + objective: "独立检查当前 claim 是否已被证据支持,并给出 verification 结论。", + scope: [ + "复核当前 claim 与 done when", + "检查已有证据是否足够支撑通过" + ], + doneWhen: [ + "只给出 verification-failed 或 accepted-with-closeout-pending", + "如果失败,明确证据缺口或失败命令" + ], + verification: phase.verificationForThisPhase + }; + case "closeout": + return { + role: "closeout", + objective: "对当前已通过 verification 的结果做收尾,准备 accepted 状态。", + scope: [ + "清理临时调试痕迹", + "记录 residual risks 与必要文档更新" + ], + doneWhen: [ + "说明完成了哪些 closeout 动作", + "只在可以安全收尾时给出 accepted" + ], + verification: [] + }; + case "hygiene": + return { + role: "hygiene", + objective: "清理会话惯性,重新锚定 committed truth,并给出下一步最小动作。", + scope: [ + "区分 verified facts、assumptions、stale inferences", + "给出当前最小可继续动作" + ], + doneWhen: ["产出一份可信的 session hygiene 结果"], + verification: [] + }; + } +} diff --git a/packages/orchestrator/src/rolePackets.test.ts b/packages/orchestrator/src/rolePackets.test.ts index 5b4bd08..b2f6f09 100644 --- a/packages/orchestrator/src/rolePackets.test.ts +++ b/packages/orchestrator/src/rolePackets.test.ts @@ -1,6 +1,8 @@ -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; import { afterEach, describe, expect, it } from "vitest"; import { STATE_FILES, @@ -24,10 +26,12 @@ import { } from "./rolePackets.ts"; const createdRoots: string[] = []; +const execFileAsync = promisify(execFile); async function createProjectRoot() { const projectRoot = await mkdtemp(join(tmpdir(), "threadsmith-role-packets-")); createdRoots.push(projectRoot); + await execFileAsync("git", ["init"], { cwd: projectRoot }); await initializeProjectState(projectRoot); return projectRoot; } @@ -367,6 +371,24 @@ describe("rolePackets", () => { ); }); + it("filters phase evidence git status to the nested project root", async () => { + const parentRoot = await mkdtemp(join(tmpdir(), "threadsmith-parent-repo-")); + createdRoots.push(parentRoot); + await execFileAsync("git", ["init"], { cwd: parentRoot }); + await writeFile(join(parentRoot, "parent-only.txt"), "parent change\n", "utf8"); + const projectRoot = join(parentRoot, "nested-project"); + await mkdir(projectRoot, { recursive: true }); + await initializeProjectState(projectRoot); + await seedLatestPhaseRun(projectRoot); + + await buildPlannerPacket(projectRoot, "planner-run"); + const evidenceBundle = await readPhaseRunEvidenceBundle(projectRoot, "phase-run-1"); + + expect(evidenceBundle?.git.status).toBe("clean"); + expect(evidenceBundle?.git.changedFiles).toEqual([]); + expect(evidenceBundle?.verification.recommendedLevel).toBe("standard"); + }); + it("builds reviewer, verifier, and closeout packets with explicit gate decisions", async () => { const projectRoot = await createProjectRoot(); await seedLatestExecutorRun(projectRoot); diff --git a/packages/orchestrator/src/rolePackets.ts b/packages/orchestrator/src/rolePackets.ts index a84cfb6..6d1d15e 100644 --- a/packages/orchestrator/src/rolePackets.ts +++ b/packages/orchestrator/src/rolePackets.ts @@ -3,8 +3,6 @@ import { type ExecutionPacket, type PhaseOwner, type ProviderId, - type SkillCapability, - type SkillOrchestratorConfig, type VerificationPolicyDecision, executionPacketSchema } from "@threadsmith/domain"; @@ -12,18 +10,17 @@ import { buildMiniProtocolInstruction, resolveSkillRoute } from "@threadsmith/runtime"; -import { - loadProjectState, - readLatestAgentRuns, - readRecentEvents -} from "@threadsmith/fs-bridge"; import type { RoleExecutionRequest } from "./providerTypes.ts"; -import { buildAndWritePhaseEvidenceBundle } from "./phaseEvidence.ts"; -import { decidePlannerMode, type PlannerMode } from "./plannerMode.ts"; import { decideVerificationPolicy } from "./verificationPolicy.ts"; +import { + builtInOnlyOrchestratorConfig, + protocolCapabilityForRole +} from "./roleOrchestratorDefaults.ts"; +import { buildRolePacketContext } from "./rolePacketBuildContext.ts"; +import { buildRolePacketSpec } from "./rolePacketSpecs.ts"; +import { rolePromptContractFor } from "./rolePromptContracts.ts"; import { currentContextPacketRef, - latestPhaseRunRefs, roleContextPacketRef, roleContextRefs } from "./rolePacketContextRefs.ts"; @@ -35,48 +32,6 @@ function defaultOutput(runId: string) { }; } -function builtInOnlyOrchestratorConfig(): SkillOrchestratorConfig { - return { - version: 1, - builtInProtocols: [ - "brief", - "plan", - "debug", - "review", - "verify", - "closeout", - "handoff", - "recover", - "research" - ], - adapters: [], - routePreferences: [], - defaultFallback: "plan", - selfHosting: { - activeController: "installed-skill", - repositorySkillPath: "codex/skills/threadsmith/SKILL.md", - installedSkillPath: "~/.codex/skills/threadsmith/SKILL.md", - allowGlobalSkillMutation: false - } - }; -} - -function protocolCapabilityForRole(role: PhaseOwner): SkillCapability { - switch (role) { - case "planner": - case "executor": - return "plan"; - case "reviewer": - return "review"; - case "verifier": - return "verify"; - case "closeout": - return "closeout"; - case "hygiene": - return "recover"; - } -} - function packetContextLines(contextRefs: ContextReference[]) { return contextRefs.length > 0 ? contextRefs @@ -137,98 +92,6 @@ function packetVerificationPolicyLines(policy: ExecutionPacket["verificationPoli ]; } -function plannerSliceKindForMode(mode: PlannerMode) { - return mode === "planner-reset" ? "repair" : "primary"; -} - -function plannerDoneWhen(mode: PlannerMode) { - const sliceKind = plannerSliceKindForMode(mode); - - if (mode === "planning-phase") { - return [ - "产出当前 planning phase 要求的计划、边界或 brief", - "明确下一步是否可以进入 executor,还是需要 operator 审核", - "如果当前真相不足以安全继续,则改为给出 pause recommendation" - ]; - } - - if (mode === "planner-lite") { - return [ - "只收窄下一条最小 slice,不重新规划整个项目", - "明确 slice 的 scope、done when 与 verification", - "如果当前真相不足以安全继续,则改为给出 pause recommendation" - ]; - } - - return [ - `产出一条不超出当前 phase 的 ${sliceKind} slice 建议`, - "明确 slice 的 scope、done when 与 verification", - "如果当前真相不足以安全继续,则改为给出 pause recommendation" - ]; -} - -function roleRules(role: PhaseOwner) { - switch (role) { - case "planner": - return { - contract: [ - "只允许在当前锁定 phase 内收窄下一条最小 slice。", - "不要改写 locked phase contract,不要直接修改代码。", - "如果无法安全继续,使用 pauseRecommendation 而不是硬推。" - ], - decisions: ["slice-ready", "pause-recommended"] - }; - case "executor": - return { - contract: [ - "只推进当前 slice,不要扩大范围。", - "先阅读 committed truth 和必要文件,再开始修改。", - "完成后如实填写 changedFiles、verification、evidenceRefs。" - ], - decisions: ["ready-for-review"] - }; - case "reviewer": - return { - contract: [ - "只判断当前输出能否进入 verification,不要自己补代码。", - "遇到阻塞时明确指出 blocker 与关键发现。", - "不要把不确定性包装成通过。" - ], - decisions: ["ready-for-verification", "review-blocked"] - }; - case "verifier": - return { - contract: [ - "只根据证据做 verification 结论,不要把缺失证据当成通过。", - "需要时运行或复核 verification 命令,并如实回填结果。", - "不要直接给最终 accepted。" - ], - decisions: [ - "verification-failed", - "accepted-with-closeout-pending" - ] - }; - case "closeout": - return { - contract: [ - "只在 verification 已通过的前提下执行 closeout。", - "清理临时痕迹、记录 residual risks、补齐必要文档。", - "不要掩盖尚未解决的问题。" - ], - decisions: ["accepted"] - }; - case "hygiene": - return { - contract: [ - "重新锚定 committed truth,区分 verified facts 与 assumptions。", - "只提出下一步最小动作,不要重开整个任务。", - "不要伪造验证结论。" - ], - decisions: [] - }; - } -} - function renderRoleHeader(packet: ExecutionPacket) { return [ `你正在执行一条 Threadsmith ${packet.role} packet。`, @@ -242,7 +105,7 @@ function renderRoleHeader(packet: ExecutionPacket) { } export function renderRolePrompt(packet: ExecutionPacket) { - const rules = roleRules(packet.role); + const rules = rolePromptContractFor(packet.role); const decisionLines = rules.decisions.length > 0 ? rules.decisions.map((item) => `- ${item}`) @@ -291,23 +154,6 @@ export function renderRolePrompt(packet: ExecutionPacket) { ].join("\n"); } -function buildPlannerObjective( - phaseGoal: string, - mode: PlannerMode, - reason: string -) { - switch (mode) { - case "planning-phase": - return `Planner mode: planning-phase。当前 phase 的交付物就是计划/边界/brief;请完成该 planning deliverable,而不是直接写实现。原因:${reason} 当前 phase goal:${phaseGoal}`; - case "planner-reset": - return `Planner mode: planner-reset。为当前锁定 phase 收束下一条 repair slice,并基于失败或阻塞信号缩小范围。原因:${reason} 当前 phase goal:${phaseGoal}`; - case "planner-lite": - return `Planner mode: planner-lite。只为当前锁定 phase 收窄下一条最小 slice,不重新规划整个项目。原因:${reason} 当前 phase goal:${phaseGoal}`; - case "direct-executor": - return `Planner mode: direct-executor。当前 truth 已指向后续执行角色;只有在被显式调用为 planner 时才重新校准。原因:${reason} 当前 phase goal:${phaseGoal}`; - } -} - function buildRolePacket(input: { projectRoot: string; runId: string; @@ -353,11 +199,13 @@ function buildRolePacket(input: { export async function buildPacketForRole( input: RoleExecutionRequest ): Promise { - const state = await loadProjectState(input.projectRoot); - const recentEvents = await readRecentEvents(input.projectRoot, 4); - const latestRuns = await readLatestAgentRuns(input.projectRoot, 4); - const phaseRefs = await latestPhaseRunRefs(input.projectRoot); - const evidenceBundle = await buildAndWritePhaseEvidenceBundle(input.projectRoot); + const { + state, + recentEvents, + latestRuns, + phaseRefs, + evidenceBundle + } = await buildRolePacketContext(input.projectRoot); const evidenceRefs = evidenceBundle ? [evidenceBundle.ref] : []; const verificationPolicy = evidenceBundle?.bundle.verification ?? @@ -374,119 +222,23 @@ export async function buildPacketForRole( evidenceRefs, latestRuns }); + const spec = buildRolePacketSpec({ + role: input.role, + state + }); - switch (input.role) { - case "planner": { - const mode = decidePlannerMode(state); - - return buildRolePacket({ - projectRoot: input.projectRoot, - runId: input.runId, - provider: input.provider, - role: "planner", - objective: buildPlannerObjective( - state.currentPhase.phaseGoal, - mode.mode, - mode.reason - ), - scope: - state.currentPhase.inScope.length > 0 - ? state.currentPhase.inScope - : [state.currentPhase.deliverable], - doneWhen: plannerDoneWhen(mode.mode), - verification: [], - contextRefs - }); - } - case "executor": - return buildRolePacket({ - projectRoot: input.projectRoot, - runId: input.runId, - provider: input.provider, - role: "executor", - objective: state.currentPhase.phaseGoal, - scope: - state.currentPhase.inScope.length > 0 - ? state.currentPhase.inScope - : [state.currentPhase.deliverable], - doneWhen: - state.acceptanceState.doneWhenChecklist.length > 0 - ? state.acceptanceState.doneWhenChecklist.map((item) => item.label) - : [state.currentPhase.stopCondition], - verification: state.currentPhase.verificationForThisPhase, - contextRefs - }); - case "reviewer": - return buildRolePacket({ - projectRoot: input.projectRoot, - runId: input.runId, - provider: input.provider, - role: "reviewer", - objective: "复核当前 slice 输出是否符合 Project Brief、当前 phase 与当前 claim。", - scope: [ - "检查当前输出是否仍在 locked phase 范围内", - "判断当前结果是否可以进入 verification" - ], - doneWhen: [ - "只给出 ready-for-verification 或 review-blocked", - "如果阻塞,明确 blocker 与关键发现" - ], - verification: [], - contextRefs - }); - case "verifier": - return buildRolePacket({ - projectRoot: input.projectRoot, - runId: input.runId, - provider: input.provider, - role: "verifier", - objective: "独立检查当前 claim 是否已被证据支持,并给出 verification 结论。", - scope: [ - "复核当前 claim 与 done when", - "检查已有证据是否足够支撑通过" - ], - doneWhen: [ - "只给出 verification-failed 或 accepted-with-closeout-pending", - "如果失败,明确证据缺口或失败命令" - ], - verification: state.currentPhase.verificationForThisPhase, - verificationPolicy, - contextRefs - }); - case "closeout": - return buildRolePacket({ - projectRoot: input.projectRoot, - runId: input.runId, - provider: input.provider, - role: "closeout", - objective: "对当前已通过 verification 的结果做收尾,准备 accepted 状态。", - scope: [ - "清理临时调试痕迹", - "记录 residual risks 与必要文档更新" - ], - doneWhen: [ - "说明完成了哪些 closeout 动作", - "只在可以安全收尾时给出 accepted" - ], - verification: [], - contextRefs - }); - case "hygiene": - return buildRolePacket({ - projectRoot: input.projectRoot, - runId: input.runId, - provider: input.provider, - role: "hygiene", - objective: "清理会话惯性,重新锚定 committed truth,并给出下一步最小动作。", - scope: [ - "区分 verified facts、assumptions、stale inferences", - "给出当前最小可继续动作" - ], - doneWhen: ["产出一份可信的 session hygiene 结果"], - verification: [], - contextRefs - }); - } + return buildRolePacket({ + projectRoot: input.projectRoot, + runId: input.runId, + provider: input.provider, + role: spec.role, + objective: spec.objective, + scope: spec.scope, + doneWhen: spec.doneWhen, + verification: spec.verification, + verificationPolicy: spec.role === "verifier" ? verificationPolicy : undefined, + contextRefs + }); } export async function buildPlannerPacket( diff --git a/packages/orchestrator/src/rolePromptContracts.ts b/packages/orchestrator/src/rolePromptContracts.ts new file mode 100644 index 0000000..d2826e3 --- /dev/null +++ b/packages/orchestrator/src/rolePromptContracts.ts @@ -0,0 +1,63 @@ +import type { PhaseOwner } from "@threadsmith/domain"; + +export function rolePromptContractFor(role: PhaseOwner) { + switch (role) { + case "planner": + return { + contract: [ + "只允许在当前锁定 phase 内收窄下一条最小 slice。", + "不要改写 locked phase contract,不要直接修改代码。", + "如果无法安全继续,使用 pauseRecommendation 而不是硬推。" + ], + decisions: ["slice-ready", "pause-recommended"] + }; + case "executor": + return { + contract: [ + "只推进当前 slice,不要扩大范围。", + "先阅读 committed truth 和必要文件,再开始修改。", + "完成后如实填写 changedFiles、verification、evidenceRefs。" + ], + decisions: ["ready-for-review"] + }; + case "reviewer": + return { + contract: [ + "只判断当前输出能否进入 verification,不要自己补代码。", + "遇到阻塞时明确指出 blocker 与关键发现。", + "不要把不确定性包装成通过。" + ], + decisions: ["ready-for-verification", "review-blocked"] + }; + case "verifier": + return { + contract: [ + "只根据证据做 verification 结论,不要把缺失证据当成通过。", + "需要时运行或复核 verification 命令,并如实回填结果。", + "不要直接给最终 accepted。" + ], + decisions: [ + "verification-failed", + "accepted-with-closeout-pending" + ] + }; + case "closeout": + return { + contract: [ + "只在 verification 已通过的前提下执行 closeout。", + "清理临时痕迹、记录 residual risks、补齐必要文档。", + "不要掩盖尚未解决的问题。" + ], + decisions: ["accepted"] + }; + case "hygiene": + return { + contract: [ + "重新锚定 committed truth,区分 verified facts 与 assumptions。", + "只提出下一步最小动作,不要重开整个任务。", + "不要伪造验证结论。" + ], + decisions: [] + }; + } +}