Public: anyone can read this; the crew can change it.
New app promises a first tell from your sentence and drops it when you pick nobody #316
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/282-new-app-promises-a-first-tell-from-your → main 1 commit fork
4 files
changed +88
−16
src/actions/apps.ts +11 −6
| @@ -331,12 +331,17 @@ export async function newApp( | ||
| 331 | 331 | actorId: user.value.id, |
| 332 | 332 | payload: { agent: chosen.user.username }, |
| 333 | 333 | }); |
| 334 | if (body !== "") { | |
| 335 | const opened = openIssue(app, user.value, repo.value, { | |
| 336 | title: `Build ${input.name}`, | |
| 337 | body, | |
| 338 | }); | |
| 339 | if (opened.ok) { | |
| 334 | } | |
| 335 | // The sentence is the one thing the person wrote: it opens the first | |
| 336 | // issue whether or not anyone was picked for it, unassigned when not. | |
| 337 | if (body !== "") { | |
| 338 | const opened = openIssue(app, user.value, repo.value, { | |
| 339 | title: `Build ${input.name}`, | |
| 340 | body, | |
| 341 | }); | |
| 342 | if (opened.ok) { | |
| 343 | issue = opened.value; | |
| 344 | if (chosen) { | |
| 340 | 345 | const assigned = assignIssue( |
| 341 | 346 | app, |
| 342 | 347 | user.value, |
src/tui/screens/apps.ts +3 −1
| @@ -287,7 +287,9 @@ export function newAppFlow(f: Session, ctx: Ctx, parent: Screen): void { | ||
| 287 | 287 | const who = made.value.plan |
| 288 | 288 | ? ` · ${made.value.plan.agent} was picked — ${made.value.plan.summary}` |
| 289 | 289 | : input.agent === undefined |
| 290 | ? "" | |
| 290 | ? made.value.issue === undefined | |
| 291 | ? "" | |
| 292 | : ` · #${made.value.issue.number} is waiting for someone` | |
| 291 | 293 | : made.value.issue === undefined |
| 292 | 294 | ? ` · ${input.agent} is on the crew` |
| 293 | 295 | : ` · ${input.agent} is on #${made.value.issue.number}`; |
src/web/pages/apps.ts +6 −4
| @@ -525,11 +525,13 @@ export async function newAppSubmit(ctx: Ctx): Promise<Response> { | ||
| 525 | 525 | ? `${repo.name} is an app, but nothing is serving apps on this forge.` |
| 526 | 526 | : `${repo.name} is starting at ${url}.`; |
| 527 | 527 | const second = |
| 528 | issue === undefined || issue.assigneeName === null | |
| 528 | issue === undefined | |
| 529 | 529 | ? "" |
| 530 | : plan | |
| 531 | ? ` ${plan.agent} was picked for #${issue.number} — ${plan.summary}` | |
| 532 | : ` #${issue.number} is assigned to ${issue.assigneeName}.`; | |
| 530 | : issue.assigneeName === null | |
| 531 | ? ` #${issue.number} is waiting for someone.` | |
| 532 | : plan | |
| 533 | ? ` ${plan.agent} was picked for #${issue.number} — ${plan.summary}` | |
| 534 | : ` #${issue.number} is assigned to ${issue.assigneeName}.`; | |
| 533 | 535 | // The studio's app page (repoHome, once the record exists) is where this |
| 534 | 536 | // moment belongs; dev mode keeps the controls' page. |
| 535 | 537 | const target = ctx.mode === "studio" ? repoBase(repo) : `${repoBase(repo)}/app`; |
test/apps.test.ts +68 −5
| @@ -1351,7 +1351,7 @@ printf '%s\\n' '{"is_error":false,"result":"{\\"summary\\":\\"Build a notes app\ | ||
| 1351 | 1351 | expect(made.value.issue?.body).toContain("- [ ] Add a form"); |
| 1352 | 1352 | }); |
| 1353 | 1353 | |
| 1354 | test("a plan naming an agent not on the forge is refused; nothing changes", async () => { | |
| 1354 | test("a plan naming an agent not on the forge is refused; the sentence still opens unassigned", async () => { | |
| 1355 | 1355 | const made = await newApp(app, alice, { |
| 1356 | 1356 | name: "notes-b", |
| 1357 | 1357 | description: "BAD-AGENT: pick someone who does not exist", |
| @@ -1360,12 +1360,15 @@ printf '%s\\n' '{"is_error":false,"result":"{\\"summary\\":\\"Build a notes app\ | ||
| 1360 | 1360 | expect(made.ok).toBe(true); |
| 1361 | 1361 | if (!made.ok) return; |
| 1362 | 1362 | expect(made.value.plan).toBeUndefined(); |
| 1363 | expect(made.value.issue).toBeUndefined(); | |
| 1363 | expect(made.value.issue?.assigneeName).toBeNull(); | |
| 1364 | expect(made.value.issue?.body).toBe( | |
| 1365 | "BAD-AGENT: pick someone who does not exist", | |
| 1366 | ); | |
| 1364 | 1367 | const crew = listCrew(app, alice, made.value.repo); |
| 1365 | 1368 | expect(crew.ok && crew.value.length).toBe(0); |
| 1366 | 1369 | }); |
| 1367 | 1370 | |
| 1368 | test("a plan that times out changes nothing", async () => { | |
| 1371 | test("a plan that times out still opens the sentence, unassigned", async () => { | |
| 1369 | 1372 | const made = await newApp(app, alice, { |
| 1370 | 1373 | name: "notes-c", |
| 1371 | 1374 | description: "HANG-ME: never answer", |
| @@ -1374,7 +1377,7 @@ printf '%s\\n' '{"is_error":false,"result":"{\\"summary\\":\\"Build a notes app\ | ||
| 1374 | 1377 | expect(made.ok).toBe(true); |
| 1375 | 1378 | if (!made.ok) return; |
| 1376 | 1379 | expect(made.value.plan).toBeUndefined(); |
| 1377 | expect(made.value.issue).toBeUndefined(); | |
| 1380 | expect(made.value.issue?.assigneeName).toBeNull(); | |
| 1378 | 1381 | const crew = listCrew(app, alice, made.value.repo); |
| 1379 | 1382 | expect(crew.ok && crew.value.length).toBe(0); |
| 1380 | 1383 | }, 10_000); |
| @@ -1390,7 +1393,7 @@ printf '%s\\n' '{"is_error":false,"result":"{\\"summary\\":\\"Build a notes app\ | ||
| 1390 | 1393 | expect(made.ok).toBe(true); |
| 1391 | 1394 | if (!made.ok) return; |
| 1392 | 1395 | expect(made.value.plan).toBeUndefined(); |
| 1393 | expect(made.value.issue).toBeUndefined(); | |
| 1396 | expect(made.value.issue?.assigneeName).toBeNull(); | |
| 1394 | 1397 | }); |
| 1395 | 1398 | |
| 1396 | 1399 | test("no claude on PATH behaves exactly as without pickAgent", async () => { |
| @@ -1405,10 +1408,70 @@ printf '%s\\n' '{"is_error":false,"result":"{\\"summary\\":\\"Build a notes app\ | ||
| 1405 | 1408 | expect(made.ok).toBe(true); |
| 1406 | 1409 | if (!made.ok) return; |
| 1407 | 1410 | expect(made.value.plan).toBeUndefined(); |
| 1411 | expect(made.value.issue?.assigneeName).toBeNull(); | |
| 1412 | }); | |
| 1413 | ||
| 1414 | test("agent: \"\" — nobody picked — still opens the sentence, unassigned", async () => { | |
| 1415 | const made = await newApp(app, alice, { | |
| 1416 | name: "notes-f", | |
| 1417 | description: "Keep a list of notes", | |
| 1418 | agent: "", | |
| 1419 | }); | |
| 1420 | expect(made.ok).toBe(true); | |
| 1421 | if (!made.ok) return; | |
| 1422 | expect(made.value.issue?.title).toBe("Build notes-f"); | |
| 1423 | expect(made.value.issue?.body).toBe("Keep a list of notes"); | |
| 1424 | expect(made.value.issue?.assigneeName).toBeNull(); | |
| 1425 | expect(made.value.issue?.state).toBe("open"); | |
| 1426 | const crew = listCrew(app, alice, made.value.repo); | |
| 1427 | expect(crew.ok && crew.value.length).toBe(0); | |
| 1428 | }); | |
| 1429 | ||
| 1430 | test("an empty sentence still opens nothing", async () => { | |
| 1431 | const made = await newApp(app, alice, { name: "notes-g" }); | |
| 1432 | expect(made.ok).toBe(true); | |
| 1433 | if (!made.ok) return; | |
| 1408 | 1434 | expect(made.value.issue).toBeUndefined(); |
| 1409 | 1435 | }); |
| 1410 | 1436 | }); |
| 1411 | 1437 | |
| 1438 | describe("New app on a forge with no agents at all: the sentence still opens", () => { | |
| 1439 | let dataDir: string; | |
| 1440 | let app: App; | |
| 1441 | let alice: User; | |
| 1442 | ||
| 1443 | beforeAll(async () => { | |
| 1444 | dataDir = await mkdtemp(join(tmpdir(), "forge-noagents-")); | |
| 1445 | app = createApp({ dataDir, port: 0 }); | |
| 1446 | const created = await createUser(app.db, { | |
| 1447 | username: "alice", | |
| 1448 | email: "[email protected]", | |
| 1449 | password: "a-good-passphrase", | |
| 1450 | adminIfFirst: true, | |
| 1451 | }); | |
| 1452 | if (!created.ok) throw new Error(created.error); | |
| 1453 | alice = created.value; | |
| 1454 | }); | |
| 1455 | ||
| 1456 | afterAll(async () => { | |
| 1457 | app.close(); | |
| 1458 | await rm(dataDir, { recursive: true, force: true }); | |
| 1459 | }); | |
| 1460 | ||
| 1461 | test("the sentence opens the first issue, unassigned", async () => { | |
| 1462 | const made = await newApp(app, alice, { | |
| 1463 | name: "solo", | |
| 1464 | description: "Keep a list of notes", | |
| 1465 | }); | |
| 1466 | expect(made.ok).toBe(true); | |
| 1467 | if (!made.ok) return; | |
| 1468 | expect(made.value.issue?.title).toBe("Build solo"); | |
| 1469 | expect(made.value.issue?.body).toBe("Keep a list of notes"); | |
| 1470 | expect(made.value.issue?.assigneeName).toBeNull(); | |
| 1471 | expect(made.value.issue?.state).toBe("open"); | |
| 1472 | }); | |
| 1473 | }); | |
| 1474 | ||
| 1412 | 1475 | describe("New app: the form asks for the sentence, and names it (issue #60)", () => { |
| 1413 | 1476 | let dataDir: string; |
| 1414 | 1477 | let app: App; |
new above
offline