Skip to content

Commit bd63682

Browse files
author
Baudbot
committed
fix: handle braces in strings in parseTodo, clarify hoursStuck parens
- parseTodo JSON scanner now tracks inString/escapeNext state so braces inside string values (e.g. "Fix {bug}") don't break parsing - Added explicit parens to hoursStuck for clarity (same result, safer) - Added 3 new tests for braces-in-strings and escaped quotes (53 total)
1 parent 2b2db3f commit bd63682

2 files changed

Lines changed: 48 additions & 8 deletions

File tree

pi/extensions/heartbeat.test.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,28 @@ Some markdown body here.`;
301301
assert.equal(result.id, "ws");
302302
});
303303

304+
it("handles braces inside string values", () => {
305+
const content = `{"id": "brace1", "title": "Fix {bug} in {module}", "status": "open"}`;
306+
const result = parseTodo(content);
307+
assert.equal(result.id, "brace1");
308+
assert.equal(result.title, "Fix {bug} in {module}");
309+
assert.equal(result.status, "open");
310+
});
311+
312+
it("handles escaped quotes inside strings", () => {
313+
const content = `{"id": "esc1", "title": "Fix \\"quoted\\" thing", "status": "done"}`;
314+
const result = parseTodo(content);
315+
assert.equal(result.id, "esc1");
316+
assert.equal(result.status, "done");
317+
});
318+
319+
it("handles braces and escapes together", () => {
320+
const content = `{"id": "combo", "title": "Deploy {v2} with \\"zero-token\\" mode", "status": "in-progress", "created_at": "2026-01-01T00:00:00Z"}`;
321+
const result = parseTodo(content);
322+
assert.equal(result.id, "combo");
323+
assert.equal(result.status, "in-progress");
324+
});
325+
304326
it("ignores content after closing brace", () => {
305327
const content = `{"id": "extra", "status": "open"}
306328

pi/extensions/heartbeat.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -206,16 +206,34 @@ function parseTodo(content: string): { status?: string; title?: string; created_
206206
const trimmed = content.trim();
207207
if (!trimmed.startsWith("{")) return null;
208208

209-
// Find the closing brace for the top-level JSON object
209+
// Find the closing brace for the top-level JSON object,
210+
// skipping braces inside string values (handles escaped quotes too)
210211
let depth = 0;
211212
let jsonEnd = -1;
213+
let inString = false;
214+
let escapeNext = false;
212215
for (let i = 0; i < trimmed.length; i++) {
213-
if (trimmed[i] === "{") depth++;
214-
else if (trimmed[i] === "}") {
215-
depth--;
216-
if (depth === 0) {
217-
jsonEnd = i + 1;
218-
break;
216+
const char = trimmed[i];
217+
if (escapeNext) {
218+
escapeNext = false;
219+
continue;
220+
}
221+
if (char === "\\") {
222+
escapeNext = true;
223+
continue;
224+
}
225+
if (char === '"') {
226+
inString = !inString;
227+
continue;
228+
}
229+
if (!inString) {
230+
if (char === "{") depth++;
231+
else if (char === "}") {
232+
depth--;
233+
if (depth === 0) {
234+
jsonEnd = i + 1;
235+
break;
236+
}
219237
}
220238
}
221239
}
@@ -257,7 +275,7 @@ function checkStuckTodos(): CheckResult[] {
257275

258276
if (!hasAgent) {
259277
const title = todo.title || todoId;
260-
const hoursStuck = Math.round(age / (60 * 60 * 1000) * 10) / 10;
278+
const hoursStuck = Math.round((age / (60 * 60 * 1000)) * 10) / 10;
261279

262280
results.push({
263281
name: `stuck:TODO-${todoId}`,

0 commit comments

Comments
 (0)