Public: anyone can read this; the crew can change it.
The crew's one-line reasons are cut with `clip`: a parked reason ending in an emoji keeps it whole #6768
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/6628-the-crew-s-one-line-reasons-are-cut-with → main 1 commit fork 2 threads · 2 unresolved
5 files
changed +42
−22
src/crew/handed.ts +3 −3
| @@ -10,6 +10,7 @@ import type { Database } from "bun:sqlite"; | ||
| 10 | 10 | import { PARK_LIKE } from "../core/done_on_main.ts"; |
| 11 | 11 | import { getReposByIds } from "../core/repos.ts"; |
| 12 | 12 | import { lastDrill } from "../snapshot/drills.ts"; |
| 13 | import { clip } from "../text/clip.ts"; | |
| 13 | 14 | import { PARK_STRIKES, parkedTwice } from "./parks.ts"; |
| 14 | 15 | |
| 15 | 16 | /** A park reason is cut to this many characters on a card or a terminal row. */ |
| @@ -34,10 +35,9 @@ export interface CrewHandover { | ||
| 34 | 35 | readonly drill: DrillLine | undefined; |
| 35 | 36 | } |
| 36 | 37 | |
| 37 | /** One line, at most `REASON_MAX` characters. */ | |
| 38 | /** One line, at most `REASON_MAX` code points. */ | |
| 38 | 39 | export function oneLine(reason: string): string { |
| 39 | const flat = reason.replace(/\s+/g, " ").trim(); | |
| 40 | return flat.length <= REASON_MAX ? flat : `${flat.slice(0, REASON_MAX - 1)}…`; | |
| 40 | return clip(reason, REASON_MAX); | |
| 41 | 41 | } |
| 42 | 42 | |
| 43 | 43 | /** The issues parked twice in any repository, newest park first. */ |
src/crew/plan.ts +1 −4
| @@ -16,6 +16,7 @@ | ||
| 16 | 16 | |
| 17 | 17 | import { tmpdir } from "node:os"; |
| 18 | 18 | import { childEnv } from "../apps/process.ts"; |
| 19 | import { clip } from "../text/clip.ts"; | |
| 19 | 20 | |
| 20 | 21 | export interface PlanAgent { |
| 21 | 22 | readonly name: string; |
| @@ -65,10 +66,6 @@ const SCHEMA = { | ||
| 65 | 66 | additionalProperties: false, |
| 66 | 67 | } as const; |
| 67 | 68 | |
| 68 | function clip(text: string, max: number): string { | |
| 69 | const flat = text.replace(/\s+/g, " ").trim(); | |
| 70 | return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat; | |
| 71 | } | |
| 72 | 69 | |
| 73 | 70 | function buildPrompt(input: PlanInput): string { |
| 74 | 71 | const roster = input.agents |
src/crew/sentinel.ts +8 −8
| @@ -32,6 +32,7 @@ import { spans } from "../kernel/degraded_history.ts"; | ||
| 32 | 32 | import { QUEUE_WAIT_MAX_MS } from "../kernel/vitals.ts"; |
| 33 | 33 | import { STALE_AFTER_MS } from "../runs/lease.ts"; |
| 34 | 34 | import { isOutageSummary } from "./network.ts"; |
| 35 | import { clip } from "../text/clip.ts"; | |
Put this import in path order with the other ../ imports. |
||
| 35 | 36 | |
| 36 | 37 | const log = createLog("sentinel"); |
| 37 | 38 | |
| @@ -63,8 +64,7 @@ export interface Finding { | ||
| 63 | 64 | readonly incident?: string; |
| 64 | 65 | } |
| 65 | 66 | |
| 66 | const clip = (text: string, n = 200): string => | |
| 67 | text.length > n ? `${text.slice(0, n - 1)}…` : text; | |
| 67 | const QUOTE_MAX = 200; | |
| 68 | 68 | /** |
| 69 | 69 | * Evidence is filed where others read it, and may be public: anything |
| 70 | 70 | * credential-shaped, or a long unbroken token, never leaves as quoted. |
| @@ -111,7 +111,7 @@ export function strayTriggers(db: Database): Finding[] { | ||
| 111 | 111 | title: `Trigger ${t.name} on ${t.tbl_name} is not in the schema`, |
| 112 | 112 | evidence: [ |
| 113 | 113 | `sqlite_master: trigger \`${t.name}\` on table \`${t.tbl_name}\``, |
| 114 | `sql: ${clip(oneLine(t.sql ?? ""))}`, | |
| 114 | `sql: ${clip(t.sql ?? "", QUOTE_MAX)}`, | |
| 115 | 115 | ], |
| 116 | 116 | pillar: "Trust: the database does only what the schema says", |
| 117 | 117 | lookFor: `\`SELECT name FROM sqlite_master WHERE type = 'trigger'\` lists only triggers the schema creates.`, |
| @@ -300,7 +300,7 @@ export function runStreaks(db: Database): Finding[] { | ||
| 300 | 300 | title: `The check in ${repoSlug(db, repoId)} has not been green in its last ${runs.length} runs`, |
| 301 | 301 | evidence: [ |
| 302 | 302 | `${failed.length} of the last ${runs.length} check runs failed, none passed`, |
| 303 | `${hits.length} say: "${clip(line)}"`, | |
| 303 | `${hits.length} say: "${clip(line, QUOTE_MAX)}"`, | |
| 304 | 304 | `runs: ${hits.slice(0, 5).map((r) => r.id).join(", ")}`, |
| 305 | 305 | ], |
| 306 | 306 | pillar: "Trust: a green check means something", |
| @@ -323,7 +323,7 @@ export function runStreaks(db: Database): Finding[] { | ||
| 323 | 323 | title: `Crew runs in ${repoSlug(db, repoId)} keep failing at the push or the sandbox`, |
| 324 | 324 | evidence: [ |
| 325 | 325 | `${pushes.length} of the last ${runs.length} agent runs failed with a push or sandbox line`, |
| 326 | `e.g. "${clip(oneLine(pushes[0]?.summary ?? ""))}"`, | |
| 326 | `e.g. "${clip(pushes[0]?.summary ?? "", QUOTE_MAX)}"`, | |
| 327 | 327 | `runs: ${pushes.slice(0, 5).map((r) => r.id).join(", ")}`, |
| 328 | 328 | ], |
| 329 | 329 | pillar: "Throughput: a crew run can finish what it starts", |
| @@ -351,7 +351,7 @@ export function repeatedErrors(now: number): Finding[] { | ||
| 351 | 351 | title: `The log says "${clip(k, 90)}" ${lines.length} times`, |
| 352 | 352 | evidence: [ |
| 353 | 353 | `${lines.length} ERROR lines, newest ${lines[0]?.at ?? ""}`, |
| 354 | `line: ${clip(oneLine(lines[0]?.message ?? ""))}`, | |
| 354 | `line: ${clip(lines[0]?.message ?? "", QUOTE_MAX)}`, | |
| 355 | 355 | ], |
| 356 | 356 | pillar: "Reliability: a quiet log", |
| 357 | 357 | lookFor: "No ERROR line of this shape in an hour.", |
| @@ -387,7 +387,7 @@ export function degradedCauses(db: Database, now: number, forgeRepoId?: string): | ||
| 387 | 387 | const minutes = Math.round(d.ms / 60_000); |
| 388 | 388 | out.push({ |
| 389 | 389 | key: `degraded:${keyPart(cause)}`, |
| 390 | title: `${cause} was degraded ${minutes} minutes today: ${clip(oneLine(d.sentence), 120)}`, | |
| 390 | title: `${cause} was degraded ${minutes} minutes today: ${clip(d.sentence, 120)}`, | |
| 391 | 391 | evidence: [ |
| 392 | 392 | `cause: ${cause}`, |
| 393 | 393 | `sentence: ${oneLine(d.sentence)}`, |
| @@ -438,7 +438,7 @@ export function budgetBreaches(db: Database, now: number, forgeRepoId?: string): | ||
| 438 | 438 | if (s.closedAt !== null || !BREACH_CAUSE.test(s.cause) || !ownCause(s.cause, forgeRepoId)) continue; |
| 439 | 439 | out.push({ |
| 440 | 440 | key: `degraded:${keyPart(s.cause)}`, |
| 441 | title: `${s.cause} is past its budget: ${clip(oneLine(s.sentence), 120)}`, | |
| 441 | title: `${s.cause} is past its budget: ${clip(s.sentence, 120)}`, | |
| 442 | 442 | evidence: [ |
| 443 | 443 | `cause: ${s.cause}`, |
| 444 | 444 | `sentence: ${oneLine(s.sentence)}`, |
src/crew/vision.ts +6 −7
| @@ -17,6 +17,7 @@ import type { Repo } from "../core/repos.ts"; | ||
| 17 | 17 | import type { User } from "../core/users.ts"; |
| 18 | 18 | import { listTree, readerFor } from "../git/read.ts"; |
| 19 | 19 | import { repoDir } from "../git/repos.ts"; |
| 20 | import { clip } from "../text/clip.ts"; | |
| 20 | 21 | |
| 21 | 22 | const log = createLog("vision"); |
| 22 | 23 | |
| @@ -26,9 +27,7 @@ const LINE_SHOWN = 240; | ||
| 26 | 27 | |
| 27 | 28 | /** The line a thought adds: one flat, dated bullet that carries the thought's id. */ |
| 28 | 29 | export function directionLine(text: string, id: string, now: Date): string { |
| 29 | const flat = text.replace(/\s+/g, " ").trim(); | |
| 30 | const said = flat.length > LINE_SHOWN ? `${flat.slice(0, LINE_SHOWN - 1)}…` : flat; | |
| 31 | return `- ${now.toISOString().slice(0, 10)}: ${said} (thought \`${id}\`)`; | |
| 30 | return `- ${now.toISOString().slice(0, 10)}: ${clip(text, LINE_SHOWN)} (thought \`${id}\`)`; | |
| 32 | 31 | } |
| 33 | 32 | |
| 34 | 33 | /** `current` with `line` last under the `Changes of direction` section, made when missing. `current` null means no file. */ |
| @@ -92,15 +91,15 @@ export const BLOCK_START = "<!-- forge:vision:start -->"; | ||
| 92 | 91 | export const BLOCK_END = "<!-- forge:vision:end -->"; |
| 93 | 92 | export const DIRECTION_LIMIT = 2000; |
| 94 | 93 | |
| 95 | const clip = (s: string, n: number): string => (s.length > n ? `${s.slice(0, n - 1).trimEnd()}…` : s); | |
| 94 | /** `clip`, never leaving a space before its "…". */ | |
| 95 | const clipTight = (s: string, n: number): string => clip(s, n).replace(/ …$/, "…"); | |
| 96 | 96 | |
| 97 | 97 | /** The block for a vision, markers included, no longer than `room` characters; null when not even its head fits. */ |
| 98 | 98 | export function visionBlock(vision: Vision, room = DIRECTION_LIMIT): string | null { |
| 99 | const flat = (s: string): string => s.replace(/\s+/g, " ").trim(); | |
| 100 | 99 | const parts: string[] = []; |
| 101 | if (vision.purpose) parts.push(`Purpose: ${clip(flat(vision.purpose), 400)}`); | |
| 100 | if (vision.purpose) parts.push(`Purpose: ${clipTight(vision.purpose, 400)}`); | |
| 102 | 101 | if (vision.order.length > 0) parts.push(`Order of work: ${vision.order.map((o) => `${o.n}. ${o.name}`).join("; ")}.`); |
| 103 | for (const rule of vision.rules) parts.push(`Rule: ${clip(flat(rule), 160)}`); | |
| 102 | for (const rule of vision.rules) parts.push(`Rule: ${clipTight(rule, 160)}`); | |
| 104 | 103 | const kept: string[] = []; |
| 105 | 104 | for (const part of parts) { |
| 106 | 105 | const next = `${BLOCK_START}\n${[...kept, part].join("\n")}\n${BLOCK_END}`; |
test/text-clip-crew.test.ts added +24
| @@ -0,0 +1,24 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { oneLine, REASON_MAX } from "../src/crew/handed"; | |
| 3 | import { directionLine } from "../src/crew/vision"; | |
| 4 | ||
| 5 | const loneSurrogate = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/; | |
| 6 | ||
| 7 | test("a parked reason with an emoji at characters 139–140 keeps it whole", () => { | |
| 8 | const reason = `${"a".repeat(138)}🙂${"b".repeat(60)}`; | |
| 9 | expect(reason.length).toBe(200); | |
| 10 | const line = oneLine(reason); | |
| 11 | expect(loneSurrogate.test(line)).toBe(false); | |
| 12 | expect(line).toBe(`${"a".repeat(138)}🙂…`); | |
| 13 | expect(Array.from(line).length).toBe(REASON_MAX); | |
| 14 | }); | |
| 15 | ||
| 16 | test("a short reason is folded to one line and left whole", () => { | |
| 17 | expect(oneLine(" parked \n twice ")).toBe("parked twice"); | |
| 18 | }); | |
| 19 | ||
| 20 | test("a direction line cut across an emoji leaves no lone surrogate", () => { | |
| 21 | const line = directionLine(`${"a".repeat(238)}🙂${"b".repeat(60)}`, "t1", new Date("2026-10-27T00:00:00Z")); | |
| 22 | expect(loneSurrogate.test(line)).toBe(false); | |
| 23 | expect(line).toBe(`- 2026-10-27: ${"a".repeat(238)}🙂… (thought \`t1\`)`); | |
| 24 | }); |
new above
offline
Two blank lines are left where the local clip was removed; keep one.