@neriros/ralphy 3.10.13 → 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.13")
18932
- return "3.10.13";
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 {
@@ -103114,11 +103066,21 @@ function formatLinearError(err) {
103114
103066
  async function linearRequest(apiKey, query, variables) {
103115
103067
  let lastHttpError;
103116
103068
  for (let attempt2 = 1;attempt2 <= MAX_LINEAR_ATTEMPTS; attempt2++) {
103117
- const res = await fetch(LINEAR_API, {
103118
- method: "POST",
103119
- headers: { "Content-Type": "application/json", Authorization: apiKey },
103120
- body: JSON.stringify({ query, variables })
103121
- });
103069
+ let res;
103070
+ try {
103071
+ res = await fetch(LINEAR_API, {
103072
+ method: "POST",
103073
+ headers: { "Content-Type": "application/json", Authorization: apiKey },
103074
+ body: JSON.stringify({ query, variables })
103075
+ });
103076
+ } catch (netErr) {
103077
+ lastHttpError = netErr;
103078
+ if (attempt2 < MAX_LINEAR_ATTEMPTS) {
103079
+ await linearRequestInternals.sleep(Math.min(backoffMs(attempt2), MAX_RETRY_AFTER_MS));
103080
+ continue;
103081
+ }
103082
+ throw netErr;
103083
+ }
103122
103084
  if (!res.ok) {
103123
103085
  const err = new Error("Linear API request failed");
103124
103086
  err.status = res.status;
@@ -103346,6 +103308,28 @@ async function fetchAttachmentsForIssues(apiKey, issueIds) {
103346
103308
  }
103347
103309
  return out;
103348
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
+ }
103349
103333
  async function findIssueAttachmentByTitle(apiKey, issueId, title) {
103350
103334
  const query = `query IssueAttachmentByTitle($id: String!) {
103351
103335
  issue(id: $id) {
@@ -103566,6 +103550,326 @@ var init_linear_client = __esm(() => {
103566
103550
  };
103567
103551
  });
103568
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
+
103569
103873
  // apps/agent/src/agent/linear.ts
103570
103874
  var init_linear = __esm(() => {
103571
103875
  init_linear_client();
@@ -103760,131 +104064,14 @@ function classifyGhBucket(bucket) {
103760
104064
  return "pending";
103761
104065
  return "pass";
103762
104066
  }
103763
- var TRANSIENT_GH_RE, NO_CHECKS_RE, GH_RETRY_DELAYS;
104067
+ var TRANSIENT_GH_RE, NO_CHECKS_RE, PARTIAL_ACCESS_RE, GH_RETRY_DELAYS;
103764
104068
  var init_ci_classify = __esm(() => {
103765
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;
103766
104070
  NO_CHECKS_RE = /no checks reported/i;
104071
+ PARTIAL_ACCESS_RE = /Resource not accessible by personal access token/i;
103767
104072
  GH_RETRY_DELAYS = [5000, 15000, 45000];
103768
104073
  });
103769
104074
 
103770
- // apps/agent/src/agent/ci.ts
103771
- async function getPrChecksStatus(prRef, runner, cwd2, onTransientRetry, ignoreCiChecks = []) {
103772
- let out;
103773
- try {
103774
- out = await runGhWithRetry(["gh", "pr", "checks", prRef, "--json", PR_CHECKS_FIELDS], runner, cwd2, onTransientRetry);
103775
- } catch (err) {
103776
- const e = err;
103777
- const blob = `${e.message}
103778
- ${e.stderr ?? ""}
103779
- ${e.stdout ?? ""}`;
103780
- if (NO_CHECKS_RE.test(blob))
103781
- return { bucket: "pass", failedRunIds: [], failedCheckNames: [] };
103782
- throw err;
103783
- }
103784
- const ignoredLower = ignoreCiChecks.map((n) => n.toLowerCase());
103785
- const checks3 = JSON.parse(out.stdout || "[]").filter((c) => !ignoredLower.includes(c.name.toLowerCase())).filter((c) => classifyGhBucket(c.bucket) !== "skip");
103786
- if (checks3.some((c) => classifyGhBucket(c.bucket) === "pending")) {
103787
- return { bucket: "pending", failedRunIds: [], failedCheckNames: [] };
103788
- }
103789
- const failed = checks3.filter((c) => classifyGhBucket(c.bucket) === "fail");
103790
- if (failed.length === 0)
103791
- return { bucket: "pass", failedRunIds: [], failedCheckNames: [] };
103792
- const ids = new Set;
103793
- for (const c of failed) {
103794
- const m = c.link?.match(/\/actions\/runs\/(\d+)/);
103795
- if (m)
103796
- ids.add(m[1]);
103797
- }
103798
- return { bucket: "fail", failedRunIds: [...ids], failedCheckNames: failed.map((c) => c.name) };
103799
- }
103800
- async function fetchFailedRunLogs(runIds, runner, cwd2, maxCharsPerRun = 4000) {
103801
- const chunks = [];
103802
- for (const id of runIds) {
103803
- try {
103804
- const r = await runner.run(["gh", "run", "view", id, "--log-failed"], cwd2);
103805
- const text = r.stdout.trim();
103806
- const truncated = text.length > maxCharsPerRun ? text.slice(0, maxCharsPerRun) + `
103807
- \u2026[truncated ${text.length - maxCharsPerRun} chars]` : text;
103808
- chunks.push(`--- run ${id} ---
103809
- ${truncated}`);
103810
- } catch (err) {
103811
- chunks.push(`--- run ${id} ---
103812
- (failed to fetch logs: ${err.message})`);
103813
- }
103814
- }
103815
- return chunks.join(`
103816
-
103817
- `);
103818
- }
103819
- async function safeSha(getHeadSha) {
103820
- try {
103821
- const sha = (await getHeadSha()).trim();
103822
- return sha || null;
103823
- } catch {
103824
- return null;
103825
- }
103826
- }
103827
- async function fixCiUntilGreen(deps, opts) {
103828
- for (let attempt2 = 1;attempt2 <= opts.maxAttempts; attempt2++) {
103829
- let pollN = 0;
103830
- while (true) {
103831
- if (deps.cancelled?.())
103832
- return { success: false, attempts: attempt2 - 1, reason: "cancelled" };
103833
- pollN += 1;
103834
- deps.onPhase?.("ci-poll", `attempt ${attempt2}/${opts.maxAttempts} \xB7 poll ${pollN}`);
103835
- let s;
103836
- try {
103837
- s = await deps.getStatus();
103838
- } catch (err) {
103839
- deps.log(`! gh pr checks failed permanently: ${err.message} \u2014 giving up CI watch`, "red");
103840
- return { success: false, attempts: attempt2 - 1, reason: "gh-failed" };
103841
- }
103842
- if (s.bucket === "pass") {
103843
- deps.log(`\u2713 CI green for PR (after ${attempt2 - 1} fix attempts)`, "green");
103844
- return { success: true, attempts: attempt2 - 1 };
103845
- }
103846
- if (s.bucket === "fail") {
103847
- deps.log(`\u2717 CI failing (attempt ${attempt2}/${opts.maxAttempts}) \u2014 fetching logs and re-running task`, "yellow");
103848
- deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 fetching logs`);
103849
- const logs = await deps.getFailedLogs(s.failedRunIds);
103850
- deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 re-running worker`);
103851
- const steering = `CI is failing on this PR. Investigate and fix:
103852
-
103853
- \`\`\`
103854
- ${logs}
103855
- \`\`\``;
103856
- const shaBefore = deps.getHeadSha ? await safeSha(deps.getHeadSha) : null;
103857
- const code = await deps.runTaskWithSteering(steering);
103858
- if (code !== 0) {
103859
- deps.log(`! task loop exited code ${code} during CI fix attempt ${attempt2}`, "red");
103860
- }
103861
- if (shaBefore !== null) {
103862
- const shaAfter = await safeSha(deps.getHeadSha);
103863
- if (shaAfter !== null && shaAfter === shaBefore) {
103864
- 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");
103865
- return { success: false, attempts: attempt2, reason: "no-progress" };
103866
- }
103867
- }
103868
- try {
103869
- deps.onPhase?.("ci-fix", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pushing fix`);
103870
- await deps.pushBranch();
103871
- } catch (err) {
103872
- deps.log(`! push failed during CI fix: ${err.message}`, "red");
103873
- return { success: false, attempts: attempt2, reason: "push-failed" };
103874
- }
103875
- break;
103876
- }
103877
- deps.onPhase?.("ci-poll", `attempt ${attempt2}/${opts.maxAttempts} \xB7 pending, waiting`);
103878
- await deps.sleep(opts.pollIntervalSeconds * 1000);
103879
- }
103880
- }
103881
- return { success: false, attempts: opts.maxAttempts, reason: "max-attempts" };
103882
- }
103883
- var PR_CHECKS_FIELDS = "name,bucket,link,workflow,event";
103884
- var init_ci = __esm(() => {
103885
- init_ci_classify();
103886
- });
103887
-
103888
104075
  // apps/agent/src/pr-status.ts
103889
104076
  function bucketChecks(rollup, prState, ignoreCiChecks = []) {
103890
104077
  if (rollup === null || rollup === undefined) {
@@ -104488,33 +104675,9 @@ async function runWorkerWithFixTask(ctx, heading, body) {
104488
104675
  }
104489
104676
  return code;
104490
104677
  }
104491
- async function pushBranchSafely(ctx) {
104492
- try {
104493
- ctx.emit("pushing", "after conflict resolution");
104494
- await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
104495
- return true;
104496
- } catch (pushErr) {
104497
- const pe = pushErr;
104498
- const blob = `${pe.message}
104499
- ${pe.stderr ?? ""}`;
104500
- if (!/non-fast-forward|Updates were rejected/i.test(blob)) {
104501
- ctx.log(`! push after conflict fix failed: ${pe.message}`, "red");
104502
- return false;
104503
- }
104504
- try {
104505
- await ctx.cmd.run(["git", "fetch", "origin", ctx.branch], ctx.cwd);
104506
- await ctx.cmd.run(["git", "merge", "--no-edit", `origin/${ctx.branch}`], ctx.cwd);
104507
- await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
104508
- return true;
104509
- } catch (retryErr) {
104510
- ctx.log(`! push after merging origin/${ctx.branch} failed: ${retryErr.message}`, "red");
104511
- return false;
104512
- }
104513
- }
104514
- }
104515
104678
  async function createPrWithRetry(ctx, issue2) {
104516
104679
  const base2 = ctx.base;
104517
- const maxAttempts = ctx.cfg.maxCiFixAttempts;
104680
+ const maxAttempts = MAX_PR_CREATE_ATTEMPTS;
104518
104681
  let hookFixAttempt = 0;
104519
104682
  let nonFfRebaseAttempted = false;
104520
104683
  let pr = null;
@@ -104627,86 +104790,6 @@ ${reBlob.trim()}`);
104627
104790
  }
104628
104791
  }
104629
104792
  }
104630
- async function fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict) {
104631
- const wantConflictLoop = !!checkPrConflict;
104632
- const maxOuterAttempts = ctx.cfg.maxCiFixAttempts;
104633
- let outerAttempt = 0;
104634
- let ciConfirmedGreen = false;
104635
- while (outerAttempt < maxOuterAttempts) {
104636
- if (wantConflictLoop) {
104637
- ctx.emit("conflict-check");
104638
- let conflicting = false;
104639
- try {
104640
- conflicting = await checkPrConflict(prUrl);
104641
- } catch (err) {
104642
- ctx.log(`! conflict check failed: ${err.message}`, "yellow");
104643
- }
104644
- if (!conflicting && ciConfirmedGreen)
104645
- return 0;
104646
- if (conflicting) {
104647
- outerAttempt++;
104648
- ciConfirmedGreen = false;
104649
- ctx.emit("conflict-fix-inner", `attempt ${outerAttempt}/${maxOuterAttempts}`);
104650
- ctx.log(` merge conflicts on PR (attempt ${outerAttempt}/${maxOuterAttempts}) \u2014 spawning resolution task`, "yellow");
104651
- const conflictCode = await runWorkerWithFixTask(ctx, "Resolve PR merge conflicts", [
104652
- `The PR ${prUrl} has merge conflicts with \`${ctx.base}\`.`,
104653
- "",
104654
- "Steps:",
104655
- `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.`,
104656
- "2. Resolve conflicts in the files git lists.",
104657
- "3. Stage and commit the resolution as a new merge commit."
104658
- ].join(`
104659
- `));
104660
- if (conflictCode !== 0) {
104661
- ctx.log(`! conflict resolution worker exited code ${conflictCode} \u2014 giving up`, "red");
104662
- return PR_FAILED_EXIT;
104663
- }
104664
- const pushed = await pushBranchSafely(ctx);
104665
- if (!pushed)
104666
- return PR_FAILED_EXIT;
104667
- continue;
104668
- }
104669
- }
104670
- if (!wantFixCi)
104671
- break;
104672
- if (!ciConfirmedGreen) {
104673
- ctx.log(` watching CI for ${prUrl} (max ${ctx.cfg.maxCiFixAttempts} fix attempts)`, "gray");
104674
- ctx.emit("ci-poll", "starting");
104675
- const result2 = await fixCiUntilGreen({
104676
- onPhase: (p, d) => ctx.emit(p, d),
104677
- 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),
104678
- getFailedLogs: (ids) => fetchFailedRunLogs(ids, ctx.cmd, ctx.cwd),
104679
- runTaskWithSteering: (steering) => runWorkerWithFixTask(ctx, "Fix failing CI checks", steering),
104680
- pushBranch: async () => {
104681
- await ctx.cmd.run(["git", "push", "origin", ctx.branch], ctx.cwd);
104682
- },
104683
- getHeadSha: async () => {
104684
- const r = await ctx.cmd.run(["git", "rev-parse", "HEAD"], ctx.cwd);
104685
- return r.stdout.trim();
104686
- },
104687
- log: ctx.log,
104688
- sleep: (ms) => new Promise((r) => setTimeout(r, ms))
104689
- }, {
104690
- maxAttempts: ctx.cfg.maxCiFixAttempts,
104691
- pollIntervalSeconds: ctx.cfg.ciPollIntervalSeconds
104692
- });
104693
- if (!result2.success) {
104694
- ctx.log(`! CI fix loop gave up after ${result2.attempts} attempts (${result2.reason ?? "unknown"}) \u2014 withholding done-status until CI passes`, "red");
104695
- return CI_FAILED_EXIT;
104696
- }
104697
- ciConfirmedGreen = true;
104698
- }
104699
- if (wantConflictLoop) {
104700
- continue;
104701
- }
104702
- return 0;
104703
- }
104704
- if (outerAttempt >= maxOuterAttempts) {
104705
- ctx.log(`! outer fix loop exhausted ${maxOuterAttempts} attempts \u2014 giving up`, "red");
104706
- return CI_FAILED_EXIT;
104707
- }
104708
- return 0;
104709
- }
104710
104793
  async function findNeverTouchViolations(cmd, cwd2, base2, neverTouch) {
104711
104794
  if (neverTouch.length === 0)
104712
104795
  return [];
@@ -104727,27 +104810,8 @@ async function findNeverTouchViolations(cmd, cwd2, base2, neverTouch) {
104727
104810
  return findBoundaryViolations(files, neverTouch);
104728
104811
  }
104729
104812
  async function runPrPhase(input, deps) {
104730
- const {
104731
- changeName,
104732
- cwd: cwd2,
104733
- branch,
104734
- changeDir,
104735
- stateFilePath,
104736
- issue: issue2,
104737
- wantFixCi,
104738
- wantAutoMerge,
104739
- cfg
104740
- } = input;
104741
- const {
104742
- cmd,
104743
- log: log3,
104744
- emit: emit3,
104745
- respawnWorker,
104746
- registerPr,
104747
- onPrReady,
104748
- checkPrConflict,
104749
- resolveDependencyBaseBranch
104750
- } = 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;
104751
104815
  if (!branch || !issue2) {
104752
104816
  log3(`! createPr requested but no worktree branch is tracked for ${changeName} (use --worktree)`, "yellow");
104753
104817
  return PR_FAILED_EXIT;
@@ -104814,7 +104878,7 @@ ${indented}${suffix}`, "yellow");
104814
104878
  }
104815
104879
  return PR_FAILED_EXIT;
104816
104880
  }
104817
- const maxOuterAttempts = cfg.maxCiFixAttempts;
104881
+ const maxOuterAttempts = MAX_PR_CREATE_ATTEMPTS;
104818
104882
  let onlyMetaAttempts = 0;
104819
104883
  let pr = null;
104820
104884
  const finalizeNoOpAsDone = cfg.finalizeNoOpAsDone !== false;
@@ -104889,36 +104953,8 @@ ${indented}${suffix}`, "yellow");
104889
104953
  }
104890
104954
  log3(` ${pr.created ? "opened" : "found existing"} PR: ${prUrl}`, "green");
104891
104955
  registerPr?.(changeName, prUrl);
104892
- let manualMergePending = false;
104893
- const prReadyNeeded = cfg.prDraft === true;
104894
- if (!prReadyNeeded && wantAutoMerge) {
104895
- const fallbackEnabled = cfg.manualMergeWhenAutoMergeDisabled !== false;
104896
- const repoAllowsAutoMerge = await detectRepoAutoMergeAllowed(prUrl, cmd, cwd2, log3);
104897
- if (repoAllowsAutoMerge === false && fallbackEnabled) {
104898
- log3(` repo has auto-merge disabled \u2014 will poll ${prUrl} and merge via gh pr merge once checks pass`, "yellow");
104899
- manualMergePending = true;
104900
- } else {
104901
- try {
104902
- await cmd.run(["gh", "pr", "merge", prUrl, "--auto", `--${cfg.autoMergeStrategy}`], cwd2);
104903
- log3(` enabled auto-merge (${cfg.autoMergeStrategy}) on ${prUrl}`, "green");
104904
- emit3("auto-merge-enabled", cfg.autoMergeStrategy);
104905
- } catch (err) {
104906
- const e = err;
104907
- const detail = e.stderr?.trim() || e.message;
104908
- log3(`! failed to enable auto-merge on ${prUrl}: ${detail}`, "yellow");
104909
- if (fallbackEnabled && /auto[- ]merge/i.test(detail)) {
104910
- log3(` falling back to manual merge after CI passes for ${prUrl}`, "yellow");
104911
- manualMergePending = true;
104912
- }
104913
- }
104914
- }
104915
- } else if (prReadyNeeded && wantAutoMerge) {
104916
- manualMergePending = true;
104917
- }
104918
- const ciResult = await fixConflictsAndCiLoop(ctx, prUrl, wantFixCi, checkPrConflict);
104919
- if (ciResult !== 0)
104920
- return ciResult;
104921
- if (prReadyNeeded) {
104956
+ let readyOk = true;
104957
+ if (cfg.prDraft === true) {
104922
104958
  emit3("pr-ready");
104923
104959
  try {
104924
104960
  await cmd.run(["gh", "pr", "ready", prUrl], cwd2);
@@ -104926,22 +104962,25 @@ ${indented}${suffix}`, "yellow");
104926
104962
  } catch (err) {
104927
104963
  const e = err;
104928
104964
  log3(`! gh pr ready failed for ${prUrl}: ${e.stderr?.trim() || e.message}`, "yellow");
104929
- manualMergePending = false;
104965
+ readyOk = false;
104930
104966
  }
104931
104967
  }
104932
- if (manualMergePending) {
104933
- try {
104934
- await cmd.run(["gh", "pr", "merge", prUrl, `--${cfg.autoMergeStrategy}`], cwd2);
104935
- log3(` manually merged (${cfg.autoMergeStrategy}) ${prUrl}`, "green");
104936
- emit3("auto-merge-enabled", `manual:${cfg.autoMergeStrategy}`);
104937
- } catch (err) {
104938
- const e = err;
104939
- 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
+ }
104940
104981
  }
104941
104982
  }
104942
- if (!(wantAutoMerge && !prReadyNeeded)) {
104943
- await onPrReady?.(prUrl);
104944
- }
104983
+ await onPrReady?.(prUrl);
104945
104984
  return 0;
104946
104985
  }
104947
104986
  async function runWorktreeCleanupPhase(input, deps) {
@@ -105048,7 +105087,6 @@ async function runPostTask(input, deps) {
105048
105087
  exitCode,
105049
105088
  useWorktree,
105050
105089
  wantPr,
105051
- wantFixCi,
105052
105090
  wantAutoMerge,
105053
105091
  wantValidateOnly,
105054
105092
  cfg,
@@ -105138,7 +105176,6 @@ async function runPostTask(input, deps) {
105138
105176
  changeDir,
105139
105177
  stateFilePath,
105140
105178
  issue: issue2,
105141
- wantFixCi,
105142
105179
  wantAutoMerge,
105143
105180
  cfg
105144
105181
  }, {
@@ -105148,7 +105185,6 @@ async function runPostTask(input, deps) {
105148
105185
  respawnWorker,
105149
105186
  ...deps.registerPr !== undefined ? { registerPr: deps.registerPr } : {},
105150
105187
  ...deps.onPrReady !== undefined ? { onPrReady: deps.onPrReady } : {},
105151
- ...deps.checkPrConflict !== undefined ? { checkPrConflict: deps.checkPrConflict } : {},
105152
105188
  ...deps.resolveDependencyBaseBranch !== undefined ? { resolveDependencyBaseBranch: deps.resolveDependencyBaseBranch } : {}
105153
105189
  });
105154
105190
  }
@@ -105169,7 +105205,7 @@ async function runPostTask(input, deps) {
105169
105205
  await runTeardownPhase({ cwd: cwd2, teardownScript: cfg.teardownScript }, { runScript, log: log3, emit: emit3 });
105170
105206
  return effectiveCode;
105171
105207
  }
105172
- 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) => {
105173
105209
  const proc = Bun.spawnSync({
105174
105210
  cmd: ["sh", "-c", cmd],
105175
105211
  cwd: cwd2,
@@ -105188,7 +105224,6 @@ var init_post_task = __esm(() => {
105188
105224
  init_git2();
105189
105225
  init_linear();
105190
105226
  init_pr();
105191
- init_ci();
105192
105227
  init_pr_status();
105193
105228
  init_wait_for_mergeability();
105194
105229
  init_worktree();
@@ -105593,10 +105628,12 @@ class AgentCoordinator {
105593
105628
  continue;
105594
105629
  if (!this.dependenciesResolved(issue2))
105595
105630
  continue;
105596
- if (await this.maybePromoteFinishedConflicted(issue2))
105597
- continue;
105598
105631
  const changeDir = this.deps.getChangeDir?.(issue2) ?? undefined;
105599
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;
105600
105637
  actor.send({ type: "RESUME_DETECTED" });
105601
105638
  if (changeDir) {
105602
105639
  await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
@@ -105741,6 +105778,12 @@ class AgentCoordinator {
105741
105778
  return false;
105742
105779
  if (pr.status !== "conflicted" && pr.status !== "ci_failed")
105743
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;
105744
105787
  const stateLabel = pr.status === "conflicted" ? "conflicting with main" : "failing CI";
105745
105788
  if (this.conflictPromoted.has(issue2.id))
105746
105789
  return true;
@@ -105878,6 +105921,8 @@ class AgentCoordinator {
105878
105921
  }
105879
105922
  async scanPrMergeStates() {
105880
105923
  const counts = emptyPrStatus();
105924
+ if (!this.opts.prRecovery?.enabled)
105925
+ return counts;
105881
105926
  let candidates = [];
105882
105927
  try {
105883
105928
  candidates = await this.deps.fetchDoneCandidates();
@@ -105922,7 +105967,26 @@ class AgentCoordinator {
105922
105967
  this.deps.onLog(`! pr-tracker clear failed for ${issue2.identifier}: ${err.message}`, "yellow");
105923
105968
  }
105924
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
+ }
105925
105987
  if (pr.status === "conflicted") {
105988
+ if (!this.opts.prRecovery?.fixConflicts)
105989
+ continue;
105926
105990
  if (tracker?.isBailed(issue2.identifier)) {
105927
105991
  counts.quarantined += 1;
105928
105992
  continue;
@@ -105956,6 +106020,8 @@ class AgentCoordinator {
105956
106020
  continue;
105957
106021
  }
105958
106022
  if (pr.status === "ci_failed") {
106023
+ if (!this.opts.prRecovery?.fixCi)
106024
+ continue;
105959
106025
  if (tracker?.isBailed(issue2.identifier)) {
105960
106026
  counts.quarantined += 1;
105961
106027
  continue;
@@ -106002,6 +106068,49 @@ class AgentCoordinator {
106002
106068
  }
106003
106069
  return counts;
106004
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
+ }
106005
106114
  errorMarkerCleared(issue2) {
106006
106115
  const se = this.opts.setError;
106007
106116
  if (!se)
@@ -106310,9 +106419,12 @@ class AgentCoordinator {
106310
106419
  async notifyExited(issue2, changeName, code, trigger, workerCwd) {
106311
106420
  const noChanges = code === NO_CHANGES_EXIT;
106312
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;
106313
106425
  const changeDir = this.deps.getChangeDir?.(issue2) ?? undefined;
106314
106426
  const exitActor = await this.flowStore.getActor(issue2.id, changeDir);
106315
- exitActor.send({ type: ok ? "WORKER_SUCCEEDED" : "WORKER_FAILED" });
106427
+ exitActor.send({ type: !ok ? "WORKER_FAILED" : deferDone ? "PR_OPENED" : "WORKER_SUCCEEDED" });
106316
106428
  if (changeDir) {
106317
106429
  await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106318
106430
  }
@@ -106365,6 +106477,8 @@ class AgentCoordinator {
106365
106477
  } else if (trigger === "ci-fix") {
106366
106478
  this.ciFailedNotified.delete(issue2.id);
106367
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");
106368
106482
  } else if (this.opts.setDone) {
106369
106483
  try {
106370
106484
  await this.deps.applyIndicator(issue2, this.opts.setDone);
@@ -106446,6 +106560,7 @@ var emptyPrStatus = () => ({
106446
106560
  });
106447
106561
  var init_coordinator = __esm(() => {
106448
106562
  init_types2();
106563
+ init_linear_client();
106449
106564
  init_post_task();
106450
106565
  init_queue_order();
106451
106566
  init_src();
@@ -106983,7 +107098,7 @@ function issueInAwaitingStatus(issue2, indicators) {
106983
107098
  async function releaseAwaitingMarker(issue2, statePath, deps) {
106984
107099
  const { stateObj, confirmation } = await readConfirmationState(statePath);
106985
107100
  if (!confirmation.awaitingMarkerAppliedAt && !issueInAwaitingStatus(issue2, deps.indicators)) {
106986
- return;
107101
+ return false;
106987
107102
  }
106988
107103
  if (deps.indicators.clearAwaitingConfirmation) {
106989
107104
  try {
@@ -107005,6 +107120,7 @@ async function releaseAwaitingMarker(issue2, statePath, deps) {
107005
107120
  } catch (err) {
107006
107121
  deps.onLog(`! persist cleared awaitingMarkerAppliedAt for ${issue2.identifier}: ${err.message}`, "yellow");
107007
107122
  }
107123
+ return true;
107008
107124
  }
107009
107125
  function confirmationUsesCommentIndicator(cfg) {
107010
107126
  const { getApproved, getAutoApprove, getConfirmGate } = cfg.linear.indicators;
@@ -107066,13 +107182,15 @@ async function processAwaitingForIssue(issue2, deps) {
107066
107182
  persistedConfirmation: confirmation
107067
107183
  });
107068
107184
  if (!active) {
107069
- deps.awaitingChangeSet.delete(changeName);
107070
- await releaseAwaitingMarker(issue2, statePath, {
107185
+ const wasTracked = deps.awaitingChangeSet.delete(changeName);
107186
+ const released = await releaseAwaitingMarker(issue2, statePath, {
107071
107187
  indicators,
107072
107188
  applyIndicator: deps.applyIndicator,
107073
107189
  onLog: deps.onLog
107074
107190
  });
107075
- 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
+ }
107076
107194
  return false;
107077
107195
  }
107078
107196
  if (!hasUnchecked(tasks2 ?? "")) {
@@ -107216,8 +107334,28 @@ function parsePrNumber(url2) {
107216
107334
  const m = PR_NUMBER_RE.exec(url2);
107217
107335
  return m ? Number(m[1]) : null;
107218
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
+ }
107219
107350
  async function resolveDependencyBaseBranchImpl(issue2, runner, runnerCwd, deps) {
107220
- 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
+ }
107221
107359
  if (blockerIds.length === 0)
107222
107360
  return null;
107223
107361
  let attachmentsByBlocker;
@@ -107251,17 +107389,30 @@ async function resolveDependencyBaseBranchImpl(issue2, runner, runnerCwd, deps)
107251
107389
  }
107252
107390
  }
107253
107391
  if (openPrs.length === 1) {
107254
- candidates.push(openPrs[0]);
107392
+ candidates.push({ blockerId, base: openPrs[0] });
107255
107393
  } else if (openPrs.length > 1) {
107256
107394
  deps.onLog(` ${issue2.identifier}: blocker ${blockerId} has ${openPrs.length} open PRs \u2014 skipping dependency base resolution`, "gray");
107257
107395
  }
107258
107396
  }
107259
107397
  if (candidates.length === 1)
107260
- return candidates[0];
107261
- if (candidates.length > 1) {
107262
- 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;
107263
107408
  }
107264
- 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;
107265
107416
  }
107266
107417
  function createOpenDraftPr(deps) {
107267
107418
  const create3 = deps.createPr ?? createPullRequest;
@@ -107305,11 +107456,18 @@ function traceCmdRunner(base2, onStart, onEnd) {
107305
107456
  }
107306
107457
  };
107307
107458
  }
107308
- var bunGitRunner, bunCmdRunner;
107459
+ var ghAuthEnv = () => scrubGithubAppTokenEnv(), bunGitRunner, bunCmdRunner;
107309
107460
  var init_runners = __esm(() => {
107461
+ init_preflight();
107310
107462
  bunGitRunner = {
107311
107463
  run: async (args, cwd2) => {
107312
- 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
+ });
107313
107471
  const stdout = await new Response(proc.stdout).text();
107314
107472
  const stderr = await new Response(proc.stderr).text();
107315
107473
  const code = await proc.exited;
@@ -107327,7 +107485,7 @@ var init_runners = __esm(() => {
107327
107485
  };
107328
107486
  bunCmdRunner = {
107329
107487
  run: async (cmd, cwd2) => {
107330
- 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" });
107331
107489
  const stdout = await new Response(proc.stdout).text();
107332
107490
  const stderr = await new Response(proc.stderr).text();
107333
107491
  const code = await proc.exited;
@@ -107392,7 +107550,7 @@ var init_indicators = __esm(() => {
107392
107550
 
107393
107551
  // apps/agent/src/agent/wire/linear-resolvers.ts
107394
107552
  function createLinearResolvers(input) {
107395
- const { apiKey, team, assignee, anyAssignee, diag } = input;
107553
+ const { apiKey, team, assignee, anyAssignee, requireAllLabels, diag } = input;
107396
107554
  const ticketNumbers = input.ticketNumbers ?? [];
107397
107555
  const stateCache = new Map;
107398
107556
  const labelCache = new Map;
@@ -107509,6 +107667,7 @@ function createLinearResolvers(input) {
107509
107667
  team,
107510
107668
  assignee,
107511
107669
  anyAssignee,
107670
+ requireAllLabels,
107512
107671
  include,
107513
107672
  exclude: excl,
107514
107673
  ...ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {}
@@ -107531,7 +107690,18 @@ function createLinearResolvers(input) {
107531
107690
  resolveLabelIdForTeam
107532
107691
  };
107533
107692
  }
107534
- 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) {
107535
107705
  const getIndicators = [
107536
107706
  indicators.getTodo,
107537
107707
  indicators.getInProgress,
@@ -107550,13 +107720,7 @@ async function fetchDoneCandidatesWith(apiKey, team, _assignee, indicators, tick
107550
107720
  const include = ind.filter ?? [];
107551
107721
  if (include.length === 0)
107552
107722
  return;
107553
- const issues = await fetchOpenIssues(apiKey, {
107554
- team,
107555
- anyAssignee: true,
107556
- include,
107557
- exclude: [],
107558
- ...ticketNumbers && ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {}
107559
- });
107723
+ const issues = await fetchOpenIssues(apiKey, doneCandidateSpec(team, assignee, anyAssignee, requireAllLabels, include, ticketNumbers));
107560
107724
  for (const issue2 of issues) {
107561
107725
  if (!seen.has(issue2.id)) {
107562
107726
  seen.add(issue2.id);
@@ -107593,6 +107757,7 @@ function createPrepareHelpers(input) {
107593
107757
  maps,
107594
107758
  scriptRunner
107595
107759
  } = input;
107760
+ const worktreeProvider = input.worktreeProvider ?? defaultWorktreeProvider;
107596
107761
  async function runScript(label, cmd, cwd2) {
107597
107762
  diag("script", ` ${label}: ${cmd}`, "gray");
107598
107763
  const code = await scriptRunner(cmd, cwd2);
@@ -107611,7 +107776,7 @@ function createPrepareHelpers(input) {
107611
107776
  const baseBranch = baseBranchFromLabels(issue2.labels) ?? cfg.prBaseBranch;
107612
107777
  let wt;
107613
107778
  try {
107614
- wt = await runCapability(git.createWorktree, {
107779
+ wt = await worktreeProvider.create({
107615
107780
  projectRoot,
107616
107781
  changeName: probeName,
107617
107782
  baseBranch,
@@ -107628,7 +107793,7 @@ function createPrepareHelpers(input) {
107628
107793
  scaffoldStatesDir = wtLayout.statesDir;
107629
107794
  diag("worktree", ` ${issue2.identifier} worktree: ${wt.cwd} (${wt.branch})`, "gray");
107630
107795
  try {
107631
- await runCapability(git.seedWorktreeMcpConfig, {
107796
+ await worktreeProvider.seedMcpConfig({
107632
107797
  projectRoot,
107633
107798
  worktreeCwd: wt.cwd
107634
107799
  });
@@ -107824,6 +107989,7 @@ PR: ${ciPrUrl}` : ""
107824
107989
  }
107825
107990
  return { prepare, prepareTaskForTrigger, runScript, reactivateState: reactivateState2 };
107826
107991
  }
107992
+ var defaultWorktreeProvider;
107827
107993
  var init_prepare = __esm(() => {
107828
107994
  init_layout();
107829
107995
  init_tasks_md();
@@ -107834,6 +108000,10 @@ var init_prepare = __esm(() => {
107834
108000
  init_scaffold();
107835
108001
  init_worktree();
107836
108002
  init_task_bodies();
108003
+ defaultWorktreeProvider = {
108004
+ create: (args) => runCapability(git.createWorktree, args),
108005
+ seedMcpConfig: (args) => runCapability(git.seedWorktreeMcpConfig, args)
108006
+ };
107837
108007
  });
107838
108008
 
107839
108009
  // apps/agent/src/agent/pr-url/index.ts
@@ -107893,9 +108063,65 @@ var init_pr_url = __esm(() => {
107893
108063
  init_task_bodies();
107894
108064
  });
107895
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
+
107896
108113
  // apps/agent/src/agent/wire/pr-discovery.ts
107897
108114
  function createPrDiscovery(input) {
107898
- 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;
107899
108125
  const prUnavailable = new Map;
107900
108126
  const prUrlByIssue = createPrUrlCache(5 * 60 * 1000);
107901
108127
  function isPrUnavailable(changeName) {
@@ -107967,9 +108193,11 @@ function createPrDiscovery(input) {
107967
108193
  if (outcome.kind === "conflicting")
107968
108194
  return { url: prUrl, status: "conflicted" };
107969
108195
  try {
107970
- const ci = await getPrChecksStatus(prUrl, cmdRunner, projectRoot);
108196
+ const ci = await getPrChecksStatus(prUrl, cmdRunner, projectRoot, undefined, ignoreCiChecks);
107971
108197
  if (ci.bucket === "fail")
107972
108198
  return { url: prUrl, status: "ci_failed" };
108199
+ if (ci.bucket === "pending")
108200
+ return { url: prUrl, status: "unknown" };
107973
108201
  } catch (err) {
107974
108202
  diag("ci", `! gh pr checks ${prUrl} failed (PR scan): ${err.message}`, "yellow");
107975
108203
  }
@@ -108250,6 +108478,7 @@ function createMentionScanner(input) {
108250
108478
  team,
108251
108479
  assignee,
108252
108480
  anyAssignee,
108481
+ requireAllLabels,
108253
108482
  indicators,
108254
108483
  projectRoot,
108255
108484
  useWorktree,
@@ -108274,6 +108503,7 @@ function createMentionScanner(input) {
108274
108503
  team,
108275
108504
  assignee,
108276
108505
  anyAssignee,
108506
+ ...requireAllLabels && requireAllLabels.length > 0 ? { requireAllLabels } : {},
108277
108507
  ...ticketNumbers && ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {},
108278
108508
  indicators: {
108279
108509
  ...indicators.getTodo !== undefined ? { getTodo: indicators.getTodo } : {},
@@ -108430,6 +108660,31 @@ var init_mention_scan = __esm(() => {
108430
108660
  init_task_bodies();
108431
108661
  });
108432
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
+
108433
108688
  // apps/agent/src/agent/wire/spawn/default.ts
108434
108689
  import { join as join30 } from "path";
108435
108690
  function defaultSpawn(changeName, cmd, cwd2, logsDir, onWorkerOutput, note) {
@@ -108517,7 +108772,7 @@ function dispositionFromExitCode(code) {
108517
108772
  return "done";
108518
108773
  case NO_CHANGES_EXIT2:
108519
108774
  return "no-changes";
108520
- case CI_FAILED_EXIT2:
108775
+ case CI_FAILED_EXIT:
108521
108776
  return "ci-failed";
108522
108777
  case PR_FAILED_EXIT2:
108523
108778
  return "pr-failed";
@@ -108525,7 +108780,7 @@ function dispositionFromExitCode(code) {
108525
108780
  return "error";
108526
108781
  }
108527
108782
  }
108528
- 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;
108529
108784
 
108530
108785
  // packages/retro/src/paths.ts
108531
108786
  import { homedir as homedir7 } from "os";
@@ -108752,17 +109007,13 @@ function buildPostTaskInput(input) {
108752
109007
  exitCode: input.exitCode,
108753
109008
  useWorktree: input.useWorktree,
108754
109009
  wantPr: input.wantPr,
108755
- wantFixCi: input.wantFixCi,
108756
109010
  wantAutoMerge: input.wantAutoMerge,
108757
109011
  wantValidateOnly: input.wantValidateOnly,
108758
109012
  cfg: {
108759
109013
  teardownScript: cfg.teardownScript ?? null,
108760
109014
  prBaseBranch: cfg.prBaseBranch,
108761
109015
  autoMergeStrategy: cfg.autoMergeStrategy,
108762
- maxCiFixAttempts: cfg.maxCiFixAttempts,
108763
- ciPollIntervalSeconds: cfg.ciPollIntervalSeconds,
108764
109016
  cleanupWorktreeOnSuccess: cfg.cleanupWorktreeOnSuccess,
108765
- ignoreCiChecks: cfg.ignoreCiChecks,
108766
109017
  stackPrsOnDependencies: args.stackPrs || cfg.stackPrsOnDependencies,
108767
109018
  neverTouch: cfg.boundaries.never_touch,
108768
109019
  metaOnlyFiles: cfg.boundaries.meta_only_files,
@@ -108858,6 +109109,8 @@ function createSpawnWorker(input) {
108858
109109
  const f2 = Bun.file(missionTasksPath);
108859
109110
  return await f2.exists() ? await f2.text() : "";
108860
109111
  })();
109112
+ const guardOn = useWorktree && cwd2 !== projectRoot;
109113
+ const beforeSnapshotPromise = guardOn ? snapshotCheckout(projectRoot, gitRunner) : Promise.resolve(null);
108861
109114
  let logFilePath;
108862
109115
  let handle;
108863
109116
  if (injected) {
@@ -108878,10 +109131,29 @@ function createSpawnWorker(input) {
108878
109131
  onWorkerPhase?.(changeName, "working");
108879
109132
  const tracedCmd = onWorkerCmd ? traceCmdRunner(cmdRunner, (cmd) => onWorkerCmd(changeName, cmd, "start"), (cmd, ms, ok) => onWorkerCmd(changeName, cmd, "end", ms, ok)) : cmdRunner;
108880
109133
  const wantPrBase = args.createPr || cfg.createPrOnSuccess;
108881
- const wantFixCi = args.fixCi || cfg.fixCiOnFailure;
108882
109134
  const issueForChange = issueByChange.get(changeName);
108883
109135
  const wantAutoMerge = issueForChange ? issueMatchesGetIndicator(issueForChange, indicators.getAutoMerge) : false;
108884
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
+ }
108885
109157
  const workerLayout = projectLayout(cwd2);
108886
109158
  const validateSpecPath = join33(workerLayout.changeDir(changeName), "specs", "validate.md");
108887
109159
  const hasValidateSpec = await Bun.file(validateSpecPath).exists();
@@ -108943,7 +109215,6 @@ function createSpawnWorker(input) {
108943
109215
  exitCode: code,
108944
109216
  useWorktree,
108945
109217
  wantPr,
108946
- wantFixCi,
108947
109218
  wantAutoMerge,
108948
109219
  wantValidateOnly,
108949
109220
  ...trigger ? { trigger } : {},
@@ -108976,16 +109247,6 @@ function createSpawnWorker(input) {
108976
109247
  ...onWorkerPhase && {
108977
109248
  onPhase: (phase2, detail) => onWorkerPhase(changeName, phase2, detail)
108978
109249
  },
108979
- checkPrConflict: async (prUrl) => {
108980
- const outcome = await waitForMergeability({
108981
- bailOnError: true,
108982
- probe: async () => {
108983
- const res = await tracedCmd.run(["gh", "pr", "view", prUrl, "--json", "state,mergeable,mergeStateStatus"], cwd2);
108984
- return JSON.parse(res.stdout || "{}");
108985
- }
108986
- });
108987
- return outcome.kind === "conflicting";
108988
- },
108989
109250
  resolveDependencyBaseBranch: (issue2) => resolveDependencyBaseBranchImpl(issue2, tracedCmd, cwd2, { apiKey, onLog })
108990
109251
  });
108991
109252
  releaseWorkerMaps({ cwdByChange, statesDirByChange, branchByChange, issueByChange }, changeName);
@@ -109003,7 +109264,6 @@ var init_worker = __esm(() => {
109003
109264
  init_default2();
109004
109265
  init_runners();
109005
109266
  init_pr_helpers();
109006
- init_wait_for_mergeability();
109007
109267
  init_agent_run_state();
109008
109268
  init_retro();
109009
109269
  init_engine();
@@ -261431,6 +261691,7 @@ function renderListItem(doc2, item, indent, marker) {
261431
261691
  const tokens = item.tokens ?? [];
261432
261692
  let inlineRun = [];
261433
261693
  let placedInline = false;
261694
+ let renderedBlock = false;
261434
261695
  const placeInline = () => {
261435
261696
  if (inlineRun.length === 0)
261436
261697
  return;
@@ -261454,10 +261715,13 @@ function renderListItem(doc2, item, indent, marker) {
261454
261715
  continue;
261455
261716
  }
261456
261717
  placeInline();
261718
+ if (!placedInline && !renderedBlock)
261719
+ doc2.y = startY;
261457
261720
  renderBlock(doc2, tok, indent + LIST_INDENT);
261721
+ renderedBlock = true;
261458
261722
  }
261459
261723
  placeInline();
261460
- if (!placedInline) {
261724
+ if (!placedInline && !renderedBlock) {
261461
261725
  doc2.y = startY;
261462
261726
  doc2.text(" ", bodyX, startY, { width: bodyWidth });
261463
261727
  }
@@ -262432,8 +262696,7 @@ function buildAgentCoordinator(input) {
262432
262696
  const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
262433
262697
  const indicators = mergeIndicators(cfg.linear.indicators, args.indicators);
262434
262698
  const team = args.linearTeam || cfg.linear.team;
262435
- const effectiveFilter = args.linearFilter || (args.linearAssignee ? `assignee = ${args.linearAssignee}` : "") || cfg.linear.filter;
262436
- const { assignee, anyAssignee } = parseLinearFilter(effectiveFilter);
262699
+ const { assignee, anyAssignee, requireAllLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, args.linearAssignee));
262437
262700
  const ticketNumbers = resolveTicketNumbers(args.ticketTokens, team);
262438
262701
  const excludeFromTodo = unionMarkers(indicators.setDone, indicators.setError);
262439
262702
  const gitRunner = input.runners?.git ?? bunGitRunner;
@@ -262475,6 +262738,7 @@ function buildAgentCoordinator(input) {
262475
262738
  team,
262476
262739
  assignee,
262477
262740
  anyAssignee,
262741
+ requireAllLabels,
262478
262742
  diag,
262479
262743
  ...ticketNumbers.length > 0 ? { ticketNumbers } : {}
262480
262744
  });
@@ -262491,7 +262755,8 @@ function buildAgentCoordinator(input) {
262491
262755
  onLog,
262492
262756
  diag,
262493
262757
  prByChange,
262494
- getPollContext: () => pollContext
262758
+ getPollContext: () => pollContext,
262759
+ ignoreCiChecks: cfg.prRecovery.ignoreChecks
262495
262760
  });
262496
262761
  const prep = createPrepareHelpers({
262497
262762
  args,
@@ -262504,7 +262769,8 @@ function buildAgentCoordinator(input) {
262504
262769
  gitRunner,
262505
262770
  diag,
262506
262771
  maps: { cwdByChange, statesDirByChange, issueByChange, branchByChange, prByChange },
262507
- scriptRunner
262772
+ scriptRunner,
262773
+ ...input.runners?.worktree ? { worktreeProvider: input.runners.worktree } : {}
262508
262774
  });
262509
262775
  const fetchMentions = createMentionScanner({
262510
262776
  apiKey,
@@ -262513,6 +262779,7 @@ function buildAgentCoordinator(input) {
262513
262779
  team,
262514
262780
  assignee,
262515
262781
  anyAssignee,
262782
+ requireAllLabels,
262516
262783
  indicators,
262517
262784
  projectRoot,
262518
262785
  useWorktree,
@@ -262606,10 +262873,10 @@ function buildAgentCoordinator(input) {
262606
262873
  now: () => new Date
262607
262874
  };
262608
262875
  }
262609
- const prTrackerEnabled = args.prTrackerEnabled === undefined ? cfg.prTracker.enabled : args.prTrackerEnabled;
262610
- const prTracker = prTrackerEnabled ? new PrTracker({
262876
+ const prRecoveryEnabled = args.prRecoveryEnabled === undefined ? cfg.prRecovery.enabled : args.prRecoveryEnabled;
262877
+ const prTracker = prRecoveryEnabled ? new PrTracker({
262611
262878
  projectRoot,
262612
- maxRecoveryAttempts: cfg.prTracker.maxRecoveryAttempts
262879
+ maxRecoveryAttempts: cfg.prRecovery.maxRecoverySessions
262613
262880
  }) : null;
262614
262881
  const commentSync = createCommentSyncHooks({
262615
262882
  apiKey,
@@ -262627,7 +262894,7 @@ function buildAgentCoordinator(input) {
262627
262894
  fetchTodo: () => resolvers.fetchByGet(indicators.getTodo, excludeFromTodo),
262628
262895
  fetchInProgress: () => resolvers.fetchByGet(indicators.getInProgress, unionMarkers(indicators.setError)),
262629
262896
  fetchMentions,
262630
- 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),
262631
262898
  prepare: prep.prepare,
262632
262899
  prepareTaskForTrigger: prep.prepareTaskForTrigger,
262633
262900
  spawnWorker,
@@ -262639,6 +262906,7 @@ function buildAgentCoordinator(input) {
262639
262906
  return c.map((x2) => ({ body: x2.body }));
262640
262907
  },
262641
262908
  checkPrStatus: prDiscovery.checkPrStatus,
262909
+ hasPrForChange: (changeName) => prByChange.has(changeName),
262642
262910
  isChangeArchivedForIssue: (issue2) => isChangeArchivedForIssue(issue2, cwdByChange, projectRoot),
262643
262911
  onLog,
262644
262912
  ...onFileLog ? { onFileLog } : {},
@@ -262675,7 +262943,13 @@ function buildAgentCoordinator(input) {
262675
262943
  postComments: cfg.linear.postComments,
262676
262944
  commentEveryIterations: cfg.linear.updateEveryIterations,
262677
262945
  ...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {},
262678
- ...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
+ }
262679
262953
  });
262680
262954
  coordRef.current = coord;
262681
262955
  const filterDesc = describeIndicators(indicators, team, assignee, anyAssignee);
@@ -262836,23 +263110,23 @@ function SteeringField({
262836
263110
  initialFocused = false,
262837
263111
  onStateChange
262838
263112
  }) {
262839
- const [state, dispatch] = import_react61.useReducer(reducer2, { initialBuffer, initialCursor, initialFocused }, (init2) => ({
263113
+ const [state, dispatch] = import_react62.useReducer(reducer2, { initialBuffer, initialCursor, initialFocused }, (init2) => ({
262840
263114
  buffer: init2.initialBuffer,
262841
263115
  cursor: init2.initialCursor ?? init2.initialBuffer.length,
262842
263116
  focused: init2.initialFocused,
262843
263117
  status: "idle"
262844
263118
  }));
262845
263119
  const { buffer, cursor: cursor4, focused, status } = state;
262846
- const stateRef = import_react61.useRef(state);
263120
+ const stateRef = import_react62.useRef(state);
262847
263121
  stateRef.current = state;
262848
- const hintTimerRef = import_react61.useRef(null);
262849
- import_react61.useEffect(() => {
263122
+ const hintTimerRef = import_react62.useRef(null);
263123
+ import_react62.useEffect(() => {
262850
263124
  onFocusChange?.(focused);
262851
263125
  }, [focused, onFocusChange]);
262852
- import_react61.useEffect(() => {
263126
+ import_react62.useEffect(() => {
262853
263127
  onStateChange?.({ buffer, cursor: cursor4, focused });
262854
263128
  }, [buffer, cursor4, focused, onStateChange]);
262855
- import_react61.useEffect(() => {
263129
+ import_react62.useEffect(() => {
262856
263130
  return () => {
262857
263131
  if (hintTimerRef.current)
262858
263132
  clearTimeout(hintTimerRef.current);
@@ -262961,10 +263235,10 @@ function SteeringField({
262961
263235
  ]
262962
263236
  }, undefined, true, undefined, this);
262963
263237
  }
262964
- 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";
262965
263239
  var init_SteeringField = __esm(async () => {
262966
263240
  await init_build2();
262967
- import_react61 = __toESM(require_react(), 1);
263241
+ import_react62 = __toESM(require_react(), 1);
262968
263242
  jsx_dev_runtime10 = __toESM(require_jsx_dev_runtime(), 1);
262969
263243
  });
262970
263244
 
@@ -263333,21 +263607,35 @@ function AgentMode({
263333
263607
  const { exit } = use_app_default();
263334
263608
  const { isRawModeSupported } = use_stdin_default();
263335
263609
  const { columns, rows, resizeKey } = useTerminalSize();
263336
- const [logs, setLogs] = import_react62.useState([]);
263337
- const [preflightError, setPreflightError] = import_react62.useState(null);
263338
- const [, setTick] = import_react62.useState(0);
263339
- const [clock, setClock] = import_react62.useState(0);
263340
- const [focusedIdx, setFocusedIdx] = import_react62.useState(0);
263341
- const [showPendingTasks, setShowPendingTasks] = import_react62.useState(false);
263342
- const [showAllSubtasks, setShowAllSubtasks] = import_react62.useState(false);
263343
- const [gaveUpCount, setGaveUpCount] = import_react62.useState(0);
263344
- const coordRef = import_react62.useRef(null);
263345
- const workerMetaRef = import_react62.useRef(new Map);
263346
- const gatedTicketsRef = import_react62.useRef(new Map);
263347
- const nextPollAtRef = import_react62.useRef(0);
263348
- const cfgRef = import_react62.useRef(null);
263349
- const [effective, setEffective] = import_react62.useState(null);
263350
- 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({
263351
263639
  state: "idle",
263352
263640
  lastFound: null,
263353
263641
  lastAdded: null,
@@ -263360,14 +263648,14 @@ function AgentMode({
263360
263648
  setLogs((prev) => [...prev, { id: nextId(), text, color }]);
263361
263649
  logCoord(text, workerLogFile);
263362
263650
  }
263363
- const fileSinkRef = import_react62.useRef(null);
263651
+ const fileSinkRef = import_react63.useRef(null);
263364
263652
  if (fileSinkRef.current === null) {
263365
263653
  fileSinkRef.current = createJsonLogFileSink(args.jsonLogFile);
263366
263654
  }
263367
263655
  const fileEmit = (event) => {
263368
263656
  fileSinkRef.current?.emit(event);
263369
263657
  };
263370
- import_react62.useEffect(() => {
263658
+ import_react63.useEffect(() => {
263371
263659
  let pollTimer = null;
263372
263660
  let cancelled = false;
263373
263661
  async function init2() {
@@ -263380,15 +263668,14 @@ function AgentMode({
263380
263668
  if (!apiKey) {
263381
263669
  throw new Error("LINEAR_API_KEY not set \u2014 cannot poll Linear");
263382
263670
  }
263383
- const pf = await runPreflight2();
263671
+ const pf = await runPreflight2({
263672
+ requireRepoWrite: args.createPr || cfg2.createPrOnSuccess,
263673
+ repoCwd: projectRoot
263674
+ });
263384
263675
  if (!pf.ok) {
263385
263676
  fileEmit({ type: "error", code: "auth_failure", tool: pf.tool, text: pf.message });
263386
263677
  setPreflightError({ tool: pf.tool, message: pf.message });
263387
- process.exitCode = 2;
263388
- setTimeout(() => {
263389
- exit();
263390
- setTimeout(() => process.exit(2), 200);
263391
- }, 100);
263678
+ setFatalExit(2);
263392
263679
  return;
263393
263680
  }
263394
263681
  const { coord: coord2, filterDesc, concurrency, pollInterval, runBaselineGate: runBaselineGate2, getGaveUpTotal } = buildCoordinator({
@@ -263564,10 +263851,7 @@ function AgentMode({
263564
263851
  const message = err instanceof Error ? err.message : String(err);
263565
263852
  fileEmit({ type: "error", code: "init_failure", text: message });
263566
263853
  appendLog(`! ${message}`, "red");
263567
- setTimeout(() => {
263568
- exit();
263569
- setTimeout(() => process.exit(1), 200);
263570
- }, 100);
263854
+ setFatalExit(1);
263571
263855
  });
263572
263856
  let shuttingDown = false;
263573
263857
  const onSig = () => {
@@ -263610,8 +263894,8 @@ function AgentMode({
263610
263894
  process.off("SIGTERM", onSig);
263611
263895
  };
263612
263896
  }, []);
263613
- const lastPauseRef = import_react62.useRef(null);
263614
- import_react62.useEffect(() => {
263897
+ const lastPauseRef = import_react63.useRef(null);
263898
+ import_react63.useEffect(() => {
263615
263899
  let cancelled = false;
263616
263900
  const interval = setInterval(() => {
263617
263901
  if (cancelled)
@@ -263675,10 +263959,10 @@ function AgentMode({
263675
263959
  const termWidth = columns - 2;
263676
263960
  const termHeight = rows;
263677
263961
  const safeFocusedIdx = activeCount > 0 ? Math.min(focusedIdx, activeCount - 1) : 0;
263678
- const steeringFocusedRef = import_react62.useRef(false);
263679
- const steeringBufferRef = import_react62.useRef("");
263680
- const steeringCursorRef = import_react62.useRef(0);
263681
- 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);
263682
263966
  use_input_default((input, key) => {
263683
263967
  if (steeringFocusedRef.current)
263684
263968
  return;
@@ -263730,7 +264014,15 @@ function AgentMode({
263730
264014
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
263731
264015
  color: "red",
263732
264016
  children: preflightError.message
263733
- }, 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)
263734
264026
  ]
263735
264027
  }, undefined, true, undefined, this);
263736
264028
  }
@@ -263847,10 +264139,13 @@ function AgentMode({
263847
264139
  color: "green",
263848
264140
  children: " \u25CF PR"
263849
264141
  }, undefined, false, undefined, this),
263850
- cfg.fixCiOnFailure && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264142
+ cfg.prRecovery.enabled && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
263851
264143
  color: "green",
263852
- children: " \u25CF fixCI"
263853
- }, undefined, false, undefined, this),
264144
+ children: [
264145
+ " \u25CF recover",
264146
+ cfg.prRecovery.fixCi ? "+CI" : ""
264147
+ ]
264148
+ }, undefined, true, undefined, this),
263854
264149
  cfg.useWorktree && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
263855
264150
  color: "green",
263856
264151
  children: " \u25CF worktree"
@@ -264750,11 +265045,15 @@ function AgentMode({
264750
265045
  }, w2.changeName, true, undefined, this);
264751
265046
  })
264752
265047
  ]
264753
- }, 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)
264754
265053
  ]
264755
265054
  }, resizeKey, true, undefined, this);
264756
265055
  }
264757
- 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;
264758
265057
  var init_AgentMode = __esm(async () => {
264759
265058
  init_cli2();
264760
265059
  init_config();
@@ -264770,9 +265069,10 @@ var init_AgentMode = __esm(async () => {
264770
265069
  init_worker_state_poll();
264771
265070
  await __promiseAll([
264772
265071
  init_build2(),
265072
+ init_useHoldToClose(),
264773
265073
  init_SteeringField()
264774
265074
  ]);
264775
- import_react62 = __toESM(require_react(), 1);
265075
+ import_react63 = __toESM(require_react(), 1);
264776
265076
  jsx_dev_runtime11 = __toESM(require_jsx_dev_runtime(), 1);
264777
265077
  SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
264778
265078
  HYPERLINKS_SUPPORTED = !process.env["TMUX"];
@@ -264834,7 +265134,7 @@ function createSession(name, command, env3) {
264834
265134
  envArgs.push("-e", `${key}=${value}`);
264835
265135
  }
264836
265136
  const quoted = command.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
264837
- 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`;
264838
265138
  const result2 = Bun.spawnSync({
264839
265139
  cmd: ["tmux", "new-session", "-d", "-s", name, ...envArgs, "sh", "-c", shellCmd],
264840
265140
  stderr: "pipe"
@@ -265075,23 +265375,20 @@ function buildBuckets(indicators) {
265075
265375
  { label: "auto-merge", indicator: indicators.getAutoMerge, exclude: [] }
265076
265376
  ];
265077
265377
  }
265078
- async function fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, ticketNumbers) {
265378
+ async function fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, requireAllLabels, ticketNumbers) {
265079
265379
  if (!bucket.indicator || bucket.indicator.filter.length === 0)
265080
265380
  return [];
265081
265381
  const spec = {
265082
265382
  team,
265083
265383
  assignee,
265084
265384
  anyAssignee,
265385
+ requireAllLabels,
265085
265386
  include: bucket.indicator.filter,
265086
265387
  exclude: bucket.exclude,
265087
265388
  ...ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {}
265088
265389
  };
265089
265390
  return fetchOpenIssues(apiKey, spec);
265090
265391
  }
265091
- function resolveLinearFilter(filterOverride, assigneeOverride, configFilter) {
265092
- const effective = filterOverride || (assigneeOverride ? `assignee = ${assigneeOverride}` : "") || configFilter;
265093
- return parseLinearFilter(effective);
265094
- }
265095
265392
  function formatReviewCell(prUrl, count) {
265096
265393
  if (!prUrl)
265097
265394
  return "-";
@@ -265138,13 +265435,13 @@ function backlogRankByIssueId(issues) {
265138
265435
  ordered.forEach((o, i) => rankById.set(o.id, i));
265139
265436
  return rankById;
265140
265437
  }
265141
- 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 = []) {
265142
265439
  const bucketResults = await Promise.all(buckets.map(async (bucket) => {
265143
265440
  if (!bucket.indicator || bucket.indicator.filter.length === 0) {
265144
265441
  return { bucket, issues: [], error: null };
265145
265442
  }
265146
265443
  try {
265147
- const issues = await fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, ticketNumbers);
265444
+ const issues = await fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, requireAllLabels, ticketNumbers);
265148
265445
  return { bucket, issues, error: null };
265149
265446
  } catch (err) {
265150
265447
  return {
@@ -265270,7 +265567,6 @@ async function runList(input) {
265270
265567
  identifier: name,
265271
265568
  projectRoot,
265272
265569
  linearTeamOverride: input.linearTeamOverride,
265273
- linearFilterOverride: input.linearFilterOverride,
265274
265570
  linearAssigneeOverride: input.linearAssigneeOverride
265275
265571
  });
265276
265572
  return;
@@ -265281,7 +265577,7 @@ async function runList(input) {
265281
265577
  const apiKey = process.env["LINEAR_API_KEY"];
265282
265578
  const indicators = cfg.linear.indicators;
265283
265579
  const team = input.linearTeamOverride || cfg.linear.team;
265284
- const { assignee, anyAssignee } = resolveLinearFilter(input.linearFilterOverride, input.linearAssigneeOverride, cfg.linear.filter);
265580
+ const { assignee, anyAssignee, requireAllLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, input.linearAssigneeOverride));
265285
265581
  const buckets = buildBuckets(indicators);
265286
265582
  const anyConfigured = buckets.some((b2) => b2.indicator && b2.indicator.filter.length > 0);
265287
265583
  if (!anyConfigured) {
@@ -265321,7 +265617,7 @@ team: ${team}
265321
265617
  if (ticketNumbers.length > 0)
265322
265618
  process.stdout.write(`ticket: ${ticketNumbers.join(", ")}
265323
265619
  `);
265324
- 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);
265325
265621
  }
265326
265622
  function normalizeIdentifier(input) {
265327
265623
  let parsed;
@@ -265404,7 +265700,7 @@ async function runListDebug(input) {
265404
265700
  const cfg = await loadRalphyConfig(projectRoot, getArgs().workflowFile);
265405
265701
  const indicators = cfg.linear.indicators;
265406
265702
  const team = input.linearTeamOverride || cfg.linear.team;
265407
- const { assignee, anyAssignee } = resolveLinearFilter(input.linearFilterOverride, input.linearAssigneeOverride, cfg.linear.filter);
265703
+ const { assignee, anyAssignee, requireAllLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, input.linearAssigneeOverride));
265408
265704
  const assigneeLabel = anyAssignee ? "any" : assignee ?? "*";
265409
265705
  const normalized = normalizeIdentifier(identifier);
265410
265706
  if (!normalized) {
@@ -265448,6 +265744,13 @@ Per-bucket diagnostics:
265448
265744
  if (!assigneeMatches(issue2, assignee, anyAssignee)) {
265449
265745
  reasons.push(`assignee mismatch: issue=${issue2.assignee ? issue2.assignee.email ?? issue2.assignee.id : "unassigned"}, config=${assigneeLabel}`);
265450
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
+ }
265451
265754
  const includeMatches = bucket.indicator.filter.some((m2) => markerMatches(issue2, m2));
265452
265755
  if (!includeMatches) {
265453
265756
  const want = bucket.indicator.filter.map((m2) => `${m2.type}:${m2.value}`).join(" OR ");
@@ -265555,7 +265858,10 @@ async function runAgentJson({
265555
265858
  process.exitCode = 1;
265556
265859
  return;
265557
265860
  }
265558
- const pf = await runPreflight2();
265861
+ const pf = await runPreflight2({
265862
+ requireRepoWrite: args.createPr || cfg.createPrOnSuccess,
265863
+ repoCwd: projectRoot
265864
+ });
265559
265865
  if (!pf.ok) {
265560
265866
  emit3({ type: "error", code: "auth_failure", tool: pf.tool, text: pf.message });
265561
265867
  process.exitCode = 2;
@@ -265776,7 +266082,6 @@ async function main3(argv) {
265776
266082
  await runWithContext(createDefaultContext({ layout, args }), async () => {
265777
266083
  await runList2({
265778
266084
  linearTeamOverride: args.linearTeam,
265779
- linearFilterOverride: args.linearFilter,
265780
266085
  linearAssigneeOverride: args.linearAssignee,
265781
266086
  debug: args.debug,
265782
266087
  name: args.name,
@@ -265852,12 +266157,12 @@ async function main3(argv) {
265852
266157
  return 0;
265853
266158
  }
265854
266159
  await runWithContext(createDefaultContext({ layout, args }), async () => {
265855
- 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 }));
265856
266161
  await waitUntilExit();
265857
266162
  });
265858
266163
  return typeof process.exitCode === "number" ? process.exitCode : 0;
265859
266164
  }
265860
- var import_react63;
266165
+ var import_react64;
265861
266166
  var init_src8 = __esm(async () => {
265862
266167
  init_context();
265863
266168
  init_layout();
@@ -265868,7 +266173,7 @@ var init_src8 = __esm(async () => {
265868
266173
  init_build2(),
265869
266174
  init_AgentMode()
265870
266175
  ]);
265871
- import_react63 = __toESM(require_react(), 1);
266176
+ import_react64 = __toESM(require_react(), 1);
265872
266177
  });
265873
266178
 
265874
266179
  // apps/shell/src/index.ts
@@ -265957,11 +266262,21 @@ ${HELP}
265957
266262
  capture("command_run", { subcommand });
265958
266263
  bus.emit({ type: "command_run", subcommand });
265959
266264
  try {
265960
- if (shouldOfferSetup(subcommand, argv.slice(1))) {
266265
+ if (CONFIG_SUBCOMMANDS.has(subcommand)) {
265961
266266
  try {
265962
- const { maybeRunSetupWizard: maybeRunSetupWizard2 } = await init_src4().then(() => exports_src);
266267
+ const { maybeRunSetupWizard: maybeRunSetupWizard2, maybeUpgradeWorkflow: maybeUpgradeWorkflow2 } = await init_src4().then(() => exports_src);
265963
266268
  const { projectRoot, workflowFile } = parseWorkflowPathArgs(argv.slice(1));
265964
- 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
+ }
265965
266280
  } catch (setupErr) {
265966
266281
  captureError("setup_wizard_error", setupErr, { subcommand });
265967
266282
  }