@autonoma-ai/planner 0.1.24 → 0.1.25

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/dist/index.js CHANGED
@@ -42,6 +42,11 @@ var init_env = __esm({
42
42
  AUTONOMA_API_URL: z.string().optional(),
43
43
  AUTONOMA_API_TOKEN: z.string().optional(),
44
44
  AUTONOMA_GENERATION_ID: z.string().optional(),
45
+ // The Autonoma application this run belongs to. Unlike AUTONOMA_GENERATION_ID
46
+ // (which identifies one setup's uploads) this identifies the app itself, and is
47
+ // what the onboarding calls - reading state, minting a pairing code for an agent
48
+ // this CLI spawns - are keyed by.
49
+ AUTONOMA_APPLICATION_ID: z.string().optional(),
45
50
  AUTONOMA_POSTHOG_KEY: z.string().optional(),
46
51
  AUTONOMA_POSTHOG_HOST: z.string().optional(),
47
52
  AUTONOMA_DISTINCT_ID: z.string().optional(),
@@ -498,13 +503,13 @@ function wrapStyledLines(lines, width) {
498
503
  continue;
499
504
  }
500
505
  const leading = leadingSpaces(line);
501
- const indent = leading > 0 && leading <= Math.floor(width / 2) ? leading : 0;
506
+ const indent2 = leading > 0 && leading <= Math.floor(width / 2) ? leading : 0;
502
507
  const chars = [];
503
508
  for (const span of line) for (const ch of span.text) chars.push({ ch, span });
504
509
  let start = 0;
505
510
  let first = true;
506
511
  while (start < chars.length) {
507
- const avail = first ? width : width - indent;
512
+ const avail = first ? width : width - indent2;
508
513
  let end = Math.min(start + avail, chars.length);
509
514
  if (end < chars.length) {
510
515
  let back = end;
@@ -518,7 +523,7 @@ function wrapStyledLines(lines, width) {
518
523
  if (last != null && chars[i - 1]?.span === span && i > start) last.text += ch;
519
524
  else folded.push({ text: ch, color: span.color, bold: span.bold, dim: span.dim });
520
525
  }
521
- if (!first && indent > 0) folded.unshift({ text: " ".repeat(indent) });
526
+ if (!first && indent2 > 0) folded.unshift({ text: " ".repeat(indent2) });
522
527
  out.push(folded.length ? folded : [{ text: "" }]);
523
528
  start = end;
524
529
  while (start < chars.length && chars[start].ch === " ") start++;
@@ -2017,7 +2022,7 @@ var init_evidence_tokens = __esm({
2017
2022
 
2018
2023
  // ../../packages/types/src/schemas/investigation-report.ts
2019
2024
  import { z as z6 } from "zod";
2020
- var investigationEvidenceSchema, investigationPointSchema, investigationRunStepSchema, investigationFindingSchema, investigationValidationSchema, investigationSuggestedTestSchema, investigationDeployedPerTestSchema, investigationDeployedComparisonSchema, investigationReportDataSchema;
2025
+ var investigationEvidenceSchema, investigationPointSchema, investigationRunStepSchema, investigationFindingSchema, investigationDeployedPerTestSchema, investigationDeployedComparisonSchema;
2021
2026
  var init_investigation_report = __esm({
2022
2027
  "../../packages/types/src/schemas/investigation-report.ts"() {
2023
2028
  "use strict";
@@ -2096,7 +2101,9 @@ var init_investigation_report = __esm({
2096
2101
  /** Browser-openable HTTPS URL of the dead-time-stripped mp4 recording, signed on read. When present, the
2097
2102
  * finding page shows an Optimized/Original toggle; absent for runs recorded before the optimizer landed. */
2098
2103
  optimizedVideoUrl: z6.string().optional(),
2099
- finalScreenshotUrl: z6.string().optional(),
2104
+ /** Browser-openable HTTPS URL of the frame the classifier chose (`keyStepIndex`), signed on read. Absent when
2105
+ * it chose none, in which case the finding deliberately shows no still. */
2106
+ keyScreenshotUrl: z6.string().optional(),
2100
2107
  /** Present instead of the verdict fields when the model failed to classify this test. */
2101
2108
  error: z6.string().optional(),
2102
2109
  /**
@@ -2113,17 +2120,6 @@ var init_investigation_report = __esm({
2113
2120
  issueId: z6.string().optional(),
2114
2121
  issueTitle: z6.string().optional()
2115
2122
  });
2116
- investigationValidationSchema = z6.object({
2117
- passed: z6.boolean(),
2118
- iterations: z6.number(),
2119
- failureReason: z6.string().optional()
2120
- });
2121
- investigationSuggestedTestSchema = z6.object({
2122
- name: z6.string(),
2123
- instruction: z6.string(),
2124
- reasoning: z6.string(),
2125
- validation: investigationValidationSchema.optional()
2126
- });
2127
2123
  investigationDeployedPerTestSchema = z6.object({
2128
2124
  testSlug: z6.string(),
2129
2125
  affectedReason: z6.string().optional(),
@@ -2138,20 +2134,6 @@ var init_investigation_report = __esm({
2138
2134
  failureReason: z6.string().optional(),
2139
2135
  perTest: z6.array(investigationDeployedPerTestSchema)
2140
2136
  });
2141
- investigationReportDataSchema = z6.object({
2142
- client: z6.string(),
2143
- appSlug: z6.string(),
2144
- prNumber: z6.number(),
2145
- prTitle: z6.string().optional(),
2146
- prBody: z6.string().optional(),
2147
- /** owner/repo for the app's GitHub repository - used to build code permalinks. Absent on legacy reports. */
2148
- repoFullName: z6.string().optional(),
2149
- /** The PR head commit the run tested - the permalink ref. Absent on legacy reports. */
2150
- commitSha: z6.string().optional(),
2151
- findings: z6.array(investigationFindingSchema),
2152
- suggested: z6.array(investigationSuggestedTestSchema),
2153
- deployed: investigationDeployedComparisonSchema.optional()
2154
- });
2155
2137
  }
2156
2138
  });
2157
2139
 
@@ -2187,7 +2169,7 @@ function analysisVerdictPlane(category) {
2187
2169
  const tier = analysisFindingTier(category);
2188
2170
  return tier === "bug" || tier === "passed" ? "app_health" : "coverage";
2189
2171
  }
2190
- var analysisVerdictSchema, ANALYSIS_VERDICT, VERDICT_TIER, coverageVerdicts, analysisTestOriginSchema, coverageCategoryCountSchema, coverageSummarySchema, analysisClassificationReportSchema, analysisClassificationSummarySchema, analysisFindingViewSchema, analysisReportDataSchema, analysisIssueKindSchema, analysisIssueSeveritySchema, analysisIssueStatusSchema, resolvedPrimaryScreenshotSchema, analysisIssueSummarySchema, analysisIssueFindingInstanceSchema, analysisIssueDetailSchema, analysisPrCoveredTestSchema, analysisPrIssueSchema, analysisPrNewerRunSchema, analysisForPrSchema, analysisSnapshotIssueChangesSchema;
2172
+ var analysisVerdictSchema, ANALYSIS_VERDICT, VERDICT_TIER, coverageVerdicts, analysisTestOriginSchema, coverageCategoryCountSchema, coverageSummarySchema, analysisClassificationReportSchema, analysisClassificationSummarySchema, analysisFindingViewSchema, analysisReportDataSchema, analysisIssueKindSchema, analysisIssueSeveritySchema, analysisIssueStatusSchema, resolvedPrimaryScreenshotSchema, analysisIssueSummarySchema, analysisIssueFindingInstanceSchema, analysisIssueDetailSchema, analysisPrCoveredTestSchema, analysisPrIssueSchema, analysisPrNewerRunSchema, analysisForPrSchema, mainProblemSourceSchema, mainOpenProblemSchema, mainOpenProblemsSchema, analysisSnapshotIssueChangesSchema;
2191
2173
  var init_analysis = __esm({
2192
2174
  "../../packages/types/src/schemas/analysis.ts"() {
2193
2175
  "use strict";
@@ -2445,6 +2427,21 @@ var init_analysis = __esm({
2445
2427
  newerRun: analysisPrNewerRunSchema.optional()
2446
2428
  })
2447
2429
  ]);
2430
+ mainProblemSourceSchema = z8.enum(["legacy_bug", "analysis_issue"]);
2431
+ mainOpenProblemSchema = z8.object({
2432
+ id: z8.string(),
2433
+ title: z8.string(),
2434
+ kind: analysisIssueKindSchema,
2435
+ severity: analysisIssueSeveritySchema,
2436
+ /** The problem's own account of what went wrong (a bug's description, an issue's actual behavior). */
2437
+ detail: z8.string().optional(),
2438
+ occurrences: z8.number().int().nonnegative(),
2439
+ lastSeenAt: z8.date()
2440
+ });
2441
+ mainOpenProblemsSchema = z8.object({
2442
+ source: mainProblemSourceSchema,
2443
+ problems: z8.array(mainOpenProblemSchema)
2444
+ });
2448
2445
  analysisSnapshotIssueChangesSchema = z8.object({
2449
2446
  opened: z8.array(analysisIssueSummarySchema),
2450
2447
  carriedForward: z8.array(analysisIssueSummarySchema),
@@ -2542,7 +2539,7 @@ function findRecipeCreateGraphProblems(create, declaredTokens = /* @__PURE__ */
2542
2539
  }
2543
2540
  return problems;
2544
2541
  }
2545
- var SdkFieldSchema, SdkModelSchema, SdkEdgeSchema, SdkRelationSchema, SdkSchemaSchema, SdkDiscoverResponseSchema, ScenarioRecipeValidationSchema, ScenarioVariableScalarSchema, ScenarioVariableDefinitionSchema, ScenarioRecipeVariablesSchema, ScenarioStructureModelSchema, ScenarioStructureJsonSchema, ScenarioRecipeSchema, ScenarioCreateGraphSchema, TEST_RUN_ID_TOKEN, TEST_RUN_SHORT_ID_TOKEN, BUILT_IN_RECIPE_TOKENS, BUILT_IN_RECIPE_TOKEN_LIST, RECIPE_TEMPLATE_PATTERN, ScenarioRecipesFileSchema, AuthCookieSchema, AuthHeadersSchema, AuthCredentialsSchema, AuthPayloadSchema, RefsSchema, UpResponseSchema, DownResponseSchema, ConfigureWebhookInputSchema, RemoveWebhookInputSchema, DiscoverInputSchema, ListScenariosInputSchema, ListInstancesInputSchema, ListWebhookCallsInputSchema, DryRunInputSchema, GetRecipeInputSchema, UpdateRecipeInputSchema, PreviewkitEnvFactoryOptionsInputSchema, PreviewkitEnvFactoryUpInputSchema, PreviewkitEnvFactoryDownInputSchema, TestUserOptionsInputSchema, TestUserProvisionInputSchema, TestUserTeardownInputSchema;
2542
+ var SdkFieldSchema, SdkModelSchema, SdkEdgeSchema, SdkRelationSchema, SdkSchemaSchema, SdkDiscoverResponseSchema, SCENARIO_VALIDATION_METHODS, ScenarioRecipeValidationSchema, ScenarioVariableScalarSchema, ScenarioVariableDefinitionSchema, ScenarioRecipeVariablesSchema, ScenarioStructureModelSchema, ScenarioStructureJsonSchema, ScenarioRecipeSchema, ScenarioCreateGraphSchema, TEST_RUN_ID_TOKEN, TEST_RUN_SHORT_ID_TOKEN, BUILT_IN_RECIPE_TOKENS, BUILT_IN_RECIPE_TOKEN_LIST, RECIPE_TEMPLATE_PATTERN, ScenarioRecipesFileSchema, AuthCookieSchema, AuthHeadersSchema, AuthCredentialsSchema, AuthPayloadSchema, RefsSchema, UpResponseSchema, DownResponseSchema, ConfigureWebhookInputSchema, RemoveWebhookInputSchema, DiscoverInputSchema, ListScenariosInputSchema, ListInstancesInputSchema, ListWebhookCallsInputSchema, DryRunInputSchema, GetRecipeInputSchema, UpdateRecipeInputSchema, PreviewkitEnvFactoryOptionsInputSchema, PreviewkitEnvFactoryUpInputSchema, PreviewkitEnvFactoryDownInputSchema, TestUserOptionsInputSchema, TestUserProvisionInputSchema, TestUserTeardownInputSchema;
2546
2543
  var init_scenarios = __esm({
2547
2544
  "../../packages/types/src/schemas/scenarios.ts"() {
2548
2545
  "use strict";
@@ -2582,9 +2579,10 @@ var init_scenarios = __esm({
2582
2579
  sdk: z9.record(z9.string(), z9.unknown()).optional(),
2583
2580
  schema: SdkSchemaSchema
2584
2581
  }).passthrough();
2582
+ SCENARIO_VALIDATION_METHODS = ["checkScenario", "checkAllScenarios", "endpoint-up-down"];
2585
2583
  ScenarioRecipeValidationSchema = z9.object({
2586
2584
  status: z9.literal("validated"),
2587
- method: z9.enum(["checkScenario", "checkAllScenarios", "endpoint-up-down"]),
2585
+ method: z9.enum(SCENARIO_VALIDATION_METHODS),
2588
2586
  phase: z9.literal("ok"),
2589
2587
  up_ms: z9.number().int().nonnegative().optional(),
2590
2588
  down_ms: z9.number().int().nonnegative().optional()
@@ -3162,33 +3160,11 @@ var init_secrets = __esm({
3162
3160
  }
3163
3161
  });
3164
3162
 
3165
- // ../../packages/types/src/schemas/org-secrets.ts
3166
- import { z as z17 } from "zod";
3167
- var ORG_SECRET_NAME_REGEX, OrgSecretNameSchema, ListOrgSecretsInputSchema, UpsertOrgSecretInputSchema, DeleteOrgSecretKeyInputSchema;
3168
- var init_org_secrets = __esm({
3169
- "../../packages/types/src/schemas/org-secrets.ts"() {
3163
+ // ../../packages/types/src/schemas/previewkit-node-pm.ts
3164
+ var init_previewkit_node_pm = __esm({
3165
+ "../../packages/types/src/schemas/previewkit-node-pm.ts"() {
3170
3166
  "use strict";
3171
3167
  init_esm_shims();
3172
- init_secrets();
3173
- ORG_SECRET_NAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
3174
- OrgSecretNameSchema = z17.string().min(2).max(63).regex(
3175
- ORG_SECRET_NAME_REGEX,
3176
- "Org secret name must be lowercase alphanumeric with hyphens (matches the preview config auth_secret field)"
3177
- );
3178
- ListOrgSecretsInputSchema = z17.object({
3179
- name: OrgSecretNameSchema
3180
- });
3181
- UpsertOrgSecretInputSchema = z17.object({
3182
- name: OrgSecretNameSchema,
3183
- // Same item shape as per-app secrets: { key, value } where the AWS SM
3184
- // SecretString stores them as a flat JSON map. Providers pick the keys
3185
- // they care about (NeonProvider expects `token`).
3186
- items: z17.array(SecretItemSchema).min(1).max(50)
3187
- });
3188
- DeleteOrgSecretKeyInputSchema = z17.object({
3189
- name: OrgSecretNameSchema,
3190
- key: z17.string().min(1)
3191
- });
3192
3168
  }
3193
3169
  });
3194
3170
 
@@ -3348,8 +3324,268 @@ var init_previewkit_runtimes = __esm({
3348
3324
  }
3349
3325
  });
3350
3326
 
3327
+ // ../../packages/types/src/schemas/previewkit-presets.ts
3328
+ function previewkitPresetSpec(preset) {
3329
+ return PREVIEWKIT_PRESET_CATALOG[preset];
3330
+ }
3331
+ var PREVIEWKIT_PRESET_IDS, SERVER_TREE, PREVIEWKIT_PRESET_CATALOG, PREVIEWKIT_PRESETS;
3332
+ var init_previewkit_presets = __esm({
3333
+ "../../packages/types/src/schemas/previewkit-presets.ts"() {
3334
+ "use strict";
3335
+ init_esm_shims();
3336
+ init_previewkit_runtimes();
3337
+ PREVIEWKIT_PRESET_IDS = [
3338
+ "nextjs",
3339
+ "nuxt",
3340
+ "sveltekit",
3341
+ "remix",
3342
+ "hono",
3343
+ "express",
3344
+ "astro",
3345
+ "vite",
3346
+ "django",
3347
+ "fastapi",
3348
+ "python",
3349
+ "rails",
3350
+ "ruby",
3351
+ "node",
3352
+ "static"
3353
+ ];
3354
+ SERVER_TREE = { mode: "server", copy: "tree" };
3355
+ PREVIEWKIT_PRESET_CATALOG = {
3356
+ nextjs: {
3357
+ id: "nextjs",
3358
+ label: "Next.js",
3359
+ toolchain: "node",
3360
+ detect: {
3361
+ dependencies: ["next"],
3362
+ configFiles: ["next.config.js", "next.config.mjs", "next.config.ts"]
3363
+ },
3364
+ buildCommand: "run build",
3365
+ runCommand: "run start",
3366
+ defaultPort: 3e3,
3367
+ // `next start` needs `.next` + node_modules + config, so carry the whole
3368
+ // built tree. (Standalone-output slimming is a later optimization.)
3369
+ output: SERVER_TREE
3370
+ },
3371
+ nuxt: {
3372
+ id: "nuxt",
3373
+ label: "Nuxt",
3374
+ toolchain: "node",
3375
+ detect: {
3376
+ dependencies: ["nuxt", "nuxt3"],
3377
+ configFiles: ["nuxt.config.js", "nuxt.config.ts", "nuxt.config.mjs"]
3378
+ },
3379
+ buildCommand: "run build",
3380
+ // Nuxt 3's default Nitro preset is a self-contained node server under
3381
+ // `.output`; run it directly rather than through a package script.
3382
+ runCommand: "node .output/server/index.mjs",
3383
+ defaultPort: 3e3,
3384
+ output: SERVER_TREE
3385
+ },
3386
+ sveltekit: {
3387
+ id: "sveltekit",
3388
+ label: "SvelteKit",
3389
+ toolchain: "node",
3390
+ detect: {
3391
+ dependencies: ["@sveltejs/kit"],
3392
+ configFiles: ["svelte.config.js"]
3393
+ },
3394
+ buildCommand: "run build",
3395
+ // Assumes adapter-node (the previewkit-supported adapter): it emits a
3396
+ // `build/` server started with `node build`. Apps on another adapter must
3397
+ // override run_command / output_directory.
3398
+ runCommand: "node build",
3399
+ defaultPort: 3e3,
3400
+ output: SERVER_TREE
3401
+ },
3402
+ remix: {
3403
+ id: "remix",
3404
+ label: "Remix",
3405
+ toolchain: "node",
3406
+ detect: {
3407
+ dependencies: ["@remix-run/dev", "@remix-run/node", "@remix-run/serve"],
3408
+ configFiles: ["remix.config.js"]
3409
+ },
3410
+ buildCommand: "run build",
3411
+ runCommand: "run start",
3412
+ defaultPort: 3e3,
3413
+ output: SERVER_TREE
3414
+ },
3415
+ hono: {
3416
+ id: "hono",
3417
+ label: "Hono",
3418
+ toolchain: "node",
3419
+ detect: {
3420
+ dependencies: ["hono"],
3421
+ configFiles: []
3422
+ },
3423
+ buildCommand: "run build",
3424
+ runCommand: "run start",
3425
+ defaultPort: 3e3,
3426
+ // Assumes the Node adapter (@hono/node-server). Edge / Workers / Bun
3427
+ // deploys build differently and would override the run command.
3428
+ output: SERVER_TREE
3429
+ },
3430
+ express: {
3431
+ id: "express",
3432
+ label: "Express",
3433
+ toolchain: "node",
3434
+ detect: {
3435
+ dependencies: ["express"],
3436
+ configFiles: []
3437
+ },
3438
+ // A plain-JS Express API: the derived dependency install is the only prep, so
3439
+ // there is no build step. Assumes a `start` script; an app without one overrides
3440
+ // run_command with its entry file (e.g. `node server.js`).
3441
+ buildCommand: "",
3442
+ runCommand: "run start",
3443
+ defaultPort: 3e3,
3444
+ output: SERVER_TREE
3445
+ },
3446
+ astro: {
3447
+ id: "astro",
3448
+ label: "Astro",
3449
+ toolchain: "node",
3450
+ detect: {
3451
+ dependencies: ["astro"],
3452
+ configFiles: ["astro.config.mjs", "astro.config.ts", "astro.config.js"]
3453
+ },
3454
+ buildCommand: "run build",
3455
+ runCommand: "",
3456
+ defaultPort: 80,
3457
+ // Default Astro output is a static site in `dist`. An SSR Astro app (with a
3458
+ // server adapter) must override output to server mode.
3459
+ output: { mode: "static", dir: "dist" }
3460
+ },
3461
+ vite: {
3462
+ id: "vite",
3463
+ label: "Vite",
3464
+ toolchain: "node",
3465
+ detect: {
3466
+ dependencies: ["vite"],
3467
+ configFiles: ["vite.config.js", "vite.config.ts", "vite.config.mjs"]
3468
+ },
3469
+ buildCommand: "run build",
3470
+ runCommand: "",
3471
+ defaultPort: 80,
3472
+ output: { mode: "static", dir: "dist" }
3473
+ },
3474
+ django: {
3475
+ id: "django",
3476
+ label: "Django",
3477
+ toolchain: "python",
3478
+ detect: {
3479
+ dependencies: ["django", "Django"],
3480
+ configFiles: ["manage.py"]
3481
+ },
3482
+ // No build step in dev mode (runserver serves static with DEBUG=True). For
3483
+ // production, override build_command with `python manage.py collectstatic --noinput`.
3484
+ buildCommand: "",
3485
+ runCommand: "python manage.py runserver 0.0.0.0:$PORT",
3486
+ defaultPort: 8e3,
3487
+ output: SERVER_TREE
3488
+ },
3489
+ fastapi: {
3490
+ id: "fastapi",
3491
+ label: "FastAPI",
3492
+ toolchain: "python",
3493
+ detect: {
3494
+ dependencies: ["fastapi"],
3495
+ configFiles: []
3496
+ },
3497
+ buildCommand: "",
3498
+ // Assumes the app object is `main:app`; override for another module path
3499
+ // (e.g. `uvicorn app.main:app --host 0.0.0.0 --port $PORT`).
3500
+ runCommand: "uvicorn main:app --host 0.0.0.0 --port $PORT",
3501
+ defaultPort: 8e3,
3502
+ output: SERVER_TREE
3503
+ },
3504
+ python: {
3505
+ id: "python",
3506
+ label: "Python",
3507
+ toolchain: "python",
3508
+ detect: {
3509
+ dependencies: [],
3510
+ configFiles: ["pyproject.toml", "requirements.txt", "Pipfile"]
3511
+ },
3512
+ // Dependency install (uv) is derived from the toolchain; most Python web
3513
+ // apps have no separate build step.
3514
+ buildCommand: "",
3515
+ // Generic default - override run_command for your framework (e.g.
3516
+ // `python manage.py runserver 0.0.0.0:$PORT`, `uvicorn main:app --port $PORT`).
3517
+ runCommand: "python main.py",
3518
+ defaultPort: 8e3,
3519
+ output: SERVER_TREE
3520
+ },
3521
+ rails: {
3522
+ id: "rails",
3523
+ label: "Ruby on Rails",
3524
+ toolchain: "ruby",
3525
+ detect: {
3526
+ dependencies: ["rails"],
3527
+ configFiles: ["bin/rails", "config/application.rb"]
3528
+ },
3529
+ // No build step in development mode (assets compile on the fly). For
3530
+ // production, override with `SECRET_KEY_BASE_DUMMY=1 bin/rails assets:precompile`.
3531
+ buildCommand: "",
3532
+ runCommand: "bin/rails server -b 0.0.0.0 -p $PORT",
3533
+ defaultPort: 3e3,
3534
+ output: SERVER_TREE
3535
+ },
3536
+ ruby: {
3537
+ id: "ruby",
3538
+ label: "Ruby",
3539
+ toolchain: "ruby",
3540
+ detect: {
3541
+ dependencies: [],
3542
+ configFiles: ["Gemfile", "config.ru", "Rakefile"]
3543
+ },
3544
+ // `bundle install` is derived from the toolchain; no separate build step.
3545
+ buildCommand: "",
3546
+ // Rack default - override for Rails (`bin/rails server -b 0.0.0.0 -p $PORT`).
3547
+ runCommand: "bundle exec rackup --host 0.0.0.0 --port $PORT",
3548
+ defaultPort: 9292,
3549
+ output: SERVER_TREE
3550
+ },
3551
+ node: {
3552
+ id: "node",
3553
+ label: "Node.js",
3554
+ toolchain: "node",
3555
+ // Fallback for any package.json that matched no specific framework above.
3556
+ detect: {
3557
+ dependencies: [],
3558
+ configFiles: ["package.json"]
3559
+ },
3560
+ buildCommand: "run build",
3561
+ runCommand: "run start",
3562
+ defaultPort: 3e3,
3563
+ output: SERVER_TREE
3564
+ },
3565
+ static: {
3566
+ id: "static",
3567
+ label: "Static",
3568
+ toolchain: "node",
3569
+ // Last-resort fallback: a plain static site with no build step. The output
3570
+ // directory is the most common thing to override.
3571
+ detect: {
3572
+ dependencies: [],
3573
+ configFiles: ["index.html"]
3574
+ },
3575
+ buildCommand: "",
3576
+ runCommand: "",
3577
+ defaultPort: 80,
3578
+ output: { mode: "static", dir: "." }
3579
+ }
3580
+ };
3581
+ PREVIEWKIT_PRESETS = PREVIEWKIT_PRESET_IDS.map(
3582
+ (id) => PREVIEWKIT_PRESET_CATALOG[id]
3583
+ );
3584
+ }
3585
+ });
3586
+
3351
3587
  // ../../packages/types/src/schemas/previewkit-config.ts
3352
- import { z as z18 } from "zod";
3588
+ import { z as z17 } from "zod";
3353
3589
  function buildResourcesSchema(role, allowCustomResources) {
3354
3590
  return resourcesInput.transform((input) => {
3355
3591
  if (!allowCustomResources || input == null) {
@@ -3363,65 +3599,96 @@ function buildResourcesSchema(role, allowCustomResources) {
3363
3599
  };
3364
3600
  });
3365
3601
  }
3602
+ function singleLineCommand(label) {
3603
+ return z17.string().min(1).regex(/^[^\r\n]+$/, `${label} must be a single line (no line breaks)`);
3604
+ }
3605
+ function heredocSafeScript(label) {
3606
+ return z17.string().min(1).refine(
3607
+ (value) => !value.split("\n").includes(PREVIEWKIT_BUILD_SCRIPT_HEREDOC),
3608
+ `${label} cannot contain a line equal to "${PREVIEWKIT_BUILD_SCRIPT_HEREDOC}" (reserved heredoc delimiter)`
3609
+ );
3610
+ }
3366
3611
  function nodeFrameworkBuildSchema(framework) {
3367
- return z18.object({
3368
- framework: z18.literal(framework),
3369
- package_manager: z18.enum(["npm", "pnpm", "yarn"]).default("pnpm"),
3370
- node_version: z18.string().regex(nodeVersionRegex, "must look like 22, 22.5, or 22.5.0").default("22"),
3371
- install_command: z18.string().min(1).optional(),
3372
- build_command: z18.string().min(1).optional(),
3373
- run_command: z18.string().min(1).optional(),
3612
+ return z17.object({
3613
+ framework: z17.literal(framework),
3614
+ package_manager: z17.enum(["npm", "pnpm", "yarn"]).default("pnpm"),
3615
+ node_version: z17.string().regex(nodeVersionRegex, "must look like 22, 22.5, or 22.5.0").default("22"),
3616
+ install_command: z17.string().min(1).optional(),
3617
+ build_command: z17.string().min(1).optional(),
3618
+ run_command: z17.string().min(1).optional(),
3374
3619
  build_context: buildContextSchema
3375
3620
  });
3376
3621
  }
3377
3622
  function buildPreviewConfigSchema(build, allowCustomResources) {
3378
- const appSchema = z18.object({
3379
- name: z18.string().regex(k8sNameRegex, "Must be a valid Kubernetes name"),
3380
- path: z18.string().default("."),
3381
- build_context: z18.string().optional(),
3382
- dockerfile: z18.string().optional(),
3623
+ const appSchema = z17.object({
3624
+ name: z17.string().regex(k8sNameRegex, "Must be a valid Kubernetes name"),
3625
+ repository: z17.string().regex(repoFullNameRegex, "Must be an owner/repo full name").describe(
3626
+ "The owner/repo full name of the GitHub repository this app builds from. Mandatory even in single-repo setups. Any value other than the application's own repository makes this app a multirepo dependency: that repo is cloned at the branch the branch_convention (or its repositories[] fallback_branch) resolves to."
3627
+ ),
3628
+ path: z17.string().default("."),
3629
+ build_context: z17.string().optional(),
3630
+ dockerfile: z17.string().optional(),
3383
3631
  build: build.optional(),
3632
+ // The preset-based deploy model, mutually exclusive with `build` (enforced
3633
+ // by the superRefine below). Lowered to a `runtime` Build by the generator.
3634
+ blueprint: blueprintSchema.optional(),
3384
3635
  // The AWS-secret keys to also inject at build time (Docker build args).
3385
3636
  // Runtime secret values live in AWS Secrets Manager, never in this document.
3386
- build_secrets: z18.array(z18.string()).default([]),
3387
- port: z18.number().int().positive(),
3637
+ build_secrets: z17.array(z17.string()).default([]),
3638
+ port: z17.number().int().positive(),
3388
3639
  // Non-secret variables wired to the topology, resolved at deploy time.
3389
3640
  // All user-typed values are secrets (AWS), so they never appear here.
3390
- connections: z18.array(connectionSchema).default([]),
3391
- command: z18.string().optional(),
3392
- health_check: z18.string().optional(),
3393
- primary: z18.boolean().optional(),
3641
+ connections: z17.array(connectionSchema).default([]),
3642
+ command: z17.string().optional(),
3643
+ health_check: z17.string().optional(),
3644
+ primary: z17.boolean().optional(),
3394
3645
  // This app serves the Environment Factory handler (`/api/autonoma`), so
3395
3646
  // scenario up/down calls go to its preview URL. Independent of `primary`:
3396
3647
  // a full-stack app (Next.js, Rails) is both the browsed frontend and the
3397
3648
  // SDK host, while a split topology mounts the handler on its API service.
3398
- sdk_implemented: z18.boolean().optional(),
3649
+ sdk_implemented: z17.boolean().optional(),
3399
3650
  resources: buildResourcesSchema("app", allowCustomResources),
3400
- depends_on: z18.array(z18.string()).optional()
3651
+ depends_on: z17.array(z17.string()).optional()
3652
+ }).superRefine((app, ctx) => {
3653
+ if (app.build != null && app.blueprint != null) {
3654
+ ctx.addIssue({
3655
+ code: z17.ZodIssueCode.custom,
3656
+ message: "an app cannot set both `build` and `blueprint` - `blueprint` is the preset-based deploy model, `build` is the manual one",
3657
+ path: ["blueprint"]
3658
+ });
3659
+ }
3401
3660
  });
3402
- const serviceSchema = z18.object({
3403
- name: z18.string().regex(k8sNameRegex, "Must be a valid Kubernetes name"),
3404
- recipe: z18.string(),
3405
- version: z18.string().optional(),
3661
+ const serviceSchema = z17.object({
3662
+ name: z17.string().regex(k8sNameRegex, "Must be a valid Kubernetes name"),
3663
+ recipe: z17.string(),
3664
+ version: z17.string().optional(),
3406
3665
  // Recipe-functional knobs (e.g. postgres user/database, or a docker-image
3407
3666
  // service's image/ports/env) live in `options`, validated per-recipe.
3408
- options: z18.record(z18.string(), z18.unknown()).default({}),
3667
+ options: z17.record(z17.string(), z17.unknown()).default({}),
3409
3668
  // Guided setup for database-recipe services (schema, seed, migrations),
3410
3669
  // run with the repo checked out. Empty for non-database services.
3411
- setup_tasks: z18.array(databaseSetupTaskSchema).default([]),
3670
+ setup_tasks: z17.array(databaseSetupTaskSchema).default([]),
3412
3671
  resources: buildResourcesSchema("service", allowCustomResources),
3413
- s3: z18.boolean().optional(),
3414
- sqs: z18.boolean().optional(),
3415
- sns: z18.boolean().optional()
3672
+ s3: z17.boolean().optional(),
3673
+ sqs: z17.boolean().optional(),
3674
+ sns: z17.boolean().optional()
3416
3675
  });
3417
- return z18.object({
3418
- version: z18.literal(1),
3419
- domain: z18.string().optional(),
3420
- registry: z18.string().optional(),
3421
- config: configSchema.optional(),
3422
- apps: z18.array(appSchema).min(1, "At least one app is required"),
3423
- services: z18.array(serviceSchema).default([]),
3424
- addons: z18.array(addonSchema).default([]),
3676
+ return z17.object({
3677
+ version: z17.literal(2),
3678
+ domain: z17.string().optional(),
3679
+ registry: z17.string().optional(),
3680
+ // Per-repository overrides + deploy provenance; the repo set itself
3681
+ // is derived from `apps[].repository`. See repositorySettingsSchema.
3682
+ repositories: z17.array(repositorySettingsSchema).default([]).describe(
3683
+ "Optional per-repository settings. The topology's repositories are derived from apps[].repository - an entry here only overrides defaults (fallback_branch: which branch to clone when the PR's branch does not exist in that repo; default main)."
3684
+ ),
3685
+ // Topology-wide: how a dependency repo's branch is derived from the
3686
+ // PR's branch. Defaults to same_branch_name behavior when absent.
3687
+ branch_convention: branchConventionSchema.optional().describe(
3688
+ "How a dependency repo's branch is derived from the PR branch: same_branch_name (default), regex (pattern + replacement rewrite), or manual (always the fallback_branch)."
3689
+ ),
3690
+ apps: z17.array(appSchema).min(1, "At least one app is required"),
3691
+ services: z17.array(serviceSchema).default([]),
3425
3692
  hooks: hooksSchema
3426
3693
  }).superRefine((cfg, ctx) => {
3427
3694
  const seen = /* @__PURE__ */ new Map();
@@ -3429,8 +3696,8 @@ function buildPreviewConfigSchema(build, allowCustomResources) {
3429
3696
  const existing = seen.get(name);
3430
3697
  if (existing != null) {
3431
3698
  ctx.addIssue({
3432
- code: z18.ZodIssueCode.custom,
3433
- message: `Name "${name}" is used by both a ${existing} and an ${kind} - names must be unique across apps, services, and addons`
3699
+ code: z17.ZodIssueCode.custom,
3700
+ message: `Name "${name}" is used by both a ${existing} and an ${kind} - names must be unique across apps and services`
3434
3701
  });
3435
3702
  return;
3436
3703
  }
@@ -3438,7 +3705,18 @@ function buildPreviewConfigSchema(build, allowCustomResources) {
3438
3705
  };
3439
3706
  for (const app of cfg.apps) check(app.name, "app");
3440
3707
  for (const service of cfg.services) check(service.name, "service");
3441
- for (const addon of cfg.addons) check(addon.name, "addon");
3708
+ const seenRepos = /* @__PURE__ */ new Set();
3709
+ cfg.repositories.forEach((settings, index) => {
3710
+ const key = settings.repo.toLowerCase();
3711
+ if (seenRepos.has(key)) {
3712
+ ctx.addIssue({
3713
+ code: z17.ZodIssueCode.custom,
3714
+ path: ["repositories", index, "repo"],
3715
+ message: `Repository "${settings.repo}" has more than one settings entry`
3716
+ });
3717
+ }
3718
+ seenRepos.add(key);
3719
+ });
3442
3720
  });
3443
3721
  }
3444
3722
  function standardResources(role) {
@@ -3449,12 +3727,14 @@ function standardResources(role) {
3449
3727
  memoryLimit: standard.memoryLimit
3450
3728
  };
3451
3729
  }
3452
- var STANDARD_RESOURCES, k8sNameRegex, resourcesInput, addonSchema, branchConventionSchema, repoDependencySchema, multirepoConfigSchema, configSchema, hookStepSchema, hooksSchema, databaseSetupLocationSchema, databaseSetupTaskSchema, nodeVersionRegex, buildContextSchema, PREVIEWKIT_BUILD_SCRIPT_HEREDOC, DEPRECATED_BUILD_FRAMEWORKS, UNSUPPORTED_BUILD_METHOD_MESSAGE, deprecatedBuildArms, authoredBuildArms, authoredBuildSchema, storedBuildSchema, connectionSchema, previewConfigSchema, trustedPreviewConfigSchema, authoringPreviewConfigSchema;
3730
+ var STANDARD_RESOURCES, k8sNameRegex, repoFullNameRegex, resourcesInput, branchConventionSchema, repositorySettingsSchema, hookStepSchema, hooksSchema, databaseSetupLocationSchema, databaseSetupTaskSchema, nodeVersionRegex, buildContextSchema, PREVIEWKIT_BUILD_SCRIPT_HEREDOC, imageTagSchema, DEPRECATED_BUILD_FRAMEWORKS, UNSUPPORTED_BUILD_METHOD_MESSAGE, deprecatedBuildArms, authoredBuildArms, authoredBuildSchema, storedBuildSchema, presetBlueprintSchema, dockerfileBlueprintSchema, blueprintSchema, connectionSchema, previewConfigSchema, trustedPreviewConfigSchema, authoringPreviewConfigSchema;
3453
3731
  var init_previewkit_config = __esm({
3454
3732
  "../../packages/types/src/schemas/previewkit-config.ts"() {
3455
3733
  "use strict";
3456
3734
  init_esm_shims();
3457
3735
  init_previewkit_builtins();
3736
+ init_previewkit_node_pm();
3737
+ init_previewkit_presets();
3458
3738
  init_previewkit_runtimes();
3459
3739
  init_secrets();
3460
3740
  STANDARD_RESOURCES = {
@@ -3462,23 +3742,18 @@ var init_previewkit_config = __esm({
3462
3742
  service: { cpu: "100m", memoryRequest: "256Mi", memoryLimit: "1Gi" }
3463
3743
  };
3464
3744
  k8sNameRegex = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
3465
- resourcesInput = z18.object({
3466
- cpu: z18.string().optional(),
3467
- memory: z18.string().optional(),
3468
- memoryRequest: z18.string().optional(),
3469
- memoryLimit: z18.string().optional()
3745
+ repoFullNameRegex = /^[^/\s]+\/[^/\s]+$/;
3746
+ resourcesInput = z17.object({
3747
+ cpu: z17.string().optional(),
3748
+ memory: z17.string().optional(),
3749
+ memoryRequest: z17.string().optional(),
3750
+ memoryLimit: z17.string().optional()
3470
3751
  }).optional();
3471
- addonSchema = z18.object({
3472
- name: z18.string().regex(k8sNameRegex, "Must be a valid Kubernetes name"),
3473
- provider: z18.string().min(1, "provider is required"),
3474
- auth_secret: z18.string().min(1, "auth_secret is required"),
3475
- options: z18.record(z18.string(), z18.unknown()).default({})
3476
- });
3477
- branchConventionSchema = z18.discriminatedUnion("type", [
3478
- z18.object({ type: z18.literal("same_branch_name") }),
3479
- z18.object({
3480
- type: z18.literal("regex"),
3481
- pattern: z18.string().refine((pattern) => {
3752
+ branchConventionSchema = z17.discriminatedUnion("type", [
3753
+ z17.object({ type: z17.literal("same_branch_name") }),
3754
+ z17.object({
3755
+ type: z17.literal("regex"),
3756
+ pattern: z17.string().refine((pattern) => {
3482
3757
  try {
3483
3758
  new RegExp(pattern);
3484
3759
  return true;
@@ -3486,121 +3761,145 @@ var init_previewkit_config = __esm({
3486
3761
  return false;
3487
3762
  }
3488
3763
  }, "Invalid regular expression pattern"),
3489
- replacement: z18.string()
3764
+ replacement: z17.string()
3490
3765
  }),
3491
- z18.object({ type: z18.literal("manual") })
3766
+ z17.object({ type: z17.literal("manual") })
3492
3767
  ]);
3493
- repoDependencySchema = z18.object({
3494
- name: z18.string().regex(k8sNameRegex, "Must be a valid Kubernetes name"),
3495
- repo: z18.string(),
3496
- fallback_branch: z18.string().default("main"),
3768
+ repositorySettingsSchema = z17.object({
3769
+ repo: z17.string().regex(repoFullNameRegex, "Must be an owner/repo full name"),
3770
+ fallback_branch: z17.string().default("main"),
3497
3771
  /**
3498
- * The concrete commit SHA the dependency was deployed at. Absent in
3499
- * user-authored config: previewkit resolves the dependency's branch to a
3500
- * commit at deploy time and records it here by enriching the stored
3772
+ * The concrete commit SHA the repository was deployed at. Absent in
3773
+ * user-authored config: previewkit resolves each dependency repo's branch to
3774
+ * a commit at deploy time and records it here by enriching the stored
3501
3775
  * `resolvedConfig` (deploy provenance, not authored input). Multi-repo
3502
3776
  * grounding reads this back to inspect the exact code that was live.
3503
3777
  */
3504
- sha: z18.string().optional()
3505
- });
3506
- multirepoConfigSchema = z18.object({
3507
- branch_convention: branchConventionSchema.optional(),
3508
- repos: z18.array(repoDependencySchema).default([])
3778
+ sha: z17.string().optional()
3509
3779
  });
3510
- configSchema = z18.object({
3511
- multirepo: multirepoConfigSchema.optional()
3780
+ hookStepSchema = z17.object({
3781
+ app: z17.string(),
3782
+ command: z17.string()
3512
3783
  });
3513
- hookStepSchema = z18.object({
3514
- app: z18.string(),
3515
- command: z18.string()
3516
- });
3517
- hooksSchema = z18.object({
3518
- pre_deploy: z18.array(hookStepSchema).default([]),
3519
- post_deploy: z18.array(hookStepSchema).default([])
3784
+ hooksSchema = z17.object({
3785
+ pre_deploy: z17.array(hookStepSchema).default([]),
3786
+ post_deploy: z17.array(hookStepSchema).default([])
3520
3787
  }).default({ pre_deploy: [], post_deploy: [] });
3521
- databaseSetupLocationSchema = z18.discriminatedUnion("type", [
3522
- z18.object({
3523
- type: z18.literal("in_build"),
3524
- app: z18.string().min(1, "an app is required for an in-build task"),
3525
- position: z18.enum(["before", "after"])
3788
+ databaseSetupLocationSchema = z17.discriminatedUnion("type", [
3789
+ z17.object({
3790
+ type: z17.literal("in_build"),
3791
+ app: z17.string().min(1, "an app is required for an in-build task"),
3792
+ position: z17.enum(["before", "after"])
3526
3793
  }),
3527
- z18.object({
3528
- type: z18.literal("separate_job"),
3529
- repo: z18.string().optional()
3794
+ z17.object({
3795
+ type: z17.literal("separate_job"),
3796
+ repo: z17.string().optional()
3530
3797
  })
3531
3798
  ]);
3532
- databaseSetupTaskSchema = z18.object({
3533
- command: z18.string(),
3534
- frequency: z18.enum(["on_create", "every_commit"]),
3799
+ databaseSetupTaskSchema = z17.object({
3800
+ command: z17.string(),
3801
+ frequency: z17.enum(["on_create", "every_commit"]),
3535
3802
  location: databaseSetupLocationSchema
3536
3803
  });
3537
3804
  nodeVersionRegex = /^\d+(\.\d+)?(\.\d+)?$/;
3538
- buildContextSchema = z18.enum(["app", "root"]).default("app");
3805
+ buildContextSchema = z17.enum(["app", "root"]).default("app");
3539
3806
  PREVIEWKIT_BUILD_SCRIPT_HEREDOC = "AUTONOMA_BUILD_EOF";
3807
+ imageTagSchema = z17.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "must be a valid image tag");
3540
3808
  DEPRECATED_BUILD_FRAMEWORKS = ["node", "next", "vite", "bun"];
3541
3809
  UNSUPPORTED_BUILD_METHOD_MESSAGE = `Unsupported build method. Use "runtime" (pick a runtime, then write a bash build_script and a single-line entrypoint) or "dockerfile" (build a Dockerfile committed in the repo). The ${DEPRECATED_BUILD_FRAMEWORKS.join(" / ")} framework presets are retired and can no longer be saved - express them as "runtime" with the install and build commands in build_script and the start command as entrypoint.`;
3542
3810
  deprecatedBuildArms = [
3543
3811
  nodeFrameworkBuildSchema("node"),
3544
3812
  nodeFrameworkBuildSchema("next"),
3545
3813
  nodeFrameworkBuildSchema("vite"),
3546
- z18.object({
3547
- framework: z18.literal("bun"),
3548
- install_command: z18.string().min(1).optional(),
3549
- build_command: z18.string().min(1).optional(),
3550
- run_command: z18.string().min(1).optional(),
3814
+ z17.object({
3815
+ framework: z17.literal("bun"),
3816
+ install_command: z17.string().min(1).optional(),
3817
+ build_command: z17.string().min(1).optional(),
3818
+ run_command: z17.string().min(1).optional(),
3551
3819
  build_context: buildContextSchema
3552
3820
  })
3553
3821
  ];
3554
3822
  authoredBuildArms = [
3555
- z18.object({
3556
- framework: z18.literal("dockerfile"),
3557
- dockerfile: z18.string().min(1, "dockerfile path is required"),
3558
- target: z18.string().min(1).optional(),
3823
+ z17.object({
3824
+ framework: z17.literal("dockerfile"),
3825
+ dockerfile: z17.string().min(1, "dockerfile path is required"),
3826
+ target: z17.string().min(1).optional(),
3559
3827
  build_context: buildContextSchema
3560
3828
  }),
3561
- z18.object({
3562
- framework: z18.literal("runtime"),
3563
- runtime: z18.enum(PREVIEWKIT_RUNTIME_IDS),
3829
+ z17.object({
3830
+ framework: z17.literal("runtime"),
3831
+ runtime: z17.enum(PREVIEWKIT_RUNTIME_IDS),
3564
3832
  // Image tag version, e.g. "22" for node. Optional - defaults to the
3565
3833
  // catalog's default per runtime. The user picks it so a repo pinned to an
3566
3834
  // older toolchain is not forced onto our default (which would defeat the
3567
3835
  // escape hatch). Constrained to a safe tag charset so it can never break
3568
3836
  // out of the generated `FROM` line.
3569
- version: z18.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "must be a valid image tag").optional(),
3837
+ version: imageTagSchema.optional(),
3570
3838
  // Both are raw bash. `build_script` bakes into the image (cached); the
3571
3839
  // entrypoint is the container start command. `build_script` is optional
3572
3840
  // (some apps need no build step); `entrypoint` is required - the
3573
3841
  // container has to start somehow. `app.command` still overrides it at
3574
3842
  // deploy time.
3575
- build_script: z18.string().min(1).refine(
3576
- (script) => !script.split("\n").includes(PREVIEWKIT_BUILD_SCRIPT_HEREDOC),
3577
- `build script cannot contain a line equal to "${PREVIEWKIT_BUILD_SCRIPT_HEREDOC}" (reserved heredoc delimiter)`
3578
- ).optional(),
3579
- // The entrypoint is baked verbatim into a single-line `CMD`, so a newline
3580
- // would break out of the CMD and inject a bogus Dockerfile instruction
3581
- // (e.g. "npm start\nnode server.js"). Constrain it to one line; use a
3582
- // start script referenced from here if you need multiple commands.
3583
- entrypoint: z18.string().min(1, "entrypoint is required").regex(/^[^\r\n]+$/, "entrypoint must be a single line (no line breaks)"),
3843
+ build_script: heredocSafeScript("build script").optional(),
3844
+ entrypoint: singleLineCommand("entrypoint"),
3584
3845
  build_context: buildContextSchema
3585
3846
  })
3586
3847
  ];
3587
- authoredBuildSchema = z18.discriminatedUnion("framework", authoredBuildArms, {
3848
+ authoredBuildSchema = z17.discriminatedUnion("framework", authoredBuildArms, {
3588
3849
  error: () => UNSUPPORTED_BUILD_METHOD_MESSAGE
3589
3850
  });
3590
- storedBuildSchema = z18.discriminatedUnion("framework", [...authoredBuildArms, ...deprecatedBuildArms]);
3591
- connectionSchema = z18.object({
3851
+ storedBuildSchema = z17.discriminatedUnion("framework", [...authoredBuildArms, ...deprecatedBuildArms]);
3852
+ presetBlueprintSchema = z17.object({
3853
+ preset: z17.enum(PREVIEWKIT_PRESET_IDS),
3854
+ // Overridable per app; each defaults from the preset (version from the runtime catalog).
3855
+ version: imageTagSchema.optional(),
3856
+ // These overrides flow into the generated Dockerfile via blueprintToBuild, so they
3857
+ // carry the same guards as the `runtime` build arm: install/build concatenate into
3858
+ // the build_script heredoc; run_command and output_directory reach the single-line CMD.
3859
+ install_command: heredocSafeScript("install_command").optional(),
3860
+ build_command: heredocSafeScript("build_command").optional(),
3861
+ run_command: singleLineCommand("run_command").optional(),
3862
+ // For static presets: the built output directory to serve (defaults to the preset's).
3863
+ output_directory: singleLineCommand("output_directory").optional(),
3864
+ // Monorepo: `root` builds from the repo root so sibling workspace packages resolve.
3865
+ // Works for every preset: commands run in the app directory; node installs at the
3866
+ // repo root (workspace linking) and builds through turbo's `--filter` when the repo
3867
+ // has turbo. Absent = app context; no default is stamped in.
3868
+ build_context: buildContextSchema.removeDefault().optional()
3869
+ }).strict().superRefine((blueprint, ctx) => {
3870
+ const toolchain = previewkitPresetSpec(blueprint.preset).toolchain;
3871
+ if (toolchain === "node" && blueprint.version != null && !nodeVersionRegex.test(blueprint.version)) {
3872
+ ctx.addIssue({
3873
+ code: z17.ZodIssueCode.custom,
3874
+ message: "version must look like 22, 22.5, or 22.5.0 for a node preset",
3875
+ path: ["version"]
3876
+ });
3877
+ }
3878
+ });
3879
+ dockerfileBlueprintSchema = z17.object({
3880
+ // Path to a committed Dockerfile, built as-is - ALWAYS relative to the app dir,
3881
+ // whatever the build context (the pipeline resolves it against the context).
3882
+ // `target` selects a stage in a multi-stage build.
3883
+ dockerfile: z17.string().min(1, "dockerfile path is required"),
3884
+ target: z17.string().min(1).optional(),
3885
+ // Monorepo: `root` builds from the repo root (so a workspace-aware Dockerfile can reach
3886
+ // sibling packages); `app` (absent) builds from the app dir.
3887
+ build_context: buildContextSchema.removeDefault().optional()
3888
+ }).strict();
3889
+ blueprintSchema = z17.union([presetBlueprintSchema, dockerfileBlueprintSchema]);
3890
+ connectionSchema = z17.object({
3592
3891
  key: SecretKeySchema.superRefine((key, ctx) => {
3593
3892
  if (isReservedPreviewkitEnvKey(key)) {
3594
3893
  ctx.addIssue({
3595
- code: z18.ZodIssueCode.custom,
3894
+ code: z17.ZodIssueCode.custom,
3596
3895
  message: `${key} is a reserved built-in variable and cannot be set.`
3597
3896
  });
3598
3897
  }
3599
3898
  }).describe("The env var name to inject into the app at runtime (e.g. DATABASE_URL)."),
3600
- value: z18.string().min(1, "value is required").describe(
3601
- "Template resolved at deploy time. {{name.property}} tokens reference apps/services/addons in this config by name. For a service: {{db.url}} = the full canonical connection string (postgres -> postgresql://preview:preview@<host>:<port>/preview; redis -> redis://...; mongodb -> mongodb://...), {{db.host}} = in-cluster DNS name, {{db.port}}. For an app: {{api.url}} = its public HTTPS URL, {{api.hostname}}. For an addon: {{name.<outputKey>}} (e.g. a Neon connectionString). Also available: {{pr}}, {{namespace}}, {{owner}}. Tokens can mix with literal text ('postgresql://user:pass@{{db.host}}:5432/mydb'), or be a plain literal ('production'). Prefer {{db.url}} to wire a database."
3899
+ value: z17.string().min(1, "value is required").describe(
3900
+ "Template resolved at deploy time. {{name.property}} tokens reference apps/services in this config by name. For a service: {{db.url}} = the full canonical connection string (postgres -> postgresql://preview:preview@<host>:<port>/preview; redis -> redis://...; mongodb -> mongodb://...), {{db.host}} = in-cluster DNS name, {{db.port}}. For an app: {{api.url}} = its public HTTPS URL, {{api.hostname}}. Also available: {{pr}}, {{namespace}}, {{owner}}. Tokens can mix with literal text ('postgresql://user:pass@{{db.host}}:5432/mydb'), or be a plain literal ('production'). Prefer {{db.url}} to wire a database."
3602
3901
  ),
3603
- build_time: z18.boolean().default(false).describe("Also pass the resolved value as a Docker build arg.")
3902
+ build_time: z17.boolean().default(false).describe("Also pass the resolved value as a Docker build arg.")
3604
3903
  });
3605
3904
  previewConfigSchema = buildPreviewConfigSchema(storedBuildSchema, false);
3606
3905
  trustedPreviewConfigSchema = buildPreviewConfigSchema(storedBuildSchema, true);
@@ -3608,6 +3907,68 @@ var init_previewkit_config = __esm({
3608
3907
  }
3609
3908
  });
3610
3909
 
3910
+ // ../../packages/types/src/schemas/previewkit-job-spec.ts
3911
+ import { z as z18 } from "zod";
3912
+ var deployTargetSchema, redeployTargetSchema, teardownTargetSchema, previewJobSpecSchema;
3913
+ var init_previewkit_job_spec = __esm({
3914
+ "../../packages/types/src/schemas/previewkit-job-spec.ts"() {
3915
+ "use strict";
3916
+ init_esm_shims();
3917
+ deployTargetSchema = z18.object({
3918
+ repoFullName: z18.string().min(1),
3919
+ prNumber: z18.number().int().nonnegative(),
3920
+ organizationId: z18.string().min(1),
3921
+ githubRepositoryId: z18.number().int(),
3922
+ headSha: z18.string().min(1),
3923
+ headRef: z18.string().min(1),
3924
+ branchId: z18.string().optional()
3925
+ });
3926
+ redeployTargetSchema = z18.object({
3927
+ repoFullName: z18.string().min(1),
3928
+ prNumber: z18.number().int().nonnegative(),
3929
+ organizationId: z18.string().min(1),
3930
+ githubRepositoryId: z18.number().int(),
3931
+ headSha: z18.string().min(1),
3932
+ headRef: z18.string().min(1)
3933
+ });
3934
+ teardownTargetSchema = z18.object({
3935
+ repoFullName: z18.string().min(1),
3936
+ prNumber: z18.number().int().nonnegative(),
3937
+ organizationId: z18.string().min(1),
3938
+ headSha: z18.string().min(1).optional()
3939
+ });
3940
+ previewJobSpecSchema = z18.discriminatedUnion("mode", [
3941
+ z18.object({ mode: z18.literal("deploy"), target: deployTargetSchema }),
3942
+ z18.object({ mode: z18.literal("teardown"), target: teardownTargetSchema }),
3943
+ z18.object({
3944
+ mode: z18.literal("redeploy-app"),
3945
+ target: redeployTargetSchema,
3946
+ namespace: z18.string().min(1),
3947
+ appName: z18.string().min(1),
3948
+ redeployMode: z18.enum(["rebuild", "restart"])
3949
+ })
3950
+ ]);
3951
+ }
3952
+ });
3953
+
3954
+ // ../../packages/types/src/schemas/previewkit-node-build.ts
3955
+ var init_previewkit_node_build = __esm({
3956
+ "../../packages/types/src/schemas/previewkit-node-build.ts"() {
3957
+ "use strict";
3958
+ init_esm_shims();
3959
+ init_previewkit_node_pm();
3960
+ }
3961
+ });
3962
+
3963
+ // ../../packages/types/src/schemas/previewkit-authorable-config.ts
3964
+ var init_previewkit_authorable_config = __esm({
3965
+ "../../packages/types/src/schemas/previewkit-authorable-config.ts"() {
3966
+ "use strict";
3967
+ init_esm_shims();
3968
+ init_previewkit_node_build();
3969
+ }
3970
+ });
3971
+
3611
3972
  // ../../packages/types/src/schemas/previewkit-redeploy-app.ts
3612
3973
  import { z as z19 } from "zod";
3613
3974
  var RedeployPreviewkitAppInputSchema;
@@ -3769,138 +4130,15 @@ var init_snapshot_dependency_pin = __esm({
3769
4130
  }
3770
4131
  });
3771
4132
 
3772
- // ../../packages/types/src/schemas/healing-actions.ts
3773
- import { z as z24 } from "zod";
3774
- var reviewSeveritySchema, primaryScreenshotRefSchema, evidenceItemSchema, healingReviewLinkSchema, updatePlanActionSchema, reportBugActionSchema, reportEngineLimitationActionSchema, reportUnknownIssueActionSchema, reportScenarioUnsupportedActionSchema, removeTestActionSchema, healingActionSchema, issueReportInputSchema, updatePlanInputSchema, reportBugInputSchema, reportEngineLimitationInputSchema, reportUnknownIssueInputSchema, reportScenarioUnsupportedInputSchema, removeTestInputSchema;
3775
- var init_healing_actions = __esm({
3776
- "../../packages/types/src/schemas/healing-actions.ts"() {
3777
- "use strict";
3778
- init_esm_shims();
3779
- init_issue_report();
3780
- init_suspected_cause();
3781
- reviewSeveritySchema = z24.enum(["critical", "high", "medium", "low"]);
3782
- primaryScreenshotRefSchema = z24.object({
3783
- stepOrder: z24.number().int().min(0).describe("The step order (from the failure's Execution steps / fetch_step_evidence) whose frame to feature."),
3784
- timing: z24.enum(["before", "after"]).describe("Which captured frame of that step best shows the bug: the screenshot before or after the step ran.")
3785
- });
3786
- evidenceItemSchema = z24.object({
3787
- type: z24.enum(["screenshot", "video", "conversation", "step_output"]),
3788
- description: z24.string(),
3789
- s3Key: z24.string().optional()
3790
- });
3791
- healingReviewLinkSchema = z24.object({ generationReviewId: z24.string() });
3792
- updatePlanActionSchema = z24.object({
3793
- kind: z24.literal("update_plan"),
3794
- planId: z24.string().describe("ID of the test plan to update"),
3795
- testCaseId: z24.string().describe("ID of the test case the plan belongs to"),
3796
- newPrompt: z24.string().describe("Replacement plan prompt - the natural language test instruction"),
3797
- reasoning: z24.string().describe("Why this rewrite addresses the failure")
3798
- });
3799
- reportBugActionSchema = z24.object({
3800
- kind: z24.literal("report_bug"),
3801
- testCaseId: z24.string().describe("ID of the test case that surfaced the bug"),
3802
- title: z24.string().describe("Short bug title"),
3803
- description: z24.string().describe("Full bug description with reproduction steps and root cause hypothesis"),
3804
- severity: reviewSeveritySchema,
3805
- evidence: z24.array(evidenceItemSchema).describe("Screenshots, videos, step outputs supporting the bug report"),
3806
- reasoning: z24.string().describe("Why this is an application bug rather than a test or engine issue"),
3807
- suspectedCause: suspectedCauseSchema.describe(
3808
- "The concrete code cause you re-grounded independently (>= 1 code reference). If you cannot reproduce the cause in the checked-out code, downgrade to report_unknown_issue instead of reporting a bug."
3809
- ),
3810
- // The persisted report carries the full shape (authored text + the
3811
- // system-derived evidenceManifest the report tool attaches from the agent's
3812
- // actual fetches). Optional so actions persisted without a report still parse
3813
- // (loadPriorActions / eval fixtures); the tool input (reportBugInputSchema)
3814
- // re-declares an authored (manifest-free) report as required, so every new
3815
- // report_bug carries one and the manifest is never model-authored.
3816
- report: issueReportSchema.optional().describe(
3817
- "The customer-facing, evidence-grounded report shown on the bug page: Expected vs Actual plus a rich narrative. Author it from the evidence you fetched with fetch_step_evidence, not from the plan text alone."
3818
- ),
3819
- reviewLink: healingReviewLinkSchema
3820
- });
3821
- reportEngineLimitationActionSchema = z24.object({
3822
- kind: z24.literal("report_engine_limitation"),
3823
- testCaseId: z24.string().describe("ID of the test case that surfaced the limitation"),
3824
- title: z24.string(),
3825
- description: z24.string().describe("What the engine/agent could not do, and why no workaround is feasible"),
3826
- severity: reviewSeveritySchema,
3827
- evidence: z24.array(evidenceItemSchema),
3828
- reasoning: z24.string(),
3829
- reviewLink: healingReviewLinkSchema
3830
- });
3831
- reportUnknownIssueActionSchema = z24.object({
3832
- kind: z24.literal("report_unknown_issue"),
3833
- testCaseId: z24.string().describe("ID of the test case that surfaced the suspected issue"),
3834
- title: z24.string(),
3835
- description: z24.string().describe("What the application appeared to do wrong, and why the cause could not be grounded in the code"),
3836
- severity: reviewSeveritySchema,
3837
- evidence: z24.array(evidenceItemSchema),
3838
- reasoning: z24.string().describe("Why this is a suspected issue you could not ground in code rather than a confirmed application bug"),
3839
- reviewLink: healingReviewLinkSchema
3840
- });
3841
- reportScenarioUnsupportedActionSchema = z24.object({
3842
- kind: z24.literal("report_scenario_unsupported"),
3843
- testCaseId: z24.string().describe("ID of the test case that is impossible given the current scenario data"),
3844
- title: z24.string(),
3845
- description: z24.string().describe(
3846
- "What the test needs that the scenario data cannot currently provide, with the proposed scenario extension woven in as prose - surfaced verbatim to a human, who decides whether to extend the scenario"
3847
- ),
3848
- severity: reviewSeveritySchema,
3849
- evidence: z24.array(evidenceItemSchema),
3850
- reasoning: z24.string().describe(
3851
- "Why this test is impossible given the current scenario data (a true data gap) rather than a stale plan that should be rewritten"
3852
- ),
3853
- reviewLink: healingReviewLinkSchema
3854
- });
3855
- removeTestActionSchema = z24.object({
3856
- kind: z24.literal("remove_test"),
3857
- testCaseId: z24.string().describe("ID of the test case to delete from the suite"),
3858
- reason: z24.string().describe(
3859
- "Why this test should be removed: either it is invalid (not a viable flow, never useful without becoming a different test) or its feature was deleted from the app"
3860
- ),
3861
- evidence: z24.array(evidenceItemSchema).optional().describe("Optional screenshots, videos, step outputs supporting the removal"),
3862
- reviewLink: healingReviewLinkSchema
3863
- });
3864
- healingActionSchema = z24.discriminatedUnion("kind", [
3865
- updatePlanActionSchema,
3866
- reportBugActionSchema,
3867
- reportEngineLimitationActionSchema,
3868
- reportUnknownIssueActionSchema,
3869
- reportScenarioUnsupportedActionSchema,
3870
- removeTestActionSchema
3871
- ]);
3872
- issueReportInputSchema = authoredIssueReportSchema.extend({
3873
- primaryScreenshot: primaryScreenshotRefSchema.optional().describe(
3874
- "Optional: the frame that best shows the bug, referenced by the step you inspected with fetch_step_evidence (its order + before/after). Designate one when a step's frame shows the bug more clearly than the mechanical failing step; omit it to let the page fall back to the failing-step screenshot. Do not invent a step you did not fetch."
3875
- )
3876
- });
3877
- updatePlanInputSchema = updatePlanActionSchema.omit({ kind: true });
3878
- reportBugInputSchema = reportBugActionSchema.omit({ kind: true, reviewLink: true }).extend({ report: issueReportInputSchema });
3879
- reportEngineLimitationInputSchema = reportEngineLimitationActionSchema.omit({
3880
- kind: true,
3881
- reviewLink: true
3882
- });
3883
- reportUnknownIssueInputSchema = reportUnknownIssueActionSchema.omit({
3884
- kind: true,
3885
- reviewLink: true
3886
- });
3887
- reportScenarioUnsupportedInputSchema = reportScenarioUnsupportedActionSchema.omit({
3888
- kind: true,
3889
- reviewLink: true
3890
- });
3891
- removeTestInputSchema = removeTestActionSchema.omit({ kind: true, reviewLink: true });
3892
- }
3893
- });
3894
-
3895
4133
  // ../../packages/types/src/schemas/checkpoint-summary.ts
3896
- import { z as z25 } from "zod";
4134
+ import { z as z24 } from "zod";
3897
4135
  var checkpointToneSchema, checkpointExecutionStateSchema, checkpointTestCountsSchema, checkpointFailingByKindSchema, checkpointAnalysisSummarySchema, checkpointPresentationSummarySchema;
3898
4136
  var init_checkpoint_summary = __esm({
3899
4137
  "../../packages/types/src/schemas/checkpoint-summary.ts"() {
3900
4138
  "use strict";
3901
4139
  init_esm_shims();
3902
- checkpointToneSchema = z25.enum(["success", "critical", "warning", "neutral"]);
3903
- checkpointExecutionStateSchema = z25.enum([
4140
+ checkpointToneSchema = z24.enum(["success", "critical", "warning", "neutral"]);
4141
+ checkpointExecutionStateSchema = z24.enum([
3904
4142
  "not_started",
3905
4143
  "running",
3906
4144
  "stale",
@@ -3909,43 +4147,43 @@ var init_checkpoint_summary = __esm({
3909
4147
  "pipeline_failed",
3910
4148
  "unknown"
3911
4149
  ]);
3912
- checkpointTestCountsSchema = z25.object({
3913
- assigned: z25.number(),
3914
- run: z25.number(),
3915
- passed: z25.number(),
3916
- failed: z25.number(),
4150
+ checkpointTestCountsSchema = z24.object({
4151
+ assigned: z24.number(),
4152
+ run: z24.number(),
4153
+ passed: z24.number(),
4154
+ failed: z24.number(),
3917
4155
  // Tests that never ran because their scenario setup failed.
3918
- setupFailed: z25.number(),
3919
- running: z25.number(),
3920
- notRun: z25.number()
4156
+ setupFailed: z24.number(),
4157
+ running: z24.number(),
4158
+ notRun: z24.number()
3921
4159
  });
3922
- checkpointFailingByKindSchema = z25.object({
3923
- engine: z25.number(),
3924
- app: z25.number()
4160
+ checkpointFailingByKindSchema = z24.object({
4161
+ engine: z24.number(),
4162
+ app: z24.number()
3925
4163
  });
3926
- checkpointAnalysisSummarySchema = z25.object({
4164
+ checkpointAnalysisSummarySchema = z24.object({
3927
4165
  // The AnalysisJob lifecycle. Mirrors the `AnalysisJobStatus` db enum (types cannot import it).
3928
- jobStatus: z25.enum(["running", "completed", "failed"]),
4166
+ jobStatus: z24.enum(["running", "completed", "failed"]),
3929
4167
  // Client-bug findings - the only plane that counts against the PR (turns the checkpoint red).
3930
- bugCount: z25.number().int().nonnegative(),
4168
+ bugCount: z24.number().int().nonnegative(),
3931
4169
  // Findings that passed on the app-health plane.
3932
- passedCount: z25.number().int().nonnegative(),
4170
+ passedCount: z24.number().int().nonnegative(),
3933
4171
  // Coverage-plane findings (engine_artifact / environment_failure / scenario_issue / delete) - never a
3934
4172
  // failure; surfaced as "couldn't confirm".
3935
- coverageCount: z25.number().int().nonnegative()
4173
+ coverageCount: z24.number().int().nonnegative()
3936
4174
  });
3937
- checkpointPresentationSummarySchema = z25.object({
4175
+ checkpointPresentationSummarySchema = z24.object({
3938
4176
  tone: checkpointToneSchema,
3939
- label: z25.string(),
3940
- reason: z25.string().optional(),
4177
+ label: z24.string(),
4178
+ reason: z24.string().optional(),
3941
4179
  executionState: checkpointExecutionStateSchema,
3942
4180
  // Unique open application bugs.
3943
- openBugCount: z25.number(),
4181
+ openBugCount: z24.number(),
3944
4182
  // Raw application-issue occurrences.
3945
- issueOccurrenceCount: z25.number(),
4183
+ issueOccurrenceCount: z24.number(),
3946
4184
  testCounts: checkpointTestCountsSchema,
3947
4185
  failingByKind: checkpointFailingByKindSchema,
3948
- suiteChangeCount: z25.number(),
4186
+ suiteChangeCount: z24.number(),
3949
4187
  // Set only for authoritative-analysis snapshots; absent for legacy diffs/shadow snapshots.
3950
4188
  analysis: checkpointAnalysisSummarySchema.optional()
3951
4189
  });
@@ -3953,96 +4191,82 @@ var init_checkpoint_summary = __esm({
3953
4191
  });
3954
4192
 
3955
4193
  // ../../packages/types/src/schemas/snapshot-report.ts
3956
- import { z as z26 } from "zod";
3957
- var reportHealthSchema, reportTestStatusSchema, reportCommitFileSchema, snapshotReportTriggerSchema, snapshotReportSelectedTestSchema, snapshotReportSelectionSchema, snapshotReportTestResultSchema, snapshotReportResultsSchema, snapshotReportBugSchema, snapshotReportHealthCountsSchema, snapshotReportSchema;
4194
+ import { z as z25 } from "zod";
4195
+ var reportHealthSchema, reportTestStatusSchema, reportCommitFileSchema, snapshotReportTriggerSchema, snapshotReportTestResultSchema, snapshotReportResultsSchema, snapshotReportBugSchema, snapshotReportHealthCountsSchema, snapshotReportSchema;
3958
4196
  var init_snapshot_report = __esm({
3959
4197
  "../../packages/types/src/schemas/snapshot-report.ts"() {
3960
4198
  "use strict";
3961
4199
  init_esm_shims();
3962
4200
  init_checkpoint_summary();
3963
- reportHealthSchema = z26.enum(["healthy", "critical", "running", "unknown"]);
3964
- reportTestStatusSchema = z26.enum(["passed", "failed", "setup_failed", "running", "pending"]);
3965
- reportCommitFileSchema = z26.object({
3966
- filename: z26.string(),
3967
- status: z26.string(),
3968
- additions: z26.number(),
3969
- deletions: z26.number()
3970
- });
3971
- snapshotReportTriggerSchema = z26.object({
3972
- headSha: z26.string().optional(),
3973
- baseSha: z26.string().optional(),
3974
- source: z26.string(),
3975
- createdAt: z26.date(),
3976
- commit: z26.object({ message: z26.string(), authorLogin: z26.string().optional() }).optional(),
3977
- filesChanged: z26.array(reportCommitFileSchema),
3978
- filesChangedTruncated: z26.boolean()
3979
- });
3980
- snapshotReportSelectedTestSchema = z26.object({
3981
- testCaseId: z26.string(),
3982
- name: z26.string(),
3983
- slug: z26.string(),
3984
- affectedReason: z26.string().optional(),
3985
- reasoning: z26.string().optional()
3986
- });
3987
- snapshotReportSelectionSchema = z26.object({
3988
- totalSuiteTests: z26.number(),
3989
- selected: z26.array(snapshotReportSelectedTestSchema),
3990
- analysisReasoning: z26.string().optional()
3991
- });
3992
- snapshotReportTestResultSchema = z26.object({
3993
- testCaseId: z26.string(),
3994
- name: z26.string(),
3995
- slug: z26.string(),
4201
+ reportHealthSchema = z25.enum(["healthy", "critical", "running", "unknown"]);
4202
+ reportTestStatusSchema = z25.enum(["passed", "failed", "setup_failed", "running", "pending"]);
4203
+ reportCommitFileSchema = z25.object({
4204
+ filename: z25.string(),
4205
+ status: z25.string(),
4206
+ additions: z25.number(),
4207
+ deletions: z25.number()
4208
+ });
4209
+ snapshotReportTriggerSchema = z25.object({
4210
+ headSha: z25.string().optional(),
4211
+ baseSha: z25.string().optional(),
4212
+ source: z25.string(),
4213
+ createdAt: z25.date(),
4214
+ commit: z25.object({ message: z25.string(), authorLogin: z25.string().optional() }).optional(),
4215
+ filesChanged: z25.array(reportCommitFileSchema),
4216
+ filesChangedTruncated: z25.boolean()
4217
+ });
4218
+ snapshotReportTestResultSchema = z25.object({
4219
+ testCaseId: z25.string(),
4220
+ name: z25.string(),
4221
+ slug: z25.string(),
3996
4222
  status: reportTestStatusSchema,
3997
- runId: z26.string().optional(),
3998
- durationMs: z26.number().optional()
3999
- });
4000
- snapshotReportResultsSchema = z26.object({
4001
- durationMs: z26.number().optional(),
4002
- passed: z26.number(),
4003
- failed: z26.number(),
4004
- setupFailed: z26.number(),
4005
- pending: z26.number(),
4006
- running: z26.number(),
4007
- total: z26.number(),
4008
- tests: z26.array(snapshotReportTestResultSchema)
4009
- });
4010
- snapshotReportBugSchema = z26.object({
4011
- bugId: z26.string(),
4012
- title: z26.string(),
4013
- description: z26.string(),
4014
- severity: z26.string(),
4015
- status: z26.string(),
4016
- occurrences: z26.number(),
4017
- testSlug: z26.string().optional(),
4018
- stepIndex: z26.number().optional(),
4019
- stepTotal: z26.number().optional(),
4020
- screenshotUrl: z26.string().optional(),
4021
- issueId: z26.string().optional()
4022
- });
4023
- snapshotReportHealthCountsSchema = z26.object({
4024
- failing: z26.number(),
4025
- passing: z26.number(),
4026
- running: z26.number(),
4027
- setupFailed: z26.number(),
4028
- notAffected: z26.number(),
4029
- totalTests: z26.number()
4030
- });
4031
- snapshotReportSchema = z26.object({
4032
- snapshot: z26.object({
4033
- id: z26.string(),
4034
- status: z26.string(),
4035
- source: z26.string(),
4036
- headSha: z26.string().optional(),
4037
- baseSha: z26.string().optional(),
4038
- createdAt: z26.date(),
4039
- branch: z26.object({ id: z26.string(), name: z26.string(), prNumber: z26.number().optional() })
4223
+ runId: z25.string().optional(),
4224
+ durationMs: z25.number().optional()
4225
+ });
4226
+ snapshotReportResultsSchema = z25.object({
4227
+ durationMs: z25.number().optional(),
4228
+ passed: z25.number(),
4229
+ failed: z25.number(),
4230
+ setupFailed: z25.number(),
4231
+ pending: z25.number(),
4232
+ running: z25.number(),
4233
+ total: z25.number(),
4234
+ tests: z25.array(snapshotReportTestResultSchema)
4235
+ });
4236
+ snapshotReportBugSchema = z25.object({
4237
+ bugId: z25.string(),
4238
+ title: z25.string(),
4239
+ description: z25.string(),
4240
+ severity: z25.string(),
4241
+ status: z25.string(),
4242
+ occurrences: z25.number(),
4243
+ testSlug: z25.string().optional(),
4244
+ stepIndex: z25.number().optional(),
4245
+ stepTotal: z25.number().optional(),
4246
+ screenshotUrl: z25.string().optional(),
4247
+ issueId: z25.string().optional()
4248
+ });
4249
+ snapshotReportHealthCountsSchema = z25.object({
4250
+ failing: z25.number(),
4251
+ passing: z25.number(),
4252
+ running: z25.number(),
4253
+ setupFailed: z25.number(),
4254
+ notAffected: z25.number(),
4255
+ totalTests: z25.number()
4256
+ });
4257
+ snapshotReportSchema = z25.object({
4258
+ snapshot: z25.object({
4259
+ id: z25.string(),
4260
+ status: z25.string(),
4261
+ source: z25.string(),
4262
+ headSha: z25.string().optional(),
4263
+ baseSha: z25.string().optional(),
4264
+ createdAt: z25.date(),
4265
+ branch: z25.object({ id: z25.string(), name: z25.string(), prNumber: z25.number().optional() })
4040
4266
  }),
4041
4267
  trigger: snapshotReportTriggerSchema,
4042
- selection: snapshotReportSelectionSchema,
4043
4268
  results: snapshotReportResultsSchema,
4044
- bugs: z26.array(snapshotReportBugSchema),
4045
- firstIterationReasoning: z26.string().optional(),
4269
+ bugs: z25.array(snapshotReportBugSchema),
4046
4270
  health: reportHealthSchema,
4047
4271
  healthCounts: snapshotReportHealthCountsSchema,
4048
4272
  summary: checkpointPresentationSummarySchema.optional()
@@ -4051,55 +4275,74 @@ var init_snapshot_report = __esm({
4051
4275
  });
4052
4276
 
4053
4277
  // ../../packages/types/src/schemas/pr-pipeline-status.ts
4054
- import { z as z27 } from "zod";
4278
+ import { z as z26 } from "zod";
4055
4279
  var prPipelineStatusSchema;
4056
4280
  var init_pr_pipeline_status = __esm({
4057
4281
  "../../packages/types/src/schemas/pr-pipeline-status.ts"() {
4058
4282
  "use strict";
4059
4283
  init_esm_shims();
4060
4284
  init_checkpoint_summary();
4061
- prPipelineStatusSchema = z27.discriminatedUnion("kind", [
4062
- z27.object({ kind: z27.literal("checkpoint"), summary: checkpointPresentationSummarySchema }),
4063
- z27.object({ kind: z27.literal("building") }),
4064
- z27.object({ kind: z27.literal("pending_checks") }),
4065
- z27.object({ kind: z27.literal("analyzing") }),
4066
- z27.object({ kind: z27.literal("analysis_failed") }),
4067
- z27.object({ kind: z27.literal("build_failed") }),
4068
- z27.object({ kind: z27.literal("none") })
4285
+ prPipelineStatusSchema = z26.discriminatedUnion("kind", [
4286
+ z26.object({ kind: z26.literal("checkpoint"), summary: checkpointPresentationSummarySchema }),
4287
+ z26.object({ kind: z26.literal("building") }),
4288
+ z26.object({ kind: z26.literal("pending_checks") }),
4289
+ z26.object({ kind: z26.literal("analyzing") }),
4290
+ z26.object({ kind: z26.literal("analysis_failed") }),
4291
+ z26.object({ kind: z26.literal("build_failed") }),
4292
+ z26.object({ kind: z26.literal("none") })
4069
4293
  ]);
4070
4294
  }
4071
4295
  });
4072
4296
 
4073
4297
  // ../../packages/types/src/schemas/bug-detail.ts
4074
- import { z as z28 } from "zod";
4298
+ import { z as z27 } from "zod";
4075
4299
  var runAnalysisSchema, pointSchema2, stepOutputDataSchema, bugOccurrenceSchema;
4076
4300
  var init_bug_detail = __esm({
4077
4301
  "../../packages/types/src/schemas/bug-detail.ts"() {
4078
4302
  "use strict";
4079
4303
  init_esm_shims();
4080
4304
  init_generation_verdict();
4081
- runAnalysisSchema = z28.object({
4305
+ runAnalysisSchema = z27.object({
4082
4306
  failurePoint: failurePointSchema.optional(),
4083
- evidence: z28.array(reviewEvidenceSchema).default([])
4307
+ evidence: z27.array(reviewEvidenceSchema).default([])
4084
4308
  });
4085
- pointSchema2 = z28.object({ x: z28.number(), y: z28.number() });
4086
- stepOutputDataSchema = z28.object({
4087
- outcome: z28.string().optional(),
4309
+ pointSchema2 = z27.object({ x: z27.number(), y: z27.number() });
4310
+ stepOutputDataSchema = z27.object({
4311
+ outcome: z27.string().optional(),
4088
4312
  point: pointSchema2.optional(),
4089
4313
  startPoint: pointSchema2.optional(),
4090
4314
  endPoint: pointSchema2.optional()
4091
4315
  });
4092
- bugOccurrenceSchema = z28.object({
4093
- issueId: z28.string(),
4094
- source: z28.enum(["run", "generation"]),
4095
- runId: z28.string().optional(),
4096
- generationId: z28.string().optional(),
4097
- createdAt: z28.date(),
4098
- isLatest: z28.boolean(),
4099
- snapshotId: z28.string().optional(),
4100
- sha: z28.string().optional(),
4101
- prNumber: z28.number().optional(),
4102
- branchName: z28.string().optional()
4316
+ bugOccurrenceSchema = z27.object({
4317
+ issueId: z27.string(),
4318
+ source: z27.enum(["run", "generation"]),
4319
+ runId: z27.string().optional(),
4320
+ generationId: z27.string().optional(),
4321
+ createdAt: z27.date(),
4322
+ isLatest: z27.boolean(),
4323
+ snapshotId: z27.string().optional(),
4324
+ sha: z27.string().optional(),
4325
+ prNumber: z27.number().optional(),
4326
+ branchName: z27.string().optional()
4327
+ });
4328
+ }
4329
+ });
4330
+
4331
+ // ../../packages/types/src/schemas/activation-triggers.ts
4332
+ import { z as z28 } from "zod";
4333
+ var MAX_LABEL_LENGTH, CONTROL_CHARS, AnalysisTriggerLabelSchema, TriggerConfigSchema;
4334
+ var init_activation_triggers = __esm({
4335
+ "../../packages/types/src/schemas/activation-triggers.ts"() {
4336
+ "use strict";
4337
+ init_esm_shims();
4338
+ MAX_LABEL_LENGTH = 50;
4339
+ CONTROL_CHARS = /[\u0000-\u001F\u007F]/;
4340
+ AnalysisTriggerLabelSchema = z28.string().trim().min(1, "Label cannot be empty").max(MAX_LABEL_LENGTH, `Label must be at most ${MAX_LABEL_LENGTH} characters`).refine((value) => !CONTROL_CHARS.test(value), "Label cannot contain control characters");
4341
+ TriggerConfigSchema = z28.object({
4342
+ /** Whether marking a PR ready-for-review automatically starts an analysis run. */
4343
+ autoRunOnReadyForReview: z28.boolean(),
4344
+ /** The PR label whose addition starts an analysis run. */
4345
+ analysisTriggerLabel: AnalysisTriggerLabelSchema
4103
4346
  });
4104
4347
  }
4105
4348
  });
@@ -4254,21 +4497,25 @@ var init_schemas = __esm({
4254
4497
  init_generation();
4255
4498
  init_api_key();
4256
4499
  init_secrets();
4257
- init_org_secrets();
4258
4500
  init_previewkit_builtins();
4259
4501
  init_previewkit_config();
4502
+ init_previewkit_job_spec();
4503
+ init_previewkit_node_build();
4504
+ init_previewkit_node_pm();
4505
+ init_previewkit_presets();
4506
+ init_previewkit_authorable_config();
4260
4507
  init_previewkit_runtimes();
4261
4508
  init_previewkit_redeploy_app();
4262
4509
  init_previewkit_service_suggestion();
4263
4510
  init_previewkit_env_suggestion();
4264
4511
  init_previewkit_diagnosis();
4265
4512
  init_snapshot_dependency_pin();
4266
- init_healing_actions();
4267
4513
  init_snapshot_report();
4268
4514
  init_checkpoint_summary();
4269
4515
  init_pr_pipeline_status();
4270
4516
  init_bug_detail();
4271
4517
  init_investigation_report();
4518
+ init_activation_triggers();
4272
4519
  init_suite_health();
4273
4520
  init_suite_health_fix_plan();
4274
4521
  PlatformSchema = z31.enum(["web", "ios", "android"]);
@@ -4463,6 +4710,14 @@ var init_designated_run = __esm({
4463
4710
  }
4464
4711
  });
4465
4712
 
4713
+ // ../../packages/types/src/deployment-signal-template.ts
4714
+ var init_deployment_signal_template = __esm({
4715
+ "../../packages/types/src/deployment-signal-template.ts"() {
4716
+ "use strict";
4717
+ init_esm_shims();
4718
+ }
4719
+ });
4720
+
4466
4721
  // ../../packages/types/src/constants/pipeline-labels.ts
4467
4722
  var init_pipeline_labels = __esm({
4468
4723
  "../../packages/types/src/constants/pipeline-labels.ts"() {
@@ -4497,6 +4752,32 @@ var init_previewkit = __esm({
4497
4752
  }
4498
4753
  });
4499
4754
 
4755
+ // ../../packages/types/src/types/parse-string-record.ts
4756
+ var init_parse_string_record = __esm({
4757
+ "../../packages/types/src/types/parse-string-record.ts"() {
4758
+ "use strict";
4759
+ init_esm_shims();
4760
+ }
4761
+ });
4762
+
4763
+ // ../../packages/types/src/types/previewkit-manifest.ts
4764
+ var init_previewkit_manifest = __esm({
4765
+ "../../packages/types/src/types/previewkit-manifest.ts"() {
4766
+ "use strict";
4767
+ init_esm_shims();
4768
+ init_previewkit_config();
4769
+ }
4770
+ });
4771
+
4772
+ // ../../packages/types/src/types/previewkit-preview-urls.ts
4773
+ var init_previewkit_preview_urls = __esm({
4774
+ "../../packages/types/src/types/previewkit-preview-urls.ts"() {
4775
+ "use strict";
4776
+ init_esm_shims();
4777
+ init_previewkit_config();
4778
+ }
4779
+ });
4780
+
4500
4781
  // ../../packages/types/src/index.ts
4501
4782
  var init_src = __esm({
4502
4783
  "../../packages/types/src/index.ts"() {
@@ -4510,10 +4791,14 @@ var init_src = __esm({
4510
4791
  init_preview_url();
4511
4792
  init_app_links();
4512
4793
  init_designated_run();
4794
+ init_deployment_signal_template();
4513
4795
  init_constants();
4514
4796
  init_architecture();
4515
4797
  init_step_overlay_points();
4516
4798
  init_previewkit();
4799
+ init_parse_string_record();
4800
+ init_previewkit_manifest();
4801
+ init_previewkit_preview_urls();
4517
4802
  }
4518
4803
  });
4519
4804
 
@@ -4521,7 +4806,9 @@ var init_src = __esm({
4521
4806
  import { readFile as readFile2 } from "fs/promises";
4522
4807
  import { join as join6 } from "path";
4523
4808
  async function loadRecipe(outputDir) {
4524
- const path3 = join6(outputDir, RECIPE_FILE);
4809
+ return await loadRecipeFile(join6(outputDir, RECIPE_FILE));
4810
+ }
4811
+ async function loadRecipeFile(path3) {
4525
4812
  let raw;
4526
4813
  try {
4527
4814
  raw = await readFile2(path3, "utf-8");
@@ -4535,19 +4822,35 @@ async function loadRecipe(outputDir) {
4535
4822
  } catch (err) {
4536
4823
  const message = err instanceof Error ? err.message : String(err);
4537
4824
  debugLog("recipe.json is not valid JSON", { path: path3, err });
4538
- return { status: "invalid", problems: [`${RECIPE_FILE} is not valid JSON: ${message}`] };
4825
+ return { status: "invalid", problems: [`${path3} is not valid JSON: ${message}`] };
4539
4826
  }
4540
- const parsed = ScenarioRecipesFileSchema.safeParse(withValidationCeremony(json));
4827
+ const normalized = withValidationCeremony(json);
4828
+ const parsed = ScenarioRecipesFileSchema.safeParse(normalized);
4541
4829
  if (!parsed.success) {
4542
- const problems = parsed.error.issues.map((issue) => {
4543
- const path4 = issue.path.length > 0 ? issue.path.join(".") : "(root)";
4544
- return `${path4}: ${issue.message}`;
4545
- });
4830
+ const problems = parsed.error.issues.map((issue) => describeIssue(issue, normalized));
4546
4831
  debugLog("recipe.json failed schema validation", { path: path3, problems });
4547
4832
  return { status: "invalid", problems };
4548
4833
  }
4549
4834
  return { status: "ok", recipe: parsed.data };
4550
4835
  }
4836
+ function describeIssue(issue, root) {
4837
+ const field = issue.path.length > 0 ? issue.path.join(".") : "(root)";
4838
+ const found = valueAtPath(root, issue.path);
4839
+ if (found === void 0) return `${field}: ${issue.message}`;
4840
+ return `${field}: ${issue.message} (found: ${JSON.stringify(found)})`;
4841
+ }
4842
+ function valueAtPath(root, path3) {
4843
+ let current = root;
4844
+ for (const key of path3) {
4845
+ if (Array.isArray(current) && typeof key === "number") {
4846
+ current = current[key];
4847
+ continue;
4848
+ }
4849
+ if (!isRecord(current)) return void 0;
4850
+ current = current[String(key)];
4851
+ }
4852
+ return current;
4853
+ }
4551
4854
  function findRecipeUploadProblems(recipe) {
4552
4855
  return recipe.recipes.flatMap((entry) => {
4553
4856
  const declaredTokens = new Set(Object.keys(entry.variables ?? {}));
@@ -4569,14 +4872,18 @@ function withValidationCeremony(json) {
4569
4872
  ...entry,
4570
4873
  validation: {
4571
4874
  ...validation,
4572
- status: validation.status ?? VALIDATION_STATUS,
4573
- phase: validation.phase ?? VALIDATION_PHASE
4875
+ status: VALIDATION_STATUS,
4876
+ phase: VALIDATION_PHASE,
4877
+ method: isKnownValidationMethod(validation.method) ? validation.method : VALIDATION_METHOD
4574
4878
  }
4575
4879
  };
4576
4880
  });
4577
4881
  return { ...json, recipes };
4578
4882
  }
4579
- var RECIPE_FILE, VALIDATION_STATUS, VALIDATION_PHASE;
4883
+ function isKnownValidationMethod(method) {
4884
+ return typeof method === "string" && VALIDATION_METHODS.has(method);
4885
+ }
4886
+ var RECIPE_FILE, VALIDATION_STATUS, VALIDATION_PHASE, VALIDATION_METHOD, VALIDATION_METHODS;
4580
4887
  var init_recipe = __esm({
4581
4888
  "src/agents/04-recipe-builder/recipe.ts"() {
4582
4889
  "use strict";
@@ -4586,6 +4893,8 @@ var init_recipe = __esm({
4586
4893
  RECIPE_FILE = "recipe.json";
4587
4894
  VALIDATION_STATUS = "validated";
4588
4895
  VALIDATION_PHASE = "ok";
4896
+ VALIDATION_METHOD = "endpoint-up-down";
4897
+ VALIDATION_METHODS = new Set(SCENARIO_VALIDATION_METHODS);
4589
4898
  }
4590
4899
  });
4591
4900
 
@@ -4700,6 +5009,11 @@ var init_submit = __esm({
4700
5009
  function resolveApiUrl(override) {
4701
5010
  return (override ?? DEFAULT_API_URL).replace(/\/+$/, "");
4702
5011
  }
5012
+ function resolveMcpUrl(apiUrl) {
5013
+ const base = apiUrl.replace(/\/+$/, "");
5014
+ if (base !== DEFAULT_API_URL) return `${base}/v1/mcp`;
5015
+ return `https://api.${new URL(DEFAULT_API_URL).hostname}/v1/mcp`;
5016
+ }
4703
5017
  var DEFAULT_API_URL;
4704
5018
  var init_api_url = __esm({
4705
5019
  "src/core/api-url.ts"() {
@@ -4958,6 +5272,37 @@ var init_errors = __esm({
4958
5272
  }
4959
5273
  });
4960
5274
 
5275
+ // src/core/onboarding-phase.ts
5276
+ function isStepAtOrPast(step, target) {
5277
+ return STEP_ORDER2.indexOf(step) >= STEP_ORDER2.indexOf(target);
5278
+ }
5279
+ function resolveEntryPhase(state) {
5280
+ if (!isStepAtOrPast(state.step, PREVIEW_DONE_STEP)) return "preview";
5281
+ if (!state.artifactsUploaded || !state.sdkConfigured) return "planner";
5282
+ if (!state.dryRunPassed) return "dryRun";
5283
+ return "done";
5284
+ }
5285
+ var STEP_ORDER2, PREVIEW_DONE_STEP, LIVE_STEP;
5286
+ var init_onboarding_phase = __esm({
5287
+ "src/core/onboarding-phase.ts"() {
5288
+ "use strict";
5289
+ init_esm_shims();
5290
+ STEP_ORDER2 = [
5291
+ "github",
5292
+ "preview_environment",
5293
+ "previewkit_configuring",
5294
+ "previewkit_deploying",
5295
+ "existing_deploys_configuring",
5296
+ "existing_deploys_waiting",
5297
+ "preview_verified",
5298
+ "diff_trigger",
5299
+ "completed"
5300
+ ];
5301
+ PREVIEW_DONE_STEP = "preview_verified";
5302
+ LIVE_STEP = "completed";
5303
+ }
5304
+ });
5305
+
4961
5306
  // src/replay/replay-transport.ts
4962
5307
  async function flushReplay(timeoutMs = 1500) {
4963
5308
  if (pending2.size === 0) return;
@@ -4993,52 +5338,520 @@ var init_replay_transport = __esm({
4993
5338
  this.buffer.push(event);
4994
5339
  this.bufferBytes += JSON.stringify(event).length;
4995
5340
  }
4996
- if (this.bufferBytes >= MAX_BATCH_BYTES) this.send();
5341
+ if (this.bufferBytes >= MAX_BATCH_BYTES) this.send();
5342
+ }
5343
+ flush() {
5344
+ this.send();
5345
+ }
5346
+ send() {
5347
+ if (this.buffer.length === 0) return;
5348
+ const events = this.buffer;
5349
+ const bytes = this.bufferBytes;
5350
+ this.buffer = [];
5351
+ this.bufferBytes = 0;
5352
+ this.sessionBytes += bytes;
5353
+ if (this.sessionBytes > MAX_SESSION_BYTES) {
5354
+ this.exhausted = true;
5355
+ debugLog("Session replay cap reached; stopping capture", {
5356
+ sessionBytes: this.sessionBytes,
5357
+ cap: MAX_SESSION_BYTES
5358
+ });
5359
+ return;
5360
+ }
5361
+ const body = JSON.stringify([
5362
+ {
5363
+ api_key: this.config.apiKey,
5364
+ event: "$snapshot",
5365
+ distinct_id: this.config.distinctId,
5366
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5367
+ properties: {
5368
+ $session_id: this.config.sessionId,
5369
+ $window_id: this.config.windowId,
5370
+ $snapshot_data: events,
5371
+ $snapshot_bytes: bytes,
5372
+ // The player treats a recording as web; the synthesized DOM is
5373
+ // a real DOM as far as it is concerned.
5374
+ $lib: "web",
5375
+ $lib_version: "1.0.0"
5376
+ }
5377
+ }
5378
+ ]);
5379
+ const promise = fetch(`${this.config.host}/s/`, {
5380
+ method: "POST",
5381
+ headers: { "Content-Type": "application/json" },
5382
+ body
5383
+ }).catch((err) => {
5384
+ debugLog("Session replay upload failed (ignored)", { err });
5385
+ }).finally(() => pending2.delete(promise));
5386
+ pending2.add(promise);
5387
+ }
5388
+ };
5389
+ }
5390
+ });
5391
+
5392
+ // src/core/autonoma-client.ts
5393
+ import { createTRPCClient, httpLink } from "@trpc/client";
5394
+ import superjson from "superjson";
5395
+ function buildClient(apiUrl, apiToken, timeoutMs) {
5396
+ return createTRPCClient({
5397
+ links: [
5398
+ // httpLink, not httpBatchLink: the CLI makes a handful of calls minutes
5399
+ // apart, so batching has nothing to batch and only widens the blast
5400
+ // radius of one slow procedure.
5401
+ httpLink({
5402
+ url: `${apiUrl}${TRPC_PATH}`,
5403
+ transformer: superjson,
5404
+ headers: () => ({ Authorization: `Bearer ${apiToken}` }),
5405
+ // AbortSignal.timeout rather than a manual controller: the timer is
5406
+ // cleared with the signal, so a slow-but-successful call cannot leave
5407
+ // a pending timeout holding the event loop open at exit.
5408
+ fetch: (url, options) => fetch(url, { ...options, signal: AbortSignal.timeout(timeoutMs) })
5409
+ })
5410
+ ]
5411
+ });
5412
+ }
5413
+ var TRPC_PATH, REQUEST_TIMEOUT_MS, SDK_REQUEST_TIMEOUT_MS, AutonomaClient;
5414
+ var init_autonoma_client = __esm({
5415
+ "src/core/autonoma-client.ts"() {
5416
+ "use strict";
5417
+ init_esm_shims();
5418
+ init_debug();
5419
+ init_logs();
5420
+ TRPC_PATH = "/v1/trpc";
5421
+ REQUEST_TIMEOUT_MS = 15e3;
5422
+ SDK_REQUEST_TIMEOUT_MS = 5 * 6e4;
5423
+ AutonomaClient = class {
5424
+ /** Status reads and other calls the API answers on its own. */
5425
+ trpc;
5426
+ /** Calls that reach through the API into the customer's deployed app. */
5427
+ slowTrpc;
5428
+ constructor(apiUrl, apiToken) {
5429
+ this.trpc = buildClient(apiUrl, apiToken, REQUEST_TIMEOUT_MS);
5430
+ this.slowTrpc = buildClient(apiUrl, apiToken, SDK_REQUEST_TIMEOUT_MS);
5431
+ }
5432
+ /** Onboarding state for the app, as the platform currently sees it. */
5433
+ async getOnboardingState(applicationId) {
5434
+ debugLog("Reading onboarding state", { applicationId });
5435
+ const state = await this.trpc.onboarding.getState.query({ applicationId });
5436
+ captureLog("info", "Read onboarding state", {
5437
+ source: "onboarding",
5438
+ step: state.step,
5439
+ preview_mode: state.previewEnvironmentMode ?? "unset",
5440
+ sdk_configured: state.sdkConfigured,
5441
+ dry_run_passed: state.dryRunPassed
5442
+ });
5443
+ return state;
5444
+ }
5445
+ /**
5446
+ * Re-check whether this app's preview environment is up.
5447
+ *
5448
+ * Called for its SIDE EFFECT as much as its answer: reading readiness is what
5449
+ * stamps a preview that has come up as `preview_verified`, and nothing else does.
5450
+ * `getOnboardingState` only reports the step someone else already stamped, so a
5451
+ * preview that goes ready after the agent stops polling would never be noticed by
5452
+ * a caller watching the step alone.
5453
+ */
5454
+ async refreshPreviewReadiness(applicationId) {
5455
+ debugLog("Refreshing preview readiness", { applicationId });
5456
+ await this.trpc.onboarding.getPreviewReadiness.query({ applicationId });
5457
+ }
5458
+ /**
5459
+ * Take the app live: from a verified preview through to Autonoma reviewing its
5460
+ * pull requests. Idempotent, and called without asking whether anyone already did.
5461
+ *
5462
+ * The CLI does this itself rather than leaving it to the coding agent, for the
5463
+ * same reason it makes the SDK and dry-run calls itself: no judgement is involved,
5464
+ * and asking an agent to make it is one more way for it not to happen. Here that
5465
+ * is not hypothetical - the preview phase stops the agent as soon as the platform
5466
+ * reports the preview verified, which is the exact moment the agent would have
5467
+ * gone live. Left to the agent, an app finishes a whole run one step short.
5468
+ */
5469
+ async takeAppLive(applicationId) {
5470
+ debugLog("Taking the app live", { applicationId });
5471
+ const result = await this.trpc.onboarding.takeLive.mutate({ applicationId });
5472
+ captureLog("info", "Took the app live", {
5473
+ source: "onboarding",
5474
+ step: result.step,
5475
+ already_live: result.alreadyLive
5476
+ });
5477
+ return { alreadyLive: result.alreadyLive, step: result.step };
5478
+ }
5479
+ /**
5480
+ * Mint a single-use pairing code for a coding agent this CLI is about to spawn.
5481
+ * Codes expire, so mint one per handoff rather than reusing an earlier one.
5482
+ */
5483
+ async createAgentPairing(applicationId) {
5484
+ debugLog("Minting agent pairing code", { applicationId });
5485
+ const { code } = await this.trpc.onboarding.createAgentPairing.mutate({ applicationId });
5486
+ captureLog("info", "Minted agent pairing code", { source: "onboarding" });
5487
+ return code;
5488
+ }
5489
+ /**
5490
+ * The preview environments the SDK validation and the dry run can run against:
5491
+ * the app's main preview plus one per open pull request, each carrying whether it
5492
+ * is deployed yet and which one the platform recognizes as the SDK handler's PR.
5493
+ */
5494
+ async listDryRunTargets(applicationId) {
5495
+ debugLog("Listing SDK dry-run targets", { applicationId });
5496
+ const targets = await this.trpc.onboarding.listSdkDryRunTargets.query({ applicationId });
5497
+ captureLog("info", "Listed SDK dry-run targets", {
5498
+ source: "dry_run",
5499
+ target_count: targets.targets.length,
5500
+ auto_detected: targets.autoDetectedTargetId != null
5501
+ });
5502
+ return targets;
5503
+ }
5504
+ /**
5505
+ * Provision a managed preview's Autonoma secrets so it can be validated. Returns
5506
+ * `redeploy_started` when mounting them changed the running app, in which case the
5507
+ * preview has to come back up before anything else is worth trying.
5508
+ */
5509
+ async prepareSdkTarget(applicationId, targetId) {
5510
+ debugLog("Preparing SDK target", { applicationId, targetId });
5511
+ return this.slowTrpc.onboarding.prepareSdkTarget.mutate({ applicationId, targetId });
5512
+ }
5513
+ /**
5514
+ * Call the app's SDK handler and store the schema it reports.
5515
+ *
5516
+ * `allowSelfHeal` lets the API redeploy once when the handler rejects our
5517
+ * signature, which for a managed preview can only be the platform's own secret
5518
+ * drift. Pass it on the first attempt and not on the retry, so a rejection that
5519
+ * survives a redeploy surfaces instead of looping.
5520
+ */
5521
+ async configureAndDiscoverSdkTarget(applicationId, targetId, allowSelfHeal) {
5522
+ debugLog("Discovering the SDK schema", { applicationId, targetId, allowSelfHeal });
5523
+ return this.slowTrpc.onboarding.configureAndDiscoverSdkTarget.mutate({
5524
+ applicationId,
5525
+ targetId,
5526
+ allowSelfHeal
5527
+ });
5528
+ }
5529
+ /** The app's scenarios - the named states its tests depend on. */
5530
+ async listScenarios(applicationId) {
5531
+ debugLog("Listing scenarios", { applicationId });
5532
+ return this.trpc.scenarios.list.query({ applicationId });
5533
+ }
5534
+ /**
5535
+ * Provision a scenario against a preview and tear it back down. The platform
5536
+ * records the app's dry run as passed once every scenario completes that cycle.
5537
+ */
5538
+ async runScenarioDryRun(applicationId, scenarioId, targetId) {
5539
+ debugLog("Running a scenario dry run", { applicationId, scenarioId, targetId });
5540
+ return this.slowTrpc.onboarding.runScenarioDryRun.mutate({ applicationId, scenarioId, targetId });
5541
+ }
5542
+ /**
5543
+ * Take the onboarding config mutex for this run, so the web app stops offering the
5544
+ * steps this run is about to do and points the user at their terminal instead.
5545
+ *
5546
+ * This is the same mutation behind the UI's "hand it back to the agent" control:
5547
+ * handing the mutex over is one state change whoever asks for it, and a second
5548
+ * procedure that set the same column would only be a way for the two to drift.
5549
+ *
5550
+ * Unconditional by design - a user starting this run IS the handoff, including
5551
+ * after an earlier take-over. What makes take-over stick is that this is called
5552
+ * once at the start of a run rather than polled, so a user who takes the config
5553
+ * back mid-run keeps it.
5554
+ */
5555
+ async claimAgentHold(applicationId) {
5556
+ debugLog("Claiming the onboarding config for this run", { applicationId });
5557
+ await this.trpc.onboarding.resumeAgent.mutate({ applicationId });
5558
+ captureLog("info", "Claimed the onboarding config for this run", { source: "onboarding" });
5559
+ }
5560
+ };
5561
+ }
5562
+ });
5563
+
5564
+ // src/core/coding-agent.ts
5565
+ var coding_agent_exports = {};
5566
+ __export(coding_agent_exports, {
5567
+ BaseLauncher: () => BaseLauncher,
5568
+ ClaudeLauncher: () => ClaudeLauncher,
5569
+ CodexLauncher: () => CodexLauncher,
5570
+ DEFAULT_PERMISSION_MODE: () => DEFAULT_PERMISSION_MODE,
5571
+ PERMISSION_MODE_LABELS: () => PERMISSION_MODE_LABELS,
5572
+ SPAWNED_BY_PLANNER_ENV: () => SPAWNED_BY_PLANNER_ENV,
5573
+ buildAllLaunchers: () => buildAllLaunchers,
5574
+ isSpawnedByPlanner: () => isSpawnedByPlanner,
5575
+ parsePermissionMode: () => parsePermissionMode,
5576
+ selectLauncher: () => selectLauncher,
5577
+ selectPermissionMode: () => selectPermissionMode
5578
+ });
5579
+ import spawn from "cross-spawn";
5580
+ import which from "which";
5581
+ function isSpawnedByPlanner(env = process.env) {
5582
+ return env[SPAWNED_BY_PLANNER_ENV] != null && env[SPAWNED_BY_PLANNER_ENV] !== "";
5583
+ }
5584
+ function buildAllLaunchers(opts) {
5585
+ return [new ClaudeLauncher(opts), new CodexLauncher(opts)];
5586
+ }
5587
+ async function selectLauncher(launchers, presetId, interactive = true) {
5588
+ const availability = await Promise.all(launchers.map((l) => l.isAvailable()));
5589
+ const available = launchers.filter((_, i) => availability[i]);
5590
+ debugLog("Detected available agents", { available: available.map((l) => l.id), presetId, interactive });
5591
+ if (presetId != null) {
5592
+ const preset = available.find((l) => l.id === presetId);
5593
+ if (preset != null) return preset;
5594
+ log.warn(`Requested agent "${presetId}" is not installed or not supported.`);
5595
+ }
5596
+ if (available.length === 0) return void 0;
5597
+ if (available.length === 1) {
5598
+ const only = available[0];
5599
+ log.info(`Found ${only.label} - will use it for the integration.`);
5600
+ return only;
5601
+ }
5602
+ if (!interactive) {
5603
+ const first = available[0];
5604
+ log.warn(
5605
+ `Several coding agents are installed and there is nobody to ask - using ${first.label}. Pass --agent to choose.`
5606
+ );
5607
+ return first;
5608
+ }
5609
+ const selected = await select({
5610
+ message: "Which agent should implement the integration?",
5611
+ options: available.map((l) => ({ value: l.id, label: l.label }))
5612
+ });
5613
+ if (isCancel(selected)) throw new Error("Agent selection cancelled");
5614
+ return available.find((l) => l.id === selected);
5615
+ }
5616
+ async function selectPermissionMode(preset) {
5617
+ if (preset != null) return preset;
5618
+ const selected = await select({
5619
+ message: "How much autonomy should the agent have?",
5620
+ options: [
5621
+ { value: "bypassPermissions", label: PERMISSION_MODE_LABELS.bypassPermissions },
5622
+ { value: "acceptEdits", label: PERMISSION_MODE_LABELS.acceptEdits },
5623
+ { value: "default", label: PERMISSION_MODE_LABELS.default }
5624
+ ],
5625
+ initialValue: DEFAULT_PERMISSION_MODE
5626
+ });
5627
+ if (isCancel(selected)) throw new Error("Permission mode selection cancelled");
5628
+ return selected;
5629
+ }
5630
+ function parsePermissionMode(value) {
5631
+ if (value === "default" || value === "acceptEdits" || value === "bypassPermissions") return value;
5632
+ return void 0;
5633
+ }
5634
+ function firstLine(output) {
5635
+ return output.split("\n").find((line) => line.trim().length > 0)?.trim() ?? "";
5636
+ }
5637
+ var MCP_COMMAND_TIMEOUT_MS, SPAWNED_BY_PLANNER_ENV, CODEX_BEARER_TOKEN_ENV, CLAUDE_CONNECTED_MARKER, DEFAULT_PERMISSION_MODE, PERMISSION_MODE_LABELS, CLAUDE_ID, CODEX_ID, BaseLauncher, ClaudeLauncher, CodexLauncher;
5638
+ var init_coding_agent = __esm({
5639
+ "src/core/coding-agent.ts"() {
5640
+ "use strict";
5641
+ init_esm_shims();
5642
+ init_prompts();
5643
+ init_debug();
5644
+ MCP_COMMAND_TIMEOUT_MS = 6e4;
5645
+ SPAWNED_BY_PLANNER_ENV = "AUTONOMA_PLANNER_SPAWNED_AGENT";
5646
+ CODEX_BEARER_TOKEN_ENV = "AUTONOMA_MCP_TOKEN";
5647
+ CLAUDE_CONNECTED_MARKER = "Connected";
5648
+ DEFAULT_PERMISSION_MODE = "bypassPermissions";
5649
+ PERMISSION_MODE_LABELS = {
5650
+ default: "Approve each command",
5651
+ acceptEdits: "Auto-edit files, approve commands",
5652
+ bypassPermissions: "Fully autonomous"
5653
+ };
5654
+ CLAUDE_ID = "claude";
5655
+ CODEX_ID = "codex";
5656
+ BaseLauncher = class {
5657
+ constructor(opts) {
5658
+ this.opts = opts;
5659
+ }
5660
+ async isAvailable() {
5661
+ const resolved = await which(this.command, { nothrow: true });
5662
+ debugLog("Probed for agent on PATH", { command: this.command, found: resolved != null });
5663
+ return resolved != null;
5664
+ }
5665
+ async launch(request) {
5666
+ if (isSpawnedByPlanner(this.opts.env)) {
5667
+ throw new Error(
5668
+ `Refusing to launch ${this.label}: this planner run was itself started by an agent the planner spawned (${SPAWNED_BY_PLANNER_ENV} is set). Run the planner directly instead.`
5669
+ );
5670
+ }
5671
+ debugLog(`Launching ${this.label}`, {
5672
+ permissionMode: request.permissionMode,
5673
+ interactive: request.interactive
5674
+ });
5675
+ const args = this.buildArgs(request.message, request.permissionMode, request.interactive);
5676
+ const stdio = request.interactive ? "inherit" : ["ignore", "inherit", "inherit"];
5677
+ return new Promise((resolve6) => {
5678
+ const proc = spawn(this.command, args, {
5679
+ cwd: this.opts.cwd,
5680
+ env: this.spawnEnv(request.env),
5681
+ stdio
5682
+ });
5683
+ const stopWatching = request.watch?.(proc) ?? (() => {
5684
+ });
5685
+ proc.on("error", (err) => {
5686
+ debugLog(`${this.label} failed to spawn`, { err });
5687
+ log.error(`Couldn't launch ${this.label}: ${err.message}`);
5688
+ stopWatching();
5689
+ resolve6(void 0);
5690
+ });
5691
+ proc.on("close", (code) => {
5692
+ debugLog(`${this.label} exited`, { code });
5693
+ stopWatching();
5694
+ resolve6(code ?? void 0);
5695
+ });
5696
+ });
5697
+ }
5698
+ /**
5699
+ * The env the agent runs with: what the CLI was given (so the canonical shared
5700
+ * secret reaches the app and the signed `sdk` calls), whatever this run adds
5701
+ * (an MCP registration's token), plus the recursion marker.
5702
+ */
5703
+ spawnEnv(extra) {
5704
+ return { ...this.opts.env, ...extra, [SPAWNED_BY_PLANNER_ENV]: "1" };
5705
+ }
5706
+ /**
5707
+ * Run one of this client's own subcommands and capture its output. Never attached
5708
+ * to the terminal: these are configuration calls whose result the CLI inspects,
5709
+ * not the handoff itself.
5710
+ */
5711
+ runCommand(args) {
5712
+ debugLog(`Running ${this.command} ${args[0] ?? ""}`, { args });
5713
+ return new Promise((resolve6) => {
5714
+ const proc = spawn(this.command, args, {
5715
+ cwd: this.opts.cwd,
5716
+ env: this.opts.env,
5717
+ stdio: ["ignore", "pipe", "pipe"],
5718
+ timeout: MCP_COMMAND_TIMEOUT_MS
5719
+ });
5720
+ let stdout = "";
5721
+ let stderr = "";
5722
+ proc.stdout?.on("data", (chunk) => stdout += chunk.toString());
5723
+ proc.stderr?.on("data", (chunk) => stderr += chunk.toString());
5724
+ proc.on("error", (err) => {
5725
+ debugLog(`${this.command} ${args[0] ?? ""} failed to spawn`, { err });
5726
+ resolve6({ code: void 0, stdout, stderr: err.message });
5727
+ });
5728
+ proc.on("close", (code) => resolve6({ code: code ?? void 0, stdout, stderr }));
5729
+ });
5730
+ }
5731
+ /**
5732
+ * Sign this client in to a server through its own browser flow.
5733
+ *
5734
+ * Refused outright without a terminal. The flow prints a URL, opens a browser and
5735
+ * blocks on the callback, so a run with nobody at the keyboard would not fail - it
5736
+ * would hang, indefinitely, on a prompt nobody can see. A run without a terminal
5737
+ * is meant to have passed an API token instead, and the error says so rather than
5738
+ * describing the sign-in that could not happen.
5739
+ */
5740
+ async signIn(serverName) {
5741
+ if (process.stdin.isTTY !== true) {
5742
+ throw new Error(
5743
+ `Cannot sign ${this.label} in to the ${serverName} MCP server without a terminal: that flow opens a browser and waits for it. Set AUTONOMA_API_TOKEN so the run authorizes with an API key instead.`
5744
+ );
5745
+ }
5746
+ log.info(`Authorizing ${this.label} with Autonoma - approve it in the browser that opens.`);
5747
+ const login = await this.runAttached(["mcp", "login", serverName]);
5748
+ if (login !== 0) {
5749
+ throw new Error(
5750
+ `${this.label} could not sign in to the ${serverName} MCP server (exit ${login ?? "unknown"}).`
5751
+ );
5752
+ }
5753
+ }
5754
+ /**
5755
+ * Run a subcommand that hands the terminal over - an interactive OAuth sign-in.
5756
+ * The client prints a URL, opens a browser and waits on the callback, so it needs
5757
+ * this process's stdio rather than pipes.
5758
+ */
5759
+ runAttached(args) {
5760
+ debugLog(`Running ${this.command} ${args.join(" ")} attached`);
5761
+ return new Promise((resolve6) => {
5762
+ const proc = spawn(this.command, args, { cwd: this.opts.cwd, env: this.opts.env, stdio: "inherit" });
5763
+ proc.on("error", (err) => {
5764
+ debugLog(`${this.command} ${args[0] ?? ""} failed to spawn`, { err });
5765
+ resolve6(void 0);
5766
+ });
5767
+ proc.on("close", (code) => resolve6(code ?? void 0));
5768
+ });
5769
+ }
5770
+ };
5771
+ ClaudeLauncher = class extends BaseLauncher {
5772
+ id = CLAUDE_ID;
5773
+ label = "Claude Code";
5774
+ command = CLAUDE_ID;
5775
+ buildArgs(message, permissionMode, interactive) {
5776
+ return interactive ? ["--permission-mode", permissionMode, message] : ["-p", message, "--permission-mode", permissionMode, "--verbose"];
5777
+ }
5778
+ /**
5779
+ * `--scope user` and not the default (`local`): the default binds the server to the
5780
+ * directory the command ran in. The planner can be invoked from anywhere in a repo,
5781
+ * and a server registered against the wrong directory looks exactly like one that
5782
+ * was never authorized - the agent simply has no Autonoma tools.
5783
+ *
5784
+ * With a token the header goes into Claude's own config and there is nothing to
5785
+ * sign in to. Without one, the browser sign-in runs attached to the terminal.
5786
+ */
5787
+ async registerMcpServer(spec) {
5788
+ const add = ["mcp", "add", "--transport", "http", "--scope", "user", spec.name, spec.url];
5789
+ if (spec.apiToken != null) add.push("--header", `Authorization: Bearer ${spec.apiToken}`);
5790
+ const existing = await this.runCommand(["mcp", "get", spec.name]);
5791
+ if (existing.code === 0 && !existing.stdout.includes(spec.url)) {
5792
+ debugLog("Replacing an MCP registration that points elsewhere", { server: spec.name, url: spec.url });
5793
+ await this.runCommand(["mcp", "remove", spec.name, "-s", "user"]);
5794
+ }
5795
+ await this.runCommand(add);
5796
+ const registered = await this.runCommand(["mcp", "get", spec.name]);
5797
+ if (registered.code !== 0) {
5798
+ throw new Error(
5799
+ `Could not register the ${spec.name} MCP server with ${this.label}: ${firstLine(registered.stderr) || firstLine(registered.stdout) || "unknown error"}`
5800
+ );
5801
+ }
5802
+ if (spec.apiToken == null && !registered.stdout.includes(CLAUDE_CONNECTED_MARKER)) {
5803
+ await this.signIn(spec.name);
5804
+ }
5805
+ return { env: {} };
4997
5806
  }
4998
- flush() {
4999
- this.send();
5807
+ };
5808
+ CodexLauncher = class extends BaseLauncher {
5809
+ id = CODEX_ID;
5810
+ label = "Codex CLI";
5811
+ command = CODEX_ID;
5812
+ /**
5813
+ * Codex's autonomy is two orthogonal axes - `--sandbox` (what it may touch) and
5814
+ * `--ask-for-approval` (when it pauses) - so we translate the shared,
5815
+ * Claude-flavoured `PermissionMode` onto them.
5816
+ *
5817
+ * The handoff's whole job is to install the SDK, boot the app, and validate
5818
+ * against a live DB, so the sandbox is always `danger-full-access` (Codex's
5819
+ * `workspace-write` disables network, which breaks the install). The only real
5820
+ * knob is approval strictness, which exists only interactively: headless `exec`
5821
+ * can't prompt, so `default`/`acceptEdits` collapse to the same autonomous run.
5822
+ */
5823
+ buildArgs(message, permissionMode, interactive) {
5824
+ if (permissionMode === "bypassPermissions") {
5825
+ const bypass = ["--dangerously-bypass-approvals-and-sandbox"];
5826
+ return interactive ? [...bypass, message] : ["exec", ...bypass, message];
5827
+ }
5828
+ const sandbox = ["--sandbox", "danger-full-access"];
5829
+ if (!interactive) return ["exec", ...sandbox, message];
5830
+ const approval = permissionMode === "acceptEdits" ? "on-failure" : "untrusted";
5831
+ return [...sandbox, "--ask-for-approval", approval, message];
5000
5832
  }
5001
- send() {
5002
- if (this.buffer.length === 0) return;
5003
- const events = this.buffer;
5004
- const bytes = this.bufferBytes;
5005
- this.buffer = [];
5006
- this.bufferBytes = 0;
5007
- this.sessionBytes += bytes;
5008
- if (this.sessionBytes > MAX_SESSION_BYTES) {
5009
- this.exhausted = true;
5010
- debugLog("Session replay cap reached; stopping capture", {
5011
- sessionBytes: this.sessionBytes,
5012
- cap: MAX_SESSION_BYTES
5013
- });
5014
- return;
5833
+ /**
5834
+ * Codex speaks streamable HTTP natively, so this needs no `mcp-remote` bridge.
5835
+ * `--bearer-token-env-var` stores the NAME of the variable rather than the token,
5836
+ * which is why the registration hands an env back for the spawn to carry.
5837
+ *
5838
+ * `mcp add` is idempotent here in the way that matters: re-adding an existing
5839
+ * server overwrites its entry, so a re-run cannot leave a stale URL behind.
5840
+ */
5841
+ async registerMcpServer(spec) {
5842
+ const add = ["mcp", "add", spec.name, "--url", spec.url];
5843
+ if (spec.apiToken != null) add.push("--bearer-token-env-var", CODEX_BEARER_TOKEN_ENV);
5844
+ const added = await this.runCommand(add);
5845
+ if (added.code !== 0) {
5846
+ throw new Error(
5847
+ `Could not register the ${spec.name} MCP server with ${this.label}: ${firstLine(added.stderr) || firstLine(added.stdout) || "unknown error"}`
5848
+ );
5015
5849
  }
5016
- const body = JSON.stringify([
5017
- {
5018
- api_key: this.config.apiKey,
5019
- event: "$snapshot",
5020
- distinct_id: this.config.distinctId,
5021
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5022
- properties: {
5023
- $session_id: this.config.sessionId,
5024
- $window_id: this.config.windowId,
5025
- $snapshot_data: events,
5026
- $snapshot_bytes: bytes,
5027
- // The player treats a recording as web; the synthesized DOM is
5028
- // a real DOM as far as it is concerned.
5029
- $lib: "web",
5030
- $lib_version: "1.0.0"
5031
- }
5032
- }
5033
- ]);
5034
- const promise = fetch(`${this.config.host}/s/`, {
5035
- method: "POST",
5036
- headers: { "Content-Type": "application/json" },
5037
- body
5038
- }).catch((err) => {
5039
- debugLog("Session replay upload failed (ignored)", { err });
5040
- }).finally(() => pending2.delete(promise));
5041
- pending2.add(promise);
5850
+ if (spec.apiToken == null) {
5851
+ await this.signIn(spec.name);
5852
+ return { env: {} };
5853
+ }
5854
+ return { env: { [CODEX_BEARER_TOKEN_ENV]: spec.apiToken } };
5042
5855
  }
5043
5856
  };
5044
5857
  }
@@ -5216,6 +6029,309 @@ var init_interrupt = __esm({
5216
6029
  }
5217
6030
  });
5218
6031
 
6032
+ // src/core/preview-phase.ts
6033
+ function previewPrompt(code, interactive) {
6034
+ const instruction = `set up my preview environments with the ${MCP_SERVER_NAME} MCP, code ${code}`;
6035
+ return interactive ? instruction : `${instruction}.${HEADLESS_GUIDANCE}`;
6036
+ }
6037
+ async function runPreviewPhase(deps) {
6038
+ const timing = deps.timing ?? DEFAULT_PHASE_TIMING;
6039
+ log.info(`Connecting ${deps.launcher.label} to Autonoma...`);
6040
+ const registration = await deps.launcher.registerMcpServer({
6041
+ name: MCP_SERVER_NAME,
6042
+ url: deps.mcpUrl,
6043
+ apiToken: deps.apiToken
6044
+ });
6045
+ const code = await deps.client.createAgentPairing(deps.applicationId);
6046
+ captureLog("info", "Handing the preview environment to a coding agent", {
6047
+ source: "preview_phase",
6048
+ agent: deps.launcher.id
6049
+ });
6050
+ log.info("Your agent is setting up your preview environment - watch it work, and steer it if you want to.");
6051
+ await deps.launcher.launch({
6052
+ message: previewPrompt(code, deps.interactive),
6053
+ permissionMode: deps.permissionMode,
6054
+ interactive: deps.interactive,
6055
+ env: registration.env,
6056
+ // Both modes, not just interactive. An interactive session plainly never exits
6057
+ // on its own - it sits open after its final message - but a headless one is no
6058
+ // safer to wait on: observed in practice finishing the actual work and then
6059
+ // continuing to potter, with the run blocked behind it. Onboarding state is
6060
+ // what says "done", so it ends the handoff either way.
6061
+ watch: (proc) => watchForPreviewPhase(deps.client, deps.applicationId, proc, timing)
6062
+ });
6063
+ const state = await readVerifiedState(deps.client, deps.applicationId);
6064
+ if (isStepAtOrPast(state.step, PREVIEW_DONE_STEP)) {
6065
+ log.success("Your preview environment is up.");
6066
+ captureLog("info", "Preview phase complete", { source: "preview_phase", step: state.step });
6067
+ return { kind: "verified" };
6068
+ }
6069
+ captureLog("warn", "Preview phase ended without a verified preview", {
6070
+ source: "preview_phase",
6071
+ step: state.step
6072
+ });
6073
+ return { kind: "incomplete", step: state.step };
6074
+ }
6075
+ function watchForPreviewPhase(client, applicationId, proc, timing = DEFAULT_PHASE_TIMING) {
6076
+ let graceTimer;
6077
+ let killTimer;
6078
+ const poll = setInterval(() => {
6079
+ void readVerifiedState(client, applicationId).then((state) => {
6080
+ if (!isStepAtOrPast(state.step, PREVIEW_DONE_STEP) || graceTimer != null) return;
6081
+ debugLog("Preview verified while the agent runs; scheduling terminal reclaim", { step: state.step });
6082
+ clearInterval(poll);
6083
+ graceTimer = setTimeout(() => {
6084
+ proc.kill("SIGTERM");
6085
+ killTimer = setTimeout(() => proc.kill("SIGKILL"), timing.killMs);
6086
+ }, timing.graceMs);
6087
+ }).catch((err) => {
6088
+ debugLog("Could not read onboarding state while the agent runs", { err });
6089
+ });
6090
+ }, timing.pollMs);
6091
+ return () => {
6092
+ clearInterval(poll);
6093
+ if (graceTimer != null) clearTimeout(graceTimer);
6094
+ if (killTimer != null) clearTimeout(killTimer);
6095
+ };
6096
+ }
6097
+ async function readVerifiedState(client, applicationId) {
6098
+ await client.refreshPreviewReadiness(applicationId).catch((err) => {
6099
+ debugLog("Could not refresh preview readiness", { err });
6100
+ });
6101
+ return client.getOnboardingState(applicationId);
6102
+ }
6103
+ var MCP_SERVER_NAME, PHASE_POLL_MS, PHASE_EXIT_GRACE_MS, KILL_ESCALATION_MS, DEFAULT_PHASE_TIMING, HEADLESS_GUIDANCE;
6104
+ var init_preview_phase = __esm({
6105
+ "src/core/preview-phase.ts"() {
6106
+ "use strict";
6107
+ init_esm_shims();
6108
+ init_prompts();
6109
+ init_debug();
6110
+ init_logs();
6111
+ init_onboarding_phase();
6112
+ MCP_SERVER_NAME = "autonoma";
6113
+ PHASE_POLL_MS = 5e3;
6114
+ PHASE_EXIT_GRACE_MS = 15e3;
6115
+ KILL_ESCALATION_MS = 1e4;
6116
+ DEFAULT_PHASE_TIMING = {
6117
+ pollMs: PHASE_POLL_MS,
6118
+ graceMs: PHASE_EXIT_GRACE_MS,
6119
+ killMs: KILL_ESCALATION_MS
6120
+ };
6121
+ HEADLESS_GUIDANCE = " You are running with no human to answer you, so where you would ask a question, choose the safer option and say which you chose. You are not finished when you have tried - you are finished when the preview is verified. If a deploy fails, read its logs, fix the cause and deploy again. Never report success you have not confirmed.";
6122
+ }
6123
+ });
6124
+
6125
+ // src/core/sdk-repair-phase.ts
6126
+ function repairPrompt(code, interactive) {
6127
+ const instruction = `use the ${MCP_SERVER_NAME} MCP, code ${code}, to get my Autonoma SDK answering and my scenario dry run passing - fix whatever is in the way, in this repo or in the config, and keep going until Autonoma reports both`;
6128
+ return interactive ? instruction : `${instruction}.${HEADLESS_GUIDANCE2}`;
6129
+ }
6130
+ function isDone(state) {
6131
+ return state.sdkConfigured && state.dryRunPassed;
6132
+ }
6133
+ async function runSdkRepairPhase(deps) {
6134
+ const timing = deps.timing ?? DEFAULT_PHASE_TIMING;
6135
+ const before = await deps.client.getOnboardingState(deps.applicationId);
6136
+ if (isDone(before)) return { kind: "passed" };
6137
+ log.info(`Connecting ${deps.launcher.label} to Autonoma...`);
6138
+ const registration = await deps.launcher.registerMcpServer({
6139
+ name: MCP_SERVER_NAME,
6140
+ url: deps.mcpUrl,
6141
+ apiToken: deps.apiToken
6142
+ });
6143
+ const code = await deps.client.createAgentPairing(deps.applicationId);
6144
+ captureLog("info", "Handing the SDK and dry run to a coding agent", {
6145
+ source: "sdk_repair_phase",
6146
+ agent: deps.launcher.id,
6147
+ sdk_configured: before.sdkConfigured,
6148
+ dry_run_passed: before.dryRunPassed
6149
+ });
6150
+ log.info("Your agent is getting your test data working - watch it, and steer it if you want to.");
6151
+ await deps.launcher.launch({
6152
+ message: repairPrompt(code, deps.interactive),
6153
+ permissionMode: deps.permissionMode,
6154
+ interactive: deps.interactive,
6155
+ env: registration.env,
6156
+ watch: (proc) => watchForSdkRepair(deps.client, deps.applicationId, proc, timing)
6157
+ });
6158
+ const after = await deps.client.getOnboardingState(deps.applicationId);
6159
+ if (isDone(after)) {
6160
+ log.success("Your test data provisions against your preview.");
6161
+ captureLog("info", "SDK repair phase complete", { source: "sdk_repair_phase" });
6162
+ return { kind: "passed" };
6163
+ }
6164
+ captureLog("warn", "SDK repair phase ended with work outstanding", {
6165
+ source: "sdk_repair_phase",
6166
+ sdk_configured: after.sdkConfigured,
6167
+ dry_run_passed: after.dryRunPassed
6168
+ });
6169
+ return { kind: "incomplete", sdkConfigured: after.sdkConfigured, dryRunPassed: after.dryRunPassed };
6170
+ }
6171
+ function watchForSdkRepair(client, applicationId, proc, timing = DEFAULT_PHASE_TIMING) {
6172
+ let graceTimer;
6173
+ let killTimer;
6174
+ const poll = setInterval(() => {
6175
+ void client.getOnboardingState(applicationId).then((state) => {
6176
+ if (!isDone(state) || graceTimer != null) return;
6177
+ debugLog("SDK and dry run both good while the agent runs; scheduling terminal reclaim");
6178
+ clearInterval(poll);
6179
+ graceTimer = setTimeout(() => {
6180
+ proc.kill("SIGTERM");
6181
+ killTimer = setTimeout(() => proc.kill("SIGKILL"), timing.killMs);
6182
+ }, timing.graceMs);
6183
+ }).catch((err) => {
6184
+ debugLog("Could not read onboarding state while the agent runs", { err });
6185
+ });
6186
+ }, timing.pollMs);
6187
+ return () => {
6188
+ clearInterval(poll);
6189
+ if (graceTimer != null) clearTimeout(graceTimer);
6190
+ if (killTimer != null) clearTimeout(killTimer);
6191
+ };
6192
+ }
6193
+ var HEADLESS_GUIDANCE2;
6194
+ var init_sdk_repair_phase = __esm({
6195
+ "src/core/sdk-repair-phase.ts"() {
6196
+ "use strict";
6197
+ init_esm_shims();
6198
+ init_prompts();
6199
+ init_debug();
6200
+ init_logs();
6201
+ init_preview_phase();
6202
+ HEADLESS_GUIDANCE2 = " You are running with no human to answer you, so where you would ask a question, choose the safer option and say which you chose. You are not finished when you have tried - you are finished when Autonoma reports the scenario dry run passed. Never report success you have not confirmed.";
6203
+ }
6204
+ });
6205
+
6206
+ // src/core/front-door.ts
6207
+ var front_door_exports = {};
6208
+ __export(front_door_exports, {
6209
+ describeIncompletePreview: () => describeIncompletePreview,
6210
+ planFrontDoor: () => planFrontDoor,
6211
+ runPreviewHandoff: () => runPreviewHandoff,
6212
+ runSdkRepairHandoff: () => runSdkRepairHandoff
6213
+ });
6214
+ async function planFrontDoor(config) {
6215
+ const { autonomaApplicationId: applicationId, autonomaApiToken: apiToken } = config;
6216
+ if (applicationId == null || apiToken == null) {
6217
+ debugLog("No application id or token; running the pipeline standalone");
6218
+ return void 0;
6219
+ }
6220
+ const client = new AutonomaClient(config.autonomaApiUrl, apiToken);
6221
+ try {
6222
+ const state = await client.getOnboardingState(applicationId);
6223
+ const phase = resolveEntryPhase(state);
6224
+ await claimHoldForRun(client, applicationId, phase);
6225
+ return { client, applicationId, phase };
6226
+ } catch (err) {
6227
+ log.warn("Couldn't read your setup status from Autonoma - continuing with the test-suite run.");
6228
+ debugLog("Front-door planning failed", { err });
6229
+ captureLog("warn", "Could not resolve the onboarding entry phase", { source: "front_door" });
6230
+ return void 0;
6231
+ }
6232
+ }
6233
+ async function claimHoldForRun(client, applicationId, phase) {
6234
+ if (phase === "preview" || phase === "done") return;
6235
+ try {
6236
+ await client.claimAgentHold(applicationId);
6237
+ } catch (err) {
6238
+ debugLog("Could not claim the onboarding config for this run", { err });
6239
+ captureLog("warn", "Could not claim the onboarding config for this run", { source: "front_door" });
6240
+ }
6241
+ }
6242
+ async function runPreviewHandoff(deps) {
6243
+ const { plan, config, nonInteractive } = deps;
6244
+ const interactive = !nonInteractive;
6245
+ const launchers = deps.launchers ?? buildAllLaunchers({ cwd: config.projectRoot, env: process.env });
6246
+ const launcher = await selectLauncher(launchers, config.agent, interactive);
6247
+ if (launcher == null) {
6248
+ captureLog("warn", "No coding agent available for the preview phase", { source: "front_door" });
6249
+ return { kind: "no-agent" };
6250
+ }
6251
+ const preset = parsePermissionMode(config.permissionMode);
6252
+ const permissionMode = interactive ? await selectPermissionMode(preset) : preset ?? DEFAULT_PERMISSION_MODE;
6253
+ suspend();
6254
+ let outcome;
6255
+ try {
6256
+ outcome = await runPreviewPhase({
6257
+ client: plan.client,
6258
+ applicationId: plan.applicationId,
6259
+ launcher,
6260
+ permissionMode,
6261
+ apiToken: interactive ? void 0 : config.autonomaApiToken,
6262
+ mcpUrl: resolveMcpUrl(config.autonomaApiUrl),
6263
+ interactive
6264
+ });
6265
+ } finally {
6266
+ resume();
6267
+ }
6268
+ if (outcome.kind === "verified") await takeAppLive(plan);
6269
+ return outcome;
6270
+ }
6271
+ async function takeAppLive(plan) {
6272
+ try {
6273
+ const { alreadyLive } = await plan.client.takeAppLive(plan.applicationId);
6274
+ log.success(
6275
+ alreadyLive ? "Autonoma is reviewing your pull requests." : "Autonoma is now reviewing your pull requests."
6276
+ );
6277
+ } catch (err) {
6278
+ debugLog("Could not take the app live", { err });
6279
+ captureLog("warn", "Could not take the app live", { source: "front_door" });
6280
+ log.warn(
6281
+ "Your preview is up, but Autonoma is not reviewing your pull requests yet - take the app live in the Autonoma app to finish."
6282
+ );
6283
+ }
6284
+ }
6285
+ async function runSdkRepairHandoff(deps) {
6286
+ const { plan, config, nonInteractive } = deps;
6287
+ const interactive = !nonInteractive;
6288
+ const launchers = deps.launchers ?? buildAllLaunchers({ cwd: config.projectRoot, env: process.env });
6289
+ const launcher = await selectLauncher(launchers, config.agent, interactive);
6290
+ if (launcher == null) {
6291
+ captureLog("warn", "No coding agent available for the SDK repair phase", { source: "front_door" });
6292
+ return void 0;
6293
+ }
6294
+ const preset = parsePermissionMode(config.permissionMode);
6295
+ const permissionMode = interactive ? await selectPermissionMode(preset) : preset ?? DEFAULT_PERMISSION_MODE;
6296
+ suspend();
6297
+ try {
6298
+ return await runSdkRepairPhase({
6299
+ client: plan.client,
6300
+ applicationId: plan.applicationId,
6301
+ launcher,
6302
+ permissionMode,
6303
+ apiToken: interactive ? void 0 : config.autonomaApiToken,
6304
+ mcpUrl: resolveMcpUrl(config.autonomaApiUrl),
6305
+ interactive
6306
+ });
6307
+ } finally {
6308
+ resume();
6309
+ }
6310
+ }
6311
+ function describeIncompletePreview(result) {
6312
+ if (result.kind === "verified") return void 0;
6313
+ if (result.kind === "no-agent") {
6314
+ return "No supported coding agent was found on your PATH, so the preview environment was skipped. Install Claude Code or the Codex CLI and run again, or set the preview up in the Autonoma app.";
6315
+ }
6316
+ return "Your preview environment isn't confirmed yet, so scenario dry runs will have nothing to run against. Finish it in the Autonoma app (or run again) - the rest of the run continues either way.";
6317
+ }
6318
+ var init_front_door = __esm({
6319
+ "src/core/front-door.ts"() {
6320
+ "use strict";
6321
+ init_esm_shims();
6322
+ init_prompts();
6323
+ init_api_url();
6324
+ init_autonoma_client();
6325
+ init_coding_agent();
6326
+ init_debug();
6327
+ init_interrupt();
6328
+ init_logs();
6329
+ init_onboarding_phase();
6330
+ init_preview_phase();
6331
+ init_sdk_repair_phase();
6332
+ }
6333
+ });
6334
+
5219
6335
  // src/core/model.ts
5220
6336
  import { createOpenRouter } from "@openrouter/ai-sdk-provider";
5221
6337
  function getProvider() {
@@ -8093,26 +9209,7 @@ var init_completion = __esm({
8093
9209
  }
8094
9210
  });
8095
9211
 
8096
- // src/agents/04-recipe-builder/launcher.ts
8097
- var launcher_exports = {};
8098
- __export(launcher_exports, {
8099
- BaseLauncher: () => BaseLauncher,
8100
- ClaudeLauncher: () => ClaudeLauncher,
8101
- CodexLauncher: () => CodexLauncher,
8102
- DEFAULT_PERMISSION_MODE: () => DEFAULT_PERMISSION_MODE,
8103
- PERMISSION_MODE_LABELS: () => PERMISSION_MODE_LABELS,
8104
- buildAllLaunchers: () => buildAllLaunchers,
8105
- parsePermissionMode: () => parsePermissionMode,
8106
- selectLauncher: () => selectLauncher,
8107
- selectPermissionMode: () => selectPermissionMode,
8108
- watchForCompletion: () => watchForCompletion
8109
- });
8110
- import { dirname as dirname3 } from "path";
8111
- import spawn from "cross-spawn";
8112
- import which from "which";
8113
- function launchMessage(promptFile) {
8114
- return `Read the file ${promptFile} and follow its instructions exactly to integrate Autonoma into this application. It is your complete spec. Do not stop until every item in it is done and you have written the completion marker it describes.`;
8115
- }
9212
+ // src/agents/04-recipe-builder/completion-watch.ts
8116
9213
  function watchForCompletion(outputDir, proc, timing = DEFAULT_WATCH_TIMING) {
8117
9214
  let graceTimer;
8118
9215
  let killTimer;
@@ -8131,148 +9228,26 @@ function watchForCompletion(outputDir, proc, timing = DEFAULT_WATCH_TIMING) {
8131
9228
  }, timing.graceMs);
8132
9229
  });
8133
9230
  }, timing.pollMs);
8134
- return () => {
8135
- clearInterval(poll);
8136
- if (graceTimer != null) clearTimeout(graceTimer);
8137
- if (killTimer != null) clearTimeout(killTimer);
8138
- };
8139
- }
8140
- function buildAllLaunchers(opts) {
8141
- return [new ClaudeLauncher(opts), new CodexLauncher(opts)];
8142
- }
8143
- async function selectLauncher(launchers, presetId, interactive = true) {
8144
- const availability = await Promise.all(launchers.map((l) => l.isAvailable()));
8145
- const available = launchers.filter((_, i) => availability[i]);
8146
- debugLog("Detected available agents", { available: available.map((l) => l.id), presetId, interactive });
8147
- if (presetId != null) {
8148
- const preset = available.find((l) => l.id === presetId);
8149
- if (preset != null) return preset;
8150
- log.warn(`Requested agent "${presetId}" is not installed or not supported.`);
8151
- }
8152
- if (available.length === 0) return void 0;
8153
- if (available.length === 1) {
8154
- const only = available[0];
8155
- log.info(`Found ${only.label} - will use it for the integration.`);
8156
- return only;
8157
- }
8158
- if (!interactive) return void 0;
8159
- const selected = await select({
8160
- message: "Which agent should implement the integration?",
8161
- options: available.map((l) => ({ value: l.id, label: l.label }))
8162
- });
8163
- if (isCancel(selected)) throw new Error("Agent selection cancelled");
8164
- return available.find((l) => l.id === selected);
8165
- }
8166
- async function selectPermissionMode(preset) {
8167
- if (preset != null) return preset;
8168
- const selected = await select({
8169
- message: "How much autonomy should the agent have?",
8170
- options: [
8171
- { value: "bypassPermissions", label: PERMISSION_MODE_LABELS.bypassPermissions },
8172
- { value: "acceptEdits", label: PERMISSION_MODE_LABELS.acceptEdits },
8173
- { value: "default", label: PERMISSION_MODE_LABELS.default }
8174
- ],
8175
- initialValue: DEFAULT_PERMISSION_MODE
8176
- });
8177
- if (isCancel(selected)) throw new Error("Permission mode selection cancelled");
8178
- return selected;
8179
- }
8180
- function parsePermissionMode(value) {
8181
- if (value === "default" || value === "acceptEdits" || value === "bypassPermissions") return value;
8182
- return void 0;
9231
+ return () => {
9232
+ clearInterval(poll);
9233
+ if (graceTimer != null) clearTimeout(graceTimer);
9234
+ if (killTimer != null) clearTimeout(killTimer);
9235
+ };
8183
9236
  }
8184
- var MARKER_POLL_MS, MARKER_EXIT_GRACE_MS, KILL_ESCALATION_MS, DEFAULT_PERMISSION_MODE, PERMISSION_MODE_LABELS, CLAUDE_ID, CODEX_ID, BaseLauncher, ClaudeLauncher, CodexLauncher, DEFAULT_WATCH_TIMING;
8185
- var init_launcher = __esm({
8186
- "src/agents/04-recipe-builder/launcher.ts"() {
9237
+ var MARKER_POLL_MS, MARKER_EXIT_GRACE_MS, KILL_ESCALATION_MS2, DEFAULT_WATCH_TIMING;
9238
+ var init_completion_watch = __esm({
9239
+ "src/agents/04-recipe-builder/completion-watch.ts"() {
8187
9240
  "use strict";
8188
9241
  init_esm_shims();
8189
9242
  init_debug();
8190
- init_prompts();
8191
9243
  init_completion();
8192
9244
  MARKER_POLL_MS = 2e3;
8193
9245
  MARKER_EXIT_GRACE_MS = 3e4;
8194
- KILL_ESCALATION_MS = 1e4;
8195
- DEFAULT_PERMISSION_MODE = "bypassPermissions";
8196
- PERMISSION_MODE_LABELS = {
8197
- default: "Approve each command",
8198
- acceptEdits: "Auto-edit files, approve commands",
8199
- bypassPermissions: "Fully autonomous"
8200
- };
8201
- CLAUDE_ID = "claude";
8202
- CODEX_ID = "codex";
8203
- BaseLauncher = class {
8204
- constructor(opts) {
8205
- this.opts = opts;
8206
- }
8207
- async isAvailable() {
8208
- const resolved = await which(this.command, { nothrow: true });
8209
- debugLog("Probed for agent on PATH", { command: this.command, found: resolved != null });
8210
- return resolved != null;
8211
- }
8212
- launch(promptFile, permissionMode, interactive) {
8213
- debugLog(`Launching ${this.label}`, { promptFile, permissionMode, interactive });
8214
- const args = this.buildArgs(launchMessage(promptFile), permissionMode, interactive);
8215
- const stdio = interactive ? "inherit" : ["ignore", "inherit", "inherit"];
8216
- return new Promise((resolve6) => {
8217
- const proc = spawn(this.command, args, {
8218
- cwd: this.opts.cwd,
8219
- env: this.opts.env,
8220
- stdio
8221
- });
8222
- const stopWatching = interactive ? watchForCompletion(dirname3(promptFile), proc) : () => {
8223
- };
8224
- proc.on("error", (err) => {
8225
- debugLog(`${this.label} failed to spawn`, { err });
8226
- log.error(`Couldn't launch ${this.label}: ${err.message}`);
8227
- stopWatching();
8228
- resolve6(void 0);
8229
- });
8230
- proc.on("close", (code) => {
8231
- debugLog(`${this.label} exited`, { code });
8232
- stopWatching();
8233
- resolve6(code ?? void 0);
8234
- });
8235
- });
8236
- }
8237
- };
8238
- ClaudeLauncher = class extends BaseLauncher {
8239
- id = CLAUDE_ID;
8240
- label = "Claude Code";
8241
- command = CLAUDE_ID;
8242
- buildArgs(message, permissionMode, interactive) {
8243
- return interactive ? ["--permission-mode", permissionMode, message] : ["-p", message, "--permission-mode", permissionMode, "--verbose"];
8244
- }
8245
- };
8246
- CodexLauncher = class extends BaseLauncher {
8247
- id = CODEX_ID;
8248
- label = "Codex CLI";
8249
- command = CODEX_ID;
8250
- /**
8251
- * Codex's autonomy is two orthogonal axes - `--sandbox` (what it may touch) and
8252
- * `--ask-for-approval` (when it pauses) - so we translate the shared,
8253
- * Claude-flavoured `PermissionMode` onto them.
8254
- *
8255
- * The handoff's whole job is to install the SDK, boot the app, and validate
8256
- * against a live DB, so the sandbox is always `danger-full-access` (Codex's
8257
- * `workspace-write` disables network, which breaks the install). The only real
8258
- * knob is approval strictness, which exists only interactively: headless `exec`
8259
- * can't prompt, so `default`/`acceptEdits` collapse to the same autonomous run.
8260
- */
8261
- buildArgs(message, permissionMode, interactive) {
8262
- if (permissionMode === "bypassPermissions") {
8263
- const bypass = ["--dangerously-bypass-approvals-and-sandbox"];
8264
- return interactive ? [...bypass, message] : ["exec", ...bypass, message];
8265
- }
8266
- const sandbox = ["--sandbox", "danger-full-access"];
8267
- if (!interactive) return ["exec", ...sandbox, message];
8268
- const approval = permissionMode === "acceptEdits" ? "on-failure" : "untrusted";
8269
- return [...sandbox, "--ask-for-approval", approval, message];
8270
- }
8271
- };
9246
+ KILL_ESCALATION_MS2 = 1e4;
8272
9247
  DEFAULT_WATCH_TIMING = {
8273
9248
  pollMs: MARKER_POLL_MS,
8274
9249
  graceMs: MARKER_EXIT_GRACE_MS,
8275
- killMs: KILL_ESCALATION_MS
9250
+ killMs: KILL_ESCALATION_MS2
8276
9251
  };
8277
9252
  }
8278
9253
  });
@@ -8287,6 +9262,10 @@ ${params.priorFailure}
8287
9262
  Re-read your IMPLEMENTATION.md checklist, pick up at the first unfinished entity,
8288
9263
  and do NOT redo entities already validated. Finish every remaining item, then write
8289
9264
  the completion marker.
9265
+ ` : "";
9266
+ const userGuidanceSection = params.userGuidance != null ? `
9267
+ \u2550\u2550\u2550 THE DEVELOPER ASKED FOR THIS - IT OVERRIDES YOUR OWN PLAN \u2550\u2550\u2550
9268
+ ${params.userGuidance}
8290
9269
  ` : "";
8291
9270
  return `<!-- Autonoma integration prompt v${INTEGRATION_PROMPT_VERSION} -->
8292
9271
  You are integrating Autonoma into THIS application, working in a LOCAL checkout of
@@ -8296,7 +9275,7 @@ the repo. The Autonoma planner has ALREADY run locally and produced its artifact
8296
9275
  You are the developer picking up exactly where the planner hands off: implement the
8297
9276
  test-data layer (the SDK integration), GENERATE the test-data recipe, and validate
8298
9277
  it. Do NOT re-run the planner; read its artifacts as your spec.
8299
- ${priorFailureSection}
9278
+ ${priorFailureSection}${userGuidanceSection}
8300
9279
  Work without asking questions. Make reasonable, codebase-grounded decisions. Only
8301
9280
  stop for missing secrets, credentials, or external services that genuinely cannot
8302
9281
  be mocked or run locally - and when you do, say exactly what you need and why.
@@ -8448,7 +9427,8 @@ Before implementing, write a checklist file inside the app (e.g. IMPLEMENTATION.
8448
9427
  and keep it updated. It must enumerate, as explicit checkboxes: EVERY entity the
8449
9428
  entity audit says needs a factory (by name, copied from the audit), plus the
8450
9429
  endpoint, teardown, the auth callback, the maintenance note, the full-recipe pass,
8451
- the two-concurrent-instances proof, and the pushed branch + opened pull request.
9430
+ the concurrent-instances proof, a clean \`sdk check\` on the recipe file, and the
9431
+ pushed branch + opened pull request.
8452
9432
  Check items off only when actually done and verified. The single most common failure is stopping with entities left uncovered.
8453
9433
 
8454
9434
  \u2550\u2550\u2550 VALIDATE - ENTITY BY ENTITY, THEN THE WHOLE RECIPE \u2550\u2550\u2550
@@ -8459,6 +9439,12 @@ from the environment, so you never construct signatures yourself. The commands:
8459
9439
  \u2022 ${params.cliCommand} sdk up --url <endpoint-url> --recipe <file> [--test-run-id <id>] [--timeout <seconds>]
8460
9440
  (prints JSON; the response body includes a "refsToken")
8461
9441
  \u2022 ${params.cliCommand} sdk down --url <endpoint-url> --refs-token <token-from-up>
9442
+ \u2022 ${params.cliCommand} sdk check --recipe <file>
9443
+ (no url, no request: holds the FILE to the format Autonoma accepts and prints
9444
+ every problem it finds. Run it whenever you edit the recipe.)
9445
+ \u2022 ${params.cliCommand} sdk up --url <endpoint-url> --recipe <file> --repeat <n>
9446
+ (seeds the recipe n times over WITHOUT tearing down in between, so every
9447
+ instance is live at once - the concurrency proof below)
8462
9448
  The --recipe file may be your full recipe.json or a slice containing just the
8463
9449
  entities under test. Each request times out after 120s by default; a cold
8464
9450
  full-recipe up (first compile + many real-service inserts) can exceed that, so
@@ -8490,19 +9476,34 @@ Once every entity passes independently, run the FULL recipe as one pass:
8490
9476
  \u2022 confirm a WRONG signature is rejected (the SDK does this for you - do not disable it)
8491
9477
  \u2022 confirm the up response's auth payload contains real credentials, not a placeholder
8492
9478
 
8493
- \u2550\u2550\u2550 PROVE TWO INSTANCES CAN COEXIST - MANDATORY, LAST \u2550\u2550\u2550
9479
+ \u2550\u2550\u2550 CHECK THE RECIPE FILE - THE GATE YOU CANNOT SKIP \u2550\u2550\u2550
9480
+ A recipe that seeds a database perfectly can still be a file Autonoma refuses, because
9481
+ \`sdk up\` only ever reads the "create" graph out of it - it never looks at the envelope
9482
+ around it. The planner DOES, the moment you exit, and a file it rejects there costs a
9483
+ whole re-launch. So before you write the completion marker, run:
9484
+ ${params.cliCommand} sdk check --recipe ${params.recipePath}
9485
+ It exits 0 and prints "ok": true only when the file is submittable. Anything else prints
9486
+ a "problems" array naming the exact field and what is wrong with it - fix each one in
9487
+ ${params.recipePath} and run it again. Do NOT write the completion marker until it is
9488
+ clean; a passing \`up\` is not a substitute for it.
9489
+
9490
+ \u2550\u2550\u2550 PROVE MANY INSTANCES CAN COEXIST - MANDATORY, LAST \u2550\u2550\u2550
8494
9491
  Every check above tears down before the next up, so a recipe whose unique columns hold
8495
- hardcoded values passes all of them. Real test runs OVERLAP: the customer runs two tests
8496
- at once and the second seed hits the first one's rows. Prove yours survives that:
8497
- 1. ${params.cliCommand} sdk up --url <url> --recipe ${params.recipePath} --test-run-id concurrent-a
8498
- 2. ${params.cliCommand} sdk up --url <url> --recipe ${params.recipePath} --test-run-id concurrent-b
8499
- (do NOT tear down A first - both instances must be up at the same time)
8500
- 3. BOTH must succeed. Then down A, then down B, and confirm in the DB that each
8501
- teardown removed only its own rows and that nothing is left behind.
8502
- A failure here is a unique-constraint violation, and it names the exact column that is
8503
- not per-run. Fix it where it lives: put a token in that field if the recipe supplies it,
8504
- or derive it from the "testRunId" your handler receives if your factory generates it.
8505
- Then repeat from step 1. Do not weaken the constraint and do not disable the check.
9492
+ hardcoded values passes all of them. Real test runs OVERLAP: the customer runs several
9493
+ tests at once and the second seed hits the first one's rows. Prove yours survives that:
9494
+ ${params.cliCommand} sdk up --url <url> --recipe ${params.recipePath} --repeat 3
9495
+ It seeds the recipe three times over WITHOUT tearing down in between, so all three are
9496
+ live at once, then removes every instance it created and reports the teardown. It exits 0
9497
+ and prints "ok": true only when every instance came up.
9498
+ A failure here is a unique-constraint violation, and the endpoint's error names the exact
9499
+ column that is not per-run. Fix it where it lives: put a token in that field if the recipe
9500
+ supplies it, or derive it from the "testRunId" your handler receives if your factory
9501
+ generates it. Then run it again, and keep going until it passes - each run surfaces the
9502
+ next reused value. Do not weaken the constraint and do not disable the check.
9503
+
9504
+ If the teardown it reports did NOT complete, go clean those rows out of the database by
9505
+ hand before re-running: leftovers from a failed attempt collide with the next one and
9506
+ look like a defect in whatever you just changed.
8506
9507
 
8507
9508
  Escape hatch, only after honest attempts: if an entity truly cannot be seeded twice
8508
9509
  concurrently because the app's own schema forces a global singleton, write that in
@@ -8533,9 +9534,10 @@ sitting uncommitted in the working tree:
8533
9534
 
8534
9535
  \u2550\u2550\u2550 FINISH - THE LAST THING YOU DO \u2550\u2550\u2550
8535
9536
  Write the completion marker once ALL of these hold:
8536
- \u2022 every entity, the full-recipe pass, and the two-concurrent-instances proof are green
9537
+ \u2022 every entity, the full-recipe pass, and the concurrent-instances proof are green
8537
9538
  - or the blocking constraint is documented in IMPLEMENTATION.md
8538
- \u2022 ${params.recipePath} holds the recipe you validated
9539
+ \u2022 ${params.recipePath} holds the recipe you validated, and \`sdk check\` on it prints
9540
+ "ok": true - this one has no escape hatch; a rejected file blocks the whole setup
8539
9541
  \u2022 your work is committed, and pushed with a pull request open - or the reason you could
8540
9542
  not push / open one is documented in IMPLEMENTATION.md
8541
9543
  A step you documented as genuinely blocked NEVER justifies withholding the marker; a
@@ -8564,7 +9566,7 @@ var init_integration_prompt = __esm({
8564
9566
  init_esm_shims();
8565
9567
  init_src();
8566
9568
  init_completion();
8567
- INTEGRATION_PROMPT_VERSION = 9;
9569
+ INTEGRATION_PROMPT_VERSION = 10;
8568
9570
  INTEGRATION_PROMPT_FILE = "integration-prompt.md";
8569
9571
  DEFAULT_ENDPOINT_PATH = "/api/autonoma";
8570
9572
  INTEGRATION_BRANCH = "autonoma-integration";
@@ -8605,6 +9607,9 @@ var init_state2 = __esm({
8605
9607
  // src/agents/04-recipe-builder/phases/handoff.ts
8606
9608
  import { rm as rm2 } from "fs/promises";
8607
9609
  import { join as join27 } from "path";
9610
+ function launchMessage(promptFile) {
9611
+ return `Read the file ${promptFile} and follow its instructions exactly to integrate Autonoma into this application. It is your complete spec. Do not stop until every item in it is done and you have written the completion marker it describes.`;
9612
+ }
8608
9613
  async function runHandoffPhase(state, deps, outputDir) {
8609
9614
  const recipePath = state.recipePath ?? join27(outputDir, RECIPE_FILE);
8610
9615
  state.recipePath = recipePath;
@@ -8626,7 +9631,9 @@ async function runHandoffPhase(state, deps, outputDir) {
8626
9631
  outputDir,
8627
9632
  recipePath,
8628
9633
  cliCommand: deps.cliCommand,
8629
- interactive: deps.interactive
9634
+ interactive: deps.interactive,
9635
+ priorFailure: state.priorFailure,
9636
+ userGuidance: state.userGuidance
8630
9637
  });
8631
9638
  state.launchAttempts = (state.launchAttempts ?? 0) + 1;
8632
9639
  await saveRecipeState(outputDir, state);
@@ -8642,7 +9649,7 @@ async function runCompletionPhase(state, deps, outputDir) {
8642
9649
  log.success("The agent reported the integration complete and validated.");
8643
9650
  return { kind: "advance" };
8644
9651
  }
8645
- const failure = describeIncompleteRecipe(read, uploadProblems);
9652
+ const failure = describeIncompleteRecipe(read, uploadProblems, recipePath, deps.cliCommand);
8646
9653
  const launcher = await selectLauncher(deps.launchers, state.agentId ?? deps.presetAgentId, deps.interactive);
8647
9654
  if (launcher != null && (state.launchAttempts ?? 0) < MAX_LAUNCH_ATTEMPTS) {
8648
9655
  log.warn("The integration isn't complete yet. Launching the agent to finish it...");
@@ -8654,7 +9661,8 @@ async function runCompletionPhase(state, deps, outputDir) {
8654
9661
  recipePath,
8655
9662
  cliCommand: deps.cliCommand,
8656
9663
  interactive: deps.interactive,
8657
- priorFailure: failure
9664
+ priorFailure: failure,
9665
+ userGuidance: state.userGuidance
8658
9666
  });
8659
9667
  state.launchAttempts = (state.launchAttempts ?? 0) + 1;
8660
9668
  await saveRecipeState(outputDir, state);
@@ -8673,7 +9681,8 @@ async function launchAgent(launcher, permissionMode, target) {
8673
9681
  outputDir: target.outputDir,
8674
9682
  recipePath: target.recipePath,
8675
9683
  cliCommand: target.cliCommand,
8676
- priorFailure: target.priorFailure
9684
+ priorFailure: target.priorFailure,
9685
+ userGuidance: target.userGuidance
8677
9686
  });
8678
9687
  await rm2(join27(target.outputDir, COMPLETION_MARKER_FILE), { force: true }).catch((err) => {
8679
9688
  debugLog("Could not clear a stale completion marker", { err });
@@ -8693,23 +9702,35 @@ async function launchAgent(launcher, permissionMode, target) {
8693
9702
  }
8694
9703
  suspend();
8695
9704
  try {
8696
- const exitCode = await launcher.launch(promptFile, permissionMode, target.interactive);
9705
+ const exitCode = await launcher.launch({
9706
+ message: launchMessage(promptFile),
9707
+ permissionMode,
9708
+ interactive: target.interactive,
9709
+ // An interactive session sits open after its final message, so the marker
9710
+ // the agent writes is the real "done" signal. A headless run exits on its
9711
+ // own and needs no watcher.
9712
+ watch: target.interactive ? (proc) => watchForCompletion(target.outputDir, proc) : void 0
9713
+ });
8697
9714
  log.info(`${launcher.label} exited (code ${exitCode ?? "unknown"}). Back to the planner.`);
8698
9715
  } finally {
8699
9716
  resume();
8700
9717
  }
8701
9718
  }
8702
- function describeIncompleteRecipe(read, uploadProblems) {
9719
+ function describeIncompleteRecipe(read, uploadProblems, recipePath, cliCommand) {
9720
+ const recheck = `Fix ${recipePath}, then re-check it with:
9721
+ ${cliCommand} sdk check --recipe ${recipePath}`;
8703
9722
  if (read.status === "absent") {
8704
- return "No completed recipe.json was produced, so there is nothing validated to submit.";
9723
+ return `No completed recipe.json was produced, so there is nothing validated to submit. It belongs at ${recipePath}.`;
8705
9724
  }
8706
9725
  if (read.status === "invalid") {
8707
- return `${RECIPE_FILE} does not match the recipe format Autonoma accepts:
8708
- ` + read.problems.map((problem) => ` - ${problem}`).join("\n");
9726
+ return `${recipePath} does not match the recipe format Autonoma accepts:
9727
+ ` + read.problems.map((problem) => ` - ${problem}`).join("\n") + `
9728
+ ${recheck}`;
8709
9729
  }
8710
9730
  if (uploadProblems.length > 0) {
8711
- return `recipe.json will be rejected on upload:
8712
- ` + uploadProblems.map((problem) => ` - ${problem}`).join("\n");
9731
+ return `${RECIPE_FILE} will be rejected on upload:
9732
+ ` + uploadProblems.map((problem) => ` - ${problem}`).join("\n") + `
9733
+ ${recheck}`;
8713
9734
  }
8714
9735
  return "The agent exited without marking the integration complete (see its IMPLEMENTATION.md for what's left).";
8715
9736
  }
@@ -8732,13 +9753,14 @@ var init_handoff = __esm({
8732
9753
  "src/agents/04-recipe-builder/phases/handoff.ts"() {
8733
9754
  "use strict";
8734
9755
  init_esm_shims();
9756
+ init_coding_agent();
8735
9757
  init_debug();
8736
9758
  init_interrupt();
8737
9759
  init_notify();
8738
9760
  init_prompts();
8739
9761
  init_completion();
9762
+ init_completion_watch();
8740
9763
  init_integration_prompt();
8741
- init_launcher();
8742
9764
  init_recipe();
8743
9765
  init_state2();
8744
9766
  MAX_LAUNCH_ATTEMPTS = 2;
@@ -8771,6 +9793,9 @@ function outcomeToResult(outcome) {
8771
9793
  async function runRecipeBuilder(input) {
8772
9794
  const { outputDir } = input;
8773
9795
  const state = await loadRecipeState(outputDir) ?? initialRecipeState();
9796
+ state.launchAttempts = 0;
9797
+ if (input.retryGuidance != null) state.userGuidance = input.retryGuidance;
9798
+ await saveRecipeState(outputDir, state);
8774
9799
  if (state.sharedSecret == null) {
8775
9800
  state.sharedSecret = input.config.sharedSecret ?? generateSharedSecret();
8776
9801
  await saveRecipeState(outputDir, state);
@@ -8839,9 +9864,9 @@ var init_recipe_builder = __esm({
8839
9864
  "src/agents/04-recipe-builder/index.ts"() {
8840
9865
  "use strict";
8841
9866
  init_esm_shims();
9867
+ init_coding_agent();
8842
9868
  init_prompts();
8843
9869
  init_entity_order();
8844
- init_launcher();
8845
9870
  init_handoff();
8846
9871
  init_submit();
8847
9872
  init_state2();
@@ -9707,7 +10732,7 @@ var init_recipe_context = __esm({
9707
10732
 
9708
10733
  // src/agents/05-test-generator/restore-deleted-test.ts
9709
10734
  import { access, mkdir as mkdir3, writeFile as writeFile11 } from "fs/promises";
9710
- import { dirname as dirname4 } from "path";
10735
+ import { dirname as dirname3 } from "path";
9711
10736
  async function restoreDeletedTest(testPath, originalContent) {
9712
10737
  try {
9713
10738
  await access(testPath);
@@ -9716,7 +10741,7 @@ async function restoreDeletedTest(testPath, originalContent) {
9716
10741
  debugLog("Test absent after its fix pass - restoring the pre-review content", { testPath, err });
9717
10742
  }
9718
10743
  try {
9719
- await mkdir3(dirname4(testPath), { recursive: true });
10744
+ await mkdir3(dirname3(testPath), { recursive: true });
9720
10745
  await writeFile11(testPath, originalContent, "utf-8");
9721
10746
  return true;
9722
10747
  } catch (err) {
@@ -10454,7 +11479,7 @@ var init_test_spec = __esm({
10454
11479
 
10455
11480
  // src/agents/05-test-generator/tools.ts
10456
11481
  import { mkdir as mkdir4, writeFile as writeFile12 } from "fs/promises";
10457
- import { dirname as dirname5, join as join32 } from "path";
11482
+ import { dirname as dirname4, join as join32 } from "path";
10458
11483
  import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as tool16, ToolLoopAgent as ToolLoopAgent3 } from "ai";
10459
11484
  import { z as z56 } from "zod";
10460
11485
  function buildWriteTestTool(state, outputDir, onWritten) {
@@ -10489,7 +11514,7 @@ function buildWriteTestTool(state, outputDir, onWritten) {
10489
11514
  };
10490
11515
  }
10491
11516
  try {
10492
- await mkdir4(dirname5(absPath), { recursive: true });
11517
+ await mkdir4(dirname4(absPath), { recursive: true });
10493
11518
  await writeFile12(absPath, content, "utf-8");
10494
11519
  state.markTested(nodeId, [relPath]);
10495
11520
  await saveBfsState(outputDir, state);
@@ -11878,7 +12903,7 @@ var init_countdown = __esm({
11878
12903
  });
11879
12904
 
11880
12905
  // src/ui/draw/help.ts
11881
- function wrap(text2, maxW) {
12906
+ function wrap2(text2, maxW) {
11882
12907
  const words = text2.split(/\s+/);
11883
12908
  const lines = [];
11884
12909
  let cur = "";
@@ -11900,7 +12925,7 @@ function drawHelpModal(g, state) {
11900
12925
  const innerW = w - 6;
11901
12926
  const step = state.currentStep;
11902
12927
  const intro2 = step != null ? STEP_INTROS[step] : "The run has no active step right now.";
11903
- const introLines = wrap(intro2, innerW);
12928
+ const introLines = wrap2(intro2, innerW);
11904
12929
  const docsUrl = step != null ? STEP_DOCS[step] : void 0;
11905
12930
  const pipelineLines = state.stepOrder.length;
11906
12931
  const keys = [
@@ -13577,6 +14602,114 @@ function shortHash(seed) {
13577
14602
  // src/agents/04-recipe-builder/sdk-command.ts
13578
14603
  import { z as z35 } from "zod";
13579
14604
 
14605
+ // src/core/cli-args.ts
14606
+ init_esm_shims();
14607
+ var CliArgs = class _CliArgs {
14608
+ /** Absent value means the flag was given with no value at all. */
14609
+ constructor(flags) {
14610
+ this.flags = flags;
14611
+ }
14612
+ static parse(argv) {
14613
+ const flags = /* @__PURE__ */ new Map();
14614
+ for (let i = 0; i < argv.length; i++) {
14615
+ const arg = argv[i] ?? "";
14616
+ if (!arg.startsWith("--")) continue;
14617
+ const body = arg.slice(2);
14618
+ const equals = body.indexOf("=");
14619
+ if (equals !== -1) {
14620
+ push(flags, body.slice(0, equals), body.slice(equals + 1));
14621
+ continue;
14622
+ }
14623
+ const next = argv[i + 1];
14624
+ if (next != null && next.length > 0 && !next.startsWith("--")) {
14625
+ push(flags, body, next);
14626
+ i++;
14627
+ continue;
14628
+ }
14629
+ push(flags, body, void 0);
14630
+ }
14631
+ return new _CliArgs(flags);
14632
+ }
14633
+ /** Whether any of these spellings was given at all, with or without a value. */
14634
+ has(...names) {
14635
+ return names.some((name) => this.flags.has(name));
14636
+ }
14637
+ /**
14638
+ * The value of the first of these spellings that carries one. A flag given with
14639
+ * no value has none - `--project` alone means the caller forgot the path, not
14640
+ * that the path is "true".
14641
+ */
14642
+ value(...names) {
14643
+ for (const name of names) {
14644
+ const values = this.flags.get(name);
14645
+ if (values != null && values.length > 0) return values[values.length - 1];
14646
+ }
14647
+ return void 0;
14648
+ }
14649
+ /**
14650
+ * Every value given across these spellings, comma-separated lists expanded. So
14651
+ * `--backend api --backend db` and `--backends api,db` mean the same thing, and
14652
+ * neither has to be the one the caller happened to guess.
14653
+ *
14654
+ * Undefined when the flag was never given - which is different from given-empty,
14655
+ * because a caller who explicitly asked for no backends should get none rather
14656
+ * than the ones something else inferred.
14657
+ */
14658
+ list(...names) {
14659
+ const given = names.flatMap((name) => this.flags.get(name) ?? []);
14660
+ if (!this.has(...names)) return void 0;
14661
+ return given.flatMap((value) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
14662
+ }
14663
+ /**
14664
+ * Flags that are not in `known`, with the closest thing that is.
14665
+ *
14666
+ * A misspelled `--non-interactive` is the worst kind of typo here: nothing
14667
+ * refuses it, and the run instead waits on questions nobody will answer. Naming
14668
+ * it back turns a silent stall into one line of output.
14669
+ */
14670
+ unrecognized(known) {
14671
+ const found = [];
14672
+ for (const name of this.flags.keys()) {
14673
+ if (known.has(name)) continue;
14674
+ const meant = closestMatch(name, known);
14675
+ found.push(meant != null ? { given: name, meant } : { given: name });
14676
+ }
14677
+ return found;
14678
+ }
14679
+ };
14680
+ function push(flags, name, value) {
14681
+ const existing = flags.get(name) ?? [];
14682
+ if (value != null) existing.push(value);
14683
+ flags.set(name, existing);
14684
+ }
14685
+ var MAX_TYPO_DISTANCE = 3;
14686
+ function closestMatch(name, known) {
14687
+ let best;
14688
+ let bestDistance = MAX_TYPO_DISTANCE + 1;
14689
+ for (const candidate of known) {
14690
+ const distance = editDistance(name, candidate);
14691
+ if (distance < bestDistance) {
14692
+ best = candidate;
14693
+ bestDistance = distance;
14694
+ }
14695
+ }
14696
+ return bestDistance <= MAX_TYPO_DISTANCE ? best : void 0;
14697
+ }
14698
+ function editDistance(a, b) {
14699
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
14700
+ for (let i = 1; i <= a.length; i++) {
14701
+ const current = [i];
14702
+ for (let j = 1; j <= b.length; j++) {
14703
+ const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
14704
+ const deletion = (previous[j] ?? 0) + 1;
14705
+ const insertion = (current[j - 1] ?? 0) + 1;
14706
+ current.push(Math.min(substitution, deletion, insertion));
14707
+ }
14708
+ previous = current;
14709
+ }
14710
+ return previous[b.length] ?? 0;
14711
+ }
14712
+
13580
14713
  // src/agents/04-recipe-builder/http-client.ts
13581
14714
  init_esm_shims();
13582
14715
  import { createHmac } from "crypto";
@@ -13623,7 +14756,9 @@ async function down(config, refsToken) {
13623
14756
  }
13624
14757
 
13625
14758
  // src/agents/04-recipe-builder/sdk-command.ts
14759
+ init_recipe();
13626
14760
  var DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
14761
+ var MIN_REPEAT_INSTANCES = 2;
13627
14762
  var createSchema = z35.record(z35.string(), z35.array(z35.unknown()));
13628
14763
  var recipeSliceSchema = z35.object({ create: createSchema, variables: ScenarioRecipeVariablesSchema.optional() });
13629
14764
  var envelopeSchema = z35.object({ recipes: z35.array(recipeSliceSchema).min(1) });
@@ -13632,16 +14767,40 @@ var flagsSchema = z35.object({
13632
14767
  recipe: z35.string().min(1).optional(),
13633
14768
  "refs-token": z35.string().min(1).optional(),
13634
14769
  "test-run-id": z35.string().min(1).optional(),
13635
- timeout: z35.coerce.number().int().positive().optional()
13636
- }).strict();
14770
+ timeout: z35.coerce.number().int().positive().optional(),
14771
+ repeat: z35.coerce.number().int().min(MIN_REPEAT_INSTANCES).optional()
14772
+ });
14773
+ var SDK_FLAGS = new Set(Object.keys(flagsSchema.shape));
13637
14774
  async function runSdkCommand(argv, io) {
13638
14775
  const action = argv[0];
14776
+ const args = CliArgs.parse(argv.slice(1));
14777
+ const unknown = args.unrecognized(SDK_FLAGS);
14778
+ if (unknown.length > 0) {
14779
+ io.stderr(`Unknown flag(s): ${unknown.map(describeUnknownFlag).join("; ")}
14780
+ `);
14781
+ return 2;
14782
+ }
14783
+ if (action === "check") {
14784
+ const recipe = args.value("recipe");
14785
+ if (recipe == null || recipe === "") {
14786
+ io.stderr("--recipe <file> is required for `check`.\n");
14787
+ return 2;
14788
+ }
14789
+ return await runCheck(recipe, io);
14790
+ }
13639
14791
  const sharedSecret = io.env.AUTONOMA_SHARED_SECRET;
13640
14792
  if (sharedSecret == null || sharedSecret === "") {
13641
14793
  io.stderr("AUTONOMA_SHARED_SECRET is not set in the environment.\n");
13642
14794
  return 2;
13643
14795
  }
13644
- const parsedFlags = flagsSchema.safeParse(tokenizeFlags(argv.slice(1)));
14796
+ const parsedFlags = flagsSchema.safeParse({
14797
+ url: args.value("url"),
14798
+ recipe: args.value("recipe"),
14799
+ "refs-token": args.value("refs-token"),
14800
+ "test-run-id": args.value("test-run-id"),
14801
+ timeout: args.value("timeout"),
14802
+ repeat: args.value("repeat")
14803
+ });
13645
14804
  if (!parsedFlags.success) {
13646
14805
  io.stderr(`Invalid flags: ${formatIssues(parsedFlags.error)}
13647
14806
  `);
@@ -13661,16 +14820,17 @@ async function runSdkCommand(argv, io) {
13661
14820
  io.stderr("--recipe <file> is required for `up`.\n");
13662
14821
  return 2;
13663
14822
  }
13664
- const testRunId = flags["test-run-id"] ?? `cli-${Date.now()}`;
14823
+ const baseRunId = flags["test-run-id"] ?? `cli-${Date.now()}`;
13665
14824
  const slice = await loadRecipeSlice(flags.recipe);
14825
+ if (flags.repeat != null) return await runRepeatedUps(config, slice, baseRunId, flags.repeat, io);
13666
14826
  const resolution = resolveRecipeCreateGraph({
13667
14827
  create: slice.create,
13668
14828
  variables: slice.variables,
13669
- testRunId
14829
+ testRunId: baseRunId
13670
14830
  });
13671
14831
  const create = createSchema.parse(resolution.createPayload);
13672
- return emit2(io, await up(config, create, testRunId), {
13673
- testRunId,
14832
+ return emit2(io, await up(config, create, baseRunId), {
14833
+ testRunId: baseRunId,
13674
14834
  resolvedVariables: resolution.resolvedVariables
13675
14835
  });
13676
14836
  }
@@ -13681,7 +14841,7 @@ async function runSdkCommand(argv, io) {
13681
14841
  }
13682
14842
  return emit2(io, await down(config, flags["refs-token"]));
13683
14843
  }
13684
- io.stderr(`Unknown sdk action "${action ?? ""}". Use one of: discover | up | down.
14844
+ io.stderr(`Unknown sdk action "${action ?? ""}". Use one of: discover | up | down | check.
13685
14845
  `);
13686
14846
  return 2;
13687
14847
  } catch (err) {
@@ -13693,6 +14853,116 @@ async function runSdkCommand(argv, io) {
13693
14853
  return 1;
13694
14854
  }
13695
14855
  }
14856
+ async function runRepeatedUps(config, slice, baseRunId, instances, io) {
14857
+ const seeded = [];
14858
+ for (let index = 1; index <= instances; index++) {
14859
+ const testRunId = `${baseRunId}-${index}`;
14860
+ const resolution = resolveRecipeCreateGraph({
14861
+ create: slice.create,
14862
+ variables: slice.variables,
14863
+ testRunId
14864
+ });
14865
+ const create = createSchema.parse(resolution.createPayload);
14866
+ const res = await up(config, create, testRunId);
14867
+ const refsToken = readRefsToken(res.body);
14868
+ seeded.push({
14869
+ instance: index,
14870
+ testRunId,
14871
+ ok: res.ok,
14872
+ status: res.status,
14873
+ refsToken,
14874
+ body: res.ok ? void 0 : res.body
14875
+ });
14876
+ if (!res.ok) break;
14877
+ }
14878
+ const failed = seeded.find((instance) => !instance.ok);
14879
+ const teardown2 = await tearDownInstances(config, seeded);
14880
+ io.stdout(
14881
+ JSON.stringify(
14882
+ {
14883
+ ok: failed == null,
14884
+ action: "up",
14885
+ repeat: instances,
14886
+ instances: seeded,
14887
+ teardown: teardown2,
14888
+ hint: repeatHint(failed, instances, teardown2)
14889
+ },
14890
+ null,
14891
+ 2
14892
+ ) + "\n"
14893
+ );
14894
+ return failed == null && teardown2.every((result) => result.ok) ? 0 : 1;
14895
+ }
14896
+ async function tearDownInstances(config, seeded) {
14897
+ const results = [];
14898
+ for (const instance of [...seeded].reverse()) {
14899
+ if (instance.refsToken == null) {
14900
+ results.push({
14901
+ testRunId: instance.testRunId,
14902
+ ok: false,
14903
+ detail: "no refsToken in the up response - anything it wrote before failing is still there"
14904
+ });
14905
+ continue;
14906
+ }
14907
+ try {
14908
+ const res = await down(config, instance.refsToken);
14909
+ results.push({
14910
+ testRunId: instance.testRunId,
14911
+ ok: res.ok,
14912
+ detail: res.ok ? void 0 : `down returned HTTP ${res.status}`
14913
+ });
14914
+ } catch (err) {
14915
+ results.push({
14916
+ testRunId: instance.testRunId,
14917
+ ok: false,
14918
+ detail: `down failed: ${err instanceof Error ? err.message : String(err)}`
14919
+ });
14920
+ }
14921
+ }
14922
+ return results;
14923
+ }
14924
+ function repeatHint(failed, instances, teardown2) {
14925
+ const leaked = teardown2.filter((result) => !result.ok).map((result) => result.testRunId);
14926
+ const leakNote = leaked.length > 0 ? ` Teardown did NOT complete for ${leaked.join(", ")} - check the database by hand.` : "";
14927
+ if (failed == null) {
14928
+ return `All ${instances} instances were live at the same time, so nothing in this recipe is single-instance.${leakNote}`;
14929
+ }
14930
+ return `Instance ${failed.instance} failed to seed while instance ${failed.instance - 1} was still up (HTTP ${failed.status}). That is a value this recipe reuses across runs: read the error above for the constraint it violated, then put {{testRunId}} or {{testRunShortId}} inside that field - or derive it from the testRunId your factory receives. Do not weaken the constraint.${leakNote}`;
14931
+ }
14932
+ function readRefsToken(body) {
14933
+ if (!isRecord(body)) return void 0;
14934
+ return typeof body.refsToken === "string" ? body.refsToken : void 0;
14935
+ }
14936
+ async function runCheck(recipePath, io) {
14937
+ const read = await loadRecipeFile(recipePath);
14938
+ if (read.status === "absent") {
14939
+ io.stdout(
14940
+ JSON.stringify(
14941
+ { ok: false, recipe: recipePath, problems: [`${recipePath} does not exist - write it first.`] },
14942
+ null,
14943
+ 2
14944
+ ) + "\n"
14945
+ );
14946
+ return 1;
14947
+ }
14948
+ if (read.status === "invalid") {
14949
+ io.stdout(JSON.stringify({ ok: false, recipe: recipePath, problems: read.problems }, null, 2) + "\n");
14950
+ return 1;
14951
+ }
14952
+ const problems = findRecipeUploadProblems(read.recipe);
14953
+ const summary = read.recipe.recipes.map((entry) => ({
14954
+ name: entry.name,
14955
+ entities: Object.keys(entry.create).length,
14956
+ records: Object.values(entry.create).reduce(
14957
+ (total, rows) => total + (Array.isArray(rows) ? rows.length : 0),
14958
+ 0
14959
+ )
14960
+ }));
14961
+ io.stdout(
14962
+ JSON.stringify({ ok: problems.length === 0, recipe: recipePath, problems, recipes: summary }, null, 2) + "\n"
14963
+ );
14964
+ return problems.length === 0 ? 0 : 1;
14965
+ }
13696
14966
  function emit2(io, res, resolution) {
13697
14967
  const output = resolution == null ? { ok: res.ok, status: res.status, body: res.body } : {
13698
14968
  ok: res.ok,
@@ -13722,26 +14992,8 @@ async function loadRecipeSlice(file) {
13722
14992
  `${file} is not a valid recipe: expected a full recipe envelope, a { create } object, or a bare { Entity: [records] } map.`
13723
14993
  );
13724
14994
  }
13725
- function tokenizeFlags(argv) {
13726
- const flags = {};
13727
- for (let i = 0; i < argv.length; i++) {
13728
- const arg = argv[i];
13729
- if (!arg.startsWith("--")) continue;
13730
- const body = arg.slice(2);
13731
- const eq = body.indexOf("=");
13732
- if (eq !== -1) {
13733
- flags[body.slice(0, eq)] = body.slice(eq + 1);
13734
- continue;
13735
- }
13736
- const next = argv[i + 1];
13737
- if (next != null && !next.startsWith("--")) {
13738
- flags[body] = next;
13739
- i++;
13740
- } else {
13741
- flags[body] = "true";
13742
- }
13743
- }
13744
- return flags;
14995
+ function describeUnknownFlag(unknown) {
14996
+ return unknown.meant != null ? `--${unknown.given} (did you mean --${unknown.meant}?)` : `--${unknown.given}`;
13745
14997
  }
13746
14998
  function formatIssues(error) {
13747
14999
  return error.issues.map((i) => i.path.length > 0 ? `--${i.path.join(".")}: ${i.message}` : i.message).join("; ");
@@ -13833,6 +15085,7 @@ function loadConfig(args) {
13833
15085
  autonomaApiUrl: resolveApiUrl(env.AUTONOMA_API_URL),
13834
15086
  autonomaApiToken: env.AUTONOMA_API_TOKEN,
13835
15087
  autonomaGenerationId: env.AUTONOMA_GENERATION_ID,
15088
+ autonomaApplicationId: env.AUTONOMA_APPLICATION_ID,
13836
15089
  agent: args.agent,
13837
15090
  permissionMode: args.permissionMode
13838
15091
  };
@@ -13841,8 +15094,228 @@ function loadConfig(args) {
13841
15094
  // src/index.ts
13842
15095
  init_analytics();
13843
15096
  init_colors();
15097
+
15098
+ // src/core/dry-run-phase.ts
15099
+ init_esm_shims();
15100
+ init_prompts();
15101
+ init_debug();
15102
+ init_errors();
15103
+ init_logs();
15104
+ var TARGET_POLL_MS = 1e4;
15105
+ var TARGET_READY_TIMEOUT_MS = 20 * 6e4;
15106
+ var NO_PREVIEW_GRACE_MS = 6e4;
15107
+ var MAX_QUOTED_FAILURES = 3;
15108
+ var DEFAULT_TIMING = {
15109
+ pollMs: TARGET_POLL_MS,
15110
+ readyTimeoutMs: TARGET_READY_TIMEOUT_MS,
15111
+ noPreviewGraceMs: NO_PREVIEW_GRACE_MS
15112
+ };
15113
+ async function runDryRunPhase(deps) {
15114
+ const timing = deps.timing ?? DEFAULT_TIMING;
15115
+ const { client, applicationId } = deps;
15116
+ const [scenarios, listing] = await Promise.all([
15117
+ client.listScenarios(applicationId),
15118
+ client.listDryRunTargets(applicationId)
15119
+ ]);
15120
+ if (scenarios.length === 0) {
15121
+ captureLog("info", "No scenarios to dry run", { source: "dry_run" });
15122
+ return { kind: "no-scenarios" };
15123
+ }
15124
+ const chosen = pickDryRunTarget(listing, deps.checkedOutBranch);
15125
+ if (chosen == null) {
15126
+ return {
15127
+ kind: "no-target",
15128
+ reason: "Autonoma has no preview environment to run your scenarios against yet. Open a pull request (or wait for your main preview to deploy) and run again."
15129
+ };
15130
+ }
15131
+ const unsupported = describeUnsupportedTarget(chosen);
15132
+ if (unsupported != null) return { kind: "unsupported-target", reason: unsupported };
15133
+ log.info(`Checking your Autonoma SDK against the "${chosen.label}" preview...`);
15134
+ const target = await waitForReadyTarget(client, applicationId, chosen, timing);
15135
+ if (target.kind === "gone") return { kind: "no-target", reason: target.reason };
15136
+ const discovery = await discoverSdk(client, applicationId, target.target, timing);
15137
+ if (discovery != null) return { kind: "discovery-failed", reason: discovery };
15138
+ return await dryRunScenarios(client, applicationId, target.target.id, scenarios);
15139
+ }
15140
+ function pickDryRunTarget(listing, checkedOutBranch) {
15141
+ const onCheckedOutBranch = checkedOutBranch == null ? void 0 : listing.targets.find((target) => target.branchName === checkedOutBranch);
15142
+ if (onCheckedOutBranch != null) return onCheckedOutBranch;
15143
+ const detected = listing.targets.find((target) => target.id === listing.autoDetectedTargetId);
15144
+ if (detected != null) return detected;
15145
+ return listing.targets.find((target) => target.availability === "ready");
15146
+ }
15147
+ function describeUnsupportedTarget(target) {
15148
+ if (target.source === "previewkit") return void 0;
15149
+ if (target.source === "vercel") {
15150
+ return `Your previews are built by Vercel, so the scenario dry run runs against the deployment you pick. Choose it on the SDK step in the Autonoma app and validate there - everything else in this run is done.`;
15151
+ }
15152
+ return `Your previews come from your own pipeline, and validating one needs the signing secret your pipeline signs with - which never leaves your side. Enter it on the SDK step in the Autonoma app to finish - everything else in this run is done.`;
15153
+ }
15154
+ async function waitForReadyTarget(client, applicationId, initial, timing) {
15155
+ const startedAt = Date.now();
15156
+ const deadline = startedAt + timing.readyTimeoutMs;
15157
+ const noPreviewDeadline = startedAt + Math.min(timing.noPreviewGraceMs, timing.readyTimeoutMs);
15158
+ let target = initial;
15159
+ while (true) {
15160
+ if (target.availability === "ready") return { kind: "ready", target };
15161
+ if (target.availability === "failed") {
15162
+ return {
15163
+ kind: "gone",
15164
+ reason: `The "${target.label}" preview failed to deploy, so there is nothing to run your scenarios against${target.error != null ? `: ${target.error}` : "."}`
15165
+ };
15166
+ }
15167
+ if (target.availability === "no_preview" && Date.now() >= noPreviewDeadline) {
15168
+ return {
15169
+ kind: "gone",
15170
+ reason: `The "${target.label}" pull request has no preview environment, so there is nothing to run your scenarios against. A draft pull request does not get one - mark it ready for review so a preview builds, then run again.`
15171
+ };
15172
+ }
15173
+ if (Date.now() >= deadline) {
15174
+ return {
15175
+ kind: "gone",
15176
+ reason: `The "${target.label}" preview was still building after ${Math.round(timing.readyTimeoutMs / 6e4)} minutes, so the scenario dry run was skipped. Run again once it is up.`
15177
+ };
15178
+ }
15179
+ debugLog("Waiting for the dry-run preview to deploy", {
15180
+ target: target.id,
15181
+ availability: target.availability
15182
+ });
15183
+ await sleep(timing.pollMs);
15184
+ const listing = await client.listDryRunTargets(applicationId).catch((err) => {
15185
+ debugLog("Could not re-read the dry-run targets", { err });
15186
+ return void 0;
15187
+ });
15188
+ const refreshed = listing?.targets.find((candidate) => candidate.id === target.id);
15189
+ if (refreshed != null) target = refreshed;
15190
+ }
15191
+ }
15192
+ async function discoverSdk(client, applicationId, target, timing) {
15193
+ try {
15194
+ const prepared = await client.prepareSdkTarget(applicationId, target.id);
15195
+ if (prepared.status === "redeploy_started") {
15196
+ log.info(`Redeploying the "${target.label}" preview with its Autonoma secrets...`);
15197
+ const back = await waitForRedeploy(client, applicationId, target, timing);
15198
+ if (back != null) return back;
15199
+ }
15200
+ for (const allowSelfHeal of [true, false]) {
15201
+ const result = await client.configureAndDiscoverSdkTarget(applicationId, target.id, allowSelfHeal);
15202
+ if (result.status === "discovered") {
15203
+ log.success("Your Autonoma SDK answered - Autonoma knows your data models.");
15204
+ captureLog("info", "SDK discovery succeeded", { source: "dry_run", target: target.id });
15205
+ return void 0;
15206
+ }
15207
+ log.info(`Refreshing the "${target.label}" preview's Autonoma secrets...`);
15208
+ const back = await waitForRedeploy(client, applicationId, target, timing);
15209
+ if (back != null) return back;
15210
+ }
15211
+ return `The "${target.label}" preview kept redeploying instead of answering. Try again from the Autonoma app.`;
15212
+ } catch (err) {
15213
+ const message = err instanceof Error ? err.message : String(err);
15214
+ captureLog("warn", "SDK discovery failed", { source: "dry_run", target: target.id });
15215
+ return `Your Autonoma SDK endpoint didn't answer, so the scenario dry run was skipped: ${message}
15216
+ Its own logs are on the SDK step in the Autonoma app, which is also where you can retry.`;
15217
+ }
15218
+ }
15219
+ async function waitForRedeploy(client, applicationId, target, timing) {
15220
+ const back = await waitForReadyTarget(client, applicationId, { ...target, availability: "building" }, timing);
15221
+ return back.kind === "ready" ? void 0 : back.reason;
15222
+ }
15223
+ async function dryRunScenarios(client, applicationId, targetId, scenarios) {
15224
+ log.info(`Running ${scenarios.length} scenario${scenarios.length === 1 ? "" : "s"} against your preview...`);
15225
+ const failures = [];
15226
+ for (const scenario of scenarios) {
15227
+ const result = await client.runScenarioDryRun(applicationId, scenario.id, targetId).catch((err) => ({ success: false, phase: void 0, error: err }));
15228
+ if (result.success) {
15229
+ log.success(`${scenario.name} - provisioned and torn down.`);
15230
+ continue;
15231
+ }
15232
+ const failure = {
15233
+ scenario: scenario.name,
15234
+ phase: result.phase,
15235
+ reason: formatDryRunError(result.error)
15236
+ };
15237
+ failures.push(failure);
15238
+ log.warn(`${scenario.name} - failed${failure.reason != null ? `: ${failure.reason}` : "."}`);
15239
+ }
15240
+ const passed = scenarios.length - failures.length;
15241
+ captureLog(failures.length > 0 ? "warn" : "info", "Scenario dry run finished", {
15242
+ source: "dry_run",
15243
+ scenario_count: scenarios.length,
15244
+ passed_count: passed
15245
+ });
15246
+ if (failures.length === 0) return { kind: "passed", scenarios: scenarios.length };
15247
+ return { kind: "failed", passed, failures };
15248
+ }
15249
+ function formatDryRunError(error) {
15250
+ if (error == null) return void 0;
15251
+ if (typeof error === "string") return error.length > 0 ? error : void 0;
15252
+ if (error instanceof Error) return error.message;
15253
+ return JSON.stringify(error);
15254
+ }
15255
+ function describeDryRunOutcome(outcome) {
15256
+ if (outcome.kind === "passed") return void 0;
15257
+ if (outcome.kind === "no-scenarios") {
15258
+ return "There were no scenarios to provision, so nothing was dry run. Autonoma's tests will run against whatever state your app is already in.";
15259
+ }
15260
+ if (outcome.kind === "no-target" || outcome.kind === "unsupported-target") return outcome.reason;
15261
+ if (outcome.kind === "discovery-failed") return outcome.reason;
15262
+ const quoted = outcome.failures.slice(0, MAX_QUOTED_FAILURES).map((failure) => {
15263
+ const where = failure.phase != null ? ` (${failure.phase})` : "";
15264
+ return ` - ${failure.scenario}${where}${failure.reason != null ? `: ${failure.reason}` : ""}`;
15265
+ }).join("\n");
15266
+ const rest = outcome.failures.length - MAX_QUOTED_FAILURES;
15267
+ return `${outcome.failures.length} of ${outcome.failures.length + outcome.passed} scenarios could not be provisioned:
15268
+ ${quoted}${rest > 0 ? `
15269
+ ...and ${rest} more` : ""}
15270
+ Fix them on the dry-run step in the Autonoma app, where a coding agent can read your SDK's own logs.`;
15271
+ }
15272
+
15273
+ // src/index.ts
13844
15274
  init_errors();
13845
15275
 
15276
+ // src/core/finish-phase.ts
15277
+ init_esm_shims();
15278
+ init_debug();
15279
+ init_logs();
15280
+ init_onboarding_phase();
15281
+ async function runFinishPhase(deps) {
15282
+ const dryRun = await runDryRunPhase({
15283
+ client: deps.client,
15284
+ applicationId: deps.applicationId,
15285
+ checkedOutBranch: deps.checkedOutBranch,
15286
+ timing: deps.timing
15287
+ });
15288
+ const repair = dryRun.kind === "passed" ? void 0 : await deps.repair?.(dryRun);
15289
+ const state = await deps.client.getOnboardingState(deps.applicationId);
15290
+ const live = isStepAtOrPast(state.step, LIVE_STEP);
15291
+ debugLog("Finish phase complete", { dryRun: dryRun.kind, repair: repair?.kind, step: state.step, live });
15292
+ captureLog("info", "Front-door run finished", {
15293
+ source: "finish_phase",
15294
+ dry_run: dryRun.kind,
15295
+ repair: repair?.kind ?? "not_needed",
15296
+ step: state.step,
15297
+ live,
15298
+ artifacts_uploaded: state.artifactsUploaded,
15299
+ sdk_configured: state.sdkConfigured,
15300
+ dry_run_passed: state.dryRunPassed
15301
+ });
15302
+ return { dryRun, state, live };
15303
+ }
15304
+ function describeFinishPhase(result) {
15305
+ const { state } = result;
15306
+ return [
15307
+ `Test suite: ${state.artifactsUploaded ? "uploaded to Autonoma" : "not uploaded yet"}`,
15308
+ `Autonoma SDK: ${state.sdkConfigured ? "connected to your app" : "not answering yet"}`,
15309
+ `Scenario data: ${describeDryRunState(result)}`,
15310
+ result.live ? "Autonoma is reviewing your pull requests." : "Autonoma is not reviewing your pull requests yet - take your app live in the Autonoma app to finish."
15311
+ ];
15312
+ }
15313
+ function describeDryRunState(result) {
15314
+ if (result.state.dryRunPassed) return "provisions against your preview";
15315
+ if (result.dryRun.kind === "no-scenarios") return "no scenarios to provision";
15316
+ return "not confirmed yet";
15317
+ }
15318
+
13846
15319
  // src/core/flush-telemetry.ts
13847
15320
  init_esm_shims();
13848
15321
  init_replay_transport();
@@ -13852,6 +15325,176 @@ async function flushTelemetry() {
13852
15325
  await Promise.all([flushAnalytics(), flushLogs(), flushReplay()]);
13853
15326
  }
13854
15327
 
15328
+ // src/index.ts
15329
+ init_front_door();
15330
+
15331
+ // src/core/help.ts
15332
+ init_esm_shims();
15333
+ init_colors();
15334
+ var DOCS_URL = "https://docs.autonoma.app/test-planner/";
15335
+ var LLMS_URL = "https://docs.autonoma.app/llms.txt";
15336
+ var LLMS_FULL_URL = "https://docs.autonoma.app/llms-full.txt";
15337
+ var FLAGS = [
15338
+ {
15339
+ names: ["project"],
15340
+ argument: "<path>",
15341
+ summary: "The repository to plan tests for. Defaults to the current directory."
15342
+ },
15343
+ {
15344
+ names: ["non-interactive"],
15345
+ summary: "Never ask a question. Every input has to be a flag, the coding agent runs headless and autonomous, and the run authorizes with an API key instead of a browser."
15346
+ },
15347
+ {
15348
+ names: ["frontend"],
15349
+ argument: "<path>",
15350
+ summary: "The one frontend directory to plan tests for. Only needed in a repository with more than one - a single-app repository resolves itself."
15351
+ },
15352
+ {
15353
+ names: ["backend", "backends"],
15354
+ argument: "<path>",
15355
+ summary: "A backend or data layer that frontend talks to. Repeatable, and also accepts a comma-separated list. Omit to use the dependencies the run infers."
15356
+ },
15357
+ {
15358
+ names: ["agent", "coding-agent"],
15359
+ argument: "<claude|codex>",
15360
+ summary: "Which coding agent to hand the preview environment and the SDK integration to. Omit to use whichever is installed; required when both are and nobody can be asked."
15361
+ },
15362
+ {
15363
+ names: ["permission-mode"],
15364
+ argument: "<default|acceptEdits|bypassPermissions>",
15365
+ summary: "How much autonomy that agent runs with. Defaults to bypassPermissions, which is the only one that means anything without a human at the keyboard."
15366
+ },
15367
+ {
15368
+ names: ["resume"],
15369
+ summary: "Continue a previous run from where it stopped instead of starting over."
15370
+ },
15371
+ {
15372
+ names: ["fresh"],
15373
+ summary: "Discard a previous run's output and start over. The opposite of --resume."
15374
+ },
15375
+ {
15376
+ names: ["step"],
15377
+ argument: "<name>",
15378
+ summary: "Run a single step and stop. For debugging one step in isolation - it is not how a caller sequences a run, which happens in one invocation."
15379
+ },
15380
+ {
15381
+ names: ["model"],
15382
+ argument: "<id>",
15383
+ summary: "Override the model the analysis runs on."
15384
+ },
15385
+ {
15386
+ names: ["slug"],
15387
+ argument: "<name>",
15388
+ summary: "Override the output folder name under ~/.autonoma/."
15389
+ },
15390
+ {
15391
+ names: ["help"],
15392
+ summary: "Print this."
15393
+ }
15394
+ ];
15395
+ var KNOWN_FLAGS = new Set(FLAGS.flatMap((flag) => flag.names));
15396
+ var FLAG_COLUMN = 34;
15397
+ var WHAT_IT_DOES = [
15398
+ "Autonoma's planner reads a codebase and produces the end-to-end test suite for it - the pages,",
15399
+ "the data models, the flows a user actually takes - and hands the parts that need judgement to a",
15400
+ "coding agent you already have installed. One invocation does the whole thing:",
15401
+ "",
15402
+ " 1. Preview environment Your coding agent sets up a real deployment of your app, built per",
15403
+ " pull request, that Autonoma tests against. Skipped when you already",
15404
+ " have one.",
15405
+ " 2. Project map Which directories are frontends, which are backends, which to ignore.",
15406
+ " 3. Pages and flows What your app can do, read from its source rather than guessed.",
15407
+ " 4. Knowledge base A written account of the app the later steps plan against.",
15408
+ " 5. Data models An audit of the entities your test data has to create.",
15409
+ " 6. Test scenarios The named app states your tests depend on.",
15410
+ " 7. Test data Your coding agent wires the Autonoma SDK into your repo and writes a",
15411
+ " factory per entity, validating each against your running app.",
15412
+ " 8. Test suite The test cases themselves, then uploaded to Autonoma.",
15413
+ "",
15414
+ "Nothing here is a step list to run one at a time. There is one command, it runs to the end, and",
15415
+ "it resumes from where it stopped if it is interrupted."
15416
+ ];
15417
+ var HEADLESS = [
15418
+ "With --non-interactive there is nobody to answer a question, so anything that would have been",
15419
+ "asked has to arrive as a flag. What the run does about that:",
15420
+ "",
15421
+ " - It never opens a browser. The coding agent's Autonoma connection is authorized with the",
15422
+ " AUTONOMA_API_TOKEN this run already holds.",
15423
+ " - It never blocks on a question. Where it has to assume an answer it says so on stdout,",
15424
+ " naming what was asked and what it took.",
15425
+ " - It reports each step as it starts and finishes, with how long it took, so the process that",
15426
+ " launched it can follow along.",
15427
+ " - It refuses rather than guesses when a choice would be arbitrary - two coding agents",
15428
+ " installed and no --agent, several frontends and no --frontend."
15429
+ ];
15430
+ var ENVIRONMENT = [
15431
+ " AUTONOMA_API_TOKEN Required. The run's credential. Create one in Settings -> API keys.",
15432
+ " AUTONOMA_APPLICATION_ID The Autonoma app this run belongs to. Set it and the run also sets",
15433
+ " up the preview environment and validates the result; leave it unset",
15434
+ " and the planner runs standalone against any repository.",
15435
+ " AUTONOMA_API_URL Point at a non-production Autonoma. Defaults to production.",
15436
+ " AUTONOMA_DEBUG Set to anything for verbose diagnostics on stderr.",
15437
+ " DONT_TRACK Set to anything to turn off analytics, logs and session replay."
15438
+ ];
15439
+ function renderHelp() {
15440
+ return [
15441
+ `${BOLD}autonoma-planner${RESET} - generate an end-to-end test suite from your codebase`,
15442
+ "",
15443
+ `${BOLD}USAGE${RESET}`,
15444
+ " autonoma-planner [run] [flags] Plan and generate the suite. `run` may be omitted.",
15445
+ " autonoma-planner status Show what a previous run completed.",
15446
+ " autonoma-planner upload Re-upload an already-generated suite.",
15447
+ " autonoma-planner help Print this.",
15448
+ "",
15449
+ `${BOLD}WHAT IT DOES${RESET}`,
15450
+ ...WHAT_IT_DOES.map(indent),
15451
+ "",
15452
+ `${BOLD}FLAGS${RESET}`,
15453
+ ...FLAGS.map(renderFlag),
15454
+ "",
15455
+ `${BOLD}RUNNING WITHOUT A HUMAN${RESET}`,
15456
+ ...HEADLESS.map(indent),
15457
+ "",
15458
+ `${BOLD}ENVIRONMENT${RESET}`,
15459
+ ...ENVIRONMENT,
15460
+ "",
15461
+ `${BOLD}DOCUMENTATION${RESET}`,
15462
+ ` ${DOCS_URL}`,
15463
+ ` ${DIM}${LLMS_URL} - every page, as a list, for an agent to read${RESET}`,
15464
+ ` ${DIM}${LLMS_FULL_URL} - all of it in one file${RESET}`,
15465
+ ""
15466
+ ].join("\n");
15467
+ }
15468
+ function indent(line) {
15469
+ return line.length > 0 ? ` ${line}` : line;
15470
+ }
15471
+ function renderFlag(flag) {
15472
+ const spellings = flag.names.map((name) => `--${name}`).join(", ");
15473
+ const left = ` ${spellings}${flag.argument != null ? ` ${flag.argument}` : ""}`;
15474
+ const indented = wrap(flag.summary, 92 - FLAG_COLUMN).map((line) => `${" ".repeat(FLAG_COLUMN)}${line}`);
15475
+ if (left.length >= FLAG_COLUMN) return [left, ...indented].join("\n");
15476
+ const [first, ...rest] = indented;
15477
+ return [`${left}${(first ?? "").slice(left.length)}`, ...rest].join("\n");
15478
+ }
15479
+ function wrap(text2, width) {
15480
+ const lines = [];
15481
+ let current = "";
15482
+ for (const word of text2.split(" ")) {
15483
+ if (current.length === 0) {
15484
+ current = word;
15485
+ continue;
15486
+ }
15487
+ if (current.length + 1 + word.length > width) {
15488
+ lines.push(current);
15489
+ current = word;
15490
+ continue;
15491
+ }
15492
+ current = `${current} ${word}`;
15493
+ }
15494
+ if (current.length > 0) lines.push(current);
15495
+ return lines;
15496
+ }
15497
+
13855
15498
  // src/index.ts
13856
15499
  init_interrupt();
13857
15500
  init_logs();
@@ -14033,12 +15676,13 @@ async function uploadArtifacts(config, outputDir) {
14033
15676
  await postJson(`${setupUrl}/artifacts`, autonomaApiToken, { testCases, artifacts, commitSha: gitInfo?.sha });
14034
15677
  await patchJson(setupUrl, autonomaApiToken, { status: "completed" });
14035
15678
  log.success(
14036
- `Uploaded ${testCases.length} test case${testCases.length === 1 ? "" : "s"} and ${artifacts.length} artifact${artifacts.length === 1 ? "" : "s"}. Return to your browser to continue onboarding.`
15679
+ `Uploaded ${testCases.length} test case${testCases.length === 1 ? "" : "s"} and ${artifacts.length} artifact${artifacts.length === 1 ? "" : "s"}.`
14037
15680
  );
14038
15681
  }
14039
15682
 
14040
15683
  // src/index.ts
14041
15684
  init_version();
15685
+ init_eta();
14042
15686
  init_steps();
14043
15687
  init_store();
14044
15688
  process.env.NODE_ENV ??= "production";
@@ -14074,26 +15718,10 @@ async function collectRunStats(outputDir) {
14074
15718
  stat3(tests, "E2E test", "E2E tests")
14075
15719
  ];
14076
15720
  }
14077
- function parseArgs(argv) {
14078
- const args = {};
14079
- for (let i = 0; i < argv.length; i++) {
14080
- const arg = argv[i];
14081
- if (arg.startsWith("--")) {
14082
- const key = arg.slice(2);
14083
- const next = argv[i + 1];
14084
- if (next && !next.startsWith("--")) {
14085
- args[key] = next;
14086
- i++;
14087
- } else {
14088
- args[key] = true;
14089
- }
14090
- }
15721
+ function warnAboutUnknownFlags(args) {
15722
+ for (const { given, meant } of args.unrecognized(KNOWN_FLAGS)) {
15723
+ log.warn(`Unknown flag --${given}${meant != null ? `. Did you mean --${meant}?` : "."} Ignoring it.`);
14091
15724
  }
14092
- return args;
14093
- }
14094
- function strArg(args, key) {
14095
- const value = args[key];
14096
- return typeof value === "string" ? value : void 0;
14097
15725
  }
14098
15726
  var STEP_LABELS = {
14099
15727
  projectMapper: "Map your project structure",
@@ -14140,8 +15768,9 @@ async function promptScopeSelection(map) {
14140
15768
  }
14141
15769
  async function runStep(step, outputDir, state, config, nonInteractive, retryGuidance) {
14142
15770
  const label = STEP_LABELS[step];
14143
- note(STEP_INTROS[step], `Step: ${label}`);
15771
+ note(STEP_INTROS[step], `Step ${STEP_ORDER.indexOf(step) + 1}/${STEP_ORDER.length}: ${label}`);
14144
15772
  const stepStartedAt = Date.now();
15773
+ const elapsed = () => formatClock(Date.now() - stepStartedAt);
14145
15774
  track("cli_step_started", { step });
14146
15775
  captureLog("info", `Step started: ${label}`, {
14147
15776
  source: "pipeline",
@@ -14251,7 +15880,7 @@ async function runStep(step, outputDir, state, config, nonInteractive, retryGuid
14251
15880
  }
14252
15881
  case "recipeBuilder": {
14253
15882
  const { runRecipeBuilder: runRecipeBuilder2 } = await Promise.resolve().then(() => (init_recipe_builder(), recipe_builder_exports));
14254
- const { parsePermissionMode: parsePermissionMode2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
15883
+ const { parsePermissionMode: parsePermissionMode2 } = await Promise.resolve().then(() => (init_coding_agent(), coding_agent_exports));
14255
15884
  result = await runRecipeBuilder2({
14256
15885
  projectRoot: config.projectRoot,
14257
15886
  outputDir,
@@ -14283,26 +15912,26 @@ async function runStep(step, outputDir, state, config, nonInteractive, retryGuid
14283
15912
  if (result && !result.success) {
14284
15913
  if (result.paused) {
14285
15914
  state = await markStep(outputDir, state, step, "paused");
14286
- log.info(`Paused: ${label} - ${result.summary}`);
15915
+ log.info(`Paused after ${elapsed()}: ${label} - ${result.summary}`);
14287
15916
  } else {
14288
15917
  state = await markStep(outputDir, state, step, "failed");
14289
- log.error(`Failed: ${label} - ${result.summary}`);
15918
+ log.error(`Failed after ${elapsed()}: ${label} - ${result.summary}`);
14290
15919
  trackError(new Error(result.summary), { step, source: "step_result" });
14291
15920
  }
14292
15921
  } else {
14293
15922
  state = await markStep(outputDir, state, step, "done");
14294
- log.success(`Completed: ${label}`);
15923
+ log.success(`Completed in ${elapsed()}: ${label}`);
14295
15924
  }
14296
15925
  } catch (err) {
14297
15926
  if (isUserCancellation(err)) throw err;
14298
15927
  state = await markStep(outputDir, state, step, "failed");
14299
15928
  const known = describeKnownError(err);
14300
15929
  if (known) {
14301
- log.error(`Failed: ${label} - ${known.title}`);
15930
+ log.error(`Failed after ${elapsed()}: ${label} - ${known.title}`);
14302
15931
  log.info(known.hint);
14303
15932
  } else {
14304
15933
  const message = err instanceof Error ? err.message : String(err);
14305
- log.error(`Failed: ${label} - ${message}`);
15934
+ log.error(`Failed after ${elapsed()}: ${label} - ${message}`);
14306
15935
  console.error(`\x1B[2m${formatException(err)}\x1B[0m`);
14307
15936
  console.error(`\x1B[2m${supportReference({ step })}\x1B[0m`);
14308
15937
  log.info("If you report this, please include the error output above.");
@@ -14416,16 +16045,45 @@ function ensureAutonomaAuth() {
14416
16045
  );
14417
16046
  return false;
14418
16047
  }
16048
+ async function finishFrontDoorRun(frontDoor, config, nonInteractive) {
16049
+ try {
16050
+ const gitInfo = await readGitInfo(config.projectRoot);
16051
+ const result = await runFinishPhase({
16052
+ client: frontDoor.client,
16053
+ applicationId: frontDoor.applicationId,
16054
+ checkedOutBranch: gitInfo?.branch,
16055
+ // Only reached when the direct calls did not pass. What is wrong then needs
16056
+ // the repo and a decision, which is an agent's job rather than an API call's.
16057
+ repair: () => runSdkRepairHandoff({ plan: frontDoor, config, nonInteractive })
16058
+ });
16059
+ track("cli_dry_run_finished", { outcome: result.dryRun.kind, live: result.live });
16060
+ const problem = describeDryRunOutcome(result.dryRun);
16061
+ if (problem != null) log.warn(problem);
16062
+ note(describeFinishPhase(result).join("\n"), "Where your app stands");
16063
+ return result;
16064
+ } catch (err) {
16065
+ const message = err instanceof Error ? err.message : String(err);
16066
+ log.warn(`Couldn't confirm your scenarios against a preview: ${message}`);
16067
+ log.info("Your test suite is uploaded either way - finish the dry run in the Autonoma app.");
16068
+ trackError(err, { source: "finish_phase" });
16069
+ return void 0;
16070
+ }
16071
+ }
16072
+ function nextStepLine(finish, nonInteractive) {
16073
+ if (finish?.live === true) return "Autonoma is reviewing your pull requests - open one to see it work.";
16074
+ if (nonInteractive) return "Setup is not finished - the steps above that are still outstanding say why.";
16075
+ return "Next: continue on autonoma.app";
16076
+ }
14419
16077
  async function main() {
14420
16078
  installTerminationDiagnostics();
14421
- const args = parseArgs(process.argv.slice(2));
16079
+ const args = CliArgs.parse(process.argv.slice(2));
14422
16080
  const command = process.argv[2];
14423
16081
  if (command === "status") {
14424
16082
  const config2 = loadConfig({
14425
- project: strArg(args, "project"),
14426
- slug: strArg(args, "slug")
16083
+ project: args.value("project"),
16084
+ slug: args.value("slug")
14427
16085
  });
14428
- if (!args.project) {
16086
+ if (!args.has("project")) {
14429
16087
  console.log(`No --project flag passed; using current working directory: ${config2.projectRoot}`);
14430
16088
  }
14431
16089
  const outputDir2 = await ensureOutputDir(config2.projectSlug);
@@ -14434,8 +16092,8 @@ async function main() {
14434
16092
  }
14435
16093
  if (command === "upload") {
14436
16094
  const config2 = loadConfig({
14437
- project: strArg(args, "project"),
14438
- slug: strArg(args, "slug")
16095
+ project: args.value("project"),
16096
+ slug: args.value("slug")
14439
16097
  });
14440
16098
  const outputDir2 = await ensureOutputDir(config2.projectSlug);
14441
16099
  const recipeUploaded = await uploadRecipeFromDisk(outputDir2, config2);
@@ -14451,19 +16109,13 @@ async function main() {
14451
16109
  });
14452
16110
  process.exit(exitCode);
14453
16111
  }
14454
- if (command === "help" || args.help) {
14455
- console.log("Usage:");
14456
- console.log(
14457
- " test-planner [run] [--project <path>] [--frontend <path>] [--backends <path,path>] [--model <id>] [--step <name>] [--resume] [--non-interactive] [--agent <claude|codex>] [--permission-mode <default|acceptEdits|bypassPermissions>]"
14458
- );
14459
- console.log(" test-planner status [--project <path>]");
14460
- console.log(" test-planner upload [--project <path>] # re-upload already-generated recipe + artifacts");
14461
- console.log("");
14462
- console.log("`run` is the default command; it may be omitted.");
16112
+ if (command === "help" || args.has("help")) {
16113
+ console.log(renderHelp());
14463
16114
  return;
14464
16115
  }
14465
16116
  console.log(BANNER);
14466
- const resumeCommand = `autonoma-planner --resume` + (args.project ? ` --project ${args.project}` : "");
16117
+ const projectArg = args.value("project");
16118
+ const resumeCommand = `autonoma-planner --resume` + (projectArg != null ? ` --project ${projectArg}` : "");
14467
16119
  let mountedUi;
14468
16120
  installInterruptHandler({
14469
16121
  // exitCode defaults to 0 for a user-initiated Ctrl+C (progress saved, a clean stop);
@@ -14481,24 +16133,24 @@ async function main() {
14481
16133
  void flushTelemetry().finally(() => process.exit(exitCode));
14482
16134
  }
14483
16135
  });
14484
- const backendsArg = strArg(args, "backends");
14485
16136
  const config = loadConfig({
14486
- project: strArg(args, "project"),
14487
- model: strArg(args, "model"),
14488
- slug: strArg(args, "slug"),
14489
- frontend: strArg(args, "frontend"),
14490
- backends: backendsArg != null ? backendsArg.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0,
14491
- agent: strArg(args, "agent"),
14492
- permissionMode: strArg(args, "permission-mode")
16137
+ project: projectArg,
16138
+ model: args.value("model"),
16139
+ slug: args.value("slug"),
16140
+ frontend: args.value("frontend"),
16141
+ backends: args.list("backend", "backends"),
16142
+ agent: args.value("agent", "coding-agent"),
16143
+ permissionMode: args.value("permission-mode")
14493
16144
  });
14494
16145
  initSession({ generationId: config.autonomaGenerationId, projectSlug: config.projectSlug });
14495
16146
  intro("Let's generate your test suite");
14496
16147
  if (!ensureAutonomaAuth()) {
14497
16148
  return;
14498
16149
  }
14499
- const nonInteractive = !!args["non-interactive"];
16150
+ warnAboutUnknownFlags(args);
16151
+ const nonInteractive = args.has("non-interactive");
14500
16152
  const modelName = config.modelId ?? readEnv().OPENROUTER_MODEL ?? DEFAULT_MODEL;
14501
- if (!args.project) {
16153
+ if (projectArg == null) {
14502
16154
  log.info(`No --project flag passed; using current working directory.`);
14503
16155
  }
14504
16156
  log.info(`Project: ${config.projectRoot}`);
@@ -14519,12 +16171,26 @@ async function main() {
14519
16171
  }
14520
16172
  }
14521
16173
  if (!nonInteractive) mountedUi = await mountDashboard(outputDir, config.projectSlug);
14522
- let isResuming = !!(args.resume || args.step);
16174
+ if (args.has("fresh") && args.has("resume")) {
16175
+ mountedUi?.unmount();
16176
+ mountedUi = void 0;
16177
+ log.error("--fresh and --resume ask for opposite things. Pass one of them, not both.");
16178
+ process.exitCode = 1;
16179
+ return;
16180
+ }
16181
+ let isResuming = args.has("resume") || args.has("step");
14523
16182
  const hasProgress = Object.values(state.steps).some((s) => s === "done" || s === "running");
16183
+ const { planFrontDoor: planFrontDoor2 } = await Promise.resolve().then(() => (init_front_door(), front_door_exports));
16184
+ const frontDoor = await planFrontDoor2(config);
16185
+ const previewPending = frontDoor?.phase === "preview";
14524
16186
  if (!nonInteractive && !isResuming && !hasProgress) {
14525
16187
  await welcome({
14526
- title: "Let's build your test suite.",
14527
- lines: [
16188
+ title: previewPending ? "Let's get Autonoma running on your app." : "Let's build your test suite.",
16189
+ lines: previewPending ? [
16190
+ "First your coding agent sets up a preview environment - a real deployment of your app, built per pull request, that Autonoma tests against. It reads your repo and works out how to build and run it.",
16191
+ "Then Autonoma analyzes your codebase - its pages, data models, and user flows - and generates a full suite of end-to-end test cases that cover them.",
16192
+ "It all happens right here, in this terminal, so you can watch and steer it."
16193
+ ] : [
14528
16194
  "Autonoma analyzes your codebase - its pages, data models, and user flows - and generates a full suite of end-to-end test cases that cover them, so you get real test coverage without writing a single test yourself.",
14529
16195
  "It takes a little while, and the whole run happens right here so you can watch it work.",
14530
16196
  "This analysis is free for new accounts."
@@ -14532,7 +16198,23 @@ async function main() {
14532
16198
  cta: "Press enter to begin"
14533
16199
  });
14534
16200
  }
14535
- if (!isResuming && !nonInteractive && hasProgress) {
16201
+ if (frontDoor != null && previewPending) {
16202
+ const { runPreviewHandoff: runPreviewHandoff2, describeIncompletePreview: describeIncompletePreview2 } = await Promise.resolve().then(() => (init_front_door(), front_door_exports));
16203
+ const result = await runPreviewHandoff2({ plan: frontDoor, config, nonInteractive });
16204
+ track("cli_preview_phase_finished", { outcome: result.kind });
16205
+ const problem = describeIncompletePreview2(result);
16206
+ if (problem != null) log.warn(problem);
16207
+ }
16208
+ if (args.has("fresh") && hasProgress) {
16209
+ await clearOutputDir(config.projectSlug);
16210
+ state = initialState();
16211
+ await saveState(outputDir, state);
16212
+ log.info(`Cleared ${displayPath(outputDir)} - starting from scratch.`);
16213
+ } else if (!isResuming && nonInteractive && hasProgress) {
16214
+ log.info(
16215
+ "A previous run left output in this folder and no flag said what to do with it, so this run continues from it. Pass --fresh to discard it, or --resume to say so deliberately."
16216
+ );
16217
+ } else if (!isResuming && !nonInteractive && hasProgress) {
14536
16218
  const completedSteps = Object.entries(state.steps).filter(([, s]) => s === "done").map(([name]) => isStepName(name) ? STEP_LABELS[name] : name).join(", ");
14537
16219
  const resume2 = await confirm({
14538
16220
  message: `Found a previous run${completedSteps ? ` (completed: ${completedSteps})` : ""}. Resume from where you left off?`,
@@ -14561,7 +16243,7 @@ It's a hidden folder in your home directory - in Finder/Explorer use "Go to fold
14561
16243
  or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
14562
16244
  "Output folder"
14563
16245
  );
14564
- const stepArg = strArg(args, "step");
16246
+ const stepArg = args.value("step");
14565
16247
  const targetStep = stepArg != null && isStepName(stepArg) ? stepArg : void 0;
14566
16248
  if (stepArg != null && targetStep == null) {
14567
16249
  mountedUi?.unmount();
@@ -14580,7 +16262,7 @@ or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
14580
16262
  mountedUi?.unmount();
14581
16263
  mountedUi = void 0;
14582
16264
  if (state.steps[targetStep] === "failed") {
14583
- const retryCommand = `autonoma-planner --step ${targetStep}` + (args.project ? ` --project ${args.project}` : "");
16265
+ const retryCommand = `autonoma-planner --step ${targetStep}` + (projectArg != null ? ` --project ${projectArg}` : "");
14584
16266
  log.warn(`Your progress is saved. To retry this step, run:
14585
16267
  ${retryCommand}`);
14586
16268
  process.exitCode = 1;
@@ -14597,7 +16279,12 @@ or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
14597
16279
  }
14598
16280
  const steps = STEP_ORDER;
14599
16281
  const startIdx = steps.indexOf(startStep);
14600
- note(steps.map((s, idx) => `${idx + 1}. ${STEP_LABELS[s]} - ${STEP_SUMMARIES[s]}`).join("\n"), "Here's the plan");
16282
+ note(
16283
+ `${steps.map((s, idx) => `${idx + 1}. ${STEP_LABELS[s]} - ${STEP_SUMMARIES[s]}`).join("\n")}
16284
+
16285
+ All of it runs from this one command - there is nothing to launch step by step.`,
16286
+ "Here's the plan"
16287
+ );
14601
16288
  try {
14602
16289
  for (let i = startIdx; i < steps.length; i++) {
14603
16290
  const step = steps[i];
@@ -14643,13 +16330,14 @@ or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
14643
16330
  trackError(err, { source: "artifact_upload" });
14644
16331
  }
14645
16332
  }
16333
+ const finish = frontDoor != null && allStepsDone ? await finishFrontDoorRun(frontDoor, config, nonInteractive) : void 0;
14646
16334
  const anyFailed = Object.values(state.steps).some((s) => s === "failed");
14647
16335
  getActiveStore()?.finish({ kind: allStepsDone ? "complete" : anyFailed ? "failed" : "paused" });
14648
16336
  if (allStepsDone) {
14649
16337
  const choice = await completion({
14650
16338
  title: "Your test suite is ready.",
14651
16339
  stats: await collectRunStats(outputDir),
14652
- lines: [`Saved in ${displayPath(outputDir)}`, "Next: continue on autonoma.app"]
16340
+ lines: [`Saved in ${displayPath(outputDir)}`, nextStepLine(finish, nonInteractive)]
14653
16341
  });
14654
16342
  track("cli_completion_choice", { choice });
14655
16343
  if (choice === "browse") {
@@ -14662,7 +16350,8 @@ or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
14662
16350
  if (allStepsDone) {
14663
16351
  log.success("Your test suite is ready.");
14664
16352
  log.info(`Saved in ${displayPath(outputDir)}`);
14665
- log.info("Next: continue on autonoma.app");
16353
+ if (finish != null) for (const line of describeFinishPhase(finish)) log.info(line);
16354
+ log.info(nextStepLine(finish, nonInteractive));
14666
16355
  }
14667
16356
  outro("Done");
14668
16357
  }