brnch-cli 0.18.3 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/brnch.mjs +726 -142
  3. package/package.json +1 -1
package/brnch.mjs CHANGED
@@ -29789,8 +29789,8 @@ function convertBaseSchema(schema, ctx) {
29789
29789
  });
29790
29790
  }
29791
29791
  ctx.processing.add(refPath);
29792
- const resolved = resolveRef(refPath, ctx);
29793
- const zodSchema2 = convertSchema(resolved, ctx);
29792
+ const resolved2 = resolveRef(refPath, ctx);
29793
+ const zodSchema2 = convertSchema(resolved2, ctx);
29794
29794
  ctx.refs.set(refPath, zodSchema2);
29795
29795
  ctx.processing.delete(refPath);
29796
29796
  return zodSchema2;
@@ -30573,12 +30573,13 @@ var PLATFORM_TOKEN_PREFIXES = {
30573
30573
  fsfa: "pending second-factor sign-in",
30574
30574
  fbkc: "Buildkite checkout token",
30575
30575
  fsvw: "staff read-only view",
30576
- fcrt: "container registry token"
30576
+ fcrt: "container registry token",
30577
+ fcpk: "clone-pack build token"
30577
30578
  };
30578
30579
  var PLATFORM_TOKEN_RE = new RegExp(`^(${Object.keys(PLATFORM_TOKEN_PREFIXES).join("|")})_[0-9a-f]{64}$`);
30579
30580
 
30580
30581
  // packages/domain/src/capabilities.ts
30581
- var VIEWER = ["org:read", "members:read", "repo:read", "task:read", "change:read", "session:read", "packages:read"];
30582
+ var VIEWER = ["org:read", "members:read", "repo:read", "task:read", "change:read", "session:read", "packages:read", "migrations:read"];
30582
30583
  var REVIEWER = [...VIEWER, "review:comment", "review:resolve", "review:approve"];
30583
30584
  var DEVELOPER = [
30584
30585
  ...VIEWER,
@@ -30591,9 +30592,11 @@ var DEVELOPER = [
30591
30592
  "review:comment",
30592
30593
  "evidence:report",
30593
30594
  "promotion:request",
30594
- "packages:write"
30595
+ "packages:write",
30596
+ "deploy:read"
30595
30597
  ];
30596
- var MAINTAINER = [...DEVELOPER, ...REVIEWER, "repo:admin", "policy:write", "session:manage", "secret_scanning:read", "secret_scanning:resolve"];
30598
+ var DEPLOY_AND_MIGRATIONS = ["deploy:write", "deploy:trigger", "deploy:manage", "migrations:manage", "migrations:approve"];
30599
+ var MAINTAINER = [...DEVELOPER, ...REVIEWER, "repo:admin", ...DEPLOY_AND_MIGRATIONS, "policy:write", "session:manage", "secret_scanning:read", "secret_scanning:resolve"];
30597
30600
  var ADMIN = [
30598
30601
  ...MAINTAINER,
30599
30602
  "org:admin",
@@ -31634,7 +31637,6 @@ var looksLikeHostedLabel = (label2) => /^brnch-hosted(?:-|$)/.test(label2);
31634
31637
  var DEPLOY_LIMITS = {
31635
31638
  maxStepsPerTarget: 12,
31636
31639
  maxWatchPaths: 50,
31637
- maxTargetsPerRepository: 10,
31638
31640
  maxCommandLength: 2e3,
31639
31641
  maxSummaryLength: 500,
31640
31642
  maxDetailKeys: 20,
@@ -31744,7 +31746,8 @@ var PLANS = Object.freeze({
31744
31746
  storage_bytes_per_seat: 0,
31745
31747
  ai: { fast: 100, strong: 25, agent: 0, assistant: 0 },
31746
31748
  private_repo_members: 5,
31747
- overage: false
31749
+ overage: false,
31750
+ deploy_targets_per_repository: 1
31748
31751
  },
31749
31752
  pro: {
31750
31753
  id: "pro",
@@ -31763,7 +31766,8 @@ var PLANS = Object.freeze({
31763
31766
  storage_bytes_per_seat: 0,
31764
31767
  ai: { fast: 300, strong: 100, agent: 10, assistant: 100 },
31765
31768
  private_repo_members: 5,
31766
- overage: true
31769
+ overage: true,
31770
+ deploy_targets_per_repository: 10
31767
31771
  },
31768
31772
  team: {
31769
31773
  id: "team",
@@ -31782,7 +31786,8 @@ var PLANS = Object.freeze({
31782
31786
  storage_bytes_per_seat: 2 * GB,
31783
31787
  ai: { fast: 600, strong: 200, agent: 10, assistant: 100 },
31784
31788
  private_repo_members: Number.POSITIVE_INFINITY,
31785
- overage: true
31789
+ overage: true,
31790
+ deploy_targets_per_repository: 10
31786
31791
  },
31787
31792
  enterprise: {
31788
31793
  id: "enterprise",
@@ -31801,7 +31806,8 @@ var PLANS = Object.freeze({
31801
31806
  storage_bytes_per_seat: 5 * GB,
31802
31807
  ai: { fast: 1500, strong: 500, agent: 40, assistant: 300 },
31803
31808
  private_repo_members: Number.POSITIVE_INFINITY,
31804
- overage: true
31809
+ overage: true,
31810
+ deploy_targets_per_repository: 1e3
31805
31811
  }
31806
31812
  });
31807
31813
 
@@ -32159,12 +32165,16 @@ var CreateInvitationRequest = external_exports.object({ email: external_exports.
32159
32165
  var AcceptInvitationRequest = external_exports.object({ token: external_exports.string() });
32160
32166
  var CreateTeamRequest = external_exports.object({ slug: Slug, name: displayLine().min(1).max(100) });
32161
32167
  var AddTeamMemberRequest = external_exports.object({ user_id: external_exports.string() });
32168
+ var GrantExtraCapabilitySchema = external_exports.enum(["deploy:read", "deploy:trigger", "deploy:manage", "migrations:read", "migrations:manage", "migrations:approve"]);
32162
32169
  var CreateGrantRequest = external_exports.object({
32163
32170
  principal_type: external_exports.enum(["user", "team"]),
32164
32171
  principal_id: external_exports.string().min(1).max(100),
32165
32172
  role: RepoGrantRoleSchema,
32173
+ /** Beyond the role, e.g. a developers team that approves a database's migrations. */
32174
+ capabilities: external_exports.array(GrantExtraCapabilitySchema).max(6).optional(),
32166
32175
  expires_at: external_exports.iso.datetime().nullable().optional()
32167
32176
  });
32177
+ var UpdateGrantRequest = external_exports.object({ capabilities: external_exports.array(GrantExtraCapabilitySchema).max(6) });
32168
32178
  var CreateRepoRequest = external_exports.object({
32169
32179
  name: RepoName,
32170
32180
  description: displayText().max(500).default(""),
@@ -32205,15 +32215,27 @@ var UpdateRepoRequest = external_exports.object({
32205
32215
  default_branch: external_exports.string().min(1).max(100).optional(),
32206
32216
  /** Replaces the topic list (≤20, lowercase). */
32207
32217
  topics: Topics.optional(),
32218
+ /** The repository Actions switch (ADR-0009 addendum 2026-10-18). Off: no automatic runs, workflow_dispatch refused. */
32219
+ actions_enabled: external_exports.boolean().optional(),
32208
32220
  version: external_exports.number().int()
32209
32221
  });
32210
32222
  var PATH_HISTORY_SCAN_MAX = 500;
32211
32223
  var RepoPathQuery = external_exports.string().min(1).max(4096).transform((p) => p.replace(/\/{2,}/g, "/").replace(/\/+$/, "")).refine((p) => p.length > 0 && !p.startsWith("/"), "path must be relative to the repository root").refine((p) => !/[\u0000-\u001f\u007f]/.test(p), "path must not contain control characters").refine((p) => p.split("/").every((s) => s !== "." && s !== ".."), "path must not contain . or .. segments");
32224
+ var isoDate = external_exports.string().max(64).refine((v) => !Number.isNaN(Date.parse(v)), { message: "must be an ISO-8601 date" });
32225
+ var queryFlag = external_exports.enum(["1", "true", "0", "false"]).transform((v) => v === "1" || v === "true");
32212
32226
  var CommitListQuery = external_exports.object({
32213
32227
  ref: external_exports.string().optional(),
32214
32228
  path: RepoPathQuery.optional(),
32215
- scan_limit: external_exports.coerce.number().int().min(1).max(PATH_HISTORY_SCAN_MAX).optional()
32216
- }).refine((q) => q.scan_limit === void 0 || q.path !== void 0, { message: "scan_limit requires path", path: ["scan_limit"] });
32229
+ scan_limit: external_exports.coerce.number().int().min(1).max(PATH_HISTORY_SCAN_MAX).optional(),
32230
+ // Log filters (M6-SCL-02): substring matches, case-insensitive (no regular expressions); dates bound the committer
32231
+ // date, inclusive.
32232
+ author: external_exports.string().min(1).max(200).optional(),
32233
+ grep: external_exports.string().min(1).max(200).optional(),
32234
+ since: isoDate.optional(),
32235
+ until: isoDate.optional(),
32236
+ all_parents: queryFlag.optional()
32237
+ }).refine((q) => q.scan_limit === void 0 || q.path !== void 0 || isLogFiltered(q), { message: "scan_limit requires path or a filter", path: ["scan_limit"] });
32238
+ var isLogFiltered = (q) => q.author !== void 0 || q.grep !== void 0 || q.since !== void 0 || q.until !== void 0 || q.all_parents === true;
32217
32239
  var RequiredCheckSchema = external_exports.object({
32218
32240
  name: external_exports.string().min(1).max(200),
32219
32241
  subject: external_exports.enum(["head", "candidate"]).default("head"),
@@ -32253,11 +32275,16 @@ var PolicyRulesSchema = external_exports.object({
32253
32275
  });
32254
32276
  var AdoptPolicyRequest = external_exports.object({ rules: PolicyRulesSchema, expected_version: external_exports.number().int().nonnegative(), note: external_exports.string().max(1e3).optional() });
32255
32277
  var ApplySecureDefaultsRequest = external_exports.object({ expected_version: external_exports.number().int().nonnegative() });
32278
+ var MAX_TASK_BLOCKERS = 50;
32279
+ var TaskRef = external_exports.union([external_exports.number().int().positive(), external_exports.string().trim().regex(/^(task_[0-9a-z]{1,64}|#?\d{1,9})$/, "expected a task number (12 or #12) or a task_\u2026 id")]);
32280
+ var blockedBy = external_exports.array(TaskRef).max(MAX_TASK_BLOCKERS);
32256
32281
  var CreateTaskRequest = external_exports.object({
32257
32282
  title: displayLine().min(1).max(300),
32258
32283
  description: external_exports.string().max(65536).default(""),
32259
32284
  acceptance_criteria: external_exports.array(external_exports.string().max(1e3)).max(100).default([]),
32260
- labels: external_exports.array(external_exports.string().max(50)).max(20).default([])
32285
+ labels: external_exports.array(external_exports.string().max(50)).max(20).default([]),
32286
+ /** Tasks in the same repository this one is blocked by. */
32287
+ blocked_by: blockedBy.default([])
32261
32288
  });
32262
32289
  var UpdateTaskRequest = external_exports.object({
32263
32290
  title: displayLine().min(1).max(300).optional(),
@@ -32266,8 +32293,13 @@ var UpdateTaskRequest = external_exports.object({
32266
32293
  labels: external_exports.array(external_exports.string().max(50)).max(20).optional(),
32267
32294
  state: external_exports.enum(["open", "in_progress", "blocked", "done", "cancelled"]).optional(),
32268
32295
  owner_id: external_exports.string().nullable().optional(),
32296
+ /** Replaces the whole set of blockers. Links are not task fields: changing them alone leaves `version` as it is. */
32297
+ blocked_by: blockedBy.optional(),
32269
32298
  version: external_exports.number().int()
32270
32299
  });
32300
+ var AddTaskDependencyRequest = external_exports.object({ blocker: TaskRef });
32301
+ var TaskListQuery = external_exports.object({ ready: external_exports.enum(["true", "false"]).optional().transform((v) => v === void 0 ? void 0 : v === "true") });
32302
+ var TaskNumberParam = external_exports.coerce.number().int().positive().max(999999999);
32271
32303
  var ClaimTaskRequest = external_exports.object({ session_id: external_exports.string().optional() });
32272
32304
  var AddLinkRequest = external_exports.object({ kind: external_exports.enum(["external", "change", "task"]), url: external_exports.url().optional(), target_id: external_exports.string().optional(), title: external_exports.string().max(300).optional() });
32273
32305
  var CreateCommentRequest = external_exports.object({ body: external_exports.string().min(1).max(65536) });
@@ -32438,7 +32470,9 @@ var UpdateOrgRequest = external_exports.object({
32438
32470
  /** Idle timeout of new agent sessions in minutes (ADR-0040). */
32439
32471
  agent_session_idle_minutes: external_exports.number().int().min(15).max(1440).optional(),
32440
32472
  /** Brnch Secure Defaults for new repositories (ADR-0081). Turning it on also requires a second factor for admins. */
32441
- secure_defaults: external_exports.boolean().optional()
32473
+ secure_defaults: external_exports.boolean().optional(),
32474
+ /** The organization's Actions switch (ADR-0009 addendum 2026-10-19): off, no repository of it starts workflow runs. */
32475
+ actions_enabled: external_exports.boolean().optional()
32442
32476
  });
32443
32477
  var OAuthDecisionRequest = external_exports.object({
32444
32478
  approve: external_exports.boolean(),
@@ -33700,6 +33734,16 @@ var CreateConnectionRequest = external_exports.discriminatedUnion("auth", [
33700
33734
  name: Name3,
33701
33735
  credentials: external_exports.object({ api_token: Secret2 })
33702
33736
  }),
33737
+ external_exports.object({
33738
+ // Codex / Claude Code reviews with the key already saved in Settings → AI providers (an OpenAI or Anthropic
33739
+ // provider of this organization). Nothing is copied: the key is read from the provider at every review, so
33740
+ // rotating it there applies here, and deleting the provider stops the reviews.
33741
+ auth: external_exports.literal("ai_provider"),
33742
+ provider: external_exports.enum(["codex", "claude"]),
33743
+ purpose: external_exports.literal("code_review").default("code_review"),
33744
+ name: Name3,
33745
+ config: external_exports.object({ ai_provider_id: external_exports.string().min(1).max(100) })
33746
+ }),
33703
33747
  external_exports.object({
33704
33748
  auth: external_exports.literal("infisical_oidc"),
33705
33749
  provider: external_exports.literal("infisical"),
@@ -33888,18 +33932,26 @@ var UpsertWorkerEnvRequest = external_exports.object({
33888
33932
  });
33889
33933
 
33890
33934
  // packages/protocol/src/db-migrations.ts
33891
- var DB_MIGRATION_FORMATS = ["atlas", "plain", "golang_migrate", "drizzle", "prisma"];
33892
- var DB_ENGINE_KINDS = ["postgres", "vitess"];
33893
33935
  var DB_EXEC_ORDERS = ["linear", "linear_skip", "non_linear"];
33894
33936
  var DB_CREDENTIAL_MODES = ["managed_role", "dsn", "branch_password"];
33937
+ var DB_APPLY_ON = ["merge", "deploy", "manual"];
33938
+ var DB_LINT_LEVELS = ["error", "warning", "off"];
33939
+ var DB_DRIFT_MODES = ["off", "on_run", "daily"];
33940
+ var DbLintPolicySchema = external_exports.object({
33941
+ levels: external_exports.record(external_exports.string().regex(/^[A-Za-z0-9_]{1,40}$/), external_exports.enum(DB_LINT_LEVELS)).refine((r) => Object.keys(r).length <= 50, "at most 50 entries").default({}),
33942
+ nolint: external_exports.enum(["honor", "ignore"]).default("honor"),
33943
+ /** Drift found by the last check fails the deploy's migrations step. */
33944
+ drift_blocks_deploy: external_exports.boolean().default(false)
33945
+ });
33895
33946
  var Ident2 = external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_$]{0,62}$/, "letters, digits and _ only (at most 63)");
33896
33947
  var PsName = external_exports.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/, "a PlanetScale name: letters, digits, '.', '_' and '-'");
33897
33948
  var SourceFields = {
33898
33949
  name: external_exports.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9_-]*$/, "lowercase letters, digits, - and _"),
33899
33950
  /** Directory relative to the repository root ("" = the root). */
33900
33951
  path: external_exports.string().max(300),
33901
- format: external_exports.enum(DB_MIGRATION_FORMATS),
33902
- engine: external_exports.enum(DB_ENGINE_KINDS),
33952
+ /** `wrangler` directories are D1 sources, which come from the Wrangler configuration. */
33953
+ format: external_exports.enum(["atlas", "plain", "golang_migrate", "drizzle", "prisma"]),
33954
+ engine: external_exports.enum(["postgres", "vitess"]),
33903
33955
  /** Branch whose merges create runs; the repository's default branch when omitted. */
33904
33956
  branch: external_exports.string().min(1).max(200).optional(),
33905
33957
  /** PlanetScale connection (purpose `db_migrations`) of the organization or the repository. */
@@ -33921,12 +33973,19 @@ var SourceFields = {
33921
33973
  credential_mode: external_exports.enum(DB_CREDENTIAL_MODES).optional(),
33922
33974
  /** Postgres: the role new objects belong to (`SET ROLE`); `postgres` by default for managed roles, none for a DSN. */
33923
33975
  run_as_role: external_exports.string().regex(/^[a-z_][a-z0-9_]{0,62}$/).nullable().optional(),
33924
- /** `approval`: merges create runs that wait for approval. `manual_only`: runs start only by hand. */
33925
- on_merge: external_exports.enum(["approval", "manual_only"]).default("approval"),
33976
+ /** Deprecated: `apply_on` (`approval` = `merge`, `manual_only` = `manual`). */
33977
+ on_merge: external_exports.enum(["approval", "manual_only"]).optional(),
33978
+ apply_on: external_exports.enum(DB_APPLY_ON).optional(),
33979
+ /** `deploy` and `manual` only: whether a person approves before anything is applied (`merge` always requires it). */
33980
+ require_approval: external_exports.boolean().optional(),
33926
33981
  /** Validate the directory on changes (evidence `migrations/<name>`). */
33927
- change_checks: external_exports.boolean().default(true)
33982
+ change_checks: external_exports.boolean().default(true),
33983
+ lint: DbLintPolicySchema.optional(),
33984
+ drift_mode: external_exports.enum(DB_DRIFT_MODES).optional()
33928
33985
  };
33929
33986
  var CreateDbMigrationSourceRequest = external_exports.object({
33987
+ /** Only PlanetScale sources are created here: D1 sources come from the Wrangler configuration. */
33988
+ provider: external_exports.literal("planetscale").default("planetscale"),
33930
33989
  ...SourceFields,
33931
33990
  /** Postgres `credential_mode: dsn` only: `postgresql://user:password@host:5432/db?sslmode=verify-full`. Write-only. */
33932
33991
  dsn: external_exports.string().min(10).max(2e3).optional()
@@ -33948,7 +34007,11 @@ var UpdateDbMigrationSourceRequest = external_exports.object({
33948
34007
  credential_mode: external_exports.enum(DB_CREDENTIAL_MODES),
33949
34008
  run_as_role: external_exports.string().regex(/^[a-z_][a-z0-9_]{0,62}$/).nullable(),
33950
34009
  on_merge: external_exports.enum(["approval", "manual_only"]),
34010
+ apply_on: external_exports.enum(DB_APPLY_ON),
34011
+ require_approval: external_exports.boolean(),
33951
34012
  change_checks: external_exports.boolean(),
34013
+ lint: DbLintPolicySchema,
34014
+ drift_mode: external_exports.enum(DB_DRIFT_MODES),
33952
34015
  state: external_exports.enum(["active", "disabled"]),
33953
34016
  dsn: external_exports.string().min(10).max(2e3).nullable()
33954
34017
  }).partial().extend({ version: external_exports.number().int().min(1) });
@@ -33956,7 +34019,22 @@ var StartDbMigrationRunRequest = external_exports.object({
33956
34019
  /** Branch, tag or commit to apply; the source's branch when omitted. */
33957
34020
  ref: external_exports.string().min(1).max(200).optional()
33958
34021
  });
33959
- var DecideDbMigrationRunRequest = external_exports.object({ reason: external_exports.string().max(500).optional() });
34022
+ var DecideDbMigrationRunRequest = external_exports.object({
34023
+ reason: external_exports.string().max(500).optional(),
34024
+ /** Approving a run a deploy waits on: redeploy that deploy at the same commit once the run succeeds. */
34025
+ redeploy: external_exports.boolean().optional()
34026
+ });
34027
+
34028
+ // packages/protocol/src/approvals.ts
34029
+ var PutApprovalRuleRequest = external_exports.object({
34030
+ required: external_exports.number().int().min(1).max(10),
34031
+ users: external_exports.array(external_exports.string().min(1).max(100)).max(20).default([]),
34032
+ teams: external_exports.array(external_exports.string().min(1).max(100)).max(10).default([]),
34033
+ separation_of_duties: external_exports.boolean().default(false),
34034
+ /** The rule's version when replacing one (optimistic concurrency); omit to create. */
34035
+ version: external_exports.number().int().positive().optional()
34036
+ });
34037
+ var DecideDeploymentRequest = external_exports.object({ reason: external_exports.string().max(500).optional() });
33960
34038
 
33961
34039
  // packages/protocol/src/buildkite.ts
33962
34040
  var BUILDKITE_TRUST_MODES = ["contributor_controlled", "pipeline_settings", "target_ref_upload"];
@@ -34445,6 +34523,48 @@ var MirrorStatus = external_exports.object({
34445
34523
  timings: external_exports.record(external_exports.string(), external_exports.number())
34446
34524
  });
34447
34525
 
34526
+ // packages/protocol/src/clone-pack.ts
34527
+ var CLONE_PACK_ID = /^cpk_[0-9a-z]{10,40}$/;
34528
+ var Authorization2 = external_exports.string().regex(/^Bearer [A-Za-z0-9+/=._~-]{8,4096}$/);
34529
+ var HttpUrl2 = external_exports.string().max(2048).regex(/^https?:\/\/[^\s@]+$/);
34530
+ var Oid2 = external_exports.string().regex(/^[0-9a-f]{40}$/);
34531
+ var RefName3 = external_exports.string().max(1024).regex(/^refs\/[^\s\0]+$/);
34532
+ var PackRef = external_exports.object({ name: RefName3, oid: Oid2 });
34533
+ var PackJob = external_exports.object({
34534
+ /** One build; one container per id. */
34535
+ id: external_exports.string().regex(CLONE_PACK_ID),
34536
+ /**
34537
+ * The platform's endpoint for this build, `<base>/git` its git remote (storage behind the storage gate, private refs
34538
+ * hidden) and `<base>/pack/:n`, `<base>/idx`, `<base>/complete` its upload; `authorization` holds for all of them and
34539
+ * for nothing else.
34540
+ */
34541
+ base_url: HttpUrl2,
34542
+ authorization: Authorization2,
34543
+ default_branch: external_exports.string().regex(/^[A-Za-z0-9._\/-]{1,200}$/).refine((b) => !b.includes(".."), "invalid branch"),
34544
+ /** The last ready pack, downloaded from `<base>/previous/pack` and `/previous/idx`, and the refs it holds. */
34545
+ previous: external_exports.object({ pack_sha1: Oid2, refs: external_exports.array(PackRef).max(2e4) }).nullable(),
34546
+ limits: external_exports.object({ max_pack_bytes: external_exports.number().int().positive(), part_bytes: external_exports.number().int().positive() }),
34547
+ expires_at: external_exports.iso.datetime()
34548
+ });
34549
+ var PackComplete = external_exports.object({
34550
+ pack_sha1: Oid2,
34551
+ pack_bytes: external_exports.number().int().positive(),
34552
+ parts: external_exports.number().int().positive().max(1e4),
34553
+ objects: external_exports.number().int().nonnegative(),
34554
+ refs: external_exports.array(PackRef).max(2e4)
34555
+ });
34556
+ var PackStatus = external_exports.object({
34557
+ id: external_exports.string(),
34558
+ state: external_exports.enum(["running", "completed", "failed"]),
34559
+ phase: external_exports.string().max(40),
34560
+ reason: external_exports.enum(["fetch_failed", "too_large", "upload_failed", "error"]).optional(),
34561
+ message: external_exports.string().max(4e3).nullable(),
34562
+ pack_bytes: external_exports.number().nullable(),
34563
+ objects: external_exports.number().nullable(),
34564
+ updated_at: external_exports.string(),
34565
+ timings: external_exports.record(external_exports.string(), external_exports.number())
34566
+ });
34567
+
34448
34568
  // packages/protocol/src/repo-mirrors.ts
34449
34569
  var StopMirrorRequest = external_exports.object({ confirm: external_exports.string().min(3).max(300), expected_version: external_exports.number().int().positive().optional() }).strict();
34450
34570
  var ResetMirrorRefsRequest = external_exports.object({ refs: external_exports.array(external_exports.string().min(1).max(1024)).min(1).max(100) }).strict();
@@ -35973,6 +36093,7 @@ function orgsGroup(t) {
35973
36093
  create: (body, o) => t.json({ method: "POST", path: "/v1/orgs", json: validate2(CreateOrgRequest, body, "CreateOrgRequest"), options: o }),
35974
36094
  list: (q = {}, o) => t.json({ method: "GET", path: "/v1/orgs", query: { ...q }, options: o }),
35975
36095
  get: (org, o) => t.json({ method: "GET", path: path2`/v1/orgs/${org}`, options: o }),
36096
+ update: (org, body, o) => t.json({ method: "PATCH", path: path2`/v1/orgs/${org}`, json: validate2(UpdateOrgRequest, body, "UpdateOrgRequest"), options: o }),
35976
36097
  members: {
35977
36098
  list: (org, q = {}, o) => t.json({ method: "GET", path: path2`/v1/orgs/${org}/members`, query: { ...q }, options: o }),
35978
36099
  update: (org, userId, body, o) => t.json({
@@ -36264,13 +36385,25 @@ function reposGroup(t) {
36264
36385
  /** Raw file bytes; never interpret them as HTML. */
36265
36386
  raw: (repo, q, o) => t.bytes({ method: "GET", path: path2`/v1/repos/${repo}/raw`, query: { ...q }, options: o }),
36266
36387
  /** First-parent history of `ref`; with `path`, only commits that changed it — each page examines at most
36267
- * `scan_limit` commits and reports how many in `path_scan` (`complete` is false until the walk reaches the root). */
36388
+ * `scan_limit` commits and reports how many in `path_scan` (`complete` is false until the walk reaches the root).
36389
+ * With log filters (`author`, `grep`, `since`, `until`, `all_parents`; substring matches), the same in `log_scan`. */
36268
36390
  commits: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/commits`, query: { ...q }, options: o }),
36269
36391
  /** Write text files to a branch as one commit, through the same checks as a push (ADR-0078). Retry with the same
36270
36392
  * `idempotencyKey`: a retry after an uncertain outcome never commits twice. */
36271
36393
  commitFiles: (repo, body, o) => t.json({ method: "POST", path: path2`/v1/repos/${repo}/commits`, json: validate2(CommitFilesRequest, body, "CommitFilesRequest"), options: o }),
36272
36394
  commit: (repo, oid, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/commits/${oid}`, options: o }),
36395
+ /** The changes that brought a commit onto each ref it landed on (M6-SCL-01). */
36396
+ commitChange: (repo, oid, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/commits/${oid}/change`, options: o }),
36397
+ /** `mode: "two-dot"` diffs the base tree against the head tree instead of against their merge base. */
36273
36398
  compare: (repo, q, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/compare`, query: { ...q }, options: o }),
36399
+ /** The commits in `base..head` (newest first, at most `limit`, max 250) with ahead/behind counts (M6-SCL-02). */
36400
+ compareCommits: (repo, q, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/compare/commits`, query: { ...q }, options: o }),
36401
+ /** Every best common ancestor of two refs or commits (`git merge-base --all`). */
36402
+ mergeBase: (repo, q, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/merge-base`, query: { ...q }, options: o }),
36403
+ /** Whether `ancestor` is in `descendant`'s history; `is_ancestor` is null when the bounded walk cannot decide. */
36404
+ isAncestor: (repo, q, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/is-ancestor`, query: { ...q }, options: o }),
36405
+ /** Branches and tags whose history contains a commit (`git for-each-ref --contains`). */
36406
+ refsContaining: (repo, commit, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/commits/${commit}/refs`, options: o }),
36274
36407
  diff: (repo, q, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/diff`, query: { ...q }, options: o }),
36275
36408
  pushes: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/pushes`, query: { ...q }, options: o }),
36276
36409
  search: (repo, q, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/search`, query: { ...q }, options: o }),
@@ -36299,6 +36432,8 @@ function reposGroup(t) {
36299
36432
  grants: {
36300
36433
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/grants`, query: { ...q }, options: o }),
36301
36434
  create: (repo, body, o) => t.json({ method: "POST", path: path2`/v1/repos/${repo}/grants`, json: validate2(CreateGrantRequest, body, "CreateGrantRequest"), options: o }),
36435
+ /** Replace the capabilities the grant carries beyond its role (ADR-0088), e.g. `["migrations:approve"]`. */
36436
+ update: (repo, grant, body, o) => t.json({ method: "PATCH", path: path2`/v1/repos/${repo}/grants/${grant}`, json: validate2(UpdateGrantRequest, body, "UpdateGrantRequest"), options: o }),
36302
36437
  delete: (repo, grant, o) => t.void({ method: "DELETE", path: path2`/v1/repos/${repo}/grants/${grant}`, options: o })
36303
36438
  }
36304
36439
  };
@@ -36501,9 +36636,13 @@ function delegationsGroup(t) {
36501
36636
  function tasksGroup(t) {
36502
36637
  return {
36503
36638
  create: (repo, body, o) => t.json({ method: "POST", path: path2`/v1/repos/${repo}/tasks`, json: validate2(CreateTaskRequest, body, "CreateTaskRequest"), options: o }),
36504
- /** `label` (name or id) and `assignee` (handle or user id) filter the list. */
36639
+ /**
36640
+ * `label` (name or id) and `assignee` (handle or user id) filter the list. `ready: true` keeps only the frontier:
36641
+ * open tasks whose blockers are all done or cancelled (`false`: every other task).
36642
+ */
36505
36643
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/tasks`, query: { ...q }, options: o }),
36506
36644
  get: (task, o) => t.json({ method: "GET", path: path2`/v1/tasks/${task}`, options: o }),
36645
+ byNumber: (repo, number4, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/tasks/by-number/${number4}`, options: o }),
36507
36646
  /** `version` is required: a stale version fails with VERSION_CONFLICT instead of overwriting someone else's edit. */
36508
36647
  update: (task, body, o) => t.json({ method: "PATCH", path: path2`/v1/tasks/${task}`, json: validate2(UpdateTaskRequest, body, "UpdateTaskRequest"), options: o }),
36509
36648
  /** Advisory claim; CONFLICT when someone else holds it. */
@@ -36513,28 +36652,50 @@ function tasksGroup(t) {
36513
36652
  create: (task, body, o) => t.json({ method: "POST", path: path2`/v1/tasks/${task}/comments`, json: validate2(CreateCommentRequest, body, "CreateCommentRequest"), options: o }),
36514
36653
  list: (task, q = {}, o) => t.json({ method: "GET", path: path2`/v1/tasks/${task}/comments`, query: { ...q }, options: o })
36515
36654
  },
36655
+ /** "Blocked by" links within the task's repository (ADR-0089). A blocker is a task number or `task_…` id. */
36656
+ dependencies: {
36657
+ list: (task, o) => t.json({ method: "GET", path: path2`/v1/tasks/${task}/dependencies`, options: o }),
36658
+ /** Idempotent: adding an existing link changes nothing. CYCLE_DETECTED if the blocker already waits on this task. */
36659
+ add: (task, body, o) => t.json({ method: "POST", path: path2`/v1/tasks/${task}/dependencies`, json: validate2(AddTaskDependencyRequest, body, "AddTaskDependencyRequest"), options: o }),
36660
+ remove: (task, blocker, o) => t.void({ method: "DELETE", path: path2`/v1/tasks/${task}/dependencies/${String(blocker)}`, options: o })
36661
+ },
36516
36662
  addLink: (task, body, o) => t.json({ method: "POST", path: path2`/v1/tasks/${task}/links`, json: validate2(AddLinkRequest, body, "AddLinkRequest"), options: o })
36517
36663
  };
36518
36664
  }
36519
36665
 
36520
36666
  // packages/sdk/src/groups/db-migrations.ts
36521
36667
  function dbMigrationsGroup(t) {
36522
- const decide = (decision) => (id2, reason, o) => t.json({ method: "POST", path: path2`/v1/db-migration-runs/${id2}/${decision}`, json: validate2(DecideDbMigrationRunRequest, reason ? { reason } : {}, "DecideDbMigrationRunRequest"), options: o });
36668
+ const decide = (decision) => (id2, reason, o) => {
36669
+ const body = typeof reason === "string" ? { reason } : { ...reason?.reason ? { reason: reason.reason } : {}, ...reason?.redeploy ? { redeploy: true } : {} };
36670
+ return t.json({ method: "POST", path: path2`/v1/db-migration-runs/${id2}/${decision}`, json: validate2(DecideDbMigrationRunRequest, body, "DecideDbMigrationRunRequest"), options: o });
36671
+ };
36523
36672
  return {
36524
36673
  sources: {
36525
36674
  list: (repo, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/sources`, options: o }),
36526
36675
  create: (repo, body, o) => t.json({ method: "POST", path: path2`/v1/repos/${repo}/db-migrations/sources`, json: validate2(CreateDbMigrationSourceRequest, body, "CreateDbMigrationSourceRequest"), options: o }),
36527
36676
  get: (id2, o) => t.json({ method: "GET", path: path2`/v1/db-migration-sources/${id2}`, options: o }),
36528
36677
  update: (id2, body, o) => t.json({ method: "PATCH", path: path2`/v1/db-migration-sources/${id2}`, json: validate2(UpdateDbMigrationSourceRequest, body, "UpdateDbMigrationSourceRequest"), options: o }),
36529
- delete: (id2, o) => t.void({ method: "DELETE", path: path2`/v1/db-migration-sources/${id2}`, options: o })
36678
+ delete: (id2, o) => t.void({ method: "DELETE", path: path2`/v1/db-migration-sources/${id2}`, options: o }),
36679
+ /** Who may approve the source's runs now (ADR-0088). */
36680
+ approvers: (id2, o) => t.json({ method: "GET", path: path2`/v1/db-migration-sources/${id2}/approvers`, options: o }),
36681
+ /** Protect the database: every run waits for these approvals, and it can no longer apply without approval. */
36682
+ protect: (id2, body, o) => t.json({ method: "PUT", path: path2`/v1/db-migration-sources/${id2}/approval-rule`, json: validate2(PutApprovalRuleRequest, body, "PutApprovalRuleRequest"), options: o }),
36683
+ unprotect: (id2, o) => t.json({ method: "DELETE", path: path2`/v1/db-migration-sources/${id2}/approval-rule`, options: o })
36530
36684
  },
36531
36685
  /** Migration directories found in the repository tree; `configured` marks those a source already covers. */
36532
36686
  suggestions: (repo, ref, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/suggestions`, query: ref ? { ref } : {}, options: o }),
36533
36687
  /** What applying the source at `ref` would do. `live` reads the database now instead of the last snapshot. */
36534
36688
  plan: (sourceId2, q = {}, o) => t.json({ method: "GET", path: path2`/v1/db-migration-sources/${sourceId2}/plan`, query: { ref: q.ref, live: q.live ? "true" : void 0 }, options: o }),
36689
+ /** The schema the source's migrations produce at `ref` (the source's branch by default), from replaying them. */
36690
+ schema: (sourceId2, ref, o) => t.json({ method: "GET", path: path2`/v1/db-migration-sources/${sourceId2}/schema`, query: ref ? { ref } : {}, options: o }),
36691
+ /** Compares the database with what its applied migrations produce, now, and stores the result (admins). */
36692
+ checkDrift: (sourceId2, o) => t.json({ method: "POST", path: path2`/v1/db-migration-sources/${sourceId2}/drift`, options: o }),
36535
36693
  /** Reads the applied versions from the database again and returns the plan at the source's branch. */
36536
36694
  refresh: (sourceId2, o) => t.json({ method: "POST", path: path2`/v1/db-migration-sources/${sourceId2}/refresh`, options: o }),
36537
- /** A manual run: the caller's request is the approval. `run` is null when nothing is pending. */
36695
+ /**
36696
+ * A manual run (`migrations:manage`). The request is the approval when the caller also holds `migrations:approve` and
36697
+ * the source is not protected; otherwise the run waits for approval. `run` is null when nothing is pending.
36698
+ */
36538
36699
  startRun: (sourceId2, body = {}, o) => t.json({ method: "POST", path: path2`/v1/db-migration-sources/${sourceId2}/runs`, json: validate2(StartDbMigrationRunRequest, body, "StartDbMigrationRunRequest"), options: o }),
36539
36700
  runs: {
36540
36701
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/db-migrations/runs`, query: { ...q }, options: o }),
@@ -36544,12 +36705,13 @@ function dbMigrationsGroup(t) {
36544
36705
  reject: decide("reject"),
36545
36706
  retry: decide("retry"),
36546
36707
  cancel: decide("cancel"),
36547
- /** The plan of every source a change touches, with the files the change adds or edits marked. */
36708
+ /** The plan of every source a change touches: the files the change adds (with statements), lint, schema effect. */
36548
36709
  changePlans: (changeId, o) => t.json({ method: "GET", path: path2`/v1/changes/${changeId}/db-migrations`, options: o })
36549
36710
  };
36550
36711
  }
36551
36712
 
36552
36713
  // packages/sdk/src/groups/deploy.ts
36714
+ var isCallOptions = (x) => "signal" in x || "idempotencyKey" in x;
36553
36715
  function deployGroup(t) {
36554
36716
  return {
36555
36717
  stepKinds: (o) => t.json({ method: "GET", path: "/v1/deploy/step-kinds", options: o }),
@@ -36559,19 +36721,36 @@ function deployGroup(t) {
36559
36721
  suggestions: (repo, ref, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/deploy-suggestions`, query: ref ? { ref } : {}, options: o }),
36560
36722
  resources: (repo, connection, type, parent, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/deploy-connections/${connection}/resources`, query: { type, ...parent ? { parent } : {} }, options: o }),
36561
36723
  targets: {
36562
- list: (repo, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/deploy-targets`, options: o }),
36724
+ /**
36725
+ * A page of the repository's targets (50 by default); `name` finds one target by name. Callers from before
36726
+ * pagination passed call options second: those are still taken as call options.
36727
+ */
36728
+ list: (repo, q = {}, o) => {
36729
+ if (isCallOptions(q)) [q, o] = [{}, q];
36730
+ return t.json({ method: "GET", path: path2`/v1/repos/${repo}/deploy-targets`, query: { ...q }, options: o });
36731
+ },
36563
36732
  create: (repo, body, o) => t.json({ method: "POST", path: path2`/v1/repos/${repo}/deploy-targets`, json: validate2(CreateDeployTargetRequest, body, "CreateDeployTargetRequest"), options: o }),
36564
36733
  get: (id2, o) => t.json({ method: "GET", path: path2`/v1/deploy-targets/${id2}`, options: o }),
36565
36734
  update: (id2, body, o) => t.json({ method: "PATCH", path: path2`/v1/deploy-targets/${id2}`, json: validate2(UpdateDeployTargetRequest, body, "UpdateDeployTargetRequest"), options: o }),
36566
36735
  remove: (id2, o) => t.json({ method: "DELETE", path: path2`/v1/deploy-targets/${id2}`, options: o }),
36567
36736
  /** Deploy the branch head again (or a commit this target deployed before). */
36568
36737
  deploy: (id2, body = {}, o) => t.json({ method: "POST", path: path2`/v1/deploy-targets/${id2}/deploy`, json: validate2(RedeployRequest, body, "RedeployRequest"), options: o }),
36569
- rollback: (id2, body, o) => t.json({ method: "POST", path: path2`/v1/deploy-targets/${id2}/rollback`, json: validate2(RollbackRequest, body, "RollbackRequest"), options: o })
36738
+ rollback: (id2, body, o) => t.json({ method: "POST", path: path2`/v1/deploy-targets/${id2}/rollback`, json: validate2(RollbackRequest, body, "RollbackRequest"), options: o }),
36739
+ /** Who may approve the target's production deploys now (ADR-0088). */
36740
+ approvers: (id2, o) => t.json({ method: "GET", path: path2`/v1/deploy-targets/${id2}/approvers`, options: o }),
36741
+ /** Protect the target: production deploys and rollbacks wait for these approvals. Pass `version` to replace a rule. */
36742
+ protect: (id2, body, o) => t.json({ method: "PUT", path: path2`/v1/deploy-targets/${id2}/approval-rule`, json: validate2(PutApprovalRuleRequest, body, "PutApprovalRuleRequest"), options: o }),
36743
+ /** Remove the rule; deploys waiting for it go on. */
36744
+ unprotect: (id2, o) => t.json({ method: "DELETE", path: path2`/v1/deploy-targets/${id2}/approval-rule`, options: o })
36570
36745
  },
36571
36746
  deployments: {
36572
36747
  list: (repo, q = {}, o) => t.json({ method: "GET", path: path2`/v1/repos/${repo}/deployments`, query: { ...q }, options: o }),
36573
36748
  get: (id2, o) => t.json({ method: "GET", path: path2`/v1/deployments/${id2}`, options: o }),
36574
36749
  cancel: (id2, o) => t.json({ method: "POST", path: path2`/v1/deployments/${id2}/cancel`, options: o }),
36750
+ /** Approve a deploy of a protected target (people only; the last approval needed starts it). */
36751
+ approve: (id2, o) => t.json({ method: "POST", path: path2`/v1/deployments/${id2}/approve`, json: {}, options: o }),
36752
+ /** Reject it: the deployment is cancelled. */
36753
+ reject: (id2, reason, o) => t.json({ method: "POST", path: path2`/v1/deployments/${id2}/reject`, json: validate2(DecideDeploymentRequest, reason ? { reason } : {}, "DecideDeploymentRequest"), options: o }),
36575
36754
  ofChange: (change, o) => t.json({ method: "GET", path: path2`/v1/changes/${change}/deployments`, options: o })
36576
36755
  }
36577
36756
  };
@@ -37050,7 +37229,7 @@ function invalidAgentToolFlag(flag) {
37050
37229
  }
37051
37230
 
37052
37231
  // packages/cli/package.json
37053
- var version2 = "0.18.3";
37232
+ var version2 = "0.19.1";
37054
37233
 
37055
37234
  // packages/cli/src/brand.ts
37056
37235
  var BRAND = {
@@ -38787,18 +38966,13 @@ async function gitConfigPath(deps) {
38787
38966
  }
38788
38967
 
38789
38968
  // packages/cli/src/work-ref.ts
38790
- var MAX_PAGES = 20;
38791
- var PAGE = 100;
38792
38969
  async function issueByNumber(client, repo, n) {
38793
- let cursor2;
38794
- for (let i = 0; i < MAX_PAGES; i++) {
38795
- const page = await client.tasks.list(repo, { limit: PAGE, ...cursor2 ? { cursor: cursor2 } : {} });
38796
- const hit = page.items.find((t) => t.number === n);
38797
- if (hit) return hit.id;
38798
- if (!page.next_cursor) return null;
38799
- cursor2 = page.next_cursor;
38970
+ try {
38971
+ return (await client.tasks.byNumber(repo, n)).id;
38972
+ } catch (e) {
38973
+ if (isBrnchApiError(e) && e.code === "NOT_FOUND") return null;
38974
+ throw e;
38800
38975
  }
38801
- return null;
38802
38976
  }
38803
38977
  async function changeByNumber(client, repo, n) {
38804
38978
  try {
@@ -38824,6 +38998,21 @@ async function resolveChangeId(spec, client, repo) {
38824
38998
  if (!id2) throw notFound(`change #${n}`, repo.spec());
38825
38999
  return id2;
38826
39000
  }
39001
+ var TASK_ARG = "task number (12 or #12) in --repo, or a task_\u2026 id";
39002
+ async function resolveTaskId(spec, client, repo) {
39003
+ const text2 = spec.trim();
39004
+ const rest = text2.startsWith("issue:") ? text2.slice("issue:".length) : text2;
39005
+ if (rest.startsWith("task_")) return rest;
39006
+ const digits = /^#?(\d{1,9})$/.exec(rest)?.[1];
39007
+ if (!digits) {
39008
+ if (rest.startsWith("chg_") || rest.startsWith("change:")) throw usage(`${spec} is a change, not a task`);
39009
+ throw usage(`cannot read task "${spec}": expected a number (12 or #12) or a task_\u2026 id`);
39010
+ }
39011
+ const n = Number(digits);
39012
+ const id2 = await issueByNumber(client, await repo.id(), n);
39013
+ if (!id2) throw notFound(`task #${n}`, repo.spec());
39014
+ return id2;
39015
+ }
38827
39016
  async function resolveWorkRef(spec, client, repoId) {
38828
39017
  const m = /^(issue|change):(.+)$/.exec(spec.trim());
38829
39018
  const kind = m?.[1] ?? null;
@@ -39449,6 +39638,9 @@ async function createContext(deps, globals, state2) {
39449
39638
  function changeId(spec, pref = "agent") {
39450
39639
  return resolveChangeId(spec, client(pref), { id: () => repoId(void 0, pref), spec: () => repoSpec() });
39451
39640
  }
39641
+ function taskId(spec, pref = "agent") {
39642
+ return resolveTaskId(spec, client(pref), { id: () => repoId(void 0, pref), spec: () => repoSpec() });
39643
+ }
39452
39644
  function org() {
39453
39645
  const o = targetOrg ?? storedCred?.account?.default_org;
39454
39646
  if (!o) {
@@ -39519,6 +39711,7 @@ async function createContext(deps, globals, state2) {
39519
39711
  repoSpec,
39520
39712
  repoId,
39521
39713
  changeId,
39714
+ taskId,
39522
39715
  org,
39523
39716
  sessionId,
39524
39717
  info,
@@ -39638,6 +39831,15 @@ function cell(v) {
39638
39831
  if (typeof v === "number" || typeof v === "boolean") return String(v);
39639
39832
  return JSON.stringify(v);
39640
39833
  }
39834
+ function actorLabel(a) {
39835
+ const person = a.handle ? `@${a.handle}` : a.user_name ?? null;
39836
+ if (a.type === "human") return person ?? a.display_name ?? `${a.type} ${a.id}`;
39837
+ if (a.type === "agent") {
39838
+ const name = a.display_name ?? `agent ${a.id}`;
39839
+ return person ? `${name} (agent for ${person})` : name;
39840
+ }
39841
+ return a.display_name ?? `${a.type} ${a.id}`;
39842
+ }
39641
39843
  var tildePath = (path3, home2) => home2 && path3.startsWith(`${home2}/`) ? `~${path3.slice(home2.length)}` : path3;
39642
39844
  function kv(obj3, keys) {
39643
39845
  const o = obj3 ?? {};
@@ -39710,6 +39912,7 @@ function instructionsSnippet(tool) {
39710
39912
  ` - Commit only what you wrote: stage your paths (\`git add <path>\u2026\`), not the person's uncommitted edits (\`git add -A\` takes them too). Do not squash other sessions' or the person's commits into yours.`,
39711
39913
  ` - Commit authors and trailers (\`Agent-Session:\`, \`Co-Authored-By:\`) are claims and attribute nothing; you do not need them for attribution.`,
39712
39914
  ` - Hooks wired: \`${cli} hooks status\`. Codex runs them only after the person trusts them once (\`${cli} hooks trust\` in their terminal).`,
39915
+ `- Ask Brnch which change brought a commit instead of searching \`git log\` or guessing from commit-message words: \`${cli} repo commit-change <commit>\` (MCP: \`commit_change_get\`), for every merge strategy. The change's description and review say why.`,
39713
39916
  `- Record key decisions as you make them: \`${cli} decision "\u2026" --reason "\u2026" --file <path>\`. Blame shows them with the lines they explain.`,
39714
39917
  `- Other agents may be working here at the same time. Run \`${cli} status\` before you start to see who is working on what.`,
39715
39918
  `- Claim ADR numbers, migration numbers, release versions and exclusive operations before you use them, and use exactly the key you get: \`${cli} claim adr:<n> --next\` (also \`migration:<n>\`, \`version:<pkg>:<x.y.z>\`, \`lock:<operation>\`). If a key is taken, take the next; never reuse another session's key.`,
@@ -39737,7 +39940,7 @@ ${snippet}
39737
39940
 
39738
39941
  // packages/cli/src/commands/session.ts
39739
39942
  var renderSession = (s) => kv(
39740
- { ...s, agent: `${s.agent.name} (${s.agent.id})`, sponsor: `${s.sponsor.type} ${s.sponsor.id}` },
39943
+ { ...s, agent: `${s.agent.name} (${s.agent.id})`, sponsor: actorLabel(s.sponsor) },
39741
39944
  ["id", "title", "agent", "state", "presence", "attention", "repository_id", "task_id", "claimed_task_id", "sponsor", "scopes", "open_blockers", "last_seen_at", "expires_at", "max_expires_at", "end_reason"]
39742
39945
  );
39743
39946
  var renderStarted = (s) => `${renderSession(s.session)}
@@ -41242,7 +41445,7 @@ function sessionLines(s, now, you) {
41242
41445
  ...s.paths.length ? [` touching: ${pathList(s.paths)}`] : []
41243
41446
  ];
41244
41447
  }
41245
- var claimLine = (c8) => ` ${c8.key} ${c8.holder.display_name ?? c8.holder.id}${c8.session_id ? ` (${c8.session_id})` : ""}${c8.change ? ` \xB7 change #${c8.change.number}` : ""}${c8.note ? ` \xB7 ${c8.note}` : ""}`;
41448
+ var claimLine = (c8) => ` ${c8.key} ${actorLabel(c8.holder)}${c8.session_id ? ` (${c8.session_id})` : ""}${c8.change ? ` \xB7 change #${c8.change.number}` : ""}${c8.note ? ` \xB7 ${c8.note}` : ""}`;
41246
41449
  function renderStatus(st, now = Date.now()) {
41247
41450
  const others = st.sessions.filter((s) => s.id !== st.you);
41248
41451
  const out = [others.length ? `${others.length} other agent session${others.length === 1 ? "" : "s"} working in this repository.` : "No other agent sessions are working in this repository."];
@@ -41797,9 +42000,9 @@ async function runHook(deps, state2, tool, eventArg) {
41797
42000
  let meta3 = await readMeta(deps.env, id2);
41798
42001
  if (!meta3) {
41799
42002
  const baseUrl = await baseUrlOf(deps);
41800
- const resolved = await resolveRepo(deps, info.cwd ?? deps.cwd, baseUrl);
41801
- if (!resolved) return;
41802
- meta3 = { client_key: clientKey, tool: t, model: null, repo: resolved.repo, repo_root: resolved.root, base_url: baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: false };
42003
+ const resolved2 = await resolveRepo(deps, info.cwd ?? deps.cwd, baseUrl);
42004
+ if (!resolved2) return;
42005
+ meta3 = { client_key: clientKey, tool: t, model: null, repo: resolved2.repo, repo_root: resolved2.root, base_url: baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: false };
41803
42006
  }
41804
42007
  if (info.model && !meta3.model) meta3.model = redactText(info.model).text.slice(0, 200);
41805
42008
  if (!meta3.model && t === "claude-code" && typeof payload.transcript_path === "string" && /^(Stop|SessionEnd)$/.test(info.event)) meta3.model = await modelFromTranscript(deps, payload.transcript_path);
@@ -42003,22 +42206,22 @@ function registerCapture(program2, rt) {
42003
42206
  act(rt, async (ctx, o, words) => {
42004
42207
  const summary = words.join(" ").trim();
42005
42208
  if (!summary) throw usage("decision text is required");
42006
- const resolved = await resolveRepo(ctx.deps, ctx.deps.cwd, ctx.baseUrl);
42007
- if (!resolved) throw usage(`this directory is not a repository hosted on ${ctx.baseUrl}`);
42209
+ const resolved2 = await resolveRepo(ctx.deps, ctx.deps.cwd, ctx.baseUrl);
42210
+ if (!resolved2) throw usage(`this directory is not a repository hosted on ${ctx.baseUrl}`);
42008
42211
  const now = Date.now();
42009
- const live = (await listSpool(ctx.deps.env)).filter((s) => s.meta.repo_root === resolved.root && !s.meta.ended && now - Date.parse(s.meta.last_event_at) < 12 * 36e5).sort((a, b) => b.meta.last_event_at.localeCompare(a.meta.last_event_at))[0];
42212
+ const live = (await listSpool(ctx.deps.env)).filter((s) => s.meta.repo_root === resolved2.root && !s.meta.ended && now - Date.parse(s.meta.last_event_at) < 12 * 36e5).sort((a, b) => b.meta.last_event_at.localeCompare(a.meta.last_event_at))[0];
42010
42213
  let id2;
42011
42214
  let meta3;
42012
42215
  if (live) {
42013
42216
  id2 = live.id;
42014
42217
  meta3 = live.meta;
42015
42218
  } else {
42016
- const key = `manual:${spoolId(resolved.root)}:${new Date(now).toISOString().slice(0, 10)}`;
42219
+ const key = `manual:${spoolId(resolved2.root)}:${new Date(now).toISOString().slice(0, 10)}`;
42017
42220
  id2 = spoolId(key);
42018
- meta3 = await readMeta(ctx.deps.env, id2) ?? { client_key: key, tool: "manual", model: null, repo: resolved.repo, repo_root: resolved.root, base_url: ctx.baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: false };
42221
+ meta3 = await readMeta(ctx.deps.env, id2) ?? { client_key: key, tool: "manual", model: null, repo: resolved2.repo, repo_root: resolved2.root, base_url: ctx.baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: false };
42019
42222
  }
42020
42223
  const paths = (o.file ?? []).map((f) => {
42021
- const rel = relative2(canonical(resolved.root), canonical(isAbsolute6(f) ? f : resolve4(ctx.deps.cwd, f)));
42224
+ const rel = relative2(canonical(resolved2.root), canonical(isAbsolute6(f) ? f : resolve4(ctx.deps.cwd, f)));
42022
42225
  if (!rel || rel.startsWith("..") || isAbsolute6(rel)) throw usage(`${f} is outside the repository`);
42023
42226
  return rel.split(sep2).join("/");
42024
42227
  });
@@ -42062,22 +42265,22 @@ ${report3.errors.join("\n")}` : ""}`);
42062
42265
  rt.state.noUpdateHint = true;
42063
42266
  const [cmd, ...args] = argv;
42064
42267
  if (!cmd) throw usage(`usage: ${BRAND.cli} capture wrap -- <command> [args\u2026]`);
42065
- const resolved = await resolveRepo(ctx.deps, ctx.deps.cwd, ctx.baseUrl);
42268
+ const resolved2 = await resolveRepo(ctx.deps, ctx.deps.cwd, ctx.baseUrl);
42066
42269
  const cfg = await readCaptureConfig(ctx.deps.env);
42067
- const before = resolved && cfg.enabled ? await snapshotWorktree(ctx.deps, resolved.root) : null;
42068
- if (!resolved) ctx.info(`not a repository hosted on ${ctx.baseUrl}: running without capture`);
42270
+ const before = resolved2 && cfg.enabled ? await snapshotWorktree(ctx.deps, resolved2.root) : null;
42271
+ if (!resolved2) ctx.info(`not a repository hosted on ${ctx.baseUrl}: running without capture`);
42069
42272
  const code = await runInherit(cmd, args, ctx.deps.cwd, ctx.deps.env);
42070
42273
  ctx.setExit(code);
42071
- if (!resolved || !before) return;
42072
- const after = await snapshotWorktree(ctx.deps, resolved.root);
42274
+ if (!resolved2 || !before) return;
42275
+ const after = await snapshotWorktree(ctx.deps, resolved2.root);
42073
42276
  if (!after) return;
42074
- const diff = await ctx.deps.sys.run("git", ["-c", "core.hooksPath=/dev/null", "diff", "--no-color", "--no-renames", "--no-ext-diff", "-U0", before, after], { cwd: resolved.root, timeoutMs: 12e4 });
42277
+ const diff = await ctx.deps.sys.run("git", ["-c", "core.hooksPath=/dev/null", "diff", "--no-color", "--no-renames", "--no-ext-diff", "-U0", before, after], { cwd: resolved2.root, timeoutMs: 12e4 });
42075
42278
  const label2 = (o.tool ?? cmd.split(/[\\/]/).pop() ?? "command").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40);
42076
42279
  const now = Date.now();
42077
42280
  const key = `wrapped:${label2}:${randomBytes4(8).toString("hex")}`;
42078
42281
  const id2 = spoolId(key);
42079
- const meta3 = { client_key: key, tool: "wrapped", model: o.model ? redactText(o.model).text.slice(0, 200) : null, repo: resolved.repo, repo_root: resolved.root, base_url: ctx.baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: true };
42080
- const edits = await editsFromDiff(diff.stdout, { repoRoot: resolved.root, config: cfg, readFile: (p) => ctx.deps.readFile(p).catch(() => null) }, label2);
42282
+ const meta3 = { client_key: key, tool: "wrapped", model: o.model ? redactText(o.model).text.slice(0, 200) : null, repo: resolved2.repo, repo_root: resolved2.root, base_url: ctx.baseUrl, started_at: new Date(now).toISOString(), last_event_at: new Date(now).toISOString(), ended: true };
42283
+ const edits = await editsFromDiff(diff.stdout, { repoRoot: resolved2.root, config: cfg, readFile: (p) => ctx.deps.readFile(p).catch(() => null) }, label2);
42081
42284
  await writeMeta(ctx.deps.env, id2, meta3);
42082
42285
  await appendEvents(ctx.deps.env, id2, [{ type: "capture.started", payload: { source: label2 } }, ...edits, { type: "capture.ended", payload: { reason: `exit ${code}` } }], now);
42083
42286
  startFlush(ctx.deps);
@@ -45918,12 +46121,35 @@ function seedJson(p) {
45918
46121
  }
45919
46122
 
45920
46123
  // packages/cli/src/commands/task.ts
45921
- var renderTask = (t) => kv({ ...t, claimed_by: t.claim ? `${t.claim.actor.type} ${t.claim.actor.id}` : null }, ["id", "number", "title", "state", "owner_id", "claimed_by", "labels", "updated_at", "version"]) + (t.description ? `
46124
+ var BLOCKER_ARG = "the task it is blocked by: number (12 or #12) or task_\u2026 id";
46125
+ var resolved = (t) => t.state === "done" || t.state === "cancelled";
46126
+ var summaries = (list3) => list3.map((t) => `#${t.number} ${t.title} (${t.state})`).join("\n ") || "(none)";
46127
+ function renderDependencies(d) {
46128
+ const more = d.blocks_count > d.blocks.length ? `
46129
+ \u2026 and ${d.blocks_count - d.blocks.length} more` : "";
46130
+ return `Ready: ${d.is_ready ? "yes" : "no"}
46131
+ Blocked by:
46132
+ ${summaries(d.blocked_by)}
46133
+ Blocks:
46134
+ ${summaries(d.blocks)}${more}`;
46135
+ }
46136
+ var renderTask = (t) => kv({ ...t, claimed_by: t.claim ? actorLabel(t.claim.actor) : null }, ["id", "number", "title", "state", "owner_id", "claimed_by", "labels", "updated_at", "version"]) + (t.blocked_by?.length || t.blocks_count ? `
46137
+
46138
+ ${renderDependencies(t)}` : "") + (t.description ? `
45922
46139
 
45923
46140
  ${t.description}` : "") + (t.acceptance_criteria.length ? `
45924
46141
 
45925
46142
  Acceptance criteria:
45926
46143
  ${t.acceptance_criteria.map((c8) => ` - ${c8}`).join("\n")}` : "");
46144
+ var waitingOn = (t) => (t.blocked_by ?? []).filter((b) => !resolved(b)).map((b) => `#${b.number}`).join(" ");
46145
+ function blockersFrom(values, allowNone) {
46146
+ if (!values) return void 0;
46147
+ if (values.length === 1 && values[0] === "none") {
46148
+ if (!allowNone) throw usage("--blocked-by none only clears blockers on task update");
46149
+ return [];
46150
+ }
46151
+ return values.map((v) => v.trim().replace(/^issue:/, ""));
46152
+ }
45927
46153
  async function bodyFrom(ctx, o, required2 = true) {
45928
46154
  if (o.body !== void 0 && o.file !== void 0) throw usage("pass either --body or --file, not both");
45929
46155
  if (o.file !== void 0) return readText(fileSpec(o.file), ctx.input);
@@ -45933,23 +46159,24 @@ async function bodyFrom(ctx, o, required2 = true) {
45933
46159
  }
45934
46160
  function registerTask(program2, rt) {
45935
46161
  const task = program2.command("task").description("Tasks (issues) in --repo");
45936
- task.command("create").description("Create a task").requiredOption("--title <title>", "title").option("--body <text>", "description (Markdown; @file or - for stdin)").option("--file <path>", "read the description from a file or - for stdin").option("--criterion <text>", "acceptance criterion (repeatable)", repeat).option("--label <label>", "label (repeatable)", repeat).action(
46162
+ task.command("create").description("Create a task").requiredOption("--title <title>", "title").option("--body <text>", "description (Markdown; @file or - for stdin)").option("--file <path>", "read the description from a file or - for stdin").option("--criterion <text>", "acceptance criterion (repeatable)", repeat).option("--label <label>", "label (repeatable)", repeat).option("--blocked-by <task>", `${BLOCKER_ARG} (repeatable)`, repeat).action(
45937
46163
  act(rt, async (ctx, o) => {
45938
46164
  const description = await bodyFrom(ctx, o, false);
45939
- const t = await ctx.client().tasks.create(await ctx.repoId(), { title: o.title, description, acceptance_criteria: o.criterion, labels: o.label });
46165
+ const t = await ctx.client().tasks.create(await ctx.repoId(), { title: o.title, description, acceptance_criteria: o.criterion, labels: o.label, blocked_by: blockersFrom(o.blockedBy, false) });
45940
46166
  ctx.result(t, renderTask);
45941
46167
  })
45942
46168
  );
45943
- task.command("list").description("List tasks").option("--state <state>", "open | in_progress | blocked | done | cancelled").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--cursor <cursor>", "page cursor").action(
46169
+ task.command("list").description("List tasks").option("--state <state>", "open | in_progress | blocked | done | cancelled").option("--ready", "only tasks ready to start: open, with every blocker done or cancelled").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--cursor <cursor>", "page cursor").action(
45944
46170
  act(rt, async (ctx, o) => {
45945
- const page = await ctx.client().tasks.list(await ctx.repoId(), { state: o.state, limit: o.limit, cursor: o.cursor });
45946
- ctx.result(page, (v) => listWithCursor(v, ["number", "state", "title", "id"]));
46171
+ const page = await ctx.client().tasks.list(await ctx.repoId(), { state: o.state, ...o.ready ? { ready: true } : {}, limit: o.limit, cursor: o.cursor });
46172
+ ctx.result(page, (v) => listWithCursor({ ...v, items: v.items.map((t) => ({ ...t, waiting_on: waitingOn(t) })) }, ["number", "state", "title", "waiting_on", "id"]));
45947
46173
  })
45948
46174
  );
45949
- task.command("view <task>").description("Show a task").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client().tasks.get(id2), renderTask)));
45950
- task.command("update <task>").description("Update fields; uses the current version unless --expected-version pins it (then stale edits fail with exit 5)").option("--title <title>", "title").option("--body <text>", "description").option("--file <path>", "description from a file or -").option("--state <state>", "open | in_progress | blocked | done | cancelled").option("--owner <user-id>", "owner user id ('none' to clear)").option("--label <label>", "replace labels (repeatable)", repeat).option("--expected-version <n>", "expected version (optimistic concurrency)", (v) => parseIntOption(v, "--expected-version")).action(
45951
- act(rt, async (ctx, o, [id2]) => {
46175
+ task.command("view").argument("<task>", TASK_ARG).description("Show a task, with what it is blocked by and what it blocks").action(act(rt, async (ctx, _o, [spec]) => ctx.result(await ctx.client().tasks.get(await ctx.taskId(spec)), renderTask)));
46176
+ task.command("update").argument("<task>", TASK_ARG).description("Update fields; uses the current version unless --expected-version pins it (then stale edits fail with exit 5)").option("--title <title>", "title").option("--body <text>", "description").option("--file <path>", "description from a file or -").option("--state <state>", "open | in_progress | blocked | done | cancelled").option("--owner <user-id>", "owner user id ('none' to clear)").option("--label <label>", "replace labels (repeatable)", repeat).option("--blocked-by <task>", `replace the blockers: ${BLOCKER_ARG} (repeatable; 'none' to clear)`, repeat).option("--expected-version <n>", "expected version (optimistic concurrency)", (v) => parseIntOption(v, "--expected-version")).action(
46177
+ act(rt, async (ctx, o, [spec]) => {
45952
46178
  const c8 = ctx.client();
46179
+ const id2 = await ctx.taskId(spec);
45953
46180
  const version4 = o.expectedVersion ?? (await c8.tasks.get(id2)).version;
45954
46181
  const t = await c8.tasks.update(id2, {
45955
46182
  title: o.title,
@@ -45957,17 +46184,35 @@ function registerTask(program2, rt) {
45957
46184
  state: o.state,
45958
46185
  owner_id: o.owner === void 0 ? void 0 : o.owner === "none" ? null : o.owner,
45959
46186
  labels: o.label,
46187
+ blocked_by: blockersFrom(o.blockedBy, true),
45960
46188
  version: version4
45961
46189
  });
45962
46190
  ctx.result(t, renderTask);
45963
46191
  })
45964
46192
  );
45965
- task.command("claim <task>").description("Advisory claim (exit 5 if someone else holds it)").option("--session <id>", "claim on behalf of this agent session").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client().tasks.claim(id2, { session_id: o.session }), renderTask)));
45966
- task.command("release <task>").description("Release your claim").option("--session <id>", "agent session that holds the claim").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client().tasks.release(id2, { session_id: o.session }), renderTask)));
45967
- task.command("comment <task>").description("Add a comment").option("--body <text>", "comment (Markdown; @file or - for stdin)").option("--file <path>", "read the comment from a file or -").action(
45968
- act(rt, async (ctx, o, [id2]) => {
46193
+ task.command("depend").argument("<task>", TASK_ARG).argument("[blocker]", BLOCKER_ARG).description("Mark <task> as blocked by [blocker] (same repository); without it, show what <task> is blocked by and blocks").action(
46194
+ act(rt, async (ctx, _o, [spec, blocker]) => {
46195
+ const c8 = ctx.client();
46196
+ const id2 = await ctx.taskId(spec);
46197
+ const deps = blocker ? await c8.tasks.dependencies.add(id2, { blocker: blockersFrom([blocker], false)[0] }) : await c8.tasks.dependencies.list(id2);
46198
+ ctx.result(deps, renderDependencies);
46199
+ })
46200
+ );
46201
+ task.command("undepend").argument("<task>", TASK_ARG).argument("<blocker>", BLOCKER_ARG).description("Remove <blocker> from what <task> is blocked by").action(
46202
+ act(rt, async (ctx, _o, [spec, blocker]) => {
46203
+ const id2 = await ctx.taskId(spec);
46204
+ const ref = blockersFrom([blocker], false)[0];
46205
+ await ctx.client().tasks.dependencies.remove(id2, ref);
46206
+ ctx.result({ task_id: id2, removed: ref }, `${spec} is no longer blocked by ${ref}`);
46207
+ })
46208
+ );
46209
+ task.command("claim").argument("<task>", TASK_ARG).description("Advisory claim (exit 5 if someone else holds it)").option("--session <id>", "claim on behalf of this agent session").action(act(rt, async (ctx, o, [spec]) => ctx.result(await ctx.client().tasks.claim(await ctx.taskId(spec), { session_id: o.session }), renderTask)));
46210
+ task.command("release").argument("<task>", TASK_ARG).description("Release your claim").option("--session <id>", "agent session that holds the claim").action(act(rt, async (ctx, o, [spec]) => ctx.result(await ctx.client().tasks.release(await ctx.taskId(spec), { session_id: o.session }), renderTask)));
46211
+ task.command("comment").argument("<task>", TASK_ARG).description("Add a comment").option("--body <text>", "comment (Markdown; @file or - for stdin)").option("--file <path>", "read the comment from a file or -").action(
46212
+ act(rt, async (ctx, o, [spec]) => {
46213
+ const id2 = await ctx.taskId(spec);
45969
46214
  const comment = await ctx.client().tasks.comments.create(id2, { body: await bodyFrom(ctx, o) });
45970
- ctx.result(comment, `Comment ${comment.id} added to ${id2}`);
46215
+ ctx.result(comment, `Comment ${comment.id} added to ${spec}`);
45971
46216
  })
45972
46217
  );
45973
46218
  }
@@ -46249,7 +46494,7 @@ var how = (strategy) => strategy ? `with ${strategy}` : "with the policy's defau
46249
46494
  function renderAutoMerge(a, change, p, now) {
46250
46495
  const ref = change ? String(change.number) : a.change_id;
46251
46496
  const name = change ? `${p.bold(`#${change.number}`)} ${change.title}` : p.bold(a.change_id);
46252
- const who3 = a.enabled_by ? a.enabled_by.display_name ?? a.enabled_by.id : null;
46497
+ const who3 = a.enabled_by ? actorLabel(a.enabled_by) : null;
46253
46498
  const out = [];
46254
46499
  switch (a.state) {
46255
46500
  case "enabled":
@@ -46269,7 +46514,7 @@ function renderAutoMerge(a, change, p, now) {
46269
46514
  out.push(p.dim(` Turn it on: ${BRAND.cli} change auto-merge ${ref}`));
46270
46515
  }
46271
46516
  if (a.history.length) {
46272
- const rows2 = a.history.map((h) => ({ when: ago2(h.at, now), action: h.action, actor: h.actor.display_name ?? h.actor.id, reason: h.reason }));
46517
+ const rows2 = a.history.map((h) => ({ when: ago2(h.at, now), action: h.action, actor: actorLabel(h.actor), reason: h.reason }));
46273
46518
  const w = { when: Math.max(...rows2.map((r) => r.when.length)), action: Math.max(...rows2.map((r) => r.action.length)) };
46274
46519
  out.push("", p.bold("History"));
46275
46520
  for (const r of rows2) out.push(` ${p.dim(r.when.padEnd(w.when))} ${r.action.padEnd(w.action)} ${r.actor}${r.reason ? p.dim(` \xB7 ${r.reason}`) : ""}`);
@@ -46335,11 +46580,11 @@ function stepOf(state2, previous) {
46335
46580
 
46336
46581
  // packages/cli/src/commands/change.ts
46337
46582
  var renderChange = (c8) => kv(
46338
- { ...c8, source: c8.source.workspace_id ?? c8.source.ref, author: `${c8.author.type} ${c8.author.id}`, revision: `#${c8.current_revision.number} ${c8.current_revision.id}` },
46583
+ { ...c8, source: c8.source.workspace_id ?? c8.source.ref, author: actorLabel(c8.author), revision: `#${c8.current_revision.number} ${c8.current_revision.id}` },
46339
46584
  ["number", "id", "title", "state", "target_ref", "source", "task_id", "author", "revision", "current_revision.head_oid", "revision_count", "merged", "version"]
46340
46585
  );
46341
46586
  var renderThread = (t) => `${t.id} [${t.state}${t.blocking ? ", blocking" : ""}${t.anchor.outdated ? ", outdated" : ""}] ${t.anchor.path ?? "(change)"}${t.anchor.start_line ? `:${t.anchor.start_line}` : ""}
46342
- ` + t.comments.map((c8) => ` ${c8.author.display_name ?? c8.author.id}: ${c8.body.split("\n").join("\n ")}`).join("\n");
46587
+ ` + t.comments.map((c8) => ` ${actorLabel(c8.author)}: ${c8.body.split("\n").join("\n ")}`).join("\n");
46343
46588
  function registerChange(program2, rt) {
46344
46589
  const change = program2.command("change").description("Changes (proposed updates to a protected ref)");
46345
46590
  change.command("create").description("Create a change from a workspace or a ref in --repo").requiredOption("--title <title>", "title").option("--body <text>", "description (Markdown; @file or - for stdin)").option("--file <path>", "read the description from a file or -").option("--workspace <id>", "source workspace").option("--ref <ref>", "source ref (e.g. refs/heads/feature)").option("--target <ref>", "target ref (default: the repository's default branch)").option("--task <id>", "linked task").option("--draft", "create as draft").action(
@@ -46359,7 +46604,7 @@ function registerChange(program2, rt) {
46359
46604
  change.command("list").description("List changes in --repo").option("--state <state>", "draft | open | merged | closed").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--cursor <cursor>", "page cursor").action(
46360
46605
  act(rt, async (ctx, o) => {
46361
46606
  const page = await ctx.client().changes.list(await ctx.repoId(), { state: o.state, limit: o.limit, cursor: o.cursor });
46362
- ctx.result(page, (v) => listWithCursor(v, ["number", "state", "title", "target_ref", "id"]));
46607
+ ctx.result(page, (v) => listWithCursor({ ...v, items: v.items.map((c8) => ({ ...c8, author: actorLabel(c8.author) })) }, ["number", "state", "title", "author", "target_ref", "id"]));
46363
46608
  })
46364
46609
  );
46365
46610
  change.command("view").argument("<change>", CHANGE_ARG).description("Show a change").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client().changes.get(await ctx.changeId(id2)), renderChange)));
@@ -46448,7 +46693,7 @@ function registerReview(program2, rt) {
46448
46693
  const id2 = await ctx.changeId(spec);
46449
46694
  const revision = o.revision ?? (await c8.changes.get(id2)).current_revision.id;
46450
46695
  const r = await c8.reviews.create(id2, { revision_id: revision, decision, body: await bodyFrom(ctx, o, bodyRequired) });
46451
- ctx.result(r, (v) => kv({ ...v, reviewer: `${v.reviewer.type} ${v.reviewer.id}` }, ["id", "decision", "revision_id", "reviewer", "current"]));
46696
+ ctx.result(r, (v) => kv({ ...v, reviewer: actorLabel(v.reviewer) }, ["id", "decision", "revision_id", "reviewer", "current"]));
46452
46697
  })
46453
46698
  );
46454
46699
  decide("approve", "approved", "Approve a revision (approvals are bound to that revision)", false);
@@ -46569,7 +46814,7 @@ Wait for the findings: code-review wait ${spec}`);
46569
46814
 
46570
46815
  // packages/cli/src/commands/evidence.ts
46571
46816
  import { basename as basename4, extname, isAbsolute as isAbsolute8, resolve as resolve8 } from "node:path";
46572
- var renderEvidence = (e) => kv({ ...e, issuer: `${e.issuer.type} ${e.issuer.id}` }, ["id", "check_name", "result", "subject_kind", "subject_oid", "origin", "independence", "issuer", "summary", "attachment_ids", "revoked_at"]);
46817
+ var renderEvidence = (e) => kv({ ...e, issuer: actorLabel(e.issuer) }, ["id", "check_name", "result", "subject_kind", "subject_oid", "origin", "independence", "issuer", "summary", "attachment_ids", "revoked_at"]);
46573
46818
  var CONTENT_TYPES = { ".txt": "text/plain", ".log": "text/plain", ".json": "application/json", ".xml": "application/xml", ".html": "text/plain" };
46574
46819
  function registerEvidence(program2, rt) {
46575
46820
  const ev = program2.command("evidence").description("Verification evidence");
@@ -46746,7 +46991,7 @@ var renderCoverage = (c8) => [
46746
46991
  `code owner review ${c8.required ? "required" : "not required"} by policy v${c8.policy_version} (revision ${c8.revision_id})${c8.complete ? "" : " \u2014 path list truncated"}`,
46747
46992
  table(c8.paths.map((p) => ({ ...p, owners: p.owners.join(" ") || "-", approved_by: p.approved_by.join(" ") || "-" })), ["path", "owners", "satisfied", "approved_by"])
46748
46993
  ].join("\n");
46749
- var renderAlerts = (p) => listWithCursor({ ...p, items: p.items.map((a) => ({ ...a, where: `${a.path}:${a.line}`, pushed_by: a.pushed_by.display_name ?? a.pushed_by.id })) }, ["id", "state", "rule", "where", "blob_oid", "masked", "pushed_by", "created_at"]);
46994
+ var renderAlerts = (p) => listWithCursor({ ...p, items: p.items.map((a) => ({ ...a, where: `${a.path}:${a.line}`, pushed_by: actorLabel(a.pushed_by) })) }, ["id", "state", "rule", "where", "blob_oid", "masked", "pushed_by", "created_at"]);
46750
46995
  function registerGovernance(program2, rt) {
46751
46996
  const change = program2.commands.find((c8) => c8.name() === "change") ?? program2.command("change");
46752
46997
  change.command("draft").argument("<change>", CHANGE_ARG).description("Convert an open change back to a draft (drafts cannot merge and notify nobody)").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client().changes.draft(await ctx.changeId(id2)), (c8) => kv(c8, ["id", "number", "title", "state"]))));
@@ -46836,7 +47081,7 @@ new revision #${r.revision.number} ${r.revision.id}`);
46836
47081
  }
46837
47082
  const b = await ctx.client("user").pushProtection.bypasses.create(await ctx.repoId(), { rule: o.rule, path: o.path, blob_oid: o.blob, reason: o.reason, ...principal ? { principal } : {}, ...o.ref ? { ref: o.ref } : {} });
46838
47083
  ctx.result(b, (v) => {
46839
- const who3 = v.principal ? v.principal.display_name ?? `${v.principal.type}:${v.principal.id}` : "nobody";
47084
+ const who3 = v.principal ? actorLabel(v.principal) : "nobody";
46840
47085
  return `${kv({ ...v, for: who3, ref: v.ref ?? "any" }, ["id", "rule", "path", "blob_oid", "for", "ref", "expires_at"])}
46841
47086
  Only ${who3} can use it, for one push, before it expires.`;
46842
47087
  });
@@ -46847,7 +47092,7 @@ Only ${who3} can use it, for one push, before it expires.`;
46847
47092
  rt,
46848
47093
  async (ctx, o) => ctx.result(
46849
47094
  await ctx.client().pushProtection.bypasses.list(await ctx.repoId(), { limit: o.limit, cursor: o.cursor }),
46850
- (p) => listWithCursor({ ...p, items: p.items.map((b) => ({ ...b, for: b.principal ? b.principal.display_name ?? b.principal.id : "-", created_by: b.created_by.display_name ?? b.created_by.id, used: b.used_at ? "yes" : "no" })) }, ["id", "rule", "path", "blob_oid", "for", "created_by", "used", "expires_at", "reason"])
47095
+ (p) => listWithCursor({ ...p, items: p.items.map((b) => ({ ...b, for: b.principal ? actorLabel(b.principal) : "-", created_by: actorLabel(b.created_by), used: b.used_at ? "yes" : "no" })) }, ["id", "rule", "path", "blob_oid", "for", "created_by", "used", "expires_at", "reason"])
46851
47096
  )
46852
47097
  )
46853
47098
  );
@@ -46888,7 +47133,7 @@ var renderAlerts2 = (p) => listWithCursor(
46888
47133
  ["id", "state", "resolution", "confidence", "rule", "repo", "where", "masked", "source", "created_at"]
46889
47134
  );
46890
47135
  var renderAlert = (a) => [
46891
- kv({ ...a, where: where(a), resolved_by: a.resolved_by ? a.resolved_by.display_name ?? a.resolved_by.id : null, looks_fake: a.looks_fake?.label ?? null }, [
47136
+ kv({ ...a, where: where(a), resolved_by: a.resolved_by ? actorLabel(a.resolved_by) : null, looks_fake: a.looks_fake?.label ?? null }, [
46892
47137
  "id",
46893
47138
  "state",
46894
47139
  "resolution",
@@ -47229,8 +47474,8 @@ var renderDevices = (v) => v.items.length ? table(
47229
47474
  v.items.map((d) => ({ id: d.id, name: d.name ?? "-", environment: d.environment, app_version: d.app_version ?? "-", last_seen: d.last_seen_at, current: d.current ? "yes" : "" })),
47230
47475
  ["id", "name", "environment", "app_version", "last_seen", "current"]
47231
47476
  ) : "No push devices. The iOS app registers itself when you allow notifications.";
47232
- var PAGE2 = 100;
47233
- var MAX_PAGES2 = 10;
47477
+ var PAGE = 100;
47478
+ var MAX_PAGES = 10;
47234
47479
  async function findItems(client, specs) {
47235
47480
  const wanted = specs.map((s) => s.trim().toLowerCase());
47236
47481
  for (const s of wanted) if (s.length < 4) throw usage(`"${s}" is too short to name an inbox item: use the id \`${c4} inbox list\` shows`);
@@ -47238,8 +47483,8 @@ async function findItems(client, specs) {
47238
47483
  const seen = /* @__PURE__ */ new Set();
47239
47484
  for (const view of ["all", "snoozed", "done"]) {
47240
47485
  let cursor2;
47241
- for (let i = 0; i < MAX_PAGES2; i++) {
47242
- const page = await client.inbox.list({ view, limit: PAGE2, ...cursor2 ? { cursor: cursor2 } : {} });
47486
+ for (let i = 0; i < MAX_PAGES; i++) {
47487
+ const page = await client.inbox.list({ view, limit: PAGE, ...cursor2 ? { cursor: cursor2 } : {} });
47243
47488
  for (const item of page.items) {
47244
47489
  if (seen.has(item.id)) continue;
47245
47490
  seen.add(item.id);
@@ -47694,13 +47939,13 @@ function renderEntry(e, verb) {
47694
47939
  if (e.flags.length) lines2.push(`flagged: ${e.flags.join(", ")}${e.flags.includes("instructions") ? "; never given to the assistant on its own" : ""}`);
47695
47940
  return lines2.join("\n");
47696
47941
  }
47697
- var PAGE3 = 100;
47698
- var MAX_PAGES3 = 10;
47942
+ var PAGE2 = 100;
47943
+ var MAX_PAGES2 = 10;
47699
47944
  async function reviewable(ctx, org, q) {
47700
47945
  const out = [];
47701
47946
  let cursor2;
47702
- for (let i = 0; i < MAX_PAGES3; i++) {
47703
- const page = await ctx.client("user").assistant.memory.list(org, { ...q, status: "pending", limit: PAGE3, ...cursor2 ? { cursor: cursor2 } : {} });
47947
+ for (let i = 0; i < MAX_PAGES2; i++) {
47948
+ const page = await ctx.client("user").assistant.memory.list(org, { ...q, status: "pending", limit: PAGE2, ...cursor2 ? { cursor: cursor2 } : {} });
47704
47949
  out.push(...page.items.filter((e) => e.can_review));
47705
47950
  if (!page.next_cursor) break;
47706
47951
  cursor2 = page.next_cursor;
@@ -48008,6 +48253,38 @@ function registerIntegrations(program2, rt) {
48008
48253
  integrations.command("inherit <provider> <purpose>").description("Drop --repo's choice and inherit the nearest shared connection again").action(act(rt, async (ctx, _o, [provider, purpose]) => ctx.result(await ctx.client("user").grants.inherit(await ctx.repoId(void 0, "user"), provider, purpose), renderRepo)));
48009
48254
  }
48010
48255
 
48256
+ // packages/cli/src/commands/approvals.ts
48257
+ function protectOptions(cmd) {
48258
+ return cmd.option("--user <handle>", "a person who may approve (@handle or id; repeatable)", repeat).option("--team <slug>", "a team whose members may approve (slug or id; repeatable)", repeat).option("--required <n>", "approvals needed from different people (default 1)", (v) => parseIntOption(v, "--required")).option("--separation-of-duties", "the change's author, its merger and whoever requested the deploy or run cannot approve it");
48259
+ }
48260
+ function protectBody(o, current) {
48261
+ const users = o.user ?? [];
48262
+ const teams = o.team ?? [];
48263
+ if (!users.length && !teams.length && o.required === void 0 && !o.separationOfDuties) throw usage("give --user, --team, --required or --separation-of-duties");
48264
+ return { required: o.required ?? 1, users, teams, separation_of_duties: !!o.separationOfDuties, ...current ? { version: current.version } : {} };
48265
+ }
48266
+ var who2 = (p) => p.handle ? `@${p.handle}` : p.name;
48267
+ var ruleSummary = (r) => r ? `${r.required} approval${r.required === 1 ? "" : "s"} from ${[...r.teams.map((t) => `@${t.slug} team`), ...r.users.map(who2)].join(", ") || "anyone who may approve"}${r.separation_of_duties ? "; not the author, merger or requester" : ""}` : "not protected";
48268
+ var renderApprovers = (v) => kv(
48269
+ {
48270
+ protected: ruleSummary(v.rule),
48271
+ capability: v.capability,
48272
+ eligible: v.eligible.map(who2).join(", ") || "nobody",
48273
+ lacking_capability: v.lacking_capability.length ? `${v.lacking_capability.map(who2).join(", ")} (named, but cannot approve without ${v.capability})` : null,
48274
+ unsatisfiable: v.unsatisfiable ? "yes: fewer people can approve than it needs" : null
48275
+ },
48276
+ ["protected", "capability", "eligible", "lacking_capability", "unsatisfiable"]
48277
+ );
48278
+ var renderRule = (r) => `protected: ${ruleSummary(r)} (version ${r.version})`;
48279
+ function approvalLines(a) {
48280
+ if (!a) return "";
48281
+ const by2 = a.approvals.filter((x) => x.counted).map((x) => who2(x.user));
48282
+ const lines2 = [a.line ?? (a.satisfied ? `approved (${a.counted} of ${a.required})` : null), by2.length ? `approved by ${by2.join(", ")}` : null, a.you && !a.you.can_approve && a.line ? `you cannot approve: ${a.you.message}` : null].filter(Boolean);
48283
+ return lines2.length ? `
48284
+
48285
+ ${lines2.join("\n")}` : "";
48286
+ }
48287
+
48011
48288
  // packages/cli/src/commands/db.ts
48012
48289
  async function sourceId(ctx, ref, repo) {
48013
48290
  if (ref.startsWith("dms_")) return ref;
@@ -48018,18 +48295,58 @@ async function sourceId(ctx, ref, repo) {
48018
48295
  }
48019
48296
  var plural3 = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
48020
48297
  var short = (oid) => oid.slice(0, 12);
48021
- var database = (s) => `${s.ps_organization}/${s.ps_database}@${s.ps_branch}`;
48298
+ var database = (s) => {
48299
+ const t = s.target;
48300
+ return t.provider === "cloudflare_d1" ? `d1:${t.binding}\u2192${t.database_name ?? t.database_id}${t.environment ? ` (env ${t.environment})` : ""}` : `${t.organization}/${t.database}@${t.branch}`;
48301
+ };
48302
+ var PS_FORMATS = ["atlas", "plain", "golang_migrate", "drizzle", "prisma"];
48303
+ var PS_ENGINES = ["postgres", "vitess"];
48304
+ var LEVELS = ["error", "warning", "off"];
48305
+ function lintLines(findings2) {
48306
+ if (!findings2?.length) return [];
48307
+ return ["lint:", ...findings2.map((f) => ` ${f.level.padEnd(12)} ${(f.atlas_code ?? f.code).padEnd(6)} ${f.file}:${f.line} ${f.message}`)];
48308
+ }
48309
+ function diffLines(d) {
48310
+ if (!d) return [];
48311
+ const t = d.tables;
48312
+ const out = [];
48313
+ for (const x of t.added) out.push(` + table ${x.name} (${x.columns.map((c8) => c8.name).join(", ")})`);
48314
+ for (const x of t.renamed) out.push(` ~ table ${x.from} \u2192 ${x.to}`);
48315
+ for (const x of t.changed) {
48316
+ for (const c8 of x.columns.added) out.push(` + ${x.name}.${c8.name} ${c8.type}${c8.nullable ? "" : " NOT NULL"}`);
48317
+ for (const c8 of x.columns.renamed) out.push(` ~ ${x.name}.${c8.from} \u2192 ${c8.to}`);
48318
+ for (const c8 of x.columns.changed) out.push(` ~ ${x.name}.${c8.name} ${c8.before.type} \u2192 ${c8.after.type}`);
48319
+ for (const c8 of x.columns.dropped) out.push(` - ${x.name}.${c8}`);
48320
+ for (const i of x.indexes.added) out.push(` + index ${i} on ${x.name}`);
48321
+ for (const i of x.indexes.dropped) out.push(` - index ${i} on ${x.name}`);
48322
+ for (const k of x.foreign_keys.added) out.push(` + foreign key ${k} on ${x.name}`);
48323
+ for (const k of x.foreign_keys.dropped) out.push(` - foreign key ${k} on ${x.name}`);
48324
+ }
48325
+ for (const x of t.dropped) out.push(` - table ${x.name}`);
48326
+ if (!out.length && !d.unparsed.length) return ["schema effect: none"];
48327
+ return ["schema effect:", ...out, ...d.unparsed.length ? [` (${plural3(d.unparsed.length, "statement")} could not be interpreted, first at ${d.unparsed[0].file}:${d.unparsed[0].line})`] : []];
48328
+ }
48329
+ function renderDrift(d) {
48330
+ if (!d) return "drift: not checked yet";
48331
+ const lines2 = [`drift: ${d.status} (checked ${d.checked_at} against ${plural3(d.applied, "applied migration")})`];
48332
+ if (d.note) lines2.push(`note: ${d.note}`);
48333
+ for (const f of d.findings) lines2.push(` ${f.kind.padEnd(20)} ${f.object} ${f.detail}`);
48334
+ return lines2.join("\n");
48335
+ }
48022
48336
  function issueLines(issues) {
48023
48337
  if (!issues.length) return [];
48024
48338
  return ["issues:", ...issues.map((i) => ` ${i.level.padEnd(7)} ${i.code} ${i.file ? `${i.file}: ` : ""}${i.message}`)];
48025
48339
  }
48026
48340
  var renderSources = (v) => table(
48027
- v.items.map((s) => ({ ...s, database: database(s), applied: s.snapshot ? `${s.snapshot.applied.length} (read ${s.snapshot.read_at})` : "not read yet" })),
48028
- ["name", "id", "path", "format", "engine", "database", "branch", "on_merge", "state", "applied"]
48341
+ v.items.map((s) => ({ ...s, database: database(s), applied: s.snapshot ? `${s.snapshot.applied.length} (read ${s.snapshot.read_at})` : "not read yet", drift: s.drift?.status ?? "-" })),
48342
+ ["name", "id", "provider", "path", "format", "database", "branch", "apply_on", "state", "applied", "drift"]
48029
48343
  );
48030
- var renderSource = (s) => `${kv({ ...s, database: database(s), path: s.path || "(repository root)" }, ["id", "name", "path", "format", "engine", "database", "branch", "revisions_location", "baseline_version", "exec_order", "credential_mode", "dsn_set", "run_as_role", "on_merge", "change_checks", "state", "version"])}
48344
+ var renderSource = (s) => `${kv(
48345
+ { ...s, approvers: ruleSummary(s.approval_rule), database: database(s), path: s.path || "(repository root)", destructive: s.lint.levels.destructive ?? "error", nolint: s.lint.nolint, ...s.target.provider === "cloudflare_d1" ? { from: `${s.target.config_path} on ${s.branch}` } : {} },
48346
+ s.provider === "cloudflare_d1" ? ["id", "name", "provider", "path", "format", "database", "from", "branch", "revisions_location", "apply_on", "require_approval", "approvers", "change_checks", "destructive", "nolint", "drift_mode", "state", "version"] : ["id", "name", "provider", "path", "format", "engine", "database", "branch", "revisions_location", "baseline_version", "exec_order", "credential_mode", "dsn_set", "run_as_role", "apply_on", "require_approval", "approvers", "change_checks", "destructive", "nolint", "drift_mode", "state", "version"]
48347
+ )}
48031
48348
 
48032
- ` + (s.on_merge === "approval" ? `Merges to ${s.branch} that add migrations create a run waiting for approval: ${BRAND.cli} db runs` : `Runs start only by hand: ${BRAND.cli} db apply ${s.name}`);
48349
+ ` + (s.apply_on === "merge" ? `Merges to ${s.branch} that add migrations create a run waiting for approval: ${BRAND.cli} db runs` : s.apply_on === "deploy" ? `The production deploy's migrations step applies them${s.require_approval ? ", once the run is approved" : ""}` : `Runs start only by hand: ${BRAND.cli} db apply ${s.name}`);
48033
48350
  var renderSuggestions = (v) => v.items.length ? table(
48034
48351
  v.items.map((s) => ({ ...s, path: s.path || "(root)", configured: s.configured ? "yes" : "no" })),
48035
48352
  ["path", "format", "files", "revisions_schema", "baseline_version", "configured", "evidence"]
@@ -48048,16 +48365,17 @@ function renderPlan2(p) {
48048
48365
  lines2.push(` ${f.name} ${plural3(f.statements, "statement")}${notes.length ? ` (${notes.join(", ")})` : ""}`);
48049
48366
  }
48050
48367
  }
48051
- return [...lines2, ...issueLines(p.issues)].join("\n");
48368
+ return [...lines2, ...issueLines(p.issues), ...lintLines(p.lint), ...diffLines(p.schema_diff)].join("\n");
48052
48369
  }
48053
48370
  var renderPlans = (v) => v.items.length ? v.items.map(renderPlan2).join("\n\n") : "no migration sources";
48054
48371
  var NEXT = {
48055
- awaiting_approval: (id2) => `Waiting for a repository admin: ${BRAND.cli} db approve ${id2} (or reject)`,
48372
+ awaiting_approval: (id2) => `Waiting for approval: ${BRAND.cli} db approve ${id2} (or reject)`,
48056
48373
  failed: (id2) => `Fix the migration and merge it, then: ${BRAND.cli} db retry ${id2}`,
48057
- needs_attention: (id2) => `Look at the run in PlanetScale, then: ${BRAND.cli} db retry ${id2} or cancel`
48374
+ needs_attention: (id2) => `Look at the database (PlanetScale, or the D1 bookmark's restore command), then: ${BRAND.cli} db retry ${id2} or cancel`
48058
48375
  };
48059
48376
  function renderRun(r) {
48060
- const ps = r.planetscale;
48377
+ const ps = r.backend?.provider === "planetscale" ? r.backend : null;
48378
+ const d1 = r.backend?.provider === "cloudflare_d1" ? r.backend : null;
48061
48379
  const head = kv(
48062
48380
  {
48063
48381
  ...r,
@@ -48065,16 +48383,18 @@ function renderRun(r) {
48065
48383
  change: r.change_id ?? "-",
48066
48384
  requested_by: r.requested_by ? `${r.requested_by.type} ${r.requested_by.id}` : "-",
48067
48385
  deploy_request: ps?.deploy_request_url ?? (ps?.deploy_request ? `#${ps.deploy_request}` : null),
48068
- deployment_state: ps?.deployment_state ?? null
48386
+ deployment_state: ps?.deployment_state ?? null,
48387
+ bookmark: d1?.bookmark ?? null,
48388
+ restore: d1?.restore?.wrangler ?? null
48069
48389
  },
48070
- ["id", "source_name", "status", "kind", "engine", "commit", "change", "baseline", "requested_by", "approved_by", "approved_at", "decision_reason", "deploy_request", "deployment_state", "error", "note", "created_at"]
48390
+ ["id", "source_name", "status", "kind", "provider", "engine", "commit", "change", "deployment_id", "baseline", "requested_by", "approved_by", "approved_at", "decision_reason", "deploy_request", "deployment_state", "bookmark", "restore", "error", "note", "created_at"]
48071
48391
  );
48072
48392
  const files = r.files.length ? table(
48073
48393
  r.files.map((f) => ({ ...f, statements: f.applied_statements ? `${f.applied_statements}/${f.statements}` : f.statements, error: f.error ? `${f.error}${f.error_statement ? ` (in: ${f.error_statement})` : ""}` : null })),
48074
48394
  ["name", "status", "statements", "execution_ms", "error"]
48075
48395
  ) : "no files";
48076
48396
  const next = NEXT[r.status]?.(r.id);
48077
- return [head, "", files, ...r.issues.length ? ["", ...issueLines(r.issues)] : [], ...next ? ["", next] : []].join("\n");
48397
+ return [head, "", files, ...r.issues.length ? ["", ...issueLines(r.issues)] : [], ...next ? ["", next] : []].join("\n") + approvalLines(r.approval);
48078
48398
  }
48079
48399
  var renderRuns = (v) => listWithCursor(
48080
48400
  { ...v, items: v.items.map((r) => ({ ...r, commit: short(r.commit_oid), files: `${r.files.filter((f) => f.status === "applied").length}/${r.files.length} applied` })) },
@@ -48095,9 +48415,9 @@ function sessionSettings(pairs) {
48095
48415
  return out;
48096
48416
  }
48097
48417
  function registerDb(program2, rt) {
48098
- const db = program2.command("db").description("Database migrations on PlanetScale for --repo: sources, plans, and runs applied after merge and approval");
48099
- const sources = db.command("sources").description("Migration sources of --repo: a directory bound to a PlanetScale database branch").action(act(rt, async (ctx) => ctx.result(await ctx.client("user").dbMigrations.sources.list(await ctx.repoId(void 0, "user")), renderSources)));
48100
- sources.command("add").description("Add a migration source (repository admins). The source is checked against PlanetScale; `db suggest` shows the directories found").requiredOption("--name <name>", "source name (lowercase letters, digits, - and _)").requiredOption("--path <dir>", 'migration directory relative to the repository root ("" for the root)').requiredOption("--format <format>", DB_MIGRATION_FORMATS.join(" | "), oneOf("--format", DB_MIGRATION_FORMATS)).requiredOption("--engine <engine>", DB_ENGINE_KINDS.join(" | "), oneOf("--engine", DB_ENGINE_KINDS)).requiredOption("--connection <connection>", "PlanetScale connection for database migrations (icn_\u2026)").requiredOption("--organization <org>", "PlanetScale organization").requiredOption("--database <db>", "PlanetScale database").option("--ps-branch <branch>", "PlanetScale branch migrations are applied to", "main").option("--branch <branch>", "repository branch whose merges create runs (default: the default branch)").option("--revisions-schema <schema>", "where the revisions table lives (Postgres schema)").option("--revisions-table <table>", "revisions table name (default: the format's own)").option("--baseline <version>", "with no revision history yet, record this version as applied instead of applying files up to it").option("--exec-order <order>", DB_EXEC_ORDERS.join(" | "), oneOf("--exec-order", DB_EXEC_ORDERS)).option("--on-merge <mode>", "approval (merges create runs waiting for approval) | manual_only", oneOf("--on-merge", ["approval", "manual_only"])).option("--credential-mode <mode>", DB_CREDENTIAL_MODES.join(" | "), oneOf("--credential-mode", DB_CREDENTIAL_MODES)).option("--run-as-role <role>", "Postgres: SET ROLE for migration sessions").option("--session-setting <name=value>", "Postgres session setting for migration sessions (repeatable)", repeat).option("--statement-timeout <seconds>", "statement timeout (at most 600)", (v) => parseIntOption(v, "--statement-timeout")).option("--lock-timeout <seconds>", "lock timeout (at most 60)", (v) => parseIntOption(v, "--lock-timeout")).option("--no-change-checks", "do not validate the directory on changes").option("--dsn-file <path|->", "--credential-mode dsn: file holding the connection string, or - for stdin (never a flag value; never printed)").action(
48418
+ const db = program2.command("db").description("Database migrations on PlanetScale and Cloudflare D1 for --repo: sources, plans, lint, runs applied after approval, and drift");
48419
+ const sources = db.command("sources").description("Migration sources of --repo: a directory bound to a PlanetScale database branch or a D1 database (D1 sources come from the Wrangler configuration)").action(act(rt, async (ctx) => ctx.result(await ctx.client("user").dbMigrations.sources.list(await ctx.repoId(void 0, "user")), renderSources)));
48420
+ sources.command("add").description("Add a PlanetScale migration source (migrations:manage). The source is checked against PlanetScale; `db suggest` shows the directories found. D1 sources need no adding: they come from the Wrangler configuration").requiredOption("--name <name>", "source name (lowercase letters, digits, - and _)").requiredOption("--path <dir>", 'migration directory relative to the repository root ("" for the root)').requiredOption("--format <format>", PS_FORMATS.join(" | "), oneOf("--format", PS_FORMATS)).requiredOption("--engine <engine>", PS_ENGINES.join(" | "), oneOf("--engine", PS_ENGINES)).requiredOption("--connection <connection>", "PlanetScale connection for database migrations (icn_\u2026)").requiredOption("--organization <org>", "PlanetScale organization").requiredOption("--database <db>", "PlanetScale database").option("--ps-branch <branch>", "PlanetScale branch migrations are applied to", "main").option("--branch <branch>", "repository branch whose merges create runs (default: the default branch)").option("--revisions-schema <schema>", "where the revisions table lives (Postgres schema)").option("--revisions-table <table>", "revisions table name (default: the format's own)").option("--baseline <version>", "with no revision history yet, record this version as applied instead of applying files up to it").option("--exec-order <order>", DB_EXEC_ORDERS.join(" | "), oneOf("--exec-order", DB_EXEC_ORDERS)).option("--on-merge <mode>", "deprecated: --apply-on (approval = merge, manual_only = manual)", oneOf("--on-merge", ["approval", "manual_only"])).option("--apply-on <when>", `${DB_APPLY_ON.join(" | ")} (default merge: runs wait for approval after merge)`, oneOf("--apply-on", DB_APPLY_ON)).option("--no-approval", "--apply-on deploy or manual: apply without a person's approval").option("--destructive <level>", "lint level for destructive changes: error (default, fails the check) | warning | off", oneOf("--destructive", LEVELS)).option("--drift <mode>", DB_DRIFT_MODES.join(" | "), oneOf("--drift", DB_DRIFT_MODES)).option("--credential-mode <mode>", DB_CREDENTIAL_MODES.join(" | "), oneOf("--credential-mode", DB_CREDENTIAL_MODES)).option("--run-as-role <role>", "Postgres: SET ROLE for migration sessions").option("--session-setting <name=value>", "Postgres session setting for migration sessions (repeatable)", repeat).option("--statement-timeout <seconds>", "statement timeout (at most 600)", (v) => parseIntOption(v, "--statement-timeout")).option("--lock-timeout <seconds>", "lock timeout (at most 60)", (v) => parseIntOption(v, "--lock-timeout")).option("--no-change-checks", "do not validate the directory on changes").option("--dsn-file <path|->", "--credential-mode dsn: file holding the connection string, or - for stdin (never a flag value; never printed)").action(
48101
48421
  act(rt, async (ctx, o) => {
48102
48422
  let dsn;
48103
48423
  if (o.dsnFile !== void 0) {
@@ -48124,6 +48444,10 @@ function registerDb(program2, rt) {
48124
48444
  ...o.baseline ? { baseline_version: o.baseline } : {},
48125
48445
  ...o.execOrder ? { exec_order: o.execOrder } : {},
48126
48446
  ...o.onMerge ? { on_merge: o.onMerge } : {},
48447
+ ...o.applyOn ? { apply_on: o.applyOn } : {},
48448
+ ...o.approval === false ? { require_approval: false } : {},
48449
+ ...o.destructive ? { lint: { levels: { destructive: o.destructive }, nolint: "honor", drift_blocks_deploy: false } } : {},
48450
+ ...o.drift ? { drift_mode: o.drift } : {},
48127
48451
  ...o.credentialMode ? { credential_mode: o.credentialMode } : {},
48128
48452
  ...o.runAsRole ? { run_as_role: o.runAsRole } : {},
48129
48453
  ...o.sessionSetting ? { session_settings: sessionSettings(o.sessionSetting) } : {},
@@ -48134,6 +48458,45 @@ function registerDb(program2, rt) {
48134
48458
  ctx.result(await ctx.client("user").dbMigrations.sources.create(await ctx.repoId(void 0, "user"), body), renderSource);
48135
48459
  })
48136
48460
  );
48461
+ sources.command("set <source>").description("Change how a source applies, its lint and drift checks (migrations:manage; D1 sources: only these)").option("--apply-on <when>", DB_APPLY_ON.join(" | "), oneOf("--apply-on", DB_APPLY_ON)).option("--approval", "each run waits for approval (migrations:approve; a protected source's approvers)").option("--no-approval", "--apply-on deploy or manual: apply without approval").option("--destructive <level>", "lint level for destructive changes: error | warning | off", oneOf("--destructive", LEVELS)).option("--nolint <mode>", "honor | ignore: whether `-- atlas:nolint` in a file acknowledges a finding", oneOf("--nolint", ["honor", "ignore"])).option("--drift <mode>", DB_DRIFT_MODES.join(" | "), oneOf("--drift", DB_DRIFT_MODES)).option("--drift-blocks-deploy <yes|no>", "drift found by the last check fails the deploy's migrations step", oneOf("--drift-blocks-deploy", ["yes", "no"])).option("--enable", "activate the source").option("--disable", "disable the source (no runs, no checks)").action(
48462
+ act(rt, async (ctx, o, [ref]) => {
48463
+ if (o.enable && o.disable) throw usage("--enable and --disable exclude each other");
48464
+ const c8 = ctx.client("user").dbMigrations;
48465
+ const s = await c8.sources.get(await sourceId(ctx, ref));
48466
+ const levels = { ...s.lint.levels };
48467
+ if (o.destructive) {
48468
+ if (o.destructive === "error") delete levels.destructive;
48469
+ else levels.destructive = o.destructive;
48470
+ }
48471
+ const lintChanged = o.destructive || o.nolint || o.driftBlocksDeploy;
48472
+ const body = {
48473
+ version: s.version,
48474
+ ...o.applyOn ? { apply_on: o.applyOn } : {},
48475
+ ...o.approval !== void 0 ? { require_approval: o.approval } : {},
48476
+ ...lintChanged ? { lint: { levels, nolint: o.nolint ?? s.lint.nolint, drift_blocks_deploy: o.driftBlocksDeploy ? o.driftBlocksDeploy === "yes" : s.lint.drift_blocks_deploy } } : {},
48477
+ ...o.drift ? { drift_mode: o.drift } : {},
48478
+ ...o.enable ? { state: "active" } : o.disable ? { state: "disabled" } : {}
48479
+ };
48480
+ if (Object.keys(body).length === 1) throw usage("nothing to change: give --apply-on, --approval, --destructive, --nolint, --drift, --drift-blocks-deploy, --enable or --disable");
48481
+ ctx.result(await c8.sources.update(s.id, body), renderSource);
48482
+ })
48483
+ );
48484
+ protectOptions(sources.command("protect <source>").description("Require approvals for every run of the source (people with migrations:manage). A protected database can no longer apply without approval. Replaces the current rule")).action(
48485
+ act(rt, async (ctx, o, [ref]) => {
48486
+ const c8 = ctx.client("user").dbMigrations.sources;
48487
+ const s = await c8.get(await sourceId(ctx, ref));
48488
+ ctx.result(await c8.protect(s.id, protectBody(o, s.approval_rule)), renderRule);
48489
+ })
48490
+ );
48491
+ sources.command("unprotect <source>").description("Remove the source's approval rule (runs still need approval as its apply mode says). Requires --yes when non-interactive").action(
48492
+ act(rt, async (ctx, _o, [ref]) => {
48493
+ const id2 = await sourceId(ctx, ref);
48494
+ await ctx.confirm(`remove the approval rule of ${ref}`);
48495
+ await ctx.client("user").dbMigrations.sources.unprotect(id2);
48496
+ ctx.result({ unprotected: id2 }, `${ref} is no longer protected`);
48497
+ })
48498
+ );
48499
+ sources.command("approvers <source>").description("Who may approve the source's runs now").action(act(rt, async (ctx, _o, [ref]) => ctx.result(await ctx.client("user").dbMigrations.sources.approvers(await sourceId(ctx, ref)), renderApprovers)));
48137
48500
  sources.command("rm <source>").description("Delete a migration source (name or id; its runs stay listed). Requires --yes when non-interactive").action(
48138
48501
  act(rt, async (ctx, _o, [ref]) => {
48139
48502
  const id2 = await sourceId(ctx, ref);
@@ -48143,7 +48506,7 @@ function registerDb(program2, rt) {
48143
48506
  })
48144
48507
  );
48145
48508
  db.command("suggest").description("Migration directories found in --repo (atlas.sum, drizzle journals, *.up.sql, numbered SQL files), and whether a source covers each").option("--ref <ref>", "branch, tag or commit (default: the default branch)").action(act(rt, async (ctx, o) => ctx.result(await ctx.client("user").dbMigrations.suggestions(await ctx.repoId(void 0, "user"), o.ref), renderSuggestions)));
48146
- db.command("plan [source]").description("What applying a source (every source when omitted) would do: pending files and issues. Exits 7 when a plan has errors").option("--ref <ref>", "branch, tag or commit (default: the source's branch)").option("--live", "read the database now instead of its last snapshot (repository admins)").action(
48509
+ db.command("plan [source]").description("What applying a source (every source when omitted) would do: pending files and issues. Exits 7 when a plan has errors").option("--ref <ref>", "branch, tag or commit (default: the source's branch)").option("--live", "read the database now instead of its last snapshot (migrations:manage)").action(
48147
48510
  act(rt, async (ctx, o, [ref]) => {
48148
48511
  const c8 = ctx.client("user").dbMigrations;
48149
48512
  const q = { ref: o.ref, live: o.live };
@@ -48159,7 +48522,29 @@ function registerDb(program2, rt) {
48159
48522
  if (plans.items.some((p) => !p.ok)) ctx.setExit(EXIT.NOT_READY);
48160
48523
  })
48161
48524
  );
48162
- db.command("refresh <source>").description("Read the applied versions from the database now and show the plan at the source's branch (repository admins)").action(act(rt, async (ctx, _o, [ref]) => ctx.result(await ctx.client("user").dbMigrations.refresh(await sourceId(ctx, ref)), renderPlan2)));
48525
+ db.command("diff <change>").description("What a change's migrations do: per source, the files it adds, lint findings and the schema effect. Exits 7 when a source would block the change").action(
48526
+ act(rt, async (ctx, _o, [change]) => {
48527
+ const plans = await ctx.client("user").dbMigrations.changePlans(await ctx.changeId(change, "user"));
48528
+ ctx.result(
48529
+ plans,
48530
+ (v) => v.items.length ? v.items.map((p) => {
48531
+ const mine = p.pending.filter((f) => f.introduced_by_change);
48532
+ return [`${p.source_name} (${p.provider === "cloudflare_d1" ? "Cloudflare D1" : `PlanetScale ${p.engine}`} ${p.target_label}): ${p.ok ? "ok" : "blocked"}`, mine.length ? `this change adds: ${mine.map((f) => f.name).join(", ")}` : "this change adds no migrations", ...issueLines(p.issues), ...lintLines(p.lint), ...diffLines(mine.length ? p.schema_diff : null)].join("\n");
48533
+ }).join("\n\n") : "no migration sources follow the change's target branch"
48534
+ );
48535
+ if (plans.items.some((p) => !p.ok)) ctx.setExit(EXIT.NOT_READY);
48536
+ })
48537
+ );
48538
+ db.command("drift <source>").description("The source's last drift check: its database against what the applied migrations produce. --check reads the database now (migrations:manage)").option("--check", "check now").action(
48539
+ act(rt, async (ctx, o, [ref]) => {
48540
+ const c8 = ctx.client("user").dbMigrations;
48541
+ const id2 = await sourceId(ctx, ref);
48542
+ const d = o.check ? await c8.checkDrift(id2) : (await c8.sources.get(id2)).drift;
48543
+ ctx.result(d, renderDrift);
48544
+ if (d?.status === "drift") ctx.setExit(EXIT.NOT_READY);
48545
+ })
48546
+ );
48547
+ db.command("refresh <source>").description("Read the applied versions from the database now and show the plan at the source's branch (migrations:manage)").action(act(rt, async (ctx, _o, [ref]) => ctx.result(await ctx.client("user").dbMigrations.refresh(await sourceId(ctx, ref)), renderPlan2)));
48163
48548
  db.command("runs").description("Migration runs of --repo, newest first").option("--source <source>", "only this source (name or id)").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--cursor <cursor>", "page cursor").action(
48164
48549
  act(rt, async (ctx, o) => {
48165
48550
  const repo = { id: await ctx.repoId(void 0, "user") };
@@ -48168,7 +48553,7 @@ function registerDb(program2, rt) {
48168
48553
  })
48169
48554
  );
48170
48555
  db.command("run <run>").description("One migration run: status, files and issues").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").dbMigrations.runs.get(id2), renderRun)));
48171
- db.command("apply <source>").description("Apply a source's pending migrations now (repository admins; your request is the approval). Requires --yes when non-interactive").option("--ref <ref>", "branch, tag or commit to apply (default: the source's branch)").action(
48556
+ db.command("apply <source>").description("Apply a source's pending migrations now (migrations:manage). Your request is the approval when you may approve and the source is not protected; otherwise the run waits for approval. Requires --yes when non-interactive").option("--ref <ref>", "branch, tag or commit to apply (default: the source's branch)").action(
48172
48557
  act(rt, async (ctx, o, [ref]) => {
48173
48558
  const id2 = await sourceId(ctx, ref);
48174
48559
  await ctx.confirm(`apply the pending migrations of ${ref}${o.ref ? ` at ${o.ref}` : ""} to its database`);
@@ -48179,24 +48564,26 @@ ${renderPlan2(v.plan)}`);
48179
48564
  })
48180
48565
  );
48181
48566
  const decisions = [
48182
- ["approve", "Approve a run waiting for approval: Brnch applies exactly the files it shows (repository admins)"],
48183
- ["reject", "Reject a run waiting for approval; nothing is applied (repository admins)"],
48184
- ["retry", "Retry a failed run from where it stopped (repository admins)"],
48185
- ["cancel", "Cancel a run that has not finished (repository admins)"]
48567
+ ["approve", "Approve a run waiting for approval: Brnch applies exactly the files it shows once it has the approvals it needs (migrations:approve; a protected source's approvers)"],
48568
+ ["reject", "Reject a run waiting for approval; nothing is applied (migrations:approve)"],
48569
+ ["retry", "Retry a failed run from where it stopped (migrations:approve)"],
48570
+ ["cancel", "Cancel a run that has not finished (migrations:approve)"]
48186
48571
  ];
48187
48572
  for (const [decision, description] of decisions) {
48188
- db.command(`${decision} <run>`).description(description).option("--reason <text>", "why (recorded with the decision)").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client("user").dbMigrations[decision](id2, o.reason), renderRun)));
48573
+ const cmd = db.command(`${decision} <run>`).description(description).option("--reason <text>", "why (recorded with the decision)");
48574
+ if (decision === "approve") cmd.option("--redeploy", "a run a production deploy waits on: deploy the same commit again, which applies it");
48575
+ cmd.action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client("user").dbMigrations[decision](id2, o.redeploy ? { reason: o.reason, redeploy: true } : o.reason), renderRun)));
48189
48576
  }
48190
48577
  }
48191
48578
 
48192
48579
  // packages/cli/src/commands/deploy.ts
48193
48580
  var TERMINAL4 = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "superseded", "blocked"]);
48194
48581
  var NO_DO_PREVIEWS = "previews off: Cloudflare makes no version URLs for Workers that implement a Durable Object (--previews --preview-mode worker_previews uses Worker Previews; it needs a `previews` block in the Wrangler configuration)";
48195
- var renderTargets = (v) => table(
48196
- v.items.map((t) => ({ ...t, resource: t.resource_name, env: t.environment ?? "-", last_production: t.last_production ? `${t.last_production.status} ${t.last_production.commit_oid.slice(0, 12)}` : "-" })),
48582
+ var renderTargets = (v) => listWithCursor(
48583
+ { ...v, items: v.items.map((t) => ({ ...t, resource: t.resource_name, env: t.environment ?? "-", last_production: t.last_production ? `${t.last_production.status} ${t.last_production.commit_oid.slice(0, 12)}` : "-" })) },
48197
48584
  ["name", "id", "platform", "resource", "env", "branch", "runs_on", "previews", "state", "last_production"]
48198
48585
  );
48199
- var renderTarget = (t) => kv({ ...t, watch_paths: t.watch_paths?.length ? t.watch_paths.join(" ") : "every change", connection: t.connection ? `${t.connection.name} (${t.connection.id}, ${t.connection.state})` : "-", last_production: t.last_production ? `${t.last_production.id} ${t.last_production.status}` : "-" }, [
48586
+ var renderTarget = (t) => kv({ ...t, approvers: ruleSummary(t.approval_rule), watch_paths: t.watch_paths?.length ? t.watch_paths.join(" ") : "every change", connection: t.connection ? `${t.connection.name} (${t.connection.id}, ${t.connection.state})` : "-", last_production: t.last_production ? `${t.last_production.id} ${t.last_production.status}` : "-" }, [
48200
48587
  "id",
48201
48588
  "name",
48202
48589
  "platform",
@@ -48214,12 +48601,13 @@ var renderTarget = (t) => kv({ ...t, watch_paths: t.watch_paths?.length ? t.watc
48214
48601
  "watch_paths",
48215
48602
  "previews",
48216
48603
  "preview_check_name",
48604
+ "approvers",
48217
48605
  "last_production",
48218
48606
  "version"
48219
48607
  ]);
48220
48608
  var renderDeployment = (d) => kv({ ...d, change: d.change ? `#${d.change.number}` : "-", logs: d.run?.logs_url ?? "-" }, ["id", "target_name", "kind", "environment", "status", "status_reason", "trigger", "commit_oid", "change", "version_id", "url", "alias_url", "verified", "duration_ms", "logs", "created_at"]) + (d.steps.length ? `
48221
48609
 
48222
- ${table(d.steps, ["name", "phase", "status", "summary"])}` : "");
48610
+ ${table(d.steps, ["name", "phase", "status", "summary"])}` : "") + approvalLines(d.approval);
48223
48611
  var renderDeployments = (v) => listWithCursor({ ...v, items: v.items.map((d) => ({ ...d, commit: d.commit_oid.slice(0, 12), change: d.change ? `#${d.change.number}` : "-" })) }, ["id", "target_name", "kind", "status", "commit", "change", "trigger", "created_at"]);
48224
48612
  async function pick3(ctx, what, flag, options, label2) {
48225
48613
  if (options.length === 1) return options[0];
@@ -48235,10 +48623,12 @@ ${lines2}
48235
48623
  }
48236
48624
  async function targetId(ctx, ref) {
48237
48625
  if (ref.startsWith("dpt_")) return ref;
48238
- const { items } = await ctx.client("user").deploy.targets.list(await ctx.repoId(void 0, "user"));
48239
- const t = items.find((x) => x.name === ref);
48240
- if (!t) throw new CliError(`no deploy target named "${ref}" (have: ${items.map((x) => x.name).join(", ") || "none"})`, EXIT.NOT_FOUND, "NOT_FOUND");
48241
- return t.id;
48626
+ const client = ctx.client("user");
48627
+ const repo = await ctx.repoId(void 0, "user");
48628
+ const t = (await client.deploy.targets.list(repo, { name: ref })).items.find((x) => x.name === ref);
48629
+ if (t) return t.id;
48630
+ const { items, next_cursor } = await client.deploy.targets.list(repo, { limit: 20 });
48631
+ throw new CliError(`no deploy target named "${ref}" (have: ${items.map((x) => x.name).join(", ") || "none"}${next_cursor ? ", \u2026" : ""})`, EXIT.NOT_FOUND, "NOT_FOUND");
48242
48632
  }
48243
48633
  var orNull = (v) => v === void 0 ? void 0 : v || null;
48244
48634
  async function stepsOf(ctx, spec) {
@@ -48355,7 +48745,23 @@ function registerDeploy(program2, rt) {
48355
48745
  Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}`);
48356
48746
  })
48357
48747
  );
48358
- deploy.command("targets").description("Deploy targets of --repo").action(act(rt, async (ctx) => ctx.result(await ctx.client("user").deploy.targets.list(await ctx.repoId(void 0, "user")), renderTargets)));
48748
+ const targets2 = deploy.command("targets").description("Deploy targets of --repo; `protect` requires approvers for a target's production deploys").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--cursor <cursor>", "page cursor").action(act(rt, async (ctx, o) => ctx.result(await ctx.client("user").deploy.targets.list(await ctx.repoId(void 0, "user"), { limit: o.limit, cursor: o.cursor }), renderTargets)));
48749
+ protectOptions(targets2.command("protect <target>").description("Require approvals for the target's production deploys and rollbacks (people with deploy:manage). Replaces the current rule")).action(
48750
+ act(rt, async (ctx, o, [ref]) => {
48751
+ const c8 = ctx.client("user").deploy.targets;
48752
+ const t = await c8.get(await targetId(ctx, ref));
48753
+ ctx.result(await c8.protect(t.id, protectBody(o, t.approval_rule)), renderRule);
48754
+ })
48755
+ );
48756
+ targets2.command("unprotect <target>").description("Remove the target's approval rule; deploys waiting for it go on. Requires --yes when non-interactive").action(
48757
+ act(rt, async (ctx, _o, [ref]) => {
48758
+ const id2 = await targetId(ctx, ref);
48759
+ await ctx.confirm(`remove the approval rule of ${ref}: its production deploys will no longer wait for approval`);
48760
+ await ctx.client("user").deploy.targets.unprotect(id2);
48761
+ ctx.result({ unprotected: id2 }, `${ref} is no longer protected`);
48762
+ })
48763
+ );
48764
+ targets2.command("approvers <target>").description("Who may approve the target's production deploys now").action(act(rt, async (ctx, _o, [ref]) => ctx.result(await ctx.client("user").deploy.targets.approvers(await targetId(ctx, ref)), renderApprovers)));
48359
48765
  deploy.command("target <target>").description("One deploy target (id, or name in --repo)").action(act(rt, async (ctx, _o, [ref]) => ctx.result(await ctx.client("user").deploy.targets.get(await targetId(ctx, ref)), renderTarget)));
48360
48766
  targetOptions(deploy.command("update <target>").description("Change a deploy target (only the given flags; an empty value clears a command or the environment)")).option("--disable", "stop deploying (merges no longer deploy)").option("--enable", "deploy again").action(
48361
48767
  act(rt, async (ctx, o, [ref]) => {
@@ -48414,6 +48820,8 @@ Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}
48414
48820
  );
48415
48821
  deploy.command("show <deployment>").description("One deployment with its steps").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").deploy.deployments.get(id2), renderDeployment)));
48416
48822
  deploy.command("cancel <deployment>").description("Cancel a queued or running deployment").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").deploy.deployments.cancel(id2), renderDeployment)));
48823
+ deploy.command("approve <deployment>").description("Approve a deploy of a protected target (people who may approve; the last approval needed starts it)").action(act(rt, async (ctx, _o, [id2]) => ctx.result(await ctx.client("user").deploy.deployments.approve(id2), renderDeployment)));
48824
+ deploy.command("reject <deployment>").description("Reject a deploy waiting for approval; it is cancelled").option("--reason <text>", "why (recorded with the decision)").action(act(rt, async (ctx, o, [id2]) => ctx.result(await ctx.client("user").deploy.deployments.reject(id2, o.reason), renderDeployment)));
48417
48825
  deploy.command("rollback <target>").description("Send all traffic back to the version of an earlier verified production deployment").requiredOption("--to <deployment>", "the deployment to roll back to (dpl_\u2026)").option("--message <text>", "deployment message").action(
48418
48826
  act(rt, async (ctx, o, [ref]) => {
48419
48827
  const id2 = await targetId(ctx, ref);
@@ -48426,7 +48834,6 @@ Merges to ${v.branch} deploy automatically. Deploy now: brnch deploy run ${v.id}
48426
48834
  // packages/cli/src/commands/delegation.ts
48427
48835
  var ITERATION_DONE = /* @__PURE__ */ new Set(["succeeded", "no_changes", "failed", "cancelled"]);
48428
48836
  var STATES = ["working", "waiting", "stopped"];
48429
- var who2 = (a) => a.display_name ?? `${a.type} ${a.id}`;
48430
48837
  var short2 = (oid) => oid ? oid.slice(0, 12) : null;
48431
48838
  function vendorArg(v) {
48432
48839
  if (!CODING_AGENT_VENDORS.includes(v)) throw usage(`unknown agent "${v}"; one of ${CODING_AGENT_VENDORS.join(", ")}`);
@@ -48446,7 +48853,7 @@ function renderIterations(d) {
48446
48853
  const rows2 = d.iterations.map((i) => ({
48447
48854
  ...i,
48448
48855
  trigger: i.trigger_ref ? `${i.trigger} (${i.trigger_ref})` : i.trigger,
48449
- by: who2(i.trigger_actor),
48856
+ by: actorLabel(i.trigger_actor),
48450
48857
  head: short2(i.head_oid)
48451
48858
  }));
48452
48859
  const claims = d.iterations.map(reported).filter((x) => !!x);
@@ -48468,7 +48875,7 @@ var renderDelegation = (d) => kv(
48468
48875
  ...d,
48469
48876
  agent: `${d.agent.name} (${d.agent.vendor})`,
48470
48877
  status: delegationStatusLabel(d),
48471
- sponsor: who2(d.sponsor),
48878
+ sponsor: actorLabel(d.sponsor),
48472
48879
  branch: `${d.source_ref} \u2192 ${d.target_ref}`,
48473
48880
  automatic: `on review ${d.auto_on_review ? "on" : "off"}, on check failure ${d.auto_on_check_failure ? "on" : "off"}`,
48474
48881
  budget: `iterations ${d.usage.iterations_used}/${d.budget.max_iterations}, tokens ${d.usage.tokens_used}/${d.budget.max_tokens}, ${d.budget.iteration_minutes} min per iteration`
@@ -49029,15 +49436,78 @@ function authGuidance(cli, helperConfigured) {
49029
49436
  }
49030
49437
 
49031
49438
  // packages/cli/src/commands/repo.ts
49032
- var REPO_FIELDS = ["id", "org_slug", "name", "description", "visibility", "default_branch", "lifecycle", "clone_url", "updated_at"];
49439
+ var REPO_FIELDS = ["id", "org_slug", "name", "description", "visibility", "default_branch", "lifecycle", "clone_url", "actions_enabled", "updated_at"];
49033
49440
  var renderRepo2 = (r) => kv(r, REPO_FIELDS) + (r.import ? `
49034
49441
  import ${r.import.status}${r.import.shallow ? " (shallow)" : ""}` : "");
49035
49442
  function renderLog(p) {
49036
49443
  const rows2 = p.items.map((c8) => ({ oid: c8.oid.slice(0, 12), date: c8.committer.date.slice(0, 10), author: c8.author.name, subject: c8.message.split("\n", 1)[0] }));
49037
49444
  const lines2 = [table(rows2, ["oid", "date", "author", "subject"])];
49038
49445
  const s = p.path_scan;
49446
+ const l = p.log_scan;
49039
49447
  if (s) lines2.push(s.complete ? `scanned ${s.scanned_total} commits: full first-parent history of ${s.path}` : `scanned ${s.scanned_total} commits so far; older history not searched yet (more: --cursor ${p.next_cursor})`);
49040
- else if (p.next_cursor) lines2.push(`(more: --cursor ${p.next_cursor})`);
49448
+ else if (l) {
49449
+ if (l.complete) lines2.push(`scanned ${l.scanned_total} commits: all matching history searched`);
49450
+ else if (l.exhausted) lines2.push(`scanned ${l.scanned_total} commits, the most one search walks; older history not searched`);
49451
+ else lines2.push(`scanned ${l.scanned_total} commits so far; older history not searched yet (more: --cursor ${p.next_cursor})`);
49452
+ } else if (p.next_cursor) lines2.push(`(more: --cursor ${p.next_cursor})`);
49453
+ return lines2.join("\n");
49454
+ }
49455
+ function renderCommitChange(r) {
49456
+ const lines2 = r.landings.map((l) => {
49457
+ const how2 = l.role === "landing" ? `${l.strategy} commit of` : `brought in by ${l.strategy} of`;
49458
+ const receipt = l.receipt_id ? `, receipt ${l.receipt_id}` : "";
49459
+ return `${l.target_ref.replace(/^refs\/heads\//, "")}: ${how2} #${l.change.number} ${l.change.title} (${l.change.state}; landing ${l.landing_oid.slice(0, 12)}${receipt})`;
49460
+ });
49461
+ if (!lines2.length) lines2.push(r.indexing ? "no change on record yet: earlier landings are still being indexed, ask again shortly" : "no change brought this commit (pushed straight to a branch, or older than the repository on Brnch)");
49462
+ else if (r.indexing) lines2.push("(earlier landings are still being indexed)");
49463
+ return lines2.join("\n");
49464
+ }
49465
+ var renderTree = (t) => [`${t.path || "/"} at ${t.commit_oid.slice(0, 12)}`, table(t.entries.map((e) => ({ ...e, name: e.type === "tree" ? `${e.name}/` : e.name })), ["mode", "type", "name"])].join("\n");
49466
+ function renderBlob(b) {
49467
+ if (b.binary) return `(binary file, ${b.size} bytes; ${BRAND.cli} --json repo blob for its id)`;
49468
+ if (b.truncated || b.encoding !== "utf8") return `(${b.size} bytes, too large to show inline: clone, or download ${b.path} at ${b.commit_oid.slice(0, 12)})`;
49469
+ return b.content;
49470
+ }
49471
+ function renderBlame(b) {
49472
+ const rows2 = b.lines.map((l) => ({ line: l.line, oid: l.commit_oid.slice(0, 12), date: l.date.slice(0, 10), author: l.author, summary: l.summary }));
49473
+ const lines2 = [table(rows2, ["line", "oid", "date", "author", "summary"])];
49474
+ if (b.partial) lines2.push("partial: the walk ran out of time, older lines may belong to newer commits; the full blame is being computed, ask again shortly");
49475
+ else if (b.truncated) lines2.push("history not walked to the end: unresolved lines are attributed to the oldest commit examined");
49476
+ return lines2.join("\n");
49477
+ }
49478
+ function renderSearch(r) {
49479
+ const rows2 = r.items.map((i) => ({ kind: i.kind, where: i.path ?? i.title, snippet: i.snippet.replace(/\s+/g, " ").slice(0, 100) }));
49480
+ const lines2 = [table(rows2, ["kind", "where", "snippet"])];
49481
+ const ref = r.index.ref;
49482
+ if (ref) {
49483
+ lines2.push(
49484
+ ref.complete ? `code as of ${ref.name} (${ref.commit_oid.slice(0, 12)}): the index plus the ${ref.searched_files} files it changed` : `incomplete: ${ref.name} changed ${ref.changed_files}${ref.changed_files >= 500 ? "+" : ""} files and ${ref.searched_files} were searched; a missing hit is not proof of absence`
49485
+ );
49486
+ } else if (r.index.commit_oid && !r.index.fresh) lines2.push(`code index is behind the default branch (indexed ${r.index.commit_oid.slice(0, 12)}); catching up`);
49487
+ else if (!r.index.commit_oid && r.index.indexed_at === null) lines2.push("code index not built yet; catching up");
49488
+ if (r.index.truncated) lines2.push("the code index stopped at its size bounds: some files are not searchable");
49489
+ return lines2.join("\n");
49490
+ }
49491
+ function renderCompare(c8) {
49492
+ const rows2 = c8.files.map((f) => ({ status: f.status, path: f.status === "renamed" ? `${f.old_path} \u2192 ${f.new_path}` : f.new_path ?? f.old_path }));
49493
+ const head = c8.mode === "two-dot" ? `${c8.base_oid.slice(0, 12)}..${c8.head_oid.slice(0, 12)} (trees)` : `${c8.base_oid.slice(0, 12)}...${c8.head_oid.slice(0, 12)} (since merge base ${c8.merge_base_oid?.slice(0, 12) ?? "none: unrelated histories"})`;
49494
+ return [head, table(rows2, ["status", "path"]), ...c8.truncated ? ["(more files changed than listed)"] : []].join("\n");
49495
+ }
49496
+ function renderRange(r) {
49497
+ const rows2 = r.commits.map((c8) => ({ oid: c8.oid.slice(0, 12), date: c8.committer.date.slice(0, 10), author: c8.author.name, subject: c8.message.split("\n", 1)[0] }));
49498
+ const count = (n) => n === null ? "unknown" : String(n);
49499
+ return [
49500
+ `ahead ${count(r.ahead_by)}, behind ${count(r.behind_by)}`,
49501
+ table(rows2, ["oid", "date", "author", "subject"]),
49502
+ ...r.truncated ? ["(more commits than listed: --limit up to 250)"] : [],
49503
+ ...r.complete ? [] : ["incomplete: the bounded history walk did not finish; counts and the list may be short"]
49504
+ ].join("\n");
49505
+ }
49506
+ var renderMergeBase = (m) => m.merge_bases.length ? m.merge_bases.join("\n") : m.complete ? "(none: unrelated histories)" : "unknown: the bounded history walk did not finish";
49507
+ var renderAncestry = (a) => a.is_ancestor === null ? "unknown: the bounded history walk could not decide (not a no)" : `${a.is_ancestor ? "yes" : "no"}: ${a.ancestor_oid.slice(0, 12)} is ${a.is_ancestor ? "" : "not "}in the history of ${a.descendant_oid.slice(0, 12)}`;
49508
+ function renderContaining(r) {
49509
+ const lines2 = r.refs.length ? [...r.refs] : [r.complete ? "(no branch or tag contains it)" : "(none found so far)"];
49510
+ if (r.unknown.length) lines2.push(`not settled (may contain it): ${r.unknown.join(", ")}`);
49041
49511
  return lines2.join("\n");
49042
49512
  }
49043
49513
  function registerOrg(program2, rt) {
@@ -49048,6 +49518,14 @@ function registerOrg(program2, rt) {
49048
49518
  ctx.result(created, (v) => kv(v, ["id", "slug", "name", "created_at"]));
49049
49519
  })
49050
49520
  );
49521
+ org.command("edit [org]").description("Change an organization's settings (organization admins)").option("--actions <on|off>", "turn Actions on or off for every repository of the organization: off starts no runs from pushes, changes or schedules and refuses workflow_dispatch; runs already started go on. A repository can also turn it off for itself (repo edit --actions)").action(
49522
+ act(rt, async (ctx, o, [spec]) => {
49523
+ if (o.actions === void 0) throw usage("nothing to change: pass --actions on|off");
49524
+ if (o.actions !== "on" && o.actions !== "off") throw usage("--actions must be on or off");
49525
+ const updated = await ctx.client("user").orgs.update(spec ?? ctx.org(), { actions_enabled: o.actions === "on" });
49526
+ ctx.result(updated, (v) => kv({ ...v, actions: v.settings.actions_enabled === false ? "off" : "on" }, ["id", "slug", "name", "actions"]));
49527
+ })
49528
+ );
49051
49529
  org.command("list").description("Your personal account and the organizations you belong to").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--cursor <cursor>", "page cursor").action(
49052
49530
  act(rt, async (ctx, o) => {
49053
49531
  const page = await ctx.client("user").orgs.list({ limit: o.limit, cursor: o.cursor });
@@ -49150,7 +49628,18 @@ function registerRepo(program2, rt) {
49150
49628
  await new Promise((r) => setTimeout(r, 2e3));
49151
49629
  job2 = await c8.migrations.get(job2.id);
49152
49630
  }
49153
- const view = { id: job2.id, state: job2.state, issues: job2.report.counts.issues, pull_requests: job2.report.counts.pull_requests, comments: job2.report.counts.comments, releases: job2.report.counts.releases, missing: job2.report.missing.join("; ") || "-" };
49631
+ const deps = job2.report.dependencies;
49632
+ const view = {
49633
+ id: job2.id,
49634
+ state: job2.state,
49635
+ issues: job2.report.counts.issues,
49636
+ pull_requests: job2.report.counts.pull_requests,
49637
+ comments: job2.report.counts.comments,
49638
+ releases: job2.report.counts.releases,
49639
+ ...deps ? { blocked_by_links: `${deps.imported} imported${deps.skipped ? `, ${deps.skipped} skipped` : ""}` } : {},
49640
+ missing: job2.report.missing.join("; ") || "-",
49641
+ warnings: job2.report.warnings.join("; ") || "-"
49642
+ };
49154
49643
  ctx.result(job2, () => kv(view, Object.keys(view)));
49155
49644
  if (o.wait && job2.state !== "completed") throw new CliError(`migration ${job2.state}: ${job2.error ?? job2.report.missing.join("; ")}`, EXIT.NOT_READY);
49156
49645
  })
@@ -49166,13 +49655,93 @@ function registerRepo(program2, rt) {
49166
49655
  ctx.result(await ctx.client("agent").repos.get(await ctx.repoId(spec)), renderRepo2);
49167
49656
  })
49168
49657
  );
49169
- repo.command("log [repo]").description("First-parent commit history of a ref; with --path, only commits that changed that file or directory (no rename detection)").option("--ref <ref>", "branch, tag or commit (default branch when omitted)").option("--path <path>", "repository-relative file or directory").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--scan-limit <n>", "with --path: commits examined per page (server default 200, max 500)", (v) => parseIntOption(v, "--scan-limit")).option("--cursor <cursor>", "page cursor (resumes a --path walk where it stopped)").action(
49658
+ repo.command("edit [repo]").description("Change a repository's settings (repository admins)").option("--actions <on|off>", "turn Actions on or off: off starts no runs from pushes, changes or schedules and refuses workflow_dispatch; runs already started go on").action(
49170
49659
  act(rt, async (ctx, o, [spec]) => {
49171
- if (o.scanLimit !== void 0 && o.path === void 0) throw usage("--scan-limit requires --path");
49172
- const page = await ctx.client("agent").repos.commits(await ctx.repoId(spec), { ref: o.ref, path: o.path, limit: o.limit, cursor: o.cursor, scan_limit: o.scanLimit });
49660
+ if (o.actions === void 0) throw usage("nothing to change: pass --actions on|off");
49661
+ if (o.actions !== "on" && o.actions !== "off") throw usage("--actions must be on or off");
49662
+ const c8 = ctx.client("user");
49663
+ const id2 = await ctx.repoId(spec);
49664
+ const current = await c8.repos.get(id2);
49665
+ ctx.result(await c8.repos.update(id2, { actions_enabled: o.actions === "on", version: current.version }), renderRepo2);
49666
+ })
49667
+ );
49668
+ repo.command("log [repo]").description("First-parent commit history of a ref; with --path, only commits that changed that file or directory (no rename detection); filter like git log with --author, --grep, --since, --until (substring matches) and --all-parents (with --path: git log --full-history)").option("--ref <ref>", "branch, tag or commit (default branch when omitted)").option("--path <path>", "repository-relative file or directory").option("--limit <n>", "page size", (v) => parseIntOption(v, "--limit")).option("--scan-limit <n>", "with --path or a filter: commits examined per page (server default 200, 50 with --all-parents; max 500)", (v) => parseIntOption(v, "--scan-limit")).option("--author <text>", "author name or email contains this (case-insensitive)").option("--grep <text>", "commit message contains this (case-insensitive; not a regular expression)").option("--since <date>", "committed at or after this date (ISO-8601)").option("--until <date>", "committed at or before this date (ISO-8601)").option("--all-parents", "walk merged branches too, not just the first-parent line").option("--cursor <cursor>", "page cursor (resumes a --path walk where it stopped)").action(
49669
+ act(rt, async (ctx, o, [spec]) => {
49670
+ const filtered = o.author !== void 0 || o.grep !== void 0 || o.since !== void 0 || o.until !== void 0 || !!o.allParents;
49671
+ if (o.scanLimit !== void 0 && o.path === void 0 && !filtered) throw usage("--scan-limit requires --path or a filter");
49672
+ const page = await ctx.client("agent").repos.commits(await ctx.repoId(spec), {
49673
+ ref: o.ref,
49674
+ path: o.path,
49675
+ limit: o.limit,
49676
+ cursor: o.cursor,
49677
+ scan_limit: o.scanLimit,
49678
+ author: o.author,
49679
+ grep: o.grep,
49680
+ since: o.since,
49681
+ until: o.until,
49682
+ all_parents: o.allParents || void 0
49683
+ });
49173
49684
  ctx.result(page, renderLog);
49174
49685
  })
49175
49686
  );
49687
+ repo.command("tree [repo]").description("List a directory of a ref without a clone").option("--ref <ref>", "branch, tag or commit (default branch when omitted)").option("--path <path>", "repository-relative directory (root when omitted)").action(
49688
+ act(rt, async (ctx, o, [spec]) => {
49689
+ ctx.result(await ctx.client("agent").repos.tree(await ctx.repoId(spec), { ref: o.ref, path: o.path }), renderTree);
49690
+ })
49691
+ );
49692
+ repo.command("blob <path> [repo]").description("Print a file of a ref without a clone (binary and very large files are described, not printed)").option("--ref <ref>", "branch, tag or commit (default branch when omitted)").action(
49693
+ act(rt, async (ctx, o, [path3, spec]) => {
49694
+ ctx.result(await ctx.client("agent").repos.blob(await ctx.repoId(spec), { ref: o.ref, path: path3 }), renderBlob);
49695
+ })
49696
+ );
49697
+ repo.command("blame <path> [repo]").description("Who last changed each line of a file, without a clone").option("--ref <ref>", "branch, tag or commit (default branch when omitted)").action(
49698
+ act(rt, async (ctx, o, [path3, spec]) => {
49699
+ ctx.result(await ctx.client("agent").repos.blame(await ctx.repoId(spec), { ref: o.ref, path: path3 }), renderBlame);
49700
+ })
49701
+ );
49702
+ repo.command("search <query> [repo]").description("Search code, tasks, changes and comments; with --ref, code as of that branch, tag or commit (says when a ref was searched incompletely)").option("--kind <kind>", "code, task, change or comment").option("--ref <ref>", "search code as of this branch, tag or commit instead of the default branch").action(
49703
+ act(rt, async (ctx, o, [q, spec]) => {
49704
+ if (o.kind !== void 0 && !["code", "task", "change", "comment"].includes(o.kind)) throw usage("--kind must be code, task, change or comment");
49705
+ if (o.ref !== void 0 && o.kind !== void 0 && o.kind !== "code") throw usage("--ref searches code only");
49706
+ ctx.result(await ctx.client("agent").repos.search(await ctx.repoId(spec), { q, kind: o.kind, ref: o.ref }), renderSearch);
49707
+ })
49708
+ );
49709
+ repo.command("compare <base> <head> [repo]").description("Files head changed since its merge base with base (git diff base...head); --two-dot compares the trees; --commits lists the commits in base..head with ahead/behind counts").option("--two-dot", "diff base's tree against head's tree (git diff base head)").option("--commits", "list the commits in base..head instead of the files").option("--limit <n>", "with --commits: commits listed (default 100, max 250)", (v) => parseIntOption(v, "--limit")).action(
49710
+ act(rt, async (ctx, o, [base2, head, spec]) => {
49711
+ if (o.commits && o.twoDot) throw usage("--commits cannot be combined with --two-dot");
49712
+ if (o.limit !== void 0 && !o.commits) throw usage("--limit requires --commits");
49713
+ const c8 = ctx.client("agent");
49714
+ const id2 = await ctx.repoId(spec);
49715
+ if (o.commits) return ctx.result(await c8.repos.compareCommits(id2, { base: base2, head, limit: o.limit }), renderRange);
49716
+ ctx.result(await c8.repos.compare(id2, { base: base2, head, mode: o.twoDot ? "two-dot" : void 0 }), renderCompare);
49717
+ })
49718
+ );
49719
+ repo.command("merge-base <base> <head> [repo]").description("Every best common ancestor of two branches, tags or commits (git merge-base --all), without a clone").action(
49720
+ act(rt, async (ctx, _o, [base2, head, spec]) => {
49721
+ ctx.result(await ctx.client("agent").repos.mergeBase(await ctx.repoId(spec), { base: base2, head }), renderMergeBase);
49722
+ })
49723
+ );
49724
+ repo.command("is-ancestor <ancestor> <descendant> [repo]").description("Whether a commit is in another's history (git merge-base --is-ancestor): yes, no, or unknown when the bounded walk cannot decide").action(
49725
+ act(rt, async (ctx, _o, [ancestor, descendant, spec]) => {
49726
+ ctx.result(await ctx.client("agent").repos.isAncestor(await ctx.repoId(spec), { ancestor, descendant }), renderAncestry);
49727
+ })
49728
+ );
49729
+ repo.command("contains <commit> [repo]").description("Branches and tags whose history contains a commit (git for-each-ref --contains), without a clone").action(
49730
+ act(rt, async (ctx, _o, [commit, spec]) => {
49731
+ ctx.result(await ctx.client("agent").repos.refsContaining(await ctx.repoId(spec), commit), renderContaining);
49732
+ })
49733
+ );
49734
+ repo.command("commit-change <commit> [repo]").description("The change that brought a commit onto each branch it landed on, from Brnch's record of the landing (every merge strategy); ask this instead of searching git log").action(
49735
+ act(rt, async (ctx, _o, [commit, spec]) => {
49736
+ let oid = /^[0-9a-f]{40}$/.test(commit) ? commit : null;
49737
+ if (!oid) {
49738
+ const r = await ctx.deps.gitOutput(["rev-parse", "--verify", "--quiet", `${commit}^{commit}`], ctx.deps.cwd);
49739
+ if (r.code !== 0) throw usage("pass a full 40-character commit id; short ids and revisions like HEAD resolve only inside a clone");
49740
+ oid = r.stdout.trim();
49741
+ }
49742
+ ctx.result(await ctx.client("agent").repos.commitChange(await ctx.repoId(spec), oid), renderCommitChange);
49743
+ })
49744
+ );
49176
49745
  repo.command("clone [repo] [directory]").description("git clone over SSH when this machine is set up for it (`ssh setup`) and the key can be used now, else HTTPS with your login (never written to .git/config); at a terminal without a repository, pick one of yours").option("--ssh", "clone over SSH").option("--https", "clone over HTTPS").action(
49177
49746
  act(rt, async (ctx, o, [spec, dir]) => {
49178
49747
  if (o.ssh && o.https) throw usage("--ssh and --https are mutually exclusive");
@@ -49183,24 +49752,39 @@ function registerRepo(program2, rt) {
49183
49752
  if (t.transport === "ssh") {
49184
49753
  if (!t.ssh.ready) throw usage(`this machine is not set up for SSH${t.ssh.alias ? ` to ${t.ssh.alias}` : ""} yet: run \`${BRAND.cli} ssh setup\`, or clone with --https`);
49185
49754
  const url2 = sshCloneUrl(t.ssh.alias, r.org_slug, r.name);
49186
- const code2 = await ctx.deps.spawnGit(["clone", "--", url2, directory], { env: { ...ctx.deps.env, GIT_TERMINAL_PROMPT: "0" }, cwd: ctx.deps.cwd, stdoutToStderr: true });
49755
+ const code2 = await ctx.deps.spawnGit([...SNAPSHOT_CLONE, "clone", "--", url2, directory], { env: { ...ctx.deps.env, GIT_TERMINAL_PROMPT: "0" }, cwd: ctx.deps.cwd, stdoutToStderr: true });
49187
49756
  if (code2 !== 0) throw new CliError(`git clone over SSH exited with status ${code2}; \`${BRAND.cli} ssh test\` explains SSH problems, --https clones without SSH`, EXIT.UNEXPECTED, "GIT_FAILED");
49188
49757
  return ctx.result({ repository: r, directory, transport: "ssh", transport_reason: t.reason, clone_url: url2 }, cloned(r, directory, `over SSH (${url2})`, paint(colorEnabled(ctx.deps, "stdout"))));
49189
49758
  }
49190
49759
  if (t.approval) ctx.info(`Cloning over HTTPS: ${t.reason}. Pass --ssh to use SSH anyway.`);
49191
49760
  const token = await ctx.freshToken("agent");
49192
- const code = await ctx.deps.spawnGit(["clone", "--", r.clone_url, directory], {
49761
+ const helper2 = await credentialHelperConfigured(ctx);
49762
+ const list3 = helper2 && await gitAtLeast(ctx, "2.40.0") && await bundlesOffered(ctx, r.clone_url, token) ? [`--bundle-uri=${r.clone_url}/bundles`] : [];
49763
+ const code = await ctx.deps.spawnGit([...SNAPSHOT_CLONE, "clone", ...list3, "--", r.clone_url, directory], {
49193
49764
  env: gitAuthEnv(ctx.deps.env, token, r.clone_url),
49194
49765
  cwd: ctx.deps.cwd,
49195
49766
  stdoutToStderr: true
49196
49767
  });
49197
49768
  if (code !== 0) throw new CliError(`git clone exited with status ${code}`, EXIT.UNEXPECTED, "GIT_FAILED");
49198
- const helper2 = await credentialHelperConfigured(ctx);
49199
49769
  ctx.info(authGuidance(BRAND.cli, helper2));
49200
49770
  ctx.result({ repository: r, directory, transport: "https", transport_reason: t.reason, clone_url: r.clone_url, credential_helper: helper2 }, cloned(r, directory, "over HTTPS", paint(colorEnabled(ctx.deps, "stdout"))));
49201
49771
  })
49202
49772
  );
49203
49773
  }
49774
+ var SNAPSHOT_CLONE = ["-c", "transfer.bundleURI=true"];
49775
+ async function bundlesOffered(ctx, cloneUrl, token) {
49776
+ try {
49777
+ const res = await ctx.deps.sys.fetch(`${cloneUrl}/bundles`, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5e3) });
49778
+ return res.ok && (await res.text()).includes('[bundle "');
49779
+ } catch {
49780
+ return false;
49781
+ }
49782
+ }
49783
+ async function gitAtLeast(ctx, min) {
49784
+ const r = await ctx.deps.sys.run("git", ["--version"], { timeoutMs: 1e4 }).catch(() => null);
49785
+ const version4 = r?.code === 0 ? /git version (\d+\.\d+\.\d+)/.exec(r.stdout)?.[1] : void 0;
49786
+ return !!version4 && compareVersions(version4, min) >= 0;
49787
+ }
49204
49788
  var hasRepo = (ctx) => {
49205
49789
  try {
49206
49790
  ctx.repoSpec();
@@ -49268,7 +49852,7 @@ function registerGit(program2, rt) {
49268
49852
  }
49269
49853
 
49270
49854
  // packages/cli/src/commands/release.ts
49271
- var renderRelease = (r) => kv({ ...r, author: r.author.display_name ?? r.author.id }, ["id", "tag", "name", "commit_oid", "draft", "author", "created_at", "notes"]);
49855
+ var renderRelease = (r) => kv({ ...r, author: actorLabel(r.author) }, ["id", "tag", "name", "commit_oid", "draft", "author", "created_at", "notes"]);
49272
49856
  function registerRelease(program2, rt) {
49273
49857
  const release = program2.command("release").description("Releases of --repo (a tag with notes)");
49274
49858
  release.command("list").description("Releases, newest first").action(
@@ -49711,7 +50295,7 @@ var renderRangeDiff = (r) => {
49711
50295
  if (r.truncated) lines2.push("(truncated)");
49712
50296
  return lines2.join("\n");
49713
50297
  };
49714
- var renderOperation = (o) => `${o.id} ${o.created_at} ${o.operation}/${o.kind} ${o.ref} ${o.old_oid.slice(0, 12)} \u2192 ${o.new_oid.slice(0, 12)} by ${o.actor.display_name ?? o.actor.id}${o.protected ? " [protected]" : ""}${o.undone_by ? ` (undone by ${o.undone_by})` : ""}`;
50298
+ var renderOperation = (o) => `${o.id} ${o.created_at} ${o.operation}/${o.kind} ${o.ref} ${o.old_oid.slice(0, 12)} \u2192 ${o.new_oid.slice(0, 12)} by ${actorLabel(o.actor)}${o.protected ? " [protected]" : ""}${o.undone_by ? ` (undone by ${o.undone_by})` : ""}`;
49715
50299
  async function waitForPromotion(ctx, p, timeout, interval) {
49716
50300
  const deadline = Date.now() + timeout * 1e3;
49717
50301
  let last = p.state;
@@ -49788,7 +50372,7 @@ next: --cursor ${v.next_cursor}` : "") : listWithCursor(v, ["id"]));
49788
50372
  }
49789
50373
 
49790
50374
  // packages/cli/src/commands/coordination.ts
49791
- var holderOf = (c8) => `${c8.holder.display_name ?? c8.holder.id}${c8.session_id ? ` (${c8.session_id})` : ""}`;
50375
+ var holderOf = (c8) => `${actorLabel(c8.holder)}${c8.session_id ? ` (${c8.session_id})` : ""}`;
49792
50376
  var renderClaim = (r) => [
49793
50377
  `${r.already_held ? "Already yours" : "Claimed"}: ${r.claim.key}${r.claim.change ? ` (change #${r.claim.change.number})` : ""}`,
49794
50378
  ...r.skipped.length ? [`Taken by others: ${r.skipped.join(", ")}`] : [],
@@ -52091,7 +52675,7 @@ function safeLegs(m) {
52091
52675
  }
52092
52676
 
52093
52677
  // packages/cli/src/commands/actions.ts
52094
- var renderRun2 = (r) => kv({ ...r, actor: r.actor.display_name ?? r.actor.id, trust: `${r.definition.source} ${r.definition.ref}${r.definition.trusted ? " (trusted)" : " (contributor-controlled)"}` }, [
52678
+ var renderRun2 = (r) => kv({ ...r, actor: actorLabel(r.actor), trust: `${r.definition.source} ${r.definition.ref}${r.definition.trusted ? " (trusted)" : " (contributor-controlled)"}` }, [
52095
52679
  "id",
52096
52680
  "workflow_name",
52097
52681
  "run_number",