@bastani/atomic 0.5.0 → 0.5.1-0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.5.0",
3
+ "version": "0.5.1-0",
4
4
  "description": "Configuration management CLI and SDK for coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -2,25 +2,25 @@
2
2
  * Workflow Builder — defines a workflow with a single `.run()` entry point.
3
3
  *
4
4
  * Usage:
5
- * defineWorkflow({ name: "my-workflow", description: "..." })
5
+ * defineWorkflow<"copilot">({ name: "my-workflow", description: "..." })
6
6
  * .run(async (ctx) => {
7
- * await ctx.session({ name: "research" }, async (s) => { ... });
8
- * await ctx.session({ name: "plan" }, async (s) => { ... });
7
+ * await ctx.stage({ name: "research" }, {}, {}, async (s) => { ... });
8
+ * await ctx.stage({ name: "plan" }, {}, {}, async (s) => { ... });
9
9
  * })
10
10
  * .compile()
11
11
  */
12
12
 
13
- import type { WorkflowOptions, WorkflowContext, WorkflowDefinition } from "./types.ts";
13
+ import type { AgentType, WorkflowOptions, WorkflowContext, WorkflowDefinition } from "./types.ts";
14
14
 
15
15
  /**
16
16
  * Chainable workflow builder. Records the run callback,
17
17
  * then .compile() seals it into a WorkflowDefinition.
18
18
  */
19
- export class WorkflowBuilder {
19
+ export class WorkflowBuilder<A extends AgentType = AgentType> {
20
20
  /** @internal Brand for detection across package boundaries */
21
21
  readonly __brand = "WorkflowBuilder" as const;
22
22
  private readonly options: WorkflowOptions;
23
- private runFn: ((ctx: WorkflowContext) => Promise<void>) | null = null;
23
+ private runFn: ((ctx: WorkflowContext<A>) => Promise<void>) | null = null;
24
24
 
25
25
  constructor(options: WorkflowOptions) {
26
26
  this.options = options;
@@ -29,12 +29,12 @@ export class WorkflowBuilder {
29
29
  /**
30
30
  * Set the workflow's entry point.
31
31
  *
32
- * The callback receives a {@link WorkflowContext} with `session()` for
32
+ * The callback receives a {@link WorkflowContext} with `stage()` for
33
33
  * spawning agent sessions, and `transcript()` / `getMessages()` for
34
34
  * reading completed session outputs. Use native TypeScript control flow
35
35
  * (loops, conditionals, `Promise.all()`) for orchestration.
36
36
  */
37
- run(fn: (ctx: WorkflowContext) => Promise<void>): this {
37
+ run(fn: (ctx: WorkflowContext<A>) => Promise<void>): this {
38
38
  if (this.runFn) {
39
39
  throw new Error("run() can only be called once per workflow.");
40
40
  }
@@ -51,7 +51,7 @@ export class WorkflowBuilder {
51
51
  * After calling compile(), the returned object is consumed by the
52
52
  * Atomic CLI runtime.
53
53
  */
54
- compile(): WorkflowDefinition {
54
+ compile(): WorkflowDefinition<A> {
55
55
  if (!this.runFn) {
56
56
  throw new Error(
57
57
  `Workflow "${this.options.name}" has no run callback. ` +
@@ -73,29 +73,45 @@ export class WorkflowBuilder {
73
73
  /**
74
74
  * Entry point for defining a workflow.
75
75
  *
76
+ * Pass a type parameter to narrow all context types to a specific agent:
77
+ *
76
78
  * @example
77
79
  * ```typescript
78
80
  * import { defineWorkflow } from "@bastani/atomic/workflows";
79
81
  *
80
- * export default defineWorkflow({
82
+ * export default defineWorkflow<"copilot">({
81
83
  * name: "hello",
82
84
  * description: "Two-session demo",
83
85
  * })
84
86
  * .run(async (ctx) => {
85
- * const describe = await ctx.session({ name: "describe" }, async (s) => {
86
- * // ... agent SDK code using s.serverUrl, s.paneId, s.save() ...
87
- * });
88
- * await ctx.session({ name: "summarize" }, async (s) => {
89
- * const research = await s.transcript(describe);
90
- * // ...
91
- * });
87
+ * const describe = await ctx.stage(
88
+ * { name: "describe" },
89
+ * {},
90
+ * {},
91
+ * async (s) => {
92
+ * // s.client: CopilotClient, s.session: CopilotSession
93
+ * await s.session.sendAndWait({ prompt: s.userPrompt });
94
+ * s.save(await s.session.getMessages());
95
+ * },
96
+ * );
97
+ * await ctx.stage(
98
+ * { name: "summarize" },
99
+ * {},
100
+ * {},
101
+ * async (s) => {
102
+ * const research = await s.transcript(describe);
103
+ * // ...
104
+ * },
105
+ * );
92
106
  * })
93
107
  * .compile();
94
108
  * ```
95
109
  */
96
- export function defineWorkflow(options: WorkflowOptions): WorkflowBuilder {
110
+ export function defineWorkflow<A extends AgentType = AgentType>(
111
+ options: WorkflowOptions,
112
+ ): WorkflowBuilder<A> {
97
113
  if (!options.name || options.name.trim() === "") {
98
114
  throw new Error("Workflow name is required.");
99
115
  }
100
- return new WorkflowBuilder(options);
116
+ return new WorkflowBuilder<A>(options);
101
117
  }
package/src/sdk/index.ts CHANGED
@@ -25,6 +25,10 @@ export type {
25
25
  WorkflowContext,
26
26
  WorkflowOptions,
27
27
  WorkflowDefinition,
28
+ StageClientOptions,
29
+ StageSessionOptions,
30
+ ProviderClient,
31
+ ProviderSession,
28
32
  } from "./types.ts";
29
33
 
30
34
  // Workflow SDK (also available as atomic/workflows subpath)
@@ -283,6 +283,92 @@ export async function claudeQuery(options: ClaudeQueryOptions): Promise<ClaudeQu
283
283
  return { output: lastContent || capturePaneScrollback(paneId), delivered };
284
284
  }
285
285
 
286
+ // ---------------------------------------------------------------------------
287
+ // Synthetic wrappers — uniform s.client / s.session API for Claude stages
288
+ // ---------------------------------------------------------------------------
289
+
290
+ /**
291
+ * Default query options the user can set per-stage via the `sessionOpts` arg.
292
+ * These become defaults for every `s.session.query()` call within that stage.
293
+ */
294
+ export interface ClaudeQueryDefaults {
295
+ /** Timeout in ms waiting for Claude to finish responding (default: 300s) */
296
+ timeoutMs?: number;
297
+ /** Polling interval in ms (default: 2000) */
298
+ pollIntervalMs?: number;
299
+ /** Number of C-m presses per submit round (default: 1) */
300
+ submitPresses?: number;
301
+ /** Max submit rounds if text isn't consumed (default: 6) */
302
+ maxSubmitRounds?: number;
303
+ /** Timeout in ms waiting for pane to be ready before sending (default: 30s) */
304
+ readyTimeoutMs?: number;
305
+ }
306
+
307
+ /**
308
+ * Synthetic client wrapper for Claude stages.
309
+ * Auto-starts the Claude CLI in the tmux pane during `start()`.
310
+ */
311
+ export class ClaudeClientWrapper {
312
+ readonly paneId: string;
313
+ private readonly opts: { chatFlags?: string[]; readyTimeoutMs?: number };
314
+
315
+ constructor(
316
+ paneId: string,
317
+ opts: { chatFlags?: string[]; readyTimeoutMs?: number } = {},
318
+ ) {
319
+ this.paneId = paneId;
320
+ this.opts = opts;
321
+ }
322
+
323
+ /** Start the Claude CLI in the tmux pane. Called by the runtime during init. */
324
+ async start(): Promise<void> {
325
+ await createClaudeSession({
326
+ paneId: this.paneId,
327
+ chatFlags: this.opts.chatFlags,
328
+ readyTimeoutMs: this.opts.readyTimeoutMs,
329
+ });
330
+ }
331
+
332
+ /** Noop — cleanup is handled by the runtime via `clearClaudeSession`. */
333
+ async stop(): Promise<void> {}
334
+ }
335
+
336
+ /**
337
+ * Synthetic session wrapper for Claude stages.
338
+ * Wraps `claudeQuery()` so users call `s.session.query(prompt)`.
339
+ */
340
+ export class ClaudeSessionWrapper {
341
+ readonly paneId: string;
342
+ readonly sessionId: string;
343
+ private readonly defaults: ClaudeQueryDefaults;
344
+
345
+ constructor(
346
+ paneId: string,
347
+ sessionId: string,
348
+ defaults: ClaudeQueryDefaults = {},
349
+ ) {
350
+ this.paneId = paneId;
351
+ this.sessionId = sessionId;
352
+ this.defaults = defaults;
353
+ }
354
+
355
+ /** Send a prompt to Claude and wait for the response. */
356
+ async query(
357
+ prompt: string,
358
+ opts?: Partial<ClaudeQueryDefaults>,
359
+ ): Promise<ClaudeQueryResult> {
360
+ return claudeQuery({
361
+ paneId: this.paneId,
362
+ prompt,
363
+ ...this.defaults,
364
+ ...opts,
365
+ });
366
+ }
367
+
368
+ /** Noop — for API symmetry with CopilotSession.disconnect(). */
369
+ async disconnect(): Promise<void> {}
370
+ }
371
+
286
372
  // ---------------------------------------------------------------------------
287
373
  // Static source validation
288
374
  // ---------------------------------------------------------------------------
@@ -295,21 +381,28 @@ export interface ClaudeValidationWarning {
295
381
  /**
296
382
  * Validate a Claude workflow source file for common mistakes.
297
383
  *
298
- * Checks that `createClaudeSession` is called when `claudeQuery` is used,
299
- * paralleling the validation patterns for Copilot and OpenCode workflows.
384
+ * Warns on direct usage of createClaudeSession/claudeQuery the runtime
385
+ * now handles init/cleanup automatically via s.client and s.session.
300
386
  */
301
387
  export function validateClaudeWorkflow(source: string): ClaudeValidationWarning[] {
302
388
  const warnings: ClaudeValidationWarning[] = [];
303
389
 
390
+ if (/\bcreateClaudeSession\b/.test(source)) {
391
+ warnings.push({
392
+ rule: "claude/manual-session",
393
+ message:
394
+ "Manual createClaudeSession() call detected. The runtime auto-starts the Claude CLI — " +
395
+ "use s.session.query() instead of claudeQuery(). Pass chatFlags via the second arg to ctx.stage().",
396
+ });
397
+ }
398
+
304
399
  if (/\bclaudeQuery\b/.test(source)) {
305
- if (!/\bcreateClaudeSession\b/.test(source)) {
306
- warnings.push({
307
- rule: "claude/create-session",
308
- message:
309
- "Could not verify that createClaudeSession is called before claudeQuery(). " +
310
- "Call createClaudeSession({ paneId: s.paneId }) to start the Claude CLI before sending queries.",
311
- });
312
- }
400
+ warnings.push({
401
+ rule: "claude/manual-query",
402
+ message:
403
+ "Direct claudeQuery() call detected. Use s.session.query(prompt) instead — " +
404
+ "it wraps claudeQuery with the correct paneId.",
405
+ });
313
406
  }
314
407
 
315
408
  return warnings;
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Copilot workflow source validation.
3
3
  *
4
- * Checks that Copilot workflow source files follow required patterns:
5
- * - `cliUrl` is wired to the session context's `serverUrl`
6
- * - `setForegroundSessionId` is called after creating a session
4
+ * Checks that Copilot workflow source files use the runtime-managed
5
+ * `s.client` and `s.session` instead of manual SDK client creation.
7
6
  */
8
7
 
9
8
  export interface CopilotValidationWarning {
@@ -17,28 +16,22 @@ export interface CopilotValidationWarning {
17
16
  export function validateCopilotWorkflow(source: string): CopilotValidationWarning[] {
18
17
  const warnings: CopilotValidationWarning[] = [];
19
18
 
20
- if (/\bCopilotClient\b/.test(source)) {
21
- // Accept any identifier before .serverUrl (e.g., s.serverUrl, ctx.serverUrl)
22
- // or a destructured `serverUrl` variable
23
- if (!/cliUrl\s*:\s*(?:\w+\.serverUrl|serverUrl)/.test(source)) {
24
- warnings.push({
25
- rule: "copilot/cli-url",
26
- message:
27
- "Could not verify that CopilotClient is created with { cliUrl: s.serverUrl }. " +
28
- "This is required to connect to the workflow's agent pane.",
29
- });
30
- }
19
+ if (/\bnew\s+CopilotClient\b/.test(source)) {
20
+ warnings.push({
21
+ rule: "copilot/manual-client",
22
+ message:
23
+ "Manual CopilotClient creation detected. Use s.client instead — " +
24
+ "the runtime auto-creates and cleans up the client.",
25
+ });
31
26
  }
32
27
 
33
- if (/\bcreateSession\b/.test(source)) {
34
- if (!/\bsetForegroundSessionId\b/.test(source)) {
35
- warnings.push({
36
- rule: "copilot/foreground-session",
37
- message:
38
- "Could not verify that setForegroundSessionId is called after createSession(). " +
39
- "Call client.setForegroundSessionId(session.sessionId) so the TUI displays the workflow session.",
40
- });
41
- }
28
+ if (/\bclient\.createSession\b/.test(source)) {
29
+ warnings.push({
30
+ rule: "copilot/manual-session",
31
+ message:
32
+ "Manual createSession() call detected. Use s.session instead — " +
33
+ "the runtime auto-creates the session. Pass session config as the third arg to ctx.stage().",
34
+ });
42
35
  }
43
36
 
44
37
  return warnings;
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * OpenCode workflow source validation.
3
3
  *
4
- * Checks that OpenCode workflow source files follow required patterns:
5
- * - `baseUrl` is wired to the session context's `serverUrl`
6
- * - `tui.selectSession` is called after creating a session
4
+ * Checks that OpenCode workflow source files use the runtime-managed
5
+ * `s.client` and `s.session` instead of manual SDK client creation.
7
6
  */
8
7
 
9
8
  export interface OpenCodeValidationWarning {
@@ -18,27 +17,21 @@ export function validateOpenCodeWorkflow(source: string): OpenCodeValidationWarn
18
17
  const warnings: OpenCodeValidationWarning[] = [];
19
18
 
20
19
  if (/\bcreateOpencodeClient\b/.test(source)) {
21
- // Accept any identifier before .serverUrl (e.g., s.serverUrl, ctx.serverUrl)
22
- // or a destructured `serverUrl` variable
23
- if (!/baseUrl\s*:\s*(?:\w+\.serverUrl|serverUrl)/.test(source)) {
24
- warnings.push({
25
- rule: "opencode/base-url",
26
- message:
27
- "Could not verify that createOpencodeClient is called with { baseUrl: s.serverUrl }. " +
28
- "This is required to connect to the workflow's agent pane.",
29
- });
30
- }
20
+ warnings.push({
21
+ rule: "opencode/manual-client",
22
+ message:
23
+ "Manual createOpencodeClient() call detected. Use s.client instead — " +
24
+ "the runtime auto-creates the client. Pass client config as the second arg to ctx.stage().",
25
+ });
31
26
  }
32
27
 
33
- if (/\bsession\.create\b/.test(source)) {
34
- if (!/\btui\.selectSession\b/.test(source)) {
35
- warnings.push({
36
- rule: "opencode/select-session",
37
- message:
38
- "Could not verify that tui.selectSession is called after session.create(). " +
39
- "Call client.tui.selectSession({ sessionID }) so the TUI displays the workflow session.",
40
- });
41
- }
28
+ if (/\bclient\.session\.create\b/.test(source)) {
29
+ warnings.push({
30
+ rule: "opencode/manual-session",
31
+ message:
32
+ "Manual client.session.create() call detected. Use s.session instead — " +
33
+ "the runtime auto-creates the session. Pass session config as the third arg to ctx.stage().",
34
+ });
42
35
  }
43
36
 
44
37
  return warnings;