ralphctl 0.7.0 → 0.7.2

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.
package/dist/cli.mjs CHANGED
@@ -2253,30 +2253,6 @@ var runSignalParsers = (text, timestamp, parsers = DEFAULT_SIGNAL_PARSERS) => {
2253
2253
  var parseHarnessSignals = (text, timestamp) => runSignalParsers(text, timestamp);
2254
2254
 
2255
2255
  // src/integration/ai/providers/claude/parse-stream.ts
2256
- var parseClaudeJsonEnvelope = (stdout) => {
2257
- const trimmed = stdout.trim();
2258
- if (trimmed.length === 0) {
2259
- return { body: "", sessionId: void 0, model: void 0 };
2260
- }
2261
- let parsed;
2262
- try {
2263
- parsed = JSON.parse(trimmed);
2264
- } catch {
2265
- return { body: stdout, sessionId: void 0, model: void 0 };
2266
- }
2267
- if (typeof parsed !== "object" || parsed === null) {
2268
- return { body: stdout, sessionId: void 0, model: void 0 };
2269
- }
2270
- const obj = parsed;
2271
- const result = stringField(obj, "result");
2272
- const sessionId2 = stringField(obj, "session_id", "sessionId");
2273
- const model = stringField(obj, "model");
2274
- return {
2275
- body: result ?? stdout,
2276
- sessionId: sessionId2,
2277
- model
2278
- };
2279
- };
2280
2256
  var stringField = (obj, ...names) => {
2281
2257
  for (const name of names) {
2282
2258
  const v = obj[name];
@@ -2284,6 +2260,63 @@ var stringField = (obj, ...names) => {
2284
2260
  }
2285
2261
  return void 0;
2286
2262
  };
2263
+ var createClaudeStreamParser = () => {
2264
+ let buffer = "";
2265
+ let body = "";
2266
+ let sessionId2;
2267
+ let model;
2268
+ const emit = (raw, onLine) => {
2269
+ if (raw.length === 0) return;
2270
+ if (raw.startsWith("{") && raw.endsWith("}")) {
2271
+ try {
2272
+ const json = JSON.parse(raw);
2273
+ onLine({ raw, json });
2274
+ return;
2275
+ } catch {
2276
+ }
2277
+ }
2278
+ onLine({ raw });
2279
+ };
2280
+ const ingest = (line) => {
2281
+ const json = line.json;
2282
+ if (json === void 0) return;
2283
+ if (sessionId2 === void 0) {
2284
+ const seen = stringField(json, "session_id", "sessionId");
2285
+ if (seen !== void 0) sessionId2 = seen;
2286
+ }
2287
+ const type = stringField(json, "type");
2288
+ if (type === "system" && model === void 0) {
2289
+ const m = stringField(json, "model");
2290
+ if (m !== void 0) model = m;
2291
+ }
2292
+ if (type === "result") {
2293
+ const r = stringField(json, "result");
2294
+ if (r !== void 0) body = r;
2295
+ }
2296
+ };
2297
+ return {
2298
+ feed(chunk, onLine) {
2299
+ buffer += chunk;
2300
+ let nl = buffer.indexOf("\n");
2301
+ while (nl !== -1) {
2302
+ const line = buffer.slice(0, nl);
2303
+ buffer = buffer.slice(nl + 1);
2304
+ emit(line, onLine);
2305
+ nl = buffer.indexOf("\n");
2306
+ }
2307
+ },
2308
+ flush(onLine) {
2309
+ if (buffer.length > 0) {
2310
+ emit(buffer, onLine);
2311
+ buffer = "";
2312
+ }
2313
+ },
2314
+ ingest,
2315
+ snapshot() {
2316
+ return { body, sessionId: sessionId2, model };
2317
+ }
2318
+ };
2319
+ };
2287
2320
 
2288
2321
  // src/integration/ai/providers/_engine/idle-watchdog.ts
2289
2322
  var DEFAULT_IDLE_MS = 5 * 6e4;
@@ -2428,7 +2461,7 @@ var buildClaudeArgs = (session) => {
2428
2461
  })
2429
2462
  );
2430
2463
  }
2431
- const args = ["-p", "--output-format", "json", "--model", session.model];
2464
+ const args = ["-p", "--verbose", "--output-format", "stream-json", "--model", session.model];
2432
2465
  args.push("--permission-mode", "bypassPermissions");
2433
2466
  const denied = disallowedToolsFor(session.permissions);
2434
2467
  if (denied.length > 0) {
@@ -2505,13 +2538,14 @@ var spawnAttempt = async (input) => {
2505
2538
  const child = spawnFn(command, args, {
2506
2539
  stdio: ["pipe", "pipe", "pipe"]
2507
2540
  });
2508
- let stdoutBuf = "";
2541
+ const parser = createClaudeStreamParser();
2509
2542
  let stderrBuf = "";
2543
+ const onLine = (line) => {
2544
+ parser.ingest(line);
2545
+ };
2510
2546
  const { code, signal } = await runHeadlessSpawn({
2511
2547
  child,
2512
- onStdout: (chunk) => {
2513
- stdoutBuf += chunk;
2514
- },
2548
+ onStdout: (chunk) => parser.feed(chunk, onLine),
2515
2549
  onStderr: (chunk) => {
2516
2550
  stderrBuf += chunk;
2517
2551
  },
@@ -2530,6 +2564,7 @@ var spawnAttempt = async (input) => {
2530
2564
  });
2531
2565
  }
2532
2566
  });
2567
+ parser.flush(onLine);
2533
2568
  if (signal === "SIGTERM") {
2534
2569
  return {
2535
2570
  kind: "error",
@@ -2541,9 +2576,8 @@ var spawnAttempt = async (input) => {
2541
2576
  })
2542
2577
  };
2543
2578
  }
2579
+ const envelope = parser.snapshot();
2544
2580
  if (code === 0) {
2545
- const envelope = parseClaudeJsonEnvelope(stdoutBuf);
2546
- stdoutBuf = "";
2547
2581
  if (envelope.sessionId !== void 0) {
2548
2582
  deps.eventBus.publish({
2549
2583
  type: "log",
@@ -2578,13 +2612,12 @@ var spawnAttempt = async (input) => {
2578
2612
  };
2579
2613
  }
2580
2614
  if (RATE_LIMIT_RE.test(stderrBuf)) {
2581
- const sessionIdOnFailure = parseClaudeJsonEnvelope(stdoutBuf).sessionId;
2582
2615
  return {
2583
2616
  kind: "rate-limit",
2584
2617
  error: new RateLimitError({
2585
2618
  subCode: "spawn-stderr",
2586
2619
  message: `claude-provider: rate-limit detected in stderr (exit ${String(code)})`,
2587
- ...sessionIdOnFailure !== void 0 ? { sessionId: sessionIdOnFailure } : {}
2620
+ ...envelope.sessionId !== void 0 ? { sessionId: envelope.sessionId } : {}
2588
2621
  })
2589
2622
  };
2590
2623
  }
@@ -2634,24 +2667,16 @@ var buildCodexArgs = (session, opts) => {
2634
2667
  const perms = sandboxFor(session.permissions);
2635
2668
  if (!perms.ok) return Result.error(perms.error);
2636
2669
  const args = ["exec"];
2637
- if (session.resume !== void 0) {
2670
+ const isResume = session.resume !== void 0;
2671
+ if (isResume) {
2638
2672
  args.push("resume", String(session.resume));
2639
2673
  }
2640
- args.push(
2641
- "--ephemeral",
2642
- "--skip-git-repo-check",
2643
- "-o",
2644
- opts.outputFile,
2645
- "--json",
2646
- "-m",
2647
- session.model,
2648
- "-C",
2649
- String(session.cwd),
2650
- "-s",
2651
- perms.value.sandbox
2652
- );
2653
- for (const root of session.additionalRoots ?? []) {
2654
- args.push("--add-dir", String(root));
2674
+ args.push("--ephemeral", "--skip-git-repo-check", "-o", opts.outputFile, "--json", "-m", session.model);
2675
+ if (!isResume) {
2676
+ args.push("-C", String(session.cwd), "-s", perms.value.sandbox);
2677
+ for (const root of session.additionalRoots ?? []) {
2678
+ args.push("--add-dir", String(root));
2679
+ }
2655
2680
  }
2656
2681
  if (opts.reasoningEffort !== void 0) {
2657
2682
  args.push("-c", `model_reasoning_effort=${opts.reasoningEffort}`);
@@ -2829,6 +2854,18 @@ var spawnAttempt2 = async (input) => {
2829
2854
  const signals = parseHarnessSignals(body, IsoTimestamp.now());
2830
2855
  const wrote = await writeJsonAtomic(String(session.signalsFile), signals);
2831
2856
  if (!wrote.ok) return { kind: "error", error: wrote.error };
2857
+ if (session.bodyFile !== void 0) {
2858
+ const bodyWrote = await writeTextAtomic(String(session.bodyFile), body);
2859
+ if (!bodyWrote.ok) {
2860
+ deps.eventBus.publish({
2861
+ type: "log",
2862
+ level: "warn",
2863
+ message: `codex-provider: failed to write body file \u2014 diagnostic capture skipped`,
2864
+ meta: { bodyFile: String(session.bodyFile), error: bodyWrote.error.message },
2865
+ at: IsoTimestamp.now()
2866
+ });
2867
+ }
2868
+ }
2832
2869
  return {
2833
2870
  kind: "success",
2834
2871
  output: {
@@ -3211,6 +3248,7 @@ var stringifyError4 = (cause) => cause instanceof Error ? cause.message : String
3211
3248
 
3212
3249
  // src/integration/ai/providers/codex/interactive.ts
3213
3250
  import { spawn as nodeSpawn7 } from "child_process";
3251
+ import { dirname as dirname4 } from "path";
3214
3252
  var defaultSpawn7 = (command, args, options) => nodeSpawn7(command, [...args], { stdio: options.stdio, cwd: options.cwd });
3215
3253
  var createInteractiveCodexProvider = (deps) => {
3216
3254
  const spawnFn = deps.spawn ?? defaultSpawn7;
@@ -3227,10 +3265,23 @@ var createInteractiveCodexProvider = (deps) => {
3227
3265
  })
3228
3266
  );
3229
3267
  }
3268
+ const allRoots = [
3269
+ String(input.cwd),
3270
+ ...input.additionalRoots?.map((r) => String(r)) ?? [],
3271
+ dirname4(String(input.outputFile)),
3272
+ dirname4(String(input.promptFile))
3273
+ ];
3274
+ const seen = /* @__PURE__ */ new Set();
3275
+ const dirFlags = allRoots.filter((p) => {
3276
+ if (seen.has(p)) return false;
3277
+ seen.add(p);
3278
+ return true;
3279
+ }).flatMap((p) => ["--add-dir", shellQuote2(p)]);
3230
3280
  const inner = [
3231
3281
  "codex",
3232
3282
  "--cd",
3233
3283
  shellQuote2(String(input.cwd)),
3284
+ ...dirFlags,
3234
3285
  "--model",
3235
3286
  shellQuote2(input.model),
3236
3287
  "-s",
@@ -3286,7 +3337,7 @@ var stringifyError5 = (cause) => cause instanceof Error ? cause.message : String
3286
3337
  // src/integration/ai/providers/copilot/interactive.ts
3287
3338
  import { promises as fs6 } from "fs";
3288
3339
  import { spawn as nodeSpawn8 } from "child_process";
3289
- import { dirname as dirname4 } from "path";
3340
+ import { dirname as dirname5 } from "path";
3290
3341
  var defaultSpawn8 = (command, args, options) => nodeSpawn8(command, [...args], { stdio: options.stdio, cwd: options.cwd });
3291
3342
  var defaultReadFile = (path) => fs6.readFile(path, "utf8");
3292
3343
  var createInteractiveCopilotProvider = (deps) => {
@@ -3320,8 +3371,8 @@ var createInteractiveCopilotProvider = (deps) => {
3320
3371
  const allRoots = [
3321
3372
  String(input.cwd),
3322
3373
  ...input.additionalRoots?.map((r) => String(r)) ?? [],
3323
- dirname4(String(input.outputFile)),
3324
- dirname4(String(input.promptFile))
3374
+ dirname5(String(input.outputFile)),
3375
+ dirname5(String(input.promptFile))
3325
3376
  ];
3326
3377
  const seen = /* @__PURE__ */ new Set();
3327
3378
  const dirFlags = allRoots.filter((p) => {
@@ -3864,7 +3915,7 @@ var createPullRequestCreator = (deps) => async (input) => {
3864
3915
 
3865
3916
  // src/integration/ai/prompts/_engine/fs-template-loader.ts
3866
3917
  import { promises as fs7 } from "fs";
3867
- import { dirname as dirname5, join as join6 } from "path";
3918
+ import { dirname as dirname6, join as join6 } from "path";
3868
3919
  import { fileURLToPath } from "url";
3869
3920
  var createFsTemplateLoader = (templatesDir) => ({
3870
3921
  async load(name) {
@@ -3898,7 +3949,7 @@ var tryRead = async (path) => {
3898
3949
  }
3899
3950
  };
3900
3951
  var TEMPLATES_DIR = (() => {
3901
- const here = dirname5(fileURLToPath(import.meta.url));
3952
+ const here = dirname6(fileURLToPath(import.meta.url));
3902
3953
  const isBundled = import.meta.url.endsWith("/cli.mjs") || import.meta.url.endsWith("\\cli.mjs");
3903
3954
  const path = isBundled ? join6(here, "prompts") : join6(here, "..");
3904
3955
  const parsed = AbsolutePath.parse(path);
@@ -3911,11 +3962,7 @@ var defaultTemplatesDir = () => TEMPLATES_DIR;
3911
3962
  var isNodeErrnoCode2 = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
3912
3963
 
3913
3964
  // src/integration/ai/readiness/claude/probe.ts
3914
- import { promises as fs8 } from "fs";
3915
- import { basename, join as join7 } from "path";
3916
-
3917
- // src/domain/value/kebab-case.ts
3918
- var toKebabCase = (input) => input.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
3965
+ import { join as join8 } from "path";
3919
3966
 
3920
3967
  // src/domain/value/error/probe-error.ts
3921
3968
  var ProbeError = class extends Error {
@@ -3936,66 +3983,22 @@ var ProbeError = class extends Error {
3936
3983
  }
3937
3984
  };
3938
3985
 
3939
- // src/integration/ai/readiness/_engine/state.ts
3940
- var unknownState = { kind: "unknown" };
3941
- var absentState = (evaluatedAt) => ({ kind: "absent", evaluatedAt });
3942
- var presentState = (evaluatedAt, artifacts) => ({
3943
- kind: "present",
3944
- evaluatedAt,
3945
- artifacts
3946
- });
3986
+ // src/integration/ai/readiness/_engine/probe-fs.ts
3987
+ import { promises as fs8 } from "fs";
3988
+ import { basename, join as join7 } from "path";
3947
3989
 
3948
- // src/integration/ai/readiness/_engine/predicates.ts
3949
- var isPresent = (state) => state.kind === "present";
3950
- var hasAnyClaudeArtifact = (a) => a.claudeMd !== void 0 || a.agentsMd !== void 0 || a.settings !== void 0 || a.settingsLocal !== void 0 || a.mcpConfig !== void 0 || a.skills.length > 0 || a.commands.length > 0 || a.agents.length > 0 || a.hooks.length > 0;
3951
- var hasAnyCopilotArtifact = (a) => a.copilotInstructions !== void 0;
3990
+ // src/domain/value/kebab-case.ts
3991
+ var toKebabCase = (input) => input.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
3952
3992
 
3953
- // src/integration/ai/readiness/claude/probe.ts
3954
- var claudeProbe = {
3955
- tool: "claude-code",
3956
- async evaluate(repository, now) {
3957
- const root = repository.path;
3958
- const claudeMd = await probeFile(join7(root, "CLAUDE.md"));
3959
- if (!claudeMd.ok) return Result.error(claudeMd.error);
3960
- const agentsMd = await probeFile(join7(root, "AGENTS.md"));
3961
- if (!agentsMd.ok) return Result.error(agentsMd.error);
3962
- const settings = await probeFile(join7(root, ".claude/settings.json"));
3963
- if (!settings.ok) return Result.error(settings.error);
3964
- const settingsLocal = await probeFile(join7(root, ".claude/settings.local.json"));
3965
- if (!settingsLocal.ok) return Result.error(settingsLocal.error);
3966
- const mcpConfig = await probeFile(join7(root, ".mcp.json"));
3967
- if (!mcpConfig.ok) return Result.error(mcpConfig.error);
3968
- const skills = await probeNamedDirCollection(join7(root, ".claude/skills"), "SKILL.md");
3969
- if (!skills.ok) return Result.error(skills.error);
3970
- const commands = await probeNamedFileCollection(join7(root, ".claude/commands"));
3971
- if (!commands.ok) return Result.error(commands.error);
3972
- const agents = await probeNamedFileCollection(join7(root, ".claude/agents"));
3973
- if (!agents.ok) return Result.error(agents.error);
3974
- const hooks = await readHooks([settings.value, settingsLocal.value]);
3975
- if (!hooks.ok) return Result.error(hooks.error);
3976
- const artifacts = {
3977
- tool: "claude-code",
3978
- ...claudeMd.value !== void 0 ? { claudeMd: claudeMd.value } : {},
3979
- ...agentsMd.value !== void 0 ? { agentsMd: agentsMd.value } : {},
3980
- ...settings.value !== void 0 ? { settings: settings.value } : {},
3981
- ...settingsLocal.value !== void 0 ? { settingsLocal: settingsLocal.value } : {},
3982
- ...mcpConfig.value !== void 0 ? { mcpConfig: mcpConfig.value } : {},
3983
- skills: skills.value,
3984
- commands: commands.value,
3985
- agents: agents.value,
3986
- hooks: hooks.value
3987
- };
3988
- return Result.ok(hasAnyClaudeArtifact(artifacts) ? presentState(now, artifacts) : absentState(now));
3989
- }
3990
- };
3993
+ // src/integration/ai/readiness/_engine/probe-fs.ts
3991
3994
  var probeFile = async (path) => {
3992
3995
  try {
3993
3996
  const stat = await fs8.stat(path);
3994
3997
  if (!stat.isFile()) return Result.ok(void 0);
3995
3998
  return Result.ok({ path });
3996
3999
  } catch (cause) {
3997
- if (isNodeErrnoCode3(cause, "ENOENT")) return Result.ok(void 0);
3998
- if (isNodeErrnoCode3(cause, "EACCES")) {
4000
+ if (isNodeErrnoCode(cause, "ENOENT")) return Result.ok(void 0);
4001
+ if (isNodeErrnoCode(cause, "EACCES")) {
3999
4002
  return Result.error(
4000
4003
  new ProbeError({ subCode: "fs-permission", message: `permission denied reading ${path}`, path, cause })
4001
4004
  );
@@ -4003,23 +4006,6 @@ var probeFile = async (path) => {
4003
4006
  return Result.error(new ProbeError({ subCode: "fs-read", message: `failed to stat ${path}`, path, cause }));
4004
4007
  }
4005
4008
  };
4006
- var probeNamedFileCollection = async (dir) => {
4007
- const entries = await listDir2(dir);
4008
- if (!entries.ok) return Result.error(entries.error);
4009
- const refs = [];
4010
- for (const entry of entries.value) {
4011
- if (!entry.endsWith(".md")) continue;
4012
- const full = join7(dir, entry);
4013
- const stat = await statSafely(full);
4014
- if (!stat.ok) return Result.error(stat.error);
4015
- if (stat.value === void 0 || !stat.value.isFile()) continue;
4016
- const baseName = entry.slice(0, -".md".length);
4017
- const slug = Slug.parse(toKebabCase(baseName));
4018
- if (!slug.ok) continue;
4019
- refs.push({ name: slug.value, path: full });
4020
- }
4021
- return Result.ok(refs);
4022
- };
4023
4009
  var probeNamedDirCollection = async (dir, childMarker) => {
4024
4010
  const entries = await listDir2(dir);
4025
4011
  if (!entries.ok) return Result.error(entries.error);
@@ -4039,12 +4025,29 @@ var probeNamedDirCollection = async (dir, childMarker) => {
4039
4025
  }
4040
4026
  return Result.ok(refs);
4041
4027
  };
4028
+ var probeNamedFileCollection = async (dir) => {
4029
+ const entries = await listDir2(dir);
4030
+ if (!entries.ok) return Result.error(entries.error);
4031
+ const refs = [];
4032
+ for (const entry of entries.value) {
4033
+ if (!entry.endsWith(".md")) continue;
4034
+ const full = join7(dir, entry);
4035
+ const stat = await statSafely(full);
4036
+ if (!stat.ok) return Result.error(stat.error);
4037
+ if (stat.value === void 0 || !stat.value.isFile()) continue;
4038
+ const baseName = entry.slice(0, -".md".length);
4039
+ const slug = Slug.parse(toKebabCase(baseName));
4040
+ if (!slug.ok) continue;
4041
+ refs.push({ name: slug.value, path: full });
4042
+ }
4043
+ return Result.ok(refs);
4044
+ };
4042
4045
  var listDir2 = async (dir) => {
4043
4046
  try {
4044
4047
  return Result.ok(await fs8.readdir(dir));
4045
4048
  } catch (cause) {
4046
- if (isNodeErrnoCode3(cause, "ENOENT") || isNodeErrnoCode3(cause, "ENOTDIR")) return Result.ok([]);
4047
- if (isNodeErrnoCode3(cause, "EACCES")) {
4049
+ if (isNodeErrnoCode(cause, "ENOENT") || isNodeErrnoCode(cause, "ENOTDIR")) return Result.ok([]);
4050
+ if (isNodeErrnoCode(cause, "EACCES")) {
4048
4051
  return Result.error(
4049
4052
  new ProbeError({ subCode: "fs-permission", message: `permission denied listing ${dir}`, path: dir, cause })
4050
4053
  );
@@ -4056,8 +4059,8 @@ var statSafely = async (path) => {
4056
4059
  try {
4057
4060
  return Result.ok(await fs8.stat(path));
4058
4061
  } catch (cause) {
4059
- if (isNodeErrnoCode3(cause, "ENOENT")) return Result.ok(void 0);
4060
- if (isNodeErrnoCode3(cause, "EACCES")) {
4062
+ if (isNodeErrnoCode(cause, "ENOENT")) return Result.ok(void 0);
4063
+ if (isNodeErrnoCode(cause, "EACCES")) {
4061
4064
  return Result.error(
4062
4065
  new ProbeError({ subCode: "fs-permission", message: `permission denied stat ${path}`, path, cause })
4063
4066
  );
@@ -4065,6 +4068,73 @@ var statSafely = async (path) => {
4065
4068
  return Result.error(new ProbeError({ subCode: "fs-read", message: `failed to stat ${path}`, path, cause }));
4066
4069
  }
4067
4070
  };
4071
+ var readFileSafely = async (path) => {
4072
+ try {
4073
+ return Result.ok(await fs8.readFile(path, "utf8"));
4074
+ } catch (cause) {
4075
+ if (isNodeErrnoCode(cause, "ENOENT")) return Result.ok(void 0);
4076
+ if (isNodeErrnoCode(cause, "EACCES")) {
4077
+ return Result.error(
4078
+ new ProbeError({ subCode: "fs-permission", message: `permission denied reading ${path}`, path, cause })
4079
+ );
4080
+ }
4081
+ return Result.error(new ProbeError({ subCode: "fs-read", message: `failed to read ${path}`, path, cause }));
4082
+ }
4083
+ };
4084
+
4085
+ // src/integration/ai/readiness/_engine/state.ts
4086
+ var unknownState = { kind: "unknown" };
4087
+ var absentState = (evaluatedAt) => ({ kind: "absent", evaluatedAt });
4088
+ var presentState = (evaluatedAt, artifacts) => ({
4089
+ kind: "present",
4090
+ evaluatedAt,
4091
+ artifacts
4092
+ });
4093
+
4094
+ // src/integration/ai/readiness/_engine/predicates.ts
4095
+ var isPresent = (state) => state.kind === "present";
4096
+ var hasAnyClaudeArtifact = (a) => a.claudeMd !== void 0 || a.agentsMd !== void 0 || a.settings !== void 0 || a.settingsLocal !== void 0 || a.mcpConfig !== void 0 || a.skills.length > 0 || a.commands.length > 0 || a.agents.length > 0 || a.hooks.length > 0;
4097
+ var hasAnyCopilotArtifact = (a) => a.copilotInstructions !== void 0;
4098
+ var hasAnyCodexArtifact = (a) => a.agentsMd !== void 0 || a.skills.length > 0;
4099
+
4100
+ // src/integration/ai/readiness/claude/probe.ts
4101
+ var claudeProbe = {
4102
+ tool: "claude-code",
4103
+ async evaluate(repository, now) {
4104
+ const root = repository.path;
4105
+ const claudeMd = await probeFile(join8(root, "CLAUDE.md"));
4106
+ if (!claudeMd.ok) return Result.error(claudeMd.error);
4107
+ const agentsMd = await probeFile(join8(root, "AGENTS.md"));
4108
+ if (!agentsMd.ok) return Result.error(agentsMd.error);
4109
+ const settings = await probeFile(join8(root, ".claude/settings.json"));
4110
+ if (!settings.ok) return Result.error(settings.error);
4111
+ const settingsLocal = await probeFile(join8(root, ".claude/settings.local.json"));
4112
+ if (!settingsLocal.ok) return Result.error(settingsLocal.error);
4113
+ const mcpConfig = await probeFile(join8(root, ".mcp.json"));
4114
+ if (!mcpConfig.ok) return Result.error(mcpConfig.error);
4115
+ const skills = await probeNamedDirCollection(join8(root, ".claude/skills"), "SKILL.md");
4116
+ if (!skills.ok) return Result.error(skills.error);
4117
+ const commands = await probeNamedFileCollection(join8(root, ".claude/commands"));
4118
+ if (!commands.ok) return Result.error(commands.error);
4119
+ const agents = await probeNamedFileCollection(join8(root, ".claude/agents"));
4120
+ if (!agents.ok) return Result.error(agents.error);
4121
+ const hooks = await readHooks([settings.value, settingsLocal.value]);
4122
+ if (!hooks.ok) return Result.error(hooks.error);
4123
+ const artifacts = {
4124
+ tool: "claude-code",
4125
+ ...claudeMd.value !== void 0 ? { claudeMd: claudeMd.value } : {},
4126
+ ...agentsMd.value !== void 0 ? { agentsMd: agentsMd.value } : {},
4127
+ ...settings.value !== void 0 ? { settings: settings.value } : {},
4128
+ ...settingsLocal.value !== void 0 ? { settingsLocal: settingsLocal.value } : {},
4129
+ ...mcpConfig.value !== void 0 ? { mcpConfig: mcpConfig.value } : {},
4130
+ skills: skills.value,
4131
+ commands: commands.value,
4132
+ agents: agents.value,
4133
+ hooks: hooks.value
4134
+ };
4135
+ return Result.ok(hasAnyClaudeArtifact(artifacts) ? presentState(now, artifacts) : absentState(now));
4136
+ }
4137
+ };
4068
4138
  var readHooks = async (settingsRefs) => {
4069
4139
  const hooks = [];
4070
4140
  for (const ref2 of settingsRefs) {
@@ -4084,19 +4154,6 @@ var readHooks = async (settingsRefs) => {
4084
4154
  }
4085
4155
  return Result.ok(hooks);
4086
4156
  };
4087
- var readFileSafely = async (path) => {
4088
- try {
4089
- return Result.ok(await fs8.readFile(path, "utf8"));
4090
- } catch (cause) {
4091
- if (isNodeErrnoCode3(cause, "ENOENT")) return Result.ok(void 0);
4092
- if (isNodeErrnoCode3(cause, "EACCES")) {
4093
- return Result.error(
4094
- new ProbeError({ subCode: "fs-permission", message: `permission denied reading ${path}`, path, cause })
4095
- );
4096
- }
4097
- return Result.error(new ProbeError({ subCode: "fs-read", message: `failed to read ${path}`, path, cause }));
4098
- }
4099
- };
4100
4157
  var extractHooks = (settings, sink) => {
4101
4158
  if (typeof settings !== "object" || settings === null) return;
4102
4159
  const hooks = settings.hooks;
@@ -4117,33 +4174,41 @@ var extractHooks = (settings, sink) => {
4117
4174
  }
4118
4175
  }
4119
4176
  };
4120
- var isNodeErrnoCode3 = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
4121
4177
 
4122
4178
  // src/integration/ai/readiness/codex/probe.ts
4179
+ import { join as join9 } from "path";
4123
4180
  var codexProbe = {
4124
4181
  tool: "codex",
4125
- evaluate(repository, now) {
4126
- void repository;
4127
- void now;
4128
- return Promise.resolve(Result.ok(unknownState));
4182
+ async evaluate(repository, now) {
4183
+ const root = repository.path;
4184
+ const agentsMd = await probeFile(join9(root, "AGENTS.md"));
4185
+ if (!agentsMd.ok) return Result.error(agentsMd.error);
4186
+ const skills = await probeNamedDirCollection(join9(root, ".agents/skills"), "SKILL.md");
4187
+ if (!skills.ok) return Result.error(skills.error);
4188
+ const artifacts = {
4189
+ tool: "codex",
4190
+ ...agentsMd.value !== void 0 ? { agentsMd: agentsMd.value } : {},
4191
+ skills: skills.value
4192
+ };
4193
+ return Result.ok(hasAnyCodexArtifact(artifacts) ? presentState(now, artifacts) : absentState(now));
4129
4194
  }
4130
4195
  };
4131
4196
 
4132
4197
  // src/integration/ai/readiness/copilot/probe.ts
4133
4198
  import { promises as fs9 } from "fs";
4134
- import { join as join8 } from "path";
4199
+ import { join as join10 } from "path";
4135
4200
  var copilotProbe = {
4136
4201
  tool: "copilot",
4137
4202
  async evaluate(repository, now) {
4138
- const path = join8(repository.path, ".github/copilot-instructions.md");
4203
+ const path = join10(repository.path, ".github/copilot-instructions.md");
4139
4204
  try {
4140
4205
  const stat = await fs9.stat(path);
4141
4206
  if (!stat.isFile()) return Result.ok(absentState(now));
4142
4207
  const artifacts = { tool: "copilot", copilotInstructions: { path } };
4143
4208
  return Result.ok(hasAnyCopilotArtifact(artifacts) ? presentState(now, artifacts) : absentState(now));
4144
4209
  } catch (cause) {
4145
- if (isNodeErrnoCode4(cause, "ENOENT")) return Result.ok(absentState(now));
4146
- if (isNodeErrnoCode4(cause, "EACCES")) {
4210
+ if (isNodeErrnoCode(cause, "ENOENT")) return Result.ok(absentState(now));
4211
+ if (isNodeErrnoCode(cause, "EACCES")) {
4147
4212
  return Result.error(
4148
4213
  new ProbeError({ subCode: "fs-permission", message: `permission denied reading ${path}`, path, cause })
4149
4214
  );
@@ -4152,7 +4217,6 @@ var copilotProbe = {
4152
4217
  }
4153
4218
  }
4154
4219
  };
4155
- var isNodeErrnoCode4 = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
4156
4220
 
4157
4221
  // src/integration/observability/in-memory-event-bus.ts
4158
4222
  var createInMemoryEventBus = () => {
@@ -4200,7 +4264,7 @@ var loggerForScope = (deps, scope) => {
4200
4264
  };
4201
4265
 
4202
4266
  // src/integration/version/npm-version-checker.ts
4203
- import { join as join9 } from "path";
4267
+ import { join as join11 } from "path";
4204
4268
  import { z as z15 } from "zod";
4205
4269
 
4206
4270
  // src/business/version/version-check.ts
@@ -4242,7 +4306,7 @@ var buildVersionCheck = (current, latest, now) => ({
4242
4306
  var DEFAULT_CACHE_TTL_MS = 60 * 60 * 1e3;
4243
4307
  var DEFAULT_FETCH_TIMEOUT_MS = 3e3;
4244
4308
  var RegistryPayloadSchema = z15.object({ version: z15.string() });
4245
- var cachePath = (stateRoot) => join9(String(stateRoot), "version-check.json");
4309
+ var cachePath = (stateRoot) => join11(String(stateRoot), "version-check.json");
4246
4310
  var registryUrl = (packageName) => `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
4247
4311
  var shouldSkip = (env) => env["NO_NETWORK"] !== void 0 || env["VITEST"] !== void 0;
4248
4312
  var readCache = async (path) => {
@@ -4296,7 +4360,7 @@ var createNpmVersionChecker = (deps) => {
4296
4360
  // package.json
4297
4361
  var package_default = {
4298
4362
  name: "ralphctl",
4299
- version: "0.7.0",
4363
+ version: "0.7.2",
4300
4364
  description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
4301
4365
  homepage: "https://github.com/lukas-grigis/ralphctl",
4302
4366
  type: "module",
@@ -4398,11 +4462,11 @@ var CLI_METADATA = {
4398
4462
  // src/integration/ai/skills/_engine/filesystem-skills-adapter.ts
4399
4463
  import { existsSync } from "fs";
4400
4464
  import { mkdir, rm, rmdir, writeFile } from "fs/promises";
4401
- import { join as join11 } from "path";
4465
+ import { join as join13 } from "path";
4402
4466
 
4403
4467
  // src/integration/io/git-exclude.ts
4404
4468
  import { promises as fs10 } from "fs";
4405
- import { isAbsolute as isAbsolute2, join as join10 } from "path";
4469
+ import { isAbsolute as isAbsolute2, join as join12 } from "path";
4406
4470
  var ensureGitExcludeWildcard = async (repoRoot, pattern) => {
4407
4471
  const resolved = await resolveExcludePath(String(repoRoot));
4408
4472
  if (resolved === void 0) return Result.ok(void 0);
@@ -4410,7 +4474,7 @@ var ensureGitExcludeWildcard = async (repoRoot, pattern) => {
4410
4474
  try {
4411
4475
  existing = await fs10.readFile(resolved, "utf8");
4412
4476
  } catch (cause) {
4413
- if (isNodeErrnoCode5(cause, "ENOENT")) {
4477
+ if (isNodeErrnoCode3(cause, "ENOENT")) {
4414
4478
  } else {
4415
4479
  return Result.error(
4416
4480
  new StorageError({
@@ -4431,16 +4495,16 @@ var ensureGitExcludeWildcard = async (repoRoot, pattern) => {
4431
4495
  return writeTextAtomic(resolved, next);
4432
4496
  };
4433
4497
  var resolveExcludePath = async (repoRoot) => {
4434
- const gitMarker = join10(repoRoot, ".git");
4498
+ const gitMarker = join12(repoRoot, ".git");
4435
4499
  let stat;
4436
4500
  try {
4437
4501
  stat = await fs10.stat(gitMarker);
4438
4502
  } catch (cause) {
4439
- if (isNodeErrnoCode5(cause, "ENOENT") || isNodeErrnoCode5(cause, "ENOTDIR")) return void 0;
4503
+ if (isNodeErrnoCode3(cause, "ENOENT") || isNodeErrnoCode3(cause, "ENOTDIR")) return void 0;
4440
4504
  throw cause;
4441
4505
  }
4442
4506
  if (stat.isDirectory()) {
4443
- return join10(gitMarker, "info", "exclude");
4507
+ return join12(gitMarker, "info", "exclude");
4444
4508
  }
4445
4509
  if (!stat.isFile()) return void 0;
4446
4510
  let pointer;
@@ -4452,10 +4516,10 @@ var resolveExcludePath = async (repoRoot) => {
4452
4516
  const match = /^gitdir:\s*(.+)\s*$/m.exec(pointer);
4453
4517
  if (match === null) return void 0;
4454
4518
  const gitdir = match[1].trim();
4455
- const absoluteGitdir = isAbsolute2(gitdir) ? gitdir : join10(repoRoot, gitdir);
4456
- return join10(absoluteGitdir, "info", "exclude");
4519
+ const absoluteGitdir = isAbsolute2(gitdir) ? gitdir : join12(repoRoot, gitdir);
4520
+ return join12(absoluteGitdir, "info", "exclude");
4457
4521
  };
4458
- var isNodeErrnoCode5 = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
4522
+ var isNodeErrnoCode3 = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
4459
4523
 
4460
4524
  // src/integration/ai/skills/_engine/filesystem-skills-adapter.ts
4461
4525
  var renderSkill = (skill) => {
@@ -4478,7 +4542,7 @@ var tryRmdirIfEmpty = async (path) => {
4478
4542
  var createFilesystemSkillsAdapter = (deps) => {
4479
4543
  const installed = /* @__PURE__ */ new Map();
4480
4544
  const excludeAttempted = /* @__PURE__ */ new Set();
4481
- const skillsSubdir = join11(deps.parentDir, "skills");
4545
+ const skillsSubdir = join13(deps.parentDir, "skills");
4482
4546
  const excludePattern = `${skillsSubdir}/ralphctl-*`;
4483
4547
  const pruneStale = () => {
4484
4548
  for (const key of [...installed.keys()]) {
@@ -4488,14 +4552,14 @@ var createFilesystemSkillsAdapter = (deps) => {
4488
4552
  return {
4489
4553
  async install(sessionDir, skills) {
4490
4554
  pruneStale();
4491
- const skillsDir = join11(String(sessionDir), skillsSubdir);
4555
+ const skillsDir = join13(String(sessionDir), skillsSubdir);
4492
4556
  const tracked = installed.get(String(sessionDir)) ?? /* @__PURE__ */ new Set();
4493
4557
  for (const skill of skills) {
4494
- const dst = join11(skillsDir, skill.name);
4558
+ const dst = join13(skillsDir, skill.name);
4495
4559
  if (existsSync(dst)) continue;
4496
4560
  try {
4497
4561
  await mkdir(dst, { recursive: true });
4498
- await writeFile(join11(dst, "SKILL.md"), renderSkill(skill), "utf-8");
4562
+ await writeFile(join13(dst, "SKILL.md"), renderSkill(skill), "utf-8");
4499
4563
  tracked.add(skill.name);
4500
4564
  } catch (cause) {
4501
4565
  if (tracked.size > 0) installed.set(String(sessionDir), tracked);
@@ -4526,10 +4590,10 @@ var createFilesystemSkillsAdapter = (deps) => {
4526
4590
  const key = String(sessionDir);
4527
4591
  const tracked = installed.get(key);
4528
4592
  if (tracked === void 0 || tracked.size === 0) return Result.ok(void 0);
4529
- const skillsDir = join11(key, skillsSubdir);
4593
+ const skillsDir = join13(key, skillsSubdir);
4530
4594
  try {
4531
4595
  for (const id of tracked) {
4532
- await rm(join11(skillsDir, id), { recursive: true, force: true });
4596
+ await rm(join13(skillsDir, id), { recursive: true, force: true });
4533
4597
  }
4534
4598
  installed.delete(key);
4535
4599
  } catch (cause) {
@@ -4543,7 +4607,7 @@ var createFilesystemSkillsAdapter = (deps) => {
4543
4607
  );
4544
4608
  }
4545
4609
  await tryRmdirIfEmpty(skillsDir);
4546
- await tryRmdirIfEmpty(join11(key, deps.parentDir));
4610
+ await tryRmdirIfEmpty(join13(key, deps.parentDir));
4547
4611
  return Result.ok(void 0);
4548
4612
  }
4549
4613
  };
@@ -4605,7 +4669,7 @@ var createSkillsAdapter = (deps) => {
4605
4669
  };
4606
4670
 
4607
4671
  // src/integration/ai/skills/bundled/source.ts
4608
- import { dirname as dirname6, join as join12 } from "path";
4672
+ import { dirname as dirname7, join as join14 } from "path";
4609
4673
  import { fileURLToPath as fileURLToPath2 } from "url";
4610
4674
  import { readFile } from "fs/promises";
4611
4675
 
@@ -4632,9 +4696,9 @@ var skillsForFlow = (flowId) => FLOW_SKILLS[flowId];
4632
4696
 
4633
4697
  // src/integration/ai/skills/bundled/source.ts
4634
4698
  var defaultBundledRoot = (() => {
4635
- const here = dirname6(fileURLToPath2(import.meta.url));
4699
+ const here = dirname7(fileURLToPath2(import.meta.url));
4636
4700
  const isBundled = import.meta.url.endsWith("/cli.mjs") || import.meta.url.endsWith("\\cli.mjs");
4637
- return isBundled ? join12(here, "skills") : here;
4701
+ return isBundled ? join14(here, "skills") : here;
4638
4702
  })();
4639
4703
  var splitFrontmatter = (raw) => {
4640
4704
  const trimmed = raw.replace(/^\uFEFF/u, "");
@@ -4662,7 +4726,7 @@ var parseSimpleYaml = (input) => {
4662
4726
  return result;
4663
4727
  };
4664
4728
  var readSkill = async (root, name) => {
4665
- const path = join12(root, name, "SKILL.md");
4729
+ const path = join14(root, name, "SKILL.md");
4666
4730
  let raw;
4667
4731
  try {
4668
4732
  raw = await readFile(path, "utf-8");
@@ -5652,13 +5716,13 @@ var probeGitConfig = async (id, label, key, runCommand2, hint) => {
5652
5716
  }
5653
5717
  return { id, label, status: "warn", detail: `${key} not set`, hint, group: "vcs" };
5654
5718
  };
5655
- var probeCliAuth = async (id, label, binary, args, hint, runCommand2) => {
5719
+ var probeCliAuth = async (id, label, binary, args, hint, runCommand2, group = "vcs") => {
5656
5720
  const r = await runCommand2(binary, args);
5657
5721
  if (r.ok) {
5658
- return { id, label, status: "pass", detail: "authenticated", group: "vcs" };
5722
+ return { id, label, status: "pass", detail: "authenticated", group };
5659
5723
  }
5660
5724
  const detail = r.stderr.split("\n").find((line) => line.trim().length > 0)?.trim() ?? "not authenticated";
5661
- return { id, label, status: "warn", detail, hint, group: "vcs" };
5725
+ return { id, label, status: "warn", detail, hint, group };
5662
5726
  };
5663
5727
  var createDoctorFlow = (deps) => leaf("doctor", {
5664
5728
  useCase: {
@@ -5770,6 +5834,7 @@ var createDoctorFlow = (deps) => leaf("doctor", {
5770
5834
  }
5771
5835
  const settings = await deps.settingsRepo.load();
5772
5836
  const configuredProvider = settings.ok ? settings.value.ai.provider : void 0;
5837
+ let codexInstalled = false;
5773
5838
  for (const provider of Object.keys(PROVIDER_BINARY)) {
5774
5839
  const binary = PROVIDER_BINARY[provider];
5775
5840
  const isConfigured = provider === configuredProvider;
@@ -5781,12 +5846,26 @@ var createDoctorFlow = (deps) => leaf("doctor", {
5781
5846
  deps.commandExists,
5782
5847
  `install the '${binary}' CLI and ensure it is on your PATH`
5783
5848
  );
5849
+ if (provider === "openai-codex") codexInstalled = probe.status === "pass";
5784
5850
  if (probe.status === "fail") {
5785
5851
  probes.push({ ...probe, status: "warn" });
5786
5852
  } else {
5787
5853
  probes.push(probe);
5788
5854
  }
5789
5855
  }
5856
+ if (configuredProvider === "openai-codex" && codexInstalled) {
5857
+ probes.push(
5858
+ await probeCliAuth(
5859
+ "codex-auth",
5860
+ "OpenAI Codex CLI authenticated",
5861
+ "codex",
5862
+ ["login", "status"],
5863
+ "run `codex login` to sign in",
5864
+ deps.runCommand,
5865
+ "ai"
5866
+ )
5867
+ );
5868
+ }
5790
5869
  const projects = await deps.projectRepo.list();
5791
5870
  probes.push({
5792
5871
  id: "projects-list",
@@ -9129,14 +9208,14 @@ var launchAddTickets = (ctx) => {
9129
9208
  };
9130
9209
 
9131
9210
  // src/application/ui/shared/launch/refine.ts
9132
- import { join as join15 } from "path";
9211
+ import { join as join17 } from "path";
9133
9212
 
9134
9213
  // src/application/flows/refine/flow.ts
9135
- import { join as join14 } from "path";
9214
+ import { join as join16 } from "path";
9136
9215
 
9137
9216
  // src/application/flows/_shared/build-unit.ts
9138
9217
  import { promises as fs11 } from "fs";
9139
- import { join as join13 } from "path";
9218
+ import { join as join15 } from "path";
9140
9219
  var buildUnitLeaf = (opts) => leaf(opts.name, {
9141
9220
  useCase: {
9142
9221
  execute: async (input) => {
@@ -9157,7 +9236,7 @@ var buildUnitLeaf = (opts) => leaf(opts.name, {
9157
9236
  return Result.ok(parsed.value);
9158
9237
  }
9159
9238
  },
9160
- input: (ctx) => ({ path: join13(String(opts.parent(ctx)), opts.slug(ctx)) }),
9239
+ input: (ctx) => ({ path: join15(String(opts.parent(ctx)), opts.slug(ctx)) }),
9161
9240
  output: (ctx, root) => opts.write(ctx, root)
9162
9241
  });
9163
9242
 
@@ -9350,7 +9429,7 @@ var refineTicketInteractiveLeaf = (deps, ticket) => leaf(`refine-ticket-${String
9350
9429
  execute: async (input) => {
9351
9430
  const session = await deps.runInTerminal(
9352
9431
  async () => deps.interactiveAi.run({
9353
- cwd: deps.cwd,
9432
+ cwd: input.cwd,
9354
9433
  promptFile: input.promptFile,
9355
9434
  outputFile: input.outputFile,
9356
9435
  model: deps.model
@@ -9407,9 +9486,18 @@ var refineTicketInteractiveLeaf = (deps, ticket) => leaf(`refine-ticket-${String
9407
9486
  message: `refine-ticket-${String(ticket.id)}: prompt/output paths missing \u2014 render-prompt-to-file must run first`
9408
9487
  });
9409
9488
  }
9489
+ if (ctx.currentUnitRoot === void 0) {
9490
+ throw new InvalidStateError({
9491
+ entity: "chain",
9492
+ currentState: "pre-refine",
9493
+ attemptedAction: `refine-ticket-${String(ticket.id)}`,
9494
+ message: `refine-ticket-${String(ticket.id)}: unit root missing \u2014 build-refine-unit must run first`
9495
+ });
9496
+ }
9410
9497
  return {
9411
9498
  sprint: ctx.sprint,
9412
9499
  ticket,
9500
+ cwd: ctx.currentUnitRoot,
9413
9501
  promptFile: ctx.currentPromptFile,
9414
9502
  outputFile: ctx.currentOutputFile
9415
9503
  };
@@ -9610,8 +9698,8 @@ var createRefineFlow = (deps, opts) => {
9610
9698
  parent: () => opts.refinementRoot,
9611
9699
  slug: () => ticketSlug(ticket),
9612
9700
  write: (ctx, root) => {
9613
- const promptPath = AbsolutePath.parse(join14(String(root), "prompt.md"));
9614
- const outputPath = AbsolutePath.parse(join14(String(root), "requirements.md"));
9701
+ const promptPath = AbsolutePath.parse(join16(String(root), "prompt.md"));
9702
+ const outputPath = AbsolutePath.parse(join16(String(root), "requirements.md"));
9615
9703
  if (!promptPath.ok || !outputPath.ok) {
9616
9704
  throw promptPath.ok ? outputPath.ok ? new Error("unreachable") : outputPath.error : promptPath.error;
9617
9705
  }
@@ -9648,9 +9736,17 @@ var createRefineFlow = (deps, opts) => {
9648
9736
  {
9649
9737
  name: `install-skills-${String(ticket.id)}`,
9650
9738
  flowId: "refine",
9651
- // Skills land in the AI session's cwd (the repo) the provider-native conventions
9652
- // only auto-discover skills from cwd, not from `--add-dir` roots.
9653
- cwdPicker: () => opts.cwd
9739
+ // Skills land in the AI session's cwd for refine that's the per-ticket unit root
9740
+ // (`<sprintDir>/refinement/<ticket-slug>/`), not the user's repo. Refinement is
9741
+ // implementation-agnostic, so we keep the AI out of the repo's auto-discovered context.
9742
+ cwdPicker: (ctx) => {
9743
+ if (ctx.currentUnitRoot === void 0) {
9744
+ throw new Error(
9745
+ `install-skills-${String(ticket.id)}: currentUnitRoot missing \u2014 build-refine-unit must run first`
9746
+ );
9747
+ }
9748
+ return ctx.currentUnitRoot;
9749
+ }
9654
9750
  }
9655
9751
  ),
9656
9752
  refineTicketInteractiveLeaf(
@@ -9658,7 +9754,6 @@ var createRefineFlow = (deps, opts) => {
9658
9754
  interactiveAi: deps.interactiveAi,
9659
9755
  runInTerminal: deps.runInTerminal,
9660
9756
  logger: deps.logger,
9661
- cwd: opts.cwd,
9662
9757
  model: opts.model,
9663
9758
  ...deps.reviewBeforeApprove !== void 0 ? { reviewBeforeApprove: deps.reviewBeforeApprove } : {},
9664
9759
  ...deps.issuePusher !== void 0 ? { issuePusher: deps.issuePusher } : {},
@@ -9668,7 +9763,17 @@ var createRefineFlow = (deps, opts) => {
9668
9763
  ),
9669
9764
  uninstallSkillsLeaf(
9670
9765
  { skillsAdapter: deps.skillsAdapter },
9671
- { name: `uninstall-skills-${String(ticket.id)}`, cwdPicker: () => opts.cwd }
9766
+ {
9767
+ name: `uninstall-skills-${String(ticket.id)}`,
9768
+ cwdPicker: (ctx) => {
9769
+ if (ctx.currentUnitRoot === void 0) {
9770
+ throw new Error(
9771
+ `uninstall-skills-${String(ticket.id)}: currentUnitRoot missing \u2014 build-refine-unit must run first`
9772
+ );
9773
+ }
9774
+ return ctx.currentUnitRoot;
9775
+ }
9776
+ }
9672
9777
  ),
9673
9778
  saveSprintLeaf({ sprintRepo: deps.sprintRepo }, `save-after-${String(ticket.id)}`)
9674
9779
  ])
@@ -9691,12 +9796,11 @@ var deriveOriginFromGit = async (cwd, gitRunner) => {
9691
9796
  return parsed === null ? void 0 : parsed;
9692
9797
  };
9693
9798
  var launchRefine = async (ctx) => {
9694
- const { deps, snapshot, extras, settings, interactiveAi, skillsAdapter, skillSource, cwd, bridge, sessionId: sessionId2 } = ctx;
9799
+ const { deps, snapshot, extras, settings, interactiveAi, skillsAdapter, skillSource, bridge, sessionId: sessionId2 } = ctx;
9695
9800
  if (!snapshot.sprint) return { ok: false, reason: "No sprint selected." };
9696
- if (!cwd) return { ok: false, reason: "No repository path resolvable from the project." };
9697
9801
  const pending = snapshot.sprint.tickets.filter((t) => t.status === "pending");
9698
9802
  const refinementRoot = AbsolutePath.parse(
9699
- join15(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "refinement")
9803
+ join17(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "refinement")
9700
9804
  );
9701
9805
  if (!refinementRoot.ok) return { ok: false, reason: refinementRoot.error.message };
9702
9806
  let defaultIssueOrigin = snapshot.project?.defaultIssueOrigin;
@@ -9756,7 +9860,6 @@ ${body.trim()}`;
9756
9860
  {
9757
9861
  sprintId: snapshot.sprint.id,
9758
9862
  pendingTickets: pending,
9759
- cwd,
9760
9863
  model: extras.modelOverride ?? settings.ai.models.refine,
9761
9864
  refinementRoot: refinementRoot.value
9762
9865
  }
@@ -9770,10 +9873,10 @@ ${body.trim()}`;
9770
9873
  };
9771
9874
 
9772
9875
  // src/application/ui/shared/launch/plan.ts
9773
- import { join as join17 } from "path";
9876
+ import { join as join19 } from "path";
9774
9877
 
9775
9878
  // src/application/flows/plan/flow.ts
9776
- import { join as join16 } from "path";
9879
+ import { join as join18 } from "path";
9777
9880
 
9778
9881
  // src/application/flows/_shared/sprint/load-execution.ts
9779
9882
  var loadSprintExecutionLeaf = (deps, name = "load-sprint-execution") => leaf(name, {
@@ -10214,6 +10317,13 @@ var markTaskBlocked = (task, reason) => {
10214
10317
  if (!guard2.ok) return Result.error(guard2.error);
10215
10318
  return Result.ok({ ...guard2.value, status: "blocked", blockedReason: reason });
10216
10319
  };
10320
+ var unblockTask = (task) => {
10321
+ const guard2 = requireStatus("task", task, ["blocked"], "unblock");
10322
+ if (!guard2.ok) return Result.error(guard2.error);
10323
+ const { blockedReason: _ignored, ...rest } = guard2.value;
10324
+ void _ignored;
10325
+ return Result.ok({ ...rest, status: "todo" });
10326
+ };
10217
10327
  var latestCritique = (task) => {
10218
10328
  for (let i = task.attempts.length - 1; i >= 0; i--) {
10219
10329
  const att = task.attempts[i];
@@ -10394,7 +10504,7 @@ var callPlannerInteractiveLeaf = (deps) => leaf("call-planner-interactive", {
10394
10504
  const additionalRoots = deps.additionalRoots ?? [];
10395
10505
  const session = await deps.runInTerminal(
10396
10506
  async () => deps.interactiveAi.run({
10397
- cwd: deps.cwd,
10507
+ cwd: input.cwd,
10398
10508
  promptFile: input.promptFile,
10399
10509
  outputFile: input.outputFile,
10400
10510
  model: deps.model,
@@ -10471,10 +10581,19 @@ var callPlannerInteractiveLeaf = (deps) => leaf("call-planner-interactive", {
10471
10581
  message: "call-planner-interactive: prompt/output paths missing \u2014 render-prompt-to-file must run first"
10472
10582
  });
10473
10583
  }
10584
+ if (ctx.currentUnitRoot === void 0) {
10585
+ throw new InvalidStateError({
10586
+ entity: "chain",
10587
+ currentState: "pre-plan",
10588
+ attemptedAction: "call-planner-interactive",
10589
+ message: "call-planner-interactive: unit root missing \u2014 build-plan-unit must run first"
10590
+ });
10591
+ }
10474
10592
  return {
10475
10593
  sprint: ctx.sprint,
10476
10594
  project: ctx.project,
10477
10595
  existingTasks: ctx.tasks ?? [],
10596
+ cwd: ctx.currentUnitRoot,
10478
10597
  promptFile: ctx.currentPromptFile,
10479
10598
  outputFile: ctx.currentOutputFile
10480
10599
  };
@@ -10498,8 +10617,8 @@ var createPlanFlow = (deps, opts) => {
10498
10617
  parent: () => opts.planRoot,
10499
10618
  slug: () => slug,
10500
10619
  write: (ctx, root) => {
10501
- const promptPath = AbsolutePath.parse(join16(String(root), "prompt.md"));
10502
- const outputPath = AbsolutePath.parse(join16(String(root), "plan.json"));
10620
+ const promptPath = AbsolutePath.parse(join18(String(root), "prompt.md"));
10621
+ const outputPath = AbsolutePath.parse(join18(String(root), "plan.json"));
10503
10622
  if (!promptPath.ok) throw promptPath.error;
10504
10623
  if (!outputPath.ok) throw outputPath.error;
10505
10624
  return {
@@ -10536,9 +10655,21 @@ var createPlanFlow = (deps, opts) => {
10536
10655
  { skillsAdapter: deps.skillsAdapter, skillSource: deps.skillSource },
10537
10656
  {
10538
10657
  flowId: "plan",
10539
- // Skills land in the AI session's cwd (the repo) the provider-native conventions
10540
- // only auto-discover skills from cwd, not from `--add-dir` roots.
10541
- cwdPicker: () => opts.cwd
10658
+ // Skills land in the AI session's cwd for plan that's the per-sprint plan unit root
10659
+ // (`<sprintDir>/plan/<run-slug>/`), not any project repo. Plan mounts every repo as an
10660
+ // equal `--add-dir` source; rooting the session in any one repo would auto-load that
10661
+ // repo's `CLAUDE.md` / agents / `.mcp.json` and bias the planner toward it.
10662
+ cwdPicker: (ctx) => {
10663
+ if (ctx.currentUnitRoot === void 0) {
10664
+ throw new InvalidStateError({
10665
+ entity: "chain",
10666
+ currentState: "pre-plan",
10667
+ attemptedAction: "install-skills",
10668
+ message: "install-skills: currentUnitRoot missing \u2014 build-plan-unit must run first"
10669
+ });
10670
+ }
10671
+ return ctx.currentUnitRoot;
10672
+ }
10542
10673
  }
10543
10674
  ),
10544
10675
  callPlannerInteractiveLeaf({
@@ -10546,12 +10677,26 @@ var createPlanFlow = (deps, opts) => {
10546
10677
  runInTerminal: deps.runInTerminal,
10547
10678
  logger: deps.logger,
10548
10679
  clock: deps.clock,
10549
- cwd: opts.cwd,
10550
10680
  model: opts.model,
10551
10681
  ...opts.additionalRoots !== void 0 && opts.additionalRoots.length > 0 ? { additionalRoots: opts.additionalRoots } : {},
10552
10682
  ...deps.reviewBeforeApprove !== void 0 ? { reviewBeforeApprove: deps.reviewBeforeApprove } : {}
10553
10683
  }),
10554
- uninstallSkillsLeaf({ skillsAdapter: deps.skillsAdapter }, { cwdPicker: () => opts.cwd }),
10684
+ uninstallSkillsLeaf(
10685
+ { skillsAdapter: deps.skillsAdapter },
10686
+ {
10687
+ cwdPicker: (ctx) => {
10688
+ if (ctx.currentUnitRoot === void 0) {
10689
+ throw new InvalidStateError({
10690
+ entity: "chain",
10691
+ currentState: "pre-plan",
10692
+ attemptedAction: "uninstall-skills",
10693
+ message: "uninstall-skills: currentUnitRoot missing \u2014 build-plan-unit must run first"
10694
+ });
10695
+ }
10696
+ return ctx.currentUnitRoot;
10697
+ }
10698
+ }
10699
+ ),
10555
10700
  saveTasksLeaf({ taskRepo: deps.taskRepo }),
10556
10701
  saveSprintLeaf({ sprintRepo: deps.sprintRepo })
10557
10702
  ]);
@@ -10559,12 +10704,11 @@ var createPlanFlow = (deps, opts) => {
10559
10704
 
10560
10705
  // src/application/ui/shared/launch/plan.ts
10561
10706
  var launchPlan = (ctx) => {
10562
- const { deps, snapshot, extras, settings, interactiveAi, skillsAdapter, skillSource, cwd, bridge, sessionId: sessionId2 } = ctx;
10707
+ const { deps, snapshot, extras, settings, interactiveAi, skillsAdapter, skillSource, bridge, sessionId: sessionId2 } = ctx;
10563
10708
  if (!snapshot.project) return { ok: false, reason: "No project loaded." };
10564
10709
  if (!snapshot.sprint) return { ok: false, reason: "No sprint selected." };
10565
- if (!cwd) return { ok: false, reason: "No repository path resolvable from the project." };
10566
10710
  const planRoot = AbsolutePath.parse(
10567
- join17(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "plan")
10711
+ join19(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "plan")
10568
10712
  );
10569
10713
  if (!planRoot.ok) return { ok: false, reason: planRoot.error.message };
10570
10714
  const reviewBeforeApprove = async (proposedTasks) => {
@@ -10601,9 +10745,9 @@ ${summary}`;
10601
10745
  {
10602
10746
  sprintId: snapshot.sprint.id,
10603
10747
  projectId: snapshot.project.id,
10604
- cwd,
10605
- // Mount every repo on the project so the planner can navigate across them without per-file
10606
- // approval prompts. Duplicates with `cwd` are folded out by the adapter.
10748
+ // Mount every repo on the project as an equal `--add-dir` source so the planner can
10749
+ // navigate across them without per-file approval prompts. No repo enjoys cwd privilege
10750
+ // the session's cwd is the per-sprint plan unit root.
10607
10751
  additionalRoots: snapshot.project.repositories.map((r) => r.path),
10608
10752
  model: extras.modelOverride ?? settings.ai.models.plan,
10609
10753
  planRoot: planRoot.value
@@ -10618,7 +10762,7 @@ ${summary}`;
10618
10762
  };
10619
10763
 
10620
10764
  // src/application/ui/shared/launch/implement.ts
10621
- import { join as join21 } from "path";
10765
+ import { join as join23 } from "path";
10622
10766
 
10623
10767
  // src/application/chain/build/guard.ts
10624
10768
  var guard = (name, predicate, body) => ({
@@ -10675,7 +10819,7 @@ var loop = (name, body, opts = {}) => ({
10675
10819
 
10676
10820
  // src/integration/observability/sinks/progress-file-sink.ts
10677
10821
  import { promises as fs14 } from "fs";
10678
- import { dirname as dirname7 } from "path";
10822
+ import { dirname as dirname8 } from "path";
10679
10823
  var MAX_QUEUE_DEPTH = 1e4;
10680
10824
  var createProgressFileSink = (deps) => {
10681
10825
  const queue = [];
@@ -10795,7 +10939,7 @@ var TEMPLATE = `${TITLE}
10795
10939
  ${HEADINGS.map((h) => `## ${h}
10796
10940
  `).join("\n")}`;
10797
10941
  var mergeSection = async (path, rendered) => {
10798
- await fs14.mkdir(dirname7(path), { recursive: true });
10942
+ await fs14.mkdir(dirname8(path), { recursive: true });
10799
10943
  let current;
10800
10944
  try {
10801
10945
  current = await fs14.readFile(path, "utf8");
@@ -11017,6 +11161,45 @@ var gitCommitWithMessage = async (runner, cwd, message) => {
11017
11161
  if (!head.ok) return Result.error(head.error);
11018
11162
  return Result.ok({ committed: true, headSha: head.value });
11019
11163
  };
11164
+ var gitStashPush = async (runner, cwd, message) => {
11165
+ const dirty = await gitHasUncommittedChanges(runner, cwd);
11166
+ if (!dirty.ok) return Result.error(dirty.error);
11167
+ if (!dirty.value) return Result.ok({ stashed: false });
11168
+ const stash = await runner.run(cwd, ["stash", "push", "-u", "-m", message]);
11169
+ if (!stash.ok) return Result.error(stash.error);
11170
+ if (stash.value.exitCode !== 0) {
11171
+ return Result.error(
11172
+ new StorageError({
11173
+ subCode: "io",
11174
+ message: `git stash push failed: ${(stash.value.stderr || stash.value.stdout).trim()}`
11175
+ })
11176
+ );
11177
+ }
11178
+ return Result.ok({ stashed: true });
11179
+ };
11180
+ var gitResetHard = async (runner, cwd) => {
11181
+ const reset = await runner.run(cwd, ["reset", "--hard", "HEAD"]);
11182
+ if (!reset.ok) return Result.error(reset.error);
11183
+ if (reset.value.exitCode !== 0) {
11184
+ return Result.error(
11185
+ new StorageError({
11186
+ subCode: "io",
11187
+ message: `git reset --hard failed: ${(reset.value.stderr || reset.value.stdout).trim()}`
11188
+ })
11189
+ );
11190
+ }
11191
+ const clean = await runner.run(cwd, ["clean", "-fd"]);
11192
+ if (!clean.ok) return Result.error(clean.error);
11193
+ if (clean.value.exitCode !== 0) {
11194
+ return Result.error(
11195
+ new StorageError({
11196
+ subCode: "io",
11197
+ message: `git clean -fd failed: ${(clean.value.stderr || clean.value.stdout).trim()}`
11198
+ })
11199
+ );
11200
+ }
11201
+ return Result.ok(void 0);
11202
+ };
11020
11203
  var gitGetCurrentBranch = async (runner, cwd) => {
11021
11204
  const result = await runner.run(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
11022
11205
  if (!result.ok) return Result.error(result.error);
@@ -11492,9 +11675,9 @@ var implementSession = (sandboxCwd, repoPath, prompt, model, signalsFile) => ({
11492
11675
  });
11493
11676
 
11494
11677
  // src/application/flows/implement/leaves/round-artifacts.ts
11495
- import { join as join18 } from "path";
11678
+ import { join as join20 } from "path";
11496
11679
  var nextRoundNum = async (workspaceRoot) => {
11497
- const entries = await listDir(join18(String(workspaceRoot), "rounds"));
11680
+ const entries = await listDir(join20(String(workspaceRoot), "rounds"));
11498
11681
  if (!entries.ok) return 1;
11499
11682
  let max = 0;
11500
11683
  for (const name of entries.value) {
@@ -11503,12 +11686,12 @@ var nextRoundNum = async (workspaceRoot) => {
11503
11686
  }
11504
11687
  return max + 1;
11505
11688
  };
11506
- var roundSignalsPath = (workspaceRoot, round, role) => join18(String(workspaceRoot), "rounds", String(round), role, "signals.json");
11507
- var roundEvaluationRelativePath = (round) => join18("rounds", String(round), "evaluator", "evaluation.md");
11508
- var roundDir = (workspaceRoot, round, role) => join18(String(workspaceRoot), "rounds", String(round), role);
11689
+ var roundSignalsPath = (workspaceRoot, round, role) => join20(String(workspaceRoot), "rounds", String(round), role, "signals.json");
11690
+ var roundEvaluationRelativePath = (round) => join20("rounds", String(round), "evaluator", "evaluation.md");
11691
+ var roundDir = (workspaceRoot, round, role) => join20(String(workspaceRoot), "rounds", String(round), role);
11509
11692
  var writeEvaluatorRoundArtifacts = async (workspaceRoot, round, signals, logger) => {
11510
11693
  const base = roundDir(workspaceRoot, round, "evaluator");
11511
- const evaluation = await writeTextAtomic(join18(base, "evaluation.md"), renderEvaluation(findEvaluation2(signals)));
11694
+ const evaluation = await writeTextAtomic(join20(base, "evaluation.md"), renderEvaluation(findEvaluation2(signals)));
11512
11695
  if (!evaluation.ok) {
11513
11696
  logger?.warn("failed to write evaluator round artifact", { round, base, error: evaluation.error.message });
11514
11697
  }
@@ -12015,6 +12198,7 @@ var postTaskCheckLeaf = (deps, opts, taskId) => {
12015
12198
  };
12016
12199
 
12017
12200
  // src/business/task/preflight-task.ts
12201
+ var ELEMENT_NAME = "preflight-task";
12018
12202
  var preflightTaskUseCase = async (props) => {
12019
12203
  const log = props.logger.named("task.preflight");
12020
12204
  log.debug("checking working tree", { cwd: props.cwd });
@@ -12035,6 +12219,9 @@ var preflightTaskUseCase = async (props) => {
12035
12219
  });
12036
12220
  return Result.ok(void 0);
12037
12221
  }
12222
+ if (policy === "prompt") {
12223
+ return resolveViaPrompt(props, count.value);
12224
+ }
12038
12225
  log.warn("refusing to start a task on a dirty tree", { cwd: props.cwd, dirtyEntries: count.value });
12039
12226
  return Result.error(
12040
12227
  new InvalidStateError({
@@ -12046,24 +12233,105 @@ var preflightTaskUseCase = async (props) => {
12046
12233
  })
12047
12234
  );
12048
12235
  };
12236
+ var resolveViaPrompt = async (props, dirtyEntries) => {
12237
+ const log = props.logger.named("task.preflight");
12238
+ if (props.askDirtyTreeChoice === void 0 || props.gitStash === void 0 || props.gitReset === void 0 || props.clock === void 0) {
12239
+ throw new InvalidStateError({
12240
+ entity: ELEMENT_NAME,
12241
+ currentState: "prompt-without-deps",
12242
+ attemptedAction: "configure-prompt-deps",
12243
+ message: "preflight-task: dirtyTreePolicy='prompt' requires askDirtyTreeChoice, gitStash, gitReset, and clock dependencies"
12244
+ });
12245
+ }
12246
+ const choice = await props.askDirtyTreeChoice({ cwd: props.cwd, dirtyEntries });
12247
+ if (!choice.ok) {
12248
+ return Result.error(choice.error);
12249
+ }
12250
+ switch (choice.value) {
12251
+ case "keep":
12252
+ log.info(`working tree dirty (${String(dirtyEntries)} entries) \u2014 proceeding (user chose 'keep')`, {
12253
+ cwd: props.cwd,
12254
+ dirtyEntries
12255
+ });
12256
+ return Result.ok(void 0);
12257
+ case "stash": {
12258
+ const sprintLabel = props.sprintId !== void 0 && props.sprintId.length > 0 ? props.sprintId : "unknown";
12259
+ const message = `ralphctl preflight stash (sprint ${sprintLabel}, ${String(props.clock())})`;
12260
+ const stashed = await props.gitStash(props.cwd, message);
12261
+ if (!stashed.ok) return Result.error(stashed.error);
12262
+ if (!stashed.value.stashed) {
12263
+ log.warn("stash reported no changes despite dirty status \u2014 proceeding", { cwd: props.cwd });
12264
+ return Result.ok(void 0);
12265
+ }
12266
+ log.info(`stashed working tree \u2014 recoverable as: ${message}`, { cwd: props.cwd, stashMessage: message });
12267
+ return Result.ok(void 0);
12268
+ }
12269
+ case "reset": {
12270
+ const reset = await props.gitReset(props.cwd);
12271
+ if (!reset.ok) return Result.error(reset.error);
12272
+ log.info("reset working tree \u2014 discarded uncommitted + untracked changes", { cwd: props.cwd });
12273
+ return Result.ok(void 0);
12274
+ }
12275
+ case "cancel":
12276
+ return Result.error(
12277
+ new AbortError({ elementName: ELEMENT_NAME, reason: "user cancelled on dirty working tree" })
12278
+ );
12279
+ }
12280
+ };
12049
12281
 
12050
12282
  // src/application/flows/implement/leaves/preflight-task.ts
12283
+ var ELEMENT_NAME2 = "preflight-task";
12051
12284
  var preflightTaskLeaf = (deps, cwd, name = "preflight-task") => {
12052
12285
  const gitStatusEntryCount = async (path) => {
12053
12286
  const status = await gitStatusPorcelain(deps.gitRunner, path);
12054
12287
  if (!status.ok) return status;
12055
12288
  return { ok: true, value: status.value.length };
12056
12289
  };
12290
+ const gitStash = (path, message) => gitStashPush(deps.gitRunner, path, message);
12291
+ const gitReset = (path) => gitResetHard(deps.gitRunner, path);
12292
+ const askDirtyTreeChoice = async ({
12293
+ cwd: dirtyCwd,
12294
+ dirtyEntries
12295
+ }) => {
12296
+ const choice = await deps.interactive.askChoice(
12297
+ `Working tree at ${String(dirtyCwd)} has ${String(dirtyEntries)} uncommitted change(s). How do you want to handle it?`,
12298
+ [
12299
+ {
12300
+ label: "Keep changes \u2014 proceed on the dirty tree",
12301
+ value: "keep",
12302
+ description: "AI may build on / overwrite the pending diff"
12303
+ },
12304
+ { label: "Stash \u2014 save changes to a recoverable stash, then proceed", value: "stash" },
12305
+ {
12306
+ label: "Reset \u2014 discard all uncommitted + untracked changes, then proceed",
12307
+ value: "reset",
12308
+ description: "git reset --hard && git clean -fd"
12309
+ },
12310
+ { label: "Cancel \u2014 abort the implement run", value: "cancel" }
12311
+ ]
12312
+ );
12313
+ if (!choice.ok) {
12314
+ return Result.error(
12315
+ new AbortError({ elementName: ELEMENT_NAME2, reason: `dirty-tree prompt cancelled \u2014 ${choice.error.message}` })
12316
+ );
12317
+ }
12318
+ return Result.ok(choice.value);
12319
+ };
12057
12320
  return leaf(name, {
12058
12321
  useCase: {
12059
- execute: async () => preflightTaskUseCase({
12322
+ execute: async (input) => preflightTaskUseCase({
12060
12323
  cwd,
12061
12324
  gitStatusEntryCount,
12325
+ gitStash,
12326
+ gitReset,
12327
+ askDirtyTreeChoice,
12328
+ clock: deps.clock,
12329
+ sprintId: input.sprintId,
12062
12330
  logger: deps.logger,
12063
12331
  ...deps.dirtyTreePolicy !== void 0 ? { dirtyTreePolicy: deps.dirtyTreePolicy } : {}
12064
12332
  })
12065
12333
  },
12066
- input: () => void 0,
12334
+ input: (ctx) => ({ sprintId: String(ctx.sprintId) }),
12067
12335
  output: (ctx) => ctx
12068
12336
  });
12069
12337
  };
@@ -12547,7 +12815,7 @@ var startAttemptLeaf = (deps, taskId) => leaf(
12547
12815
 
12548
12816
  // src/application/flows/implement/leaves/build-task-workspace.ts
12549
12817
  import { promises as fs16 } from "fs";
12550
- import { join as join19 } from "path";
12818
+ import { join as join21 } from "path";
12551
12819
  var renderDoneCriteria = (task) => {
12552
12820
  const header = `# Done criteria \u2014 ${task.name}
12553
12821
 
@@ -12580,7 +12848,7 @@ var buildTaskWorkspaceLeaf = (deps, opts, taskId) => leaf(`build-task-workspace-
12580
12848
  useCase: {
12581
12849
  execute: async (input) => {
12582
12850
  const log = deps.logger.named("implement.workspace");
12583
- const workspaceRoot = join19(String(opts.sprintDir), "implement", String(input.task.id));
12851
+ const workspaceRoot = join21(String(opts.sprintDir), "implement", String(input.task.id));
12584
12852
  const prompt = await buildImplementPrompt(deps.templateLoader, {
12585
12853
  task: input.task,
12586
12854
  projectPath: String(opts.cwd),
@@ -12588,10 +12856,10 @@ var buildTaskWorkspaceLeaf = (deps, opts, taskId) => leaf(`build-task-workspace-
12588
12856
  ...opts.checkScript !== void 0 ? { checkScript: opts.checkScript } : {}
12589
12857
  });
12590
12858
  if (!prompt.ok) return Result.error(prompt.error);
12591
- const wrotePrompt = await writeOrError(join19(workspaceRoot, "prompt.md"), String(prompt.value));
12859
+ const wrotePrompt = await writeOrError(join21(workspaceRoot, "prompt.md"), String(prompt.value));
12592
12860
  if (!wrotePrompt.ok) return Result.error(wrotePrompt.error);
12593
12861
  const wroteCriteria = await writeOrError(
12594
- join19(workspaceRoot, "done-criteria.md"),
12862
+ join21(workspaceRoot, "done-criteria.md"),
12595
12863
  renderDoneCriteria(input.task)
12596
12864
  );
12597
12865
  if (!wroteCriteria.ok) return Result.error(wroteCriteria.error);
@@ -12662,15 +12930,20 @@ var transitionSprintToReviewLeaf = (deps) => leaf("transition-sprint-to-review",
12662
12930
 
12663
12931
  // src/integration/io/lock-paths.ts
12664
12932
  import { createHash } from "crypto";
12665
- import { join as join20 } from "path";
12933
+ import { join as join22 } from "path";
12666
12934
  var repoLockFile = (locksRoot, worktreePath) => {
12667
12935
  const hash = createHash("sha1").update(String(worktreePath)).digest("hex").slice(0, 16);
12668
- return AbsolutePath.parse(join20(String(locksRoot), `repo-${hash}.lock`));
12936
+ return AbsolutePath.parse(join22(String(locksRoot), `repo-${hash}.lock`));
12669
12937
  };
12670
12938
 
12671
12939
  // src/application/flows/implement/leaves/with-repo-lock.ts
12672
12940
  var withRepoLock = (opts, inner) => ({
12673
12941
  name: `with-repo-lock(${inner.name})`,
12942
+ // Expose the wrapped chain through the composite-pattern `children` slot so `flattenLeaves`
12943
+ // walks into it when the TUI builds its planned-leaf list. Without this the wrapper looked
12944
+ // like an opaque single leaf and the Flow-steps panel rendered only "with-repo-lock(…)" —
12945
+ // never the real setup / per-task / teardown sequence inside the lock.
12946
+ children: [inner],
12674
12947
  async execute(ctx, signal, onTrace) {
12675
12948
  const lockPath = repoLockFile(opts.locksRoot, opts.worktreePath);
12676
12949
  if (!lockPath.ok) {
@@ -12837,12 +13110,15 @@ var createImplementFlow = (deps, opts) => {
12837
13110
  path: r.path,
12838
13111
  ...r.setupScript !== void 0 ? { setupScript: r.setupScript } : {}
12839
13112
  }));
13113
+ const dirtyTreePolicy = opts.dirtyTreePolicy ?? "prompt";
12840
13114
  const preflightLeaves = uniqueRepoCwds.map(
12841
13115
  (cwd, i) => preflightTaskLeaf(
12842
13116
  {
12843
13117
  gitRunner: deps.gitRunner,
13118
+ interactive: deps.interactive,
13119
+ clock: deps.clock,
12844
13120
  logger: deps.logger,
12845
- ...opts.dirtyTreePolicy !== void 0 ? { dirtyTreePolicy: opts.dirtyTreePolicy } : {}
13121
+ dirtyTreePolicy
12846
13122
  },
12847
13123
  cwd,
12848
13124
  `preflight-task-${String(i + 1)}-${String(cwd)}`
@@ -12903,7 +13179,7 @@ var createImplementFlow = (deps, opts) => {
12903
13179
 
12904
13180
  // src/integration/observability/sinks/file-log-sink.ts
12905
13181
  import { promises as fs17 } from "fs";
12906
- import { dirname as dirname8 } from "path";
13182
+ import { dirname as dirname9 } from "path";
12907
13183
  var startFileLogSink = (deps) => {
12908
13184
  const queue = [];
12909
13185
  let draining;
@@ -12915,7 +13191,7 @@ var startFileLogSink = (deps) => {
12915
13191
  if (next === void 0) continue;
12916
13192
  try {
12917
13193
  if (!dirEnsured) {
12918
- await fs17.mkdir(dirname8(String(deps.file)), { recursive: true });
13194
+ await fs17.mkdir(dirname9(String(deps.file)), { recursive: true });
12919
13195
  dirEnsured = true;
12920
13196
  }
12921
13197
  await fs17.appendFile(String(deps.file), `${JSON.stringify(next)}
@@ -12957,11 +13233,11 @@ var launchImplement = (ctx) => {
12957
13233
  return a.status === "in_progress" ? -1 : 1;
12958
13234
  });
12959
13235
  if (todoTasks.length === 0) return { ok: false, reason: "No tasks to implement or resume." };
12960
- const sprintDirPath = AbsolutePath.parse(join21(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id)));
13236
+ const sprintDirPath = AbsolutePath.parse(join23(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id)));
12961
13237
  if (!sprintDirPath.ok) return { ok: false, reason: sprintDirPath.error.message };
12962
- const progressPath = AbsolutePath.parse(join21(String(sprintDirPath.value), "progress.md"));
13238
+ const progressPath = AbsolutePath.parse(join23(String(sprintDirPath.value), "progress.md"));
12963
13239
  if (!progressPath.ok) return { ok: false, reason: progressPath.error.message };
12964
- const chainLogPath = AbsolutePath.parse(join21(String(sprintDirPath.value), "chain.log"));
13240
+ const chainLogPath = AbsolutePath.parse(join23(String(sprintDirPath.value), "chain.log"));
12965
13241
  if (!chainLogPath.ok) return { ok: false, reason: chainLogPath.error.message };
12966
13242
  const chainLog = startFileLogSink({ file: chainLogPath.value, bus: deps.app.eventBus });
12967
13243
  const repositories = /* @__PURE__ */ new Map();
@@ -13026,7 +13302,7 @@ var launchImplement = (ctx) => {
13026
13302
  };
13027
13303
 
13028
13304
  // src/application/ui/shared/launch/review.ts
13029
- import { join as join23 } from "path";
13305
+ import { join as join25 } from "path";
13030
13306
 
13031
13307
  // src/application/flows/review/leaves/ensure-feedback-file.ts
13032
13308
  import { promises as fs18 } from "fs";
@@ -13269,12 +13545,12 @@ var buildApplyFeedbackPrompt = async (deps, input) => buildPrompt(deps, applyFee
13269
13545
 
13270
13546
  // src/integration/ai/signals/_engine/temp-signals-file.ts
13271
13547
  import { tmpdir as tmpdir2 } from "os";
13272
- import { join as join22 } from "path";
13548
+ import { join as join24 } from "path";
13273
13549
  var counter = 0;
13274
13550
  var allocSignalsTempPath = (label) => {
13275
13551
  counter += 1;
13276
13552
  const filename = `ralphctl-signals-${label}-${String(process.pid)}-${String(Date.now())}-${String(counter)}.json`;
13277
- return AbsolutePath.parse(join22(tmpdir2(), filename));
13553
+ return AbsolutePath.parse(join24(tmpdir2(), filename));
13278
13554
  };
13279
13555
  var withSignalsTempPath = async (label, fn) => {
13280
13556
  const path = allocSignalsTempPath(label);
@@ -13533,11 +13809,11 @@ var launchReview = (ctx) => {
13533
13809
  if (!snapshot.sprint) return { ok: false, reason: "No sprint selected." };
13534
13810
  if (!cwd) return { ok: false, reason: "No repository path resolvable from the project." };
13535
13811
  const feedbackPath = AbsolutePath.parse(
13536
- join23(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "feedback.md")
13812
+ join25(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "feedback.md")
13537
13813
  );
13538
13814
  if (!feedbackPath.ok) return { ok: false, reason: feedbackPath.error.message };
13539
13815
  const progressPath = AbsolutePath.parse(
13540
- join23(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "progress.md")
13816
+ join25(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "progress.md")
13541
13817
  );
13542
13818
  if (!progressPath.ok) return { ok: false, reason: progressPath.error.message };
13543
13819
  const checkScript = snapshot.project?.repositories.find((r) => r.checkScript !== void 0)?.checkScript;
@@ -13766,11 +14042,11 @@ var probeReadinessLeaf = (deps) => leaf("probe", {
13766
14042
  import { promises as fs21 } from "fs";
13767
14043
 
13768
14044
  // src/integration/ai/readiness/_engine/setup.ts
13769
- import { join as join25 } from "path";
14045
+ import { join as join27 } from "path";
13770
14046
 
13771
14047
  // src/integration/ai/runs/_engine/run-artifacts.ts
13772
14048
  import { promises as fs20 } from "fs";
13773
- import { join as join24 } from "path";
14049
+ import { join as join26 } from "path";
13774
14050
  var BODY_PREVIEW_LIMIT = 800;
13775
14051
  var buildRunDirName = () => {
13776
14052
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -13780,7 +14056,7 @@ var buildRunDirName = () => {
13780
14056
  var readRunBodyPreview = async (runDir, options) => {
13781
14057
  let raw;
13782
14058
  try {
13783
- raw = await fs20.readFile(join24(String(runDir), "body.txt"), "utf8");
14059
+ raw = await fs20.readFile(join26(String(runDir), "body.txt"), "utf8");
13784
14060
  } catch (cause) {
13785
14061
  if (isErrnoException(cause) && cause.code === "ENOENT") return void 0;
13786
14062
  const code = isErrnoException(cause) ? cause.code : "unknown";
@@ -13909,11 +14185,11 @@ var setupReadinessUseCase = async (deps, input) => {
13909
14185
  ...input.existingContextFile !== void 0 ? { existingContextFile: input.existingContextFile } : {}
13910
14186
  });
13911
14187
  if (!prompt.ok) return Result.error(prompt.error);
13912
- const runDir = AbsolutePath.parse(join25(String(deps.runsRoot), "readiness", buildRunDirName()));
14188
+ const runDir = AbsolutePath.parse(join27(String(deps.runsRoot), "readiness", buildRunDirName()));
13913
14189
  if (!runDir.ok) return Result.error(runDir.error);
13914
- const promptFile = AbsolutePath.parse(join25(String(runDir.value), "prompt.md"));
14190
+ const promptFile = AbsolutePath.parse(join27(String(runDir.value), "prompt.md"));
13915
14191
  if (!promptFile.ok) return Result.error(promptFile.error);
13916
- const bodyFile = AbsolutePath.parse(join25(String(runDir.value), "body.txt"));
14192
+ const bodyFile = AbsolutePath.parse(join27(String(runDir.value), "body.txt"));
13917
14193
  if (!bodyFile.ok) return Result.error(bodyFile.error);
13918
14194
  const promptWrote = await writeTextAtomic(String(promptFile.value), String(prompt.value));
13919
14195
  if (!promptWrote.ok) return Result.error(promptWrote.error);
@@ -13962,7 +14238,7 @@ var setupReadinessUseCase = async (deps, input) => {
13962
14238
  const verifyScript = signals.value.find(
13963
14239
  (s) => s.type === "verify-script"
13964
14240
  )?.command;
13965
- const targetPath = AbsolutePath.parse(join25(String(input.repository.path), targetPathFor(input.tool)));
14241
+ const targetPath = AbsolutePath.parse(join27(String(input.repository.path), targetPathFor(input.tool)));
13966
14242
  if (!targetPath.ok) return Result.error(targetPath.error);
13967
14243
  log.info(`proposal ready for repo ${input.repository.name}`, {
13968
14244
  repositoryId: String(input.repository.id),
@@ -14028,6 +14304,9 @@ var pickExistingContextPath = (tool, state) => {
14028
14304
  if (tool === "copilot" && a.tool === "copilot") {
14029
14305
  return a.copilotInstructions !== void 0 ? String(a.copilotInstructions.path) : void 0;
14030
14306
  }
14307
+ if (tool === "codex" && a.tool === "codex") {
14308
+ return a.agentsMd !== void 0 ? String(a.agentsMd.path) : void 0;
14309
+ }
14031
14310
  return void 0;
14032
14311
  };
14033
14312
  var proposeReadinessLeaf = (deps) => leaf("propose", {
@@ -14208,7 +14487,7 @@ var launchReadiness = (ctx) => {
14208
14487
  };
14209
14488
 
14210
14489
  // src/application/flows/detect-skills/leaves/propose.ts
14211
- import { join as join26 } from "path";
14490
+ import { join as join28 } from "path";
14212
14491
 
14213
14492
  // src/integration/ai/prompts/detect-skills/definition.ts
14214
14493
  var detectSkillsPromptDef = {
@@ -14272,11 +14551,11 @@ var proposeUseCase = async (deps, input) => {
14272
14551
  skillsConvention: deps.skillsAdapter.describeSkillsConvention()
14273
14552
  });
14274
14553
  if (!prompt.ok) return Result.error(prompt.error);
14275
- const runDir = AbsolutePath.parse(join26(String(deps.runsRoot), "detect-skills", buildRunDirName()));
14554
+ const runDir = AbsolutePath.parse(join28(String(deps.runsRoot), "detect-skills", buildRunDirName()));
14276
14555
  if (!runDir.ok) return Result.error(runDir.error);
14277
- const promptFile = AbsolutePath.parse(join26(String(runDir.value), "prompt.md"));
14556
+ const promptFile = AbsolutePath.parse(join28(String(runDir.value), "prompt.md"));
14278
14557
  if (!promptFile.ok) return Result.error(promptFile.error);
14279
- const bodyFile = AbsolutePath.parse(join26(String(runDir.value), "body.txt"));
14558
+ const bodyFile = AbsolutePath.parse(join28(String(runDir.value), "body.txt"));
14280
14559
  if (!bodyFile.ok) return Result.error(bodyFile.error);
14281
14560
  const promptWrote = await writeTextAtomic(String(promptFile.value), String(prompt.value));
14282
14561
  if (!promptWrote.ok) return Result.error(promptWrote.error);
@@ -14890,7 +15169,7 @@ var launchDetectSkills = (ctx) => {
14890
15169
  };
14891
15170
 
14892
15171
  // src/application/flows/detect-scripts/leaves/propose.ts
14893
- import { join as join27 } from "path";
15172
+ import { join as join29 } from "path";
14894
15173
 
14895
15174
  // src/integration/ai/prompts/detect-scripts/definition.ts
14896
15175
  var detectScriptsPromptDef = {
@@ -14935,11 +15214,11 @@ var proposeUseCase2 = async (deps, input) => {
14935
15214
  repositoryPath: String(input.repository.path)
14936
15215
  });
14937
15216
  if (!prompt.ok) return Result.error(prompt.error);
14938
- const runDir = AbsolutePath.parse(join27(String(deps.runsRoot), "detect-scripts", buildRunDirName()));
15217
+ const runDir = AbsolutePath.parse(join29(String(deps.runsRoot), "detect-scripts", buildRunDirName()));
14939
15218
  if (!runDir.ok) return Result.error(runDir.error);
14940
- const promptFile = AbsolutePath.parse(join27(String(runDir.value), "prompt.md"));
15219
+ const promptFile = AbsolutePath.parse(join29(String(runDir.value), "prompt.md"));
14941
15220
  if (!promptFile.ok) return Result.error(promptFile.error);
14942
- const bodyFile = AbsolutePath.parse(join27(String(runDir.value), "body.txt"));
15221
+ const bodyFile = AbsolutePath.parse(join29(String(runDir.value), "body.txt"));
14943
15222
  if (!bodyFile.ok) return Result.error(bodyFile.error);
14944
15223
  const promptWrote = await writeTextAtomic(String(promptFile.value), String(prompt.value));
14945
15224
  if (!promptWrote.ok) return Result.error(promptWrote.error);
@@ -15283,10 +15562,10 @@ var launchDetectScripts = (ctx) => {
15283
15562
  };
15284
15563
 
15285
15564
  // src/application/ui/shared/launch/ideate.ts
15286
- import { join as join29 } from "path";
15565
+ import { join as join31 } from "path";
15287
15566
 
15288
15567
  // src/application/flows/ideate/flow.ts
15289
- import { join as join28 } from "path";
15568
+ import { join as join30 } from "path";
15290
15569
 
15291
15570
  // src/integration/ai/prompts/ideate/definition.ts
15292
15571
  var nonEmpty2 = (field) => (v) => v.trim().length === 0 ? Result.error(new ValidationError({ field, value: v, message: `${field} must not be empty` })) : Result.ok(v);
@@ -15523,8 +15802,8 @@ var createIdeateFlow = (deps, opts) => {
15523
15802
  parent: () => opts.ideateRoot,
15524
15803
  slug: () => slug,
15525
15804
  write: (ctx, root) => {
15526
- const promptPath = AbsolutePath.parse(join28(String(root), "prompt.md"));
15527
- const outputPath = AbsolutePath.parse(join28(String(root), "ideate.json"));
15805
+ const promptPath = AbsolutePath.parse(join30(String(root), "prompt.md"));
15806
+ const outputPath = AbsolutePath.parse(join30(String(root), "ideate.json"));
15528
15807
  if (!promptPath.ok) throw promptPath.error;
15529
15808
  if (!outputPath.ok) throw outputPath.error;
15530
15809
  return {
@@ -15588,7 +15867,7 @@ var launchIdeate = async (ctx) => {
15588
15867
  const bodyAns = await deps.interactive.askText("Idea description (paste or type)");
15589
15868
  if (!bodyAns.ok) return { ok: false, reason: bodyAns.error.message };
15590
15869
  const ideateRoot = AbsolutePath.parse(
15591
- join29(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "ideate")
15870
+ join31(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "ideate")
15592
15871
  );
15593
15872
  if (!ideateRoot.ok) return { ok: false, reason: ideateRoot.error.message };
15594
15873
  const element = createIdeateFlow(
@@ -16704,6 +16983,39 @@ var createTicketRemoveFlow = (deps) => leaf("ticket-remove", {
16704
16983
  output: (c, o) => ({ ...c, output: o })
16705
16984
  });
16706
16985
 
16986
+ // src/business/task/unblock-task.ts
16987
+ var unblockTaskUseCase = async (props) => {
16988
+ const log = props.logger.named("task.unblock");
16989
+ if (props.task.status === "todo") {
16990
+ log.debug("already todo, skipping", { taskId: props.task.id, sprintId: props.sprintId });
16991
+ return Result.ok(props.task);
16992
+ }
16993
+ log.debug("unblocking task", {
16994
+ taskId: props.task.id,
16995
+ sprintId: props.sprintId,
16996
+ from: props.task.status
16997
+ });
16998
+ const transitioned = unblockTask(props.task);
16999
+ if (!transitioned.ok) {
17000
+ log.warn("invalid state transition", {
17001
+ taskId: props.task.id,
17002
+ from: props.task.status,
17003
+ error: transitioned.error.message
17004
+ });
17005
+ return Result.error(transitioned.error);
17006
+ }
17007
+ const persisted = await props.taskRepo.update(props.sprintId, transitioned.value);
17008
+ if (!persisted.ok) {
17009
+ log.error("persist failed", { taskId: transitioned.value.id, error: persisted.error.message });
17010
+ return Result.error(persisted.error);
17011
+ }
17012
+ log.info(`unblocked task '${transitioned.value.name}'`, {
17013
+ taskId: transitioned.value.id,
17014
+ sprintId: props.sprintId
17015
+ });
17016
+ return Result.ok(transitioned.value);
17017
+ };
17018
+
16707
17019
  // src/application/ui/tui/views/sprint-detail-view.tsx
16708
17020
  import { jsx as jsx41, jsxs as jsxs30 } from "react/jsx-runtime";
16709
17021
  var buildFocusList = (sprint, tasks) => [
@@ -16772,12 +17084,15 @@ var SprintDetailView = () => {
16772
17084
  const [feedback, setFeedback] = useState25(void 0);
16773
17085
  const ticketsEditable = sprint?.status === "draft";
16774
17086
  const inDetail = openIdx !== void 0;
17087
+ const focusedNow = focusList[Math.min(cursorIdx, Math.max(0, focusList.length - 1))];
17088
+ const focusedBlockedTask = focusedNow?.kind === "task" && focusedNow.task.status === "blocked" ? focusedNow.task : void 0;
16775
17089
  useViewHints(
16776
17090
  inDetail ? [{ keys: "esc", label: "back to list" }] : [
16777
17091
  { keys: "n", label: "flows" },
16778
17092
  { keys: "\u21B5/o", label: "open" },
16779
17093
  { keys: "a", label: "add ticket" },
16780
- { keys: "d", label: "remove ticket" }
17094
+ { keys: "d", label: "remove ticket" },
17095
+ ...focusedBlockedTask !== void 0 ? [{ keys: "u", label: "unblock" }] : []
16781
17096
  ]
16782
17097
  );
16783
17098
  useInput14((input, key) => {
@@ -16805,6 +17120,10 @@ var SprintDetailView = () => {
16805
17120
  if (input === "d" && ticketsEditable) {
16806
17121
  const focused = focusList[Math.min(cursorIdx, focusList.length - 1)];
16807
17122
  if (focused?.kind === "ticket") setConfirmRemove(focused.ticket);
17123
+ return;
17124
+ }
17125
+ if (input === "u" && focusedBlockedTask !== void 0) {
17126
+ void handleUnblock(focusedBlockedTask);
16808
17127
  }
16809
17128
  });
16810
17129
  useEffect17(() => confirmRemove !== void 0 ? ui.claimPrompt() : void 0, [confirmRemove, ui.claimPrompt]);
@@ -16820,6 +17139,21 @@ var SprintDetailView = () => {
16820
17139
  setFeedback(`\u2713 removed "${target.title}"`);
16821
17140
  reload();
16822
17141
  };
17142
+ const handleUnblock = async (target) => {
17143
+ if (sprint === void 0) return;
17144
+ const r = await unblockTaskUseCase({
17145
+ task: target,
17146
+ sprintId: sprint.id,
17147
+ taskRepo: deps.taskRepo,
17148
+ logger: deps.logger
17149
+ });
17150
+ if (!r.ok) {
17151
+ setFeedback(`\u2717 ${r.error.message}`);
17152
+ return;
17153
+ }
17154
+ setFeedback(`\u2713 unblocked "${target.name}"`);
17155
+ reload();
17156
+ };
16823
17157
  return /* @__PURE__ */ jsx41(ViewShell, { title: "Sprint", subtitle: state.kind === "ok" ? state.value.sprint.name : "loading", children: ui.helpOpen ? /* @__PURE__ */ jsx41(HelpOverlay, {}) : state.kind === "loading" || state.kind === "idle" ? /* @__PURE__ */ jsx41(Box29, { paddingX: spacing.indent, children: /* @__PURE__ */ jsx41(Spinner, { label: "Loading\u2026" }) }) : state.kind === "error" ? /* @__PURE__ */ jsx41(Box29, { paddingX: spacing.indent, children: /* @__PURE__ */ jsx41(Text31, { children: "Failed to load sprint." }) }) : confirmRemove !== void 0 ? /* @__PURE__ */ jsxs30(Box29, { flexDirection: "column", paddingX: spacing.indent, children: [
16824
17158
  /* @__PURE__ */ jsxs30(Text31, { children: [
16825
17159
  "Remove ticket ",
@@ -17688,30 +18022,23 @@ var SignalLine = ({ signal }) => {
17688
18022
  /* @__PURE__ */ jsx43(Text33, { bold: row.bold ?? false, children: truncate2(row.text, 80) })
17689
18023
  ] });
17690
18024
  };
18025
+ var LEGEND_ENTRIES = [
18026
+ { label: "change", color: inkColors.info, description: "file/code edit" },
18027
+ { label: "learning", color: inkColors.highlight, description: "cross-task insight" },
18028
+ { label: "decision", color: inkColors.highlight, description: "design choice" },
18029
+ { label: "verified", color: inkColors.success, description: "task self-check passed" },
18030
+ { label: "blocked", color: inkColors.error, description: "task self-blocked" },
18031
+ { label: "commit", color: inkColors.info, description: "proposed commit message" }
18032
+ ];
17691
18033
  var SignalLegend = () => /* @__PURE__ */ jsxs32(Box31, { flexDirection: "column", paddingX: spacing.indent, marginBottom: spacing.section, children: [
17692
- /* @__PURE__ */ jsxs32(Text33, { dimColor: true, children: [
17693
- /* @__PURE__ */ jsx43(Text33, { bold: true, children: "legend" }),
17694
- " ",
17695
- /* @__PURE__ */ jsx43(Text33, { color: inkColors.info, children: "change" }),
17696
- " = file/code edit",
17697
- " ",
17698
- /* @__PURE__ */ jsx43(Text33, { color: inkColors.highlight, children: "learning" }),
17699
- " = cross-task insight",
17700
- " ",
17701
- /* @__PURE__ */ jsx43(Text33, { color: inkColors.highlight, children: "decision" }),
17702
- " = design choice"
17703
- ] }),
17704
- /* @__PURE__ */ jsxs32(Text33, { dimColor: true, children: [
17705
- " ",
17706
- /* @__PURE__ */ jsx43(Text33, { color: inkColors.success, children: "verified" }),
17707
- " = task self-check passed",
17708
- " ",
17709
- /* @__PURE__ */ jsx43(Text33, { color: inkColors.error, children: "blocked" }),
17710
- " = task self-blocked",
17711
- " ",
17712
- /* @__PURE__ */ jsx43(Text33, { color: inkColors.info, children: "commit" }),
17713
- " = proposed commit message"
17714
- ] })
18034
+ /* @__PURE__ */ jsx43(Text33, { dimColor: true, bold: true, children: "legend" }),
18035
+ /* @__PURE__ */ jsx43(Box31, { flexDirection: "column", paddingLeft: 2, children: LEGEND_ENTRIES.map((entry) => /* @__PURE__ */ jsxs32(Box31, { children: [
18036
+ /* @__PURE__ */ jsx43(Text33, { color: entry.color, bold: true, children: padLabel2(entry.label) }),
18037
+ /* @__PURE__ */ jsxs32(Text33, { dimColor: true, children: [
18038
+ "= ",
18039
+ entry.description
18040
+ ] })
18041
+ ] }, entry.label)) })
17715
18042
  ] });
17716
18043
  var EvaluationLine = ({ evaluation }) => {
17717
18044
  const color = evaluation.status === "passed" ? inkColors.success : evaluation.status === "failed" ? inkColors.error : inkColors.warning;
@@ -18288,7 +18615,7 @@ var ExecuteView = () => {
18288
18615
  ] })
18289
18616
  ] })
18290
18617
  ] }) });
18291
- const outerFlowFilter = (name) => !isPerTaskLeaf(name);
18618
+ const outerFlowFilter = (name) => !isPerTaskLeaf(name) && !name.startsWith("with-repo-lock(");
18292
18619
  const flowStepsPanel = /* @__PURE__ */ jsx46(
18293
18620
  StepTrace,
18294
18621
  {
@@ -19020,7 +19347,7 @@ var ProbeRow = ({ probe }) => /* @__PURE__ */ jsxs39(Box38, { flexDirection: "co
19020
19347
  // src/application/ui/tui/views/export-context-view.tsx
19021
19348
  import { useCallback as useCallback7, useEffect as useEffect25, useState as useState31 } from "react";
19022
19349
  import { Box as Box39, Text as Text41, useInput as useInput20 } from "ink";
19023
- import { join as join30 } from "path";
19350
+ import { join as join32 } from "path";
19024
19351
 
19025
19352
  // src/business/sprint/views/context-md.ts
19026
19353
  var renderSprintContextMarkdown = (input) => {
@@ -19152,7 +19479,7 @@ var ExportContextView = () => {
19152
19479
  return;
19153
19480
  }
19154
19481
  const outputPath = AbsolutePath.parse(
19155
- join30(String(storage2.dataRoot), "sprints", String(selection.sprintId), "context.md")
19482
+ join32(String(storage2.dataRoot), "sprints", String(selection.sprintId), "context.md")
19156
19483
  );
19157
19484
  if (!outputPath.ok) {
19158
19485
  setRun({ kind: "error", message: outputPath.error.message });
@@ -19210,7 +19537,7 @@ var ExportContextView = () => {
19210
19537
  // src/application/ui/tui/views/export-requirements-view.tsx
19211
19538
  import { useCallback as useCallback8, useEffect as useEffect26, useState as useState32 } from "react";
19212
19539
  import { Box as Box40, Text as Text42, useInput as useInput21 } from "ink";
19213
- import { join as join31 } from "path";
19540
+ import { join as join33 } from "path";
19214
19541
 
19215
19542
  // src/business/sprint/views/requirements-md.ts
19216
19543
  var renderSprintRequirementsMarkdown = (sprint) => {
@@ -19275,7 +19602,7 @@ var ExportRequirementsView = () => {
19275
19602
  return;
19276
19603
  }
19277
19604
  const outputPath = AbsolutePath.parse(
19278
- join31(String(storage2.dataRoot), "sprints", String(selection.sprintId), "requirements.md")
19605
+ join33(String(storage2.dataRoot), "sprints", String(selection.sprintId), "requirements.md")
19279
19606
  );
19280
19607
  if (!outputPath.ok) {
19281
19608
  setRun({ kind: "error", message: outputPath.error.message });
@@ -19610,12 +19937,12 @@ var WelcomeView = () => {
19610
19937
  import { useEffect as useEffect29, useState as useState36 } from "react";
19611
19938
  import { Box as Box44, Text as Text46 } from "ink";
19612
19939
  import { homedir as osHomedir2 } from "os";
19613
- import { basename as basename3, join as join33 } from "path";
19940
+ import { basename as basename3, join as join35 } from "path";
19614
19941
 
19615
19942
  // src/application/ui/tui/prompts/path-picker-prompt.tsx
19616
19943
  import { useEffect as useEffect28, useState as useState35 } from "react";
19617
19944
  import { promises as fs24 } from "fs";
19618
- import { dirname as dirname9, join as join32 } from "path";
19945
+ import { dirname as dirname10, join as join34 } from "path";
19619
19946
  import { homedir } from "os";
19620
19947
  import { Box as Box43, Text as Text45, useInput as useInput23 } from "ink";
19621
19948
  import { jsx as jsx55, jsxs as jsxs44 } from "react/jsx-runtime";
@@ -19623,7 +19950,7 @@ var VISIBLE_ROWS2 = 12;
19623
19950
  var clamp5 = (n, min, max) => Math.max(min, Math.min(max, n));
19624
19951
  var expandHome = (input) => {
19625
19952
  if (input === "~") return homedir();
19626
- if (input.startsWith("~/")) return join32(homedir(), input.slice(2));
19953
+ if (input.startsWith("~/")) return join34(homedir(), input.slice(2));
19627
19954
  return input;
19628
19955
  };
19629
19956
  var PathPickerPrompt = ({
@@ -19675,7 +20002,7 @@ var PathPickerPrompt = ({
19675
20002
  return;
19676
20003
  }
19677
20004
  if (key.backspace || key.delete) {
19678
- const parent = dirname9(cwd);
20005
+ const parent = dirname10(cwd);
19679
20006
  if (parent !== cwd) {
19680
20007
  setCwd(parent);
19681
20008
  setCursor(1);
@@ -19700,7 +20027,7 @@ var PathPickerPrompt = ({
19700
20027
  const row = rows[cursor];
19701
20028
  if (row === void 0) return;
19702
20029
  if (row.kind === "parent") {
19703
- const parent = dirname9(cwd);
20030
+ const parent = dirname10(cwd);
19704
20031
  if (parent !== cwd) {
19705
20032
  setCwd(parent);
19706
20033
  setCursor(1);
@@ -19711,7 +20038,7 @@ var PathPickerPrompt = ({
19711
20038
  onSubmit(cwd);
19712
20039
  return;
19713
20040
  }
19714
- setCwd(join32(cwd, row.entry.name));
20041
+ setCwd(join34(cwd, row.entry.name));
19715
20042
  setCursor(1);
19716
20043
  }
19717
20044
  },
@@ -19793,7 +20120,7 @@ var labelFor = (row) => {
19793
20120
  import { jsx as jsx56, jsxs as jsxs45 } from "react/jsx-runtime";
19794
20121
  var expandHome2 = (input) => {
19795
20122
  if (input === "~") return osHomedir2();
19796
- if (input.startsWith("~/")) return join33(osHomedir2(), input.slice(2));
20123
+ if (input.startsWith("~/")) return join35(osHomedir2(), input.slice(2));
19797
20124
  return input;
19798
20125
  };
19799
20126
  var backStep = (step) => {
@@ -20009,11 +20336,11 @@ var StepView = ({ step, onChange, onCancel, onSubmit }) => {
20009
20336
  import { useEffect as useEffect30, useState as useState37 } from "react";
20010
20337
  import { Box as Box45, Text as Text47 } from "ink";
20011
20338
  import { homedir as osHomedir3 } from "os";
20012
- import { basename as basename4, join as join34 } from "path";
20339
+ import { basename as basename4, join as join36 } from "path";
20013
20340
  import { jsx as jsx57, jsxs as jsxs46 } from "react/jsx-runtime";
20014
20341
  var expandHome3 = (input) => {
20015
20342
  if (input === "~") return osHomedir3();
20016
- if (input.startsWith("~/")) return join34(osHomedir3(), input.slice(2));
20343
+ if (input.startsWith("~/")) return join36(osHomedir3(), input.slice(2));
20017
20344
  return input;
20018
20345
  };
20019
20346
  var backStep2 = (step) => {
@@ -20844,10 +21171,10 @@ var resolveInitialState = ({
20844
21171
 
20845
21172
  // src/integration/persistence/selection/last-selection-store.ts
20846
21173
  import { promises as fs25 } from "fs";
20847
- import { join as join35 } from "path";
21174
+ import { join as join37 } from "path";
20848
21175
  var FILE_NAME = "last-selection.json";
20849
21176
  var createLastSelectionStore = (stateRoot) => {
20850
- const path = join35(String(stateRoot), FILE_NAME);
21177
+ const path = join37(String(stateRoot), FILE_NAME);
20851
21178
  return {
20852
21179
  async read() {
20853
21180
  try {
@@ -21734,6 +22061,44 @@ var registerTaskCommand = (program) => {
21734
22061
  return;
21735
22062
  }
21736
22063
  process.stdout.write(`${JSON.stringify(result.value, null, 2)}
22064
+ `);
22065
+ });
22066
+ task.command("unblock <taskId>").description("flip a blocked task back to todo so the implement loop picks it up again").requiredOption("-s, --sprint <id>", "sprint id").action(async (rawTaskId, opts) => {
22067
+ const sprintId = SprintId.parse(opts.sprint);
22068
+ if (!sprintId.ok) {
22069
+ process.stderr.write(`error: invalid sprint id: ${sprintId.error.message}
22070
+ `);
22071
+ process.exit(1);
22072
+ return;
22073
+ }
22074
+ const taskId = TaskId.parse(rawTaskId);
22075
+ if (!taskId.ok) {
22076
+ process.stderr.write(`error: invalid task id: ${taskId.error.message}
22077
+ `);
22078
+ process.exit(1);
22079
+ return;
22080
+ }
22081
+ const { deps } = await bootstrapCli();
22082
+ const loaded = await deps.taskRepo.findById(sprintId.value, taskId.value);
22083
+ if (!loaded.ok) {
22084
+ process.stderr.write(`error: ${loaded.error.message}
22085
+ `);
22086
+ process.exit(1);
22087
+ return;
22088
+ }
22089
+ const result = await unblockTaskUseCase({
22090
+ task: loaded.value,
22091
+ sprintId: sprintId.value,
22092
+ taskRepo: deps.taskRepo,
22093
+ logger: deps.logger
22094
+ });
22095
+ if (!result.ok) {
22096
+ process.stderr.write(`error: ${result.error.message}
22097
+ `);
22098
+ process.exit(1);
22099
+ return;
22100
+ }
22101
+ process.stdout.write(`unblocked task '${result.value.name}' (${String(result.value.id)})
21737
22102
  `);
21738
22103
  });
21739
22104
  };