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
10 changes: 10 additions & 0 deletions packages/core/src/plugin/command/orchestration-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,16 @@ Declare `output_schema` for gates and arbiters and normalize `verdict` to `ACCEP

## Verdict Disposal Contract

**持续核验至上线标准 · Verify to the delivery bar.** Implementation is not the
finish line: after implementation, keep iterating verification until the
delivery bar the user demanded is genuinely met — 实现之后,持续迭代核验,直至
达成用户要求的上线标准。The Router owns every judgment on that loop: verdict
`replan` when the evidence is insufficient, reorganize the flow (a correction
wave via `extend`, a reshape via `replan`) when the outcome deviates, and a
new DAG when the current graph is exhausted. Neither a completed node nor a
green build closes the loop on its own — only the user's delivery standard
does.

A gate, arbiter, or auditor verdict is a work order, not a summary. When a
checkpoint reports `REVISE`, `REJECT`, or `BLOCKED`, the parent MUST dispose
of it in the same wake turn with exactly one of:
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/plugin/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ describe("CommandPlugin.Plugin", () => {
expect(CommandPlugin.OrchestrationPolicyContent).toContain("The parent conversation owns")
expect(CommandPlugin.OrchestrationPolicyContent).toContain("MUST NOT perform executable leaf work")
expect(CommandPlugin.OrchestrationPolicyContent).not.toContain("## Execution Mode Selection")
expect(CommandPlugin.OrchestrationPolicyContent).toContain("持续核验至上线标准 · Verify to the delivery bar")
expect(CommandPlugin.OrchestrationPolicyContent).toContain("keep iterating verification until the")
expect(CommandPlugin.OrchestrationPolicyContent).toContain("only the user's delivery standard")
expect(CommandPlugin.WorkflowContent).toContain("One `task` child")
expect(CommandPlugin.WorkflowContent).toContain("One `workflow` DAG")
expect(CommandPlugin.DagFlowContent).toMatch(/one consolidated\s+graph/)
Expand Down
17 changes: 15 additions & 2 deletions packages/opencode/test/server/httpapi-exercise/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,23 @@ type CachedApp = BackendApp & { readonly dispose: () => Promise<void> }

const appCache: Partial<Record<string, CachedApp>> = {}

export async function disposeApps() {
export async function disposeApps(heartbeat?: (label: string) => void) {
const apps = Object.values(appCache)
for (const key of Object.keys(appCache)) delete appCache[key]
await Promise.all(apps.flatMap((app) => (app === undefined ? [] : [app.dispose()])))
heartbeat?.(`teardown: disposing ${apps.filter((app) => app !== undefined).length} app(s)`)
await Promise.all(
apps.flatMap((app, i) =>
app === undefined
? []
: [
app.dispose().then(
() => heartbeat?.(`teardown: app[${i}] disposed`),
(err) => heartbeat?.(`teardown: app[${i}] dispose error: ${err}`),
),
],
),
)
heartbeat?.("teardown: all apps disposed")
}

function app(modules: Runtime, options: CallOptions) {
Expand Down
8 changes: 7 additions & 1 deletion packages/opencode/test/server/httpapi-exercise/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2151,7 +2151,13 @@ const llmScenarios = new Set([
])

const main = Effect.gen(function* () {
yield* Effect.addFinalizer(() => Effect.promise(() => disposeApps()).pipe(Effect.andThen(cleanupExercisePaths)))
yield* Effect.addFinalizer(() =>
Effect.promise(() => disposeApps(options.heartbeat)).pipe(
Effect.andThen(Effect.sync(() => options.heartbeat?.("teardown: cleanupExercisePaths"))),
Effect.andThen(cleanupExercisePaths),
Effect.andThen(Effect.sync(() => options.heartbeat?.("teardown: complete"))),
),
)
const parsed = parseOptions(Bun.argv.slice(2))
const options: Options = parsed.progress ? { ...parsed, heartbeat: startProgressWatchdog() } : parsed
const modules = yield* Effect.promise(() => runtime())
Expand Down
15 changes: 13 additions & 2 deletions packages/opencode/test/server/httpapi-exercise/watchdog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ const pid = Number(process.env.WATCHDOG_PID)
const file = process.env.WATCHDOG_FILE
const timeoutMs = Number(process.env.WATCHDOG_TIMEOUT_MS)
const pollMs = Number(process.env.WATCHDOG_POLL_MS)
// A missing heartbeat file is treated as stalled (tracked via missingSince),
// never as healthy: CI runners can reclaim tmpdir files, and silently
// standing down when the file disappears is what let the 2026-08-17
// teardown hang burn the full 15m step timeout without a kill.
let missingSince = 0
setInterval(() => {
let alive = true
try {
Expand All @@ -36,12 +41,18 @@ setInterval(() => {
try {
mtime = fs.statSync(file).mtimeMs
} catch {}
if (!mtime || Date.now() - mtime <= timeoutMs) return
if (mtime) {
missingSince = 0
if (Date.now() - mtime <= timeoutMs) return
} else {
if (!missingSince) missingSince = Date.now()
if (Date.now() - missingSince <= timeoutMs) return
}
let last = "<none>"
try {
last = fs.readFileSync(file, "utf8")
} catch {}
console.error("[watchdog] no progress for " + Math.round((Date.now() - mtime) / 1000) + "s; last activity: " + last + " — killing pid " + pid)
console.error("[watchdog] no progress for " + Math.round((Date.now() - (mtime || missingSince)) / 1000) + "s; last activity: " + last + " — killing pid " + pid)
try {
process.kill(pid, "SIGKILL")
} catch {}
Expand Down
Loading