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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/6628-the-crew-s-one-line-reasons-are-cut-with → main 1 commit fork 2 threads · 2 unresolved

  • Merged. mason/6628-the-crew-s-one-line-reasons-are-cut-with was merged into main as 30d5ba9.

Review

Sign in to review this change.
5 files changed +42 −22
src/crew/handed.ts +3 −3
@@ -10,6 +10,7 @@ import type { Database } from "bun:sqlite";
1010 import { PARK_LIKE } from "../core/done_on_main.ts";
1111 import { getReposByIds } from "../core/repos.ts";
1212 import { lastDrill } from "../snapshot/drills.ts";
13 import { clip } from "../text/clip.ts";
1314 import { PARK_STRIKES, parkedTwice } from "./parks.ts";
1415
1516 /** A park reason is cut to this many characters on a card or a terminal row. */
@@ -34,10 +35,9 @@ export interface CrewHandover {
3435 readonly drill: DrillLine | undefined;
3536 }
3637
37 /** One line, at most `REASON_MAX` characters. */
38 /** One line, at most `REASON_MAX` code points. */
3839 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);
4141 }
4242
4343 /** The issues parked twice in any repository, newest park first. */
src/crew/plan.ts +1 −4
@@ -16,6 +16,7 @@
1616
1717 import { tmpdir } from "node:os";
1818 import { childEnv } from "../apps/process.ts";
19 import { clip } from "../text/clip.ts";
1920
2021 export interface PlanAgent {
2122 readonly name: string;
@@ -65,10 +66,6 @@ const SCHEMA = {
6566 additionalProperties: false,
6667 } as const;
6768

Two blank lines are left where the local clip was removed; keep one.

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 }
7269
7370 function buildPrompt(input: PlanInput): string {
7471 const roster = input.agents
src/crew/sentinel.ts +8 −8
@@ -32,6 +32,7 @@ import { spans } from "../kernel/degraded_history.ts";
3232 import { QUEUE_WAIT_MAX_MS } from "../kernel/vitals.ts";
3333 import { STALE_AFTER_MS } from "../runs/lease.ts";
3434 import { isOutageSummary } from "./network.ts";
35 import { clip } from "../text/clip.ts";

Put this import in path order with the other ../ imports.

3536
3637 const log = createLog("sentinel");
3738
@@ -63,8 +64,7 @@ export interface Finding {
6364 readonly incident?: string;
6465 }
6566
66 const clip = (text: string, n = 200): string =>
67 text.length > n ? `${text.slice(0, n - 1)}…` : text;
67 const QUOTE_MAX = 200;
6868 /**
6969 * Evidence is filed where others read it, and may be public: anything
7070 * credential-shaped, or a long unbroken token, never leaves as quoted.
@@ -111,7 +111,7 @@ export function strayTriggers(db: Database): Finding[] {
111111 title: `Trigger ${t.name} on ${t.tbl_name} is not in the schema`,
112112 evidence: [
113113 `sqlite_master: trigger \`${t.name}\` on table \`${t.tbl_name}\``,
114 `sql: ${clip(oneLine(t.sql ?? ""))}`,
114 `sql: ${clip(t.sql ?? "", QUOTE_MAX)}`,
115115 ],
116116 pillar: "Trust: the database does only what the schema says",
117117 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[] {
300300 title: `The check in ${repoSlug(db, repoId)} has not been green in its last ${runs.length} runs`,
301301 evidence: [
302302 `${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)}"`,
304304 `runs: ${hits.slice(0, 5).map((r) => r.id).join(", ")}`,
305305 ],
306306 pillar: "Trust: a green check means something",
@@ -323,7 +323,7 @@ export function runStreaks(db: Database): Finding[] {
323323 title: `Crew runs in ${repoSlug(db, repoId)} keep failing at the push or the sandbox`,
324324 evidence: [
325325 `${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)}"`,
327327 `runs: ${pushes.slice(0, 5).map((r) => r.id).join(", ")}`,
328328 ],
329329 pillar: "Throughput: a crew run can finish what it starts",
@@ -351,7 +351,7 @@ export function repeatedErrors(now: number): Finding[] {
351351 title: `The log says "${clip(k, 90)}" ${lines.length} times`,
352352 evidence: [
353353 `${lines.length} ERROR lines, newest ${lines[0]?.at ?? ""}`,
354 `line: ${clip(oneLine(lines[0]?.message ?? ""))}`,
354 `line: ${clip(lines[0]?.message ?? "", QUOTE_MAX)}`,
355355 ],
356356 pillar: "Reliability: a quiet log",
357357 lookFor: "No ERROR line of this shape in an hour.",
@@ -387,7 +387,7 @@ export function degradedCauses(db: Database, now: number, forgeRepoId?: string):
387387 const minutes = Math.round(d.ms / 60_000);
388388 out.push({
389389 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)}`,
391391 evidence: [
392392 `cause: ${cause}`,
393393 `sentence: ${oneLine(d.sentence)}`,
@@ -438,7 +438,7 @@ export function budgetBreaches(db: Database, now: number, forgeRepoId?: string):
438438 if (s.closedAt !== null || !BREACH_CAUSE.test(s.cause) || !ownCause(s.cause, forgeRepoId)) continue;
439439 out.push({
440440 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)}`,
442442 evidence: [
443443 `cause: ${s.cause}`,
444444 `sentence: ${oneLine(s.sentence)}`,
src/crew/vision.ts +6 −7
@@ -17,6 +17,7 @@ import type { Repo } from "../core/repos.ts";
1717 import type { User } from "../core/users.ts";
1818 import { listTree, readerFor } from "../git/read.ts";
1919 import { repoDir } from "../git/repos.ts";
20 import { clip } from "../text/clip.ts";
2021
2122 const log = createLog("vision");
2223
@@ -26,9 +27,7 @@ const LINE_SHOWN = 240;
2627
2728 /** The line a thought adds: one flat, dated bullet that carries the thought's id. */
2829 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}\`)`;
3231 }
3332
3433 /** `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 -->";
9291 export const BLOCK_END = "<!-- forge:vision:end -->";
9392 export const DIRECTION_LIMIT = 2000;
9493
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(/ …$/, "…");
9696
9797 /** The block for a vision, markers included, no longer than `room` characters; null when not even its head fits. */
9898 export function visionBlock(vision: Vision, room = DIRECTION_LIMIT): string | null {
99 const flat = (s: string): string => s.replace(/\s+/g, " ").trim();
10099 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)}`);
102101 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)}`);
104103 const kept: string[] = [];
105104 for (const part of parts) {
106105 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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs