@neriros/ralphy 3.10.5 → 3.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18928,8 +18928,8 @@ import { readFileSync } from "fs";
18928
18928
  import { resolve } from "path";
18929
18929
  function getVersion() {
18930
18930
  try {
18931
- if ("3.10.5")
18932
- return "3.10.5";
18931
+ if ("3.10.7")
18932
+ return "3.10.7";
18933
18933
  } catch {}
18934
18934
  const dirsToTry = [];
18935
18935
  try {
@@ -81072,7 +81072,7 @@ function modelOptionValues() {
81072
81072
  const field = findField("model");
81073
81073
  return field && field.spec.kind === "select" ? field.spec.options.map((o) => o.value) : [];
81074
81074
  }
81075
- var PROMPT_BODY_FIELD_ID = "promptBody", REPO_LINK_FIELD_ID = "repo.link", yes = () => ({ kind: "confirm", defaultChoice: "confirm" }), no = () => ({ kind: "confirm", defaultChoice: "cancel" }), PROJECT_NAME, LINEAR_TEAM, REPO_LINK, LINEAR_FILTER, QUICK_FIELDS, isOn = (id) => (answers) => answers[id] === true, CUSTOMIZED_FIELDS, COMMON_CLI_OPTIONS, FIELD_DESCRIPTIONS;
81075
+ var PROMPT_BODY_FIELD_ID = "promptBody", REPO_LINK_FIELD_ID = "repo.link", AWAITING_STATUS_FIELD_ID = "linear.confirmationMode.awaitingStatus", yes = () => ({ kind: "confirm", defaultChoice: "confirm" }), no = () => ({ kind: "confirm", defaultChoice: "cancel" }), PROJECT_NAME, LINEAR_TEAM, REPO_LINK, LINEAR_FILTER, QUICK_FIELDS, isOn = (id) => (answers) => answers[id] === true, CUSTOMIZED_FIELDS, COMMON_CLI_OPTIONS, FIELD_DESCRIPTIONS;
81076
81076
  var init_fields = __esm(() => {
81077
81077
  PROJECT_NAME = {
81078
81078
  id: "project.name",
@@ -81436,6 +81436,14 @@ var init_fields = __esm(() => {
81436
81436
  spec: { kind: "number", placeholder: "3" },
81437
81437
  when: isOn("linear.confirmationMode.enabled")
81438
81438
  },
81439
+ {
81440
+ id: AWAITING_STATUS_FIELD_ID,
81441
+ label: "Park awaiting-approval tickets in a status?",
81442
+ hint: "e.g. Planned \u2014 blank keeps them In Progress",
81443
+ description: "When the confirmation gate opens, move the ticket to this Linear status so the board shows it waiting on a human (it must be a real status in your team). Ralphy also adds it to the in-progress pickup filter so the parked ticket keeps being polled, and re-asserts In Progress on approval. Leave blank to keep parked tickets in In Progress. Pairs with status-based indicators.",
81444
+ spec: { kind: "text", placeholder: "Planned" },
81445
+ when: isOn("linear.confirmationMode.enabled")
81446
+ },
81439
81447
  {
81440
81448
  id: "linear.indicators",
81441
81449
  label: "Linear lifecycle indicators",
@@ -82908,6 +82916,20 @@ function buildFromAnswers(mode, answers, build = buildWorkflowMarkdown) {
82908
82916
  values2["linear.indicators"] = map3;
82909
82917
  }
82910
82918
  }
82919
+ const parkStatusRaw = values2[AWAITING_STATUS_FIELD_ID];
82920
+ const parkStatus = typeof parkStatusRaw === "string" ? parkStatusRaw.trim() : "";
82921
+ if (values2["linear.confirmationMode.enabled"] === true && parkStatus && values2["linear.indicators"] && typeof values2["linear.indicators"] === "object") {
82922
+ const map3 = { ...values2["linear.indicators"] };
82923
+ map3.setAwaitingConfirmation = { type: "status", value: parkStatus };
82924
+ const existing = map3.getInProgress;
82925
+ const filter2 = existing && !Array.isArray(existing) && "filter" in existing ? [...existing.filter] : [];
82926
+ if (!filter2.some((m) => m.type === "status" && m.value === parkStatus)) {
82927
+ filter2.push({ type: "status", value: parkStatus });
82928
+ }
82929
+ map3.getInProgress = { filter: filter2 };
82930
+ values2["linear.indicators"] = map3;
82931
+ }
82932
+ delete values2[AWAITING_STATUS_FIELD_ID];
82911
82933
  const linkRepo = values2[REPO_LINK_FIELD_ID] === true;
82912
82934
  delete values2[REPO_LINK_FIELD_ID];
82913
82935
  if (!linkRepo) {
@@ -85044,6 +85066,89 @@ var init_schema2 = __esm(() => {
85044
85066
  ALL_OWNED_SLOTS = new Set(Object.values(OWNERSHIP).flatMap((slots) => [...slots]));
85045
85067
  });
85046
85068
 
85069
+ // packages/core/src/state/sidecar.ts
85070
+ import { dirname as dirname5, join as join9 } from "path";
85071
+ import { mkdir as mkdir3, rename, unlink } from "fs/promises";
85072
+ function slotSidecarPath(changeDir, slot) {
85073
+ return join9(changeDir, `${CORE_STATE_FILE.replace(/\.json$/, "")}.${slot}.json`);
85074
+ }
85075
+ function parseObject(text) {
85076
+ if (text === null)
85077
+ return null;
85078
+ try {
85079
+ const parsed = JSON.parse(text);
85080
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
85081
+ return parsed;
85082
+ }
85083
+ return null;
85084
+ } catch {
85085
+ return null;
85086
+ }
85087
+ }
85088
+ function deepSet(target, path, value) {
85089
+ if (path === "") {
85090
+ for (const k of Object.keys(target))
85091
+ delete target[k];
85092
+ if (value && typeof value === "object" && !Array.isArray(value)) {
85093
+ Object.assign(target, value);
85094
+ }
85095
+ return;
85096
+ }
85097
+ const segments = path.split(".");
85098
+ let cursor = target;
85099
+ for (let i = 0;i < segments.length - 1; i++) {
85100
+ const key = segments[i];
85101
+ const existing = cursor[key];
85102
+ if (existing === null || typeof existing !== "object" || Array.isArray(existing)) {
85103
+ const next = {};
85104
+ cursor[key] = next;
85105
+ cursor = next;
85106
+ } else {
85107
+ cursor = existing;
85108
+ }
85109
+ }
85110
+ cursor[segments[segments.length - 1]] = value;
85111
+ }
85112
+ async function atomicWrite(path, content) {
85113
+ await mkdir3(dirname5(path), { recursive: true });
85114
+ const tmp = `${path}.tmp-${process.pid}-${writeSeq++}`;
85115
+ try {
85116
+ await Bun.write(tmp, content);
85117
+ await rename(tmp, path);
85118
+ } catch (err) {
85119
+ await unlink(tmp).catch(() => {});
85120
+ throw err;
85121
+ }
85122
+ }
85123
+ async function readSlotSidecar(changeDir, slot) {
85124
+ const file2 = Bun.file(slotSidecarPath(changeDir, slot));
85125
+ if (!await file2.exists())
85126
+ return;
85127
+ const obj = parseObject(await file2.text().catch(() => null));
85128
+ return obj ?? undefined;
85129
+ }
85130
+ async function writeSlotField(changeDir, path, value, seedInline) {
85131
+ const [slot, ...rest2] = path.split(".");
85132
+ const sidecarPath = slotSidecarPath(changeDir, slot);
85133
+ const existing = parseObject(await Bun.file(sidecarPath).text().catch(() => null));
85134
+ const obj = existing ?? (seedInline ? structuredClone(seedInline) : {});
85135
+ deepSet(obj, rest2.join("."), value);
85136
+ await atomicWrite(sidecarPath, JSON.stringify(obj, null, 2) + `
85137
+ `);
85138
+ }
85139
+ function overlaySidecarsSync(changeDir, target, read) {
85140
+ for (const slot of ALL_OWNED_SLOTS) {
85141
+ const obj = parseObject(read(slotSidecarPath(changeDir, slot)));
85142
+ if (obj !== undefined && obj !== null)
85143
+ target[slot] = obj;
85144
+ }
85145
+ return target;
85146
+ }
85147
+ var CORE_STATE_FILE = ".ralph-state.json", writeSeq = 0;
85148
+ var init_sidecar = __esm(() => {
85149
+ init_schema2();
85150
+ });
85151
+
85047
85152
  // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
85048
85153
  var util, objectUtil, ZodParsedType, getParsedType2 = (data) => {
85049
85154
  const t = typeof data;
@@ -89147,16 +89252,24 @@ function formatTaskName(name) {
89147
89252
  }
89148
89253
 
89149
89254
  // packages/core/src/state.ts
89150
- import { join as join9 } from "path";
89255
+ import { join as join10 } from "path";
89256
+ function stripOwnedSlots(state) {
89257
+ const out = { ...state };
89258
+ for (const slot of ALL_OWNED_SLOTS)
89259
+ delete out[slot];
89260
+ return out;
89261
+ }
89151
89262
  function readState(changeDir) {
89152
- const filePath = join9(changeDir, STATE_FILE2);
89263
+ const filePath = join10(changeDir, STATE_FILE2);
89153
89264
  const raw = getStorage().read(filePath);
89154
89265
  if (raw === null)
89155
89266
  throw new Error(".ralph-state.json not found");
89156
- return StateSchema.parse(JSON.parse(raw));
89267
+ const base2 = JSON.parse(raw);
89268
+ overlaySidecarsSync(changeDir, base2, (p) => getStorage().read(p));
89269
+ return StateSchema.parse(base2);
89157
89270
  }
89158
89271
  function tryReadStateRaw(changeDir) {
89159
- const filePath = join9(changeDir, STATE_FILE2);
89272
+ const filePath = join10(changeDir, STATE_FILE2);
89160
89273
  const text = getStorage().read(filePath);
89161
89274
  if (text === null)
89162
89275
  return { state: null, raw: null };
@@ -89167,12 +89280,14 @@ function tryReadStateRaw(changeDir) {
89167
89280
  return { state: null, raw: null };
89168
89281
  }
89169
89282
  const raw = parsed && typeof parsed === "object" ? parsed : {};
89170
- const result2 = StateSchema.safeParse(parsed);
89283
+ overlaySidecarsSync(changeDir, raw, (p) => getStorage().read(p));
89284
+ const result2 = StateSchema.safeParse(raw);
89171
89285
  return { state: result2.success ? result2.data : null, raw };
89172
89286
  }
89173
89287
  function writeState(changeDir, state) {
89174
- const filePath = join9(changeDir, STATE_FILE2);
89175
- getStorage().write(filePath, JSON.stringify(state, null, 2) + `
89288
+ const filePath = join10(changeDir, STATE_FILE2);
89289
+ const core2 = stripOwnedSlots(state);
89290
+ getStorage().write(filePath, JSON.stringify(core2, null, 2) + `
89176
89291
  `);
89177
89292
  }
89178
89293
  function updateState(changeDir, updater) {
@@ -89211,7 +89326,7 @@ function buildInitialState(options) {
89211
89326
  });
89212
89327
  }
89213
89328
  function ensureState(changeDir) {
89214
- const filePath = join9(changeDir, STATE_FILE2);
89329
+ const filePath = join10(changeDir, STATE_FILE2);
89215
89330
  const storage = getStorage();
89216
89331
  if (storage.read(filePath) !== null) {
89217
89332
  return readState(changeDir);
@@ -89225,11 +89340,12 @@ var STATE_FILE2 = ".ralph-state.json";
89225
89340
  var init_state = __esm(() => {
89226
89341
  init_types2();
89227
89342
  init_context();
89343
+ init_schema2();
89344
+ init_sidecar();
89228
89345
  });
89229
89346
 
89230
89347
  // packages/core/src/state/store.ts
89231
- import { dirname as dirname5, join as join10 } from "path";
89232
- import { mkdir as mkdir3 } from "fs/promises";
89348
+ import { join as join11 } from "path";
89233
89349
  async function readJson(filePath) {
89234
89350
  const file2 = Bun.file(filePath);
89235
89351
  if (!await file2.exists())
@@ -89244,22 +89360,6 @@ async function readJson(filePath) {
89244
89360
  return {};
89245
89361
  }
89246
89362
  }
89247
- function deepSet(target, path, value) {
89248
- const segments = path.split(".");
89249
- let cursor = target;
89250
- for (let i = 0;i < segments.length - 1; i++) {
89251
- const key = segments[i];
89252
- const existing = cursor[key];
89253
- if (existing === undefined || existing === null || typeof existing !== "object" || Array.isArray(existing)) {
89254
- const next = {};
89255
- cursor[key] = next;
89256
- cursor = next;
89257
- } else {
89258
- cursor = existing;
89259
- }
89260
- }
89261
- cursor[segments[segments.length - 1]] = value;
89262
- }
89263
89363
  async function writeField(changeDir, featureName, path, value) {
89264
89364
  const allowed = OWNERSHIP[featureName];
89265
89365
  if (!allowed) {
@@ -89269,16 +89369,15 @@ async function writeField(changeDir, featureName, path, value) {
89269
89369
  if (!allowed.includes(topSlot)) {
89270
89370
  throw new OwnershipError(featureName, path, `feature '${featureName}' may not write '${path}' (owns ${allowed.join(", ")})`);
89271
89371
  }
89272
- const filePath = join10(changeDir, STATE_FILE3);
89273
- const existing = await readJson(filePath);
89274
- deepSet(existing, path, value);
89275
- await mkdir3(dirname5(filePath), { recursive: true });
89276
- await Bun.write(filePath, JSON.stringify(existing, null, 2) + `
89277
- `);
89372
+ const inline = (await readJson(join11(changeDir, STATE_FILE3)))[topSlot];
89373
+ const seed = inline && typeof inline === "object" && !Array.isArray(inline) ? inline : undefined;
89374
+ await writeSlotField(changeDir, path, value, seed);
89278
89375
  }
89279
89376
  var STATE_FILE3 = ".ralph-state.json", OwnershipError;
89280
89377
  var init_store = __esm(() => {
89281
89378
  init_schema2();
89379
+ init_sidecar();
89380
+ init_sidecar();
89282
89381
  init_state();
89283
89382
  OwnershipError = class OwnershipError extends Error {
89284
89383
  featureName;
@@ -89293,14 +89392,14 @@ var init_store = __esm(() => {
89293
89392
  });
89294
89393
 
89295
89394
  // apps/loop/src/components/TaskStatus.tsx
89296
- import { join as join11 } from "path";
89395
+ import { join as join12 } from "path";
89297
89396
  function TaskStatus({ state, stateDir }) {
89298
89397
  const storage = getStorage();
89299
89398
  const cost = Math.round(state.usage.total_cost_usd * 100) / 100;
89300
89399
  const time3 = Math.round(state.usage.total_duration_ms / 1000 * 10) / 10 + "s";
89301
89400
  const artifacts = OPENSPEC_ARTIFACTS.map((name) => ({
89302
89401
  name,
89303
- exists: storage.read(join11(stateDir, name)) !== null
89402
+ exists: storage.read(join12(stateDir, name)) !== null
89304
89403
  }));
89305
89404
  const recent = state.history.slice(-10);
89306
89405
  return /* @__PURE__ */ jsx_dev_runtime2.jsxDEV(Box_default, {
@@ -98017,8 +98116,8 @@ var init_rate_limit_detection = __esm(() => {
98017
98116
  });
98018
98117
 
98019
98118
  // packages/engine/src/agents/claude.ts
98020
- import { mkdtemp, unlink } from "fs/promises";
98021
- import { join as join12 } from "path";
98119
+ import { mkdtemp, unlink as unlink2 } from "fs/promises";
98120
+ import { join as join13 } from "path";
98022
98121
  import { tmpdir } from "os";
98023
98122
  function buildClaudeArgs(model, resumeSessionId, reviewerContextStrategy, reviewerModel) {
98024
98123
  const effectiveModel = reviewerModel ?? model;
@@ -98039,7 +98138,7 @@ function buildClaudeArgs(model, resumeSessionId, reviewerContextStrategy, review
98039
98138
  }
98040
98139
  async function runInteractive(req) {
98041
98140
  const { model, prompt, taskDir } = req;
98042
- const promptFile = taskDir ? join12(taskDir, "_interactive_prompt.md") : join12(await mkdtemp(join12(tmpdir(), "ralph-")), "prompt.md");
98141
+ const promptFile = taskDir ? join13(taskDir, "_interactive_prompt.md") : join13(await mkdtemp(join13(tmpdir(), "ralph-")), "prompt.md");
98043
98142
  await Bun.write(promptFile, prompt);
98044
98143
  try {
98045
98144
  const cmd = [
@@ -98066,14 +98165,14 @@ async function runInteractive(req) {
98066
98165
  env: scrubClaudeEnv(process.env)
98067
98166
  });
98068
98167
  const exitCode = await proc.exited;
98069
- const doneFile = taskDir ? join12(taskDir, "_interactive_done") : null;
98168
+ const doneFile = taskDir ? join13(taskDir, "_interactive_done") : null;
98070
98169
  if (doneFile && await Bun.file(doneFile).exists()) {
98071
98170
  return { exitCode: 0, usage: null, sessionId: null, rateLimited: false };
98072
98171
  }
98073
98172
  return { exitCode, usage: null, sessionId: null, rateLimited: false };
98074
98173
  } finally {
98075
98174
  try {
98076
- await unlink(promptFile);
98175
+ await unlink2(promptFile);
98077
98176
  } catch {}
98078
98177
  }
98079
98178
  }
@@ -99105,6 +99204,12 @@ class FlowActorStore {
99105
99204
  return typeof s.value === "string" || typeof s.status === "string";
99106
99205
  }
99107
99206
  async loadSnapshot(changeDir) {
99207
+ const sidecar = await readSlotSidecar(changeDir, "flow");
99208
+ if (sidecar && typeof sidecar === "object") {
99209
+ const snap = sidecar.actorSnapshot;
99210
+ if (snap !== undefined && snap !== null)
99211
+ return snap;
99212
+ }
99108
99213
  const filePath = `${changeDir}/${STATE_FILE4}`;
99109
99214
  const file2 = Bun.file(filePath);
99110
99215
  if (!await file2.exists())
@@ -99563,11 +99668,11 @@ var init_meta_prompt = __esm(() => {
99563
99668
  });
99564
99669
 
99565
99670
  // packages/core/src/loop.ts
99566
- import { join as join13 } from "path";
99671
+ import { join as join14 } from "path";
99567
99672
  function buildTaskPrompt(state, taskDir, reviewPhase) {
99568
99673
  const storage = getStorage();
99569
99674
  let prompt = "";
99570
- const steeringContent = storage.read(join13(taskDir, "steering.md"));
99675
+ const steeringContent = storage.read(join14(taskDir, "steering.md"));
99571
99676
  if (steeringContent !== null) {
99572
99677
  const steeringLines = steeringContent.split(`
99573
99678
  `).filter((line) => !line.startsWith("#")).filter((line) => line.trim()).slice(0, STEERING_MAX_LINES);
@@ -99586,8 +99691,8 @@ function buildTaskPrompt(state, taskDir, reviewPhase) {
99586
99691
  `;
99587
99692
  }
99588
99693
  }
99589
- const agentTasksPath = join13(taskDir, AGENT_TASKS_FILENAME);
99590
- const missionTasksPath = join13(taskDir, MISSION_TASKS_FILENAME);
99694
+ const agentTasksPath = join14(taskDir, AGENT_TASKS_FILENAME);
99695
+ const missionTasksPath = join14(taskDir, MISSION_TASKS_FILENAME);
99591
99696
  const agentTasksContent = storage.read(agentTasksPath);
99592
99697
  const missionTasksContent = storage.read(missionTasksPath);
99593
99698
  let activePath = null;
@@ -99663,7 +99768,7 @@ function buildTaskPrompt(state, taskDir, reviewPhase) {
99663
99768
  }
99664
99769
  }
99665
99770
  if (reviewPhase?.enabled) {
99666
- const reviewFindingsPath = join13(taskDir, "review-findings.md");
99771
+ const reviewFindingsPath = join14(taskDir, "review-findings.md");
99667
99772
  const reviewFindingsContent = storage.read(reviewFindingsPath);
99668
99773
  const hasUncheckedMission = missionTasksContent !== null && /^- \[ \]/m.test(missionTasksContent);
99669
99774
  const hasUncheckedAgent = agentTasksContent !== null && /^- \[ \]/m.test(agentTasksContent);
@@ -99737,7 +99842,7 @@ When all tasks are complete and all files are committed, push your branch and op
99737
99842
  }
99738
99843
  function buildSteeringBlock(taskDir) {
99739
99844
  const storage = getStorage();
99740
- const steeringContent = storage.read(join13(taskDir, "steering.md"));
99845
+ const steeringContent = storage.read(join14(taskDir, "steering.md"));
99741
99846
  if (steeringContent === null)
99742
99847
  return "";
99743
99848
  const steeringLines = steeringContent.split(`
@@ -99835,7 +99940,7 @@ function buildPlanPrompt(state, taskDir) {
99835
99940
  return prompt;
99836
99941
  }
99837
99942
  function buildReviewPrompt(state, taskDir) {
99838
- const reviewFindingsPath = join13(taskDir, "review-findings.md");
99943
+ const reviewFindingsPath = join14(taskDir, "review-findings.md");
99839
99944
  let prompt = buildSteeringBlock(taskDir);
99840
99945
  prompt += `---
99841
99946
 
@@ -99900,7 +100005,7 @@ function buildPhasePrompt(phase, state, taskDir, reviewPhase, metaPromptOptions)
99900
100005
  }
99901
100006
  function checkStopSignal(taskDir, stateDir) {
99902
100007
  const storage = getStorage();
99903
- const stopFile = join13(taskDir, "STOP");
100008
+ const stopFile = join14(taskDir, "STOP");
99904
100009
  const reason = storage.read(stopFile);
99905
100010
  if (reason === null)
99906
100011
  return null;
@@ -99960,7 +100065,7 @@ function updateStateIteration(stateDir, result2, startedAt, engine, model, usage
99960
100065
  }
99961
100066
  function appendSteeringMessage(taskDir, message) {
99962
100067
  const storage = getStorage();
99963
- const steeringPath = join13(taskDir, "steering.md");
100068
+ const steeringPath = join14(taskDir, "steering.md");
99964
100069
  const existing = storage.read(steeringPath);
99965
100070
  const updated = existing ? `${message}
99966
100071
 
@@ -100010,7 +100115,7 @@ var init_loop2 = __esm(() => {
100010
100115
  });
100011
100116
 
100012
100117
  // apps/loop/src/hooks/useLoop.ts
100013
- import { join as join14 } from "path";
100118
+ import { join as join15 } from "path";
100014
100119
  function sleep(seconds) {
100015
100120
  return new Promise((resolve3) => setTimeout(resolve3, seconds * 1000));
100016
100121
  }
@@ -100072,7 +100177,7 @@ function useLoop(opts) {
100072
100177
  }
100073
100178
  } else {
100074
100179
  if (rawState !== null) {
100075
- addInfo(`.ralph-state.json was malformed \u2014 reinitialising. External fields (linearComments, specAttachments) preserved.`);
100180
+ addInfo(`.ralph-state.json was malformed \u2014 reinitialising. Feature-owned slots (linearComments, specAttachments, \u2026) live in their own sidecar files and are unaffected.`);
100076
100181
  }
100077
100182
  currentState = buildInitialState({
100078
100183
  name: opts.name,
@@ -100082,12 +100187,6 @@ function useLoop(opts) {
100082
100187
  manualTest: opts.manualTest,
100083
100188
  createPr: opts.createPr ?? false
100084
100189
  });
100085
- if (rawState !== null && rawState.linearComments) {
100086
- currentState.linearComments = rawState.linearComments;
100087
- }
100088
- if (rawState !== null && rawState.specAttachments) {
100089
- currentState.specAttachments = rawState.specAttachments;
100090
- }
100091
100190
  writeState(stateDir, currentState);
100092
100191
  }
100093
100192
  const isResume2 = currentState.iteration > 0;
@@ -100132,8 +100231,8 @@ function useLoop(opts) {
100132
100231
  setState(currentState);
100133
100232
  if (!actor.getSnapshot().matches("running"))
100134
100233
  break;
100135
- const tasksContent = storage.read(join14(tasksDir, MISSION_TASKS_FILENAME));
100136
- const agentTasksContent = storage.read(join14(tasksDir, AGENT_TASKS_FILENAME));
100234
+ const tasksContent = storage.read(join15(tasksDir, MISSION_TASKS_FILENAME));
100235
+ const agentTasksContent = storage.read(join15(tasksDir, AGENT_TASKS_FILENAME));
100137
100236
  if (tasksContent === null && currentState.iteration > 0 && typeof opts.changeStore.listChanges === "function") {
100138
100237
  let stillActive = true;
100139
100238
  try {
@@ -100170,7 +100269,7 @@ function useLoop(opts) {
100170
100269
  const agentDone = agentTasksContent === null || allCompleted(agentTasksContent);
100171
100270
  if (missionDone && agentDone && tasksContent !== null) {
100172
100271
  if (opts.reviewPhase?.enabled) {
100173
- const reviewFindingsPath = join14(tasksDir, "review-findings.md");
100272
+ const reviewFindingsPath = join15(tasksDir, "review-findings.md");
100174
100273
  const reviewFindingsFile = Bun.file(reviewFindingsPath);
100175
100274
  const findingsExists = await reviewFindingsFile.exists();
100176
100275
  const findingsContent = findingsExists ? await reviewFindingsFile.text() : null;
@@ -100199,7 +100298,7 @@ function useLoop(opts) {
100199
100298
  model: opts.reviewPhase.reviewerModel ?? opts.model,
100200
100299
  prompt: reviewPrompt,
100201
100300
  logFlag: opts.log,
100202
- logFile: join14(stateDir, `log-review-${roundNum}.json`),
100301
+ logFile: join15(stateDir, `log-review-${roundNum}.json`),
100203
100302
  taskDir: tasksDir,
100204
100303
  reviewerContextStrategy: opts.reviewPhase.reviewerContextStrategy ?? "fresh",
100205
100304
  onFeedEvent: addFeedEvent
@@ -100273,8 +100372,8 @@ function useLoop(opts) {
100273
100372
  const time3 = new Date().toLocaleTimeString("en-US", { hour12: false });
100274
100373
  addIterationHeader(localIter, time3);
100275
100374
  addInfo(`Iteration ${localIter} (total: ${currentState.iteration})`);
100276
- const proposalContent = storage.read(join14(tasksDir, "proposal.md"));
100277
- const designContent = storage.read(join14(tasksDir, "design.md"));
100375
+ const proposalContent = storage.read(join15(tasksDir, "proposal.md"));
100376
+ const designContent = storage.read(join15(tasksDir, "design.md"));
100278
100377
  const routedPhase = routeTaskPhase(opts.phase, {
100279
100378
  proposal: proposalContent,
100280
100379
  design: designContent,
@@ -100294,7 +100393,7 @@ function useLoop(opts) {
100294
100393
  model: opts.model,
100295
100394
  prompt,
100296
100395
  logFlag: opts.log,
100297
- logFile: join14(stateDir, "log.json"),
100396
+ logFile: join15(stateDir, "log.json"),
100298
100397
  taskDir: tasksDir,
100299
100398
  interactive: false,
100300
100399
  onFeedEvent: addFeedEvent,
@@ -100317,7 +100416,7 @@ function useLoop(opts) {
100317
100416
  model: opts.model,
100318
100417
  prompt: buildSteeringPrompt(steerMessage),
100319
100418
  logFlag: opts.log,
100320
- logFile: join14(stateDir, "log.json"),
100419
+ logFile: join15(stateDir, "log.json"),
100321
100420
  taskDir: tasksDir,
100322
100421
  onFeedEvent: addResumeFeedEvent,
100323
100422
  signal: resumeController.signal,
@@ -100624,7 +100723,7 @@ var init_TaskLoop = __esm(async () => {
100624
100723
  });
100625
100724
 
100626
100725
  // apps/loop/src/components/App.tsx
100627
- import { join as join15 } from "path";
100726
+ import { join as join16 } from "path";
100628
100727
  function ExitAfterRender({ children }) {
100629
100728
  const { exit } = use_app_default();
100630
100729
  import_react59.useEffect(() => {
@@ -100677,7 +100776,7 @@ function App2({ args, taskPhase }) {
100677
100776
  }
100678
100777
  const layout = getLayout();
100679
100778
  const stateDir = layout.taskStateDir(args.name);
100680
- if (getStorage().read(join15(stateDir, ".ralph-state.json")) === null) {
100779
+ if (getStorage().read(join16(stateDir, ".ralph-state.json")) === null) {
100681
100780
  return /* @__PURE__ */ jsx_dev_runtime9.jsxDEV(ErrorMessage, {
100682
100781
  message: `Error: change '${args.name}' not found`
100683
100782
  }, undefined, false, undefined, this);
@@ -100731,7 +100830,7 @@ var init_App2 = __esm(async () => {
100731
100830
 
100732
100831
  // packages/log/src/log.ts
100733
100832
  import { appendFile } from "fs/promises";
100734
- import { join as join16, dirname as dirname7 } from "path";
100833
+ import { join as join17, dirname as dirname7 } from "path";
100735
100834
  import { homedir as homedir4 } from "os";
100736
100835
  import { mkdir as mkdir5 } from "fs/promises";
100737
100836
  function fmt(type, text) {
@@ -100780,14 +100879,14 @@ var init_log = __esm(() => {
100780
100879
  init_version();
100781
100880
  jsonLogChains = new Map;
100782
100881
  ANSI_RE = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
100783
- AGENT_LOG_PATH = join16(homedir4(), ".ralph", "agent-mode.log");
100882
+ AGENT_LOG_PATH = join17(homedir4(), ".ralph", "agent-mode.log");
100784
100883
  mkdir5(dirname7(AGENT_LOG_PATH), { recursive: true }).catch(() => {
100785
100884
  return;
100786
100885
  });
100787
100886
  });
100788
100887
 
100789
100888
  // apps/loop/src/debug.ts
100790
- import { join as join17 } from "path";
100889
+ import { join as join18 } from "path";
100791
100890
  function fmtTs(d) {
100792
100891
  return d.toISOString().replace("T", " ").slice(0, 23);
100793
100892
  }
@@ -100899,7 +100998,7 @@ function detectDebugStuck(lines) {
100899
100998
  };
100900
100999
  }
100901
101000
  async function inspectBinary(projectRoot) {
100902
- const binPath = join17(projectRoot, ".ralph", "bin", "cli.js");
101001
+ const binPath = join18(projectRoot, ".ralph", "bin", "cli.js");
100903
101002
  const file2 = Bun.file(binPath);
100904
101003
  if (!await file2.exists())
100905
101004
  return null;
@@ -100924,7 +101023,7 @@ async function inspectBinary(projectRoot) {
100924
101023
  async function resolveDebugTarget(projectRoot, opts) {
100925
101024
  const agentLogFile = Bun.file(AGENT_LOG_PATH);
100926
101025
  const textLines = await agentLogFile.exists() ? parseTextLog(await agentLogFile.text()) : [];
100927
- const jsonlLogFile = Bun.file(join17(projectRoot, ".ralph", "agent.log"));
101026
+ const jsonlLogFile = Bun.file(join18(projectRoot, ".ralph", "agent.log"));
100928
101027
  const jsonlLines = await jsonlLogFile.exists() ? parseJsonlLog(await jsonlLogFile.text()) : [];
100929
101028
  const allLines = [...textLines, ...jsonlLines];
100930
101029
  if (opts.name && !opts.issue) {
@@ -101029,7 +101128,7 @@ async function runDebug(opts) {
101029
101128
  `);
101030
101129
  const agentLogFile = Bun.file(AGENT_LOG_PATH);
101031
101130
  const textLines = await agentLogFile.exists() ? parseTextLog(await agentLogFile.text()) : [];
101032
- const jsonlLogPath = join17(projectRoot, ".ralph", "agent.log");
101131
+ const jsonlLogPath = join18(projectRoot, ".ralph", "agent.log");
101033
101132
  const jsonlLogFile = Bun.file(jsonlLogPath);
101034
101133
  const hasJsonlLog = await jsonlLogFile.exists();
101035
101134
  let { changeName, identifier: issueIdentifier } = await resolveDebugTarget(projectRoot, {
@@ -101043,7 +101142,7 @@ async function runDebug(opts) {
101043
101142
  }
101044
101143
  const jsonlLines = hasJsonlLog ? parseJsonlLog(await jsonlLogFile.text(), changeName) : [];
101045
101144
  const relevantText = textLines.filter((l) => l.text.includes(changeName) || issueIdentifier !== undefined && l.text.includes(issueIdentifier));
101046
- const workerLogFile = Bun.file(join17(projectRoot, ".ralph", "logs", `${changeName}.log`));
101145
+ const workerLogFile = Bun.file(join18(projectRoot, ".ralph", "logs", `${changeName}.log`));
101047
101146
  const workerLines = await workerLogFile.exists() ? parseTextLog(await workerLogFile.text()) : [];
101048
101147
  const merged = [...relevantText, ...jsonlLines, ...workerLines].sort((a, b) => +a.ts - +b.ts);
101049
101148
  const seen = new Set;
@@ -101200,8 +101299,8 @@ async function runDebug(opts) {
101200
101299
  out(" \u26A0 PR currently has merge conflicts");
101201
101300
  if (pr?.checks.some((c) => c.conclusion === "FAILURE"))
101202
101301
  out(" \u26A0 PR has failing CI checks");
101203
- const worktreePath = join17(projectRoot, ".ralph", "worktrees", changeName);
101204
- if (await Bun.file(join17(worktreePath, ".git")).exists()) {
101302
+ const worktreePath = join18(projectRoot, ".ralph", "worktrees", changeName);
101303
+ if (await Bun.file(join18(worktreePath, ".git")).exists()) {
101205
101304
  out(` Worktree : ${worktreePath}`);
101206
101305
  }
101207
101306
  if (!timeline.length)
@@ -101221,12 +101320,12 @@ __export(exports_src2, {
101221
101320
  taskMain: () => taskMain,
101222
101321
  main: () => main2
101223
101322
  });
101224
- import { join as join18 } from "path";
101323
+ import { join as join19 } from "path";
101225
101324
  import { exists as exists2, mkdir as mkdir6, rm as rm2 } from "fs/promises";
101226
101325
  async function ensureRalphGitignore(projectRoot) {
101227
- const ralphDir = join18(projectRoot, ".ralph");
101326
+ const ralphDir = join19(projectRoot, ".ralph");
101228
101327
  await mkdir6(ralphDir, { recursive: true });
101229
- const gitignorePath = join18(ralphDir, ".gitignore");
101328
+ const gitignorePath = join19(ralphDir, ".gitignore");
101230
101329
  const file2 = Bun.file(gitignorePath);
101231
101330
  if (await file2.exists()) {
101232
101331
  const existing = await file2.text();
@@ -101293,9 +101392,9 @@ async function main2(argv) {
101293
101392
  `);
101294
101393
  return 1;
101295
101394
  }
101296
- const worktreeDir = join18(worktreesDir(projectRoot), args.name);
101297
- const changeDir = join18(tasksDir, args.name);
101298
- const stateDir = join18(statesDir, args.name);
101395
+ const worktreeDir = join19(worktreesDir(projectRoot), args.name);
101396
+ const changeDir = join19(tasksDir, args.name);
101397
+ const stateDir = join19(statesDir, args.name);
101299
101398
  const branch = `ralph/${args.name}`;
101300
101399
  const removed = [];
101301
101400
  if (await exists2(worktreeDir)) {
@@ -101346,8 +101445,8 @@ async function main2(argv) {
101346
101445
  return 0;
101347
101446
  }
101348
101447
  if (args.mode === "task" && args.name) {
101349
- await mkdir6(join18(statesDir, args.name), { recursive: true });
101350
- await mkdir6(join18(tasksDir, args.name), { recursive: true });
101448
+ await mkdir6(join19(statesDir, args.name), { recursive: true });
101449
+ await mkdir6(join19(tasksDir, args.name), { recursive: true });
101351
101450
  await ensureRalphGitignore(projectRoot);
101352
101451
  }
101353
101452
  await runWithContext(createDefaultContext({ layout, args }), async () => {
@@ -101375,8 +101474,8 @@ async function taskMain(argv) {
101375
101474
  const layout = projectLayout(projectRoot);
101376
101475
  const statesDir = layout.statesDir;
101377
101476
  const tasksDir = layout.tasksDir;
101378
- await mkdir6(join18(statesDir, args.name), { recursive: true });
101379
- await mkdir6(join18(tasksDir, args.name), { recursive: true });
101477
+ await mkdir6(join19(statesDir, args.name), { recursive: true });
101478
+ await mkdir6(join19(tasksDir, args.name), { recursive: true });
101380
101479
  await ensureRalphGitignore(projectRoot);
101381
101480
  await runWithContext(createDefaultContext({ layout, args }), async () => {
101382
101481
  const { waitUntilExit } = render_default(import_react60.createElement(App2, {
@@ -101471,6 +101570,7 @@ async function parseAgentArgs(argv) {
101471
101570
  noTmux: false,
101472
101571
  checks: false,
101473
101572
  review: false,
101573
+ agentDebug: false,
101474
101574
  ticketTokens: []
101475
101575
  };
101476
101576
  const state = emptyParseState();
@@ -101593,6 +101693,9 @@ async function parseAgentArgs(argv) {
101593
101693
  case "--debug":
101594
101694
  result2.debug = true;
101595
101695
  break;
101696
+ case "--agent-debug":
101697
+ result2.agentDebug = true;
101698
+ break;
101596
101699
  case "--pre-existing-error-check":
101597
101700
  result2.preExistingErrorCheck = true;
101598
101701
  break;
@@ -101692,6 +101795,7 @@ var init_cli2 = __esm(() => {
101692
101795
  " --checks List mode: show failing CI check names per PR",
101693
101796
  " --review List mode: show unresolved review comment count per PR",
101694
101797
  " --debug List mode: explain why a Linear ticket was not picked up (use with --name)",
101798
+ " --agent-debug After each ticket finishes, run a one-shot self-review and write a report to ~/.ralph/retro/",
101695
101799
  " --help, -h Show this help message",
101696
101800
  "",
101697
101801
  "Examples:",
@@ -101768,7 +101872,7 @@ function formatError2(err) {
101768
101872
  }
101769
101873
 
101770
101874
  // apps/agent/src/shared/capabilities/fs-change.ts
101771
- import { join as join19, dirname as dirname8 } from "path";
101875
+ import { join as join20, dirname as dirname8 } from "path";
101772
101876
  import { mkdir as mkdir7 } from "fs/promises";
101773
101877
  var scaffold, prependTask, appendSteering, fsChange;
101774
101878
  var init_fs_change = __esm(() => {
@@ -101781,11 +101885,11 @@ var init_fs_change = __esm(() => {
101781
101885
  errorFormatter: formatError2,
101782
101886
  run: async (args) => {
101783
101887
  await mkdir7(args.changeDir, { recursive: true });
101784
- await mkdir7(join19(args.changeDir, "specs"), { recursive: true });
101888
+ await mkdir7(join20(args.changeDir, "specs"), { recursive: true });
101785
101889
  await mkdir7(args.stateDir, { recursive: true });
101786
- await Bun.write(join19(args.changeDir, "proposal.md"), args.proposal);
101787
- await Bun.write(join19(args.changeDir, "tasks.md"), args.tasks);
101788
- await Bun.write(join19(args.changeDir, "design.md"), args.design);
101890
+ await Bun.write(join20(args.changeDir, "proposal.md"), args.proposal);
101891
+ await Bun.write(join20(args.changeDir, "tasks.md"), args.tasks);
101892
+ await Bun.write(join20(args.changeDir, "design.md"), args.design);
101789
101893
  }
101790
101894
  };
101791
101895
  prependTask = {
@@ -101803,7 +101907,7 @@ var init_fs_change = __esm(() => {
101803
101907
  retryPolicy: NO_RETRY,
101804
101908
  errorFormatter: formatError2,
101805
101909
  run: async (args) => {
101806
- const path = join19(args.changeDir, "steering.md");
101910
+ const path = join20(args.changeDir, "steering.md");
101807
101911
  const f2 = Bun.file(path);
101808
101912
  const existing = await f2.exists() ? await f2.text() : null;
101809
101913
  const updated = existing ? `${args.message}
@@ -101818,11 +101922,11 @@ ${existing.trimStart()}` : `${args.message}
101818
101922
  });
101819
101923
 
101820
101924
  // apps/agent/src/agent/worktree.ts
101821
- import { basename as basename2, join as join20 } from "path";
101925
+ import { basename as basename2, join as join21 } from "path";
101822
101926
  import { homedir as homedir5 } from "os";
101823
101927
  import { exists as exists3 } from "fs/promises";
101824
101928
  function worktreesDir2(projectRoot) {
101825
- return join20(homedir5(), ".ralph", basename2(projectRoot), "worktrees");
101929
+ return join21(homedir5(), ".ralph", basename2(projectRoot), "worktrees");
101826
101930
  }
101827
101931
  function branchForChange(changeName) {
101828
101932
  return `ralph/${changeName}`;
@@ -101841,7 +101945,7 @@ function createWorktree(projectRoot, changeName, baseBranch, runner) {
101841
101945
  }
101842
101946
  async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
101843
101947
  const dir = worktreesDir2(projectRoot);
101844
- const cwd2 = join20(dir, changeName);
101948
+ const cwd2 = join21(dir, changeName);
101845
101949
  const branch = branchForChange(changeName);
101846
101950
  const list = await runner.run(["worktree", "list", "--porcelain"], projectRoot);
101847
101951
  if (list.stdout.includes(`worktree ${cwd2}
@@ -101866,7 +101970,7 @@ async function provisionWorktree(projectRoot, changeName, baseBranch, runner) {
101866
101970
  return { cwd: cwd2, branch };
101867
101971
  }
101868
101972
  async function installPrePushHook(cwd2, runner) {
101869
- const hookPath = join20(cwd2, ".ralph-hooks", "pre-push");
101973
+ const hookPath = join21(cwd2, ".ralph-hooks", "pre-push");
101870
101974
  await Bun.write(hookPath, PRE_PUSH_HOOK_SCRIPT);
101871
101975
  const chmod = Bun.spawn(["chmod", "+x", hookPath]);
101872
101976
  await chmod.exited;
@@ -101912,8 +102016,8 @@ async function isWorktreeSafeToRemove(cwd2, base2, runner) {
101912
102016
  return { safe: true, dirty, unpushedCommits };
101913
102017
  }
101914
102018
  async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
101915
- const dst = join20(worktreeCwd, ".mcp.json");
101916
- const src = join20(projectRoot, ".mcp.json");
102019
+ const dst = join21(worktreeCwd, ".mcp.json");
102020
+ const src = join21(projectRoot, ".mcp.json");
101917
102021
  const source = await exists3(dst) ? dst : await exists3(src) ? src : null;
101918
102022
  if (!source)
101919
102023
  return;
@@ -101927,7 +102031,7 @@ async function seedWorktreeMcpConfig(projectRoot, worktreeCwd) {
101927
102031
  if (servers && typeof servers === "object") {
101928
102032
  for (const cfg of Object.values(servers)) {
101929
102033
  if (Array.isArray(cfg.args)) {
101930
- cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join20(projectRoot, a) : a);
102034
+ cfg.args = cfg.args.map((a) => typeof a === "string" && a.startsWith(".ralph/") ? join21(projectRoot, a) : a);
101931
102035
  }
101932
102036
  }
101933
102037
  }
@@ -103777,7 +103881,7 @@ function emitFeatureSkipped(bus, id, reason) {
103777
103881
  var init_run_feature = () => {};
103778
103882
 
103779
103883
  // apps/agent/src/agent/post-task.ts
103780
- import { join as join21, dirname as dirname9 } from "path";
103884
+ import { join as join22, dirname as dirname9 } from "path";
103781
103885
  function summarizeUncommittedStatus(stdout) {
103782
103886
  const lines = stdout.split(`
103783
103887
  `).filter((line) => line.length > 0);
@@ -103849,7 +103953,7 @@ async function reactivateState(stateFilePath, log3, changeName) {
103849
103953
  async function runWorkerWithFixTask(ctx, heading, body) {
103850
103954
  try {
103851
103955
  await runCapability(fsChange.prependTask, {
103852
- tasksPath: join21(ctx.changeDir, AGENT_TASKS_FILENAME),
103956
+ tasksPath: join22(ctx.changeDir, AGENT_TASKS_FILENAME),
103853
103957
  heading,
103854
103958
  failureOutput: body
103855
103959
  });
@@ -104396,7 +104500,7 @@ async function runValidateOnlyPhase(input, deps) {
104396
104500
  emit3("validate-fix", command);
104397
104501
  log3(`! validation check failed: ${command}`, "yellow");
104398
104502
  try {
104399
- await prependFixTask(join21(changeDir, AGENT_TASKS_FILENAME), `Fix failing validation: ${command}`, output || `Command exited with code ${exitCode}`);
104503
+ await prependFixTask(join22(changeDir, AGENT_TASKS_FILENAME), `Fix failing validation: ${command}`, output || `Command exited with code ${exitCode}`);
104400
104504
  } catch (err) {
104401
104505
  log3(`! could not prepend fix task: ${err.message}`, "red");
104402
104506
  return 1;
@@ -104407,7 +104511,7 @@ async function runValidateOnlyPhase(input, deps) {
104407
104511
  }
104408
104512
  }
104409
104513
  try {
104410
- await prependFixTask(join21(changeDir, AGENT_TASKS_FILENAME), "Run openspec validation", [
104514
+ await prependFixTask(join22(changeDir, AGENT_TASKS_FILENAME), "Run openspec validation", [
104411
104515
  `Run \`bunx openspec validate ${changeName}\` to validate the change artifacts.`,
104412
104516
  `Commit any pending changes before running the validation command.`
104413
104517
  ].join(`
@@ -104420,7 +104524,7 @@ async function runValidateOnlyPhase(input, deps) {
104420
104524
  return respawnWorker();
104421
104525
  }
104422
104526
  async function recordGaveUp(stateFilePath, log3, changeName) {
104423
- const path = join21(dirname9(stateFilePath), GAVEUP_COUNT_FILE);
104527
+ const path = join22(dirname9(stateFilePath), GAVEUP_COUNT_FILE);
104424
104528
  try {
104425
104529
  const file2 = Bun.file(path);
104426
104530
  const current = await file2.exists() ? Number.parseInt(await file2.text(), 10) || 0 : 0;
@@ -104551,6 +104655,15 @@ async function runPostTask(input, deps) {
104551
104655
  emit3(succeeded ? "done" : "gave-up", succeeded ? undefined : `exit ${effectiveCode}`);
104552
104656
  if (!succeeded)
104553
104657
  await recordGaveUp(stateFilePath, log3, changeName);
104658
+ await deps.runRetrospective?.({
104659
+ changeName,
104660
+ cwd: cwd2,
104661
+ changeDir,
104662
+ stateFilePath,
104663
+ branch,
104664
+ issue: issue2,
104665
+ effectiveCode
104666
+ });
104554
104667
  await runWorktreeCleanupPhase({ changeName, cwd: cwd2, projectRoot, useWorktree, effectiveCode, cfg }, { git: git2, log: log3, emit: emit3 });
104555
104668
  await runTeardownPhase({ cwd: cwd2, teardownScript: cfg.teardownScript }, { runScript, log: log3, emit: emit3 });
104556
104669
  return effectiveCode;
@@ -105218,6 +105331,32 @@ class AgentCoordinator {
105218
105331
  if (!this.deps.syncTasks || !this.deps.getIterationCount)
105219
105332
  return;
105220
105333
  for (const w of this.workers) {
105334
+ if (this.deps.getTasksFingerprint) {
105335
+ let fingerprint;
105336
+ try {
105337
+ fingerprint = await this.deps.getTasksFingerprint(w.changeName);
105338
+ } catch (err) {
105339
+ this.deps.onLog(`! tasks fingerprint read failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
105340
+ continue;
105341
+ }
105342
+ if (fingerprint === null || fingerprint === w.lastSyncedTasksFingerprint) {
105343
+ continue;
105344
+ }
105345
+ let iteration;
105346
+ try {
105347
+ iteration = await this.deps.getIterationCount(w.changeName);
105348
+ } catch (err) {
105349
+ this.deps.onLog(`! iteration count read failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
105350
+ continue;
105351
+ }
105352
+ try {
105353
+ await this.deps.syncTasks(w, iteration);
105354
+ w.lastSyncedTasksFingerprint = fingerprint;
105355
+ } catch (err) {
105356
+ this.deps.onLog(`! sync-tasks (poll) failed for ${w.issueIdentifier}: ${err.message}`, "yellow");
105357
+ }
105358
+ continue;
105359
+ }
105221
105360
  let count;
105222
105361
  try {
105223
105362
  count = await this.deps.getIterationCount(w.changeName);
@@ -105497,6 +105636,7 @@ class AgentCoordinator {
105497
105636
  kill: handle.kill,
105498
105637
  lastReportedIteration: 0,
105499
105638
  lastSyncedIteration: 0,
105639
+ lastSyncedTasksFingerprint: null,
105500
105640
  restarting: false,
105501
105641
  reapedForAwaiting: false
105502
105642
  };
@@ -105660,6 +105800,7 @@ class AgentCoordinator {
105660
105800
  kill: () => {},
105661
105801
  lastReportedIteration: 0,
105662
105802
  lastSyncedIteration: 0,
105803
+ lastSyncedTasksFingerprint: null,
105663
105804
  restarting: false,
105664
105805
  reapedForAwaiting: false
105665
105806
  };
@@ -105789,15 +105930,15 @@ var init_coordinator2 = __esm(() => {
105789
105930
  });
105790
105931
 
105791
105932
  // apps/agent/src/agent/scaffold.ts
105792
- import { join as join22 } from "path";
105933
+ import { join as join23 } from "path";
105793
105934
  function changeNameForIssue(issue2) {
105794
105935
  const slug = issue2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40).replace(/^-+|-+$/g, "");
105795
105936
  return slug ? `${issue2.identifier.toLowerCase()}-${slug}` : issue2.identifier.toLowerCase();
105796
105937
  }
105797
105938
  async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = [], appendPrompt = "", attachments = []) {
105798
105939
  const name = changeNameForIssue(issue2);
105799
- const changeDir = join22(tasksDir, name);
105800
- const stateDir = join22(statesDir, name);
105940
+ const changeDir = join23(tasksDir, name);
105941
+ const stateDir = join23(statesDir, name);
105801
105942
  const commentsBlock = comments.length > 0 ? [
105802
105943
  "",
105803
105944
  "## Linear comments",
@@ -105850,8 +105991,8 @@ async function scaffoldChangeForIssue(tasksDir, statesDir, issue2, comments = []
105850
105991
  `- [ ] Refine proposal.md with the problem statement, approach, and acceptance criteria derived from the research`,
105851
105992
  `- [ ] Fill in \`## Why\` and \`## What Changes\` in proposal.md so \`openspec validate\` passes (these sections are required by the validator)`,
105852
105993
  `- [ ] Add at least one spec delta under \`specs/<capability>/spec.md\` describing the behavior added/modified/removed by this change`,
105853
- `- [ ] Fill in design.md with the technical design (files to touch, data flow, edge cases)`,
105854
- `- [ ] Append an \`## Implementation\` section below 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.`,
105994
+ `- [ ] 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).`,
105995
+ `- [ ] 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.`,
105855
105996
  `- [ ] Is there anything else to add? Review the complete change context and document any additional edge cases, constraints, or open questions not captured above.`,
105856
105997
  ""
105857
105998
  ].join(`
@@ -105933,19 +106074,22 @@ var init_detections = __esm(() => {
105933
106074
  });
105934
106075
 
105935
106076
  // apps/agent/src/features/confirmation/state.ts
105936
- import { dirname as dirname10, join as join23 } from "path";
105937
- import { mkdir as mkdir8 } from "fs/promises";
105938
- async function readConfirmationState(statePath) {
106077
+ import { dirname as dirname10, join as join24 } from "path";
106078
+ async function readInlineConfirmation(statePath) {
105939
106079
  const f2 = Bun.file(statePath);
105940
- let stateObj = {};
105941
- if (await f2.exists()) {
105942
- try {
105943
- stateObj = await f2.json();
105944
- } catch {
105945
- stateObj = {};
105946
- }
106080
+ if (!await f2.exists())
106081
+ return null;
106082
+ try {
106083
+ const obj = await f2.json();
106084
+ return obj.confirmation ?? null;
106085
+ } catch {
106086
+ return null;
105947
106087
  }
105948
- const existing = stateObj.confirmation ?? null;
106088
+ }
106089
+ async function readConfirmationState(statePath) {
106090
+ const changeDir = dirname10(statePath);
106091
+ const sidecar = await readSlotSidecar(changeDir, "confirmation");
106092
+ const existing = sidecar ?? await readInlineConfirmation(statePath) ?? null;
105949
106093
  const confirmation = {
105950
106094
  askedAt: existing?.askedAt ?? null,
105951
106095
  lastReminderAt: existing?.lastReminderAt ?? null,
@@ -105953,14 +106097,13 @@ async function readConfirmationState(statePath) {
105953
106097
  rounds: existing?.rounds ?? 0,
105954
106098
  stuckPostedAt: existing?.stuckPostedAt ?? null,
105955
106099
  lastReviseConsumedAt: existing?.lastReviseConsumedAt ?? null,
105956
- awaitingMarkerAppliedAt: existing?.awaitingMarkerAppliedAt ?? null
106100
+ awaitingMarkerAppliedAt: existing?.awaitingMarkerAppliedAt ?? null,
106101
+ earlyDraftPrAt: existing?.earlyDraftPrAt ?? null
105957
106102
  };
105958
- return { stateObj, confirmation };
106103
+ return { stateObj: {}, confirmation };
105959
106104
  }
105960
- async function writeConfirmationState(statePath, stateObj, confirmation) {
105961
- await mkdir8(dirname10(statePath), { recursive: true });
105962
- await Bun.write(statePath, JSON.stringify({ ...stateObj, confirmation }, null, 2) + `
105963
- `);
106105
+ async function writeConfirmationState(statePath, _stateObj, confirmation) {
106106
+ await writeSlotField(dirname10(statePath), "confirmation", confirmation);
105964
106107
  }
105965
106108
  async function restartFromDesign(changeDir, changeName) {
105966
106109
  const designStub = [
@@ -105970,8 +106113,8 @@ async function restartFromDesign(changeDir, changeName) {
105970
106113
  ""
105971
106114
  ].join(`
105972
106115
  `);
105973
- await Bun.write(join23(changeDir, "design.md"), designStub);
105974
- const tasksPath = join23(changeDir, "tasks.md");
106116
+ await Bun.write(join24(changeDir, "design.md"), designStub);
106117
+ const tasksPath = join24(changeDir, "tasks.md");
105975
106118
  if (await Bun.file(tasksPath).exists()) {
105976
106119
  await Bun.write(tasksPath, `# Tasks
105977
106120
 
@@ -105983,6 +106126,7 @@ async function appendSteeringNote(changeDir, message) {
105983
106126
  await runCapability(fsChange.appendSteering, { changeDir, message });
105984
106127
  }
105985
106128
  var init_state2 = __esm(() => {
106129
+ init_store();
105986
106130
  init_fs_change();
105987
106131
  });
105988
106132
 
@@ -106200,8 +106344,7 @@ var init_inspect = __esm(() => {
106200
106344
  });
106201
106345
 
106202
106346
  // apps/agent/src/features/confirmation/awaiting.ts
106203
- import { join as join24, dirname as dirname11 } from "path";
106204
- import { mkdir as mkdir9 } from "fs/promises";
106347
+ import { join as join25 } from "path";
106205
106348
  async function resolveChangeCwdForIssue(issue2, changeName, deps) {
106206
106349
  const tracked = deps.cwdOf(changeName);
106207
106350
  if (tracked)
@@ -106209,12 +106352,12 @@ async function resolveChangeCwdForIssue(issue2, changeName, deps) {
106209
106352
  if (!deps.useWorktree)
106210
106353
  return deps.projectRoot;
106211
106354
  const root = worktreesDir2(deps.projectRoot);
106212
- const canonical = join24(root, worktreeDirNameForIssue(issue2));
106213
- if (await Bun.file(join24(canonical, "openspec", "changes", changeName, "tasks.md")).exists()) {
106355
+ const canonical = join25(root, worktreeDirNameForIssue(issue2));
106356
+ if (await Bun.file(join25(canonical, "openspec", "changes", changeName, "tasks.md")).exists()) {
106214
106357
  return canonical;
106215
106358
  }
106216
- const legacy = join24(root, changeName);
106217
- if (await Bun.file(join24(legacy, "openspec", "changes", changeName, "tasks.md")).exists()) {
106359
+ const legacy = join25(root, changeName);
106360
+ if (await Bun.file(join25(legacy, "openspec", "changes", changeName, "tasks.md")).exists()) {
106218
106361
  return legacy;
106219
106362
  }
106220
106363
  return deps.projectRoot;
@@ -106234,17 +106377,8 @@ async function postPlanReadyCommentOnce(issue2, statePath, changeName, deps) {
106234
106377
  return;
106235
106378
  if (deps.cfg.linear.postComments === false)
106236
106379
  return;
106237
- let stateObj = {};
106238
- const f2 = Bun.file(statePath);
106239
- if (await f2.exists()) {
106240
- try {
106241
- stateObj = await f2.json();
106242
- } catch {
106243
- stateObj = {};
106244
- }
106245
- }
106246
- const confirmation = stateObj.confirmation ?? null;
106247
- if (confirmation?.askedAt)
106380
+ const { confirmation } = await readConfirmationState(statePath);
106381
+ if (confirmation.askedAt)
106248
106382
  return;
106249
106383
  const approvalSentence = describeApprovalMarker(deps.cfg.linear.indicators.getApproved);
106250
106384
  const handle = deps.cfg.linear.mentionHandle;
@@ -106255,16 +106389,8 @@ async function postPlanReadyCommentOnce(issue2, statePath, changeName, deps) {
106255
106389
  deps.onLog(`! Linear plan-ready comment failed for ${issue2.identifier}: ${err.message}`, "yellow");
106256
106390
  return;
106257
106391
  }
106258
- const nextConfirmation = {
106259
- askedAt: new Date().toISOString(),
106260
- lastReminderAt: confirmation?.lastReminderAt ?? null,
106261
- confirmedAt: confirmation?.confirmedAt ?? null,
106262
- rounds: confirmation?.rounds ?? 0
106263
- };
106264
106392
  try {
106265
- await mkdir9(dirname11(statePath), { recursive: true });
106266
- await Bun.write(statePath, JSON.stringify({ ...stateObj, confirmation: nextConfirmation }, null, 2) + `
106267
- `);
106393
+ await writeConfirmationState(statePath, {}, { ...confirmation, askedAt: new Date().toISOString() });
106268
106394
  } catch (err) {
106269
106395
  deps.onLog(`! could not persist confirmation.askedAt for ${issue2.identifier}: ${err.message}`, "yellow");
106270
106396
  }
@@ -106288,10 +106414,42 @@ async function applyAwaitingMarkerOnce(issue2, statePath, state, deps) {
106288
106414
  deps.onLog(`! persist awaitingMarkerAppliedAt for ${issue2.identifier}: ${err.message}`, "yellow");
106289
106415
  }
106290
106416
  }
106417
+ async function openDraftPrOnce(issue2, statePath, changeName, cwd2, state, deps) {
106418
+ if (deps.cfg.prDraft !== true)
106419
+ return;
106420
+ if (!deps.openDraftPr)
106421
+ return;
106422
+ if (state.confirmation.earlyDraftPrAt)
106423
+ return;
106424
+ let url2 = null;
106425
+ try {
106426
+ url2 = await deps.openDraftPr(issue2, changeName, cwd2);
106427
+ } catch (err) {
106428
+ deps.onLog(`! early draft PR open failed for ${issue2.identifier}: ${err.message}`, "yellow");
106429
+ }
106430
+ state.confirmation.earlyDraftPrAt = new Date().toISOString();
106431
+ try {
106432
+ await writeConfirmationState(statePath, state.stateObj, state.confirmation);
106433
+ } catch (err) {
106434
+ deps.onLog(`! persist earlyDraftPrAt for ${issue2.identifier}: ${err.message}`, "yellow");
106435
+ }
106436
+ if (url2)
106437
+ deps.onLog(` ${issue2.identifier}: opened draft PR for design \u2014 ${url2}`, "gray");
106438
+ }
106439
+ function issueInAwaitingStatus(issue2, indicators) {
106440
+ const set3 = indicators.setAwaitingConfirmation;
106441
+ if (!set3)
106442
+ return false;
106443
+ const current = issue2.state?.name;
106444
+ if (!current)
106445
+ return false;
106446
+ return markersOf(set3).some((m) => m.type === "status" && m.value === current);
106447
+ }
106291
106448
  async function releaseAwaitingMarker(issue2, statePath, deps) {
106292
106449
  const { stateObj, confirmation } = await readConfirmationState(statePath);
106293
- if (!confirmation.awaitingMarkerAppliedAt)
106450
+ if (!confirmation.awaitingMarkerAppliedAt && !issueInAwaitingStatus(issue2, deps.indicators)) {
106294
106451
  return;
106452
+ }
106295
106453
  if (deps.indicators.clearAwaitingConfirmation) {
106296
106454
  try {
106297
106455
  await deps.applyIndicator(issue2, deps.indicators.clearAwaitingConfirmation);
@@ -106299,6 +106457,13 @@ async function releaseAwaitingMarker(issue2, statePath, deps) {
106299
106457
  deps.onLog(`! clearAwaitingConfirmation failed for ${issue2.identifier}: ${err.message}`, "yellow");
106300
106458
  }
106301
106459
  }
106460
+ if (deps.indicators.setInProgress) {
106461
+ try {
106462
+ await deps.applyIndicator(issue2, deps.indicators.setInProgress);
106463
+ } catch (err) {
106464
+ deps.onLog(`! restore setInProgress after awaiting release failed for ${issue2.identifier}: ${err.message}`, "yellow");
106465
+ }
106466
+ }
106302
106467
  confirmation.awaitingMarkerAppliedAt = null;
106303
106468
  try {
106304
106469
  await writeConfirmationState(statePath, stateObj, confirmation);
@@ -106327,9 +106492,9 @@ async function processAwaitingForIssue(issue2, deps) {
106327
106492
  const layout = projectLayout(cwd2);
106328
106493
  const changeDir = layout.changeDir(changeName);
106329
106494
  const statePath = layout.stateFile(changeName);
106330
- const tasks2 = await readTextOrNull(join24(changeDir, "tasks.md"));
106331
- const proposal = await readTextOrNull(join24(changeDir, "proposal.md"));
106332
- const design = await readTextOrNull(join24(changeDir, "design.md"));
106495
+ const tasks2 = await readTextOrNull(join25(changeDir, "tasks.md"));
106496
+ const proposal = await readTextOrNull(join25(changeDir, "proposal.md"));
106497
+ const design = await readTextOrNull(join25(changeDir, "design.md"));
106333
106498
  let commentsCache = null;
106334
106499
  const getComments = async () => {
106335
106500
  if (commentsCache)
@@ -106413,6 +106578,7 @@ async function processAwaitingForIssue(issue2, deps) {
106413
106578
  cfg,
106414
106579
  onLog: deps.onLog
106415
106580
  });
106581
+ await openDraftPrOnce(issue2, statePath, changeName, cwd2, { stateObj, confirmation }, { cfg, openDraftPr: deps.openDraftPr, onLog: deps.onLog });
106416
106582
  const { stateObj: state2, confirmation: confirmation2 } = await readConfirmationState(statePath);
106417
106583
  const { outcome, next } = await inspectAwaitingTicket(confirmation2, {
106418
106584
  mentionHandle: cfg.linear.mentionHandle,
@@ -106487,6 +106653,7 @@ var init_awaiting = __esm(() => {
106487
106653
  init_worktree();
106488
106654
  init_scaffold();
106489
106655
  init_linear();
106656
+ init_types2();
106490
106657
  init_workflow();
106491
106658
  init_state2();
106492
106659
  init_inspect();
@@ -106561,9 +106728,26 @@ async function resolveDependencyBaseBranchImpl(issue2, runner, runnerCwd, deps)
106561
106728
  }
106562
106729
  return null;
106563
106730
  }
106731
+ function createOpenDraftPr(deps) {
106732
+ const create3 = deps.createPr ?? createPullRequest;
106733
+ return async (issue2, changeName, cwd2) => {
106734
+ const branch = deps.branchByChange.get(changeName);
106735
+ if (!branch)
106736
+ return null;
106737
+ const base2 = baseBranchFromLabels(issue2.labels) ?? deps.prBaseBranch;
106738
+ const result2 = await create3({ cwd: cwd2, branch, issue: issue2, base: base2, draft: true }, deps.cmdRunner);
106739
+ const url2 = result2?.url ?? null;
106740
+ if (url2) {
106741
+ deps.prByChange.set(changeName, url2);
106742
+ deps.invalidatePrUrlForIssue(issue2.id);
106743
+ }
106744
+ return url2;
106745
+ };
106746
+ }
106564
106747
  var GITHUB_PR_URL_RE, PR_NUMBER_RE, TICKET_IN_TITLE_RE;
106565
106748
  var init_pr_helpers = __esm(() => {
106566
106749
  init_linear();
106750
+ init_pr();
106567
106751
  GITHUB_PR_URL_RE = /^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/;
106568
106752
  PR_NUMBER_RE = /\/pull\/(\d+)/;
106569
106753
  TICKET_IN_TITLE_RE = /^([A-Za-z][A-Za-z0-9]*-\d+)\b/;
@@ -106853,8 +107037,8 @@ var init_linear_resolvers = __esm(() => {
106853
107037
  });
106854
107038
 
106855
107039
  // apps/agent/src/agent/wire/prepare.ts
106856
- import { mkdir as mkdir10 } from "fs/promises";
106857
- import { join as join25 } from "path";
107040
+ import { mkdir as mkdir8 } from "fs/promises";
107041
+ import { join as join26 } from "path";
106858
107042
  function createPrepareHelpers(input) {
106859
107043
  const {
106860
107044
  args,
@@ -106918,7 +107102,7 @@ function createPrepareHelpers(input) {
106918
107102
  let changeName;
106919
107103
  const wtLayoutPre = projectLayout(workerCwd);
106920
107104
  const derivedName = changeNameForIssue(issue2);
106921
- const tasksMdPath = join25(wtLayoutPre.changeDir(derivedName), "tasks.md");
107105
+ const tasksMdPath = join26(wtLayoutPre.changeDir(derivedName), "tasks.md");
106922
107106
  const tasksMdExists = await Bun.file(tasksMdPath).exists();
106923
107107
  const isFresh = !tasksMdExists;
106924
107108
  if (isFresh) {
@@ -106957,8 +107141,8 @@ function createPrepareHelpers(input) {
106957
107141
  changeName = await scaffoldChangeForIssue(scaffoldTasksDir, scaffoldStatesDir, issue2, comments, appendPrompt, attachments);
106958
107142
  } else {
106959
107143
  changeName = derivedName;
106960
- await mkdir10(wtLayoutPre.changeDir(changeName), { recursive: true });
106961
- await mkdir10(wtLayoutPre.taskStateDir(changeName), { recursive: true });
107144
+ await mkdir8(wtLayoutPre.changeDir(changeName), { recursive: true });
107145
+ await mkdir8(wtLayoutPre.taskStateDir(changeName), { recursive: true });
106962
107146
  }
106963
107147
  maps.cwdByChange.set(changeName, workerCwd);
106964
107148
  maps.statesDirByChange.set(changeName, scaffoldStatesDir);
@@ -106996,7 +107180,7 @@ function createPrepareHelpers(input) {
106996
107180
  if (!workerCwd)
106997
107181
  return;
106998
107182
  const wtLayout = projectLayout(workerCwd);
106999
- const tasksFile = join25(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
107183
+ const tasksFile = join26(wtLayout.changeDir(changeName), AGENT_TASKS_FILENAME);
107000
107184
  if (trigger === "review") {
107001
107185
  let body2;
107002
107186
  let heading;
@@ -107289,21 +107473,24 @@ var init_pr_discovery = __esm(() => {
107289
107473
  });
107290
107474
 
107291
107475
  // apps/agent/src/features/review-followup/scan.ts
107292
- import { dirname as dirname12, join as join26 } from "path";
107476
+ import { dirname as dirname11, join as join27 } from "path";
107293
107477
  async function resolveReviewStateDir(changeName, deps) {
107294
107478
  const root = deps.cwdOf(changeName);
107295
107479
  if (root)
107296
- return dirname12(projectLayout(root).stateFile(changeName));
107480
+ return dirname11(projectLayout(root).stateFile(changeName));
107297
107481
  if (!deps.useWorktree)
107298
- return dirname12(projectLayout(deps.projectRoot).stateFile(changeName));
107299
- const wtPath = join26(worktreesDir2(deps.projectRoot), changeName);
107482
+ return dirname11(projectLayout(deps.projectRoot).stateFile(changeName));
107483
+ const wtPath = join27(worktreesDir2(deps.projectRoot), changeName);
107300
107484
  const statePath = projectLayout(wtPath).stateFile(changeName);
107301
107485
  if (await Bun.file(statePath).exists())
107302
- return dirname12(statePath);
107486
+ return dirname11(statePath);
107303
107487
  return null;
107304
107488
  }
107305
107489
  async function readReviewWatermark(stateDir) {
107306
- const file2 = Bun.file(join26(stateDir, ".ralph-state.json"));
107490
+ const sidecar = await readSlotSidecar(stateDir, "review");
107491
+ if (sidecar)
107492
+ return sidecar.lastConsumedCommentAt ?? null;
107493
+ const file2 = Bun.file(join27(stateDir, ".ralph-state.json"));
107307
107494
  if (!await file2.exists())
107308
107495
  return null;
107309
107496
  try {
@@ -107515,7 +107702,7 @@ var init_github = __esm(() => {
107515
107702
 
107516
107703
  // apps/agent/src/agent/wire/mention-scan.ts
107517
107704
  import { readdir as readdir2 } from "fs/promises";
107518
- import { join as join27 } from "path";
107705
+ import { join as join28 } from "path";
107519
107706
  function createMentionScanner(input) {
107520
107707
  const {
107521
107708
  apiKey,
@@ -107681,7 +107868,7 @@ function createMentionScanner(input) {
107681
107868
  async function isChangeArchivedForIssue(issue2, cwdByChange, projectRoot) {
107682
107869
  const changeName = changeNameForIssue(issue2);
107683
107870
  const root = cwdByChange.get(changeName) ?? projectRoot;
107684
- const archiveDir = join27(projectLayout(root).tasksDir, "archive");
107871
+ const archiveDir = join28(projectLayout(root).tasksDir, "archive");
107685
107872
  let entries;
107686
107873
  try {
107687
107874
  entries = await readdir2(archiveDir);
@@ -107705,9 +107892,9 @@ var init_mention_scan = __esm(() => {
107705
107892
  });
107706
107893
 
107707
107894
  // apps/agent/src/agent/wire/spawn/default.ts
107708
- import { join as join28 } from "path";
107895
+ import { join as join29 } from "path";
107709
107896
  function defaultSpawn(changeName, cmd, cwd2, logsDir, onWorkerOutput, note) {
107710
- const logFilePath = join28(logsDir, `${changeName}.log`);
107897
+ const logFilePath = join29(logsDir, `${changeName}.log`);
107711
107898
  const ANSI_RE2 = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g;
107712
107899
  const BOX_ONLY_RE = /^[\s\u2500\u2502\u256D\u256E\u2570\u256F\u254C\u2504\u2501\u2503]+$/;
107713
107900
  const STATUS_BAR_LINE_RE = /^[\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F\u2713\u2717]\s+iter\s+\d+/;
@@ -107767,8 +107954,190 @@ var init_default2 = __esm(() => {
107767
107954
  init_log();
107768
107955
  });
107769
107956
 
107957
+ // apps/agent/src/agent/state/agent-run-state.ts
107958
+ import { basename as basename3, join as join30 } from "path";
107959
+ import { homedir as homedir6 } from "os";
107960
+ import { mkdir as mkdir9, writeFile } from "fs/promises";
107961
+ function agentRunStatePath(projectRoot) {
107962
+ return join30(homedir6(), ".ralph", basename3(projectRoot), "agent-state.json");
107963
+ }
107964
+ async function writeAgentRunState(state) {
107965
+ const path = agentRunStatePath(state.projectRoot);
107966
+ try {
107967
+ await mkdir9(join30(homedir6(), ".ralph", basename3(state.projectRoot)), { recursive: true });
107968
+ await writeFile(path, JSON.stringify(state, null, 2) + `
107969
+ `, "utf-8");
107970
+ } catch {}
107971
+ }
107972
+ var init_agent_run_state = () => {};
107973
+
107974
+ // packages/retro/src/disposition.ts
107975
+ function dispositionFromExitCode(code) {
107976
+ switch (code) {
107977
+ case 0:
107978
+ return "done";
107979
+ case NO_CHANGES_EXIT2:
107980
+ return "no-changes";
107981
+ case CI_FAILED_EXIT2:
107982
+ return "ci-failed";
107983
+ case PR_FAILED_EXIT2:
107984
+ return "pr-failed";
107985
+ default:
107986
+ return "error";
107987
+ }
107988
+ }
107989
+ var CI_FAILED_EXIT2 = 70, PR_FAILED_EXIT2 = 71, NO_CHANGES_EXIT2 = 72;
107990
+
107991
+ // packages/retro/src/paths.ts
107992
+ import { homedir as homedir7 } from "os";
107993
+ import { mkdir as mkdir10 } from "fs/promises";
107994
+ import { join as join31 } from "path";
107995
+ function retroDir() {
107996
+ return join31(homedir7(), ".ralph", "retro");
107997
+ }
107998
+ async function resolveRetroOutputPath(identifier, date5, dir = retroDir()) {
107999
+ await mkdir10(dir, { recursive: true });
108000
+ const base2 = join31(dir, `${identifier}-${date5}.md`);
108001
+ if (!await Bun.file(base2).exists())
108002
+ return base2;
108003
+ for (let n = 2;; n++) {
108004
+ const candidate = join31(dir, `${identifier}-${date5}-${n}.md`);
108005
+ if (!await Bun.file(candidate).exists())
108006
+ return candidate;
108007
+ }
108008
+ }
108009
+ var init_paths2 = () => {};
108010
+
108011
+ // packages/retro/src/prompt.ts
108012
+ function buildRetroPrompt(ctx, outputPath) {
108013
+ const disposition = dispositionFromExitCode(ctx.exitCode);
108014
+ const { paths } = ctx;
108015
+ const line = (label, value) => value ? `- ${label}: ${value}` : `- ${label}: (unavailable \u2014 note this in the report)`;
108016
+ return [
108017
+ `You are a retrospective analysis agent reviewing a finished automated ticket run.`,
108018
+ `Your job is to read the run's artifacts and write a thorough, honest self-review`,
108019
+ `to a markdown file. You are NOT fixing anything \u2014 this is analysis only.`,
108020
+ ``,
108021
+ `## Ticket`,
108022
+ ``,
108023
+ `- Identifier: ${ctx.identifier}`,
108024
+ `- Change name: ${ctx.changeName}`,
108025
+ `- Terminal disposition: ${disposition} (worker exit code ${ctx.exitCode})`,
108026
+ ctx.prUrl ? `- Pull request: ${ctx.prUrl}` : `- Pull request: none was opened`,
108027
+ `- Date: ${ctx.date}`,
108028
+ ``,
108029
+ `### Ticket details`,
108030
+ ``,
108031
+ ctx.ticketDigest,
108032
+ ``,
108033
+ `## Data sources`,
108034
+ ``,
108035
+ `Read whatever of the following exist. If a path is missing or empty, say so`,
108036
+ `explicitly in the report rather than guessing.`,
108037
+ ``,
108038
+ line("Change directory (proposal/design/tasks/specs)", paths.changeDir),
108039
+ line("Loop state file", paths.stateFilePath),
108040
+ line("Worker log", paths.logFile),
108041
+ line("JSON event log", paths.jsonLogFile),
108042
+ line("Agent run state", paths.agentStateFile),
108043
+ ctx.prUrl ? `- You may inspect the PR read-only with \`gh pr view ${ctx.prUrl}\` and \`gh pr diff ${ctx.prUrl}\`.` : `- No PR exists; skip the PR section and note "no PR".`,
108044
+ ``,
108045
+ `## Required report structure`,
108046
+ ``,
108047
+ `Write GitHub-flavored markdown with these sections:`,
108048
+ `1. **Summary** \u2014 what the ticket asked for and how the run ended.`,
108049
+ `2. **What went well** \u2014 concrete things the run did right.`,
108050
+ `3. **What went wrong / friction** \u2014 failures, retries, wasted iterations,`,
108051
+ ` wrong turns, anything that cost time or quality.`,
108052
+ `4. **Root-cause analysis** \u2014 for each problem, why it happened.`,
108053
+ `5. **Recommendations** \u2014 specific, actionable improvements (to the prompt,`,
108054
+ ` the tasks, the codebase, or the workflow).`,
108055
+ `6. **Data gaps** \u2014 which data sources were unavailable or unread.`,
108056
+ ``,
108057
+ `## Output`,
108058
+ ``,
108059
+ `Write the complete report to this exact path using your file-write tool:`,
108060
+ ``,
108061
+ ` ${outputPath}`,
108062
+ ``,
108063
+ `## Hard rules`,
108064
+ ``,
108065
+ `- Do NOT run any git mutation: no commit, add, push, rebase, reset, checkout,`,
108066
+ ` branch, merge, tag, or stash.`,
108067
+ `- Do NOT create, edit, comment on, close, or merge any pull request or issue.`,
108068
+ `- Do NOT modify any source file. The ONLY file you may write is the report at`,
108069
+ ` the path above.`,
108070
+ `- Read-only inspection commands (\`git log\`, \`git diff\`, \`gh pr view\`,`,
108071
+ ` \`gh pr diff\`, reading files) are allowed.`
108072
+ ].join(`
108073
+ `);
108074
+ }
108075
+ var init_prompt = () => {};
108076
+
108077
+ // packages/retro/src/retro.ts
108078
+ async function runRetrospective(ctx, deps) {
108079
+ const { log: log3, runEngine: runEngine2, seen } = deps;
108080
+ const disposition = dispositionFromExitCode(ctx.exitCode);
108081
+ const key = `${ctx.identifier}:${disposition}:${ctx.date}`;
108082
+ if (seen.has(key)) {
108083
+ log3(` retrospective skipped for ${ctx.identifier} (already generated this run)`, "gray");
108084
+ return { written: false, skipped: "duplicate", disposition };
108085
+ }
108086
+ seen.add(key);
108087
+ try {
108088
+ const outputPath = await resolveRetroOutputPath(ctx.identifier, ctx.date);
108089
+ const prompt = buildRetroPrompt(ctx, outputPath);
108090
+ log3(` running retrospective for ${ctx.identifier} (${disposition}) \u2192 ${outputPath}`, "cyan");
108091
+ await runEngine2({
108092
+ engine: ctx.engine,
108093
+ model: ctx.model,
108094
+ prompt,
108095
+ cwd: ctx.cwd,
108096
+ onOutput: (l) => log3(l, "gray")
108097
+ });
108098
+ const written = await Bun.file(outputPath).exists();
108099
+ if (written) {
108100
+ log3(` retrospective written: ${outputPath}`, "green");
108101
+ } else {
108102
+ log3(`! retrospective engine finished but no report was written at ${outputPath}`, "yellow");
108103
+ }
108104
+ return { written, outputPath, disposition };
108105
+ } catch (err) {
108106
+ log3(`! retrospective failed for ${ctx.identifier}: ${err.message}`, "yellow");
108107
+ return { written: false, disposition };
108108
+ }
108109
+ }
108110
+ var init_retro = __esm(() => {
108111
+ init_paths2();
108112
+ init_prompt();
108113
+ init_paths2();
108114
+ init_prompt();
108115
+ });
108116
+
107770
108117
  // apps/agent/src/agent/wire/spawn/worker.ts
107771
- import { join as join29 } from "path";
108118
+ import { join as join32 } from "path";
108119
+ function localDateStamp(d) {
108120
+ const y = d.getFullYear();
108121
+ const m = String(d.getMonth() + 1).padStart(2, "0");
108122
+ const day = String(d.getDate()).padStart(2, "0");
108123
+ return `${y}-${m}-${day}`;
108124
+ }
108125
+ function buildTicketDigest(issue2, comments) {
108126
+ if (!issue2)
108127
+ return "(ticket details unavailable)";
108128
+ const lines = [`Title: ${issue2.title}`, "", issue2.description?.trim() || "(no description)"];
108129
+ if (comments.length > 0) {
108130
+ lines.push("", "Comments:");
108131
+ for (const c of comments) {
108132
+ lines.push(`- ${c.user?.name ?? "unknown"}: ${c.body}`);
108133
+ }
108134
+ }
108135
+ return lines.join(`
108136
+ `);
108137
+ }
108138
+ function retroDepEntry(agentDebug, hook) {
108139
+ return agentDebug ? { runRetrospective: hook } : {};
108140
+ }
107772
108141
  function createSpawnWorker(input) {
107773
108142
  const {
107774
108143
  args,
@@ -107847,10 +108216,52 @@ function createSpawnWorker(input) {
107847
108216
  c.push("--from-agent");
107848
108217
  return c;
107849
108218
  }
108219
+ const retroSeen = new Set;
108220
+ const runRetrospectiveHook = async (info) => {
108221
+ try {
108222
+ const identifier = info.issue?.identifier ?? info.changeName;
108223
+ const prUrl = prByChange?.get(info.changeName) ?? null;
108224
+ let digest = "(ticket details unavailable)";
108225
+ if (info.issue) {
108226
+ let comments = [];
108227
+ try {
108228
+ comments = await fetchIssueComments(apiKey, info.issue.id);
108229
+ } catch {}
108230
+ digest = buildTicketDigest(info.issue, comments);
108231
+ }
108232
+ const engine = args.engineSet ? args.engine : cfg.engine;
108233
+ const model = args.engineSet ? args.model : cfg.model;
108234
+ const ctx = {
108235
+ identifier,
108236
+ changeName: info.changeName,
108237
+ cwd: info.cwd,
108238
+ engine,
108239
+ model,
108240
+ exitCode: info.effectiveCode,
108241
+ prUrl,
108242
+ date: localDateStamp(new Date),
108243
+ ticketDigest: digest,
108244
+ paths: {
108245
+ changeDir: info.changeDir,
108246
+ stateFilePath: info.stateFilePath,
108247
+ logFile: join32(logsDir, `${info.changeName}.log`),
108248
+ jsonLogFile: args.jsonLogFile ?? null,
108249
+ agentStateFile: agentRunStatePath(projectRoot)
108250
+ }
108251
+ };
108252
+ await runRetrospective(ctx, {
108253
+ runEngine: (opts) => runEngine(opts),
108254
+ log: onLog,
108255
+ seen: retroSeen
108256
+ });
108257
+ } catch (err) {
108258
+ onLog(`! retrospective failed: ${err.message}`, "yellow");
108259
+ }
108260
+ };
107850
108261
  return function spawnWorker(changeName, _issue, trigger) {
107851
108262
  const cwd2 = cwdByChange.get(changeName) ?? projectRoot;
107852
108263
  const injected = runners?.spawnWorker;
107853
- const missionTasksPath = join29(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
108264
+ const missionTasksPath = join32(projectLayout(cwd2).changeDir(changeName), MISSION_TASKS_FILENAME);
107854
108265
  const prevTasksPromise = (async () => {
107855
108266
  const f2 = Bun.file(missionTasksPath);
107856
108267
  return await f2.exists() ? await f2.text() : "";
@@ -107858,7 +108269,7 @@ function createSpawnWorker(input) {
107858
108269
  let logFilePath;
107859
108270
  let handle;
107860
108271
  if (injected) {
107861
- logFilePath = join29(logsDir, `${changeName}.log`);
108272
+ logFilePath = join32(logsDir, `${changeName}.log`);
107862
108273
  handle = injected(buildTaskCmdFor(changeName), cwd2);
107863
108274
  } else {
107864
108275
  const r = defaultSpawn(changeName, buildTaskCmdFor(changeName), cwd2, logsDir, onWorkerOutput, `spawn at ${new Date().toISOString()}`);
@@ -107880,7 +108291,7 @@ function createSpawnWorker(input) {
107880
108291
  const wantAutoMerge = issueForChange ? issueMatchesGetIndicator(issueForChange, indicators.getAutoMerge) : false;
107881
108292
  const wrapped = handle.exited.then(async (code) => {
107882
108293
  const workerLayout = projectLayout(cwd2);
107883
- const validateSpecPath = join29(workerLayout.changeDir(changeName), "specs", "validate.md");
108294
+ const validateSpecPath = join32(workerLayout.changeDir(changeName), "specs", "validate.md");
107884
108295
  const hasValidateSpec = await Bun.file(validateSpecPath).exists();
107885
108296
  const wantValidateOnly = hasValidateSpec && !wantPrBase;
107886
108297
  if (hasValidateSpec) {
@@ -107965,6 +108376,7 @@ function createSpawnWorker(input) {
107965
108376
  git: gitRunner,
107966
108377
  log: onLog,
107967
108378
  runScript,
108379
+ ...retroDepEntry(args.agentDebug, runRetrospectiveHook),
107968
108380
  registerPr: (cn, url2) => onPrRegistered(cn, url2),
107969
108381
  ...onWorkerPhase && {
107970
108382
  onPhase: (phase2, detail) => onWorkerPhase(changeName, phase2, detail)
@@ -108000,6 +108412,9 @@ var init_worker = __esm(() => {
108000
108412
  init_runners();
108001
108413
  init_pr_helpers();
108002
108414
  init_wait_for_mergeability();
108415
+ init_agent_run_state();
108416
+ init_retro();
108417
+ init_engine();
108003
108418
  });
108004
108419
 
108005
108420
  // apps/agent/src/agent/baseline/runner.ts
@@ -108342,44 +108757,33 @@ var init_linear_sync = __esm(() => {
108342
108757
  });
108343
108758
 
108344
108759
  // apps/agent/src/agent/linear-sync/comment-sync.ts
108345
- import { dirname as dirname13, join as join30 } from "path";
108346
- import { mkdir as mkdir11, rename, unlink as unlink2 } from "fs/promises";
108347
- async function readStateJson(statePath) {
108760
+ import { dirname as dirname12, join as join33 } from "path";
108761
+ async function readInlineLinearComments(statePath) {
108348
108762
  const file2 = Bun.file(statePath);
108349
108763
  if (!await file2.exists())
108350
- return null;
108764
+ return;
108351
108765
  try {
108352
- return await file2.json();
108766
+ const obj = await file2.json();
108767
+ return obj.linearComments ?? undefined;
108353
108768
  } catch {
108354
- return null;
108355
- }
108356
- }
108357
- async function writeStateJson(statePath, state) {
108358
- await mkdir11(dirname13(statePath), { recursive: true });
108359
- const tmp = `${statePath}.tmp-${process.pid}-${writeStateSeq++}`;
108360
- try {
108361
- await Bun.write(tmp, JSON.stringify(state, null, 2) + `
108362
- `);
108363
- await rename(tmp, statePath);
108364
- } catch (err) {
108365
- await unlink2(tmp).catch(() => {});
108366
- throw err;
108769
+ return;
108367
108770
  }
108368
108771
  }
108369
- function readComments(state) {
108370
- const raw = state?.linearComments ?? {};
108772
+ async function readComments(statePath) {
108773
+ const changeDir = dirname12(statePath);
108774
+ const raw = await readSlotSidecar(changeDir, "linearComments") ?? await readInlineLinearComments(statePath) ?? {};
108775
+ const r = raw;
108371
108776
  return {
108372
- planCommentId: raw?.planCommentId ?? null,
108373
- tasksCommentId: raw?.tasksCommentId ?? null,
108374
- planPostedAt: raw?.planPostedAt ?? null,
108375
- tasksCommentSha256: raw?.tasksCommentSha256 ?? null
108777
+ planCommentId: r.planCommentId ?? null,
108778
+ tasksCommentId: r.tasksCommentId ?? null,
108779
+ planPostedAt: r.planPostedAt ?? null,
108780
+ tasksCommentSha256: r.tasksCommentSha256 ?? null
108376
108781
  };
108377
108782
  }
108378
108783
  async function patchComments(statePath, patch) {
108379
- const existing = await readStateJson(statePath) ?? {};
108380
- const current = readComments(existing);
108784
+ const current = await readComments(statePath);
108381
108785
  const next = { ...current, ...patch };
108382
- await writeStateJson(statePath, { ...existing, linearComments: next });
108786
+ await writeSlotField(dirname12(statePath), "linearComments", next);
108383
108787
  }
108384
108788
  function isCommentNotFoundError(err) {
108385
108789
  if (!err)
@@ -108394,7 +108798,7 @@ function isCommentNotFoundError(err) {
108394
108798
  return text.includes("not found") || text.includes("could not find") || text.includes("entity not found");
108395
108799
  }
108396
108800
  async function readTasksMd(changeDir, log3) {
108397
- const file2 = Bun.file(join30(changeDir, "tasks.md"));
108801
+ const file2 = Bun.file(join33(changeDir, "tasks.md"));
108398
108802
  if (!await file2.exists()) {
108399
108803
  log3(` comment-sync: tasks.md missing in ${changeDir}, skipping`, "gray");
108400
108804
  return null;
@@ -108415,8 +108819,7 @@ async function postOrUpdateTasksComment(deps) {
108415
108819
  return null;
108416
108820
  const body = renderTasksCommentBody(tasksMd, deps.changeName, deps.iteration);
108417
108821
  const hash2 = sha256Hex(tasksMd);
108418
- const state = await readStateJson(deps.statePath);
108419
- const comments = readComments(state);
108822
+ const comments = await readComments(deps.statePath);
108420
108823
  if (comments.tasksCommentId) {
108421
108824
  if (comments.tasksCommentSha256 === hash2) {
108422
108825
  deps.log(` comment-sync: tasks.md unchanged for ${deps.changeName}, skipping`, "gray");
@@ -108492,8 +108895,7 @@ async function readSection(path, heading) {
108492
108895
  return body.trim() || null;
108493
108896
  }
108494
108897
  async function postPlanCommentOnce(deps) {
108495
- const state = await readStateJson(deps.statePath);
108496
- const comments = readComments(state);
108898
+ const comments = await readComments(deps.statePath);
108497
108899
  if (comments.planCommentId)
108498
108900
  return null;
108499
108901
  const tasksMd = await readTasksMd(deps.changeDir, deps.log);
@@ -108502,14 +108904,14 @@ async function postPlanCommentOnce(deps) {
108502
108904
  const check2 = parsePlanningSection(tasksMd);
108503
108905
  if (!check2.allChecked)
108504
108906
  return null;
108505
- const proposalPath = join30(deps.changeDir, "proposal.md");
108907
+ const proposalPath = join33(deps.changeDir, "proposal.md");
108506
108908
  const why = await readSection(proposalPath, "Why");
108507
108909
  const whatChanges = await readSection(proposalPath, "What Changes");
108508
108910
  if (!why && !whatChanges) {
108509
108911
  deps.log(` comment-sync: proposal.md has no Why/What Changes, skipping plan comment`, "gray");
108510
108912
  return null;
108511
108913
  }
108512
- const designSummary = await readFirstParagraph(join30(deps.changeDir, "design.md"));
108914
+ const designSummary = await readFirstParagraph(join33(deps.changeDir, "design.md"));
108513
108915
  const parts = [`### ${PLAN_COMMENT_TITLE} \u2014 \`${deps.changeName}\``];
108514
108916
  if (why) {
108515
108917
  parts.push("", "**Why**", "", why);
@@ -108547,8 +108949,7 @@ ${deps.message.trim()}`;
108547
108949
  } catch (err) {
108548
108950
  deps.log(`! comment-sync: steering comment create failed: ${err.message}`, "yellow");
108549
108951
  }
108550
- const state = await readStateJson(deps.statePath);
108551
- const comments = readComments(state);
108952
+ const comments = await readComments(deps.statePath);
108552
108953
  if (comments.tasksCommentId) {
108553
108954
  try {
108554
108955
  await deps.mutations.deleteIssueComment(deps.apiKey, comments.tasksCommentId);
@@ -108571,8 +108972,9 @@ ${deps.message.trim()}`;
108571
108972
  iteration: deps.iteration
108572
108973
  });
108573
108974
  }
108574
- var PLAN_COMMENT_TITLE = "\uD83D\uDCCB Ralph plan", STEERING_COMMENT_TITLE = "\uD83E\uDDED Ralph steering", writeStateSeq = 0;
108975
+ var PLAN_COMMENT_TITLE = "\uD83D\uDCCB Ralph plan", STEERING_COMMENT_TITLE = "\uD83E\uDDED Ralph steering";
108575
108976
  var init_comment_sync = __esm(() => {
108977
+ init_store();
108576
108978
  init_linear_sync();
108577
108979
  });
108578
108980
 
@@ -260785,7 +261187,7 @@ var init_render_pdf = __esm(() => {
260785
261187
  });
260786
261188
 
260787
261189
  // apps/agent/src/agent/linear-sync/spec-attachments.ts
260788
- import { dirname as dirname14, join as join31 } from "path";
261190
+ import { dirname as dirname13, join as join34 } from "path";
260789
261191
  function describeLinearError(err) {
260790
261192
  const e = err;
260791
261193
  const parts = [e.message ?? String(err)];
@@ -260800,25 +261202,29 @@ function describeLinearError(err) {
260800
261202
  return parts.join(" ");
260801
261203
  }
260802
261204
  function stateDirOf(statePath) {
260803
- return dirname14(statePath);
261205
+ return dirname13(statePath);
260804
261206
  }
260805
- async function readRawState(statePath) {
261207
+ async function readInlineSpecAttachments(statePath) {
260806
261208
  const file2 = Bun.file(statePath);
260807
261209
  if (!await file2.exists())
260808
261210
  return {};
260809
261211
  try {
260810
261212
  const parsed = await file2.json();
260811
261213
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
260812
- return parsed;
261214
+ const sa = parsed.specAttachments;
261215
+ return sa && typeof sa === "object" && !Array.isArray(sa) ? sa : {};
260813
261216
  }
260814
261217
  return {};
260815
261218
  } catch {
260816
261219
  return {};
260817
261220
  }
260818
261221
  }
261222
+ async function readSpecAttachmentsSubtree(statePath) {
261223
+ const sidecar = await readSlotSidecar(dirname13(statePath), "specAttachments");
261224
+ return sidecar ?? await readInlineSpecAttachments(statePath);
261225
+ }
260819
261226
  async function readSpecAttachments(statePath) {
260820
- const raw = await readRawState(statePath);
260821
- const sa = raw.specAttachments ?? {};
261227
+ const sa = await readSpecAttachmentsSubtree(statePath);
260822
261228
  return {
260823
261229
  proposal: {
260824
261230
  attachmentId: sa.proposal?.attachmentId ?? null,
@@ -260872,12 +261278,25 @@ function hasMeaningfulContent(bytes) {
260872
261278
  }
260873
261279
  return false;
260874
261280
  }
261281
+ function extractImplementationSection(tasksMarkdown) {
261282
+ const captured = [];
261283
+ let capturing = false;
261284
+ for (const line of tasksMarkdown.split(/\r?\n/)) {
261285
+ const heading = /^##\s+(.+?)\s*$/.exec(line)?.[1];
261286
+ if (heading !== undefined)
261287
+ capturing = heading.trim().toLowerCase() === "implementation";
261288
+ if (capturing)
261289
+ captured.push(line);
261290
+ }
261291
+ return captured.join(`
261292
+ `).trim();
261293
+ }
260875
261294
  async function syncSlot(deps, slot) {
260876
261295
  const spec = SLOT_SPECS[slot];
260877
261296
  const [primaryName, ...trailingNames] = spec.sourceFiles;
260878
261297
  if (!primaryName)
260879
261298
  return;
260880
- const primary = Bun.file(join31(deps.changeDir, primaryName));
261299
+ const primary = Bun.file(join34(deps.changeDir, primaryName));
260881
261300
  if (!await primary.exists()) {
260882
261301
  deps.log(` spec-attachments: ${primaryName} missing, skipping`, "gray");
260883
261302
  return;
@@ -260896,24 +261315,28 @@ async function syncSlot(deps, slot) {
260896
261315
  const parts = [primaryBytes];
260897
261316
  const enc = new TextEncoder;
260898
261317
  for (const name of trailingNames) {
260899
- const f2 = Bun.file(join31(deps.changeDir, name));
261318
+ const f2 = Bun.file(join34(deps.changeDir, name));
260900
261319
  if (!await f2.exists())
260901
261320
  continue;
261321
+ let raw;
260902
261322
  try {
260903
- const bytes = await f2.bytes();
260904
- if (bytes.length === 0)
260905
- continue;
260906
- parts.push(enc.encode(`
261323
+ raw = await f2.bytes();
261324
+ } catch (err) {
261325
+ deps.log(`! spec-attachments: read ${name} failed (continuing without it): ${err.message}`, "yellow");
261326
+ continue;
261327
+ }
261328
+ if (raw.length === 0)
261329
+ continue;
261330
+ const decoded = new TextDecoder().decode(raw);
261331
+ const body = name === "tasks.md" ? extractImplementationSection(decoded) : decoded.trim();
261332
+ if (!body)
261333
+ continue;
261334
+ parts.push(enc.encode(`
260907
261335
 
260908
261336
  ---
260909
261337
 
260910
- # ${name}
260911
-
261338
+ ${body}
260912
261339
  `));
260913
- parts.push(bytes);
260914
- } catch (err) {
260915
- deps.log(`! spec-attachments: read ${name} failed (continuing without it): ${err.message}`, "yellow");
260916
- }
260917
261340
  }
260918
261341
  const totalLen = parts.reduce((n, p) => n + p.length, 0);
260919
261342
  const sourceBytes = new Uint8Array(totalLen);
@@ -260982,8 +261405,7 @@ async function syncSlot(deps, slot) {
260982
261405
  deps.log(` spec-attachments: created ${spec.uploadFilename} attachment`, "gray");
260983
261406
  }
260984
261407
  async function purgeLegacyProposalSlots(deps) {
260985
- const raw = await readRawState(deps.statePath);
260986
- const sa = raw.specAttachments ?? {};
261408
+ const sa = await readSpecAttachmentsSubtree(deps.statePath);
260987
261409
  if (sa.legacyProposalPurged === true)
260988
261410
  return;
260989
261411
  const state = await readSpecAttachments(deps.statePath);
@@ -261161,9 +261583,9 @@ var init_comment_sync2 = __esm(() => {
261161
261583
  });
261162
261584
 
261163
261585
  // apps/agent/src/features/pr-tracker/state.ts
261164
- import { join as join32 } from "path";
261586
+ import { join as join35 } from "path";
261165
261587
  async function readState2(projectRoot) {
261166
- const path = join32(projectRoot, PR_TRACKER_STATE_RELPATH);
261588
+ const path = join35(projectRoot, PR_TRACKER_STATE_RELPATH);
261167
261589
  const file2 = Bun.file(path);
261168
261590
  if (!await file2.exists())
261169
261591
  return {};
@@ -261179,7 +261601,7 @@ async function readState2(projectRoot) {
261179
261601
  }
261180
261602
  }
261181
261603
  async function writeState2(projectRoot, state) {
261182
- const path = join32(projectRoot, PR_TRACKER_STATE_RELPATH);
261604
+ const path = join35(projectRoot, PR_TRACKER_STATE_RELPATH);
261183
261605
  await Bun.write(path, JSON.stringify(state, null, 2));
261184
261606
  }
261185
261607
  var PR_TRACKER_STATE_RELPATH = ".ralph/pr-tracker-state.json";
@@ -261258,7 +261680,7 @@ var init_pr_tracker = __esm(() => {
261258
261680
  });
261259
261681
 
261260
261682
  // apps/agent/src/agent/wire.ts
261261
- import { join as join33 } from "path";
261683
+ import { join as join36 } from "path";
261262
261684
  function buildAgentCoordinator(input) {
261263
261685
  const {
261264
261686
  args,
@@ -261277,7 +261699,7 @@ function buildAgentCoordinator(input) {
261277
261699
  onWorkerCmd,
261278
261700
  onAwaitingTicket
261279
261701
  } = input;
261280
- const logsDir = join33(projectRoot, ".ralph", "logs");
261702
+ const logsDir = join36(projectRoot, ".ralph", "logs");
261281
261703
  const bus = createBus();
261282
261704
  subscribeAgentDiag(bus, onLog);
261283
261705
  const diag = (area, message, color) => {
@@ -261412,6 +261834,13 @@ function buildAgentCoordinator(input) {
261412
261834
  ...onWorkerOutput ? { onWorkerOutput } : {},
261413
261835
  ...onWorkerCmd ? { onWorkerCmd } : {}
261414
261836
  });
261837
+ const openDraftPr = createOpenDraftPr({
261838
+ branchByChange,
261839
+ prByChange,
261840
+ cmdRunner,
261841
+ prBaseBranch: cfg.prBaseBranch,
261842
+ invalidatePrUrlForIssue: (issueId) => prDiscovery.invalidatePrUrlForIssue(issueId)
261843
+ });
261415
261844
  const confirmationCaps = {
261416
261845
  detect: (issue2) => processAwaitingForIssue(issue2, {
261417
261846
  cfg,
@@ -261424,6 +261853,7 @@ function buildAgentCoordinator(input) {
261424
261853
  reapForAwaiting: (cn) => coordRef.current?.reapForAwaiting(cn),
261425
261854
  applyIndicator: resolvers.applyIndicator,
261426
261855
  applyMarker: resolvers.applyMarker,
261856
+ openDraftPr,
261427
261857
  ...onAwaitingTicket ? { onAwaitingTicket } : {},
261428
261858
  onLog
261429
261859
  }),
@@ -261493,6 +261923,18 @@ function buildAgentCoordinator(input) {
261493
261923
  const json2 = await file2.json();
261494
261924
  return json2.iteration ?? 0;
261495
261925
  },
261926
+ getTasksFingerprint: async (changeName) => {
261927
+ const root = cwdByChange.get(changeName) ?? projectRoot;
261928
+ const changeDir = projectLayout(root).changeDir(changeName);
261929
+ const parts = [];
261930
+ for (const name of ["tasks.md", "proposal.md", "design.md"]) {
261931
+ const file2 = Bun.file(join36(changeDir, name));
261932
+ if (!await file2.exists())
261933
+ continue;
261934
+ parts.push(`${name}:${file2.lastModified}:${file2.size}`);
261935
+ }
261936
+ return parts.length > 0 ? parts.join("|") : null;
261937
+ },
261496
261938
  ...commentSync.enabled && commentSync.syncTasks ? { syncTasks: commentSync.syncTasks } : {},
261497
261939
  ...commentSync.enabled && commentSync.onSteeringAppended ? { onSteeringAppended: commentSync.onSteeringAppended } : {}
261498
261940
  }, {
@@ -261531,7 +261973,7 @@ function buildAgentCoordinator(input) {
261531
261973
  getGaveUpTotal: async () => {
261532
261974
  let total = 0;
261533
261975
  for (const [changeName, root] of cwdByChange) {
261534
- const file2 = Bun.file(join33(projectLayout(root).taskStateDir(changeName), GAVEUP_COUNT_FILE));
261976
+ const file2 = Bun.file(join36(projectLayout(root).taskStateDir(changeName), GAVEUP_COUNT_FILE));
261535
261977
  if (!await file2.exists())
261536
261978
  continue;
261537
261979
  try {
@@ -261566,14 +262008,14 @@ var init_wire = __esm(() => {
261566
262008
  });
261567
262009
 
261568
262010
  // apps/agent/src/agent/json-log/json-log-file.ts
261569
- import { mkdir as mkdir12, appendFile as appendFile2 } from "fs/promises";
261570
- import { dirname as dirname15 } from "path";
262011
+ import { mkdir as mkdir11, appendFile as appendFile2 } from "fs/promises";
262012
+ import { dirname as dirname14 } from "path";
261571
262013
  function createJsonLogFileSink(path) {
261572
262014
  if (!path)
261573
262015
  return { emit: () => {} };
261574
262016
  let chain = (async () => {
261575
262017
  try {
261576
- await mkdir12(dirname15(path), { recursive: true });
262018
+ await mkdir11(dirname14(path), { recursive: true });
261577
262019
  await Bun.write(path, "");
261578
262020
  } catch {}
261579
262021
  })();
@@ -261819,7 +262261,7 @@ var init_output_utils = __esm(() => {
261819
262261
  });
261820
262262
 
261821
262263
  // apps/agent/src/agent/state/worker-state-poll.ts
261822
- import { join as join34 } from "path";
262264
+ import { join as join37 } from "path";
261823
262265
  function parseSubtasks(tasksMd) {
261824
262266
  const out = [];
261825
262267
  let skipSection = false;
@@ -261852,7 +262294,7 @@ function initialWorkerSnapshot() {
261852
262294
  async function readWorkerSnapshot(input) {
261853
262295
  const next = { ...input.prev };
261854
262296
  try {
261855
- const file2 = Bun.file(join34(input.statesDir, input.changeName, ".ralph-state.json"));
262297
+ const file2 = Bun.file(join37(input.statesDir, input.changeName, ".ralph-state.json"));
261856
262298
  if (await file2.exists()) {
261857
262299
  const json2 = await file2.json();
261858
262300
  next.iter = json2.iteration ?? next.iter;
@@ -261861,10 +262303,10 @@ async function readWorkerSnapshot(input) {
261861
262303
  } catch {}
261862
262304
  if (input.changeDir) {
261863
262305
  try {
261864
- const tasksFile = Bun.file(join34(input.changeDir, "tasks.md"));
261865
- const proposalFile = Bun.file(join34(input.changeDir, "proposal.md"));
261866
- const designFile = Bun.file(join34(input.changeDir, "design.md"));
261867
- const reviewFindingsFile = Bun.file(join34(input.changeDir, "review-findings.md"));
262306
+ const tasksFile = Bun.file(join37(input.changeDir, "tasks.md"));
262307
+ const proposalFile = Bun.file(join37(input.changeDir, "proposal.md"));
262308
+ const designFile = Bun.file(join37(input.changeDir, "design.md"));
262309
+ const reviewFindingsFile = Bun.file(join37(input.changeDir, "review-findings.md"));
261868
262310
  const [tasksText, proposalText, designText, reviewFindingsText] = await Promise.all([
261869
262311
  tasksFile.exists().then((ok) => ok ? tasksFile.text() : null),
261870
262312
  proposalFile.exists().then((ok) => ok ? proposalFile.text() : null),
@@ -261927,7 +262369,7 @@ var init_worker_state_poll = __esm(() => {
261927
262369
  });
261928
262370
 
261929
262371
  // apps/agent/src/components/AgentMode.tsx
261930
- import { join as join35 } from "path";
262372
+ import { join as join38 } from "path";
261931
262373
  async function appendSteeringImpl(changeDir, message) {
261932
262374
  await runWithContext(createDefaultContext(), async () => {
261933
262375
  appendSteeringMessage(changeDir, message);
@@ -263500,7 +263942,7 @@ function AgentMode({
263500
263942
  },
263501
263943
  onSubmit: async (message) => {
263502
263944
  try {
263503
- await appendSteering2(join35(tasksDir, w2.changeName), message);
263945
+ await appendSteering2(join38(tasksDir, w2.changeName), message);
263504
263946
  fileEmit({ type: "steering_submitted", changeName: w2.changeName, message });
263505
263947
  } catch (err) {
263506
263948
  const text = err.message;
@@ -263614,7 +264056,7 @@ function shouldFallbackToJsonOutput(args, stdinIsTty) {
263614
264056
  }
263615
264057
 
263616
264058
  // apps/agent/src/runtime/tmux.ts
263617
- import { basename as basename3 } from "path";
264059
+ import { basename as basename4 } from "path";
263618
264060
  function tmuxAvailable() {
263619
264061
  const result2 = Bun.spawnSync({ cmd: ["tmux", "-V"], stderr: "pipe" });
263620
264062
  return result2.exitCode === 0;
@@ -263623,7 +264065,7 @@ function sessionName(projectRoot) {
263623
264065
  const override = process.env["RALPH_SESSION_NAME"];
263624
264066
  if (override)
263625
264067
  return override;
263626
- return `ralphy-agent-${basename3(projectRoot)}`;
264068
+ return `ralphy-agent-${basename4(projectRoot)}`;
263627
264069
  }
263628
264070
  function sessionExists(name) {
263629
264071
  const result2 = Bun.spawnSync({ cmd: ["tmux", "has-session", "-t", name], stderr: "pipe" });
@@ -263804,7 +264246,7 @@ __export(exports_list, {
263804
264246
  buildBuckets: () => buildBuckets,
263805
264247
  backlogRankByIssueId: () => backlogRankByIssueId
263806
264248
  });
263807
- import { join as join36 } from "path";
264249
+ import { join as join39 } from "path";
263808
264250
  function countTaskItems(content) {
263809
264251
  const checked = (content.match(/^- \[x\]/gm) ?? []).length;
263810
264252
  const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
@@ -263820,13 +264262,13 @@ function buildLocalRows() {
263820
264262
  const sources = [{ dir: statesDir, label: "main" }];
263821
264263
  const worktreesRoot = worktreesDir2(projectRoot);
263822
264264
  for (const wt of storage.list(worktreesRoot)) {
263823
- sources.push({ dir: join36(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
264265
+ sources.push({ dir: join39(worktreesRoot, wt, ".ralph", "tasks"), label: `wt:${wt}` });
263824
264266
  }
263825
264267
  for (const { dir, label } of sources) {
263826
264268
  for (const entry of storage.list(dir)) {
263827
264269
  if (seen.has(entry))
263828
264270
  continue;
263829
- const raw = storage.read(join36(dir, entry, ".ralph-state.json"));
264271
+ const raw = storage.read(join39(dir, entry, ".ralph-state.json"));
263830
264272
  if (raw === null)
263831
264273
  continue;
263832
264274
  let state;
@@ -263841,7 +264283,7 @@ function buildLocalRows() {
263841
264283
  const firstLine = promptRaw.split(`
263842
264284
  `).find((l3) => l3.trim() !== "") ?? "";
263843
264285
  let progress = "\u2014";
263844
- const tasksContent = storage.read(join36(dir, entry, "tasks.md"));
264286
+ const tasksContent = storage.read(join39(dir, entry, "tasks.md"));
263845
264287
  if (tasksContent !== null) {
263846
264288
  const { checked, unchecked } = countTaskItems(tasksContent);
263847
264289
  const total = checked + unchecked;
@@ -264336,31 +264778,14 @@ var init_list = __esm(() => {
264336
264778
  };
264337
264779
  });
264338
264780
 
264339
- // apps/agent/src/agent/state/agent-run-state.ts
264340
- import { basename as basename4, join as join37 } from "path";
264341
- import { homedir as homedir6 } from "os";
264342
- import { mkdir as mkdir13, writeFile } from "fs/promises";
264343
- function agentRunStatePath(projectRoot) {
264344
- return join37(homedir6(), ".ralph", basename4(projectRoot), "agent-state.json");
264345
- }
264346
- async function writeAgentRunState(state) {
264347
- const path = agentRunStatePath(state.projectRoot);
264348
- try {
264349
- await mkdir13(join37(homedir6(), ".ralph", basename4(state.projectRoot)), { recursive: true });
264350
- await writeFile(path, JSON.stringify(state, null, 2) + `
264351
- `, "utf-8");
264352
- } catch {}
264353
- }
264354
- var init_agent_run_state = () => {};
264355
-
264356
264781
  // apps/agent/src/agent/json-runner.ts
264357
264782
  var exports_json_runner = {};
264358
264783
  __export(exports_json_runner, {
264359
264784
  runAgentJson: () => runAgentJson
264360
264785
  });
264361
- import { join as join38 } from "path";
264362
- import { mkdir as mkdir14 } from "fs/promises";
264363
- import { homedir as homedir7 } from "os";
264786
+ import { join as join40 } from "path";
264787
+ import { mkdir as mkdir12 } from "fs/promises";
264788
+ import { homedir as homedir8 } from "os";
264364
264789
  function makeEmit(fileSink) {
264365
264790
  return (event) => {
264366
264791
  const payload = { ts: Date.now(), ...event };
@@ -264380,7 +264805,7 @@ async function runAgentJson({
264380
264805
  tasksDir,
264381
264806
  runPreflight: runPreflight2 = runPreflight
264382
264807
  }) {
264383
- await mkdir14(join38(homedir7(), ".ralph"), { recursive: true }).catch(() => {
264808
+ await mkdir12(join40(homedir8(), ".ralph"), { recursive: true }).catch(() => {
264384
264809
  return;
264385
264810
  });
264386
264811
  const fileSink = createJsonLogFileSink(args.jsonLogFile);
@@ -264596,8 +265021,8 @@ var exports_src3 = {};
264596
265021
  __export(exports_src3, {
264597
265022
  main: () => main3
264598
265023
  });
264599
- import { mkdir as mkdir15 } from "fs/promises";
264600
- import { join as join39 } from "path";
265024
+ import { mkdir as mkdir13 } from "fs/promises";
265025
+ import { join as join41 } from "path";
264601
265026
  async function main3(argv) {
264602
265027
  if (argv.includes("--help") || argv.includes("-h")) {
264603
265028
  printAgentHelp();
@@ -264661,9 +265086,9 @@ async function main3(argv) {
264661
265086
  return 1;
264662
265087
  }
264663
265088
  }
264664
- await mkdir15(statesDir, { recursive: true });
264665
- await mkdir15(tasksDir, { recursive: true });
264666
- await mkdir15(join39(projectRoot, ".ralph"), { recursive: true });
265089
+ await mkdir13(statesDir, { recursive: true });
265090
+ await mkdir13(tasksDir, { recursive: true });
265091
+ await mkdir13(join41(projectRoot, ".ralph"), { recursive: true });
264667
265092
  if (shouldFallbackToJsonOutput(args, process.stdin.isTTY)) {
264668
265093
  process.stderr.write(`agent: stdin is not a TTY \u2014 falling back to --json-output mode.
264669
265094
  `);