@neriros/ralphy 3.10.17 → 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 +1370 -1124
  2. package/package.json +2 -1
@@ -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.17")
18932
- return "3.10.17";
18931
+ if ("3.10.18")
18932
+ return "3.10.18";
18933
18933
  } catch {}
18934
18934
  const dirsToTry = [];
18935
18935
  try {
@@ -19655,8 +19655,8 @@ var init_fields = __esm(() => {
19655
19655
  },
19656
19656
  {
19657
19657
  id: "setupScript",
19658
- label: "Worktree setup script (runs before each task)",
19659
- 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.",
19660
19660
  spec: { kind: "text" },
19661
19661
  when: worktreeEnabled
19662
19662
  },
@@ -19699,6 +19699,13 @@ var init_fields = __esm(() => {
19699
19699
  spec: { kind: "text", placeholder: "main" },
19700
19700
  when: isOn("createPrOnSuccess")
19701
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
+ },
19702
19709
  {
19703
19710
  id: "stackPrsOnDependencies",
19704
19711
  label: "Stack dependent issues' PRs onto their blocker's PR?",
@@ -81315,7 +81322,7 @@ function foldLegacyAssignee(v) {
81315
81322
  }
81316
81323
  return rest2;
81317
81324
  }
81318
- var CURRENT_WORKFLOW_VERSION = 7, 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;
81319
81326
  var init_schema = __esm(() => {
81320
81327
  init_zod();
81321
81328
  MarkerSchema = exports_external.discriminatedUnion("type", [
@@ -81455,6 +81462,7 @@ var init_schema = __esm(() => {
81455
81462
  createPrOnSuccess: exports_external.boolean().default(false),
81456
81463
  prDraft: exports_external.boolean().default(false),
81457
81464
  prBaseBranch: exports_external.string().default("main"),
81465
+ prLabels: exports_external.array(exports_external.string()).default([]),
81458
81466
  stackPrsOnDependencies: exports_external.boolean().default(false),
81459
81467
  autoMergeStrategy: exports_external.enum(["squash", "merge", "rebase"]).default("squash"),
81460
81468
  manualMergeWhenAutoMergeDisabled: exports_external.boolean().default(true),
@@ -81509,6 +81517,7 @@ var init_schema = __esm(() => {
81509
81517
  github: exports_external.object({
81510
81518
  base_branch: exports_external.string().optional(),
81511
81519
  auto_merge_strategy: exports_external.enum(["squash", "merge", "rebase"]).optional(),
81520
+ pr_labels: exports_external.array(exports_external.string()).optional(),
81512
81521
  issues: exports_external.object({
81513
81522
  repo: exports_external.string().optional(),
81514
81523
  label: exports_external.string().optional(),
@@ -81591,7 +81600,7 @@ var init_schema = __esm(() => {
81591
81600
  var FRONTMATTER_RE, DEFAULT_WORKFLOW_MD = `---
81592
81601
  # WORKFLOW.md schema version \u2014 managed by \`ralphy init\`. When a newer version
81593
81602
  # ships, re-running init migrates this file and fills in the new settings.
81594
- version: 7
81603
+ version: 8
81595
81604
 
81596
81605
  project:
81597
81606
  name: ralphy
@@ -81643,6 +81652,11 @@ prDraft: true
81643
81652
  prBaseBranch: main
81644
81653
  stackPrsOnDependencies: false
81645
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
81646
81660
 
81647
81661
  prRecovery:
81648
81662
  enabled: true
@@ -82375,6 +82389,9 @@ function applyAliases(cfg) {
82375
82389
  if (cfg.github.auto_merge_strategy !== undefined && cfg.autoMergeStrategy === "squash") {
82376
82390
  cfg.autoMergeStrategy = cfg.github.auto_merge_strategy;
82377
82391
  }
82392
+ if (cfg.github.pr_labels !== undefined && cfg.prLabels.length === 0) {
82393
+ cfg.prLabels = cfg.github.pr_labels;
82394
+ }
82378
82395
  }
82379
82396
  if (cfg.agent) {
82380
82397
  if (cfg.agent.engine !== undefined)
@@ -84691,6 +84708,11 @@ var init_migrations = __esm(() => {
84691
84708
  "github.issues.statusLabels.done",
84692
84709
  "github.issues.statusLabels.error"
84693
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"]
84694
84716
  }
84695
84717
  ];
84696
84718
  LATEST_MIGRATION_VERSION = MIGRATIONS.reduce((max2, migration) => Math.max(max2, migration.version), 0);
@@ -85262,7 +85284,7 @@ function projectLayout(root) {
85262
85284
  stateFile: (name) => join7(statesDir, name, STATE_FILE)
85263
85285
  };
85264
85286
  }
85265
- var STATE_FILE = ".ralph-state.json", GAVEUP_COUNT_FILE = ".ralph-gaveup-count";
85287
+ var STATE_FILE = ".ralph-state.json";
85266
85288
  var init_layout = __esm(() => {
85267
85289
  init_context();
85268
85290
  });
@@ -99729,6 +99751,39 @@ var init_example_machine = __esm(() => {
99729
99751
  });
99730
99752
 
99731
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
+ }
99732
99787
  var preemptionActorLogic, flowMachine;
99733
99788
  var init_flow_machine = __esm(() => {
99734
99789
  init_xstate_development_cjs();
@@ -99775,14 +99830,20 @@ var init_flow_machine = __esm(() => {
99775
99830
  }).createMachine({
99776
99831
  id: "flow",
99777
99832
  context: ({ input }) => ({
99778
- issueId: input?.issueId ?? "",
99779
- bus: input?.bus ?? createNoopBus(),
99780
- persist: input?.persist ?? (() => {}),
99781
- graceMs: input?.graceMs ?? 5000,
99782
- worker: undefined,
99783
- teardown: undefined,
99784
- currentAssignment: undefined,
99785
- 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
+ }
99786
99847
  }),
99787
99848
  initial: "idle",
99788
99849
  states: {
@@ -99791,29 +99852,63 @@ var init_flow_machine = __esm(() => {
99791
99852
  FRESH_PICKED_UP: "working",
99792
99853
  RESUME_DETECTED: "working",
99793
99854
  REVIEW_TRIGGERED: "review",
99794
- CONFLICT_DETECTED: "conflict-fix",
99795
- 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
+ ]
99796
99871
  }
99797
99872
  },
99798
99873
  working: {
99799
99874
  on: {
99800
99875
  AWAITING_DETECTED: "awaiting",
99801
- CONFLICT_DETECTED: "conflict-fix",
99802
- 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
+ ],
99803
99892
  PR_OPENED: "awaiting-ci",
99804
99893
  WORKER_SUCCEEDED: "done",
99805
99894
  WORKER_FAILED: "error",
99806
99895
  PREEMPT: {
99807
99896
  target: "preempting",
99808
99897
  actions: import_xstate_development_cjs.assign({
99809
- pendingAssignment: ({ event }) => event.newAssignment
99898
+ data: ({ context, event }) => ({
99899
+ ...context.data,
99900
+ pendingAssignment: event.newAssignment
99901
+ })
99810
99902
  })
99811
99903
  },
99812
99904
  WORKER_SPAWNED: {
99813
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99814
- worker: event.worker,
99815
- teardown: event.teardown ?? undefined,
99816
- 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
+ }
99817
99912
  }))
99818
99913
  }
99819
99914
  }
@@ -99825,14 +99920,20 @@ var init_flow_machine = __esm(() => {
99825
99920
  PREEMPT: {
99826
99921
  target: "preempting",
99827
99922
  actions: import_xstate_development_cjs.assign({
99828
- pendingAssignment: ({ event }) => event.newAssignment
99923
+ data: ({ context, event }) => ({
99924
+ ...context.data,
99925
+ pendingAssignment: event.newAssignment
99926
+ })
99829
99927
  })
99830
99928
  },
99831
99929
  WORKER_SPAWNED: {
99832
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99833
- worker: event.worker,
99834
- teardown: event.teardown ?? undefined,
99835
- 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
+ }
99836
99937
  }))
99837
99938
  }
99838
99939
  }
@@ -99844,14 +99945,20 @@ var init_flow_machine = __esm(() => {
99844
99945
  PREEMPT: {
99845
99946
  target: "preempting",
99846
99947
  actions: import_xstate_development_cjs.assign({
99847
- pendingAssignment: ({ event }) => event.newAssignment
99948
+ data: ({ context, event }) => ({
99949
+ ...context.data,
99950
+ pendingAssignment: event.newAssignment
99951
+ })
99848
99952
  })
99849
99953
  },
99850
99954
  WORKER_SPAWNED: {
99851
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99852
- worker: event.worker,
99853
- teardown: event.teardown ?? undefined,
99854
- 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
+ }
99855
99962
  }))
99856
99963
  }
99857
99964
  }
@@ -99862,7 +99969,10 @@ var init_flow_machine = __esm(() => {
99862
99969
  PREEMPT: {
99863
99970
  target: "preempting",
99864
99971
  actions: import_xstate_development_cjs.assign({
99865
- pendingAssignment: ({ event }) => event.newAssignment
99972
+ data: ({ context, event }) => ({
99973
+ ...context.data,
99974
+ pendingAssignment: event.newAssignment
99975
+ })
99866
99976
  })
99867
99977
  }
99868
99978
  }
@@ -99870,20 +99980,41 @@ var init_flow_machine = __esm(() => {
99870
99980
  "awaiting-ci": {
99871
99981
  on: {
99872
99982
  PR_PASSED: "done",
99873
- CONFLICT_DETECTED: "conflict-fix",
99874
- 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
+ ],
99875
100000
  REVIEW_TRIGGERED: "review",
99876
100001
  PREEMPT: {
99877
100002
  target: "preempting",
99878
100003
  actions: import_xstate_development_cjs.assign({
99879
- pendingAssignment: ({ event }) => event.newAssignment
100004
+ data: ({ context, event }) => ({
100005
+ ...context.data,
100006
+ pendingAssignment: event.newAssignment
100007
+ })
99880
100008
  })
99881
100009
  },
99882
100010
  WORKER_SPAWNED: {
99883
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99884
- worker: event.worker,
99885
- teardown: event.teardown ?? undefined,
99886
- 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
+ }
99887
100018
  }))
99888
100019
  }
99889
100020
  }
@@ -99894,10 +100025,13 @@ var init_flow_machine = __esm(() => {
99894
100025
  PR_OPENED: "awaiting-ci",
99895
100026
  WORKER_FAILED: "error",
99896
100027
  WORKER_SPAWNED: {
99897
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99898
- worker: event.worker,
99899
- teardown: event.teardown ?? undefined,
99900
- 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
+ }
99901
100035
  }))
99902
100036
  }
99903
100037
  }
@@ -99906,20 +100040,19 @@ var init_flow_machine = __esm(() => {
99906
100040
  invoke: {
99907
100041
  src: "preemption",
99908
100042
  input: ({ context }) => ({
99909
- graceMs: context.graceMs,
99910
- bus: context.bus,
99911
- persist: context.persist,
99912
- issueId: context.issueId,
99913
- newAssignment: context.pendingAssignment,
99914
- ...context.currentAssignment !== undefined ? { from: context.currentAssignment.flowId } : {},
99915
- ...context.worker !== undefined ? { worker: context.worker } : {},
99916
- ...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 } : {}
99917
100051
  }),
99918
100052
  onDone: {
99919
100053
  actions: import_xstate_development_cjs.assign(({ context }) => ({
99920
- worker: undefined,
99921
- teardown: undefined,
99922
- currentAssignment: context.pendingAssignment
100054
+ data: { ...context.data, currentAssignment: context.data.pendingAssignment },
100055
+ runtime: { ...context.runtime, worker: undefined, teardown: undefined }
99923
100056
  })),
99924
100057
  target: "routing-after-preempt"
99925
100058
  },
@@ -99929,32 +100062,40 @@ var init_flow_machine = __esm(() => {
99929
100062
  "routing-after-preempt": {
99930
100063
  always: [
99931
100064
  {
99932
- guard: ({ context }) => context.pendingAssignment?.flowId === "conflict-fix",
100065
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "conflict-fix",
99933
100066
  target: "conflict-fix"
99934
100067
  },
99935
100068
  {
99936
- guard: ({ context }) => context.pendingAssignment?.flowId === "ci-fix",
100069
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "ci-fix",
99937
100070
  target: "ci-fix"
99938
100071
  },
99939
100072
  {
99940
- guard: ({ context }) => context.pendingAssignment?.flowId === "awaiting-ci",
100073
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "awaiting-ci",
99941
100074
  target: "awaiting-ci"
99942
100075
  },
99943
100076
  {
99944
- guard: ({ context }) => context.pendingAssignment?.flowId === "confirmation",
100077
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "confirmation",
99945
100078
  target: "awaiting"
99946
100079
  },
99947
100080
  {
99948
- guard: ({ context }) => context.pendingAssignment?.flowId === "review-followup",
100081
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "review-followup",
99949
100082
  target: "review"
99950
100083
  },
99951
100084
  {
99952
- guard: ({ context }) => context.pendingAssignment?.flowId === "idle",
100085
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "idle",
99953
100086
  target: "idle"
99954
100087
  },
99955
100088
  { target: "working" }
99956
100089
  ]
99957
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
+ },
99958
100099
  done: {
99959
100100
  type: "final"
99960
100101
  },
@@ -99983,17 +100124,22 @@ class FlowActorStore {
99983
100124
  ...this.deps ? {
99984
100125
  bus: this.deps.bus,
99985
100126
  persist: this.deps.persist,
99986
- ...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 } : {}
99987
100129
  } : {}
99988
100130
  };
100131
+ const inspector = this.deps?.onTransition ? this.makeInspect(key, changeDir) : null;
99989
100132
  if (changeDir) {
99990
100133
  const snapshot = await this.loadSnapshot(changeDir);
99991
100134
  if (snapshot !== null && this.isValidSnapshot(snapshot)) {
99992
100135
  try {
100136
+ const restored = this.withRestoredRuntime(snapshot);
99993
100137
  const a2 = import_xstate_development_cjs.createActor(this.machine, {
99994
- snapshot,
99995
- input
100138
+ snapshot: restored,
100139
+ input,
100140
+ ...inspector ? { inspect: inspector.inspect } : {}
99996
100141
  });
100142
+ inspector?.setRoot(a2);
99997
100143
  a2.start();
99998
100144
  if (a2.getSnapshot().value !== undefined) {
99999
100145
  this.actors.set(key, a2);
@@ -100005,11 +100151,61 @@ class FlowActorStore {
100005
100151
  } catch {}
100006
100152
  }
100007
100153
  }
100008
- 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);
100009
100159
  a.start();
100010
100160
  this.actors.set(key, a);
100011
100161
  return a;
100012
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
+ }
100013
100209
  peekActor(key) {
100014
100210
  return this.actors.get(key) ?? null;
100015
100211
  }
@@ -100065,6 +100261,7 @@ var init_flow_actor_store = __esm(() => {
100065
100261
  init_xstate_development_cjs();
100066
100262
  init_flow_machine();
100067
100263
  init_store();
100264
+ init_src2();
100068
100265
  });
100069
100266
 
100070
100267
  // packages/core/src/machines/loop.machine.ts
@@ -102921,6 +103118,9 @@ function formatTicketError(err) {
102921
103118
  parts.push(`configured team: ${e.team}`);
102922
103119
  return parts.length > 0 ? `${e.message} (${parts.join(", ")})` : e.message;
102923
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
+ }
102924
103124
  function partition2(markers) {
102925
103125
  const statuses = [];
102926
103126
  const labels = [];
@@ -103140,8 +103340,8 @@ async function fetchMentionScanIssues(apiKey, spec) {
103140
103340
  project { id name priority }
103141
103341
  projectMilestone { id name sortOrder targetDate }
103142
103342
  labels { nodes { name } }
103143
- relations(first: 50) {
103144
- nodes { type relatedIssue { id identifier state { type } } }
103343
+ inverseRelations(first: 50) {
103344
+ nodes { type issue { id identifier state { type } } }
103145
103345
  }
103146
103346
  comments(first: 50) {
103147
103347
  nodes { id body createdAt user { name email } }
@@ -103152,24 +103352,26 @@ async function fetchMentionScanIssues(apiKey, spec) {
103152
103352
  const data = await linearRequest(apiKey, query, {
103153
103353
  filter: where
103154
103354
  });
103155
- const DONE_STATE_TYPES = new Set(["completed", "cancelled"]);
103156
- return data.issues.nodes.map((n) => ({
103157
- id: n.id,
103158
- identifier: n.identifier,
103159
- title: n.title,
103160
- description: n.description,
103161
- url: n.url,
103162
- state: n.state,
103163
- assignee: n.assignee,
103164
- project: mapNodeProject(n),
103165
- ...milestoneSpread(n),
103166
- labels: n.labels.nodes.map((l) => l.name),
103167
- priority: n.priority,
103168
- createdAt: n.createdAt ?? "",
103169
- blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id),
103170
- blockedByIdentifiers: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.identifier),
103171
- comments: n.comments?.nodes ?? []
103172
- }));
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
+ });
103173
103375
  }
103174
103376
  async function fetchOpenIssues(apiKey, spec, options) {
103175
103377
  const where = buildIssueFilter(spec);
@@ -103186,10 +103388,10 @@ async function fetchOpenIssues(apiKey, spec, options) {
103186
103388
  project { id name priority }
103187
103389
  projectMilestone { id name sortOrder targetDate }
103188
103390
  labels { nodes { name } }
103189
- relations(first: 50) {
103391
+ inverseRelations(first: 50) {
103190
103392
  nodes {
103191
103393
  type
103192
- relatedIssue { id identifier state { type } }
103394
+ issue { id identifier state { type } }
103193
103395
  }
103194
103396
  }
103195
103397
  ${commentsSlice}
@@ -103199,24 +103401,26 @@ async function fetchOpenIssues(apiKey, spec, options) {
103199
103401
  const data = await linearRequest(apiKey, query, {
103200
103402
  filter: where
103201
103403
  });
103202
- const DONE_STATE_TYPES = new Set(["completed", "cancelled"]);
103203
- return data.issues.nodes.map((n) => ({
103204
- id: n.id,
103205
- identifier: n.identifier,
103206
- title: n.title,
103207
- description: n.description,
103208
- url: n.url,
103209
- state: n.state,
103210
- assignee: n.assignee,
103211
- project: mapNodeProject(n),
103212
- ...milestoneSpread(n),
103213
- labels: n.labels.nodes.map((l) => l.name),
103214
- priority: n.priority,
103215
- createdAt: n.createdAt ?? "",
103216
- blockedByIds: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.id),
103217
- blockedByIdentifiers: (n.relations?.nodes ?? []).filter((r) => r.type === "blocked_by" && !DONE_STATE_TYPES.has(r.relatedIssue.state.type)).map((r) => r.relatedIssue.identifier),
103218
- ...includeComments ? { comments: n.comments?.nodes ?? [] } : {}
103219
- }));
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
+ });
103220
103424
  }
103221
103425
  function isRetryableStatus(status) {
103222
103426
  return status >= 500 && status <= 599;
@@ -103530,17 +103734,15 @@ async function fetchBlockedByForIssues(apiKey, issueIds) {
103530
103734
  issues(filter: { id: { in: $ids } }, first: 250) {
103531
103735
  nodes {
103532
103736
  id
103533
- relations(first: 50) {
103534
- nodes { type relatedIssue { id identifier state { type } } }
103737
+ inverseRelations(first: 50) {
103738
+ nodes { type issue { id identifier state { type } } }
103535
103739
  }
103536
103740
  }
103537
103741
  }
103538
103742
  }`;
103539
103743
  const data = await linearRequest(apiKey, query, { ids: issueIds });
103540
- const DONE_STATE_TYPES = new Set(["completed", "cancelled"]);
103541
103744
  for (const node2 of data.issues.nodes) {
103542
- 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 }));
103543
- out.set(node2.id, blockers);
103745
+ out.set(node2.id, openBlockersFromInverse(node2.inverseRelations?.nodes));
103544
103746
  }
103545
103747
  return out;
103546
103748
  }
@@ -103754,12 +103956,13 @@ async function removeLabelFromIssue(apiKey, issueId, labelId) {
103754
103956
  labelId
103755
103957
  });
103756
103958
  }
103757
- var LINEAR_API = "https://api.linear.app/graphql", TICKET_IDENTIFIER_RE, TICKET_BARE_NUMBER_RE, RALPHY_ATTACHMENT_TITLE_FILTER = "Ralphy", linearRequestInternals, MAX_LINEAR_ATTEMPTS = 3, MAX_RETRY_AFTER_MS = 2000, BODY_TRUNCATE_CHARS = 512, RALPHY_ATTACHMENT_TITLE = "Ralphy", BRANCH_LABEL_PREFIX = "ralph:branch:";
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:";
103758
103960
  var init_linear_client = __esm(() => {
103759
103961
  init_types2();
103760
103962
  init_ralph_comment();
103761
103963
  TICKET_IDENTIFIER_RE = /^([A-Za-z]+)-(\d+)(?:-.*)?$/;
103762
103964
  TICKET_BARE_NUMBER_RE = /^(\d+)$/;
103965
+ DONE_BLOCKER_STATE_TYPES = new Set(["completed", "cancelled"]);
103763
103966
  linearRequestInternals = {
103764
103967
  sleep: (ms) => Bun.sleep(ms)
103765
103968
  };
@@ -103866,7 +104069,7 @@ async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
103866
104069
  if (list.stdout.includes(`worktree ${cwd2}
103867
104070
  `)) {
103868
104071
  await installPrePushHook(cwd2, runner);
103869
- return { cwd: cwd2, branch };
104072
+ return { cwd: cwd2, branch, created: false };
103870
104073
  }
103871
104074
  let branchExists = true;
103872
104075
  try {
@@ -103877,12 +104080,12 @@ async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
103877
104080
  if (branchExists) {
103878
104081
  await runner.run(["worktree", "add", cwd2, branch], projectRoot);
103879
104082
  await installPrePushHook(cwd2, runner);
103880
- return { cwd: cwd2, branch };
104083
+ return { cwd: cwd2, branch, created: true };
103881
104084
  }
103882
104085
  await runner.run(["fetch", "origin", baseBranch], projectRoot);
103883
104086
  await runner.run(["worktree", "add", "-b", branch, cwd2, `origin/${baseBranch}`], projectRoot);
103884
104087
  await installPrePushHook(cwd2, runner);
103885
- return { cwd: cwd2, branch };
104088
+ return { cwd: cwd2, branch, created: true };
103886
104089
  }
103887
104090
  async function installPrePushHook(cwd2, runner) {
103888
104091
  const hookPath = join22(cwd2, ".ralph-hooks", "pre-push");
@@ -104188,6 +104391,14 @@ async function branchAlreadyMerged(runner, cwd2, branch, base2) {
104188
104391
  } catch {}
104189
104392
  return false;
104190
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
+ }
104191
104402
  async function createPullRequest(input, runner) {
104192
104403
  const base2 = input.base ?? "main";
104193
104404
  const log3 = await runner.run(["git", "log", "--oneline", `${base2}..HEAD`, "--no-merges"], input.cwd);
@@ -104224,8 +104435,10 @@ async function createPullRequest(input, runner) {
104224
104435
  ".[0].url // empty"
104225
104436
  ], input.cwd);
104226
104437
  const existingUrl = existing.stdout.trim();
104227
- if (existingUrl)
104438
+ if (existingUrl) {
104439
+ await applyPrLabels(runner, input.cwd, existingUrl, input.labels ?? []);
104228
104440
  return { url: existingUrl, created: false };
104441
+ }
104229
104442
  const title = defaultTitle(input.issue);
104230
104443
  const body = defaultBody(input.issue, input.branch, input.stackedOn);
104231
104444
  const createArgs = ["gh", "pr", "create", "--base", base2, "--title", title, "--body", body];
@@ -104234,6 +104447,7 @@ async function createPullRequest(input, runner) {
104234
104447
  const created = await runner.run(createArgs, input.cwd);
104235
104448
  const url2 = created.stdout.trim().split(`
104236
104449
  `).pop() ?? "";
104450
+ await applyPrLabels(runner, input.cwd, url2, input.labels ?? []);
104237
104451
  return { url: url2, created: true };
104238
104452
  }
104239
104453
  var init_pr = __esm(() => {
@@ -104787,7 +105001,7 @@ function emitFeatureSkipped(bus, id, reason) {
104787
105001
  var init_run_feature = () => {};
104788
105002
 
104789
105003
  // apps/agent/src/agent/post-task.ts
104790
- import { join as join23, dirname as dirname9 } from "path";
105004
+ import { join as join23 } from "path";
104791
105005
  function summarizeUncommittedStatus(stdout) {
104792
105006
  const lines = stdout.split(`
104793
105007
  `).filter((line) => line.length > 0);
@@ -104914,6 +105128,7 @@ async function createPrWithRetry(ctx, issue2) {
104914
105128
  base: base2,
104915
105129
  metaOnlyFiles: ctx.cfg.metaOnlyFiles ?? [],
104916
105130
  draft: ctx.cfg.prDraft ?? false,
105131
+ labels: ctx.cfg.prLabels ?? [],
104917
105132
  ...ctx.stackedOn ? {
104918
105133
  stackedOn: {
104919
105134
  prUrl: ctx.stackedOn.prUrl,
@@ -105285,17 +105500,6 @@ async function runValidateOnlyPhase(input, deps) {
105285
105500
  await reactivateState(stateFilePath, log3, changeName);
105286
105501
  return respawnWorker();
105287
105502
  }
105288
- async function recordGaveUp(stateFilePath, log3, changeName) {
105289
- const path = join23(dirname9(stateFilePath), GAVEUP_COUNT_FILE);
105290
- try {
105291
- const file2 = Bun.file(path);
105292
- const current = await file2.exists() ? Number.parseInt(await file2.text(), 10) || 0 : 0;
105293
- await Bun.write(path, String(current + 1) + `
105294
- `);
105295
- } catch (err) {
105296
- log3(`! could not record gave-up for ${changeName}: ${err.message}`, "yellow");
105297
- }
105298
- }
105299
105503
  async function runPostTask(input, deps) {
105300
105504
  const { log: log3, cmd, git: git2, runScript } = deps;
105301
105505
  const emit3 = (phase, detail) => deps.onPhase?.(phase, detail);
@@ -105338,8 +105542,6 @@ async function runPostTask(input, deps) {
105338
105542
  respawnWorker
105339
105543
  });
105340
105544
  emit3(effectiveCode === 0 ? "done" : "gave-up", effectiveCode !== 0 ? `exit ${effectiveCode}` : undefined);
105341
- if (effectiveCode !== 0)
105342
- await recordGaveUp(stateFilePath, log3, changeName);
105343
105545
  await runWorktreeCleanupPhase({ changeName, cwd: cwd2, projectRoot, useWorktree, effectiveCode, cfg }, { git: git2, log: log3, emit: emit3 });
105344
105546
  await runTeardownPhase({ cwd: cwd2, teardownScript: cfg.teardownScript }, { runScript, log: log3, emit: emit3 });
105345
105547
  return effectiveCode;
@@ -105362,7 +105564,6 @@ async function runPostTask(input, deps) {
105362
105564
  if (checked && aheadCount > 0) {
105363
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");
105364
105566
  emit3("gave-up", "unpushed conflict resolution");
105365
- await recordGaveUp(stateFilePath, log3, changeName);
105366
105567
  await runWorktreeCleanupPhase({ changeName, cwd: cwd2, projectRoot, useWorktree, effectiveCode: PR_FAILED_EXIT, cfg }, { git: git2, log: log3, emit: emit3 });
105367
105568
  await runTeardownPhase({ cwd: cwd2, teardownScript: cfg.teardownScript }, { runScript, log: log3, emit: emit3 });
105368
105569
  return PR_FAILED_EXIT;
@@ -105432,8 +105633,6 @@ async function runPostTask(input, deps) {
105432
105633
  }
105433
105634
  const succeeded = effectiveCode === 0 || effectiveCode === NO_CHANGES_EXIT;
105434
105635
  emit3(succeeded ? "done" : "gave-up", succeeded ? undefined : `exit ${effectiveCode}`);
105435
- if (!succeeded)
105436
- await recordGaveUp(stateFilePath, log3, changeName);
105437
105636
  await deps.runRetrospective?.({
105438
105637
  changeName,
105439
105638
  cwd: cwd2,
@@ -105460,7 +105659,6 @@ var PR_FAILED_EXIT = 71, MAX_PR_CREATE_ATTEMPTS = 5, NO_CHANGES_EXIT = 72, repoA
105460
105659
  return { exitCode: proc.exitCode ?? 1, output };
105461
105660
  };
105462
105661
  var init_post_task = __esm(() => {
105463
- init_layout();
105464
105662
  init_tasks_md();
105465
105663
  init_fs_change();
105466
105664
  init_git2();
@@ -105474,6 +105672,300 @@ var init_post_task = __esm(() => {
105474
105672
  repoAutoMergeCache = new Map;
105475
105673
  });
105476
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
+
105477
105969
  // packages/core/src/ordering/hierarchical-order.ts
105478
105970
  function rank(priority) {
105479
105971
  return !priority ? Number.POSITIVE_INFINITY : priority;
@@ -105735,12 +106227,13 @@ var init_queue_order = __esm(() => {
105735
106227
  });
105736
106228
 
105737
106229
  // apps/agent/src/runtime/coordinator.ts
106230
+ import { appendFile as appendFile2 } from "fs/promises";
105738
106231
  function emitCapture(bus, event, properties) {
105739
106232
  capture(event, properties);
105740
106233
  bus.emit({ type: event, ...properties });
105741
106234
  }
105742
106235
  function completionCommentBody(args) {
105743
- const { noChanges, ok, trigger, changeName, code } = args;
106236
+ const { noChanges, ok, trigger, changeName, code, reachedDone } = args;
105744
106237
  if (noChanges) {
105745
106238
  return buildRalphyComment({
105746
106239
  type: "completed-noop",
@@ -105767,6 +106260,23 @@ function completionCommentBody(args) {
105767
106260
  fields: { change: changeName }
105768
106261
  });
105769
106262
  }
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
+ }
105770
106280
  return buildRalphyComment({
105771
106281
  type: "completed",
105772
106282
  action: "completed work",
@@ -105799,7 +106309,19 @@ class AgentCoordinator {
105799
106309
  this.opts = opts;
105800
106310
  this.bus = deps.bus ?? createNoopBus();
105801
106311
  const providedMachine = flowMachine.provide({ actors: { preemption: preemptionActorLogic } });
105802
- 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);
105803
106325
  }
105804
106326
  get activeCount() {
105805
106327
  return this.workers.length;
@@ -105871,7 +106393,15 @@ class AgentCoordinator {
105871
106393
  awaiting: awaitingCount
105872
106394
  };
105873
106395
  const found2 = buckets2.todo + buckets2.inProgress + buckets2.mentions + buckets2.awaiting;
105874
- 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
+ };
105875
106405
  }
105876
106406
  const maxT = this.opts.maxTickets ?? 0;
105877
106407
  const atTicketLimit = () => {
@@ -105950,7 +106480,7 @@ class AgentCoordinator {
105950
106480
  added += 1;
105951
106481
  this.deps.onLog(` \u21B3 ${issue2.identifier} queued (fresh)`, "gray");
105952
106482
  }
105953
- const prStatus = await this.scanPrMergeStates();
106483
+ const { counts: prStatus, prByIssue } = await this.scanPrMergeStates();
105954
106484
  if (this.queue.length > 0) {
105955
106485
  this.queue = orderQueueEntries(this.queue, this.opts.getAutoMerge);
105956
106486
  }
@@ -105980,7 +106510,103 @@ class AgentCoordinator {
105980
106510
  flow2[w.changeName] = "working";
105981
106511
  }
105982
106512
  }
105983
- 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
+ };
105984
106610
  }
105985
106611
  async walkRegistryForInProgress(inProgress) {
105986
106612
  const claimed = new Map;
@@ -106191,20 +106817,20 @@ class AgentCoordinator {
106191
106817
  }
106192
106818
  async scanPrMergeStates() {
106193
106819
  const counts = emptyPrStatus();
106820
+ const prByIssue = new Map;
106194
106821
  if (!this.opts.prRecovery?.enabled)
106195
- return counts;
106822
+ return { counts, prByIssue };
106196
106823
  let candidates = [];
106197
106824
  try {
106198
106825
  candidates = await this.deps.fetchDoneCandidates();
106199
106826
  } catch (err) {
106200
106827
  this.deps.onLog(`! PR merge-state scan fetch failed: ${err.message}`, "yellow");
106201
- return counts;
106828
+ return { counts, prByIssue };
106202
106829
  }
106203
106830
  if (candidates.length === 0)
106204
- return counts;
106831
+ return { counts, prByIssue };
106205
106832
  const preQueue = this.queue.map((q) => ({ id: q.issue.id, trigger: q.trigger }));
106206
106833
  const preWorkers = this.workers.map((w) => ({ id: w.issueId, trigger: w.trigger }));
106207
- const tracker = this.opts.prTracker;
106208
106834
  for (const issue2 of candidates) {
106209
106835
  if (this.workers.some((w) => w.issueId === issue2.id))
106210
106836
  continue;
@@ -106212,12 +106838,17 @@ class AgentCoordinator {
106212
106838
  continue;
106213
106839
  if (this.queue.some((q) => q.issue.id === issue2.id))
106214
106840
  continue;
106215
- if (tracker?.isBailed(issue2.identifier) && this.errorMarkerCleared(issue2)) {
106216
- 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(() => {});
106217
106848
  this.conflictNotified.delete(issue2.id);
106218
106849
  this.ciFailedNotified.delete(issue2.id);
106219
106850
  this.conflictPromoted.delete(issue2.id);
106220
- 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");
106221
106852
  }
106222
106853
  let pr;
106223
106854
  try {
@@ -106228,45 +106859,47 @@ class AgentCoordinator {
106228
106859
  }
106229
106860
  if (!pr)
106230
106861
  continue;
106862
+ prByIssue.set(issue2.id, pr);
106231
106863
  if (pr.status === "mergeable")
106232
106864
  counts.mergeable += 1;
106233
- if (pr.status === "mergeable" && this.opts.prTracker) {
106234
- try {
106235
- await this.opts.prTracker.clear(issue2.identifier);
106236
- } catch (err) {
106237
- this.deps.onLog(`! pr-tracker clear failed for ${issue2.identifier}: ${err.message}`, "yellow");
106238
- }
106239
- }
106240
106865
  if (pr.status === "mergeable") {
106241
- const changeDir = this.deps.getChangeDir?.(issue2) ?? undefined;
106242
- const actor = await this.flowStore.getActor(issue2.id, changeDir);
106243
- if (actor.getSnapshot().value === "awaiting-ci") {
106866
+ const value = stateValue();
106867
+ if (value === "awaiting-ci" || value === "quarantined") {
106244
106868
  if (this.issueInSetDoneState(issue2)) {
106869
+ actor.send({ type: "RECOVERY_CLEARED" });
106245
106870
  actor.send({ type: "PR_PASSED" });
106246
106871
  if (changeDir)
106247
106872
  await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106248
- if (actor.getSnapshot().value === "done") {
106873
+ if (stateValue() === "done")
106249
106874
  this.flowStore.disposeActor(issue2.id);
106250
- }
106251
106875
  } else {
106252
106876
  await this.advancePrToDone(issue2, pr.url, actor, changeDir);
106253
106877
  }
106878
+ } else {
106879
+ actor.send({ type: "RECOVERY_CLEARED" });
106880
+ if (changeDir)
106881
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106254
106882
  }
106255
106883
  continue;
106256
106884
  }
106257
106885
  if (pr.status === "conflicted") {
106258
106886
  if (!this.opts.prRecovery?.fixConflicts)
106259
106887
  continue;
106260
- if (tracker?.isBailed(issue2.identifier)) {
106888
+ if (stateValue() === "quarantined") {
106261
106889
  counts.quarantined += 1;
106262
106890
  continue;
106263
106891
  }
106264
106892
  counts.conflicted += 1;
106265
106893
  if (this.conflictNotified.has(issue2.id))
106266
106894
  continue;
106267
- 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") {
106268
106900
  counts.conflicted -= 1;
106269
106901
  counts.quarantined += 1;
106902
+ await this.quarantineBail(issue2, pr.url, "conflicting", actor);
106270
106903
  continue;
106271
106904
  }
106272
106905
  emitCapture(this.bus, "agent_conflict_detected", { issue_identifier: issue2.identifier });
@@ -106284,9 +106917,6 @@ class AgentCoordinator {
106284
106917
  this.deps.onLog(`! Linear conflict comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106285
106918
  }
106286
106919
  }
106287
- const conflictActor = await this.flowStore.getActor(issue2.id, this.deps.getChangeDir?.(issue2) ?? undefined);
106288
- conflictActor.send({ type: "RESUME_DETECTED" });
106289
- conflictActor.send({ type: "CONFLICT_DETECTED" });
106290
106920
  this.queue.push({
106291
106921
  issue: issue2,
106292
106922
  trigger: "conflict-fix",
@@ -106297,16 +106927,21 @@ class AgentCoordinator {
106297
106927
  if (pr.status === "ci_failed") {
106298
106928
  if (!this.opts.prRecovery?.fixCi)
106299
106929
  continue;
106300
- if (tracker?.isBailed(issue2.identifier)) {
106930
+ if (stateValue() === "quarantined") {
106301
106931
  counts.quarantined += 1;
106302
106932
  continue;
106303
106933
  }
106304
106934
  counts.ciFailed += 1;
106305
106935
  if (this.ciFailedNotified.has(issue2.id))
106306
106936
  continue;
106307
- 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") {
106308
106942
  counts.ciFailed -= 1;
106309
106943
  counts.quarantined += 1;
106944
+ await this.quarantineBail(issue2, pr.url, "ci_failed", actor);
106310
106945
  continue;
106311
106946
  }
106312
106947
  emitCapture(this.bus, "agent_ci_failed_detected", { issue_identifier: issue2.identifier });
@@ -106324,9 +106959,6 @@ class AgentCoordinator {
106324
106959
  this.deps.onLog(`! Linear ci-failed comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106325
106960
  }
106326
106961
  }
106327
- const ciActor = await this.flowStore.getActor(issue2.id, this.deps.getChangeDir?.(issue2) ?? undefined);
106328
- ciActor.send({ type: "RESUME_DETECTED" });
106329
- ciActor.send({ type: "CI_FAILED_DETECTED" });
106330
106962
  this.queue.push({
106331
106963
  issue: issue2,
106332
106964
  trigger: "ci-fix",
@@ -106346,7 +106978,7 @@ class AgentCoordinator {
106346
106978
  else if (w.trigger === "ci-fix")
106347
106979
  counts.ciFailed += 1;
106348
106980
  }
106349
- return counts;
106981
+ return { counts, prByIssue };
106350
106982
  }
106351
106983
  issueInSetDoneState(issue2) {
106352
106984
  const sd = this.opts.setDone;
@@ -106356,6 +106988,7 @@ class AgentCoordinator {
106356
106988
  }
106357
106989
  async advancePrToDone(issue2, prUrl, actor, changeDir) {
106358
106990
  this.deps.onLog(` ${issue2.identifier}: PR ${prUrl} mergeable \u2014 moving to done`, "green");
106991
+ actor.send({ type: "RECOVERY_CLEARED" });
106359
106992
  if (this.opts.setDone) {
106360
106993
  try {
106361
106994
  await this.deps.applyIndicator(issue2, this.opts.setDone);
@@ -106367,6 +107000,8 @@ class AgentCoordinator {
106367
107000
  issue_identifier: issue2.identifier,
106368
107001
  error: err.message
106369
107002
  });
107003
+ if (changeDir)
107004
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106370
107005
  return;
106371
107006
  }
106372
107007
  if (this.opts.setInProgress) {
@@ -106406,48 +107041,34 @@ class AgentCoordinator {
106406
107041
  const have = new Set(issue2.labels.map((l) => l.toLowerCase()));
106407
107042
  return !wantLabels.some((v) => have.has(v));
106408
107043
  }
106409
- async prTrackerBail(issue2, prUrl, reason) {
106410
- const tracker = this.opts.prTracker;
106411
- if (!tracker)
106412
- return false;
106413
- let decision;
106414
- try {
106415
- decision = await tracker.recordFailure(issue2.identifier, reason);
106416
- } catch (err) {
106417
- this.deps.onLog(`! pr-tracker record failed for ${issue2.identifier}: ${err.message}`, "yellow");
106418
- return false;
106419
- }
106420
- if (decision.kind === "demote")
106421
- return false;
106422
- if (decision.firstBail) {
106423
- this.deps.onLog(` ${issue2.identifier}: pr-tracker bailing after ${decision.attempts} recovery attempts (${reason}) \u2014 applying setError`, "red");
106424
- emitCapture(this.bus, "agent_pr_tracker_bailed", {
106425
- issue_identifier: issue2.identifier,
106426
- reason,
106427
- attempts: decision.attempts
106428
- });
106429
- if (this.opts.setError) {
106430
- try {
106431
- await this.deps.applyIndicator(issue2, this.opts.setError);
106432
- } catch (err) {
106433
- this.deps.onLog(`! Linear setError failed for ${issue2.identifier}: ${err.message}`, "yellow");
106434
- }
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");
106435
107057
  }
106436
- if (this.opts.postComments !== false) {
106437
- const human = reason === "conflicting" ? "merge conflicts" : "failing CI";
106438
- try {
106439
- await this.deps.postComment(issue2, buildRalphyComment({
106440
- type: "recovery-gaveup",
106441
- action: "gave up auto-recovering PR",
106442
- body: `Gave up auto-recovering this PR (${prUrl}) after ${decision.attempts} attempts \u2014 last failure: ${human}. The \`ralph:error\` label has been applied; clear it (or merge the PR) once a human has looked at it.`,
106443
- fields: { pr: extractPrNumber(prUrl) ?? prUrl, attempts: decision.attempts }
106444
- }));
106445
- } catch (err) {
106446
- this.deps.onLog(`! Linear bail comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106447
- }
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");
106448
107070
  }
106449
107071
  }
106450
- return true;
106451
107072
  }
106452
107073
  spawnNext() {
106453
107074
  if (this.stopped)
@@ -106762,7 +107383,14 @@ class AgentCoordinator {
106762
107383
  }
106763
107384
  }
106764
107385
  if (this.opts.postComments !== false) {
106765
- 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
+ });
106766
107394
  try {
106767
107395
  await this.deps.postComment(issue2, body);
106768
107396
  this.deps.onLog(` ${issue2.identifier}: posted completion comment`, "gray");
@@ -106856,12 +107484,15 @@ var emptyPrStatus = () => ({
106856
107484
  },
106857
107485
  prStatus: emptyPrStatus(),
106858
107486
  phase: {},
106859
- flow: {}
107487
+ flow: {},
107488
+ board: []
106860
107489
  });
106861
107490
  var init_coordinator = __esm(() => {
106862
107491
  init_types2();
106863
107492
  init_linear_client();
106864
107493
  init_post_task();
107494
+ init_scaffold();
107495
+ init_task_pipeline();
106865
107496
  init_queue_order();
106866
107497
  init_src();
106867
107498
  init_src2();
@@ -106876,122 +107507,6 @@ var init_coordinator2 = __esm(() => {
106876
107507
  init_coordinator();
106877
107508
  });
106878
107509
 
106879
- // apps/agent/src/shared/capabilities/gh-client.ts
106880
- var init_gh_client = () => {};
106881
-
106882
- // apps/agent/src/shared/capabilities/github/github-client.ts
106883
- var STARTED_LABEL_NAMES, ISSUE_FIELDS = "id,number,title,body,state,stateReason,labels,assignees,author,createdAt,url", ISSUE_FIELDS_WITH_COMMENTS;
106884
- var init_github_client = __esm(() => {
106885
- init_worktree();
106886
- init_gh_client();
106887
- STARTED_LABEL_NAMES = new Set(["in progress", "in-progress", "started"]);
106888
- ISSUE_FIELDS_WITH_COMMENTS = `${ISSUE_FIELDS},comments`;
106889
- });
106890
-
106891
- // apps/agent/src/shared/capabilities/github/identifier-strategy.ts
106892
- function linearChangeName(issue2) {
106893
- const slug = issue2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40).replace(/^-+|-+$/g, "");
106894
- return slug ? `${issue2.identifier.toLowerCase()}-${slug}` : issue2.identifier.toLowerCase();
106895
- }
106896
- var linearIdentifierStrategy;
106897
- var init_identifier_strategy = __esm(() => {
106898
- init_worktree();
106899
- init_github_client();
106900
- linearIdentifierStrategy = {
106901
- scopeKey: (issue2) => issue2.identifier.split("-")[0],
106902
- changeName: linearChangeName,
106903
- branchName: (issue2) => branchForChange(linearChangeName(issue2))
106904
- };
106905
- });
106906
-
106907
- // apps/agent/src/agent/scaffold.ts
106908
- import { join as join24 } from "path";
106909
- function changeNameForIssue(issue2) {
106910
- return linearIdentifierStrategy.changeName(issue2);
106911
- }
106912
- async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = [], appendPrompt = "", attachments = []) {
106913
- const name = changeNameForIssue(issue2);
106914
- const changeDir = join24(tasksDir, name);
106915
- const stateDir = join24(statesDir, name);
106916
- const commentsBlock = comments.length > 0 ? [
106917
- "",
106918
- "## Linear comments",
106919
- "",
106920
- ...comments.flatMap((c) => [
106921
- `**${c.user?.name ?? "unknown"}** \u2014 ${c.createdAt}`,
106922
- "",
106923
- c.body.trim(),
106924
- ""
106925
- ])
106926
- ] : [];
106927
- const attachmentsBlock = attachments.length > 0 ? [
106928
- "",
106929
- "## Ticket Attachments",
106930
- "",
106931
- ...attachments.map((a) => `- [${a.title ?? "Attachment"}](${a.url})`)
106932
- ] : [];
106933
- const descriptionBody = issue2.description?.trim() || "_No description provided in Linear._";
106934
- const proposal = [
106935
- `# ${issue2.identifier}: ${issue2.title}`,
106936
- "",
106937
- `Source: [${issue2.identifier}](${issue2.url})`,
106938
- `Status: ${issue2.state.name}`,
106939
- issue2.assignee ? `Assignee: ${issue2.assignee.name}` : "",
106940
- issue2.labels.length ? `Labels: ${issue2.labels.join(", ")}` : "",
106941
- "",
106942
- "## Why",
106943
- "",
106944
- descriptionBody,
106945
- "",
106946
- "## What Changes",
106947
- "",
106948
- "_Describe the concrete changes this proposal introduces (one bullet per change)._",
106949
- ...commentsBlock,
106950
- ...attachmentsBlock,
106951
- ...appendPrompt.trim() ? ["", "## Additional instructions", "", appendPrompt.trim()] : [],
106952
- "",
106953
- "## Steering",
106954
- "",
106955
- "_Add steering notes here as the loop runs._",
106956
- ""
106957
- ].filter((l) => l !== "").join(`
106958
- `);
106959
- const tasks = [
106960
- `# Tasks for ${issue2.identifier}`,
106961
- "",
106962
- "## Planning",
106963
- "",
106964
- `- [ ] Read the Linear issue at ${issue2.url} and research the codebase to understand the mission and its scope`,
106965
- `- [ ] Refine proposal.md with the problem statement, approach, and acceptance criteria derived from the research`,
106966
- `- [ ] Fill in \`## Why\` and \`## What Changes\` in proposal.md so \`openspec validate\` passes (these sections are required by the validator)`,
106967
- `- [ ] Add at least one spec delta under \`specs/<capability>/spec.md\` describing the behavior added/modified/removed by this change`,
106968
- `- [ ] 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).`,
106969
- `- [ ] 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.`,
106970
- `- [ ] Is there anything else to add? Review the complete change context and document any additional edge cases, constraints, or open questions not captured above.`,
106971
- ""
106972
- ].join(`
106973
- `);
106974
- const design = [
106975
- `# Design for ${issue2.identifier}`,
106976
- "",
106977
- "_Fill in the technical design as you work through the issue._",
106978
- ""
106979
- ].join(`
106980
- `);
106981
- await runCapability(fsChange.scaffold, {
106982
- changeDir,
106983
- stateDir,
106984
- proposal,
106985
- tasks,
106986
- design
106987
- });
106988
- return name;
106989
- }
106990
- var init_scaffold = __esm(() => {
106991
- init_fs_change();
106992
- init_identifier_strategy();
106993
- });
106994
-
106995
107510
  // packages/core/src/detections/tasks.ts
106996
107511
  function hasUnchecked(content) {
106997
107512
  return /^- \[ \]/m.test(content);
@@ -107033,13 +107548,8 @@ function gateActive(inputs) {
107033
107548
  }
107034
107549
 
107035
107550
  // packages/core/src/detections/mention.ts
107036
- function buildMentionAckComment(_body, author) {
107037
- const greeting = author ? `Got it, ${author} \u2014 picked up your mention and queued a review pass.` : `Acknowledged \u2014 picked up your mention and queued a review pass.`;
107038
- return buildRalphyComment({
107039
- type: "mention-ack",
107040
- action: "picked up your mention",
107041
- body: greeting
107042
- });
107551
+ function buildMentionAckComment() {
107552
+ return buildRalphyMarker("mention-ack", { status: "handled" });
107043
107553
  }
107044
107554
  var init_mention2 = __esm(() => {
107045
107555
  init_src8();
@@ -107051,7 +107561,7 @@ var init_detections = __esm(() => {
107051
107561
  });
107052
107562
 
107053
107563
  // apps/agent/src/features/confirmation/state.ts
107054
- import { dirname as dirname10, join as join25 } from "path";
107564
+ import { dirname as dirname9, join as join25 } from "path";
107055
107565
  async function readInlineConfirmation(statePath) {
107056
107566
  const f2 = Bun.file(statePath);
107057
107567
  if (!await f2.exists())
@@ -107064,7 +107574,7 @@ async function readInlineConfirmation(statePath) {
107064
107574
  }
107065
107575
  }
107066
107576
  async function readConfirmationState(statePath) {
107067
- const changeDir = dirname10(statePath);
107577
+ const changeDir = dirname9(statePath);
107068
107578
  const sidecar = await readSlotSidecar(changeDir, "confirmation");
107069
107579
  const existing = sidecar ?? await readInlineConfirmation(statePath) ?? null;
107070
107580
  const confirmation = {
@@ -107080,7 +107590,7 @@ async function readConfirmationState(statePath) {
107080
107590
  return { stateObj: {}, confirmation };
107081
107591
  }
107082
107592
  async function writeConfirmationState(statePath, _stateObj, confirmation) {
107083
- await writeSlotField(dirname10(statePath), "confirmation", confirmation);
107593
+ await writeSlotField(dirname9(statePath), "confirmation", confirmation);
107084
107594
  }
107085
107595
  async function restartFromDesign(changeDir, changeName) {
107086
107596
  const designStub = [
@@ -107794,7 +108304,7 @@ function createOpenDraftPr(deps) {
107794
108304
  if (!branch)
107795
108305
  return null;
107796
108306
  const base2 = baseBranchFromLabels(issue2.labels) ?? deps.prBaseBranch;
107797
- 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);
107798
108308
  const url2 = result2?.url ?? null;
107799
108309
  if (url2) {
107800
108310
  deps.prByChange.set(changeName, url2);
@@ -108360,7 +108870,7 @@ function createPrepareHelpers(input) {
108360
108870
  let scaffoldStatesDir = statesDir;
108361
108871
  let branch = null;
108362
108872
  if (!useWorktree)
108363
- return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
108873
+ return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch, worktreeCreated: null };
108364
108874
  const probeName = worktreeDirNameForIssue(issue2);
108365
108875
  const baseBranch = baseBranchFromLabels(issue2.labels) ?? cfg.prBaseBranch;
108366
108876
  let wt;
@@ -108389,10 +108899,10 @@ function createPrepareHelpers(input) {
108389
108899
  } catch (err) {
108390
108900
  diag("worktree", `! seeding .mcp.json failed for ${issue2.identifier}: ${err.message}`, "yellow");
108391
108901
  }
108392
- return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
108902
+ return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch, worktreeCreated: wt.created };
108393
108903
  }
108394
108904
  async function prepare(issue2) {
108395
- const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch } = await setupWorktree(issue2);
108905
+ const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch, worktreeCreated } = await setupWorktree(issue2);
108396
108906
  let changeName;
108397
108907
  const wtLayoutPre = projectLayout(workerCwd);
108398
108908
  const derivedName = changeNameForIssue(issue2);
@@ -108441,7 +108951,8 @@ function createPrepareHelpers(input) {
108441
108951
  maps.issueByChange.set(changeName, issue2);
108442
108952
  if (branch)
108443
108953
  maps.branchByChange.set(changeName, branch);
108444
- if (cfg.setupScript) {
108954
+ const runSetup = worktreeCreated ?? isFresh;
108955
+ if (cfg.setupScript && runSetup) {
108445
108956
  await runScript("setup", cfg.setupScript, workerCwd);
108446
108957
  }
108447
108958
  return {
@@ -108790,6 +109301,18 @@ function createPrDiscovery(input) {
108790
109301
  } catch (err) {
108791
109302
  diag("ci", `! gh pr checks ${prUrl} failed (PR scan): ${err.message}`, "yellow");
108792
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
+ }
108793
109316
  return { url: prUrl, status: "mergeable" };
108794
109317
  }
108795
109318
  async function resolvePrUrlForIssue(issue2) {
@@ -108829,17 +109352,17 @@ var init_pr_discovery = __esm(() => {
108829
109352
  });
108830
109353
 
108831
109354
  // apps/agent/src/features/review-followup/scan.ts
108832
- import { dirname as dirname11, join as join28 } from "path";
109355
+ import { dirname as dirname10, join as join28 } from "path";
108833
109356
  async function resolveReviewStateDir(changeName, deps) {
108834
109357
  const root = deps.cwdOf(changeName);
108835
109358
  if (root)
108836
- return dirname11(projectLayout(root).stateFile(changeName));
109359
+ return dirname10(projectLayout(root).stateFile(changeName));
108837
109360
  if (!deps.useWorktree)
108838
- return dirname11(projectLayout(deps.projectRoot).stateFile(changeName));
109361
+ return dirname10(projectLayout(deps.projectRoot).stateFile(changeName));
108839
109362
  const wtPath = join28(worktreesDir2(deps.projectRoot), changeName);
108840
109363
  const statePath = projectLayout(wtPath).stateFile(changeName);
108841
109364
  if (await Bun.file(statePath).exists())
108842
- return dirname11(statePath);
109365
+ return dirname10(statePath);
108843
109366
  return null;
108844
109367
  }
108845
109368
  async function readReviewWatermark(stateDir) {
@@ -109172,7 +109695,7 @@ function createMentionScanner(input) {
109172
109695
  }
109173
109696
  if (cfg.linear.postComments !== false) {
109174
109697
  try {
109175
- await createIssueComment(apiKey, issue2.id, buildMentionAckComment(c.body, c.user?.name));
109698
+ await createIssueComment(apiKey, issue2.id, buildMentionAckComment());
109176
109699
  } catch (err) {
109177
109700
  diag("mention", `! mention scan: ack comment failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
109178
109701
  }
@@ -109221,7 +109744,7 @@ function createMentionScanner(input) {
109221
109744
  }
109222
109745
  }
109223
109746
  if (cfg.linear.postComments !== false) {
109224
- await postGithubPrComment(cmdRunner, projectRoot, prUrl, buildMentionAckComment(c.body, c.author), onLog);
109747
+ await postGithubPrComment(cmdRunner, projectRoot, prUrl, buildMentionAckComment(), onLog);
109225
109748
  }
109226
109749
  }
109227
109750
  queued.add(issue2.id);
@@ -109636,6 +110159,7 @@ function buildPostTaskInput(input) {
109636
110159
  finalizeNoOpAsDone: cfg.finalizeNoOpAsDone,
109637
110160
  manualMergeWhenAutoMergeDisabled: cfg.manualMergeWhenAutoMergeDisabled,
109638
110161
  prDraft: cfg.prDraft,
110162
+ prLabels: cfg.prLabels,
109639
110163
  validateCommands: [cfg.commands.test, cfg.commands.lint, cfg.commands.typecheck].filter((c) => Boolean(c))
109640
110164
  },
109641
110165
  respawnWorker: input.respawnWorker
@@ -110226,7 +110750,7 @@ var init_linear_sync = __esm(() => {
110226
110750
  });
110227
110751
 
110228
110752
  // apps/agent/src/agent/linear-sync/comment-sync.ts
110229
- import { dirname as dirname12, join as join34 } from "path";
110753
+ import { dirname as dirname11, join as join34 } from "path";
110230
110754
  async function readInlineLinearComments(statePath) {
110231
110755
  const file2 = Bun.file(statePath);
110232
110756
  if (!await file2.exists())
@@ -110239,7 +110763,7 @@ async function readInlineLinearComments(statePath) {
110239
110763
  }
110240
110764
  }
110241
110765
  async function readComments(statePath) {
110242
- const changeDir = dirname12(statePath);
110766
+ const changeDir = dirname11(statePath);
110243
110767
  const raw = await readSlotSidecar(changeDir, "linearComments") ?? await readInlineLinearComments(statePath) ?? {};
110244
110768
  const r = raw;
110245
110769
  return {
@@ -110252,7 +110776,7 @@ async function readComments(statePath) {
110252
110776
  async function patchComments(statePath, patch) {
110253
110777
  const current = await readComments(statePath);
110254
110778
  const next = { ...current, ...patch };
110255
- await writeSlotField(dirname12(statePath), "linearComments", next);
110779
+ await writeSlotField(dirname11(statePath), "linearComments", next);
110256
110780
  }
110257
110781
  function isCommentNotFoundError(err) {
110258
110782
  if (!err)
@@ -262362,22 +262886,20 @@ function renderCodeBlock(doc2, token, indent) {
262362
262886
  const text = token.text ?? "";
262363
262887
  const x2 = MARGIN + indent;
262364
262888
  const width = doc2.page.width - 2 * MARGIN - indent;
262889
+ const textWidth = width - 2 * CODE_PADDING_X;
262365
262890
  doc2.font(FONT_MONO).fontSize(CODE_SIZE).fillColor(COLOR_TEXT);
262366
- const lineHeight = doc2.currentLineHeight(true);
262367
262891
  const lines = text.split(/\r?\n/);
262368
262892
  doc2.y += CODE_PADDING_Y / 2;
262369
262893
  for (const line of lines) {
262370
- 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) {
262371
262897
  doc2.addPage();
262372
262898
  }
262373
262899
  const yTop = doc2.y;
262374
- const safe = toPdfSafe(line);
262375
262900
  doc2.rect(x2, yTop, width, lineHeight).fill(COLOR_CODE_BG);
262376
262901
  doc2.fillColor(COLOR_TEXT);
262377
- doc2.text(safe.length > 0 ? safe : " ", x2 + CODE_PADDING_X, yTop, {
262378
- width: width - 2 * CODE_PADDING_X,
262379
- lineBreak: false
262380
- });
262902
+ doc2.text(safe, x2 + CODE_PADDING_X, yTop, { width: textWidth, lineBreak: true });
262381
262903
  doc2.y = yTop + lineHeight;
262382
262904
  }
262383
262905
  doc2.y += CODE_PADDING_Y / 2;
@@ -262465,38 +262987,85 @@ function flattenInline(tokens, flags = {}) {
262465
262987
  }
262466
262988
  return out;
262467
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
+ }
262468
263023
  function emitInline(doc2, tokens, x2, width) {
262469
263024
  const flat = flattenInline(tokens);
262470
263025
  if (flat.length === 0)
262471
263026
  return;
262472
- for (let i = 0;i < flat.length; i++) {
262473
- const run = flat[i];
262474
- const last3 = i === flat.length - 1;
262475
- const opts = {
262476
- width,
262477
- continued: !last3,
262478
- lineBreak: true
262479
- };
262480
- if (run.code) {
262481
- doc2.font(FONT_MONO).fontSize(BODY_SIZE).fillColor(COLOR_INLINE_CODE_FG);
262482
- } else {
262483
- doc2.fontSize(BODY_SIZE).fillColor(COLOR_TEXT);
262484
- if (run.bold && run.italic)
262485
- doc2.font(FONT_BOLD_ITALIC);
262486
- else if (run.bold)
262487
- doc2.font(FONT_BOLD);
262488
- else if (run.italic)
262489
- doc2.font(FONT_ITALIC);
262490
- else
262491
- 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;
262492
263044
  }
262493
- const safe = toPdfSafe(run.text);
262494
- if (i === 0) {
262495
- 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();
262496
263060
  } else {
262497
- doc2.text(safe, opts);
263061
+ cursorX += pendingSpace;
263062
+ pendingSpace = 0;
262498
263063
  }
263064
+ applyInlineStyle(doc2, atom.run);
263065
+ doc2.text(atom.text, cursorX, cursorY, { lineBreak: false, continued: false });
263066
+ cursorX += wordWidth;
262499
263067
  }
263068
+ doc2.y = cursorY + lineHeight;
262500
263069
  doc2.font(FONT_BODY).fontSize(BODY_SIZE).fillColor(COLOR_TEXT);
262501
263070
  }
262502
263071
  function plainInline(tokens) {
@@ -262676,7 +263245,7 @@ var init_render_pdf = __esm(() => {
262676
263245
  });
262677
263246
 
262678
263247
  // apps/agent/src/agent/linear-sync/spec-attachments.ts
262679
- import { dirname as dirname13, join as join35 } from "path";
263248
+ import { dirname as dirname12, join as join35 } from "path";
262680
263249
  function describeLinearError(err) {
262681
263250
  const e = err;
262682
263251
  const parts = [e.message ?? String(err)];
@@ -262691,7 +263260,7 @@ function describeLinearError(err) {
262691
263260
  return parts.join(" ");
262692
263261
  }
262693
263262
  function stateDirOf(statePath) {
262694
- return dirname13(statePath);
263263
+ return dirname12(statePath);
262695
263264
  }
262696
263265
  async function readInlineSpecAttachments(statePath) {
262697
263266
  const file2 = Bun.file(statePath);
@@ -262709,7 +263278,7 @@ async function readInlineSpecAttachments(statePath) {
262709
263278
  }
262710
263279
  }
262711
263280
  async function readSpecAttachmentsSubtree(statePath) {
262712
- const sidecar = await readSlotSidecar(dirname13(statePath), "specAttachments");
263281
+ const sidecar = await readSlotSidecar(dirname12(statePath), "specAttachments");
262713
263282
  return sidecar ?? await readInlineSpecAttachments(statePath);
262714
263283
  }
262715
263284
  function asRevisions(value) {
@@ -263201,105 +263770,8 @@ var init_comment_sync2 = __esm(() => {
263201
263770
  init_linear();
263202
263771
  });
263203
263772
 
263204
- // apps/agent/src/features/pr-tracker/state.ts
263205
- import { join as join36 } from "path";
263206
- async function readState2(projectRoot) {
263207
- const path = join36(projectRoot, PR_TRACKER_STATE_RELPATH);
263208
- const file2 = Bun.file(path);
263209
- if (!await file2.exists())
263210
- return {};
263211
- try {
263212
- const raw = await file2.text();
263213
- const parsed = JSON.parse(raw);
263214
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
263215
- return parsed;
263216
- }
263217
- return {};
263218
- } catch {
263219
- return {};
263220
- }
263221
- }
263222
- async function writeState2(projectRoot, state) {
263223
- const path = join36(projectRoot, PR_TRACKER_STATE_RELPATH);
263224
- await Bun.write(path, JSON.stringify(state, null, 2));
263225
- }
263226
- var PR_TRACKER_STATE_RELPATH = ".ralph/pr-tracker-state.json";
263227
- var init_state3 = () => {};
263228
-
263229
- // apps/agent/src/features/pr-tracker/tracker.ts
263230
- class PrTracker {
263231
- opts;
263232
- state = {};
263233
- loaded = false;
263234
- now;
263235
- constructor(opts) {
263236
- this.opts = opts;
263237
- this.now = opts.now ?? (() => new Date);
263238
- }
263239
- async load() {
263240
- if (this.loaded)
263241
- return;
263242
- this.state = await readState2(this.opts.projectRoot);
263243
- this.loaded = true;
263244
- }
263245
- snapshot() {
263246
- return JSON.parse(JSON.stringify(this.state));
263247
- }
263248
- isBailed(identifier) {
263249
- return this.state[identifier]?.bailed === true;
263250
- }
263251
- getAttempts(identifier) {
263252
- return this.state[identifier]?.attempts ?? 0;
263253
- }
263254
- async recordFailure(identifier, reason) {
263255
- await this.load();
263256
- const nowIso = this.now().toISOString();
263257
- const existing = this.state[identifier];
263258
- if (existing?.bailed) {
263259
- existing.lastReason = reason;
263260
- await this.flush();
263261
- return { kind: "bail", attempts: existing.attempts, firstBail: false };
263262
- }
263263
- const attempts = (existing?.attempts ?? 0) + 1;
263264
- const entry = {
263265
- attempts,
263266
- firstFailedAt: existing?.firstFailedAt ?? nowIso,
263267
- lastDemotedAt: nowIso,
263268
- lastReason: reason
263269
- };
263270
- if (attempts >= this.opts.maxRecoveryAttempts) {
263271
- entry.bailed = true;
263272
- this.state[identifier] = entry;
263273
- await this.flush();
263274
- return { kind: "bail", attempts, firstBail: true };
263275
- }
263276
- this.state[identifier] = entry;
263277
- await this.flush();
263278
- return { kind: "demote", attempts };
263279
- }
263280
- async clear(identifier) {
263281
- await this.load();
263282
- if (!(identifier in this.state))
263283
- return;
263284
- delete this.state[identifier];
263285
- await this.flush();
263286
- }
263287
- async flush() {
263288
- await writeState2(this.opts.projectRoot, this.state);
263289
- }
263290
- }
263291
- var init_tracker = __esm(() => {
263292
- init_state3();
263293
- });
263294
-
263295
- // apps/agent/src/features/pr-tracker/index.ts
263296
- var init_pr_tracker = __esm(() => {
263297
- init_tracker();
263298
- init_state3();
263299
- });
263300
-
263301
263773
  // apps/agent/src/agent/wire.ts
263302
- import { join as join37 } from "path";
263774
+ import { join as join36 } from "path";
263303
263775
  function buildAgentCoordinator(input) {
263304
263776
  const {
263305
263777
  args,
@@ -263318,7 +263790,7 @@ function buildAgentCoordinator(input) {
263318
263790
  onWorkerCmd,
263319
263791
  onAwaitingTicket
263320
263792
  } = input;
263321
- const logsDir = join37(projectRoot, ".ralph", "logs");
263793
+ const logsDir = join36(projectRoot, ".ralph", "logs");
263322
263794
  const bus = createBus();
263323
263795
  subscribeAgentDiag(bus, onLog);
263324
263796
  const diag = (area, message, color) => {
@@ -263498,6 +263970,7 @@ function buildAgentCoordinator(input) {
263498
263970
  prByChange,
263499
263971
  cmdRunner,
263500
263972
  prBaseBranch: cfg.prBaseBranch,
263973
+ prLabels: cfg.prLabels,
263501
263974
  invalidatePrUrlForIssue: (issueId) => prDiscovery.invalidatePrUrlForIssue(issueId)
263502
263975
  });
263503
263976
  const confirmationCaps = {
@@ -263537,10 +264010,6 @@ function buildAgentCoordinator(input) {
263537
264010
  };
263538
264011
  }
263539
264012
  const prRecoveryEnabled = args.prRecoveryEnabled === undefined ? cfg.prRecovery.enabled : args.prRecoveryEnabled;
263540
- const prTracker = prRecoveryEnabled ? new PrTracker({
263541
- projectRoot,
263542
- maxRecoveryAttempts: cfg.prRecovery.maxRecoverySessions
263543
- }) : null;
263544
264013
  const commentSync = isGithubTracker ? { enabled: false } : createCommentSyncHooks({
263545
264014
  apiKey,
263546
264015
  cfg,
@@ -263586,7 +264055,7 @@ function buildAgentCoordinator(input) {
263586
264055
  const changeDir = projectLayout(root).changeDir(changeName);
263587
264056
  const parts = [];
263588
264057
  for (const name of ["tasks.md", "proposal.md", "design.md"]) {
263589
- const file2 = Bun.file(join37(changeDir, name));
264058
+ const file2 = Bun.file(join36(changeDir, name));
263590
264059
  if (!await file2.exists())
263591
264060
  continue;
263592
264061
  parts.push(`${name}:${file2.lastModified}:${file2.size}`);
@@ -263605,11 +264074,11 @@ function buildAgentCoordinator(input) {
263605
264074
  commentEveryIterations: cfg.linear.updateEveryIterations,
263606
264075
  ...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {},
263607
264076
  createsPrs: args.createPr || cfg.createPrOnSuccess,
263608
- ...prTracker ? { prTracker } : {},
263609
264077
  prRecovery: {
263610
264078
  enabled: prRecoveryEnabled,
263611
264079
  fixCi: cfg.prRecovery.fixCi,
263612
- fixConflicts: cfg.prRecovery.fixConflicts
264080
+ fixConflicts: cfg.prRecovery.fixConflicts,
264081
+ maxRecoverySessions: cfg.prRecovery.maxRecoverySessions
263613
264082
  }
263614
264083
  });
263615
264084
  coordRef.current = coord;
@@ -263633,19 +264102,7 @@ function buildAgentCoordinator(input) {
263633
264102
  pollInterval,
263634
264103
  getWorkerCwd: (changeName) => cwdByChange.get(changeName),
263635
264104
  syncTasksEnabled: commentSync.enabled,
263636
- runBaselineGate: runBaselineGateOnce,
263637
- getGaveUpTotal: async () => {
263638
- let total = 0;
263639
- for (const [changeName, root] of cwdByChange) {
263640
- const file2 = Bun.file(join37(projectLayout(root).taskStateDir(changeName), GAVEUP_COUNT_FILE));
263641
- if (!await file2.exists())
263642
- continue;
263643
- try {
263644
- total += Number.parseInt(await file2.text(), 10) || 0;
263645
- } catch {}
263646
- }
263647
- return total;
263648
- }
264105
+ runBaselineGate: runBaselineGateOnce
263649
264106
  };
263650
264107
  }
263651
264108
  var init_wire = __esm(() => {
@@ -263669,18 +264126,17 @@ var init_wire = __esm(() => {
263669
264126
  init_worker();
263670
264127
  init_baseline();
263671
264128
  init_comment_sync2();
263672
- init_pr_tracker();
263673
264129
  });
263674
264130
 
263675
264131
  // apps/agent/src/agent/json-log/json-log-file.ts
263676
- import { mkdir as mkdir11, appendFile as appendFile2 } from "fs/promises";
263677
- import { dirname as dirname14 } from "path";
264132
+ import { mkdir as mkdir11, appendFile as appendFile3 } from "fs/promises";
264133
+ import { dirname as dirname13 } from "path";
263678
264134
  function createJsonLogFileSink(path) {
263679
264135
  if (!path)
263680
264136
  return { emit: () => {} };
263681
264137
  let chain = (async () => {
263682
264138
  try {
263683
- await mkdir11(dirname14(path), { recursive: true });
264139
+ await mkdir11(dirname13(path), { recursive: true });
263684
264140
  await Bun.write(path, "");
263685
264141
  } catch {}
263686
264142
  })();
@@ -263690,7 +264146,7 @@ function createJsonLogFileSink(path) {
263690
264146
  `;
263691
264147
  chain = chain.then(async () => {
263692
264148
  try {
263693
- await appendFile2(path, line);
264149
+ await appendFile3(path, line);
263694
264150
  } catch {}
263695
264151
  });
263696
264152
  }
@@ -263926,7 +264382,7 @@ var init_output_utils = __esm(() => {
263926
264382
  });
263927
264383
 
263928
264384
  // apps/agent/src/agent/state/worker-state-poll.ts
263929
- import { join as join38 } from "path";
264385
+ import { join as join37 } from "path";
263930
264386
  function parseSubtasks(tasksMd) {
263931
264387
  const out = [];
263932
264388
  let skipSection = false;
@@ -263959,7 +264415,7 @@ function initialWorkerSnapshot() {
263959
264415
  async function readWorkerSnapshot(input) {
263960
264416
  const next = { ...input.prev };
263961
264417
  try {
263962
- 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"));
263963
264419
  if (await file2.exists()) {
263964
264420
  const json2 = await file2.json();
263965
264421
  next.iter = json2.iteration ?? next.iter;
@@ -263968,10 +264424,10 @@ async function readWorkerSnapshot(input) {
263968
264424
  } catch {}
263969
264425
  if (input.changeDir) {
263970
264426
  try {
263971
- const tasksFile = Bun.file(join38(input.changeDir, "tasks.md"));
263972
- const proposalFile = Bun.file(join38(input.changeDir, "proposal.md"));
263973
- const designFile = Bun.file(join38(input.changeDir, "design.md"));
263974
- 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"));
263975
264431
  const [tasksText, proposalText, designText, reviewFindingsText] = await Promise.all([
263976
264432
  tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
263977
264433
  proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
@@ -264034,7 +264490,7 @@ var init_worker_state_poll = __esm(() => {
264034
264490
  });
264035
264491
 
264036
264492
  // apps/agent/src/components/AgentMode.tsx
264037
- import { join as join39 } from "path";
264493
+ import { join as join38 } from "path";
264038
264494
  async function appendSteeringImpl(changeDir, message) {
264039
264495
  await runWithContext(createDefaultContext(), async () => {
264040
264496
  appendSteeringMessage(changeDir, message);
@@ -264051,16 +264507,6 @@ function orderSubtasksForCappedDisplay(subtasks) {
264051
264507
  (s.done ? done : pending).push(s);
264052
264508
  return [...pending, ...done];
264053
264509
  }
264054
- function pickLatestGatedTicket(tickets) {
264055
- if (tickets.size === 0)
264056
- return { top: null, moreCount: 0 };
264057
- const sorted = Array.from(tickets.entries()).sort(([, a], [, b2]) => {
264058
- const aTime = a.since ? new Date(a.since).getTime() : 0;
264059
- const bTime = b2.since ? new Date(b2.since).getTime() : 0;
264060
- return bTime - aTime;
264061
- });
264062
- return { top: sorted[0], moreCount: sorted.length - 1 };
264063
- }
264064
264510
  function fmtCmd(argv) {
264065
264511
  const joined = argv.join(" ");
264066
264512
  return joined.length > CMD_DISPLAY_MAX ? joined.slice(0, CMD_DISPLAY_MAX - 1) + "\u2026" : joined;
@@ -264156,20 +264602,6 @@ function Link({ url: url2, label, color }) {
264156
264602
  }, undefined, false, undefined, this)
264157
264603
  }, undefined, false, undefined, this);
264158
264604
  }
264159
- function priorityBadge(p) {
264160
- switch (p) {
264161
- case 1:
264162
- return { text: "\u25B2", color: "red", label: "URGENT" };
264163
- case 2:
264164
- return { text: "\u2191", color: "yellow", label: "HIGH" };
264165
- case 3:
264166
- return { text: "\xB7", color: "blue", label: "MED" };
264167
- case 4:
264168
- return { text: "\u2193", color: "gray", label: "LOW" };
264169
- default:
264170
- return { text: " ", color: "gray", label: "" };
264171
- }
264172
- }
264173
264605
  function modeBadge(mode) {
264174
264606
  switch (mode) {
264175
264607
  case "fresh":
@@ -264246,14 +264678,52 @@ function workerBorderColor(phase2) {
264246
264678
  return "gray";
264247
264679
  }
264248
264680
  }
264249
- function displayTailLines(activeCount) {
264250
- if (activeCount <= 1)
264251
- return 20;
264252
- if (activeCount <= 2)
264253
- return 12;
264254
- if (activeCount <= 3)
264255
- return 8;
264256
- 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);
264257
264727
  }
264258
264728
  function AgentMode({
264259
264729
  args,
@@ -264287,24 +264757,19 @@ function AgentMode({
264287
264757
  }, [awaitingClose]);
264288
264758
  const [, setTick] = import_react63.useState(0);
264289
264759
  const [clock, setClock] = import_react63.useState(0);
264290
- const [focusedIdx, setFocusedIdx] = import_react63.useState(0);
264760
+ const [focusedId, setFocusedId] = import_react63.useState(null);
264291
264761
  const [showPendingTasks, setShowPendingTasks] = import_react63.useState(false);
264292
264762
  const [showAllSubtasks, setShowAllSubtasks] = import_react63.useState(false);
264293
- const [gaveUpCount, setGaveUpCount] = import_react63.useState(0);
264294
264763
  const coordRef = import_react63.useRef(null);
264295
264764
  const workerMetaRef = import_react63.useRef(new Map);
264296
- const gatedTicketsRef = import_react63.useRef(new Map);
264297
264765
  const nextPollAtRef = import_react63.useRef(0);
264298
264766
  const cfgRef = import_react63.useRef(null);
264299
264767
  const [effective, setEffective] = import_react63.useState(null);
264300
264768
  const [pollStatus, setPollStatus] = import_react63.useState({
264301
264769
  state: "idle",
264302
- lastFound: null,
264303
- lastAdded: null,
264304
264770
  lastAt: null,
264305
264771
  filterDesc: "",
264306
- lastBuckets: null,
264307
- lastPrStatus: null
264772
+ lastBoard: []
264308
264773
  });
264309
264774
  function appendLog(text, color, workerLogFile) {
264310
264775
  setLogs((prev) => [...prev, { id: nextId(), text, color }]);
@@ -264340,7 +264805,7 @@ function AgentMode({
264340
264805
  setFatalExit(2);
264341
264806
  return;
264342
264807
  }
264343
- const { coord: coord2, filterDesc, concurrency, pollInterval, runBaselineGate: runBaselineGate2, getGaveUpTotal } = buildCoordinator({
264808
+ const { coord: coord2, filterDesc, concurrency, pollInterval, runBaselineGate: runBaselineGate2 } = buildCoordinator({
264344
264809
  args,
264345
264810
  cfg: cfg2,
264346
264811
  projectRoot,
@@ -264447,13 +264912,6 @@ function AgentMode({
264447
264912
  since: info.since,
264448
264913
  round: info.round
264449
264914
  });
264450
- gatedTicketsRef.current.set(info.changeName, {
264451
- issueIdentifier: info.issueIdentifier,
264452
- issueUrl: info.issueUrl,
264453
- issueTitle: info.issueTitle,
264454
- since: info.since,
264455
- round: info.round
264456
- });
264457
264915
  }
264458
264916
  });
264459
264917
  setEffective({ concurrency, pollInterval });
@@ -264481,28 +264939,18 @@ function AgentMode({
264481
264939
  }
264482
264940
  if (cancelled)
264483
264941
  return;
264484
- gatedTicketsRef.current.clear();
264485
- const { found, added, buckets, prStatus } = await coord2.pollOnce();
264942
+ const { found, added, buckets, prStatus, board: board2 } = await coord2.pollOnce();
264486
264943
  if (cancelled)
264487
264944
  return;
264488
264945
  fileEmit({ type: "poll_done", found, added, buckets, prStatus });
264489
- getGaveUpTotal().then((total) => {
264490
- if (!cancelled)
264491
- setGaveUpCount(total);
264492
- }).catch(() => {
264493
- return;
264494
- });
264495
264946
  if (added > 0) {
264496
264947
  appendLog(` ${added} new issue${added === 1 ? "" : "s"} queued (found ${found} open)`);
264497
264948
  }
264498
264949
  setPollStatus({
264499
264950
  state: "idle",
264500
- lastFound: found,
264501
- lastAdded: added,
264502
264951
  lastAt: Date.now(),
264503
264952
  filterDesc,
264504
- lastBuckets: buckets,
264505
- lastPrStatus: prStatus
264953
+ lastBoard: board2
264506
264954
  });
264507
264955
  nextPollAtRef.current = Date.now() + pollInterval * 1000;
264508
264956
  pollTimer = setTimeout(tick, pollInterval * 1000);
@@ -264617,10 +265065,21 @@ function AgentMode({
264617
265065
  const spinnerFrame = SPINNER_FRAMES[clock % SPINNER_FRAMES.length];
264618
265066
  const now2 = Date.now();
264619
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` : ""}`;
264620
265070
  const activeCount = coord?.activeCount ?? 0;
264621
265071
  const termWidth = columns - 2;
264622
265072
  const termHeight = rows;
264623
- 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;
264624
265083
  const steeringFocusedRef = import_react63.useRef(false);
264625
265084
  const steeringBufferRef = import_react63.useRef("");
264626
265085
  const steeringCursorRef = import_react63.useRef(0);
@@ -264638,26 +265097,23 @@ function AgentMode({
264638
265097
  setShowPendingTasks((v2) => !v2);
264639
265098
  return;
264640
265099
  }
264641
- if (activeCount === 0)
265100
+ if (tree.length === 0)
264642
265101
  return;
264643
- if (key.tab || key.rightArrow) {
264644
- setFocusedIdx((i) => (Math.min(i, activeCount - 1) + 1) % activeCount);
264645
- } else if (key.leftArrow) {
264646
- 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);
264647
265107
  } else {
264648
265108
  const n = parseInt(input, 10);
264649
- if (!isNaN(n) && n >= 1 && n <= activeCount)
264650
- setFocusedIdx(n - 1);
264651
- }
264652
- }, { isActive: isRawModeSupported && activeCount > 0 });
264653
- const focusedWorker = coordRef.current?.activeWorkers[safeFocusedIdx];
264654
- const steeringActive = isRawModeSupported && activeCount > 0 && focusedWorker !== undefined;
264655
- const nonFocusedCount = Math.max(0, activeCount - 1);
264656
- 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;
264657
265114
  const steeringBoxLines = steeringActive ? 3 : 0;
264658
- const FIXED_OVERHEAD = 5 + 7 + tasksBoxLines + 8 + steeringBoxLines + nonFocusedCount * 4;
264659
- const focusedTailLines = Math.max(3, termHeight - FIXED_OVERHEAD);
264660
- const compactTailLines = displayTailLines(activeCount);
265115
+ const FIXED_OVERHEAD = 5 + (4 + board.length) + 8 + steeringBoxLines;
265116
+ const focusedTailLines = focusedCardTailLines(termHeight, FIXED_OVERHEAD);
264661
265117
  if (preflightError) {
264662
265118
  return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264663
265119
  flexDirection: "column",
@@ -264811,14 +265267,7 @@ function AgentMode({
264811
265267
  cfg.useWorktree && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264812
265268
  color: "green",
264813
265269
  children: " \u25CF worktree"
264814
- }, undefined, false, undefined, this),
264815
- gaveUpCount > 0 && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264816
- color: "red",
264817
- children: [
264818
- " \u2502 gave-up \xD7",
264819
- gaveUpCount
264820
- ]
264821
- }, undefined, true, undefined, this)
265270
+ }, undefined, false, undefined, this)
264822
265271
  ]
264823
265272
  }, undefined, true, undefined, this)
264824
265273
  ]
@@ -264845,418 +265294,196 @@ function AgentMode({
264845
265294
  })()
264846
265295
  ]
264847
265296
  }, undefined, true, undefined, this),
264848
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264849
- flexDirection: "row",
264850
- gap: 1,
264851
- marginTop: 0,
264852
- width: termWidth,
264853
- children: [
264854
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
264855
- label: "POLL STATUS",
264856
- borderColor: "gray",
264857
- width: termWidth - 17,
264858
- paddingX: 1,
264859
- flexDirection: "column",
264860
- children: [
264861
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264862
- gap: 2,
264863
- children: [
264864
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264865
- color: "gray",
264866
- children: spinnerFrame
264867
- }, undefined, false, undefined, this),
264868
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264869
- children: pollStatus.state === "polling" ? "Polling Linear\u2026" : pollStatus.lastAt !== null ? "Idle" : "Starting\u2026"
264870
- }, undefined, false, undefined, this),
264871
- pollStatus.lastAt !== null && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
264872
- children: pollStatus.lastBuckets && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
264873
- children: [
264874
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264875
- dimColor: true,
264876
- children: "\u2502"
264877
- }, undefined, false, undefined, this),
264878
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264879
- dimColor: true,
264880
- children: "todo"
264881
- }, undefined, false, undefined, this),
264882
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264883
- color: "white",
264884
- children: pollStatus.lastBuckets.todo
264885
- }, undefined, false, undefined, this),
264886
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264887
- dimColor: true,
264888
- children: "\xB7"
264889
- }, undefined, false, undefined, this),
264890
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264891
- dimColor: true,
264892
- children: "resume"
264893
- }, undefined, false, undefined, this),
264894
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264895
- color: pollStatus.lastBuckets.inProgress > 0 ? "cyan" : "white",
264896
- children: pollStatus.lastBuckets.inProgress
264897
- }, undefined, false, undefined, this),
264898
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264899
- dimColor: true,
264900
- children: "\xB7"
264901
- }, undefined, false, undefined, this),
264902
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264903
- dimColor: true,
264904
- children: "review"
264905
- }, undefined, false, undefined, this),
264906
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264907
- color: pollStatus.lastBuckets.review > 0 ? "yellow" : "white",
264908
- children: pollStatus.lastBuckets.review
264909
- }, undefined, false, undefined, this),
264910
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264911
- dimColor: true,
264912
- children: "\xB7"
264913
- }, undefined, false, undefined, this),
264914
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264915
- dimColor: true,
264916
- children: "mentions"
264917
- }, undefined, false, undefined, this),
264918
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264919
- color: pollStatus.lastBuckets.mentions > 0 ? "magenta" : "white",
264920
- children: pollStatus.lastBuckets.mentions
264921
- }, undefined, false, undefined, this),
264922
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264923
- dimColor: true,
264924
- children: "\xB7"
264925
- }, undefined, false, undefined, this),
264926
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264927
- dimColor: true,
264928
- children: "awaiting"
264929
- }, undefined, false, undefined, this),
264930
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264931
- color: pollStatus.lastBuckets.awaiting > 0 ? "yellow" : "white",
264932
- children: pollStatus.lastBuckets.awaiting
264933
- }, undefined, false, undefined, this)
264934
- ]
264935
- }, undefined, true, undefined, this)
264936
- }, undefined, false, undefined, this)
264937
- ]
264938
- }, undefined, true, undefined, this),
264939
- pollStatus.lastAt !== null && pollStatus.lastPrStatus && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264940
- gap: 2,
264941
- children: [
264942
- secsToNextPoll !== null ? /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264943
- gap: 1,
264944
- width: 7,
264945
- children: [
264946
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264947
- dimColor: true,
264948
- children: "\u21BA"
264949
- }, undefined, false, undefined, this),
264950
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264951
- color: "gray",
264952
- children: [
264953
- secsToNextPoll,
264954
- "s"
264955
- ]
264956
- }, undefined, true, undefined, this)
264957
- ]
264958
- }, undefined, true, undefined, this) : /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264959
- children: " ".repeat(7)
264960
- }, undefined, false, undefined, this),
264961
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264962
- dimColor: true,
264963
- children: "\u2502"
264964
- }, undefined, false, undefined, this),
264965
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264966
- dimColor: true,
264967
- children: "mergeable"
264968
- }, undefined, false, undefined, this),
264969
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264970
- color: pollStatus.lastPrStatus.mergeable > 0 ? "green" : "white",
264971
- children: pollStatus.lastPrStatus.mergeable
264972
- }, undefined, false, undefined, this),
264973
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264974
- dimColor: true,
264975
- children: "\xB7"
264976
- }, undefined, false, undefined, this),
264977
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264978
- dimColor: true,
264979
- children: "conflicted"
264980
- }, undefined, false, undefined, this),
264981
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264982
- color: pollStatus.lastPrStatus.conflicted > 0 ? "red" : "white",
264983
- children: pollStatus.lastPrStatus.conflicted
264984
- }, undefined, false, undefined, this),
264985
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264986
- dimColor: true,
264987
- children: "\xB7"
264988
- }, undefined, false, undefined, this),
264989
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264990
- dimColor: true,
264991
- children: "ci-failed"
264992
- }, undefined, false, undefined, this),
264993
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264994
- color: pollStatus.lastPrStatus.ciFailed > 0 ? "red" : "white",
264995
- children: pollStatus.lastPrStatus.ciFailed
264996
- }, undefined, false, undefined, this),
264997
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264998
- dimColor: true,
264999
- children: "\xB7"
265000
- }, undefined, false, undefined, this),
265001
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265002
- dimColor: true,
265003
- children: "quarantined"
265004
- }, undefined, false, undefined, this),
265005
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265006
- color: pollStatus.lastPrStatus.quarantined > 0 ? "magenta" : "white",
265007
- bold: true,
265008
- children: pollStatus.lastPrStatus.quarantined
265009
- }, undefined, false, undefined, this)
265010
- ]
265011
- }, undefined, true, undefined, this)
265012
- ]
265013
- }, undefined, true, undefined, this),
265014
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
265015
- label: "WORKERS",
265016
- borderColor: "gray",
265017
- width: 16,
265018
- paddingX: 1,
265019
- flexDirection: "column",
265020
- children: [
265021
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265022
- gap: 1,
265023
- children: [
265024
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265025
- dimColor: true,
265026
- children: "active"
265027
- }, undefined, false, undefined, this),
265028
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265029
- color: activeCount > 0 ? "cyan" : "gray",
265030
- bold: true,
265031
- children: activeCount
265032
- }, undefined, false, undefined, this)
265033
- ]
265034
- }, undefined, true, undefined, this),
265035
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265036
- gap: 1,
265037
- children: [
265038
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265039
- dimColor: true,
265040
- children: "queue"
265041
- }, undefined, false, undefined, this),
265042
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265043
- color: (coord?.queuedCount ?? 0) > 0 ? "yellow" : "gray",
265044
- bold: true,
265045
- children: coord?.queuedCount ?? 0
265046
- }, undefined, false, undefined, this)
265047
- ]
265048
- }, undefined, true, undefined, this)
265049
- ]
265050
- }, undefined, true, undefined, this)
265051
- ]
265052
- }, undefined, true, undefined, this),
265053
- activeCount > 1 && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
265054
- label: `TASKS${activeCount > 1 ? " Tab/\u2190 \u2192 \xB7 1-9" : ""}`,
265055
- borderColor: "gray",
265056
- width: termWidth,
265057
- paddingX: 1,
265058
- flexDirection: "column",
265059
- children: /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265060
- gap: 3,
265061
- flexWrap: "wrap",
265062
- children: coord?.activeWorkers.map((w2, idx) => {
265063
- const meta3 = workerMetaRef.current.get(w2.changeName);
265064
- const phase2 = meta3?.phase ?? "working";
265065
- const pBadge = priorityBadge(w2.issue.priority);
265066
- const isFocused = idx === safeFocusedIdx;
265067
- return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265068
- gap: 1,
265069
- children: [
265070
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265071
- color: isFocused ? "white" : "gray",
265072
- bold: isFocused,
265073
- children: [
265074
- "[",
265075
- idx + 1,
265076
- "]"
265077
- ]
265078
- }, undefined, true, undefined, this),
265079
- pBadge.label && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265080
- color: pBadge.color,
265081
- children: [
265082
- pBadge.text,
265083
- " ",
265084
- pBadge.label
265085
- ]
265086
- }, undefined, true, undefined, this),
265087
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265088
- url: w2.issue.url,
265089
- label: w2.issueIdentifier,
265090
- color: isFocused ? "cyan" : "gray"
265091
- }, undefined, false, undefined, this),
265092
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265093
- color: phaseColor(phase2),
265094
- dimColor: !isFocused,
265095
- children: phase2
265096
- }, undefined, false, undefined, this),
265097
- isFocused && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265098
- color: "white",
265099
- children: "\u25C0"
265100
- }, undefined, false, undefined, this)
265101
- ]
265102
- }, w2.changeName, true, undefined, this);
265103
- })
265104
- }, undefined, false, undefined, this)
265105
- }, undefined, false, undefined, this),
265106
265297
  (() => {
265107
- const gated = gatedTicketsRef.current;
265108
- if (gated.size === 0)
265109
- return null;
265110
- if (gated.size >= 2) {
265111
- const entries = Array.from(gated.entries()).sort(([, a], [, b2]) => {
265112
- const aTime = a.since ? new Date(a.since).getTime() : 0;
265113
- const bTime = b2.since ? new Date(b2.since).getTime() : 0;
265114
- return bTime - aTime;
265115
- });
265116
- const idLen = entries.reduce((sum2, [, g3]) => sum2 + g3.issueIdentifier.length, 0);
265117
- const multiLabelWidth = idLen + (entries.length - 1) * 3 + 2;
265118
- const labelParts = [];
265119
- entries.forEach(([, g3], i) => {
265120
- labelParts.push(/* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265121
- url: g3.issueUrl,
265122
- label: g3.issueIdentifier,
265123
- color: "yellow"
265124
- }, g3.issueIdentifier, false, undefined, this));
265125
- if (i < entries.length - 1) {
265126
- labelParts.push(/* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265127
- color: "yellow",
265128
- children: " \xB7 "
265129
- }, `sep-${i}`, false, undefined, this));
265130
- }
265131
- });
265132
- 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",
265133
265309
  children: [
265134
265310
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265135
- color: "yellow",
265136
- children: " "
265311
+ color: "gray",
265312
+ children: lead
265137
265313
  }, undefined, false, undefined, this),
265138
- labelParts,
265139
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265140
- color: "yellow",
265141
- children: " "
265142
- }, undefined, false, undefined, this)
265143
- ]
265144
- }, undefined, true, undefined, this);
265145
- return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
265146
- labelNode: multiLabelNode,
265147
- labelVisualWidth: multiLabelWidth,
265148
- borderColor: "yellow",
265149
- paddingX: 1,
265150
- gap: 2,
265151
- width: termWidth,
265152
- children: [
265153
265314
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265154
- color: "yellow",
265155
265315
  bold: true,
265156
- children: "[GATE]"
265157
- }, undefined, false, undefined, this),
265158
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265159
- color: "yellow",
265160
- children: "Awaiting confirmation"
265316
+ children: "TASKS"
265161
265317
  }, undefined, false, undefined, this),
265162
265318
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265163
265319
  dimColor: true,
265164
- children: "\xB7"
265320
+ children: hint
265165
265321
  }, undefined, false, undefined, this),
265166
265322
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265167
- color: "white",
265168
- bold: true,
265169
- children: gated.size
265323
+ color: "gray",
265324
+ children: "\u2500".repeat(fill2)
265170
265325
  }, undefined, false, undefined, this),
265171
265326
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265172
265327
  dimColor: true,
265173
- children: "tickets"
265328
+ children: live
265329
+ }, undefined, false, undefined, this),
265330
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265331
+ color: "gray",
265332
+ children: trail
265174
265333
  }, undefined, false, undefined, this)
265175
265334
  ]
265176
- }, undefined, true, undefined, this);
265177
- }
265178
- const { top } = pickLatestGatedTicket(gated);
265179
- if (!top)
265180
- return null;
265181
- const [changeName, g2] = top;
265182
- const askedAgo = g2.since ? fmtElapsed(now2 - Date.parse(g2.since)) : "just now";
265183
- const cardLabelWidth = g2.issueIdentifier.length + 2;
265184
- const cardLabelNode = /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265185
- children: [
265186
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265187
- color: "yellow",
265188
- children: " "
265189
- }, undefined, false, undefined, this),
265190
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265191
- url: g2.issueUrl,
265192
- label: g2.issueIdentifier,
265193
- color: "yellow"
265194
- }, undefined, false, undefined, this),
265195
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265196
- color: "yellow",
265197
- children: " "
265198
- }, undefined, false, undefined, this)
265199
- ]
265200
- }, undefined, true, undefined, this);
265201
- return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
265202
- labelNode: cardLabelNode,
265203
- labelVisualWidth: cardLabelWidth,
265204
- borderColor: "yellow",
265205
- paddingX: 1,
265206
- gap: 2,
265335
+ }, undefined, true, undefined, this),
265336
+ borderColor: "gray",
265207
265337
  width: termWidth,
265208
- children: [
265209
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265210
- color: "yellow",
265211
- bold: true,
265212
- children: "[GATE]"
265213
- }, undefined, false, undefined, this),
265214
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265215
- color: "yellow",
265216
- children: "Awaiting confirmation"
265217
- }, undefined, false, undefined, this),
265218
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265219
- dimColor: true,
265220
- children: "\xB7"
265221
- }, undefined, false, undefined, this),
265222
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265223
- dimColor: true,
265224
- children: "round"
265225
- }, undefined, false, undefined, this),
265226
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265227
- color: "white",
265228
- bold: true,
265229
- children: g2.round
265230
- }, undefined, false, undefined, this),
265231
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265232
- dimColor: true,
265233
- children: "\xB7"
265234
- }, undefined, false, undefined, this),
265235
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265236
- dimColor: true,
265237
- children: "asked"
265238
- }, undefined, false, undefined, this),
265239
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265240
- color: "white",
265241
- children: askedAgo
265242
- }, undefined, false, undefined, this),
265243
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265244
- dimColor: true,
265245
- children: "ago"
265246
- }, undefined, false, undefined, this),
265247
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265248
- dimColor: true,
265249
- children: "\u2502"
265250
- }, undefined, false, undefined, this),
265251
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265252
- dimColor: true,
265253
- children: trunc(g2.issueTitle, Math.max(20, termWidth - 70))
265254
- }, undefined, false, undefined, this)
265255
- ]
265256
- }, `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);
265257
265484
  })(),
265258
- coord?.activeWorkers.map((w2, idx) => {
265259
- const isFocused = idx === safeFocusedIdx;
265485
+ focusedWorker && (() => {
265486
+ const w2 = focusedWorker;
265260
265487
  const meta3 = workerMetaRef.current.get(w2.changeName);
265261
265488
  const elapsed = meta3 ? fmtElapsed(now2 - meta3.startedAt) : "\u2013";
265262
265489
  const iter = meta3?.iter ?? 0;
@@ -265270,122 +265497,10 @@ function AgentMode({
265270
265497
  const taskProgress = meta3?.taskProgress ?? null;
265271
265498
  const openspecPhase = meta3?.openspecPhase ?? null;
265272
265499
  const subtasks = meta3?.subtasks ?? [];
265273
- const pBadge = priorityBadge(w2.issue.priority);
265274
265500
  const mBadge = modeBadge(w2.trigger);
265275
265501
  const pColor = phaseColor(phase2);
265276
- const bColor = isFocused ? workerBorderColor(phase2) : "gray";
265277
- const visibleTailLines = isFocused ? focusedTailLines : compactTailLines;
265278
- if (!isFocused && activeCount > 1) {
265279
- const cardLabelWidth2 = (prUrl ? prLabel(prUrl).length + 3 : 0) + w2.issueIdentifier.length + 2;
265280
- const cardLabelNode2 = /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265281
- children: [
265282
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265283
- color: "gray",
265284
- children: " "
265285
- }, undefined, false, undefined, this),
265286
- prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265287
- url: prUrl,
265288
- label: prLabel(prUrl),
265289
- color: "green"
265290
- }, undefined, false, undefined, this),
265291
- prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265292
- color: "gray",
265293
- children: " \xB7 "
265294
- }, undefined, false, undefined, this),
265295
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265296
- url: w2.issue.url,
265297
- label: w2.issueIdentifier,
265298
- color: "cyan"
265299
- }, undefined, false, undefined, this),
265300
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265301
- color: "gray",
265302
- children: " "
265303
- }, undefined, false, undefined, this)
265304
- ]
265305
- }, undefined, true, undefined, this);
265306
- return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
265307
- labelNode: cardLabelNode2,
265308
- labelVisualWidth: cardLabelWidth2,
265309
- borderColor: "gray",
265310
- paddingX: 1,
265311
- gap: 2,
265312
- width: termWidth,
265313
- children: [
265314
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265315
- dimColor: true,
265316
- children: [
265317
- "[",
265318
- idx + 1,
265319
- "]"
265320
- ]
265321
- }, undefined, true, undefined, this),
265322
- pBadge.label && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265323
- color: pBadge.color,
265324
- children: pBadge.text
265325
- }, undefined, false, undefined, this),
265326
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265327
- color: "gray",
265328
- bold: true,
265329
- children: w2.issueIdentifier
265330
- }, undefined, false, undefined, this),
265331
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265332
- dimColor: true,
265333
- children: trunc(w2.issue.title, 40)
265334
- }, undefined, false, undefined, this),
265335
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265336
- dimColor: true,
265337
- children: "\u2502"
265338
- }, undefined, false, undefined, this),
265339
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265340
- color: pColor,
265341
- dimColor: true,
265342
- children: phase2
265343
- }, undefined, false, undefined, this),
265344
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265345
- dimColor: true,
265346
- children: "\u2502"
265347
- }, undefined, false, undefined, this),
265348
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265349
- dimColor: true,
265350
- children: elapsed
265351
- }, undefined, false, undefined, this),
265352
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265353
- dimColor: true,
265354
- children: "\xB7"
265355
- }, undefined, false, undefined, this),
265356
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265357
- dimColor: true,
265358
- children: [
265359
- "\u21BA ",
265360
- iter
265361
- ]
265362
- }, undefined, true, undefined, this),
265363
- currentTask && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265364
- children: [
265365
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265366
- dimColor: true,
265367
- children: "\u2502"
265368
- }, undefined, false, undefined, this),
265369
- openspecPhase && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265370
- color: openspecPhaseColor(openspecPhase),
265371
- children: [
265372
- "[",
265373
- openspecPhase,
265374
- "]"
265375
- ]
265376
- }, undefined, true, undefined, this),
265377
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265378
- dimColor: true,
265379
- children: [
265380
- "\u25B6 ",
265381
- trunc(currentTask, 40)
265382
- ]
265383
- }, undefined, true, undefined, this)
265384
- ]
265385
- }, undefined, true, undefined, this)
265386
- ]
265387
- }, w2.changeName, true, undefined, this);
265388
- }
265502
+ const bColor = workerBorderColor(phase2);
265503
+ const visibleTailLines = focusedTailLines;
265389
265504
  const cardLabelWidth = (prUrl ? prLabel(prUrl).length + 3 : 0) + w2.issueIdentifier.length + 2;
265390
265505
  const cardLabelNode = /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265391
265506
  children: [
@@ -265610,7 +265725,7 @@ function AgentMode({
265610
265725
  }, undefined, false, undefined, this)
265611
265726
  ]
265612
265727
  }, undefined, true, undefined, this),
265613
- steeringActive && idx === safeFocusedIdx && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265728
+ steeringActive && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265614
265729
  marginTop: 0,
265615
265730
  children: /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(SteeringField, {
265616
265731
  active: steeringActive,
@@ -265628,8 +265743,12 @@ function AgentMode({
265628
265743
  },
265629
265744
  onSubmit: async (message) => {
265630
265745
  try {
265631
- await appendSteering2(join39(tasksDir, w2.changeName), message);
265632
- 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
+ });
265633
265752
  } catch (err) {
265634
265753
  const text = err.message;
265635
265754
  fileEmit({
@@ -265705,7 +265824,108 @@ function AgentMode({
265705
265824
  })()
265706
265825
  ]
265707
265826
  }, w2.changeName, true, undefined, this);
265708
- })
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
+ })()
265709
265929
  ]
265710
265930
  }, undefined, true, undefined, this),
265711
265931
  awaitingClose && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
@@ -265715,13 +265935,14 @@ function AgentMode({
265715
265935
  ]
265716
265936
  }, resizeKey, true, undefined, this);
265717
265937
  }
265718
- 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;
265719
265939
  var init_AgentMode = __esm(async () => {
265720
265940
  init_cli2();
265721
265941
  init_config();
265722
265942
  init_wire();
265723
265943
  init_preflight();
265724
265944
  init_json_log_file();
265945
+ init_task_pipeline();
265725
265946
  init_phase();
265726
265947
  init_log();
265727
265948
  init_useTerminalSize();
@@ -265737,7 +265958,32 @@ var init_AgentMode = __esm(async () => {
265737
265958
  import_react63 = __toESM(require_react(), 1);
265738
265959
  jsx_dev_runtime11 = __toESM(require_jsx_dev_runtime(), 1);
265739
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
+ ]);
265740
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
+ };
265741
265987
  SESSION_START = new Date().toISOString();
265742
265988
  });
265743
265989
 
@@ -265937,7 +266183,7 @@ __export(exports_list, {
265937
266183
  buildBuckets: () => buildBuckets,
265938
266184
  backlogRankByIssueId: () => backlogRankByIssueId
265939
266185
  });
265940
- import { join as join40 } from "path";
266186
+ import { join as join39 } from "path";
265941
266187
  function countTaskItems(content) {
265942
266188
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
265943
266189
  const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
@@ -265953,13 +266199,13 @@ function buildLocalRows() {
265953
266199
  const sources = [{ dir: statesDir, label: "main" }];
265954
266200
  const worktreesRoot = worktreesDir2(projectRoot);
265955
266201
  for (const wt of storage.list(worktreesRoot)) {
265956
- sources.push({ dir: join40(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
266202
+ sources.push({ dir: join39(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
265957
266203
  }
265958
266204
  for (const { dir, label } of sources) {
265959
266205
  for (const entry of storage.list(dir)) {
265960
266206
  if (seen.has(entry))
265961
266207
  continue;
265962
- const raw = storage.read(join40(dir, entry, ".ralph-state.json"));
266208
+ const raw = storage.read(join39(dir, entry, ".ralph-state.json"));
265963
266209
  if (raw === null)
265964
266210
  continue;
265965
266211
  let state;
@@ -265974,7 +266220,7 @@ function buildLocalRows() {
265974
266220
  const firstLine = promptRaw.split(`
265975
266221
  `).find((l3) => l3.trim() !== "") ?? "";
265976
266222
  let progress = "\u2014";
265977
- const tasksContent = storage.read(join40(dir, entry, "tasks.md"));
266223
+ const tasksContent = storage.read(join39(dir, entry, "tasks.md"));
265978
266224
  if (tasksContent !== null) {
265979
266225
  const { checked, unchecked } = countTaskItems(tasksContent);
265980
266226
  const total = checked + unchecked;
@@ -266307,8 +266553,8 @@ async function fetchIssueByIdentifier(apiKey, identifier) {
266307
266553
  team { key }
266308
266554
  labels { nodes { name } }
266309
266555
  attachments(first: 25) { nodes { title subtitle } }
266310
- relations(first: 50) {
266311
- nodes { type relatedIssue { id identifier state { type } } }
266556
+ inverseRelations(first: 50) {
266557
+ nodes { type issue { id identifier state { type } } }
266312
266558
  }
266313
266559
  }
266314
266560
  }
@@ -266387,7 +266633,7 @@ Found ${issue2.identifier} \u2014 "${issue2.title}"
266387
266633
  ` + ` assignee: ${issue2.assignee ? `${issue2.assignee.name} <${issue2.assignee.email ?? "no-email"}>` : "(unassigned)"}
266388
266634
  ` + ` labels: ${issue2.labels.nodes.map((l3) => l3.name).join(", ") || "(none)"}
266389
266635
  `);
266390
- 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);
266391
266637
  process.stdout.write(`
266392
266638
  Per-bucket diagnostics:
266393
266639
  `);
@@ -266477,7 +266723,7 @@ var exports_json_runner = {};
266477
266723
  __export(exports_json_runner, {
266478
266724
  runAgentJson: () => runAgentJson
266479
266725
  });
266480
- import { join as join41 } from "path";
266726
+ import { join as join40 } from "path";
266481
266727
  import { mkdir as mkdir12 } from "fs/promises";
266482
266728
  import { homedir as homedir8 } from "os";
266483
266729
  function makeEmit(fileSink) {
@@ -266499,7 +266745,7 @@ async function runAgentJson({
266499
266745
  tasksDir,
266500
266746
  runPreflight: runPreflight2 = runPreflight
266501
266747
  }) {
266502
- await mkdir12(join41(homedir8(), ".ralph"), { recursive: true }).catch(() => {
266748
+ await mkdir12(join40(homedir8(), ".ralph"), { recursive: true }).catch(() => {
266503
266749
  return;
266504
266750
  });
266505
266751
  const fileSink = createJsonLogFileSink(args.jsonLogFile);
@@ -266719,7 +266965,7 @@ __export(exports_src3, {
266719
266965
  main: () => main3
266720
266966
  });
266721
266967
  import { mkdir as mkdir13 } from "fs/promises";
266722
- import { join as join42 } from "path";
266968
+ import { join as join41 } from "path";
266723
266969
  async function main3(argv) {
266724
266970
  if (argv.includes("--help") || argv.includes("-h")) {
266725
266971
  printAgentHelp();
@@ -266784,7 +267030,7 @@ async function main3(argv) {
266784
267030
  }
266785
267031
  await mkdir13(statesDir, { recursive: true });
266786
267032
  await mkdir13(tasksDir, { recursive: true });
266787
- await mkdir13(join42(projectRoot, ".ralph"), { recursive: true });
267033
+ await mkdir13(join41(projectRoot, ".ralph"), { recursive: true });
266788
267034
  if (shouldFallbackToJsonOutput(args, process.stdin.isTTY)) {
266789
267035
  process.stderr.write(`agent: stdin is not a TTY \u2014 falling back to --json-output mode.
266790
267036
  `);