@neriros/ralphy 3.10.15 → 3.10.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/shell/index.js +755 -93
  2. package/package.json +2 -2
@@ -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.15")
18932
- return "3.10.15";
18931
+ if ("3.10.17")
18932
+ return "3.10.17";
18933
18933
  } catch {}
18934
18934
  const dirsToTry = [];
18935
18935
  try {
@@ -19395,7 +19395,7 @@ function modelOptionValues() {
19395
19395
  const field = findField("model");
19396
19396
  return field && field.spec.kind === "select" ? field.spec.options.map((o) => o.value) : [];
19397
19397
  }
19398
- var PROMPT_BODY_FIELD_ID = "promptBody", REPO_LINK_FIELD_ID = "repo.link", yes = () => ({ kind: "confirm", defaultChoice: "confirm" }), no = () => ({ kind: "confirm", defaultChoice: "cancel" }), PROJECT_NAME, LINEAR_TEAM, REPO_LINK, LINEAR_ASSIGNEE_CHOICE_FIELD_ID = "linear.assigneeChoice", LINEAR_ASSIGNEE_VALUE_FIELD_ID = "linear.assigneeValue", LINEAR_FILTER_DESCRIPTION, LINEAR_ASSIGNEE_CHOICE, LINEAR_ASSIGNEE_VALUE, QUICK_FIELDS, isOn = (id) => (answers) => answers[id] === true, concurrencyForcesWorktree = (answers) => {
19398
+ var PROMPT_BODY_FIELD_ID = "promptBody", REPO_LINK_FIELD_ID = "repo.link", yes = () => ({ kind: "confirm", defaultChoice: "confirm" }), no = () => ({ kind: "confirm", defaultChoice: "cancel" }), PROJECT_NAME, LINEAR_TEAM, REPO_LINK, LINEAR_ASSIGNEE_CHOICE_FIELD_ID = "linear.assigneeChoice", LINEAR_ASSIGNEE_VALUE_FIELD_ID = "linear.assigneeValue", LINEAR_FILTER_DESCRIPTION, LINEAR_ASSIGNEE_CHOICE, LINEAR_ASSIGNEE_VALUE, QUICK_FIELDS, isOn = (id) => (answers) => answers[id] === true, trackerIsGithub = (answers) => answers["tracker.kind"] === "github", concurrencyForcesWorktree = (answers) => {
19399
19399
  const value = answers["concurrency"];
19400
19400
  return typeof value === "number" && value > 1;
19401
19401
  }, worktreeEnabled = (answers) => answers["useWorktree"] === true || concurrencyForcesWorktree(answers), HIDDEN_FIELD_IDS, CUSTOMIZED_FIELDS, COMMON_CLI_OPTIONS, FIELD_DESCRIPTIONS;
@@ -19537,6 +19537,66 @@ var init_fields = __esm(() => {
19537
19537
  description: "Show detailed per-task output (passes --verbose to the task sub-process) for extra diagnostics.",
19538
19538
  spec: no()
19539
19539
  },
19540
+ {
19541
+ id: "tracker.kind",
19542
+ label: "Issue tracker",
19543
+ description: "Which issue tracker drives the loop: 'linear' (the default) or 'github' (GitHub Issues, via the gh CLI).",
19544
+ spec: {
19545
+ kind: "select",
19546
+ options: [
19547
+ { label: "Linear", value: "linear" },
19548
+ { label: "GitHub", value: "github" }
19549
+ ]
19550
+ }
19551
+ },
19552
+ {
19553
+ id: "github.issues.repo",
19554
+ label: "GitHub repository (owner/name)",
19555
+ hint: "blank = detected from origin",
19556
+ description: "The owner/name of the GitHub repo whose issues drive the loop. Leave blank to use the repo detected from the git 'origin' remote.",
19557
+ emptyLabel: "detected from origin",
19558
+ spec: { kind: "text", placeholder: "owner/name" },
19559
+ when: trackerIsGithub
19560
+ },
19561
+ {
19562
+ id: "github.issues.label",
19563
+ label: "Todo label",
19564
+ hint: "blank = any open issue",
19565
+ description: "Only pick up GitHub issues carrying this label. Leave blank to consider every open issue.",
19566
+ emptyLabel: "any open issue",
19567
+ spec: { kind: "text", placeholder: "ralph:todo" },
19568
+ when: trackerIsGithub
19569
+ },
19570
+ {
19571
+ id: "github.issues.assignee",
19572
+ label: "Assignee filter",
19573
+ hint: "blank = any assignee; @me = you",
19574
+ description: "Only pick up GitHub issues assigned to this login. Use '@me' for yourself; leave blank to ignore the assignee.",
19575
+ emptyLabel: "any assignee",
19576
+ spec: { kind: "text", placeholder: "@me" },
19577
+ when: trackerIsGithub
19578
+ },
19579
+ {
19580
+ id: "github.issues.statusLabels.inProgress",
19581
+ label: "In-progress label",
19582
+ description: "GitHub label applied to an issue while Ralphy is actively working on it (and removed from the todo label).",
19583
+ spec: { kind: "text", placeholder: "ralph:in-progress" },
19584
+ when: trackerIsGithub
19585
+ },
19586
+ {
19587
+ id: "github.issues.statusLabels.done",
19588
+ label: "Done label",
19589
+ description: "GitHub label applied to an issue when its work completes; the issue is also closed.",
19590
+ spec: { kind: "text", placeholder: "ralph:done" },
19591
+ when: trackerIsGithub
19592
+ },
19593
+ {
19594
+ id: "github.issues.statusLabels.error",
19595
+ label: "Error label",
19596
+ description: "GitHub label applied to an issue when Ralphy quarantines it after repeated failures.",
19597
+ spec: { kind: "text", placeholder: "ralph:error" },
19598
+ when: trackerIsGithub
19599
+ },
19540
19600
  {
19541
19601
  id: "concurrency",
19542
19602
  label: "Concurrency (parallel tasks)",
@@ -81255,7 +81315,7 @@ function foldLegacyAssignee(v) {
81255
81315
  }
81256
81316
  return rest2;
81257
81317
  }
81258
- var CURRENT_WORKFLOW_VERSION = 6, MarkerSchema, FilterMarkerSchema, LinearFilterSchema, SET_INDICATOR_KEYS, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, DEFAULT_META_ONLY_FILES, BoundariesSchema, WorkflowConfigSchema;
81318
+ var CURRENT_WORKFLOW_VERSION = 7, MarkerSchema, FilterMarkerSchema, LinearFilterSchema, SET_INDICATOR_KEYS, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, DEFAULT_META_ONLY_FILES, BoundariesSchema, WorkflowConfigSchema;
81259
81319
  var init_schema = __esm(() => {
81260
81320
  init_zod();
81261
81321
  MarkerSchema = exports_external.discriminatedUnion("type", [
@@ -81401,6 +81461,9 @@ var init_schema = __esm(() => {
81401
81461
  finalizeNoOpAsDone: exports_external.boolean().default(true),
81402
81462
  engine: exports_external.enum(["claude", "codex"]).default("claude"),
81403
81463
  model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
81464
+ tracker: exports_external.object({
81465
+ kind: exports_external.enum(["linear", "github"]).default("linear")
81466
+ }).strict().default({ kind: "linear" }),
81404
81467
  linear: exports_external.preprocess(foldLegacyAssignee, exports_external.object({
81405
81468
  team: exports_external.string().optional(),
81406
81469
  filter: LinearFilterSchema,
@@ -81445,7 +81508,21 @@ var init_schema = __esm(() => {
81445
81508
  }),
81446
81509
  github: exports_external.object({
81447
81510
  base_branch: exports_external.string().optional(),
81448
- auto_merge_strategy: exports_external.enum(["squash", "merge", "rebase"]).optional()
81511
+ auto_merge_strategy: exports_external.enum(["squash", "merge", "rebase"]).optional(),
81512
+ issues: exports_external.object({
81513
+ repo: exports_external.string().optional(),
81514
+ label: exports_external.string().optional(),
81515
+ assignee: exports_external.string().optional(),
81516
+ statusLabels: exports_external.object({
81517
+ inProgress: exports_external.string().default("ralph:in-progress"),
81518
+ done: exports_external.string().default("ralph:done"),
81519
+ error: exports_external.string().default("ralph:error")
81520
+ }).strict().default({
81521
+ inProgress: "ralph:in-progress",
81522
+ done: "ralph:done",
81523
+ error: "ralph:error"
81524
+ })
81525
+ }).strict().optional()
81449
81526
  }).strict().optional(),
81450
81527
  agent: exports_external.object({
81451
81528
  engine: exports_external.enum(["claude", "codex"]).optional(),
@@ -81514,7 +81591,7 @@ var init_schema = __esm(() => {
81514
81591
  var FRONTMATTER_RE, DEFAULT_WORKFLOW_MD = `---
81515
81592
  # WORKFLOW.md schema version \u2014 managed by \`ralphy init\`. When a newer version
81516
81593
  # ships, re-running init migrates this file and fills in the new settings.
81517
- version: 6
81594
+ version: 7
81518
81595
 
81519
81596
  project:
81520
81597
  name: ralphy
@@ -81581,6 +81658,16 @@ preExistingErrorCheck:
81581
81658
  label: "ralph:pre-existing-error"
81582
81659
  outputCharLimit: 4000
81583
81660
 
81661
+ tracker:
81662
+ kind: linear
81663
+
81664
+ github:
81665
+ issues:
81666
+ statusLabels:
81667
+ inProgress: "ralph:in-progress"
81668
+ done: "ralph:done"
81669
+ error: "ralph:error"
81670
+
81584
81671
  linear:
81585
81672
  # Global filter ANDed into every Linear query (and the GitHub PR searches
81586
81673
  # rooted at those issues). A marker list of \`assignee\` and \`label\` clauses;
@@ -83405,6 +83492,12 @@ function buildFromAnswers(mode, answers, build = buildWorkflowMarkdown) {
83405
83492
  }
83406
83493
  }
83407
83494
  }
83495
+ if (values2["tracker.kind"] !== "github") {
83496
+ for (const id of Object.keys(values2)) {
83497
+ if (id.startsWith("github.issues."))
83498
+ delete values2[id];
83499
+ }
83500
+ }
83408
83501
  const linkRepo = values2[REPO_LINK_FIELD_ID] === true;
83409
83502
  delete values2[REPO_LINK_FIELD_ID];
83410
83503
  if (!linkRepo) {
@@ -84585,6 +84678,19 @@ var init_migrations = __esm(() => {
84585
84678
  version: 6,
84586
84679
  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
84680
  fields: ["prRecovery.enabled", "prRecovery.maxRecoverySessions", "prRecovery.ignoreChecks"]
84681
+ },
84682
+ {
84683
+ version: 7,
84684
+ description: "Pick your issue tracker: Linear (default) or GitHub Issues. The new " + "`tracker.kind` switch selects the provider; choosing GitHub drives the " + "loop off `gh` issues, filtered by `github.issues.label`/`assignee` and " + "moved through the `github.issues.statusLabels` (in-progress / done / " + "error). Existing files default to Linear, so nothing changes unless you " + "switch.",
84685
+ fields: [
84686
+ "tracker.kind",
84687
+ "github.issues.repo",
84688
+ "github.issues.label",
84689
+ "github.issues.assignee",
84690
+ "github.issues.statusLabels.inProgress",
84691
+ "github.issues.statusLabels.done",
84692
+ "github.issues.statusLabels.error"
84693
+ ]
84588
84694
  }
84589
84695
  ];
84590
84696
  LATEST_MIGRATION_VERSION = MIGRATIONS.reduce((max2, migration) => Math.max(max2, migration.version), 0);
@@ -100164,7 +100270,7 @@ function normalizeNewlyAppendedSectionWithReport(previous, current) {
100164
100270
  });
100165
100271
  return { text: count > 0 ? out.join("") : current, headings, count };
100166
100272
  }
100167
- var MISSION_TASKS_FILENAME = "tasks.md", AGENT_TASKS_FILENAME = "agent-tasks.md", FLOW_TASK_HEADING_PREFIXES;
100273
+ var MISSION_TASKS_FILENAME = "tasks.md", AGENT_TASKS_FILENAME = "agent-tasks.md", HANDOFF_FILENAME = "handoff.md", FLOW_TASK_HEADING_PREFIXES;
100168
100274
  var init_tasks_md = __esm(() => {
100169
100275
  FLOW_TASK_HEADING_PREFIXES = [
100170
100276
  "Fix failing CI checks",
@@ -100417,6 +100523,20 @@ function buildTaskPrompt(state, taskDir, reviewPhase) {
100417
100523
  `;
100418
100524
  }
100419
100525
  }
100526
+ const handoffContent = storage.read(join15(taskDir, HANDOFF_FILENAME));
100527
+ if (handoffContent !== null && handoffContent.trim()) {
100528
+ prompt += `---
100529
+ `;
100530
+ prompt += `# Previous Iteration Handoff (context from the last iteration)
100531
+
100532
+ `;
100533
+ prompt += handoffContent.trim() + `
100534
+
100535
+ `;
100536
+ prompt += `---
100537
+
100538
+ `;
100539
+ }
100420
100540
  const agentTasksPath = join15(taskDir, AGENT_TASKS_FILENAME);
100421
100541
  const missionTasksPath = join15(taskDir, MISSION_TASKS_FILENAME);
100422
100542
  const agentTasksContent = storage.read(agentTasksPath);
@@ -100564,6 +100684,22 @@ When all tasks are complete and all files are committed, push your branch and op
100564
100684
  prompt += `Use the change name as the PR title and write a concise summary of the implementation in the body.
100565
100685
  `;
100566
100686
  }
100687
+ const handoffPath = join15(taskDir, HANDOFF_FILENAME);
100688
+ prompt += `
100689
+ ---
100690
+
100691
+ ## Write Handoff (do this LAST, before you finish)
100692
+ `;
100693
+ prompt += `Before ending this iteration, record a handoff for the next iteration:
100694
+ `;
100695
+ prompt += `- If a \`handoff\` skill is available, invoke it; otherwise write the document yourself.
100696
+ `;
100697
+ prompt += `- Save it to \`${handoffPath}\` (OVERWRITE the existing file \u2014 do NOT append, do NOT save to a temp dir).
100698
+ `;
100699
+ prompt += `- Keep it compact: what you did this iteration, what remains, key decisions, and any blockers/gotchas. Reference artifacts by path instead of duplicating them.
100700
+ `;
100701
+ prompt += `- This file is loop scratch \u2014 do NOT commit it.
100702
+ `;
100567
100703
  return prompt;
100568
100704
  }
100569
100705
  function buildSteeringBlock(taskDir) {
@@ -102600,13 +102736,91 @@ class PollContext {
102600
102736
  }
102601
102737
  }
102602
102738
 
102603
- // apps/agent/src/shared/utils/ralph-comment.ts
102604
- function isRalphComment(body) {
102739
+ // packages/comms/src/index.ts
102740
+ function sanitizeMarkerValue(value) {
102741
+ return value.replace(/--+>?/g, "-").replace(/\s+/g, "_").trim();
102742
+ }
102743
+ function buildRalphyMarker(type, fields) {
102744
+ const pairs = [`v=${RALPHY_MARKER_VERSION}`, `type=${type}`];
102745
+ for (const [key, raw] of Object.entries(fields ?? {})) {
102746
+ if (raw === undefined || raw === null || raw === "")
102747
+ continue;
102748
+ pairs.push(`${key}=${sanitizeMarkerValue(String(raw))}`);
102749
+ }
102750
+ return `<!-- ralphy:${pairs.join(" ")} -->`;
102751
+ }
102752
+ function parseRalphyMarker(body) {
102753
+ const match = /<!--\s*ralphy:([^>]*?\btype=[^>]*?)\s*-->/.exec(body);
102754
+ if (!match)
102755
+ return null;
102756
+ const fields = {};
102757
+ let type = "";
102758
+ let version3 = 0;
102759
+ for (const token of match[1].trim().split(/\s+/)) {
102760
+ const eq = token.indexOf("=");
102761
+ if (eq < 0)
102762
+ continue;
102763
+ const key = token.slice(0, eq);
102764
+ const value = token.slice(eq + 1);
102765
+ if (key === "v")
102766
+ version3 = Number(value) || 0;
102767
+ else if (key === "type")
102768
+ type = value;
102769
+ else
102770
+ fields[key] = value;
102771
+ }
102772
+ if (!type)
102773
+ return null;
102774
+ return { version: version3, type, fields };
102775
+ }
102776
+ function buildRalphyComment(input) {
102777
+ const lines = [`${RALPHY_TITLE_PREFIX}${input.action}`];
102778
+ const body = input.body?.trim();
102779
+ if (body)
102780
+ lines.push("", body);
102781
+ lines.push("", buildRalphyMarker(input.type, input.fields));
102782
+ return lines.join(`
102783
+ `);
102784
+ }
102785
+ function isRalphyComment(body) {
102605
102786
  const trimmed = body.trimStart();
102606
- if (/^(\uD83E\uDD16|\uD83D\uDD04|\u2705|\u2717|\u274C|\u26A0|\uD83D\uDD01|\uD83D\uDCCB|\u23F0)\s*Ralphy?\b/.test(trimmed))
102787
+ if (trimmed.startsWith(RALPHY_BRAND))
102788
+ return true;
102789
+ if (parseRalphyMarker(body))
102790
+ return true;
102791
+ if (LEGACY_RALPH_LEAD.test(trimmed))
102607
102792
  return true;
102608
- return /^\uD83D\uDC40\s*(Got it\b|Acknowledged\b)/.test(trimmed);
102793
+ return LEGACY_MENTION_ACK.test(trimmed);
102609
102794
  }
102795
+ function isPickupComment(body) {
102796
+ if (parseRalphyMarker(body)?.type === "review-pickup")
102797
+ return true;
102798
+ return /^\uD83D\uDD01\s*Ralph picked up/u.test(body.trimStart());
102799
+ }
102800
+ function isStartedComment(body) {
102801
+ if (parseRalphyMarker(body)?.type === "started")
102802
+ return true;
102803
+ return /^\uD83E\uDD16\s*Ralph started working/u.test(body.trimStart());
102804
+ }
102805
+ function isMentionAckComment(body) {
102806
+ if (parseRalphyMarker(body)?.type === "mention-ack")
102807
+ return true;
102808
+ return LEGACY_MENTION_ACK.test(body.trimStart());
102809
+ }
102810
+ var RALPHY_BRAND = "\uD83E\uDD16 Ralphy", RALPHY_TITLE_PREFIX, RALPHY_MARKER_VERSION = 1, LEGACY_RALPH_LEAD, LEGACY_MENTION_ACK;
102811
+ var init_src8 = __esm(() => {
102812
+ RALPHY_TITLE_PREFIX = `${RALPHY_BRAND} \xB7 `;
102813
+ LEGACY_RALPH_LEAD = /^(\uD83E\uDD16|\uD83D\uDD04|\u2705|\u2717|\u274C|\u26A0\uFE0F?|\uD83D\uDD01|\uD83D\uDCCB|\u23F0|\u2139\uFE0F)\s*Ralphy?\b/u;
102814
+ LEGACY_MENTION_ACK = /^\uD83D\uDC40\s*(Got it\b|Acknowledged\b)/u;
102815
+ });
102816
+
102817
+ // apps/agent/src/shared/utils/ralph-comment.ts
102818
+ function isRalphComment(body) {
102819
+ return isRalphyComment(body);
102820
+ }
102821
+ var init_ralph_comment = __esm(() => {
102822
+ init_src8();
102823
+ });
102610
102824
 
102611
102825
  // apps/agent/src/shared/capabilities/linear-client.ts
102612
102826
  var exports_linear_client = {};
@@ -103543,6 +103757,7 @@ async function removeLabelFromIssue(apiKey, issueId, labelId) {
103543
103757
  var LINEAR_API = "https://api.linear.app/graphql", TICKET_IDENTIFIER_RE, TICKET_BARE_NUMBER_RE, RALPHY_ATTACHMENT_TITLE_FILTER = "Ralphy", linearRequestInternals, MAX_LINEAR_ATTEMPTS = 3, MAX_RETRY_AFTER_MS = 2000, BODY_TRUNCATE_CHARS = 512, RALPHY_ATTACHMENT_TITLE = "Ralphy", BRANCH_LABEL_PREFIX = "ralph:branch:";
103544
103758
  var init_linear_client = __esm(() => {
103545
103759
  init_types2();
103760
+ init_ralph_comment();
103546
103761
  TICKET_IDENTIFIER_RE = /^([A-Za-z]+)-(\d+)(?:-.*)?$/;
103547
103762
  TICKET_BARE_NUMBER_RE = /^(\d+)$/;
103548
103763
  linearRequestInternals = {
@@ -103885,7 +104100,7 @@ function stackedOnLine(stackedOn) {
103885
104100
  return `> \uD83E\uDD5E Stacked on ${prRef}${ticket} \u2014 review/merge that PR first. Base is its branch, not the default branch.`;
103886
104101
  }
103887
104102
  function defaultBody(issue2, branch, stackedOn) {
103888
- return [
104103
+ const detail = [
103889
104104
  `Auto-generated by Ralph for ${issue2.identifier}.`,
103890
104105
  stackedOn ? stackedOnLine(stackedOn) : "",
103891
104106
  `Source: ${issue2.url}`,
@@ -103896,6 +104111,12 @@ function defaultBody(issue2, branch, stackedOn) {
103896
104111
  ${issue2.description.trim()}` : ""
103897
104112
  ].filter(Boolean).join(`
103898
104113
  `);
104114
+ return buildRalphyComment({
104115
+ type: "pr-body",
104116
+ action: "opened PR",
104117
+ body: detail,
104118
+ fields: { issue: issue2.identifier }
104119
+ });
103899
104120
  }
103900
104121
  async function diffFilesAgainstBase(runner, cwd2, base2) {
103901
104122
  let raw = "";
@@ -104015,7 +104236,9 @@ async function createPullRequest(input, runner) {
104015
104236
  `).pop() ?? "";
104016
104237
  return { url: url2, created: true };
104017
104238
  }
104018
- var init_pr = () => {};
104239
+ var init_pr = __esm(() => {
104240
+ init_src8();
104241
+ });
104019
104242
 
104020
104243
  // apps/agent/src/shared/pr/ci-classify.ts
104021
104244
  async function runGhWithRetry(cmd, runner, cwd2, onRetry, sleep2 = (ms) => new Promise((r) => setTimeout(r, ms))) {
@@ -105126,6 +105349,25 @@ async function runPostTask(input, deps) {
105126
105349
  }
105127
105350
  if (input.mode === "conflict-fix" && effectiveCode === 0) {
105128
105351
  const identifier = issue2?.identifier ?? changeName;
105352
+ if (branch) {
105353
+ let aheadCount = 0;
105354
+ let checked = true;
105355
+ try {
105356
+ const r = await cmd.run(["git", "rev-list", "--count", `origin/${branch}..HEAD`], cwd2);
105357
+ aheadCount = Number.parseInt(r.stdout.trim(), 10) || 0;
105358
+ } catch (err) {
105359
+ checked = false;
105360
+ log3(`! ${identifier}: could not check for unpushed conflict-fix commits: ${err.message}`, "yellow");
105361
+ }
105362
+ if (checked && aheadCount > 0) {
105363
+ log3(`! ${identifier}: conflict-fix worker left ${aheadCount} unpushed commit(s) ahead of ` + `origin/${branch} \u2014 the resolution never reached the PR. Failing the iteration so it ` + `is retried instead of reported as resolved.`, "red");
105364
+ emit3("gave-up", "unpushed conflict resolution");
105365
+ await recordGaveUp(stateFilePath, log3, changeName);
105366
+ await runWorktreeCleanupPhase({ changeName, cwd: cwd2, projectRoot, useWorktree, effectiveCode: PR_FAILED_EXIT, cfg }, { git: git2, log: log3, emit: emit3 });
105367
+ await runTeardownPhase({ cwd: cwd2, teardownScript: cfg.teardownScript }, { runScript, log: log3, emit: emit3 });
105368
+ return PR_FAILED_EXIT;
105369
+ }
105370
+ }
105129
105371
  let prUrl = input.prUrl ?? null;
105130
105372
  if (!prUrl && branch) {
105131
105373
  prUrl = await findExistingOpenPrUrl(cmd, cwd2, branch);
@@ -105500,19 +105742,37 @@ function emitCapture(bus, event, properties) {
105500
105742
  function completionCommentBody(args) {
105501
105743
  const { noChanges, ok, trigger, changeName, code } = args;
105502
105744
  if (noChanges) {
105503
- return `\u2139\uFE0F Ralph completed all tasks for this issue but produced no code changes \u2014 ` + `the requested work appears to already be present on the base branch (or was a ` + `no-op). No PR was opened. Change: \`${changeName}\`
105745
+ return buildRalphyComment({
105746
+ type: "completed-noop",
105747
+ action: "completed \u2014 no code changes",
105748
+ body: `Completed all tasks for this issue but produced no code changes \u2014 the requested ` + `work appears to already be present on the base branch (or was a no-op). No PR was ` + `opened. Change: \`${changeName}\`
105504
105749
 
105505
- ` + `Marking this done; please verify the work is genuinely in place. If it is not, ` + `reopen the issue with more specifics.`;
105750
+ ` + `Marking this done; please verify the work is genuinely in place. If it is not, ` + `reopen the issue with more specifics.`,
105751
+ fields: { change: changeName }
105752
+ });
105506
105753
  }
105507
105754
  if (!ok) {
105508
- return `\u2717 Ralph exited with code ${code} on this issue. Change: \`${changeName}\`
105509
-
105510
- ` + `This issue has been quarantined and will not be auto-resumed on the next poll. ` + `Inspect the worktree at \`~/.ralph/<project>/worktrees/${changeName}\`, fix the ` + `underlying failure, then remove the error marker on this Linear issue (or run ` + `\`ralph clean --name ${changeName}\`) to clear the quarantine.`;
105755
+ return buildRalphyComment({
105756
+ type: "exited",
105757
+ action: `exited with code ${code}`,
105758
+ body: `This issue has been quarantined and will not be auto-resumed on the next poll. ` + `Inspect the worktree at \`~/.ralph/<project>/worktrees/${changeName}\`, fix the ` + `underlying failure, then remove the error marker on this Linear issue (or run ` + `\`ralph clean --name ${changeName}\`) to clear the quarantine. Change: \`${changeName}\``,
105759
+ fields: { change: changeName, code }
105760
+ });
105511
105761
  }
105512
105762
  if (trigger === "conflict-fix") {
105513
- return `\u2705 Ralph resolved merge conflicts on this issue. Change: \`${changeName}\``;
105763
+ return buildRalphyComment({
105764
+ type: "conflicts-resolved",
105765
+ action: "resolved merge conflicts",
105766
+ body: `Change: \`${changeName}\``,
105767
+ fields: { change: changeName }
105768
+ });
105514
105769
  }
105515
- return `\u2705 Ralph completed work on this issue. Change: \`${changeName}\``;
105770
+ return buildRalphyComment({
105771
+ type: "completed",
105772
+ action: "completed work",
105773
+ body: `Change: \`${changeName}\``,
105774
+ fields: { change: changeName }
105775
+ });
105516
105776
  }
105517
105777
  function extractPrNumber(url2) {
105518
105778
  const m = /\/pull\/(\d+)(?:[/?#]|$)/.exec(url2);
@@ -105811,7 +106071,12 @@ class AgentCoordinator {
105811
106071
  const prNum = extractPrNumber(pr.url);
105812
106072
  const ref = prNum !== null ? `PR #${prNum}` : `PR ${pr.url}`;
105813
106073
  try {
105814
- await this.deps.postComment(issue2, `\u26A0\uFE0F ${ref} is ${stateLabel} \u2014 promoted to ${trigger} flow.`);
106074
+ await this.deps.postComment(issue2, buildRalphyComment({
106075
+ type: "promoted",
106076
+ action: `promoted to ${trigger} flow`,
106077
+ body: `${ref} is ${stateLabel} \u2014 promoted to ${trigger} flow.`,
106078
+ fields: { trigger, pr: extractPrNumber(pr.url) ?? pr.url }
106079
+ }));
105815
106080
  this.deps.onLog(` ${issue2.identifier}: posted ${trigger}-promotion comment`, "gray");
105816
106081
  } catch (err) {
105817
106082
  this.deps.onLog(`! Linear ${trigger}-promotion comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
@@ -105864,7 +106129,12 @@ class AgentCoordinator {
105864
106129
  if (currMilestone <= lastMilestone)
105865
106130
  continue;
105866
106131
  try {
105867
- await this.deps.postComment(w.issue, `\uD83D\uDD04 Ralph progress update: iteration ${count} on \`${w.changeName}\``);
106132
+ await this.deps.postComment(w.issue, buildRalphyComment({
106133
+ type: "progress",
106134
+ action: `progress update \u2014 iteration ${count}`,
106135
+ body: `Iteration ${count} on \`${w.changeName}\``,
106136
+ fields: { change: w.changeName, iter: count }
106137
+ }));
105868
106138
  w.lastReportedIteration = count;
105869
106139
  this.deps.onLog(` ${w.issueIdentifier}: posted progress comment (iteration ${count})`, "gray");
105870
106140
  } catch (err) {
@@ -106004,7 +106274,12 @@ class AgentCoordinator {
106004
106274
  this.deps.onLog(` ${issue2.identifier}: PR ${pr.url} conflicting \u2014 queued (conflict-fix)`, "yellow");
106005
106275
  if (this.opts.postComments !== false) {
106006
106276
  try {
106007
- await this.deps.postComment(issue2, `\u26A0 Ralph detected merge conflicts on this PR (${pr.url}) \u2014 re-running to resolve`);
106277
+ await this.deps.postComment(issue2, buildRalphyComment({
106278
+ type: "conflict-detected",
106279
+ action: "detected merge conflicts",
106280
+ body: `Detected merge conflicts on this PR (${pr.url}) \u2014 re-running to resolve.`,
106281
+ fields: { pr: extractPrNumber(pr.url) ?? pr.url }
106282
+ }));
106008
106283
  } catch (err) {
106009
106284
  this.deps.onLog(`! Linear conflict comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106010
106285
  }
@@ -106039,7 +106314,12 @@ class AgentCoordinator {
106039
106314
  this.deps.onLog(` ${issue2.identifier}: PR ${pr.url} CI failing \u2014 queued (ci-fix)`, "yellow");
106040
106315
  if (this.opts.postComments !== false) {
106041
106316
  try {
106042
- await this.deps.postComment(issue2, `\u26A0 Ralph detected failing CI on this PR (${pr.url}) \u2014 re-running to fix`);
106317
+ await this.deps.postComment(issue2, buildRalphyComment({
106318
+ type: "ci-failed",
106319
+ action: "detected failing CI",
106320
+ body: `Detected failing CI on this PR (${pr.url}) \u2014 re-running to fix.`,
106321
+ fields: { pr: extractPrNumber(pr.url) ?? pr.url }
106322
+ }));
106043
106323
  } catch (err) {
106044
106324
  this.deps.onLog(`! Linear ci-failed comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106045
106325
  }
@@ -106105,7 +106385,12 @@ class AgentCoordinator {
106105
106385
  }
106106
106386
  if (this.opts.postComments !== false) {
106107
106387
  try {
106108
- await this.deps.postComment(issue2, `\u2705 Ralph verified this PR (${prUrl}) is mergeable (CI green, no conflicts) \u2014 moving to done`);
106388
+ await this.deps.postComment(issue2, buildRalphyComment({
106389
+ type: "verified",
106390
+ action: "verified PR mergeable",
106391
+ body: `Verified this PR (${prUrl}) is mergeable (CI green, no conflicts) \u2014 moving to done.`,
106392
+ fields: { pr: extractPrNumber(prUrl) ?? prUrl }
106393
+ }));
106109
106394
  } catch (err) {
106110
106395
  this.deps.onLog(`! Linear done comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106111
106396
  }
@@ -106151,7 +106436,12 @@ class AgentCoordinator {
106151
106436
  if (this.opts.postComments !== false) {
106152
106437
  const human = reason === "conflicting" ? "merge conflicts" : "failing CI";
106153
106438
  try {
106154
- await this.deps.postComment(issue2, `\u274C Ralph gave up auto-recovering this PR (${prUrl}) after ${decision.attempts} attempts \u2014 last failure: ${human}. The \`ralph:error\` label has been applied; clear it (or merge the PR) once a human has looked at it.`);
106439
+ await this.deps.postComment(issue2, buildRalphyComment({
106440
+ type: "recovery-gaveup",
106441
+ action: "gave up auto-recovering PR",
106442
+ body: `Gave up auto-recovering this PR (${prUrl}) after ${decision.attempts} attempts \u2014 last failure: ${human}. The \`ralph:error\` label has been applied; clear it (or merge the PR) once a human has looked at it.`,
106443
+ fields: { pr: extractPrNumber(prUrl) ?? prUrl, attempts: decision.attempts }
106444
+ }));
106155
106445
  } catch (err) {
106156
106446
  this.deps.onLog(`! Linear bail comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106157
106447
  }
@@ -106238,7 +106528,12 @@ class AgentCoordinator {
106238
106528
  if (trigger === "review" && this.opts.postComments !== false) {
106239
106529
  const sourceTag = mention ? mention.source === "github" ? " (GitHub @mention)" : mention.source === "github-review" ? " (GitHub code review)" : " (Linear @mention)" : "";
106240
106530
  try {
106241
- await this.deps.postComment(issue2, `\uD83D\uDD01 Ralph picked up new review comments${sourceTag}. Tracking change: \`${prep.changeName}\``);
106531
+ await this.deps.postComment(issue2, buildRalphyComment({
106532
+ type: "review-pickup",
106533
+ action: "picked up review comments",
106534
+ body: `Picked up new review comments${sourceTag}. Tracking change: \`${prep.changeName}\``,
106535
+ fields: { change: prep.changeName }
106536
+ }));
106242
106537
  } catch (err) {
106243
106538
  this.deps.onLog(`! Linear review comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106244
106539
  }
@@ -106247,13 +106542,18 @@ class AgentCoordinator {
106247
106542
  let alreadyPosted = false;
106248
106543
  try {
106249
106544
  const comments = await this.deps.fetchComments(issue2.id);
106250
- alreadyPosted = comments.some((c) => c.body.startsWith("\uD83E\uDD16 Ralph started working"));
106545
+ alreadyPosted = comments.some((c) => isStartedComment(c.body));
106251
106546
  } catch (err) {
106252
106547
  this.deps.onLog(`! Linear comment fetch failed for ${issue2.identifier}: ${err.message}`, "yellow");
106253
106548
  }
106254
106549
  if (!alreadyPosted) {
106255
106550
  try {
106256
- await this.deps.postComment(issue2, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${prep.changeName}\``);
106551
+ await this.deps.postComment(issue2, buildRalphyComment({
106552
+ type: "started",
106553
+ action: "started working",
106554
+ body: `Tracking change: \`${prep.changeName}\``,
106555
+ fields: { change: prep.changeName }
106556
+ }));
106257
106557
  this.deps.onLog(` ${issue2.identifier}: posted "started" comment`, "gray");
106258
106558
  } catch (err) {
106259
106559
  this.deps.onLog(`! Linear comment failed for ${issue2.identifier}: ${err.message}`, "red");
@@ -106568,6 +106868,7 @@ var init_coordinator = __esm(() => {
106568
106868
  init_registry();
106569
106869
  init_run_feature();
106570
106870
  init_machines();
106871
+ init_src8();
106571
106872
  });
106572
106873
 
106573
106874
  // apps/agent/src/agent/coordinator.ts
@@ -106575,11 +106876,38 @@ var init_coordinator2 = __esm(() => {
106575
106876
  init_coordinator();
106576
106877
  });
106577
106878
 
106879
+ // apps/agent/src/shared/capabilities/gh-client.ts
106880
+ var init_gh_client = () => {};
106881
+
106882
+ // apps/agent/src/shared/capabilities/github/github-client.ts
106883
+ var STARTED_LABEL_NAMES, ISSUE_FIELDS = "id,number,title,body,state,stateReason,labels,assignees,author,createdAt,url", ISSUE_FIELDS_WITH_COMMENTS;
106884
+ var init_github_client = __esm(() => {
106885
+ init_worktree();
106886
+ init_gh_client();
106887
+ STARTED_LABEL_NAMES = new Set(["in progress", "in-progress", "started"]);
106888
+ ISSUE_FIELDS_WITH_COMMENTS = `${ISSUE_FIELDS},comments`;
106889
+ });
106890
+
106891
+ // apps/agent/src/shared/capabilities/github/identifier-strategy.ts
106892
+ function linearChangeName(issue2) {
106893
+ const slug = issue2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40).replace(/^-+|-+$/g, "");
106894
+ return slug ? `${issue2.identifier.toLowerCase()}-${slug}` : issue2.identifier.toLowerCase();
106895
+ }
106896
+ var linearIdentifierStrategy;
106897
+ var init_identifier_strategy = __esm(() => {
106898
+ init_worktree();
106899
+ init_github_client();
106900
+ linearIdentifierStrategy = {
106901
+ scopeKey: (issue2) => issue2.identifier.split("-")[0],
106902
+ changeName: linearChangeName,
106903
+ branchName: (issue2) => branchForChange(linearChangeName(issue2))
106904
+ };
106905
+ });
106906
+
106578
106907
  // apps/agent/src/agent/scaffold.ts
106579
106908
  import { join as join24 } from "path";
106580
106909
  function changeNameForIssue(issue2) {
106581
- const slug = issue2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40).replace(/^-+|-+$/g, "");
106582
- return slug ? `${issue2.identifier.toLowerCase()}-${slug}` : issue2.identifier.toLowerCase();
106910
+ return linearIdentifierStrategy.changeName(issue2);
106583
106911
  }
106584
106912
  async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = [], appendPrompt = "", attachments = []) {
106585
106913
  const name = changeNameForIssue(issue2);
@@ -106661,6 +106989,7 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = []
106661
106989
  }
106662
106990
  var init_scaffold = __esm(() => {
106663
106991
  init_fs_change();
106992
+ init_identifier_strategy();
106664
106993
  });
106665
106994
 
106666
106995
  // packages/core/src/detections/tasks.ts
@@ -106704,19 +107033,21 @@ function gateActive(inputs) {
106704
107033
  }
106705
107034
 
106706
107035
  // packages/core/src/detections/mention.ts
106707
- function buildMentionAckComment(body, author) {
106708
- const firstLine = body.split(`
106709
- `)[0];
106710
- const truncated = firstLine.slice(0, 200);
106711
- const excerpt = truncated + (truncated.length < firstLine.length ? "\u2026" : "");
106712
- const greeting = author ? `\uD83D\uDC40 Got it, ${author}! I've picked up your mention and queued a review pass.` : `\uD83D\uDC40 Acknowledged! I've picked up your mention and queued a review pass.`;
106713
- return `${greeting}
106714
-
106715
- > ${excerpt}`;
107036
+ function buildMentionAckComment(_body, author) {
107037
+ const greeting = author ? `Got it, ${author} \u2014 picked up your mention and queued a review pass.` : `Acknowledged \u2014 picked up your mention and queued a review pass.`;
107038
+ return buildRalphyComment({
107039
+ type: "mention-ack",
107040
+ action: "picked up your mention",
107041
+ body: greeting
107042
+ });
106716
107043
  }
107044
+ var init_mention2 = __esm(() => {
107045
+ init_src8();
107046
+ });
106717
107047
  // packages/core/src/detections/index.ts
106718
107048
  var init_detections = __esm(() => {
106719
107049
  init_phase2();
107050
+ init_mention2();
106720
107051
  });
106721
107052
 
106722
107053
  // apps/agent/src/features/confirmation/state.ts
@@ -106839,7 +107170,17 @@ function buildMentionTaskBody(trigger, issueUrl) {
106839
107170
  function findLastRalphPickupISO(comments) {
106840
107171
  let latest = null;
106841
107172
  for (const c of comments) {
106842
- if (!/^\uD83D\uDD01\s*Ralph picked up/.test(c.body.trimStart()))
107173
+ if (!isPickupComment(c.body))
107174
+ continue;
107175
+ if (latest === null || c.createdAt > latest)
107176
+ latest = c.createdAt;
107177
+ }
107178
+ return latest;
107179
+ }
107180
+ function findLastMentionAckISO(comments) {
107181
+ let latest = null;
107182
+ for (const c of comments) {
107183
+ if (!isMentionAckComment(c.body))
106843
107184
  continue;
106844
107185
  if (latest === null || c.createdAt > latest)
106845
107186
  latest = c.createdAt;
@@ -106875,7 +107216,10 @@ function githubReactionSlug(emoji3) {
106875
107216
  return emoji3;
106876
107217
  }
106877
107218
  }
106878
- var init_task_bodies = () => {};
107219
+ var init_task_bodies = __esm(() => {
107220
+ init_src8();
107221
+ init_ralph_comment();
107222
+ });
106879
107223
 
106880
107224
  // apps/agent/src/features/confirmation/inspect.ts
106881
107225
  function buildReviseRegex(handle) {
@@ -106902,7 +107246,12 @@ async function inspectAwaitingTicket(state, cfg, deps) {
106902
107246
  if (!next.stuckPostedAt) {
106903
107247
  if (cfg.postComments) {
106904
107248
  try {
106905
- await deps.postComment(`\u26A0 Ralphy: confirmation gate stuck after ${next.rounds} revise round(s) ` + `(max ${cfg.maxConfirmationRounds}). Applying \`ralph:stuck\` \u2014 ` + `clear the label to retry, or apply the approval marker to proceed.`);
107249
+ await deps.postComment(buildRalphyComment({
107250
+ type: "confirmation-stuck",
107251
+ action: "confirmation gate stuck",
107252
+ body: `Confirmation gate stuck after ${next.rounds} revise round(s) ` + `(max ${cfg.maxConfirmationRounds}). Applying \`ralph:stuck\` \u2014 ` + `clear the label to retry, or apply the approval marker to proceed.`,
107253
+ fields: { rounds: next.rounds }
107254
+ }));
106906
107255
  } catch (err) {
106907
107256
  deps.log(`! plan-stuck comment failed: ${err.message}`, "yellow");
106908
107257
  }
@@ -106947,7 +107296,12 @@ ${revise.reason}`);
106947
107296
  }
106948
107297
  if (cfg.postComments) {
106949
107298
  try {
106950
- await deps.postComment(`\uD83D\uDD01 Ralphy: revise request acknowledged \u2014 restarting at design (round ${next.rounds + 1}/${cfg.maxConfirmationRounds}).`);
107299
+ await deps.postComment(buildRalphyComment({
107300
+ type: "revise-ack",
107301
+ action: "revise request acknowledged",
107302
+ body: `Revise request acknowledged \u2014 restarting at design (round ${next.rounds + 1}/${cfg.maxConfirmationRounds}).`,
107303
+ fields: { round: next.rounds + 1 }
107304
+ }));
106951
107305
  } catch (err) {
106952
107306
  deps.log(`! revise ack comment failed: ${err.message}`, "yellow");
106953
107307
  }
@@ -106976,7 +107330,12 @@ ${revise.reason}`);
106976
107330
  const limitMs = cfg.timeoutHours * 60 * 60 * 1000;
106977
107331
  if (elapsedMs >= limitMs && cfg.postComments) {
106978
107332
  try {
106979
- await deps.postComment(`\u23F0 Ralphy: still awaiting confirmation on this plan (round ${next.rounds + 1}/${cfg.maxConfirmationRounds}). ` + `Approve to continue or reply \`${cfg.mentionHandle} revise: <reason>\` to send it back.`);
107333
+ await deps.postComment(buildRalphyComment({
107334
+ type: "confirmation-reminder",
107335
+ action: "awaiting confirmation",
107336
+ body: `Still awaiting confirmation on this plan (round ${next.rounds + 1}/${cfg.maxConfirmationRounds}). ` + `Approve to continue or reply \`${cfg.mentionHandle} revise: <reason>\` to send it back.`,
107337
+ fields: { round: next.rounds + 1 }
107338
+ }));
106980
107339
  next.lastReminderAt = nowIso;
106981
107340
  } catch (err) {
106982
107341
  deps.log(`! reminder comment failed: ${err.message}`, "yellow");
@@ -106986,6 +107345,7 @@ ${revise.reason}`);
106986
107345
  return { outcome: "stay-awaiting", next };
106987
107346
  }
106988
107347
  var init_inspect = __esm(() => {
107348
+ init_src8();
106989
107349
  init_task_bodies();
106990
107350
  });
106991
107351
 
@@ -107028,7 +107388,12 @@ async function postPlanReadyCommentOnce(issue2, statePath, changeName, deps) {
107028
107388
  return;
107029
107389
  const approvalSentence = describeApprovalMarker(deps.cfg.linear.indicators.getApproved);
107030
107390
  const handle = deps.cfg.linear.mentionHandle;
107031
- const body = `\uD83D\uDCCB Ralphy plan ready for \`${changeName}\` \u2014 review proposal.md / design.md / tasks.md ` + `and ${approvalSentence} to continue, ` + `or reply with \`${handle} revise: <reason>\` to send it back to design.`;
107391
+ const body = buildRalphyComment({
107392
+ type: "plan-ready",
107393
+ action: "plan ready",
107394
+ body: `Plan ready for \`${changeName}\` \u2014 review proposal.md / design.md / tasks.md ` + `and ${approvalSentence} to continue, ` + `or reply with \`${handle} revise: <reason>\` to send it back to design.`,
107395
+ fields: { change: changeName }
107396
+ });
107032
107397
  try {
107033
107398
  await addIssueComment(deps.apiKey, issue2.id, body);
107034
107399
  } catch (err) {
@@ -107194,33 +107559,39 @@ async function processAwaitingForIssue(issue2, deps) {
107194
107559
  return false;
107195
107560
  }
107196
107561
  if (!hasUnchecked(tasks2 ?? "")) {
107197
- deps.awaitingChangeSet.delete(changeName);
107198
- await releaseAwaitingMarker(issue2, statePath, {
107562
+ const wasTracked = deps.awaitingChangeSet.delete(changeName);
107563
+ const released = await releaseAwaitingMarker(issue2, statePath, {
107199
107564
  indicators,
107200
107565
  applyIndicator: deps.applyIndicator,
107201
107566
  onLog: deps.onLog
107202
107567
  });
107203
- deps.onLog(` ${issue2.identifier}: confirmation detect released \u2014 tasks-empty`);
107568
+ if (wasTracked || released) {
107569
+ deps.onLog(` ${issue2.identifier}: confirmation detect released \u2014 tasks-empty`);
107570
+ }
107204
107571
  return false;
107205
107572
  }
107206
107573
  if (!planningComplete(tasks2 ?? "")) {
107207
- deps.awaitingChangeSet.delete(changeName);
107208
- await releaseAwaitingMarker(issue2, statePath, {
107574
+ const wasTracked = deps.awaitingChangeSet.delete(changeName);
107575
+ const released = await releaseAwaitingMarker(issue2, statePath, {
107209
107576
  indicators,
107210
107577
  applyIndicator: deps.applyIndicator,
107211
107578
  onLog: deps.onLog
107212
107579
  });
107213
- deps.onLog(` ${issue2.identifier}: confirmation detect released \u2014 planning-incomplete`);
107580
+ if (wasTracked || released) {
107581
+ deps.onLog(` ${issue2.identifier}: confirmation detect released \u2014 planning-incomplete`);
107582
+ }
107214
107583
  return false;
107215
107584
  }
107216
107585
  if (isStubArtifact(proposal) || isStubArtifact(design)) {
107217
- deps.awaitingChangeSet.delete(changeName);
107218
- await releaseAwaitingMarker(issue2, statePath, {
107586
+ const wasTracked = deps.awaitingChangeSet.delete(changeName);
107587
+ const released = await releaseAwaitingMarker(issue2, statePath, {
107219
107588
  indicators,
107220
107589
  applyIndicator: deps.applyIndicator,
107221
107590
  onLog: deps.onLog
107222
107591
  });
107223
- deps.onLog(` ${issue2.identifier}: confirmation detect released \u2014 proposal/design not yet filled in`);
107592
+ if (wasTracked || released) {
107593
+ deps.onLog(` ${issue2.identifier}: confirmation detect released \u2014 proposal/design not yet filled in`);
107594
+ }
107224
107595
  return false;
107225
107596
  }
107226
107597
  deps.awaitingChangeSet.add(changeName);
@@ -107301,11 +107672,13 @@ async function processAwaitingForIssue(issue2, deps) {
107301
107672
  }
107302
107673
  var init_awaiting = __esm(() => {
107303
107674
  init_layout();
107675
+ init_src8();
107304
107676
  init_detections();
107305
107677
  init_phase();
107306
107678
  init_worktree();
107307
107679
  init_scaffold();
107308
107680
  init_linear();
107681
+ init_ralph_comment();
107309
107682
  init_types2();
107310
107683
  init_workflow();
107311
107684
  init_state2();
@@ -107532,10 +107905,13 @@ function unionMarkers(...sets) {
107532
107905
  }
107533
107906
  return out;
107534
107907
  }
107535
- function describeIndicators(indicators, team, assignee, anyAssignee) {
107908
+ function describeIndicators(indicators, team, assignee, anyAssignee, requireAllLabels) {
107536
107909
  const parts = [];
107537
107910
  parts.push(`team=${team ?? "*"}`);
107538
107911
  parts.push(`assignee=${anyAssignee ? "any" : assignee ?? "*"}`);
107912
+ if (requireAllLabels && requireAllLabels.length > 0) {
107913
+ parts.push(`labels=[${requireAllLabels.join(",")}]`);
107914
+ }
107539
107915
  if (indicators.getTodo) {
107540
107916
  parts.push(`todo=[${indicators.getTodo.filter.map((m) => `${m.type}:${m.value}`).join(",")}]`);
107541
107917
  }
@@ -107555,7 +107931,7 @@ function createLinearResolvers(input) {
107555
107931
  const stateCache = new Map;
107556
107932
  const labelCache = new Map;
107557
107933
  const teamIdCache = new Map;
107558
- const teamKeyOf = (issue2) => issue2.identifier.split("-")[0];
107934
+ const teamKeyOf = (issue2) => linearIdentifierStrategy.scopeKey(issue2);
107559
107935
  async function resolveStateId(issue2, name) {
107560
107936
  const t = teamKeyOf(issue2);
107561
107937
  let map3 = stateCache.get(t);
@@ -107601,6 +107977,22 @@ function createLinearResolvers(input) {
107601
107977
  return null;
107602
107978
  }
107603
107979
  }
107980
+ async function stripSiblingGroupLabels(issue2, group, keepId) {
107981
+ const map3 = labelCache.get(teamKeyOf(issue2));
107982
+ if (!map3)
107983
+ return;
107984
+ for (const name of issue2.labels) {
107985
+ const siblingId = map3.get(`${group}:${name}`.toLowerCase());
107986
+ if (!siblingId || siblingId === keepId)
107987
+ continue;
107988
+ try {
107989
+ await removeLabelFromIssue(apiKey, issue2.id, siblingId);
107990
+ diag("linear-marker", ` \u2192 ${issue2.identifier} -label='${group}:${name}' (group swap)`, "gray");
107991
+ } catch (err) {
107992
+ diag("linear-marker", `! could not remove sibling label '${group}:${name}' from ${issue2.identifier}: ${err.message}`, "yellow");
107993
+ }
107994
+ }
107995
+ }
107604
107996
  async function applyMarker(issue2, m) {
107605
107997
  if (m.type === "status") {
107606
107998
  const id = await resolveStateId(issue2, m.value);
@@ -107634,6 +108026,8 @@ function createLinearResolvers(input) {
107634
108026
  err.issue = issue2.identifier;
107635
108027
  throw err;
107636
108028
  }
108029
+ if (m.group)
108030
+ await stripSiblingGroupLabels(issue2, m.group, id);
107637
108031
  await addLabelToIssue(apiKey, issue2.id, id);
107638
108032
  diag("linear-marker", ` \u2192 ${issue2.identifier} +label='${display}'`, "gray");
107639
108033
  }
@@ -107733,6 +108127,201 @@ async function fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requ
107733
108127
  var init_linear_resolvers = __esm(() => {
107734
108128
  init_types2();
107735
108129
  init_linear();
108130
+ init_identifier_strategy();
108131
+ });
108132
+
108133
+ // apps/agent/src/agent/wire/tracker/github.ts
108134
+ function identifierForNumber(n) {
108135
+ return `issue-${n}`;
108136
+ }
108137
+ function toLinearIssue(gh) {
108138
+ const open = (gh.state ?? "OPEN").toUpperCase() === "OPEN";
108139
+ return {
108140
+ id: String(gh.number),
108141
+ identifier: identifierForNumber(gh.number),
108142
+ title: gh.title,
108143
+ description: gh.body ?? null,
108144
+ url: gh.url,
108145
+ state: { name: open ? "Open" : "Closed", type: open ? "started" : "completed" },
108146
+ assignee: null,
108147
+ project: null,
108148
+ labels: (gh.labels ?? []).map((l) => l.name),
108149
+ priority: 0,
108150
+ createdAt: gh.createdAt ?? "",
108151
+ blockedByIds: []
108152
+ };
108153
+ }
108154
+ function labelValues(markers) {
108155
+ return markers.filter((m) => m.type === "label").map((m) => m.value);
108156
+ }
108157
+ function createGithubTrackerProvider(input) {
108158
+ const { cmdRunner, projectRoot, diag } = input;
108159
+ const statusLabels = input.issues?.statusLabels ?? DEFAULT_STATUS_LABELS;
108160
+ const todoLabel = input.issues?.label;
108161
+ const assignee = input.issues?.assignee;
108162
+ const configuredRepo = input.issues?.repo;
108163
+ let repoPromise = null;
108164
+ async function repo() {
108165
+ if (configuredRepo && configuredRepo.trim() !== "")
108166
+ return configuredRepo.trim();
108167
+ if (!repoPromise) {
108168
+ repoPromise = (async () => {
108169
+ try {
108170
+ const { stdout } = await cmdRunner.run(["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], projectRoot);
108171
+ const detected = stdout.trim();
108172
+ if (!detected)
108173
+ throw new Error("empty");
108174
+ diag("github-tracker", ` using detected GitHub repo ${detected}`, "gray");
108175
+ return detected;
108176
+ } catch (err) {
108177
+ throw new Error("github tracker: could not determine the repository \u2014 set " + "`github.issues.repo` (owner/name) in WORKFLOW.md or run inside a " + `repo with a GitHub 'origin' remote (${err.message})`);
108178
+ }
108179
+ })();
108180
+ }
108181
+ return repoPromise;
108182
+ }
108183
+ async function listIssues(args) {
108184
+ const r = await repo();
108185
+ const { stdout } = await cmdRunner.run([
108186
+ "gh",
108187
+ "issue",
108188
+ "list",
108189
+ "--repo",
108190
+ r,
108191
+ "--state",
108192
+ "open",
108193
+ "--json",
108194
+ "number,title,url,body,state,createdAt,labels",
108195
+ "--limit",
108196
+ "100",
108197
+ ...args
108198
+ ], projectRoot);
108199
+ const parsed = JSON.parse(stdout.trim() || "[]");
108200
+ return parsed.map(toLinearIssue);
108201
+ }
108202
+ async function fetchByGet(inc, excl) {
108203
+ if (!inc)
108204
+ return [];
108205
+ const include = !Array.isArray(inc) && "filter" in inc ? inc.filter : [];
108206
+ const wantLabels = labelValues(include);
108207
+ const args = [];
108208
+ for (const label of wantLabels)
108209
+ args.push("--label", label);
108210
+ if (assignee && assignee.trim() !== "")
108211
+ args.push("--assignee", assignee.trim());
108212
+ const issues = await listIssues(args);
108213
+ const excludeLabels = new Set(labelValues(excl));
108214
+ if (excludeLabels.size === 0)
108215
+ return issues;
108216
+ return issues.filter((issue2) => !issue2.labels.some((l) => excludeLabels.has(l)));
108217
+ }
108218
+ async function ghIssue(issueNumber, ...args) {
108219
+ const r = await repo();
108220
+ await cmdRunner.run(["gh", "issue", ...args, issueNumber, "--repo", r], projectRoot);
108221
+ }
108222
+ async function applyMarker(issue2, m) {
108223
+ if (m.type === "comment") {
108224
+ await ghIssue(issue2.id, "comment", "--body", m.value);
108225
+ diag("github-marker", ` \u2192 ${issue2.identifier} comment`, "gray");
108226
+ return;
108227
+ }
108228
+ if (m.type !== "label") {
108229
+ diag("github-marker", `! ${issue2.identifier}: '${m.type}' markers are not supported by the GitHub tracker \u2014 skipped`, "yellow");
108230
+ return;
108231
+ }
108232
+ await ghIssue(issue2.id, "edit", "--add-label", m.value);
108233
+ diag("github-marker", ` \u2192 ${issue2.identifier} +label='${m.value}'`, "gray");
108234
+ if (m.value === statusLabels.inProgress && todoLabel && todoLabel.trim() !== "") {
108235
+ await ghIssue(issue2.id, "edit", "--remove-label", todoLabel.trim());
108236
+ diag("github-marker", ` \u2192 ${issue2.identifier} -label='${todoLabel.trim()}'`, "gray");
108237
+ }
108238
+ if (m.value === statusLabels.done) {
108239
+ await ghIssue(issue2.id, "close");
108240
+ diag("github-marker", ` \u2192 ${issue2.identifier} closed`, "gray");
108241
+ }
108242
+ }
108243
+ async function applyIndicator(issue2, ind) {
108244
+ for (const m of markersOf(ind))
108245
+ await applyMarker(issue2, m);
108246
+ }
108247
+ async function removeIndicator(issue2, ind) {
108248
+ for (const m of markersOf(ind)) {
108249
+ if (m.type !== "label")
108250
+ continue;
108251
+ await ghIssue(issue2.id, "edit", "--remove-label", m.value);
108252
+ diag("github-marker", ` \u2192 ${issue2.identifier} -label='${m.value}'`, "gray");
108253
+ }
108254
+ }
108255
+ async function fetchDoneCandidates() {
108256
+ return listIssues(["--label", statusLabels.inProgress]);
108257
+ }
108258
+ async function resolveLabelIdForTeam() {
108259
+ return null;
108260
+ }
108261
+ return {
108262
+ fetchByGet,
108263
+ applyIndicator,
108264
+ removeIndicator,
108265
+ applyMarker,
108266
+ fetchDoneCandidates,
108267
+ resolveLabelIdForTeam
108268
+ };
108269
+ }
108270
+ function githubIndicators(issues) {
108271
+ const statusLabels = issues?.statusLabels ?? DEFAULT_STATUS_LABELS;
108272
+ const todoLabel = issues?.label?.trim();
108273
+ return {
108274
+ getTodo: { filter: todoLabel ? [{ type: "label", value: todoLabel }] : [] },
108275
+ getInProgress: { filter: [{ type: "label", value: statusLabels.inProgress }] },
108276
+ setInProgress: { type: "label", value: statusLabels.inProgress },
108277
+ setDone: { type: "label", value: statusLabels.done },
108278
+ setError: { type: "label", value: statusLabels.error }
108279
+ };
108280
+ }
108281
+ var DEFAULT_STATUS_LABELS;
108282
+ var init_github = __esm(() => {
108283
+ init_types2();
108284
+ DEFAULT_STATUS_LABELS = {
108285
+ inProgress: "ralph:in-progress",
108286
+ done: "ralph:done",
108287
+ error: "ralph:error"
108288
+ };
108289
+ });
108290
+
108291
+ // apps/agent/src/agent/wire/tracker/linear-tracker-provider.ts
108292
+ function createLinearTrackerProvider(input) {
108293
+ const {
108294
+ apiKey,
108295
+ team,
108296
+ assignee,
108297
+ anyAssignee,
108298
+ requireAllLabels,
108299
+ indicators,
108300
+ resolvers,
108301
+ fetchMentions,
108302
+ ticketNumbers
108303
+ } = input;
108304
+ const excludeFromTodo = unionMarkers(indicators.setDone, indicators.setError);
108305
+ const excludeFromInProgress = unionMarkers(indicators.setError);
108306
+ return {
108307
+ fetchTodo: () => resolvers.fetchByGet(indicators.getTodo, excludeFromTodo),
108308
+ fetchInProgress: () => resolvers.fetchByGet(indicators.getInProgress, excludeFromInProgress),
108309
+ fetchReview: () => resolvers.fetchByGet(indicators.getReview, []),
108310
+ fetchMentions,
108311
+ fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requireAllLabels, indicators, ticketNumbers && ticketNumbers.length > 0 ? ticketNumbers : undefined),
108312
+ applyIndicator: resolvers.applyIndicator,
108313
+ removeIndicator: resolvers.removeIndicator,
108314
+ postComment: (issue2, body) => addIssueComment(apiKey, issue2.id, body),
108315
+ fetchComments: async (issueId) => {
108316
+ const c = await fetchIssueComments(apiKey, issueId);
108317
+ return c.map((x) => ({ body: x.body }));
108318
+ }
108319
+ };
108320
+ }
108321
+ var init_linear_tracker_provider = __esm(() => {
108322
+ init_linear();
108323
+ init_indicators();
108324
+ init_linear_resolvers();
107736
108325
  });
107737
108326
 
107738
108327
  // apps/agent/src/agent/wire/prepare.ts
@@ -108347,7 +108936,12 @@ async function maybePingStaleReviewer(issue2, prUrl, state, newestReviewerActivi
108347
108936
  if (!m)
108348
108937
  return;
108349
108938
  const [, owner, repo, num] = m;
108350
- const body = `\uD83D\uDD14 @${reviewer} \u2014 Ralph has been waiting ${elapsedH.toFixed(0)}h on a re-review for ${prUrl}. Could you take another look when you have a moment?`;
108939
+ const body = buildRalphyComment({
108940
+ type: "reviewer-ping",
108941
+ action: "nudging reviewer",
108942
+ body: `@${reviewer} \u2014 Ralph has been waiting ${elapsedH.toFixed(0)}h on a re-review for ${prUrl}. Could you take another look when you have a moment?`,
108943
+ fields: { reviewer }
108944
+ });
108351
108945
  try {
108352
108946
  await deps.cmdRunner.run(["gh", "api", `repos/${owner}/${repo}/issues/${num}/comments`, "-f", `body=${body}`], deps.projectRoot);
108353
108947
  deps.stalePingedAt.set(prUrl, now2);
@@ -108413,6 +109007,7 @@ async function scanCodeReview(issue2, prUrl, lastRalphPickup, deps) {
108413
109007
  var init_scan = __esm(() => {
108414
109008
  init_layout();
108415
109009
  init_store();
109010
+ init_src8();
108416
109011
  init_scaffold();
108417
109012
  init_worktree();
108418
109013
  });
@@ -108462,7 +109057,7 @@ async function fetchPrIssueComments(cmdRunner, projectRoot, prUrl, onLog) {
108462
109057
  return [];
108463
109058
  }
108464
109059
  }
108465
- var init_github = __esm(() => {
109060
+ var init_github2 = __esm(() => {
108466
109061
  init_linear();
108467
109062
  init_task_bodies();
108468
109063
  });
@@ -108470,6 +109065,19 @@ var init_github = __esm(() => {
108470
109065
  // apps/agent/src/agent/wire/mention-scan.ts
108471
109066
  import { readdir as readdir2 } from "fs/promises";
108472
109067
  import { join as join29 } from "path";
109068
+ function latestIso(...values2) {
109069
+ let latest = null;
109070
+ for (const value of values2) {
109071
+ if (value && (latest === null || value > latest))
109072
+ latest = value;
109073
+ }
109074
+ return latest;
109075
+ }
109076
+ function isAlreadyReactedError(err) {
109077
+ const e = err;
109078
+ const text = [...e?.messages ?? [], e?.message ?? ""].join(" ").toLowerCase();
109079
+ return text.includes("conflict on insert of reaction") || text.includes("already exists") || text.includes("already reacted");
109080
+ }
108473
109081
  function createMentionScanner(input) {
108474
109082
  const {
108475
109083
  apiKey,
@@ -108531,13 +109139,14 @@ function createMentionScanner(input) {
108531
109139
  for (const issue2 of candidates) {
108532
109140
  const comments = issue2.comments ?? [];
108533
109141
  const lastRalphPickup = findLastRalphPickupISO(comments);
109142
+ const linearMentionGate = latestIso(lastRalphPickup, findLastMentionAckISO(comments));
108534
109143
  if (wantMention) {
108535
109144
  for (const c of comments) {
108536
109145
  if (isRalphComment(c.body))
108537
109146
  continue;
108538
109147
  if (!containsHandle(c.body, handle))
108539
109148
  continue;
108540
- if (lastRalphPickup && c.createdAt <= lastRalphPickup)
109149
+ if (linearMentionGate && c.createdAt <= linearMentionGate)
108541
109150
  continue;
108542
109151
  out.push({
108543
109152
  issue: issue2,
@@ -108557,7 +109166,9 @@ function createMentionScanner(input) {
108557
109166
  queued.add(issue2.id);
108558
109167
  break;
108559
109168
  }
108560
- diag("mention", `! mention scan: Linear reaction failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
109169
+ if (!isAlreadyReactedError(err)) {
109170
+ diag("mention", `! mention scan: Linear reaction failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
109171
+ }
108561
109172
  }
108562
109173
  if (cfg.linear.postComments !== false) {
108563
109174
  try {
@@ -108581,11 +109192,14 @@ function createMentionScanner(input) {
108581
109192
  continue;
108582
109193
  if (wantMention) {
108583
109194
  const ghComments = await fetchPrIssueComments(cmdRunner, projectRoot, prUrl, onLog);
109195
+ const ghMentionGate = latestIso(lastRalphPickup, findLastMentionAckISO(ghComments));
108584
109196
  const prMatch = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/\d+/.exec(prUrl);
108585
109197
  for (const c of ghComments) {
109198
+ if (isRalphComment(c.body))
109199
+ continue;
108586
109200
  if (!containsHandle(c.body, handle))
108587
109201
  continue;
108588
- if (lastRalphPickup && c.createdAt <= lastRalphPickup)
109202
+ if (ghMentionGate && c.createdAt <= ghMentionGate)
108589
109203
  continue;
108590
109204
  out.push({
108591
109205
  issue: issue2,
@@ -108602,7 +109216,9 @@ function createMentionScanner(input) {
108602
109216
  try {
108603
109217
  await addGithubReactionToComment(cmdRunner, projectRoot, { owner, repo, kind: "issue" }, c.id, "\uD83D\uDC40");
108604
109218
  } catch (err) {
108605
- diag("mention", `! mention scan: GitHub reaction failed for ${prUrl}: ${formatLinearError(err)}`, "yellow");
109219
+ if (!isAlreadyReactedError(err)) {
109220
+ diag("mention", `! mention scan: GitHub reaction failed for ${prUrl}: ${formatLinearError(err)}`, "yellow");
109221
+ }
108606
109222
  }
108607
109223
  if (cfg.linear.postComments !== false) {
108608
109224
  await postGithubPrComment(cmdRunner, projectRoot, prUrl, buildMentionAckComment(c.body, c.author), onLog);
@@ -108656,7 +109272,7 @@ var init_mention_scan = __esm(() => {
108656
109272
  init_detections();
108657
109273
  init_scaffold();
108658
109274
  init_scan();
108659
- init_github();
109275
+ init_github2();
108660
109276
  init_task_bodies();
108661
109277
  });
108662
109278
 
@@ -109664,7 +110280,12 @@ async function readTasksMd(changeDir, log3) {
109664
110280
  }
109665
110281
  }
109666
110282
  function renderTasksCommentBody(tasksMd, changeName, iteration) {
109667
- return renderTasksBlock(tasksMd, { changeName, iteration });
110283
+ return buildRalphyComment({
110284
+ type: "tasks",
110285
+ action: "task progress",
110286
+ body: renderTasksBlock(tasksMd, { changeName, iteration }),
110287
+ fields: { change: changeName, iter: iteration }
110288
+ });
109668
110289
  }
109669
110290
  async function postOrUpdateTasksComment(deps) {
109670
110291
  const tasksMd = await readTasksMd(deps.changeDir, deps.log);
@@ -109775,8 +110396,13 @@ async function postPlanCommentOnce(deps) {
109775
110396
  if (designSummary) {
109776
110397
  parts.push("", "**Design**", "", designSummary);
109777
110398
  }
109778
- const body = parts.join(`
109779
- `);
110399
+ const body = buildRalphyComment({
110400
+ type: "plan",
110401
+ action: "plan",
110402
+ body: parts.join(`
110403
+ `),
110404
+ fields: { change: deps.changeName }
110405
+ });
109780
110406
  let id;
109781
110407
  try {
109782
110408
  id = await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, body);
@@ -109793,9 +110419,14 @@ async function postPlanCommentOnce(deps) {
109793
110419
  }
109794
110420
  async function postSteeringAndRefreshTasks(deps) {
109795
110421
  const firstLine = deps.message.split(/\r?\n/, 1)[0].trim() || deps.message.trim();
109796
- const steeringBody = `### ${STEERING_COMMENT_TITLE}
110422
+ const steeringBody = buildRalphyComment({
110423
+ type: "steering",
110424
+ action: "steering",
110425
+ body: `### ${STEERING_COMMENT_TITLE}
109797
110426
 
109798
- ${deps.message.trim()}`;
110427
+ ${deps.message.trim()}`,
110428
+ fields: { change: deps.changeName }
110429
+ });
109799
110430
  try {
109800
110431
  await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, steeringBody);
109801
110432
  deps.log(` comment-sync: posted steering comment (${firstLine})`, "gray");
@@ -109828,6 +110459,7 @@ ${deps.message.trim()}`;
109828
110459
  var PLAN_COMMENT_TITLE = "\uD83D\uDCCB Ralph plan", STEERING_COMMENT_TITLE = "\uD83E\uDDED Ralph steering";
109829
110460
  var init_comment_sync = __esm(() => {
109830
110461
  init_store();
110462
+ init_src8();
109831
110463
  init_linear_sync();
109832
110464
  });
109833
110465
 
@@ -262694,11 +263326,12 @@ function buildAgentCoordinator(input) {
262694
263326
  };
262695
263327
  const concurrency = args.concurrency || cfg.concurrency;
262696
263328
  const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
262697
- const indicators = mergeIndicators(cfg.linear.indicators, args.indicators);
263329
+ const isGithubTracker = cfg.tracker.kind === "github";
263330
+ const indicators = isGithubTracker ? githubIndicators(cfg.github?.issues) : mergeIndicators(cfg.linear.indicators, args.indicators);
262698
263331
  const team = args.linearTeam || cfg.linear.team;
262699
263332
  const { assignee, anyAssignee, requireAllLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, args.linearAssignee));
262700
263333
  const ticketNumbers = resolveTicketNumbers(args.ticketTokens, team);
262701
- const excludeFromTodo = unionMarkers(indicators.setDone, indicators.setError);
263334
+ const excludeFromTodo = isGithubTracker ? unionMarkers(indicators.setDone, indicators.setError, indicators.setInProgress) : unionMarkers(indicators.setDone, indicators.setError);
262702
263335
  const gitRunner = input.runners?.git ?? bunGitRunner;
262703
263336
  const cmdRunner = input.runners?.cmd ?? bunCmdRunner;
262704
263337
  const cwdByChange = new Map;
@@ -262733,7 +263366,7 @@ function buildAgentCoordinator(input) {
262733
263366
  }
262734
263367
  return code;
262735
263368
  });
262736
- const resolvers = createLinearResolvers({
263369
+ const resolvers = isGithubTracker ? null : createLinearResolvers({
262737
263370
  apiKey,
262738
263371
  team,
262739
263372
  assignee,
@@ -262742,6 +263375,15 @@ function buildAgentCoordinator(input) {
262742
263375
  diag,
262743
263376
  ...ticketNumbers.length > 0 ? { ticketNumbers } : {}
262744
263377
  });
263378
+ const provider = isGithubTracker ? createGithubTrackerProvider({
263379
+ issues: cfg.github?.issues,
263380
+ cmdRunner,
263381
+ projectRoot,
263382
+ diag
263383
+ }) : {
263384
+ ...resolvers,
263385
+ fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requireAllLabels, indicators, ticketNumbers.length > 0 ? ticketNumbers : undefined)
263386
+ };
262745
263387
  if (ticketNumbers.length > 0) {
262746
263388
  const hasGetIndicator = [indicators.getTodo, indicators.getInProgress].some((ind) => ind && ind.filter.length > 0);
262747
263389
  if (!hasGetIndicator) {
@@ -262772,7 +263414,7 @@ function buildAgentCoordinator(input) {
262772
263414
  scriptRunner,
262773
263415
  ...input.runners?.worktree ? { worktreeProvider: input.runners.worktree } : {}
262774
263416
  });
262775
- const fetchMentions = createMentionScanner({
263417
+ const fetchMentions = isGithubTracker ? async () => [] : createMentionScanner({
262776
263418
  apiKey,
262777
263419
  args,
262778
263420
  cfg,
@@ -262792,6 +263434,27 @@ function buildAgentCoordinator(input) {
262792
263434
  lastHandledReviewActivity,
262793
263435
  resolvePrUrlForIssue: prDiscovery.resolvePrUrlForIssue
262794
263436
  });
263437
+ const tracker = isGithubTracker ? {
263438
+ fetchTodo: () => provider.fetchByGet(indicators.getTodo, excludeFromTodo),
263439
+ fetchInProgress: () => provider.fetchByGet(indicators.getInProgress, unionMarkers(indicators.setError)),
263440
+ fetchReview: async () => [],
263441
+ fetchMentions,
263442
+ fetchDoneCandidates: () => provider.fetchDoneCandidates(),
263443
+ applyIndicator: provider.applyIndicator,
263444
+ removeIndicator: provider.removeIndicator,
263445
+ postComment: (issue2, body) => provider.applyMarker(issue2, { type: "comment", value: body }),
263446
+ fetchComments: async () => []
263447
+ } : createLinearTrackerProvider({
263448
+ apiKey,
263449
+ team,
263450
+ assignee,
263451
+ anyAssignee,
263452
+ requireAllLabels,
263453
+ indicators,
263454
+ resolvers,
263455
+ fetchMentions,
263456
+ ...ticketNumbers.length > 0 ? { ticketNumbers } : {}
263457
+ });
262795
263458
  const spawnWorker = createSpawnWorker({
262796
263459
  args,
262797
263460
  cfg,
@@ -262803,7 +263466,7 @@ function buildAgentCoordinator(input) {
262803
263466
  indicators,
262804
263467
  cmdRunner,
262805
263468
  gitRunner,
262806
- applyIndicator: resolvers.applyIndicator,
263469
+ applyIndicator: provider.applyIndicator,
262807
263470
  bus,
262808
263471
  onLog,
262809
263472
  diag,
@@ -262847,8 +263510,8 @@ function buildAgentCoordinator(input) {
262847
263510
  cwdOf: (cn) => cwdByChange.get(cn),
262848
263511
  awaitingChangeSet,
262849
263512
  reapForAwaiting: (cn) => coordRef.current?.reapForAwaiting(cn),
262850
- applyIndicator: resolvers.applyIndicator,
262851
- applyMarker: resolvers.applyMarker,
263513
+ applyIndicator: provider.applyIndicator,
263514
+ applyMarker: provider.applyMarker,
262852
263515
  openDraftPr,
262853
263516
  ...onAwaitingTicket ? { onAwaitingTicket } : {},
262854
263517
  onLog
@@ -262878,7 +263541,7 @@ function buildAgentCoordinator(input) {
262878
263541
  projectRoot,
262879
263542
  maxRecoveryAttempts: cfg.prRecovery.maxRecoverySessions
262880
263543
  }) : null;
262881
- const commentSync = createCommentSyncHooks({
263544
+ const commentSync = isGithubTracker ? { enabled: false } : createCommentSyncHooks({
262882
263545
  apiKey,
262883
263546
  cfg,
262884
263547
  projectRoot,
@@ -262891,20 +263554,18 @@ function buildAgentCoordinator(input) {
262891
263554
  beforePoll: () => {
262892
263555
  pollContext = new PollContext;
262893
263556
  },
262894
- fetchTodo: () => resolvers.fetchByGet(indicators.getTodo, excludeFromTodo),
262895
- fetchInProgress: () => resolvers.fetchByGet(indicators.getInProgress, unionMarkers(indicators.setError)),
262896
- fetchMentions,
262897
- fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requireAllLabels, indicators, ticketNumbers.length > 0 ? ticketNumbers : undefined),
263557
+ fetchTodo: tracker.fetchTodo,
263558
+ fetchInProgress: tracker.fetchInProgress,
263559
+ fetchMentions: tracker.fetchMentions,
263560
+ fetchDoneCandidates: tracker.fetchDoneCandidates,
263561
+ fetchReview: tracker.fetchReview,
262898
263562
  prepare: prep.prepare,
262899
263563
  prepareTaskForTrigger: prep.prepareTaskForTrigger,
262900
263564
  spawnWorker,
262901
- applyIndicator: resolvers.applyIndicator,
262902
- removeIndicator: resolvers.removeIndicator,
262903
- postComment: (issue2, body) => addIssueComment(apiKey, issue2.id, body),
262904
- fetchComments: async (issueId) => {
262905
- const c = await fetchIssueComments(apiKey, issueId);
262906
- return c.map((x2) => ({ body: x2.body }));
262907
- },
263565
+ applyIndicator: tracker.applyIndicator,
263566
+ removeIndicator: tracker.removeIndicator,
263567
+ postComment: tracker.postComment,
263568
+ fetchComments: tracker.fetchComments,
262908
263569
  checkPrStatus: prDiscovery.checkPrStatus,
262909
263570
  hasPrForChange: (changeName) => prByChange.has(changeName),
262910
263571
  isChangeArchivedForIssue: (issue2) => isChangeArchivedForIssue(issue2, cwdByChange, projectRoot),
@@ -262952,7 +263613,7 @@ function buildAgentCoordinator(input) {
262952
263613
  }
262953
263614
  });
262954
263615
  coordRef.current = coord;
262955
- const filterDesc = describeIndicators(indicators, team, assignee, anyAssignee);
263616
+ const filterDesc = describeIndicators(indicators, team, assignee, anyAssignee, requireAllLabels);
262956
263617
  const runBaselineGateOnce = createBaselineGateRunner({
262957
263618
  args,
262958
263619
  cfg,
@@ -262963,7 +263624,7 @@ function buildAgentCoordinator(input) {
262963
263624
  gitRunner,
262964
263625
  coord,
262965
263626
  onLog,
262966
- resolveLabelIdForTeam: resolvers.resolveLabelIdForTeam
263627
+ resolveLabelIdForTeam: provider.resolveLabelIdForTeam
262967
263628
  });
262968
263629
  return {
262969
263630
  coord,
@@ -262991,7 +263652,6 @@ var init_wire = __esm(() => {
262991
263652
  init_workflow();
262992
263653
  init_src2();
262993
263654
  init_coordinator2();
262994
- init_linear();
262995
263655
  init_layout();
262996
263656
  init_scaffold();
262997
263657
  init_awaiting();
@@ -263000,6 +263660,8 @@ var init_wire = __esm(() => {
263000
263660
  init_indicators();
263001
263661
  init_task_bodies();
263002
263662
  init_linear_resolvers();
263663
+ init_github();
263664
+ init_linear_tracker_provider();
263003
263665
  init_linear_client();
263004
263666
  init_prepare();
263005
263667
  init_pr_discovery();
@@ -266163,7 +266825,7 @@ async function main3(argv) {
266163
266825
  return typeof process.exitCode === "number" ? process.exitCode : 0;
266164
266826
  }
266165
266827
  var import_react64;
266166
- var init_src8 = __esm(async () => {
266828
+ var init_src9 = __esm(async () => {
266167
266829
  init_context();
266168
266830
  init_layout();
266169
266831
  init_paths();
@@ -266221,7 +266883,7 @@ async function dispatch(subcommand, rest2) {
266221
266883
  return main4(rest2);
266222
266884
  }
266223
266885
  if (subcommand === "agent") {
266224
- const { main: main4 } = await init_src8().then(() => exports_src3);
266886
+ const { main: main4 } = await init_src9().then(() => exports_src3);
266225
266887
  return main4(rest2);
266226
266888
  }
266227
266889
  if (subcommand === "task") {