@neriros/ralphy 3.10.17 → 3.10.19

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 +1585 -1175
  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.19")
18932
+ return "3.10.19";
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?",
@@ -81301,6 +81308,20 @@ var init_zod = __esm(() => {
81301
81308
  });
81302
81309
 
81303
81310
  // packages/workflow/src/schema.ts
81311
+ function normalizeNegationShorthand(v) {
81312
+ if (!v || typeof v !== "object" || Array.isArray(v))
81313
+ return v;
81314
+ const obj = v;
81315
+ const t = obj["type"];
81316
+ const negatable = t === "label" || t === "status" || t === "project";
81317
+ if (!negatable || obj["negate"] !== undefined)
81318
+ return v;
81319
+ const value = obj["value"];
81320
+ if (typeof value === "string" && value.startsWith("!")) {
81321
+ return { ...obj, value: value.slice(1), negate: true };
81322
+ }
81323
+ return v;
81324
+ }
81304
81325
  function foldLegacyAssignee(v) {
81305
81326
  if (!v || typeof v !== "object" || Array.isArray(v))
81306
81327
  return v;
@@ -81315,24 +81336,42 @@ function foldLegacyAssignee(v) {
81315
81336
  }
81316
81337
  return rest2;
81317
81338
  }
81318
- var CURRENT_WORKFLOW_VERSION = 7, MarkerSchema, FilterMarkerSchema, LinearFilterSchema, SET_INDICATOR_KEYS, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, DEFAULT_META_ONLY_FILES, BoundariesSchema, WorkflowConfigSchema;
81339
+ var CURRENT_WORKFLOW_VERSION = 8, MarkerSchema, FilterMarkerSchema, LinearFilterSchema, SET_INDICATOR_KEYS, GetIndicatorSchema, SetIndicatorSchema, IndicatorsSchema, ProjectSchema, CommandsSchema, DEFAULT_META_ONLY_FILES, BoundariesSchema, WorkflowConfigSchema;
81319
81340
  var init_schema = __esm(() => {
81320
81341
  init_zod();
81321
- MarkerSchema = exports_external.discriminatedUnion("type", [
81342
+ MarkerSchema = exports_external.preprocess(normalizeNegationShorthand, exports_external.discriminatedUnion("type", [
81322
81343
  exports_external.object({
81323
81344
  type: exports_external.literal("label"),
81324
81345
  value: exports_external.string().min(1),
81325
- group: exports_external.string().min(1).optional()
81346
+ group: exports_external.string().min(1).optional(),
81347
+ negate: exports_external.boolean().optional()
81326
81348
  }),
81327
- exports_external.object({ type: exports_external.literal("status"), value: exports_external.string().min(1) }).strict(),
81349
+ exports_external.object({
81350
+ type: exports_external.literal("status"),
81351
+ value: exports_external.string().min(1),
81352
+ negate: exports_external.boolean().optional()
81353
+ }).strict(),
81328
81354
  exports_external.object({ type: exports_external.literal("attachment"), value: exports_external.string().min(1) }).strict(),
81329
- exports_external.object({ type: exports_external.literal("project"), value: exports_external.string().min(1) }).strict(),
81355
+ exports_external.object({
81356
+ type: exports_external.literal("project"),
81357
+ value: exports_external.string().min(1),
81358
+ negate: exports_external.boolean().optional()
81359
+ }).strict(),
81330
81360
  exports_external.object({ type: exports_external.literal("comment"), value: exports_external.string().min(1) }).strict()
81331
- ]);
81332
- FilterMarkerSchema = exports_external.discriminatedUnion("type", [
81333
- exports_external.object({ type: exports_external.literal("label"), value: exports_external.string().min(1) }).strict(),
81361
+ ]));
81362
+ FilterMarkerSchema = exports_external.preprocess(normalizeNegationShorthand, exports_external.discriminatedUnion("type", [
81363
+ exports_external.object({
81364
+ type: exports_external.literal("label"),
81365
+ value: exports_external.string().min(1),
81366
+ negate: exports_external.boolean().optional()
81367
+ }).strict(),
81368
+ exports_external.object({
81369
+ type: exports_external.literal("project"),
81370
+ value: exports_external.string().min(1),
81371
+ negate: exports_external.boolean().optional()
81372
+ }).strict(),
81334
81373
  exports_external.object({ type: exports_external.literal("assignee"), value: exports_external.string().min(1) }).strict()
81335
- ]);
81374
+ ]));
81336
81375
  LinearFilterSchema = exports_external.array(FilterMarkerSchema).superRefine((markers, ctx) => {
81337
81376
  const assigneeCount = markers.filter((m) => m.type === "assignee").length;
81338
81377
  if (assigneeCount > 1) {
@@ -81341,6 +81380,13 @@ var init_schema = __esm(() => {
81341
81380
  message: `linear.filter allows at most one "assignee" clause, found ${assigneeCount}.`
81342
81381
  });
81343
81382
  }
81383
+ const positiveProjects = markers.filter((m) => m.type === "project" && !m.negate).length;
81384
+ if (positiveProjects > 1) {
81385
+ ctx.addIssue({
81386
+ code: exports_external.ZodIssueCode.custom,
81387
+ message: `linear.filter allows at most one positive "project" clause, found ${positiveProjects}.`
81388
+ });
81389
+ }
81344
81390
  }).default([{ type: "assignee", value: "me" }]);
81345
81391
  SET_INDICATOR_KEYS = [
81346
81392
  "setInProgress",
@@ -81374,6 +81420,14 @@ var init_schema = __esm(() => {
81374
81420
  continue;
81375
81421
  const markers = Array.isArray(clear) ? clear : [clear];
81376
81422
  for (const m of markers) {
81423
+ if ("negate" in m && m.negate) {
81424
+ ctx.addIssue({
81425
+ code: exports_external.ZodIssueCode.custom,
81426
+ path: [key],
81427
+ message: `${key} cannot use a negated marker \u2014 negation is only meaningful in getX filters`
81428
+ });
81429
+ break;
81430
+ }
81377
81431
  if (m.type === "comment")
81378
81432
  continue;
81379
81433
  if (m.type !== "label") {
@@ -81392,6 +81446,14 @@ var init_schema = __esm(() => {
81392
81446
  continue;
81393
81447
  const markers = Array.isArray(set3) ? set3 : [set3];
81394
81448
  for (const m of markers) {
81449
+ if ("negate" in m && m.negate) {
81450
+ ctx.addIssue({
81451
+ code: exports_external.ZodIssueCode.custom,
81452
+ path: [key],
81453
+ message: `${key} cannot use a negated marker \u2014 negation is only meaningful in getX filters`
81454
+ });
81455
+ break;
81456
+ }
81395
81457
  if (m.type === "comment") {
81396
81458
  ctx.addIssue({
81397
81459
  code: exports_external.ZodIssueCode.custom,
@@ -81455,6 +81517,7 @@ var init_schema = __esm(() => {
81455
81517
  createPrOnSuccess: exports_external.boolean().default(false),
81456
81518
  prDraft: exports_external.boolean().default(false),
81457
81519
  prBaseBranch: exports_external.string().default("main"),
81520
+ prLabels: exports_external.array(exports_external.string()).default([]),
81458
81521
  stackPrsOnDependencies: exports_external.boolean().default(false),
81459
81522
  autoMergeStrategy: exports_external.enum(["squash", "merge", "rebase"]).default("squash"),
81460
81523
  manualMergeWhenAutoMergeDisabled: exports_external.boolean().default(true),
@@ -81509,6 +81572,7 @@ var init_schema = __esm(() => {
81509
81572
  github: exports_external.object({
81510
81573
  base_branch: exports_external.string().optional(),
81511
81574
  auto_merge_strategy: exports_external.enum(["squash", "merge", "rebase"]).optional(),
81575
+ pr_labels: exports_external.array(exports_external.string()).optional(),
81512
81576
  issues: exports_external.object({
81513
81577
  repo: exports_external.string().optional(),
81514
81578
  label: exports_external.string().optional(),
@@ -81591,7 +81655,7 @@ var init_schema = __esm(() => {
81591
81655
  var FRONTMATTER_RE, DEFAULT_WORKFLOW_MD = `---
81592
81656
  # WORKFLOW.md schema version \u2014 managed by \`ralphy init\`. When a newer version
81593
81657
  # ships, re-running init migrates this file and fills in the new settings.
81594
- version: 7
81658
+ version: 8
81595
81659
 
81596
81660
  project:
81597
81661
  name: ralphy
@@ -81643,6 +81707,11 @@ prDraft: true
81643
81707
  prBaseBranch: main
81644
81708
  stackPrsOnDependencies: false
81645
81709
  autoMergeStrategy: squash
81710
+ # Labels attached to every PR Ralph opens (best-effort \u2014 a missing label is
81711
+ # logged, never fatal). The labels must already exist in the repo.
81712
+ # prLabels:
81713
+ # - ralph
81714
+ # - automated
81646
81715
 
81647
81716
  prRecovery:
81648
81717
  enabled: true
@@ -82247,34 +82316,64 @@ function describeApprovalMarker(indicator) {
82247
82316
  }
82248
82317
 
82249
82318
  // packages/workflow/src/linear-filter.ts
82319
+ function linearFilterScope(resolved) {
82320
+ const scope = { requireAllLabels: resolved.requireAllLabels };
82321
+ if (resolved.excludeLabels)
82322
+ scope.excludeLabels = resolved.excludeLabels;
82323
+ if (resolved.requireProject)
82324
+ scope.requireProject = resolved.requireProject;
82325
+ if (resolved.excludeProjects)
82326
+ scope.excludeProjects = resolved.excludeProjects;
82327
+ return scope;
82328
+ }
82250
82329
  function resolveLinearFilter(filter2) {
82251
82330
  const assigneeClauses = filter2.filter((marker) => marker.type === "assignee");
82252
82331
  if (assigneeClauses.length > 1) {
82253
82332
  throw new Error(`Invalid linear.filter: at most one "assignee" clause is allowed, found ${assigneeClauses.length}.`);
82254
82333
  }
82255
- const requireAllLabels = [];
82256
- const seenLabels = new Set;
82257
- for (const marker of filter2) {
82258
- if (marker.type !== "label")
82259
- continue;
82260
- if (seenLabels.has(marker.value))
82261
- continue;
82262
- seenLabels.add(marker.value);
82263
- requireAllLabels.push(marker.value);
82264
- }
82334
+ const requireAllLabels = collectDeduped(filter2, "label", false);
82335
+ const excludeLabels = collectDeduped(filter2, "label", true);
82336
+ const requireProjects = collectDeduped(filter2, "project", false);
82337
+ const excludeProjects = collectDeduped(filter2, "project", true);
82338
+ if (requireProjects.length > 1) {
82339
+ throw new Error(`Invalid linear.filter: at most one positive "project" clause is allowed, found ${requireProjects.length}.`);
82340
+ }
82341
+ const optional2 = {};
82342
+ if (excludeLabels.length > 0)
82343
+ optional2.excludeLabels = excludeLabels;
82344
+ if (requireProjects[0] !== undefined)
82345
+ optional2.requireProject = requireProjects[0];
82346
+ if (excludeProjects.length > 0)
82347
+ optional2.excludeProjects = excludeProjects;
82348
+ const base2 = { requireAllLabels, ...optional2 };
82265
82349
  const assigneeClause = assigneeClauses[0];
82266
82350
  if (!assigneeClause)
82267
- return { requireAllLabels };
82351
+ return base2;
82268
82352
  const value = assigneeClause.value.trim();
82269
82353
  const lower = value.toLowerCase();
82270
82354
  if (lower === "any")
82271
- return { anyAssignee: true, requireAllLabels };
82355
+ return { anyAssignee: true, ...base2 };
82272
82356
  if (lower === "" || lower === "unassigned") {
82273
- return { assignee: "unassigned", requireAllLabels };
82357
+ return { assignee: "unassigned", ...base2 };
82274
82358
  }
82275
82359
  if (lower === "me")
82276
- return { assignee: "me", requireAllLabels };
82277
- return { assignee: value, requireAllLabels };
82360
+ return { assignee: "me", ...base2 };
82361
+ return { assignee: value, ...base2 };
82362
+ }
82363
+ function collectDeduped(filter2, type, negated) {
82364
+ const out = [];
82365
+ const seen = new Set;
82366
+ for (const marker of filter2) {
82367
+ if (marker.type !== type)
82368
+ continue;
82369
+ if (Boolean(marker.negate) !== negated)
82370
+ continue;
82371
+ if (seen.has(marker.value))
82372
+ continue;
82373
+ seen.add(marker.value);
82374
+ out.push(marker.value);
82375
+ }
82376
+ return out;
82278
82377
  }
82279
82378
  function applyAssigneeOverride(filter2, assignee) {
82280
82379
  const trimmed = assignee.trim();
@@ -82301,6 +82400,7 @@ __export(exports_workflow, {
82301
82400
  migrateWorkflowMarkdown: () => migrateWorkflowMarkdown,
82302
82401
  matchesIndicator: () => matchesIndicator,
82303
82402
  loadWorkflow: () => loadWorkflow,
82403
+ linearFilterScope: () => linearFilterScope,
82304
82404
  ensureWorkflow: () => ensureWorkflow,
82305
82405
  describeApprovalMarker: () => describeApprovalMarker,
82306
82406
  computeConfirmationFlags: () => computeConfirmationFlags,
@@ -82375,6 +82475,9 @@ function applyAliases(cfg) {
82375
82475
  if (cfg.github.auto_merge_strategy !== undefined && cfg.autoMergeStrategy === "squash") {
82376
82476
  cfg.autoMergeStrategy = cfg.github.auto_merge_strategy;
82377
82477
  }
82478
+ if (cfg.github.pr_labels !== undefined && cfg.prLabels.length === 0) {
82479
+ cfg.prLabels = cfg.github.pr_labels;
82480
+ }
82378
82481
  }
82379
82482
  if (cfg.agent) {
82380
82483
  if (cfg.agent.engine !== undefined)
@@ -84691,6 +84794,11 @@ var init_migrations = __esm(() => {
84691
84794
  "github.issues.statusLabels.done",
84692
84795
  "github.issues.statusLabels.error"
84693
84796
  ]
84797
+ },
84798
+ {
84799
+ version: 8,
84800
+ 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.",
84801
+ fields: ["prLabels"]
84694
84802
  }
84695
84803
  ];
84696
84804
  LATEST_MIGRATION_VERSION = MIGRATIONS.reduce((max2, migration) => Math.max(max2, migration.version), 0);
@@ -85262,7 +85370,7 @@ function projectLayout(root) {
85262
85370
  stateFile: (name) => join7(statesDir, name, STATE_FILE)
85263
85371
  };
85264
85372
  }
85265
- var STATE_FILE = ".ralph-state.json", GAVEUP_COUNT_FILE = ".ralph-gaveup-count";
85373
+ var STATE_FILE = ".ralph-state.json";
85266
85374
  var init_layout = __esm(() => {
85267
85375
  init_context();
85268
85376
  });
@@ -99729,6 +99837,39 @@ var init_example_machine = __esm(() => {
99729
99837
  });
99730
99838
 
99731
99839
  // packages/core/src/machines/flow.machine.ts
99840
+ function recordDetection(reason) {
99841
+ return ({
99842
+ context,
99843
+ event
99844
+ }) => {
99845
+ const previous = context.data.recovery;
99846
+ return {
99847
+ data: {
99848
+ ...context.data,
99849
+ recovery: {
99850
+ attempts: (previous?.attempts ?? 0) + 1,
99851
+ lastReason: reason,
99852
+ firstFailedAt: previous?.firstFailedAt ?? event.at ?? ""
99853
+ }
99854
+ }
99855
+ };
99856
+ };
99857
+ }
99858
+ function reachesQuarantine({ context }) {
99859
+ const max2 = context.data.maxRecoveryAttempts;
99860
+ return max2 > 0 && (context.data.recovery?.attempts ?? 0) + 1 >= max2;
99861
+ }
99862
+ function refreshReason(reason) {
99863
+ return ({ context }) => ({
99864
+ data: {
99865
+ ...context.data,
99866
+ recovery: context.data.recovery ? { ...context.data.recovery, lastReason: reason } : { attempts: 0, lastReason: reason, firstFailedAt: "" }
99867
+ }
99868
+ });
99869
+ }
99870
+ function clearRecovery({ context }) {
99871
+ return { data: { ...context.data, recovery: undefined } };
99872
+ }
99732
99873
  var preemptionActorLogic, flowMachine;
99733
99874
  var init_flow_machine = __esm(() => {
99734
99875
  init_xstate_development_cjs();
@@ -99775,14 +99916,20 @@ var init_flow_machine = __esm(() => {
99775
99916
  }).createMachine({
99776
99917
  id: "flow",
99777
99918
  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
99919
+ data: {
99920
+ issueId: input?.issueId ?? "",
99921
+ graceMs: input?.graceMs ?? 5000,
99922
+ maxRecoveryAttempts: input?.maxRecoveryAttempts ?? 0,
99923
+ currentAssignment: undefined,
99924
+ pendingAssignment: undefined,
99925
+ recovery: undefined
99926
+ },
99927
+ runtime: {
99928
+ bus: input?.bus ?? createNoopBus(),
99929
+ persist: input?.persist ?? (() => {}),
99930
+ worker: undefined,
99931
+ teardown: undefined
99932
+ }
99786
99933
  }),
99787
99934
  initial: "idle",
99788
99935
  states: {
@@ -99791,29 +99938,63 @@ var init_flow_machine = __esm(() => {
99791
99938
  FRESH_PICKED_UP: "working",
99792
99939
  RESUME_DETECTED: "working",
99793
99940
  REVIEW_TRIGGERED: "review",
99794
- CONFLICT_DETECTED: "conflict-fix",
99795
- CI_FAILED_DETECTED: "ci-fix"
99941
+ CONFLICT_DETECTED: [
99942
+ {
99943
+ guard: reachesQuarantine,
99944
+ target: "quarantined",
99945
+ actions: import_xstate_development_cjs.assign(recordDetection("conflicting"))
99946
+ },
99947
+ { target: "conflict-fix", actions: import_xstate_development_cjs.assign(recordDetection("conflicting")) }
99948
+ ],
99949
+ CI_FAILED_DETECTED: [
99950
+ {
99951
+ guard: reachesQuarantine,
99952
+ target: "quarantined",
99953
+ actions: import_xstate_development_cjs.assign(recordDetection("ci_failed"))
99954
+ },
99955
+ { target: "ci-fix", actions: import_xstate_development_cjs.assign(recordDetection("ci_failed")) }
99956
+ ]
99796
99957
  }
99797
99958
  },
99798
99959
  working: {
99799
99960
  on: {
99800
99961
  AWAITING_DETECTED: "awaiting",
99801
- CONFLICT_DETECTED: "conflict-fix",
99802
- CI_FAILED_DETECTED: "ci-fix",
99962
+ CONFLICT_DETECTED: [
99963
+ {
99964
+ guard: reachesQuarantine,
99965
+ target: "quarantined",
99966
+ actions: import_xstate_development_cjs.assign(recordDetection("conflicting"))
99967
+ },
99968
+ { target: "conflict-fix", actions: import_xstate_development_cjs.assign(recordDetection("conflicting")) }
99969
+ ],
99970
+ CI_FAILED_DETECTED: [
99971
+ {
99972
+ guard: reachesQuarantine,
99973
+ target: "quarantined",
99974
+ actions: import_xstate_development_cjs.assign(recordDetection("ci_failed"))
99975
+ },
99976
+ { target: "ci-fix", actions: import_xstate_development_cjs.assign(recordDetection("ci_failed")) }
99977
+ ],
99803
99978
  PR_OPENED: "awaiting-ci",
99804
99979
  WORKER_SUCCEEDED: "done",
99805
99980
  WORKER_FAILED: "error",
99806
99981
  PREEMPT: {
99807
99982
  target: "preempting",
99808
99983
  actions: import_xstate_development_cjs.assign({
99809
- pendingAssignment: ({ event }) => event.newAssignment
99984
+ data: ({ context, event }) => ({
99985
+ ...context.data,
99986
+ pendingAssignment: event.newAssignment
99987
+ })
99810
99988
  })
99811
99989
  },
99812
99990
  WORKER_SPAWNED: {
99813
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99814
- worker: event.worker,
99815
- teardown: event.teardown ?? undefined,
99816
- currentAssignment: event.assignment
99991
+ actions: import_xstate_development_cjs.assign(({ context, event }) => ({
99992
+ data: { ...context.data, currentAssignment: event.assignment },
99993
+ runtime: {
99994
+ ...context.runtime,
99995
+ worker: event.worker,
99996
+ teardown: event.teardown ?? undefined
99997
+ }
99817
99998
  }))
99818
99999
  }
99819
100000
  }
@@ -99825,14 +100006,20 @@ var init_flow_machine = __esm(() => {
99825
100006
  PREEMPT: {
99826
100007
  target: "preempting",
99827
100008
  actions: import_xstate_development_cjs.assign({
99828
- pendingAssignment: ({ event }) => event.newAssignment
100009
+ data: ({ context, event }) => ({
100010
+ ...context.data,
100011
+ pendingAssignment: event.newAssignment
100012
+ })
99829
100013
  })
99830
100014
  },
99831
100015
  WORKER_SPAWNED: {
99832
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99833
- worker: event.worker,
99834
- teardown: event.teardown ?? undefined,
99835
- currentAssignment: event.assignment
100016
+ actions: import_xstate_development_cjs.assign(({ context, event }) => ({
100017
+ data: { ...context.data, currentAssignment: event.assignment },
100018
+ runtime: {
100019
+ ...context.runtime,
100020
+ worker: event.worker,
100021
+ teardown: event.teardown ?? undefined
100022
+ }
99836
100023
  }))
99837
100024
  }
99838
100025
  }
@@ -99844,14 +100031,20 @@ var init_flow_machine = __esm(() => {
99844
100031
  PREEMPT: {
99845
100032
  target: "preempting",
99846
100033
  actions: import_xstate_development_cjs.assign({
99847
- pendingAssignment: ({ event }) => event.newAssignment
100034
+ data: ({ context, event }) => ({
100035
+ ...context.data,
100036
+ pendingAssignment: event.newAssignment
100037
+ })
99848
100038
  })
99849
100039
  },
99850
100040
  WORKER_SPAWNED: {
99851
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99852
- worker: event.worker,
99853
- teardown: event.teardown ?? undefined,
99854
- currentAssignment: event.assignment
100041
+ actions: import_xstate_development_cjs.assign(({ context, event }) => ({
100042
+ data: { ...context.data, currentAssignment: event.assignment },
100043
+ runtime: {
100044
+ ...context.runtime,
100045
+ worker: event.worker,
100046
+ teardown: event.teardown ?? undefined
100047
+ }
99855
100048
  }))
99856
100049
  }
99857
100050
  }
@@ -99862,7 +100055,10 @@ var init_flow_machine = __esm(() => {
99862
100055
  PREEMPT: {
99863
100056
  target: "preempting",
99864
100057
  actions: import_xstate_development_cjs.assign({
99865
- pendingAssignment: ({ event }) => event.newAssignment
100058
+ data: ({ context, event }) => ({
100059
+ ...context.data,
100060
+ pendingAssignment: event.newAssignment
100061
+ })
99866
100062
  })
99867
100063
  }
99868
100064
  }
@@ -99870,20 +100066,41 @@ var init_flow_machine = __esm(() => {
99870
100066
  "awaiting-ci": {
99871
100067
  on: {
99872
100068
  PR_PASSED: "done",
99873
- CONFLICT_DETECTED: "conflict-fix",
99874
- CI_FAILED_DETECTED: "ci-fix",
100069
+ RECOVERY_CLEARED: { actions: import_xstate_development_cjs.assign(clearRecovery) },
100070
+ CONFLICT_DETECTED: [
100071
+ {
100072
+ guard: reachesQuarantine,
100073
+ target: "quarantined",
100074
+ actions: import_xstate_development_cjs.assign(recordDetection("conflicting"))
100075
+ },
100076
+ { target: "conflict-fix", actions: import_xstate_development_cjs.assign(recordDetection("conflicting")) }
100077
+ ],
100078
+ CI_FAILED_DETECTED: [
100079
+ {
100080
+ guard: reachesQuarantine,
100081
+ target: "quarantined",
100082
+ actions: import_xstate_development_cjs.assign(recordDetection("ci_failed"))
100083
+ },
100084
+ { target: "ci-fix", actions: import_xstate_development_cjs.assign(recordDetection("ci_failed")) }
100085
+ ],
99875
100086
  REVIEW_TRIGGERED: "review",
99876
100087
  PREEMPT: {
99877
100088
  target: "preempting",
99878
100089
  actions: import_xstate_development_cjs.assign({
99879
- pendingAssignment: ({ event }) => event.newAssignment
100090
+ data: ({ context, event }) => ({
100091
+ ...context.data,
100092
+ pendingAssignment: event.newAssignment
100093
+ })
99880
100094
  })
99881
100095
  },
99882
100096
  WORKER_SPAWNED: {
99883
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99884
- worker: event.worker,
99885
- teardown: event.teardown ?? undefined,
99886
- currentAssignment: event.assignment
100097
+ actions: import_xstate_development_cjs.assign(({ context, event }) => ({
100098
+ data: { ...context.data, currentAssignment: event.assignment },
100099
+ runtime: {
100100
+ ...context.runtime,
100101
+ worker: event.worker,
100102
+ teardown: event.teardown ?? undefined
100103
+ }
99887
100104
  }))
99888
100105
  }
99889
100106
  }
@@ -99894,10 +100111,13 @@ var init_flow_machine = __esm(() => {
99894
100111
  PR_OPENED: "awaiting-ci",
99895
100112
  WORKER_FAILED: "error",
99896
100113
  WORKER_SPAWNED: {
99897
- actions: import_xstate_development_cjs.assign(({ event }) => ({
99898
- worker: event.worker,
99899
- teardown: event.teardown ?? undefined,
99900
- currentAssignment: event.assignment
100114
+ actions: import_xstate_development_cjs.assign(({ context, event }) => ({
100115
+ data: { ...context.data, currentAssignment: event.assignment },
100116
+ runtime: {
100117
+ ...context.runtime,
100118
+ worker: event.worker,
100119
+ teardown: event.teardown ?? undefined
100120
+ }
99901
100121
  }))
99902
100122
  }
99903
100123
  }
@@ -99906,20 +100126,19 @@ var init_flow_machine = __esm(() => {
99906
100126
  invoke: {
99907
100127
  src: "preemption",
99908
100128
  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 } : {}
100129
+ graceMs: context.data.graceMs,
100130
+ bus: context.runtime.bus,
100131
+ persist: context.runtime.persist,
100132
+ issueId: context.data.issueId,
100133
+ newAssignment: context.data.pendingAssignment,
100134
+ ...context.data.currentAssignment !== undefined ? { from: context.data.currentAssignment.flowId } : {},
100135
+ ...context.runtime.worker !== undefined ? { worker: context.runtime.worker } : {},
100136
+ ...context.runtime.teardown !== undefined ? { teardown: context.runtime.teardown } : {}
99917
100137
  }),
99918
100138
  onDone: {
99919
100139
  actions: import_xstate_development_cjs.assign(({ context }) => ({
99920
- worker: undefined,
99921
- teardown: undefined,
99922
- currentAssignment: context.pendingAssignment
100140
+ data: { ...context.data, currentAssignment: context.data.pendingAssignment },
100141
+ runtime: { ...context.runtime, worker: undefined, teardown: undefined }
99923
100142
  })),
99924
100143
  target: "routing-after-preempt"
99925
100144
  },
@@ -99929,32 +100148,40 @@ var init_flow_machine = __esm(() => {
99929
100148
  "routing-after-preempt": {
99930
100149
  always: [
99931
100150
  {
99932
- guard: ({ context }) => context.pendingAssignment?.flowId === "conflict-fix",
100151
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "conflict-fix",
99933
100152
  target: "conflict-fix"
99934
100153
  },
99935
100154
  {
99936
- guard: ({ context }) => context.pendingAssignment?.flowId === "ci-fix",
100155
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "ci-fix",
99937
100156
  target: "ci-fix"
99938
100157
  },
99939
100158
  {
99940
- guard: ({ context }) => context.pendingAssignment?.flowId === "awaiting-ci",
100159
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "awaiting-ci",
99941
100160
  target: "awaiting-ci"
99942
100161
  },
99943
100162
  {
99944
- guard: ({ context }) => context.pendingAssignment?.flowId === "confirmation",
100163
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "confirmation",
99945
100164
  target: "awaiting"
99946
100165
  },
99947
100166
  {
99948
- guard: ({ context }) => context.pendingAssignment?.flowId === "review-followup",
100167
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "review-followup",
99949
100168
  target: "review"
99950
100169
  },
99951
100170
  {
99952
- guard: ({ context }) => context.pendingAssignment?.flowId === "idle",
100171
+ guard: ({ context }) => context.data.pendingAssignment?.flowId === "idle",
99953
100172
  target: "idle"
99954
100173
  },
99955
100174
  { target: "working" }
99956
100175
  ]
99957
100176
  },
100177
+ quarantined: {
100178
+ on: {
100179
+ PR_PASSED: "done",
100180
+ QUARANTINE_CLEARED: { target: "idle", actions: import_xstate_development_cjs.assign(clearRecovery) },
100181
+ CONFLICT_DETECTED: { actions: import_xstate_development_cjs.assign(refreshReason("conflicting")) },
100182
+ CI_FAILED_DETECTED: { actions: import_xstate_development_cjs.assign(refreshReason("ci_failed")) }
100183
+ }
100184
+ },
99958
100185
  done: {
99959
100186
  type: "final"
99960
100187
  },
@@ -99983,17 +100210,22 @@ class FlowActorStore {
99983
100210
  ...this.deps ? {
99984
100211
  bus: this.deps.bus,
99985
100212
  persist: this.deps.persist,
99986
- ...this.deps.graceMs !== undefined ? { graceMs: this.deps.graceMs } : {}
100213
+ ...this.deps.graceMs !== undefined ? { graceMs: this.deps.graceMs } : {},
100214
+ ...this.deps.maxRecoveryAttempts !== undefined ? { maxRecoveryAttempts: this.deps.maxRecoveryAttempts } : {}
99987
100215
  } : {}
99988
100216
  };
100217
+ const inspector = this.deps?.onTransition ? this.makeInspect(key, changeDir) : null;
99989
100218
  if (changeDir) {
99990
100219
  const snapshot = await this.loadSnapshot(changeDir);
99991
100220
  if (snapshot !== null && this.isValidSnapshot(snapshot)) {
99992
100221
  try {
100222
+ const restored = this.withRestoredRuntime(snapshot);
99993
100223
  const a2 = import_xstate_development_cjs.createActor(this.machine, {
99994
- snapshot,
99995
- input
100224
+ snapshot: restored,
100225
+ input,
100226
+ ...inspector ? { inspect: inspector.inspect } : {}
99996
100227
  });
100228
+ inspector?.setRoot(a2);
99997
100229
  a2.start();
99998
100230
  if (a2.getSnapshot().value !== undefined) {
99999
100231
  this.actors.set(key, a2);
@@ -100005,11 +100237,61 @@ class FlowActorStore {
100005
100237
  } catch {}
100006
100238
  }
100007
100239
  }
100008
- const a = import_xstate_development_cjs.createActor(this.machine, { input });
100240
+ const a = import_xstate_development_cjs.createActor(this.machine, {
100241
+ input,
100242
+ ...inspector ? { inspect: inspector.inspect } : {}
100243
+ });
100244
+ inspector?.setRoot(a);
100009
100245
  a.start();
100010
100246
  this.actors.set(key, a);
100011
100247
  return a;
100012
100248
  }
100249
+ buildRuntime() {
100250
+ return {
100251
+ bus: this.deps?.bus ?? createNoopBus(),
100252
+ persist: this.deps?.persist ?? (() => {}),
100253
+ worker: undefined,
100254
+ teardown: undefined
100255
+ };
100256
+ }
100257
+ withRestoredRuntime(snapshot) {
100258
+ if (!snapshot || typeof snapshot !== "object")
100259
+ return snapshot;
100260
+ const snap = snapshot;
100261
+ const context = snap.context ?? {};
100262
+ const data = context.data && typeof context.data === "object" ? context.data : {
100263
+ issueId: context.issueId,
100264
+ graceMs: context.graceMs ?? 5000,
100265
+ maxRecoveryAttempts: this.deps?.maxRecoveryAttempts ?? 0,
100266
+ currentAssignment: context.currentAssignment,
100267
+ pendingAssignment: context.pendingAssignment,
100268
+ recovery: undefined
100269
+ };
100270
+ return { ...snap, context: { data, runtime: this.buildRuntime() } };
100271
+ }
100272
+ makeInspect(issueId, changeDir) {
100273
+ let root;
100274
+ let previous;
100275
+ const inspect = (event) => {
100276
+ if (event.type !== "@xstate.snapshot" || event.actorRef !== root)
100277
+ return;
100278
+ const value = event.snapshot.value;
100279
+ const to = typeof value === "string" ? value : JSON.stringify(value);
100280
+ const eventType = event.event.type ?? "?";
100281
+ if (previous !== undefined && previous !== to) {
100282
+ try {
100283
+ this.deps?.onTransition?.(issueId, changeDir, { from: previous, event: eventType, to });
100284
+ } catch {}
100285
+ }
100286
+ previous = to;
100287
+ };
100288
+ return {
100289
+ inspect,
100290
+ setRoot: (actor) => {
100291
+ root = actor;
100292
+ }
100293
+ };
100294
+ }
100013
100295
  peekActor(key) {
100014
100296
  return this.actors.get(key) ?? null;
100015
100297
  }
@@ -100065,6 +100347,7 @@ var init_flow_actor_store = __esm(() => {
100065
100347
  init_xstate_development_cjs();
100066
100348
  init_flow_machine();
100067
100349
  init_store();
100350
+ init_src2();
100068
100351
  });
100069
100352
 
100070
100353
  // packages/core/src/machines/loop.machine.ts
@@ -102921,6 +103204,9 @@ function formatTicketError(err) {
102921
103204
  parts.push(`configured team: ${e.team}`);
102922
103205
  return parts.length > 0 ? `${e.message} (${parts.join(", ")})` : e.message;
102923
103206
  }
103207
+ function openBlockersFromInverse(nodes) {
103208
+ 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 }));
103209
+ }
102924
103210
  function partition2(markers) {
102925
103211
  const statuses = [];
102926
103212
  const labels = [];
@@ -102951,6 +103237,39 @@ function applyRequiredLabels(where, requireAllLabels) {
102951
103237
  }
102952
103238
  where.and = and2;
102953
103239
  }
103240
+ function isNegatedMarker(m) {
103241
+ return "negate" in m && Boolean(m.negate);
103242
+ }
103243
+ function applyRequiredProject(where, requireProject) {
103244
+ if (!requireProject)
103245
+ return;
103246
+ const clause = { project: { name: { in: [requireProject] } } };
103247
+ const existing = where.project;
103248
+ if (existing === undefined) {
103249
+ const and3 = where.and;
103250
+ if (and3 !== undefined)
103251
+ and3.push(clause);
103252
+ else
103253
+ where.project = clause.project;
103254
+ return;
103255
+ }
103256
+ const and2 = where.and ?? [];
103257
+ and2.push({ project: existing }, clause);
103258
+ delete where.project;
103259
+ where.and = and2;
103260
+ }
103261
+ function applyGlobalExcludes(where, excludeLabels, excludeProjects) {
103262
+ if (excludeLabels && excludeLabels.length > 0) {
103263
+ const and2 = where.and ?? [];
103264
+ and2.push({ labels: { every: { name: { nin: excludeLabels } } } });
103265
+ where.and = and2;
103266
+ }
103267
+ if (excludeProjects && excludeProjects.length > 0) {
103268
+ const and2 = where.and ?? [];
103269
+ and2.push({ project: { name: { nin: excludeProjects } } });
103270
+ where.and = and2;
103271
+ }
103272
+ }
102954
103273
  function buildIssueFilter(spec) {
102955
103274
  const where = {};
102956
103275
  if (spec.team)
@@ -102969,9 +103288,13 @@ function buildIssueFilter(spec) {
102969
103288
  if (spec.numbers && spec.numbers.length > 0) {
102970
103289
  where.number = { in: spec.numbers };
102971
103290
  }
102972
- const inc = spec.include ?? [];
103291
+ const incAll = spec.include ?? [];
103292
+ const inc = incAll.filter((m) => !isNegatedMarker(m));
103293
+ const negatedInc = incAll.filter(isNegatedMarker);
103294
+ let pinnedStatus = false;
102973
103295
  if (inc.length > 0) {
102974
103296
  const { statuses, labels, attachmentSubtitles, projects } = partition2(inc);
103297
+ pinnedStatus = statuses.length > 0;
102975
103298
  const branches = [];
102976
103299
  if (statuses.length > 0)
102977
103300
  branches.push({ state: { name: { in: statuses } } });
@@ -102991,10 +103314,16 @@ function buildIssueFilter(spec) {
102991
103314
  branches.push({ project: { name: { in: projects } } });
102992
103315
  for (const b of branches)
102993
103316
  Object.assign(where, b);
102994
- } else {
103317
+ }
103318
+ if (!pinnedStatus) {
102995
103319
  where.state = { type: { in: ["unstarted", "started", "backlog"] } };
102996
103320
  }
102997
- const exc = spec.exclude ?? [];
103321
+ const exc = [
103322
+ ...spec.exclude ?? [],
103323
+ ...negatedInc,
103324
+ ...(spec.excludeLabels ?? []).map((value) => ({ type: "label", value })),
103325
+ ...(spec.excludeProjects ?? []).map((value) => ({ type: "project", value }))
103326
+ ];
102998
103327
  if (exc.length > 0) {
102999
103328
  const {
103000
103329
  statuses,
@@ -103050,12 +103379,13 @@ function buildIssueFilter(spec) {
103050
103379
  }
103051
103380
  }
103052
103381
  applyRequiredLabels(where, spec.requireAllLabels);
103382
+ applyRequiredProject(where, spec.requireProject);
103053
103383
  return where;
103054
103384
  }
103055
103385
  function clauseFromMarkers(markers) {
103056
103386
  if (markers.length === 0)
103057
103387
  return null;
103058
- const { statuses, labels, attachmentSubtitles, projects } = partition2(markers);
103388
+ const { statuses, labels, attachmentSubtitles, projects } = partition2(markers.filter((m) => !isNegatedMarker(m)));
103059
103389
  const parts = {};
103060
103390
  if (statuses.length > 0)
103061
103391
  parts.state = { name: { in: statuses } };
@@ -103071,6 +103401,16 @@ function clauseFromMarkers(markers) {
103071
103401
  }
103072
103402
  if (projects.length > 0)
103073
103403
  parts.project = { name: { in: projects } };
103404
+ const negated = partition2(markers.filter(isNegatedMarker));
103405
+ const negClauses = [];
103406
+ if (negated.statuses.length > 0)
103407
+ negClauses.push({ state: { name: { nin: negated.statuses } } });
103408
+ if (negated.labels.length > 0)
103409
+ negClauses.push({ labels: { every: { name: { nin: negated.labels } } } });
103410
+ if (negated.projects.length > 0)
103411
+ negClauses.push({ project: { name: { nin: negated.projects } } });
103412
+ if (negClauses.length > 0)
103413
+ parts.and = negClauses;
103074
103414
  return Object.keys(parts).length > 0 ? parts : null;
103075
103415
  }
103076
103416
  function mapNodeProject(node2) {
@@ -103131,6 +103471,8 @@ async function fetchMentionScanIssues(apiKey, spec) {
103131
103471
  where.number = { in: spec.numbers };
103132
103472
  }
103133
103473
  applyRequiredLabels(where, spec.requireAllLabels);
103474
+ applyRequiredProject(where, spec.requireProject);
103475
+ applyGlobalExcludes(where, spec.excludeLabels, spec.excludeProjects);
103134
103476
  const query = `query MentionScanIssues($filter: IssueFilter) {
103135
103477
  issues(filter: $filter, first: 50) {
103136
103478
  nodes {
@@ -103140,8 +103482,8 @@ async function fetchMentionScanIssues(apiKey, spec) {
103140
103482
  project { id name priority }
103141
103483
  projectMilestone { id name sortOrder targetDate }
103142
103484
  labels { nodes { name } }
103143
- relations(first: 50) {
103144
- nodes { type relatedIssue { id identifier state { type } } }
103485
+ inverseRelations(first: 50) {
103486
+ nodes { type issue { id identifier state { type } } }
103145
103487
  }
103146
103488
  comments(first: 50) {
103147
103489
  nodes { id body createdAt user { name email } }
@@ -103152,24 +103494,26 @@ async function fetchMentionScanIssues(apiKey, spec) {
103152
103494
  const data = await linearRequest(apiKey, query, {
103153
103495
  filter: where
103154
103496
  });
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
- }));
103497
+ return data.issues.nodes.map((n) => {
103498
+ const blockers = openBlockersFromInverse(n.inverseRelations?.nodes);
103499
+ return {
103500
+ id: n.id,
103501
+ identifier: n.identifier,
103502
+ title: n.title,
103503
+ description: n.description,
103504
+ url: n.url,
103505
+ state: n.state,
103506
+ assignee: n.assignee,
103507
+ project: mapNodeProject(n),
103508
+ ...milestoneSpread(n),
103509
+ labels: n.labels.nodes.map((l) => l.name),
103510
+ priority: n.priority,
103511
+ createdAt: n.createdAt ?? "",
103512
+ blockedByIds: blockers.map((b) => b.id),
103513
+ blockedByIdentifiers: blockers.map((b) => b.identifier),
103514
+ comments: n.comments?.nodes ?? []
103515
+ };
103516
+ });
103173
103517
  }
103174
103518
  async function fetchOpenIssues(apiKey, spec, options) {
103175
103519
  const where = buildIssueFilter(spec);
@@ -103186,10 +103530,10 @@ async function fetchOpenIssues(apiKey, spec, options) {
103186
103530
  project { id name priority }
103187
103531
  projectMilestone { id name sortOrder targetDate }
103188
103532
  labels { nodes { name } }
103189
- relations(first: 50) {
103533
+ inverseRelations(first: 50) {
103190
103534
  nodes {
103191
103535
  type
103192
- relatedIssue { id identifier state { type } }
103536
+ issue { id identifier state { type } }
103193
103537
  }
103194
103538
  }
103195
103539
  ${commentsSlice}
@@ -103199,24 +103543,26 @@ async function fetchOpenIssues(apiKey, spec, options) {
103199
103543
  const data = await linearRequest(apiKey, query, {
103200
103544
  filter: where
103201
103545
  });
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
- }));
103546
+ return data.issues.nodes.map((n) => {
103547
+ const blockers = openBlockersFromInverse(n.inverseRelations?.nodes);
103548
+ return {
103549
+ id: n.id,
103550
+ identifier: n.identifier,
103551
+ title: n.title,
103552
+ description: n.description,
103553
+ url: n.url,
103554
+ state: n.state,
103555
+ assignee: n.assignee,
103556
+ project: mapNodeProject(n),
103557
+ ...milestoneSpread(n),
103558
+ labels: n.labels.nodes.map((l) => l.name),
103559
+ priority: n.priority,
103560
+ createdAt: n.createdAt ?? "",
103561
+ blockedByIds: blockers.map((b) => b.id),
103562
+ blockedByIdentifiers: blockers.map((b) => b.identifier),
103563
+ ...includeComments ? { comments: n.comments?.nodes ?? [] } : {}
103564
+ };
103565
+ });
103220
103566
  }
103221
103567
  function isRetryableStatus(status) {
103222
103568
  return status >= 500 && status <= 599;
@@ -103530,17 +103876,15 @@ async function fetchBlockedByForIssues(apiKey, issueIds) {
103530
103876
  issues(filter: { id: { in: $ids } }, first: 250) {
103531
103877
  nodes {
103532
103878
  id
103533
- relations(first: 50) {
103534
- nodes { type relatedIssue { id identifier state { type } } }
103879
+ inverseRelations(first: 50) {
103880
+ nodes { type issue { id identifier state { type } } }
103535
103881
  }
103536
103882
  }
103537
103883
  }
103538
103884
  }`;
103539
103885
  const data = await linearRequest(apiKey, query, { ids: issueIds });
103540
- const DONE_STATE_TYPES = new Set(["completed", "cancelled"]);
103541
103886
  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);
103887
+ out.set(node2.id, openBlockersFromInverse(node2.inverseRelations?.nodes));
103544
103888
  }
103545
103889
  return out;
103546
103890
  }
@@ -103656,7 +104000,7 @@ function issueMatchesGetIndicator(issue2, indicator) {
103656
104000
  const labels = new Set(issue2.labels.map((l) => l.toLowerCase()));
103657
104001
  const stateName = issue2.state.name.toLowerCase();
103658
104002
  const projectName = issue2.project?.name.toLowerCase() ?? null;
103659
- return indicator.filter.some((m) => {
104003
+ const baseMatch = (m) => {
103660
104004
  if (m.type === "label")
103661
104005
  return labels.has(m.value.toLowerCase());
103662
104006
  if (m.type === "status")
@@ -103676,7 +104020,8 @@ function issueMatchesGetIndicator(issue2, indicator) {
103676
104020
  return comments.some((c) => !isRalphComment(c.body) && c.body.toLowerCase().includes(needle));
103677
104021
  }
103678
104022
  return false;
103679
- });
104023
+ };
104024
+ return indicator.filter.some((m) => isNegatedMarker(m) ? !baseMatch(m) : baseMatch(m));
103680
104025
  }
103681
104026
  async function fetchProjectIdByName(apiKey, name) {
103682
104027
  const query = `query ProjectId($name: String!) {
@@ -103754,12 +104099,13 @@ async function removeLabelFromIssue(apiKey, issueId, labelId) {
103754
104099
  labelId
103755
104100
  });
103756
104101
  }
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:";
104102
+ 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
104103
  var init_linear_client = __esm(() => {
103759
104104
  init_types2();
103760
104105
  init_ralph_comment();
103761
104106
  TICKET_IDENTIFIER_RE = /^([A-Za-z]+)-(\d+)(?:-.*)?$/;
103762
104107
  TICKET_BARE_NUMBER_RE = /^(\d+)$/;
104108
+ DONE_BLOCKER_STATE_TYPES = new Set(["completed", "cancelled"]);
103763
104109
  linearRequestInternals = {
103764
104110
  sleep: (ms) => Bun.sleep(ms)
103765
104111
  };
@@ -103866,7 +104212,7 @@ async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
103866
104212
  if (list.stdout.includes(`worktree ${cwd2}
103867
104213
  `)) {
103868
104214
  await installPrePushHook(cwd2, runner);
103869
- return { cwd: cwd2, branch };
104215
+ return { cwd: cwd2, branch, created: false };
103870
104216
  }
103871
104217
  let branchExists = true;
103872
104218
  try {
@@ -103877,12 +104223,12 @@ async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
103877
104223
  if (branchExists) {
103878
104224
  await runner.run(["worktree", "add", cwd2, branch], projectRoot);
103879
104225
  await installPrePushHook(cwd2, runner);
103880
- return { cwd: cwd2, branch };
104226
+ return { cwd: cwd2, branch, created: true };
103881
104227
  }
103882
104228
  await runner.run(["fetch", "origin", baseBranch], projectRoot);
103883
104229
  await runner.run(["worktree", "add", "-b", branch, cwd2, `origin/${baseBranch}`], projectRoot);
103884
104230
  await installPrePushHook(cwd2, runner);
103885
- return { cwd: cwd2, branch };
104231
+ return { cwd: cwd2, branch, created: true };
103886
104232
  }
103887
104233
  async function installPrePushHook(cwd2, runner) {
103888
104234
  const hookPath = join22(cwd2, ".ralph-hooks", "pre-push");
@@ -104188,6 +104534,14 @@ async function branchAlreadyMerged(runner, cwd2, branch, base2) {
104188
104534
  } catch {}
104189
104535
  return false;
104190
104536
  }
104537
+ async function applyPrLabels(runner, cwd2, prRef, labels) {
104538
+ const clean = labels.map((l) => l.trim()).filter(Boolean);
104539
+ if (clean.length === 0 || !prRef)
104540
+ return;
104541
+ try {
104542
+ await runner.run(["gh", "pr", "edit", prRef, "--add-label", clean.join(",")], cwd2);
104543
+ } catch {}
104544
+ }
104191
104545
  async function createPullRequest(input, runner) {
104192
104546
  const base2 = input.base ?? "main";
104193
104547
  const log3 = await runner.run(["git", "log", "--oneline", `${base2}..HEAD`, "--no-merges"], input.cwd);
@@ -104224,8 +104578,10 @@ async function createPullRequest(input, runner) {
104224
104578
  ".[0].url // empty"
104225
104579
  ], input.cwd);
104226
104580
  const existingUrl = existing.stdout.trim();
104227
- if (existingUrl)
104581
+ if (existingUrl) {
104582
+ await applyPrLabels(runner, input.cwd, existingUrl, input.labels ?? []);
104228
104583
  return { url: existingUrl, created: false };
104584
+ }
104229
104585
  const title = defaultTitle(input.issue);
104230
104586
  const body = defaultBody(input.issue, input.branch, input.stackedOn);
104231
104587
  const createArgs = ["gh", "pr", "create", "--base", base2, "--title", title, "--body", body];
@@ -104234,6 +104590,7 @@ async function createPullRequest(input, runner) {
104234
104590
  const created = await runner.run(createArgs, input.cwd);
104235
104591
  const url2 = created.stdout.trim().split(`
104236
104592
  `).pop() ?? "";
104593
+ await applyPrLabels(runner, input.cwd, url2, input.labels ?? []);
104237
104594
  return { url: url2, created: true };
104238
104595
  }
104239
104596
  var init_pr = __esm(() => {
@@ -104787,7 +105144,7 @@ function emitFeatureSkipped(bus, id, reason) {
104787
105144
  var init_run_feature = () => {};
104788
105145
 
104789
105146
  // apps/agent/src/agent/post-task.ts
104790
- import { join as join23, dirname as dirname9 } from "path";
105147
+ import { join as join23 } from "path";
104791
105148
  function summarizeUncommittedStatus(stdout) {
104792
105149
  const lines = stdout.split(`
104793
105150
  `).filter((line) => line.length > 0);
@@ -104914,6 +105271,7 @@ async function createPrWithRetry(ctx, issue2) {
104914
105271
  base: base2,
104915
105272
  metaOnlyFiles: ctx.cfg.metaOnlyFiles ?? [],
104916
105273
  draft: ctx.cfg.prDraft ?? false,
105274
+ labels: ctx.cfg.prLabels ?? [],
104917
105275
  ...ctx.stackedOn ? {
104918
105276
  stackedOn: {
104919
105277
  prUrl: ctx.stackedOn.prUrl,
@@ -105285,17 +105643,6 @@ async function runValidateOnlyPhase(input, deps) {
105285
105643
  await reactivateState(stateFilePath, log3, changeName);
105286
105644
  return respawnWorker();
105287
105645
  }
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
105646
  async function runPostTask(input, deps) {
105300
105647
  const { log: log3, cmd, git: git2, runScript } = deps;
105301
105648
  const emit3 = (phase, detail) => deps.onPhase?.(phase, detail);
@@ -105338,8 +105685,6 @@ async function runPostTask(input, deps) {
105338
105685
  respawnWorker
105339
105686
  });
105340
105687
  emit3(effectiveCode === 0 ? "done" : "gave-up", effectiveCode !== 0 ? `exit ${effectiveCode}` : undefined);
105341
- if (effectiveCode !== 0)
105342
- await recordGaveUp(stateFilePath, log3, changeName);
105343
105688
  await runWorktreeCleanupPhase({ changeName, cwd: cwd2, projectRoot, useWorktree, effectiveCode, cfg }, { git: git2, log: log3, emit: emit3 });
105344
105689
  await runTeardownPhase({ cwd: cwd2, teardownScript: cfg.teardownScript }, { runScript, log: log3, emit: emit3 });
105345
105690
  return effectiveCode;
@@ -105362,7 +105707,6 @@ async function runPostTask(input, deps) {
105362
105707
  if (checked && aheadCount > 0) {
105363
105708
  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
105709
  emit3("gave-up", "unpushed conflict resolution");
105365
- await recordGaveUp(stateFilePath, log3, changeName);
105366
105710
  await runWorktreeCleanupPhase({ changeName, cwd: cwd2, projectRoot, useWorktree, effectiveCode: PR_FAILED_EXIT, cfg }, { git: git2, log: log3, emit: emit3 });
105367
105711
  await runTeardownPhase({ cwd: cwd2, teardownScript: cfg.teardownScript }, { runScript, log: log3, emit: emit3 });
105368
105712
  return PR_FAILED_EXIT;
@@ -105432,8 +105776,6 @@ async function runPostTask(input, deps) {
105432
105776
  }
105433
105777
  const succeeded = effectiveCode === 0 || effectiveCode === NO_CHANGES_EXIT;
105434
105778
  emit3(succeeded ? "done" : "gave-up", succeeded ? undefined : `exit ${effectiveCode}`);
105435
- if (!succeeded)
105436
- await recordGaveUp(stateFilePath, log3, changeName);
105437
105779
  await deps.runRetrospective?.({
105438
105780
  changeName,
105439
105781
  cwd: cwd2,
@@ -105460,7 +105802,6 @@ var PR_FAILED_EXIT = 71, MAX_PR_CREATE_ATTEMPTS = 5, NO_CHANGES_EXIT = 72, repoA
105460
105802
  return { exitCode: proc.exitCode ?? 1, output };
105461
105803
  };
105462
105804
  var init_post_task = __esm(() => {
105463
- init_layout();
105464
105805
  init_tasks_md();
105465
105806
  init_fs_change();
105466
105807
  init_git2();
@@ -105474,6 +105815,309 @@ var init_post_task = __esm(() => {
105474
105815
  repoAutoMergeCache = new Map;
105475
105816
  });
105476
105817
 
105818
+ // apps/agent/src/shared/capabilities/gh-client.ts
105819
+ var init_gh_client = () => {};
105820
+
105821
+ // apps/agent/src/shared/capabilities/github/github-client.ts
105822
+ var STARTED_LABEL_NAMES, ISSUE_FIELDS = "id,number,title,body,state,stateReason,labels,assignees,author,createdAt,url", ISSUE_FIELDS_WITH_COMMENTS;
105823
+ var init_github_client = __esm(() => {
105824
+ init_worktree();
105825
+ init_gh_client();
105826
+ STARTED_LABEL_NAMES = new Set(["in progress", "in-progress", "started"]);
105827
+ ISSUE_FIELDS_WITH_COMMENTS = `${ISSUE_FIELDS},comments`;
105828
+ });
105829
+
105830
+ // apps/agent/src/shared/capabilities/github/identifier-strategy.ts
105831
+ function linearChangeName(issue2) {
105832
+ const slug = issue2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40).replace(/^-+|-+$/g, "");
105833
+ return slug ? `${issue2.identifier.toLowerCase()}-${slug}` : issue2.identifier.toLowerCase();
105834
+ }
105835
+ var linearIdentifierStrategy;
105836
+ var init_identifier_strategy = __esm(() => {
105837
+ init_worktree();
105838
+ init_github_client();
105839
+ linearIdentifierStrategy = {
105840
+ scopeKey: (issue2) => issue2.identifier.split("-")[0],
105841
+ changeName: linearChangeName,
105842
+ branchName: (issue2) => branchForChange(linearChangeName(issue2))
105843
+ };
105844
+ });
105845
+
105846
+ // apps/agent/src/agent/scaffold.ts
105847
+ import { join as join24 } from "path";
105848
+ function changeNameForIssue(issue2) {
105849
+ return linearIdentifierStrategy.changeName(issue2);
105850
+ }
105851
+ async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = [], appendPrompt = "", attachments = []) {
105852
+ const name = changeNameForIssue(issue2);
105853
+ const changeDir = join24(tasksDir, name);
105854
+ const stateDir = join24(statesDir, name);
105855
+ const commentsBlock = comments.length > 0 ? [
105856
+ "",
105857
+ "## Linear comments",
105858
+ "",
105859
+ ...comments.flatMap((c) => [
105860
+ `**${c.user?.name ?? "unknown"}** \u2014 ${c.createdAt}`,
105861
+ "",
105862
+ c.body.trim(),
105863
+ ""
105864
+ ])
105865
+ ] : [];
105866
+ const attachmentsBlock = attachments.length > 0 ? [
105867
+ "",
105868
+ "## Ticket Attachments",
105869
+ "",
105870
+ ...attachments.map((a) => `- [${a.title ?? "Attachment"}](${a.url})`)
105871
+ ] : [];
105872
+ const descriptionBody = issue2.description?.trim() || "_No description provided in Linear._";
105873
+ const proposal = [
105874
+ `# ${issue2.identifier}: ${issue2.title}`,
105875
+ "",
105876
+ `Source: [${issue2.identifier}](${issue2.url})`,
105877
+ `Status: ${issue2.state.name}`,
105878
+ issue2.assignee ? `Assignee: ${issue2.assignee.name}` : "",
105879
+ issue2.labels.length ? `Labels: ${issue2.labels.join(", ")}` : "",
105880
+ "",
105881
+ "## Why",
105882
+ "",
105883
+ descriptionBody,
105884
+ "",
105885
+ "## What Changes",
105886
+ "",
105887
+ "_Describe the concrete changes this proposal introduces (one bullet per change)._",
105888
+ ...commentsBlock,
105889
+ ...attachmentsBlock,
105890
+ ...appendPrompt.trim() ? ["", "## Additional instructions", "", appendPrompt.trim()] : [],
105891
+ "",
105892
+ "## Steering",
105893
+ "",
105894
+ "_Add steering notes here as the loop runs._",
105895
+ ""
105896
+ ].filter((l) => l !== "").join(`
105897
+ `);
105898
+ const tasks = [
105899
+ `# Tasks for ${issue2.identifier}`,
105900
+ "",
105901
+ "## Planning",
105902
+ "",
105903
+ `- [ ] Read the Linear issue at ${issue2.url} and research the codebase to understand the mission and its scope`,
105904
+ `- [ ] Refine proposal.md with the problem statement, approach, and acceptance criteria derived from the research`,
105905
+ `- [ ] Fill in \`## Why\` and \`## What Changes\` in proposal.md so \`openspec validate\` passes (these sections are required by the validator)`,
105906
+ `- [ ] Add at least one spec delta under \`specs/<capability>/spec.md\` describing the behavior added/modified/removed by this change`,
105907
+ `- [ ] 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).`,
105908
+ `- [ ] 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.`,
105909
+ `- [ ] Is there anything else to add? Review the complete change context and document any additional edge cases, constraints, or open questions not captured above.`,
105910
+ ""
105911
+ ].join(`
105912
+ `);
105913
+ const design = [
105914
+ `# Design for ${issue2.identifier}`,
105915
+ "",
105916
+ "_Fill in the technical design as you work through the issue._",
105917
+ ""
105918
+ ].join(`
105919
+ `);
105920
+ await runCapability(fsChange.scaffold, {
105921
+ changeDir,
105922
+ stateDir,
105923
+ proposal,
105924
+ tasks,
105925
+ design
105926
+ });
105927
+ return name;
105928
+ }
105929
+ var init_scaffold = __esm(() => {
105930
+ init_fs_change();
105931
+ init_identifier_strategy();
105932
+ });
105933
+
105934
+ // apps/agent/src/components/task-pipeline.ts
105935
+ function stages(todo, confirmation, work, pr, ci, done) {
105936
+ return [
105937
+ { node: "todo", status: todo },
105938
+ { node: "confirmation", status: confirmation },
105939
+ { node: "work", status: work },
105940
+ { node: "PR", status: pr },
105941
+ { node: "CI", status: ci },
105942
+ { node: "done", status: done }
105943
+ ];
105944
+ }
105945
+ function pipelineStages(row) {
105946
+ const state = row.state;
105947
+ switch (state) {
105948
+ case "todo":
105949
+ return stages("current", "pending", "pending", "pending", "pending", "pending");
105950
+ case "awaiting":
105951
+ return stages("done", "current", "pending", "pending", "pending", "pending");
105952
+ case "queued":
105953
+ case "working":
105954
+ case "in-progress":
105955
+ return stages("done", "done", "current", "pending", "pending", "pending");
105956
+ case "awaiting-ci":
105957
+ return stages("done", "done", "done", "done", "current", "pending");
105958
+ case "conflict-fix":
105959
+ return stages("done", "done", "done", "failed", "pending", "pending");
105960
+ case "ci-fix":
105961
+ return stages("done", "done", "done", "done", "failed", "pending");
105962
+ case "review":
105963
+ return stages("done", "done", "current", "done", "done", "pending");
105964
+ case "quarantined":
105965
+ return row.recovery?.lastReason === "conflicting" ? stages("done", "done", "done", "bailed", "pending", "pending") : stages("done", "done", "done", "done", "bailed", "pending");
105966
+ case "done":
105967
+ return stages("done", "done", "done", "done", "done", "done");
105968
+ case "error":
105969
+ return stages("done", "done", "failed", "pending", "pending", "pending");
105970
+ default: {
105971
+ const exhaustive = state;
105972
+ return exhaustive;
105973
+ }
105974
+ }
105975
+ }
105976
+ function attemptCount(plural) {
105977
+ return `${plural} fix attempt${plural === 1 ? "" : "s"}`;
105978
+ }
105979
+ function statusLabel(row) {
105980
+ const state = row.state;
105981
+ switch (state) {
105982
+ case "todo":
105983
+ return "todo";
105984
+ case "queued":
105985
+ return "queued";
105986
+ case "working":
105987
+ return "working";
105988
+ case "in-progress":
105989
+ return "in progress";
105990
+ case "awaiting":
105991
+ return "awaiting confirmation";
105992
+ case "awaiting-ci":
105993
+ return "awaiting CI";
105994
+ case "conflict-fix":
105995
+ return `conflict \xB7 ${attemptCount(row.recovery?.attempts ?? 0)}`;
105996
+ case "ci-fix":
105997
+ return `CI red \xB7 ${attemptCount(row.recovery?.attempts ?? 0)}`;
105998
+ case "review":
105999
+ return "addressing review";
106000
+ case "quarantined": {
106001
+ const tries = row.recovery?.attempts ?? 0;
106002
+ const reason = row.recovery?.lastReason === "conflicting" ? "conflict" : "CI";
106003
+ return `quarantined \xB7 ${tries} tries (${reason}), bailed`;
106004
+ }
106005
+ case "done":
106006
+ return "done";
106007
+ case "error":
106008
+ return "error";
106009
+ default: {
106010
+ const exhaustive = state;
106011
+ return exhaustive;
106012
+ }
106013
+ }
106014
+ }
106015
+ function orderActiveWorkersFirst(rows, activeWorkerIds) {
106016
+ if (activeWorkerIds.size === 0)
106017
+ return rows.slice();
106018
+ const active = [];
106019
+ const rest2 = [];
106020
+ for (const r of rows)
106021
+ (activeWorkerIds.has(r.id) ? active : rest2).push(r);
106022
+ return [...active, ...rest2];
106023
+ }
106024
+ function buildBoardTree(rows) {
106025
+ const byId = new Map(rows.map((r) => [r.id, r]));
106026
+ const orderIndex = new Map(rows.map((r, i) => [r.id, i]));
106027
+ const blockersOf = (r) => (r.blockedByIds ?? []).filter((id) => id !== r.id && byId.has(id));
106028
+ const childrenOf = new Map;
106029
+ for (const r of rows) {
106030
+ for (const blockerId of blockersOf(r)) {
106031
+ const list = childrenOf.get(blockerId);
106032
+ if (list)
106033
+ list.push(r);
106034
+ else
106035
+ childrenOf.set(blockerId, [r]);
106036
+ }
106037
+ }
106038
+ for (const list of childrenOf.values()) {
106039
+ list.sort((a, b) => orderIndex.get(a.id) - orderIndex.get(b.id));
106040
+ }
106041
+ const emitted = new Set;
106042
+ const depthById = new Map;
106043
+ const result2 = [];
106044
+ const blockerIdentifiersOf = (r) => blockersOf(r).map((id) => byId.get(id).identifier);
106045
+ const tryEmit = (r) => {
106046
+ if (emitted.has(r.id))
106047
+ return;
106048
+ const blockers = blockersOf(r);
106049
+ if (!blockers.every((id) => emitted.has(id)))
106050
+ return;
106051
+ const depth = blockers.length === 0 ? 0 : Math.max(...blockers.map((id) => depthById.get(id))) + 1;
106052
+ depthById.set(r.id, depth);
106053
+ emitted.add(r.id);
106054
+ result2.push({ row: r, depth, blockerIdentifiers: blockerIdentifiersOf(r) });
106055
+ for (const child of childrenOf.get(r.id) ?? [])
106056
+ tryEmit(child);
106057
+ };
106058
+ for (const r of rows) {
106059
+ if (blockersOf(r).length === 0)
106060
+ tryEmit(r);
106061
+ }
106062
+ for (const r of rows) {
106063
+ if (emitted.has(r.id))
106064
+ continue;
106065
+ depthById.set(r.id, 0);
106066
+ emitted.add(r.id);
106067
+ result2.push({ row: r, depth: 0, blockerIdentifiers: blockerIdentifiersOf(r) });
106068
+ for (const child of childrenOf.get(r.id) ?? [])
106069
+ tryEmit(child);
106070
+ }
106071
+ return result2;
106072
+ }
106073
+ function machineStateToTicketState(value) {
106074
+ switch (value) {
106075
+ case "idle":
106076
+ return "in-progress";
106077
+ case "working":
106078
+ return "working";
106079
+ case "conflict-fix":
106080
+ return "conflict-fix";
106081
+ case "ci-fix":
106082
+ return "ci-fix";
106083
+ case "awaiting":
106084
+ return "awaiting";
106085
+ case "awaiting-ci":
106086
+ return "awaiting-ci";
106087
+ case "review":
106088
+ return "review";
106089
+ case "quarantined":
106090
+ return "quarantined";
106091
+ case "preempting":
106092
+ case "routing-after-preempt":
106093
+ return "working";
106094
+ case "done":
106095
+ return "done";
106096
+ case "error":
106097
+ return "error";
106098
+ default:
106099
+ return "working";
106100
+ }
106101
+ }
106102
+ var STATUS_GLYPH, PIPELINE_NODES;
106103
+ var init_task_pipeline = __esm(() => {
106104
+ STATUS_GLYPH = {
106105
+ done: "\u2713",
106106
+ current: "\u25CF",
106107
+ pending: "\u25CB",
106108
+ failed: "\u2717",
106109
+ bailed: "\u26D4"
106110
+ };
106111
+ PIPELINE_NODES = [
106112
+ "todo",
106113
+ "confirmation",
106114
+ "work",
106115
+ "PR",
106116
+ "CI",
106117
+ "done"
106118
+ ];
106119
+ });
106120
+
105477
106121
  // packages/core/src/ordering/hierarchical-order.ts
105478
106122
  function rank(priority) {
105479
106123
  return !priority ? Number.POSITIVE_INFINITY : priority;
@@ -105735,12 +106379,13 @@ var init_queue_order = __esm(() => {
105735
106379
  });
105736
106380
 
105737
106381
  // apps/agent/src/runtime/coordinator.ts
106382
+ import { appendFile as appendFile2 } from "fs/promises";
105738
106383
  function emitCapture(bus, event, properties) {
105739
106384
  capture(event, properties);
105740
106385
  bus.emit({ type: event, ...properties });
105741
106386
  }
105742
106387
  function completionCommentBody(args) {
105743
- const { noChanges, ok, trigger, changeName, code } = args;
106388
+ const { noChanges, ok, trigger, changeName, code, reachedDone } = args;
105744
106389
  if (noChanges) {
105745
106390
  return buildRalphyComment({
105746
106391
  type: "completed-noop",
@@ -105767,6 +106412,23 @@ function completionCommentBody(args) {
105767
106412
  fields: { change: changeName }
105768
106413
  });
105769
106414
  }
106415
+ if (trigger === "ci-fix") {
106416
+ return buildRalphyComment({
106417
+ type: "ci-fix-pushed",
106418
+ action: "pushed a CI fix",
106419
+ 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}\``,
106420
+ fields: { change: changeName }
106421
+ });
106422
+ }
106423
+ if (!reachedDone) {
106424
+ const isReview = trigger === "review";
106425
+ return buildRalphyComment({
106426
+ type: "awaiting-ci",
106427
+ action: isReview ? "addressed review feedback" : "opened a PR",
106428
+ 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}\``,
106429
+ fields: { change: changeName }
106430
+ });
106431
+ }
105770
106432
  return buildRalphyComment({
105771
106433
  type: "completed",
105772
106434
  action: "completed work",
@@ -105799,7 +106461,19 @@ class AgentCoordinator {
105799
106461
  this.opts = opts;
105800
106462
  this.bus = deps.bus ?? createNoopBus();
105801
106463
  const providedMachine = flowMachine.provide({ actors: { preemption: preemptionActorLogic } });
105802
- this.flowStore = new FlowActorStore({ bus: this.bus, persist: () => {} }, providedMachine);
106464
+ this.flowStore = new FlowActorStore({
106465
+ bus: this.bus,
106466
+ persist: () => {},
106467
+ maxRecoveryAttempts: this.opts.prRecovery?.maxRecoverySessions ?? 0,
106468
+ onTransition: (_issueId, changeDir, transition2) => {
106469
+ if (!changeDir)
106470
+ return;
106471
+ const path = `${changeDir}/.ralph-state.flow-history.jsonl`;
106472
+ const line = `${JSON.stringify({ ts: new Date().toISOString(), ...transition2 })}
106473
+ `;
106474
+ appendFile2(path, line).catch(() => {});
106475
+ }
106476
+ }, providedMachine);
105803
106477
  }
105804
106478
  get activeCount() {
105805
106479
  return this.workers.length;
@@ -105871,7 +106545,15 @@ class AgentCoordinator {
105871
106545
  awaiting: awaitingCount
105872
106546
  };
105873
106547
  const found2 = buckets2.todo + buckets2.inProgress + buckets2.mentions + buckets2.awaiting;
105874
- return { found: found2, added: 0, buckets: buckets2, prStatus: emptyPrStatus(), phase: {}, flow: {} };
106548
+ return {
106549
+ found: found2,
106550
+ added: 0,
106551
+ buckets: buckets2,
106552
+ prStatus: emptyPrStatus(),
106553
+ phase: {},
106554
+ flow: {},
106555
+ board: []
106556
+ };
105875
106557
  }
105876
106558
  const maxT = this.opts.maxTickets ?? 0;
105877
106559
  const atTicketLimit = () => {
@@ -105950,7 +106632,7 @@ class AgentCoordinator {
105950
106632
  added += 1;
105951
106633
  this.deps.onLog(` \u21B3 ${issue2.identifier} queued (fresh)`, "gray");
105952
106634
  }
105953
- const prStatus = await this.scanPrMergeStates();
106635
+ const { counts: prStatus, prByIssue } = await this.scanPrMergeStates();
105954
106636
  if (this.queue.length > 0) {
105955
106637
  this.queue = orderQueueEntries(this.queue, this.opts.getAutoMerge);
105956
106638
  }
@@ -105980,7 +106662,103 @@ class AgentCoordinator {
105980
106662
  flow2[w.changeName] = "working";
105981
106663
  }
105982
106664
  }
105983
- return { found, added, buckets, prStatus, phase: {}, flow: flow2 };
106665
+ const board = await this.buildBoard({
106666
+ todo,
106667
+ inProgress,
106668
+ mentions,
106669
+ prByIssue,
106670
+ awaitingIds: awaitingClaimed
106671
+ });
106672
+ return { found, added, buckets, prStatus, phase: {}, flow: flow2, board };
106673
+ }
106674
+ async buildBoard(args) {
106675
+ const { todo, inProgress, mentions, prByIssue, awaitingIds } = args;
106676
+ const order = [
106677
+ ...this.workers.map((w) => ({
106678
+ issue: w.issue,
106679
+ kind: "worker",
106680
+ changeName: w.changeName
106681
+ })),
106682
+ ...this.queue.map((q) => ({
106683
+ issue: q.issue,
106684
+ kind: "queued",
106685
+ changeName: changeNameForIssue(q.issue)
106686
+ })),
106687
+ ...inProgress.map((issue2) => ({
106688
+ issue: issue2,
106689
+ kind: awaitingIds.has(issue2.id) ? "awaiting" : "in-progress",
106690
+ changeName: changeNameForIssue(issue2)
106691
+ })),
106692
+ ...todo.map((issue2) => ({
106693
+ issue: issue2,
106694
+ kind: "todo",
106695
+ changeName: changeNameForIssue(issue2)
106696
+ })),
106697
+ ...mentions.map((m) => ({
106698
+ issue: m.issue,
106699
+ kind: "mention",
106700
+ changeName: changeNameForIssue(m.issue)
106701
+ }))
106702
+ ];
106703
+ const seen = new Set;
106704
+ const rows = [];
106705
+ for (const src of order) {
106706
+ if (seen.has(src.issue.id))
106707
+ continue;
106708
+ seen.add(src.issue.id);
106709
+ const row = await this.resolveBoardRow(src.issue, src.kind, src.changeName, prByIssue);
106710
+ if (row)
106711
+ rows.push(row);
106712
+ }
106713
+ return rows;
106714
+ }
106715
+ async resolveBoardRow(issue2, kind, changeName, prByIssue) {
106716
+ let state;
106717
+ let recovery;
106718
+ if (kind === "todo") {
106719
+ state = "todo";
106720
+ } else if (kind === "mention") {
106721
+ state = "review";
106722
+ } else if (kind === "awaiting") {
106723
+ state = "awaiting";
106724
+ } else if (kind !== "worker" && issue2.blockedByIds.length > 0) {
106725
+ state = "todo";
106726
+ } else {
106727
+ const changeDir = this.deps.getChangeDir?.(issue2) ?? undefined;
106728
+ const actor = await this.flowStore.getActor(issue2.id, changeDir);
106729
+ const snapshot = actor.getSnapshot();
106730
+ state = machineStateToTicketState(snapshot.value);
106731
+ if (state === "done")
106732
+ return null;
106733
+ if (kind === "queued" && (state === "working" || state === "in-progress"))
106734
+ state = "queued";
106735
+ const flowRecovery = snapshot.context.data.recovery;
106736
+ if (flowRecovery) {
106737
+ recovery = {
106738
+ attempts: flowRecovery.attempts,
106739
+ bailed: state === "quarantined",
106740
+ firstFailedAt: flowRecovery.firstFailedAt,
106741
+ lastReason: flowRecovery.lastReason
106742
+ };
106743
+ if (state === "awaiting-ci" || state === "in-progress" || state === "working") {
106744
+ state = flowRecovery.lastReason === "conflicting" ? "conflict-fix" : "ci-fix";
106745
+ }
106746
+ }
106747
+ }
106748
+ const prUrl = prByIssue.get(issue2.id)?.url;
106749
+ return {
106750
+ changeName,
106751
+ id: issue2.id,
106752
+ identifier: issue2.identifier,
106753
+ title: issue2.title,
106754
+ url: issue2.url,
106755
+ priority: issue2.priority,
106756
+ state,
106757
+ blockedByIds: issue2.blockedByIds,
106758
+ blockedByIdentifiers: issue2.blockedByIdentifiers ?? [],
106759
+ ...recovery ? { recovery } : {},
106760
+ ...prUrl ? { prUrl } : {}
106761
+ };
105984
106762
  }
105985
106763
  async walkRegistryForInProgress(inProgress) {
105986
106764
  const claimed = new Map;
@@ -106191,20 +106969,20 @@ class AgentCoordinator {
106191
106969
  }
106192
106970
  async scanPrMergeStates() {
106193
106971
  const counts = emptyPrStatus();
106972
+ const prByIssue = new Map;
106194
106973
  if (!this.opts.prRecovery?.enabled)
106195
- return counts;
106974
+ return { counts, prByIssue };
106196
106975
  let candidates = [];
106197
106976
  try {
106198
106977
  candidates = await this.deps.fetchDoneCandidates();
106199
106978
  } catch (err) {
106200
106979
  this.deps.onLog(`! PR merge-state scan fetch failed: ${err.message}`, "yellow");
106201
- return counts;
106980
+ return { counts, prByIssue };
106202
106981
  }
106203
106982
  if (candidates.length === 0)
106204
- return counts;
106983
+ return { counts, prByIssue };
106205
106984
  const preQueue = this.queue.map((q) => ({ id: q.issue.id, trigger: q.trigger }));
106206
106985
  const preWorkers = this.workers.map((w) => ({ id: w.issueId, trigger: w.trigger }));
106207
- const tracker = this.opts.prTracker;
106208
106986
  for (const issue2 of candidates) {
106209
106987
  if (this.workers.some((w) => w.issueId === issue2.id))
106210
106988
  continue;
@@ -106212,12 +106990,17 @@ class AgentCoordinator {
106212
106990
  continue;
106213
106991
  if (this.queue.some((q) => q.issue.id === issue2.id))
106214
106992
  continue;
106215
- if (tracker?.isBailed(issue2.identifier) && this.errorMarkerCleared(issue2)) {
106216
- await tracker.clear(issue2.identifier).catch(() => {});
106993
+ const changeDir = this.deps.getChangeDir?.(issue2) ?? undefined;
106994
+ const actor = await this.flowStore.getActor(issue2.id, changeDir);
106995
+ const stateValue = () => actor.getSnapshot().value;
106996
+ if (stateValue() === "quarantined" && this.errorMarkerCleared(issue2)) {
106997
+ actor.send({ type: "QUARANTINE_CLEARED" });
106998
+ if (changeDir)
106999
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106217
107000
  this.conflictNotified.delete(issue2.id);
106218
107001
  this.ciFailedNotified.delete(issue2.id);
106219
107002
  this.conflictPromoted.delete(issue2.id);
106220
- this.deps.onLog(` ${issue2.identifier}: pr-tracker bail cleared (ticket back in Todo) \u2014 retrying recovery`, "cyan");
107003
+ this.deps.onLog(` ${issue2.identifier}: quarantine cleared (ralph:error removed) \u2014 retrying recovery`, "cyan");
106221
107004
  }
106222
107005
  let pr;
106223
107006
  try {
@@ -106228,45 +107011,47 @@ class AgentCoordinator {
106228
107011
  }
106229
107012
  if (!pr)
106230
107013
  continue;
107014
+ prByIssue.set(issue2.id, pr);
106231
107015
  if (pr.status === "mergeable")
106232
107016
  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
107017
  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") {
107018
+ const value = stateValue();
107019
+ if (value === "awaiting-ci" || value === "quarantined") {
106244
107020
  if (this.issueInSetDoneState(issue2)) {
107021
+ actor.send({ type: "RECOVERY_CLEARED" });
106245
107022
  actor.send({ type: "PR_PASSED" });
106246
107023
  if (changeDir)
106247
107024
  await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106248
- if (actor.getSnapshot().value === "done") {
107025
+ if (stateValue() === "done")
106249
107026
  this.flowStore.disposeActor(issue2.id);
106250
- }
106251
107027
  } else {
106252
107028
  await this.advancePrToDone(issue2, pr.url, actor, changeDir);
106253
107029
  }
107030
+ } else {
107031
+ actor.send({ type: "RECOVERY_CLEARED" });
107032
+ if (changeDir)
107033
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106254
107034
  }
106255
107035
  continue;
106256
107036
  }
106257
107037
  if (pr.status === "conflicted") {
106258
107038
  if (!this.opts.prRecovery?.fixConflicts)
106259
107039
  continue;
106260
- if (tracker?.isBailed(issue2.identifier)) {
107040
+ if (stateValue() === "quarantined") {
106261
107041
  counts.quarantined += 1;
106262
107042
  continue;
106263
107043
  }
106264
107044
  counts.conflicted += 1;
106265
107045
  if (this.conflictNotified.has(issue2.id))
106266
107046
  continue;
106267
- if (await this.prTrackerBail(issue2, pr.url, "conflicting")) {
107047
+ actor.send({ type: "RESUME_DETECTED" });
107048
+ actor.send({ type: "CONFLICT_DETECTED", at: new Date().toISOString() });
107049
+ if (changeDir)
107050
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
107051
+ if (stateValue() === "quarantined") {
106268
107052
  counts.conflicted -= 1;
106269
107053
  counts.quarantined += 1;
107054
+ await this.quarantineBail(issue2, pr.url, "conflicting", actor);
106270
107055
  continue;
106271
107056
  }
106272
107057
  emitCapture(this.bus, "agent_conflict_detected", { issue_identifier: issue2.identifier });
@@ -106284,9 +107069,6 @@ class AgentCoordinator {
106284
107069
  this.deps.onLog(`! Linear conflict comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106285
107070
  }
106286
107071
  }
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
107072
  this.queue.push({
106291
107073
  issue: issue2,
106292
107074
  trigger: "conflict-fix",
@@ -106297,16 +107079,21 @@ class AgentCoordinator {
106297
107079
  if (pr.status === "ci_failed") {
106298
107080
  if (!this.opts.prRecovery?.fixCi)
106299
107081
  continue;
106300
- if (tracker?.isBailed(issue2.identifier)) {
107082
+ if (stateValue() === "quarantined") {
106301
107083
  counts.quarantined += 1;
106302
107084
  continue;
106303
107085
  }
106304
107086
  counts.ciFailed += 1;
106305
107087
  if (this.ciFailedNotified.has(issue2.id))
106306
107088
  continue;
106307
- if (await this.prTrackerBail(issue2, pr.url, "ci_failed")) {
107089
+ actor.send({ type: "RESUME_DETECTED" });
107090
+ actor.send({ type: "CI_FAILED_DETECTED", at: new Date().toISOString() });
107091
+ if (changeDir)
107092
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
107093
+ if (stateValue() === "quarantined") {
106308
107094
  counts.ciFailed -= 1;
106309
107095
  counts.quarantined += 1;
107096
+ await this.quarantineBail(issue2, pr.url, "ci_failed", actor);
106310
107097
  continue;
106311
107098
  }
106312
107099
  emitCapture(this.bus, "agent_ci_failed_detected", { issue_identifier: issue2.identifier });
@@ -106324,9 +107111,6 @@ class AgentCoordinator {
106324
107111
  this.deps.onLog(`! Linear ci-failed comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106325
107112
  }
106326
107113
  }
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
107114
  this.queue.push({
106331
107115
  issue: issue2,
106332
107116
  trigger: "ci-fix",
@@ -106346,7 +107130,7 @@ class AgentCoordinator {
106346
107130
  else if (w.trigger === "ci-fix")
106347
107131
  counts.ciFailed += 1;
106348
107132
  }
106349
- return counts;
107133
+ return { counts, prByIssue };
106350
107134
  }
106351
107135
  issueInSetDoneState(issue2) {
106352
107136
  const sd = this.opts.setDone;
@@ -106356,6 +107140,7 @@ class AgentCoordinator {
106356
107140
  }
106357
107141
  async advancePrToDone(issue2, prUrl, actor, changeDir) {
106358
107142
  this.deps.onLog(` ${issue2.identifier}: PR ${prUrl} mergeable \u2014 moving to done`, "green");
107143
+ actor.send({ type: "RECOVERY_CLEARED" });
106359
107144
  if (this.opts.setDone) {
106360
107145
  try {
106361
107146
  await this.deps.applyIndicator(issue2, this.opts.setDone);
@@ -106367,6 +107152,8 @@ class AgentCoordinator {
106367
107152
  issue_identifier: issue2.identifier,
106368
107153
  error: err.message
106369
107154
  });
107155
+ if (changeDir)
107156
+ await this.flowStore.persistActor(issue2.id, changeDir).catch(() => {});
106370
107157
  return;
106371
107158
  }
106372
107159
  if (this.opts.setInProgress) {
@@ -106406,48 +107193,34 @@ class AgentCoordinator {
106406
107193
  const have = new Set(issue2.labels.map((l) => l.toLowerCase()));
106407
107194
  return !wantLabels.some((v) => have.has(v));
106408
107195
  }
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
- }
107196
+ async quarantineBail(issue2, prUrl, reason, actor) {
107197
+ const attempts = actor.getSnapshot().context.data.recovery?.attempts ?? 0;
107198
+ this.deps.onLog(` ${issue2.identifier}: quarantined after ${attempts} recovery attempts (${reason}) \u2014 applying setError`, "red");
107199
+ emitCapture(this.bus, "agent_pr_tracker_bailed", {
107200
+ issue_identifier: issue2.identifier,
107201
+ reason,
107202
+ attempts
107203
+ });
107204
+ if (this.opts.setError) {
107205
+ try {
107206
+ await this.deps.applyIndicator(issue2, this.opts.setError);
107207
+ } catch (err) {
107208
+ this.deps.onLog(`! Linear setError failed for ${issue2.identifier}: ${err.message}`, "yellow");
106435
107209
  }
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
- }
107210
+ }
107211
+ if (this.opts.postComments !== false) {
107212
+ const human = reason === "conflicting" ? "merge conflicts" : "failing CI";
107213
+ try {
107214
+ await this.deps.postComment(issue2, buildRalphyComment({
107215
+ type: "recovery-gaveup",
107216
+ action: "gave up auto-recovering PR",
107217
+ 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.`,
107218
+ fields: { pr: extractPrNumber(prUrl) ?? prUrl, attempts }
107219
+ }));
107220
+ } catch (err) {
107221
+ this.deps.onLog(`! Linear bail comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106448
107222
  }
106449
107223
  }
106450
- return true;
106451
107224
  }
106452
107225
  spawnNext() {
106453
107226
  if (this.stopped)
@@ -106762,7 +107535,14 @@ class AgentCoordinator {
106762
107535
  }
106763
107536
  }
106764
107537
  if (this.opts.postComments !== false) {
106765
- const body = completionCommentBody({ noChanges, ok, trigger, changeName, code });
107538
+ const body = completionCommentBody({
107539
+ noChanges,
107540
+ ok,
107541
+ trigger,
107542
+ changeName,
107543
+ code,
107544
+ reachedDone: exitActorState === "done"
107545
+ });
106766
107546
  try {
106767
107547
  await this.deps.postComment(issue2, body);
106768
107548
  this.deps.onLog(` ${issue2.identifier}: posted completion comment`, "gray");
@@ -106856,12 +107636,15 @@ var emptyPrStatus = () => ({
106856
107636
  },
106857
107637
  prStatus: emptyPrStatus(),
106858
107638
  phase: {},
106859
- flow: {}
107639
+ flow: {},
107640
+ board: []
106860
107641
  });
106861
107642
  var init_coordinator = __esm(() => {
106862
107643
  init_types2();
106863
107644
  init_linear_client();
106864
107645
  init_post_task();
107646
+ init_scaffold();
107647
+ init_task_pipeline();
106865
107648
  init_queue_order();
106866
107649
  init_src();
106867
107650
  init_src2();
@@ -106876,122 +107659,6 @@ var init_coordinator2 = __esm(() => {
106876
107659
  init_coordinator();
106877
107660
  });
106878
107661
 
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
107662
  // packages/core/src/detections/tasks.ts
106996
107663
  function hasUnchecked(content) {
106997
107664
  return /^- \[ \]/m.test(content);
@@ -107033,13 +107700,8 @@ function gateActive(inputs) {
107033
107700
  }
107034
107701
 
107035
107702
  // 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
- });
107703
+ function buildMentionAckComment() {
107704
+ return buildRalphyMarker("mention-ack", { status: "handled" });
107043
107705
  }
107044
107706
  var init_mention2 = __esm(() => {
107045
107707
  init_src8();
@@ -107051,7 +107713,7 @@ var init_detections = __esm(() => {
107051
107713
  });
107052
107714
 
107053
107715
  // apps/agent/src/features/confirmation/state.ts
107054
- import { dirname as dirname10, join as join25 } from "path";
107716
+ import { dirname as dirname9, join as join25 } from "path";
107055
107717
  async function readInlineConfirmation(statePath) {
107056
107718
  const f2 = Bun.file(statePath);
107057
107719
  if (!await f2.exists())
@@ -107064,7 +107726,7 @@ async function readInlineConfirmation(statePath) {
107064
107726
  }
107065
107727
  }
107066
107728
  async function readConfirmationState(statePath) {
107067
- const changeDir = dirname10(statePath);
107729
+ const changeDir = dirname9(statePath);
107068
107730
  const sidecar = await readSlotSidecar(changeDir, "confirmation");
107069
107731
  const existing = sidecar ?? await readInlineConfirmation(statePath) ?? null;
107070
107732
  const confirmation = {
@@ -107080,7 +107742,7 @@ async function readConfirmationState(statePath) {
107080
107742
  return { stateObj: {}, confirmation };
107081
107743
  }
107082
107744
  async function writeConfirmationState(statePath, _stateObj, confirmation) {
107083
- await writeSlotField(dirname10(statePath), "confirmation", confirmation);
107745
+ await writeSlotField(dirname9(statePath), "confirmation", confirmation);
107084
107746
  }
107085
107747
  async function restartFromDesign(changeDir, changeName) {
107086
107748
  const designStub = [
@@ -107794,7 +108456,7 @@ function createOpenDraftPr(deps) {
107794
108456
  if (!branch)
107795
108457
  return null;
107796
108458
  const base2 = baseBranchFromLabels(issue2.labels) ?? deps.prBaseBranch;
107797
- const result2 = await create3({ cwd: cwd2, branch, issue: issue2, base: base2, draft: true }, deps.cmdRunner);
108459
+ const result2 = await create3({ cwd: cwd2, branch, issue: issue2, base: base2, draft: true, labels: deps.prLabels ?? [] }, deps.cmdRunner);
107798
108460
  const url2 = result2?.url ?? null;
107799
108461
  if (url2) {
107800
108462
  deps.prByChange.set(changeName, url2);
@@ -107926,7 +108588,7 @@ var init_indicators = __esm(() => {
107926
108588
 
107927
108589
  // apps/agent/src/agent/wire/linear-resolvers.ts
107928
108590
  function createLinearResolvers(input) {
107929
- const { apiKey, team, assignee, anyAssignee, requireAllLabels, diag } = input;
108591
+ const { apiKey, team, assignee, anyAssignee, scope, diag } = input;
107930
108592
  const ticketNumbers = input.ticketNumbers ?? [];
107931
108593
  const stateCache = new Map;
107932
108594
  const labelCache = new Map;
@@ -108061,7 +108723,7 @@ function createLinearResolvers(input) {
108061
108723
  team,
108062
108724
  assignee,
108063
108725
  anyAssignee,
108064
- requireAllLabels,
108726
+ ...scope,
108065
108727
  include,
108066
108728
  exclude: excl,
108067
108729
  ...ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {}
@@ -108084,18 +108746,18 @@ function createLinearResolvers(input) {
108084
108746
  resolveLabelIdForTeam
108085
108747
  };
108086
108748
  }
108087
- function doneCandidateSpec(team, assignee, anyAssignee, requireAllLabels, include, ticketNumbers) {
108749
+ function doneCandidateSpec(team, assignee, anyAssignee, scope, include, ticketNumbers) {
108088
108750
  return {
108089
108751
  team,
108090
108752
  assignee,
108091
108753
  anyAssignee,
108092
- requireAllLabels,
108754
+ ...scope,
108093
108755
  include,
108094
108756
  exclude: [],
108095
108757
  ...ticketNumbers && ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {}
108096
108758
  };
108097
108759
  }
108098
- async function fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requireAllLabels, indicators, ticketNumbers) {
108760
+ async function fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, scope, indicators, ticketNumbers) {
108099
108761
  const getIndicators = [
108100
108762
  indicators.getTodo,
108101
108763
  indicators.getInProgress,
@@ -108114,7 +108776,7 @@ async function fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requ
108114
108776
  const include = ind.filter ?? [];
108115
108777
  if (include.length === 0)
108116
108778
  return;
108117
- const issues = await fetchOpenIssues(apiKey, doneCandidateSpec(team, assignee, anyAssignee, requireAllLabels, include, ticketNumbers));
108779
+ const issues = await fetchOpenIssues(apiKey, doneCandidateSpec(team, assignee, anyAssignee, scope, include, ticketNumbers));
108118
108780
  for (const issue2 of issues) {
108119
108781
  if (!seen.has(issue2.id)) {
108120
108782
  seen.add(issue2.id);
@@ -108295,7 +108957,7 @@ function createLinearTrackerProvider(input) {
108295
108957
  team,
108296
108958
  assignee,
108297
108959
  anyAssignee,
108298
- requireAllLabels,
108960
+ scope,
108299
108961
  indicators,
108300
108962
  resolvers,
108301
108963
  fetchMentions,
@@ -108308,7 +108970,7 @@ function createLinearTrackerProvider(input) {
108308
108970
  fetchInProgress: () => resolvers.fetchByGet(indicators.getInProgress, excludeFromInProgress),
108309
108971
  fetchReview: () => resolvers.fetchByGet(indicators.getReview, []),
108310
108972
  fetchMentions,
108311
- fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requireAllLabels, indicators, ticketNumbers && ticketNumbers.length > 0 ? ticketNumbers : undefined),
108973
+ fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, scope, indicators, ticketNumbers && ticketNumbers.length > 0 ? ticketNumbers : undefined),
108312
108974
  applyIndicator: resolvers.applyIndicator,
108313
108975
  removeIndicator: resolvers.removeIndicator,
108314
108976
  postComment: (issue2, body) => addIssueComment(apiKey, issue2.id, body),
@@ -108360,7 +109022,7 @@ function createPrepareHelpers(input) {
108360
109022
  let scaffoldStatesDir = statesDir;
108361
109023
  let branch = null;
108362
109024
  if (!useWorktree)
108363
- return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
109025
+ return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch, worktreeCreated: null };
108364
109026
  const probeName = worktreeDirNameForIssue(issue2);
108365
109027
  const baseBranch = baseBranchFromLabels(issue2.labels) ?? cfg.prBaseBranch;
108366
109028
  let wt;
@@ -108389,10 +109051,10 @@ function createPrepareHelpers(input) {
108389
109051
  } catch (err) {
108390
109052
  diag("worktree", `! seeding .mcp.json failed for ${issue2.identifier}: ${err.message}`, "yellow");
108391
109053
  }
108392
- return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch };
109054
+ return { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch, worktreeCreated: wt.created };
108393
109055
  }
108394
109056
  async function prepare(issue2) {
108395
- const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch } = await setupWorktree(issue2);
109057
+ const { workerCwd, scaffoldTasksDir, scaffoldStatesDir, branch, worktreeCreated } = await setupWorktree(issue2);
108396
109058
  let changeName;
108397
109059
  const wtLayoutPre = projectLayout(workerCwd);
108398
109060
  const derivedName = changeNameForIssue(issue2);
@@ -108441,7 +109103,8 @@ function createPrepareHelpers(input) {
108441
109103
  maps.issueByChange.set(changeName, issue2);
108442
109104
  if (branch)
108443
109105
  maps.branchByChange.set(changeName, branch);
108444
- if (cfg.setupScript) {
109106
+ const runSetup = worktreeCreated ?? isFresh;
109107
+ if (cfg.setupScript && runSetup) {
108445
109108
  await runScript("setup", cfg.setupScript, workerCwd);
108446
109109
  }
108447
109110
  return {
@@ -108790,6 +109453,18 @@ function createPrDiscovery(input) {
108790
109453
  } catch (err) {
108791
109454
  diag("ci", `! gh pr checks ${prUrl} failed (PR scan): ${err.message}`, "yellow");
108792
109455
  }
109456
+ try {
109457
+ const readiness = await getPollContext().fetchPrOnce(prUrl, ["isDraft", "reviewDecision"], cmdRunner, projectRoot);
109458
+ const isDraft = readiness.isDraft === true;
109459
+ const reviewDecision = readiness.reviewDecision?.toUpperCase();
109460
+ const awaitingApproval = reviewDecision === "REVIEW_REQUIRED" || reviewDecision === "CHANGES_REQUESTED";
109461
+ if (isDraft || awaitingApproval) {
109462
+ 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");
109463
+ return { url: prUrl, status: "unknown" };
109464
+ }
109465
+ } catch (err) {
109466
+ diag("pr", `! gh pr view ${prUrl} readiness check failed (PR scan): ${err.message} \u2014 treating as ready`, "yellow");
109467
+ }
108793
109468
  return { url: prUrl, status: "mergeable" };
108794
109469
  }
108795
109470
  async function resolvePrUrlForIssue(issue2) {
@@ -108829,17 +109504,17 @@ var init_pr_discovery = __esm(() => {
108829
109504
  });
108830
109505
 
108831
109506
  // apps/agent/src/features/review-followup/scan.ts
108832
- import { dirname as dirname11, join as join28 } from "path";
109507
+ import { dirname as dirname10, join as join28 } from "path";
108833
109508
  async function resolveReviewStateDir(changeName, deps) {
108834
109509
  const root = deps.cwdOf(changeName);
108835
109510
  if (root)
108836
- return dirname11(projectLayout(root).stateFile(changeName));
109511
+ return dirname10(projectLayout(root).stateFile(changeName));
108837
109512
  if (!deps.useWorktree)
108838
- return dirname11(projectLayout(deps.projectRoot).stateFile(changeName));
109513
+ return dirname10(projectLayout(deps.projectRoot).stateFile(changeName));
108839
109514
  const wtPath = join28(worktreesDir2(deps.projectRoot), changeName);
108840
109515
  const statePath = projectLayout(wtPath).stateFile(changeName);
108841
109516
  if (await Bun.file(statePath).exists())
108842
- return dirname11(statePath);
109517
+ return dirname10(statePath);
108843
109518
  return null;
108844
109519
  }
108845
109520
  async function readReviewWatermark(stateDir) {
@@ -109086,7 +109761,7 @@ function createMentionScanner(input) {
109086
109761
  team,
109087
109762
  assignee,
109088
109763
  anyAssignee,
109089
- requireAllLabels,
109764
+ scope,
109090
109765
  indicators,
109091
109766
  projectRoot,
109092
109767
  useWorktree,
@@ -109111,7 +109786,7 @@ function createMentionScanner(input) {
109111
109786
  team,
109112
109787
  assignee,
109113
109788
  anyAssignee,
109114
- ...requireAllLabels && requireAllLabels.length > 0 ? { requireAllLabels } : {},
109789
+ ...scope,
109115
109790
  ...ticketNumbers && ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {},
109116
109791
  indicators: {
109117
109792
  ...indicators.getTodo !== undefined ? { getTodo: indicators.getTodo } : {},
@@ -109172,7 +109847,7 @@ function createMentionScanner(input) {
109172
109847
  }
109173
109848
  if (cfg.linear.postComments !== false) {
109174
109849
  try {
109175
- await createIssueComment(apiKey, issue2.id, buildMentionAckComment(c.body, c.user?.name));
109850
+ await createIssueComment(apiKey, issue2.id, buildMentionAckComment());
109176
109851
  } catch (err) {
109177
109852
  diag("mention", `! mention scan: ack comment failed for ${issue2.identifier}: ${formatLinearError(err)}`, "yellow");
109178
109853
  }
@@ -109221,7 +109896,7 @@ function createMentionScanner(input) {
109221
109896
  }
109222
109897
  }
109223
109898
  if (cfg.linear.postComments !== false) {
109224
- await postGithubPrComment(cmdRunner, projectRoot, prUrl, buildMentionAckComment(c.body, c.author), onLog);
109899
+ await postGithubPrComment(cmdRunner, projectRoot, prUrl, buildMentionAckComment(), onLog);
109225
109900
  }
109226
109901
  }
109227
109902
  queued.add(issue2.id);
@@ -109636,6 +110311,7 @@ function buildPostTaskInput(input) {
109636
110311
  finalizeNoOpAsDone: cfg.finalizeNoOpAsDone,
109637
110312
  manualMergeWhenAutoMergeDisabled: cfg.manualMergeWhenAutoMergeDisabled,
109638
110313
  prDraft: cfg.prDraft,
110314
+ prLabels: cfg.prLabels,
109639
110315
  validateCommands: [cfg.commands.test, cfg.commands.lint, cfg.commands.typecheck].filter((c) => Boolean(c))
109640
110316
  },
109641
110317
  respawnWorker: input.respawnWorker
@@ -110226,7 +110902,7 @@ var init_linear_sync = __esm(() => {
110226
110902
  });
110227
110903
 
110228
110904
  // apps/agent/src/agent/linear-sync/comment-sync.ts
110229
- import { dirname as dirname12, join as join34 } from "path";
110905
+ import { dirname as dirname11, join as join34 } from "path";
110230
110906
  async function readInlineLinearComments(statePath) {
110231
110907
  const file2 = Bun.file(statePath);
110232
110908
  if (!await file2.exists())
@@ -110239,7 +110915,7 @@ async function readInlineLinearComments(statePath) {
110239
110915
  }
110240
110916
  }
110241
110917
  async function readComments(statePath) {
110242
- const changeDir = dirname12(statePath);
110918
+ const changeDir = dirname11(statePath);
110243
110919
  const raw = await readSlotSidecar(changeDir, "linearComments") ?? await readInlineLinearComments(statePath) ?? {};
110244
110920
  const r = raw;
110245
110921
  return {
@@ -110252,7 +110928,7 @@ async function readComments(statePath) {
110252
110928
  async function patchComments(statePath, patch) {
110253
110929
  const current = await readComments(statePath);
110254
110930
  const next = { ...current, ...patch };
110255
- await writeSlotField(dirname12(statePath), "linearComments", next);
110931
+ await writeSlotField(dirname11(statePath), "linearComments", next);
110256
110932
  }
110257
110933
  function isCommentNotFoundError(err) {
110258
110934
  if (!err)
@@ -262362,22 +263038,20 @@ function renderCodeBlock(doc2, token, indent) {
262362
263038
  const text = token.text ?? "";
262363
263039
  const x2 = MARGIN + indent;
262364
263040
  const width = doc2.page.width - 2 * MARGIN - indent;
263041
+ const textWidth = width - 2 * CODE_PADDING_X;
262365
263042
  doc2.font(FONT_MONO).fontSize(CODE_SIZE).fillColor(COLOR_TEXT);
262366
- const lineHeight = doc2.currentLineHeight(true);
262367
263043
  const lines = text.split(/\r?\n/);
262368
263044
  doc2.y += CODE_PADDING_Y / 2;
262369
263045
  for (const line of lines) {
262370
- if (doc2.y + lineHeight + CODE_PADDING_Y > doc2.page.height - MARGIN) {
263046
+ const safe = toPdfSafe(line.length > 0 ? line : " ");
263047
+ const lineHeight = doc2.heightOfString(safe, { width: textWidth, lineBreak: true });
263048
+ if (doc2.y + lineHeight + CODE_PADDING_Y > doc2.page.height - MARGIN && doc2.y > doc2.page.margins.top) {
262371
263049
  doc2.addPage();
262372
263050
  }
262373
263051
  const yTop = doc2.y;
262374
- const safe = toPdfSafe(line);
262375
263052
  doc2.rect(x2, yTop, width, lineHeight).fill(COLOR_CODE_BG);
262376
263053
  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
- });
263054
+ doc2.text(safe, x2 + CODE_PADDING_X, yTop, { width: textWidth, lineBreak: true });
262381
263055
  doc2.y = yTop + lineHeight;
262382
263056
  }
262383
263057
  doc2.y += CODE_PADDING_Y / 2;
@@ -262465,38 +263139,85 @@ function flattenInline(tokens, flags = {}) {
262465
263139
  }
262466
263140
  return out;
262467
263141
  }
263142
+ function applyInlineStyle(doc2, run) {
263143
+ if (run.code) {
263144
+ doc2.font(FONT_MONO).fontSize(BODY_SIZE).fillColor(COLOR_INLINE_CODE_FG);
263145
+ return;
263146
+ }
263147
+ doc2.fontSize(BODY_SIZE).fillColor(COLOR_TEXT);
263148
+ if (run.bold && run.italic)
263149
+ doc2.font(FONT_BOLD_ITALIC);
263150
+ else if (run.bold)
263151
+ doc2.font(FONT_BOLD);
263152
+ else if (run.italic)
263153
+ doc2.font(FONT_ITALIC);
263154
+ else
263155
+ doc2.font(FONT_BODY);
263156
+ }
263157
+ function atomizeInline(flat) {
263158
+ const atoms = [];
263159
+ for (const run of flat) {
263160
+ const safe = toPdfSafe(run.text);
263161
+ for (const part of safe.split(/(\n|[ \t]+)/)) {
263162
+ if (part === "")
263163
+ continue;
263164
+ if (part === `
263165
+ `)
263166
+ atoms.push({ text: "", run, space: false, br: true });
263167
+ else if (/^[ \t]+$/.test(part))
263168
+ atoms.push({ text: part, run, space: true, br: false });
263169
+ else
263170
+ atoms.push({ text: part, run, space: false, br: false });
263171
+ }
263172
+ }
263173
+ return atoms;
263174
+ }
262468
263175
  function emitInline(doc2, tokens, x2, width) {
262469
263176
  const flat = flattenInline(tokens);
262470
263177
  if (flat.length === 0)
262471
263178
  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);
263179
+ const atoms = atomizeInline(flat);
263180
+ if (atoms.length === 0)
263181
+ return;
263182
+ doc2.font(FONT_BODY).fontSize(BODY_SIZE);
263183
+ const lineHeight = doc2.currentLineHeight(true);
263184
+ const right = x2 + width;
263185
+ const bottom = doc2.page.height - MARGIN;
263186
+ let cursorX = x2;
263187
+ let cursorY = doc2.y;
263188
+ let pendingSpace = 0;
263189
+ const newLine = () => {
263190
+ cursorX = x2;
263191
+ cursorY += lineHeight;
263192
+ pendingSpace = 0;
263193
+ if (cursorY + lineHeight > bottom) {
263194
+ doc2.addPage();
263195
+ cursorY = doc2.page.margins.top;
262492
263196
  }
262493
- const safe = toPdfSafe(run.text);
262494
- if (i === 0) {
262495
- doc2.text(safe, x2, doc2.y, opts);
263197
+ };
263198
+ for (const atom of atoms) {
263199
+ if (atom.br) {
263200
+ newLine();
263201
+ continue;
263202
+ }
263203
+ applyInlineStyle(doc2, atom.run);
263204
+ if (atom.space) {
263205
+ if (cursorX > x2)
263206
+ pendingSpace += doc2.widthOfString(atom.text);
263207
+ continue;
263208
+ }
263209
+ const wordWidth = doc2.widthOfString(atom.text);
263210
+ if (cursorX > x2 && cursorX + pendingSpace + wordWidth > right + 0.01) {
263211
+ newLine();
262496
263212
  } else {
262497
- doc2.text(safe, opts);
263213
+ cursorX += pendingSpace;
263214
+ pendingSpace = 0;
262498
263215
  }
263216
+ applyInlineStyle(doc2, atom.run);
263217
+ doc2.text(atom.text, cursorX, cursorY, { lineBreak: false, continued: false });
263218
+ cursorX += wordWidth;
262499
263219
  }
263220
+ doc2.y = cursorY + lineHeight;
262500
263221
  doc2.font(FONT_BODY).fontSize(BODY_SIZE).fillColor(COLOR_TEXT);
262501
263222
  }
262502
263223
  function plainInline(tokens) {
@@ -262676,7 +263397,7 @@ var init_render_pdf = __esm(() => {
262676
263397
  });
262677
263398
 
262678
263399
  // apps/agent/src/agent/linear-sync/spec-attachments.ts
262679
- import { dirname as dirname13, join as join35 } from "path";
263400
+ import { dirname as dirname12, join as join35 } from "path";
262680
263401
  function describeLinearError(err) {
262681
263402
  const e = err;
262682
263403
  const parts = [e.message ?? String(err)];
@@ -262691,7 +263412,7 @@ function describeLinearError(err) {
262691
263412
  return parts.join(" ");
262692
263413
  }
262693
263414
  function stateDirOf(statePath) {
262694
- return dirname13(statePath);
263415
+ return dirname12(statePath);
262695
263416
  }
262696
263417
  async function readInlineSpecAttachments(statePath) {
262697
263418
  const file2 = Bun.file(statePath);
@@ -262709,7 +263430,7 @@ async function readInlineSpecAttachments(statePath) {
262709
263430
  }
262710
263431
  }
262711
263432
  async function readSpecAttachmentsSubtree(statePath) {
262712
- const sidecar = await readSlotSidecar(dirname13(statePath), "specAttachments");
263433
+ const sidecar = await readSlotSidecar(dirname12(statePath), "specAttachments");
262713
263434
  return sidecar ?? await readInlineSpecAttachments(statePath);
262714
263435
  }
262715
263436
  function asRevisions(value) {
@@ -263201,105 +263922,8 @@ var init_comment_sync2 = __esm(() => {
263201
263922
  init_linear();
263202
263923
  });
263203
263924
 
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
263925
  // apps/agent/src/agent/wire.ts
263302
- import { join as join37 } from "path";
263926
+ import { join as join36 } from "path";
263303
263927
  function buildAgentCoordinator(input) {
263304
263928
  const {
263305
263929
  args,
@@ -263318,7 +263942,7 @@ function buildAgentCoordinator(input) {
263318
263942
  onWorkerCmd,
263319
263943
  onAwaitingTicket
263320
263944
  } = input;
263321
- const logsDir = join37(projectRoot, ".ralph", "logs");
263945
+ const logsDir = join36(projectRoot, ".ralph", "logs");
263322
263946
  const bus = createBus();
263323
263947
  subscribeAgentDiag(bus, onLog);
263324
263948
  const diag = (area, message, color) => {
@@ -263329,7 +263953,9 @@ function buildAgentCoordinator(input) {
263329
263953
  const isGithubTracker = cfg.tracker.kind === "github";
263330
263954
  const indicators = isGithubTracker ? githubIndicators(cfg.github?.issues) : mergeIndicators(cfg.linear.indicators, args.indicators);
263331
263955
  const team = args.linearTeam || cfg.linear.team;
263332
- const { assignee, anyAssignee, requireAllLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, args.linearAssignee));
263956
+ const resolvedFilter = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, args.linearAssignee));
263957
+ const { assignee, anyAssignee, requireAllLabels } = resolvedFilter;
263958
+ const scope = linearFilterScope(resolvedFilter);
263333
263959
  const ticketNumbers = resolveTicketNumbers(args.ticketTokens, team);
263334
263960
  const excludeFromTodo = isGithubTracker ? unionMarkers(indicators.setDone, indicators.setError, indicators.setInProgress) : unionMarkers(indicators.setDone, indicators.setError);
263335
263961
  const gitRunner = input.runners?.git ?? bunGitRunner;
@@ -263371,7 +263997,7 @@ function buildAgentCoordinator(input) {
263371
263997
  team,
263372
263998
  assignee,
263373
263999
  anyAssignee,
263374
- requireAllLabels,
264000
+ scope,
263375
264001
  diag,
263376
264002
  ...ticketNumbers.length > 0 ? { ticketNumbers } : {}
263377
264003
  });
@@ -263382,7 +264008,7 @@ function buildAgentCoordinator(input) {
263382
264008
  diag
263383
264009
  }) : {
263384
264010
  ...resolvers,
263385
- fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, requireAllLabels, indicators, ticketNumbers.length > 0 ? ticketNumbers : undefined)
264011
+ fetchDoneCandidates: () => fetchDoneCandidatesWith(apiKey, team, assignee, anyAssignee, scope, indicators, ticketNumbers.length > 0 ? ticketNumbers : undefined)
263386
264012
  };
263387
264013
  if (ticketNumbers.length > 0) {
263388
264014
  const hasGetIndicator = [indicators.getTodo, indicators.getInProgress].some((ind) => ind && ind.filter.length > 0);
@@ -263421,7 +264047,7 @@ function buildAgentCoordinator(input) {
263421
264047
  team,
263422
264048
  assignee,
263423
264049
  anyAssignee,
263424
- requireAllLabels,
264050
+ scope,
263425
264051
  indicators,
263426
264052
  projectRoot,
263427
264053
  useWorktree,
@@ -263449,7 +264075,7 @@ function buildAgentCoordinator(input) {
263449
264075
  team,
263450
264076
  assignee,
263451
264077
  anyAssignee,
263452
- requireAllLabels,
264078
+ scope,
263453
264079
  indicators,
263454
264080
  resolvers,
263455
264081
  fetchMentions,
@@ -263498,6 +264124,7 @@ function buildAgentCoordinator(input) {
263498
264124
  prByChange,
263499
264125
  cmdRunner,
263500
264126
  prBaseBranch: cfg.prBaseBranch,
264127
+ prLabels: cfg.prLabels,
263501
264128
  invalidatePrUrlForIssue: (issueId) => prDiscovery.invalidatePrUrlForIssue(issueId)
263502
264129
  });
263503
264130
  const confirmationCaps = {
@@ -263537,10 +264164,6 @@ function buildAgentCoordinator(input) {
263537
264164
  };
263538
264165
  }
263539
264166
  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
264167
  const commentSync = isGithubTracker ? { enabled: false } : createCommentSyncHooks({
263545
264168
  apiKey,
263546
264169
  cfg,
@@ -263586,7 +264209,7 @@ function buildAgentCoordinator(input) {
263586
264209
  const changeDir = projectLayout(root).changeDir(changeName);
263587
264210
  const parts = [];
263588
264211
  for (const name of ["tasks.md", "proposal.md", "design.md"]) {
263589
- const file2 = Bun.file(join37(changeDir, name));
264212
+ const file2 = Bun.file(join36(changeDir, name));
263590
264213
  if (!await file2.exists())
263591
264214
  continue;
263592
264215
  parts.push(`${name}:${file2.lastModified}:${file2.size}`);
@@ -263605,11 +264228,11 @@ function buildAgentCoordinator(input) {
263605
264228
  commentEveryIterations: cfg.linear.updateEveryIterations,
263606
264229
  ...args.maxTickets > 0 ? { maxTickets: args.maxTickets } : {},
263607
264230
  createsPrs: args.createPr || cfg.createPrOnSuccess,
263608
- ...prTracker ? { prTracker } : {},
263609
264231
  prRecovery: {
263610
264232
  enabled: prRecoveryEnabled,
263611
264233
  fixCi: cfg.prRecovery.fixCi,
263612
- fixConflicts: cfg.prRecovery.fixConflicts
264234
+ fixConflicts: cfg.prRecovery.fixConflicts,
264235
+ maxRecoverySessions: cfg.prRecovery.maxRecoverySessions
263613
264236
  }
263614
264237
  });
263615
264238
  coordRef.current = coord;
@@ -263633,19 +264256,7 @@ function buildAgentCoordinator(input) {
263633
264256
  pollInterval,
263634
264257
  getWorkerCwd: (changeName) => cwdByChange.get(changeName),
263635
264258
  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
- }
264259
+ runBaselineGate: runBaselineGateOnce
263649
264260
  };
263650
264261
  }
263651
264262
  var init_wire = __esm(() => {
@@ -263669,18 +264280,17 @@ var init_wire = __esm(() => {
263669
264280
  init_worker();
263670
264281
  init_baseline();
263671
264282
  init_comment_sync2();
263672
- init_pr_tracker();
263673
264283
  });
263674
264284
 
263675
264285
  // 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";
264286
+ import { mkdir as mkdir11, appendFile as appendFile3 } from "fs/promises";
264287
+ import { dirname as dirname13 } from "path";
263678
264288
  function createJsonLogFileSink(path) {
263679
264289
  if (!path)
263680
264290
  return { emit: () => {} };
263681
264291
  let chain = (async () => {
263682
264292
  try {
263683
- await mkdir11(dirname14(path), { recursive: true });
264293
+ await mkdir11(dirname13(path), { recursive: true });
263684
264294
  await Bun.write(path, "");
263685
264295
  } catch {}
263686
264296
  })();
@@ -263690,7 +264300,7 @@ function createJsonLogFileSink(path) {
263690
264300
  `;
263691
264301
  chain = chain.then(async () => {
263692
264302
  try {
263693
- await appendFile2(path, line);
264303
+ await appendFile3(path, line);
263694
264304
  } catch {}
263695
264305
  });
263696
264306
  }
@@ -263926,7 +264536,7 @@ var init_output_utils = __esm(() => {
263926
264536
  });
263927
264537
 
263928
264538
  // apps/agent/src/agent/state/worker-state-poll.ts
263929
- import { join as join38 } from "path";
264539
+ import { join as join37 } from "path";
263930
264540
  function parseSubtasks(tasksMd) {
263931
264541
  const out = [];
263932
264542
  let skipSection = false;
@@ -263959,7 +264569,7 @@ function initialWorkerSnapshot() {
263959
264569
  async function readWorkerSnapshot(input) {
263960
264570
  const next = { ...input.prev };
263961
264571
  try {
263962
- const file2 = Bun.file(join38(input.statesDir, input.changeName, ".ralph-state.json"));
264572
+ const file2 = Bun.file(join37(input.statesDir, input.changeName, ".ralph-state.json"));
263963
264573
  if (await file2.exists()) {
263964
264574
  const json2 = await file2.json();
263965
264575
  next.iter = json2.iteration ?? next.iter;
@@ -263968,10 +264578,10 @@ async function readWorkerSnapshot(input) {
263968
264578
  } catch {}
263969
264579
  if (input.changeDir) {
263970
264580
  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"));
264581
+ const tasksFile = Bun.file(join37(input.changeDir, "tasks.md"));
264582
+ const proposalFile = Bun.file(join37(input.changeDir, "proposal.md"));
264583
+ const designFile = Bun.file(join37(input.changeDir, "design.md"));
264584
+ const reviewFindingsFile = Bun.file(join37(input.changeDir, "review-findings.md"));
263975
264585
  const [tasksText, proposalText, designText, reviewFindingsText] = await Promise.all([
263976
264586
  tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
263977
264587
  proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
@@ -264034,7 +264644,7 @@ var init_worker_state_poll = __esm(() => {
264034
264644
  });
264035
264645
 
264036
264646
  // apps/agent/src/components/AgentMode.tsx
264037
- import { join as join39 } from "path";
264647
+ import { join as join38 } from "path";
264038
264648
  async function appendSteeringImpl(changeDir, message) {
264039
264649
  await runWithContext(createDefaultContext(), async () => {
264040
264650
  appendSteeringMessage(changeDir, message);
@@ -264051,16 +264661,6 @@ function orderSubtasksForCappedDisplay(subtasks) {
264051
264661
  (s.done ? done : pending).push(s);
264052
264662
  return [...pending, ...done];
264053
264663
  }
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
264664
  function fmtCmd(argv) {
264065
264665
  const joined = argv.join(" ");
264066
264666
  return joined.length > CMD_DISPLAY_MAX ? joined.slice(0, CMD_DISPLAY_MAX - 1) + "\u2026" : joined;
@@ -264156,20 +264756,6 @@ function Link({ url: url2, label, color }) {
264156
264756
  }, undefined, false, undefined, this)
264157
264757
  }, undefined, false, undefined, this);
264158
264758
  }
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
264759
  function modeBadge(mode) {
264174
264760
  switch (mode) {
264175
264761
  case "fresh":
@@ -264246,14 +264832,52 @@ function workerBorderColor(phase2) {
264246
264832
  return "gray";
264247
264833
  }
264248
264834
  }
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;
264835
+ function focusedCardTailLines(termHeight, fixedOverhead) {
264836
+ return Math.max(3, termHeight - fixedOverhead);
264837
+ }
264838
+ function glyphColor(status) {
264839
+ switch (status) {
264840
+ case "done":
264841
+ return "green";
264842
+ case "current":
264843
+ return "cyan";
264844
+ case "pending":
264845
+ return "gray";
264846
+ case "failed":
264847
+ return "red";
264848
+ case "bailed":
264849
+ return "magenta";
264850
+ }
264851
+ }
264852
+ function PipelineCells({
264853
+ glyphs
264854
+ }) {
264855
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264856
+ children: PIPELINE_NODES.map((node2, i) => {
264857
+ const isHeader = glyphs === null;
264858
+ const status = isHeader ? null : glyphs[i];
264859
+ const content = isHeader ? NODE_LABELS[node2] : STATUS_GLYPH[status];
264860
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264861
+ children: [
264862
+ i > 0 && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264863
+ dimColor: true,
264864
+ children: PIPELINE_CONNECTOR
264865
+ }, undefined, false, undefined, this),
264866
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264867
+ width: NODE_CELL_WIDTH,
264868
+ justifyContent: "center",
264869
+ children: isHeader ? /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264870
+ dimColor: true,
264871
+ children: content
264872
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264873
+ color: glyphColor(status),
264874
+ children: content
264875
+ }, undefined, false, undefined, this)
264876
+ }, undefined, false, undefined, this)
264877
+ ]
264878
+ }, node2, true, undefined, this);
264879
+ })
264880
+ }, undefined, false, undefined, this);
264257
264881
  }
264258
264882
  function AgentMode({
264259
264883
  args,
@@ -264287,24 +264911,19 @@ function AgentMode({
264287
264911
  }, [awaitingClose]);
264288
264912
  const [, setTick] = import_react63.useState(0);
264289
264913
  const [clock, setClock] = import_react63.useState(0);
264290
- const [focusedIdx, setFocusedIdx] = import_react63.useState(0);
264914
+ const [focusedId, setFocusedId] = import_react63.useState(null);
264291
264915
  const [showPendingTasks, setShowPendingTasks] = import_react63.useState(false);
264292
264916
  const [showAllSubtasks, setShowAllSubtasks] = import_react63.useState(false);
264293
- const [gaveUpCount, setGaveUpCount] = import_react63.useState(0);
264294
264917
  const coordRef = import_react63.useRef(null);
264295
264918
  const workerMetaRef = import_react63.useRef(new Map);
264296
- const gatedTicketsRef = import_react63.useRef(new Map);
264297
264919
  const nextPollAtRef = import_react63.useRef(0);
264298
264920
  const cfgRef = import_react63.useRef(null);
264299
264921
  const [effective, setEffective] = import_react63.useState(null);
264300
264922
  const [pollStatus, setPollStatus] = import_react63.useState({
264301
264923
  state: "idle",
264302
- lastFound: null,
264303
- lastAdded: null,
264304
264924
  lastAt: null,
264305
264925
  filterDesc: "",
264306
- lastBuckets: null,
264307
- lastPrStatus: null
264926
+ lastBoard: []
264308
264927
  });
264309
264928
  function appendLog(text, color, workerLogFile) {
264310
264929
  setLogs((prev) => [...prev, { id: nextId(), text, color }]);
@@ -264340,7 +264959,7 @@ function AgentMode({
264340
264959
  setFatalExit(2);
264341
264960
  return;
264342
264961
  }
264343
- const { coord: coord2, filterDesc, concurrency, pollInterval, runBaselineGate: runBaselineGate2, getGaveUpTotal } = buildCoordinator({
264962
+ const { coord: coord2, filterDesc, concurrency, pollInterval, runBaselineGate: runBaselineGate2 } = buildCoordinator({
264344
264963
  args,
264345
264964
  cfg: cfg2,
264346
264965
  projectRoot,
@@ -264447,13 +265066,6 @@ function AgentMode({
264447
265066
  since: info.since,
264448
265067
  round: info.round
264449
265068
  });
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
265069
  }
264458
265070
  });
264459
265071
  setEffective({ concurrency, pollInterval });
@@ -264481,28 +265093,18 @@ function AgentMode({
264481
265093
  }
264482
265094
  if (cancelled)
264483
265095
  return;
264484
- gatedTicketsRef.current.clear();
264485
- const { found, added, buckets, prStatus } = await coord2.pollOnce();
265096
+ const { found, added, buckets, prStatus, board: board2 } = await coord2.pollOnce();
264486
265097
  if (cancelled)
264487
265098
  return;
264488
265099
  fileEmit({ type: "poll_done", found, added, buckets, prStatus });
264489
- getGaveUpTotal().then((total) => {
264490
- if (!cancelled)
264491
- setGaveUpCount(total);
264492
- }).catch(() => {
264493
- return;
264494
- });
264495
265100
  if (added > 0) {
264496
265101
  appendLog(` ${added} new issue${added === 1 ? "" : "s"} queued (found ${found} open)`);
264497
265102
  }
264498
265103
  setPollStatus({
264499
265104
  state: "idle",
264500
- lastFound: found,
264501
- lastAdded: added,
264502
265105
  lastAt: Date.now(),
264503
265106
  filterDesc,
264504
- lastBuckets: buckets,
264505
- lastPrStatus: prStatus
265107
+ lastBoard: board2
264506
265108
  });
264507
265109
  nextPollAtRef.current = Date.now() + pollInterval * 1000;
264508
265110
  pollTimer = setTimeout(tick, pollInterval * 1000);
@@ -264617,10 +265219,22 @@ function AgentMode({
264617
265219
  const spinnerFrame = SPINNER_FRAMES[clock % SPINNER_FRAMES.length];
264618
265220
  const now2 = Date.now();
264619
265221
  const secsToNextPoll = nextPollAtRef.current ? Math.max(0, Math.ceil((nextPollAtRef.current - now2) / 1000)) : null;
265222
+ const pollState = pollStatus.state === "polling" ? "polling\u2026" : pollStatus.lastAt !== null ? "idle" : "starting\u2026";
265223
+ const tasksLiveness = `${pollState}${secsToNextPoll !== null ? ` \xB7 ${secsToNextPoll}s \u21BB` : ""}`;
264620
265224
  const activeCount = coord?.activeCount ?? 0;
264621
265225
  const termWidth = columns - 2;
264622
265226
  const termHeight = rows;
264623
- const safeFocusedIdx = activeCount > 0 ? Math.min(focusedIdx, activeCount - 1) : 0;
265227
+ const board = pollStatus.lastBoard;
265228
+ const liveWorkerIds = new Set(coordRef.current?.activeWorkers.map((w2) => w2.issueId) ?? []);
265229
+ const tree = buildBoardTree(orderActiveWorkersFirst(board, liveWorkerIds));
265230
+ const focusedIndex = (() => {
265231
+ if (tree.length === 0)
265232
+ return -1;
265233
+ const i = tree.findIndex((t) => t.row.id === focusedId);
265234
+ return i >= 0 ? i : 0;
265235
+ })();
265236
+ const focusedRow = focusedIndex >= 0 ? tree[focusedIndex].row : undefined;
265237
+ const focusedWorker = focusedRow ? coordRef.current?.activeWorkers.find((w2) => w2.issueId === focusedRow.id) : undefined;
264624
265238
  const steeringFocusedRef = import_react63.useRef(false);
264625
265239
  const steeringBufferRef = import_react63.useRef("");
264626
265240
  const steeringCursorRef = import_react63.useRef(0);
@@ -264638,26 +265252,23 @@ function AgentMode({
264638
265252
  setShowPendingTasks((v2) => !v2);
264639
265253
  return;
264640
265254
  }
264641
- if (activeCount === 0)
265255
+ if (tree.length === 0)
264642
265256
  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);
265257
+ const idx = focusedIndex < 0 ? 0 : focusedIndex;
265258
+ if (key.tab || key.downArrow) {
265259
+ setFocusedId(tree[(idx + 1) % tree.length].row.id);
265260
+ } else if (key.upArrow) {
265261
+ setFocusedId(tree[(idx - 1 + tree.length) % tree.length].row.id);
264647
265262
  } else {
264648
265263
  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;
265264
+ if (!isNaN(n) && n >= 1 && n <= Math.min(9, tree.length))
265265
+ setFocusedId(tree[n - 1].row.id);
265266
+ }
265267
+ }, { isActive: isRawModeSupported && board.length > 0 });
265268
+ const steeringActive = isRawModeSupported && focusedWorker !== undefined;
264657
265269
  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);
265270
+ const FIXED_OVERHEAD = 5 + (4 + board.length) + 8 + steeringBoxLines;
265271
+ const focusedTailLines = focusedCardTailLines(termHeight, FIXED_OVERHEAD);
264661
265272
  if (preflightError) {
264662
265273
  return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
264663
265274
  flexDirection: "column",
@@ -264811,14 +265422,7 @@ function AgentMode({
264811
265422
  cfg.useWorktree && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
264812
265423
  color: "green",
264813
265424
  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)
265425
+ }, undefined, false, undefined, this)
264822
265426
  ]
264823
265427
  }, undefined, true, undefined, this)
264824
265428
  ]
@@ -264845,418 +265449,196 @@ function AgentMode({
264845
265449
  })()
264846
265450
  ]
264847
265451
  }, 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
265452
  (() => {
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, {
265453
+ const tasksInnerWidth = Math.max(0, termWidth - 2);
265454
+ const lead = "\u2500 ";
265455
+ const hint = " Tab/\u2191\u2193\xB71-9 ";
265456
+ const live = ` ${tasksLiveness} `;
265457
+ const trail = "\u2500";
265458
+ const fixed = lead.length + "TASKS".length + hint.length + live.length + trail.length;
265459
+ const fill2 = Math.max(1, tasksInnerWidth - fixed);
265460
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
265461
+ labelVisualWidth: tasksInnerWidth,
265462
+ labelNode: /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265463
+ flexDirection: "row",
265133
265464
  children: [
265134
265465
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265135
- color: "yellow",
265136
- children: " "
265466
+ color: "gray",
265467
+ children: lead
265137
265468
  }, 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
265469
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265154
- color: "yellow",
265155
265470
  bold: true,
265156
- children: "[GATE]"
265157
- }, undefined, false, undefined, this),
265158
- /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265159
- color: "yellow",
265160
- children: "Awaiting confirmation"
265471
+ children: "TASKS"
265161
265472
  }, undefined, false, undefined, this),
265162
265473
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265163
265474
  dimColor: true,
265164
- children: "\xB7"
265475
+ children: hint
265165
265476
  }, undefined, false, undefined, this),
265166
265477
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265167
- color: "white",
265168
- bold: true,
265169
- children: gated.size
265478
+ color: "gray",
265479
+ children: "\u2500".repeat(fill2)
265170
265480
  }, undefined, false, undefined, this),
265171
265481
  /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265172
265482
  dimColor: true,
265173
- children: "tickets"
265483
+ children: live
265484
+ }, undefined, false, undefined, this),
265485
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265486
+ color: "gray",
265487
+ children: trail
265174
265488
  }, undefined, false, undefined, this)
265175
265489
  ]
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,
265490
+ }, undefined, true, undefined, this),
265491
+ borderColor: "gray",
265207
265492
  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);
265493
+ paddingX: 1,
265494
+ flexDirection: "column",
265495
+ children: board.length === 0 ? /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265496
+ dimColor: true,
265497
+ children: "no active tickets"
265498
+ }, undefined, false, undefined, this) : (() => {
265499
+ const idColWidth = Math.max(8, ...tree.map((t) => t.depth * 2 + t.row.identifier.length));
265500
+ const idxWidth = String(tree.length).length + 3;
265501
+ const prefixWidth = 2 + idxWidth + idColWidth + 1;
265502
+ const advancing = activeCount > 0 || tree.some((t) => ADVANCING_STATES.has(t.row.state));
265503
+ const hasStartableTodo = tree.some((t) => t.row.state === "todo" && !(t.row.blockedByIds?.length ?? 0));
265504
+ const stalled = !advancing && !hasStartableTodo;
265505
+ const blockedCount = tree.filter((t) => t.row.state === "todo" && (t.row.blockedByIds?.length ?? 0) > 0).length;
265506
+ const awaitingCount = tree.filter((t) => t.row.state === "awaiting").length;
265507
+ const quarantinedCount = tree.filter((t) => t.row.state === "quarantined").length;
265508
+ const stallParts = [
265509
+ blockedCount > 0 ? `${blockedCount} blocked` : null,
265510
+ awaitingCount > 0 ? `${awaitingCount} awaiting confirmation` : null,
265511
+ quarantinedCount > 0 ? `${quarantinedCount} quarantined` : null
265512
+ ].filter((p) => p !== null);
265513
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265514
+ children: [
265515
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265516
+ children: [
265517
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265518
+ children: " ".repeat(prefixWidth)
265519
+ }, undefined, false, undefined, this),
265520
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(PipelineCells, {
265521
+ glyphs: null
265522
+ }, undefined, false, undefined, this)
265523
+ ]
265524
+ }, undefined, true, undefined, this),
265525
+ tree.map(({ row, depth }, i) => {
265526
+ const isFocused = row.id === focusedRow?.id;
265527
+ const indent = depth > 0 ? " ".repeat(depth - 1) + "\u2514 " : "";
265528
+ const blockers = row.blockedByIdentifiers ?? [];
265529
+ const activeW = coordRef.current?.activeWorkers.find((w2) => w2.issueId === row.id);
265530
+ const meta3 = activeW ? workerMetaRef.current.get(activeW.changeName) : undefined;
265531
+ const waitingForWorker = !activeW && WORKER_WAIT_STATES.has(row.state);
265532
+ let age = "\u2013";
265533
+ if (meta3?.startedAt) {
265534
+ age = fmtElapsed(now2 - meta3.startedAt);
265535
+ } else if (!waitingForWorker && row.recovery?.firstFailedAt) {
265536
+ const failedAt = Date.parse(row.recovery.firstFailedAt);
265537
+ if (!Number.isNaN(failedAt))
265538
+ age = fmtElapsed(now2 - failedAt);
265539
+ }
265540
+ const prUrl = meta3?.prUrl ?? row.prUrl ?? null;
265541
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265542
+ children: [
265543
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265544
+ width: 2,
265545
+ children: /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265546
+ color: "white",
265547
+ bold: true,
265548
+ children: isFocused ? "\u25B6" : " "
265549
+ }, undefined, false, undefined, this)
265550
+ }, undefined, false, undefined, this),
265551
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265552
+ width: idxWidth,
265553
+ children: /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265554
+ dimColor: !isFocused,
265555
+ children: [
265556
+ "[",
265557
+ i + 1,
265558
+ "]"
265559
+ ]
265560
+ }, undefined, true, undefined, this)
265561
+ }, undefined, false, undefined, this),
265562
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265563
+ width: idColWidth + 1,
265564
+ children: [
265565
+ indent && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265566
+ dimColor: true,
265567
+ children: indent
265568
+ }, undefined, false, undefined, this),
265569
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265570
+ url: row.url,
265571
+ label: row.identifier,
265572
+ color: isFocused ? "cyan" : "gray"
265573
+ }, undefined, false, undefined, this)
265574
+ ]
265575
+ }, undefined, true, undefined, this),
265576
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(PipelineCells, {
265577
+ glyphs: pipelineStages(row).map((s) => s.status)
265578
+ }, undefined, false, undefined, this),
265579
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265580
+ color: isFocused ? "white" : "gray",
265581
+ dimColor: !isFocused,
265582
+ children: [
265583
+ " ",
265584
+ statusLabel(row)
265585
+ ]
265586
+ }, undefined, true, undefined, this),
265587
+ blockers.length > 0 && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265588
+ color: "yellow",
265589
+ dimColor: !isFocused,
265590
+ children: [
265591
+ " \u26D3 ",
265592
+ trunc(blockers.join(", "), 28)
265593
+ ]
265594
+ }, undefined, true, undefined, this),
265595
+ waitingForWorker ? /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265596
+ color: "yellow",
265597
+ children: " waiting for worker"
265598
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265599
+ dimColor: true,
265600
+ children: [
265601
+ " ",
265602
+ age
265603
+ ]
265604
+ }, undefined, true, undefined, this),
265605
+ prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265606
+ children: [
265607
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265608
+ dimColor: true,
265609
+ children: " \u2197"
265610
+ }, undefined, false, undefined, this),
265611
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
265612
+ url: prUrl,
265613
+ label: prLabel(prUrl),
265614
+ color: "green"
265615
+ }, undefined, false, undefined, this)
265616
+ ]
265617
+ }, undefined, true, undefined, this)
265618
+ ]
265619
+ }, row.id, true, undefined, this);
265620
+ }),
265621
+ stalled && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265622
+ marginTop: 1,
265623
+ children: [
265624
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265625
+ color: "yellow",
265626
+ bold: true,
265627
+ children: "\u23F8 nothing can start"
265628
+ }, undefined, false, undefined, this),
265629
+ stallParts.length > 0 && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265630
+ color: "yellow",
265631
+ children: ` \u2014 ${stallParts.join(" \xB7 ")}`
265632
+ }, undefined, false, undefined, this)
265633
+ ]
265634
+ }, undefined, true, undefined, this)
265635
+ ]
265636
+ }, undefined, true, undefined, this);
265637
+ })()
265638
+ }, undefined, false, undefined, this);
265257
265639
  })(),
265258
- coord?.activeWorkers.map((w2, idx) => {
265259
- const isFocused = idx === safeFocusedIdx;
265640
+ focusedWorker && (() => {
265641
+ const w2 = focusedWorker;
265260
265642
  const meta3 = workerMetaRef.current.get(w2.changeName);
265261
265643
  const elapsed = meta3 ? fmtElapsed(now2 - meta3.startedAt) : "\u2013";
265262
265644
  const iter = meta3?.iter ?? 0;
@@ -265270,122 +265652,10 @@ function AgentMode({
265270
265652
  const taskProgress = meta3?.taskProgress ?? null;
265271
265653
  const openspecPhase = meta3?.openspecPhase ?? null;
265272
265654
  const subtasks = meta3?.subtasks ?? [];
265273
- const pBadge = priorityBadge(w2.issue.priority);
265274
265655
  const mBadge = modeBadge(w2.trigger);
265275
265656
  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
- }
265657
+ const bColor = workerBorderColor(phase2);
265658
+ const visibleTailLines = focusedTailLines;
265389
265659
  const cardLabelWidth = (prUrl ? prLabel(prUrl).length + 3 : 0) + w2.issueIdentifier.length + 2;
265390
265660
  const cardLabelNode = /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265391
265661
  children: [
@@ -265610,7 +265880,7 @@ function AgentMode({
265610
265880
  }, undefined, false, undefined, this)
265611
265881
  ]
265612
265882
  }, undefined, true, undefined, this),
265613
- steeringActive && idx === safeFocusedIdx && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265883
+ steeringActive && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
265614
265884
  marginTop: 0,
265615
265885
  children: /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(SteeringField, {
265616
265886
  active: steeringActive,
@@ -265628,8 +265898,12 @@ function AgentMode({
265628
265898
  },
265629
265899
  onSubmit: async (message) => {
265630
265900
  try {
265631
- await appendSteering2(join39(tasksDir, w2.changeName), message);
265632
- fileEmit({ type: "steering_submitted", changeName: w2.changeName, message });
265901
+ await appendSteering2(join38(tasksDir, w2.changeName), message);
265902
+ fileEmit({
265903
+ type: "steering_submitted",
265904
+ changeName: w2.changeName,
265905
+ message
265906
+ });
265633
265907
  } catch (err) {
265634
265908
  const text = err.message;
265635
265909
  fileEmit({
@@ -265705,7 +265979,108 @@ function AgentMode({
265705
265979
  })()
265706
265980
  ]
265707
265981
  }, w2.changeName, true, undefined, this);
265708
- })
265982
+ })(),
265983
+ !focusedWorker && focusedRow && (() => {
265984
+ const row = focusedRow;
265985
+ let age = "\u2013";
265986
+ if (row.recovery?.firstFailedAt) {
265987
+ const failedAt = Date.parse(row.recovery.firstFailedAt);
265988
+ if (!Number.isNaN(failedAt))
265989
+ age = fmtElapsed(now2 - failedAt);
265990
+ }
265991
+ const prUrl = row.prUrl ?? null;
265992
+ const cardLabelWidth = (prUrl ? prLabel(prUrl).length + 3 : 0) + row.identifier.length + 2;
265993
+ const cardLabelNode = /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
265994
+ children: [
265995
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
265996
+ color: "gray",
265997
+ children: " "
265998
+ }, undefined, false, undefined, this),
265999
+ prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
266000
+ url: prUrl,
266001
+ label: prLabel(prUrl),
266002
+ color: "green"
266003
+ }, undefined, false, undefined, this),
266004
+ prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
266005
+ color: "gray",
266006
+ children: " \xB7 "
266007
+ }, undefined, false, undefined, this),
266008
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
266009
+ url: row.url,
266010
+ label: row.identifier,
266011
+ color: "cyan"
266012
+ }, undefined, false, undefined, this),
266013
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
266014
+ color: "gray",
266015
+ children: " "
266016
+ }, undefined, false, undefined, this)
266017
+ ]
266018
+ }, undefined, true, undefined, this);
266019
+ return /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(LabeledBox, {
266020
+ labelNode: cardLabelNode,
266021
+ labelVisualWidth: cardLabelWidth,
266022
+ borderColor: "gray",
266023
+ flexDirection: "column",
266024
+ paddingX: 1,
266025
+ width: termWidth,
266026
+ children: [
266027
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
266028
+ color: "white",
266029
+ bold: true,
266030
+ children: trunc(row.title, Math.max(20, termWidth - 20))
266031
+ }, undefined, false, undefined, this),
266032
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
266033
+ marginTop: 0,
266034
+ children: [
266035
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(PipelineCells, {
266036
+ glyphs: pipelineStages(row).map((s) => s.status)
266037
+ }, undefined, false, undefined, this),
266038
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
266039
+ color: "white",
266040
+ children: [
266041
+ " ",
266042
+ statusLabel(row)
266043
+ ]
266044
+ }, undefined, true, undefined, this)
266045
+ ]
266046
+ }, undefined, true, undefined, this),
266047
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Box_default, {
266048
+ gap: 2,
266049
+ marginTop: 0,
266050
+ children: [
266051
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
266052
+ dimColor: true,
266053
+ children: "parked \xB7 no live worker"
266054
+ }, undefined, false, undefined, this),
266055
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
266056
+ dimColor: true,
266057
+ children: "\u2502"
266058
+ }, undefined, false, undefined, this),
266059
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
266060
+ dimColor: true,
266061
+ children: [
266062
+ "age ",
266063
+ age
266064
+ ]
266065
+ }, undefined, true, undefined, this),
266066
+ prUrl && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(jsx_dev_runtime11.Fragment, {
266067
+ children: [
266068
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
266069
+ dimColor: true,
266070
+ children: "\u2502"
266071
+ }, undefined, false, undefined, this),
266072
+ /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Link, {
266073
+ url: prUrl,
266074
+ label: prLabel(prUrl),
266075
+ color: "green"
266076
+ }, undefined, false, undefined, this)
266077
+ ]
266078
+ }, undefined, true, undefined, this)
266079
+ ]
266080
+ }, undefined, true, undefined, this)
266081
+ ]
266082
+ }, row.id, true, undefined, this);
266083
+ })()
265709
266084
  ]
265710
266085
  }, undefined, true, undefined, this),
265711
266086
  awaitingClose && /* @__PURE__ */ jsx_dev_runtime11.jsxDEV(Text, {
@@ -265715,13 +266090,14 @@ function AgentMode({
265715
266090
  ]
265716
266091
  }, resizeKey, true, undefined, this);
265717
266092
  }
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;
266093
+ 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
266094
  var init_AgentMode = __esm(async () => {
265720
266095
  init_cli2();
265721
266096
  init_config();
265722
266097
  init_wire();
265723
266098
  init_preflight();
265724
266099
  init_json_log_file();
266100
+ init_task_pipeline();
265725
266101
  init_phase();
265726
266102
  init_log();
265727
266103
  init_useTerminalSize();
@@ -265737,7 +266113,32 @@ var init_AgentMode = __esm(async () => {
265737
266113
  import_react63 = __toESM(require_react(), 1);
265738
266114
  jsx_dev_runtime11 = __toESM(require_jsx_dev_runtime(), 1);
265739
266115
  SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
266116
+ WORKER_WAIT_STATES = new Set([
266117
+ "queued",
266118
+ "working",
266119
+ "in-progress",
266120
+ "conflict-fix",
266121
+ "ci-fix",
266122
+ "review"
266123
+ ]);
266124
+ ADVANCING_STATES = new Set([
266125
+ "queued",
266126
+ "working",
266127
+ "in-progress",
266128
+ "conflict-fix",
266129
+ "ci-fix",
266130
+ "review",
266131
+ "awaiting-ci"
266132
+ ]);
265740
266133
  HYPERLINKS_SUPPORTED = !process.env["TMUX"];
266134
+ NODE_LABELS = {
266135
+ todo: "todo",
266136
+ confirmation: "conf",
266137
+ work: "work",
266138
+ PR: "PR",
266139
+ CI: "CI",
266140
+ done: "done"
266141
+ };
265741
266142
  SESSION_START = new Date().toISOString();
265742
266143
  });
265743
266144
 
@@ -265937,7 +266338,7 @@ __export(exports_list, {
265937
266338
  buildBuckets: () => buildBuckets,
265938
266339
  backlogRankByIssueId: () => backlogRankByIssueId
265939
266340
  });
265940
- import { join as join40 } from "path";
266341
+ import { join as join39 } from "path";
265941
266342
  function countTaskItems(content) {
265942
266343
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
265943
266344
  const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
@@ -265953,13 +266354,13 @@ function buildLocalRows() {
265953
266354
  const sources = [{ dir: statesDir, label: "main" }];
265954
266355
  const worktreesRoot = worktreesDir2(projectRoot);
265955
266356
  for (const wt of storage.list(worktreesRoot)) {
265956
- sources.push({ dir: join40(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
266357
+ sources.push({ dir: join39(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
265957
266358
  }
265958
266359
  for (const { dir, label } of sources) {
265959
266360
  for (const entry of storage.list(dir)) {
265960
266361
  if (seen.has(entry))
265961
266362
  continue;
265962
- const raw = storage.read(join40(dir, entry, ".ralph-state.json"));
266363
+ const raw = storage.read(join39(dir, entry, ".ralph-state.json"));
265963
266364
  if (raw === null)
265964
266365
  continue;
265965
266366
  let state;
@@ -265974,7 +266375,7 @@ function buildLocalRows() {
265974
266375
  const firstLine = promptRaw.split(`
265975
266376
  `).find((l3) => l3.trim() !== "") ?? "";
265976
266377
  let progress = "\u2014";
265977
- const tasksContent = storage.read(join40(dir, entry, "tasks.md"));
266378
+ const tasksContent = storage.read(join39(dir, entry, "tasks.md"));
265978
266379
  if (tasksContent !== null) {
265979
266380
  const { checked, unchecked } = countTaskItems(tasksContent);
265980
266381
  const total = checked + unchecked;
@@ -266037,14 +266438,14 @@ function buildBuckets(indicators) {
266037
266438
  { label: "auto-merge", indicator: indicators.getAutoMerge, exclude: [] }
266038
266439
  ];
266039
266440
  }
266040
- async function fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, requireAllLabels, ticketNumbers) {
266441
+ async function fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, scope, ticketNumbers) {
266041
266442
  if (!bucket.indicator || bucket.indicator.filter.length === 0)
266042
266443
  return [];
266043
266444
  const spec = {
266044
266445
  team,
266045
266446
  assignee,
266046
266447
  anyAssignee,
266047
- requireAllLabels,
266448
+ ...scope,
266048
266449
  include: bucket.indicator.filter,
266049
266450
  exclude: bucket.exclude,
266050
266451
  ...ticketNumbers.length > 0 ? { numbers: ticketNumbers } : {}
@@ -266097,13 +266498,13 @@ function backlogRankByIssueId(issues) {
266097
266498
  ordered.forEach((o, i) => rankById.set(o.id, i));
266098
266499
  return rankById;
266099
266500
  }
266100
- async function fetchAndPrintLinear(apiKey, buckets, team, assignee, anyAssignee, requireAllLabels, cwd2, runner, ignoreCiChecks = [], checks3 = false, review = false, ticketNumbers = []) {
266501
+ async function fetchAndPrintLinear(apiKey, buckets, team, assignee, anyAssignee, scope, cwd2, runner, ignoreCiChecks = [], checks3 = false, review = false, ticketNumbers = []) {
266101
266502
  const bucketResults = await Promise.all(buckets.map(async (bucket) => {
266102
266503
  if (!bucket.indicator || bucket.indicator.filter.length === 0) {
266103
266504
  return { bucket, issues: [], error: null };
266104
266505
  }
266105
266506
  try {
266106
- const issues = await fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, requireAllLabels, ticketNumbers);
266507
+ const issues = await fetchBucketIssues(apiKey, bucket, team, assignee, anyAssignee, scope, ticketNumbers);
266107
266508
  return { bucket, issues, error: null };
266108
266509
  } catch (err) {
266109
266510
  return {
@@ -266239,7 +266640,9 @@ async function runList(input) {
266239
266640
  const apiKey = process.env["LINEAR_API_KEY"];
266240
266641
  const indicators = cfg.linear.indicators;
266241
266642
  const team = input.linearTeamOverride || cfg.linear.team;
266242
- const { assignee, anyAssignee, requireAllLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, input.linearAssigneeOverride));
266643
+ const resolved = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, input.linearAssigneeOverride));
266644
+ const { assignee, anyAssignee } = resolved;
266645
+ const scope = linearFilterScope(resolved);
266243
266646
  const buckets = buildBuckets(indicators);
266244
266647
  const anyConfigured = buckets.some((b2) => b2.indicator && b2.indicator.filter.length > 0);
266245
266648
  if (!anyConfigured) {
@@ -266279,7 +266682,7 @@ team: ${team}
266279
266682
  if (ticketNumbers.length > 0)
266280
266683
  process.stdout.write(`ticket: ${ticketNumbers.join(", ")}
266281
266684
  `);
266282
- await fetchAndPrintLinear(apiKey, buckets, team, assignee, anyAssignee, requireAllLabels, projectRoot, localCmdRunner, cfg.prRecovery.ignoreChecks, input.checks, input.review, ticketNumbers);
266685
+ await fetchAndPrintLinear(apiKey, buckets, team, assignee, anyAssignee, scope, projectRoot, localCmdRunner, cfg.prRecovery.ignoreChecks, input.checks, input.review, ticketNumbers);
266283
266686
  }
266284
266687
  function normalizeIdentifier(input) {
266285
266688
  let parsed;
@@ -266307,8 +266710,8 @@ async function fetchIssueByIdentifier(apiKey, identifier) {
266307
266710
  team { key }
266308
266711
  labels { nodes { name } }
266309
266712
  attachments(first: 25) { nodes { title subtitle } }
266310
- relations(first: 50) {
266311
- nodes { type relatedIssue { id identifier state { type } } }
266713
+ inverseRelations(first: 50) {
266714
+ nodes { type issue { id identifier state { type } } }
266312
266715
  }
266313
266716
  }
266314
266717
  }
@@ -266362,7 +266765,7 @@ async function runListDebug(input) {
266362
266765
  const cfg = await loadRalphyConfig(projectRoot, getArgs().workflowFile);
266363
266766
  const indicators = cfg.linear.indicators;
266364
266767
  const team = input.linearTeamOverride || cfg.linear.team;
266365
- const { assignee, anyAssignee, requireAllLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, input.linearAssigneeOverride));
266768
+ const { assignee, anyAssignee, requireAllLabels, excludeLabels } = resolveLinearFilter(applyAssigneeOverride(cfg.linear.filter, input.linearAssigneeOverride));
266366
266769
  const assigneeLabel = anyAssignee ? "any" : assignee ?? "*";
266367
266770
  const normalized = normalizeIdentifier(identifier);
266368
266771
  if (!normalized) {
@@ -266387,7 +266790,7 @@ Found ${issue2.identifier} \u2014 "${issue2.title}"
266387
266790
  ` + ` assignee: ${issue2.assignee ? `${issue2.assignee.name} <${issue2.assignee.email ?? "no-email"}>` : "(unassigned)"}
266388
266791
  ` + ` labels: ${issue2.labels.nodes.map((l3) => l3.name).join(", ") || "(none)"}
266389
266792
  `);
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);
266793
+ 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
266794
  process.stdout.write(`
266392
266795
  Per-bucket diagnostics:
266393
266796
  `);
@@ -266413,6 +266816,13 @@ Per-bucket diagnostics:
266413
266816
  reasons.push(`missing required linear.filter label(s): ${missing.join(", ")}`);
266414
266817
  }
266415
266818
  }
266819
+ if (excludeLabels && excludeLabels.length > 0) {
266820
+ const issueLabels = new Set(issue2.labels.nodes.map((l3) => l3.name));
266821
+ const present = excludeLabels.filter((label) => issueLabels.has(label));
266822
+ if (present.length > 0) {
266823
+ reasons.push(`carries excluded linear.filter label(s): ${present.join(", ")}`);
266824
+ }
266825
+ }
266416
266826
  const includeMatches = bucket.indicator.filter.some((m2) => markerMatches(issue2, m2));
266417
266827
  if (!includeMatches) {
266418
266828
  const want = bucket.indicator.filter.map((m2) => `${m2.type}:${m2.value}`).join(" OR ");
@@ -266477,7 +266887,7 @@ var exports_json_runner = {};
266477
266887
  __export(exports_json_runner, {
266478
266888
  runAgentJson: () => runAgentJson
266479
266889
  });
266480
- import { join as join41 } from "path";
266890
+ import { join as join40 } from "path";
266481
266891
  import { mkdir as mkdir12 } from "fs/promises";
266482
266892
  import { homedir as homedir8 } from "os";
266483
266893
  function makeEmit(fileSink) {
@@ -266499,7 +266909,7 @@ async function runAgentJson({
266499
266909
  tasksDir,
266500
266910
  runPreflight: runPreflight2 = runPreflight
266501
266911
  }) {
266502
- await mkdir12(join41(homedir8(), ".ralph"), { recursive: true }).catch(() => {
266912
+ await mkdir12(join40(homedir8(), ".ralph"), { recursive: true }).catch(() => {
266503
266913
  return;
266504
266914
  });
266505
266915
  const fileSink = createJsonLogFileSink(args.jsonLogFile);
@@ -266719,7 +267129,7 @@ __export(exports_src3, {
266719
267129
  main: () => main3
266720
267130
  });
266721
267131
  import { mkdir as mkdir13 } from "fs/promises";
266722
- import { join as join42 } from "path";
267132
+ import { join as join41 } from "path";
266723
267133
  async function main3(argv) {
266724
267134
  if (argv.includes("--help") || argv.includes("-h")) {
266725
267135
  printAgentHelp();
@@ -266784,7 +267194,7 @@ async function main3(argv) {
266784
267194
  }
266785
267195
  await mkdir13(statesDir, { recursive: true });
266786
267196
  await mkdir13(tasksDir, { recursive: true });
266787
- await mkdir13(join42(projectRoot, ".ralph"), { recursive: true });
267197
+ await mkdir13(join41(projectRoot, ".ralph"), { recursive: true });
266788
267198
  if (shouldFallbackToJsonOutput(args, process.stdin.isTTY)) {
266789
267199
  process.stderr.write(`agent: stdin is not a TTY \u2014 falling back to --json-output mode.
266790
267200
  `);