@neriros/ralphy 3.10.16 → 3.10.18

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 +2044 -1160
  2. package/package.json +3 -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.16")
18932
- return "3.10.16";
18931
+ if ("3.10.18")
18932
+ return "3.10.18";
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)",
@@ -19595,8 +19655,8 @@ var init_fields = __esm(() => {
19595
19655
  },
19596
19656
  {
19597
19657
  id: "setupScript",
19598
- label: "Worktree setup script (runs before each task)",
19599
- description: "Part of the worktree flow: a shell script run once in each task's fresh worktree before the task starts \u2014 e.g. to install dependencies in the new working copy.",
19658
+ label: "Worktree setup script (runs once per worktree)",
19659
+ description: "Part of the worktree flow: a shell script run once when a task's worktree is first created \u2014 e.g. to install dependencies in the new working copy. It does NOT re-run on resume, conflict-fix, ci-fix, or review re-runs that reuse an existing worktree.",
19600
19660
  spec: { kind: "text" },
19601
19661
  when: worktreeEnabled
19602
19662
  },
@@ -19639,6 +19699,13 @@ var init_fields = __esm(() => {
19639
19699
  spec: { kind: "text", placeholder: "main" },
19640
19700
  when: isOn("createPrOnSuccess")
19641
19701
  },
19702
+ {
19703
+ id: "prLabels",
19704
+ label: "PR labels",
19705
+ description: "GitHub labels attached to every pull request Ralph opens. The labels must already exist in the repo; a missing one is skipped, never fatal. One label per entry.",
19706
+ spec: { kind: "list", placeholder: "ralph" },
19707
+ when: isOn("createPrOnSuccess")
19708
+ },
19642
19709
  {
19643
19710
  id: "stackPrsOnDependencies",
19644
19711
  label: "Stack dependent issues' PRs onto their blocker's PR?",
@@ -81255,7 +81322,7 @@ function foldLegacyAssignee(v) {
81255
81322
  }
81256
81323
  return rest2;
81257
81324
  }
81258
- var CURRENT_WORKFLOW_VERSION = 6, MarkerSchema, FilterMarkerSchema, LinearFilterSchema, SET_INDICATOR_KEYS, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, DEFAULT_META_ONLY_FILES, BoundariesSchema, WorkflowConfigSchema;
81325
+ var CURRENT_WORKFLOW_VERSION = 8, MarkerSchema, FilterMarkerSchema, LinearFilterSchema, SET_INDICATOR_KEYS, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, DEFAULT_META_ONLY_FILES, BoundariesSchema, WorkflowConfigSchema;
81259
81326
  var init_schema = __esm(() => {
81260
81327
  init_zod();
81261
81328
  MarkerSchema = exports_external.discriminatedUnion("type", [
@@ -81395,12 +81462,16 @@ var init_schema = __esm(() => {
81395
81462
  createPrOnSuccess: exports_external.boolean().default(false),
81396
81463
  prDraft: exports_external.boolean().default(false),
81397
81464
  prBaseBranch: exports_external.string().default("main"),
81465
+ prLabels: exports_external.array(exports_external.string()).default([]),
81398
81466
  stackPrsOnDependencies: exports_external.boolean().default(false),
81399
81467
  autoMergeStrategy: exports_external.enum(["squash", "merge", "rebase"]).default("squash"),
81400
81468
  manualMergeWhenAutoMergeDisabled: exports_external.boolean().default(true),
81401
81469
  finalizeNoOpAsDone: exports_external.boolean().default(true),
81402
81470
  engine: exports_external.enum(["claude", "codex"]).default("claude"),
81403
81471
  model: exports_external.enum(["haiku", "sonnet", "opus"]).default("opus"),
81472
+ tracker: exports_external.object({
81473
+ kind: exports_external.enum(["linear", "github"]).default("linear")
81474
+ }).strict().default({ kind: "linear" }),
81404
81475
  linear: exports_external.preprocess(foldLegacyAssignee, exports_external.object({
81405
81476
  team: exports_external.string().optional(),
81406
81477
  filter: LinearFilterSchema,
@@ -81445,7 +81516,22 @@ var init_schema = __esm(() => {
81445
81516
  }),
81446
81517
  github: exports_external.object({
81447
81518
  base_branch: exports_external.string().optional(),
81448
- auto_merge_strategy: exports_external.enum(["squash", "merge", "rebase"]).optional()
81519
+ auto_merge_strategy: exports_external.enum(["squash", "merge", "rebase"]).optional(),
81520
+ pr_labels: exports_external.array(exports_external.string()).optional(),
81521
+ issues: exports_external.object({
81522
+ repo: exports_external.string().optional(),
81523
+ label: exports_external.string().optional(),
81524
+ assignee: exports_external.string().optional(),
81525
+ statusLabels: exports_external.object({
81526
+ inProgress: exports_external.string().default("ralph:in-progress"),
81527
+ done: exports_external.string().default("ralph:done"),
81528
+ error: exports_external.string().default("ralph:error")
81529
+ }).strict().default({
81530
+ inProgress: "ralph:in-progress",
81531
+ done: "ralph:done",
81532
+ error: "ralph:error"
81533
+ })
81534
+ }).strict().optional()
81449
81535
  }).strict().optional(),
81450
81536
  agent: exports_external.object({
81451
81537
  engine: exports_external.enum(["claude", "codex"]).optional(),
@@ -81514,7 +81600,7 @@ var init_schema = __esm(() => {
81514
81600
  var FRONTMATTER_RE, DEFAULT_WORKFLOW_MD = `---
81515
81601
  # WORKFLOW.md schema version \u2014 managed by \`ralphy init\`. When a newer version
81516
81602
  # ships, re-running init migrates this file and fills in the new settings.
81517
- version: 6
81603
+ version: 8
81518
81604
 
81519
81605
  project:
81520
81606
  name: ralphy
@@ -81566,6 +81652,11 @@ prDraft: true
81566
81652
  prBaseBranch: main
81567
81653
  stackPrsOnDependencies: false
81568
81654
  autoMergeStrategy: squash
81655
+ # Labels attached to every PR Ralph opens (best-effort \u2014 a missing label is
81656
+ # logged, never fatal). The labels must already exist in the repo.
81657
+ # prLabels:
81658
+ # - ralph
81659
+ # - automated
81569
81660
 
81570
81661
  prRecovery:
81571
81662
  enabled: true
@@ -81581,6 +81672,16 @@ preExistingErrorCheck:
81581
81672
  label: "ralph:pre-existing-error"
81582
81673
  outputCharLimit: 4000
81583
81674
 
81675
+ tracker:
81676
+ kind: linear
81677
+
81678
+ github:
81679
+ issues:
81680
+ statusLabels:
81681
+ inProgress: "ralph:in-progress"
81682
+ done: "ralph:done"
81683
+ error: "ralph:error"
81684
+
81584
81685
  linear:
81585
81686
  # Global filter ANDed into every Linear query (and the GitHub PR searches
81586
81687
  # rooted at those issues). A marker list of \`assignee\` and \`label\` clauses;
@@ -82288,6 +82389,9 @@ function applyAliases(cfg) {
82288
82389
  if (cfg.github.auto_merge_strategy !== undefined && cfg.autoMergeStrategy === "squash") {
82289
82390
  cfg.autoMergeStrategy = cfg.github.auto_merge_strategy;
82290
82391
  }
82392
+ if (cfg.github.pr_labels !== undefined && cfg.prLabels.length === 0) {
82393
+ cfg.prLabels = cfg.github.pr_labels;
82394
+ }
82291
82395
  }
82292
82396
  if (cfg.agent) {
82293
82397
  if (cfg.agent.engine !== undefined)
@@ -83405,6 +83509,12 @@ function buildFromAnswers(mode, answers, build = buildWorkflowMarkdown) {
83405
83509
  }
83406
83510
  }
83407
83511
  }
83512
+ if (values2["tracker.kind"] !== "github") {
83513
+ for (const id of Object.keys(values2)) {
83514
+ if (id.startsWith("github.issues."))
83515
+ delete values2[id];
83516
+ }
83517
+ }
83408
83518
  const linkRepo = values2[REPO_LINK_FIELD_ID] === true;
83409
83519
  delete values2[REPO_LINK_FIELD_ID];
83410
83520
  if (!linkRepo) {
@@ -84585,6 +84695,24 @@ var init_migrations = __esm(() => {
84585
84695
  version: 6,
84586
84696
  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
84697
  fields: ["prRecovery.enabled", "prRecovery.maxRecoverySessions", "prRecovery.ignoreChecks"]
84698
+ },
84699
+ {
84700
+ version: 7,
84701
+ 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.",
84702
+ fields: [
84703
+ "tracker.kind",
84704
+ "github.issues.repo",
84705
+ "github.issues.label",
84706
+ "github.issues.assignee",
84707
+ "github.issues.statusLabels.inProgress",
84708
+ "github.issues.statusLabels.done",
84709
+ "github.issues.statusLabels.error"
84710
+ ]
84711
+ },
84712
+ {
84713
+ version: 8,
84714
+ description: "Pull requests Ralph opens can now carry GitHub labels via `prLabels`. " + "List the labels to attach to every PR (they must already exist in the " + "repo; a missing label is skipped, never fatal). Only asked when " + "PR-creation is enabled \u2014 leave empty to attach no labels.",
84715
+ fields: ["prLabels"]
84588
84716
  }
84589
84717
  ];
84590
84718
  LATEST_MIGRATION_VERSION = MIGRATIONS.reduce((max2, migration) => Math.max(max2, migration.version), 0);
@@ -85156,7 +85284,7 @@ function projectLayout(root) {
85156
85284
  stateFile: (name) => join7(statesDir, name, STATE_FILE)
85157
85285
  };
85158
85286
  }
85159
- var STATE_FILE = ".ralph-state.json", GAVEUP_COUNT_FILE = ".ralph-gaveup-count";
85287
+ var STATE_FILE = ".ralph-state.json";
85160
85288
  var init_layout = __esm(() => {
85161
85289
  init_context();
85162
85290
  });
@@ -99623,6 +99751,39 @@ var init_example_machine = __esm(() => {
99623
99751
  });
99624
99752
 
99625
99753
  // packages/core/src/machines/flow.machine.ts
99754
+ function recordDetection(reason) {
99755
+ return ({
99756
+ context,
99757
+ event
99758
+ }) => {
99759
+ const previous = context.data.recovery;
99760
+ return {
99761
+ data: {
99762
+ ...context.data,
99763
+ recovery: {
99764
+ attempts: (previous?.attempts ?? 0) + 1,
99765
+ lastReason: reason,
99766
+ firstFailedAt: previous?.firstFailedAt ?? event.at ?? ""
99767
+ }
99768
+ }
99769
+ };
99770
+ };
99771
+ }
99772
+ function reachesQuarantine({ context }) {
99773
+ const max2 = context.data.maxRecoveryAttempts;
99774
+ return max2 > 0 && (context.data.recovery?.attempts ?? 0) + 1 >= max2;
99775
+ }
99776
+ function refreshReason(reason) {
99777
+ return ({ context }) => ({
99778
+ data: {
99779
+ ...context.data,
99780
+ recovery: context.data.recovery ? { ...context.data.recovery, lastReason: reason } : { attempts: 0, lastReason: reason, firstFailedAt: "" }
99781
+ }
99782
+ });
99783
+ }
99784
+ function clearRecovery({ context }) {
99785
+ return { data: { ...context.data, recovery: undefined } };
99786
+ }
99626
99787
  var preemptionActorLogic, flowMachine;
99627
99788
  var init_flow_machine = __esm(() => {
99628
99789
  init_xstate_development_cjs();
@@ -99669,14 +99830,20 @@ var init_flow_machine = __esm(() => {
99669
99830
  }).createMachine({
99670
99831
  id: "flow",
99671
99832
  context: ({ input }) => ({
99672
- issueId: input?.issueId ?? "",
99673
- bus: input?.bus ?? createNoopBus(),
99674
- persist: input?.persist ?? (() => {}),
99675
- graceMs: input?.graceMs ?? 5000,
99676
- worker: undefined,
99677
- teardown: undefined,
99678
- currentAssignment: undefined,
99679
- pendingAssignment: undefined
99833
+ data: {
99834
+ issueId: input?.issueId ?? "",
99835
+ graceMs: input?.graceMs ?? 5000,
99836
+ maxRecoveryAttempts: input?.maxRecoveryAttempts ?? 0,
99837
+ currentAssignment: undefined,
99838
+ pendingAssignment: undefined,
99839
+ recovery: undefined
99840
+ },
99841
+ runtime: {
99842
+ bus: input?.bus ?? createNoopBus(),
99843
+ persist: input?.persist ?? (() => {}),
99844
+ worker: undefined,
99845
+ teardown: undefined
99846
+ }
99680
99847
  }),
99681
99848
  initial: "idle",
99682
99849
  states: {
@@ -99685,29 +99852,63 @@ var init_flow_machine = __esm(() => {
99685
99852
  FRESH_PICKED_UP: "working",
99686
99853
  RESUME_DETECTED: "working",
99687
99854
  REVIEW_TRIGGERED: "review",
99688
- CONFLICT_DETECTED: "conflict-fix",
99689
- CI_FAILED_DETECTED: "ci-fix"
99855
+ CONFLICT_DETECTED: [
99856
+ {
99857
+ guard: reachesQuarantine,
99858
+ target: "quarantined",
99859
+ actions: import_xstate_development_cjs.assign(recordDetection("conflicting"))
99860
+ },
99861
+ { target: "conflict-fix", actions: import_xstate_development_cjs.assign(recordDetection("conflicting")) }
99862
+ ],
99863
+ CI_FAILED_DETECTED: [
99864
+ {
99865
+ guard: reachesQuarantine,
99866
+ target: "quarantined",
99867
+ actions: import_xstate_development_cjs.assign(recordDetection("ci_failed"))
99868
+ },
99869
+ { target: "ci-fix", actions: import_xstate_development_cjs.assign(recordDetection("ci_failed")) }
99870
+ ]
99690
99871
  }
99691
99872
  },
99692
99873
  working: {
99693
99874
  on: {
99694
99875
  AWAITING_DETECTED: "awaiting",
99695
- CONFLICT_DETECTED: "conflict-fix",
99696
- CI_FAILED_DETECTED: "ci-fix",
99876
+ CONFLICT_DETECTED: [
99877
+ {
99878
+ guard: reachesQuarantine,
99879
+ target: "quarantined",
99880
+ actions: import_xstate_development_cjs.assign(recordDetection("conflicting"))
99881
+ },
99882
+ { target: "conflict-fix", actions: import_xstate_development_cjs.assign(recordDetection("conflicting")) }
99883
+ ],
99884
+ CI_FAILED_DETECTED: [
99885
+ {
99886
+ guard: reachesQuarantine,
99887
+ target: "quarantined",
99888
+ actions: import_xstate_development_cjs.assign(recordDetection("ci_failed"))
99889
+ },
99890
+ { target: "ci-fix", actions: import_xstate_development_cjs.assign(recordDetection("ci_failed")) }
99891
+ ],
99697
99892
  PR_OPENED: "awaiting-ci",
99698
99893
  WORKER_SUCCEEDED: "done",
99699
99894
  WORKER_FAILED: "error",
99700
99895
  PREEMPT: {
99701
99896
  target: "preempting",
99702
99897
  actions: import_xstate_development_cjs.assign({
99703
- pendingAssignment: ({ event }) => event.newAssignment
99898
+ data: ({ context, event }) => ({
99899
+ ...context.data,
99900
+ pendingAssignment: event.newAssignment
99901
+ })
99704
99902
  })
99705
99903
  },
99706
99904
  WORKER_SPAWNED: {
99707
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99708
- worker: event.worker,
99709
- teardown: event.teardown ?? undefined,
99710
- currentAssignment: event.assignment
99905
+ actions: import_xstate_development_cjs.assign(({ context, event }) => ({
99906
+ data: { ...context.data, currentAssignment: event.assignment },
99907
+ runtime: {
99908
+ ...context.runtime,
99909
+ worker: event.worker,
99910
+ teardown: event.teardown ?? undefined
99911
+ }
99711
99912
  }))
99712
99913
  }
99713
99914
  }
@@ -99719,14 +99920,20 @@ var init_flow_machine = __esm(() => {
99719
99920
  PREEMPT: {
99720
99921
  target: "preempting",
99721
99922
  actions: import_xstate_development_cjs.assign({
99722
- pendingAssignment: ({ event }) => event.newAssignment
99923
+ data: ({ context, event }) => ({
99924
+ ...context.data,
99925
+ pendingAssignment: event.newAssignment
99926
+ })
99723
99927
  })
99724
99928
  },
99725
99929
  WORKER_SPAWNED: {
99726
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99727
- worker: event.worker,
99728
- teardown: event.teardown ?? undefined,
99729
- currentAssignment: event.assignment
99930
+ actions: import_xstate_development_cjs.assign(({ context, event }) => ({
99931
+ data: { ...context.data, currentAssignment: event.assignment },
99932
+ runtime: {
99933
+ ...context.runtime,
99934
+ worker: event.worker,
99935
+ teardown: event.teardown ?? undefined
99936
+ }
99730
99937
  }))
99731
99938
  }
99732
99939
  }
@@ -99738,14 +99945,20 @@ var init_flow_machine = __esm(() => {
99738
99945
  PREEMPT: {
99739
99946
  target: "preempting",
99740
99947
  actions: import_xstate_development_cjs.assign({
99741
- pendingAssignment: ({ event }) => event.newAssignment
99948
+ data: ({ context, event }) => ({
99949
+ ...context.data,
99950
+ pendingAssignment: event.newAssignment
99951
+ })
99742
99952
  })
99743
99953
  },
99744
99954
  WORKER_SPAWNED: {
99745
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99746
- worker: event.worker,
99747
- teardown: event.teardown ?? undefined,
99748
- currentAssignment: event.assignment
99955
+ actions: import_xstate_development_cjs.assign(({ context, event }) => ({
99956
+ data: { ...context.data, currentAssignment: event.assignment },
99957
+ runtime: {
99958
+ ...context.runtime,
99959
+ worker: event.worker,
99960
+ teardown: event.teardown ?? undefined
99961
+ }
99749
99962
  }))
99750
99963
  }
99751
99964
  }
@@ -99756,7 +99969,10 @@ var init_flow_machine = __esm(() => {
99756
99969
  PREEMPT: {
99757
99970
  target: "preempting",
99758
99971
  actions: import_xstate_development_cjs.assign({
99759
- pendingAssignment: ({ event }) => event.newAssignment
99972
+ data: ({ context, event }) => ({
99973
+ ...context.data,
99974
+ pendingAssignment: event.newAssignment
99975
+ })
99760
99976
  })
99761
99977
  }
99762
99978
  }
@@ -99764,20 +99980,41 @@ var init_flow_machine = __esm(() => {
99764
99980
  "awaiting-ci": {
99765
99981
  on: {
99766
99982
  PR_PASSED: "done",
99767
- CONFLICT_DETECTED: "conflict-fix",
99768
- CI_FAILED_DETECTED: "ci-fix",
99983
+ RECOVERY_CLEARED: { actions: import_xstate_development_cjs.assign(clearRecovery) },
99984
+ CONFLICT_DETECTED: [
99985
+ {
99986
+ guard: reachesQuarantine,
99987
+ target: "quarantined",
99988
+ actions: import_xstate_development_cjs.assign(recordDetection("conflicting"))
99989
+ },
99990
+ { target: "conflict-fix", actions: import_xstate_development_cjs.assign(recordDetection("conflicting")) }
99991
+ ],
99992
+ CI_FAILED_DETECTED: [
99993
+ {
99994
+ guard: reachesQuarantine,
99995
+ target: "quarantined",
99996
+ actions: import_xstate_development_cjs.assign(recordDetection("ci_failed"))
99997
+ },
99998
+ { target: "ci-fix", actions: import_xstate_development_cjs.assign(recordDetection("ci_failed")) }
99999
+ ],
99769
100000
  REVIEW_TRIGGERED: "review",
99770
100001
  PREEMPT: {
99771
100002
  target: "preempting",
99772
100003
  actions: import_xstate_development_cjs.assign({
99773
- pendingAssignment: ({ event }) => event.newAssignment
100004
+ data: ({ context, event }) => ({
100005
+ ...context.data,
100006
+ pendingAssignment: event.newAssignment
100007
+ })
99774
100008
  })
99775
100009
  },
99776
100010
  WORKER_SPAWNED: {
99777
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99778
- worker: event.worker,
99779
- teardown: event.teardown ?? undefined,
99780
- currentAssignment: event.assignment
100011
+ actions: import_xstate_development_cjs.assign(({ context, event }) => ({
100012
+ data: { ...context.data, currentAssignment: event.assignment },
100013
+ runtime: {
100014
+ ...context.runtime,
100015
+ worker: event.worker,
100016
+ teardown: event.teardown ?? undefined
100017
+ }
99781
100018
  }))
99782
100019
  }
99783
100020
  }
@@ -99788,10 +100025,13 @@ var init_flow_machine = __esm(() => {
99788
100025
  PR_OPENED: "awaiting-ci",
99789
100026
  WORKER_FAILED: "error",
99790
100027
  WORKER_SPAWNED: {
99791
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99792
- worker: event.worker,
99793
- teardown: event.teardown ?? undefined,
99794
- currentAssignment: event.assignment
100028
+ actions: import_xstate_development_cjs.assign(({ context, event }) => ({
100029
+ data: { ...context.data, currentAssignment: event.assignment },
100030
+ runtime: {
100031
+ ...context.runtime,
100032
+ worker: event.worker,
100033
+ teardown: event.teardown ?? undefined
100034
+ }
99795
100035
  }))
99796
100036
  }
99797
100037
  }
@@ -99800,20 +100040,19 @@ var init_flow_machine = __esm(() => {
99800
100040
  invoke: {
99801
100041
  src: "preemption",
99802
100042
  input: ({ context }) => ({
99803
- graceMs: context.graceMs,
99804
- bus: context.bus,
99805
- persist: context.persist,
99806
- issueId: context.issueId,
99807
- newAssignment: context.pendingAssignment,
99808
- ...context.currentAssignment !== undefined ? { from: context.currentAssignment.flowId } : {},
99809
- ...context.worker !== undefined ? { worker: context.worker } : {},
99810
- ...context.teardown !== undefined ? { teardown: context.teardown } : {}
100043
+ graceMs: context.data.graceMs,
100044
+ bus: context.runtime.bus,
100045
+ persist: context.runtime.persist,
100046
+ issueId: context.data.issueId,
100047
+ newAssignment: context.data.pendingAssignment,
100048
+ ...context.data.currentAssignment !== undefined ? { from: context.data.currentAssignment.flowId } : {},
100049
+ ...context.runtime.worker !== undefined ? { worker: context.runtime.worker } : {},
100050
+ ...context.runtime.teardown !== undefined ? { teardown: context.runtime.teardown } : {}
99811
100051
  }),
99812
100052
  onDone: {
99813
100053
  actions: import_xstate_development_cjs.assign(({ context }) => ({
99814
- worker: undefined,
99815
- teardown: undefined,
99816
- currentAssignment: context.pendingAssignment
100054
+ data: { ...context.data, currentAssignment: context.data.pendingAssignment },
100055
+ runtime: { ...context.runtime, worker: undefined, teardown: undefined }
99817
100056
  })),
99818
100057
  target: "routing-after-preempt"
99819
100058
  },
@@ -99823,32 +100062,40 @@ var init_flow_machine = __esm(() => {
99823
100062
  "routing-after-preempt": {
99824
100063
  always: [
99825
100064
  {
99826
- guard: ({ context }) => context.pendingAssignment?.flowId === "conflict-fix",
100065
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "conflict-fix",
99827
100066
  target: "conflict-fix"
99828
100067
  },
99829
100068
  {
99830
- guard: ({ context }) => context.pendingAssignment?.flowId === "ci-fix",
100069
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "ci-fix",
99831
100070
  target: "ci-fix"
99832
100071
  },
99833
100072
  {
99834
- guard: ({ context }) => context.pendingAssignment?.flowId === "awaiting-ci",
100073
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "awaiting-ci",
99835
100074
  target: "awaiting-ci"
99836
100075
  },
99837
100076
  {
99838
- guard: ({ context }) => context.pendingAssignment?.flowId === "confirmation",
100077
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "confirmation",
99839
100078
  target: "awaiting"
99840
100079
  },
99841
100080
  {
99842
- guard: ({ context }) => context.pendingAssignment?.flowId === "review-followup",
100081
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "review-followup",
99843
100082
  target: "review"
99844
100083
  },
99845
100084
  {
99846
- guard: ({ context }) => context.pendingAssignment?.flowId === "idle",
100085
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "idle",
99847
100086
  target: "idle"
99848
100087
  },
99849
100088
  { target: "working" }
99850
100089
  ]
99851
100090
  },
100091
+ quarantined: {
100092
+ on: {
100093
+ PR_PASSED: "done",
100094
+ QUARANTINE_CLEARED: { target: "idle", actions: import_xstate_development_cjs.assign(clearRecovery) },
100095
+ CONFLICT_DETECTED: { actions: import_xstate_development_cjs.assign(refreshReason("conflicting")) },
100096
+ CI_FAILED_DETECTED: { actions: import_xstate_development_cjs.assign(refreshReason("ci_failed")) }
100097
+ }
100098
+ },
99852
100099
  done: {
99853
100100
  type: "final"
99854
100101
  },
@@ -99877,17 +100124,22 @@ class FlowActorStore {
99877
100124
  ...this.deps ? {
99878
100125
  bus: this.deps.bus,
99879
100126
  persist: this.deps.persist,
99880
- ...this.deps.graceMs !== undefined ? { graceMs: this.deps.graceMs } : {}
100127
+ ...this.deps.graceMs !== undefined ? { graceMs: this.deps.graceMs } : {},
100128
+ ...this.deps.maxRecoveryAttempts !== undefined ? { maxRecoveryAttempts: this.deps.maxRecoveryAttempts } : {}
99881
100129
  } : {}
99882
100130
  };
100131
+ const inspector = this.deps?.onTransition ? this.makeInspect(key, changeDir) : null;
99883
100132
  if (changeDir) {
99884
100133
  const snapshot = await this.loadSnapshot(changeDir);
99885
100134
  if (snapshot !== null && this.isValidSnapshot(snapshot)) {
99886
100135
  try {
100136
+ const restored = this.withRestoredRuntime(snapshot);
99887
100137
  const a2 = import_xstate_development_cjs.createActor(this.machine, {
99888
- snapshot,
99889
- input
100138
+ snapshot: restored,
100139
+ input,
100140
+ ...inspector ? { inspect: inspector.inspect } : {}
99890
100141
  });
100142
+ inspector?.setRoot(a2);
99891
100143
  a2.start();
99892
100144
  if (a2.getSnapshot().value !== undefined) {
99893
100145
  this.actors.set(key, a2);
@@ -99899,11 +100151,61 @@ class FlowActorStore {
99899
100151
  } catch {}
99900
100152
  }
99901
100153
  }
99902
- const a = import_xstate_development_cjs.createActor(this.machine, { input });
100154
+ const a = import_xstate_development_cjs.createActor(this.machine, {
100155
+ input,
100156
+ ...inspector ? { inspect: inspector.inspect } : {}
100157
+ });
100158
+ inspector?.setRoot(a);
99903
100159
  a.start();
99904
100160
  this.actors.set(key, a);
99905
100161
  return a;
99906
100162
  }
100163
+ buildRuntime() {
100164
+ return {
100165
+ bus: this.deps?.bus ?? createNoopBus(),
100166
+ persist: this.deps?.persist ?? (() => {}),
100167
+ worker: undefined,
100168
+ teardown: undefined
100169
+ };
100170
+ }
100171
+ withRestoredRuntime(snapshot) {
100172
+ if (!snapshot || typeof snapshot !== "object")
100173
+ return snapshot;
100174
+ const snap = snapshot;
100175
+ const context = snap.context ?? {};
100176
+ const data = context.data && typeof context.data === "object" ? context.data : {
100177
+ issueId: context.issueId,
100178
+ graceMs: context.graceMs ?? 5000,
100179
+ maxRecoveryAttempts: this.deps?.maxRecoveryAttempts ?? 0,
100180
+ currentAssignment: context.currentAssignment,
100181
+ pendingAssignment: context.pendingAssignment,
100182
+ recovery: undefined
100183
+ };
100184
+ return { ...snap, context: { data, runtime: this.buildRuntime() } };
100185
+ }
100186
+ makeInspect(issueId, changeDir) {
100187
+ let root;
100188
+ let previous;
100189
+ const inspect = (event) => {
100190
+ if (event.type !== "@xstate.snapshot" || event.actorRef !== root)
100191
+ return;
100192
+ const value = event.snapshot.value;
100193
+ const to = typeof value === "string" ? value : JSON.stringify(value);
100194
+ const eventType = event.event.type ?? "?";
100195
+ if (previous !== undefined && previous !== to) {
100196
+ try {
100197
+ this.deps?.onTransition?.(issueId, changeDir, { from: previous, event: eventType, to });
100198
+ } catch {}
100199
+ }
100200
+ previous = to;
100201
+ };
100202
+ return {
100203
+ inspect,
100204
+ setRoot: (actor) => {
100205
+ root = actor;
100206
+ }
100207
+ };
100208
+ }
99907
100209
  peekActor(key) {
99908
100210
  return this.actors.get(key) ?? null;
99909
100211
  }
@@ -99959,6 +100261,7 @@ var init_flow_actor_store = __esm(() => {
99959
100261
  init_xstate_development_cjs();
99960
100262
  init_flow_machine();
99961
100263
  init_store();
100264
+ init_src2();
99962
100265
  });
99963
100266
 
99964
100267
  // packages/core/src/machines/loop.machine.ts
@@ -100164,7 +100467,7 @@ function normalizeNewlyAppendedSectionWithReport(previous, current) {
100164
100467
  });
100165
100468
  return { text: count > 0 ? out.join("") : current, headings, count };
100166
100469
  }
100167
- var MISSION_TASKS_FILENAME = "tasks.md", AGENT_TASKS_FILENAME = "agent-tasks.md", FLOW_TASK_HEADING_PREFIXES;
100470
+ var MISSION_TASKS_FILENAME = "tasks.md", AGENT_TASKS_FILENAME = "agent-tasks.md", HANDOFF_FILENAME = "handoff.md", FLOW_TASK_HEADING_PREFIXES;
100168
100471
  var init_tasks_md = __esm(() => {
100169
100472
  FLOW_TASK_HEADING_PREFIXES = [
100170
100473
  "Fix failing CI checks",
@@ -100417,6 +100720,20 @@ function buildTaskPrompt(state, taskDir, reviewPhase) {
100417
100720
  `;
100418
100721
  }
100419
100722
  }
100723
+ const handoffContent = storage.read(join15(taskDir, HANDOFF_FILENAME));
100724
+ if (handoffContent !== null && handoffContent.trim()) {
100725
+ prompt += `---
100726
+ `;
100727
+ prompt += `# Previous Iteration Handoff (context from the last iteration)
100728
+
100729
+ `;
100730
+ prompt += handoffContent.trim() + `
100731
+
100732
+ `;
100733
+ prompt += `---
100734
+
100735
+ `;
100736
+ }
100420
100737
  const agentTasksPath = join15(taskDir, AGENT_TASKS_FILENAME);
100421
100738
  const missionTasksPath = join15(taskDir, MISSION_TASKS_FILENAME);
100422
100739
  const agentTasksContent = storage.read(agentTasksPath);
@@ -100564,6 +100881,22 @@ When all tasks are complete and all files are committed, push your branch and op
100564
100881
  prompt += `Use the change name as the PR title and write a concise summary of the implementation in the body.
100565
100882
  `;
100566
100883
  }
100884
+ const handoffPath = join15(taskDir, HANDOFF_FILENAME);
100885
+ prompt += `
100886
+ ---
100887
+
100888
+ ## Write Handoff (do this LAST, before you finish)
100889
+ `;
100890
+ prompt += `Before ending this iteration, record a handoff for the next iteration:
100891
+ `;
100892
+ prompt += `- If a \`handoff\` skill is available, invoke it; otherwise write the document yourself.
100893
+ `;
100894
+ prompt += `- Save it to \`${handoffPath}\` (OVERWRITE the existing file \u2014 do NOT append, do NOT save to a temp dir).
100895
+ `;
100896
+ 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.
100897
+ `;
100898
+ prompt += `- This file is loop scratch \u2014 do NOT commit it.
100899
+ `;
100567
100900
  return prompt;
100568
100901
  }
100569
100902
  function buildSteeringBlock(taskDir) {
@@ -102600,13 +102933,91 @@ class PollContext {
102600
102933
  }
102601
102934
  }
102602
102935
 
102603
- // apps/agent/src/shared/utils/ralph-comment.ts
102604
- function isRalphComment(body) {
102936
+ // packages/comms/src/index.ts
102937
+ function sanitizeMarkerValue(value) {
102938
+ return value.replace(/--+>?/g, "-").replace(/\s+/g, "_").trim();
102939
+ }
102940
+ function buildRalphyMarker(type, fields) {
102941
+ const pairs = [`v=${RALPHY_MARKER_VERSION}`, `type=${type}`];
102942
+ for (const [key, raw] of Object.entries(fields ?? {})) {
102943
+ if (raw === undefined || raw === null || raw === "")
102944
+ continue;
102945
+ pairs.push(`${key}=${sanitizeMarkerValue(String(raw))}`);
102946
+ }
102947
+ return `<!-- ralphy:${pairs.join(" ")} -->`;
102948
+ }
102949
+ function parseRalphyMarker(body) {
102950
+ const match = /<!--\s*ralphy:([^>]*?\btype=[^>]*?)\s*-->/.exec(body);
102951
+ if (!match)
102952
+ return null;
102953
+ const fields = {};
102954
+ let type = "";
102955
+ let version3 = 0;
102956
+ for (const token of match[1].trim().split(/\s+/)) {
102957
+ const eq = token.indexOf("=");
102958
+ if (eq < 0)
102959
+ continue;
102960
+ const key = token.slice(0, eq);
102961
+ const value = token.slice(eq + 1);
102962
+ if (key === "v")
102963
+ version3 = Number(value) || 0;
102964
+ else if (key === "type")
102965
+ type = value;
102966
+ else
102967
+ fields[key] = value;
102968
+ }
102969
+ if (!type)
102970
+ return null;
102971
+ return { version: version3, type, fields };
102972
+ }
102973
+ function buildRalphyComment(input) {
102974
+ const lines = [`${RALPHY_TITLE_PREFIX}${input.action}`];
102975
+ const body = input.body?.trim();
102976
+ if (body)
102977
+ lines.push("", body);
102978
+ lines.push("", buildRalphyMarker(input.type, input.fields));
102979
+ return lines.join(`
102980
+ `);
102981
+ }
102982
+ function isRalphyComment(body) {
102605
102983
  const trimmed = body.trimStart();
102606
- if (/^(\uD83E\uDD16|\uD83D\uDD04|\u2705|\u2717|\u274C|\u26A0|\uD83D\uDD01|\uD83D\uDCCB|\u23F0)\s*Ralphy?\b/.test(trimmed))
102984
+ if (trimmed.startsWith(RALPHY_BRAND))
102985
+ return true;
102986
+ if (parseRalphyMarker(body))
102607
102987
  return true;
102608
- return /^\uD83D\uDC40\s*(Got it\b|Acknowledged\b)/.test(trimmed);
102988
+ if (LEGACY_RALPH_LEAD.test(trimmed))
102989
+ return true;
102990
+ return LEGACY_MENTION_ACK.test(trimmed);
102609
102991
  }
102992
+ function isPickupComment(body) {
102993
+ if (parseRalphyMarker(body)?.type === "review-pickup")
102994
+ return true;
102995
+ return /^\uD83D\uDD01\s*Ralph picked up/u.test(body.trimStart());
102996
+ }
102997
+ function isStartedComment(body) {
102998
+ if (parseRalphyMarker(body)?.type === "started")
102999
+ return true;
103000
+ return /^\uD83E\uDD16\s*Ralph started working/u.test(body.trimStart());
103001
+ }
103002
+ function isMentionAckComment(body) {
103003
+ if (parseRalphyMarker(body)?.type === "mention-ack")
103004
+ return true;
103005
+ return LEGACY_MENTION_ACK.test(body.trimStart());
103006
+ }
103007
+ var RALPHY_BRAND = "\uD83E\uDD16 Ralphy", RALPHY_TITLE_PREFIX, RALPHY_MARKER_VERSION = 1, LEGACY_RALPH_LEAD, LEGACY_MENTION_ACK;
103008
+ var init_src8 = __esm(() => {
103009
+ RALPHY_TITLE_PREFIX = `${RALPHY_BRAND} \xB7 `;
103010
+ LEGACY_RALPH_LEAD = /^(\uD83E\uDD16|\uD83D\uDD04|\u2705|\u2717|\u274C|\u26A0\uFE0F?|\uD83D\uDD01|\uD83D\uDCCB|\u23F0|\u2139\uFE0F)\s*Ralphy?\b/u;
103011
+ LEGACY_MENTION_ACK = /^\uD83D\uDC40\s*(Got it\b|Acknowledged\b)/u;
103012
+ });
103013
+
103014
+ // apps/agent/src/shared/utils/ralph-comment.ts
103015
+ function isRalphComment(body) {
103016
+ return isRalphyComment(body);
103017
+ }
103018
+ var init_ralph_comment = __esm(() => {
103019
+ init_src8();
103020
+ });
102610
103021
 
102611
103022
  // apps/agent/src/shared/capabilities/linear-client.ts
102612
103023
  var exports_linear_client = {};
@@ -102707,6 +103118,9 @@ function formatTicketError(err) {
102707
103118
  parts.push(`configured team: ${e.team}`);
102708
103119
  return parts.length > 0 ? `${e.message} (${parts.join(", ")})` : e.message;
102709
103120
  }
103121
+ function openBlockersFromInverse(nodes) {
103122
+ return (nodes ?? []).filter((r) => r.type === "blocks" && !DONE_BLOCKER_STATE_TYPES.has(r.issue.state.type)).map((r) => ({ id: r.issue.id, identifier: r.issue.identifier }));
103123
+ }
102710
103124
  function partition2(markers) {
102711
103125
  const statuses = [];
102712
103126
  const labels = [];
@@ -102926,8 +103340,8 @@ async function fetchMentionScanIssues(apiKey, spec) {
102926
103340
  project { id name priority }
102927
103341
  projectMilestone { id name sortOrder targetDate }
102928
103342
  labels { nodes { name } }
102929
- relations(first: 50) {
102930
- nodes { type relatedIssue { id identifier state { type } } }
103343
+ inverseRelations(first: 50) {
103344
+ nodes { type issue { id identifier state { type } } }
102931
103345
  }
102932
103346
  comments(first: 50) {
102933
103347
  nodes { id body createdAt user { name email } }
@@ -102938,24 +103352,26 @@ async function fetchMentionScanIssues(apiKey, spec) {
102938
103352
  const data = await linearRequest(apiKey, query, {
102939
103353
  filter: where
102940
103354
  });
102941
- const DONE_STATE_TYPES = new Set(["completed", "cancelled"]);
102942
- return data.issues.nodes.map((n) => ({
102943
- id: n.id,
102944
- identifier: n.identifier,
102945
- title: n.title,
102946
- description: n.description,
102947
- url: n.url,
102948
- state: n.state,
102949
- assignee: n.assignee,
102950
- project: mapNodeProject(n),
102951
- ...milestoneSpread(n),
102952
- labels: n.labels.nodes.map((l) => l.name),
102953
- priority: n.priority,
102954
- createdAt: n.createdAt ?? "",
102955
- blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id),
102956
- blockedByIdentifiers: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.identifier),
102957
- comments: n.comments?.nodes ?? []
102958
- }));
103355
+ return data.issues.nodes.map((n) => {
103356
+ const blockers = openBlockersFromInverse(n.inverseRelations?.nodes);
103357
+ return {
103358
+ id: n.id,
103359
+ identifier: n.identifier,
103360
+ title: n.title,
103361
+ description: n.description,
103362
+ url: n.url,
103363
+ state: n.state,
103364
+ assignee: n.assignee,
103365
+ project: mapNodeProject(n),
103366
+ ...milestoneSpread(n),
103367
+ labels: n.labels.nodes.map((l) => l.name),
103368
+ priority: n.priority,
103369
+ createdAt: n.createdAt ?? "",
103370
+ blockedByIds: blockers.map((b) => b.id),
103371
+ blockedByIdentifiers: blockers.map((b) => b.identifier),
103372
+ comments: n.comments?.nodes ?? []
103373
+ };
103374
+ });
102959
103375
  }
102960
103376
  async function fetchOpenIssues(apiKey, spec, options) {
102961
103377
  const where = buildIssueFilter(spec);
@@ -102972,10 +103388,10 @@ async function fetchOpenIssues(apiKey, spec, options) {
102972
103388
  project { id name priority }
102973
103389
  projectMilestone { id name sortOrder targetDate }
102974
103390
  labels { nodes { name } }
102975
- relations(first: 50) {
103391
+ inverseRelations(first: 50) {
102976
103392
  nodes {
102977
103393
  type
102978
- relatedIssue { id identifier state { type } }
103394
+ issue { id identifier state { type } }
102979
103395
  }
102980
103396
  }
102981
103397
  ${commentsSlice}
@@ -102985,24 +103401,26 @@ async function fetchOpenIssues(apiKey, spec, options) {
102985
103401
  const data = await linearRequest(apiKey, query, {
102986
103402
  filter: where
102987
103403
  });
102988
- const DONE_STATE_TYPES = new Set(["completed", "cancelled"]);
102989
- return data.issues.nodes.map((n) => ({
102990
- id: n.id,
102991
- identifier: n.identifier,
102992
- title: n.title,
102993
- description: n.description,
102994
- url: n.url,
102995
- state: n.state,
102996
- assignee: n.assignee,
102997
- project: mapNodeProject(n),
102998
- ...milestoneSpread(n),
102999
- labels: n.labels.nodes.map((l) => l.name),
103000
- priority: n.priority,
103001
- createdAt: n.createdAt ?? "",
103002
- blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id),
103003
- blockedByIdentifiers: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.identifier),
103004
- ...includeComments ? { comments: n.comments?.nodes ?? [] } : {}
103005
- }));
103404
+ return data.issues.nodes.map((n) => {
103405
+ const blockers = openBlockersFromInverse(n.inverseRelations?.nodes);
103406
+ return {
103407
+ id: n.id,
103408
+ identifier: n.identifier,
103409
+ title: n.title,
103410
+ description: n.description,
103411
+ url: n.url,
103412
+ state: n.state,
103413
+ assignee: n.assignee,
103414
+ project: mapNodeProject(n),
103415
+ ...milestoneSpread(n),
103416
+ labels: n.labels.nodes.map((l) => l.name),
103417
+ priority: n.priority,
103418
+ createdAt: n.createdAt ?? "",
103419
+ blockedByIds: blockers.map((b) => b.id),
103420
+ blockedByIdentifiers: blockers.map((b) => b.identifier),
103421
+ ...includeComments ? { comments: n.comments?.nodes ?? [] } : {}
103422
+ };
103423
+ });
103006
103424
  }
103007
103425
  function isRetryableStatus(status) {
103008
103426
  return status >= 500 && status <= 599;
@@ -103316,17 +103734,15 @@ async function fetchBlockedByForIssues(apiKey, issueIds) {
103316
103734
  issues(filter: { id: { in: $ids } }, first: 250) {
103317
103735
  nodes {
103318
103736
  id
103319
- relations(first: 50) {
103320
- nodes { type relatedIssue { id identifier state { type } } }
103737
+ inverseRelations(first: 50) {
103738
+ nodes { type issue { id identifier state { type } } }
103321
103739
  }
103322
103740
  }
103323
103741
  }
103324
103742
  }`;
103325
103743
  const data = await linearRequest(apiKey, query, { ids: issueIds });
103326
- const DONE_STATE_TYPES = new Set(["completed", "cancelled"]);
103327
103744
  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);
103745
+ out.set(node2.id, openBlockersFromInverse(node2.inverseRelations?.nodes));
103330
103746
  }
103331
103747
  return out;
103332
103748
  }
@@ -103540,11 +103956,13 @@ async function removeLabelFromIssue(apiKey, issueId, labelId) {
103540
103956
  labelId
103541
103957
  });
103542
103958
  }
103543
- 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:";
103959
+ var LINEAR_API = "https://api.linear.app/graphql", TICKET_IDENTIFIER_RE, TICKET_BARE_NUMBER_RE, DONE_BLOCKER_STATE_TYPES, 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
103960
  var init_linear_client = __esm(() => {
103545
103961
  init_types2();
103962
+ init_ralph_comment();
103546
103963
  TICKET_IDENTIFIER_RE = /^([A-Za-z]+)-(\d+)(?:-.*)?$/;
103547
103964
  TICKET_BARE_NUMBER_RE = /^(\d+)$/;
103965
+ DONE_BLOCKER_STATE_TYPES = new Set(["completed", "cancelled"]);
103548
103966
  linearRequestInternals = {
103549
103967
  sleep: (ms) => Bun.sleep(ms)
103550
103968
  };
@@ -103651,7 +104069,7 @@ async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
103651
104069
  if (list.stdout.includes(`worktree ${cwd2}
103652
104070
  `)) {
103653
104071
  await installPrePushHook(cwd2, runner);
103654
- return { cwd: cwd2, branch };
104072
+ return { cwd: cwd2, branch, created: false };
103655
104073
  }
103656
104074
  let branchExists = true;
103657
104075
  try {
@@ -103662,12 +104080,12 @@ async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
103662
104080
  if (branchExists) {
103663
104081
  await runner.run(["worktree", "add", cwd2, branch], projectRoot);
103664
104082
  await installPrePushHook(cwd2, runner);
103665
- return { cwd: cwd2, branch };
104083
+ return { cwd: cwd2, branch, created: true };
103666
104084
  }
103667
104085
  await runner.run(["fetch", "origin", baseBranch], projectRoot);
103668
104086
  await runner.run(["worktree", "add", "-b", branch, cwd2, `origin/${baseBranch}`], projectRoot);
103669
104087
  await installPrePushHook(cwd2, runner);
103670
- return { cwd: cwd2, branch };
104088
+ return { cwd: cwd2, branch, created: true };
103671
104089
  }
103672
104090
  async function installPrePushHook(cwd2, runner) {
103673
104091
  const hookPath = join22(cwd2, ".ralph-hooks", "pre-push");
@@ -103885,7 +104303,7 @@ function stackedOnLine(stackedOn) {
103885
104303
  return `> \uD83E\uDD5E Stacked on ${prRef}${ticket} \u2014 review/merge that PR first. Base is its branch, not the default branch.`;
103886
104304
  }
103887
104305
  function defaultBody(issue2, branch, stackedOn) {
103888
- return [
104306
+ const detail = [
103889
104307
  `Auto-generated by Ralph for ${issue2.identifier}.`,
103890
104308
  stackedOn ? stackedOnLine(stackedOn) : "",
103891
104309
  `Source: ${issue2.url}`,
@@ -103896,6 +104314,12 @@ function defaultBody(issue2, branch, stackedOn) {
103896
104314
  ${issue2.description.trim()}` : ""
103897
104315
  ].filter(Boolean).join(`
103898
104316
  `);
104317
+ return buildRalphyComment({
104318
+ type: "pr-body",
104319
+ action: "opened PR",
104320
+ body: detail,
104321
+ fields: { issue: issue2.identifier }
104322
+ });
103899
104323
  }
103900
104324
  async function diffFilesAgainstBase(runner, cwd2, base2) {
103901
104325
  let raw = "";
@@ -103967,6 +104391,14 @@ async function branchAlreadyMerged(runner, cwd2, branch, base2) {
103967
104391
  } catch {}
103968
104392
  return false;
103969
104393
  }
104394
+ async function applyPrLabels(runner, cwd2, prRef, labels) {
104395
+ const clean = labels.map((l) => l.trim()).filter(Boolean);
104396
+ if (clean.length === 0 || !prRef)
104397
+ return;
104398
+ try {
104399
+ await runner.run(["gh", "pr", "edit", prRef, "--add-label", clean.join(",")], cwd2);
104400
+ } catch {}
104401
+ }
103970
104402
  async function createPullRequest(input, runner) {
103971
104403
  const base2 = input.base ?? "main";
103972
104404
  const log3 = await runner.run(["git", "log", "--oneline", `${base2}..HEAD`, "--no-merges"], input.cwd);
@@ -104003,8 +104435,10 @@ async function createPullRequest(input, runner) {
104003
104435
  ".[0].url // empty"
104004
104436
  ], input.cwd);
104005
104437
  const existingUrl = existing.stdout.trim();
104006
- if (existingUrl)
104438
+ if (existingUrl) {
104439
+ await applyPrLabels(runner, input.cwd, existingUrl, input.labels ?? []);
104007
104440
  return { url: existingUrl, created: false };
104441
+ }
104008
104442
  const title = defaultTitle(input.issue);
104009
104443
  const body = defaultBody(input.issue, input.branch, input.stackedOn);
104010
104444
  const createArgs = ["gh", "pr", "create", "--base", base2, "--title", title, "--body", body];
@@ -104013,9 +104447,12 @@ async function createPullRequest(input, runner) {
104013
104447
  const created = await runner.run(createArgs, input.cwd);
104014
104448
  const url2 = created.stdout.trim().split(`
104015
104449
  `).pop() ?? "";
104450
+ await applyPrLabels(runner, input.cwd, url2, input.labels ?? []);
104016
104451
  return { url: url2, created: true };
104017
104452
  }
104018
- var init_pr = () => {};
104453
+ var init_pr = __esm(() => {
104454
+ init_src8();
104455
+ });
104019
104456
 
104020
104457
  // apps/agent/src/shared/pr/ci-classify.ts
104021
104458
  async function runGhWithRetry(cmd, runner, cwd2, onRetry, sleep2 = (ms) => new Promise((r) => setTimeout(r, ms))) {
@@ -104564,7 +105001,7 @@ function emitFeatureSkipped(bus, id, reason) {
104564
105001
  var init_run_feature = () => {};
104565
105002
 
104566
105003
  // apps/agent/src/agent/post-task.ts
104567
- import { join as join23, dirname as dirname9 } from "path";
105004
+ import { join as join23 } from "path";
104568
105005
  function summarizeUncommittedStatus(stdout) {
104569
105006
  const lines = stdout.split(`
104570
105007
  `).filter((line) => line.length > 0);
@@ -104691,6 +105128,7 @@ async function createPrWithRetry(ctx, issue2) {
104691
105128
  base: base2,
104692
105129
  metaOnlyFiles: ctx.cfg.metaOnlyFiles ?? [],
104693
105130
  draft: ctx.cfg.prDraft ?? false,
105131
+ labels: ctx.cfg.prLabels ?? [],
104694
105132
  ...ctx.stackedOn ? {
104695
105133
  stackedOn: {
104696
105134
  prUrl: ctx.stackedOn.prUrl,
@@ -105062,17 +105500,6 @@ async function runValidateOnlyPhase(input, deps) {
105062
105500
  await reactivateState(stateFilePath, log3, changeName);
105063
105501
  return respawnWorker();
105064
105502
  }
105065
- async function recordGaveUp(stateFilePath, log3, changeName) {
105066
- const path = join23(dirname9(stateFilePath), GAVEUP_COUNT_FILE);
105067
- try {
105068
- const file2 = Bun.file(path);
105069
- const current = await file2.exists() ? Number.parseInt(await file2.text(), 10) || 0 : 0;
105070
- await Bun.write(path, String(current + 1) + `
105071
- `);
105072
- } catch (err) {
105073
- log3(`! could not record gave-up for ${changeName}: ${err.message}`, "yellow");
105074
- }
105075
- }
105076
105503
  async function runPostTask(input, deps) {
105077
105504
  const { log: log3, cmd, git: git2, runScript } = deps;
105078
105505
  const emit3 = (phase, detail) => deps.onPhase?.(phase, detail);
@@ -105115,8 +105542,6 @@ async function runPostTask(input, deps) {
105115
105542
  respawnWorker
105116
105543
  });
105117
105544
  emit3(effectiveCode === 0 ? "done" : "gave-up", effectiveCode !== 0 ? `exit ${effectiveCode}` : undefined);
105118
- if (effectiveCode !== 0)
105119
- await recordGaveUp(stateFilePath, log3, changeName);
105120
105545
  await runWorktreeCleanupPhase({ changeName, cwd: cwd2, projectRoot, useWorktree, effectiveCode, cfg }, { git: git2, log: log3, emit: emit3 });
105121
105546
  await runTeardownPhase({ cwd: cwd2, teardownScript: cfg.teardownScript }, { runScript, log: log3, emit: emit3 });
105122
105547
  return effectiveCode;
@@ -105126,6 +105551,24 @@ async function runPostTask(input, deps) {
105126
105551
  }
105127
105552
  if (input.mode === "conflict-fix" && effectiveCode === 0) {
105128
105553
  const identifier = issue2?.identifier ?? changeName;
105554
+ if (branch) {
105555
+ let aheadCount = 0;
105556
+ let checked = true;
105557
+ try {
105558
+ const r = await cmd.run(["git", "rev-list", "--count", `origin/${branch}..HEAD`], cwd2);
105559
+ aheadCount = Number.parseInt(r.stdout.trim(), 10) || 0;
105560
+ } catch (err) {
105561
+ checked = false;
105562
+ log3(`! ${identifier}: could not check for unpushed conflict-fix commits: ${err.message}`, "yellow");
105563
+ }
105564
+ if (checked && aheadCount > 0) {
105565
+ 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");
105566
+ emit3("gave-up", "unpushed conflict resolution");
105567
+ await runWorktreeCleanupPhase({ changeName, cwd: cwd2, projectRoot, useWorktree, effectiveCode: PR_FAILED_EXIT, cfg }, { git: git2, log: log3, emit: emit3 });
105568
+ await runTeardownPhase({ cwd: cwd2, teardownScript: cfg.teardownScript }, { runScript, log: log3, emit: emit3 });
105569
+ return PR_FAILED_EXIT;
105570
+ }
105571
+ }
105129
105572
  let prUrl = input.prUrl ?? null;
105130
105573
  if (!prUrl && branch) {
105131
105574
  prUrl = await findExistingOpenPrUrl(cmd, cwd2, branch);
@@ -105190,8 +105633,6 @@ async function runPostTask(input, deps) {
105190
105633
  }
105191
105634
  const succeeded = effectiveCode === 0 || effectiveCode === NO_CHANGES_EXIT;
105192
105635
  emit3(succeeded ? "done" : "gave-up", succeeded ? undefined : `exit ${effectiveCode}`);
105193
- if (!succeeded)
105194
- await recordGaveUp(stateFilePath, log3, changeName);
105195
105636
  await deps.runRetrospective?.({
105196
105637
  changeName,
105197
105638
  cwd: cwd2,
@@ -105218,7 +105659,6 @@ var PR_FAILED_EXIT = 71, MAX_PR_CREATE_ATTEMPTS = 5, NO_CHANGES_EXIT = 72, repoA
105218
105659
  return { exitCode: proc.exitCode ?? 1, output };
105219
105660
  };
105220
105661
  var init_post_task = __esm(() => {
105221
- init_layout();
105222
105662
  init_tasks_md();
105223
105663
  init_fs_change();
105224
105664
  init_git2();
@@ -105232,6 +105672,300 @@ var init_post_task = __esm(() => {
105232
105672
  repoAutoMergeCache = new Map;
105233
105673
  });
105234
105674
 
105675
+ // apps/agent/src/shared/capabilities/gh-client.ts
105676
+ var init_gh_client = () => {};
105677
+
105678
+ // apps/agent/src/shared/capabilities/github/github-client.ts
105679
+ var STARTED_LABEL_NAMES, ISSUE_FIELDS = "id,number,title,body,state,stateReason,labels,assignees,author,createdAt,url", ISSUE_FIELDS_WITH_COMMENTS;
105680
+ var init_github_client = __esm(() => {
105681
+ init_worktree();
105682
+ init_gh_client();
105683
+ STARTED_LABEL_NAMES = new Set(["in progress", "in-progress", "started"]);
105684
+ ISSUE_FIELDS_WITH_COMMENTS = `${ISSUE_FIELDS},comments`;
105685
+ });
105686
+
105687
+ // apps/agent/src/shared/capabilities/github/identifier-strategy.ts
105688
+ function linearChangeName(issue2) {
105689
+ const slug = issue2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40).replace(/^-+|-+$/g, "");
105690
+ return slug ? `${issue2.identifier.toLowerCase()}-${slug}` : issue2.identifier.toLowerCase();
105691
+ }
105692
+ var linearIdentifierStrategy;
105693
+ var init_identifier_strategy = __esm(() => {
105694
+ init_worktree();
105695
+ init_github_client();
105696
+ linearIdentifierStrategy = {
105697
+ scopeKey: (issue2) => issue2.identifier.split("-")[0],
105698
+ changeName: linearChangeName,
105699
+ branchName: (issue2) => branchForChange(linearChangeName(issue2))
105700
+ };
105701
+ });
105702
+
105703
+ // apps/agent/src/agent/scaffold.ts
105704
+ import { join as join24 } from "path";
105705
+ function changeNameForIssue(issue2) {
105706
+ return linearIdentifierStrategy.changeName(issue2);
105707
+ }
105708
+ async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = [], appendPrompt = "", attachments = []) {
105709
+ const name = changeNameForIssue(issue2);
105710
+ const changeDir = join24(tasksDir, name);
105711
+ const stateDir = join24(statesDir, name);
105712
+ const commentsBlock = comments.length > 0 ? [
105713
+ "",
105714
+ "## Linear comments",
105715
+ "",
105716
+ ...comments.flatMap((c) => [
105717
+ `**${c.user?.name ?? "unknown"}** \u2014 ${c.createdAt}`,
105718
+ "",
105719
+ c.body.trim(),
105720
+ ""
105721
+ ])
105722
+ ] : [];
105723
+ const attachmentsBlock = attachments.length > 0 ? [
105724
+ "",
105725
+ "## Ticket Attachments",
105726
+ "",
105727
+ ...attachments.map((a) => `- [${a.title ?? "Attachment"}](${a.url})`)
105728
+ ] : [];
105729
+ const descriptionBody = issue2.description?.trim() || "_No description provided in Linear._";
105730
+ const proposal = [
105731
+ `# ${issue2.identifier}: ${issue2.title}`,
105732
+ "",
105733
+ `Source: [${issue2.identifier}](${issue2.url})`,
105734
+ `Status: ${issue2.state.name}`,
105735
+ issue2.assignee ? `Assignee: ${issue2.assignee.name}` : "",
105736
+ issue2.labels.length ? `Labels: ${issue2.labels.join(", ")}` : "",
105737
+ "",
105738
+ "## Why",
105739
+ "",
105740
+ descriptionBody,
105741
+ "",
105742
+ "## What Changes",
105743
+ "",
105744
+ "_Describe the concrete changes this proposal introduces (one bullet per change)._",
105745
+ ...commentsBlock,
105746
+ ...attachmentsBlock,
105747
+ ...appendPrompt.trim() ? ["", "## Additional instructions", "", appendPrompt.trim()] : [],
105748
+ "",
105749
+ "## Steering",
105750
+ "",
105751
+ "_Add steering notes here as the loop runs._",
105752
+ ""
105753
+ ].filter((l) => l !== "").join(`
105754
+ `);
105755
+ const tasks = [
105756
+ `# Tasks for ${issue2.identifier}`,
105757
+ "",
105758
+ "## Planning",
105759
+ "",
105760
+ `- [ ] Read the Linear issue at ${issue2.url} and research the codebase to understand the mission and its scope`,
105761
+ `- [ ] Refine proposal.md with the problem statement, approach, and acceptance criteria derived from the research`,
105762
+ `- [ ] Fill in \`## Why\` and \`## What Changes\` in proposal.md so \`openspec validate\` passes (these sections are required by the validator)`,
105763
+ `- [ ] Add at least one spec delta under \`specs/<capability>/spec.md\` describing the behavior added/modified/removed by this change`,
105764
+ `- [ ] Fill in design.md with the technical design (files to touch, data flow, edge cases). design.md holds prose and tables ONLY \u2014 never a task checklist; the implementation tasks belong in this tasks.md file (next item).`,
105765
+ `- [ ] Append an \`## Implementation\` section to **this tasks.md file** (below the \`## Planning\` section above \u2014 NOT in design.md) with concrete mission-specific tasks derived from the plan, including tests and \`bun run lint\` / \`bun run test\`. Every item in the new section MUST start as \`- [ ]\` (unchecked) \u2014 do not pre-check items even if you already did the work during planning. The loop ticks them off in later iterations after each one is verified.`,
105766
+ `- [ ] Is there anything else to add? Review the complete change context and document any additional edge cases, constraints, or open questions not captured above.`,
105767
+ ""
105768
+ ].join(`
105769
+ `);
105770
+ const design = [
105771
+ `# Design for ${issue2.identifier}`,
105772
+ "",
105773
+ "_Fill in the technical design as you work through the issue._",
105774
+ ""
105775
+ ].join(`
105776
+ `);
105777
+ await runCapability(fsChange.scaffold, {
105778
+ changeDir,
105779
+ stateDir,
105780
+ proposal,
105781
+ tasks,
105782
+ design
105783
+ });
105784
+ return name;
105785
+ }
105786
+ var init_scaffold = __esm(() => {
105787
+ init_fs_change();
105788
+ init_identifier_strategy();
105789
+ });
105790
+
105791
+ // apps/agent/src/components/task-pipeline.ts
105792
+ function stages(todo, confirmation, work, pr, ci, done) {
105793
+ return [
105794
+ { node: "todo", status: todo },
105795
+ { node: "confirmation", status: confirmation },
105796
+ { node: "work", status: work },
105797
+ { node: "PR", status: pr },
105798
+ { node: "CI", status: ci },
105799
+ { node: "done", status: done }
105800
+ ];
105801
+ }
105802
+ function pipelineStages(row) {
105803
+ const state = row.state;
105804
+ switch (state) {
105805
+ case "todo":
105806
+ return stages("current", "pending", "pending", "pending", "pending", "pending");
105807
+ case "awaiting":
105808
+ return stages("done", "current", "pending", "pending", "pending", "pending");
105809
+ case "queued":
105810
+ case "working":
105811
+ case "in-progress":
105812
+ return stages("done", "done", "current", "pending", "pending", "pending");
105813
+ case "awaiting-ci":
105814
+ return stages("done", "done", "done", "done", "current", "pending");
105815
+ case "conflict-fix":
105816
+ return stages("done", "done", "done", "failed", "pending", "pending");
105817
+ case "ci-fix":
105818
+ return stages("done", "done", "done", "done", "failed", "pending");
105819
+ case "review":
105820
+ return stages("done", "done", "current", "done", "done", "pending");
105821
+ case "quarantined":
105822
+ return row.recovery?.lastReason === "conflicting" ? stages("done", "done", "done", "bailed", "pending", "pending") : stages("done", "done", "done", "done", "bailed", "pending");
105823
+ case "done":
105824
+ return stages("done", "done", "done", "done", "done", "done");
105825
+ case "error":
105826
+ return stages("done", "done", "failed", "pending", "pending", "pending");
105827
+ default: {
105828
+ const exhaustive = state;
105829
+ return exhaustive;
105830
+ }
105831
+ }
105832
+ }
105833
+ function attemptCount(plural) {
105834
+ return `${plural} fix attempt${plural === 1 ? "" : "s"}`;
105835
+ }
105836
+ function statusLabel(row) {
105837
+ const state = row.state;
105838
+ switch (state) {
105839
+ case "todo":
105840
+ return "todo";
105841
+ case "queued":
105842
+ return "queued";
105843
+ case "working":
105844
+ return "working";
105845
+ case "in-progress":
105846
+ return "in progress";
105847
+ case "awaiting":
105848
+ return "awaiting confirmation";
105849
+ case "awaiting-ci":
105850
+ return "awaiting CI";
105851
+ case "conflict-fix":
105852
+ return `conflict \xB7 ${attemptCount(row.recovery?.attempts ?? 0)}`;
105853
+ case "ci-fix":
105854
+ return `CI red \xB7 ${attemptCount(row.recovery?.attempts ?? 0)}`;
105855
+ case "review":
105856
+ return "addressing review";
105857
+ case "quarantined": {
105858
+ const tries = row.recovery?.attempts ?? 0;
105859
+ const reason = row.recovery?.lastReason === "conflicting" ? "conflict" : "CI";
105860
+ return `quarantined \xB7 ${tries} tries (${reason}), bailed`;
105861
+ }
105862
+ case "done":
105863
+ return "done";
105864
+ case "error":
105865
+ return "error";
105866
+ default: {
105867
+ const exhaustive = state;
105868
+ return exhaustive;
105869
+ }
105870
+ }
105871
+ }
105872
+ function buildBoardTree(rows) {
105873
+ const byId = new Map(rows.map((r) => [r.id, r]));
105874
+ const orderIndex = new Map(rows.map((r, i) => [r.id, i]));
105875
+ const blockersOf = (r) => (r.blockedByIds ?? []).filter((id) => id !== r.id && byId.has(id));
105876
+ const childrenOf = new Map;
105877
+ for (const r of rows) {
105878
+ for (const blockerId of blockersOf(r)) {
105879
+ const list = childrenOf.get(blockerId);
105880
+ if (list)
105881
+ list.push(r);
105882
+ else
105883
+ childrenOf.set(blockerId, [r]);
105884
+ }
105885
+ }
105886
+ for (const list of childrenOf.values()) {
105887
+ list.sort((a, b) => orderIndex.get(a.id) - orderIndex.get(b.id));
105888
+ }
105889
+ const emitted = new Set;
105890
+ const depthById = new Map;
105891
+ const result2 = [];
105892
+ const blockerIdentifiersOf = (r) => blockersOf(r).map((id) => byId.get(id).identifier);
105893
+ const tryEmit = (r) => {
105894
+ if (emitted.has(r.id))
105895
+ return;
105896
+ const blockers = blockersOf(r);
105897
+ if (!blockers.every((id) => emitted.has(id)))
105898
+ return;
105899
+ const depth = blockers.length === 0 ? 0 : Math.max(...blockers.map((id) => depthById.get(id))) + 1;
105900
+ depthById.set(r.id, depth);
105901
+ emitted.add(r.id);
105902
+ result2.push({ row: r, depth, blockerIdentifiers: blockerIdentifiersOf(r) });
105903
+ for (const child of childrenOf.get(r.id) ?? [])
105904
+ tryEmit(child);
105905
+ };
105906
+ for (const r of rows) {
105907
+ if (blockersOf(r).length === 0)
105908
+ tryEmit(r);
105909
+ }
105910
+ for (const r of rows) {
105911
+ if (emitted.has(r.id))
105912
+ continue;
105913
+ depthById.set(r.id, 0);
105914
+ emitted.add(r.id);
105915
+ result2.push({ row: r, depth: 0, blockerIdentifiers: blockerIdentifiersOf(r) });
105916
+ for (const child of childrenOf.get(r.id) ?? [])
105917
+ tryEmit(child);
105918
+ }
105919
+ return result2;
105920
+ }
105921
+ function machineStateToTicketState(value) {
105922
+ switch (value) {
105923
+ case "idle":
105924
+ return "in-progress";
105925
+ case "working":
105926
+ return "working";
105927
+ case "conflict-fix":
105928
+ return "conflict-fix";
105929
+ case "ci-fix":
105930
+ return "ci-fix";
105931
+ case "awaiting":
105932
+ return "awaiting";
105933
+ case "awaiting-ci":
105934
+ return "awaiting-ci";
105935
+ case "review":
105936
+ return "review";
105937
+ case "quarantined":
105938
+ return "quarantined";
105939
+ case "preempting":
105940
+ case "routing-after-preempt":
105941
+ return "working";
105942
+ case "done":
105943
+ return "done";
105944
+ case "error":
105945
+ return "error";
105946
+ default:
105947
+ return "working";
105948
+ }
105949
+ }
105950
+ var STATUS_GLYPH, PIPELINE_NODES;
105951
+ var init_task_pipeline = __esm(() => {
105952
+ STATUS_GLYPH = {
105953
+ done: "\u2713",
105954
+ current: "\u25CF",
105955
+ pending: "\u25CB",
105956
+ failed: "\u2717",
105957
+ bailed: "\u26D4"
105958
+ };
105959
+ PIPELINE_NODES = [
105960
+ "todo",
105961
+ "confirmation",
105962
+ "work",
105963
+ "PR",
105964
+ "CI",
105965
+ "done"
105966
+ ];
105967
+ });
105968
+
105235
105969
  // packages/core/src/ordering/hierarchical-order.ts
105236
105970
  function rank(priority) {
105237
105971
  return !priority ? Number.POSITIVE_INFINITY : priority;
@@ -105493,26 +106227,62 @@ var init_queue_order = __esm(() => {
105493
106227
  });
105494
106228
 
105495
106229
  // apps/agent/src/runtime/coordinator.ts
106230
+ import { appendFile as appendFile2 } from "fs/promises";
105496
106231
  function emitCapture(bus, event, properties) {
105497
106232
  capture(event, properties);
105498
106233
  bus.emit({ type: event, ...properties });
105499
106234
  }
105500
106235
  function completionCommentBody(args) {
105501
- const { noChanges, ok, trigger, changeName, code } = args;
106236
+ const { noChanges, ok, trigger, changeName, code, reachedDone } = args;
105502
106237
  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}\`
106238
+ return buildRalphyComment({
106239
+ type: "completed-noop",
106240
+ action: "completed \u2014 no code changes",
106241
+ 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
106242
 
105505
- ` + `Marking this done; please verify the work is genuinely in place. If it is not, ` + `reopen the issue with more specifics.`;
106243
+ ` + `Marking this done; please verify the work is genuinely in place. If it is not, ` + `reopen the issue with more specifics.`,
106244
+ fields: { change: changeName }
106245
+ });
105506
106246
  }
105507
106247
  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.`;
106248
+ return buildRalphyComment({
106249
+ type: "exited",
106250
+ action: `exited with code ${code}`,
106251
+ 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}\``,
106252
+ fields: { change: changeName, code }
106253
+ });
105511
106254
  }
105512
106255
  if (trigger === "conflict-fix") {
105513
- return `\u2705 Ralph resolved merge conflicts on this issue. Change: \`${changeName}\``;
106256
+ return buildRalphyComment({
106257
+ type: "conflicts-resolved",
106258
+ action: "resolved merge conflicts",
106259
+ body: `Change: \`${changeName}\``,
106260
+ fields: { change: changeName }
106261
+ });
105514
106262
  }
105515
- return `\u2705 Ralph completed work on this issue. Change: \`${changeName}\``;
106263
+ if (trigger === "ci-fix") {
106264
+ return buildRalphyComment({
106265
+ type: "ci-fix-pushed",
106266
+ action: "pushed a CI fix",
106267
+ body: `Pushed a fix for the failing CI on this PR \u2014 re-checking the checks on the ` + `next poll before marking this done. Change: \`${changeName}\``,
106268
+ fields: { change: changeName }
106269
+ });
106270
+ }
106271
+ if (!reachedDone) {
106272
+ const isReview = trigger === "review";
106273
+ return buildRalphyComment({
106274
+ type: "awaiting-ci",
106275
+ action: isReview ? "addressed review feedback" : "opened a PR",
106276
+ body: (isReview ? `Pushed changes for the review feedback to this PR. ` : `Finished the work and opened a PR. `) + `Awaiting CI, review, and a clean merge state before marking this done. ` + `Change: \`${changeName}\``,
106277
+ fields: { change: changeName }
106278
+ });
106279
+ }
106280
+ return buildRalphyComment({
106281
+ type: "completed",
106282
+ action: "completed work",
106283
+ body: `Change: \`${changeName}\``,
106284
+ fields: { change: changeName }
106285
+ });
105516
106286
  }
105517
106287
  function extractPrNumber(url2) {
105518
106288
  const m = /\/pull\/(\d+)(?:[/?#]|$)/.exec(url2);
@@ -105539,7 +106309,19 @@ class AgentCoordinator {
105539
106309
  this.opts = opts;
105540
106310
  this.bus = deps.bus ?? createNoopBus();
105541
106311
  const providedMachine = flowMachine.provide({ actors: { preemption: preemptionActorLogic } });
105542
- this.flowStore = new FlowActorStore({ bus: this.bus, persist: () => {} }, providedMachine);
106312
+ this.flowStore = new FlowActorStore({
106313
+ bus: this.bus,
106314
+ persist: () => {},
106315
+ maxRecoveryAttempts: this.opts.prRecovery?.maxRecoverySessions ?? 0,
106316
+ onTransition: (_issueId, changeDir, transition2) => {
106317
+ if (!changeDir)
106318
+ return;
106319
+ const path = `${changeDir}/.ralph-state.flow-history.jsonl`;
106320
+ const line = `${JSON.stringify({ ts: new Date().toISOString(), ...transition2 })}
106321
+ `;
106322
+ appendFile2(path, line).catch(() => {});
106323
+ }
106324
+ }, providedMachine);
105543
106325
  }
105544
106326
  get activeCount() {
105545
106327
  return this.workers.length;
@@ -105611,7 +106393,15 @@ class AgentCoordinator {
105611
106393
  awaiting: awaitingCount
105612
106394
  };
105613
106395
  const found2 = buckets2.todo + buckets2.inProgress + buckets2.mentions + buckets2.awaiting;
105614
- return { found: found2, added: 0, buckets: buckets2, prStatus: emptyPrStatus(), phase: {}, flow: {} };
106396
+ return {
106397
+ found: found2,
106398
+ added: 0,
106399
+ buckets: buckets2,
106400
+ prStatus: emptyPrStatus(),
106401
+ phase: {},
106402
+ flow: {},
106403
+ board: []
106404
+ };
105615
106405
  }
105616
106406
  const maxT = this.opts.maxTickets ?? 0;
105617
106407
  const atTicketLimit = () => {
@@ -105690,7 +106480,7 @@ class AgentCoordinator {
105690
106480
  added += 1;
105691
106481
  this.deps.onLog(` \u21B3 ${issue2.identifier} queued (fresh)`, "gray");
105692
106482
  }
105693
- const prStatus = await this.scanPrMergeStates();
106483
+ const { counts: prStatus, prByIssue } = await this.scanPrMergeStates();
105694
106484
  if (this.queue.length > 0) {
105695
106485
  this.queue = orderQueueEntries(this.queue, this.opts.getAutoMerge);
105696
106486
  }
@@ -105720,7 +106510,103 @@ class AgentCoordinator {
105720
106510
  flow2[w.changeName] = "working";
105721
106511
  }
105722
106512
  }
105723
- return { found, added, buckets, prStatus, phase: {}, flow: flow2 };
106513
+ const board = await this.buildBoard({
106514
+ todo,
106515
+ inProgress,
106516
+ mentions,
106517
+ prByIssue,
106518
+ awaitingIds: awaitingClaimed
106519
+ });
106520
+ return { found, added, buckets, prStatus, phase: {}, flow: flow2, board };
106521
+ }
106522
+ async buildBoard(args) {
106523
+ const { todo, inProgress, mentions, prByIssue, awaitingIds } = args;
106524
+ const order = [
106525
+ ...this.workers.map((w) => ({
106526
+ issue: w.issue,
106527
+ kind: "worker",
106528
+ changeName: w.changeName
106529
+ })),
106530
+ ...this.queue.map((q) => ({
106531
+ issue: q.issue,
106532
+ kind: "queued",
106533
+ changeName: changeNameForIssue(q.issue)
106534
+ })),
106535
+ ...inProgress.map((issue2) => ({
106536
+ issue: issue2,
106537
+ kind: awaitingIds.has(issue2.id) ? "awaiting" : "in-progress",
106538
+ changeName: changeNameForIssue(issue2)
106539
+ })),
106540
+ ...todo.map((issue2) => ({
106541
+ issue: issue2,
106542
+ kind: "todo",
106543
+ changeName: changeNameForIssue(issue2)
106544
+ })),
106545
+ ...mentions.map((m) => ({
106546
+ issue: m.issue,
106547
+ kind: "mention",
106548
+ changeName: changeNameForIssue(m.issue)
106549
+ }))
106550
+ ];
106551
+ const seen = new Set;
106552
+ const rows = [];
106553
+ for (const src of order) {
106554
+ if (seen.has(src.issue.id))
106555
+ continue;
106556
+ seen.add(src.issue.id);
106557
+ const row = await this.resolveBoardRow(src.issue, src.kind, src.changeName, prByIssue);
106558
+ if (row)
106559
+ rows.push(row);
106560
+ }
106561
+ return rows;
106562
+ }
106563
+ async resolveBoardRow(issue2, kind, changeName, prByIssue) {
106564
+ let state;
106565
+ let recovery;
106566
+ if (kind === "todo") {
106567
+ state = "todo";
106568
+ } else if (kind === "mention") {
106569
+ state = "review";
106570
+ } else if (kind === "awaiting") {
106571
+ state = "awaiting";
106572
+ } else if (kind !== "worker" && issue2.blockedByIds.length > 0) {
106573
+ state = "todo";
106574
+ } else {
106575
+ const changeDir = this.deps.getChangeDir?.(issue2) ?? undefined;
106576
+ const actor = await this.flowStore.getActor(issue2.id, changeDir);
106577
+ const snapshot = actor.getSnapshot();
106578
+ state = machineStateToTicketState(snapshot.value);
106579
+ if (state === "done")
106580
+ return null;
106581
+ if (kind === "queued" && state === "in-progress")
106582
+ state = "queued";
106583
+ const flowRecovery = snapshot.context.data.recovery;
106584
+ if (flowRecovery) {
106585
+ recovery = {
106586
+ attempts: flowRecovery.attempts,
106587
+ bailed: state === "quarantined",
106588
+ firstFailedAt: flowRecovery.firstFailedAt,
106589
+ lastReason: flowRecovery.lastReason
106590
+ };
106591
+ if (state === "awaiting-ci" || state === "in-progress" || state === "working") {
106592
+ state = flowRecovery.lastReason === "conflicting" ? "conflict-fix" : "ci-fix";
106593
+ }
106594
+ }
106595
+ }
106596
+ const prUrl = prByIssue.get(issue2.id)?.url;
106597
+ return {
106598
+ changeName,
106599
+ id: issue2.id,
106600
+ identifier: issue2.identifier,
106601
+ title: issue2.title,
106602
+ url: issue2.url,
106603
+ priority: issue2.priority,
106604
+ state,
106605
+ blockedByIds: issue2.blockedByIds,
106606
+ blockedByIdentifiers: issue2.blockedByIdentifiers ?? [],
106607
+ ...recovery ? { recovery } : {},
106608
+ ...prUrl ? { prUrl } : {}
106609
+ };
105724
106610
  }
105725
106611
  async walkRegistryForInProgress(inProgress) {
105726
106612
  const claimed = new Map;
@@ -105811,7 +106697,12 @@ class AgentCoordinator {
105811
106697
  const prNum = extractPrNumber(pr.url);
105812
106698
  const ref = prNum !== null ? `PR #${prNum}` : `PR ${pr.url}`;
105813
106699
  try {
105814
- await this.deps.postComment(issue2, `\u26A0\uFE0F ${ref} is ${stateLabel} \u2014 promoted to ${trigger} flow.`);
106700
+ await this.deps.postComment(issue2, buildRalphyComment({
106701
+ type: "promoted",
106702
+ action: `promoted to ${trigger} flow`,
106703
+ body: `${ref} is ${stateLabel} \u2014 promoted to ${trigger} flow.`,
106704
+ fields: { trigger, pr: extractPrNumber(pr.url) ?? pr.url }
106705
+ }));
105815
106706
  this.deps.onLog(` ${issue2.identifier}: posted ${trigger}-promotion comment`, "gray");
105816
106707
  } catch (err) {
105817
106708
  this.deps.onLog(`! Linear ${trigger}-promotion comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
@@ -105864,7 +106755,12 @@ class AgentCoordinator {
105864
106755
  if (currMilestone <= lastMilestone)
105865
106756
  continue;
105866
106757
  try {
105867
- await this.deps.postComment(w.issue, `\uD83D\uDD04 Ralph progress update: iteration ${count} on \`${w.changeName}\``);
106758
+ await this.deps.postComment(w.issue, buildRalphyComment({
106759
+ type: "progress",
106760
+ action: `progress update \u2014 iteration ${count}`,
106761
+ body: `Iteration ${count} on \`${w.changeName}\``,
106762
+ fields: { change: w.changeName, iter: count }
106763
+ }));
105868
106764
  w.lastReportedIteration = count;
105869
106765
  this.deps.onLog(` ${w.issueIdentifier}: posted progress comment (iteration ${count})`, "gray");
105870
106766
  } catch (err) {
@@ -105921,20 +106817,20 @@ class AgentCoordinator {
105921
106817
  }
105922
106818
  async scanPrMergeStates() {
105923
106819
  const counts = emptyPrStatus();
106820
+ const prByIssue = new Map;
105924
106821
  if (!this.opts.prRecovery?.enabled)
105925
- return counts;
106822
+ return { counts, prByIssue };
105926
106823
  let candidates = [];
105927
106824
  try {
105928
106825
  candidates = await this.deps.fetchDoneCandidates();
105929
106826
  } catch (err) {
105930
106827
  this.deps.onLog(`! PR merge-state scan fetch failed: ${err.message}`, "yellow");
105931
- return counts;
106828
+ return { counts, prByIssue };
105932
106829
  }
105933
106830
  if (candidates.length === 0)
105934
- return counts;
106831
+ return { counts, prByIssue };
105935
106832
  const preQueue = this.queue.map((q) => ({ id: q.issue.id, trigger: q.trigger }));
105936
106833
  const preWorkers = this.workers.map((w) => ({ id: w.issueId, trigger: w.trigger }));
105937
- const tracker = this.opts.prTracker;
105938
106834
  for (const issue2 of candidates) {
105939
106835
  if (this.workers.some((w) => w.issueId === issue2.id))
105940
106836
  continue;
@@ -105942,12 +106838,17 @@ class AgentCoordinator {
105942
106838
  continue;
105943
106839
  if (this.queue.some((q) => q.issue.id === issue2.id))
105944
106840
  continue;
105945
- if (tracker?.isBailed(issue2.identifier) && this.errorMarkerCleared(issue2)) {
105946
- await tracker.clear(issue2.identifier).catch(() => {});
106841
+ const changeDir = this.deps.getChangeDir?.(issue2) ?? undefined;
106842
+ const actor = await this.flowStore.getActor(issue2.id, changeDir);
106843
+ const stateValue = () => actor.getSnapshot().value;
106844
+ if (stateValue() === "quarantined" && this.errorMarkerCleared(issue2)) {
106845
+ actor.send({ type: "QUARANTINE_CLEARED" });
106846
+ if (changeDir)
106847
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
105947
106848
  this.conflictNotified.delete(issue2.id);
105948
106849
  this.ciFailedNotified.delete(issue2.id);
105949
106850
  this.conflictPromoted.delete(issue2.id);
105950
- this.deps.onLog(` ${issue2.identifier}: pr-tracker bail cleared (ticket back in Todo) \u2014 retrying recovery`, "cyan");
106851
+ this.deps.onLog(` ${issue2.identifier}: quarantine cleared (ralph:error removed) \u2014 retrying recovery`, "cyan");
105951
106852
  }
105952
106853
  let pr;
105953
106854
  try {
@@ -105958,45 +106859,47 @@ class AgentCoordinator {
105958
106859
  }
105959
106860
  if (!pr)
105960
106861
  continue;
106862
+ prByIssue.set(issue2.id, pr);
105961
106863
  if (pr.status === "mergeable")
105962
106864
  counts.mergeable += 1;
105963
- if (pr.status === "mergeable" && this.opts.prTracker) {
105964
- try {
105965
- await this.opts.prTracker.clear(issue2.identifier);
105966
- } catch (err) {
105967
- this.deps.onLog(`! pr-tracker clear failed for ${issue2.identifier}: ${err.message}`, "yellow");
105968
- }
105969
- }
105970
106865
  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") {
106866
+ const value = stateValue();
106867
+ if (value === "awaiting-ci" || value === "quarantined") {
105974
106868
  if (this.issueInSetDoneState(issue2)) {
106869
+ actor.send({ type: "RECOVERY_CLEARED" });
105975
106870
  actor.send({ type: "PR_PASSED" });
105976
106871
  if (changeDir)
105977
106872
  await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
105978
- if (actor.getSnapshot().value === "done") {
106873
+ if (stateValue() === "done")
105979
106874
  this.flowStore.disposeActor(issue2.id);
105980
- }
105981
106875
  } else {
105982
106876
  await this.advancePrToDone(issue2, pr.url, actor, changeDir);
105983
106877
  }
106878
+ } else {
106879
+ actor.send({ type: "RECOVERY_CLEARED" });
106880
+ if (changeDir)
106881
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
105984
106882
  }
105985
106883
  continue;
105986
106884
  }
105987
106885
  if (pr.status === "conflicted") {
105988
106886
  if (!this.opts.prRecovery?.fixConflicts)
105989
106887
  continue;
105990
- if (tracker?.isBailed(issue2.identifier)) {
106888
+ if (stateValue() === "quarantined") {
105991
106889
  counts.quarantined += 1;
105992
106890
  continue;
105993
106891
  }
105994
106892
  counts.conflicted += 1;
105995
106893
  if (this.conflictNotified.has(issue2.id))
105996
106894
  continue;
105997
- if (await this.prTrackerBail(issue2, pr.url, "conflicting")) {
106895
+ actor.send({ type: "RESUME_DETECTED" });
106896
+ actor.send({ type: "CONFLICT_DETECTED", at: new Date().toISOString() });
106897
+ if (changeDir)
106898
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106899
+ if (stateValue() === "quarantined") {
105998
106900
  counts.conflicted -= 1;
105999
106901
  counts.quarantined += 1;
106902
+ await this.quarantineBail(issue2, pr.url, "conflicting", actor);
106000
106903
  continue;
106001
106904
  }
106002
106905
  emitCapture(this.bus, "agent_conflict_detected", { issue_identifier: issue2.identifier });
@@ -106004,14 +106907,16 @@ class AgentCoordinator {
106004
106907
  this.deps.onLog(` ${issue2.identifier}: PR ${pr.url} conflicting \u2014 queued (conflict-fix)`, "yellow");
106005
106908
  if (this.opts.postComments !== false) {
106006
106909
  try {
106007
- await this.deps.postComment(issue2, `\u26A0 Ralph detected merge conflicts on this PR (${pr.url}) \u2014 re-running to resolve`);
106910
+ await this.deps.postComment(issue2, buildRalphyComment({
106911
+ type: "conflict-detected",
106912
+ action: "detected merge conflicts",
106913
+ body: `Detected merge conflicts on this PR (${pr.url}) \u2014 re-running to resolve.`,
106914
+ fields: { pr: extractPrNumber(pr.url) ?? pr.url }
106915
+ }));
106008
106916
  } catch (err) {
106009
106917
  this.deps.onLog(`! Linear conflict comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106010
106918
  }
106011
106919
  }
106012
- const conflictActor = await this.flowStore.getActor(issue2.id, this.deps.getChangeDir?.(issue2) ?? undefined);
106013
- conflictActor.send({ type: "RESUME_DETECTED" });
106014
- conflictActor.send({ type: "CONFLICT_DETECTED" });
106015
106920
  this.queue.push({
106016
106921
  issue: issue2,
106017
106922
  trigger: "conflict-fix",
@@ -106022,16 +106927,21 @@ class AgentCoordinator {
106022
106927
  if (pr.status === "ci_failed") {
106023
106928
  if (!this.opts.prRecovery?.fixCi)
106024
106929
  continue;
106025
- if (tracker?.isBailed(issue2.identifier)) {
106930
+ if (stateValue() === "quarantined") {
106026
106931
  counts.quarantined += 1;
106027
106932
  continue;
106028
106933
  }
106029
106934
  counts.ciFailed += 1;
106030
106935
  if (this.ciFailedNotified.has(issue2.id))
106031
106936
  continue;
106032
- if (await this.prTrackerBail(issue2, pr.url, "ci_failed")) {
106937
+ actor.send({ type: "RESUME_DETECTED" });
106938
+ actor.send({ type: "CI_FAILED_DETECTED", at: new Date().toISOString() });
106939
+ if (changeDir)
106940
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106941
+ if (stateValue() === "quarantined") {
106033
106942
  counts.ciFailed -= 1;
106034
106943
  counts.quarantined += 1;
106944
+ await this.quarantineBail(issue2, pr.url, "ci_failed", actor);
106035
106945
  continue;
106036
106946
  }
106037
106947
  emitCapture(this.bus, "agent_ci_failed_detected", { issue_identifier: issue2.identifier });
@@ -106039,14 +106949,16 @@ class AgentCoordinator {
106039
106949
  this.deps.onLog(` ${issue2.identifier}: PR ${pr.url} CI failing \u2014 queued (ci-fix)`, "yellow");
106040
106950
  if (this.opts.postComments !== false) {
106041
106951
  try {
106042
- await this.deps.postComment(issue2, `\u26A0 Ralph detected failing CI on this PR (${pr.url}) \u2014 re-running to fix`);
106952
+ await this.deps.postComment(issue2, buildRalphyComment({
106953
+ type: "ci-failed",
106954
+ action: "detected failing CI",
106955
+ body: `Detected failing CI on this PR (${pr.url}) \u2014 re-running to fix.`,
106956
+ fields: { pr: extractPrNumber(pr.url) ?? pr.url }
106957
+ }));
106043
106958
  } catch (err) {
106044
106959
  this.deps.onLog(`! Linear ci-failed comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106045
106960
  }
106046
106961
  }
106047
- const ciActor = await this.flowStore.getActor(issue2.id, this.deps.getChangeDir?.(issue2) ?? undefined);
106048
- ciActor.send({ type: "RESUME_DETECTED" });
106049
- ciActor.send({ type: "CI_FAILED_DETECTED" });
106050
106962
  this.queue.push({
106051
106963
  issue: issue2,
106052
106964
  trigger: "ci-fix",
@@ -106066,7 +106978,7 @@ class AgentCoordinator {
106066
106978
  else if (w.trigger === "ci-fix")
106067
106979
  counts.ciFailed += 1;
106068
106980
  }
106069
- return counts;
106981
+ return { counts, prByIssue };
106070
106982
  }
106071
106983
  issueInSetDoneState(issue2) {
106072
106984
  const sd = this.opts.setDone;
@@ -106076,6 +106988,7 @@ class AgentCoordinator {
106076
106988
  }
106077
106989
  async advancePrToDone(issue2, prUrl, actor, changeDir) {
106078
106990
  this.deps.onLog(` ${issue2.identifier}: PR ${prUrl} mergeable \u2014 moving to done`, "green");
106991
+ actor.send({ type: "RECOVERY_CLEARED" });
106079
106992
  if (this.opts.setDone) {
106080
106993
  try {
106081
106994
  await this.deps.applyIndicator(issue2, this.opts.setDone);
@@ -106087,6 +107000,8 @@ class AgentCoordinator {
106087
107000
  issue_identifier: issue2.identifier,
106088
107001
  error: err.message
106089
107002
  });
107003
+ if (changeDir)
107004
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106090
107005
  return;
106091
107006
  }
106092
107007
  if (this.opts.setInProgress) {
@@ -106105,7 +107020,12 @@ class AgentCoordinator {
106105
107020
  }
106106
107021
  if (this.opts.postComments !== false) {
106107
107022
  try {
106108
- await this.deps.postComment(issue2, `\u2705 Ralph verified this PR (${prUrl}) is mergeable (CI green, no conflicts) \u2014 moving to done`);
107023
+ await this.deps.postComment(issue2, buildRalphyComment({
107024
+ type: "verified",
107025
+ action: "verified PR mergeable",
107026
+ body: `Verified this PR (${prUrl}) is mergeable (CI green, no conflicts) \u2014 moving to done.`,
107027
+ fields: { pr: extractPrNumber(prUrl) ?? prUrl }
107028
+ }));
106109
107029
  } catch (err) {
106110
107030
  this.deps.onLog(`! Linear done comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106111
107031
  }
@@ -106121,43 +107041,34 @@ class AgentCoordinator {
106121
107041
  const have = new Set(issue2.labels.map((l) => l.toLowerCase()));
106122
107042
  return !wantLabels.some((v) => have.has(v));
106123
107043
  }
106124
- async prTrackerBail(issue2, prUrl, reason) {
106125
- const tracker = this.opts.prTracker;
106126
- if (!tracker)
106127
- return false;
106128
- let decision;
106129
- try {
106130
- decision = await tracker.recordFailure(issue2.identifier, reason);
106131
- } catch (err) {
106132
- this.deps.onLog(`! pr-tracker record failed for ${issue2.identifier}: ${err.message}`, "yellow");
106133
- return false;
106134
- }
106135
- if (decision.kind === "demote")
106136
- return false;
106137
- if (decision.firstBail) {
106138
- this.deps.onLog(` ${issue2.identifier}: pr-tracker bailing after ${decision.attempts} recovery attempts (${reason}) \u2014 applying setError`, "red");
106139
- emitCapture(this.bus, "agent_pr_tracker_bailed", {
106140
- issue_identifier: issue2.identifier,
106141
- reason,
106142
- attempts: decision.attempts
106143
- });
106144
- if (this.opts.setError) {
106145
- try {
106146
- await this.deps.applyIndicator(issue2, this.opts.setError);
106147
- } catch (err) {
106148
- this.deps.onLog(`! Linear setError failed for ${issue2.identifier}: ${err.message}`, "yellow");
106149
- }
107044
+ async quarantineBail(issue2, prUrl, reason, actor) {
107045
+ const attempts = actor.getSnapshot().context.data.recovery?.attempts ?? 0;
107046
+ this.deps.onLog(` ${issue2.identifier}: quarantined after ${attempts} recovery attempts (${reason}) \u2014 applying setError`, "red");
107047
+ emitCapture(this.bus, "agent_pr_tracker_bailed", {
107048
+ issue_identifier: issue2.identifier,
107049
+ reason,
107050
+ attempts
107051
+ });
107052
+ if (this.opts.setError) {
107053
+ try {
107054
+ await this.deps.applyIndicator(issue2, this.opts.setError);
107055
+ } catch (err) {
107056
+ this.deps.onLog(`! Linear setError failed for ${issue2.identifier}: ${err.message}`, "yellow");
106150
107057
  }
106151
- if (this.opts.postComments !== false) {
106152
- const human = reason === "conflicting" ? "merge conflicts" : "failing CI";
106153
- 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.`);
106155
- } catch (err) {
106156
- this.deps.onLog(`! Linear bail comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106157
- }
107058
+ }
107059
+ if (this.opts.postComments !== false) {
107060
+ const human = reason === "conflicting" ? "merge conflicts" : "failing CI";
107061
+ try {
107062
+ await this.deps.postComment(issue2, buildRalphyComment({
107063
+ type: "recovery-gaveup",
107064
+ action: "gave up auto-recovering PR",
107065
+ body: `Gave up auto-recovering this PR (${prUrl}) after ${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.`,
107066
+ fields: { pr: extractPrNumber(prUrl) ?? prUrl, attempts }
107067
+ }));
107068
+ } catch (err) {
107069
+ this.deps.onLog(`! Linear bail comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106158
107070
  }
106159
107071
  }
106160
- return true;
106161
107072
  }
106162
107073
  spawnNext() {
106163
107074
  if (this.stopped)
@@ -106238,7 +107149,12 @@ class AgentCoordinator {
106238
107149
  if (trigger === "review" && this.opts.postComments !== false) {
106239
107150
  const sourceTag = mention ? mention.source === "github" ? " (GitHub @mention)" : mention.source === "github-review" ? " (GitHub code review)" : " (Linear @mention)" : "";
106240
107151
  try {
106241
- await this.deps.postComment(issue2, `\uD83D\uDD01 Ralph picked up new review comments${sourceTag}. Tracking change: \`${prep.changeName}\``);
107152
+ await this.deps.postComment(issue2, buildRalphyComment({
107153
+ type: "review-pickup",
107154
+ action: "picked up review comments",
107155
+ body: `Picked up new review comments${sourceTag}. Tracking change: \`${prep.changeName}\``,
107156
+ fields: { change: prep.changeName }
107157
+ }));
106242
107158
  } catch (err) {
106243
107159
  this.deps.onLog(`! Linear review comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106244
107160
  }
@@ -106247,13 +107163,18 @@ class AgentCoordinator {
106247
107163
  let alreadyPosted = false;
106248
107164
  try {
106249
107165
  const comments = await this.deps.fetchComments(issue2.id);
106250
- alreadyPosted = comments.some((c) => c.body.startsWith("\uD83E\uDD16 Ralph started working"));
107166
+ alreadyPosted = comments.some((c) => isStartedComment(c.body));
106251
107167
  } catch (err) {
106252
107168
  this.deps.onLog(`! Linear comment fetch failed for ${issue2.identifier}: ${err.message}`, "yellow");
106253
107169
  }
106254
107170
  if (!alreadyPosted) {
106255
107171
  try {
106256
- await this.deps.postComment(issue2, `\uD83E\uDD16 Ralph started working on this issue. Tracking change: \`${prep.changeName}\``);
107172
+ await this.deps.postComment(issue2, buildRalphyComment({
107173
+ type: "started",
107174
+ action: "started working",
107175
+ body: `Tracking change: \`${prep.changeName}\``,
107176
+ fields: { change: prep.changeName }
107177
+ }));
106257
107178
  this.deps.onLog(` ${issue2.identifier}: posted "started" comment`, "gray");
106258
107179
  } catch (err) {
106259
107180
  this.deps.onLog(`! Linear comment failed for ${issue2.identifier}: ${err.message}`, "red");
@@ -106462,7 +107383,14 @@ class AgentCoordinator {
106462
107383
  }
106463
107384
  }
106464
107385
  if (this.opts.postComments !== false) {
106465
- const body = completionCommentBody({ noChanges, ok, trigger, changeName, code });
107386
+ const body = completionCommentBody({
107387
+ noChanges,
107388
+ ok,
107389
+ trigger,
107390
+ changeName,
107391
+ code,
107392
+ reachedDone: exitActorState === "done"
107393
+ });
106466
107394
  try {
106467
107395
  await this.deps.postComment(issue2, body);
106468
107396
  this.deps.onLog(` ${issue2.identifier}: posted completion comment`, "gray");
@@ -106556,18 +107484,22 @@ var emptyPrStatus = () => ({
106556
107484
  },
106557
107485
  prStatus: emptyPrStatus(),
106558
107486
  phase: {},
106559
- flow: {}
107487
+ flow: {},
107488
+ board: []
106560
107489
  });
106561
107490
  var init_coordinator = __esm(() => {
106562
107491
  init_types2();
106563
107492
  init_linear_client();
106564
107493
  init_post_task();
107494
+ init_scaffold();
107495
+ init_task_pipeline();
106565
107496
  init_queue_order();
106566
107497
  init_src();
106567
107498
  init_src2();
106568
107499
  init_registry();
106569
107500
  init_run_feature();
106570
107501
  init_machines();
107502
+ init_src8();
106571
107503
  });
106572
107504
 
106573
107505
  // apps/agent/src/agent/coordinator.ts
@@ -106575,94 +107507,6 @@ var init_coordinator2 = __esm(() => {
106575
107507
  init_coordinator();
106576
107508
  });
106577
107509
 
106578
- // apps/agent/src/agent/scaffold.ts
106579
- import { join as join24 } from "path";
106580
- 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();
106583
- }
106584
- async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = [], appendPrompt = "", attachments = []) {
106585
- const name = changeNameForIssue(issue2);
106586
- const changeDir = join24(tasksDir, name);
106587
- const stateDir = join24(statesDir, name);
106588
- const commentsBlock = comments.length > 0 ? [
106589
- "",
106590
- "## Linear comments",
106591
- "",
106592
- ...comments.flatMap((c) => [
106593
- `**${c.user?.name ?? "unknown"}** \u2014 ${c.createdAt}`,
106594
- "",
106595
- c.body.trim(),
106596
- ""
106597
- ])
106598
- ] : [];
106599
- const attachmentsBlock = attachments.length > 0 ? [
106600
- "",
106601
- "## Ticket Attachments",
106602
- "",
106603
- ...attachments.map((a) => `- [${a.title ?? "Attachment"}](${a.url})`)
106604
- ] : [];
106605
- const descriptionBody = issue2.description?.trim() || "_No description provided in Linear._";
106606
- const proposal = [
106607
- `# ${issue2.identifier}: ${issue2.title}`,
106608
- "",
106609
- `Source: [${issue2.identifier}](${issue2.url})`,
106610
- `Status: ${issue2.state.name}`,
106611
- issue2.assignee ? `Assignee: ${issue2.assignee.name}` : "",
106612
- issue2.labels.length ? `Labels: ${issue2.labels.join(", ")}` : "",
106613
- "",
106614
- "## Why",
106615
- "",
106616
- descriptionBody,
106617
- "",
106618
- "## What Changes",
106619
- "",
106620
- "_Describe the concrete changes this proposal introduces (one bullet per change)._",
106621
- ...commentsBlock,
106622
- ...attachmentsBlock,
106623
- ...appendPrompt.trim() ? ["", "## Additional instructions", "", appendPrompt.trim()] : [],
106624
- "",
106625
- "## Steering",
106626
- "",
106627
- "_Add steering notes here as the loop runs._",
106628
- ""
106629
- ].filter((l) => l !== "").join(`
106630
- `);
106631
- const tasks = [
106632
- `# Tasks for ${issue2.identifier}`,
106633
- "",
106634
- "## Planning",
106635
- "",
106636
- `- [ ] Read the Linear issue at ${issue2.url} and research the codebase to understand the mission and its scope`,
106637
- `- [ ] Refine proposal.md with the problem statement, approach, and acceptance criteria derived from the research`,
106638
- `- [ ] Fill in \`## Why\` and \`## What Changes\` in proposal.md so \`openspec validate\` passes (these sections are required by the validator)`,
106639
- `- [ ] Add at least one spec delta under \`specs/<capability>/spec.md\` describing the behavior added/modified/removed by this change`,
106640
- `- [ ] Fill in design.md with the technical design (files to touch, data flow, edge cases). design.md holds prose and tables ONLY \u2014 never a task checklist; the implementation tasks belong in this tasks.md file (next item).`,
106641
- `- [ ] Append an \`## Implementation\` section to **this tasks.md file** (below the \`## Planning\` section above \u2014 NOT in design.md) with concrete mission-specific tasks derived from the plan, including tests and \`bun run lint\` / \`bun run test\`. Every item in the new section MUST start as \`- [ ]\` (unchecked) \u2014 do not pre-check items even if you already did the work during planning. The loop ticks them off in later iterations after each one is verified.`,
106642
- `- [ ] Is there anything else to add? Review the complete change context and document any additional edge cases, constraints, or open questions not captured above.`,
106643
- ""
106644
- ].join(`
106645
- `);
106646
- const design = [
106647
- `# Design for ${issue2.identifier}`,
106648
- "",
106649
- "_Fill in the technical design as you work through the issue._",
106650
- ""
106651
- ].join(`
106652
- `);
106653
- await runCapability(fsChange.scaffold, {
106654
- changeDir,
106655
- stateDir,
106656
- proposal,
106657
- tasks,
106658
- design
106659
- });
106660
- return name;
106661
- }
106662
- var init_scaffold = __esm(() => {
106663
- init_fs_change();
106664
- });
106665
-
106666
107510
  // packages/core/src/detections/tasks.ts
106667
107511
  function hasUnchecked(content) {
106668
107512
  return /^- \[ \]/m.test(content);
@@ -106704,23 +107548,20 @@ function gateActive(inputs) {
106704
107548
  }
106705
107549
 
106706
107550
  // 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}`;
107551
+ function buildMentionAckComment() {
107552
+ return buildRalphyMarker("mention-ack", { status: "handled" });
106716
107553
  }
107554
+ var init_mention2 = __esm(() => {
107555
+ init_src8();
107556
+ });
106717
107557
  // packages/core/src/detections/index.ts
106718
107558
  var init_detections = __esm(() => {
106719
107559
  init_phase2();
107560
+ init_mention2();
106720
107561
  });
106721
107562
 
106722
107563
  // apps/agent/src/features/confirmation/state.ts
106723
- import { dirname as dirname10, join as join25 } from "path";
107564
+ import { dirname as dirname9, join as join25 } from "path";
106724
107565
  async function readInlineConfirmation(statePath) {
106725
107566
  const f2 = Bun.file(statePath);
106726
107567
  if (!await f2.exists())
@@ -106733,7 +107574,7 @@ async function readInlineConfirmation(statePath) {
106733
107574
  }
106734
107575
  }
106735
107576
  async function readConfirmationState(statePath) {
106736
- const changeDir = dirname10(statePath);
107577
+ const changeDir = dirname9(statePath);
106737
107578
  const sidecar = await readSlotSidecar(changeDir, "confirmation");
106738
107579
  const existing = sidecar ?? await readInlineConfirmation(statePath) ?? null;
106739
107580
  const confirmation = {
@@ -106749,7 +107590,7 @@ async function readConfirmationState(statePath) {
106749
107590
  return { stateObj: {}, confirmation };
106750
107591
  }
106751
107592
  async function writeConfirmationState(statePath, _stateObj, confirmation) {
106752
- await writeSlotField(dirname10(statePath), "confirmation", confirmation);
107593
+ await writeSlotField(dirname9(statePath), "confirmation", confirmation);
106753
107594
  }
106754
107595
  async function restartFromDesign(changeDir, changeName) {
106755
107596
  const designStub = [
@@ -106839,7 +107680,17 @@ function buildMentionTaskBody(trigger, issueUrl) {
106839
107680
  function findLastRalphPickupISO(comments) {
106840
107681
  let latest = null;
106841
107682
  for (const c of comments) {
106842
- if (!/^\uD83D\uDD01\s*Ralph picked up/.test(c.body.trimStart()))
107683
+ if (!isPickupComment(c.body))
107684
+ continue;
107685
+ if (latest === null || c.createdAt > latest)
107686
+ latest = c.createdAt;
107687
+ }
107688
+ return latest;
107689
+ }
107690
+ function findLastMentionAckISO(comments) {
107691
+ let latest = null;
107692
+ for (const c of comments) {
107693
+ if (!isMentionAckComment(c.body))
106843
107694
  continue;
106844
107695
  if (latest === null || c.createdAt > latest)
106845
107696
  latest = c.createdAt;
@@ -106875,7 +107726,10 @@ function githubReactionSlug(emoji3) {
106875
107726
  return emoji3;
106876
107727
  }
106877
107728
  }
106878
- var init_task_bodies = () => {};
107729
+ var init_task_bodies = __esm(() => {
107730
+ init_src8();
107731
+ init_ralph_comment();
107732
+ });
106879
107733
 
106880
107734
  // apps/agent/src/features/confirmation/inspect.ts
106881
107735
  function buildReviseRegex(handle) {
@@ -106902,7 +107756,12 @@ async function inspectAwaitingTicket(state, cfg, deps) {
106902
107756
  if (!next.stuckPostedAt) {
106903
107757
  if (cfg.postComments) {
106904
107758
  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.`);
107759
+ await deps.postComment(buildRalphyComment({
107760
+ type: "confirmation-stuck",
107761
+ action: "confirmation gate stuck",
107762
+ 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.`,
107763
+ fields: { rounds: next.rounds }
107764
+ }));
106906
107765
  } catch (err) {
106907
107766
  deps.log(`! plan-stuck comment failed: ${err.message}`, "yellow");
106908
107767
  }
@@ -106947,7 +107806,12 @@ ${revise.reason}`);
106947
107806
  }
106948
107807
  if (cfg.postComments) {
106949
107808
  try {
106950
- await deps.postComment(`\uD83D\uDD01 Ralphy: revise request acknowledged \u2014 restarting at design (round ${next.rounds + 1}/${cfg.maxConfirmationRounds}).`);
107809
+ await deps.postComment(buildRalphyComment({
107810
+ type: "revise-ack",
107811
+ action: "revise request acknowledged",
107812
+ body: `Revise request acknowledged \u2014 restarting at design (round ${next.rounds + 1}/${cfg.maxConfirmationRounds}).`,
107813
+ fields: { round: next.rounds + 1 }
107814
+ }));
106951
107815
  } catch (err) {
106952
107816
  deps.log(`! revise ack comment failed: ${err.message}`, "yellow");
106953
107817
  }
@@ -106976,7 +107840,12 @@ ${revise.reason}`);
106976
107840
  const limitMs = cfg.timeoutHours * 60 * 60 * 1000;
106977
107841
  if (elapsedMs >= limitMs && cfg.postComments) {
106978
107842
  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.`);
107843
+ await deps.postComment(buildRalphyComment({
107844
+ type: "confirmation-reminder",
107845
+ action: "awaiting confirmation",
107846
+ 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.`,
107847
+ fields: { round: next.rounds + 1 }
107848
+ }));
106980
107849
  next.lastReminderAt = nowIso;
106981
107850
  } catch (err) {
106982
107851
  deps.log(`! reminder comment failed: ${err.message}`, "yellow");
@@ -106986,6 +107855,7 @@ ${revise.reason}`);
106986
107855
  return { outcome: "stay-awaiting", next };
106987
107856
  }
106988
107857
  var init_inspect = __esm(() => {
107858
+ init_src8();
106989
107859
  init_task_bodies();
106990
107860
  });
106991
107861
 
@@ -107028,7 +107898,12 @@ async function postPlanReadyCommentOnce(issue2, statePath, changeName, deps) {
107028
107898
  return;
107029
107899
  const approvalSentence = describeApprovalMarker(deps.cfg.linear.indicators.getApproved);
107030
107900
  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.`;
107901
+ const body = buildRalphyComment({
107902
+ type: "plan-ready",
107903
+ action: "plan ready",
107904
+ 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.`,
107905
+ fields: { change: changeName }
107906
+ });
107032
107907
  try {
107033
107908
  await addIssueComment(deps.apiKey, issue2.id, body);
107034
107909
  } catch (err) {
@@ -107307,11 +108182,13 @@ async function processAwaitingForIssue(issue2, deps) {
107307
108182
  }
107308
108183
  var init_awaiting = __esm(() => {
107309
108184
  init_layout();
108185
+ init_src8();
107310
108186
  init_detections();
107311
108187
  init_phase();
107312
108188
  init_worktree();
107313
108189
  init_scaffold();
107314
108190
  init_linear();
108191
+ init_ralph_comment();
107315
108192
  init_types2();
107316
108193
  init_workflow();
107317
108194
  init_state2();
@@ -107427,7 +108304,7 @@ function createOpenDraftPr(deps) {
107427
108304
  if (!branch)
107428
108305
  return null;
107429
108306
  const base2 = baseBranchFromLabels(issue2.labels) ?? deps.prBaseBranch;
107430
- const result2 = await create3({ cwd: cwd2, branch, issue: issue2, base: base2, draft: true }, deps.cmdRunner);
108307
+ const result2 = await create3({ cwd: cwd2, branch, issue: issue2, base: base2, draft: true, labels: deps.prLabels ?? [] }, deps.cmdRunner);
107431
108308
  const url2 = result2?.url ?? null;
107432
108309
  if (url2) {
107433
108310
  deps.prByChange.set(changeName, url2);
@@ -107538,10 +108415,13 @@ function unionMarkers(...sets) {
107538
108415
  }
107539
108416
  return out;
107540
108417
  }
107541
- function describeIndicators(indicators, team, assignee, anyAssignee) {
108418
+ function describeIndicators(indicators, team, assignee, anyAssignee, requireAllLabels) {
107542
108419
  const parts = [];
107543
108420
  parts.push(`team=${team ?? "*"}`);
107544
108421
  parts.push(`assignee=${anyAssignee ? "any" : assignee ?? "*"}`);
108422
+ if (requireAllLabels && requireAllLabels.length > 0) {
108423
+ parts.push(`labels=[${requireAllLabels.join(",")}]`);
108424
+ }
107545
108425
  if (indicators.getTodo) {
107546
108426
  parts.push(`todo=[${indicators.getTodo.filter.map((m) => `${m.type}:${m.value}`).join(",")}]`);
107547
108427
  }
@@ -107561,7 +108441,7 @@ function createLinearResolvers(input) {
107561
108441
  const stateCache = new Map;
107562
108442
  const labelCache = new Map;
107563
108443
  const teamIdCache = new Map;
107564
- const teamKeyOf = (issue2) => issue2.identifier.split("-")[0];
108444
+ const teamKeyOf = (issue2) => linearIdentifierStrategy.scopeKey(issue2);
107565
108445
  async function resolveStateId(issue2, name) {
107566
108446
  const t = teamKeyOf(issue2);
107567
108447
  let map3 = stateCache.get(t);
@@ -107757,6 +108637,201 @@ async function fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requ
107757
108637
  var init_linear_resolvers = __esm(() => {
107758
108638
  init_types2();
107759
108639
  init_linear();
108640
+ init_identifier_strategy();
108641
+ });
108642
+
108643
+ // apps/agent/src/agent/wire/tracker/github.ts
108644
+ function identifierForNumber(n) {
108645
+ return `issue-${n}`;
108646
+ }
108647
+ function toLinearIssue(gh) {
108648
+ const open = (gh.state ?? "OPEN").toUpperCase() === "OPEN";
108649
+ return {
108650
+ id: String(gh.number),
108651
+ identifier: identifierForNumber(gh.number),
108652
+ title: gh.title,
108653
+ description: gh.body ?? null,
108654
+ url: gh.url,
108655
+ state: { name: open ? "Open" : "Closed", type: open ? "started" : "completed" },
108656
+ assignee: null,
108657
+ project: null,
108658
+ labels: (gh.labels ?? []).map((l) => l.name),
108659
+ priority: 0,
108660
+ createdAt: gh.createdAt ?? "",
108661
+ blockedByIds: []
108662
+ };
108663
+ }
108664
+ function labelValues(markers) {
108665
+ return markers.filter((m) => m.type === "label").map((m) => m.value);
108666
+ }
108667
+ function createGithubTrackerProvider(input) {
108668
+ const { cmdRunner, projectRoot, diag } = input;
108669
+ const statusLabels = input.issues?.statusLabels ?? DEFAULT_STATUS_LABELS;
108670
+ const todoLabel = input.issues?.label;
108671
+ const assignee = input.issues?.assignee;
108672
+ const configuredRepo = input.issues?.repo;
108673
+ let repoPromise = null;
108674
+ async function repo() {
108675
+ if (configuredRepo && configuredRepo.trim() !== "")
108676
+ return configuredRepo.trim();
108677
+ if (!repoPromise) {
108678
+ repoPromise = (async () => {
108679
+ try {
108680
+ const { stdout } = await cmdRunner.run(["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], projectRoot);
108681
+ const detected = stdout.trim();
108682
+ if (!detected)
108683
+ throw new Error("empty");
108684
+ diag("github-tracker", ` using detected GitHub repo ${detected}`, "gray");
108685
+ return detected;
108686
+ } catch (err) {
108687
+ 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})`);
108688
+ }
108689
+ })();
108690
+ }
108691
+ return repoPromise;
108692
+ }
108693
+ async function listIssues(args) {
108694
+ const r = await repo();
108695
+ const { stdout } = await cmdRunner.run([
108696
+ "gh",
108697
+ "issue",
108698
+ "list",
108699
+ "--repo",
108700
+ r,
108701
+ "--state",
108702
+ "open",
108703
+ "--json",
108704
+ "number,title,url,body,state,createdAt,labels",
108705
+ "--limit",
108706
+ "100",
108707
+ ...args
108708
+ ], projectRoot);
108709
+ const parsed = JSON.parse(stdout.trim() || "[]");
108710
+ return parsed.map(toLinearIssue);
108711
+ }
108712
+ async function fetchByGet(inc, excl) {
108713
+ if (!inc)
108714
+ return [];
108715
+ const include = !Array.isArray(inc) && "filter" in inc ? inc.filter : [];
108716
+ const wantLabels = labelValues(include);
108717
+ const args = [];
108718
+ for (const label of wantLabels)
108719
+ args.push("--label", label);
108720
+ if (assignee && assignee.trim() !== "")
108721
+ args.push("--assignee", assignee.trim());
108722
+ const issues = await listIssues(args);
108723
+ const excludeLabels = new Set(labelValues(excl));
108724
+ if (excludeLabels.size === 0)
108725
+ return issues;
108726
+ return issues.filter((issue2) => !issue2.labels.some((l) => excludeLabels.has(l)));
108727
+ }
108728
+ async function ghIssue(issueNumber, ...args) {
108729
+ const r = await repo();
108730
+ await cmdRunner.run(["gh", "issue", ...args, issueNumber, "--repo", r], projectRoot);
108731
+ }
108732
+ async function applyMarker(issue2, m) {
108733
+ if (m.type === "comment") {
108734
+ await ghIssue(issue2.id, "comment", "--body", m.value);
108735
+ diag("github-marker", ` \u2192 ${issue2.identifier} comment`, "gray");
108736
+ return;
108737
+ }
108738
+ if (m.type !== "label") {
108739
+ diag("github-marker", `! ${issue2.identifier}: '${m.type}' markers are not supported by the GitHub tracker \u2014 skipped`, "yellow");
108740
+ return;
108741
+ }
108742
+ await ghIssue(issue2.id, "edit", "--add-label", m.value);
108743
+ diag("github-marker", ` \u2192 ${issue2.identifier} +label='${m.value}'`, "gray");
108744
+ if (m.value === statusLabels.inProgress && todoLabel && todoLabel.trim() !== "") {
108745
+ await ghIssue(issue2.id, "edit", "--remove-label", todoLabel.trim());
108746
+ diag("github-marker", ` \u2192 ${issue2.identifier} -label='${todoLabel.trim()}'`, "gray");
108747
+ }
108748
+ if (m.value === statusLabels.done) {
108749
+ await ghIssue(issue2.id, "close");
108750
+ diag("github-marker", ` \u2192 ${issue2.identifier} closed`, "gray");
108751
+ }
108752
+ }
108753
+ async function applyIndicator(issue2, ind) {
108754
+ for (const m of markersOf(ind))
108755
+ await applyMarker(issue2, m);
108756
+ }
108757
+ async function removeIndicator(issue2, ind) {
108758
+ for (const m of markersOf(ind)) {
108759
+ if (m.type !== "label")
108760
+ continue;
108761
+ await ghIssue(issue2.id, "edit", "--remove-label", m.value);
108762
+ diag("github-marker", ` \u2192 ${issue2.identifier} -label='${m.value}'`, "gray");
108763
+ }
108764
+ }
108765
+ async function fetchDoneCandidates() {
108766
+ return listIssues(["--label", statusLabels.inProgress]);
108767
+ }
108768
+ async function resolveLabelIdForTeam() {
108769
+ return null;
108770
+ }
108771
+ return {
108772
+ fetchByGet,
108773
+ applyIndicator,
108774
+ removeIndicator,
108775
+ applyMarker,
108776
+ fetchDoneCandidates,
108777
+ resolveLabelIdForTeam
108778
+ };
108779
+ }
108780
+ function githubIndicators(issues) {
108781
+ const statusLabels = issues?.statusLabels ?? DEFAULT_STATUS_LABELS;
108782
+ const todoLabel = issues?.label?.trim();
108783
+ return {
108784
+ getTodo: { filter: todoLabel ? [{ type: "label", value: todoLabel }] : [] },
108785
+ getInProgress: { filter: [{ type: "label", value: statusLabels.inProgress }] },
108786
+ setInProgress: { type: "label", value: statusLabels.inProgress },
108787
+ setDone: { type: "label", value: statusLabels.done },
108788
+ setError: { type: "label", value: statusLabels.error }
108789
+ };
108790
+ }
108791
+ var DEFAULT_STATUS_LABELS;
108792
+ var init_github = __esm(() => {
108793
+ init_types2();
108794
+ DEFAULT_STATUS_LABELS = {
108795
+ inProgress: "ralph:in-progress",
108796
+ done: "ralph:done",
108797
+ error: "ralph:error"
108798
+ };
108799
+ });
108800
+
108801
+ // apps/agent/src/agent/wire/tracker/linear-tracker-provider.ts
108802
+ function createLinearTrackerProvider(input) {
108803
+ const {
108804
+ apiKey,
108805
+ team,
108806
+ assignee,
108807
+ anyAssignee,
108808
+ requireAllLabels,
108809
+ indicators,
108810
+ resolvers,
108811
+ fetchMentions,
108812
+ ticketNumbers
108813
+ } = input;
108814
+ const excludeFromTodo = unionMarkers(indicators.setDone, indicators.setError);
108815
+ const excludeFromInProgress = unionMarkers(indicators.setError);
108816
+ return {
108817
+ fetchTodo: () => resolvers.fetchByGet(indicators.getTodo, excludeFromTodo),
108818
+ fetchInProgress: () => resolvers.fetchByGet(indicators.getInProgress, excludeFromInProgress),
108819
+ fetchReview: () => resolvers.fetchByGet(indicators.getReview, []),
108820
+ fetchMentions,
108821
+ fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requireAllLabels, indicators, ticketNumbers && ticketNumbers.length > 0 ? ticketNumbers : undefined),
108822
+ applyIndicator: resolvers.applyIndicator,
108823
+ removeIndicator: resolvers.removeIndicator,
108824
+ postComment: (issue2, body) => addIssueComment(apiKey, issue2.id, body),
108825
+ fetchComments: async (issueId) => {
108826
+ const c = await fetchIssueComments(apiKey, issueId);
108827
+ return c.map((x) => ({ body: x.body }));
108828
+ }
108829
+ };
108830
+ }
108831
+ var init_linear_tracker_provider = __esm(() => {
108832
+ init_linear();
108833
+ init_indicators();
108834
+ init_linear_resolvers();
107760
108835
  });
107761
108836
 
107762
108837
  // apps/agent/src/agent/wire/prepare.ts
@@ -107795,7 +108870,7 @@ function createPrepareHelpers(input) {
107795
108870
  let scaffoldStatesDir = statesDir;
107796
108871
  let branch = null;
107797
108872
  if (!useWorktree)
107798
- return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
108873
+ return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch, worktreeCreated: null };
107799
108874
  const probeName = worktreeDirNameForIssue(issue2);
107800
108875
  const baseBranch = baseBranchFromLabels(issue2.labels) ?? cfg.prBaseBranch;
107801
108876
  let wt;
@@ -107824,10 +108899,10 @@ function createPrepareHelpers(input) {
107824
108899
  } catch (err) {
107825
108900
  diag("worktree", `! seeding .mcp.json failed for ${issue2.identifier}: ${err.message}`, "yellow");
107826
108901
  }
107827
- return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
108902
+ return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch, worktreeCreated: wt.created };
107828
108903
  }
107829
108904
  async function prepare(issue2) {
107830
- const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch } = await setupWorktree(issue2);
108905
+ const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch, worktreeCreated } = await setupWorktree(issue2);
107831
108906
  let changeName;
107832
108907
  const wtLayoutPre = projectLayout(workerCwd);
107833
108908
  const derivedName = changeNameForIssue(issue2);
@@ -107876,7 +108951,8 @@ function createPrepareHelpers(input) {
107876
108951
  maps.issueByChange.set(changeName, issue2);
107877
108952
  if (branch)
107878
108953
  maps.branchByChange.set(changeName, branch);
107879
- if (cfg.setupScript) {
108954
+ const runSetup = worktreeCreated ?? isFresh;
108955
+ if (cfg.setupScript && runSetup) {
107880
108956
  await runScript("setup", cfg.setupScript, workerCwd);
107881
108957
  }
107882
108958
  return {
@@ -108225,6 +109301,18 @@ function createPrDiscovery(input) {
108225
109301
  } catch (err) {
108226
109302
  diag("ci", `! gh pr checks ${prUrl} failed (PR scan): ${err.message}`, "yellow");
108227
109303
  }
109304
+ try {
109305
+ const readiness = await getPollContext().fetchPrOnce(prUrl, ["isDraft", "reviewDecision"], cmdRunner, projectRoot);
109306
+ const isDraft = readiness.isDraft === true;
109307
+ const reviewDecision = readiness.reviewDecision?.toUpperCase();
109308
+ const awaitingApproval = reviewDecision === "REVIEW_REQUIRED" || reviewDecision === "CHANGES_REQUESTED";
109309
+ if (isDraft || awaitingApproval) {
109310
+ diag("pr", ` ${issue2.identifier}: PR ${prUrl} is green + conflict-free but ${isDraft ? "still a draft" : "awaiting review approval"} \u2014 holding (not done) until it is ready`, "gray");
109311
+ return { url: prUrl, status: "unknown" };
109312
+ }
109313
+ } catch (err) {
109314
+ diag("pr", `! gh pr view ${prUrl} readiness check failed (PR scan): ${err.message} \u2014 treating as ready`, "yellow");
109315
+ }
108228
109316
  return { url: prUrl, status: "mergeable" };
108229
109317
  }
108230
109318
  async function resolvePrUrlForIssue(issue2) {
@@ -108264,17 +109352,17 @@ var init_pr_discovery = __esm(() => {
108264
109352
  });
108265
109353
 
108266
109354
  // apps/agent/src/features/review-followup/scan.ts
108267
- import { dirname as dirname11, join as join28 } from "path";
109355
+ import { dirname as dirname10, join as join28 } from "path";
108268
109356
  async function resolveReviewStateDir(changeName, deps) {
108269
109357
  const root = deps.cwdOf(changeName);
108270
109358
  if (root)
108271
- return dirname11(projectLayout(root).stateFile(changeName));
109359
+ return dirname10(projectLayout(root).stateFile(changeName));
108272
109360
  if (!deps.useWorktree)
108273
- return dirname11(projectLayout(deps.projectRoot).stateFile(changeName));
109361
+ return dirname10(projectLayout(deps.projectRoot).stateFile(changeName));
108274
109362
  const wtPath = join28(worktreesDir2(deps.projectRoot), changeName);
108275
109363
  const statePath = projectLayout(wtPath).stateFile(changeName);
108276
109364
  if (await Bun.file(statePath).exists())
108277
- return dirname11(statePath);
109365
+ return dirname10(statePath);
108278
109366
  return null;
108279
109367
  }
108280
109368
  async function readReviewWatermark(stateDir) {
@@ -108371,7 +109459,12 @@ async function maybePingStaleReviewer(issue2, prUrl, state, newestReviewerActivi
108371
109459
  if (!m)
108372
109460
  return;
108373
109461
  const [, owner, repo, num] = m;
108374
- 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?`;
109462
+ const body = buildRalphyComment({
109463
+ type: "reviewer-ping",
109464
+ action: "nudging reviewer",
109465
+ 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?`,
109466
+ fields: { reviewer }
109467
+ });
108375
109468
  try {
108376
109469
  await deps.cmdRunner.run(["gh", "api", `repos/${owner}/${repo}/issues/${num}/comments`, "-f", `body=${body}`], deps.projectRoot);
108377
109470
  deps.stalePingedAt.set(prUrl, now2);
@@ -108437,6 +109530,7 @@ async function scanCodeReview(issue2, prUrl, lastRalphPickup, deps) {
108437
109530
  var init_scan = __esm(() => {
108438
109531
  init_layout();
108439
109532
  init_store();
109533
+ init_src8();
108440
109534
  init_scaffold();
108441
109535
  init_worktree();
108442
109536
  });
@@ -108486,7 +109580,7 @@ async function fetchPrIssueComments(cmdRunner, projectRoot, prUrl, onLog) {
108486
109580
  return [];
108487
109581
  }
108488
109582
  }
108489
- var init_github = __esm(() => {
109583
+ var init_github2 = __esm(() => {
108490
109584
  init_linear();
108491
109585
  init_task_bodies();
108492
109586
  });
@@ -108494,6 +109588,19 @@ var init_github = __esm(() => {
108494
109588
  // apps/agent/src/agent/wire/mention-scan.ts
108495
109589
  import { readdir as readdir2 } from "fs/promises";
108496
109590
  import { join as join29 } from "path";
109591
+ function latestIso(...values2) {
109592
+ let latest = null;
109593
+ for (const value of values2) {
109594
+ if (value && (latest === null || value > latest))
109595
+ latest = value;
109596
+ }
109597
+ return latest;
109598
+ }
109599
+ function isAlreadyReactedError(err) {
109600
+ const e = err;
109601
+ const text = [...e?.messages ?? [], e?.message ?? ""].join(" ").toLowerCase();
109602
+ return text.includes("conflict on insert of reaction") || text.includes("already exists") || text.includes("already reacted");
109603
+ }
108497
109604
  function createMentionScanner(input) {
108498
109605
  const {
108499
109606
  apiKey,
@@ -108555,13 +109662,14 @@ function createMentionScanner(input) {
108555
109662
  for (const issue2 of candidates) {
108556
109663
  const comments = issue2.comments ?? [];
108557
109664
  const lastRalphPickup = findLastRalphPickupISO(comments);
109665
+ const linearMentionGate = latestIso(lastRalphPickup, findLastMentionAckISO(comments));
108558
109666
  if (wantMention) {
108559
109667
  for (const c of comments) {
108560
109668
  if (isRalphComment(c.body))
108561
109669
  continue;
108562
109670
  if (!containsHandle(c.body, handle))
108563
109671
  continue;
108564
- if (lastRalphPickup && c.createdAt <= lastRalphPickup)
109672
+ if (linearMentionGate && c.createdAt <= linearMentionGate)
108565
109673
  continue;
108566
109674
  out.push({
108567
109675
  issue: issue2,
@@ -108581,11 +109689,13 @@ function createMentionScanner(input) {
108581
109689
  queued.add(issue2.id);
108582
109690
  break;
108583
109691
  }
108584
- diag("mention", `! mention scan: Linear reaction failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
109692
+ if (!isAlreadyReactedError(err)) {
109693
+ diag("mention", `! mention scan: Linear reaction failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
109694
+ }
108585
109695
  }
108586
109696
  if (cfg.linear.postComments !== false) {
108587
109697
  try {
108588
- await createIssueComment(apiKey, issue2.id, buildMentionAckComment(c.body, c.user?.name));
109698
+ await createIssueComment(apiKey, issue2.id, buildMentionAckComment());
108589
109699
  } catch (err) {
108590
109700
  diag("mention", `! mention scan: ack comment failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
108591
109701
  }
@@ -108605,11 +109715,14 @@ function createMentionScanner(input) {
108605
109715
  continue;
108606
109716
  if (wantMention) {
108607
109717
  const ghComments = await fetchPrIssueComments(cmdRunner, projectRoot, prUrl, onLog);
109718
+ const ghMentionGate = latestIso(lastRalphPickup, findLastMentionAckISO(ghComments));
108608
109719
  const prMatch = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/\d+/.exec(prUrl);
108609
109720
  for (const c of ghComments) {
109721
+ if (isRalphComment(c.body))
109722
+ continue;
108610
109723
  if (!containsHandle(c.body, handle))
108611
109724
  continue;
108612
- if (lastRalphPickup && c.createdAt <= lastRalphPickup)
109725
+ if (ghMentionGate && c.createdAt <= ghMentionGate)
108613
109726
  continue;
108614
109727
  out.push({
108615
109728
  issue: issue2,
@@ -108626,10 +109739,12 @@ function createMentionScanner(input) {
108626
109739
  try {
108627
109740
  await addGithubReactionToComment(cmdRunner, projectRoot, { owner, repo, kind: "issue" }, c.id, "\uD83D\uDC40");
108628
109741
  } catch (err) {
108629
- diag("mention", `! mention scan: GitHub reaction failed for ${prUrl}: ${formatLinearError(err)}`, "yellow");
109742
+ if (!isAlreadyReactedError(err)) {
109743
+ diag("mention", `! mention scan: GitHub reaction failed for ${prUrl}: ${formatLinearError(err)}`, "yellow");
109744
+ }
108630
109745
  }
108631
109746
  if (cfg.linear.postComments !== false) {
108632
- await postGithubPrComment(cmdRunner, projectRoot, prUrl, buildMentionAckComment(c.body, c.author), onLog);
109747
+ await postGithubPrComment(cmdRunner, projectRoot, prUrl, buildMentionAckComment(), onLog);
108633
109748
  }
108634
109749
  }
108635
109750
  queued.add(issue2.id);
@@ -108680,7 +109795,7 @@ var init_mention_scan = __esm(() => {
108680
109795
  init_detections();
108681
109796
  init_scaffold();
108682
109797
  init_scan();
108683
- init_github();
109798
+ init_github2();
108684
109799
  init_task_bodies();
108685
109800
  });
108686
109801
 
@@ -109044,6 +110159,7 @@ function buildPostTaskInput(input) {
109044
110159
  finalizeNoOpAsDone: cfg.finalizeNoOpAsDone,
109045
110160
  manualMergeWhenAutoMergeDisabled: cfg.manualMergeWhenAutoMergeDisabled,
109046
110161
  prDraft: cfg.prDraft,
110162
+ prLabels: cfg.prLabels,
109047
110163
  validateCommands: [cfg.commands.test, cfg.commands.lint, cfg.commands.typecheck].filter((c) => Boolean(c))
109048
110164
  },
109049
110165
  respawnWorker: input.respawnWorker
@@ -109634,7 +110750,7 @@ var init_linear_sync = __esm(() => {
109634
110750
  });
109635
110751
 
109636
110752
  // apps/agent/src/agent/linear-sync/comment-sync.ts
109637
- import { dirname as dirname12, join as join34 } from "path";
110753
+ import { dirname as dirname11, join as join34 } from "path";
109638
110754
  async function readInlineLinearComments(statePath) {
109639
110755
  const file2 = Bun.file(statePath);
109640
110756
  if (!await file2.exists())
@@ -109647,7 +110763,7 @@ async function readInlineLinearComments(statePath) {
109647
110763
  }
109648
110764
  }
109649
110765
  async function readComments(statePath) {
109650
- const changeDir = dirname12(statePath);
110766
+ const changeDir = dirname11(statePath);
109651
110767
  const raw = await readSlotSidecar(changeDir, "linearComments") ?? await readInlineLinearComments(statePath) ?? {};
109652
110768
  const r = raw;
109653
110769
  return {
@@ -109660,7 +110776,7 @@ async function readComments(statePath) {
109660
110776
  async function patchComments(statePath, patch) {
109661
110777
  const current = await readComments(statePath);
109662
110778
  const next = { ...current, ...patch };
109663
- await writeSlotField(dirname12(statePath), "linearComments", next);
110779
+ await writeSlotField(dirname11(statePath), "linearComments", next);
109664
110780
  }
109665
110781
  function isCommentNotFoundError(err) {
109666
110782
  if (!err)
@@ -109688,7 +110804,12 @@ async function readTasksMd(changeDir, log3) {
109688
110804
  }
109689
110805
  }
109690
110806
  function renderTasksCommentBody(tasksMd, changeName, iteration) {
109691
- return renderTasksBlock(tasksMd, { changeName, iteration });
110807
+ return buildRalphyComment({
110808
+ type: "tasks",
110809
+ action: "task progress",
110810
+ body: renderTasksBlock(tasksMd, { changeName, iteration }),
110811
+ fields: { change: changeName, iter: iteration }
110812
+ });
109692
110813
  }
109693
110814
  async function postOrUpdateTasksComment(deps) {
109694
110815
  const tasksMd = await readTasksMd(deps.changeDir, deps.log);
@@ -109799,8 +110920,13 @@ async function postPlanCommentOnce(deps) {
109799
110920
  if (designSummary) {
109800
110921
  parts.push("", "**Design**", "", designSummary);
109801
110922
  }
109802
- const body = parts.join(`
109803
- `);
110923
+ const body = buildRalphyComment({
110924
+ type: "plan",
110925
+ action: "plan",
110926
+ body: parts.join(`
110927
+ `),
110928
+ fields: { change: deps.changeName }
110929
+ });
109804
110930
  let id;
109805
110931
  try {
109806
110932
  id = await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, body);
@@ -109817,9 +110943,14 @@ async function postPlanCommentOnce(deps) {
109817
110943
  }
109818
110944
  async function postSteeringAndRefreshTasks(deps) {
109819
110945
  const firstLine = deps.message.split(/\r?\n/, 1)[0].trim() || deps.message.trim();
109820
- const steeringBody = `### ${STEERING_COMMENT_TITLE}
110946
+ const steeringBody = buildRalphyComment({
110947
+ type: "steering",
110948
+ action: "steering",
110949
+ body: `### ${STEERING_COMMENT_TITLE}
109821
110950
 
109822
- ${deps.message.trim()}`;
110951
+ ${deps.message.trim()}`,
110952
+ fields: { change: deps.changeName }
110953
+ });
109823
110954
  try {
109824
110955
  await deps.mutations.createIssueComment(deps.apiKey, deps.issueId, steeringBody);
109825
110956
  deps.log(` comment-sync: posted steering comment (${firstLine})`, "gray");
@@ -109852,6 +110983,7 @@ ${deps.message.trim()}`;
109852
110983
  var PLAN_COMMENT_TITLE = "\uD83D\uDCCB Ralph plan", STEERING_COMMENT_TITLE = "\uD83E\uDDED Ralph steering";
109853
110984
  var init_comment_sync = __esm(() => {
109854
110985
  init_store();
110986
+ init_src8();
109855
110987
  init_linear_sync();
109856
110988
  });
109857
110989
 
@@ -261754,22 +262886,20 @@ function renderCodeBlock(doc2, token, indent) {
261754
262886
  const text = token.text ?? "";
261755
262887
  const x2 = MARGIN + indent;
261756
262888
  const width = doc2.page.width - 2 * MARGIN - indent;
262889
+ const textWidth = width - 2 * CODE_PADDING_X;
261757
262890
  doc2.font(FONT_MONO).fontSize(CODE_SIZE).fillColor(COLOR_TEXT);
261758
- const lineHeight = doc2.currentLineHeight(true);
261759
262891
  const lines = text.split(/\r?\n/);
261760
262892
  doc2.y += CODE_PADDING_Y / 2;
261761
262893
  for (const line of lines) {
261762
- if (doc2.y + lineHeight + CODE_PADDING_Y > doc2.page.height - MARGIN) {
262894
+ const safe = toPdfSafe(line.length > 0 ? line : " ");
262895
+ const lineHeight = doc2.heightOfString(safe, { width: textWidth, lineBreak: true });
262896
+ if (doc2.y + lineHeight + CODE_PADDING_Y > doc2.page.height - MARGIN && doc2.y > doc2.page.margins.top) {
261763
262897
  doc2.addPage();
261764
262898
  }
261765
262899
  const yTop = doc2.y;
261766
- const safe = toPdfSafe(line);
261767
262900
  doc2.rect(x2, yTop, width, lineHeight).fill(COLOR_CODE_BG);
261768
262901
  doc2.fillColor(COLOR_TEXT);
261769
- doc2.text(safe.length > 0 ? safe : " ", x2 + CODE_PADDING_X, yTop, {
261770
- width: width - 2 * CODE_PADDING_X,
261771
- lineBreak: false
261772
- });
262902
+ doc2.text(safe, x2 + CODE_PADDING_X, yTop, { width: textWidth, lineBreak: true });
261773
262903
  doc2.y = yTop + lineHeight;
261774
262904
  }
261775
262905
  doc2.y += CODE_PADDING_Y / 2;
@@ -261857,38 +262987,85 @@ function flattenInline(tokens, flags = {}) {
261857
262987
  }
261858
262988
  return out;
261859
262989
  }
262990
+ function applyInlineStyle(doc2, run) {
262991
+ if (run.code) {
262992
+ doc2.font(FONT_MONO).fontSize(BODY_SIZE).fillColor(COLOR_INLINE_CODE_FG);
262993
+ return;
262994
+ }
262995
+ doc2.fontSize(BODY_SIZE).fillColor(COLOR_TEXT);
262996
+ if (run.bold && run.italic)
262997
+ doc2.font(FONT_BOLD_ITALIC);
262998
+ else if (run.bold)
262999
+ doc2.font(FONT_BOLD);
263000
+ else if (run.italic)
263001
+ doc2.font(FONT_ITALIC);
263002
+ else
263003
+ doc2.font(FONT_BODY);
263004
+ }
263005
+ function atomizeInline(flat) {
263006
+ const atoms = [];
263007
+ for (const run of flat) {
263008
+ const safe = toPdfSafe(run.text);
263009
+ for (const part of safe.split(/(\n|[ \t]+)/)) {
263010
+ if (part === "")
263011
+ continue;
263012
+ if (part === `
263013
+ `)
263014
+ atoms.push({ text: "", run, space: false, br: true });
263015
+ else if (/^[ \t]+$/.test(part))
263016
+ atoms.push({ text: part, run, space: true, br: false });
263017
+ else
263018
+ atoms.push({ text: part, run, space: false, br: false });
263019
+ }
263020
+ }
263021
+ return atoms;
263022
+ }
261860
263023
  function emitInline(doc2, tokens, x2, width) {
261861
263024
  const flat = flattenInline(tokens);
261862
263025
  if (flat.length === 0)
261863
263026
  return;
261864
- for (let i = 0;i < flat.length; i++) {
261865
- const run = flat[i];
261866
- const last3 = i === flat.length - 1;
261867
- const opts = {
261868
- width,
261869
- continued: !last3,
261870
- lineBreak: true
261871
- };
261872
- if (run.code) {
261873
- doc2.font(FONT_MONO).fontSize(BODY_SIZE).fillColor(COLOR_INLINE_CODE_FG);
261874
- } else {
261875
- doc2.fontSize(BODY_SIZE).fillColor(COLOR_TEXT);
261876
- if (run.bold && run.italic)
261877
- doc2.font(FONT_BOLD_ITALIC);
261878
- else if (run.bold)
261879
- doc2.font(FONT_BOLD);
261880
- else if (run.italic)
261881
- doc2.font(FONT_ITALIC);
261882
- else
261883
- doc2.font(FONT_BODY);
263027
+ const atoms = atomizeInline(flat);
263028
+ if (atoms.length === 0)
263029
+ return;
263030
+ doc2.font(FONT_BODY).fontSize(BODY_SIZE);
263031
+ const lineHeight = doc2.currentLineHeight(true);
263032
+ const right = x2 + width;
263033
+ const bottom = doc2.page.height - MARGIN;
263034
+ let cursorX = x2;
263035
+ let cursorY = doc2.y;
263036
+ let pendingSpace = 0;
263037
+ const newLine = () => {
263038
+ cursorX = x2;
263039
+ cursorY += lineHeight;
263040
+ pendingSpace = 0;
263041
+ if (cursorY + lineHeight > bottom) {
263042
+ doc2.addPage();
263043
+ cursorY = doc2.page.margins.top;
261884
263044
  }
261885
- const safe = toPdfSafe(run.text);
261886
- if (i === 0) {
261887
- doc2.text(safe, x2, doc2.y, opts);
263045
+ };
263046
+ for (const atom of atoms) {
263047
+ if (atom.br) {
263048
+ newLine();
263049
+ continue;
263050
+ }
263051
+ applyInlineStyle(doc2, atom.run);
263052
+ if (atom.space) {
263053
+ if (cursorX > x2)
263054
+ pendingSpace += doc2.widthOfString(atom.text);
263055
+ continue;
263056
+ }
263057
+ const wordWidth = doc2.widthOfString(atom.text);
263058
+ if (cursorX > x2 && cursorX + pendingSpace + wordWidth > right + 0.01) {
263059
+ newLine();
261888
263060
  } else {
261889
- doc2.text(safe, opts);
263061
+ cursorX += pendingSpace;
263062
+ pendingSpace = 0;
261890
263063
  }
263064
+ applyInlineStyle(doc2, atom.run);
263065
+ doc2.text(atom.text, cursorX, cursorY, { lineBreak: false, continued: false });
263066
+ cursorX += wordWidth;
261891
263067
  }
263068
+ doc2.y = cursorY + lineHeight;
261892
263069
  doc2.font(FONT_BODY).fontSize(BODY_SIZE).fillColor(COLOR_TEXT);
261893
263070
  }
261894
263071
  function plainInline(tokens) {
@@ -262068,7 +263245,7 @@ var init_render_pdf = __esm(() => {
262068
263245
  });
262069
263246
 
262070
263247
  // apps/agent/src/agent/linear-sync/spec-attachments.ts
262071
- import { dirname as dirname13, join as join35 } from "path";
263248
+ import { dirname as dirname12, join as join35 } from "path";
262072
263249
  function describeLinearError(err) {
262073
263250
  const e = err;
262074
263251
  const parts = [e.message ?? String(err)];
@@ -262083,7 +263260,7 @@ function describeLinearError(err) {
262083
263260
  return parts.join(" ");
262084
263261
  }
262085
263262
  function stateDirOf(statePath) {
262086
- return dirname13(statePath);
263263
+ return dirname12(statePath);
262087
263264
  }
262088
263265
  async function readInlineSpecAttachments(statePath) {
262089
263266
  const file2 = Bun.file(statePath);
@@ -262101,7 +263278,7 @@ async function readInlineSpecAttachments(statePath) {
262101
263278
  }
262102
263279
  }
262103
263280
  async function readSpecAttachmentsSubtree(statePath) {
262104
- const sidecar = await readSlotSidecar(dirname13(statePath), "specAttachments");
263281
+ const sidecar = await readSlotSidecar(dirname12(statePath), "specAttachments");
262105
263282
  return sidecar ?? await readInlineSpecAttachments(statePath);
262106
263283
  }
262107
263284
  function asRevisions(value) {
@@ -262593,105 +263770,8 @@ var init_comment_sync2 = __esm(() => {
262593
263770
  init_linear();
262594
263771
  });
262595
263772
 
262596
- // apps/agent/src/features/pr-tracker/state.ts
262597
- import { join as join36 } from "path";
262598
- async function readState2(projectRoot) {
262599
- const path = join36(projectRoot, PR_TRACKER_STATE_RELPATH);
262600
- const file2 = Bun.file(path);
262601
- if (!await file2.exists())
262602
- return {};
262603
- try {
262604
- const raw = await file2.text();
262605
- const parsed = JSON.parse(raw);
262606
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
262607
- return parsed;
262608
- }
262609
- return {};
262610
- } catch {
262611
- return {};
262612
- }
262613
- }
262614
- async function writeState2(projectRoot, state) {
262615
- const path = join36(projectRoot, PR_TRACKER_STATE_RELPATH);
262616
- await Bun.write(path, JSON.stringify(state, null, 2));
262617
- }
262618
- var PR_TRACKER_STATE_RELPATH = ".ralph/pr-tracker-state.json";
262619
- var init_state3 = () => {};
262620
-
262621
- // apps/agent/src/features/pr-tracker/tracker.ts
262622
- class PrTracker {
262623
- opts;
262624
- state = {};
262625
- loaded = false;
262626
- now;
262627
- constructor(opts) {
262628
- this.opts = opts;
262629
- this.now = opts.now ?? (() => new Date);
262630
- }
262631
- async load() {
262632
- if (this.loaded)
262633
- return;
262634
- this.state = await readState2(this.opts.projectRoot);
262635
- this.loaded = true;
262636
- }
262637
- snapshot() {
262638
- return JSON.parse(JSON.stringify(this.state));
262639
- }
262640
- isBailed(identifier) {
262641
- return this.state[identifier]?.bailed === true;
262642
- }
262643
- getAttempts(identifier) {
262644
- return this.state[identifier]?.attempts ?? 0;
262645
- }
262646
- async recordFailure(identifier, reason) {
262647
- await this.load();
262648
- const nowIso = this.now().toISOString();
262649
- const existing = this.state[identifier];
262650
- if (existing?.bailed) {
262651
- existing.lastReason = reason;
262652
- await this.flush();
262653
- return { kind: "bail", attempts: existing.attempts, firstBail: false };
262654
- }
262655
- const attempts = (existing?.attempts ?? 0) + 1;
262656
- const entry = {
262657
- attempts,
262658
- firstFailedAt: existing?.firstFailedAt ?? nowIso,
262659
- lastDemotedAt: nowIso,
262660
- lastReason: reason
262661
- };
262662
- if (attempts >= this.opts.maxRecoveryAttempts) {
262663
- entry.bailed = true;
262664
- this.state[identifier] = entry;
262665
- await this.flush();
262666
- return { kind: "bail", attempts, firstBail: true };
262667
- }
262668
- this.state[identifier] = entry;
262669
- await this.flush();
262670
- return { kind: "demote", attempts };
262671
- }
262672
- async clear(identifier) {
262673
- await this.load();
262674
- if (!(identifier in this.state))
262675
- return;
262676
- delete this.state[identifier];
262677
- await this.flush();
262678
- }
262679
- async flush() {
262680
- await writeState2(this.opts.projectRoot, this.state);
262681
- }
262682
- }
262683
- var init_tracker = __esm(() => {
262684
- init_state3();
262685
- });
262686
-
262687
- // apps/agent/src/features/pr-tracker/index.ts
262688
- var init_pr_tracker = __esm(() => {
262689
- init_tracker();
262690
- init_state3();
262691
- });
262692
-
262693
263773
  // apps/agent/src/agent/wire.ts
262694
- import { join as join37 } from "path";
263774
+ import { join as join36 } from "path";
262695
263775
  function buildAgentCoordinator(input) {
262696
263776
  const {
262697
263777
  args,
@@ -262710,7 +263790,7 @@ function buildAgentCoordinator(input) {
262710
263790
  onWorkerCmd,
262711
263791
  onAwaitingTicket
262712
263792
  } = input;
262713
- const logsDir = join37(projectRoot, ".ralph", "logs");
263793
+ const logsDir = join36(projectRoot, ".ralph", "logs");
262714
263794
  const bus = createBus();
262715
263795
  subscribeAgentDiag(bus, onLog);
262716
263796
  const diag = (area, message, color) => {
@@ -262718,11 +263798,12 @@ function buildAgentCoordinator(input) {
262718
263798
  };
262719
263799
  const concurrency = args.concurrency || cfg.concurrency;
262720
263800
  const pollInterval = args.pollInterval || cfg.pollIntervalSeconds;
262721
- const indicators = mergeIndicators(cfg.linear.indicators, args.indicators);
263801
+ const isGithubTracker = cfg.tracker.kind === "github";
263802
+ const indicators = isGithubTracker ? githubIndicators(cfg.github?.issues) : mergeIndicators(cfg.linear.indicators, args.indicators);
262722
263803
  const team = args.linearTeam || cfg.linear.team;
262723
263804
  const { assignee, anyAssignee, requireAllLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, args.linearAssignee));
262724
263805
  const ticketNumbers = resolveTicketNumbers(args.ticketTokens, team);
262725
- const excludeFromTodo = unionMarkers(indicators.setDone, indicators.setError);
263806
+ const excludeFromTodo = isGithubTracker ? unionMarkers(indicators.setDone, indicators.setError, indicators.setInProgress) : unionMarkers(indicators.setDone, indicators.setError);
262726
263807
  const gitRunner = input.runners?.git ?? bunGitRunner;
262727
263808
  const cmdRunner = input.runners?.cmd ?? bunCmdRunner;
262728
263809
  const cwdByChange = new Map;
@@ -262757,7 +263838,7 @@ function buildAgentCoordinator(input) {
262757
263838
  }
262758
263839
  return code;
262759
263840
  });
262760
- const resolvers = createLinearResolvers({
263841
+ const resolvers = isGithubTracker ? null : createLinearResolvers({
262761
263842
  apiKey,
262762
263843
  team,
262763
263844
  assignee,
@@ -262766,6 +263847,15 @@ function buildAgentCoordinator(input) {
262766
263847
  diag,
262767
263848
  ...ticketNumbers.length > 0 ? { ticketNumbers } : {}
262768
263849
  });
263850
+ const provider = isGithubTracker ? createGithubTrackerProvider({
263851
+ issues: cfg.github?.issues,
263852
+ cmdRunner,
263853
+ projectRoot,
263854
+ diag
263855
+ }) : {
263856
+ ...resolvers,
263857
+ fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requireAllLabels, indicators, ticketNumbers.length > 0 ? ticketNumbers : undefined)
263858
+ };
262769
263859
  if (ticketNumbers.length > 0) {
262770
263860
  const hasGetIndicator = [indicators.getTodo, indicators.getInProgress].some((ind) => ind && ind.filter.length > 0);
262771
263861
  if (!hasGetIndicator) {
@@ -262796,7 +263886,7 @@ function buildAgentCoordinator(input) {
262796
263886
  scriptRunner,
262797
263887
  ...input.runners?.worktree ? { worktreeProvider: input.runners.worktree } : {}
262798
263888
  });
262799
- const fetchMentions = createMentionScanner({
263889
+ const fetchMentions = isGithubTracker ? async () => [] : createMentionScanner({
262800
263890
  apiKey,
262801
263891
  args,
262802
263892
  cfg,
@@ -262816,6 +263906,27 @@ function buildAgentCoordinator(input) {
262816
263906
  lastHandledReviewActivity,
262817
263907
  resolvePrUrlForIssue: prDiscovery.resolvePrUrlForIssue
262818
263908
  });
263909
+ const tracker = isGithubTracker ? {
263910
+ fetchTodo: () => provider.fetchByGet(indicators.getTodo, excludeFromTodo),
263911
+ fetchInProgress: () => provider.fetchByGet(indicators.getInProgress, unionMarkers(indicators.setError)),
263912
+ fetchReview: async () => [],
263913
+ fetchMentions,
263914
+ fetchDoneCandidates: () => provider.fetchDoneCandidates(),
263915
+ applyIndicator: provider.applyIndicator,
263916
+ removeIndicator: provider.removeIndicator,
263917
+ postComment: (issue2, body) => provider.applyMarker(issue2, { type: "comment", value: body }),
263918
+ fetchComments: async () => []
263919
+ } : createLinearTrackerProvider({
263920
+ apiKey,
263921
+ team,
263922
+ assignee,
263923
+ anyAssignee,
263924
+ requireAllLabels,
263925
+ indicators,
263926
+ resolvers,
263927
+ fetchMentions,
263928
+ ...ticketNumbers.length > 0 ? { ticketNumbers } : {}
263929
+ });
262819
263930
  const spawnWorker = createSpawnWorker({
262820
263931
  args,
262821
263932
  cfg,
@@ -262827,7 +263938,7 @@ function buildAgentCoordinator(input) {
262827
263938
  indicators,
262828
263939
  cmdRunner,
262829
263940
  gitRunner,
262830
- applyIndicator: resolvers.applyIndicator,
263941
+ applyIndicator: provider.applyIndicator,
262831
263942
  bus,
262832
263943
  onLog,
262833
263944
  diag,
@@ -262859,6 +263970,7 @@ function buildAgentCoordinator(input) {
262859
263970
  prByChange,
262860
263971
  cmdRunner,
262861
263972
  prBaseBranch: cfg.prBaseBranch,
263973
+ prLabels: cfg.prLabels,
262862
263974
  invalidatePrUrlForIssue: (issueId) => prDiscovery.invalidatePrUrlForIssue(issueId)
262863
263975
  });
262864
263976
  const confirmationCaps = {
@@ -262871,8 +263983,8 @@ function buildAgentCoordinator(input) {
262871
263983
  cwdOf: (cn) => cwdByChange.get(cn),
262872
263984
  awaitingChangeSet,
262873
263985
  reapForAwaiting: (cn) => coordRef.current?.reapForAwaiting(cn),
262874
- applyIndicator: resolvers.applyIndicator,
262875
- applyMarker: resolvers.applyMarker,
263986
+ applyIndicator: provider.applyIndicator,
263987
+ applyMarker: provider.applyMarker,
262876
263988
  openDraftPr,
262877
263989
  ...onAwaitingTicket ? { onAwaitingTicket } : {},
262878
263990
  onLog
@@ -262898,11 +264010,7 @@ function buildAgentCoordinator(input) {
262898
264010
  };
262899
264011
  }
262900
264012
  const prRecoveryEnabled = args.prRecoveryEnabled === undefined ? cfg.prRecovery.enabled : args.prRecoveryEnabled;
262901
- const prTracker = prRecoveryEnabled ? new PrTracker({
262902
- projectRoot,
262903
- maxRecoveryAttempts: cfg.prRecovery.maxRecoverySessions
262904
- }) : null;
262905
- const commentSync = createCommentSyncHooks({
264013
+ const commentSync = isGithubTracker ? { enabled: false } : createCommentSyncHooks({
262906
264014
  apiKey,
262907
264015
  cfg,
262908
264016
  projectRoot,
@@ -262915,20 +264023,18 @@ function buildAgentCoordinator(input) {
262915
264023
  beforePoll: () => {
262916
264024
  pollContext = new PollContext;
262917
264025
  },
262918
- fetchTodo: () => resolvers.fetchByGet(indicators.getTodo, excludeFromTodo),
262919
- fetchInProgress: () => resolvers.fetchByGet(indicators.getInProgress, unionMarkers(indicators.setError)),
262920
- fetchMentions,
262921
- fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requireAllLabels, indicators, ticketNumbers.length > 0 ? ticketNumbers : undefined),
264026
+ fetchTodo: tracker.fetchTodo,
264027
+ fetchInProgress: tracker.fetchInProgress,
264028
+ fetchMentions: tracker.fetchMentions,
264029
+ fetchDoneCandidates: tracker.fetchDoneCandidates,
264030
+ fetchReview: tracker.fetchReview,
262922
264031
  prepare: prep.prepare,
262923
264032
  prepareTaskForTrigger: prep.prepareTaskForTrigger,
262924
264033
  spawnWorker,
262925
- applyIndicator: resolvers.applyIndicator,
262926
- removeIndicator: resolvers.removeIndicator,
262927
- postComment: (issue2, body) => addIssueComment(apiKey, issue2.id, body),
262928
- fetchComments: async (issueId) => {
262929
- const c = await fetchIssueComments(apiKey, issueId);
262930
- return c.map((x2) => ({ body: x2.body }));
262931
- },
264034
+ applyIndicator: tracker.applyIndicator,
264035
+ removeIndicator: tracker.removeIndicator,
264036
+ postComment: tracker.postComment,
264037
+ fetchComments: tracker.fetchComments,
262932
264038
  checkPrStatus: prDiscovery.checkPrStatus,
262933
264039
  hasPrForChange: (changeName) => prByChange.has(changeName),
262934
264040
  isChangeArchivedForIssue: (issue2) => isChangeArchivedForIssue(issue2, cwdByChange, projectRoot),
@@ -262949,7 +264055,7 @@ function buildAgentCoordinator(input) {
262949
264055
  const changeDir = projectLayout(root).changeDir(changeName);
262950
264056
  const parts = [];
262951
264057
  for (const name of ["tasks.md", "proposal.md", "design.md"]) {
262952
- const file2 = Bun.file(join37(changeDir, name));
264058
+ const file2 = Bun.file(join36(changeDir, name));
262953
264059
  if (!await file2.exists())
262954
264060
  continue;
262955
264061
  parts.push(`${name}:${file2.lastModified}:${file2.size}`);
@@ -262968,15 +264074,15 @@ function buildAgentCoordinator(input) {
262968
264074
  commentEveryIterations: cfg.linear.updateEveryIterations,
262969
264075
  ...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {},
262970
264076
  createsPrs: args.createPr || cfg.createPrOnSuccess,
262971
- ...prTracker ? { prTracker } : {},
262972
264077
  prRecovery: {
262973
264078
  enabled: prRecoveryEnabled,
262974
264079
  fixCi: cfg.prRecovery.fixCi,
262975
- fixConflicts: cfg.prRecovery.fixConflicts
264080
+ fixConflicts: cfg.prRecovery.fixConflicts,
264081
+ maxRecoverySessions: cfg.prRecovery.maxRecoverySessions
262976
264082
  }
262977
264083
  });
262978
264084
  coordRef.current = coord;
262979
- const filterDesc = describeIndicators(indicators, team, assignee, anyAssignee);
264085
+ const filterDesc = describeIndicators(indicators, team, assignee, anyAssignee, requireAllLabels);
262980
264086
  const runBaselineGateOnce = createBaselineGateRunner({
262981
264087
  args,
262982
264088
  cfg,
@@ -262987,7 +264093,7 @@ function buildAgentCoordinator(input) {
262987
264093
  gitRunner,
262988
264094
  coord,
262989
264095
  onLog,
262990
- resolveLabelIdForTeam: resolvers.resolveLabelIdForTeam
264096
+ resolveLabelIdForTeam: provider.resolveLabelIdForTeam
262991
264097
  });
262992
264098
  return {
262993
264099
  coord,
@@ -262996,26 +264102,13 @@ function buildAgentCoordinator(input) {
262996
264102
  pollInterval,
262997
264103
  getWorkerCwd: (changeName) => cwdByChange.get(changeName),
262998
264104
  syncTasksEnabled: commentSync.enabled,
262999
- runBaselineGate: runBaselineGateOnce,
263000
- getGaveUpTotal: async () => {
263001
- let total = 0;
263002
- for (const [changeName, root] of cwdByChange) {
263003
- const file2 = Bun.file(join37(projectLayout(root).taskStateDir(changeName), GAVEUP_COUNT_FILE));
263004
- if (!await file2.exists())
263005
- continue;
263006
- try {
263007
- total += Number.parseInt(await file2.text(), 10) || 0;
263008
- } catch {}
263009
- }
263010
- return total;
263011
- }
264105
+ runBaselineGate: runBaselineGateOnce
263012
264106
  };
263013
264107
  }
263014
264108
  var init_wire = __esm(() => {
263015
264109
  init_workflow();
263016
264110
  init_src2();
263017
264111
  init_coordinator2();
263018
- init_linear();
263019
264112
  init_layout();
263020
264113
  init_scaffold();
263021
264114
  init_awaiting();
@@ -263024,6 +264117,8 @@ var init_wire = __esm(() => {
263024
264117
  init_indicators();
263025
264118
  init_task_bodies();
263026
264119
  init_linear_resolvers();
264120
+ init_github();
264121
+ init_linear_tracker_provider();
263027
264122
  init_linear_client();
263028
264123
  init_prepare();
263029
264124
  init_pr_discovery();
@@ -263031,18 +264126,17 @@ var init_wire = __esm(() => {
263031
264126
  init_worker();
263032
264127
  init_baseline();
263033
264128
  init_comment_sync2();
263034
- init_pr_tracker();
263035
264129
  });
263036
264130
 
263037
264131
  // apps/agent/src/agent/json-log/json-log-file.ts
263038
- import { mkdir as mkdir11, appendFile as appendFile2 } from "fs/promises";
263039
- import { dirname as dirname14 } from "path";
264132
+ import { mkdir as mkdir11, appendFile as appendFile3 } from "fs/promises";
264133
+ import { dirname as dirname13 } from "path";
263040
264134
  function createJsonLogFileSink(path) {
263041
264135
  if (!path)
263042
264136
  return { emit: () => {} };
263043
264137
  let chain = (async () => {
263044
264138
  try {
263045
- await mkdir11(dirname14(path), { recursive: true });
264139
+ await mkdir11(dirname13(path), { recursive: true });
263046
264140
  await Bun.write(path, "");
263047
264141
  } catch {}
263048
264142
  })();
@@ -263052,7 +264146,7 @@ function createJsonLogFileSink(path) {
263052
264146
  `;
263053
264147
  chain = chain.then(async () => {
263054
264148
  try {
263055
- await appendFile2(path, line);
264149
+ await appendFile3(path, line);
263056
264150
  } catch {}
263057
264151
  });
263058
264152
  }
@@ -263288,7 +264382,7 @@ var init_output_utils = __esm(() => {
263288
264382
  });
263289
264383
 
263290
264384
  // apps/agent/src/agent/state/worker-state-poll.ts
263291
- import { join as join38 } from "path";
264385
+ import { join as join37 } from "path";
263292
264386
  function parseSubtasks(tasksMd) {
263293
264387
  const out = [];
263294
264388
  let skipSection = false;
@@ -263321,7 +264415,7 @@ function initialWorkerSnapshot() {
263321
264415
  async function readWorkerSnapshot(input) {
263322
264416
  const next = { ...input.prev };
263323
264417
  try {
263324
- const file2 = Bun.file(join38(input.statesDir, input.changeName, ".ralph-state.json"));
264418
+ const file2 = Bun.file(join37(input.statesDir, input.changeName, ".ralph-state.json"));
263325
264419
  if (await file2.exists()) {
263326
264420
  const json2 = await file2.json();
263327
264421
  next.iter = json2.iteration ?? next.iter;
@@ -263330,10 +264424,10 @@ async function readWorkerSnapshot(input) {
263330
264424
  } catch {}
263331
264425
  if (input.changeDir) {
263332
264426
  try {
263333
- const tasksFile = Bun.file(join38(input.changeDir, "tasks.md"));
263334
- const proposalFile = Bun.file(join38(input.changeDir, "proposal.md"));
263335
- const designFile = Bun.file(join38(input.changeDir, "design.md"));
263336
- const reviewFindingsFile = Bun.file(join38(input.changeDir, "review-findings.md"));
264427
+ const tasksFile = Bun.file(join37(input.changeDir, "tasks.md"));
264428
+ const proposalFile = Bun.file(join37(input.changeDir, "proposal.md"));
264429
+ const designFile = Bun.file(join37(input.changeDir, "design.md"));
264430
+ const reviewFindingsFile = Bun.file(join37(input.changeDir, "review-findings.md"));
263337
264431
  const [tasksText, proposalText, designText, reviewFindingsText] = await Promise.all([
263338
264432
  tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
263339
264433
  proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
@@ -263396,7 +264490,7 @@ var init_worker_state_poll = __esm(() => {
263396
264490
  });
263397
264491
 
263398
264492
  // apps/agent/src/components/AgentMode.tsx
263399
- import { join as join39 } from "path";
264493
+ import { join as join38 } from "path";
263400
264494
  async function appendSteeringImpl(changeDir, message) {
263401
264495
  await runWithContext(createDefaultContext(), async () => {
263402
264496
  appendSteeringMessage(changeDir, message);
@@ -263413,16 +264507,6 @@ function orderSubtasksForCappedDisplay(subtasks) {
263413
264507
  (s.done ? done : pending).push(s);
263414
264508
  return [...pending, ...done];
263415
264509
  }
263416
- function pickLatestGatedTicket(tickets) {
263417
- if (tickets.size === 0)
263418
- return { top: null, moreCount: 0 };
263419
- const sorted = Array.from(tickets.entries()).sort(([, a], [, b2]) => {
263420
- const aTime = a.since ? new Date(a.since).getTime() : 0;
263421
- const bTime = b2.since ? new Date(b2.since).getTime() : 0;
263422
- return bTime - aTime;
263423
- });
263424
- return { top: sorted[0], moreCount: sorted.length - 1 };
263425
- }
263426
264510
  function fmtCmd(argv) {
263427
264511
  const joined = argv.join(" ");
263428
264512
  return joined.length > CMD_DISPLAY_MAX ? joined.slice(0, CMD_DISPLAY_MAX - 1) + "\u2026" : joined;
@@ -263518,20 +264602,6 @@ function Link({ url: url2, label, color }) {
263518
264602
  }, undefined, false, undefined, this)
263519
264603
  }, undefined, false, undefined, this);
263520
264604
  }
263521
- function priorityBadge(p) {
263522
- switch (p) {
263523
- case 1:
263524
- return { text: "\u25B2", color: "red", label: "URGENT" };
263525
- case 2:
263526
- return { text: "\u2191", color: "yellow", label: "HIGH" };
263527
- case 3:
263528
- return { text: "\xB7", color: "blue", label: "MED" };
263529
- case 4:
263530
- return { text: "\u2193", color: "gray", label: "LOW" };
263531
- default:
263532
- return { text: " ", color: "gray", label: "" };
263533
- }
263534
- }
263535
264605
  function modeBadge(mode) {
263536
264606
  switch (mode) {
263537
264607
  case "fresh":
@@ -263608,14 +264678,52 @@ function workerBorderColor(phase2) {
263608
264678
  return "gray";
263609
264679
  }
263610
264680
  }
263611
- function displayTailLines(activeCount) {
263612
- if (activeCount <= 1)
263613
- return 20;
263614
- if (activeCount <= 2)
263615
- return 12;
263616
- if (activeCount <= 3)
263617
- return 8;
263618
- return 5;
264681
+ function focusedCardTailLines(termHeight, fixedOverhead) {
264682
+ return Math.max(3, termHeight - fixedOverhead);
264683
+ }
264684
+ function glyphColor(status) {
264685
+ switch (status) {
264686
+ case "done":
264687
+ return "green";
264688
+ case "current":
264689
+ return "cyan";
264690
+ case "pending":
264691
+ return "gray";
264692
+ case "failed":
264693
+ return "red";
264694
+ case "bailed":
264695
+ return "magenta";
264696
+ }
264697
+ }
264698
+ function PipelineCells({
264699
+ glyphs
264700
+ }) {
264701
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264702
+ children: PIPELINE_NODES.map((node2, i) => {
264703
+ const isHeader = glyphs === null;
264704
+ const status = isHeader ? null : glyphs[i];
264705
+ const content = isHeader ? NODE_LABELS[node2] : STATUS_GLYPH[status];
264706
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264707
+ children: [
264708
+ i > 0 && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264709
+ dimColor: true,
264710
+ children: PIPELINE_CONNECTOR
264711
+ }, undefined, false, undefined, this),
264712
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264713
+ width: NODE_CELL_WIDTH,
264714
+ justifyContent: "center",
264715
+ children: isHeader ? /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264716
+ dimColor: true,
264717
+ children: content
264718
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264719
+ color: glyphColor(status),
264720
+ children: content
264721
+ }, undefined, false, undefined, this)
264722
+ }, undefined, false, undefined, this)
264723
+ ]
264724
+ }, node2, true, undefined, this);
264725
+ })
264726
+ }, undefined, false, undefined, this);
263619
264727
  }
263620
264728
  function AgentMode({
263621
264729
  args,
@@ -263649,24 +264757,19 @@ function AgentMode({
263649
264757
  }, [awaitingClose]);
263650
264758
  const [, setTick] = import_react63.useState(0);
263651
264759
  const [clock, setClock] = import_react63.useState(0);
263652
- const [focusedIdx, setFocusedIdx] = import_react63.useState(0);
264760
+ const [focusedId, setFocusedId] = import_react63.useState(null);
263653
264761
  const [showPendingTasks, setShowPendingTasks] = import_react63.useState(false);
263654
264762
  const [showAllSubtasks, setShowAllSubtasks] = import_react63.useState(false);
263655
- const [gaveUpCount, setGaveUpCount] = import_react63.useState(0);
263656
264763
  const coordRef = import_react63.useRef(null);
263657
264764
  const workerMetaRef = import_react63.useRef(new Map);
263658
- const gatedTicketsRef = import_react63.useRef(new Map);
263659
264765
  const nextPollAtRef = import_react63.useRef(0);
263660
264766
  const cfgRef = import_react63.useRef(null);
263661
264767
  const [effective, setEffective] = import_react63.useState(null);
263662
264768
  const [pollStatus, setPollStatus] = import_react63.useState({
263663
264769
  state: "idle",
263664
- lastFound: null,
263665
- lastAdded: null,
263666
264770
  lastAt: null,
263667
264771
  filterDesc: "",
263668
- lastBuckets: null,
263669
- lastPrStatus: null
264772
+ lastBoard: []
263670
264773
  });
263671
264774
  function appendLog(text, color, workerLogFile) {
263672
264775
  setLogs((prev) => [...prev, { id: nextId(), text, color }]);
@@ -263702,7 +264805,7 @@ function AgentMode({
263702
264805
  setFatalExit(2);
263703
264806
  return;
263704
264807
  }
263705
- const { coord: coord2, filterDesc, concurrency, pollInterval, runBaselineGate: runBaselineGate2, getGaveUpTotal } = buildCoordinator({
264808
+ const { coord: coord2, filterDesc, concurrency, pollInterval, runBaselineGate: runBaselineGate2 } = buildCoordinator({
263706
264809
  args,
263707
264810
  cfg: cfg2,
263708
264811
  projectRoot,
@@ -263809,13 +264912,6 @@ function AgentMode({
263809
264912
  since: info.since,
263810
264913
  round: info.round
263811
264914
  });
263812
- gatedTicketsRef.current.set(info.changeName, {
263813
- issueIdentifier: info.issueIdentifier,
263814
- issueUrl: info.issueUrl,
263815
- issueTitle: info.issueTitle,
263816
- since: info.since,
263817
- round: info.round
263818
- });
263819
264915
  }
263820
264916
  });
263821
264917
  setEffective({ concurrency, pollInterval });
@@ -263843,28 +264939,18 @@ function AgentMode({
263843
264939
  }
263844
264940
  if (cancelled)
263845
264941
  return;
263846
- gatedTicketsRef.current.clear();
263847
- const { found, added, buckets, prStatus } = await coord2.pollOnce();
264942
+ const { found, added, buckets, prStatus, board: board2 } = await coord2.pollOnce();
263848
264943
  if (cancelled)
263849
264944
  return;
263850
264945
  fileEmit({ type: "poll_done", found, added, buckets, prStatus });
263851
- getGaveUpTotal().then((total) => {
263852
- if (!cancelled)
263853
- setGaveUpCount(total);
263854
- }).catch(() => {
263855
- return;
263856
- });
263857
264946
  if (added > 0) {
263858
264947
  appendLog(` ${added} new issue${added === 1 ? "" : "s"} queued (found ${found} open)`);
263859
264948
  }
263860
264949
  setPollStatus({
263861
264950
  state: "idle",
263862
- lastFound: found,
263863
- lastAdded: added,
263864
264951
  lastAt: Date.now(),
263865
264952
  filterDesc,
263866
- lastBuckets: buckets,
263867
- lastPrStatus: prStatus
264953
+ lastBoard: board2
263868
264954
  });
263869
264955
  nextPollAtRef.current = Date.now() + pollInterval * 1000;
263870
264956
  pollTimer = setTimeout(tick, pollInterval * 1000);
@@ -263979,10 +265065,21 @@ function AgentMode({
263979
265065
  const spinnerFrame = SPINNER_FRAMES[clock % SPINNER_FRAMES.length];
263980
265066
  const now2 = Date.now();
263981
265067
  const secsToNextPoll = nextPollAtRef.current ? Math.max(0, Math.ceil((nextPollAtRef.current - now2) / 1000)) : null;
265068
+ const pollState = pollStatus.state === "polling" ? "polling\u2026" : pollStatus.lastAt !== null ? "idle" : "starting\u2026";
265069
+ const tasksLiveness = `${pollState}${secsToNextPoll !== null ? ` \xB7 ${secsToNextPoll}s \u21BB` : ""}`;
263982
265070
  const activeCount = coord?.activeCount ?? 0;
263983
265071
  const termWidth = columns - 2;
263984
265072
  const termHeight = rows;
263985
- const safeFocusedIdx = activeCount > 0 ? Math.min(focusedIdx, activeCount - 1) : 0;
265073
+ const board = pollStatus.lastBoard;
265074
+ const tree = buildBoardTree(board);
265075
+ const focusedIndex = (() => {
265076
+ if (tree.length === 0)
265077
+ return -1;
265078
+ const i = tree.findIndex((t) => t.row.id === focusedId);
265079
+ return i >= 0 ? i : 0;
265080
+ })();
265081
+ const focusedRow = focusedIndex >= 0 ? tree[focusedIndex].row : undefined;
265082
+ const focusedWorker = focusedRow ? coordRef.current?.activeWorkers.find((w2) => w2.issueId === focusedRow.id) : undefined;
263986
265083
  const steeringFocusedRef = import_react63.useRef(false);
263987
265084
  const steeringBufferRef = import_react63.useRef("");
263988
265085
  const steeringCursorRef = import_react63.useRef(0);
@@ -264000,26 +265097,23 @@ function AgentMode({
264000
265097
  setShowPendingTasks((v2) => !v2);
264001
265098
  return;
264002
265099
  }
264003
- if (activeCount === 0)
265100
+ if (tree.length === 0)
264004
265101
  return;
264005
- if (key.tab || key.rightArrow) {
264006
- setFocusedIdx((i) => (Math.min(i, activeCount - 1) + 1) % activeCount);
264007
- } else if (key.leftArrow) {
264008
- setFocusedIdx((i) => (Math.min(i, activeCount - 1) - 1 + activeCount) % activeCount);
265102
+ const idx = focusedIndex < 0 ? 0 : focusedIndex;
265103
+ if (key.tab || key.downArrow) {
265104
+ setFocusedId(tree[(idx + 1) % tree.length].row.id);
265105
+ } else if (key.upArrow) {
265106
+ setFocusedId(tree[(idx - 1 + tree.length) % tree.length].row.id);
264009
265107
  } else {
264010
265108
  const n = parseInt(input, 10);
264011
- if (!isNaN(n) && n >= 1 && n <= activeCount)
264012
- setFocusedIdx(n - 1);
264013
- }
264014
- }, { isActive: isRawModeSupported && activeCount > 0 });
264015
- const focusedWorker = coordRef.current?.activeWorkers[safeFocusedIdx];
264016
- const steeringActive = isRawModeSupported && activeCount > 0 && focusedWorker !== undefined;
264017
- const nonFocusedCount = Math.max(0, activeCount - 1);
264018
- const tasksBoxLines = activeCount > 1 ? 5 : 0;
265109
+ if (!isNaN(n) && n >= 1 && n <= Math.min(9, tree.length))
265110
+ setFocusedId(tree[n - 1].row.id);
265111
+ }
265112
+ }, { isActive: isRawModeSupported && board.length > 0 });
265113
+ const steeringActive = isRawModeSupported && focusedWorker !== undefined;
264019
265114
  const steeringBoxLines = steeringActive ? 3 : 0;
264020
- const FIXED_OVERHEAD = 5 + 7 + tasksBoxLines + 8 + steeringBoxLines + nonFocusedCount * 4;
264021
- const focusedTailLines = Math.max(3, termHeight - FIXED_OVERHEAD);
264022
- const compactTailLines = displayTailLines(activeCount);
265115
+ const FIXED_OVERHEAD = 5 + (4 + board.length) + 8 + steeringBoxLines;
265116
+ const focusedTailLines = focusedCardTailLines(termHeight, FIXED_OVERHEAD);
264023
265117
  if (preflightError) {
264024
265118
  return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264025
265119
  flexDirection: "column",
@@ -264173,14 +265267,7 @@ function AgentMode({
264173
265267
  cfg.useWorktree && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264174
265268
  color: "green",
264175
265269
  children: " \u25CF worktree"
264176
- }, undefined, false, undefined, this),
264177
- gaveUpCount > 0 && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264178
- color: "red",
264179
- children: [
264180
- " \u2502 gave-up \xD7",
264181
- gaveUpCount
264182
- ]
264183
- }, undefined, true, undefined, this)
265270
+ }, undefined, false, undefined, this)
264184
265271
  ]
264185
265272
  }, undefined, true, undefined, this)
264186
265273
  ]
@@ -264207,418 +265294,196 @@ function AgentMode({
264207
265294
  })()
264208
265295
  ]
264209
265296
  }, undefined, true, undefined, this),
264210
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264211
- flexDirection: "row",
264212
- gap: 1,
264213
- marginTop: 0,
264214
- width: termWidth,
264215
- children: [
264216
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
264217
- label: "POLL STATUS",
264218
- borderColor: "gray",
264219
- width: termWidth - 17,
264220
- paddingX: 1,
264221
- flexDirection: "column",
264222
- children: [
264223
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264224
- gap: 2,
264225
- children: [
264226
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264227
- color: "gray",
264228
- children: spinnerFrame
264229
- }, undefined, false, undefined, this),
264230
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264231
- children: pollStatus.state === "polling" ? "Polling Linear\u2026" : pollStatus.lastAt !== null ? "Idle" : "Starting\u2026"
264232
- }, undefined, false, undefined, this),
264233
- pollStatus.lastAt !== null && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
264234
- children: pollStatus.lastBuckets && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
264235
- children: [
264236
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264237
- dimColor: true,
264238
- children: "\u2502"
264239
- }, undefined, false, undefined, this),
264240
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264241
- dimColor: true,
264242
- children: "todo"
264243
- }, undefined, false, undefined, this),
264244
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264245
- color: "white",
264246
- children: pollStatus.lastBuckets.todo
264247
- }, undefined, false, undefined, this),
264248
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264249
- dimColor: true,
264250
- children: "\xB7"
264251
- }, undefined, false, undefined, this),
264252
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264253
- dimColor: true,
264254
- children: "resume"
264255
- }, undefined, false, undefined, this),
264256
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264257
- color: pollStatus.lastBuckets.inProgress > 0 ? "cyan" : "white",
264258
- children: pollStatus.lastBuckets.inProgress
264259
- }, undefined, false, undefined, this),
264260
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264261
- dimColor: true,
264262
- children: "\xB7"
264263
- }, undefined, false, undefined, this),
264264
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264265
- dimColor: true,
264266
- children: "review"
264267
- }, undefined, false, undefined, this),
264268
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264269
- color: pollStatus.lastBuckets.review > 0 ? "yellow" : "white",
264270
- children: pollStatus.lastBuckets.review
264271
- }, undefined, false, undefined, this),
264272
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264273
- dimColor: true,
264274
- children: "\xB7"
264275
- }, undefined, false, undefined, this),
264276
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264277
- dimColor: true,
264278
- children: "mentions"
264279
- }, undefined, false, undefined, this),
264280
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264281
- color: pollStatus.lastBuckets.mentions > 0 ? "magenta" : "white",
264282
- children: pollStatus.lastBuckets.mentions
264283
- }, undefined, false, undefined, this),
264284
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264285
- dimColor: true,
264286
- children: "\xB7"
264287
- }, undefined, false, undefined, this),
264288
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264289
- dimColor: true,
264290
- children: "awaiting"
264291
- }, undefined, false, undefined, this),
264292
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264293
- color: pollStatus.lastBuckets.awaiting > 0 ? "yellow" : "white",
264294
- children: pollStatus.lastBuckets.awaiting
264295
- }, undefined, false, undefined, this)
264296
- ]
264297
- }, undefined, true, undefined, this)
264298
- }, undefined, false, undefined, this)
264299
- ]
264300
- }, undefined, true, undefined, this),
264301
- pollStatus.lastAt !== null && pollStatus.lastPrStatus && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264302
- gap: 2,
264303
- children: [
264304
- secsToNextPoll !== null ? /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264305
- gap: 1,
264306
- width: 7,
264307
- children: [
264308
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264309
- dimColor: true,
264310
- children: "\u21BA"
264311
- }, undefined, false, undefined, this),
264312
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264313
- color: "gray",
264314
- children: [
264315
- secsToNextPoll,
264316
- "s"
264317
- ]
264318
- }, undefined, true, undefined, this)
264319
- ]
264320
- }, undefined, true, undefined, this) : /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264321
- children: " ".repeat(7)
264322
- }, undefined, false, undefined, this),
264323
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264324
- dimColor: true,
264325
- children: "\u2502"
264326
- }, undefined, false, undefined, this),
264327
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264328
- dimColor: true,
264329
- children: "mergeable"
264330
- }, undefined, false, undefined, this),
264331
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264332
- color: pollStatus.lastPrStatus.mergeable > 0 ? "green" : "white",
264333
- children: pollStatus.lastPrStatus.mergeable
264334
- }, undefined, false, undefined, this),
264335
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264336
- dimColor: true,
264337
- children: "\xB7"
264338
- }, undefined, false, undefined, this),
264339
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264340
- dimColor: true,
264341
- children: "conflicted"
264342
- }, undefined, false, undefined, this),
264343
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264344
- color: pollStatus.lastPrStatus.conflicted > 0 ? "red" : "white",
264345
- children: pollStatus.lastPrStatus.conflicted
264346
- }, undefined, false, undefined, this),
264347
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264348
- dimColor: true,
264349
- children: "\xB7"
264350
- }, undefined, false, undefined, this),
264351
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264352
- dimColor: true,
264353
- children: "ci-failed"
264354
- }, undefined, false, undefined, this),
264355
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264356
- color: pollStatus.lastPrStatus.ciFailed > 0 ? "red" : "white",
264357
- children: pollStatus.lastPrStatus.ciFailed
264358
- }, undefined, false, undefined, this),
264359
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264360
- dimColor: true,
264361
- children: "\xB7"
264362
- }, undefined, false, undefined, this),
264363
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264364
- dimColor: true,
264365
- children: "quarantined"
264366
- }, undefined, false, undefined, this),
264367
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264368
- color: pollStatus.lastPrStatus.quarantined > 0 ? "magenta" : "white",
264369
- bold: true,
264370
- children: pollStatus.lastPrStatus.quarantined
264371
- }, undefined, false, undefined, this)
264372
- ]
264373
- }, undefined, true, undefined, this)
264374
- ]
264375
- }, undefined, true, undefined, this),
264376
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
264377
- label: "WORKERS",
264378
- borderColor: "gray",
264379
- width: 16,
264380
- paddingX: 1,
264381
- flexDirection: "column",
264382
- children: [
264383
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264384
- gap: 1,
264385
- children: [
264386
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264387
- dimColor: true,
264388
- children: "active"
264389
- }, undefined, false, undefined, this),
264390
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264391
- color: activeCount > 0 ? "cyan" : "gray",
264392
- bold: true,
264393
- children: activeCount
264394
- }, undefined, false, undefined, this)
264395
- ]
264396
- }, undefined, true, undefined, this),
264397
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264398
- gap: 1,
264399
- children: [
264400
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264401
- dimColor: true,
264402
- children: "queue"
264403
- }, undefined, false, undefined, this),
264404
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264405
- color: (coord?.queuedCount ?? 0) > 0 ? "yellow" : "gray",
264406
- bold: true,
264407
- children: coord?.queuedCount ?? 0
264408
- }, undefined, false, undefined, this)
264409
- ]
264410
- }, undefined, true, undefined, this)
264411
- ]
264412
- }, undefined, true, undefined, this)
264413
- ]
264414
- }, undefined, true, undefined, this),
264415
- activeCount > 1 && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
264416
- label: `TASKS${activeCount > 1 ? " Tab/\u2190 \u2192 \xB7 1-9" : ""}`,
264417
- borderColor: "gray",
264418
- width: termWidth,
264419
- paddingX: 1,
264420
- flexDirection: "column",
264421
- children: /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264422
- gap: 3,
264423
- flexWrap: "wrap",
264424
- children: coord?.activeWorkers.map((w2, idx) => {
264425
- const meta3 = workerMetaRef.current.get(w2.changeName);
264426
- const phase2 = meta3?.phase ?? "working";
264427
- const pBadge = priorityBadge(w2.issue.priority);
264428
- const isFocused = idx === safeFocusedIdx;
264429
- return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264430
- gap: 1,
264431
- children: [
264432
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264433
- color: isFocused ? "white" : "gray",
264434
- bold: isFocused,
264435
- children: [
264436
- "[",
264437
- idx + 1,
264438
- "]"
264439
- ]
264440
- }, undefined, true, undefined, this),
264441
- pBadge.label && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264442
- color: pBadge.color,
264443
- children: [
264444
- pBadge.text,
264445
- " ",
264446
- pBadge.label
264447
- ]
264448
- }, undefined, true, undefined, this),
264449
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
264450
- url: w2.issue.url,
264451
- label: w2.issueIdentifier,
264452
- color: isFocused ? "cyan" : "gray"
264453
- }, undefined, false, undefined, this),
264454
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264455
- color: phaseColor(phase2),
264456
- dimColor: !isFocused,
264457
- children: phase2
264458
- }, undefined, false, undefined, this),
264459
- isFocused && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264460
- color: "white",
264461
- children: "\u25C0"
264462
- }, undefined, false, undefined, this)
264463
- ]
264464
- }, w2.changeName, true, undefined, this);
264465
- })
264466
- }, undefined, false, undefined, this)
264467
- }, undefined, false, undefined, this),
264468
265297
  (() => {
264469
- const gated = gatedTicketsRef.current;
264470
- if (gated.size === 0)
264471
- return null;
264472
- if (gated.size >= 2) {
264473
- const entries = Array.from(gated.entries()).sort(([, a], [, b2]) => {
264474
- const aTime = a.since ? new Date(a.since).getTime() : 0;
264475
- const bTime = b2.since ? new Date(b2.since).getTime() : 0;
264476
- return bTime - aTime;
264477
- });
264478
- const idLen = entries.reduce((sum2, [, g3]) => sum2 + g3.issueIdentifier.length, 0);
264479
- const multiLabelWidth = idLen + (entries.length - 1) * 3 + 2;
264480
- const labelParts = [];
264481
- entries.forEach(([, g3], i) => {
264482
- labelParts.push(/* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
264483
- url: g3.issueUrl,
264484
- label: g3.issueIdentifier,
264485
- color: "yellow"
264486
- }, g3.issueIdentifier, false, undefined, this));
264487
- if (i < entries.length - 1) {
264488
- labelParts.push(/* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264489
- color: "yellow",
264490
- children: " \xB7 "
264491
- }, `sep-${i}`, false, undefined, this));
264492
- }
264493
- });
264494
- const multiLabelNode = /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265298
+ const tasksInnerWidth = Math.max(0, termWidth - 2);
265299
+ const lead = "\u2500 ";
265300
+ const hint = " Tab/\u2191\u2193\xB71-9 ";
265301
+ const live = ` ${tasksLiveness} `;
265302
+ const trail = "\u2500";
265303
+ const fixed = lead.length + "TASKS".length + hint.length + live.length + trail.length;
265304
+ const fill2 = Math.max(1, tasksInnerWidth - fixed);
265305
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
265306
+ labelVisualWidth: tasksInnerWidth,
265307
+ labelNode: /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265308
+ flexDirection: "row",
264495
265309
  children: [
264496
265310
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264497
- color: "yellow",
264498
- children: " "
265311
+ color: "gray",
265312
+ children: lead
264499
265313
  }, undefined, false, undefined, this),
264500
- labelParts,
264501
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264502
- color: "yellow",
264503
- children: " "
264504
- }, undefined, false, undefined, this)
264505
- ]
264506
- }, undefined, true, undefined, this);
264507
- return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
264508
- labelNode: multiLabelNode,
264509
- labelVisualWidth: multiLabelWidth,
264510
- borderColor: "yellow",
264511
- paddingX: 1,
264512
- gap: 2,
264513
- width: termWidth,
264514
- children: [
264515
265314
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264516
- color: "yellow",
264517
265315
  bold: true,
264518
- children: "[GATE]"
264519
- }, undefined, false, undefined, this),
264520
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264521
- color: "yellow",
264522
- children: "Awaiting confirmation"
265316
+ children: "TASKS"
264523
265317
  }, undefined, false, undefined, this),
264524
265318
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264525
265319
  dimColor: true,
264526
- children: "\xB7"
265320
+ children: hint
264527
265321
  }, undefined, false, undefined, this),
264528
265322
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264529
- color: "white",
264530
- bold: true,
264531
- children: gated.size
265323
+ color: "gray",
265324
+ children: "\u2500".repeat(fill2)
264532
265325
  }, undefined, false, undefined, this),
264533
265326
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264534
265327
  dimColor: true,
264535
- children: "tickets"
265328
+ children: live
265329
+ }, undefined, false, undefined, this),
265330
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265331
+ color: "gray",
265332
+ children: trail
264536
265333
  }, undefined, false, undefined, this)
264537
265334
  ]
264538
- }, undefined, true, undefined, this);
264539
- }
264540
- const { top } = pickLatestGatedTicket(gated);
264541
- if (!top)
264542
- return null;
264543
- const [changeName, g2] = top;
264544
- const askedAgo = g2.since ? fmtElapsed(now2 - Date.parse(g2.since)) : "just now";
264545
- const cardLabelWidth = g2.issueIdentifier.length + 2;
264546
- const cardLabelNode = /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
264547
- children: [
264548
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264549
- color: "yellow",
264550
- children: " "
264551
- }, undefined, false, undefined, this),
264552
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
264553
- url: g2.issueUrl,
264554
- label: g2.issueIdentifier,
264555
- color: "yellow"
264556
- }, undefined, false, undefined, this),
264557
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264558
- color: "yellow",
264559
- children: " "
264560
- }, undefined, false, undefined, this)
264561
- ]
264562
- }, undefined, true, undefined, this);
264563
- return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
264564
- labelNode: cardLabelNode,
264565
- labelVisualWidth: cardLabelWidth,
264566
- borderColor: "yellow",
264567
- paddingX: 1,
264568
- gap: 2,
265335
+ }, undefined, true, undefined, this),
265336
+ borderColor: "gray",
264569
265337
  width: termWidth,
264570
- children: [
264571
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264572
- color: "yellow",
264573
- bold: true,
264574
- children: "[GATE]"
264575
- }, undefined, false, undefined, this),
264576
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264577
- color: "yellow",
264578
- children: "Awaiting confirmation"
264579
- }, undefined, false, undefined, this),
264580
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264581
- dimColor: true,
264582
- children: "\xB7"
264583
- }, undefined, false, undefined, this),
264584
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264585
- dimColor: true,
264586
- children: "round"
264587
- }, undefined, false, undefined, this),
264588
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264589
- color: "white",
264590
- bold: true,
264591
- children: g2.round
264592
- }, undefined, false, undefined, this),
264593
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264594
- dimColor: true,
264595
- children: "\xB7"
264596
- }, undefined, false, undefined, this),
264597
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264598
- dimColor: true,
264599
- children: "asked"
264600
- }, undefined, false, undefined, this),
264601
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264602
- color: "white",
264603
- children: askedAgo
264604
- }, undefined, false, undefined, this),
264605
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264606
- dimColor: true,
264607
- children: "ago"
264608
- }, undefined, false, undefined, this),
264609
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264610
- dimColor: true,
264611
- children: "\u2502"
264612
- }, undefined, false, undefined, this),
264613
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264614
- dimColor: true,
264615
- children: trunc(g2.issueTitle, Math.max(20, termWidth - 70))
264616
- }, undefined, false, undefined, this)
264617
- ]
264618
- }, `gated-${changeName}`, true, undefined, this);
265338
+ paddingX: 1,
265339
+ flexDirection: "column",
265340
+ children: board.length === 0 ? /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265341
+ dimColor: true,
265342
+ children: "no active tickets"
265343
+ }, undefined, false, undefined, this) : (() => {
265344
+ const idColWidth = Math.max(8, ...tree.map((t) => t.depth * 2 + t.row.identifier.length));
265345
+ const idxWidth = String(tree.length).length + 3;
265346
+ const prefixWidth = 2 + idxWidth + idColWidth + 1;
265347
+ const advancing = activeCount > 0 || tree.some((t) => ADVANCING_STATES.has(t.row.state));
265348
+ const hasStartableTodo = tree.some((t) => t.row.state === "todo" && !(t.row.blockedByIds?.length ?? 0));
265349
+ const stalled = !advancing && !hasStartableTodo;
265350
+ const blockedCount = tree.filter((t) => t.row.state === "todo" && (t.row.blockedByIds?.length ?? 0) > 0).length;
265351
+ const awaitingCount = tree.filter((t) => t.row.state === "awaiting").length;
265352
+ const quarantinedCount = tree.filter((t) => t.row.state === "quarantined").length;
265353
+ const stallParts = [
265354
+ blockedCount > 0 ? `${blockedCount} blocked` : null,
265355
+ awaitingCount > 0 ? `${awaitingCount} awaiting confirmation` : null,
265356
+ quarantinedCount > 0 ? `${quarantinedCount} quarantined` : null
265357
+ ].filter((p) => p !== null);
265358
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265359
+ children: [
265360
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265361
+ children: [
265362
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265363
+ children: " ".repeat(prefixWidth)
265364
+ }, undefined, false, undefined, this),
265365
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(PipelineCells, {
265366
+ glyphs: null
265367
+ }, undefined, false, undefined, this)
265368
+ ]
265369
+ }, undefined, true, undefined, this),
265370
+ tree.map(({ row, depth }, i) => {
265371
+ const isFocused = row.id === focusedRow?.id;
265372
+ const indent = depth > 0 ? " ".repeat(depth - 1) + "\u2514 " : "";
265373
+ const blockers = row.blockedByIdentifiers ?? [];
265374
+ const activeW = coordRef.current?.activeWorkers.find((w2) => w2.issueId === row.id);
265375
+ const meta3 = activeW ? workerMetaRef.current.get(activeW.changeName) : undefined;
265376
+ const waitingForWorker = !activeW && WORKER_WAIT_STATES.has(row.state);
265377
+ let age = "\u2013";
265378
+ if (meta3?.startedAt) {
265379
+ age = fmtElapsed(now2 - meta3.startedAt);
265380
+ } else if (!waitingForWorker && row.recovery?.firstFailedAt) {
265381
+ const failedAt = Date.parse(row.recovery.firstFailedAt);
265382
+ if (!Number.isNaN(failedAt))
265383
+ age = fmtElapsed(now2 - failedAt);
265384
+ }
265385
+ const prUrl = meta3?.prUrl ?? row.prUrl ?? null;
265386
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265387
+ children: [
265388
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265389
+ width: 2,
265390
+ children: /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265391
+ color: "white",
265392
+ bold: true,
265393
+ children: isFocused ? "\u25B6" : " "
265394
+ }, undefined, false, undefined, this)
265395
+ }, undefined, false, undefined, this),
265396
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265397
+ width: idxWidth,
265398
+ children: /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265399
+ dimColor: !isFocused,
265400
+ children: [
265401
+ "[",
265402
+ i + 1,
265403
+ "]"
265404
+ ]
265405
+ }, undefined, true, undefined, this)
265406
+ }, undefined, false, undefined, this),
265407
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265408
+ width: idColWidth + 1,
265409
+ children: [
265410
+ indent && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265411
+ dimColor: true,
265412
+ children: indent
265413
+ }, undefined, false, undefined, this),
265414
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265415
+ url: row.url,
265416
+ label: row.identifier,
265417
+ color: isFocused ? "cyan" : "gray"
265418
+ }, undefined, false, undefined, this)
265419
+ ]
265420
+ }, undefined, true, undefined, this),
265421
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(PipelineCells, {
265422
+ glyphs: pipelineStages(row).map((s) => s.status)
265423
+ }, undefined, false, undefined, this),
265424
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265425
+ color: isFocused ? "white" : "gray",
265426
+ dimColor: !isFocused,
265427
+ children: [
265428
+ " ",
265429
+ statusLabel(row)
265430
+ ]
265431
+ }, undefined, true, undefined, this),
265432
+ blockers.length > 0 && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265433
+ color: "yellow",
265434
+ dimColor: !isFocused,
265435
+ children: [
265436
+ " \u26D3 ",
265437
+ trunc(blockers.join(", "), 28)
265438
+ ]
265439
+ }, undefined, true, undefined, this),
265440
+ waitingForWorker ? /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265441
+ color: "yellow",
265442
+ children: " waiting for worker"
265443
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265444
+ dimColor: true,
265445
+ children: [
265446
+ " ",
265447
+ age
265448
+ ]
265449
+ }, undefined, true, undefined, this),
265450
+ prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265451
+ children: [
265452
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265453
+ dimColor: true,
265454
+ children: " \u2197"
265455
+ }, undefined, false, undefined, this),
265456
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265457
+ url: prUrl,
265458
+ label: prLabel(prUrl),
265459
+ color: "green"
265460
+ }, undefined, false, undefined, this)
265461
+ ]
265462
+ }, undefined, true, undefined, this)
265463
+ ]
265464
+ }, row.id, true, undefined, this);
265465
+ }),
265466
+ stalled && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265467
+ marginTop: 1,
265468
+ children: [
265469
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265470
+ color: "yellow",
265471
+ bold: true,
265472
+ children: "\u23F8 nothing can start"
265473
+ }, undefined, false, undefined, this),
265474
+ stallParts.length > 0 && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265475
+ color: "yellow",
265476
+ children: ` \u2014 ${stallParts.join(" \xB7 ")}`
265477
+ }, undefined, false, undefined, this)
265478
+ ]
265479
+ }, undefined, true, undefined, this)
265480
+ ]
265481
+ }, undefined, true, undefined, this);
265482
+ })()
265483
+ }, undefined, false, undefined, this);
264619
265484
  })(),
264620
- coord?.activeWorkers.map((w2, idx) => {
264621
- const isFocused = idx === safeFocusedIdx;
265485
+ focusedWorker && (() => {
265486
+ const w2 = focusedWorker;
264622
265487
  const meta3 = workerMetaRef.current.get(w2.changeName);
264623
265488
  const elapsed = meta3 ? fmtElapsed(now2 - meta3.startedAt) : "\u2013";
264624
265489
  const iter = meta3?.iter ?? 0;
@@ -264632,122 +265497,10 @@ function AgentMode({
264632
265497
  const taskProgress = meta3?.taskProgress ?? null;
264633
265498
  const openspecPhase = meta3?.openspecPhase ?? null;
264634
265499
  const subtasks = meta3?.subtasks ?? [];
264635
- const pBadge = priorityBadge(w2.issue.priority);
264636
265500
  const mBadge = modeBadge(w2.trigger);
264637
265501
  const pColor = phaseColor(phase2);
264638
- const bColor = isFocused ? workerBorderColor(phase2) : "gray";
264639
- const visibleTailLines = isFocused ? focusedTailLines : compactTailLines;
264640
- if (!isFocused && activeCount > 1) {
264641
- const cardLabelWidth2 = (prUrl ? prLabel(prUrl).length + 3 : 0) + w2.issueIdentifier.length + 2;
264642
- const cardLabelNode2 = /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
264643
- children: [
264644
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264645
- color: "gray",
264646
- children: " "
264647
- }, undefined, false, undefined, this),
264648
- prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
264649
- url: prUrl,
264650
- label: prLabel(prUrl),
264651
- color: "green"
264652
- }, undefined, false, undefined, this),
264653
- prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264654
- color: "gray",
264655
- children: " \xB7 "
264656
- }, undefined, false, undefined, this),
264657
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
264658
- url: w2.issue.url,
264659
- label: w2.issueIdentifier,
264660
- color: "cyan"
264661
- }, undefined, false, undefined, this),
264662
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264663
- color: "gray",
264664
- children: " "
264665
- }, undefined, false, undefined, this)
264666
- ]
264667
- }, undefined, true, undefined, this);
264668
- return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
264669
- labelNode: cardLabelNode2,
264670
- labelVisualWidth: cardLabelWidth2,
264671
- borderColor: "gray",
264672
- paddingX: 1,
264673
- gap: 2,
264674
- width: termWidth,
264675
- children: [
264676
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264677
- dimColor: true,
264678
- children: [
264679
- "[",
264680
- idx + 1,
264681
- "]"
264682
- ]
264683
- }, undefined, true, undefined, this),
264684
- pBadge.label && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264685
- color: pBadge.color,
264686
- children: pBadge.text
264687
- }, undefined, false, undefined, this),
264688
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264689
- color: "gray",
264690
- bold: true,
264691
- children: w2.issueIdentifier
264692
- }, undefined, false, undefined, this),
264693
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264694
- dimColor: true,
264695
- children: trunc(w2.issue.title, 40)
264696
- }, undefined, false, undefined, this),
264697
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264698
- dimColor: true,
264699
- children: "\u2502"
264700
- }, undefined, false, undefined, this),
264701
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264702
- color: pColor,
264703
- dimColor: true,
264704
- children: phase2
264705
- }, undefined, false, undefined, this),
264706
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264707
- dimColor: true,
264708
- children: "\u2502"
264709
- }, undefined, false, undefined, this),
264710
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264711
- dimColor: true,
264712
- children: elapsed
264713
- }, undefined, false, undefined, this),
264714
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264715
- dimColor: true,
264716
- children: "\xB7"
264717
- }, undefined, false, undefined, this),
264718
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264719
- dimColor: true,
264720
- children: [
264721
- "\u21BA ",
264722
- iter
264723
- ]
264724
- }, undefined, true, undefined, this),
264725
- currentTask && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
264726
- children: [
264727
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264728
- dimColor: true,
264729
- children: "\u2502"
264730
- }, undefined, false, undefined, this),
264731
- openspecPhase && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264732
- color: openspecPhaseColor(openspecPhase),
264733
- children: [
264734
- "[",
264735
- openspecPhase,
264736
- "]"
264737
- ]
264738
- }, undefined, true, undefined, this),
264739
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264740
- dimColor: true,
264741
- children: [
264742
- "\u25B6 ",
264743
- trunc(currentTask, 40)
264744
- ]
264745
- }, undefined, true, undefined, this)
264746
- ]
264747
- }, undefined, true, undefined, this)
264748
- ]
264749
- }, w2.changeName, true, undefined, this);
264750
- }
265502
+ const bColor = workerBorderColor(phase2);
265503
+ const visibleTailLines = focusedTailLines;
264751
265504
  const cardLabelWidth = (prUrl ? prLabel(prUrl).length + 3 : 0) + w2.issueIdentifier.length + 2;
264752
265505
  const cardLabelNode = /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
264753
265506
  children: [
@@ -264972,7 +265725,7 @@ function AgentMode({
264972
265725
  }, undefined, false, undefined, this)
264973
265726
  ]
264974
265727
  }, undefined, true, undefined, this),
264975
- steeringActive && idx === safeFocusedIdx && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265728
+ steeringActive && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264976
265729
  marginTop: 0,
264977
265730
  children: /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(SteeringField, {
264978
265731
  active: steeringActive,
@@ -264990,8 +265743,12 @@ function AgentMode({
264990
265743
  },
264991
265744
  onSubmit: async (message) => {
264992
265745
  try {
264993
- await appendSteering2(join39(tasksDir, w2.changeName), message);
264994
- fileEmit({ type: "steering_submitted", changeName: w2.changeName, message });
265746
+ await appendSteering2(join38(tasksDir, w2.changeName), message);
265747
+ fileEmit({
265748
+ type: "steering_submitted",
265749
+ changeName: w2.changeName,
265750
+ message
265751
+ });
264995
265752
  } catch (err) {
264996
265753
  const text = err.message;
264997
265754
  fileEmit({
@@ -265067,7 +265824,108 @@ function AgentMode({
265067
265824
  })()
265068
265825
  ]
265069
265826
  }, w2.changeName, true, undefined, this);
265070
- })
265827
+ })(),
265828
+ !focusedWorker && focusedRow && (() => {
265829
+ const row = focusedRow;
265830
+ let age = "\u2013";
265831
+ if (row.recovery?.firstFailedAt) {
265832
+ const failedAt = Date.parse(row.recovery.firstFailedAt);
265833
+ if (!Number.isNaN(failedAt))
265834
+ age = fmtElapsed(now2 - failedAt);
265835
+ }
265836
+ const prUrl = row.prUrl ?? null;
265837
+ const cardLabelWidth = (prUrl ? prLabel(prUrl).length + 3 : 0) + row.identifier.length + 2;
265838
+ const cardLabelNode = /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265839
+ children: [
265840
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265841
+ color: "gray",
265842
+ children: " "
265843
+ }, undefined, false, undefined, this),
265844
+ prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265845
+ url: prUrl,
265846
+ label: prLabel(prUrl),
265847
+ color: "green"
265848
+ }, undefined, false, undefined, this),
265849
+ prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265850
+ color: "gray",
265851
+ children: " \xB7 "
265852
+ }, undefined, false, undefined, this),
265853
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265854
+ url: row.url,
265855
+ label: row.identifier,
265856
+ color: "cyan"
265857
+ }, undefined, false, undefined, this),
265858
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265859
+ color: "gray",
265860
+ children: " "
265861
+ }, undefined, false, undefined, this)
265862
+ ]
265863
+ }, undefined, true, undefined, this);
265864
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
265865
+ labelNode: cardLabelNode,
265866
+ labelVisualWidth: cardLabelWidth,
265867
+ borderColor: "gray",
265868
+ flexDirection: "column",
265869
+ paddingX: 1,
265870
+ width: termWidth,
265871
+ children: [
265872
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265873
+ color: "white",
265874
+ bold: true,
265875
+ children: trunc(row.title, Math.max(20, termWidth - 20))
265876
+ }, undefined, false, undefined, this),
265877
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265878
+ marginTop: 0,
265879
+ children: [
265880
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(PipelineCells, {
265881
+ glyphs: pipelineStages(row).map((s) => s.status)
265882
+ }, undefined, false, undefined, this),
265883
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265884
+ color: "white",
265885
+ children: [
265886
+ " ",
265887
+ statusLabel(row)
265888
+ ]
265889
+ }, undefined, true, undefined, this)
265890
+ ]
265891
+ }, undefined, true, undefined, this),
265892
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265893
+ gap: 2,
265894
+ marginTop: 0,
265895
+ children: [
265896
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265897
+ dimColor: true,
265898
+ children: "parked \xB7 no live worker"
265899
+ }, undefined, false, undefined, this),
265900
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265901
+ dimColor: true,
265902
+ children: "\u2502"
265903
+ }, undefined, false, undefined, this),
265904
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265905
+ dimColor: true,
265906
+ children: [
265907
+ "age ",
265908
+ age
265909
+ ]
265910
+ }, undefined, true, undefined, this),
265911
+ prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265912
+ children: [
265913
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265914
+ dimColor: true,
265915
+ children: "\u2502"
265916
+ }, undefined, false, undefined, this),
265917
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265918
+ url: prUrl,
265919
+ label: prLabel(prUrl),
265920
+ color: "green"
265921
+ }, undefined, false, undefined, this)
265922
+ ]
265923
+ }, undefined, true, undefined, this)
265924
+ ]
265925
+ }, undefined, true, undefined, this)
265926
+ ]
265927
+ }, row.id, true, undefined, this);
265928
+ })()
265071
265929
  ]
265072
265930
  }, undefined, true, undefined, this),
265073
265931
  awaitingClose && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
@@ -265077,13 +265935,14 @@ function AgentMode({
265077
265935
  ]
265078
265936
  }, resizeKey, true, undefined, this);
265079
265937
  }
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;
265938
+ var import_react63, jsx_dev_runtime11, lineCounter = 0, TAIL_BUFFER_SIZE = 30, CMD_DISPLAY_MAX = 80, MAX_PENDING_DISPLAY = 15, SPINNER_FRAMES, WORKER_WAIT_STATES, ADVANCING_STATES, HYPERLINKS_SUPPORTED, NODE_LABELS, NODE_CELL_WIDTH = 4, PIPELINE_CONNECTOR = "\u2500\u2500", SESSION_START;
265081
265939
  var init_AgentMode = __esm(async () => {
265082
265940
  init_cli2();
265083
265941
  init_config();
265084
265942
  init_wire();
265085
265943
  init_preflight();
265086
265944
  init_json_log_file();
265945
+ init_task_pipeline();
265087
265946
  init_phase();
265088
265947
  init_log();
265089
265948
  init_useTerminalSize();
@@ -265099,7 +265958,32 @@ var init_AgentMode = __esm(async () => {
265099
265958
  import_react63 = __toESM(require_react(), 1);
265100
265959
  jsx_dev_runtime11 = __toESM(require_jsx_dev_runtime(), 1);
265101
265960
  SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
265961
+ WORKER_WAIT_STATES = new Set([
265962
+ "queued",
265963
+ "working",
265964
+ "in-progress",
265965
+ "conflict-fix",
265966
+ "ci-fix",
265967
+ "review"
265968
+ ]);
265969
+ ADVANCING_STATES = new Set([
265970
+ "queued",
265971
+ "working",
265972
+ "in-progress",
265973
+ "conflict-fix",
265974
+ "ci-fix",
265975
+ "review",
265976
+ "awaiting-ci"
265977
+ ]);
265102
265978
  HYPERLINKS_SUPPORTED = !process.env["TMUX"];
265979
+ NODE_LABELS = {
265980
+ todo: "todo",
265981
+ confirmation: "conf",
265982
+ work: "work",
265983
+ PR: "PR",
265984
+ CI: "CI",
265985
+ done: "done"
265986
+ };
265103
265987
  SESSION_START = new Date().toISOString();
265104
265988
  });
265105
265989
 
@@ -265299,7 +266183,7 @@ __export(exports_list, {
265299
266183
  buildBuckets: () => buildBuckets,
265300
266184
  backlogRankByIssueId: () => backlogRankByIssueId
265301
266185
  });
265302
- import { join as join40 } from "path";
266186
+ import { join as join39 } from "path";
265303
266187
  function countTaskItems(content) {
265304
266188
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
265305
266189
  const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
@@ -265315,13 +266199,13 @@ function buildLocalRows() {
265315
266199
  const sources = [{ dir: statesDir, label: "main" }];
265316
266200
  const worktreesRoot = worktreesDir2(projectRoot);
265317
266201
  for (const wt of storage.list(worktreesRoot)) {
265318
- sources.push({ dir: join40(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
266202
+ sources.push({ dir: join39(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
265319
266203
  }
265320
266204
  for (const { dir, label } of sources) {
265321
266205
  for (const entry of storage.list(dir)) {
265322
266206
  if (seen.has(entry))
265323
266207
  continue;
265324
- const raw = storage.read(join40(dir, entry, ".ralph-state.json"));
266208
+ const raw = storage.read(join39(dir, entry, ".ralph-state.json"));
265325
266209
  if (raw === null)
265326
266210
  continue;
265327
266211
  let state;
@@ -265336,7 +266220,7 @@ function buildLocalRows() {
265336
266220
  const firstLine = promptRaw.split(`
265337
266221
  `).find((l3) => l3.trim() !== "") ?? "";
265338
266222
  let progress = "\u2014";
265339
- const tasksContent = storage.read(join40(dir, entry, "tasks.md"));
266223
+ const tasksContent = storage.read(join39(dir, entry, "tasks.md"));
265340
266224
  if (tasksContent !== null) {
265341
266225
  const { checked, unchecked } = countTaskItems(tasksContent);
265342
266226
  const total = checked + unchecked;
@@ -265669,8 +266553,8 @@ async function fetchIssueByIdentifier(apiKey, identifier) {
265669
266553
  team { key }
265670
266554
  labels { nodes { name } }
265671
266555
  attachments(first: 25) { nodes { title subtitle } }
265672
- relations(first: 50) {
265673
- nodes { type relatedIssue { id identifier state { type } } }
266556
+ inverseRelations(first: 50) {
266557
+ nodes { type issue { id identifier state { type } } }
265674
266558
  }
265675
266559
  }
265676
266560
  }
@@ -265749,7 +266633,7 @@ Found ${issue2.identifier} \u2014 "${issue2.title}"
265749
266633
  ` + ` assignee: ${issue2.assignee ? `${issue2.assignee.name} <${issue2.assignee.email ?? "no-email"}>` : "(unassigned)"}
265750
266634
  ` + ` labels: ${issue2.labels.nodes.map((l3) => l3.name).join(", ") || "(none)"}
265751
266635
  `);
265752
- const blockedBy = issue2.relations.nodes.filter((r) => r.type === "blocked_by" && r.relatedIssue.state.type !== "completed" && r.relatedIssue.state.type !== "cancelled").map((r) => r.relatedIssue.identifier);
266636
+ const blockedBy = issue2.inverseRelations.nodes.filter((r) => r.type === "blocks" && r.issue.state.type !== "completed" && r.issue.state.type !== "cancelled").map((r) => r.issue.identifier);
265753
266637
  process.stdout.write(`
265754
266638
  Per-bucket diagnostics:
265755
266639
  `);
@@ -265839,7 +266723,7 @@ var exports_json_runner = {};
265839
266723
  __export(exports_json_runner, {
265840
266724
  runAgentJson: () => runAgentJson
265841
266725
  });
265842
- import { join as join41 } from "path";
266726
+ import { join as join40 } from "path";
265843
266727
  import { mkdir as mkdir12 } from "fs/promises";
265844
266728
  import { homedir as homedir8 } from "os";
265845
266729
  function makeEmit(fileSink) {
@@ -265861,7 +266745,7 @@ async function runAgentJson({
265861
266745
  tasksDir,
265862
266746
  runPreflight: runPreflight2 = runPreflight
265863
266747
  }) {
265864
- await mkdir12(join41(homedir8(), ".ralph"), { recursive: true }).catch(() => {
266748
+ await mkdir12(join40(homedir8(), ".ralph"), { recursive: true }).catch(() => {
265865
266749
  return;
265866
266750
  });
265867
266751
  const fileSink = createJsonLogFileSink(args.jsonLogFile);
@@ -266081,7 +266965,7 @@ __export(exports_src3, {
266081
266965
  main: () => main3
266082
266966
  });
266083
266967
  import { mkdir as mkdir13 } from "fs/promises";
266084
- import { join as join42 } from "path";
266968
+ import { join as join41 } from "path";
266085
266969
  async function main3(argv) {
266086
266970
  if (argv.includes("--help") || argv.includes("-h")) {
266087
266971
  printAgentHelp();
@@ -266146,7 +267030,7 @@ async function main3(argv) {
266146
267030
  }
266147
267031
  await mkdir13(statesDir, { recursive: true });
266148
267032
  await mkdir13(tasksDir, { recursive: true });
266149
- await mkdir13(join42(projectRoot, ".ralph"), { recursive: true });
267033
+ await mkdir13(join41(projectRoot, ".ralph"), { recursive: true });
266150
267034
  if (shouldFallbackToJsonOutput(args, process.stdin.isTTY)) {
266151
267035
  process.stderr.write(`agent: stdin is not a TTY \u2014 falling back to --json-output mode.
266152
267036
  `);
@@ -266187,7 +267071,7 @@ async function main3(argv) {
266187
267071
  return typeof process.exitCode === "number" ? process.exitCode : 0;
266188
267072
  }
266189
267073
  var import_react64;
266190
- var init_src8 = __esm(async () => {
267074
+ var init_src9 = __esm(async () => {
266191
267075
  init_context();
266192
267076
  init_layout();
266193
267077
  init_paths();
@@ -266245,7 +267129,7 @@ async function dispatch(subcommand, rest2) {
266245
267129
  return main4(rest2);
266246
267130
  }
266247
267131
  if (subcommand === "agent") {
266248
- const { main: main4 } = await init_src8().then(() => exports_src3);
267132
+ const { main: main4 } = await init_src9().then(() => exports_src3);
266249
267133
  return main4(rest2);
266250
267134
  }
266251
267135
  if (subcommand === "task") {