fixed regressed parsing, tool sanitation
This commit is contained in:
14
src/dag.ts
14
src/dag.ts
@@ -24,7 +24,7 @@ export function getBlockedTasks(
|
||||
for (const task of pendingTasks) {
|
||||
if (blocked.has(task.id)) continue;
|
||||
const deps = task.dependencies || [];
|
||||
if (deps.some((dep) => failedTaskIds.has(dep))) {
|
||||
if (deps.some((dep) => failedTaskIds.has(dep) || blocked.has(dep))) {
|
||||
blocked.add(task.id);
|
||||
changed = true;
|
||||
}
|
||||
@@ -46,9 +46,15 @@ export function buildExecutionPlan(
|
||||
parallelGroup?: number,
|
||||
failedTaskIds: Set<string> = new Set(),
|
||||
): ExecutionPlan {
|
||||
// Filter out already completed tasks
|
||||
const pendingTasks = project.tasks.filter((t) => !completed.has(t.id));
|
||||
const skippedTasks = project.tasks.filter((t) => completed.has(t.id));
|
||||
// Filter out already completed AND failed tasks
|
||||
// Failed tasks should not be re-scheduled — they're only re-attempted
|
||||
// via the retry mechanism inside executeTask, not via the DAG.
|
||||
const pendingTasks = project.tasks.filter(
|
||||
(t) => !completed.has(t.id) && !failedTaskIds.has(t.id),
|
||||
);
|
||||
const skippedTasks = project.tasks.filter(
|
||||
(t) => completed.has(t.id) || failedTaskIds.has(t.id),
|
||||
);
|
||||
|
||||
// With explicitly declared parallel groups, all groups are independent.
|
||||
// Since there are no cross-group dependencies by definition, standard
|
||||
|
||||
@@ -866,6 +866,20 @@ function sleep(ms: number): Promise<void> {
|
||||
|
||||
// ─── Tool Call Formatting ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Strip control characters and newlines from a display label so it
|
||||
* does not break TUI layout (tree branches, text width calculation).
|
||||
*/
|
||||
function sanitizeLabel(s: string): string {
|
||||
// Replace newlines/carriage returns with spaces (multi-line commands
|
||||
// must fit on a single tree-branch line), then strip ASCII control
|
||||
// characters except \t (which is harmless) and keep printable chars.
|
||||
return s
|
||||
.replace(/\r?\n/g, " ")
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a tool call argument into a short label.
|
||||
*/
|
||||
@@ -873,21 +887,20 @@ function formatToolArg(name: string, args: unknown): string {
|
||||
const a = args as Record<string, unknown>;
|
||||
switch (name) {
|
||||
case "bash":
|
||||
return truncateMiddle(String(a.command ?? ""), 70);
|
||||
return sanitizeLabel(truncateMiddle(String(a.command ?? ""), 70));
|
||||
case "write":
|
||||
case "read":
|
||||
return truncateMiddle(String(a.path ?? ""), 60);
|
||||
return sanitizeLabel(truncateMiddle(String(a.path ?? ""), 60));
|
||||
case "edit":
|
||||
return truncateMiddle(String(a.path ?? ""), 60);
|
||||
return sanitizeLabel(truncateMiddle(String(a.path ?? ""), 60));
|
||||
case "grep":
|
||||
return `${a.pattern ?? "?"} — ${truncateMiddle(
|
||||
String(a.path ?? ""),
|
||||
40,
|
||||
)}`;
|
||||
return sanitizeLabel(
|
||||
`${a.pattern ?? "?"} — ${truncateMiddle(String(a.path ?? ""), 40)}`,
|
||||
);
|
||||
case "find":
|
||||
return `${a.path ?? "."} — ${a.glob ?? "*"}`;
|
||||
return sanitizeLabel(`${a.path ?? "."} — ${a.glob ?? "*"}`);
|
||||
case "ls":
|
||||
return truncateMiddle(String(a.path ?? "."), 60);
|
||||
return sanitizeLabel(truncateMiddle(String(a.path ?? "."), 60));
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -44,8 +44,27 @@ export function parseTaskFile(filePath: string): Project {
|
||||
|
||||
// ─── Fio Format Parser ───────────────────────────────────────────────────────
|
||||
|
||||
/** Match both markdown heading (## Dependencies) and plain heading (Dependencies). */
|
||||
const DEP_HEADING_RE = /^(?:##\s+)?Dependencies\s*$/m;
|
||||
/** Match both markdown heading (## Tasks) and plain heading (Tasks). */
|
||||
const TASK_HEADING_RE = /^(?:##\s+)?Tasks\s*$/m;
|
||||
/** Match other markdown headings (## Something). */
|
||||
const ANY_MD_HEADING_RE = /^##\s/;
|
||||
/**
|
||||
* Detect a plain (non-markdown) section heading like "Exit criteria".
|
||||
* A plain heading must:
|
||||
* - Start with a letter
|
||||
* - Contain only letters and spaces
|
||||
* - Have no colons (avoids matching "Objective:" and "Status legend:")
|
||||
* - Not be a task/dep line (doesn't start with "-")
|
||||
*/
|
||||
function isPlainSectionHeader(line: string): boolean {
|
||||
const trimmed = line.trim();
|
||||
return trimmed.length > 0 && /^[A-Za-z][A-Za-z\s]*$/.test(trimmed);
|
||||
}
|
||||
|
||||
function hasDependenciesSection(content: string): boolean {
|
||||
return /^##\s+Dependencies\s*$/m.test(content);
|
||||
return DEP_HEADING_RE.test(content);
|
||||
}
|
||||
|
||||
function parseFioFormat(
|
||||
@@ -61,20 +80,21 @@ function parseFioFormat(
|
||||
let inDeps = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (/^##\s+Tasks\s*$/m.test(line)) {
|
||||
if (TASK_HEADING_RE.test(line)) {
|
||||
inTasks = true;
|
||||
inDeps = false;
|
||||
continue;
|
||||
}
|
||||
if (/^##\s+Dependencies\s*$/m.test(line)) {
|
||||
if (DEP_HEADING_RE.test(line)) {
|
||||
inTasks = false;
|
||||
inDeps = true;
|
||||
continue;
|
||||
}
|
||||
// Reset state on any other section heading — both ##-style and plain
|
||||
if (
|
||||
/^##\s/.test(line) &&
|
||||
!/^##\s+Tasks/.test(line) &&
|
||||
!/^##\s+Dependencies/.test(line)
|
||||
(ANY_MD_HEADING_RE.test(line) || isPlainSectionHeader(line)) &&
|
||||
!TASK_HEADING_RE.test(line) &&
|
||||
!DEP_HEADING_RE.test(line)
|
||||
) {
|
||||
inTasks = false;
|
||||
inDeps = false;
|
||||
@@ -272,12 +292,14 @@ function parseFioFormat(
|
||||
}
|
||||
}
|
||||
|
||||
// Extract exit criteria
|
||||
// Extract exit criteria — detect both ## Exit Criteria and plain Exit criteria
|
||||
const exitCriteria: string[] = [];
|
||||
const exitIdx = lines.findIndex((l) => /^##\s+Exit\s+Criteria/i.test(l));
|
||||
const exitCriteriaRe = /^(?:##\s+)?Exit\s+Criteria/i;
|
||||
const exitIdx = lines.findIndex((l) => exitCriteriaRe.test(l));
|
||||
if (exitIdx >= 0) {
|
||||
for (let i = exitIdx + 1; i < lines.length; i++) {
|
||||
if (/^##\s/.test(lines[i])) break;
|
||||
// Stop at any new section heading (##-style or plain)
|
||||
if (/^##\s/.test(lines[i]) || isPlainSectionHeader(lines[i])) break;
|
||||
const m = lines[i].match(/^-\s+(.+)$/);
|
||||
if (m) exitCriteria.push(m[1].trim());
|
||||
}
|
||||
@@ -419,9 +441,14 @@ export function updateTaskInFile(
|
||||
const char = statusToChar(status);
|
||||
|
||||
// Strategy 1: Fio numbered format — match by explicit task ID in the file
|
||||
// Try both padded (01) and raw (1) variations
|
||||
const rawId = parseInt(taskId, 10).toString();
|
||||
const idPatterns = new Set([escapeRegex(taskId), escapeRegex(rawId)]);
|
||||
// Try both padded (01) and raw (1) variations.
|
||||
// When the task ID is already zero-padded (e.g., "01"), skip the raw ID
|
||||
// to avoid partial matches ("1" matching the second digit of "01").
|
||||
const idPatterns = new Set([escapeRegex(taskId)]);
|
||||
if (!taskId.startsWith("0")) {
|
||||
const rawId = parseInt(taskId, 10).toString();
|
||||
idPatterns.add(escapeRegex(rawId));
|
||||
}
|
||||
|
||||
for (const idPattern of idPatterns) {
|
||||
const fioRegex = new RegExp(
|
||||
|
||||
Reference in New Issue
Block a user