@neriros/ralphy 3.10.14 → 3.10.15

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.
@@ -18928,8 +18928,8 @@ import { readFileSync } from "fs";
18928
18928
  import { resolve } from "path";
18929
18929
  function getVersion() {
18930
18930
  try {
18931
- if ("3.10.14")
18932
- return "3.10.14";
18931
+ if ("3.10.15")
18932
+ return "3.10.15";
18933
18933
  } catch {}
18934
18934
  const dirsToTry = [];
18935
18935
  try {
@@ -19421,7 +19421,7 @@ var init_fields = __esm(() => {
19421
19421
  spec: yes(),
19422
19422
  when: (answers) => typeof answers["repo.name"] === "string" && answers["repo.name"] !== ""
19423
19423
  };
19424
- LINEAR_FILTER_DESCRIPTION = "Global filter applied to every Linear ticket fetch, as an 'assignee = <value>' clause. " + "<value> is 'me' (issues assigned to you), 'any' (regardless of assignee), 'unassigned', " + "or a specific Linear user (email or user-id). Blank defaults to 'assignee = me'.";
19424
+ LINEAR_FILTER_DESCRIPTION = "Global filter ANDed into every Linear ticket fetch: a marker list of 'assignee' and " + "'label' clauses (all required). assignee value is 'me' (assigned to you), 'any' " + "(regardless of assignee), 'unassigned', or a specific Linear user (email or user-id). " + "Add 'label' clauses to require the ticket carry those labels. Defaults to assignee = me.";
19425
19425
  LINEAR_ASSIGNEE_CHOICE = {
19426
19426
  id: LINEAR_ASSIGNEE_CHOICE_FIELD_ID,
19427
19427
  label: "Linear assignee filter",
@@ -19674,30 +19674,24 @@ var init_fields = __esm(() => {
19674
19674
  spec: yes()
19675
19675
  },
19676
19676
  {
19677
- id: "fixCiOnFailure",
19678
- label: "Let the agent fix CI failures?",
19679
- description: "After opening a PR, watch its CI (the automated checks GitHub runs) and let the agent push fixes when they fail.",
19680
- spec: no()
19681
- },
19682
- {
19683
- id: "maxCiFixAttempts",
19684
- label: "Max CI-fix attempts per task",
19685
- description: "Stop trying to fix failing CI after this many attempts.",
19686
- spec: { kind: "number", placeholder: "5" },
19687
- when: isOn("fixCiOnFailure")
19677
+ id: "prRecovery.enabled",
19678
+ label: "Enable PR recovery (conflicts + CI)?",
19679
+ description: "After a worker opens a PR, keep watching it: advance the ticket to done once the PR is mergeable (CI green, no conflicts), and auto-recover red PRs by re-running the agent \u2014 resolving merge conflicts AND fixing failing CI checks. Turn off to mark the ticket done immediately on PR open and do no watching anywhere. (Fine-grained `fixCi` / `fixConflicts` toggles live in WORKFLOW.md, both on by default.)",
19680
+ spec: yes()
19688
19681
  },
19689
19682
  {
19690
- id: "ciPollIntervalSeconds",
19691
- label: "CI status poll interval (seconds)",
19692
- description: "How often (in seconds) to re-check the PR's CI status while waiting on or fixing it.",
19693
- spec: { kind: "number", placeholder: "30" },
19694
- when: isOn("fixCiOnFailure")
19683
+ id: "prRecovery.maxRecoverySessions",
19684
+ label: "Max PR recovery sessions",
19685
+ description: "Give up auto-recovering a red PR after this many recovery sessions, then flag it for a human.",
19686
+ spec: { kind: "number", placeholder: "3" },
19687
+ when: isOn("prRecovery.enabled")
19695
19688
  },
19696
19689
  {
19697
- id: "ignoreCiChecks",
19690
+ id: "prRecovery.ignoreChecks",
19698
19691
  label: "CI checks to ignore",
19699
19692
  description: "Names of CI checks to ignore when deciding whether a PR is green \u2014 e.g. known-flaky jobs.",
19700
- spec: { kind: "list", placeholder: "check name" }
19693
+ spec: { kind: "list", placeholder: "check name" },
19694
+ when: isOn("prRecovery.enabled")
19701
19695
  },
19702
19696
  {
19703
19697
  id: "rules",
@@ -19831,26 +19825,6 @@ var init_fields = __esm(() => {
19831
19825
  spec: { kind: "text", placeholder: "ralph:pre-existing-error" },
19832
19826
  when: isOn("preExistingErrorCheck.enabled")
19833
19827
  },
19834
- {
19835
- id: "prTracker.enabled",
19836
- label: "Enable the PR tracker?",
19837
- description: "Keep watching the PRs Ralphy opened and automatically try to recover any whose merge state goes red (conflicts or failing CI).",
19838
- spec: yes()
19839
- },
19840
- {
19841
- id: "prTracker.maxRecoveryAttempts",
19842
- label: "PR tracker max recovery attempts",
19843
- description: "Give up auto-recovering a red PR after this many attempts, then flag it for a human.",
19844
- spec: { kind: "number", placeholder: "3" },
19845
- when: isOn("prTracker.enabled")
19846
- },
19847
- {
19848
- id: "prTracker.advanceMergedToDone",
19849
- label: "Advance merged PRs to done automatically?",
19850
- description: "Move an issue to its done state as soon as its PR is merged.",
19851
- spec: no(),
19852
- when: isOn("prTracker.enabled")
19853
- },
19854
19828
  {
19855
19829
  id: "metaPrompt.enabled",
19856
19830
  label: "Enable the meta-prompt addendum?",
@@ -81277,11 +81251,11 @@ function foldLegacyAssignee(v) {
81277
81251
  if (rest2["filter"] === undefined) {
81278
81252
  const raw = typeof assignee === "string" ? assignee.trim() : "";
81279
81253
  const value = raw === "" || raw.toLowerCase() === "unassigned" ? "unassigned" : raw;
81280
- rest2["filter"] = `assignee = ${value}`;
81254
+ rest2["filter"] = [{ type: "assignee", value }];
81281
81255
  }
81282
81256
  return rest2;
81283
81257
  }
81284
- var CURRENT_WORKFLOW_VERSION = 5, MarkerSchema, SET_INDICATOR_KEYS, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, DEFAULT_META_ONLY_FILES, BoundariesSchema, WorkflowConfigSchema;
81258
+ var CURRENT_WORKFLOW_VERSION = 6, MarkerSchema, FilterMarkerSchema, LinearFilterSchema, SET_INDICATOR_KEYS, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, DEFAULT_META_ONLY_FILES, BoundariesSchema, WorkflowConfigSchema;
81285
81259
  var init_schema = __esm(() => {
81286
81260
  init_zod();
81287
81261
  MarkerSchema = exports_external.discriminatedUnion("type", [
@@ -81295,6 +81269,19 @@ var init_schema = __esm(() => {
81295
81269
  exports_external.object({ type: exports_external.literal("project"), value: exports_external.string().min(1) }).strict(),
81296
81270
  exports_external.object({ type: exports_external.literal("comment"), value: exports_external.string().min(1) }).strict()
81297
81271
  ]);
81272
+ FilterMarkerSchema = exports_external.discriminatedUnion("type", [
81273
+ exports_external.object({ type: exports_external.literal("label"), value: exports_external.string().min(1) }).strict(),
81274
+ exports_external.object({ type: exports_external.literal("assignee"), value: exports_external.string().min(1) }).strict()
81275
+ ]);
81276
+ LinearFilterSchema = exports_external.array(FilterMarkerSchema).superRefine((markers, ctx) => {
81277
+ const assigneeCount = markers.filter((m) => m.type === "assignee").length;
81278
+ if (assigneeCount > 1) {
81279
+ ctx.addIssue({
81280
+ code: exports_external.ZodIssueCode.custom,
81281
+ message: `linear.filter allows at most one "assignee" clause, found ${assigneeCount}.`
81282
+ });
81283
+ }
81284
+ }).default([{ type: "assignee", value: "me" }]);
81298
81285
  SET_INDICATOR_KEYS = [
81299
81286
  "setInProgress",
81300
81287
  "setDone",
@@ -81412,15 +81399,11 @@ var init_schema = __esm(() => {
81412
81399
  autoMergeStrategy: exports_external.enum(["squash", "merge", "rebase"]).default("squash"),
81413
81400
  manualMergeWhenAutoMergeDisabled: exports_external.boolean().default(true),
81414
81401
  finalizeNoOpAsDone: exports_external.boolean().default(true),
81415
- fixCiOnFailure: exports_external.boolean().default(false),
81416
- maxCiFixAttempts: exports_external.number().int().positive().default(5),
81417
- ciPollIntervalSeconds: exports_external.number().int().positive().default(30),
81418
- ignoreCiChecks: exports_external.array(exports_external.string()).default([]),
81419
81402
  engine: exports_external.enum(["claude", "codex"]).default("claude"),
81420
81403
  model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
81421
81404
  linear: exports_external.preprocess(foldLegacyAssignee, exports_external.object({
81422
81405
  team: exports_external.string().optional(),
81423
- filter: exports_external.string().default("assignee = me"),
81406
+ filter: LinearFilterSchema,
81424
81407
  postComments: exports_external.boolean().default(true),
81425
81408
  updateEveryIterations: exports_external.number().int().nonnegative().default(10),
81426
81409
  mentionTrigger: exports_external.boolean().default(true),
@@ -81442,7 +81425,7 @@ var init_schema = __esm(() => {
81442
81425
  }),
81443
81426
  indicators: IndicatorsSchema.default({})
81444
81427
  }).strict()).default({
81445
- filter: "assignee = me",
81428
+ filter: [{ type: "assignee", value: "me" }],
81446
81429
  postComments: true,
81447
81430
  updateEveryIterations: 10,
81448
81431
  mentionTrigger: true,
@@ -81476,11 +81459,6 @@ var init_schema = __esm(() => {
81476
81459
  cleanup_on_success: exports_external.boolean().optional(),
81477
81460
  setup_script: exports_external.string().optional()
81478
81461
  }).strict().optional(),
81479
- ci: exports_external.object({
81480
- fix_on_failure: exports_external.boolean().optional(),
81481
- max_attempts: exports_external.number().int().positive().optional(),
81482
- poll_interval_seconds: exports_external.number().int().positive().optional()
81483
- }).strict().optional(),
81484
81462
  preExistingErrorCheck: exports_external.object({
81485
81463
  enabled: exports_external.boolean().default(false),
81486
81464
  commands: exports_external.array(exports_external.string()).default([]),
@@ -81494,14 +81472,18 @@ var init_schema = __esm(() => {
81494
81472
  label: "ralph:pre-existing-error",
81495
81473
  outputCharLimit: 4000
81496
81474
  }),
81497
- prTracker: exports_external.object({
81475
+ prRecovery: exports_external.object({
81498
81476
  enabled: exports_external.boolean().default(true),
81499
- maxRecoveryAttempts: exports_external.number().int().positive().default(3),
81500
- advanceMergedToDone: exports_external.boolean().default(false)
81477
+ fixCi: exports_external.boolean().default(true),
81478
+ fixConflicts: exports_external.boolean().default(true),
81479
+ maxRecoverySessions: exports_external.number().int().positive().default(3),
81480
+ ignoreChecks: exports_external.array(exports_external.string()).default([])
81501
81481
  }).strict().default({
81502
81482
  enabled: true,
81503
- maxRecoveryAttempts: 3,
81504
- advanceMergedToDone: false
81483
+ fixCi: true,
81484
+ fixConflicts: true,
81485
+ maxRecoverySessions: 3,
81486
+ ignoreChecks: []
81505
81487
  }),
81506
81488
  metaPrompt: exports_external.object({
81507
81489
  enabled: exports_external.boolean().default(true),
@@ -81532,7 +81514,7 @@ var init_schema = __esm(() => {
81532
81514
  var FRONTMATTER_RE, DEFAULT_WORKFLOW_MD = `---
81533
81515
  # WORKFLOW.md schema version \u2014 managed by \`ralphy init\`. When a newer version
81534
81516
  # ships, re-running init migrates this file and fills in the new settings.
81535
- version: 2
81517
+ version: 6
81536
81518
 
81537
81519
  project:
81538
81520
  name: ralphy
@@ -81585,9 +81567,12 @@ prBaseBranch: main
81585
81567
  stackPrsOnDependencies: false
81586
81568
  autoMergeStrategy: squash
81587
81569
 
81588
- fixCiOnFailure: false
81589
- maxCiFixAttempts: 5
81590
- ciPollIntervalSeconds: 30
81570
+ prRecovery:
81571
+ enabled: true
81572
+ fixCi: true
81573
+ fixConflicts: true
81574
+ maxRecoverySessions: 3
81575
+ ignoreChecks: []
81591
81576
 
81592
81577
  preExistingErrorCheck:
81593
81578
  enabled: false
@@ -81597,7 +81582,18 @@ preExistingErrorCheck:
81597
81582
  outputCharLimit: 4000
81598
81583
 
81599
81584
  linear:
81600
- filter: assignee = me
81585
+ # Global filter ANDed into every Linear query (and the GitHub PR searches
81586
+ # rooted at those issues). A marker list of \`assignee\` and \`label\` clauses;
81587
+ # all are required. \`assignee\` value is me / any / unassigned / <email> / <id>.
81588
+ # Example with a required label:
81589
+ # filter:
81590
+ # - type: assignee
81591
+ # value: me
81592
+ # - type: label
81593
+ # value: ralph
81594
+ filter:
81595
+ - type: assignee
81596
+ value: me
81601
81597
  postComments: true
81602
81598
  updateEveryIterations: 10
81603
81599
  mentionTrigger: true
@@ -81858,7 +81854,8 @@ function withPresets(answers) {
81858
81854
  const values2 = { ...answers.values };
81859
81855
  if (answers.mode === "permissive") {
81860
81856
  values2["createPrOnSuccess"] = true;
81861
- values2["fixCiOnFailure"] = true;
81857
+ values2["prRecovery.enabled"] = true;
81858
+ values2["prRecovery.fixCi"] = true;
81862
81859
  values2["manualMergeWhenAutoMergeDisabled"] = false;
81863
81860
  }
81864
81861
  return values2;
@@ -82010,6 +82007,88 @@ var init_normalize = __esm(() => {
82010
82007
  };
82011
82008
  });
82012
82009
 
82010
+ // packages/workflow/src/migrate/pr-recovery.ts
82011
+ function hasLegacyPrRecoveryKey(document2) {
82012
+ if (!import_yaml3.default.isMap(document2.contents))
82013
+ return false;
82014
+ return LEGACY_TOP_LEVEL_KEYS.some((key) => document2.hasIn([key]));
82015
+ }
82016
+ function hasLegacyLinearFilter(document2) {
82017
+ if (!import_yaml3.default.isMap(document2.contents))
82018
+ return false;
82019
+ if (!document2.hasIn(["linear", "filter"]))
82020
+ return false;
82021
+ return import_yaml3.default.isScalar(document2.getIn(["linear", "filter"], true));
82022
+ }
82023
+ function scalarFilterToMarkers(raw) {
82024
+ const text = typeof raw === "string" ? raw.trim() : "";
82025
+ let value = "me";
82026
+ if (text !== "") {
82027
+ const equals = text.indexOf("=");
82028
+ const key = equals >= 0 ? text.slice(0, equals).trim().toLowerCase() : "assignee";
82029
+ const candidate = equals >= 0 ? text.slice(equals + 1).trim() : text;
82030
+ if (key === "assignee" && candidate !== "")
82031
+ value = candidate;
82032
+ }
82033
+ const lower = value.toLowerCase();
82034
+ if (lower === "unassigned" || lower === "any" || lower === "me")
82035
+ value = lower;
82036
+ return [{ type: "assignee", value }];
82037
+ }
82038
+ function migrateWorkflowMarkdown(markdown) {
82039
+ const match = FRONTMATTER_RE.exec(markdown);
82040
+ if (!match)
82041
+ return { markdown, changed: false };
82042
+ const document2 = import_yaml3.default.parseDocument(match[1] ?? "");
82043
+ if (!import_yaml3.default.isMap(document2.contents))
82044
+ return { markdown, changed: false };
82045
+ const prRecoveryLegacy = hasLegacyPrRecoveryKey(document2);
82046
+ const linearFilterLegacy = hasLegacyLinearFilter(document2);
82047
+ if (!prRecoveryLegacy && !linearFilterLegacy)
82048
+ return { markdown, changed: false };
82049
+ const body = match[2] ?? "";
82050
+ if (prRecoveryLegacy) {
82051
+ if (!document2.hasIn(["prRecovery"])) {
82052
+ const trackerEnabled = document2.getIn(["prTracker", "enabled"]);
82053
+ const enabled2 = trackerEnabled !== false;
82054
+ const maxRecovery = document2.getIn(["prTracker", "maxRecoveryAttempts"]);
82055
+ const ignoreChecks = document2.getIn(["ignoreCiChecks"]);
82056
+ document2.setIn(["prRecovery", "enabled"], enabled2);
82057
+ document2.setIn(["prRecovery", "fixCi"], enabled2);
82058
+ document2.setIn(["prRecovery", "fixConflicts"], enabled2);
82059
+ document2.setIn(["prRecovery", "maxRecoverySessions"], typeof maxRecovery === "number" ? maxRecovery : 3);
82060
+ document2.setIn(["prRecovery", "ignoreChecks"], import_yaml3.default.isSeq(ignoreChecks) ? ignoreChecks : []);
82061
+ }
82062
+ for (const key of LEGACY_TOP_LEVEL_KEYS)
82063
+ document2.deleteIn([key]);
82064
+ }
82065
+ if (linearFilterLegacy) {
82066
+ const markers = scalarFilterToMarkers(document2.getIn(["linear", "filter"]));
82067
+ document2.setIn(["linear", "filter"], document2.createNode(markers, { flow: false }));
82068
+ document2.deleteIn(["linear", "assignee"]);
82069
+ }
82070
+ document2.setIn(["version"], CURRENT_WORKFLOW_VERSION);
82071
+ const frontmatter = document2.toString({ flowCollectionPadding: false }).replace(/\n+$/, "");
82072
+ return { markdown: `---
82073
+ ${frontmatter}
82074
+ ---
82075
+ ${body}`, changed: true };
82076
+ }
82077
+ var import_yaml3, LEGACY_TOP_LEVEL_KEYS;
82078
+ var init_pr_recovery = __esm(() => {
82079
+ init_default();
82080
+ init_schema();
82081
+ import_yaml3 = __toESM(require_dist(), 1);
82082
+ LEGACY_TOP_LEVEL_KEYS = [
82083
+ "prTracker",
82084
+ "fixCiOnFailure",
82085
+ "maxCiFixAttempts",
82086
+ "ciPollIntervalSeconds",
82087
+ "ignoreCiChecks",
82088
+ "ci"
82089
+ ];
82090
+ });
82091
+
82013
82092
  // packages/workflow/src/confirmation.ts
82014
82093
  function matchesIndicator(indicator, ticket) {
82015
82094
  if (!indicator || indicator.filter.length === 0)
@@ -82081,48 +82160,64 @@ function describeApprovalMarker(indicator) {
82081
82160
  }
82082
82161
 
82083
82162
  // packages/workflow/src/linear-filter.ts
82084
- function parseLinearFilter(filter2) {
82085
- const trimmed = filter2.trim();
82086
- if (trimmed === "")
82087
- return { assignee: "me" };
82088
- const eq = trimmed.indexOf("=");
82089
- if (eq < 0) {
82090
- throw new Error(`Invalid linear.filter "${filter2}": expected "<key> = <value>" (e.g. "assignee = me").`);
82091
- }
82092
- const key = trimmed.slice(0, eq).trim().toLowerCase();
82093
- const value = trimmed.slice(eq + 1).trim();
82094
- if (!SUPPORTED_KEYS.has(key)) {
82095
- throw new Error(`Unrecognized linear.filter key "${key}" in "${filter2}". Supported keys: ${[...SUPPORTED_KEYS].join(", ")}.`);
82163
+ function resolveLinearFilter(filter2) {
82164
+ const assigneeClauses = filter2.filter((marker) => marker.type === "assignee");
82165
+ if (assigneeClauses.length > 1) {
82166
+ throw new Error(`Invalid linear.filter: at most one "assignee" clause is allowed, found ${assigneeClauses.length}.`);
82167
+ }
82168
+ const requireAllLabels = [];
82169
+ const seenLabels = new Set;
82170
+ for (const marker of filter2) {
82171
+ if (marker.type !== "label")
82172
+ continue;
82173
+ if (seenLabels.has(marker.value))
82174
+ continue;
82175
+ seenLabels.add(marker.value);
82176
+ requireAllLabels.push(marker.value);
82096
82177
  }
82178
+ const assigneeClause = assigneeClauses[0];
82179
+ if (!assigneeClause)
82180
+ return { requireAllLabels };
82181
+ const value = assigneeClause.value.trim();
82097
82182
  const lower = value.toLowerCase();
82098
82183
  if (lower === "any")
82099
- return { anyAssignee: true };
82100
- if (lower === "" || lower === "unassigned")
82101
- return { assignee: "unassigned" };
82184
+ return { anyAssignee: true, requireAllLabels };
82185
+ if (lower === "" || lower === "unassigned") {
82186
+ return { assignee: "unassigned", requireAllLabels };
82187
+ }
82102
82188
  if (lower === "me")
82103
- return { assignee: "me" };
82104
- return { assignee: value };
82189
+ return { assignee: "me", requireAllLabels };
82190
+ return { assignee: value, requireAllLabels };
82191
+ }
82192
+ function applyAssigneeOverride(filter2, assignee) {
82193
+ const trimmed = assignee.trim();
82194
+ if (trimmed === "")
82195
+ return filter2;
82196
+ return [
82197
+ ...filter2.filter((marker) => marker.type !== "assignee"),
82198
+ { type: "assignee", value: trimmed }
82199
+ ];
82105
82200
  }
82106
- var SUPPORTED_KEYS;
82107
- var init_linear_filter = __esm(() => {
82108
- SUPPORTED_KEYS = new Set(["assignee"]);
82109
- });
82110
82201
 
82111
82202
  // packages/workflow/src/workflow.ts
82112
82203
  var exports_workflow = {};
82113
82204
  __export(exports_workflow, {
82114
82205
  workflowPath: () => workflowPath,
82206
+ workflowNeedsUpgrade: () => workflowNeedsUpgrade,
82207
+ resolveLinearFilter: () => resolveLinearFilter,
82115
82208
  resolveBaselineCommands: () => resolveBaselineCommands,
82116
82209
  renderWorkflowPrompt: () => renderWorkflowPrompt,
82117
82210
  renderTemplate: () => renderTemplate,
82211
+ readWorkflowVersion: () => readWorkflowVersion,
82118
82212
  parseWorkflow: () => parseWorkflow,
82119
- parseLinearFilter: () => parseLinearFilter,
82120
82213
  normalizeWorkflowMarkdown: () => normalizeWorkflowMarkdown,
82214
+ migrateWorkflowMarkdown: () => migrateWorkflowMarkdown,
82121
82215
  matchesIndicator: () => matchesIndicator,
82122
82216
  loadWorkflow: () => loadWorkflow,
82123
82217
  ensureWorkflow: () => ensureWorkflow,
82124
82218
  describeApprovalMarker: () => describeApprovalMarker,
82125
82219
  computeConfirmationFlags: () => computeConfirmationFlags,
82220
+ applyAssigneeOverride: () => applyAssigneeOverride,
82126
82221
  WorkflowConfigSchema: () => WorkflowConfigSchema,
82127
82222
  WORKFLOW_FILE: () => WORKFLOW_FILE,
82128
82223
  FRONTMATTER_RE: () => FRONTMATTER_RE,
@@ -82141,7 +82236,7 @@ function parseWorkflow(text, path = "") {
82141
82236
  const body = m[2] ?? "";
82142
82237
  let raw;
82143
82238
  try {
82144
- raw = import_yaml3.default.parse(yamlText, { schema: "core" });
82239
+ raw = import_yaml4.default.parse(yamlText, { schema: "core" });
82145
82240
  } catch (err) {
82146
82241
  throw new Error(`WORKFLOW.md frontmatter is not valid YAML.
82147
82242
  ` + (path ? ` File: ${path}
@@ -82219,14 +82314,32 @@ function applyAliases(cfg) {
82219
82314
  cfg.setupScript = cfg.worktree.setup_script;
82220
82315
  }
82221
82316
  }
82222
- if (cfg.ci) {
82223
- if (cfg.ci.fix_on_failure !== undefined)
82224
- cfg.fixCiOnFailure = cfg.ci.fix_on_failure;
82225
- if (cfg.ci.max_attempts !== undefined)
82226
- cfg.maxCiFixAttempts = cfg.ci.max_attempts;
82227
- if (cfg.ci.poll_interval_seconds !== undefined) {
82228
- cfg.ciPollIntervalSeconds = cfg.ci.poll_interval_seconds;
82229
- }
82317
+ }
82318
+ function readWorkflowVersion(text) {
82319
+ const match = FRONTMATTER_RE.exec(text);
82320
+ if (!match)
82321
+ return 0;
82322
+ try {
82323
+ const raw = import_yaml4.default.parse(match[1] ?? "", { schema: "core" });
82324
+ return typeof raw?.version === "number" ? raw.version : 0;
82325
+ } catch {
82326
+ return 0;
82327
+ }
82328
+ }
82329
+ function workflowNeedsUpgrade(text) {
82330
+ let migrated;
82331
+ try {
82332
+ migrated = migrateWorkflowMarkdown(text);
82333
+ } catch {
82334
+ return true;
82335
+ }
82336
+ if (migrated.changed)
82337
+ return true;
82338
+ try {
82339
+ parseWorkflow(normalizeWorkflowMarkdown(migrated.markdown).markdown);
82340
+ return false;
82341
+ } catch {
82342
+ return true;
82230
82343
  }
82231
82344
  }
82232
82345
  function workflowPath(projectRoot, workflowFile) {
@@ -82240,9 +82353,11 @@ async function loadWorkflow(projectRoot, workflowFile, options = {}) {
82240
82353
  return { config: config2, body: extractDefaultBody(), path };
82241
82354
  }
82242
82355
  const text = await file2.text();
82243
- const normalized = normalizeWorkflowMarkdown(text);
82244
- if (normalized.changed && options.persist)
82356
+ const migrated = migrateWorkflowMarkdown(text);
82357
+ const normalized = normalizeWorkflowMarkdown(migrated.markdown);
82358
+ if ((migrated.changed || normalized.changed) && options.persist) {
82245
82359
  await Bun.write(path, normalized.markdown);
82360
+ }
82246
82361
  return parseWorkflow(normalized.markdown, path);
82247
82362
  }
82248
82363
  async function ensureWorkflow(projectRoot, workflowFile) {
@@ -82278,17 +82393,18 @@ function renderWorkflowPrompt(workflow, ctx) {
82278
82393
  };
82279
82394
  return renderTemplate(workflow.body, fullCtx);
82280
82395
  }
82281
- var import_yaml3, WORKFLOW_FILE = "WORKFLOW.md";
82396
+ var import_yaml4, WORKFLOW_FILE = "WORKFLOW.md";
82282
82397
  var init_workflow = __esm(() => {
82283
82398
  init_schema();
82284
82399
  init_default();
82285
82400
  init_wizard();
82286
82401
  init_normalize();
82402
+ init_pr_recovery();
82287
82403
  init_schema();
82288
82404
  init_default();
82289
- init_linear_filter();
82290
82405
  init_normalize();
82291
- import_yaml3 = __toESM(require_dist(), 1);
82406
+ init_pr_recovery();
82407
+ import_yaml4 = __toESM(require_dist(), 1);
82292
82408
  });
82293
82409
 
82294
82410
  // packages/core/src/repo/index.ts
@@ -83252,8 +83368,11 @@ function buildFromAnswers(mode, answers, build = buildWorkflowMarkdown) {
83252
83368
  } else {
83253
83369
  assignee = assigneeChoice;
83254
83370
  }
83371
+ const existing = Array.isArray(values2["linear.filter"]) ? values2["linear.filter"] : [];
83255
83372
  if (assignee)
83256
- values2["linear.filter"] = `assignee = ${assignee}`;
83373
+ values2["linear.filter"] = applyAssigneeOverride(existing, assignee);
83374
+ else if (existing.length > 0)
83375
+ values2["linear.filter"] = existing;
83257
83376
  }
83258
83377
  delete values2[LINEAR_ASSIGNEE_CHOICE_FIELD_ID];
83259
83378
  delete values2[LINEAR_ASSIGNEE_VALUE_FIELD_ID];
@@ -83341,8 +83460,8 @@ function computeEditing(field, stored, multilineFallback = "") {
83341
83460
  return {
83342
83461
  draft: textLike && stored !== undefined ? String(stored) : field.spec.kind === "multiline" ? typeof stored === "string" ? stored : multilineFallback : "",
83343
83462
  optionIndex: initialOptionIndex(field, stored),
83344
- listItems: field.spec.kind === "list" && Array.isArray(stored) ? [...stored] : [],
83345
- selected: field.spec.kind === "multiselect" && Array.isArray(stored) ? new Set(stored) : new Set
83463
+ listItems: field.spec.kind === "list" && Array.isArray(stored) ? stored.filter((item) => typeof item === "string") : [],
83464
+ selected: field.spec.kind === "multiselect" && Array.isArray(stored) ? new Set(stored.filter((item) => typeof item === "string")) : new Set
83346
83465
  };
83347
83466
  }
83348
83467
  function initialOptionIndex(field, stored) {
@@ -83471,6 +83590,12 @@ function SetupWizard({
83471
83590
  setVisited((prev) => new Set(prev).add(nextFields[index + 1].id));
83472
83591
  goTo(index + 1, source);
83473
83592
  };
83593
+ import_react22.useEffect(() => {
83594
+ if (mode !== null && !building && fields.length === 0) {
83595
+ onComplete(buildFromAnswers(mode, valuesToWrite(answers), buildMarkdown));
83596
+ exit();
83597
+ }
83598
+ }, [mode, building, fields.length]);
83474
83599
  use_input_default((input, key) => {
83475
83600
  if (key.escape) {
83476
83601
  onCancel?.();
@@ -84297,6 +84422,7 @@ var import_react22, jsx_dev_runtime, REPO_ANSWER_IDS, MODE_OPTIONS, INDICATOR_OP
84297
84422
  var init_SetupWizard = __esm(async () => {
84298
84423
  init_version();
84299
84424
  init_wizard();
84425
+ init_workflow();
84300
84426
  init_fields();
84301
84427
  await init_build2();
84302
84428
  import_react22 = __toESM(require_react(), 1);
@@ -84429,9 +84555,6 @@ var init_migrations = __esm(() => {
84429
84555
  "preExistingErrorCheck.commands",
84430
84556
  "preExistingErrorCheck.baseBranch",
84431
84557
  "preExistingErrorCheck.label",
84432
- "prTracker.enabled",
84433
- "prTracker.maxRecoveryAttempts",
84434
- "prTracker.advanceMergedToDone",
84435
84558
  "openspec.reviewPhase.enabled",
84436
84559
  "openspec.reviewPhase.maxRounds",
84437
84560
  "openspec.reviewPhase.reviewerModel",
@@ -84457,6 +84580,11 @@ var init_migrations = __esm(() => {
84457
84580
  version: 5,
84458
84581
  description: "A new `linear.specAttachmentRevisions` setting controls the sealed " + "design attachment: 'replace' (default) overwrites the single canonical " + "attachment in place; 'append' publishes each change as a new " + "'Ralph design #N' attachment. Config-file-only \u2014 set it in WORKFLOW.md " + "if you want the append audit trail.",
84459
84582
  fields: []
84583
+ },
84584
+ {
84585
+ version: 6,
84586
+ description: "PR recovery is unified under one `prRecovery` block (replacing " + "`prTracker`, `fixCiOnFailure`, `maxCiFixAttempts`, `ciPollIntervalSeconds`, " + "and `ignoreCiChecks`). Workers now open the PR and leave the ticket " + "in-review; a single background watcher advances it to done once the PR is " + "mergeable (CI green, no conflicts) and recovers red PRs \u2014 resolving merge " + "conflicts AND fixing failing CI (both `prRecovery.fixConflicts` and " + "`prRecovery.fixCi` default on; tune them in WORKFLOW.md). " + "`prRecovery.enabled: false` turns the watcher off everywhere and marks the " + "ticket done immediately on PR open. Your old values are migrated " + "automatically; review them here or keep them.",
84587
+ fields: ["prRecovery.enabled", "prRecovery.maxRecoverySessions", "prRecovery.ignoreChecks"]
84460
84588
  }
84461
84589
  ];
84462
84590
  LATEST_MIGRATION_VERSION = MIGRATIONS.reduce((max2, migration) => Math.max(max2, migration.version), 0);
@@ -84588,7 +84716,7 @@ var init_project_detect = __esm(() => {
84588
84716
  // apps/init/src/index.ts
84589
84717
  var exports_src = {};
84590
84718
  __export(exports_src, {
84591
- runSetupWizard: () => runSetupWizard,
84719
+ maybeUpgradeWorkflow: () => maybeUpgradeWorkflow,
84592
84720
  maybeRunSetupWizard: () => maybeRunSetupWizard,
84593
84721
  main: () => main
84594
84722
  });
@@ -84681,6 +84809,22 @@ async function maybeRunSetupWizard(projectRoot, workflowFile) {
84681
84809
  ...Object.keys(detected).length > 0 ? { initialValues: detected } : {}
84682
84810
  });
84683
84811
  }
84812
+ async function maybeUpgradeWorkflow(projectRoot, workflowFile) {
84813
+ const root = projectRoot ?? await findProjectRoot();
84814
+ const path = workflowPath(root, workflowFile);
84815
+ const file2 = Bun.file(path);
84816
+ if (!await file2.exists())
84817
+ return false;
84818
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
84819
+ return false;
84820
+ if (!workflowNeedsUpgrade(await file2.text()))
84821
+ return false;
84822
+ process.stdout.write(`WORKFLOW.md needs an upgrade. Starting init\u2026
84823
+ `);
84824
+ const initArgv = ["--project-root", root, ...workflowFile ? ["--workflow", workflowFile] : []];
84825
+ await main(initArgv);
84826
+ return true;
84827
+ }
84684
84828
  function initialValuesFromConfig(config2) {
84685
84829
  const values2 = {};
84686
84830
  if (config2.project.name)
@@ -84702,13 +84846,16 @@ function initialValuesFromConfig(config2) {
84702
84846
  values2["concurrency"] = config2.concurrency;
84703
84847
  values2["createPrOnSuccess"] = config2.createPrOnSuccess;
84704
84848
  values2["prBaseBranch"] = config2.prBaseBranch;
84705
- values2["fixCiOnFailure"] = config2.fixCiOnFailure;
84849
+ values2["prRecovery.enabled"] = config2.prRecovery.enabled;
84850
+ values2["prRecovery.fixCi"] = config2.prRecovery.fixCi;
84851
+ values2["prRecovery.maxRecoverySessions"] = config2.prRecovery.maxRecoverySessions;
84706
84852
  values2["useWorktree"] = config2.useWorktree;
84707
84853
  if (config2.linear.team)
84708
84854
  values2["linear.team"] = config2.linear.team;
84709
- if (config2.linear.filter) {
84710
- const match = /^assignee\s*=\s*(.+)$/i.exec(config2.linear.filter.trim());
84711
- const assignee = match ? match[1].trim() : "";
84855
+ if (config2.linear.filter && config2.linear.filter.length > 0) {
84856
+ values2["linear.filter"] = config2.linear.filter;
84857
+ const assigneeMarker = config2.linear.filter.find((marker) => marker.type === "assignee");
84858
+ const assignee = assigneeMarker?.value.trim() ?? "";
84712
84859
  if (assignee === "me" || assignee === "any" || assignee === "unassigned") {
84713
84860
  values2["linear.assigneeChoice"] = assignee;
84714
84861
  } else if (assignee !== "") {
@@ -84766,7 +84913,7 @@ async function promptMigrate(fromVersion) {
84766
84913
  return choice;
84767
84914
  }
84768
84915
  async function editExisting(projectRoot, path, config2, workflowFile, onlyFields) {
84769
- const existing = await Bun.file(path).text();
84916
+ const { markdown: existing } = migrateWorkflowMarkdown(await Bun.file(path).text());
84770
84917
  const detectedRepo = await detectRepoIdentity(projectRoot);
84771
84918
  const detected = await detectInitialValues(projectRoot);
84772
84919
  const wrote = await runSetupWizard(projectRoot, {
@@ -84823,14 +84970,15 @@ Setup cancelled \u2014 no file written.
84823
84970
  `);
84824
84971
  return 0;
84825
84972
  }
84826
- if (needsMigration(config2.version)) {
84827
- const choice2 = await promptMigrate(config2.version);
84973
+ const diskVersion = readWorkflowVersion(await Bun.file(path).text());
84974
+ if (needsMigration(diskVersion)) {
84975
+ const choice2 = await promptMigrate(diskVersion);
84828
84976
  if (choice2 === "exit") {
84829
84977
  process.stdout.write(`Exited \u2014 WORKFLOW.md unchanged.
84830
84978
  `);
84831
84979
  return 0;
84832
84980
  }
84833
- const onlyFields = choice2 === "diff" ? fieldsAddedSince(config2.version) : undefined;
84981
+ const onlyFields = choice2 === "diff" ? fieldsAddedSince(diskVersion) : undefined;
84834
84982
  return editExisting(projectRoot, path, config2, workflowFile, onlyFields);
84835
84983
  }
84836
84984
  const choice = await promptEditOrExit();
@@ -98288,7 +98436,14 @@ function scrubClaudeEnv(env3 = process.env) {
98288
98436
  }
98289
98437
  return copy;
98290
98438
  }
98291
- var CLAUDE_ENV_KEYS_TO_SCRUB;
98439
+ function scrubGithubAppTokenEnv(env3 = process.env) {
98440
+ const copy = { ...env3 };
98441
+ for (const key of GITHUB_APP_TOKEN_KEYS) {
98442
+ delete copy[key];
98443
+ }
98444
+ return copy;
98445
+ }
98446
+ var CLAUDE_ENV_KEYS_TO_SCRUB, GITHUB_APP_TOKEN_KEYS;
98292
98447
  var init_env = __esm(() => {
98293
98448
  CLAUDE_ENV_KEYS_TO_SCRUB = [
98294
98449
  "CLAUDECODE",
@@ -98297,6 +98452,7 @@ var init_env = __esm(() => {
98297
98452
  "CLAUDE_CODE_ENTRYPOINT",
98298
98453
  "AI_AGENT"
98299
98454
  ];
98455
+ GITHUB_APP_TOKEN_KEYS = ["GITHUB_TOKEN"];
98300
98456
  });
98301
98457
 
98302
98458
  // packages/engine/src/preflight/gh.ts
@@ -98304,6 +98460,7 @@ async function checkGhAuth() {
98304
98460
  try {
98305
98461
  const proc = spawn({
98306
98462
  cmd: ["gh", "auth", "status"],
98463
+ env: scrubGithubAppTokenEnv(),
98307
98464
  stdout: "ignore",
98308
98465
  stderr: "ignore"
98309
98466
  });
@@ -98319,6 +98476,7 @@ async function checkGhAuth() {
98319
98476
  var GH_AUTH_FAIL_MESSAGE = "gh is not authenticated. Run `gh auth login` (or set GH_TOKEN), then restart the agent.";
98320
98477
  var init_gh = __esm(() => {
98321
98478
  init_spawn();
98479
+ init_env();
98322
98480
  });
98323
98481
 
98324
98482
  // packages/engine/src/preflight/claude.ts
@@ -98348,19 +98506,69 @@ var init_claude = __esm(() => {
98348
98506
  NOT_LOGGED_IN_RE = /Not logged in|Please run \/login/;
98349
98507
  });
98350
98508
 
98509
+ // packages/engine/src/preflight/repo.ts
98510
+ async function checkRepoWriteAccess(cwd2) {
98511
+ let blob = "";
98512
+ try {
98513
+ const proc = spawn({
98514
+ cmd: [
98515
+ "gh",
98516
+ "api",
98517
+ "-X",
98518
+ "POST",
98519
+ "repos/{owner}/{repo}/git/refs",
98520
+ "-f",
98521
+ `ref=${PROBE_REF}`,
98522
+ "-f",
98523
+ `sha=${ZERO_SHA}`
98524
+ ],
98525
+ cwd: cwd2,
98526
+ env: scrubGithubAppTokenEnv(),
98527
+ stdout: "pipe",
98528
+ stderr: "pipe"
98529
+ });
98530
+ const stdout = await new Response(proc.stdout).text();
98531
+ const stderr = await new Response(proc.stderr).text();
98532
+ await proc.exited;
98533
+ blob = `${stdout}
98534
+ ${stderr}`;
98535
+ } catch {
98536
+ return { ok: true };
98537
+ }
98538
+ if (HAS_WRITE_RE.test(blob))
98539
+ return { ok: true };
98540
+ if (NO_WRITE_RE.test(blob))
98541
+ return { ok: false, tool: "repo", message: REPO_WRITE_FAIL_MESSAGE };
98542
+ return { ok: true };
98543
+ }
98544
+ var REPO_WRITE_FAIL_MESSAGE, PROBE_REF = "refs/heads/ralphy-preflight-write-probe", ZERO_SHA = "0000000000000000000000000000000000000000", HAS_WRITE_RE, NO_WRITE_RE;
98545
+ var init_repo2 = __esm(() => {
98546
+ init_spawn();
98547
+ init_env();
98548
+ REPO_WRITE_FAIL_MESSAGE = "No write access to this repository \u2014 the active credential can read it but cannot " + "push, so every issue would fail at PR creation (and be re-queued). Ralphy uses gh's " + "auth for all GitHub operations: grant push access to `GH_TOKEN` (or `gh auth login`), " + "or, if you rely on a fine-grained PAT, give it Contents: write + Pull requests: write " + "+ Commit statuses: read. Then restart the agent.";
98549
+ HAS_WRITE_RE = /"status":\s*"422"|Object does not exist|Unprocessable/i;
98550
+ NO_WRITE_RE = /"status":\s*"403"|not accessible by personal access token|Write access to repository not granted/i;
98551
+ });
98552
+
98351
98553
  // packages/engine/src/preflight/run.ts
98352
- async function runPreflight() {
98554
+ async function runPreflight(opts = {}) {
98353
98555
  const gh = await checkGhAuth();
98354
98556
  if (!gh.ok)
98355
98557
  return gh;
98356
98558
  const claude = await checkClaudeAuth();
98357
98559
  if (!claude.ok)
98358
98560
  return claude;
98561
+ if (opts.requireRepoWrite && opts.repoCwd) {
98562
+ const repo = await checkRepoWriteAccess(opts.repoCwd);
98563
+ if (!repo.ok)
98564
+ return repo;
98565
+ }
98359
98566
  return { ok: true };
98360
98567
  }
98361
98568
  var init_run = __esm(() => {
98362
98569
  init_gh();
98363
98570
  init_claude();
98571
+ init_repo2();
98364
98572
  });
98365
98573
 
98366
98574
  // packages/engine/src/preflight/index.ts
@@ -98368,6 +98576,7 @@ var init_preflight = __esm(() => {
98368
98576
  init_env();
98369
98577
  init_gh();
98370
98578
  init_claude();
98579
+ init_repo2();
98371
98580
  init_run();
98372
98581
  });
98373
98582
 
@@ -99485,6 +99694,7 @@ var init_flow_machine = __esm(() => {
99485
99694
  AWAITING_DETECTED: "awaiting",
99486
99695
  CONFLICT_DETECTED: "conflict-fix",
99487
99696
  CI_FAILED_DETECTED: "ci-fix",
99697
+ PR_OPENED: "awaiting-ci",
99488
99698
  WORKER_SUCCEEDED: "done",
99489
99699
  WORKER_FAILED: "error",
99490
99700
  PREEMPT: {
@@ -99504,7 +99714,7 @@ var init_flow_machine = __esm(() => {
99504
99714
  },
99505
99715
  "conflict-fix": {
99506
99716
  on: {
99507
- WORKER_SUCCEEDED: "working",
99717
+ WORKER_SUCCEEDED: "awaiting-ci",
99508
99718
  WORKER_FAILED: "error",
99509
99719
  PREEMPT: {
99510
99720
  target: "preempting",
@@ -99523,7 +99733,7 @@ var init_flow_machine = __esm(() => {
99523
99733
  },
99524
99734
  "ci-fix": {
99525
99735
  on: {
99526
- WORKER_SUCCEEDED: "working",
99736
+ WORKER_SUCCEEDED: "awaiting-ci",
99527
99737
  WORKER_FAILED: "error",
99528
99738
  PREEMPT: {
99529
99739
  target: "preempting",
@@ -99551,9 +99761,31 @@ var init_flow_machine = __esm(() => {
99551
99761
  }
99552
99762
  }
99553
99763
  },
99764
+ "awaiting-ci": {
99765
+ on: {
99766
+ PR_PASSED: "done",
99767
+ CONFLICT_DETECTED: "conflict-fix",
99768
+ CI_FAILED_DETECTED: "ci-fix",
99769
+ REVIEW_TRIGGERED: "review",
99770
+ PREEMPT: {
99771
+ target: "preempting",
99772
+ actions: import_xstate_development_cjs.assign({
99773
+ pendingAssignment: ({ event }) => event.newAssignment
99774
+ })
99775
+ },
99776
+ WORKER_SPAWNED: {
99777
+ actions: import_xstate_development_cjs.assign(({ event }) => ({
99778
+ worker: event.worker,
99779
+ teardown: event.teardown ?? undefined,
99780
+ currentAssignment: event.assignment
99781
+ }))
99782
+ }
99783
+ }
99784
+ },
99554
99785
  review: {
99555
99786
  on: {
99556
99787
  WORKER_SUCCEEDED: "done",
99788
+ PR_OPENED: "awaiting-ci",
99557
99789
  WORKER_FAILED: "error",
99558
99790
  WORKER_SPAWNED: {
99559
99791
  actions: import_xstate_development_cjs.assign(({ event }) => ({
@@ -99599,7 +99831,11 @@ var init_flow_machine = __esm(() => {
99599
99831
  target: "ci-fix"
99600
99832
  },
99601
99833
  {
99602
- guard: ({ context }) => context.pendingAssignment?.flowId === "awaiting-ci" || context.pendingAssignment?.flowId === "confirmation",
99834
+ guard: ({ context }) => context.pendingAssignment?.flowId === "awaiting-ci",
99835
+ target: "awaiting-ci"
99836
+ },
99837
+ {
99838
+ guard: ({ context }) => context.pendingAssignment?.flowId === "confirmation",
99603
99839
  target: "awaiting"
99604
99840
  },
99605
99841
  {
@@ -101030,6 +101266,36 @@ var init_useLoop = __esm(() => {
101030
101266
  import_react57 = __toESM(require_react(), 1);
101031
101267
  });
101032
101268
 
101269
+ // packages/ui-shared/src/useHoldToClose.ts
101270
+ function useHoldToClose({ finished, hold, onClose }) {
101271
+ const { exit } = use_app_default();
101272
+ const { isRawModeSupported } = use_stdin_default();
101273
+ const [awaitingClose, setAwaitingClose] = import_react58.useState(false);
101274
+ const close = () => {
101275
+ onClose?.();
101276
+ exit();
101277
+ };
101278
+ import_react58.useEffect(() => {
101279
+ if (!finished)
101280
+ return;
101281
+ if (hold && isRawModeSupported) {
101282
+ setAwaitingClose(true);
101283
+ return;
101284
+ }
101285
+ close();
101286
+ }, [finished, hold, isRawModeSupported]);
101287
+ use_input_default((_input, key) => {
101288
+ if (key.return)
101289
+ close();
101290
+ }, { isActive: awaitingClose });
101291
+ return { awaitingClose };
101292
+ }
101293
+ var import_react58;
101294
+ var init_useHoldToClose = __esm(async () => {
101295
+ await init_build2();
101296
+ import_react58 = __toESM(require_react(), 1);
101297
+ });
101298
+
101033
101299
  // apps/loop/src/components/TaskLoop.tsx
101034
101300
  function LogLine({ entry, verbose }) {
101035
101301
  switch (entry.kind) {
@@ -101081,10 +101347,10 @@ function handleSteerKeyInput(key, history, currentIndex) {
101081
101347
  return navigateHistory(history, currentIndex, dir);
101082
101348
  }
101083
101349
  function SteerInput({ onSubmit }) {
101084
- const [inputKey, setInputKey] = import_react58.useState(0);
101085
- const [defaultValue, setDefaultValue] = import_react58.useState("");
101086
- const historyRef = import_react58.useRef([]);
101087
- const historyIndexRef = import_react58.useRef(-1);
101350
+ const [inputKey, setInputKey] = import_react59.useState(0);
101351
+ const [defaultValue, setDefaultValue] = import_react59.useState("");
101352
+ const historyRef = import_react59.useRef([]);
101353
+ const historyIndexRef = import_react59.useRef(-1);
101088
101354
  use_input_default((_input, key) => {
101089
101355
  const result2 = handleSteerKeyInput(key, historyRef.current, historyIndexRef.current);
101090
101356
  if (result2) {
@@ -101113,21 +101379,19 @@ function SteerInput({ onSubmit }) {
101113
101379
  }, undefined, true, undefined, this);
101114
101380
  }
101115
101381
  function TaskLoop({ opts }) {
101116
- const { exit } = use_app_default();
101117
101382
  const loop = useLoop(opts);
101118
101383
  const { isRawModeSupported } = use_stdin_default();
101119
101384
  const { resizeKey } = useTerminalSize();
101120
- const bannerItem = import_react58.useRef({ id: "__banner__", kind: "banner" });
101121
- const [stateDir] = import_react58.useState(() => getLayout().taskStateDir(opts.name));
101122
- const feedItems = import_react58.useMemo(() => [
101385
+ const bannerItem = import_react59.useRef({ id: "__banner__", kind: "banner" });
101386
+ const [stateDir] = import_react59.useState(() => getLayout().taskStateDir(opts.name));
101387
+ const feedItems = import_react59.useMemo(() => [
101123
101388
  bannerItem.current,
101124
101389
  ...loop.logLines.map((e) => ({ id: e.id, kind: "entry", entry: e }))
101125
101390
  ], [loop.logLines]);
101126
- import_react58.useEffect(() => {
101127
- if (!loop.isRunning) {
101128
- exit();
101129
- }
101130
- }, [loop.isRunning, exit]);
101391
+ const { awaitingClose } = useHoldToClose({
101392
+ finished: !loop.isRunning,
101393
+ hold: loop.stopReason !== null && loop.stopReason !== "completed"
101394
+ });
101131
101395
  if (!loop.state)
101132
101396
  return null;
101133
101397
  return /* @__PURE__ */ jsx_dev_runtime8.jsxDEV(Box_default, {
@@ -101188,13 +101452,21 @@ function TaskLoop({ opts }) {
101188
101452
  maxCostUsd: opts.maxCostUsd,
101189
101453
  maxRuntimeMinutes: opts.maxRuntimeMinutes,
101190
101454
  consecutiveFailures: loop.consecutiveFailures
101191
- }, undefined, false, undefined, this)
101455
+ }, undefined, false, undefined, this),
101456
+ awaitingClose && /* @__PURE__ */ jsx_dev_runtime8.jsxDEV(Text, {
101457
+ color: "cyan",
101458
+ children: [
101459
+ `
101460
+ `,
101461
+ "Press Enter to close\u2026"
101462
+ ]
101463
+ }, undefined, true, undefined, this)
101192
101464
  ]
101193
101465
  }, undefined, true, undefined, this)
101194
101466
  ]
101195
101467
  }, resizeKey, true, undefined, this);
101196
101468
  }
101197
- var import_react58, jsx_dev_runtime8;
101469
+ var import_react59, jsx_dev_runtime8;
101198
101470
  var init_TaskLoop = __esm(async () => {
101199
101471
  init_useLoop();
101200
101472
  init_useTerminalSize();
@@ -101206,9 +101478,10 @@ var init_TaskLoop = __esm(async () => {
101206
101478
  init_IterationHeader(),
101207
101479
  init_FeedLine(),
101208
101480
  init_StatusBar(),
101209
- init_StopMessage()
101481
+ init_StopMessage(),
101482
+ init_useHoldToClose()
101210
101483
  ]);
101211
- import_react58 = __toESM(require_react(), 1);
101484
+ import_react59 = __toESM(require_react(), 1);
101212
101485
  jsx_dev_runtime8 = __toESM(require_jsx_dev_runtime(), 1);
101213
101486
  });
101214
101487
 
@@ -101216,7 +101489,7 @@ var init_TaskLoop = __esm(async () => {
101216
101489
  import { join as join17 } from "path";
101217
101490
  function ExitAfterRender({ children }) {
101218
101491
  const { exit } = use_app_default();
101219
- import_react59.useEffect(() => {
101492
+ import_react60.useEffect(() => {
101220
101493
  exit();
101221
101494
  }, [exit]);
101222
101495
  return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(jsx_dev_runtime9.Fragment, {
@@ -101225,7 +101498,7 @@ function ExitAfterRender({ children }) {
101225
101498
  }
101226
101499
  function ErrorMessage({ message }) {
101227
101500
  const { exit } = use_app_default();
101228
- import_react59.useEffect(() => {
101501
+ import_react60.useEffect(() => {
101229
101502
  process.exitCode = 1;
101230
101503
  exit();
101231
101504
  }, [exit]);
@@ -101304,7 +101577,7 @@ function App2({ args, taskPhase }) {
101304
101577
  }
101305
101578
  }
101306
101579
  }
101307
- var import_react59, jsx_dev_runtime9;
101580
+ var import_react60, jsx_dev_runtime9;
101308
101581
  var init_App2 = __esm(async () => {
101309
101582
  init_store();
101310
101583
  init_context();
@@ -101314,7 +101587,7 @@ var init_App2 = __esm(async () => {
101314
101587
  init_TaskStatus(),
101315
101588
  init_TaskLoop()
101316
101589
  ]);
101317
- import_react59 = __toESM(require_react(), 1);
101590
+ import_react60 = __toESM(require_react(), 1);
101318
101591
  jsx_dev_runtime9 = __toESM(require_jsx_dev_runtime(), 1);
101319
101592
  });
101320
101593
 
@@ -101942,7 +102215,7 @@ async function main2(argv) {
101942
102215
  await ensureRalphGitignore(projectRoot);
101943
102216
  }
101944
102217
  await runWithContext(createDefaultContext({ layout, args }), async () => {
101945
- const { waitUntilExit } = render_default(import_react60.createElement(App2, { args }));
102218
+ const { waitUntilExit } = render_default(import_react61.createElement(App2, { args }));
101946
102219
  await waitUntilExit();
101947
102220
  });
101948
102221
  return typeof process.exitCode === "number" ? process.exitCode : 0;
@@ -101970,7 +102243,7 @@ async function taskMain(argv) {
101970
102243
  await mkdir6(join20(tasksDir, args.name), { recursive: true });
101971
102244
  await ensureRalphGitignore(projectRoot);
101972
102245
  await runWithContext(createDefaultContext({ layout, args }), async () => {
101973
- const { waitUntilExit } = render_default(import_react60.createElement(App2, {
102246
+ const { waitUntilExit } = render_default(import_react61.createElement(App2, {
101974
102247
  args,
101975
102248
  taskPhase: args.phase
101976
102249
  }));
@@ -101978,7 +102251,7 @@ async function taskMain(argv) {
101978
102251
  });
101979
102252
  return typeof process.exitCode === "number" ? process.exitCode : 0;
101980
102253
  }
101981
- var import_react60;
102254
+ var import_react61;
101982
102255
  var init_src7 = __esm(async () => {
101983
102256
  init_context();
101984
102257
  init_layout();
@@ -101991,7 +102264,7 @@ var init_src7 = __esm(async () => {
101991
102264
  init_build2(),
101992
102265
  init_App2()
101993
102266
  ]);
101994
- import_react60 = __toESM(require_react(), 1);
102267
+ import_react61 = __toESM(require_react(), 1);
101995
102268
  });
101996
102269
 
101997
102270
  // apps/agent/src/cli.ts
@@ -102045,14 +102318,12 @@ async function parseAgentArgs(argv) {
102045
102318
  ...common2,
102046
102319
  mode: "agent",
102047
102320
  linearTeam: "",
102048
- linearFilter: "",
102049
102321
  linearAssignee: "",
102050
102322
  pollInterval: 0,
102051
102323
  concurrency: 0,
102052
102324
  worktree: false,
102053
102325
  indicators: {},
102054
102326
  createPr: false,
102055
- fixCi: false,
102056
102327
  stackPrs: false,
102057
102328
  codeReview: false,
102058
102329
  maxTickets: 0,
@@ -102067,7 +102338,6 @@ async function parseAgentArgs(argv) {
102067
102338
  };
102068
102339
  const state = emptyParseState();
102069
102340
  let expectLinearTeam = false;
102070
- let expectLinearFilter = false;
102071
102341
  let expectLinearAssignee = false;
102072
102342
  let expectPollInterval = false;
102073
102343
  let expectConcurrency = false;
@@ -102081,11 +102351,6 @@ async function parseAgentArgs(argv) {
102081
102351
  expectLinearTeam = false;
102082
102352
  continue;
102083
102353
  }
102084
- if (expectLinearFilter) {
102085
- result2.linearFilter = arg;
102086
- expectLinearFilter = false;
102087
- continue;
102088
- }
102089
102354
  if (expectLinearAssignee) {
102090
102355
  result2.linearAssignee = arg;
102091
102356
  expectLinearAssignee = false;
@@ -102131,9 +102396,6 @@ async function parseAgentArgs(argv) {
102131
102396
  case "--linear-team":
102132
102397
  expectLinearTeam = true;
102133
102398
  break;
102134
- case "--linear-filter":
102135
- expectLinearFilter = true;
102136
- break;
102137
102399
  case "--linear-assignee":
102138
102400
  expectLinearAssignee = true;
102139
102401
  break;
@@ -102158,9 +102420,6 @@ async function parseAgentArgs(argv) {
102158
102420
  case "--create-pr":
102159
102421
  result2.createPr = true;
102160
102422
  break;
102161
- case "--fix-ci":
102162
- result2.fixCi = true;
102163
- break;
102164
102423
  case "--stack-prs":
102165
102424
  result2.stackPrs = true;
102166
102425
  break;
@@ -102194,11 +102453,11 @@ async function parseAgentArgs(argv) {
102194
102453
  case "--no-tmux":
102195
102454
  result2.noTmux = true;
102196
102455
  break;
102197
- case "--no-pr-tracker":
102198
- result2.prTrackerEnabled = false;
102456
+ case "--no-pr-recovery":
102457
+ result2.prRecoveryEnabled = false;
102199
102458
  break;
102200
- case "--pr-tracker":
102201
- result2.prTrackerEnabled = true;
102459
+ case "--pr-recovery":
102460
+ result2.prRecoveryEnabled = true;
102202
102461
  break;
102203
102462
  default:
102204
102463
  if (VALID_MODES2.has(arg)) {
@@ -102211,9 +102470,6 @@ async function parseAgentArgs(argv) {
102211
102470
  }
102212
102471
  await resolvePromptFile(result2, state);
102213
102472
  resolveWorkflowFile(result2, state);
102214
- if (result2.fixCi && !result2.createPr) {
102215
- throw new Error("--fix-ci requires --create-pr");
102216
- }
102217
102473
  if (result2.stackPrs && !result2.createPr) {
102218
102474
  throw new Error("--stack-prs requires --create-pr");
102219
102475
  }
@@ -102264,8 +102520,7 @@ var init_cli2 = __esm(() => {
102264
102520
  " --log Log raw engine stream",
102265
102521
  " --verbose Verbose output",
102266
102522
  " --linear-team <key> Linear team key (e.g. ENG)",
102267
- " --linear-filter <expr> Global Linear filter (e.g. 'assignee = me', 'assignee = any')",
102268
- " --linear-assignee <id> [deprecated] Filter by assignee; use --linear-filter instead",
102523
+ " --linear-assignee <id> Assignee override (me / any / unassigned / <email> / <id>); overrides linear.filter's assignee clause",
102269
102524
  " --poll-interval <s> Seconds between Linear polls (default: 60)",
102270
102525
  " --concurrency <n> Max concurrent task loops (default: 1)",
102271
102526
  " --worktree Run each task in its own git worktree",
@@ -102277,13 +102532,12 @@ var init_cli2 = __esm(() => {
102277
102532
  " --indicator setPrReady:status:In Review (additive ready marker)",
102278
102533
  " (attachment upserts a single 'Ralphy' entry; value = subtitle)",
102279
102534
  " --create-pr Push the worker branch and open a GitHub PR on success (needs --worktree)",
102280
- " --fix-ci After opening the PR, re-run on CI failures until green (needs --create-pr)",
102281
102535
  " --stack-prs Base the PR on a blocker issue's open-PR head branch when present (needs --create-pr)",
102282
102536
  " --code-review Watch open tracked PRs for unresolved review comments",
102283
102537
  " --max-tickets <n> Stop picking up new issues after N have been started (0 = unlimited)",
102284
102538
  " --ticket <id> Restrict issue discovery to specific ticket(s); repeatable or comma-separated (e.g. RLF-208 or 208)",
102285
102539
  " --no-tmux Disable tmux session management; run agent in the foreground directly",
102286
- " --no-pr-tracker Disable RLF-173 pr-tracker bail / recovery counter for this run",
102540
+ " --no-pr-recovery Disable PR recovery (conflict + CI watcher) for this run; --pr-recovery forces it on",
102287
102541
  " --json-output Emit JSONL to stdout instead of the Ink dashboard (for scripting/CI)",
102288
102542
  " (auto-enabled when stdin is not a TTY, e.g. pipes / nohup / CI)",
102289
102543
  " --json-log-file <path> Mirror JSONL events to a file (works alongside TUI or --json-output)",
@@ -102346,330 +102600,12 @@ class PollContext {
102346
102600
  }
102347
102601
  }
102348
102602
 
102349
- // apps/agent/src/shared/capabilities/types.ts
102350
- var NO_RETRY;
102351
- var init_types4 = __esm(() => {
102352
- NO_RETRY = {
102353
- maxAttempts: 1,
102354
- isRetryable: () => false,
102355
- delayMs: () => 0
102356
- };
102357
- });
102358
-
102359
- // apps/agent/src/shared/capabilities/format-error.ts
102360
- function formatError2(err) {
102361
- if (err instanceof Error)
102362
- return err.message;
102363
- try {
102364
- return String(err);
102365
- } catch {
102366
- return "unknown error";
102367
- }
102368
- }
102369
-
102370
- // apps/agent/src/shared/capabilities/fs-change.ts
102371
- import { join as join21, dirname as dirname8 } from "path";
102372
- import { mkdir as mkdir7 } from "fs/promises";
102373
- var scaffold, prependTask, appendSteering, fsChange;
102374
- var init_fs_change = __esm(() => {
102375
- init_tasks_md();
102376
- init_types4();
102377
- scaffold = {
102378
- name: "fs.change.scaffold",
102379
- required: false,
102380
- retryPolicy: NO_RETRY,
102381
- errorFormatter: formatError2,
102382
- run: async (args) => {
102383
- await mkdir7(args.changeDir, { recursive: true });
102384
- await mkdir7(join21(args.changeDir, "specs"), { recursive: true });
102385
- await mkdir7(args.stateDir, { recursive: true });
102386
- await Bun.write(join21(args.changeDir, "proposal.md"), args.proposal);
102387
- await Bun.write(join21(args.changeDir, "tasks.md"), args.tasks);
102388
- await Bun.write(join21(args.changeDir, "design.md"), args.design);
102389
- }
102390
- };
102391
- prependTask = {
102392
- name: "fs.change.task.prepend",
102393
- required: false,
102394
- retryPolicy: NO_RETRY,
102395
- errorFormatter: formatError2,
102396
- run: async (args) => {
102397
- await prependFixTask(args.tasksPath, args.heading, args.failureOutput);
102398
- }
102399
- };
102400
- appendSteering = {
102401
- name: "fs.change.steering.append",
102402
- required: false,
102403
- retryPolicy: NO_RETRY,
102404
- errorFormatter: formatError2,
102405
- run: async (args) => {
102406
- const path = join21(args.changeDir, "steering.md");
102407
- const f2 = Bun.file(path);
102408
- const existing = await f2.exists() ? await f2.text() : null;
102409
- const updated = existing ? `${args.message}
102410
-
102411
- ${existing.trimStart()}` : `${args.message}
102412
- `;
102413
- await mkdir7(dirname8(path), { recursive: true });
102414
- await Bun.write(path, updated);
102415
- }
102416
- };
102417
- fsChange = { scaffold, prependTask, appendSteering };
102418
- });
102419
-
102420
- // apps/agent/src/agent/worktree.ts
102421
- import { basename as basename2, join as join22 } from "path";
102422
- import { homedir as homedir5 } from "os";
102423
- import { exists as exists3 } from "fs/promises";
102424
- function worktreesDir2(projectRoot) {
102425
- return join22(homedir5(), ".ralph", basename2(projectRoot), "worktrees");
102426
- }
102427
- function branchForChange(changeName) {
102428
- return `ralph/${changeName}`;
102429
- }
102430
- function worktreeDirNameForIssue(issue2) {
102431
- return issue2.identifier.toLowerCase();
102432
- }
102433
- function withRepoLock(projectRoot, fn) {
102434
- const prev = repoWorktreeLocks.get(projectRoot) ?? Promise.resolve();
102435
- const result2 = prev.then(fn, fn);
102436
- repoWorktreeLocks.set(projectRoot, result2.then(() => {}, () => {}));
102437
- return result2;
102438
- }
102439
- function createWorktree(projectRoot, changeName, baseBranch, runner) {
102440
- return withRepoLock(projectRoot, () => provisionWorktree(projectRoot, changeName, baseBranch, runner));
102441
- }
102442
- async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
102443
- const dir = worktreesDir2(projectRoot);
102444
- const cwd2 = join22(dir, changeName);
102445
- const branch = branchForChange(changeName);
102446
- const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
102447
- if (list.stdout.includes(`worktree ${cwd2}
102448
- `)) {
102449
- await installPrePushHook(cwd2, runner);
102450
- return { cwd: cwd2, branch };
102451
- }
102452
- let branchExists = true;
102453
- try {
102454
- await runner.run(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], projectRoot);
102455
- } catch {
102456
- branchExists = false;
102457
- }
102458
- if (branchExists) {
102459
- await runner.run(["worktree", "add", cwd2, branch], projectRoot);
102460
- await installPrePushHook(cwd2, runner);
102461
- return { cwd: cwd2, branch };
102462
- }
102463
- await runner.run(["fetch", "origin", baseBranch], projectRoot);
102464
- await runner.run(["worktree", "add", "-b", branch, cwd2, `origin/${baseBranch}`], projectRoot);
102465
- await installPrePushHook(cwd2, runner);
102466
- return { cwd: cwd2, branch };
102467
- }
102468
- async function installPrePushHook(cwd2, runner) {
102469
- const hookPath = join22(cwd2, ".ralph-hooks", "pre-push");
102470
- await Bun.write(hookPath, PRE_PUSH_HOOK_SCRIPT);
102471
- const chmod = Bun.spawn(["chmod", "+x", hookPath]);
102472
- await chmod.exited;
102473
- await runner.run(["config", "core.hooksPath", ".ralph-hooks"], cwd2);
102474
- }
102475
- async function removeWorktree(projectRoot, cwd2, runner) {
102476
- await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
102477
- }
102478
- async function isWorktreeSafeToRemove(cwd2, base2, runner) {
102479
- const status = await runner.run(["status", "--porcelain"], cwd2);
102480
- const dirty = status.stdout.trim();
102481
- let unpushedCommits = "";
102482
- try {
102483
- const log3 = await runner.run(["log", "--oneline", `${base2}..HEAD`, "--no-merges"], cwd2);
102484
- unpushedCommits = log3.stdout.trim();
102485
- } catch {
102486
- unpushedCommits = "<unknown: failed to compare against base>";
102487
- }
102488
- if (dirty && unpushedCommits) {
102489
- return {
102490
- safe: false,
102491
- reason: "uncommitted changes AND unpushed commits present",
102492
- dirty,
102493
- unpushedCommits
102494
- };
102495
- }
102496
- if (dirty) {
102497
- return {
102498
- safe: false,
102499
- reason: "uncommitted or untracked files present",
102500
- dirty,
102501
- unpushedCommits
102502
- };
102503
- }
102504
- if (unpushedCommits) {
102505
- return {
102506
- safe: false,
102507
- reason: `commits ahead of ${base2} were not pushed/PR'd`,
102508
- dirty,
102509
- unpushedCommits
102510
- };
102511
- }
102512
- return { safe: true, dirty, unpushedCommits };
102513
- }
102514
- async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
102515
- const dst = join22(worktreeCwd, ".mcp.json");
102516
- const src = join22(projectRoot, ".mcp.json");
102517
- const source = await exists3(dst) ? dst : await exists3(src) ? src : null;
102518
- if (!source)
102519
- return;
102520
- let parsed;
102521
- try {
102522
- parsed = await Bun.file(source).json();
102523
- } catch {
102524
- return;
102525
- }
102526
- const servers = parsed.mcpServers;
102527
- if (servers && typeof servers === "object") {
102528
- for (const cfg of Object.values(servers)) {
102529
- if (Array.isArray(cfg.args)) {
102530
- cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join22(projectRoot, a) : a);
102531
- }
102532
- }
102533
- }
102534
- await Bun.write(dst, JSON.stringify(parsed, null, 2) + `
102535
- `);
102536
- }
102537
- var repoWorktreeLocks, PRE_PUSH_HOOK_SCRIPT = `#!/usr/bin/env bash
102538
- # Installed by ralphy createWorktree (RLF-107).
102539
- # Rejects any push whose remote ref is not refs/heads/ralph/*,
102540
- # and rejects force pushes unless RALPH_ALLOW_FORCE_PUSH=1.
102541
- set -euo pipefail
102542
- ZERO="0000000000000000000000000000000000000000"
102543
- while read local_ref local_sha remote_ref remote_sha; do
102544
- case "$remote_ref" in
102545
- refs/heads/ralph/*) ;;
102546
- *) echo "ralph: refusing push to $remote_ref (only refs/heads/ralph/* allowed)" >&2; exit 1 ;;
102547
- esac
102548
- if [ "$remote_sha" != "$ZERO" ] && [ "\${RALPH_ALLOW_FORCE_PUSH:-0}" != "1" ]; then
102549
- if ! git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
102550
- echo "ralph: refusing force-push to $remote_ref (set RALPH_ALLOW_FORCE_PUSH=1 to override)" >&2
102551
- exit 1
102552
- fi
102553
- fi
102554
- done
102555
- exit 0
102556
- `;
102557
- var init_worktree = __esm(() => {
102558
- repoWorktreeLocks = new Map;
102559
- });
102560
-
102561
- // apps/agent/src/shared/capabilities/git.ts
102562
- var createWorktree2, removeWorktree2, seedWorktreeMcpConfig2, git;
102563
- var init_git2 = __esm(() => {
102564
- init_types4();
102565
- init_worktree();
102566
- createWorktree2 = {
102567
- name: "git.worktree.create",
102568
- required: true,
102569
- retryPolicy: NO_RETRY,
102570
- errorFormatter: formatError2,
102571
- run: (args) => createWorktree(args.projectRoot, args.changeName, args.baseBranch, args.runner)
102572
- };
102573
- removeWorktree2 = {
102574
- name: "git.worktree.remove",
102575
- required: false,
102576
- retryPolicy: NO_RETRY,
102577
- errorFormatter: formatError2,
102578
- run: (args) => removeWorktree(args.projectRoot, args.cwd, args.runner)
102579
- };
102580
- seedWorktreeMcpConfig2 = {
102581
- name: "git.worktree.seedMcpConfig",
102582
- required: false,
102583
- retryPolicy: NO_RETRY,
102584
- errorFormatter: formatError2,
102585
- run: (args) => seedWorktreeMcpConfig(args.projectRoot, args.worktreeCwd)
102586
- };
102587
- git = { createWorktree: createWorktree2, removeWorktree: removeWorktree2, seedWorktreeMcpConfig: seedWorktreeMcpConfig2 };
102588
- });
102589
-
102590
- // apps/agent/src/shared/capabilities/run-capability.ts
102591
- function emit2(bus, ev) {
102592
- if (!bus)
102593
- return;
102594
- bus.emit(ev);
102595
- }
102596
- async function runCapability(cap, args, ctx = {}) {
102597
- const { bus } = ctx;
102598
- emit2(bus, { type: `${cap.name}.started` });
102599
- let lastError;
102600
- for (let attempt2 = 1;attempt2 <= cap.retryPolicy.maxAttempts; attempt2++) {
102601
- try {
102602
- const raw = await cap.run(args);
102603
- const result2 = cap.adopt ? cap.adopt(raw) : raw;
102604
- emit2(bus, { type: `${cap.name}.fetched` });
102605
- return result2;
102606
- } catch (err) {
102607
- lastError = err;
102608
- const canRetry = attempt2 < cap.retryPolicy.maxAttempts && cap.retryPolicy.isRetryable(err);
102609
- if (!canRetry)
102610
- break;
102611
- const delay2 = Math.max(0, cap.retryPolicy.delayMs(attempt2, err));
102612
- if (delay2 > 0)
102613
- await sleepMs(delay2);
102614
- }
102615
- }
102616
- const message = cap.errorFormatter(lastError);
102617
- emit2(bus, { type: `${cap.name}.failed`, error: message });
102618
- if (cap.required) {
102619
- throw lastError;
102620
- }
102621
- throw lastError;
102622
- }
102623
- function sleepMs(ms) {
102624
- return new Promise((resolve4) => setTimeout(resolve4, ms));
102625
- }
102626
-
102627
- // packages/workflow/src/boundaries.ts
102628
- function globToRegex(pattern) {
102629
- let re = "^";
102630
- for (let i = 0;i < pattern.length; i++) {
102631
- const c = pattern[i];
102632
- if (c === "*") {
102633
- if (pattern[i + 1] === "*") {
102634
- re += ".*";
102635
- i++;
102636
- if (pattern[i + 1] === "/")
102637
- i++;
102638
- } else {
102639
- re += "[^/]*";
102640
- }
102641
- } else if (c === "?") {
102642
- re += "[^/]";
102643
- } else if (/[.+^${}()|[\]\\]/.test(c)) {
102644
- re += "\\" + c;
102645
- } else {
102646
- re += c;
102647
- }
102648
- }
102649
- re += "$";
102650
- return new RegExp(re);
102651
- }
102652
- function findBoundaryViolations(changedFiles, patterns) {
102653
- if (patterns.length === 0 || changedFiles.length === 0)
102654
- return [];
102655
- const compiled = patterns.map((p) => ({ pattern: p, re: globToRegex(p) }));
102656
- const out = [];
102657
- for (const file2 of changedFiles) {
102658
- const norm = file2.replace(/\\/g, "/");
102659
- for (const { pattern, re } of compiled) {
102660
- if (re.test(norm)) {
102661
- out.push({ file: norm, pattern });
102662
- break;
102663
- }
102664
- }
102665
- }
102666
- return out;
102667
- }
102668
-
102669
102603
  // apps/agent/src/shared/utils/ralph-comment.ts
102670
102604
  function isRalphComment(body) {
102671
102605
  const trimmed = body.trimStart();
102672
- return /^(\uD83E\uDD16|\uD83D\uDD04|\u2705|\u2717|\u274C|\u26A0|\uD83D\uDD01|\uD83D\uDCCB|\u23F0)\s*Ralphy?\b/.test(trimmed);
102606
+ if (/^(\uD83E\uDD16|\uD83D\uDD04|\u2705|\u2717|\u274C|\u26A0|\uD83D\uDD01|\uD83D\uDCCB|\u23F0)\s*Ralphy?\b/.test(trimmed))
102607
+ return true;
102608
+ return /^\uD83D\uDC40\s*(Got it\b|Acknowledged\b)/.test(trimmed);
102673
102609
  }
102674
102610
 
102675
102611
  // apps/agent/src/shared/capabilities/linear-client.ts
@@ -102700,6 +102636,7 @@ __export(exports_linear_client, {
102700
102636
  fetchIssueLabels: () => fetchIssueLabels,
102701
102637
  fetchIssueComments: () => fetchIssueComments,
102702
102638
  fetchIssueAttachments: () => fetchIssueAttachments,
102639
+ fetchBlockedByForIssues: () => fetchBlockedByForIssues,
102703
102640
  fetchAttachmentsForIssues: () => fetchAttachmentsForIssues,
102704
102641
  deleteIssueComment: () => deleteIssueComment,
102705
102642
  deleteAttachment: () => deleteAttachment,
@@ -102787,6 +102724,19 @@ function partition2(markers) {
102787
102724
  }
102788
102725
  return { statuses, labels, attachmentSubtitles, projects };
102789
102726
  }
102727
+ function applyRequiredLabels(where, requireAllLabels) {
102728
+ if (!requireAllLabels || requireAllLabels.length === 0)
102729
+ return;
102730
+ const and2 = where.and ?? [];
102731
+ if (where.labels !== undefined) {
102732
+ and2.push({ labels: where.labels });
102733
+ delete where.labels;
102734
+ }
102735
+ for (const label of requireAllLabels) {
102736
+ and2.push({ labels: { some: { name: { eq: label } } } });
102737
+ }
102738
+ where.and = and2;
102739
+ }
102790
102740
  function buildIssueFilter(spec) {
102791
102741
  const where = {};
102792
102742
  if (spec.team)
@@ -102885,6 +102835,7 @@ function buildIssueFilter(spec) {
102885
102835
  }
102886
102836
  }
102887
102837
  }
102838
+ applyRequiredLabels(where, spec.requireAllLabels);
102888
102839
  return where;
102889
102840
  }
102890
102841
  function clauseFromMarkers(markers) {
@@ -102965,6 +102916,7 @@ async function fetchMentionScanIssues(apiKey, spec) {
102965
102916
  if (spec.numbers && spec.numbers.length > 0) {
102966
102917
  where.number = { in: spec.numbers };
102967
102918
  }
102919
+ applyRequiredLabels(where, spec.requireAllLabels);
102968
102920
  const query = `query MentionScanIssues($filter: IssueFilter) {
102969
102921
  issues(filter: $filter, first: 50) {
102970
102922
  nodes {
@@ -103356,6 +103308,28 @@ async function fetchAttachmentsForIssues(apiKey, issueIds) {
103356
103308
  }
103357
103309
  return out;
103358
103310
  }
103311
+ async function fetchBlockedByForIssues(apiKey, issueIds) {
103312
+ const out = new Map;
103313
+ if (issueIds.length === 0)
103314
+ return out;
103315
+ const query = `query IssuesBlockedBy($ids: [ID!]!) {
103316
+ issues(filter: { id: { in: $ids } }, first: 250) {
103317
+ nodes {
103318
+ id
103319
+ relations(first: 50) {
103320
+ nodes { type relatedIssue { id identifier state { type } } }
103321
+ }
103322
+ }
103323
+ }
103324
+ }`;
103325
+ const data = await linearRequest(apiKey, query, { ids: issueIds });
103326
+ const DONE_STATE_TYPES = new Set(["completed", "cancelled"]);
103327
+ for (const node2 of data.issues.nodes) {
103328
+ const blockers = (node2.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => ({ id: r.relatedIssue.id, identifier: r.relatedIssue.identifier }));
103329
+ out.set(node2.id, blockers);
103330
+ }
103331
+ return out;
103332
+ }
103359
103333
  async function findIssueAttachmentByTitle(apiKey, issueId, title) {
103360
103334
  const query = `query IssueAttachmentByTitle($id: String!) {
103361
103335
  issue(id: $id) {
@@ -103576,6 +103550,326 @@ var init_linear_client = __esm(() => {
103576
103550
  };
103577
103551
  });
103578
103552
 
103553
+ // apps/agent/src/shared/capabilities/types.ts
103554
+ var NO_RETRY;
103555
+ var init_types4 = __esm(() => {
103556
+ NO_RETRY = {
103557
+ maxAttempts: 1,
103558
+ isRetryable: () => false,
103559
+ delayMs: () => 0
103560
+ };
103561
+ });
103562
+
103563
+ // apps/agent/src/shared/capabilities/format-error.ts
103564
+ function formatError2(err) {
103565
+ if (err instanceof Error)
103566
+ return err.message;
103567
+ try {
103568
+ return String(err);
103569
+ } catch {
103570
+ return "unknown error";
103571
+ }
103572
+ }
103573
+
103574
+ // apps/agent/src/shared/capabilities/fs-change.ts
103575
+ import { join as join21, dirname as dirname8 } from "path";
103576
+ import { mkdir as mkdir7 } from "fs/promises";
103577
+ var scaffold, prependTask, appendSteering, fsChange;
103578
+ var init_fs_change = __esm(() => {
103579
+ init_tasks_md();
103580
+ init_types4();
103581
+ scaffold = {
103582
+ name: "fs.change.scaffold",
103583
+ required: false,
103584
+ retryPolicy: NO_RETRY,
103585
+ errorFormatter: formatError2,
103586
+ run: async (args) => {
103587
+ await mkdir7(args.changeDir, { recursive: true });
103588
+ await mkdir7(join21(args.changeDir, "specs"), { recursive: true });
103589
+ await mkdir7(args.stateDir, { recursive: true });
103590
+ await Bun.write(join21(args.changeDir, "proposal.md"), args.proposal);
103591
+ await Bun.write(join21(args.changeDir, "tasks.md"), args.tasks);
103592
+ await Bun.write(join21(args.changeDir, "design.md"), args.design);
103593
+ }
103594
+ };
103595
+ prependTask = {
103596
+ name: "fs.change.task.prepend",
103597
+ required: false,
103598
+ retryPolicy: NO_RETRY,
103599
+ errorFormatter: formatError2,
103600
+ run: async (args) => {
103601
+ await prependFixTask(args.tasksPath, args.heading, args.failureOutput);
103602
+ }
103603
+ };
103604
+ appendSteering = {
103605
+ name: "fs.change.steering.append",
103606
+ required: false,
103607
+ retryPolicy: NO_RETRY,
103608
+ errorFormatter: formatError2,
103609
+ run: async (args) => {
103610
+ const path = join21(args.changeDir, "steering.md");
103611
+ const f2 = Bun.file(path);
103612
+ const existing = await f2.exists() ? await f2.text() : null;
103613
+ const updated = existing ? `${args.message}
103614
+
103615
+ ${existing.trimStart()}` : `${args.message}
103616
+ `;
103617
+ await mkdir7(dirname8(path), { recursive: true });
103618
+ await Bun.write(path, updated);
103619
+ }
103620
+ };
103621
+ fsChange = { scaffold, prependTask, appendSteering };
103622
+ });
103623
+
103624
+ // apps/agent/src/agent/worktree.ts
103625
+ import { basename as basename2, join as join22 } from "path";
103626
+ import { homedir as homedir5 } from "os";
103627
+ import { exists as exists3 } from "fs/promises";
103628
+ function worktreesDir2(projectRoot) {
103629
+ return join22(homedir5(), ".ralph", basename2(projectRoot), "worktrees");
103630
+ }
103631
+ function branchForChange(changeName) {
103632
+ return `ralph/${changeName}`;
103633
+ }
103634
+ function worktreeDirNameForIssue(issue2) {
103635
+ return issue2.identifier.toLowerCase();
103636
+ }
103637
+ function withRepoLock(projectRoot, fn) {
103638
+ const prev = repoWorktreeLocks.get(projectRoot) ?? Promise.resolve();
103639
+ const result2 = prev.then(fn, fn);
103640
+ repoWorktreeLocks.set(projectRoot, result2.then(() => {}, () => {}));
103641
+ return result2;
103642
+ }
103643
+ function createWorktree(projectRoot, changeName, baseBranch, runner) {
103644
+ return withRepoLock(projectRoot, () => provisionWorktree(projectRoot, changeName, baseBranch, runner));
103645
+ }
103646
+ async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
103647
+ const dir = worktreesDir2(projectRoot);
103648
+ const cwd2 = join22(dir, changeName);
103649
+ const branch = branchForChange(changeName);
103650
+ const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
103651
+ if (list.stdout.includes(`worktree ${cwd2}
103652
+ `)) {
103653
+ await installPrePushHook(cwd2, runner);
103654
+ return { cwd: cwd2, branch };
103655
+ }
103656
+ let branchExists = true;
103657
+ try {
103658
+ await runner.run(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], projectRoot);
103659
+ } catch {
103660
+ branchExists = false;
103661
+ }
103662
+ if (branchExists) {
103663
+ await runner.run(["worktree", "add", cwd2, branch], projectRoot);
103664
+ await installPrePushHook(cwd2, runner);
103665
+ return { cwd: cwd2, branch };
103666
+ }
103667
+ await runner.run(["fetch", "origin", baseBranch], projectRoot);
103668
+ await runner.run(["worktree", "add", "-b", branch, cwd2, `origin/${baseBranch}`], projectRoot);
103669
+ await installPrePushHook(cwd2, runner);
103670
+ return { cwd: cwd2, branch };
103671
+ }
103672
+ async function installPrePushHook(cwd2, runner) {
103673
+ const hookPath = join22(cwd2, ".ralph-hooks", "pre-push");
103674
+ await Bun.write(hookPath, PRE_PUSH_HOOK_SCRIPT);
103675
+ const chmod = Bun.spawn(["chmod", "+x", hookPath]);
103676
+ await chmod.exited;
103677
+ await runner.run(["config", "core.hooksPath", ".ralph-hooks"], cwd2);
103678
+ }
103679
+ async function removeWorktree(projectRoot, cwd2, runner) {
103680
+ await runner.run(["worktree", "remove", "--force", cwd2], projectRoot);
103681
+ }
103682
+ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
103683
+ const status = await runner.run(["status", "--porcelain"], cwd2);
103684
+ const dirty = status.stdout.trim();
103685
+ let unpushedCommits = "";
103686
+ try {
103687
+ const log3 = await runner.run(["log", "--oneline", `${base2}..HEAD`, "--no-merges"], cwd2);
103688
+ unpushedCommits = log3.stdout.trim();
103689
+ } catch {
103690
+ unpushedCommits = "<unknown: failed to compare against base>";
103691
+ }
103692
+ if (dirty && unpushedCommits) {
103693
+ return {
103694
+ safe: false,
103695
+ reason: "uncommitted changes AND unpushed commits present",
103696
+ dirty,
103697
+ unpushedCommits
103698
+ };
103699
+ }
103700
+ if (dirty) {
103701
+ return {
103702
+ safe: false,
103703
+ reason: "uncommitted or untracked files present",
103704
+ dirty,
103705
+ unpushedCommits
103706
+ };
103707
+ }
103708
+ if (unpushedCommits) {
103709
+ return {
103710
+ safe: false,
103711
+ reason: `commits ahead of ${base2} were not pushed/PR'd`,
103712
+ dirty,
103713
+ unpushedCommits
103714
+ };
103715
+ }
103716
+ return { safe: true, dirty, unpushedCommits };
103717
+ }
103718
+ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
103719
+ const dst = join22(worktreeCwd, ".mcp.json");
103720
+ const src = join22(projectRoot, ".mcp.json");
103721
+ const source = await exists3(dst) ? dst : await exists3(src) ? src : null;
103722
+ if (!source)
103723
+ return;
103724
+ let parsed;
103725
+ try {
103726
+ parsed = await Bun.file(source).json();
103727
+ } catch {
103728
+ return;
103729
+ }
103730
+ const servers = parsed.mcpServers;
103731
+ if (servers && typeof servers === "object") {
103732
+ for (const cfg of Object.values(servers)) {
103733
+ if (Array.isArray(cfg.args)) {
103734
+ cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join22(projectRoot, a) : a);
103735
+ }
103736
+ }
103737
+ }
103738
+ await Bun.write(dst, JSON.stringify(parsed, null, 2) + `
103739
+ `);
103740
+ }
103741
+ var repoWorktreeLocks, PRE_PUSH_HOOK_SCRIPT = `#!/usr/bin/env bash
103742
+ # Installed by ralphy createWorktree (RLF-107).
103743
+ # Rejects any push whose remote ref is not refs/heads/ralph/*,
103744
+ # and rejects force pushes unless RALPH_ALLOW_FORCE_PUSH=1.
103745
+ set -euo pipefail
103746
+ ZERO="0000000000000000000000000000000000000000"
103747
+ while read local_ref local_sha remote_ref remote_sha; do
103748
+ case "$remote_ref" in
103749
+ refs/heads/ralph/*) ;;
103750
+ *) echo "ralph: refusing push to $remote_ref (only refs/heads/ralph/* allowed)" >&2; exit 1 ;;
103751
+ esac
103752
+ if [ "$remote_sha" != "$ZERO" ] && [ "\${RALPH_ALLOW_FORCE_PUSH:-0}" != "1" ]; then
103753
+ if ! git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
103754
+ echo "ralph: refusing force-push to $remote_ref (set RALPH_ALLOW_FORCE_PUSH=1 to override)" >&2
103755
+ exit 1
103756
+ fi
103757
+ fi
103758
+ done
103759
+ exit 0
103760
+ `;
103761
+ var init_worktree = __esm(() => {
103762
+ repoWorktreeLocks = new Map;
103763
+ });
103764
+
103765
+ // apps/agent/src/shared/capabilities/git.ts
103766
+ var createWorktree2, removeWorktree2, seedWorktreeMcpConfig2, git;
103767
+ var init_git2 = __esm(() => {
103768
+ init_types4();
103769
+ init_worktree();
103770
+ createWorktree2 = {
103771
+ name: "git.worktree.create",
103772
+ required: true,
103773
+ retryPolicy: NO_RETRY,
103774
+ errorFormatter: formatError2,
103775
+ run: (args) => createWorktree(args.projectRoot, args.changeName, args.baseBranch, args.runner)
103776
+ };
103777
+ removeWorktree2 = {
103778
+ name: "git.worktree.remove",
103779
+ required: false,
103780
+ retryPolicy: NO_RETRY,
103781
+ errorFormatter: formatError2,
103782
+ run: (args) => removeWorktree(args.projectRoot, args.cwd, args.runner)
103783
+ };
103784
+ seedWorktreeMcpConfig2 = {
103785
+ name: "git.worktree.seedMcpConfig",
103786
+ required: false,
103787
+ retryPolicy: NO_RETRY,
103788
+ errorFormatter: formatError2,
103789
+ run: (args) => seedWorktreeMcpConfig(args.projectRoot, args.worktreeCwd)
103790
+ };
103791
+ git = { createWorktree: createWorktree2, removeWorktree: removeWorktree2, seedWorktreeMcpConfig: seedWorktreeMcpConfig2 };
103792
+ });
103793
+
103794
+ // apps/agent/src/shared/capabilities/run-capability.ts
103795
+ function emit2(bus, ev) {
103796
+ if (!bus)
103797
+ return;
103798
+ bus.emit(ev);
103799
+ }
103800
+ async function runCapability(cap, args, ctx = {}) {
103801
+ const { bus } = ctx;
103802
+ emit2(bus, { type: `${cap.name}.started` });
103803
+ let lastError;
103804
+ for (let attempt2 = 1;attempt2 <= cap.retryPolicy.maxAttempts; attempt2++) {
103805
+ try {
103806
+ const raw = await cap.run(args);
103807
+ const result2 = cap.adopt ? cap.adopt(raw) : raw;
103808
+ emit2(bus, { type: `${cap.name}.fetched` });
103809
+ return result2;
103810
+ } catch (err) {
103811
+ lastError = err;
103812
+ const canRetry = attempt2 < cap.retryPolicy.maxAttempts && cap.retryPolicy.isRetryable(err);
103813
+ if (!canRetry)
103814
+ break;
103815
+ const delay2 = Math.max(0, cap.retryPolicy.delayMs(attempt2, err));
103816
+ if (delay2 > 0)
103817
+ await sleepMs(delay2);
103818
+ }
103819
+ }
103820
+ const message = cap.errorFormatter(lastError);
103821
+ emit2(bus, { type: `${cap.name}.failed`, error: message });
103822
+ if (cap.required) {
103823
+ throw lastError;
103824
+ }
103825
+ throw lastError;
103826
+ }
103827
+ function sleepMs(ms) {
103828
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
103829
+ }
103830
+
103831
+ // packages/workflow/src/boundaries.ts
103832
+ function globToRegex(pattern) {
103833
+ let re = "^";
103834
+ for (let i = 0;i < pattern.length; i++) {
103835
+ const c = pattern[i];
103836
+ if (c === "*") {
103837
+ if (pattern[i + 1] === "*") {
103838
+ re += ".*";
103839
+ i++;
103840
+ if (pattern[i + 1] === "/")
103841
+ i++;
103842
+ } else {
103843
+ re += "[^/]*";
103844
+ }
103845
+ } else if (c === "?") {
103846
+ re += "[^/]";
103847
+ } else if (/[.+^${}()|[\]\\]/.test(c)) {
103848
+ re += "\\" + c;
103849
+ } else {
103850
+ re += c;
103851
+ }
103852
+ }
103853
+ re += "$";
103854
+ return new RegExp(re);
103855
+ }
103856
+ function findBoundaryViolations(changedFiles, patterns) {
103857
+ if (patterns.length === 0 || changedFiles.length === 0)
103858
+ return [];
103859
+ const compiled = patterns.map((p) => ({ pattern: p, re: globToRegex(p) }));
103860
+ const out = [];
103861
+ for (const file2 of changedFiles) {
103862
+ const norm = file2.replace(/\\/g, "/");
103863
+ for (const { pattern, re } of compiled) {
103864
+ if (re.test(norm)) {
103865
+ out.push({ file: norm, pattern });
103866
+ break;
103867
+ }
103868
+ }
103869
+ }
103870
+ return out;
103871
+ }
103872
+
103579
103873
  // apps/agent/src/agent/linear.ts
103580
103874
  var init_linear = __esm(() => {
103581
103875
  init_linear_client();
@@ -103770,131 +104064,14 @@ function classifyGhBucket(bucket) {
103770
104064
  return "pending";
103771
104065
  return "pass";
103772
104066
  }
103773
- var TRANSIENT_GH_RE, NO_CHECKS_RE, GH_RETRY_DELAYS;
104067
+ var TRANSIENT_GH_RE, NO_CHECKS_RE, PARTIAL_ACCESS_RE, GH_RETRY_DELAYS;
103774
104068
  var init_ci_classify = __esm(() => {
103775
104069
  TRANSIENT_GH_RE = /HTTP 5\d\d|Gateway Timeout|Bad Gateway|Service Unavailable|connection reset|ECONNRESET|ETIMEDOUT|getaddrinfo|EAI_AGAIN|could not resolve host/i;
103776
104070
  NO_CHECKS_RE = /no checks reported/i;
104071
+ PARTIAL_ACCESS_RE = /Resource not accessible by personal access token/i;
103777
104072
  GH_RETRY_DELAYS = [5000, 15000, 45000];
103778
104073
  });
103779
104074
 
103780
- // apps/agent/src/agent/ci.ts
103781
- async function getPrChecksStatus(prRef, runner, cwd2, onTransientRetry, ignoreCiChecks = []) {
103782
- let out;
103783
- try {
103784
- out = await runGhWithRetry(["gh", "pr", "checks", prRef, "--json", PR_CHECKS_FIELDS], runner, cwd2, onTransientRetry);
103785
- } catch (err) {
103786
- const e = err;
103787
- const blob = `${e.message}
103788
- ${e.stderr ?? ""}
103789
- ${e.stdout ?? ""}`;
103790
- if (NO_CHECKS_RE.test(blob))
103791
- return { bucket: "pass", failedRunIds: [], failedCheckNames: [] };
103792
- throw err;
103793
- }
103794
- const ignoredLower = ignoreCiChecks.map((n) => n.toLowerCase());
103795
- const checks3 = JSON.parse(out.stdout || "[]").filter((c) => !ignoredLower.includes(c.name.toLowerCase())).filter((c) => classifyGhBucket(c.bucket) !== "skip");
103796
- if (checks3.some((c) => classifyGhBucket(c.bucket) === "pending")) {
103797
- return { bucket: "pending", failedRunIds: [], failedCheckNames: [] };
103798
- }
103799
- const failed = checks3.filter((c) => classifyGhBucket(c.bucket) === "fail");
103800
- if (failed.length === 0)
103801
- return { bucket: "pass", failedRunIds: [], failedCheckNames: [] };
103802
- const ids = new Set;
103803
- for (const c of failed) {
103804
- const m = c.link?.match(/\/actions\/runs\/(\d+)/);
103805
- if (m)
103806
- ids.add(m[1]);
103807
- }
103808
- return { bucket: "fail", failedRunIds: [...ids], failedCheckNames: failed.map((c) => c.name) };
103809
- }
103810
- async function fetchFailedRunLogs(runIds, runner, cwd2, maxCharsPerRun = 4000) {
103811
- const chunks = [];
103812
- for (const id of runIds) {
103813
- try {
103814
- const r = await runner.run(["gh", "run", "view", id, "--log-failed"], cwd2);
103815
- const text = r.stdout.trim();
103816
- const truncated = text.length > maxCharsPerRun ? text.slice(0, maxCharsPerRun) + `
103817
- \u2026[truncated ${text.length - maxCharsPerRun} chars]` : text;
103818
- chunks.push(`--- run ${id} ---
103819
- ${truncated}`);
103820
- } catch (err) {
103821
- chunks.push(`--- run ${id} ---
103822
- (failed to fetch logs: ${err.message})`);
103823
- }
103824
- }
103825
- return chunks.join(`
103826
-
103827
- `);
103828
- }
103829
- async function safeSha(getHeadSha) {
103830
- try {
103831
- const sha = (await getHeadSha()).trim();
103832
- return sha || null;
103833
- } catch {
103834
- return null;
103835
- }
103836
- }
103837
- async function fixCiUntilGreen(deps, opts) {
103838
- for (let attempt2 = 1;attempt2 <= opts.maxAttempts; attempt2++) {
103839
- let pollN = 0;
103840
- while (true) {
103841
- if (deps.cancelled?.())
103842
- return { success: false, attempts: attempt2 - 1, reason: "cancelled" };
103843
- pollN += 1;
103844
- deps.onPhase?.("ci-poll", `attempt ${attempt2}/${opts.maxAttempts} \xB7 poll ${pollN}`);
103845
- let s;
103846
- try {
103847
- s = await deps.getStatus();
103848
- } catch (err) {
103849
- deps.log(`! gh pr checks failed permanently: ${err.message} \u2014 giving up CI watch`, "red");
103850
- return { success: false, attempts: attempt2 - 1, reason: "gh-failed" };
103851
- }
103852
- if (s.bucket === "pass") {
103853
- deps.log(`\u2713 CI green for PR (after ${attempt2 - 1} fix attempts)`, "green");
103854
- return { success: true, attempts: attempt2 - 1 };
103855
- }
103856
- if (s.bucket === "fail") {
103857
- deps.log(`\u2717 CI failing (attempt ${attempt2}/${opts.maxAttempts}) \u2014 fetching logs and re-running task`, "yellow");
103858
- deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 fetching logs`);
103859
- const logs = await deps.getFailedLogs(s.failedRunIds);
103860
- deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 re-running worker`);
103861
- const steering = `CI is failing on this PR. Investigate and fix:
103862
-
103863
- \`\`\`
103864
- ${logs}
103865
- \`\`\``;
103866
- const shaBefore = deps.getHeadSha ? await safeSha(deps.getHeadSha) : null;
103867
- const code = await deps.runTaskWithSteering(steering);
103868
- if (code !== 0) {
103869
- deps.log(`! task loop exited code ${code} during CI fix attempt ${attempt2}`, "red");
103870
- }
103871
- if (shaBefore !== null) {
103872
- const shaAfter = await safeSha(deps.getHeadSha);
103873
- if (shaAfter !== null && shaAfter === shaBefore) {
103874
- deps.log(`! worker produced no new commits on CI fix attempt ${attempt2} \u2014 failure looks external (e.g. rate-limited deploy). Giving up CI watch.`, "yellow");
103875
- return { success: false, attempts: attempt2, reason: "no-progress" };
103876
- }
103877
- }
103878
- try {
103879
- deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pushing fix`);
103880
- await deps.pushBranch();
103881
- } catch (err) {
103882
- deps.log(`! push failed during CI fix: ${err.message}`, "red");
103883
- return { success: false, attempts: attempt2, reason: "push-failed" };
103884
- }
103885
- break;
103886
- }
103887
- deps.onPhase?.("ci-poll", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pending, waiting`);
103888
- await deps.sleep(opts.pollIntervalSeconds * 1000);
103889
- }
103890
- }
103891
- return { success: false, attempts: opts.maxAttempts, reason: "max-attempts" };
103892
- }
103893
- var PR_CHECKS_FIELDS = "name,bucket,link,workflow,event";
103894
- var init_ci = __esm(() => {
103895
- init_ci_classify();
103896
- });
103897
-
103898
104075
  // apps/agent/src/pr-status.ts
103899
104076
  function bucketChecks(rollup, prState, ignoreCiChecks = []) {
103900
104077
  if (rollup === null || rollup === undefined) {
@@ -104498,33 +104675,9 @@ async function runWorkerWithFixTask(ctx, heading, body) {
104498
104675
  }
104499
104676
  return code;
104500
104677
  }
104501
- async function pushBranchSafely(ctx) {
104502
- try {
104503
- ctx.emit("pushing", "after conflict resolution");
104504
- await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
104505
- return true;
104506
- } catch (pushErr) {
104507
- const pe = pushErr;
104508
- const blob = `${pe.message}
104509
- ${pe.stderr ?? ""}`;
104510
- if (!/non-fast-forward|Updates were rejected/i.test(blob)) {
104511
- ctx.log(`! push after conflict fix failed: ${pe.message}`, "red");
104512
- return false;
104513
- }
104514
- try {
104515
- await ctx.cmd.run(["git", "fetch", "origin", ctx.branch], ctx.cwd);
104516
- await ctx.cmd.run(["git", "merge", "--no-edit", `origin/${ctx.branch}`], ctx.cwd);
104517
- await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
104518
- return true;
104519
- } catch (retryErr) {
104520
- ctx.log(`! push after merging origin/${ctx.branch} failed: ${retryErr.message}`, "red");
104521
- return false;
104522
- }
104523
- }
104524
- }
104525
104678
  async function createPrWithRetry(ctx, issue2) {
104526
104679
  const base2 = ctx.base;
104527
- const maxAttempts = ctx.cfg.maxCiFixAttempts;
104680
+ const maxAttempts = MAX_PR_CREATE_ATTEMPTS;
104528
104681
  let hookFixAttempt = 0;
104529
104682
  let nonFfRebaseAttempted = false;
104530
104683
  let pr = null;
@@ -104637,86 +104790,6 @@ ${reBlob.trim()}`);
104637
104790
  }
104638
104791
  }
104639
104792
  }
104640
- async function fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict) {
104641
- const wantConflictLoop = !!checkPrConflict;
104642
- const maxOuterAttempts = ctx.cfg.maxCiFixAttempts;
104643
- let outerAttempt = 0;
104644
- let ciConfirmedGreen = false;
104645
- while (outerAttempt < maxOuterAttempts) {
104646
- if (wantConflictLoop) {
104647
- ctx.emit("conflict-check");
104648
- let conflicting = false;
104649
- try {
104650
- conflicting = await checkPrConflict(prUrl);
104651
- } catch (err) {
104652
- ctx.log(`! conflict check failed: ${err.message}`, "yellow");
104653
- }
104654
- if (!conflicting && ciConfirmedGreen)
104655
- return 0;
104656
- if (conflicting) {
104657
- outerAttempt++;
104658
- ciConfirmedGreen = false;
104659
- ctx.emit("conflict-fix-inner", `attempt ${outerAttempt}/${maxOuterAttempts}`);
104660
- ctx.log(` merge conflicts on PR (attempt ${outerAttempt}/${maxOuterAttempts}) \u2014 spawning resolution task`, "yellow");
104661
- const conflictCode = await runWorkerWithFixTask(ctx, "Resolve PR merge conflicts", [
104662
- `The PR ${prUrl} has merge conflicts with \`${ctx.base}\`.`,
104663
- "",
104664
- "Steps:",
104665
- `1. \`git fetch origin ${ctx.base}\` then merge \`${ctx.base}\` into the current branch (\`git merge origin/${ctx.base}\`). Do NOT rebase and do NOT amend existing commits.`,
104666
- "2. Resolve conflicts in the files git lists.",
104667
- "3. Stage and commit the resolution as a new merge commit."
104668
- ].join(`
104669
- `));
104670
- if (conflictCode !== 0) {
104671
- ctx.log(`! conflict resolution worker exited code ${conflictCode} \u2014 giving up`, "red");
104672
- return PR_FAILED_EXIT;
104673
- }
104674
- const pushed = await pushBranchSafely(ctx);
104675
- if (!pushed)
104676
- return PR_FAILED_EXIT;
104677
- continue;
104678
- }
104679
- }
104680
- if (!wantFixCi)
104681
- break;
104682
- if (!ciConfirmedGreen) {
104683
- ctx.log(` watching CI for ${prUrl} (max ${ctx.cfg.maxCiFixAttempts} fix attempts)`, "gray");
104684
- ctx.emit("ci-poll", "starting");
104685
- const result2 = await fixCiUntilGreen({
104686
- onPhase: (p, d) => ctx.emit(p, d),
104687
- getStatus: () => getPrChecksStatus(prUrl, ctx.cmd, ctx.cwd, (n, ms, why) => ctx.log(` gh transient (try ${n}) \u2014 retry in ${Math.round(ms / 1000)}s \xB7 ${why}`, "yellow"), ctx.cfg.ignoreCiChecks),
104688
- getFailedLogs: (ids) => fetchFailedRunLogs(ids, ctx.cmd, ctx.cwd),
104689
- runTaskWithSteering: (steering) => runWorkerWithFixTask(ctx, "Fix failing CI checks", steering),
104690
- pushBranch: async () => {
104691
- await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
104692
- },
104693
- getHeadSha: async () => {
104694
- const r = await ctx.cmd.run(["git", "rev-parse", "HEAD"], ctx.cwd);
104695
- return r.stdout.trim();
104696
- },
104697
- log: ctx.log,
104698
- sleep: (ms) => new Promise((r) => setTimeout(r, ms))
104699
- }, {
104700
- maxAttempts: ctx.cfg.maxCiFixAttempts,
104701
- pollIntervalSeconds: ctx.cfg.ciPollIntervalSeconds
104702
- });
104703
- if (!result2.success) {
104704
- ctx.log(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"}) \u2014 withholding done-status until CI passes`, "red");
104705
- return CI_FAILED_EXIT;
104706
- }
104707
- ciConfirmedGreen = true;
104708
- }
104709
- if (wantConflictLoop) {
104710
- continue;
104711
- }
104712
- return 0;
104713
- }
104714
- if (outerAttempt >= maxOuterAttempts) {
104715
- ctx.log(`! outer fix loop exhausted ${maxOuterAttempts} attempts \u2014 giving up`, "red");
104716
- return CI_FAILED_EXIT;
104717
- }
104718
- return 0;
104719
- }
104720
104793
  async function findNeverTouchViolations(cmd, cwd2, base2, neverTouch) {
104721
104794
  if (neverTouch.length === 0)
104722
104795
  return [];
@@ -104737,27 +104810,8 @@ async function findNeverTouchViolations(cmd, cwd2, base2, neverTouch) {
104737
104810
  return findBoundaryViolations(files, neverTouch);
104738
104811
  }
104739
104812
  async function runPrPhase(input, deps) {
104740
- const {
104741
- changeName,
104742
- cwd: cwd2,
104743
- branch,
104744
- changeDir,
104745
- stateFilePath,
104746
- issue: issue2,
104747
- wantFixCi,
104748
- wantAutoMerge,
104749
- cfg
104750
- } = input;
104751
- const {
104752
- cmd,
104753
- log: log3,
104754
- emit: emit3,
104755
- respawnWorker,
104756
- registerPr,
104757
- onPrReady,
104758
- checkPrConflict,
104759
- resolveDependencyBaseBranch
104760
- } = deps;
104813
+ const { changeName, cwd: cwd2, branch, changeDir, stateFilePath, issue: issue2, wantAutoMerge, cfg } = input;
104814
+ const { cmd, log: log3, emit: emit3, respawnWorker, registerPr, onPrReady, resolveDependencyBaseBranch } = deps;
104761
104815
  if (!branch || !issue2) {
104762
104816
  log3(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
104763
104817
  return PR_FAILED_EXIT;
@@ -104824,7 +104878,7 @@ ${indented}${suffix}`, "yellow");
104824
104878
  }
104825
104879
  return PR_FAILED_EXIT;
104826
104880
  }
104827
- const maxOuterAttempts = cfg.maxCiFixAttempts;
104881
+ const maxOuterAttempts = MAX_PR_CREATE_ATTEMPTS;
104828
104882
  let onlyMetaAttempts = 0;
104829
104883
  let pr = null;
104830
104884
  const finalizeNoOpAsDone = cfg.finalizeNoOpAsDone !== false;
@@ -104899,36 +104953,8 @@ ${indented}${suffix}`, "yellow");
104899
104953
  }
104900
104954
  log3(` ${pr.created ? "opened" : "found existing"} PR: ${prUrl}`, "green");
104901
104955
  registerPr?.(changeName, prUrl);
104902
- let manualMergePending = false;
104903
- const prReadyNeeded = cfg.prDraft === true;
104904
- if (!prReadyNeeded && wantAutoMerge) {
104905
- const fallbackEnabled = cfg.manualMergeWhenAutoMergeDisabled !== false;
104906
- const repoAllowsAutoMerge = await detectRepoAutoMergeAllowed(prUrl, cmd, cwd2, log3);
104907
- if (repoAllowsAutoMerge === false && fallbackEnabled) {
104908
- log3(` repo has auto-merge disabled \u2014 will poll ${prUrl} and merge via gh pr merge once checks pass`, "yellow");
104909
- manualMergePending = true;
104910
- } else {
104911
- try {
104912
- await cmd.run(["gh", "pr", "merge", prUrl, "--auto", `--${cfg.autoMergeStrategy}`], cwd2);
104913
- log3(` enabled auto-merge (${cfg.autoMergeStrategy}) on ${prUrl}`, "green");
104914
- emit3("auto-merge-enabled", cfg.autoMergeStrategy);
104915
- } catch (err) {
104916
- const e = err;
104917
- const detail = e.stderr?.trim() || e.message;
104918
- log3(`! failed to enable auto-merge on ${prUrl}: ${detail}`, "yellow");
104919
- if (fallbackEnabled && /auto[- ]merge/i.test(detail)) {
104920
- log3(` falling back to manual merge after CI passes for ${prUrl}`, "yellow");
104921
- manualMergePending = true;
104922
- }
104923
- }
104924
- }
104925
- } else if (prReadyNeeded && wantAutoMerge) {
104926
- manualMergePending = true;
104927
- }
104928
- const ciResult = await fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict);
104929
- if (ciResult !== 0)
104930
- return ciResult;
104931
- if (prReadyNeeded) {
104956
+ let readyOk = true;
104957
+ if (cfg.prDraft === true) {
104932
104958
  emit3("pr-ready");
104933
104959
  try {
104934
104960
  await cmd.run(["gh", "pr", "ready", prUrl], cwd2);
@@ -104936,22 +104962,25 @@ ${indented}${suffix}`, "yellow");
104936
104962
  } catch (err) {
104937
104963
  const e = err;
104938
104964
  log3(`! gh pr ready failed for ${prUrl}: ${e.stderr?.trim() || e.message}`, "yellow");
104939
- manualMergePending = false;
104965
+ readyOk = false;
104940
104966
  }
104941
104967
  }
104942
- if (manualMergePending) {
104943
- try {
104944
- await cmd.run(["gh", "pr", "merge", prUrl, `--${cfg.autoMergeStrategy}`], cwd2);
104945
- log3(` manually merged (${cfg.autoMergeStrategy}) ${prUrl}`, "green");
104946
- emit3("auto-merge-enabled", `manual:${cfg.autoMergeStrategy}`);
104947
- } catch (err) {
104948
- const e = err;
104949
- log3(`! manual merge failed for ${prUrl}: ${e.stderr?.trim() || e.message}`, "yellow");
104968
+ if (wantAutoMerge && readyOk) {
104969
+ const repoAllowsAutoMerge = await detectRepoAutoMergeAllowed(prUrl, cmd, cwd2, log3);
104970
+ if (repoAllowsAutoMerge === false) {
104971
+ log3(cfg.manualMergeWhenAutoMergeDisabled !== false ? ` repo has auto-merge disabled \u2014 leaving ${prUrl} open for manual merge once checks pass` : ` repo has auto-merge disabled (manual-merge fallback off) \u2014 ${prUrl} will not auto-merge`, "yellow");
104972
+ } else {
104973
+ try {
104974
+ await cmd.run(["gh", "pr", "merge", prUrl, "--auto", `--${cfg.autoMergeStrategy}`], cwd2);
104975
+ log3(` enabled auto-merge (${cfg.autoMergeStrategy}) on ${prUrl}`, "green");
104976
+ emit3("auto-merge-enabled", cfg.autoMergeStrategy);
104977
+ } catch (err) {
104978
+ const e = err;
104979
+ log3(`! failed to enable auto-merge on ${prUrl}: ${e.stderr?.trim() || e.message}`, "yellow");
104980
+ }
104950
104981
  }
104951
104982
  }
104952
- if (!(wantAutoMerge && !prReadyNeeded)) {
104953
- await onPrReady?.(prUrl);
104954
- }
104983
+ await onPrReady?.(prUrl);
104955
104984
  return 0;
104956
104985
  }
104957
104986
  async function runWorktreeCleanupPhase(input, deps) {
@@ -105058,7 +105087,6 @@ async function runPostTask(input, deps) {
105058
105087
  exitCode,
105059
105088
  useWorktree,
105060
105089
  wantPr,
105061
- wantFixCi,
105062
105090
  wantAutoMerge,
105063
105091
  wantValidateOnly,
105064
105092
  cfg,
@@ -105148,7 +105176,6 @@ async function runPostTask(input, deps) {
105148
105176
  changeDir,
105149
105177
  stateFilePath,
105150
105178
  issue: issue2,
105151
- wantFixCi,
105152
105179
  wantAutoMerge,
105153
105180
  cfg
105154
105181
  }, {
@@ -105158,7 +105185,6 @@ async function runPostTask(input, deps) {
105158
105185
  respawnWorker,
105159
105186
  ...deps.registerPr !== undefined ? { registerPr: deps.registerPr } : {},
105160
105187
  ...deps.onPrReady !== undefined ? { onPrReady: deps.onPrReady } : {},
105161
- ...deps.checkPrConflict !== undefined ? { checkPrConflict: deps.checkPrConflict } : {},
105162
105188
  ...deps.resolveDependencyBaseBranch !== undefined ? { resolveDependencyBaseBranch: deps.resolveDependencyBaseBranch } : {}
105163
105189
  });
105164
105190
  }
@@ -105179,7 +105205,7 @@ async function runPostTask(input, deps) {
105179
105205
  await runTeardownPhase({ cwd: cwd2, teardownScript: cfg.teardownScript }, { runScript, log: log3, emit: emit3 });
105180
105206
  return effectiveCode;
105181
105207
  }
105182
- var CI_FAILED_EXIT = 70, PR_FAILED_EXIT = 71, NO_CHANGES_EXIT = 72, repoAutoMergeCache, defaultRunCommand = async (cmd, cwd2) => {
105208
+ var PR_FAILED_EXIT = 71, MAX_PR_CREATE_ATTEMPTS = 5, NO_CHANGES_EXIT = 72, repoAutoMergeCache, defaultRunCommand = async (cmd, cwd2) => {
105183
105209
  const proc = Bun.spawnSync({
105184
105210
  cmd: ["sh", "-c", cmd],
105185
105211
  cwd: cwd2,
@@ -105198,7 +105224,6 @@ var init_post_task = __esm(() => {
105198
105224
  init_git2();
105199
105225
  init_linear();
105200
105226
  init_pr();
105201
- init_ci();
105202
105227
  init_pr_status();
105203
105228
  init_wait_for_mergeability();
105204
105229
  init_worktree();
@@ -105603,10 +105628,12 @@ class AgentCoordinator {
105603
105628
  continue;
105604
105629
  if (!this.dependenciesResolved(issue2))
105605
105630
  continue;
105606
- if (await this.maybePromoteFinishedConflicted(issue2))
105607
- continue;
105608
105631
  const changeDir = this.deps.getChangeDir?.(issue2) ?? undefined;
105609
105632
  const actor = await this.flowStore.getActor(issue2.id, changeDir);
105633
+ if (actor.getSnapshot().value === "awaiting-ci")
105634
+ continue;
105635
+ if (await this.maybePromoteFinishedConflicted(issue2))
105636
+ continue;
105610
105637
  actor.send({ type: "RESUME_DETECTED" });
105611
105638
  if (changeDir) {
105612
105639
  await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
@@ -105751,6 +105778,12 @@ class AgentCoordinator {
105751
105778
  return false;
105752
105779
  if (pr.status !== "conflicted" && pr.status !== "ci_failed")
105753
105780
  return false;
105781
+ if (!this.opts.prRecovery?.enabled)
105782
+ return false;
105783
+ if (pr.status === "conflicted" && !this.opts.prRecovery.fixConflicts)
105784
+ return false;
105785
+ if (pr.status === "ci_failed" && !this.opts.prRecovery.fixCi)
105786
+ return false;
105754
105787
  const stateLabel = pr.status === "conflicted" ? "conflicting with main" : "failing CI";
105755
105788
  if (this.conflictPromoted.has(issue2.id))
105756
105789
  return true;
@@ -105888,6 +105921,8 @@ class AgentCoordinator {
105888
105921
  }
105889
105922
  async scanPrMergeStates() {
105890
105923
  const counts = emptyPrStatus();
105924
+ if (!this.opts.prRecovery?.enabled)
105925
+ return counts;
105891
105926
  let candidates = [];
105892
105927
  try {
105893
105928
  candidates = await this.deps.fetchDoneCandidates();
@@ -105932,7 +105967,26 @@ class AgentCoordinator {
105932
105967
  this.deps.onLog(`! pr-tracker clear failed for ${issue2.identifier}: ${err.message}`, "yellow");
105933
105968
  }
105934
105969
  }
105970
+ if (pr.status === "mergeable") {
105971
+ const changeDir = this.deps.getChangeDir?.(issue2) ?? undefined;
105972
+ const actor = await this.flowStore.getActor(issue2.id, changeDir);
105973
+ if (actor.getSnapshot().value === "awaiting-ci") {
105974
+ if (this.issueInSetDoneState(issue2)) {
105975
+ actor.send({ type: "PR_PASSED" });
105976
+ if (changeDir)
105977
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
105978
+ if (actor.getSnapshot().value === "done") {
105979
+ this.flowStore.disposeActor(issue2.id);
105980
+ }
105981
+ } else {
105982
+ await this.advancePrToDone(issue2, pr.url, actor, changeDir);
105983
+ }
105984
+ }
105985
+ continue;
105986
+ }
105935
105987
  if (pr.status === "conflicted") {
105988
+ if (!this.opts.prRecovery?.fixConflicts)
105989
+ continue;
105936
105990
  if (tracker?.isBailed(issue2.identifier)) {
105937
105991
  counts.quarantined += 1;
105938
105992
  continue;
@@ -105966,6 +106020,8 @@ class AgentCoordinator {
105966
106020
  continue;
105967
106021
  }
105968
106022
  if (pr.status === "ci_failed") {
106023
+ if (!this.opts.prRecovery?.fixCi)
106024
+ continue;
105969
106025
  if (tracker?.isBailed(issue2.identifier)) {
105970
106026
  counts.quarantined += 1;
105971
106027
  continue;
@@ -106012,6 +106068,49 @@ class AgentCoordinator {
106012
106068
  }
106013
106069
  return counts;
106014
106070
  }
106071
+ issueInSetDoneState(issue2) {
106072
+ const sd = this.opts.setDone;
106073
+ if (!sd)
106074
+ return false;
106075
+ return issueMatchesGetIndicator(issue2, { filter: markersOf(sd) });
106076
+ }
106077
+ async advancePrToDone(issue2, prUrl, actor, changeDir) {
106078
+ this.deps.onLog(` ${issue2.identifier}: PR ${prUrl} mergeable \u2014 moving to done`, "green");
106079
+ if (this.opts.setDone) {
106080
+ try {
106081
+ await this.deps.applyIndicator(issue2, this.opts.setDone);
106082
+ this.deps.onLog(` ${issue2.identifier}: setDone applied`, "gray");
106083
+ } catch (err) {
106084
+ this.deps.onLog(`! Linear setDone failed for ${issue2.identifier}: ${err.message}`, "red");
106085
+ emitCapture(this.bus, "agent_indicator_failed", {
106086
+ indicator: "setDone",
106087
+ issue_identifier: issue2.identifier,
106088
+ error: err.message
106089
+ });
106090
+ return;
106091
+ }
106092
+ if (this.opts.setInProgress) {
106093
+ try {
106094
+ await this.deps.removeIndicator(issue2, this.opts.setInProgress);
106095
+ this.deps.onLog(` ${issue2.identifier}: clearInProgress applied`, "gray");
106096
+ } catch {}
106097
+ }
106098
+ }
106099
+ actor.send({ type: "PR_PASSED" });
106100
+ if (changeDir) {
106101
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106102
+ }
106103
+ if (actor.getSnapshot().value === "done") {
106104
+ this.flowStore.disposeActor(issue2.id);
106105
+ }
106106
+ if (this.opts.postComments !== false) {
106107
+ try {
106108
+ await this.deps.postComment(issue2, `\u2705 Ralph verified this PR (${prUrl}) is mergeable (CI green, no conflicts) \u2014 moving to done`);
106109
+ } catch (err) {
106110
+ this.deps.onLog(`! Linear done comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106111
+ }
106112
+ }
106113
+ }
106015
106114
  errorMarkerCleared(issue2) {
106016
106115
  const se = this.opts.setError;
106017
106116
  if (!se)
@@ -106320,9 +106419,12 @@ class AgentCoordinator {
106320
106419
  async notifyExited(issue2, changeName, code, trigger, workerCwd) {
106321
106420
  const noChanges = code === NO_CHANGES_EXIT;
106322
106421
  const ok = code === 0 || noChanges;
106422
+ const isRecoveryTrigger = trigger === "conflict-fix" || trigger === "ci-fix";
106423
+ const prOpened = this.deps.hasPrForChange?.(changeName) ?? false;
106424
+ const deferDone = ok && !isRecoveryTrigger && !!this.opts.createsPrs && prOpened && !!this.opts.prRecovery?.enabled;
106323
106425
  const changeDir = this.deps.getChangeDir?.(issue2) ?? undefined;
106324
106426
  const exitActor = await this.flowStore.getActor(issue2.id, changeDir);
106325
- exitActor.send({ type: ok ? "WORKER_SUCCEEDED" : "WORKER_FAILED" });
106427
+ exitActor.send({ type: !ok ? "WORKER_FAILED" : deferDone ? "PR_OPENED" : "WORKER_SUCCEEDED" });
106326
106428
  if (changeDir) {
106327
106429
  await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106328
106430
  }
@@ -106375,6 +106477,8 @@ class AgentCoordinator {
106375
106477
  } else if (trigger === "ci-fix") {
106376
106478
  this.ciFailedNotified.delete(issue2.id);
106377
106479
  this.conflictPromoted.delete(issue2.id);
106480
+ } else if (deferDone) {
106481
+ this.deps.onLog(` ${issue2.identifier}: PR open \u2014 deferring setDone to the PR-recovery watcher`, "gray");
106378
106482
  } else if (this.opts.setDone) {
106379
106483
  try {
106380
106484
  await this.deps.applyIndicator(issue2, this.opts.setDone);
@@ -106456,6 +106560,7 @@ var emptyPrStatus = () => ({
106456
106560
  });
106457
106561
  var init_coordinator = __esm(() => {
106458
106562
  init_types2();
106563
+ init_linear_client();
106459
106564
  init_post_task();
106460
106565
  init_queue_order();
106461
106566
  init_src();
@@ -106993,7 +107098,7 @@ function issueInAwaitingStatus(issue2, indicators) {
106993
107098
  async function releaseAwaitingMarker(issue2, statePath, deps) {
106994
107099
  const { stateObj, confirmation } = await readConfirmationState(statePath);
106995
107100
  if (!confirmation.awaitingMarkerAppliedAt && !issueInAwaitingStatus(issue2, deps.indicators)) {
106996
- return;
107101
+ return false;
106997
107102
  }
106998
107103
  if (deps.indicators.clearAwaitingConfirmation) {
106999
107104
  try {
@@ -107015,6 +107120,7 @@ async function releaseAwaitingMarker(issue2, statePath, deps) {
107015
107120
  } catch (err) {
107016
107121
  deps.onLog(`! persist cleared awaitingMarkerAppliedAt for ${issue2.identifier}: ${err.message}`, "yellow");
107017
107122
  }
107123
+ return true;
107018
107124
  }
107019
107125
  function confirmationUsesCommentIndicator(cfg) {
107020
107126
  const { getApproved, getAutoApprove, getConfirmGate } = cfg.linear.indicators;
@@ -107076,13 +107182,15 @@ async function processAwaitingForIssue(issue2, deps) {
107076
107182
  persistedConfirmation: confirmation
107077
107183
  });
107078
107184
  if (!active) {
107079
- deps.awaitingChangeSet.delete(changeName);
107080
- await releaseAwaitingMarker(issue2, statePath, {
107185
+ const wasTracked = deps.awaitingChangeSet.delete(changeName);
107186
+ const released = await releaseAwaitingMarker(issue2, statePath, {
107081
107187
  indicators,
107082
107188
  applyIndicator: deps.applyIndicator,
107083
107189
  onLog: deps.onLog
107084
107190
  });
107085
- deps.onLog(` ${issue2.identifier}: confirmation detect released \u2014 gate-cleared`);
107191
+ if (wasTracked || released) {
107192
+ deps.onLog(` ${issue2.identifier}: confirmation detect released \u2014 gate-cleared`);
107193
+ }
107086
107194
  return false;
107087
107195
  }
107088
107196
  if (!hasUnchecked(tasks2 ?? "")) {
@@ -107226,8 +107334,28 @@ function parsePrNumber(url2) {
107226
107334
  const m = PR_NUMBER_RE.exec(url2);
107227
107335
  return m ? Number(m[1]) : null;
107228
107336
  }
107337
+ function pickDependencyTip(candidates, blockedByOfCandidate) {
107338
+ const candidateIds = new Set(candidates.map((c) => c.blockerId));
107339
+ const upstream = new Set;
107340
+ for (const c of candidates) {
107341
+ const blockers = blockedByOfCandidate.get(c.blockerId) ?? new Set;
107342
+ for (const otherId of blockers) {
107343
+ if (otherId !== c.blockerId && candidateIds.has(otherId))
107344
+ upstream.add(otherId);
107345
+ }
107346
+ }
107347
+ const tips = candidates.filter((c) => !upstream.has(c.blockerId));
107348
+ return tips.length === 1 ? tips[0] : null;
107349
+ }
107229
107350
  async function resolveDependencyBaseBranchImpl(issue2, runner, runnerCwd, deps) {
107230
- const blockerIds = issue2.blockedByIds;
107351
+ let blockerIds;
107352
+ try {
107353
+ const live = await fetchBlockedByForIssues(deps.apiKey, [issue2.id]);
107354
+ blockerIds = (live.get(issue2.id) ?? []).map((b) => b.id);
107355
+ } catch (err) {
107356
+ deps.onLog(`! could not refresh blockers for ${issue2.identifier}: ${err.message}`, "yellow");
107357
+ blockerIds = issue2.blockedByIds;
107358
+ }
107231
107359
  if (blockerIds.length === 0)
107232
107360
  return null;
107233
107361
  let attachmentsByBlocker;
@@ -107261,17 +107389,30 @@ async function resolveDependencyBaseBranchImpl(issue2, runner, runnerCwd, deps)
107261
107389
  }
107262
107390
  }
107263
107391
  if (openPrs.length === 1) {
107264
- candidates.push(openPrs[0]);
107392
+ candidates.push({ blockerId, base: openPrs[0] });
107265
107393
  } else if (openPrs.length > 1) {
107266
107394
  deps.onLog(` ${issue2.identifier}: blocker ${blockerId} has ${openPrs.length} open PRs \u2014 skipping dependency base resolution`, "gray");
107267
107395
  }
107268
107396
  }
107269
107397
  if (candidates.length === 1)
107270
- return candidates[0];
107271
- if (candidates.length > 1) {
107272
- deps.onLog(` ${issue2.identifier}: ${candidates.length} blockers have open PRs \u2014 falling back to default base`, "gray");
107398
+ return candidates[0].base;
107399
+ if (candidates.length === 0)
107400
+ return null;
107401
+ let blockedByOfCandidate;
107402
+ try {
107403
+ const map3 = await fetchBlockedByForIssues(deps.apiKey, candidates.map((c) => c.blockerId));
107404
+ blockedByOfCandidate = new Map([...map3.entries()].map(([id, refs]) => [id, new Set(refs.map((r) => r.id))]));
107405
+ } catch (err) {
107406
+ deps.onLog(`! could not resolve dependency order for ${issue2.identifier}: ${err.message}`, "yellow");
107407
+ return null;
107273
107408
  }
107274
- return null;
107409
+ const tip = pickDependencyTip(candidates, blockedByOfCandidate);
107410
+ if (!tip) {
107411
+ deps.onLog(` ${issue2.identifier}: ${candidates.length} blockers have open PRs with no single dependency tip \u2014 falling back to default base`, "gray");
107412
+ return null;
107413
+ }
107414
+ deps.onLog(` ${issue2.identifier}: ${candidates.length} blockers have open PRs \u2014 stacking onto tip ${tip.base.blockerIdentifier ?? tip.blockerId}`, "gray");
107415
+ return tip.base;
107275
107416
  }
107276
107417
  function createOpenDraftPr(deps) {
107277
107418
  const create3 = deps.createPr ?? createPullRequest;
@@ -107315,11 +107456,18 @@ function traceCmdRunner(base2, onStart, onEnd) {
107315
107456
  }
107316
107457
  };
107317
107458
  }
107318
- var bunGitRunner, bunCmdRunner;
107459
+ var ghAuthEnv = () => scrubGithubAppTokenEnv(), bunGitRunner, bunCmdRunner;
107319
107460
  var init_runners = __esm(() => {
107461
+ init_preflight();
107320
107462
  bunGitRunner = {
107321
107463
  run: async (args, cwd2) => {
107322
- const proc = Bun.spawn({ cmd: ["git", ...args], cwd: cwd2, stdout: "pipe", stderr: "pipe" });
107464
+ const proc = Bun.spawn({
107465
+ cmd: ["git", ...args],
107466
+ cwd: cwd2,
107467
+ env: ghAuthEnv(),
107468
+ stdout: "pipe",
107469
+ stderr: "pipe"
107470
+ });
107323
107471
  const stdout = await new Response(proc.stdout).text();
107324
107472
  const stderr = await new Response(proc.stderr).text();
107325
107473
  const code = await proc.exited;
@@ -107337,7 +107485,7 @@ var init_runners = __esm(() => {
107337
107485
  };
107338
107486
  bunCmdRunner = {
107339
107487
  run: async (cmd, cwd2) => {
107340
- const proc = Bun.spawn({ cmd, cwd: cwd2, stdout: "pipe", stderr: "pipe" });
107488
+ const proc = Bun.spawn({ cmd, cwd: cwd2, env: ghAuthEnv(), stdout: "pipe", stderr: "pipe" });
107341
107489
  const stdout = await new Response(proc.stdout).text();
107342
107490
  const stderr = await new Response(proc.stderr).text();
107343
107491
  const code = await proc.exited;
@@ -107402,7 +107550,7 @@ var init_indicators = __esm(() => {
107402
107550
 
107403
107551
  // apps/agent/src/agent/wire/linear-resolvers.ts
107404
107552
  function createLinearResolvers(input) {
107405
- const { apiKey, team, assignee, anyAssignee, diag } = input;
107553
+ const { apiKey, team, assignee, anyAssignee, requireAllLabels, diag } = input;
107406
107554
  const ticketNumbers = input.ticketNumbers ?? [];
107407
107555
  const stateCache = new Map;
107408
107556
  const labelCache = new Map;
@@ -107519,6 +107667,7 @@ function createLinearResolvers(input) {
107519
107667
  team,
107520
107668
  assignee,
107521
107669
  anyAssignee,
107670
+ requireAllLabels,
107522
107671
  include,
107523
107672
  exclude: excl,
107524
107673
  ...ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {}
@@ -107541,7 +107690,18 @@ function createLinearResolvers(input) {
107541
107690
  resolveLabelIdForTeam
107542
107691
  };
107543
107692
  }
107544
- async function fetchDoneCandidatesWith(apiKey, team, _assignee, indicators, ticketNumbers) {
107693
+ function doneCandidateSpec(team, assignee, anyAssignee, requireAllLabels, include, ticketNumbers) {
107694
+ return {
107695
+ team,
107696
+ assignee,
107697
+ anyAssignee,
107698
+ requireAllLabels,
107699
+ include,
107700
+ exclude: [],
107701
+ ...ticketNumbers && ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {}
107702
+ };
107703
+ }
107704
+ async function fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requireAllLabels, indicators, ticketNumbers) {
107545
107705
  const getIndicators = [
107546
107706
  indicators.getTodo,
107547
107707
  indicators.getInProgress,
@@ -107560,13 +107720,7 @@ async function fetchDoneCandidatesWith(apiKey, team, _assignee, indicators, tick
107560
107720
  const include = ind.filter ?? [];
107561
107721
  if (include.length === 0)
107562
107722
  return;
107563
- const issues = await fetchOpenIssues(apiKey, {
107564
- team,
107565
- anyAssignee: true,
107566
- include,
107567
- exclude: [],
107568
- ...ticketNumbers && ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {}
107569
- });
107723
+ const issues = await fetchOpenIssues(apiKey, doneCandidateSpec(team, assignee, anyAssignee, requireAllLabels, include, ticketNumbers));
107570
107724
  for (const issue2 of issues) {
107571
107725
  if (!seen.has(issue2.id)) {
107572
107726
  seen.add(issue2.id);
@@ -107603,6 +107757,7 @@ function createPrepareHelpers(input) {
107603
107757
  maps,
107604
107758
  scriptRunner
107605
107759
  } = input;
107760
+ const worktreeProvider = input.worktreeProvider ?? defaultWorktreeProvider;
107606
107761
  async function runScript(label, cmd, cwd2) {
107607
107762
  diag("script", ` ${label}: ${cmd}`, "gray");
107608
107763
  const code = await scriptRunner(cmd, cwd2);
@@ -107621,7 +107776,7 @@ function createPrepareHelpers(input) {
107621
107776
  const baseBranch = baseBranchFromLabels(issue2.labels) ?? cfg.prBaseBranch;
107622
107777
  let wt;
107623
107778
  try {
107624
- wt = await runCapability(git.createWorktree, {
107779
+ wt = await worktreeProvider.create({
107625
107780
  projectRoot,
107626
107781
  changeName: probeName,
107627
107782
  baseBranch,
@@ -107638,7 +107793,7 @@ function createPrepareHelpers(input) {
107638
107793
  scaffoldStatesDir = wtLayout.statesDir;
107639
107794
  diag("worktree", ` ${issue2.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
107640
107795
  try {
107641
- await runCapability(git.seedWorktreeMcpConfig, {
107796
+ await worktreeProvider.seedMcpConfig({
107642
107797
  projectRoot,
107643
107798
  worktreeCwd: wt.cwd
107644
107799
  });
@@ -107834,6 +107989,7 @@ PR: ${ciPrUrl}` : ""
107834
107989
  }
107835
107990
  return { prepare, prepareTaskForTrigger, runScript, reactivateState: reactivateState2 };
107836
107991
  }
107992
+ var defaultWorktreeProvider;
107837
107993
  var init_prepare = __esm(() => {
107838
107994
  init_layout();
107839
107995
  init_tasks_md();
@@ -107844,6 +108000,10 @@ var init_prepare = __esm(() => {
107844
108000
  init_scaffold();
107845
108001
  init_worktree();
107846
108002
  init_task_bodies();
108003
+ defaultWorktreeProvider = {
108004
+ create: (args) => runCapability(git.createWorktree, args),
108005
+ seedMcpConfig: (args) => runCapability(git.seedWorktreeMcpConfig, args)
108006
+ };
107847
108007
  });
107848
108008
 
107849
108009
  // apps/agent/src/agent/pr-url/index.ts
@@ -107903,9 +108063,65 @@ var init_pr_url = __esm(() => {
107903
108063
  init_task_bodies();
107904
108064
  });
107905
108065
 
108066
+ // apps/agent/src/agent/ci.ts
108067
+ function parseChecks(stdout) {
108068
+ try {
108069
+ const parsed = JSON.parse(stdout || "[]");
108070
+ return Array.isArray(parsed) ? parsed : [];
108071
+ } catch {
108072
+ return [];
108073
+ }
108074
+ }
108075
+ async function getPrChecksStatus(prRef, runner, cwd2, onTransientRetry, ignoreCiChecks = []) {
108076
+ let out;
108077
+ try {
108078
+ out = await runGhWithRetry(["gh", "pr", "checks", prRef, "--json", PR_CHECKS_FIELDS], runner, cwd2, onTransientRetry);
108079
+ } catch (err) {
108080
+ const e = err;
108081
+ const blob = `${e.message}
108082
+ ${e.stderr ?? ""}
108083
+ ${e.stdout ?? ""}`;
108084
+ if (NO_CHECKS_RE.test(blob))
108085
+ return { bucket: "pass", failedRunIds: [], failedCheckNames: [] };
108086
+ if (PARTIAL_ACCESS_RE.test(blob) && parseChecks(e.stdout).length > 0) {
108087
+ out = { stdout: e.stdout, stderr: e.stderr ?? "" };
108088
+ } else {
108089
+ throw err;
108090
+ }
108091
+ }
108092
+ const ignoredLower = ignoreCiChecks.map((n) => n.toLowerCase());
108093
+ const checks3 = parseChecks(out.stdout).filter((c) => !ignoredLower.includes(c.name.toLowerCase())).filter((c) => classifyGhBucket(c.bucket) !== "skip");
108094
+ if (checks3.some((c) => classifyGhBucket(c.bucket) === "pending")) {
108095
+ return { bucket: "pending", failedRunIds: [], failedCheckNames: [] };
108096
+ }
108097
+ const failed = checks3.filter((c) => classifyGhBucket(c.bucket) === "fail");
108098
+ if (failed.length === 0)
108099
+ return { bucket: "pass", failedRunIds: [], failedCheckNames: [] };
108100
+ const ids = new Set;
108101
+ for (const c of failed) {
108102
+ const m = c.link?.match(/\/actions\/runs\/(\d+)/);
108103
+ if (m)
108104
+ ids.add(m[1]);
108105
+ }
108106
+ return { bucket: "fail", failedRunIds: [...ids], failedCheckNames: failed.map((c) => c.name) };
108107
+ }
108108
+ var PR_CHECKS_FIELDS = "name,bucket,link,workflow,event";
108109
+ var init_ci = __esm(() => {
108110
+ init_ci_classify();
108111
+ });
108112
+
107906
108113
  // apps/agent/src/agent/wire/pr-discovery.ts
107907
108114
  function createPrDiscovery(input) {
107908
- const { apiKey, projectRoot, cmdRunner, onLog, diag, prByChange, getPollContext } = input;
108115
+ const {
108116
+ apiKey,
108117
+ projectRoot,
108118
+ cmdRunner,
108119
+ onLog,
108120
+ diag,
108121
+ prByChange,
108122
+ getPollContext,
108123
+ ignoreCiChecks
108124
+ } = input;
107909
108125
  const prUnavailable = new Map;
107910
108126
  const prUrlByIssue = createPrUrlCache(5 * 60 * 1000);
107911
108127
  function isPrUnavailable(changeName) {
@@ -107977,9 +108193,11 @@ function createPrDiscovery(input) {
107977
108193
  if (outcome.kind === "conflicting")
107978
108194
  return { url: prUrl, status: "conflicted" };
107979
108195
  try {
107980
- const ci = await getPrChecksStatus(prUrl, cmdRunner, projectRoot);
108196
+ const ci = await getPrChecksStatus(prUrl, cmdRunner, projectRoot, undefined, ignoreCiChecks);
107981
108197
  if (ci.bucket === "fail")
107982
108198
  return { url: prUrl, status: "ci_failed" };
108199
+ if (ci.bucket === "pending")
108200
+ return { url: prUrl, status: "unknown" };
107983
108201
  } catch (err) {
107984
108202
  diag("ci", `! gh pr checks ${prUrl} failed (PR scan): ${err.message}`, "yellow");
107985
108203
  }
@@ -108260,6 +108478,7 @@ function createMentionScanner(input) {
108260
108478
  team,
108261
108479
  assignee,
108262
108480
  anyAssignee,
108481
+ requireAllLabels,
108263
108482
  indicators,
108264
108483
  projectRoot,
108265
108484
  useWorktree,
@@ -108284,6 +108503,7 @@ function createMentionScanner(input) {
108284
108503
  team,
108285
108504
  assignee,
108286
108505
  anyAssignee,
108506
+ ...requireAllLabels && requireAllLabels.length > 0 ? { requireAllLabels } : {},
108287
108507
  ...ticketNumbers && ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {},
108288
108508
  indicators: {
108289
108509
  ...indicators.getTodo !== undefined ? { getTodo: indicators.getTodo } : {},
@@ -108440,6 +108660,31 @@ var init_mention_scan = __esm(() => {
108440
108660
  init_task_bodies();
108441
108661
  });
108442
108662
 
108663
+ // packages/core/src/main-checkout-sentinel/index.ts
108664
+ function isEmptySentinel(s) {
108665
+ return s.head === "" && s.entries.length === 0;
108666
+ }
108667
+ async function snapshotCheckout(root, runner) {
108668
+ try {
108669
+ const head3 = await runner.run(["rev-parse", "HEAD"], root);
108670
+ const status = await runner.run(["status", "--porcelain"], root);
108671
+ const entries = status.stdout.split(`
108672
+ `).map((line) => line.trim()).filter((line) => line.length > 0).sort();
108673
+ return { head: head3.stdout.trim(), entries };
108674
+ } catch {
108675
+ return { head: "", entries: [] };
108676
+ }
108677
+ }
108678
+ function detectCheckoutLeak(before2, after2) {
108679
+ if (isEmptySentinel(before2) || isEmptySentinel(after2)) {
108680
+ return { leaked: false, headMoved: false, newEntries: [] };
108681
+ }
108682
+ const beforeSet = new Set(before2.entries);
108683
+ const newEntries = after2.entries.filter((e) => !beforeSet.has(e));
108684
+ const headMoved = before2.head !== "" && after2.head !== "" && before2.head !== after2.head;
108685
+ return { leaked: newEntries.length > 0 || headMoved, headMoved, newEntries };
108686
+ }
108687
+
108443
108688
  // apps/agent/src/agent/wire/spawn/default.ts
108444
108689
  import { join as join30 } from "path";
108445
108690
  function defaultSpawn(changeName, cmd, cwd2, logsDir, onWorkerOutput, note) {
@@ -108527,7 +108772,7 @@ function dispositionFromExitCode(code) {
108527
108772
  return "done";
108528
108773
  case NO_CHANGES_EXIT2:
108529
108774
  return "no-changes";
108530
- case CI_FAILED_EXIT2:
108775
+ case CI_FAILED_EXIT:
108531
108776
  return "ci-failed";
108532
108777
  case PR_FAILED_EXIT2:
108533
108778
  return "pr-failed";
@@ -108535,7 +108780,7 @@ function dispositionFromExitCode(code) {
108535
108780
  return "error";
108536
108781
  }
108537
108782
  }
108538
- var CI_FAILED_EXIT2 = 70, PR_FAILED_EXIT2 = 71, NO_CHANGES_EXIT2 = 72;
108783
+ var CI_FAILED_EXIT = 70, PR_FAILED_EXIT2 = 71, NO_CHANGES_EXIT2 = 72;
108539
108784
 
108540
108785
  // packages/retro/src/paths.ts
108541
108786
  import { homedir as homedir7 } from "os";
@@ -108762,17 +109007,13 @@ function buildPostTaskInput(input) {
108762
109007
  exitCode: input.exitCode,
108763
109008
  useWorktree: input.useWorktree,
108764
109009
  wantPr: input.wantPr,
108765
- wantFixCi: input.wantFixCi,
108766
109010
  wantAutoMerge: input.wantAutoMerge,
108767
109011
  wantValidateOnly: input.wantValidateOnly,
108768
109012
  cfg: {
108769
109013
  teardownScript: cfg.teardownScript ?? null,
108770
109014
  prBaseBranch: cfg.prBaseBranch,
108771
109015
  autoMergeStrategy: cfg.autoMergeStrategy,
108772
- maxCiFixAttempts: cfg.maxCiFixAttempts,
108773
- ciPollIntervalSeconds: cfg.ciPollIntervalSeconds,
108774
109016
  cleanupWorktreeOnSuccess: cfg.cleanupWorktreeOnSuccess,
108775
- ignoreCiChecks: cfg.ignoreCiChecks,
108776
109017
  stackPrsOnDependencies: args.stackPrs || cfg.stackPrsOnDependencies,
108777
109018
  neverTouch: cfg.boundaries.never_touch,
108778
109019
  metaOnlyFiles: cfg.boundaries.meta_only_files,
@@ -108868,6 +109109,8 @@ function createSpawnWorker(input) {
108868
109109
  const f2 = Bun.file(missionTasksPath);
108869
109110
  return await f2.exists() ? await f2.text() : "";
108870
109111
  })();
109112
+ const guardOn = useWorktree && cwd2 !== projectRoot;
109113
+ const beforeSnapshotPromise = guardOn ? snapshotCheckout(projectRoot, gitRunner) : Promise.resolve(null);
108871
109114
  let logFilePath;
108872
109115
  let handle;
108873
109116
  if (injected) {
@@ -108888,10 +109131,29 @@ function createSpawnWorker(input) {
108888
109131
  onWorkerPhase?.(changeName, "working");
108889
109132
  const tracedCmd = onWorkerCmd ? traceCmdRunner(cmdRunner, (cmd) => onWorkerCmd(changeName, cmd, "start"), (cmd, ms, ok) => onWorkerCmd(changeName, cmd, "end", ms, ok)) : cmdRunner;
108890
109133
  const wantPrBase = args.createPr || cfg.createPrOnSuccess;
108891
- const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
108892
109134
  const issueForChange = issueByChange.get(changeName);
108893
109135
  const wantAutoMerge = issueForChange ? issueMatchesGetIndicator(issueForChange, indicators.getAutoMerge) : false;
108894
109136
  const wrapped = handle.exited.then(async (code) => {
109137
+ const before2 = await beforeSnapshotPromise;
109138
+ if (before2) {
109139
+ const after2 = await snapshotCheckout(projectRoot, gitRunner);
109140
+ const leak = detectCheckoutLeak(before2, after2);
109141
+ if (leak.leaked) {
109142
+ const detail = [
109143
+ leak.headMoved ? "HEAD moved" : null,
109144
+ leak.newEntries.length > 0 ? leak.newEntries.join(", ") : null
109145
+ ].filter(Boolean).join("; ");
109146
+ const msg = `main checkout leak in ${projectRoot}: ${detail}`;
109147
+ onLog(msg, "red");
109148
+ diag("sentinel", msg, "red");
109149
+ emitCapture(bus, "agent_main_checkout_leak", {
109150
+ change_name: changeName,
109151
+ head_moved: leak.headMoved,
109152
+ leaked_paths: leak.newEntries,
109153
+ ...issueForChange ? { issue_identifier: issueForChange.identifier } : {}
109154
+ });
109155
+ }
109156
+ }
108895
109157
  const workerLayout = projectLayout(cwd2);
108896
109158
  const validateSpecPath = join33(workerLayout.changeDir(changeName), "specs", "validate.md");
108897
109159
  const hasValidateSpec = await Bun.file(validateSpecPath).exists();
@@ -108953,7 +109215,6 @@ function createSpawnWorker(input) {
108953
109215
  exitCode: code,
108954
109216
  useWorktree,
108955
109217
  wantPr,
108956
- wantFixCi,
108957
109218
  wantAutoMerge,
108958
109219
  wantValidateOnly,
108959
109220
  ...trigger ? { trigger } : {},
@@ -108986,16 +109247,6 @@ function createSpawnWorker(input) {
108986
109247
  ...onWorkerPhase && {
108987
109248
  onPhase: (phase2, detail) => onWorkerPhase(changeName, phase2, detail)
108988
109249
  },
108989
- checkPrConflict: async (prUrl) => {
108990
- const outcome = await waitForMergeability({
108991
- bailOnError: true,
108992
- probe: async () => {
108993
- const res = await tracedCmd.run(["gh", "pr", "view", prUrl, "--json", "state,mergeable,mergeStateStatus"], cwd2);
108994
- return JSON.parse(res.stdout || "{}");
108995
- }
108996
- });
108997
- return outcome.kind === "conflicting";
108998
- },
108999
109250
  resolveDependencyBaseBranch: (issue2) => resolveDependencyBaseBranchImpl(issue2, tracedCmd, cwd2, { apiKey, onLog })
109000
109251
  });
109001
109252
  releaseWorkerMaps({ cwdByChange, statesDirByChange, branchByChange, issueByChange }, changeName);
@@ -109013,7 +109264,6 @@ var init_worker = __esm(() => {
109013
109264
  init_default2();
109014
109265
  init_runners();
109015
109266
  init_pr_helpers();
109016
- init_wait_for_mergeability();
109017
109267
  init_agent_run_state();
109018
109268
  init_retro();
109019
109269
  init_engine();
@@ -261441,6 +261691,7 @@ function renderListItem(doc2, item, indent, marker) {
261441
261691
  const tokens = item.tokens ?? [];
261442
261692
  let inlineRun = [];
261443
261693
  let placedInline = false;
261694
+ let renderedBlock = false;
261444
261695
  const placeInline = () => {
261445
261696
  if (inlineRun.length === 0)
261446
261697
  return;
@@ -261464,10 +261715,13 @@ function renderListItem(doc2, item, indent, marker) {
261464
261715
  continue;
261465
261716
  }
261466
261717
  placeInline();
261718
+ if (!placedInline && !renderedBlock)
261719
+ doc2.y = startY;
261467
261720
  renderBlock(doc2, tok, indent + LIST_INDENT);
261721
+ renderedBlock = true;
261468
261722
  }
261469
261723
  placeInline();
261470
- if (!placedInline) {
261724
+ if (!placedInline && !renderedBlock) {
261471
261725
  doc2.y = startY;
261472
261726
  doc2.text(" ", bodyX, startY, { width: bodyWidth });
261473
261727
  }
@@ -262442,8 +262696,7 @@ function buildAgentCoordinator(input) {
262442
262696
  const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
262443
262697
  const indicators = mergeIndicators(cfg.linear.indicators, args.indicators);
262444
262698
  const team = args.linearTeam || cfg.linear.team;
262445
- const effectiveFilter = args.linearFilter || (args.linearAssignee ? `assignee = ${args.linearAssignee}` : "") || cfg.linear.filter;
262446
- const { assignee, anyAssignee } = parseLinearFilter(effectiveFilter);
262699
+ const { assignee, anyAssignee, requireAllLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, args.linearAssignee));
262447
262700
  const ticketNumbers = resolveTicketNumbers(args.ticketTokens, team);
262448
262701
  const excludeFromTodo = unionMarkers(indicators.setDone, indicators.setError);
262449
262702
  const gitRunner = input.runners?.git ?? bunGitRunner;
@@ -262485,6 +262738,7 @@ function buildAgentCoordinator(input) {
262485
262738
  team,
262486
262739
  assignee,
262487
262740
  anyAssignee,
262741
+ requireAllLabels,
262488
262742
  diag,
262489
262743
  ...ticketNumbers.length > 0 ? { ticketNumbers } : {}
262490
262744
  });
@@ -262501,7 +262755,8 @@ function buildAgentCoordinator(input) {
262501
262755
  onLog,
262502
262756
  diag,
262503
262757
  prByChange,
262504
- getPollContext: () => pollContext
262758
+ getPollContext: () => pollContext,
262759
+ ignoreCiChecks: cfg.prRecovery.ignoreChecks
262505
262760
  });
262506
262761
  const prep = createPrepareHelpers({
262507
262762
  args,
@@ -262514,7 +262769,8 @@ function buildAgentCoordinator(input) {
262514
262769
  gitRunner,
262515
262770
  diag,
262516
262771
  maps: { cwdByChange, statesDirByChange, issueByChange, branchByChange, prByChange },
262517
- scriptRunner
262772
+ scriptRunner,
262773
+ ...input.runners?.worktree ? { worktreeProvider: input.runners.worktree } : {}
262518
262774
  });
262519
262775
  const fetchMentions = createMentionScanner({
262520
262776
  apiKey,
@@ -262523,6 +262779,7 @@ function buildAgentCoordinator(input) {
262523
262779
  team,
262524
262780
  assignee,
262525
262781
  anyAssignee,
262782
+ requireAllLabels,
262526
262783
  indicators,
262527
262784
  projectRoot,
262528
262785
  useWorktree,
@@ -262616,10 +262873,10 @@ function buildAgentCoordinator(input) {
262616
262873
  now: () => new Date
262617
262874
  };
262618
262875
  }
262619
- const prTrackerEnabled = args.prTrackerEnabled === undefined ? cfg.prTracker.enabled : args.prTrackerEnabled;
262620
- const prTracker = prTrackerEnabled ? new PrTracker({
262876
+ const prRecoveryEnabled = args.prRecoveryEnabled === undefined ? cfg.prRecovery.enabled : args.prRecoveryEnabled;
262877
+ const prTracker = prRecoveryEnabled ? new PrTracker({
262621
262878
  projectRoot,
262622
- maxRecoveryAttempts: cfg.prTracker.maxRecoveryAttempts
262879
+ maxRecoveryAttempts: cfg.prRecovery.maxRecoverySessions
262623
262880
  }) : null;
262624
262881
  const commentSync = createCommentSyncHooks({
262625
262882
  apiKey,
@@ -262637,7 +262894,7 @@ function buildAgentCoordinator(input) {
262637
262894
  fetchTodo: () => resolvers.fetchByGet(indicators.getTodo, excludeFromTodo),
262638
262895
  fetchInProgress: () => resolvers.fetchByGet(indicators.getInProgress, unionMarkers(indicators.setError)),
262639
262896
  fetchMentions,
262640
- fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, indicators, ticketNumbers.length > 0 ? ticketNumbers : undefined),
262897
+ fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requireAllLabels, indicators, ticketNumbers.length > 0 ? ticketNumbers : undefined),
262641
262898
  prepare: prep.prepare,
262642
262899
  prepareTaskForTrigger: prep.prepareTaskForTrigger,
262643
262900
  spawnWorker,
@@ -262649,6 +262906,7 @@ function buildAgentCoordinator(input) {
262649
262906
  return c.map((x2) => ({ body: x2.body }));
262650
262907
  },
262651
262908
  checkPrStatus: prDiscovery.checkPrStatus,
262909
+ hasPrForChange: (changeName) => prByChange.has(changeName),
262652
262910
  isChangeArchivedForIssue: (issue2) => isChangeArchivedForIssue(issue2, cwdByChange, projectRoot),
262653
262911
  onLog,
262654
262912
  ...onFileLog ? { onFileLog } : {},
@@ -262685,7 +262943,13 @@ function buildAgentCoordinator(input) {
262685
262943
  postComments: cfg.linear.postComments,
262686
262944
  commentEveryIterations: cfg.linear.updateEveryIterations,
262687
262945
  ...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {},
262688
- ...prTracker ? { prTracker } : {}
262946
+ createsPrs: args.createPr || cfg.createPrOnSuccess,
262947
+ ...prTracker ? { prTracker } : {},
262948
+ prRecovery: {
262949
+ enabled: prRecoveryEnabled,
262950
+ fixCi: cfg.prRecovery.fixCi,
262951
+ fixConflicts: cfg.prRecovery.fixConflicts
262952
+ }
262689
262953
  });
262690
262954
  coordRef.current = coord;
262691
262955
  const filterDesc = describeIndicators(indicators, team, assignee, anyAssignee);
@@ -262846,23 +263110,23 @@ function SteeringField({
262846
263110
  initialFocused = false,
262847
263111
  onStateChange
262848
263112
  }) {
262849
- const [state, dispatch] = import_react61.useReducer(reducer2, { initialBuffer, initialCursor, initialFocused }, (init2) => ({
263113
+ const [state, dispatch] = import_react62.useReducer(reducer2, { initialBuffer, initialCursor, initialFocused }, (init2) => ({
262850
263114
  buffer: init2.initialBuffer,
262851
263115
  cursor: init2.initialCursor ?? init2.initialBuffer.length,
262852
263116
  focused: init2.initialFocused,
262853
263117
  status: "idle"
262854
263118
  }));
262855
263119
  const { buffer, cursor: cursor4, focused, status } = state;
262856
- const stateRef = import_react61.useRef(state);
263120
+ const stateRef = import_react62.useRef(state);
262857
263121
  stateRef.current = state;
262858
- const hintTimerRef = import_react61.useRef(null);
262859
- import_react61.useEffect(() => {
263122
+ const hintTimerRef = import_react62.useRef(null);
263123
+ import_react62.useEffect(() => {
262860
263124
  onFocusChange?.(focused);
262861
263125
  }, [focused, onFocusChange]);
262862
- import_react61.useEffect(() => {
263126
+ import_react62.useEffect(() => {
262863
263127
  onStateChange?.({ buffer, cursor: cursor4, focused });
262864
263128
  }, [buffer, cursor4, focused, onStateChange]);
262865
- import_react61.useEffect(() => {
263129
+ import_react62.useEffect(() => {
262866
263130
  return () => {
262867
263131
  if (hintTimerRef.current)
262868
263132
  clearTimeout(hintTimerRef.current);
@@ -262971,10 +263235,10 @@ function SteeringField({
262971
263235
  ]
262972
263236
  }, undefined, true, undefined, this);
262973
263237
  }
262974
- var import_react61, jsx_dev_runtime10, STATUS_HINT_MS = 2000, PLACEHOLDER_IDLE = "CTRL+S to steer", PLACEHOLDER_SENT = "steered \u2192 next iteration", PLACEHOLDER_FAILED = "send failed";
263238
+ var import_react62, jsx_dev_runtime10, STATUS_HINT_MS = 2000, PLACEHOLDER_IDLE = "CTRL+S to steer", PLACEHOLDER_SENT = "steered \u2192 next iteration", PLACEHOLDER_FAILED = "send failed";
262975
263239
  var init_SteeringField = __esm(async () => {
262976
263240
  await init_build2();
262977
- import_react61 = __toESM(require_react(), 1);
263241
+ import_react62 = __toESM(require_react(), 1);
262978
263242
  jsx_dev_runtime10 = __toESM(require_jsx_dev_runtime(), 1);
262979
263243
  });
262980
263244
 
@@ -263343,21 +263607,35 @@ function AgentMode({
263343
263607
  const { exit } = use_app_default();
263344
263608
  const { isRawModeSupported } = use_stdin_default();
263345
263609
  const { columns, rows, resizeKey } = useTerminalSize();
263346
- const [logs, setLogs] = import_react62.useState([]);
263347
- const [preflightError, setPreflightError] = import_react62.useState(null);
263348
- const [, setTick] = import_react62.useState(0);
263349
- const [clock, setClock] = import_react62.useState(0);
263350
- const [focusedIdx, setFocusedIdx] = import_react62.useState(0);
263351
- const [showPendingTasks, setShowPendingTasks] = import_react62.useState(false);
263352
- const [showAllSubtasks, setShowAllSubtasks] = import_react62.useState(false);
263353
- const [gaveUpCount, setGaveUpCount] = import_react62.useState(0);
263354
- const coordRef = import_react62.useRef(null);
263355
- const workerMetaRef = import_react62.useRef(new Map);
263356
- const gatedTicketsRef = import_react62.useRef(new Map);
263357
- const nextPollAtRef = import_react62.useRef(0);
263358
- const cfgRef = import_react62.useRef(null);
263359
- const [effective, setEffective] = import_react62.useState(null);
263360
- const [pollStatus, setPollStatus] = import_react62.useState({
263610
+ const [logs, setLogs] = import_react63.useState([]);
263611
+ const [preflightError, setPreflightError] = import_react63.useState(null);
263612
+ const [fatalExit, setFatalExit] = import_react63.useState(null);
263613
+ const heldRef = import_react63.useRef(false);
263614
+ const { awaitingClose } = useHoldToClose({
263615
+ finished: fatalExit !== null,
263616
+ hold: true,
263617
+ onClose: () => {
263618
+ const code = heldRef.current ? 0 : fatalExit ?? 0;
263619
+ setTimeout(() => process.exit(code), 200);
263620
+ }
263621
+ });
263622
+ import_react63.useEffect(() => {
263623
+ if (awaitingClose)
263624
+ heldRef.current = true;
263625
+ }, [awaitingClose]);
263626
+ const [, setTick] = import_react63.useState(0);
263627
+ const [clock, setClock] = import_react63.useState(0);
263628
+ const [focusedIdx, setFocusedIdx] = import_react63.useState(0);
263629
+ const [showPendingTasks, setShowPendingTasks] = import_react63.useState(false);
263630
+ const [showAllSubtasks, setShowAllSubtasks] = import_react63.useState(false);
263631
+ const [gaveUpCount, setGaveUpCount] = import_react63.useState(0);
263632
+ const coordRef = import_react63.useRef(null);
263633
+ const workerMetaRef = import_react63.useRef(new Map);
263634
+ const gatedTicketsRef = import_react63.useRef(new Map);
263635
+ const nextPollAtRef = import_react63.useRef(0);
263636
+ const cfgRef = import_react63.useRef(null);
263637
+ const [effective, setEffective] = import_react63.useState(null);
263638
+ const [pollStatus, setPollStatus] = import_react63.useState({
263361
263639
  state: "idle",
263362
263640
  lastFound: null,
263363
263641
  lastAdded: null,
@@ -263370,14 +263648,14 @@ function AgentMode({
263370
263648
  setLogs((prev) => [...prev, { id: nextId(), text, color }]);
263371
263649
  logCoord(text, workerLogFile);
263372
263650
  }
263373
- const fileSinkRef = import_react62.useRef(null);
263651
+ const fileSinkRef = import_react63.useRef(null);
263374
263652
  if (fileSinkRef.current === null) {
263375
263653
  fileSinkRef.current = createJsonLogFileSink(args.jsonLogFile);
263376
263654
  }
263377
263655
  const fileEmit = (event) => {
263378
263656
  fileSinkRef.current?.emit(event);
263379
263657
  };
263380
- import_react62.useEffect(() => {
263658
+ import_react63.useEffect(() => {
263381
263659
  let pollTimer = null;
263382
263660
  let cancelled = false;
263383
263661
  async function init2() {
@@ -263390,15 +263668,14 @@ function AgentMode({
263390
263668
  if (!apiKey) {
263391
263669
  throw new Error("LINEAR_API_KEY not set \u2014 cannot poll Linear");
263392
263670
  }
263393
- const pf = await runPreflight2();
263671
+ const pf = await runPreflight2({
263672
+ requireRepoWrite: args.createPr || cfg2.createPrOnSuccess,
263673
+ repoCwd: projectRoot
263674
+ });
263394
263675
  if (!pf.ok) {
263395
263676
  fileEmit({ type: "error", code: "auth_failure", tool: pf.tool, text: pf.message });
263396
263677
  setPreflightError({ tool: pf.tool, message: pf.message });
263397
- process.exitCode = 2;
263398
- setTimeout(() => {
263399
- exit();
263400
- setTimeout(() => process.exit(2), 200);
263401
- }, 100);
263678
+ setFatalExit(2);
263402
263679
  return;
263403
263680
  }
263404
263681
  const { coord: coord2, filterDesc, concurrency, pollInterval, runBaselineGate: runBaselineGate2, getGaveUpTotal } = buildCoordinator({
@@ -263574,10 +263851,7 @@ function AgentMode({
263574
263851
  const message = err instanceof Error ? err.message : String(err);
263575
263852
  fileEmit({ type: "error", code: "init_failure", text: message });
263576
263853
  appendLog(`! ${message}`, "red");
263577
- setTimeout(() => {
263578
- exit();
263579
- setTimeout(() => process.exit(1), 200);
263580
- }, 100);
263854
+ setFatalExit(1);
263581
263855
  });
263582
263856
  let shuttingDown = false;
263583
263857
  const onSig = () => {
@@ -263620,8 +263894,8 @@ function AgentMode({
263620
263894
  process.off("SIGTERM", onSig);
263621
263895
  };
263622
263896
  }, []);
263623
- const lastPauseRef = import_react62.useRef(null);
263624
- import_react62.useEffect(() => {
263897
+ const lastPauseRef = import_react63.useRef(null);
263898
+ import_react63.useEffect(() => {
263625
263899
  let cancelled = false;
263626
263900
  const interval = setInterval(() => {
263627
263901
  if (cancelled)
@@ -263685,10 +263959,10 @@ function AgentMode({
263685
263959
  const termWidth = columns - 2;
263686
263960
  const termHeight = rows;
263687
263961
  const safeFocusedIdx = activeCount > 0 ? Math.min(focusedIdx, activeCount - 1) : 0;
263688
- const steeringFocusedRef = import_react62.useRef(false);
263689
- const steeringBufferRef = import_react62.useRef("");
263690
- const steeringCursorRef = import_react62.useRef(0);
263691
- const steeringFocusedInitRef = import_react62.useRef(false);
263962
+ const steeringFocusedRef = import_react63.useRef(false);
263963
+ const steeringBufferRef = import_react63.useRef("");
263964
+ const steeringCursorRef = import_react63.useRef(0);
263965
+ const steeringFocusedInitRef = import_react63.useRef(false);
263692
263966
  use_input_default((input, key) => {
263693
263967
  if (steeringFocusedRef.current)
263694
263968
  return;
@@ -263740,7 +264014,15 @@ function AgentMode({
263740
264014
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
263741
264015
  color: "red",
263742
264016
  children: preflightError.message
263743
- }, undefined, false, undefined, this)
264017
+ }, undefined, false, undefined, this),
264018
+ awaitingClose && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264019
+ color: "cyan",
264020
+ children: [
264021
+ `
264022
+ `,
264023
+ "Press Enter to close\u2026"
264024
+ ]
264025
+ }, undefined, true, undefined, this)
263744
264026
  ]
263745
264027
  }, undefined, true, undefined, this);
263746
264028
  }
@@ -263857,10 +264139,13 @@ function AgentMode({
263857
264139
  color: "green",
263858
264140
  children: " \u25CF PR"
263859
264141
  }, undefined, false, undefined, this),
263860
- cfg.fixCiOnFailure && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264142
+ cfg.prRecovery.enabled && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
263861
264143
  color: "green",
263862
- children: " \u25CF fixCI"
263863
- }, undefined, false, undefined, this),
264144
+ children: [
264145
+ " \u25CF recover",
264146
+ cfg.prRecovery.fixCi ? "+CI" : ""
264147
+ ]
264148
+ }, undefined, true, undefined, this),
263864
264149
  cfg.useWorktree && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
263865
264150
  color: "green",
263866
264151
  children: " \u25CF worktree"
@@ -264760,11 +265045,15 @@ function AgentMode({
264760
265045
  }, w2.changeName, true, undefined, this);
264761
265046
  })
264762
265047
  ]
264763
- }, undefined, true, undefined, this)
265048
+ }, undefined, true, undefined, this),
265049
+ awaitingClose && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265050
+ color: "cyan",
265051
+ children: "Stopped \u2014 press Enter to close\u2026"
265052
+ }, undefined, false, undefined, this)
264764
265053
  ]
264765
265054
  }, resizeKey, true, undefined, this);
264766
265055
  }
264767
- var import_react62, jsx_dev_runtime11, lineCounter = 0, TAIL_BUFFER_SIZE = 30, CMD_DISPLAY_MAX = 80, MAX_PENDING_DISPLAY = 15, SPINNER_FRAMES, HYPERLINKS_SUPPORTED, SESSION_START;
265056
+ var import_react63, jsx_dev_runtime11, lineCounter = 0, TAIL_BUFFER_SIZE = 30, CMD_DISPLAY_MAX = 80, MAX_PENDING_DISPLAY = 15, SPINNER_FRAMES, HYPERLINKS_SUPPORTED, SESSION_START;
264768
265057
  var init_AgentMode = __esm(async () => {
264769
265058
  init_cli2();
264770
265059
  init_config();
@@ -264780,9 +265069,10 @@ var init_AgentMode = __esm(async () => {
264780
265069
  init_worker_state_poll();
264781
265070
  await __promiseAll([
264782
265071
  init_build2(),
265072
+ init_useHoldToClose(),
264783
265073
  init_SteeringField()
264784
265074
  ]);
264785
- import_react62 = __toESM(require_react(), 1);
265075
+ import_react63 = __toESM(require_react(), 1);
264786
265076
  jsx_dev_runtime11 = __toESM(require_jsx_dev_runtime(), 1);
264787
265077
  SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
264788
265078
  HYPERLINKS_SUPPORTED = !process.env["TMUX"];
@@ -264844,7 +265134,7 @@ function createSession(name, command, env3) {
264844
265134
  envArgs.push("-e", `${key}=${value}`);
264845
265135
  }
264846
265136
  const quoted = command.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
264847
- const shellCmd = `${quoted}; printf '\\n[ralphy exited \u2014 press Enter to close]\\n'; read`;
265137
+ const shellCmd = `${quoted}; code=$?; ` + `if [ "$code" -ne 0 ]; then ` + `printf '\\n[ralphy crashed (exit %s) \u2014 press Enter to close]\\n' "$code"; read _; fi`;
264848
265138
  const result2 = Bun.spawnSync({
264849
265139
  cmd: ["tmux", "new-session", "-d", "-s", name, ...envArgs, "sh", "-c", shellCmd],
264850
265140
  stderr: "pipe"
@@ -265085,23 +265375,20 @@ function buildBuckets(indicators) {
265085
265375
  { label: "auto-merge", indicator: indicators.getAutoMerge, exclude: [] }
265086
265376
  ];
265087
265377
  }
265088
- async function fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, ticketNumbers) {
265378
+ async function fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, requireAllLabels, ticketNumbers) {
265089
265379
  if (!bucket.indicator || bucket.indicator.filter.length === 0)
265090
265380
  return [];
265091
265381
  const spec = {
265092
265382
  team,
265093
265383
  assignee,
265094
265384
  anyAssignee,
265385
+ requireAllLabels,
265095
265386
  include: bucket.indicator.filter,
265096
265387
  exclude: bucket.exclude,
265097
265388
  ...ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {}
265098
265389
  };
265099
265390
  return fetchOpenIssues(apiKey, spec);
265100
265391
  }
265101
- function resolveLinearFilter(filterOverride, assigneeOverride, configFilter) {
265102
- const effective = filterOverride || (assigneeOverride ? `assignee = ${assigneeOverride}` : "") || configFilter;
265103
- return parseLinearFilter(effective);
265104
- }
265105
265392
  function formatReviewCell(prUrl, count) {
265106
265393
  if (!prUrl)
265107
265394
  return "-";
@@ -265148,13 +265435,13 @@ function backlogRankByIssueId(issues) {
265148
265435
  ordered.forEach((o, i) => rankById.set(o.id, i));
265149
265436
  return rankById;
265150
265437
  }
265151
- async function fetchAndPrintLinear(apiKey, buckets, team, assignee, anyAssignee, cwd2, runner, ignoreCiChecks = [], checks3 = false, review = false, ticketNumbers = []) {
265438
+ async function fetchAndPrintLinear(apiKey, buckets, team, assignee, anyAssignee, requireAllLabels, cwd2, runner, ignoreCiChecks = [], checks3 = false, review = false, ticketNumbers = []) {
265152
265439
  const bucketResults = await Promise.all(buckets.map(async (bucket) => {
265153
265440
  if (!bucket.indicator || bucket.indicator.filter.length === 0) {
265154
265441
  return { bucket, issues: [], error: null };
265155
265442
  }
265156
265443
  try {
265157
- const issues = await fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, ticketNumbers);
265444
+ const issues = await fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, requireAllLabels, ticketNumbers);
265158
265445
  return { bucket, issues, error: null };
265159
265446
  } catch (err) {
265160
265447
  return {
@@ -265280,7 +265567,6 @@ async function runList(input) {
265280
265567
  identifier: name,
265281
265568
  projectRoot,
265282
265569
  linearTeamOverride: input.linearTeamOverride,
265283
- linearFilterOverride: input.linearFilterOverride,
265284
265570
  linearAssigneeOverride: input.linearAssigneeOverride
265285
265571
  });
265286
265572
  return;
@@ -265291,7 +265577,7 @@ async function runList(input) {
265291
265577
  const apiKey = process.env["LINEAR_API_KEY"];
265292
265578
  const indicators = cfg.linear.indicators;
265293
265579
  const team = input.linearTeamOverride || cfg.linear.team;
265294
- const { assignee, anyAssignee } = resolveLinearFilter(input.linearFilterOverride, input.linearAssigneeOverride, cfg.linear.filter);
265580
+ const { assignee, anyAssignee, requireAllLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, input.linearAssigneeOverride));
265295
265581
  const buckets = buildBuckets(indicators);
265296
265582
  const anyConfigured = buckets.some((b2) => b2.indicator && b2.indicator.filter.length > 0);
265297
265583
  if (!anyConfigured) {
@@ -265331,7 +265617,7 @@ team: ${team}
265331
265617
  if (ticketNumbers.length > 0)
265332
265618
  process.stdout.write(`ticket: ${ticketNumbers.join(", ")}
265333
265619
  `);
265334
- await fetchAndPrintLinear(apiKey, buckets, team, assignee, anyAssignee, projectRoot, localCmdRunner, cfg.ignoreCiChecks, input.checks, input.review, ticketNumbers);
265620
+ await fetchAndPrintLinear(apiKey, buckets, team, assignee, anyAssignee, requireAllLabels, projectRoot, localCmdRunner, cfg.prRecovery.ignoreChecks, input.checks, input.review, ticketNumbers);
265335
265621
  }
265336
265622
  function normalizeIdentifier(input) {
265337
265623
  let parsed;
@@ -265414,7 +265700,7 @@ async function runListDebug(input) {
265414
265700
  const cfg = await loadRalphyConfig(projectRoot, getArgs().workflowFile);
265415
265701
  const indicators = cfg.linear.indicators;
265416
265702
  const team = input.linearTeamOverride || cfg.linear.team;
265417
- const { assignee, anyAssignee } = resolveLinearFilter(input.linearFilterOverride, input.linearAssigneeOverride, cfg.linear.filter);
265703
+ const { assignee, anyAssignee, requireAllLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, input.linearAssigneeOverride));
265418
265704
  const assigneeLabel = anyAssignee ? "any" : assignee ?? "*";
265419
265705
  const normalized = normalizeIdentifier(identifier);
265420
265706
  if (!normalized) {
@@ -265458,6 +265744,13 @@ Per-bucket diagnostics:
265458
265744
  if (!assigneeMatches(issue2, assignee, anyAssignee)) {
265459
265745
  reasons.push(`assignee mismatch: issue=${issue2.assignee ? issue2.assignee.email ?? issue2.assignee.id : "unassigned"}, config=${assigneeLabel}`);
265460
265746
  }
265747
+ if (requireAllLabels && requireAllLabels.length > 0) {
265748
+ const issueLabels = new Set(issue2.labels.nodes.map((l3) => l3.name));
265749
+ const missing = requireAllLabels.filter((label) => !issueLabels.has(label));
265750
+ if (missing.length > 0) {
265751
+ reasons.push(`missing required linear.filter label(s): ${missing.join(", ")}`);
265752
+ }
265753
+ }
265461
265754
  const includeMatches = bucket.indicator.filter.some((m2) => markerMatches(issue2, m2));
265462
265755
  if (!includeMatches) {
265463
265756
  const want = bucket.indicator.filter.map((m2) => `${m2.type}:${m2.value}`).join(" OR ");
@@ -265565,7 +265858,10 @@ async function runAgentJson({
265565
265858
  process.exitCode = 1;
265566
265859
  return;
265567
265860
  }
265568
- const pf = await runPreflight2();
265861
+ const pf = await runPreflight2({
265862
+ requireRepoWrite: args.createPr || cfg.createPrOnSuccess,
265863
+ repoCwd: projectRoot
265864
+ });
265569
265865
  if (!pf.ok) {
265570
265866
  emit3({ type: "error", code: "auth_failure", tool: pf.tool, text: pf.message });
265571
265867
  process.exitCode = 2;
@@ -265786,7 +266082,6 @@ async function main3(argv) {
265786
266082
  await runWithContext(createDefaultContext({ layout, args }), async () => {
265787
266083
  await runList2({
265788
266084
  linearTeamOverride: args.linearTeam,
265789
- linearFilterOverride: args.linearFilter,
265790
266085
  linearAssigneeOverride: args.linearAssignee,
265791
266086
  debug: args.debug,
265792
266087
  name: args.name,
@@ -265862,12 +266157,12 @@ async function main3(argv) {
265862
266157
  return 0;
265863
266158
  }
265864
266159
  await runWithContext(createDefaultContext({ layout, args }), async () => {
265865
- const { waitUntilExit } = render_default(import_react63.createElement(AgentMode, { args, projectRoot, statesDir, tasksDir }));
266160
+ const { waitUntilExit } = render_default(import_react64.createElement(AgentMode, { args, projectRoot, statesDir, tasksDir }));
265866
266161
  await waitUntilExit();
265867
266162
  });
265868
266163
  return typeof process.exitCode === "number" ? process.exitCode : 0;
265869
266164
  }
265870
- var import_react63;
266165
+ var import_react64;
265871
266166
  var init_src8 = __esm(async () => {
265872
266167
  init_context();
265873
266168
  init_layout();
@@ -265878,7 +266173,7 @@ var init_src8 = __esm(async () => {
265878
266173
  init_build2(),
265879
266174
  init_AgentMode()
265880
266175
  ]);
265881
- import_react63 = __toESM(require_react(), 1);
266176
+ import_react64 = __toESM(require_react(), 1);
265882
266177
  });
265883
266178
 
265884
266179
  // apps/shell/src/index.ts
@@ -265967,11 +266262,21 @@ ${HELP}
265967
266262
  capture("command_run", { subcommand });
265968
266263
  bus.emit({ type: "command_run", subcommand });
265969
266264
  try {
265970
- if (shouldOfferSetup(subcommand, argv.slice(1))) {
266265
+ if (CONFIG_SUBCOMMANDS.has(subcommand)) {
265971
266266
  try {
265972
- const { maybeRunSetupWizard: maybeRunSetupWizard2 } = await init_src4().then(() => exports_src);
266267
+ const { maybeRunSetupWizard: maybeRunSetupWizard2, maybeUpgradeWorkflow: maybeUpgradeWorkflow2 } = await init_src4().then(() => exports_src);
265973
266268
  const { projectRoot, workflowFile } = parseWorkflowPathArgs(argv.slice(1));
265974
- await maybeRunSetupWizard2(projectRoot, workflowFile);
266269
+ if (shouldOfferSetup(subcommand, argv.slice(1))) {
266270
+ await maybeRunSetupWizard2(projectRoot, workflowFile);
266271
+ }
266272
+ if (await maybeUpgradeWorkflow2(projectRoot, workflowFile)) {
266273
+ process.stdout.write(`
266274
+ WORKFLOW.md updated \u2014 re-run your command.
266275
+ `);
266276
+ capture("command_exit", { subcommand, exit_code: 0 });
266277
+ bus.emit({ type: "command_exit", subcommand, exit_code: 0 });
266278
+ return 0;
266279
+ }
265975
266280
  } catch (setupErr) {
265976
266281
  captureError("setup_wizard_error", setupErr, { subcommand });
265977
266282
  }