fix: ralpi-plan properly loads task-manager prompt
This commit is contained in:
31
index.ts
31
index.ts
@@ -1,5 +1,6 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type {
|
||||
ExtensionAPI,
|
||||
ExtensionContext,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from "./src/dag";
|
||||
import { ProgressTracker } from "./src/progress";
|
||||
import { buildPlanPrompt } from "./src/prompts";
|
||||
import { loadTaskManagerPrompt } from "./src/task-manager-prompt";
|
||||
import { formatReflections } from "./src/reflection";
|
||||
import { verdictGlyph, verdictSummary, formatFindings } from "./src/review";
|
||||
import type { ReviewResult } from "./src/types";
|
||||
@@ -255,6 +257,23 @@ async function executePlanBatches(
|
||||
projectDir?: string,
|
||||
isResume?: boolean,
|
||||
): Promise<void> {
|
||||
// Refresh the model registry so the host reloads models.json before we
|
||||
// resolve the round-robin model pool. The registry snapshot is captured at
|
||||
// host startup and only reloaded here; a long-running host would otherwise
|
||||
// skip providers added to models.json after it booted (e.g. "strix").
|
||||
// Best-effort: a failed refresh shouldn't block execution — the pool just
|
||||
// resolves against the existing snapshot.
|
||||
try {
|
||||
await ctx.modelRegistry?.refresh();
|
||||
} catch (error) {
|
||||
ctx.ui.notify(
|
||||
`ralpi: model registry refresh failed — continuing with existing snapshot: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
|
||||
// Write loop-active marker so a session reload can detect an interrupted
|
||||
// loop and resume it (in-process agent sessions die on reload — the marker
|
||||
// + progress.json in_progress tasks are the signal to re-run them).
|
||||
@@ -692,12 +711,18 @@ export default function ralpiLoopExtension(pi: ExtensionAPI): void {
|
||||
},
|
||||
});
|
||||
|
||||
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
pi.registerCommand("ralpi-plan", {
|
||||
description: "Open the Task Manager to plan a ralpi run",
|
||||
handler: async (args: string, ctx: ExtensionContext) => {
|
||||
const prompt = (args || "").trim();
|
||||
const message = prompt ? `@task-manager\n\n${prompt}` : "@task-manager";
|
||||
pi.sendUserMessage(message);
|
||||
// pi.sendUserMessage() sends with expandPromptTemplates: false, so it
|
||||
// would NOT expand `/task-manager` — and `@task-manager` is an
|
||||
// @-mention, not a template invocation. Load the bundled template,
|
||||
// strip frontmatter, substitute $@ args ourselves, and send the
|
||||
// expanded body directly.
|
||||
const body = loadTaskManagerPrompt(extensionDir, args ?? "");
|
||||
pi.sendUserMessage(body);
|
||||
ctx.ui.notify("Opening Task Manager...", "info");
|
||||
},
|
||||
});
|
||||
|
||||
104
src/task-manager-prompt.ts
Normal file
104
src/task-manager-prompt.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { stripFrontmatter } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const TEMPLATE_REL = path.join("prompts", "task-manager.md");
|
||||
|
||||
/**
|
||||
* Parse command arguments respecting quoted strings (bash-style).
|
||||
* Ported from pi's core/prompt-templates.js so the task-manager template
|
||||
* receives the same arg-splitting a real `/task-manager` invocation would.
|
||||
*/
|
||||
function parseCommandArgs(argsString: string): string[] {
|
||||
const args: string[] = [];
|
||||
let current = "";
|
||||
let inQuote: string | null = null;
|
||||
for (let i = 0; i < argsString.length; i++) {
|
||||
const char = argsString[i];
|
||||
if (inQuote) {
|
||||
if (char === inQuote) {
|
||||
inQuote = null;
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
} else if (char === '"' || char === "'") {
|
||||
inQuote = char;
|
||||
} else if (/\s/.test(char)) {
|
||||
if (current) {
|
||||
args.push(current);
|
||||
current = "";
|
||||
}
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
if (current) args.push(current);
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute argument placeholders in template content.
|
||||
* Faithful port of pi's substituteArgs (core/prompt-templates.js):
|
||||
* - $1, $2, ... positional args
|
||||
* - $@ / $ARGUMENTS all args joined
|
||||
* - ${N:-default} positional N with default when missing/empty
|
||||
* - ${@:-default} all args with default when empty
|
||||
* - ${@:N} / ${@:N:L} bash-style slicing
|
||||
*
|
||||
* Replacement runs once over the template only; argument/default values
|
||||
* containing patterns like $1 or $@ are NOT recursively substituted.
|
||||
*/
|
||||
function substituteArgs(content: string, args: string[]): string {
|
||||
const allArgs = args.join(" ");
|
||||
return content.replace(
|
||||
/\$\{(\d+|ARGUMENTS|@):-([^}]*)\}|\$\{@:(\d+)(?::(\d+))?\}|\$(ARGUMENTS|@|\d+)/g,
|
||||
(_match, defaultTarget, defaultValue, sliceStart, sliceLength, simple) => {
|
||||
if (defaultTarget) {
|
||||
const value =
|
||||
defaultTarget === "@" || defaultTarget === "ARGUMENTS"
|
||||
? allArgs
|
||||
: args[parseInt(defaultTarget, 10) - 1];
|
||||
return value ? value : defaultValue;
|
||||
}
|
||||
if (sliceStart) {
|
||||
let start = parseInt(sliceStart, 10) - 1; // 1-indexed → 0-indexed
|
||||
if (start < 0) start = 0;
|
||||
if (sliceLength) {
|
||||
const length = parseInt(sliceLength, 10);
|
||||
return args.slice(start, start + length).join(" ");
|
||||
}
|
||||
return args.slice(start).join(" ");
|
||||
}
|
||||
if (simple === "ARGUMENTS" || simple === "@") {
|
||||
return allArgs;
|
||||
}
|
||||
const index = parseInt(simple, 10) - 1;
|
||||
return args[index] ?? "";
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and expand the task-manager prompt template bundled with the extension.
|
||||
*
|
||||
* `pi.sendUserMessage()` sends with `expandPromptTemplates: false`, so it will
|
||||
* NOT expand a `/task-manager` invocation — and `@task-manager` is an
|
||||
* @-mention, not a template invocation anyway. We therefore read the
|
||||
* template ourselves, strip its frontmatter, substitute args ($@ etc.), and
|
||||
* return the fully-expanded prompt body ready to send as a user message.
|
||||
*
|
||||
* @param extensionDir Absolute path to the extension root (where index.ts
|
||||
* lives), used to locate `prompts/task-manager.md`.
|
||||
* @param argsString Raw argument string from the slash command (may be "").
|
||||
* @throws if the template file is missing or unreadable.
|
||||
*/
|
||||
export function loadTaskManagerPrompt(
|
||||
extensionDir: string,
|
||||
argsString: string,
|
||||
): string {
|
||||
const templatePath = path.join(extensionDir, TEMPLATE_REL);
|
||||
const raw = fs.readFileSync(templatePath, "utf-8");
|
||||
const body = stripFrontmatter(raw);
|
||||
const args = parseCommandArgs(argsString);
|
||||
return substituteArgs(body, args).trim();
|
||||
}
|
||||
@@ -172,6 +172,23 @@ export function createWorktree(
|
||||
if (existing && existing.includes(`worktree ${wtDir}`)) {
|
||||
// The worktree is registered — sanity-check it's a valid checkout.
|
||||
if (getGitHead(wtDir)) {
|
||||
// Return the branch the worktree is ACTUALLY checked out on, NOT the
|
||||
// slug recomputed from the (possibly changed) task title. Mismatch
|
||||
// happens routinely on resume: the task agent may have created its
|
||||
// own feature branch (e.g. `proctored-exam-delivery-mode-10-exam-...`)
|
||||
// once it saw the convention in the git log, the title may have been
|
||||
// edited between runs, or an older ralpi version used a different
|
||||
// naming scheme. Returning the slug here makes `git merge <slug>`
|
||||
// fail with "not something we can merge" because no such ref exists —
|
||||
// exactly the spurious merge-conflict we see on resumes.
|
||||
const actual = getCurrentBranch(wtDir);
|
||||
if (actual && actual !== "HEAD" && actual !== "detached") {
|
||||
return { dir: wtDir, branch: actual, mainDir };
|
||||
}
|
||||
// Detached-HEAD worktree (e.g. left by a prior `--detach` fallback).
|
||||
// The slug ref doesn't exist as a branch — create one matching the
|
||||
// slug from the worktree's current HEAD so the merge step resolves.
|
||||
git(`branch "${branch}" HEAD`, mainDir);
|
||||
return { dir: wtDir, branch, mainDir };
|
||||
}
|
||||
// Registered but broken (dir gone / checkout corrupt) — drop its
|
||||
|
||||
Reference in New Issue
Block a user