ralphctl 0.7.0 → 0.7.1
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 +404 -72
- package/dist/manifest.json +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
...
|
|
2620
|
+
...envelope.sessionId !== void 0 ? { sessionId: envelope.sessionId } : {}
|
|
2588
2621
|
})
|
|
2589
2622
|
};
|
|
2590
2623
|
}
|
|
@@ -3211,6 +3244,7 @@ var stringifyError4 = (cause) => cause instanceof Error ? cause.message : String
|
|
|
3211
3244
|
|
|
3212
3245
|
// src/integration/ai/providers/codex/interactive.ts
|
|
3213
3246
|
import { spawn as nodeSpawn7 } from "child_process";
|
|
3247
|
+
import { dirname as dirname4 } from "path";
|
|
3214
3248
|
var defaultSpawn7 = (command, args, options) => nodeSpawn7(command, [...args], { stdio: options.stdio, cwd: options.cwd });
|
|
3215
3249
|
var createInteractiveCodexProvider = (deps) => {
|
|
3216
3250
|
const spawnFn = deps.spawn ?? defaultSpawn7;
|
|
@@ -3227,10 +3261,23 @@ var createInteractiveCodexProvider = (deps) => {
|
|
|
3227
3261
|
})
|
|
3228
3262
|
);
|
|
3229
3263
|
}
|
|
3264
|
+
const allRoots = [
|
|
3265
|
+
String(input.cwd),
|
|
3266
|
+
...input.additionalRoots?.map((r) => String(r)) ?? [],
|
|
3267
|
+
dirname4(String(input.outputFile)),
|
|
3268
|
+
dirname4(String(input.promptFile))
|
|
3269
|
+
];
|
|
3270
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3271
|
+
const dirFlags = allRoots.filter((p) => {
|
|
3272
|
+
if (seen.has(p)) return false;
|
|
3273
|
+
seen.add(p);
|
|
3274
|
+
return true;
|
|
3275
|
+
}).flatMap((p) => ["--add-dir", shellQuote2(p)]);
|
|
3230
3276
|
const inner = [
|
|
3231
3277
|
"codex",
|
|
3232
3278
|
"--cd",
|
|
3233
3279
|
shellQuote2(String(input.cwd)),
|
|
3280
|
+
...dirFlags,
|
|
3234
3281
|
"--model",
|
|
3235
3282
|
shellQuote2(input.model),
|
|
3236
3283
|
"-s",
|
|
@@ -3286,7 +3333,7 @@ var stringifyError5 = (cause) => cause instanceof Error ? cause.message : String
|
|
|
3286
3333
|
// src/integration/ai/providers/copilot/interactive.ts
|
|
3287
3334
|
import { promises as fs6 } from "fs";
|
|
3288
3335
|
import { spawn as nodeSpawn8 } from "child_process";
|
|
3289
|
-
import { dirname as
|
|
3336
|
+
import { dirname as dirname5 } from "path";
|
|
3290
3337
|
var defaultSpawn8 = (command, args, options) => nodeSpawn8(command, [...args], { stdio: options.stdio, cwd: options.cwd });
|
|
3291
3338
|
var defaultReadFile = (path) => fs6.readFile(path, "utf8");
|
|
3292
3339
|
var createInteractiveCopilotProvider = (deps) => {
|
|
@@ -3320,8 +3367,8 @@ var createInteractiveCopilotProvider = (deps) => {
|
|
|
3320
3367
|
const allRoots = [
|
|
3321
3368
|
String(input.cwd),
|
|
3322
3369
|
...input.additionalRoots?.map((r) => String(r)) ?? [],
|
|
3323
|
-
|
|
3324
|
-
|
|
3370
|
+
dirname5(String(input.outputFile)),
|
|
3371
|
+
dirname5(String(input.promptFile))
|
|
3325
3372
|
];
|
|
3326
3373
|
const seen = /* @__PURE__ */ new Set();
|
|
3327
3374
|
const dirFlags = allRoots.filter((p) => {
|
|
@@ -3864,7 +3911,7 @@ var createPullRequestCreator = (deps) => async (input) => {
|
|
|
3864
3911
|
|
|
3865
3912
|
// src/integration/ai/prompts/_engine/fs-template-loader.ts
|
|
3866
3913
|
import { promises as fs7 } from "fs";
|
|
3867
|
-
import { dirname as
|
|
3914
|
+
import { dirname as dirname6, join as join6 } from "path";
|
|
3868
3915
|
import { fileURLToPath } from "url";
|
|
3869
3916
|
var createFsTemplateLoader = (templatesDir) => ({
|
|
3870
3917
|
async load(name) {
|
|
@@ -3898,7 +3945,7 @@ var tryRead = async (path) => {
|
|
|
3898
3945
|
}
|
|
3899
3946
|
};
|
|
3900
3947
|
var TEMPLATES_DIR = (() => {
|
|
3901
|
-
const here =
|
|
3948
|
+
const here = dirname6(fileURLToPath(import.meta.url));
|
|
3902
3949
|
const isBundled = import.meta.url.endsWith("/cli.mjs") || import.meta.url.endsWith("\\cli.mjs");
|
|
3903
3950
|
const path = isBundled ? join6(here, "prompts") : join6(here, "..");
|
|
3904
3951
|
const parsed = AbsolutePath.parse(path);
|
|
@@ -4296,7 +4343,7 @@ var createNpmVersionChecker = (deps) => {
|
|
|
4296
4343
|
// package.json
|
|
4297
4344
|
var package_default = {
|
|
4298
4345
|
name: "ralphctl",
|
|
4299
|
-
version: "0.7.
|
|
4346
|
+
version: "0.7.1",
|
|
4300
4347
|
description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code & GitHub Copilot across repositories",
|
|
4301
4348
|
homepage: "https://github.com/lukas-grigis/ralphctl",
|
|
4302
4349
|
type: "module",
|
|
@@ -4605,7 +4652,7 @@ var createSkillsAdapter = (deps) => {
|
|
|
4605
4652
|
};
|
|
4606
4653
|
|
|
4607
4654
|
// src/integration/ai/skills/bundled/source.ts
|
|
4608
|
-
import { dirname as
|
|
4655
|
+
import { dirname as dirname7, join as join12 } from "path";
|
|
4609
4656
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4610
4657
|
import { readFile } from "fs/promises";
|
|
4611
4658
|
|
|
@@ -4632,7 +4679,7 @@ var skillsForFlow = (flowId) => FLOW_SKILLS[flowId];
|
|
|
4632
4679
|
|
|
4633
4680
|
// src/integration/ai/skills/bundled/source.ts
|
|
4634
4681
|
var defaultBundledRoot = (() => {
|
|
4635
|
-
const here =
|
|
4682
|
+
const here = dirname7(fileURLToPath2(import.meta.url));
|
|
4636
4683
|
const isBundled = import.meta.url.endsWith("/cli.mjs") || import.meta.url.endsWith("\\cli.mjs");
|
|
4637
4684
|
return isBundled ? join12(here, "skills") : here;
|
|
4638
4685
|
})();
|
|
@@ -9350,7 +9397,7 @@ var refineTicketInteractiveLeaf = (deps, ticket) => leaf(`refine-ticket-${String
|
|
|
9350
9397
|
execute: async (input) => {
|
|
9351
9398
|
const session = await deps.runInTerminal(
|
|
9352
9399
|
async () => deps.interactiveAi.run({
|
|
9353
|
-
cwd:
|
|
9400
|
+
cwd: input.cwd,
|
|
9354
9401
|
promptFile: input.promptFile,
|
|
9355
9402
|
outputFile: input.outputFile,
|
|
9356
9403
|
model: deps.model
|
|
@@ -9407,9 +9454,18 @@ var refineTicketInteractiveLeaf = (deps, ticket) => leaf(`refine-ticket-${String
|
|
|
9407
9454
|
message: `refine-ticket-${String(ticket.id)}: prompt/output paths missing \u2014 render-prompt-to-file must run first`
|
|
9408
9455
|
});
|
|
9409
9456
|
}
|
|
9457
|
+
if (ctx.currentUnitRoot === void 0) {
|
|
9458
|
+
throw new InvalidStateError({
|
|
9459
|
+
entity: "chain",
|
|
9460
|
+
currentState: "pre-refine",
|
|
9461
|
+
attemptedAction: `refine-ticket-${String(ticket.id)}`,
|
|
9462
|
+
message: `refine-ticket-${String(ticket.id)}: unit root missing \u2014 build-refine-unit must run first`
|
|
9463
|
+
});
|
|
9464
|
+
}
|
|
9410
9465
|
return {
|
|
9411
9466
|
sprint: ctx.sprint,
|
|
9412
9467
|
ticket,
|
|
9468
|
+
cwd: ctx.currentUnitRoot,
|
|
9413
9469
|
promptFile: ctx.currentPromptFile,
|
|
9414
9470
|
outputFile: ctx.currentOutputFile
|
|
9415
9471
|
};
|
|
@@ -9648,9 +9704,17 @@ var createRefineFlow = (deps, opts) => {
|
|
|
9648
9704
|
{
|
|
9649
9705
|
name: `install-skills-${String(ticket.id)}`,
|
|
9650
9706
|
flowId: "refine",
|
|
9651
|
-
// Skills land in the AI session's cwd
|
|
9652
|
-
//
|
|
9653
|
-
|
|
9707
|
+
// Skills land in the AI session's cwd — for refine that's the per-ticket unit root
|
|
9708
|
+
// (`<sprintDir>/refinement/<ticket-slug>/`), not the user's repo. Refinement is
|
|
9709
|
+
// implementation-agnostic, so we keep the AI out of the repo's auto-discovered context.
|
|
9710
|
+
cwdPicker: (ctx) => {
|
|
9711
|
+
if (ctx.currentUnitRoot === void 0) {
|
|
9712
|
+
throw new Error(
|
|
9713
|
+
`install-skills-${String(ticket.id)}: currentUnitRoot missing \u2014 build-refine-unit must run first`
|
|
9714
|
+
);
|
|
9715
|
+
}
|
|
9716
|
+
return ctx.currentUnitRoot;
|
|
9717
|
+
}
|
|
9654
9718
|
}
|
|
9655
9719
|
),
|
|
9656
9720
|
refineTicketInteractiveLeaf(
|
|
@@ -9658,7 +9722,6 @@ var createRefineFlow = (deps, opts) => {
|
|
|
9658
9722
|
interactiveAi: deps.interactiveAi,
|
|
9659
9723
|
runInTerminal: deps.runInTerminal,
|
|
9660
9724
|
logger: deps.logger,
|
|
9661
|
-
cwd: opts.cwd,
|
|
9662
9725
|
model: opts.model,
|
|
9663
9726
|
...deps.reviewBeforeApprove !== void 0 ? { reviewBeforeApprove: deps.reviewBeforeApprove } : {},
|
|
9664
9727
|
...deps.issuePusher !== void 0 ? { issuePusher: deps.issuePusher } : {},
|
|
@@ -9668,7 +9731,17 @@ var createRefineFlow = (deps, opts) => {
|
|
|
9668
9731
|
),
|
|
9669
9732
|
uninstallSkillsLeaf(
|
|
9670
9733
|
{ skillsAdapter: deps.skillsAdapter },
|
|
9671
|
-
{
|
|
9734
|
+
{
|
|
9735
|
+
name: `uninstall-skills-${String(ticket.id)}`,
|
|
9736
|
+
cwdPicker: (ctx) => {
|
|
9737
|
+
if (ctx.currentUnitRoot === void 0) {
|
|
9738
|
+
throw new Error(
|
|
9739
|
+
`uninstall-skills-${String(ticket.id)}: currentUnitRoot missing \u2014 build-refine-unit must run first`
|
|
9740
|
+
);
|
|
9741
|
+
}
|
|
9742
|
+
return ctx.currentUnitRoot;
|
|
9743
|
+
}
|
|
9744
|
+
}
|
|
9672
9745
|
),
|
|
9673
9746
|
saveSprintLeaf({ sprintRepo: deps.sprintRepo }, `save-after-${String(ticket.id)}`)
|
|
9674
9747
|
])
|
|
@@ -9691,9 +9764,8 @@ var deriveOriginFromGit = async (cwd, gitRunner) => {
|
|
|
9691
9764
|
return parsed === null ? void 0 : parsed;
|
|
9692
9765
|
};
|
|
9693
9766
|
var launchRefine = async (ctx) => {
|
|
9694
|
-
const { deps, snapshot, extras, settings, interactiveAi, skillsAdapter, skillSource,
|
|
9767
|
+
const { deps, snapshot, extras, settings, interactiveAi, skillsAdapter, skillSource, bridge, sessionId: sessionId2 } = ctx;
|
|
9695
9768
|
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
9769
|
const pending = snapshot.sprint.tickets.filter((t) => t.status === "pending");
|
|
9698
9770
|
const refinementRoot = AbsolutePath.parse(
|
|
9699
9771
|
join15(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "refinement")
|
|
@@ -9756,7 +9828,6 @@ ${body.trim()}`;
|
|
|
9756
9828
|
{
|
|
9757
9829
|
sprintId: snapshot.sprint.id,
|
|
9758
9830
|
pendingTickets: pending,
|
|
9759
|
-
cwd,
|
|
9760
9831
|
model: extras.modelOverride ?? settings.ai.models.refine,
|
|
9761
9832
|
refinementRoot: refinementRoot.value
|
|
9762
9833
|
}
|
|
@@ -10214,6 +10285,13 @@ var markTaskBlocked = (task, reason) => {
|
|
|
10214
10285
|
if (!guard2.ok) return Result.error(guard2.error);
|
|
10215
10286
|
return Result.ok({ ...guard2.value, status: "blocked", blockedReason: reason });
|
|
10216
10287
|
};
|
|
10288
|
+
var unblockTask = (task) => {
|
|
10289
|
+
const guard2 = requireStatus("task", task, ["blocked"], "unblock");
|
|
10290
|
+
if (!guard2.ok) return Result.error(guard2.error);
|
|
10291
|
+
const { blockedReason: _ignored, ...rest } = guard2.value;
|
|
10292
|
+
void _ignored;
|
|
10293
|
+
return Result.ok({ ...rest, status: "todo" });
|
|
10294
|
+
};
|
|
10217
10295
|
var latestCritique = (task) => {
|
|
10218
10296
|
for (let i = task.attempts.length - 1; i >= 0; i--) {
|
|
10219
10297
|
const att = task.attempts[i];
|
|
@@ -10394,7 +10472,7 @@ var callPlannerInteractiveLeaf = (deps) => leaf("call-planner-interactive", {
|
|
|
10394
10472
|
const additionalRoots = deps.additionalRoots ?? [];
|
|
10395
10473
|
const session = await deps.runInTerminal(
|
|
10396
10474
|
async () => deps.interactiveAi.run({
|
|
10397
|
-
cwd:
|
|
10475
|
+
cwd: input.cwd,
|
|
10398
10476
|
promptFile: input.promptFile,
|
|
10399
10477
|
outputFile: input.outputFile,
|
|
10400
10478
|
model: deps.model,
|
|
@@ -10471,10 +10549,19 @@ var callPlannerInteractiveLeaf = (deps) => leaf("call-planner-interactive", {
|
|
|
10471
10549
|
message: "call-planner-interactive: prompt/output paths missing \u2014 render-prompt-to-file must run first"
|
|
10472
10550
|
});
|
|
10473
10551
|
}
|
|
10552
|
+
if (ctx.currentUnitRoot === void 0) {
|
|
10553
|
+
throw new InvalidStateError({
|
|
10554
|
+
entity: "chain",
|
|
10555
|
+
currentState: "pre-plan",
|
|
10556
|
+
attemptedAction: "call-planner-interactive",
|
|
10557
|
+
message: "call-planner-interactive: unit root missing \u2014 build-plan-unit must run first"
|
|
10558
|
+
});
|
|
10559
|
+
}
|
|
10474
10560
|
return {
|
|
10475
10561
|
sprint: ctx.sprint,
|
|
10476
10562
|
project: ctx.project,
|
|
10477
10563
|
existingTasks: ctx.tasks ?? [],
|
|
10564
|
+
cwd: ctx.currentUnitRoot,
|
|
10478
10565
|
promptFile: ctx.currentPromptFile,
|
|
10479
10566
|
outputFile: ctx.currentOutputFile
|
|
10480
10567
|
};
|
|
@@ -10536,9 +10623,21 @@ var createPlanFlow = (deps, opts) => {
|
|
|
10536
10623
|
{ skillsAdapter: deps.skillsAdapter, skillSource: deps.skillSource },
|
|
10537
10624
|
{
|
|
10538
10625
|
flowId: "plan",
|
|
10539
|
-
// Skills land in the AI session's cwd
|
|
10540
|
-
//
|
|
10541
|
-
|
|
10626
|
+
// Skills land in the AI session's cwd — for plan that's the per-sprint plan unit root
|
|
10627
|
+
// (`<sprintDir>/plan/<run-slug>/`), not any project repo. Plan mounts every repo as an
|
|
10628
|
+
// equal `--add-dir` source; rooting the session in any one repo would auto-load that
|
|
10629
|
+
// repo's `CLAUDE.md` / agents / `.mcp.json` and bias the planner toward it.
|
|
10630
|
+
cwdPicker: (ctx) => {
|
|
10631
|
+
if (ctx.currentUnitRoot === void 0) {
|
|
10632
|
+
throw new InvalidStateError({
|
|
10633
|
+
entity: "chain",
|
|
10634
|
+
currentState: "pre-plan",
|
|
10635
|
+
attemptedAction: "install-skills",
|
|
10636
|
+
message: "install-skills: currentUnitRoot missing \u2014 build-plan-unit must run first"
|
|
10637
|
+
});
|
|
10638
|
+
}
|
|
10639
|
+
return ctx.currentUnitRoot;
|
|
10640
|
+
}
|
|
10542
10641
|
}
|
|
10543
10642
|
),
|
|
10544
10643
|
callPlannerInteractiveLeaf({
|
|
@@ -10546,12 +10645,26 @@ var createPlanFlow = (deps, opts) => {
|
|
|
10546
10645
|
runInTerminal: deps.runInTerminal,
|
|
10547
10646
|
logger: deps.logger,
|
|
10548
10647
|
clock: deps.clock,
|
|
10549
|
-
cwd: opts.cwd,
|
|
10550
10648
|
model: opts.model,
|
|
10551
10649
|
...opts.additionalRoots !== void 0 && opts.additionalRoots.length > 0 ? { additionalRoots: opts.additionalRoots } : {},
|
|
10552
10650
|
...deps.reviewBeforeApprove !== void 0 ? { reviewBeforeApprove: deps.reviewBeforeApprove } : {}
|
|
10553
10651
|
}),
|
|
10554
|
-
uninstallSkillsLeaf(
|
|
10652
|
+
uninstallSkillsLeaf(
|
|
10653
|
+
{ skillsAdapter: deps.skillsAdapter },
|
|
10654
|
+
{
|
|
10655
|
+
cwdPicker: (ctx) => {
|
|
10656
|
+
if (ctx.currentUnitRoot === void 0) {
|
|
10657
|
+
throw new InvalidStateError({
|
|
10658
|
+
entity: "chain",
|
|
10659
|
+
currentState: "pre-plan",
|
|
10660
|
+
attemptedAction: "uninstall-skills",
|
|
10661
|
+
message: "uninstall-skills: currentUnitRoot missing \u2014 build-plan-unit must run first"
|
|
10662
|
+
});
|
|
10663
|
+
}
|
|
10664
|
+
return ctx.currentUnitRoot;
|
|
10665
|
+
}
|
|
10666
|
+
}
|
|
10667
|
+
),
|
|
10555
10668
|
saveTasksLeaf({ taskRepo: deps.taskRepo }),
|
|
10556
10669
|
saveSprintLeaf({ sprintRepo: deps.sprintRepo })
|
|
10557
10670
|
]);
|
|
@@ -10559,10 +10672,9 @@ var createPlanFlow = (deps, opts) => {
|
|
|
10559
10672
|
|
|
10560
10673
|
// src/application/ui/shared/launch/plan.ts
|
|
10561
10674
|
var launchPlan = (ctx) => {
|
|
10562
|
-
const { deps, snapshot, extras, settings, interactiveAi, skillsAdapter, skillSource,
|
|
10675
|
+
const { deps, snapshot, extras, settings, interactiveAi, skillsAdapter, skillSource, bridge, sessionId: sessionId2 } = ctx;
|
|
10563
10676
|
if (!snapshot.project) return { ok: false, reason: "No project loaded." };
|
|
10564
10677
|
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
10678
|
const planRoot = AbsolutePath.parse(
|
|
10567
10679
|
join17(String(deps.storage.dataRoot), "sprints", String(snapshot.sprint.id), "plan")
|
|
10568
10680
|
);
|
|
@@ -10601,9 +10713,9 @@ ${summary}`;
|
|
|
10601
10713
|
{
|
|
10602
10714
|
sprintId: snapshot.sprint.id,
|
|
10603
10715
|
projectId: snapshot.project.id,
|
|
10604
|
-
|
|
10605
|
-
//
|
|
10606
|
-
//
|
|
10716
|
+
// Mount every repo on the project as an equal `--add-dir` source so the planner can
|
|
10717
|
+
// navigate across them without per-file approval prompts. No repo enjoys cwd privilege —
|
|
10718
|
+
// the session's cwd is the per-sprint plan unit root.
|
|
10607
10719
|
additionalRoots: snapshot.project.repositories.map((r) => r.path),
|
|
10608
10720
|
model: extras.modelOverride ?? settings.ai.models.plan,
|
|
10609
10721
|
planRoot: planRoot.value
|
|
@@ -10675,7 +10787,7 @@ var loop = (name, body, opts = {}) => ({
|
|
|
10675
10787
|
|
|
10676
10788
|
// src/integration/observability/sinks/progress-file-sink.ts
|
|
10677
10789
|
import { promises as fs14 } from "fs";
|
|
10678
|
-
import { dirname as
|
|
10790
|
+
import { dirname as dirname8 } from "path";
|
|
10679
10791
|
var MAX_QUEUE_DEPTH = 1e4;
|
|
10680
10792
|
var createProgressFileSink = (deps) => {
|
|
10681
10793
|
const queue = [];
|
|
@@ -10795,7 +10907,7 @@ var TEMPLATE = `${TITLE}
|
|
|
10795
10907
|
${HEADINGS.map((h) => `## ${h}
|
|
10796
10908
|
`).join("\n")}`;
|
|
10797
10909
|
var mergeSection = async (path, rendered) => {
|
|
10798
|
-
await fs14.mkdir(
|
|
10910
|
+
await fs14.mkdir(dirname8(path), { recursive: true });
|
|
10799
10911
|
let current;
|
|
10800
10912
|
try {
|
|
10801
10913
|
current = await fs14.readFile(path, "utf8");
|
|
@@ -11017,6 +11129,45 @@ var gitCommitWithMessage = async (runner, cwd, message) => {
|
|
|
11017
11129
|
if (!head.ok) return Result.error(head.error);
|
|
11018
11130
|
return Result.ok({ committed: true, headSha: head.value });
|
|
11019
11131
|
};
|
|
11132
|
+
var gitStashPush = async (runner, cwd, message) => {
|
|
11133
|
+
const dirty = await gitHasUncommittedChanges(runner, cwd);
|
|
11134
|
+
if (!dirty.ok) return Result.error(dirty.error);
|
|
11135
|
+
if (!dirty.value) return Result.ok({ stashed: false });
|
|
11136
|
+
const stash = await runner.run(cwd, ["stash", "push", "-u", "-m", message]);
|
|
11137
|
+
if (!stash.ok) return Result.error(stash.error);
|
|
11138
|
+
if (stash.value.exitCode !== 0) {
|
|
11139
|
+
return Result.error(
|
|
11140
|
+
new StorageError({
|
|
11141
|
+
subCode: "io",
|
|
11142
|
+
message: `git stash push failed: ${(stash.value.stderr || stash.value.stdout).trim()}`
|
|
11143
|
+
})
|
|
11144
|
+
);
|
|
11145
|
+
}
|
|
11146
|
+
return Result.ok({ stashed: true });
|
|
11147
|
+
};
|
|
11148
|
+
var gitResetHard = async (runner, cwd) => {
|
|
11149
|
+
const reset = await runner.run(cwd, ["reset", "--hard", "HEAD"]);
|
|
11150
|
+
if (!reset.ok) return Result.error(reset.error);
|
|
11151
|
+
if (reset.value.exitCode !== 0) {
|
|
11152
|
+
return Result.error(
|
|
11153
|
+
new StorageError({
|
|
11154
|
+
subCode: "io",
|
|
11155
|
+
message: `git reset --hard failed: ${(reset.value.stderr || reset.value.stdout).trim()}`
|
|
11156
|
+
})
|
|
11157
|
+
);
|
|
11158
|
+
}
|
|
11159
|
+
const clean = await runner.run(cwd, ["clean", "-fd"]);
|
|
11160
|
+
if (!clean.ok) return Result.error(clean.error);
|
|
11161
|
+
if (clean.value.exitCode !== 0) {
|
|
11162
|
+
return Result.error(
|
|
11163
|
+
new StorageError({
|
|
11164
|
+
subCode: "io",
|
|
11165
|
+
message: `git clean -fd failed: ${(clean.value.stderr || clean.value.stdout).trim()}`
|
|
11166
|
+
})
|
|
11167
|
+
);
|
|
11168
|
+
}
|
|
11169
|
+
return Result.ok(void 0);
|
|
11170
|
+
};
|
|
11020
11171
|
var gitGetCurrentBranch = async (runner, cwd) => {
|
|
11021
11172
|
const result = await runner.run(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
11022
11173
|
if (!result.ok) return Result.error(result.error);
|
|
@@ -12015,6 +12166,7 @@ var postTaskCheckLeaf = (deps, opts, taskId) => {
|
|
|
12015
12166
|
};
|
|
12016
12167
|
|
|
12017
12168
|
// src/business/task/preflight-task.ts
|
|
12169
|
+
var ELEMENT_NAME = "preflight-task";
|
|
12018
12170
|
var preflightTaskUseCase = async (props) => {
|
|
12019
12171
|
const log = props.logger.named("task.preflight");
|
|
12020
12172
|
log.debug("checking working tree", { cwd: props.cwd });
|
|
@@ -12035,6 +12187,9 @@ var preflightTaskUseCase = async (props) => {
|
|
|
12035
12187
|
});
|
|
12036
12188
|
return Result.ok(void 0);
|
|
12037
12189
|
}
|
|
12190
|
+
if (policy === "prompt") {
|
|
12191
|
+
return resolveViaPrompt(props, count.value);
|
|
12192
|
+
}
|
|
12038
12193
|
log.warn("refusing to start a task on a dirty tree", { cwd: props.cwd, dirtyEntries: count.value });
|
|
12039
12194
|
return Result.error(
|
|
12040
12195
|
new InvalidStateError({
|
|
@@ -12046,24 +12201,105 @@ var preflightTaskUseCase = async (props) => {
|
|
|
12046
12201
|
})
|
|
12047
12202
|
);
|
|
12048
12203
|
};
|
|
12204
|
+
var resolveViaPrompt = async (props, dirtyEntries) => {
|
|
12205
|
+
const log = props.logger.named("task.preflight");
|
|
12206
|
+
if (props.askDirtyTreeChoice === void 0 || props.gitStash === void 0 || props.gitReset === void 0 || props.clock === void 0) {
|
|
12207
|
+
throw new InvalidStateError({
|
|
12208
|
+
entity: ELEMENT_NAME,
|
|
12209
|
+
currentState: "prompt-without-deps",
|
|
12210
|
+
attemptedAction: "configure-prompt-deps",
|
|
12211
|
+
message: "preflight-task: dirtyTreePolicy='prompt' requires askDirtyTreeChoice, gitStash, gitReset, and clock dependencies"
|
|
12212
|
+
});
|
|
12213
|
+
}
|
|
12214
|
+
const choice = await props.askDirtyTreeChoice({ cwd: props.cwd, dirtyEntries });
|
|
12215
|
+
if (!choice.ok) {
|
|
12216
|
+
return Result.error(choice.error);
|
|
12217
|
+
}
|
|
12218
|
+
switch (choice.value) {
|
|
12219
|
+
case "keep":
|
|
12220
|
+
log.info(`working tree dirty (${String(dirtyEntries)} entries) \u2014 proceeding (user chose 'keep')`, {
|
|
12221
|
+
cwd: props.cwd,
|
|
12222
|
+
dirtyEntries
|
|
12223
|
+
});
|
|
12224
|
+
return Result.ok(void 0);
|
|
12225
|
+
case "stash": {
|
|
12226
|
+
const sprintLabel = props.sprintId !== void 0 && props.sprintId.length > 0 ? props.sprintId : "unknown";
|
|
12227
|
+
const message = `ralphctl preflight stash (sprint ${sprintLabel}, ${String(props.clock())})`;
|
|
12228
|
+
const stashed = await props.gitStash(props.cwd, message);
|
|
12229
|
+
if (!stashed.ok) return Result.error(stashed.error);
|
|
12230
|
+
if (!stashed.value.stashed) {
|
|
12231
|
+
log.warn("stash reported no changes despite dirty status \u2014 proceeding", { cwd: props.cwd });
|
|
12232
|
+
return Result.ok(void 0);
|
|
12233
|
+
}
|
|
12234
|
+
log.info(`stashed working tree \u2014 recoverable as: ${message}`, { cwd: props.cwd, stashMessage: message });
|
|
12235
|
+
return Result.ok(void 0);
|
|
12236
|
+
}
|
|
12237
|
+
case "reset": {
|
|
12238
|
+
const reset = await props.gitReset(props.cwd);
|
|
12239
|
+
if (!reset.ok) return Result.error(reset.error);
|
|
12240
|
+
log.info("reset working tree \u2014 discarded uncommitted + untracked changes", { cwd: props.cwd });
|
|
12241
|
+
return Result.ok(void 0);
|
|
12242
|
+
}
|
|
12243
|
+
case "cancel":
|
|
12244
|
+
return Result.error(
|
|
12245
|
+
new AbortError({ elementName: ELEMENT_NAME, reason: "user cancelled on dirty working tree" })
|
|
12246
|
+
);
|
|
12247
|
+
}
|
|
12248
|
+
};
|
|
12049
12249
|
|
|
12050
12250
|
// src/application/flows/implement/leaves/preflight-task.ts
|
|
12251
|
+
var ELEMENT_NAME2 = "preflight-task";
|
|
12051
12252
|
var preflightTaskLeaf = (deps, cwd, name = "preflight-task") => {
|
|
12052
12253
|
const gitStatusEntryCount = async (path) => {
|
|
12053
12254
|
const status = await gitStatusPorcelain(deps.gitRunner, path);
|
|
12054
12255
|
if (!status.ok) return status;
|
|
12055
12256
|
return { ok: true, value: status.value.length };
|
|
12056
12257
|
};
|
|
12258
|
+
const gitStash = (path, message) => gitStashPush(deps.gitRunner, path, message);
|
|
12259
|
+
const gitReset = (path) => gitResetHard(deps.gitRunner, path);
|
|
12260
|
+
const askDirtyTreeChoice = async ({
|
|
12261
|
+
cwd: dirtyCwd,
|
|
12262
|
+
dirtyEntries
|
|
12263
|
+
}) => {
|
|
12264
|
+
const choice = await deps.interactive.askChoice(
|
|
12265
|
+
`Working tree at ${String(dirtyCwd)} has ${String(dirtyEntries)} uncommitted change(s). How do you want to handle it?`,
|
|
12266
|
+
[
|
|
12267
|
+
{
|
|
12268
|
+
label: "Keep changes \u2014 proceed on the dirty tree",
|
|
12269
|
+
value: "keep",
|
|
12270
|
+
description: "AI may build on / overwrite the pending diff"
|
|
12271
|
+
},
|
|
12272
|
+
{ label: "Stash \u2014 save changes to a recoverable stash, then proceed", value: "stash" },
|
|
12273
|
+
{
|
|
12274
|
+
label: "Reset \u2014 discard all uncommitted + untracked changes, then proceed",
|
|
12275
|
+
value: "reset",
|
|
12276
|
+
description: "git reset --hard && git clean -fd"
|
|
12277
|
+
},
|
|
12278
|
+
{ label: "Cancel \u2014 abort the implement run", value: "cancel" }
|
|
12279
|
+
]
|
|
12280
|
+
);
|
|
12281
|
+
if (!choice.ok) {
|
|
12282
|
+
return Result.error(
|
|
12283
|
+
new AbortError({ elementName: ELEMENT_NAME2, reason: `dirty-tree prompt cancelled \u2014 ${choice.error.message}` })
|
|
12284
|
+
);
|
|
12285
|
+
}
|
|
12286
|
+
return Result.ok(choice.value);
|
|
12287
|
+
};
|
|
12057
12288
|
return leaf(name, {
|
|
12058
12289
|
useCase: {
|
|
12059
|
-
execute: async () => preflightTaskUseCase({
|
|
12290
|
+
execute: async (input) => preflightTaskUseCase({
|
|
12060
12291
|
cwd,
|
|
12061
12292
|
gitStatusEntryCount,
|
|
12293
|
+
gitStash,
|
|
12294
|
+
gitReset,
|
|
12295
|
+
askDirtyTreeChoice,
|
|
12296
|
+
clock: deps.clock,
|
|
12297
|
+
sprintId: input.sprintId,
|
|
12062
12298
|
logger: deps.logger,
|
|
12063
12299
|
...deps.dirtyTreePolicy !== void 0 ? { dirtyTreePolicy: deps.dirtyTreePolicy } : {}
|
|
12064
12300
|
})
|
|
12065
12301
|
},
|
|
12066
|
-
input: () =>
|
|
12302
|
+
input: (ctx) => ({ sprintId: String(ctx.sprintId) }),
|
|
12067
12303
|
output: (ctx) => ctx
|
|
12068
12304
|
});
|
|
12069
12305
|
};
|
|
@@ -12837,12 +13073,15 @@ var createImplementFlow = (deps, opts) => {
|
|
|
12837
13073
|
path: r.path,
|
|
12838
13074
|
...r.setupScript !== void 0 ? { setupScript: r.setupScript } : {}
|
|
12839
13075
|
}));
|
|
13076
|
+
const dirtyTreePolicy = opts.dirtyTreePolicy ?? "prompt";
|
|
12840
13077
|
const preflightLeaves = uniqueRepoCwds.map(
|
|
12841
13078
|
(cwd, i) => preflightTaskLeaf(
|
|
12842
13079
|
{
|
|
12843
13080
|
gitRunner: deps.gitRunner,
|
|
13081
|
+
interactive: deps.interactive,
|
|
13082
|
+
clock: deps.clock,
|
|
12844
13083
|
logger: deps.logger,
|
|
12845
|
-
|
|
13084
|
+
dirtyTreePolicy
|
|
12846
13085
|
},
|
|
12847
13086
|
cwd,
|
|
12848
13087
|
`preflight-task-${String(i + 1)}-${String(cwd)}`
|
|
@@ -12903,7 +13142,7 @@ var createImplementFlow = (deps, opts) => {
|
|
|
12903
13142
|
|
|
12904
13143
|
// src/integration/observability/sinks/file-log-sink.ts
|
|
12905
13144
|
import { promises as fs17 } from "fs";
|
|
12906
|
-
import { dirname as
|
|
13145
|
+
import { dirname as dirname9 } from "path";
|
|
12907
13146
|
var startFileLogSink = (deps) => {
|
|
12908
13147
|
const queue = [];
|
|
12909
13148
|
let draining;
|
|
@@ -12915,7 +13154,7 @@ var startFileLogSink = (deps) => {
|
|
|
12915
13154
|
if (next === void 0) continue;
|
|
12916
13155
|
try {
|
|
12917
13156
|
if (!dirEnsured) {
|
|
12918
|
-
await fs17.mkdir(
|
|
13157
|
+
await fs17.mkdir(dirname9(String(deps.file)), { recursive: true });
|
|
12919
13158
|
dirEnsured = true;
|
|
12920
13159
|
}
|
|
12921
13160
|
await fs17.appendFile(String(deps.file), `${JSON.stringify(next)}
|
|
@@ -16704,6 +16943,39 @@ var createTicketRemoveFlow = (deps) => leaf("ticket-remove", {
|
|
|
16704
16943
|
output: (c, o) => ({ ...c, output: o })
|
|
16705
16944
|
});
|
|
16706
16945
|
|
|
16946
|
+
// src/business/task/unblock-task.ts
|
|
16947
|
+
var unblockTaskUseCase = async (props) => {
|
|
16948
|
+
const log = props.logger.named("task.unblock");
|
|
16949
|
+
if (props.task.status === "todo") {
|
|
16950
|
+
log.debug("already todo, skipping", { taskId: props.task.id, sprintId: props.sprintId });
|
|
16951
|
+
return Result.ok(props.task);
|
|
16952
|
+
}
|
|
16953
|
+
log.debug("unblocking task", {
|
|
16954
|
+
taskId: props.task.id,
|
|
16955
|
+
sprintId: props.sprintId,
|
|
16956
|
+
from: props.task.status
|
|
16957
|
+
});
|
|
16958
|
+
const transitioned = unblockTask(props.task);
|
|
16959
|
+
if (!transitioned.ok) {
|
|
16960
|
+
log.warn("invalid state transition", {
|
|
16961
|
+
taskId: props.task.id,
|
|
16962
|
+
from: props.task.status,
|
|
16963
|
+
error: transitioned.error.message
|
|
16964
|
+
});
|
|
16965
|
+
return Result.error(transitioned.error);
|
|
16966
|
+
}
|
|
16967
|
+
const persisted = await props.taskRepo.update(props.sprintId, transitioned.value);
|
|
16968
|
+
if (!persisted.ok) {
|
|
16969
|
+
log.error("persist failed", { taskId: transitioned.value.id, error: persisted.error.message });
|
|
16970
|
+
return Result.error(persisted.error);
|
|
16971
|
+
}
|
|
16972
|
+
log.info(`unblocked task '${transitioned.value.name}'`, {
|
|
16973
|
+
taskId: transitioned.value.id,
|
|
16974
|
+
sprintId: props.sprintId
|
|
16975
|
+
});
|
|
16976
|
+
return Result.ok(transitioned.value);
|
|
16977
|
+
};
|
|
16978
|
+
|
|
16707
16979
|
// src/application/ui/tui/views/sprint-detail-view.tsx
|
|
16708
16980
|
import { jsx as jsx41, jsxs as jsxs30 } from "react/jsx-runtime";
|
|
16709
16981
|
var buildFocusList = (sprint, tasks) => [
|
|
@@ -16772,12 +17044,15 @@ var SprintDetailView = () => {
|
|
|
16772
17044
|
const [feedback, setFeedback] = useState25(void 0);
|
|
16773
17045
|
const ticketsEditable = sprint?.status === "draft";
|
|
16774
17046
|
const inDetail = openIdx !== void 0;
|
|
17047
|
+
const focusedNow = focusList[Math.min(cursorIdx, Math.max(0, focusList.length - 1))];
|
|
17048
|
+
const focusedBlockedTask = focusedNow?.kind === "task" && focusedNow.task.status === "blocked" ? focusedNow.task : void 0;
|
|
16775
17049
|
useViewHints(
|
|
16776
17050
|
inDetail ? [{ keys: "esc", label: "back to list" }] : [
|
|
16777
17051
|
{ keys: "n", label: "flows" },
|
|
16778
17052
|
{ keys: "\u21B5/o", label: "open" },
|
|
16779
17053
|
{ keys: "a", label: "add ticket" },
|
|
16780
|
-
{ keys: "d", label: "remove ticket" }
|
|
17054
|
+
{ keys: "d", label: "remove ticket" },
|
|
17055
|
+
...focusedBlockedTask !== void 0 ? [{ keys: "u", label: "unblock" }] : []
|
|
16781
17056
|
]
|
|
16782
17057
|
);
|
|
16783
17058
|
useInput14((input, key) => {
|
|
@@ -16805,6 +17080,10 @@ var SprintDetailView = () => {
|
|
|
16805
17080
|
if (input === "d" && ticketsEditable) {
|
|
16806
17081
|
const focused = focusList[Math.min(cursorIdx, focusList.length - 1)];
|
|
16807
17082
|
if (focused?.kind === "ticket") setConfirmRemove(focused.ticket);
|
|
17083
|
+
return;
|
|
17084
|
+
}
|
|
17085
|
+
if (input === "u" && focusedBlockedTask !== void 0) {
|
|
17086
|
+
void handleUnblock(focusedBlockedTask);
|
|
16808
17087
|
}
|
|
16809
17088
|
});
|
|
16810
17089
|
useEffect17(() => confirmRemove !== void 0 ? ui.claimPrompt() : void 0, [confirmRemove, ui.claimPrompt]);
|
|
@@ -16820,6 +17099,21 @@ var SprintDetailView = () => {
|
|
|
16820
17099
|
setFeedback(`\u2713 removed "${target.title}"`);
|
|
16821
17100
|
reload();
|
|
16822
17101
|
};
|
|
17102
|
+
const handleUnblock = async (target) => {
|
|
17103
|
+
if (sprint === void 0) return;
|
|
17104
|
+
const r = await unblockTaskUseCase({
|
|
17105
|
+
task: target,
|
|
17106
|
+
sprintId: sprint.id,
|
|
17107
|
+
taskRepo: deps.taskRepo,
|
|
17108
|
+
logger: deps.logger
|
|
17109
|
+
});
|
|
17110
|
+
if (!r.ok) {
|
|
17111
|
+
setFeedback(`\u2717 ${r.error.message}`);
|
|
17112
|
+
return;
|
|
17113
|
+
}
|
|
17114
|
+
setFeedback(`\u2713 unblocked "${target.name}"`);
|
|
17115
|
+
reload();
|
|
17116
|
+
};
|
|
16823
17117
|
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
17118
|
/* @__PURE__ */ jsxs30(Text31, { children: [
|
|
16825
17119
|
"Remove ticket ",
|
|
@@ -19615,7 +19909,7 @@ import { basename as basename3, join as join33 } from "path";
|
|
|
19615
19909
|
// src/application/ui/tui/prompts/path-picker-prompt.tsx
|
|
19616
19910
|
import { useEffect as useEffect28, useState as useState35 } from "react";
|
|
19617
19911
|
import { promises as fs24 } from "fs";
|
|
19618
|
-
import { dirname as
|
|
19912
|
+
import { dirname as dirname10, join as join32 } from "path";
|
|
19619
19913
|
import { homedir } from "os";
|
|
19620
19914
|
import { Box as Box43, Text as Text45, useInput as useInput23 } from "ink";
|
|
19621
19915
|
import { jsx as jsx55, jsxs as jsxs44 } from "react/jsx-runtime";
|
|
@@ -19675,7 +19969,7 @@ var PathPickerPrompt = ({
|
|
|
19675
19969
|
return;
|
|
19676
19970
|
}
|
|
19677
19971
|
if (key.backspace || key.delete) {
|
|
19678
|
-
const parent =
|
|
19972
|
+
const parent = dirname10(cwd);
|
|
19679
19973
|
if (parent !== cwd) {
|
|
19680
19974
|
setCwd(parent);
|
|
19681
19975
|
setCursor(1);
|
|
@@ -19700,7 +19994,7 @@ var PathPickerPrompt = ({
|
|
|
19700
19994
|
const row = rows[cursor];
|
|
19701
19995
|
if (row === void 0) return;
|
|
19702
19996
|
if (row.kind === "parent") {
|
|
19703
|
-
const parent =
|
|
19997
|
+
const parent = dirname10(cwd);
|
|
19704
19998
|
if (parent !== cwd) {
|
|
19705
19999
|
setCwd(parent);
|
|
19706
20000
|
setCursor(1);
|
|
@@ -21734,6 +22028,44 @@ var registerTaskCommand = (program) => {
|
|
|
21734
22028
|
return;
|
|
21735
22029
|
}
|
|
21736
22030
|
process.stdout.write(`${JSON.stringify(result.value, null, 2)}
|
|
22031
|
+
`);
|
|
22032
|
+
});
|
|
22033
|
+
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) => {
|
|
22034
|
+
const sprintId = SprintId.parse(opts.sprint);
|
|
22035
|
+
if (!sprintId.ok) {
|
|
22036
|
+
process.stderr.write(`error: invalid sprint id: ${sprintId.error.message}
|
|
22037
|
+
`);
|
|
22038
|
+
process.exit(1);
|
|
22039
|
+
return;
|
|
22040
|
+
}
|
|
22041
|
+
const taskId = TaskId.parse(rawTaskId);
|
|
22042
|
+
if (!taskId.ok) {
|
|
22043
|
+
process.stderr.write(`error: invalid task id: ${taskId.error.message}
|
|
22044
|
+
`);
|
|
22045
|
+
process.exit(1);
|
|
22046
|
+
return;
|
|
22047
|
+
}
|
|
22048
|
+
const { deps } = await bootstrapCli();
|
|
22049
|
+
const loaded = await deps.taskRepo.findById(sprintId.value, taskId.value);
|
|
22050
|
+
if (!loaded.ok) {
|
|
22051
|
+
process.stderr.write(`error: ${loaded.error.message}
|
|
22052
|
+
`);
|
|
22053
|
+
process.exit(1);
|
|
22054
|
+
return;
|
|
22055
|
+
}
|
|
22056
|
+
const result = await unblockTaskUseCase({
|
|
22057
|
+
task: loaded.value,
|
|
22058
|
+
sprintId: sprintId.value,
|
|
22059
|
+
taskRepo: deps.taskRepo,
|
|
22060
|
+
logger: deps.logger
|
|
22061
|
+
});
|
|
22062
|
+
if (!result.ok) {
|
|
22063
|
+
process.stderr.write(`error: ${result.error.message}
|
|
22064
|
+
`);
|
|
22065
|
+
process.exit(1);
|
|
22066
|
+
return;
|
|
22067
|
+
}
|
|
22068
|
+
process.stdout.write(`unblocked task '${result.value.name}' (${String(result.value.id)})
|
|
21737
22069
|
`);
|
|
21738
22070
|
});
|
|
21739
22071
|
};
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ralphctl",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Agent harness for long-running AI coding tasks — orchestrates Claude Code & GitHub Copilot across repositories",
|
|
5
5
|
"homepage": "https://github.com/lukas-grigis/ralphctl",
|
|
6
6
|
"type": "module",
|