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

A pure read counts the issues the crew parked in the last 24 hours and 7 days, and names the newest three #6486

Merged opened this

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

Merged

scout/forge:scout/6471-a-pure-read-counts-the-issues-the-crew-p → main 4 commits fork 3 threads · 3 unresolved

  • Merged. scout/6471-a-pure-read-counts-the-issues-the-crew-p was merged into main as 4da5975.

Review

Sign in to review this change.
6 files changed +194 −0
README.md +1
@@ -590,6 +590,7 @@ work here.
590590 A live board (any `refresh` but `off`; the form offers off or on, and `10s` and `1m` are still accepted and mean the same) redraws its grid when a run, pull, check, issue or usage event arrives, through the live-region swap
591591 (`/boards/:slug/live`, etag'd, one fetch per burst). The same doors are in the API: `GET /api/v1/boards`, `GET|PUT|DELETE /api/v1/boards/:slug`, and each panel's data as JSON or CSV at `GET /api/v1/boards/:slug/panels/:id?from=&to=&step=5m&format=csv`, bucketed in SQL to the step and never cut short (`docs/APPS.md`); each panel's card links `json` and `csv` (a plain download link) beside `explore`
592592 (an agent or anyone but an admin is 403; a panel whose query cannot run is refused with 422). The same four doors are MCP tools (`boards`, `board_write`, `board_delete`; `boards` with a `slug` reads one, and with a `panel` too that panel's data, with the same `from`, `to` and `step`) and, for the same person, a **Boards** screen in the terminal that lists boards and shows a board's panels as text, each with what its data holds now (a time panel's last bucket and step, another's row count). No dependency was added.
593 - **Parked work** — the terminal's Flow dialog says how many issues the crew parked (an open issue whose newest comment is the crew's `Parked by the crew` note) in the last 24 h and 7 d, and names the newest three; the MCP `flow` tool returns the same as `parked`. See `docs/TOUR.md`.
593594 - **Issues** — labels, threaded comments, close/reopen, markdown.
594595 - **Pull requests** — open from a branch, a **beautiful diff viewer** (syntax
595596 highlighting composed with intra-line word highlighting, mobile-first,
docs/TOUR.md +2
@@ -76,6 +76,8 @@ with the same lines, worded by the same helpers (`src/core/client-apps.ts`).
7676
7777 Under the figures, an admin's home, the terminal's dashboard and its Flow dialog carry one vitals line (#5935, `src/flow/vitals.ts`): open pulls, opened and landed in 1 h and 6 h, and conflicted, against #5924's targets; the autonomy tick line says the same as `pulls` (and `offTarget` when a target is missed). The MCP `flow` tool and `GET /api/v1/telemetry/flow` carry it as `vitals: { says, misses, … }`; the tool's description is unchanged because `tools/list` is at its size ceiling, so an agent finds `vitals` in the result, not the listing. An agent reader's counts cover public repositories only.
7878
79 Below it, one parked line (#6471, `src/flow/parked.ts`): the open issues whose newest comment is the crew's `Parked by the crew` note, counted by that note's time over the last 24 h and 7 d, with the newest three named. The terminal's Flow dialog shows it, and the MCP `flow` tool and `GET /api/v1/telemetry/flow` carry it as `parked: { says, day, week, newest }`; an agent reader's counts cover public repositories only.
80
7981 Home shows nine figures first, from `src/flow/kpis.ts`: shipped per hour (with
8082 a 24 h sparkline), the live version and since when, the median check time, the crew builders
8183 running of their slots, and the crew's spend today of the forge's daily
src/flow/parked.ts added +59
@@ -0,0 +1,59 @@
1 /**
2 * The parked count (#6471): open issues the crew tried and could not finish.
3 * An issue is parked when its newest comment starts with the park note, and
4 * it counts by that comment's time, and only the crew's note is one (`BY_CREW`): any later word, the crew's or a
5 * person's, takes it off. `newest` names the three most recently parked.
6 * `publicOnly` is `readVitals`'s: public repositories alone, and not their
7 * sealed issues (security reports, seen only by writers, the crew and the reporter).
8 */
9
10 import type { Database } from "bun:sqlite";
11 import { BY_CREW, LATER, PARK_LIKE } from "../core/done_on_main.ts";
12
13 const DAY_MS = 86_400_000;
14 const NAMED = 3;
15
16 export interface Parked {
17 /** Parked within the last 24 hours. */
18 readonly day: number;
19 /** Parked within the last 7 days (the day's included). */
20 readonly week: number;
21 readonly newest: readonly { repo: string; issue: number; title: string }[];
22 }
23
24 interface Row {
25 readonly owner: string;
26 readonly name: string;
27 readonly number: number;
28 readonly title: string;
29 readonly at: string;
30 }
31
32 export function readParked(db: Database, now = new Date(), publicOnly = false): Parked {
33 const rows = db
34 .query<Row, [number, string]>(
35 `SELECT u.username AS owner, r.name AS name, i.number AS number, i.title AS title, n.created_at AS at
36 FROM issue i
37 JOIN repo r ON r.id = i.repo_id
38 JOIN user u ON u.id = r.owner_id
39 JOIN comment n ON n.issue_id = i.id
40 WHERE i.kind = 'issue' AND i.state = 'open' AND (?1 = 0 OR (r.visibility = 'public' AND i.sealed = 0))
outdated

High: sealed issues (security reports on public repos) pass publicOnly, and their titles go out in newest to a granted agent. Add AND (?1 = 0 OR i.sealed = 0) here, plus a test with a sealed parked issue that is counted for the owner and hidden with publicOnly.

41 AND n.body LIKE '${PARK_LIKE}' AND ${BY_CREW} AND n.created_at >= ?2

Non-blocking: nothing indexes n.created_at >= ?2 or the LIKE, so on a fresh database the plan is SCAN n over every comment on each Flow read. That is acceptable for an admin read; CREATE INDEX comment_created ON comment(created_at) would bound it to the 7-day window if it ever shows up in timings.

42 AND NOT EXISTS (SELECT 1 FROM comment l WHERE l.issue_id = i.id AND ${LATER("l", "n")})
43 ORDER BY n.created_at DESC, n.id DESC`,
44 )
45 .all(publicOnly ? 1 : 0, new Date(now.getTime() - 7 * DAY_MS).toISOString());
46 const dayFrom = now.getTime() - DAY_MS;
47 return {
48 day: rows.filter((r) => Date.parse(r.at) >= dayFrom).length,
49 week: rows.length,
50 newest: rows.slice(0, NAMED).map((r) => ({ repo: `${r.owner}/${r.name}`, issue: r.number, title: r.title })),
51 };
52 }
53
54 /** The one line for the Flow dialog and the MCP `flow` tool: `3 parked in 24 h, 5 in 7 d; newest owner/repo#12 Title`. */
55 export function parkedSays(p: Parked): string {
56 if (p.week === 0) return "Nothing parked by the crew in the last 7 days.";
57 const named = p.newest.map((n) => `${n.repo}#${n.issue} ${n.title}`).join("; ");
58 return `${p.day} parked in 24 h, ${p.week} in 7 d; newest ${named}`;
59 }
src/system/telemetry.ts +4
@@ -10,6 +10,7 @@ import { SOURCES } from "../boards/sources.ts";
1010 import { conflictedPulls, conflictedSays, rebaseLane, rebaseLaneSays } from "../flow/conflicted.ts";
1111 import { firedAlarms } from "../flow/alarms.ts";
1212 import { readVitals, vitalsLine, vitalsMisses } from "../flow/vitals.ts";
13 import { parkedSays, readParked } from "../flow/parked.ts";
1314 import { readPipeline, verdict } from "../flow/pipeline.ts";
1415 import { type QueueReading, readQueues } from "../flow/queue.ts";
1516 import { reviewersBusy, reviewingSays } from "../flow/reviewing.ts";
@@ -33,8 +34,11 @@ export function flowFor(app: App, reader: Reader, now = new Date()) {
3334 const requested = requestedWaits(app.db, reader.kind === "agent");
3435 const busy = reviewersBusy(app.db, reader.kind === "agent");
3536 const vitals = readVitals(app.db, now, reader.kind === "agent");
37 const parked = readParked(app.db, now, reader.kind === "agent");
3638 return {
3739 says: verdict(stages, now.getTime()),
40 // Issues the crew parked in the last 24 h and 7 d, the newest three named (#6471).
41 parked: { says: parkedSays(parked), ...parked },
3842 // Open pulls, opened and landed in 1 h and 6 h, and conflicted, against #5924's targets (#5935).
3943 vitals: { says: vitalsLine(vitals), misses: vitalsMisses(vitals), ...vitals },
4044 // The four alarms (#5930) that are firing now; empty when none is.
src/tui/screens/flow.ts +3
@@ -18,6 +18,7 @@ import { HOLDS } from "../../crew/held.ts";
1818 import type { HeldRow } from "../../flow/held.ts";
1919 import { conflictedPulls, conflictedSays, rebaseLane, rebaseLaneSays } from "../../flow/conflicted.ts";
2020 import { readPipeline, STAGE_LABEL, verdict } from "../../flow/pipeline.ts";
21 import { parkedSays, readParked } from "../../flow/parked.ts";
2122 import { firedAlarms } from "../../flow/alarms.ts";
2223 import { readVitals, vitalsLine } from "../../flow/vitals.ts";
2324 import { readQueues, redBatchWords } from "../../flow/queue.ts";
@@ -66,6 +67,8 @@ export function flowLines(
6667 ...wrap(vitalsLine(readVitals(db, now, agent !== undefined)), inner),
6768 ...firedAlarms(db, now, { publicOnly: agent !== undefined, ceilingUsd }).flatMap((a) => wrap(a.says, inner)),
6869 "",
70 ...wrap(parkedSays(readParked(db, now, agent !== undefined)), inner),
71 "",
6972 ...wrap(throughputSays(flow), inner),
7073 "",
7174 `${pad("Out", 12)} ${pad("last hour", 9, "right")} ${pad("24 h", 5, "right")}`,
test/flow-parked.test.ts added +125
@@ -0,0 +1,125 @@
1 /** The parked count (#6471): by the park note's time, only while it is the newest word on an open issue. */
2 import { describe, expect, test } from "bun:test";
3 import { mkdtempSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp } from "../src/app.ts";
7 import { createRepo } from "../src/core/repos.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { id } from "../src/kernel/id.ts";
10 import { flowFor } from "../src/system/telemetry.ts";
11 import { flowLines } from "../src/tui/screens/flow.ts";
12 import { parkedSays, readParked } from "../src/flow/parked.ts";
13
14 const NOW = new Date();
15 const ago = (h: number) => new Date(NOW.getTime() - h * 3_600_000).toISOString();
16
17 async function world() {
18 const app = createApp({ dataDir: mkdtempSync(join(tmpdir(), "forge-parked-")), port: 0 });
19 const made = await createUser(app.db, { username: "alice", email: "[email protected]", password: "a-good-passphrase", origin: "seed", trusted: true, adminIfFirst: true });
20 if (!made.ok) throw new Error(made.error);
21 const alice = made.value;
22 const repo = async (name: string, visibility: "public" | "private") => {
23 const r = await createRepo(app.db, app.reposRoot, { ownerId: alice.id, name, visibility, description: name });
24 if (!r.ok) throw new Error(r.error);
25 // The person who set the crew going speaks for it (`BY_CREW`).
26 app.db.query("UPDATE repo SET autonomy_by = ?1 WHERE id = ?2").run(alice.id, r.value.id);
27 return r.value;
28 };
29 let n = 0;
30 const issue = (repoId: string, title: string, state = "open") => {
31 const issueId = id();
32 n++;
33 app.db
34 .query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state) VALUES (?1, ?2, ?3, 'issue', ?4, '', ?5, ?6)`)
35 .run(issueId, repoId, n, title, alice.id, state);
36 return issueId;
37 };
38 const say = (issueId: string, body: string, at: string) =>
39 app.db.query(`INSERT INTO comment (id, issue_id, author_id, body, created_at) VALUES (?1, ?2, ?3, ?4, ?5)`).run(id(), issueId, alice.id, body, at);
40 const mallory = async () => {
41 const m = await createUser(app.db, { username: "mallory", email: "[email protected]", password: "a-good-passphrase", origin: "seed", trusted: true });
42 if (!m.ok) throw new Error(m.error);
43 return m.value;
44 };
45 const park = (issueId: string, at: string) => say(issueId, "Parked by the crew: could not finish.", at);
46 return { app, alice, repo, issue, say, park, mallory };
47 }
48
49 describe("flow parked", () => {
50 test("counts by the park note's time; closed and spoken-after issues are left out", async () => {
51 const w = await world();
52 const r = await w.repo("p", "public");
53 expect(readParked(w.app.db, NOW)).toEqual({ day: 0, week: 0, newest: [] });
54 w.park(w.issue(r.id, "today"), ago(2));
55 w.park(w.issue(r.id, "three days"), ago(72));
56 w.park(w.issue(r.id, "ten days"), ago(240));
57 w.park(w.issue(r.id, "closed", "closed"), ago(1));
58 const spoken = w.issue(r.id, "spoken after");
59 w.park(spoken, ago(5));
60 w.say(spoken, "Try it smaller.", ago(3));
61 const got = readParked(w.app.db, NOW);
62 expect(got.day).toBe(1);
63 expect(got.week).toBe(2);
64 expect(got.newest.map((x) => x.title)).toEqual(["today", "three days"]);
65 expect(got.newest[0]).toEqual({ repo: "alice/p", issue: 1, title: "today" });
66 });
67
68 test("a park after a person's word counts; newest holds at most three", async () => {
69 const w = await world();
70 const r = await w.repo("p", "public");
71 const again = w.issue(r.id, "again");
72 w.park(again, ago(30));
73 w.say(again, "Try this.", ago(20));
74 w.park(again, ago(10));
75 for (const h of [1, 2, 3, 4]) w.park(w.issue(r.id, `i${h}`), ago(h));
76 const got = readParked(w.app.db, NOW);
77 expect(got.week).toBe(5);
78 expect(got.newest.map((x) => x.title)).toEqual(["i1", "i2", "i3"]);
79 });
80
81 test("publicOnly hides a private repository's issue", async () => {
82 const w = await world();
83 const pub = await w.repo("pub", "public");
84 const priv = await w.repo("priv", "private");
85 w.park(w.issue(pub.id, "open one"), ago(4));
86 w.park(w.issue(priv.id, "secret"), ago(1));
87 expect(readParked(w.app.db, NOW).week).toBe(2);
88 const only = readParked(w.app.db, NOW, true);
89 expect(only.week).toBe(1);
90 expect(only.newest.map((x) => x.title)).toEqual(["open one"]);
91 });
92
93 test("a non-crew commenter's park note is not a park", async () => {
94 const w = await world();
95 const r = await w.repo("p", "public");
96 const m = await w.mallory();
97 const issueId = w.issue(r.id, "faked");
98 w.app.db
99 .query(`INSERT INTO comment (id, issue_id, author_id, body, created_at) VALUES (?1, ?2, ?3, ?4, ?5)`)
100 .run(id(), issueId, m.id, "Parked by the crew: not really.", ago(1));
101 expect(readParked(w.app.db, NOW)).toEqual({ day: 0, week: 0, newest: [] });
102 });
103
104 test("the Flow dialog and the MCP flow reading say the same line", async () => {
105 const w = await world();
106 const r = await w.repo("p", "public");
107 expect(flowLines(w.app.db, NOW, []).join(" ")).toContain("Nothing parked by the crew");
108 w.park(w.issue(r.id, "stuck"), ago(2));
109 const says = parkedSays(readParked(w.app.db, NOW));
110 expect(says).toBe("1 parked in 24 h, 1 in 7 d; newest alice/p#1 stuck");
111 expect(flowLines(w.app.db, NOW, []).join(" ")).toContain("1 parked in 24 h, 1 in 7 d;");
112 const f = flowFor(w.app, { kind: "owner" }, NOW);
113 expect(f.parked).toMatchObject({ says, day: 1, week: 1 });
114 });
115
116 test("a sealed parked issue on a public repository is the owner's, not an agent's", async () => {
117 const w = await world();
118 const r = await w.repo("p", "public");
119 const sealed = w.issue(r.id, "security report");
120 w.app.db.query("UPDATE issue SET sealed = 1 WHERE id = ?1").run(sealed);
121 w.park(sealed, ago(1));
122 expect(readParked(w.app.db, NOW).week).toBe(1);
123 expect(readParked(w.app.db, NOW, true)).toEqual({ day: 0, week: 0, newest: [] });
124 });
125 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs