Public: anyone can read this; the crew can change it.
Every direction on the front door stops mid-word: "(serve a dep" #187
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/179-every-direction-on-the-front-door-stops → main 1 commit fork
2 files
changed +59
−2
src/crew/autonomy.ts +29 −2
| @@ -35,7 +35,12 @@ import { readyNow, repoGraph, type Ranked } from "../core/dependencies.ts"; | ||
| 35 | 35 | import { latestDecision, resolveOpen } from "../core/decisions.ts"; |
| 36 | 36 | import { certain } from "./decide.ts"; |
| 37 | 37 | import { decideAndRecord } from "./ledger.ts"; |
| 38 | import { getIssue, getIssueById, type Issue } from "../core/issues.ts"; | |
| 38 | import { | |
| 39 | getIssue, | |
| 40 | getIssueById, | |
| 41 | MAX_TITLE, | |
| 42 | type Issue, | |
| 43 | } from "../core/issues.ts"; | |
| 39 | 44 | import { |
| 40 | 45 | getPull, |
| 41 | 46 | closesFromBody, |
| @@ -524,6 +529,28 @@ export async function sweepGreen(app: App, repo: Repo): Promise<number> { | ||
| 524 | 529 | return landed; |
| 525 | 530 | } |
| 526 | 531 | |
| 532 | /** The label every direction issue's title starts with. Read elsewhere to | |
| 533 | * find a repository's standing direction — keep it exactly as it is. */ | |
| 534 | const DIRECTION_PREFIX = "Direction: "; | |
| 535 | /** How much of the direction fits in the title, capped so the title | |
| 536 | * (prefix included) never exceeds `MAX_TITLE`. */ | |
| 537 | const DIRECTION_TITLE_LIMIT = Math.min(120, MAX_TITLE - DIRECTION_PREFIX.length); | |
| 538 | ||
| 539 | /** Cuts `text` to at most `limit` characters at a word boundary, ending in | |
| 540 | * "…" when something was cut — never mid-word, never a dangling "(" before | |
| 541 | * the ellipsis. Text that already fits is returned unchanged. */ | |
| 542 | export function truncateAtWord(text: string, limit: number): string { | |
| 543 | if (text.length <= limit) return text; | |
| 544 | const ellipsis = "…"; | |
| 545 | const budget = Math.max(limit - ellipsis.length, 0); | |
| 546 | let cut = text.slice(0, budget); | |
| 547 | if (text[budget] !== " " && text[budget] !== undefined) { | |
| 548 | const lastSpace = cut.lastIndexOf(" "); | |
| 549 | cut = lastSpace === -1 ? "" : cut.slice(0, lastSpace); | |
| 550 | } | |
| 551 | return cut.replace(/[\s(]+$/, "") + ellipsis; | |
| 552 | } | |
| 553 | ||
| 527 | 554 | /** |
| 528 | 555 | * The issue the planner works from: the direction, as an open issue the |
| 529 | 556 | * run can be about. Made once, kept, and never counted as work. |
| @@ -538,7 +565,7 @@ function ensureDirectionIssue( | ||
| 538 | 565 | if (existing && existing.state === "open") return existing; |
| 539 | 566 | } |
| 540 | 567 | const opened = openIssue(app, by, repo, { |
| 541 | title: `Direction: ${repo.autonomyDirection.trim().slice(0, 120)}`, | |
| 568 | title: `${DIRECTION_PREFIX}${truncateAtWord(repo.autonomyDirection.trim(), DIRECTION_TITLE_LIMIT)}`, | |
| 542 | 569 | body: `${repo.autonomyDirection.trim()}\n\nThis issue carries the repository's direction. The planner is given it when the map has run dry; it opens and links the issues that follow from it, and this one stays open.`, |
| 543 | 570 | }); |
| 544 | 571 | if (!opened.ok) return undefined; |
test/autonomy.test.ts +30
| @@ -32,7 +32,37 @@ import { | ||
| 32 | 32 | landIfGreen, |
| 33 | 33 | pickBuilder, |
| 34 | 34 | sweepGreen, |
| 35 | truncateAtWord, | |
| 35 | 36 | } from "../src/crew/autonomy.ts"; |
| 37 | import { MAX_TITLE } from "../src/core/issues.ts"; | |
| 38 | ||
| 39 | describe("truncateAtWord", () => { | |
| 40 | test("a direction that fits is passed through whole", () => { | |
| 41 | const text = "a".repeat(40); | |
| 42 | expect(truncateAtWord(text, 120)).toBe(text); | |
| 43 | }); | |
| 44 | ||
| 45 | test("a direction that overflows is cut at a word boundary with an ellipsis", () => { | |
| 46 | const text = ( | |
| 47 | "Make the studio the front door of the forge: a change is visible in its preview within seconds of each push (serve a deployed preview from every push and say so on the page) " + | |
| 48 | "and every direction the crew has ever been given keeps going on" | |
| 49 | ).slice(0, 200); | |
| 50 | expect(text.length).toBe(200); | |
| 51 | const cut = truncateAtWord(text, 120); | |
| 52 | expect(cut.length).toBeLessThanOrEqual(120); | |
| 53 | expect(cut.endsWith("…")).toBe(true); | |
| 54 | const withoutEllipsis = cut.slice(0, -1); | |
| 55 | expect(withoutEllipsis.endsWith(" ")).toBe(false); | |
| 56 | expect(withoutEllipsis.endsWith("(")).toBe(false); | |
| 57 | // The cut never falls inside a word: the character right after it in | |
| 58 | // the source text is a space, or the word ended there anyway. | |
| 59 | expect(text[withoutEllipsis.length]).toBe(" "); | |
| 60 | }); | |
| 61 | ||
| 62 | test("Direction: plus the limit stays well under MAX_TITLE", () => { | |
| 63 | expect("Direction: ".length + 120).toBeLessThanOrEqual(MAX_TITLE); | |
| 64 | }); | |
| 65 | }); | |
| 36 | 66 | |
| 37 | 67 | describe("a repository on its own", () => { |
| 38 | 68 | let dataDir: string; |
new above
offline