Public: anyone can read this; the crew can change it.
A thought that says only a repository's name opens no issue: it waits as a note and the reply asks what should change #7740
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7697-a-thought-that-says-only-a-repository-s → main 2 commits fork 2 threads · 2 unresolved
2 files
changed +36
−0
src/crew/triage.ts +11
| @@ -199,6 +199,14 @@ export interface Thought { | ||
| 199 | 199 | readonly text: string; |
| 200 | 200 | } |
| 201 | 201 | |
| 202 | const SLUG = /^(?:https?:\/\/[^\s/]+\/)?[\w.-]+\/[\w.-]+(?:\.git)?\/?$/; | |
| 203 | ||
| 204 | /** Nothing to act on: empty, or only a repository slug or a URL to one. */ | |
| 205 | export function isNote(text: string): boolean { | |
| 206 | const t = text.trim(); | |
| 207 | return t === "" || SLUG.test(t); | |
| 208 | } | |
| 209 | ||
| 202 | 210 | /** Place one thought. Always answers: a best placement, and a question when unsure. */ |
| 203 | 211 | export async function placeThought( |
| 204 | 212 | app: App, |
| @@ -208,6 +216,9 @@ export async function placeThought( | ||
| 208 | 216 | now = new Date(), |
| 209 | 217 | ): Promise<Placement> { |
| 210 | 218 | const slug = `${repo.ownerName}/${repo.name}`; |
| 219 | // A note has no kind of its own: "decision" by "rule" with an empty `placed` stands in for it. | |
| 220 | if (isNote(thought.text)) | |
| 221 | return finish(app, thought, "decision", "rule", true, [], `Kept as a note: what should change in ${slug}?`, now); | |
The |
||
| 211 | 222 | const set = pillarsOf(app.db, repo); |
| 212 | 223 | const direction = ensureDirectionIssue(app, by, repo); |
| 213 | 224 | const open = openIssues(app, repo, set, direction); |
test/triage.test.ts +25
| @@ -98,6 +98,31 @@ describe("triage", () => { | ||
| 98 | 98 | expect(placementOf(app.db, t.id)?.reply).toBe(p.reply); |
| 99 | 99 | }); |
| 100 | 100 | |
| 101 | test("a thought with nothing to act on opens nothing and is kept as a note", async () => { | |
| 102 | app.config.decider = { name: "boom", async decide() { throw new Error("asked"); } } as never; | |
| 103 | const count = () => app.db.query<{ c: number }, []>("SELECT count(*) c FROM issue").get()!.c; | |
| 104 | const before = count(); | |
| 105 | const slug = await placeThought(app, fresh(), alice, think("trevato/forge")); | |
| 106 | expect(slug.placed).toEqual([]); | |
| 107 | expect(slug.reply).toBe("Kept as a note: what should change in alice/forge?"); | |
| 108 | const blank = await placeThought(app, fresh(), alice, { id: "blank-1", text: " " }); | |
| 109 | expect(blank.placed).toEqual([]); | |
| 110 | const url = await placeThought(app, fresh(), alice, think("https://forge.open-platform.sh/trevato/forge")); | |
| 111 | expect(url.placed).toEqual([]); | |
| 112 | expect(count()).toBe(before); | |
| 113 | }); | |
| 114 | ||
| 115 | test("a short ask reaches the decider, and #N and a plain sentence still place", async () => { | |
| 116 | app.config.decider = stub({ fix: ["amendment", 0.9, `#${n}`], the: ["work", 0.9, "none", "P7"] }); | |
| 117 | const short = await placeThought(app, fresh(), alice, think("fix login")); | |
| 118 | expect(short.decidedBy).toBe("stub"); | |
| 119 | expect(short.placed.length).toBe(1); | |
| 120 | const a = await placeThought(app, fresh(), alice, think("fix #" + n)); | |
| 121 | expect(a.placed[0]!.number).toBe(n); | |
| 122 | const w = await placeThought(app, fresh(), alice, think("the home page is slow")); | |
| 123 | expect(w.placed[0]!.action).toBe("opened"); | |
| 124 | }); | |
| 125 | ||
| 101 | 126 | test("an amendment comments on the open issue it concerns", async () => { |
| 102 | 127 | app.config.decider = stub({ also: ["amendment", 0.9, `#${n}`] }); |
| 103 | 128 | const t = think("also show it on the phone"); |
new above
offline
Remove the
split(/\s+/).length < 3clause and returnfalsehere. A two-word ask is real work, and the issue names only a bare repository name. Anything unsure should go to the decider, which already asks a question when it is unsure.