diff --git a/tools/trajectories/autonomous-pickup.test.ts b/tools/trajectories/autonomous-pickup.test.ts index 3c2b7f5c00..4a4d96d94b 100644 --- a/tools/trajectories/autonomous-pickup.test.ts +++ b/tools/trajectories/autonomous-pickup.test.ts @@ -1,6 +1,9 @@ import { describe, expect, test } from "bun:test"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import type { TrajectoryPacket } from "./autonomous-pickup"; -import { selectNextTrajectory } from "./autonomous-pickup"; +import { readTrajectoryPackets, selectNextTrajectory } from "./autonomous-pickup"; function packet(partial: Partial & Pick): TrajectoryPacket { return { @@ -124,3 +127,38 @@ describe("selectNextTrajectory", () => { expect(selection.blocked[0]?.reason).toContain("claim/factory-trajectory-surface"); }); }); + +describe("readTrajectoryPackets", () => { + test("keeps wrapped top-level next-action fields together", () => { + const repoRoot = mkdtempSync(join(tmpdir(), "zeta-trajectory-pickup-")); + try { + const packetDir = join(repoRoot, "docs", "trajectories", "typescript-bun-migration"); + mkdirSync(packetDir, { recursive: true }); + writeFileSync( + join(packetDir, "RESUME.md"), + [ + "# TypeScript / Bun migration", + "", + "**Status:** active", + "**Next concrete action:** Claim the smallest TypeScript/Bun migration slice and", + "preserve the wrapped continuation text in the generated prompt.", + "**Current blocker:** none", + "", + "## Next Child Packets", + "", + "- none currently selected", + ].join("\n"), + ); + + const packets = readTrajectoryPackets(repoRoot); + + expect(packets).toHaveLength(1); + expect(packets[0]?.nextAction).toBe( + "Claim the smallest TypeScript/Bun migration slice and preserve the wrapped continuation text in the generated prompt", + ); + expect(packets[0]?.blocker).toBe("none"); + } finally { + rmSync(repoRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/tools/trajectories/autonomous-pickup.ts b/tools/trajectories/autonomous-pickup.ts index 241e36a91e..e58508b9c6 100644 --- a/tools/trajectories/autonomous-pickup.ts +++ b/tools/trajectories/autonomous-pickup.ts @@ -157,10 +157,42 @@ function fieldValue(line: string, label: string): string | null { return stripMarkdown(stripped.slice(prefix.length)); } +function isFieldLikeLine(line: string): boolean { + const stripped = line.trim().replaceAll("**", ""); + const colonIndex = stripped.indexOf(":"); + if (colonIndex <= 0 || colonIndex > 80) { + return false; + } + const label = stripped.slice(0, colonIndex).trim(); + return /^[A-Za-z][A-Za-z0-9 /_-]*$/.test(label); +} + +function fieldWithContinuations(lines: readonly string[], index: number, label: string): string | null { + const value = fieldValue(lines[index] ?? "", label); + if (value === null) { + return null; + } + const parts = [value]; + for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex++) { + const trimmed = lines[nextIndex]?.trim() ?? ""; + if ( + trimmed === "" || + trimmed.startsWith("#") || + trimmed.startsWith("- ") || + trimmed.startsWith("|") || + isFieldLikeLine(trimmed) + ) { + break; + } + parts.push(stripMarkdown(trimmed)); + } + return stripMarkdown(parts.join(" ")); +} + function firstField(lines: readonly string[], labels: readonly string[]): string | null { - for (const line of lines) { + for (let index = 0; index < lines.length; index++) { for (const label of labels) { - const value = fieldValue(line, label); + const value = fieldWithContinuations(lines, index, label); if (value !== null && value.length > 0) { return value; }