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
1 change: 1 addition & 0 deletions packages/workflows/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
A run that is only blocked on a recoverable provider or rate-limit failure remains stored as `running` and resumable: it raises no new heartbeat while blocked, but keeps its cadence state and any card already waiting with the parent. Cleanup runs only when the run's own status is terminal.
Parent pickup is keyed by the typed `workflows:workflow-heartbeat` entry and exact run id plus scheduled time rather than rendered card text, so a foreign custom message cannot release an unread heartbeat's slot and a durable resume that reuses the run id cannot revive an old admitted card after terminal cleanup ([#1975](https://github.com/bastani-inc/atomic/issues/1975)).
The cadence-anchor record is now written at the durability boundary itself, so it is durable before the checkpoint that made the run resumable is acknowledged. The scheduler's own write is guarded against runs with no durable progress and can therefore only be issued after resumability already exists; a process that exited inside that window left a resumable run carrying no record of what it launched with, and the next process then took both a freshly minted start time and whatever cadence the definition declared by then, shifting the phase and the cadence of a run already in flight ([#1975](https://github.com/bastani-inc/atomic/issues/1975)).
- Every bundled builtin workflow now states its heartbeat cadence rather than inheriting it. `adversarial-verification`, `classify-and-act`, `fan-out-and-synthesize`, `generate-and-filter`, `goal`, `loop-until-done`, `ralph`, and `tournament` declare the `15`-minute default explicitly, so these long autonomous runs report to the parent chat on a known cadence and a future change to the global default cannot silently re-cadence them. `open-claude-design` sets `0` and runs quiet: it is driven by the user reviewing generated HTML turn by turn, so the parent chat is already holding it to its goal and a periodic alignment steer would interrupt that review rather than inform it. Behaviour is unchanged for the eight that were already resolving to the default; only `open-claude-design` changes ([#1975](https://github.com/bastani-inc/atomic/issues/1975)).

## [0.9.13] - 2026-08-13

Expand Down
4 changes: 4 additions & 0 deletions packages/workflows/builtin/adversarial-verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { runAdversarialVerification } from "./adversarial-verification-runner.js
export default workflow({
name: "adversarial-verification",
description: "Produce a candidate, challenge it with fresh-context rubric-based verifiers, and reduce their evidence through a bounded repair loop.",
// The 15-minute default, stated rather than inherited: this is a per-workflow
// product decision, so a future change to the global default must not silently
// re-cadence a long autonomous run.
heartbeatIntervalMinutes: 15,
inputs: {
task: Type.String({ description: "Task whose candidate result must be independently verified." }),
verifier_count: Type.Integer({ minimum: 1, maximum: 5, default: 3, description: "Number of independent verifiers per review round." }),
Expand Down
4 changes: 4 additions & 0 deletions packages/workflows/builtin/classify-and-act.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export const DEFAULT_ACTION_CATEGORIES = ["analysis", "implementation", "researc
export default workflow({
name: "classify-and-act",
description: "Classify a task with structured confidence, route deterministically to an isolated category action, and ask for human selection when confidence is low.",
// The 15-minute default, stated rather than inherited: this is a per-workflow
// product decision, so a future change to the global default must not silently
// re-cadence a long autonomous run.
heartbeatIntervalMinutes: 15,
inputs: {
prompt: Type.String({ description: "Task to classify and execute." }),
categories: Type.Array(Type.String({ minLength: 1 }), {
Expand Down
4 changes: 4 additions & 0 deletions packages/workflows/builtin/fan-out-and-synthesize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { runFanOutAndSynthesize } from "./fan-out-and-synthesize-runner.js";
export default workflow({
name: "fan-out-and-synthesize",
description: "Partition a task, run bounded independent artifact branches, then synthesize all evidence at an explicit barrier.",
// The 15-minute default, stated rather than inherited: this is a per-workflow
// product decision, so a future change to the global default must not silently
// re-cadence a long autonomous run.
heartbeatIntervalMinutes: 15,
inputs: {
prompt: Type.String({ description: "Task to partition, investigate, and synthesize." }),
max_branches: Type.Integer({
Expand Down
4 changes: 4 additions & 0 deletions packages/workflows/builtin/generate-and-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { runGenerateAndFilter } from "./generate-and-filter-runner.js";
export default workflow({
name: "generate-and-filter",
description: "Generate more independent candidates than needed, deduplicate and filter them by rubric, optionally judge them, and return a parent-consumable shortlist.",
// The 15-minute default, stated rather than inherited: this is a per-workflow
// product decision, so a future change to the global default must not silently
// re-cadence a long autonomous run.
heartbeatIntervalMinutes: 15,
inputs: {
prompt: Type.String({ description: "Prompt for candidate generation and selection." }),
num_candidates: Type.Integer({ minimum: 2, maximum: 20, default: 8, description: "Number of independent candidates to generate." }),
Expand Down
4 changes: 4 additions & 0 deletions packages/workflows/builtin/goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { DEFAULT_MAX_TURNS } from "./goal-types.js";
export default workflow({
name: "goal",
description: "Goal Runner workflow with bounded sub-agent orchestration turns, immutable acceptance criteria, ledger artifacts, parallel reviewers, and reducer-gated completion. When launching follow-up goal runs from review findings, pass the ORIGINAL task text as acceptance_criteria so deltas cannot drift from the literal contract. If the task includes submitting a pull request (or MR/review), remove that final action from the objective text and set create_pr=true instead when preparing the workflow inputs.",
// The 15-minute default, stated rather than inherited: this is a per-workflow
// product decision, so a future change to the global default must not silently
// re-cadence a long autonomous run.
heartbeatIntervalMinutes: 15,
inputs: {
objective: Type.String({ description: "The objective or delta for this Goal Runner workflow run. Do not include PR/MR submission instructions here; strip them from the task text and request them via create_pr=true instead." }),
acceptance_criteria: Type.Optional(Type.String({ description: "Original immutable task contract this run must remain consistent with. Defaults to objective. Orchestrators launching follow-up runs from reviewer findings should pass the ORIGINAL task text here." })),
Expand Down
4 changes: 4 additions & 0 deletions packages/workflows/builtin/loop-until-done.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { runLoopUntilDone } from "./loop-until-done-runner.js";
export default workflow({
name: "loop-until-done",
description: "Repeat evidence-producing work and independent completion evaluation against a durable ledger until done or an inspectable iteration-limit failure.",
// The 15-minute default, stated rather than inherited: this is a per-workflow
// product decision, so a future change to the global default must not silently
// re-cadence a long autonomous run.
heartbeatIntervalMinutes: 15,
inputs: {
prompt: Type.String({ description: "Objective whose explicit completion condition controls the bounded loop." }),
max_iterations: Type.Integer({
Expand Down
4 changes: 4 additions & 0 deletions packages/workflows/builtin/open-claude-design.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import {
export default workflow({
name: "open-claude-design",
description: "AI-powered design workflow: combined discovery/init → design-system/reference research → curated reference discovery → HTML generation → live-driven refinement → rich HTML handoff. The discovery stage asks what to build, the output type, and which references to emulate, then runs impeccable init for PRODUCT.md/DESIGN.md (references take precedence over project context). The user iteratively reviews the generated HTML.",
// Disabled: this workflow is driven by the user reviewing generated HTML,
// so the parent chat is already the thing holding it to its goal. A periodic
// alignment steer would interrupt that review rather than inform it.
heartbeatIntervalMinutes: 0,
inputs: {
prompt: Type.String({
description: "What to design (for example, a dashboard, page, component, or prototype). The discovery stage refines this into a confirmed brief and asks for the output type and references.",
Expand Down
4 changes: 4 additions & 0 deletions packages/workflows/builtin/ralph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import { runRalphWorkflow } from "./ralph-runner.js";
export default workflow({
name: "ralph",
description: "Raw prompt → research-prompt-refinement → research → orchestrate → multi-model parallel review loop with bounded iteration and immutable acceptance criteria. When launching follow-up ralph runs from review findings, pass the ORIGINAL task text as acceptance_criteria so deltas cannot drift from the literal contract. If the task includes submitting a pull request (or MR/review), remove that final action from the prompt text and set create_pr=true instead when preparing the workflow inputs.",
// The 15-minute default, stated rather than inherited: this is a per-workflow
// product decision, so a future change to the global default must not silently
// re-cadence a long autonomous run.
heartbeatIntervalMinutes: 15,
inputs: {
prompt: Type.String({ description: "The task or goal to research, execute, and refine. Do not include PR/MR submission instructions here; strip them from the task text and request them via create_pr=true instead." }),
acceptance_criteria: Type.Optional(Type.String({ description: "Original immutable task contract this run must remain consistent with. Defaults to prompt. Orchestrators launching follow-up runs from reviewer findings should pass the ORIGINAL task text here." })),
Expand Down
4 changes: 4 additions & 0 deletions packages/workflows/builtin/tournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { runTournament } from "./tournament-runner.js";
export default workflow({
name: "tournament",
description: "Run several independent whole-task attempts through a balanced pairwise judging bracket and return an auditable winner.",
// The 15-minute default, stated rather than inherited: this is a per-workflow
// product decision, so a future change to the global default must not silently
// re-cadence a long autonomous run.
heartbeatIntervalMinutes: 15,
inputs: {
prompt: Type.String({ description: "Task every competing agent must attempt independently." }),
num_attempts: Type.Integer({
Expand Down
54 changes: 54 additions & 0 deletions test/unit/builtin-workflow-heartbeat-cadence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import assert from "node:assert/strict";
import { describe, test } from "vitest";
import adversarialVerification from "../../packages/workflows/builtin/adversarial-verification.js";
import classifyAndAct from "../../packages/workflows/builtin/classify-and-act.js";
import fanOutAndSynthesize from "../../packages/workflows/builtin/fan-out-and-synthesize.js";
import generateAndFilter from "../../packages/workflows/builtin/generate-and-filter.js";
import goal from "../../packages/workflows/builtin/goal.js";
import loopUntilDone from "../../packages/workflows/builtin/loop-until-done.js";
import openClaudeDesign from "../../packages/workflows/builtin/open-claude-design.js";
import ralph from "../../packages/workflows/builtin/ralph.js";
import tournament from "../../packages/workflows/builtin/tournament.js";
import { DEFAULT_WORKFLOW_HEARTBEAT_INTERVAL_MINUTES } from "../../packages/workflows/src/shared/workflow-heartbeat-contract.js";

/**
* Every builtin states its heartbeat cadence rather than inheriting it, so a
* change to the global default cannot silently re-cadence a long autonomous run
* or start heartbeating a workflow that deliberately runs quiet.
*/
describe("builtin workflow heartbeat cadences", () => {
const autonomous = [
["adversarial-verification", adversarialVerification],
["classify-and-act", classifyAndAct],
["fan-out-and-synthesize", fanOutAndSynthesize],
["generate-and-filter", generateAndFilter],
["goal", goal],
["loop-until-done", loopUntilDone],
["ralph", ralph],
["tournament", tournament],
] as const;

for (const [name, definition] of autonomous) {
test(`${name} heartbeats on the 15-minute default`, () => {
assert.equal(definition.heartbeatIntervalMinutes, 15);
assert.equal(definition.heartbeatIntervalMinutes, DEFAULT_WORKFLOW_HEARTBEAT_INTERVAL_MINUTES);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
});
}

test("open-claude-design disables heartbeats", () => {
// The user reviews generated HTML turn by turn, so the parent chat is
// already holding this workflow to its goal; a periodic alignment steer
// would interrupt that review rather than inform it.
assert.equal(openClaudeDesign.heartbeatIntervalMinutes, 0);
});

test("every builtin states a cadence explicitly", () => {
for (const [name, definition] of [...autonomous, ["open-claude-design", openClaudeDesign] as const]) {
assert.equal(typeof definition.heartbeatIntervalMinutes, "number", `${name} must carry a resolved cadence`);
assert.ok(
Number.isFinite(definition.heartbeatIntervalMinutes) && definition.heartbeatIntervalMinutes >= 0,
`${name} must carry a non-negative finite cadence`,
);
}
});
});
Loading