@bastani/atomic 0.8.28-alpha.2 → 0.8.28-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +24 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +2 -1
- package/dist/builtin/workflows/builtin/goal.ts +2 -1
- package/dist/builtin/workflows/builtin/open-claude-design.ts +2 -1
- package/dist/builtin/workflows/builtin/ralph.ts +4 -2
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/authoring.d.ts +5 -2
- package/dist/builtin/workflows/src/extension/dispatcher.ts +2 -0
- package/dist/builtin/workflows/src/extension/index.ts +8 -0
- package/dist/builtin/workflows/src/extension/render-result.ts +5 -2
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +18 -0
- package/dist/builtin/workflows/src/runs/background/status.ts +4 -0
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +1251 -110
- package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +34 -10
- package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +10 -2
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +28 -9
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +9 -3
- package/dist/builtin/workflows/src/shared/store-types.ts +10 -3
- package/dist/builtin/workflows/src/shared/store.ts +29 -7
- package/dist/builtin/workflows/src/shared/types.ts +12 -10
- package/dist/builtin/workflows/src/tui/run-detail.ts +12 -0
- package/dist/builtin/workflows/src/tui/status-helpers.ts +4 -0
- package/dist/builtin/workflows/src/tui/status-list.ts +15 -1
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +1 -1
- package/dist/builtin/workflows/src/tui/widget.ts +12 -3
- package/dist/builtin/workflows/src/workflows/define-workflow.ts +3 -3
- package/dist/core/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +4 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +12 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/sdk.d.ts +4 -2
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/inline-input.d.ts +28 -0
- package/dist/core/tools/ask-user-question/state/inline-input.d.ts.map +1 -0
- package/dist/core/tools/ask-user-question/state/inline-input.js +56 -0
- package/dist/core/tools/ask-user-question/state/inline-input.js.map +1 -0
- package/dist/core/tools/ask-user-question/state/key-router.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/key-router.js +30 -4
- package/dist/core/tools/ask-user-question/state/key-router.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/questionnaire-session.js +9 -8
- package/dist/core/tools/ask-user-question/state/questionnaire-session.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/row-intent.d.ts +3 -2
- package/dist/core/tools/ask-user-question/state/row-intent.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/row-intent.js +1 -1
- package/dist/core/tools/ask-user-question/state/row-intent.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts +2 -0
- package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/contract.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/projections.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/selectors/projections.js +2 -0
- package/dist/core/tools/ask-user-question/state/selectors/projections.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/state-reducer.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/state-reducer.js +36 -24
- package/dist/core/tools/ask-user-question/state/state-reducer.js.map +1 -1
- package/dist/core/tools/ask-user-question/state/state.d.ts +8 -0
- package/dist/core/tools/ask-user-question/state/state.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/state/state.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +6 -0
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.js +19 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +3 -2
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.js +15 -3
- package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/types.d.ts +2 -1
- package/dist/core/tools/ask-user-question/tool/types.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/types.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts +5 -2
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.js +2 -0
- package/dist/core/tools/ask-user-question/view/components/chat-row-view.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts +1 -0
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +2 -1
- package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
- package/dist/core/tools/ask-user-question/view/props-adapter.d.ts +3 -3
- package/dist/core/tools/ask-user-question/view/props-adapter.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/view/props-adapter.js +11 -4
- package/dist/core/tools/ask-user-question/view/props-adapter.js.map +1 -1
- package/dist/core/tools/bash-policy.d.ts +62 -0
- package/dist/core/tools/bash-policy.d.ts.map +1 -0
- package/dist/core/tools/bash-policy.js +1069 -0
- package/dist/core/tools/bash-policy.js.map +1 -0
- package/dist/core/tools/bash.d.ts +5 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +7 -0
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/docs/sdk.md +42 -0
- package/docs/security.md +2 -0
- package/docs/workflows.md +127 -15
- package/package.json +1 -1
package/docs/workflows.md
CHANGED
|
@@ -588,6 +588,41 @@ Atomic discovers workflow definitions in this order:
|
|
|
588
588
|
|
|
589
589
|
A workflow module may export one default workflow definition and/or named workflow definitions. Discovery checks the default export first, then named exports.
|
|
590
590
|
|
|
591
|
+
Every runtime export of a discovered workflow file is validated as a workflow definition. A named export that is not a compiled definition — a widget factory, shared constant, or utility function — is rejected with an `INVALID_DEFINITION` discovery diagnostic (`export is not an object`), even when the module also has a valid default export (the valid workflow still loads; the diagnostic flags the extra export as skipped). Type-only exports (`export type` / `export interface`) are erased at runtime and never flagged.
|
|
592
|
+
|
|
593
|
+
To co-locate reusable helpers with your workflows — for example a `ctx.ui.custom<T>` widget factory you want to import in tests without running the workflow — put them in a subdirectory and import them from the workflow file. Discovery scans only the top level of each workflow directory, so subdirectories such as `.atomic/workflows/lib/` are never treated as workflow modules:
|
|
594
|
+
|
|
595
|
+
```text
|
|
596
|
+
.atomic/workflows/
|
|
597
|
+
release-picker.ts # only runtime export: defineWorkflow(...).compile()
|
|
598
|
+
lib/
|
|
599
|
+
table-selector.ts # widget factory + helpers; not scanned by discovery
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
// .atomic/workflows/release-picker.ts
|
|
604
|
+
import { defineWorkflow, Type } from "@bastani/workflows";
|
|
605
|
+
import { tableSelectorFactory } from "./lib/table-selector.js";
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
```ts
|
|
609
|
+
// .atomic/workflows/lib/table-selector.ts
|
|
610
|
+
import type { WorkflowCustomUiFactory } from "@bastani/workflows";
|
|
611
|
+
|
|
612
|
+
export const tableSelectorFactory: WorkflowCustomUiFactory<{ id: string; name: string }> = (
|
|
613
|
+
tui,
|
|
614
|
+
theme,
|
|
615
|
+
_keybindings,
|
|
616
|
+
done,
|
|
617
|
+
) => ({
|
|
618
|
+
render: (width) => ["..."],
|
|
619
|
+
invalidate: () => {},
|
|
620
|
+
handleInput: (data) => {
|
|
621
|
+
/* ... done({ id, name }) on Enter ... */
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
```
|
|
625
|
+
|
|
591
626
|
Workflow files are loaded via [jiti](https://github.com/unjs/jiti), so TypeScript works without compilation.
|
|
592
627
|
|
|
593
628
|
## Workflow Configuration
|
|
@@ -765,7 +800,7 @@ Input overrides are bare `key=value` tokens. Values are JSON-parsed when possibl
|
|
|
765
800
|
|
|
766
801
|
In the TUI, `/workflow <name>` opens an input picker when the workflow declares inputs and either no arguments were supplied or required inputs are missing. Supplied values seed the picker. Pass `--no-picker` to skip that interactive flow.
|
|
767
802
|
|
|
768
|
-
In non-interactive (`-p`, `--print`, or `--mode json`) sessions, named workflow dispatch waits for the terminal run snapshot and skips pickers. Because human input is runtime-only and workflows no longer carry a declaration-time HIL marker, headless dispatch does not reject a workflow just because its source contains `ctx.ui.*`. If you copy a HIL workflow example into a headless session, it can pass dispatch and then fail when execution reaches the prompt with an error such as `atomic-workflows:
|
|
803
|
+
In non-interactive (`-p`, `--print`, or `--mode json`) sessions, named workflow dispatch waits for the terminal run snapshot and skips pickers. Because human input is runtime-only and workflows no longer carry a declaration-time HIL marker, headless dispatch does not reject a workflow just because its source contains `ctx.ui.*`. If you copy a HIL workflow example into a headless session, it can pass dispatch and then fail when execution reaches the prompt with an error such as `atomic-workflows: interactive ctx.ui.confirm is unavailable in headless (non-interactive) mode; run the workflow in interactive mode or remove the interactive prompt from this stage` (the primitive name varies, including `ctx.ui.custom`). Run those workflows interactively, or guard/remove runtime `ctx.ui.*` calls before using headless mode.
|
|
769
804
|
|
|
770
805
|
<p align="center"><img src="images/workflow-input-picker.png" alt="Workflow Input Picker" width="600" /></p>
|
|
771
806
|
|
|
@@ -831,6 +866,7 @@ workflow({ action: "reload", reason: "added team workflow" })
|
|
|
831
866
|
Control behavior:
|
|
832
867
|
|
|
833
868
|
- `runId` accepts full run ids or unique prefixes for lifecycle and inspection actions. Status lists and run pickers show top-level user-launched workflows; nested child runs are implementation details of the expanded parent graph.
|
|
869
|
+
- `status` / `status <runId>` show terminal `ctx.exit(...)` statuses (`completed`, `skipped`, `cancelled`, or `blocked`) and the optional exit reason when one was supplied.
|
|
834
870
|
- `stages` lists stage summaries, including flattened stages from nested `ctx.workflow(...)` imports and `sessionFile`/`transcriptPath` when a stage has a persisted session. Use `statusFilter: "all"` to include completed, failed, skipped, and pending stages.
|
|
835
871
|
- `stage` returns details for one stage by stage id, unique prefix, or stage name, including nested child stages shown in the expanded graph and the persisted `sessionFile` when available.
|
|
836
872
|
- `transcript` is reference-first with a small preview by default: it returns metadata, transcript paths, and up to 5 recent entries. For targeted lookup, quote the exact `sessionFile`/`transcriptPath` value without changing platform separators (preserve Windows backslashes), search it with `rg` or `grep`, then read only small surrounding ranges. Text results include JSON-escaped `sessionFileJson`/`transcriptPathJson` lines for copy-safe path literals. Pass explicit `tail` or `limit` to override the 5-entry preview; `tail` overrides `limit`; `includeToolOutput` includes captured snapshot tool output in snapshot transcript results.
|
|
@@ -929,7 +965,7 @@ workflow({
|
|
|
929
965
|
})
|
|
930
966
|
```
|
|
931
967
|
|
|
932
|
-
Direct mode supports top-level/default options and per-task options such as `context`, `forkFromSessionFile`, `model`, `fallbackModels`, `thinkingLevel`, `tools`, `noTools`, `customTools`, `mcp`, `output`, `outputMode`, `reads`, `worktree`, `gitWorktreeDir`, `baseBranch`, `maxOutput`, `artifacts`, `sessionDir`, `cwd`, and `agentDir`. Direct chains also support `chainName`, `chainDir`, and `failFast`.
|
|
968
|
+
Direct mode supports top-level/default options and per-task options such as `context`, `forkFromSessionFile`, `model`, `fallbackModels`, `thinkingLevel`, `tools`, `noTools`, `customTools`, `bashPolicy`, `mcp`, `output`, `outputMode`, `reads`, `worktree`, `gitWorktreeDir`, `baseBranch`, `maxOutput`, `artifacts`, `sessionDir`, `cwd`, and `agentDir`. Direct chains also support `chainName`, `chainDir`, and `failFast`.
|
|
933
969
|
|
|
934
970
|
For large fan-outs, prefer `outputMode: "file-only"` so the parent result contains compact file references instead of full output. Treat intercom payloads from async direct runs as user-visible workflow output.
|
|
935
971
|
|
|
@@ -1020,7 +1056,42 @@ Builder basics:
|
|
|
1020
1056
|
|
|
1021
1057
|
`prompt` and `task` are aliases for task text. Prefer `prompt` inside authored workflow files because it mirrors lower-level `stage.prompt(...)`; `task` remains useful in direct tool calls and chain examples.
|
|
1022
1058
|
|
|
1023
|
-
Author workflows to create at least one tracked stage by calling `ctx.task()`, `ctx.chain()`, `ctx.parallel()`, `ctx.stage()`, or `ctx.workflow()` in the run body so each run has graph nodes to inspect, attach to, interrupt, resume, and render.
|
|
1059
|
+
Author workflows to create at least one tracked stage by calling `ctx.task()`, `ctx.chain()`, `ctx.parallel()`, `ctx.stage()`, or `ctx.workflow()` in the run body so each normal run has graph nodes to inspect, attach to, interrupt, resume, and render. Guard-only workflows may call `ctx.exit(...)` before creating a stage when they intentionally stop early.
|
|
1060
|
+
|
|
1061
|
+
### Early exit with `ctx.exit()`
|
|
1062
|
+
|
|
1063
|
+
Use `ctx.exit(options?)` when workflow code intentionally stops the current run from a helper, branch, loop, or precondition guard without classifying the run as failed. `ctx.exit()` throws an executor-owned control signal and is typed as `never`, so code after it is unreachable. In async `.run()` bodies, prefer `return ctx.exit(...)` when the exit is the only path so TypeScript can see the non-returning branch.
|
|
1064
|
+
|
|
1065
|
+
```ts
|
|
1066
|
+
export default defineWorkflow("guarded-import")
|
|
1067
|
+
.output("scanned", Type.Number())
|
|
1068
|
+
.run(async (ctx) => {
|
|
1069
|
+
const files = await findCandidateFiles(ctx.cwd);
|
|
1070
|
+
if (files.length === 0) {
|
|
1071
|
+
return ctx.exit({
|
|
1072
|
+
status: "skipped",
|
|
1073
|
+
reason: "No matching files",
|
|
1074
|
+
outputs: { scanned: 0 },
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const review = await ctx.task("review", { prompt: `Review ${files.join(", ")}` });
|
|
1079
|
+
return { scanned: files.length };
|
|
1080
|
+
})
|
|
1081
|
+
.compile();
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
`ctx.exit()` accepts `status: "completed" | "skipped" | "cancelled" | "blocked"`; it never accepts `"failed"` or `"killed"` because thrown errors and external run-control keep those meanings. `status` defaults to `"completed"`. `reason` is persisted and shown in status surfaces, including the default `/workflow status` list and `/workflow status <runId>` detail, so do not put secrets in it. `outputs` may contain a partial subset of declared outputs; provided keys still must be declared with `.output(...)`, match their TypeBox schema, and be JSON-serializable. Missing required outputs are allowed only on the `ctx.exit(...)` path. Exited runs are terminal and not resumable; external `kill`, `pause`, and `interrupt` keep their existing behavior.
|
|
1085
|
+
|
|
1086
|
+
The first selected `ctx.exit({ outputs })` snapshots its output payload synchronously by value before JavaScript `finally` blocks or cleanup callbacks can mutate the caller-owned object. The snapshot preserves undeclared keys and invalid values until post-cleanup validation, so deleting an undeclared key or changing an invalid value after `ctx.exit(...)` does not change the terminal validation result. If reading `status`, `reason`, or `outputs` options, or enumerating/copying the output snapshot itself, throws, Atomic still selects the exit signal, runs workflow-exit cleanup when feasible, and then records a terminal non-resumable authoring failure (`resumable: false`) if no external terminal control won first.
|
|
1087
|
+
|
|
1088
|
+
After the first `ctx.exit(...)` wins, the executor treats that exit as a level-triggered gate. Later delayed calls to `ctx.stage`, `ctx.task`, `ctx.chain`, `ctx.parallel`, `ctx.workflow`, or graph-backed `ctx.ui.*` prompts rethrow the selected exit signal before creating stages, prompt nodes, child runs, or control handles. Retained `StageContext` handles from before the exit also become inert: `prompt`, `complete`, steering/follow-up, model/thinking controls, tree navigation, compaction, abort, and attached-pane session-realization paths refuse to touch or create an `AgentSession` after the exit is selected. `ctx.parallel` stops dequeuing queued work after exit even with `failFast: false` and limited concurrency; already-started stages and prompt nodes are finalized as `skipped` with a `workflow-exit` reason that prompt-node abort handling preserves instead of overwriting with a generic run-aborted reason.
|
|
1089
|
+
|
|
1090
|
+
Continuation replay also observes the exit gate. Replayed `ctx.stage(...).prompt(...)`, replayed `complete(...)`, graph-backed prompt-node replay, and completed child-boundary replay re-check for a selected exit after their replay microtask and before writing a current-run completed stage end. If `ctx.exit(...)` wins that gap, the pending replay finalizer is skipped/suppressed with the workflow-exit reason instead of creating a misleading completed stage in the resumed run.
|
|
1091
|
+
|
|
1092
|
+
The store is the terminal authority for all run-end races. `ctx.exit(...)` starts cleanup before validating exit outputs, and an external `/workflow kill` can still win the terminal `recordRunEnd` write while that cleanup is pending. When that happens, the SDK `RunResult`, `onRunEnd` callback, live store, and persisted `workflow.run.end` entries all report the canonical `killed` state; the losing `ctx.exit` status or validation failure is not returned and does not append a second run-end entry.
|
|
1093
|
+
|
|
1094
|
+
Control-signal probing is fail-closed. When the executor inspects an arbitrary thrown value or abort reason for internal workflow-exit markers, parent-exit markers, aggregate `errors`, `cause`, `reason`, or `scope`, throwing or inaccessible accessors are treated as “no signal for that branch.” The run then continues through ordinary failure finalization, or the ordinary killed path for external abort reasons, instead of letting author-defined getters escape the executor catch path or be misclassified as `ctx.exit(...)`.
|
|
1024
1095
|
|
|
1025
1096
|
### Guiding Principles
|
|
1026
1097
|
|
|
@@ -1061,7 +1132,7 @@ In TypeScript workflow files, `.input(...)` also narrows `ctx.inputs` for better
|
|
|
1061
1132
|
|
|
1062
1133
|
### Outputs
|
|
1063
1134
|
|
|
1064
|
-
Workflow outputs are runtime contracts for completed workflow runs and for parent workflows that call a child with `ctx.workflow(childWorkflow, ...)`. A workflow returns a JSON-serializable object from `.run()`, and `.output(key, schema)` documents, validates, and exposes keys from that returned object. Primitives, arrays, `null`, functions, symbols, `undefined` properties, `NaN`, and infinite numbers fail validation.
|
|
1135
|
+
Workflow outputs are runtime contracts for completed workflow runs and for parent workflows that call a child with `ctx.workflow(childWorkflow, ...)`. A workflow normally returns a JSON-serializable object from `.run()`, and `.output(key, schema)` documents, validates, and exposes keys from that returned object. `ctx.exit({ outputs })` can expose a partial subset of the same declared output contract when the run intentionally stops early. Primitives, arrays, `null`, functions, symbols, `undefined` properties, `NaN`, and infinite numbers fail validation.
|
|
1065
1136
|
|
|
1066
1137
|
**Return convention:** outputs are return-object keys. Atomic never infers child workflow outputs from stage names, stage order, or the final assistant message. If a parent should read `child.outputs.foo`, the child workflow's `.run()` must both declare `.output("foo", schema)` and return `{ foo: value }`. `result` is not special and is never added for you: to expose `result`, declare `.output("result", schema)` and return `{ result }` exactly like any other output. Returning a key that is not declared with `.output(...)` fails the run with `atomic-workflows: workflow "<name>" returned undeclared output "<key>"; declare it with .output("<key>", Type....) or remove it from the .run() return`.
|
|
1067
1138
|
|
|
@@ -1167,7 +1238,7 @@ Tradeoff: `Type.Unsafe<T>()` does not deeply validate at runtime — it trusts t
|
|
|
1167
1238
|
|
|
1168
1239
|
- `ctx.inputs.x` is `Static<inputSchema>` for the input you declared with `.input("x", schema)` — required and defaulted schemas are always present, and `Type.Optional(...)` adds `| undefined`.
|
|
1169
1240
|
- The `.run()` return is checked against your declared outputs at **compile time** (a missing required output or a wrong value type is a TypeScript error) and at **runtime** via TypeBox `Value` (undeclared keys are rejected and the declared shape is enforced recursively).
|
|
1170
|
-
- `ctx.workflow(child).outputs` is
|
|
1241
|
+
- `ctx.workflow(child)` returns a discriminated child result. When `child.exited === false`, `child.outputs` is the child's full declared `.output(...)` contract; when `child.exited === true`, `child.outputs` is `Partial<TOutputs>` because child `ctx.exit({ outputs })` may intentionally provide only a subset.
|
|
1171
1242
|
|
|
1172
1243
|
Use `Static<typeof schema>` (both `Static` and `TSchema` are re-exported from `@bastani/workflows`) when you need the inferred TypeScript type of a schema directly — for example to type a helper that builds an output value.
|
|
1173
1244
|
|
|
@@ -1209,6 +1280,9 @@ export default defineWorkflow("research-and-synthesize")
|
|
|
1209
1280
|
inputs: { topic: ctx.inputs.topic },
|
|
1210
1281
|
stageName: "run shared research",
|
|
1211
1282
|
});
|
|
1283
|
+
if (child.exited === true) {
|
|
1284
|
+
return ctx.exit({ status: child.status, reason: child.exitReason ?? "shared research stopped early" });
|
|
1285
|
+
}
|
|
1212
1286
|
|
|
1213
1287
|
const final = await ctx.task("synthesize", {
|
|
1214
1288
|
prompt: `Synthesize:\n\n${String(child.outputs.summary)}`,
|
|
@@ -1271,6 +1345,9 @@ export default defineWorkflow("research-then-implement")
|
|
|
1271
1345
|
inputs: { prompt: topic, max_concurrency: 4 },
|
|
1272
1346
|
stageName: "deep research",
|
|
1273
1347
|
});
|
|
1348
|
+
if (research.exited === true) {
|
|
1349
|
+
return ctx.exit({ status: research.status, reason: research.exitReason ?? "deep research stopped early" });
|
|
1350
|
+
}
|
|
1274
1351
|
|
|
1275
1352
|
if (String(ctx.inputs.runner) === "ralph") {
|
|
1276
1353
|
const implementation = await ctx.workflow(ralph, {
|
|
@@ -1280,6 +1357,9 @@ export default defineWorkflow("research-then-implement")
|
|
|
1280
1357
|
},
|
|
1281
1358
|
stageName: "ralph implementation",
|
|
1282
1359
|
});
|
|
1360
|
+
if (implementation.exited === true) {
|
|
1361
|
+
return ctx.exit({ status: implementation.status, reason: implementation.exitReason ?? "ralph stopped early" });
|
|
1362
|
+
}
|
|
1283
1363
|
|
|
1284
1364
|
return {
|
|
1285
1365
|
research_doc_path: research.outputs.research_doc_path,
|
|
@@ -1295,6 +1375,9 @@ export default defineWorkflow("research-then-implement")
|
|
|
1295
1375
|
},
|
|
1296
1376
|
stageName: "goal implementation",
|
|
1297
1377
|
});
|
|
1378
|
+
if (implementation.exited === true) {
|
|
1379
|
+
return ctx.exit({ status: implementation.status, reason: implementation.exitReason ?? "goal stopped early" });
|
|
1380
|
+
}
|
|
1298
1381
|
|
|
1299
1382
|
return {
|
|
1300
1383
|
research_doc_path: research.outputs.research_doc_path,
|
|
@@ -1313,8 +1396,10 @@ Passing a compiled definition directly to `ctx.workflow(...)` uses the child wor
|
|
|
1313
1396
|
|---|---|
|
|
1314
1397
|
| `workflow` | Normalized child workflow name. |
|
|
1315
1398
|
| `runId` | Nested child run id. |
|
|
1316
|
-
| `status` | `completed` when the child
|
|
1317
|
-
| `
|
|
1399
|
+
| `status` | `completed`, or `skipped` / `cancelled` / `blocked` when the child intentionally ended with `ctx.exit(...)`. Failed or externally killed children make the parent child call fail. |
|
|
1400
|
+
| `exited` | `false` for normal child completion; `true` when the child used `ctx.exit(...)` (including `ctx.exit({ status: "completed" })`). |
|
|
1401
|
+
| `outputs` | Full declared child outputs when `exited === false`; partial declared child outputs when `exited === true`. |
|
|
1402
|
+
| `exitReason` | Optional child `ctx.exit({ reason })` text, present only on the `exited === true` branch. |
|
|
1318
1403
|
|
|
1319
1404
|
`ctx.workflow()` options:
|
|
1320
1405
|
|
|
@@ -1327,17 +1412,23 @@ Output exposure rules:
|
|
|
1327
1412
|
|
|
1328
1413
|
```ts
|
|
1329
1414
|
const child = await ctx.workflow(sharedResearch);
|
|
1330
|
-
child.
|
|
1331
|
-
child.outputs.
|
|
1415
|
+
if (child.exited === true) {
|
|
1416
|
+
child.outputs.summary; // string | undefined: ctx.exit({ outputs }) may be partial
|
|
1417
|
+
} else {
|
|
1418
|
+
child.outputs.summary; // string: normal completion returned the full declared contract
|
|
1419
|
+
child.outputs.sources; // string[] | undefined: optional output declared by sharedResearch
|
|
1420
|
+
}
|
|
1332
1421
|
```
|
|
1333
1422
|
|
|
1334
|
-
A child exposes exactly its declared outputs — the keys it declared with `.output(...)` and returned from `.run()`. There are no implicit outputs and no raw return-object passthrough. If `.run()` returns a key that was not declared with `.output(...)`, the child run fails with `atomic-workflows: workflow "<childName>" returned undeclared output "<key>"; declare it with .output("<key>", Type....) or remove it from the .run() return`, and the parent surfaces that failure through the wrapper `atomic-workflows: child workflow "<childName>" (<displayName>) failed with status failed: ...`. A child with no declared outputs therefore exposes no outputs. Missing required outputs, schema type mismatches, and non-JSON-serializable returned values fail
|
|
1423
|
+
A child exposes exactly its declared outputs — the keys it declared with `.output(...)` and returned from `.run()` or supplied to `ctx.exit({ outputs })`. There are no implicit outputs and no raw return-object passthrough. If `.run()` returns a key that was not declared with `.output(...)`, the child run fails with `atomic-workflows: workflow "<childName>" returned undeclared output "<key>"; declare it with .output("<key>", Type....) or remove it from the .run() return`, and the parent surfaces that failure through the wrapper `atomic-workflows: child workflow "<childName>" (<displayName>) failed with status failed: ...`. A child with no declared outputs therefore exposes no outputs. Missing required outputs, schema type mismatches, and non-JSON-serializable returned values fail normal child completion before the parent continues; child `ctx.exit({ outputs })` allows missing required outputs but still validates every provided key and sets `child.exited === true` so parent code must handle the partial shape.
|
|
1335
1424
|
|
|
1336
1425
|
Only compiled workflow definitions can be passed to `ctx.workflow(...)`. Import reusable workflows with TypeScript `import` statements first; use `/workflow` names such as `goal` only for launching named runs, not as `ctx.workflow(...)` arguments. If a module is missing or does not export a compiled workflow definition, workflow discovery fails when loading that module. Nested child workflows count against `maxDepth` (default `4` total workflow levels).
|
|
1337
1426
|
|
|
1338
|
-
The graph includes both the parent boundary node and the imported child workflow's own stages while the child is loading/running, so the user can observe progress and interrupt sub-workflows before they complete. Completed boundaries still retain the child workflow name, child run id prefix, and exposed output count for replay/debugging. Use `stageName` when the parent needs a more specific label, but keep it concise so the child summary remains readable in the graph.
|
|
1427
|
+
The graph includes both the parent boundary node and the imported child workflow's own stages while the child is loading/running, so the user can observe progress and interrupt sub-workflows before they complete. Completed boundaries still retain the child workflow name, child run id prefix, and exposed output count for replay/debugging. Skipped or failed boundaries do not retain child-edge metadata (`workflowChild` / `workflowChildRun`), and graph expansion ignores any stale non-completed boundary metadata from older persisted sessions instead of flattening an unrelated child run. Use `stageName` when the parent needs a more specific label, but keep it concise so the child summary remains readable in the graph.
|
|
1428
|
+
|
|
1429
|
+
If a parent workflow exits through `ctx.exit(...)` while a child workflow is in flight, the parent executor only skips the parent boundary and sends the child a typed parent-exit abort reason. The hidden child executor owns child cleanup: active child stages and prompt nodes are skipped for `workflow-exit`, live child stage handles/sessions are disposed, and the child run is finalized as terminal `cancelled` (not `killed`) and non-resumable. The child executor writes each skipped child `workflow.stage.end` exactly once before its child `workflow.run.end`, and parent exit finalization waits for that child cleanup before writing the parent `workflow.run.end`, so restored sessions do not reconstruct the child as interrupted or failed. The skipped parent boundary clears any live child-run edge before store or persistence updates, so status/graph views do not display stale child stages from a boundary that did not complete. A delayed parent branch that calls `ctx.workflow(...)` after the exit gate is selected does not create a boundary or child run.
|
|
1339
1430
|
|
|
1340
|
-
Continuation replay treats the parent child-workflow boundary as the durable checkpoint: a previously completed child boundary replays with the original exposed outputs and without re-running the child, while a child that failed or was interrupted before completion starts again from the beginning on continuation.
|
|
1431
|
+
Continuation replay treats the parent child-workflow boundary as the durable checkpoint: a previously completed child boundary replays with the original exposed outputs and without re-running the child, while a child that failed or was interrupted before completion starts again from the beginning on continuation. If `ctx.exit(...)` wins while a completed boundary is being replayed but before replay finalization, the boundary is finalized as skipped and its preloaded child metadata is omitted from store, persistence, restore, and expanded graph views.
|
|
1341
1432
|
|
|
1342
1433
|
## Workflow Primitives
|
|
1343
1434
|
|
|
@@ -1381,10 +1472,31 @@ Common task/stage options include:
|
|
|
1381
1472
|
- `previous` for small handoff context; use artifact paths plus `reads` for large outputs, logs, research bundles, or reviewer payloads
|
|
1382
1473
|
- `context: "fresh" | "fork"`, `forkFromSessionFile`
|
|
1383
1474
|
- `model`, `fallbackModels`, `thinkingLevel`, `scopedModels`, `modelRegistry` — `model` and each `fallbackModels` entry accept a `model_name:thinking_effort` reasoning suffix; the standalone `thinkingLevel` is deprecated (see [Reasoning levels](#reasoning-levels))
|
|
1384
|
-
- `tools`, `noTools`, `customTools`, `mcp: { allow?: string[], deny?: string[] }`
|
|
1475
|
+
- `tools`, `noTools`, `customTools`, `mcp: { allow?: string[], deny?: string[] }`, `bashPolicy`
|
|
1385
1476
|
- `output`, `outputMode`, `reads`, `worktree`, `gitWorktreeDir`, `baseBranch`, `maxOutput`, `artifacts`, `sessionDir`, `cwd`, `agentDir`
|
|
1386
1477
|
- advanced host-supplied SDK seams: `authStorage`, `resourceLoader`, `sessionManager`, `settingsManager`, `sessionStartEvent`
|
|
1387
1478
|
|
|
1479
|
+
`bashPolicy` scopes the built-in `bash` tool for one stage or task. `tools` must still include `"bash"` (or leave it available by default); the policy only narrows command text after the shell tool is exposed. It supports exact strings, `{ prefix }`, command-string `{ glob }`, and `{ regex, flags? }` rules, `default: "allow" | "deny"` (default `"allow"`), `deny` precedence, and `match: "segments" | "whole"` (default `"segments"`). Omitting `bashPolicy`, passing `{}`, or passing a default-allow policy with no `allow`/`deny` rules (including empty arrays or match-only default-allow policies) preserves legacy behavior and does not parse commands; malformed policy shapes such as unknown top-level keys (`denny`, `extra`), non-array `allow`/`deny`, invalid rule objects, invalid regexes, invalid glob bracket ranges, or stateful `g`/`y` regex flags fail closed as `invalid-policy`. Segment mode checks each command in pipelines/chains/substitutions before execution, treats unquoted LF, CRLF, and bare CR as command separators, keeps non-leading Bash `>|` noclobber redirections inside the current command segment, and rejects reserved/compound shell heads, leading redirections, attached command-head redirections, and command heads that are not literal words.
|
|
1480
|
+
|
|
1481
|
+
```ts
|
|
1482
|
+
await ctx.task("browser-preview", {
|
|
1483
|
+
tools: ["bash"],
|
|
1484
|
+
bashPolicy: {
|
|
1485
|
+
default: "deny",
|
|
1486
|
+
allow: [
|
|
1487
|
+
"which browse",
|
|
1488
|
+
{ prefix: "browse open " },
|
|
1489
|
+
{ prefix: "browse snapshot" },
|
|
1490
|
+
{ prefix: "grep " },
|
|
1491
|
+
],
|
|
1492
|
+
deny: [{ regex: "\\brm\\b" }],
|
|
1493
|
+
},
|
|
1494
|
+
prompt: "Open the preview with browse, then summarize the visible state.",
|
|
1495
|
+
});
|
|
1496
|
+
```
|
|
1497
|
+
|
|
1498
|
+
A command such as `browse snapshot | grep title` passes only when both segments are allowed, and `browse snapshot\nrm -rf /tmp/proof` cannot be hidden behind a `{ prefix: "browse " }` rule because the newline starts a new segment. Glob rules match command strings rather than filesystem path segments: `*` and `?` may span `/`, so `{ glob: "browse *" }` matches URLs and slash-bearing paths such as `browse http://localhost:3000`, `browse docs/index.html`, and `browse ./preview/output.html` while still matching the whole target rather than `echo browse ...`; escaped bracket-class metacharacters such as `\-`, `\^`, `\]`, `\[`, and `\\` stay literal, while malformed glob ranges such as `{ glob: "echo [z-a]" }` become `invalid-policy` denials. Segment mode accepts literal heads such as `grep`, `./script`, `/usr/bin/env`, `bun`, and `browse`, and treats non-leading `>|` as redirection syntax so `echo ok >|/tmp/out` stays one segment, but conservatively rejects reserved or compound heads (`coproc`, `if`, `for`, `while`, `case`, `{`, `}`, `!`), leading redirections (`>file cmd`, `2>file cmd`, `<file cmd`, `&>file cmd`, `&>>file cmd`, `>|file cmd`, `<&0 cmd`, `>&2 cmd`), redirections attached to the command-head word (`cmd>file`, `cmd>>file`, `cmd>|file`, `cmd2>file`, `cmd>&2`, `cmd</tmp/in`), leading environment assignments (`PATH=/tmp:$PATH browse snapshot`, `LD_PRELOAD=/tmp/x browse snapshot`, `FOO=bar`), dynamic heads such as `$cmd`, `${cmd}`, `r''m`, `r\m`, `~/bin/rm`, `r*m`, `{rm,echo}`, `r$(printf m)`, or backtick-built command names. A single denied, redirection-prefixed, attached-redirection, assignment-prefixed, dynamic, or unrecognized segment blocks the whole command with a model-readable tool error and no UI prompt, so the behavior works in headless workflow runs. Use `match: "whole"` only when raw-command matching is intentional.
|
|
1499
|
+
|
|
1388
1500
|
`gitWorktreeDir` selects a reusable Git worktree root for `ctx.stage`, `ctx.task`, `ctx.chain`, and `ctx.parallel`. If the path is missing, Atomic creates it with `git worktree add --detach <path> <baseBranch>`; if it exists, it must be a same-repository worktree root. The default stage cwd becomes the matching cwd inside the worktree and preserves the invoking repo-relative subdirectory. Explicit `cwd` still wins; relative `cwd` values resolve from the worktree cwd, while absolute `cwd` values are used as provided. `gitWorktreeDir` is mutually exclusive with `worktree: true`: use `gitWorktreeDir` for named/reusable worktrees and `worktree: true` for temporary direct-mode worktrees that are cleaned up after the run.
|
|
1389
1501
|
|
|
1390
1502
|
To bind user inputs to a workflow-wide worktree default, use the builder method:
|
|
@@ -1438,7 +1550,7 @@ This applies everywhere a stage accepts a model: direct `ctx.task`/`ctx.chain`/`
|
|
|
1438
1550
|
- `/workflow <name> key=value ...` for interactive named runs
|
|
1439
1551
|
- `/workflow connect|attach|pause|interrupt|resume|status|inputs|reload` for live control, inspection, and rediscovery
|
|
1440
1552
|
- the `workflow` tool for agent-initiated orchestration and direct one-off runs
|
|
1441
|
-
Workflow definition files must export definitions produced by `defineWorkflow(...).compile()`. The former imperative object-form runner is not part of the public SDK, and authored workflow files cannot import `runWorkflow` from `@bastani/workflows`.
|
|
1553
|
+
Workflow definition files must export definitions produced by `defineWorkflow(...).compile()`. Keep non-workflow runtime helpers (widget factories, shared utilities) in a subdirectory the discovery scan ignores, such as `.atomic/workflows/lib/` — see [Workflow Locations](#workflow-locations). The former imperative object-form runner is not part of the public SDK, and authored workflow files cannot import `runWorkflow` from `@bastani/workflows`.
|
|
1442
1554
|
|
|
1443
1555
|
Standalone TypeScript workflow packages type-check the SDK import with no hand-authored `.d.ts`, no `declare module` shim, and no `tsconfig` `paths` alias. The SDK types ship with `@bastani/atomic`, so a workflow package depends only on `@bastani/atomic` (plus a `typebox` peer):
|
|
1444
1556
|
|
|
@@ -1677,7 +1789,7 @@ Good workflows are information-flow systems, not just prompt sequences. Keep sta
|
|
|
1677
1789
|
- Do not use legacy workflow tool fields like `agent`, `stage`, or run-control `name`.
|
|
1678
1790
|
- Do not pass strings such as `"goal"` or path objects to `ctx.workflow(...)`; import the compiled workflow definition from `@bastani/workflows/builtin` or another TypeScript module first.
|
|
1679
1791
|
- Do not rely on undeclared child outputs; returning a key that is not declared with `.output(...)` fails the run. Declare `.output(...)` for every child-workflow field you expose — including `result` — and return values matching those schemas from `.run()`.
|
|
1680
|
-
- Do not expect to select or rename child outputs at the call site; parent workflows receive the child's declared output contract as `child.outputs`.
|
|
1792
|
+
- Do not expect to select or rename child outputs at the call site; parent workflows receive the child's declared output contract as `child.outputs` after checking `child.exited === false`, and a partial declared-output map when `child.exited === true`.
|
|
1681
1793
|
- Do not expect named workflow runs to block the chat turn; they are background tasks.
|
|
1682
1794
|
- Do not call `kill` when the user asks to interrupt or pause resumably.
|
|
1683
1795
|
- Keep stage names readable because they appear in workflow status and UI.
|