@bridge4dev/runner 0.11.0 → 0.22.1

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.
Files changed (45) hide show
  1. package/dist/adapters/claude.d.ts +15 -7
  2. package/dist/adapters/claude.js +1024 -70
  3. package/dist/adapters/codex.d.ts +18 -3
  4. package/dist/adapters/codex.js +224 -65
  5. package/dist/adapters/questions.d.ts +42 -0
  6. package/dist/adapters/questions.js +86 -0
  7. package/dist/adapters/types.d.ts +200 -4
  8. package/dist/attachments.d.ts +8 -1
  9. package/dist/attachments.js +22 -4
  10. package/dist/auto-resume.d.ts +18 -0
  11. package/dist/auto-resume.js +104 -0
  12. package/dist/commit-message.d.ts +51 -0
  13. package/dist/commit-message.js +224 -0
  14. package/dist/config.d.ts +29 -6
  15. package/dist/config.js +15 -0
  16. package/dist/crash-note.d.ts +54 -0
  17. package/dist/crash-note.js +105 -0
  18. package/dist/git.d.ts +71 -0
  19. package/dist/git.js +207 -10
  20. package/dist/gitops.d.ts +489 -12
  21. package/dist/gitops.js +1717 -96
  22. package/dist/index.js +435 -32
  23. package/dist/paths.d.ts +26 -0
  24. package/dist/paths.js +34 -0
  25. package/dist/policy.d.ts +63 -0
  26. package/dist/policy.js +412 -10
  27. package/dist/protocol.d.ts +382 -60
  28. package/dist/protocol.js +104 -1
  29. package/dist/recipe-schema.d.ts +310 -0
  30. package/dist/recipe-schema.js +103 -0
  31. package/dist/recipe.d.ts +94 -0
  32. package/dist/recipe.js +238 -0
  33. package/dist/self-update.d.ts +7 -0
  34. package/dist/self-update.js +171 -23
  35. package/dist/service-unit.d.ts +79 -0
  36. package/dist/service-unit.js +211 -0
  37. package/dist/supervisor.d.ts +108 -1
  38. package/dist/supervisor.js +1010 -56
  39. package/dist/verify-queue.d.ts +17 -0
  40. package/dist/verify-queue.js +100 -0
  41. package/dist/verify.d.ts +203 -0
  42. package/dist/verify.js +788 -0
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/package.json +2 -2
package/dist/protocol.js CHANGED
@@ -45,6 +45,13 @@ export const SessionDescriptorSchema = z.object({
45
45
  path: z.string(),
46
46
  projectId: z.string().uuid(),
47
47
  trustMode: z.enum(['STRICT', 'NORMAL', 'AUTO']),
48
+ /**
49
+ * Session 15. Optional: an API older than this release does not send it,
50
+ * and «not said» must keep the behaviour every deployed runner has now —
51
+ * inventing a restriction from silence would stop agents committing on
52
+ * every project the moment a runner updated ahead of its API.
53
+ */
54
+ agentAutoCommit: z.boolean().optional(),
48
55
  budgetUsd: z.number().nullable(),
49
56
  budgetMinutes: z.number().nullable(),
50
57
  }),
@@ -62,6 +69,57 @@ export const SessionDescriptorSchema = z.object({
62
69
  .regex(/^[A-Za-z0-9][A-Za-z0-9._\-/]*$/)
63
70
  .optional()
64
71
  .catch(undefined),
72
+ /**
73
+ * Session 13: the branch as a decision rather than a coincidence.
74
+ *
75
+ * `branchHint` said «call it this if you like»; the runner then guessed what
76
+ * to do about a name that already existed (silently reuse it) or did not
77
+ * (create it off whatever the folder was on). Both guesses lose work. The
78
+ * plan says which of the two the API meant, and the runner fails loudly when
79
+ * reality disagrees.
80
+ *
81
+ * `.catch(undefined)` for the same reason as `branchHint`: this field travels
82
+ * inside hello_ack, which carries EVERY session of the server, so one
83
+ * malformed plan must cost its own session at most — never the frame
84
+ * (QA-100 MAJOR-1). Falling back to `branchHint` restores exactly the old
85
+ * behaviour.
86
+ */
87
+ branchPlan: z
88
+ .object({
89
+ branch: z
90
+ .string()
91
+ .max(120)
92
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._\-/]*$/),
93
+ source: z.enum(['NEW', 'CONTINUE']),
94
+ baseBranch: z
95
+ .string()
96
+ .max(200)
97
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._\-/]*$/)
98
+ .optional(),
99
+ baseSha: z
100
+ .string()
101
+ .regex(/^[0-9a-f]{7,64}$/i)
102
+ .optional(),
103
+ })
104
+ .optional()
105
+ .catch(undefined),
106
+ /**
107
+ * Where this session works (session 16).
108
+ *
109
+ * `DIRECT` — in the project folder itself, on the branch it is already on.
110
+ * There is no worktree, no session branch and no «Apply»: the work is already
111
+ * where the person wanted it. This is the ordinary case, because the person
112
+ * runs their own terminal agents in that same folder anyway — we were paying
113
+ * the whole price of isolation and not getting any.
114
+ *
115
+ * `BRANCH` — the historical worktree-and-branch arrangement. The API hands it
116
+ * out when the folder is already taken by another live session, because two
117
+ * agents in one folder fight exactly the way a human and an agent do.
118
+ *
119
+ * Optional, and absent means `BRANCH`: an API older than this release says
120
+ * nothing, and silence must keep the behaviour every deployed runner has now.
121
+ */
122
+ workMode: z.enum(['DIRECT', 'BRANCH']).optional().catch(undefined),
65
123
  // Auto-provisioned DevBridge MCP access (dbk_ key scoped to the workspace
66
124
  // project). Preferred over the [mcp] section of config.toml when present.
67
125
  mcp: z.object({ url: z.string().url(), token: z.string().min(1) }).optional(),
@@ -115,6 +173,25 @@ export const GatewayFrameSchema = z.discriminatedUnion('type', [
115
173
  allow: z.boolean(),
116
174
  note: z.string().optional(),
117
175
  }),
176
+ // Session 12: the human's answer to a parked agent question. `action` splits
177
+ // "here is the decision" from "let me reply in words instead", and both keep
178
+ // the agent's turn alive.
179
+ z.object({
180
+ type: z.literal('question_answer'),
181
+ sessionId: z.string().uuid(),
182
+ askId: z.string().min(1).max(64),
183
+ action: z.enum(['answer', 'discuss']),
184
+ answers: z
185
+ .array(z.object({
186
+ questionId: z.string().min(1).max(120),
187
+ values: z.array(z.string().max(2_000)).max(16),
188
+ custom: z.string().max(10_000).optional(),
189
+ notes: z.string().max(2_000).optional(),
190
+ }))
191
+ .max(4)
192
+ .optional(),
193
+ text: z.string().max(20_000).optional(),
194
+ }),
118
195
  z.object({ type: z.literal('session_stop'), sessionId: z.string().uuid() }),
119
196
  z.object({ type: z.literal('session_interrupt'), sessionId: z.string().uuid() }),
120
197
  // Server-wide settings changed mid-connection (session 8) — today just the
@@ -123,6 +200,19 @@ export const GatewayFrameSchema = z.discriminatedUnion('type', [
123
200
  type: z.literal('server_settings'),
124
201
  maxSessions: z.number().int().positive().optional().catch(undefined),
125
202
  }),
203
+ /**
204
+ * Project-level policy changed while sessions are running (session 15).
205
+ *
206
+ * `trustMode` and `agentAutoCommit` used to be fixed at session launch, so a
207
+ * manager tightening trust changed a database row and nothing else while the
208
+ * card reported the new level as a fact. Both are read on every tool call.
209
+ */
210
+ z.object({
211
+ type: z.literal('workspace_settings'),
212
+ workspaceId: z.string().min(1).max(64),
213
+ trustMode: z.enum(['STRICT', 'NORMAL', 'AUTO']).optional(),
214
+ agentAutoCommit: z.boolean().optional(),
215
+ }),
126
216
  z.object({
127
217
  type: z.literal('session_settings'),
128
218
  sessionId: z.string().uuid(),
@@ -142,12 +232,25 @@ export const GatewayFrameSchema = z.discriminatedUnion('type', [
142
232
  // The strict `RunnerCommandName` union on the API side stays the authority.
143
233
  // Known names: validate_path, clean, purge_session, reset_workspace,
144
234
  // git_status, git_diff, git_commit, apply_session, revert_apply, fs_view,
145
- // auth_status, login_start, login_code, self_update.
235
+ // git_log, git_show, auth_status, login_start, login_code, self_update,
236
+ // git_branches, update_from_base, git_push, git_refs, workspace_state,
237
+ // recipe_state, verify_start, verify_status, verify_cancel,
238
+ // preview_checkout, preview_stop, propose_commit_message, git_stage,
239
+ // git_unstage, git_discard, git_pull, git_merge_abort.
146
240
  name: z.string().min(1).max(64),
147
241
  workspaceId: z.string().optional(),
148
242
  sessionId: z.string().optional(),
149
243
  args: z.record(z.unknown()).optional(),
150
244
  }),
245
+ /**
246
+ * Session 14: the API stored a verification verdict, so stop redelivering it.
247
+ *
248
+ * The verdict travels as its own frame rather than as a command answer,
249
+ * because a build takes minutes and a relayed command lives for seconds. That
250
+ * makes the frame losable — a reconnect mid-build, a closed tab, an API too
251
+ * old to know the frame — so it is queued on disk until this arrives.
252
+ */
253
+ z.object({ type: z.literal('verify_report_ack'), runId: z.string().min(1).max(64) }),
151
254
  z.object({ type: z.literal('revoked'), reason: z.string() }),
152
255
  z.object({ type: z.literal('error'), message: z.string() }),
153
256
  ]);
@@ -0,0 +1,310 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Mirror of `packages/shared/src/schemas/dev-recipe.ts` — the DevBridge side is
4
+ * the source of truth, exactly like `protocol.ts` mirrors the API's wire types.
5
+ *
6
+ * Copied rather than imported on purpose: this package is published to npm on
7
+ * its own and installed by users who have no DevBridge workspace, so a
8
+ * `@devbridge/shared` import would make the published tarball unresolvable.
9
+ * The two files are small, additive, and change together in one release.
10
+ *
11
+ * Deliberately NOT `.strict()`: the same JSON is stored in a JSONB column on
12
+ * the API side, and a strict schema turns «a newer release added a field» into
13
+ * «the whole recipe is unreadable» for every row written before it.
14
+ */
15
+ export declare const RECIPE_STEP_NAMES: readonly ["install", "build", "test", "verify", "deploy"];
16
+ export type RecipeStepName = (typeof RECIPE_STEP_NAMES)[number];
17
+ export declare const RECIPE_MAX_TIMEOUT_SEC = 3600;
18
+ export declare const RECIPE_DEFAULT_TIMEOUT_SEC = 900;
19
+ /** Where a project's recipe proposal lives, relative to the repository root. */
20
+ export declare const PROJECT_RECIPE_PATH = ".devbridge/project.json";
21
+ export declare const ProjectRecipeStepSchema: z.ZodObject<{
22
+ run: z.ZodEffects<z.ZodString, string, string>;
23
+ timeoutSec: z.ZodOptional<z.ZodNumber>;
24
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
25
+ }, "strip", z.ZodTypeAny, {
26
+ run: string;
27
+ env?: Record<string, string> | undefined;
28
+ timeoutSec?: number | undefined;
29
+ }, {
30
+ run: string;
31
+ env?: Record<string, string> | undefined;
32
+ timeoutSec?: number | undefined;
33
+ }>;
34
+ export declare const ProjectRecipeHealthSchema: z.ZodObject<{
35
+ url: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
36
+ shaPath: z.ZodOptional<z.ZodString>;
37
+ }, "strip", z.ZodTypeAny, {
38
+ url: string;
39
+ shaPath?: string | undefined;
40
+ }, {
41
+ url: string;
42
+ shaPath?: string | undefined;
43
+ }>;
44
+ export declare const ProjectRecipePreviewSchema: z.ZodObject<{
45
+ run: z.ZodEffects<z.ZodString, string, string>;
46
+ stop: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
47
+ url: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
48
+ project: z.ZodOptional<z.ZodString>;
49
+ timeoutSec: z.ZodOptional<z.ZodNumber>;
50
+ }, "strip", z.ZodTypeAny, {
51
+ run: string;
52
+ url?: string | undefined;
53
+ project?: string | undefined;
54
+ timeoutSec?: number | undefined;
55
+ stop?: string | undefined;
56
+ }, {
57
+ run: string;
58
+ url?: string | undefined;
59
+ project?: string | undefined;
60
+ timeoutSec?: number | undefined;
61
+ stop?: string | undefined;
62
+ }>;
63
+ export declare const ProjectRecipeSchema: z.ZodObject<{
64
+ version: z.ZodDefault<z.ZodOptional<z.ZodLiteral<1>>>;
65
+ steps: z.ZodDefault<z.ZodOptional<z.ZodObject<{
66
+ install: z.ZodOptional<z.ZodObject<{
67
+ run: z.ZodEffects<z.ZodString, string, string>;
68
+ timeoutSec: z.ZodOptional<z.ZodNumber>;
69
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
70
+ }, "strip", z.ZodTypeAny, {
71
+ run: string;
72
+ env?: Record<string, string> | undefined;
73
+ timeoutSec?: number | undefined;
74
+ }, {
75
+ run: string;
76
+ env?: Record<string, string> | undefined;
77
+ timeoutSec?: number | undefined;
78
+ }>>;
79
+ build: z.ZodOptional<z.ZodObject<{
80
+ run: z.ZodEffects<z.ZodString, string, string>;
81
+ timeoutSec: z.ZodOptional<z.ZodNumber>;
82
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
83
+ }, "strip", z.ZodTypeAny, {
84
+ run: string;
85
+ env?: Record<string, string> | undefined;
86
+ timeoutSec?: number | undefined;
87
+ }, {
88
+ run: string;
89
+ env?: Record<string, string> | undefined;
90
+ timeoutSec?: number | undefined;
91
+ }>>;
92
+ test: z.ZodOptional<z.ZodObject<{
93
+ run: z.ZodEffects<z.ZodString, string, string>;
94
+ timeoutSec: z.ZodOptional<z.ZodNumber>;
95
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
96
+ }, "strip", z.ZodTypeAny, {
97
+ run: string;
98
+ env?: Record<string, string> | undefined;
99
+ timeoutSec?: number | undefined;
100
+ }, {
101
+ run: string;
102
+ env?: Record<string, string> | undefined;
103
+ timeoutSec?: number | undefined;
104
+ }>>;
105
+ verify: z.ZodOptional<z.ZodObject<{
106
+ run: z.ZodEffects<z.ZodString, string, string>;
107
+ timeoutSec: z.ZodOptional<z.ZodNumber>;
108
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
109
+ }, "strip", z.ZodTypeAny, {
110
+ run: string;
111
+ env?: Record<string, string> | undefined;
112
+ timeoutSec?: number | undefined;
113
+ }, {
114
+ run: string;
115
+ env?: Record<string, string> | undefined;
116
+ timeoutSec?: number | undefined;
117
+ }>>;
118
+ deploy: z.ZodOptional<z.ZodObject<{
119
+ run: z.ZodEffects<z.ZodString, string, string>;
120
+ timeoutSec: z.ZodOptional<z.ZodNumber>;
121
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
122
+ }, "strip", z.ZodTypeAny, {
123
+ run: string;
124
+ env?: Record<string, string> | undefined;
125
+ timeoutSec?: number | undefined;
126
+ }, {
127
+ run: string;
128
+ env?: Record<string, string> | undefined;
129
+ timeoutSec?: number | undefined;
130
+ }>>;
131
+ }, "strip", z.ZodTypeAny, {
132
+ verify?: {
133
+ run: string;
134
+ env?: Record<string, string> | undefined;
135
+ timeoutSec?: number | undefined;
136
+ } | undefined;
137
+ build?: {
138
+ run: string;
139
+ env?: Record<string, string> | undefined;
140
+ timeoutSec?: number | undefined;
141
+ } | undefined;
142
+ install?: {
143
+ run: string;
144
+ env?: Record<string, string> | undefined;
145
+ timeoutSec?: number | undefined;
146
+ } | undefined;
147
+ test?: {
148
+ run: string;
149
+ env?: Record<string, string> | undefined;
150
+ timeoutSec?: number | undefined;
151
+ } | undefined;
152
+ deploy?: {
153
+ run: string;
154
+ env?: Record<string, string> | undefined;
155
+ timeoutSec?: number | undefined;
156
+ } | undefined;
157
+ }, {
158
+ verify?: {
159
+ run: string;
160
+ env?: Record<string, string> | undefined;
161
+ timeoutSec?: number | undefined;
162
+ } | undefined;
163
+ build?: {
164
+ run: string;
165
+ env?: Record<string, string> | undefined;
166
+ timeoutSec?: number | undefined;
167
+ } | undefined;
168
+ install?: {
169
+ run: string;
170
+ env?: Record<string, string> | undefined;
171
+ timeoutSec?: number | undefined;
172
+ } | undefined;
173
+ test?: {
174
+ run: string;
175
+ env?: Record<string, string> | undefined;
176
+ timeoutSec?: number | undefined;
177
+ } | undefined;
178
+ deploy?: {
179
+ run: string;
180
+ env?: Record<string, string> | undefined;
181
+ timeoutSec?: number | undefined;
182
+ } | undefined;
183
+ }>>>;
184
+ health: z.ZodOptional<z.ZodObject<{
185
+ url: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
186
+ shaPath: z.ZodOptional<z.ZodString>;
187
+ }, "strip", z.ZodTypeAny, {
188
+ url: string;
189
+ shaPath?: string | undefined;
190
+ }, {
191
+ url: string;
192
+ shaPath?: string | undefined;
193
+ }>>;
194
+ preview: z.ZodOptional<z.ZodObject<{
195
+ run: z.ZodEffects<z.ZodString, string, string>;
196
+ stop: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
197
+ url: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
198
+ project: z.ZodOptional<z.ZodString>;
199
+ timeoutSec: z.ZodOptional<z.ZodNumber>;
200
+ }, "strip", z.ZodTypeAny, {
201
+ run: string;
202
+ url?: string | undefined;
203
+ project?: string | undefined;
204
+ timeoutSec?: number | undefined;
205
+ stop?: string | undefined;
206
+ }, {
207
+ run: string;
208
+ url?: string | undefined;
209
+ project?: string | undefined;
210
+ timeoutSec?: number | undefined;
211
+ stop?: string | undefined;
212
+ }>>;
213
+ notes: z.ZodOptional<z.ZodString>;
214
+ }, "strip", z.ZodTypeAny, {
215
+ version: 1;
216
+ steps: {
217
+ verify?: {
218
+ run: string;
219
+ env?: Record<string, string> | undefined;
220
+ timeoutSec?: number | undefined;
221
+ } | undefined;
222
+ build?: {
223
+ run: string;
224
+ env?: Record<string, string> | undefined;
225
+ timeoutSec?: number | undefined;
226
+ } | undefined;
227
+ install?: {
228
+ run: string;
229
+ env?: Record<string, string> | undefined;
230
+ timeoutSec?: number | undefined;
231
+ } | undefined;
232
+ test?: {
233
+ run: string;
234
+ env?: Record<string, string> | undefined;
235
+ timeoutSec?: number | undefined;
236
+ } | undefined;
237
+ deploy?: {
238
+ run: string;
239
+ env?: Record<string, string> | undefined;
240
+ timeoutSec?: number | undefined;
241
+ } | undefined;
242
+ };
243
+ preview?: {
244
+ run: string;
245
+ url?: string | undefined;
246
+ project?: string | undefined;
247
+ timeoutSec?: number | undefined;
248
+ stop?: string | undefined;
249
+ } | undefined;
250
+ notes?: string | undefined;
251
+ health?: {
252
+ url: string;
253
+ shaPath?: string | undefined;
254
+ } | undefined;
255
+ }, {
256
+ preview?: {
257
+ run: string;
258
+ url?: string | undefined;
259
+ project?: string | undefined;
260
+ timeoutSec?: number | undefined;
261
+ stop?: string | undefined;
262
+ } | undefined;
263
+ version?: 1 | undefined;
264
+ notes?: string | undefined;
265
+ steps?: {
266
+ verify?: {
267
+ run: string;
268
+ env?: Record<string, string> | undefined;
269
+ timeoutSec?: number | undefined;
270
+ } | undefined;
271
+ build?: {
272
+ run: string;
273
+ env?: Record<string, string> | undefined;
274
+ timeoutSec?: number | undefined;
275
+ } | undefined;
276
+ install?: {
277
+ run: string;
278
+ env?: Record<string, string> | undefined;
279
+ timeoutSec?: number | undefined;
280
+ } | undefined;
281
+ test?: {
282
+ run: string;
283
+ env?: Record<string, string> | undefined;
284
+ timeoutSec?: number | undefined;
285
+ } | undefined;
286
+ deploy?: {
287
+ run: string;
288
+ env?: Record<string, string> | undefined;
289
+ timeoutSec?: number | undefined;
290
+ } | undefined;
291
+ } | undefined;
292
+ health?: {
293
+ url: string;
294
+ shaPath?: string | undefined;
295
+ } | undefined;
296
+ }>;
297
+ export type ProjectRecipe = z.infer<typeof ProjectRecipeSchema>;
298
+ export type ProjectRecipeStep = z.infer<typeof ProjectRecipeStepSchema>;
299
+ export declare function parseProjectRecipe(value: unknown): {
300
+ ok: true;
301
+ recipe: ProjectRecipe;
302
+ } | {
303
+ ok: false;
304
+ error: string;
305
+ };
306
+ /** Key-sorted JSON, so a reordered file does not read as a different recipe. */
307
+ export declare function canonicalRecipeJson(recipe: unknown): string;
308
+ /** Steps this recipe actually defines, in execution order. */
309
+ export declare function recipeSteps(recipe: ProjectRecipe | null | undefined): RecipeStepName[];
310
+ //# sourceMappingURL=recipe-schema.d.ts.map
@@ -0,0 +1,103 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Mirror of `packages/shared/src/schemas/dev-recipe.ts` — the DevBridge side is
4
+ * the source of truth, exactly like `protocol.ts` mirrors the API's wire types.
5
+ *
6
+ * Copied rather than imported on purpose: this package is published to npm on
7
+ * its own and installed by users who have no DevBridge workspace, so a
8
+ * `@devbridge/shared` import would make the published tarball unresolvable.
9
+ * The two files are small, additive, and change together in one release.
10
+ *
11
+ * Deliberately NOT `.strict()`: the same JSON is stored in a JSONB column on
12
+ * the API side, and a strict schema turns «a newer release added a field» into
13
+ * «the whole recipe is unreadable» for every row written before it.
14
+ */
15
+ export const RECIPE_STEP_NAMES = ['install', 'build', 'test', 'verify', 'deploy'];
16
+ export const RECIPE_MAX_TIMEOUT_SEC = 3_600;
17
+ export const RECIPE_DEFAULT_TIMEOUT_SEC = 900;
18
+ /** Where a project's recipe proposal lives, relative to the repository root. */
19
+ export const PROJECT_RECIPE_PATH = '.devbridge/project.json';
20
+ const RecipeCommandSchema = z
21
+ .string()
22
+ .trim()
23
+ .min(1)
24
+ .max(2_000)
25
+ .refine((value) => !/[\r\n]/.test(value), 'A recipe command must be a single line');
26
+ export const ProjectRecipeStepSchema = z.object({
27
+ run: RecipeCommandSchema,
28
+ timeoutSec: z.number().int().min(5).max(RECIPE_MAX_TIMEOUT_SEC).optional(),
29
+ env: z.record(z.string().max(200), z.string().max(4_000)).optional(),
30
+ });
31
+ export const ProjectRecipeHealthSchema = z.object({
32
+ url: z
33
+ .string()
34
+ .url()
35
+ .max(500)
36
+ .refine((value) => /^https?:\/\//i.test(value), 'The health URL must be http or https')
37
+ .refine((value) => !/^https?:\/\/[^/@]*@/i.test(value), 'The health URL must not carry credentials'),
38
+ shaPath: z.string().max(200).optional(),
39
+ });
40
+ export const ProjectRecipePreviewSchema = z.object({
41
+ run: RecipeCommandSchema,
42
+ stop: RecipeCommandSchema.optional(),
43
+ url: z
44
+ .string()
45
+ .url()
46
+ .max(500)
47
+ .refine((value) => /^https?:\/\//i.test(value), 'The preview URL must be http or https')
48
+ .optional(),
49
+ project: z
50
+ .string()
51
+ .trim()
52
+ .min(1)
53
+ .max(60)
54
+ .regex(/^[a-z0-9][a-z0-9_-]*$/i, 'Invalid docker compose project name')
55
+ .optional(),
56
+ timeoutSec: z.number().int().min(5).max(RECIPE_MAX_TIMEOUT_SEC).optional(),
57
+ });
58
+ export const ProjectRecipeSchema = z.object({
59
+ version: z.literal(1).optional().default(1),
60
+ steps: z
61
+ .object({
62
+ install: ProjectRecipeStepSchema.optional(),
63
+ build: ProjectRecipeStepSchema.optional(),
64
+ test: ProjectRecipeStepSchema.optional(),
65
+ verify: ProjectRecipeStepSchema.optional(),
66
+ deploy: ProjectRecipeStepSchema.optional(),
67
+ })
68
+ .optional()
69
+ .default({}),
70
+ health: ProjectRecipeHealthSchema.optional(),
71
+ preview: ProjectRecipePreviewSchema.optional(),
72
+ notes: z.string().max(4_000).optional(),
73
+ });
74
+ export function parseProjectRecipe(value) {
75
+ const parsed = ProjectRecipeSchema.safeParse(value);
76
+ if (parsed.success)
77
+ return { ok: true, recipe: parsed.data };
78
+ const issue = parsed.error.issues[0];
79
+ const where = issue?.path.length ? `${issue.path.join('.')}: ` : '';
80
+ return { ok: false, error: `${where}${issue?.message ?? 'the recipe is not valid'}` };
81
+ }
82
+ /** Key-sorted JSON, so a reordered file does not read as a different recipe. */
83
+ export function canonicalRecipeJson(recipe) {
84
+ const normalise = (value) => {
85
+ if (Array.isArray(value))
86
+ return value.map(normalise);
87
+ if (value && typeof value === 'object') {
88
+ const entries = Object.entries(value)
89
+ .filter(([, v]) => v !== undefined)
90
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
91
+ return Object.fromEntries(entries.map(([k, v]) => [k, normalise(v)]));
92
+ }
93
+ return value;
94
+ };
95
+ return JSON.stringify(normalise(recipe));
96
+ }
97
+ /** Steps this recipe actually defines, in execution order. */
98
+ export function recipeSteps(recipe) {
99
+ if (!recipe?.steps)
100
+ return [];
101
+ return RECIPE_STEP_NAMES.filter((name) => Boolean(recipe.steps?.[name]?.run));
102
+ }
103
+ //# sourceMappingURL=recipe-schema.js.map
@@ -0,0 +1,94 @@
1
+ import { type ProjectRecipe, type RecipeStepName } from './recipe-schema.js';
2
+ export interface RecipeStateResult {
3
+ /** Absolute path we looked at, for the message when it is missing. */
4
+ path: string;
5
+ present: boolean;
6
+ /** The file as written, masked and capped — this is what a human approves. */
7
+ raw: string | null;
8
+ recipe: ProjectRecipe | null;
9
+ /** Why it could not be used, in a sentence. */
10
+ error: string | null;
11
+ /** Fingerprint of the parsed recipe; null when it did not parse. */
12
+ sha: string | null;
13
+ /** Steps the file defines, in execution order. */
14
+ steps: RecipeStepName[];
15
+ /**
16
+ * Every command this recipe would execute, one entry each — and THIS, not
17
+ * `raw`, is what the approval screen must show (session 15).
18
+ *
19
+ * `raw` is the whole file put through `maskString`, and a mask is a rewrite:
20
+ * the private-key pattern is `-----BEGIN … KEY-----[\s\S]*?(-----END …|$)`,
21
+ * so a single unterminated BEGIN line anywhere in the file replaces
22
+ * everything after it — including the commands — with `[MASKED:private-key]`.
23
+ * An agent that can edit its own branch could therefore hand a manager a
24
+ * recipe whose `deploy` step was invisible on the screen where they approved
25
+ * it, while the runner executed the real, unmasked text. The whole recipe
26
+ * model rests on «a human read exactly these commands».
27
+ *
28
+ * Masking each command on its own keeps a redaction inside the command it
29
+ * belongs to, and `redacted` says out loud that one happened.
30
+ */
31
+ commands: Array<{
32
+ step: string;
33
+ run: string;
34
+ redacted: boolean;
35
+ }>;
36
+ /**
37
+ * Commands layer 1 would refuse. Reported BEFORE approval so nobody approves
38
+ * a recipe whose build step can never run.
39
+ */
40
+ refusals: Array<{
41
+ step: string;
42
+ reason: string;
43
+ }>;
44
+ }
45
+ /**
46
+ * A stable fingerprint of a recipe.
47
+ *
48
+ * `verify_start` carries it and the runner refuses a run whose recipe does not
49
+ * match: an approval is an approval of specific commands, and «the approved
50
+ * recipe» has to mean the same bytes on both sides.
51
+ */
52
+ export declare function recipeFingerprint(recipe: unknown): string;
53
+ /**
54
+ * Read the proposal out of a checkout.
55
+ *
56
+ * `lstat` and not `stat`: a symlink at `.devbridge/project.json` pointing at
57
+ * `~/.aws/credentials` would otherwise be read and shown verbatim on the
58
+ * approval screen.
59
+ *
60
+ * The DIRECTORY is checked the same way, and separately (session 15): `lstat`
61
+ * on the file follows every symlink above it, so `.devbridge` itself being a
62
+ * link to somewhere else in the filesystem walked straight past the guard while
63
+ * the guard's own comment said it did not.
64
+ */
65
+ export declare function readRecipeProposal(root: string): RecipeStateResult;
66
+ /**
67
+ * Every command in the recipe, in the order a person should read them —
68
+ * `preview.run` and `preview.stop` included, exactly as the refusal check
69
+ * already covers them.
70
+ */
71
+ export declare function recipeCommands(recipe: ProjectRecipe): Array<{
72
+ step: string;
73
+ run: string;
74
+ redacted: boolean;
75
+ }>;
76
+ /**
77
+ * Every command in the recipe that layer 1 would refuse, named by its step.
78
+ *
79
+ * Answered at read time, not at run time. A recipe whose `deploy` step contains
80
+ * `sudo systemctl restart` must fail the person reading the approval screen, not
81
+ * a build twenty minutes into a release.
82
+ */
83
+ export declare function recipeRefusals(recipe: ProjectRecipe): Array<{
84
+ step: string;
85
+ reason: string;
86
+ }>;
87
+ /**
88
+ * Drop `//` and `/* *\/` comments without touching what is inside strings.
89
+ *
90
+ * A hand-rolled scanner rather than a regex: a regex that ignores string
91
+ * context eats the `//` in a URL, which is exactly what `health.url` is.
92
+ */
93
+ export declare function stripJsonComments(text: string): string;
94
+ //# sourceMappingURL=recipe.d.ts.map