@replayio/app-building 1.24.1 → 1.26.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/README.md CHANGED
@@ -80,7 +80,7 @@ The agent can also run `list-secrets` to see which secrets are available, and `s
80
80
 
81
81
  | Export | Description |
82
82
  |---|---|
83
- | `ContainerConfig` | `infisical` (required `InfisicalConfig`), optional `projectRoot` (local Docker only), `registry`, `flyToken`/`flyApp` (set both for remote Fly.io), `imageRef`, `webhookUrl`/`webhookSecret`, `taskWebhookUrl` (GET endpoint for external task queue), `detached`, `initialPrompt`, `localPort`, `absorbTasks`, `namePrefix` (default: `"app-building"`). |
83
+ | `ContainerConfig` | `infisical` (required `InfisicalConfig`), optional `projectRoot` (local Docker only), `registry`, `flyToken`/`flyApp` (set both for remote Fly.io), `imageRef`, `webhookUrl`/`webhookSecret`, `taskWebhookUrl` (GET endpoint for external task queue), `addTaskWebhookUrl` (POST endpoint for tasks added by `add-task` script), `detached`, `initialPrompt`, `localPort`, `absorbTasks`, `namePrefix` (default: `"app-building"`). |
84
84
  | `RepoOptions` | Per-invocation git settings: `repoUrl`, `cloneBranch`, `pushBranch`. |
85
85
  | `AgentState` | Returned by `startContainer`. Contains `type`, `containerName`, `port`, `baseUrl`, and Fly-specific fields for remote containers. |
86
86
  | `ContainerRegistry` | Interface for container registry storage. Methods: `log`, `markStopped`, `clearStopped`, `getRecent`, `find`, `findAlive`. |
@@ -144,6 +144,13 @@ The agent can also run `list-secrets` to see which secrets are available, and `s
144
144
  |---|---|
145
145
  | `getImageRef()` | Returns `CONTAINER_IMAGE_REF` env var, or `ghcr.io/replayio/app-building:latest` by default. |
146
146
 
147
+ ### Task dependencies
148
+
149
+ | Export | Description |
150
+ |---|---|
151
+ | `findReadyTask(pendingTasks, completedTasks)` | Find the first task in `pendingTasks` whose `requiredTaskIds` are all satisfied. A required task is satisfied when it appears in `completedTasks` and has no children still pending or with incomplete children of their own (recursive). Returns the task, or `null` if all are blocked. |
152
+ | `Task` | Interface for task queue entries — includes `skill`, `subtasks`, `timestamp`, and optional fields like `id`, `requiredTaskIds`, `parentTaskId`, `command`, etc. |
153
+
147
154
  ## Container HTTP API
148
155
 
149
156
  Each container runs an HTTP server that accepts the following requests:
@@ -196,6 +203,32 @@ Each task in the queue (local or webhook) has the following fields:
196
203
  | `command` | `string` | No | Custom command to run instead of the default `claude` CLI. |
197
204
  | `maxAttempts` | `number` | No | Maximum attempts before giving up. Default: 5. |
198
205
  | `timeoutMinutes` | `number` | No | Per-attempt time limit in minutes. Agent is killed if exceeded. |
206
+ | `setup` | `string` | No | Shell command to run once when the task starts (before the first agent attempt). Task fails if it exits non-zero. |
207
+ | `requiredTaskIds` | `string[]` | No | Task IDs that must complete before this task can start. A task is "complete" when it and all its child tasks have finished. |
208
+ | `parentTaskId` | `string` | No | ID of the task that spawned this task (set automatically by `add-task`). Used for dependency resolution — a required task isn't considered complete until all its children are done. |
209
+
210
+ ### Task dependencies
211
+
212
+ Tasks can declare dependencies on other tasks via `requiredTaskIds`. The worker skips blocked
213
+ tasks and picks the first task whose dependencies are all satisfied. A dependency is satisfied
214
+ when the required task has completed **and** all of its child tasks (tasks with matching
215
+ `parentTaskId`) have also completed. This means a task that depends on a parent will
216
+ automatically wait for all work the parent spawned.
217
+
218
+ By default, `add-task` chains tasks serially — each task depends on the previous one. Use
219
+ `--parallel` to add tasks that can run in any order:
220
+
221
+ ```bash
222
+ # Serial (default): B waits for A, C waits for B
223
+ npx tsx /repo/scripts/add-task.ts <<'EOF'
224
+ [{ "skill": "...", "subtasks": ["A"] }, { "skill": "...", "subtasks": ["B"] }, { "skill": "...", "subtasks": ["C"] }]
225
+ EOF
226
+
227
+ # Parallel: A, B, C can run in any order
228
+ npx tsx /repo/scripts/add-task.ts --parallel <<'EOF'
229
+ [{ "skill": "...", "subtasks": ["A"] }, { "skill": "...", "subtasks": ["B"] }, { "skill": "...", "subtasks": ["C"] }]
230
+ EOF
231
+ ```
199
232
 
200
233
  ### External task queue
201
234
 
@@ -209,6 +242,13 @@ local task queue is empty, the container GETs this URL (with the same `Bearer` t
209
242
  The first task is processed immediately; any remaining tasks are added to the local queue.
210
243
  Tasks use the same structure documented above.
211
244
 
245
+ ### Add-task webhook
246
+
247
+ Set `config.addTaskWebhookUrl` to a POST endpoint that receives tasks added by the `add-task`
248
+ script. When set, the agent's `add-task` calls POST tasks to this URL instead of writing to
249
+ the local task file. The request body is `{ "tasks": [...] }` with the same task structure
250
+ documented above. Auth uses the same `Bearer` token from `webhookSecret`.
251
+
212
252
  ## Webhooks
213
253
 
214
254
  Set `webhookUrl` on `ContainerConfig` to receive real-time notifications of container activity. The container POSTs JSON to that URL on key events (no retries; failures are logged to stderr). Set `webhookSecret` to include a `Bearer` token in the `Authorization` header for authenticating webhook requests.
@@ -245,8 +285,8 @@ Every POST body has this shape:
245
285
  | `message.started` | Message processing begins | `iteration`, `prompt` |
246
286
  | `message.done` | Message processing complete | `messageId`, `cost_usd`, `duration_ms`, `num_turns` |
247
287
  | `message.error` | Message processing failed | `messageId`, `error` |
248
- | `task.started` | Task processing begins | `id`, `iteration`, `skill`, `subtasks`, `prompt` |
249
- | `task.done` | Task processing complete | `id`, `skill`, `subtasks`, `prompt`, `cost`, `totalCost`, `failed`, `pendingTasks`, `duration_ms` |
288
+ | `task.started` | Task processing begins | All `Task` fields (`id`, `skill`, `subtasks`, `timestamp`, `app`, `prompt`, `command`, `maxAttempts`, `timeoutMinutes`, `setup`, `requiredTaskIds`, `parentTaskId`) + `iteration` |
289
+ | `task.done` | Task processing complete | All `Task` fields + `cost`, `totalCost`, `failed`, `pendingTasks`, `duration_ms` |
250
290
  | `log` | Each log line | `line` |
251
291
 
252
292
  ### Example
package/dist/index.d.ts CHANGED
@@ -1,6 +1,188 @@
1
- export * from "./container";
2
- export * from "./container-registry";
3
- export * from "./container-utils";
4
- export * from "./http-client";
5
- export * from "./image-ref";
6
- export * from "./secrets";
1
+ interface RegistryEntry extends AgentState {
2
+ startedAt: string;
3
+ stoppedAt?: string;
4
+ }
5
+ interface ContainerRegistry {
6
+ log(state: AgentState): void;
7
+ markStopped(containerName?: string): void;
8
+ clearStopped(containerName: string): void;
9
+ getRecent(limit?: number): RegistryEntry[];
10
+ find(containerName: string): RegistryEntry | null;
11
+ findAlive(): Promise<RegistryEntry[]>;
12
+ }
13
+ declare class FileContainerRegistry implements ContainerRegistry {
14
+ private filePath;
15
+ constructor(filePath: string);
16
+ private readRegistry;
17
+ private updateEntry;
18
+ log(state: AgentState): void;
19
+ markStopped(containerName?: string): void;
20
+ clearStopped(containerName: string): void;
21
+ getRecent(limit?: number): RegistryEntry[];
22
+ find(containerName: string): RegistryEntry | null;
23
+ findAlive(): Promise<RegistryEntry[]>;
24
+ }
25
+
26
+ /**
27
+ * Infisical secrets API.
28
+ *
29
+ * API versions used (per https://infisical.com/docs/api-reference):
30
+ * - Auth: POST /api/v1/auth/universal-auth/login
31
+ * - Secrets: GET/POST/PATCH /api/v4/secrets[/{secretName}]
32
+ * - Folders: POST /api/v2/folders
33
+ */
34
+ interface InfisicalConfig {
35
+ token: string;
36
+ projectId: string;
37
+ environment: string;
38
+ }
39
+ /**
40
+ * Log in to Infisical using Universal Auth (Client ID + Client Secret).
41
+ * POST /api/v1/auth/universal-auth/login
42
+ * Returns a short-lived access token.
43
+ */
44
+ declare function infisicalLogin(clientId: string, clientSecret: string): Promise<string>;
45
+ /**
46
+ * Fetch secrets from an Infisical folder path.
47
+ * GET /api/v4/secrets?projectId=…&environment=…&secretPath=…
48
+ * Returns a key→value record.
49
+ */
50
+ declare function fetchInfisicalSecrets(config: InfisicalConfig, secretPath: string): Promise<Record<string, string>>;
51
+ /**
52
+ * Fetch global build secrets from `/global/`.
53
+ */
54
+ declare function fetchGlobalSecrets(config: InfisicalConfig): Promise<Record<string, string>>;
55
+ /**
56
+ * Fetch per-branch deployment secrets from `/branches/<branch>/`.
57
+ */
58
+ declare function fetchBranchSecrets(config: InfisicalConfig, branch: string): Promise<Record<string, string>>;
59
+ /**
60
+ * Create or update a branch secret in Infisical.
61
+ * Creates the folder path if it doesn't exist yet.
62
+ *
63
+ * POST /api/v4/secrets/{name} — create
64
+ * PATCH /api/v4/secrets/{name} — update (if secret already exists)
65
+ *
66
+ * Body: { projectId, environment, secretPath, secretValue, type }
67
+ */
68
+ declare function createBranchSecret(config: InfisicalConfig, branch: string, name: string, value: string): Promise<void>;
69
+ /**
70
+ * Extract Infisical config from environment variables and log in.
71
+ * Reads INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID,
72
+ * and INFISICAL_ENVIRONMENT from the env vars.
73
+ * Throws if any required var is missing.
74
+ */
75
+ declare function getInfisicalConfig(envVars: Record<string, string>): Promise<InfisicalConfig>;
76
+
77
+ interface AgentState {
78
+ type: "local" | "remote";
79
+ containerName: string;
80
+ port: number;
81
+ baseUrl: string;
82
+ flyApp?: string;
83
+ flyMachineId?: string;
84
+ flyVolumeId?: string;
85
+ }
86
+ interface ContainerConfig {
87
+ projectRoot?: string;
88
+ /** Infisical credentials — required for all containers. */
89
+ infisical: InfisicalConfig;
90
+ registry: ContainerRegistry;
91
+ flyToken?: string;
92
+ flyApp?: string;
93
+ imageRef?: string;
94
+ webhookUrl?: string;
95
+ webhookSecret?: string;
96
+ /** GET endpoint to fetch the next task when the local queue is empty. Uses webhookSecret for auth. */
97
+ taskWebhookUrl?: string;
98
+ /** POST endpoint to receive tasks added by the add-task script. When set, add-task POSTs tasks here instead of writing to the local task file. Uses webhookSecret for auth. */
99
+ addTaskWebhookUrl?: string;
100
+ /** Start the container in detached mode. It will exit after processing all messages and tasks. */
101
+ detached?: boolean;
102
+ /** Initial prompt to queue at container startup (before the HTTP server accepts external requests). */
103
+ initialPrompt?: string;
104
+ /** Override the host port for local containers (default: auto-selected). */
105
+ localPort?: number;
106
+ /** Absorb task files from other containers at startup. Default: false. */
107
+ absorbTasks?: boolean;
108
+ /** Container name prefix. Default: "app-building". */
109
+ namePrefix?: string;
110
+ /** Agent command override (e.g. "codex", "gemini"). Default: "claude". */
111
+ agentCommand?: string;
112
+ }
113
+ interface RepoOptions {
114
+ repoUrl: string;
115
+ cloneBranch: string;
116
+ pushBranch: string;
117
+ }
118
+ declare function loadDotEnv(projectRoot: string): Record<string, string>;
119
+ declare function buildImage(config: ContainerConfig): void;
120
+ /**
121
+ * Start a container (local Docker or remote Fly.io based on config).
122
+ * If flyToken and flyApp are set, starts remotely; otherwise locally.
123
+ */
124
+ declare function startContainer(config: ContainerConfig, repo: RepoOptions): Promise<AgentState>;
125
+ /**
126
+ * Stop a container by its state or registry entry.
127
+ */
128
+ declare function stopContainer(config: ContainerConfig, state: AgentState | RegistryEntry): Promise<void>;
129
+ /**
130
+ * Spawn an interactive test container (local only).
131
+ */
132
+ declare function spawnTestContainer(config: ContainerConfig): Promise<void>;
133
+
134
+ interface HttpOptions {
135
+ timeout?: number;
136
+ headers?: Record<string, string>;
137
+ }
138
+ declare function httpGet(url: string, opts?: HttpOptions): Promise<any>;
139
+ declare function httpPost(url: string, body?: unknown, opts?: HttpOptions): Promise<any>;
140
+
141
+ declare function httpOptsFor(state: AgentState): HttpOptions;
142
+ declare function probeAlive(entry: RegistryEntry): Promise<boolean>;
143
+
144
+ declare function getImageRef(): string;
145
+
146
+ /**
147
+ * Task dependency resolution helpers.
148
+ *
149
+ * Pure functions that take task arrays and return ready tasks — no file I/O.
150
+ */
151
+ interface Task {
152
+ skill: string;
153
+ subtasks: string[];
154
+ timestamp: string;
155
+ app?: string;
156
+ /** Optional ID for coordination (e.g. from task webhook). Included in task.started/task.done events. */
157
+ id?: string;
158
+ /** Raw prompt for message-derived tasks (no skill file). */
159
+ prompt?: string;
160
+ /** Custom command (agent + args) to run instead of the default "claude" with extraArgs. */
161
+ command?: string;
162
+ /** Maximum number of attempts before giving up. Default: 5. */
163
+ maxAttempts?: number;
164
+ /** Maximum time in minutes for each attempt. Agent is killed if exceeded. */
165
+ timeoutMinutes?: number;
166
+ /** Shell command to run once when the task starts (before the first agent attempt). */
167
+ setup?: string;
168
+ /** Task IDs that must complete before this task can start. */
169
+ requiredTaskIds?: string[];
170
+ /** The ID of the task that spawned this task via add-task. */
171
+ parentTaskId?: string;
172
+ }
173
+ /**
174
+ * Find the first task in `pendingTasks` whose dependencies are all satisfied.
175
+ * Returns the task, or null if all tasks are blocked.
176
+ *
177
+ * A required task is "fully complete" when:
178
+ * 1. It appears in `completedTasks`, AND
179
+ * 2. Every task in `completedTasks` that has it as `parentTaskId` is also
180
+ * fully complete (recursive — children's children must also be done), AND
181
+ * 3. No task in `pendingTasks` has it as `parentTaskId`.
182
+ *
183
+ * @param pendingTasks - Tasks waiting to run (ordered by priority).
184
+ * @param completedTasks - Tasks that have finished (need id and parentTaskId).
185
+ */
186
+ declare function findReadyTask(pendingTasks: Task[], completedTasks: Pick<Task, "id" | "parentTaskId">[]): Task | null;
187
+
188
+ export { type AgentState, type ContainerConfig, type ContainerRegistry, FileContainerRegistry, type HttpOptions, type InfisicalConfig, type RegistryEntry, type RepoOptions, type Task, buildImage, createBranchSecret, fetchBranchSecrets, fetchGlobalSecrets, fetchInfisicalSecrets, findReadyTask, getImageRef, getInfisicalConfig, httpGet, httpOptsFor, httpPost, infisicalLogin, loadDotEnv, probeAlive, spawnTestContainer, startContainer, stopContainer };