Public: anyone can read this; the crew can change it.

A vision pull shows the direction it will write, and Settings marks which part follows the vision and which is the owner's #6260

Merged opened this

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

Merged

scout/forge:scout/5835-a-vision-pull-shows-the-direction-it-wil → main 3 commits fork 2 threads · 2 unresolved

  • Merged. scout/5835-a-vision-pull-shows-the-direction-it-wil was merged into main as deb87f9.

Review

Sign in to review this change.
11 files changed +246 −11
README.md +5
@@ -2316,3 +2316,8 @@ ledger (`spawn/sizes.ts`). **Unset means no quota.** A spawn that would pass a
23162316 declared limit is a 409 `refused` with one sentence naming it, and the New
23172317 forge page shows that sentence above the form when even a small forge would
23182318 not fit (`spawn/quota.ts`).
2319
2320 ### Seeing the direction a vision pull will write
2321 An open vision pull's page shows, to an admin of the repository, "Direction after the merge": today's direction beside the one an admin's merge will write, computed by the same `nextDirection` the merge uses (`src/crew/vision.ts`), so the two cannot differ. Repository settings (Crew) marks the direction's fenced block `follows docs/VISION.md` and the words around it `the owner's`. Today and After the merge stack vertically at every width.
2322
2323 The same read is a second door: the MCP `vision` tool takes an optional `number`, and for an admin returns `preview` (`{ today, after }`, or `{ why }` the direction would stay) for that open vision pull, plus `directionParts` (the direction cut into `vision` and `owner` parts). The terminal (TUI) has no such view; it is not touched by this change.
docs/MCP.md +1 −1
@@ -136,7 +136,7 @@ for a tunnel and a forge that sleeps.
136136 | `upstream` | `{ repo, enabled?, url?, branch?, check? }`: the repository a fork follows (an admin reads it); with `enabled` sets it, with `check: true` checks it now; setting and checking need an admin token | yes |
137137 | `usage` | `{ repo, days? }`: what the crew's runs spent — tokens, cost, wall time — by subscription, agent, issue, and each account's 5-hour and weekly standing; a writer's | |
138138 | `variant` | `{ repo, number, answer, reason? }`: like or dislike one pull among its variants (`answer` like or dislike); a later vote replaces the earlier; a dislike closes the pull with `reason` | yes |
139 | `vision` | `{ repo }`: `{ purpose, order: [{ n, name, text }], rules, direction, updatedAt }` from docs/VISION.md on the default branch; without the document `purpose` is null and `order` empty; `direction` (the autonomy direction) only for those who may read the repository's settings, else null | |
139 | `vision` | `{ repo, number? }`: `{ purpose, order: [{ n, name, text }], rules, direction, directionParts, preview, updatedAt }` from docs/VISION.md on the default branch; without the document `purpose` is null and `order` empty; `direction` (the autonomy direction) and `directionParts` (it cut into `{ kind: vision|owner, text }`: the fenced block that follows docs/VISION.md, and the owner's words) only for those who may read the repository's settings, else null; with `number` (an open vision pull) those same people get `preview`: `{ today, after }`, the direction as it stands and as an admin's merge will write it, or `{ why }` it would stay; else null | |

docs/MCP.md is on the direction's freeze list while there are more than 40 open pulls. One row is acceptable here because the MCP door was asked for, but don't add further edits to this file in this pull.

140140 | `wait_for` | `{ app, issue, blockedBy, stop? }`: the issue waits for another, or stops with `stop: true`; the page's rules, and the answer carries the issue's URL | yes |
141141 | `watch` | `{ app, issue, watch? }`: watch a milestone, or stop with `watch: false`: one item in your feed that follows it, and one push to your phone when it lands | yes |
142142 | `whoami_can` | `{ kind? }`: your role and that role's row of the roles table, each ability yes or no; `kind: "passkeys"` lists your own passkeys (`{ items }`: id, name, created, lastUsed), people only | |
docs/design/areas/direction.md added +9
@@ -0,0 +1,9 @@
1 # Direction
2
3 The repository's direction drawn with its two authors told apart (`src/web/studio/direction.ts`), on Settings → Crew and
4 on an open vision pull's page.
5
6 - `.direction-text` — one part of the direction as plain wrapped text (`white-space: pre-wrap`, `overflow-wrap: anywhere`
7 so a long unbroken line never scrolls a 360px page sideways). A part is badged `follows docs/VISION.md` (the fenced
8 block) or `the owner's`; the stacked "Today" and "After the merge" blocks of the pull's card are `[data-direction]`
9 hooks, each part `[data-direction-part]`, used by tests only.
src/crew/vision.ts +50 −4
@@ -12,7 +12,7 @@ import { parseVision, type Vision } from "../core/vision.ts";
1212 import { canAdmin, getRepoById, updateRepoSettings } from "../core/repos.ts";
1313 import { createLog } from "../kernel/log.ts";
1414 import { editFiles } from "../actions/contents.ts";
15 import type { Pull } from "../core/pulls.ts";
15 import { pullDir, type Pull } from "../core/pulls.ts";
1616 import type { Repo } from "../core/repos.ts";
1717 import type { User } from "../core/users.ts";
1818 import { listTree, readerFor } from "../git/read.ts";
@@ -53,8 +53,11 @@ export function withDirection(current: string | null, line: string): string {
5353 }
5454
5555 async function readVision(app: App, repo: Repo): Promise<string | null> {
56 const dir = repoDir(app.reposRoot, repo.ownerName, repo.name);
57 const tree = await listTree(dir, repo.defaultBranch, "docs");
56 return readVisionAt(repoDir(app.reposRoot, repo.ownerName, repo.name), repo.defaultBranch);
57 }
58
59 async function readVisionAt(dir: string, rev: string): Promise<string | null> {
60 const tree = await listTree(dir, rev, "docs");
5861 const entry = tree.ok ? tree.value.find((e) => e.type === "blob" && e.name === "VISION.md") : undefined;
5962 if (!entry) return null;
6063 const obj = await readerFor(dir).object(entry.oid);
@@ -135,6 +138,49 @@ export function withVisionBlock(
135138 return block === null ? { ok: false, why: "the owner's words leave no room for the block" } : { ok: true, text: `${before}${block}${after}` };
136139 }
137140
141 /** What the direction becomes when `md` is the landed VISION.md: the one function both the merge and the preview call. */
142 export function nextDirection(direction: string, md: string): ReturnType<typeof withVisionBlock> {
143 return withVisionBlock(direction, parseVision(md));
144 }
145
146 export interface DirectionPart {
147 readonly kind: "vision" | "owner";
148 readonly text: string;
149 }
150
151 /** The direction cut at its fenced block: the owner's words and the part that follows docs/VISION.md. Odd markers make it all the owner's. */
152 export function directionParts(direction: string): DirectionPart[] {
153 const from = direction.indexOf(BLOCK_START);
154 const to = direction.indexOf(BLOCK_END);
155 const paired = count(direction, BLOCK_START) === 1 && count(direction, BLOCK_END) === 1 && to > from;
156 const raw: DirectionPart[] = paired
157 ? [
158 { kind: "owner", text: direction.slice(0, from) },
159 { kind: "vision", text: direction.slice(from, to + BLOCK_END.length) },
160 { kind: "owner", text: direction.slice(to + BLOCK_END.length) },
161 ]
162 : [{ kind: "owner", text: direction }];
163 return raw.filter((p) => p.text.trim() !== "").map((p) => ({ kind: p.kind, text: p.text.trim() }));
164 }
165
166 export type VisionPreview =
167 | { readonly ok: true; readonly today: string; readonly after: string }
168 | { readonly ok: false; readonly why: string };
169
170 /**
171 * For an open vision pull: the direction as it stands and as the merge of
172 * this pull (by an admin) will write it, from the VISION.md on its head.
173 * Null for any other pull.
174 */
175 export async function previewVision(app: App, repo: Repo, pull: Pull): Promise<VisionPreview | null> {
176 if (pull.state !== "open" || pull.mergedAt !== null) return null;
177 if (!isVisionPull(app.db, `${repo.ownerName}/${repo.name}`, pull.number)) return null;
178 const md = await readVisionAt(await pullDir(app.db, app.reposRoot, repo, pull), pull.headOid);
179 if (md === null) return { ok: false, why: "this pull has no docs/VISION.md" };
180 const next = nextDirection(repo.autonomyDirection, md);
181 return next.ok ? { ok: true, today: repo.autonomyDirection, after: next.text } : { ok: false, why: next.why };
182 }
183
138184 /**
139185 * After a pull lands: when it is a vision pull (its placement says so, never
140186 * its branch name) and the merger administers the repository, regenerate the
@@ -154,7 +200,7 @@ export async function landVision(app: App, merger: User, repo: Repo, pull: Pull)
154200 const fresh = getRepoById(app.db, repo.id) ?? repo;
155201 const md = await readVision(app, fresh);
156202 if (md === null) return;
157 const next = withVisionBlock(fresh.autonomyDirection, parseVision(md));
203 const next = nextDirection(fresh.autonomyDirection, md);
158204 if (!next.ok) {
159205 log.warn("left the direction unchanged", { repo: `${repo.ownerName}/${repo.name}`, pull: pull.number, why: next.why });
160206 return;
src/http/mcp/tools/vision.ts +11 −3
@@ -1,3 +1,5 @@
1 import { getPull } from "../../../core/pulls.ts";
2 import { directionParts, previewVision } from "../../../crew/vision.ts";
13 import { canAdmin } from "../../../core/repos.ts";
24 import { NO_VISION, parseVision } from "../../../core/vision.ts";
35 import { blobForRender } from "../../../git/read.ts";
@@ -7,12 +9,12 @@ import { good, type Tool, REPO_ARG, repoArg, isAnswer } from "../shared.ts";
79
810 export const tool: Tool = {
911 name: "vision",
10 doc: "`{ repo }`: `{ purpose, order: [{ n, name, text }], rules, direction, updatedAt }` from docs/VISION.md on the default branch; without the document `purpose` is null and `order` empty; `direction` (the autonomy direction) only for those who may read the repository's settings, else null",
12 doc: "`{ repo, number? }`: `{ purpose, order: [{ n, name, text }], rules, direction, directionParts, preview, updatedAt }` from docs/VISION.md on the default branch; without the document `purpose` is null and `order` empty; `direction` (the autonomy direction) and `directionParts` (it cut into `{ kind: vision|owner, text }`: the fenced block that follows docs/VISION.md, and the owner's words) only for those who may read the repository's settings, else null; with `number` (an open vision pull) those same people get `preview`: `{ today, after }`, the direction as it stands and as an admin's merge will write it, or `{ why }` it would stay; else null",
1113 description:
1214 "VISION.md: purpose, order, rules.",
1315 inputSchema: {
1416 type: "object",
15 properties: { repo: REPO_ARG },
17 properties: { repo: REPO_ARG, number: { type: "integer", minimum: 1 } },
1618 required: ["repo"],
1719 },
1820 writes: false,
@@ -26,9 +28,15 @@ export const tool: Tool = {
2628 const last = found
2729 ? await runGit(["log", "-1", "--format=%cI", repo.defaultBranch, "--", "docs/VISION.md"], { cwd: dir })
2830 : undefined;
31 const admin = canAdmin(ctx.app.db, repo, user);
32 const n = args["number"];
33 const pull = admin && typeof n === "number" && Number.isInteger(n) && n > 0 ? getPull(ctx.app.db, repo.id, n) : undefined;
34 const shown = pull ? await previewVision(ctx.app, repo, pull) : null;
2935 return good({
3036 ...vision,
31 direction: canAdmin(ctx.app.db, repo, user) ? repo.autonomyDirection : null,
37 direction: admin ? repo.autonomyDirection : null,
38 directionParts: admin ? directionParts(repo.autonomyDirection) : null,
39 preview: shown === null ? null : shown.ok ? { today: shown.today, after: shown.after } : { why: shown.why },
3240 updatedAt: last?.ok && last.value.trim() ? last.value.trim() : null,
3341 });
3442 },
src/web/studio/change.ts +8 −1
@@ -29,7 +29,7 @@ import {
2929 type Pull,
3030 pullDir,
3131 } from "../../core/pulls.ts";
32 import { canWrite, type Repo } from "../../core/repos.ts";
32 import { canAdmin, canWrite, type Repo } from "../../core/repos.ts";
3333 import { listReviews, type Review } from "../../core/reviews.ts";
3434 import { wallOf, wallSentence, type Wall } from "../../contract/policy.ts";
3535 import { checkSummary } from "../../core/runs.ts";
@@ -84,6 +84,8 @@ import {
8484 type Decision,
8585 } from "./asks.ts";
8686 import { tellLine, tellsAnswered } from "./branch_tell.ts";
87 import { previewVision } from "../../crew/vision.ts";
88 import { visionPreviewCard } from "./direction.ts";
8789 import { holdCard } from "./hold.ts";
8890 import { pullMayRun } from "../../core/pull_trust.ts";
8991 import { changeHref } from "./nouns.ts";
@@ -532,6 +534,10 @@ async function changeConversation(
532534 : ownNotReadyNote !== undefined
533535 ? html`<p class="muted small">Not ready to ship — ${ownNotReadyNote}.</p>`
534536 : undefined;
537 const visionPreview =
538 ctx.auth.user !== undefined && canAdmin(db, repo, ctx.auth.user)
539 ? await previewVision(ctx.app, repo, pull)
540 : null;
535541 const decisionSection = when(decisionBody, (body) =>
536542 card(body, {
537543 head: html`${icon("alert")}
@@ -604,6 +610,7 @@ async function changeConversation(
604610 ${approveHereCard(ctx, repo, pull, headOid, `${thread}/approve-here`)}
605611 ${teamsCard(teamVerdicts(db, { ...pull, headOid }))}
606612 ${when(wall, (w) => wallCard(w))} ${decisionSection}
613 ${when(visionPreview, (v) => visionPreviewCard(v))}
607614 ${takeBackSection}
608615 ${when(record, (r) => lookCard(ctx, repo, r, pull, pull.headOid))}
609616 <div class="split">
src/web/studio/crew.ts +2
@@ -38,6 +38,7 @@ import { html, raw, type Html } from "../html.ts";
3838 import { icon } from "../icons.ts";
3939 import { crewReadiness } from "../pages/crew.ts";
4040 import { grantsSection } from "../pages/subscriptions.ts";
41 import { directionMarked } from "./direction.ts";
4142 import { markOf } from "../../core/marks.ts";
4243 import {
4344 btn,
@@ -305,6 +306,7 @@ function autonomyCard(ctx: Ctx, repo: Repo, base: string, admin: boolean): Html
305306 })}
306307 <div class="field">
307308 <label for="f-direction">Direction</label>
309 ${directionMarked(repo.autonomyDirection)}
308310 <textarea
309311 class="input"
310312 id="f-direction"
src/web/studio/direction.ts added +51
@@ -0,0 +1,51 @@
1 /**
2 * The direction drawn with its two authors told apart (#5835): the fenced block
3 * that follows docs/VISION.md, and the owner's own words. A vision pull shows
4 * the block as the merge will write it, beside today's.
5 */
6 import { directionParts, type DirectionPart, type VisionPreview } from "../../crew/vision.ts";
7 import { html, type Html } from "../html.ts";
8 import { icon } from "../icons.ts";
9 import { badge, card } from "../ui.ts";
10
11 const FOLLOWS = "follows docs/VISION.md";
12 const OWNER = "the owner's";
13
14 function part(p: DirectionPart): Html {
15 return html`<div data-direction-part="${p.kind}">
16 <p>${p.kind === "vision" ? badge(FOLLOWS) : badge(OWNER, { variant: "outline" })}</p>
17 <div class="direction-text">${p.text}</div>
18 </div>`;
19 }
20
21 /** The direction in parts, each marked; nothing when it is empty. */
22 export function directionMarked(direction: string): Html | null {
23 const parts = directionParts(direction);
24 if (parts.length === 0) return null;
25 return html`<div class="stack" id="direction-marked">${parts.map(part)}</div>`;
26 }
27
28 /** Today's direction beside the one the merge writes, or why it will stay. */
29 export function visionPreviewCard(preview: VisionPreview): Html {
30 const head = html`${icon("code")}<h2>Direction after the merge</h2>`;
31 if (!preview.ok)
32 return card(html`<p class="muted small">An admin's merge would leave the direction as it is: ${preview.why}.</p>`, {
33 head,
34 id: "direction-preview",
35 });
36 const same = preview.today === preview.after;
37 return card(
38 html`${same ? html`<p class="muted small">The merge changes nothing in the direction.</p>` : null}
39 <div class="stack">
40 <div data-direction="today">
41 <h3 class="small">Today</h3>
42 ${directionMarked(preview.today) ?? html`<p class="muted small">(none written)</p>`}
43 </div>
44 <div data-direction="after">
45 <h3 class="small">After the merge</h3>
46 ${directionMarked(preview.after) ?? html`<p class="muted small">(none written)</p>`}
47 </div>
48 </div>`,
49 { head, id: "direction-preview" },
50 );
51 }
src/web/style/content.ts +2
@@ -175,6 +175,8 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
175175 color: var(--muted-fg); margin: 8px 0 4px;
176176 }
177177
178 .direction-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; color: var(--muted-fg); margin: 6px 0 0; }
179
178180 .chart-box { color: var(--accent); aspect-ratio: 720 / 260; max-width: 720px; }
179181 .chart-box .chart { display: block; width: 100%; height: auto; }
180182 .chart-box text { fill: var(--muted-fg); font-family: var(--mono); }
test/mcp-vision.test.ts +34 −2
@@ -5,9 +5,12 @@ import { tmpdir } from "node:os";
55 import { join } from "node:path";
66 import { createApp, type App } from "../src/app.ts";
77 import { commitToBranch } from "../src/actions/repos.ts";
8 import { createRepo } from "../src/core/repos.ts";
8 import { openPull } from "../src/actions/pulls.ts";
9 import { savePlacement } from "../src/core/placements.ts";
10 import { nextDirection } from "../src/crew/vision.ts";
11 import { createRepo, getRepo } from "../src/core/repos.ts";
912 import { createToken } from "../src/core/tokens.ts";
10 import { createUser } from "../src/core/users.ts";
13 import { createUser, getUserByName } from "../src/core/users.ts";
1114 import { serve, type Servers } from "../src/http/server.ts";
1215 import { quietLogs } from "./helpers/globals.ts";
1316
@@ -80,3 +83,32 @@ test("a reader who may not see settings gets no direction", async () => {
8083 expect(v.direction).toBeNull();
8184 expect(v.order.length).toBe(8);
8285 });
86
87 test("number: an admin reads the direction after an open vision pull; a reader reads nothing", async () => {
88 const owner = ok(getUserByName(app.db, "ada") ? { ok: true as const, value: getUserByName(app.db, "ada")! } : { ok: false as const, error: "no ada" });

Nit: this line wraps a lookup in a hand-made Result just to unwrap it. const owner = getUserByName(app.db, "ada")!; says the same thing.

89 const repo = getRepo(app.db, "ada", "with")!;
90 const md = (await readFile(join(import.meta.dir, "..", "docs", "VISION.md"), "utf8")).replace("Done means reachable", "Done means shown");
91 ok(await commitToBranch(app, owner, repo, { branch: "vision-m", message: "v", files: [{ path: "docs/VISION.md", content: md }] }));
92 const pull = ok(await openPull(app, owner, repo, { title: "Note", body: "", baseBranch: "main", headBranch: "vision-m" }));
93 const call = async (token: string, args: object): Promise<any> => {
94 const res = await fetch(`${servers.url}/mcp`, {
95 method: "POST",
96 headers: { "content-type": "application/json", accept: "application/json, text/event-stream", authorization: `Bearer ${token}` },
97 body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "vision", arguments: args } }),
98 });
99 return JSON.parse(((await res.json()) as any).result.content[0].text);
100 };
101 expect((await call(tok["owner"]!, { repo: "ada/with", number: pull.number })).preview).toBeNull();
102 savePlacement(app.db, {
103 thoughtId: "thmcp", kind: "vision", reply: "", asked: false, decidedBy: "t", placedAt: new Date().toISOString(),
104 placed: [{ action: "proposed", repo: "ada/with", number: pull.number, pull: pull.number }],
105 });
106 const v = await call(tok["owner"]!, { repo: "ada/with", number: pull.number });
107 const expected = nextDirection("ship the feed", md);
108 if (!expected.ok) throw new Error(expected.why);
109 expect(v.preview).toEqual({ today: "ship the feed", after: expected.text });
110 expect(v.directionParts).toEqual([{ kind: "owner", text: "ship the feed" }]);
111 const r = await call(tok["reader"]!, { repo: "ada/with", number: pull.number });
112 expect(r.preview).toBeNull();
113 expect(r.directionParts).toBeNull();
114 });
test/vision-land.test.ts +73
@@ -164,3 +164,76 @@ describe("landing a vision pull", () => {
164164 expect(html).toContain("the forge kept the repository's direction as it was: only an admin&#39;s merge rewrites it");
165165 });
166166 });
167
168 describe("showing the direction (#5835)", () => {
169 let dataDir: string;
170 let app: App;
171 let servers: Servers;
172 let owner: User;
173 let repo: Repo;
174 let cookie: string;
175
176 beforeAll(async () => {
177 dataDir = await mkdtemp(join(tmpdir(), "forge-vision-show-"));
178 app = createApp({ dataDir, port: 0 });
179 const made = await signUp(app, { username: "alice", email: "[email protected]", password: "a-long-passphrase" });
180 if (!made.ok) throw new Error("sign-up failed");
181 owner = made.value;
182 promote(app.db, owner);
183 const r = await createRepo(app, owner, { owner: "alice", name: "site", visibility: "public" });
184 if (!r.ok) throw new Error(String(r.error));
185 repo = r.value;
186 expect((await commitToBranch(app, owner, repo, { message: "seed", files: [{ path: "docs/VISION.md", content: MD }] })).ok).toBe(true);
187 servers = await serve(app);
188 const signin = await fetch(`${servers.url}/signin`, {
189 method: "POST",
190 headers: { origin: servers.url },
191 body: new URLSearchParams({ username: "alice", password: "a-long-passphrase" }),
192 redirect: "manual",
193 });
194 cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!;
195 }, 30_000);
196
197 afterAll(async () => {
198 servers.stop();
199 app.close();
200 await rm(dataDir, { recursive: true, force: true });
201 });
202
203 const get = async (path: string): Promise<string> => (await fetch(`${servers.url}${path}`, { headers: { cookie } })).text();
204
205 test("settings marks the fenced block as following the vision and the rest as the owner's", async () => {
206 updateRepoSettings(app.db, getRepoById(app.db, repo.id)!, {
207 autonomyDirection: `Ship small.\n\n${BLOCK_START}\nRule: Done means reachable.\n${BLOCK_END}\n\nNever page me.`,
208 });
209 const page = await get("/alice/site/settings/crew");
210 const marked = page.slice(page.indexOf('id="direction-marked"'));
211 expect(marked).toMatch(/data-direction-part="owner"[\s\S]*Ship small\./);
212 expect(marked).toMatch(/data-direction-part="vision"[\s\S]*follows docs\/VISION\.md[\s\S]*Rule: Done means reachable\./);
213 expect(marked).toMatch(/data-direction-part="owner"[\s\S]*Never page me\./);
214 expect((marked.match(/follows docs\/VISION\.md/g) ?? []).length).toBe(1);
215 });
216
217 test("a vision pull shows today's block beside the one the merge writes, and the merge writes exactly that", async () => {
218 updateRepoSettings(app.db, getRepoById(app.db, repo.id)!, { autonomyDirection: "Mine.\n\nOld block text." });
219 const md = MD.replace("Done means reachable.", "Done means shown first.");
220 expect((await commitToBranch(app, owner, repo, { branch: "vision-x", message: "v", files: [{ path: "docs/VISION.md", content: md }] })).ok).toBe(true);
221 const pull = await openPull(app, owner, repo, { title: "Note a change of direction", body: "", baseBranch: "main", headBranch: "vision-x" });
222 if (!pull.ok) throw new Error(String(pull.error));
223 const plain = await get(`/alice/site/pulls/${pull.value.number}`);
224 expect(plain).not.toContain("direction-preview");
225 savePlacement(app.db, {
226 thoughtId: "thshow", kind: "vision", reply: "", asked: false, decidedBy: "t", placedAt: new Date().toISOString(),
227 placed: [{ action: "proposed", repo: "alice/site", number: pull.value.number, pull: pull.value.number }],
228 });
229 const page = await get(`/alice/site/pulls/${pull.value.number}`);
230 const card = page.slice(page.indexOf('id="direction-preview"'));
231 expect(card).toMatch(/data-direction="today"[\s\S]*Old block text\./);
232 expect(card).toMatch(/data-direction="after"[\s\S]*follows docs\/VISION\.md[\s\S]*Rule: Done means shown first\./);
233 const expected = withVisionBlock("Mine.\n\nOld block text.", parseVision(md));
234 if (!expected.ok) throw new Error(expected.why);
235 expect((await mergePull(app, owner, repo, pull.value, "squash")).ok).toBe(true);
236 expect(getRepoById(app.db, repo.id)!.autonomyDirection).toBe(expected.text);
237 expect(await get(`/alice/site/pulls/${pull.value.number}`)).not.toContain('id="direction-preview"');
238 });
239 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs