@neriros/ralphy 3.10.14 → 3.10.16

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