plotcoder-board 0.1.36 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,7 +53,7 @@ An agent should call the tools, never fake mouse drags. The skill in `.cursor/sk
53
53
  A **blind run** is a fresh agent given the on-ramp and a treatment and nothing
54
54
  else, asked to build a wall and to keep a log of everything that made the job
55
55
  harder than it should have been. The friction log is the product; the wall is
56
- just what produces it. Twenty rounds have been run, the first three through a
56
+ just what produces it. Twenty-one rounds have been run, the first three through a
57
57
  repo checkout and the rest through the account door; every finding from the
58
58
  first seventeen is fixed or decided. Round seventeen, the first to start from
59
59
  a page of notes instead of a treatment, found the wall has no way to hold a
@@ -107,4 +107,4 @@ next round into a test of `claim_account` instead of the door it meant to test.
107
107
 
108
108
  ## Status
109
109
 
110
- Version 0.1.36. A project of boards; sign in with your email and a password from the PlotCoder mark and your projects follow you to every device, share one with another writer by email and write it together live, or stay signed out and work on this device as before. Pages sit beside the wall: a scene's text lives on its card, measures it, paginates to the industry's rules, prints, goes out and comes in as Fountain or Final Draft, and goes out as Markdown or plain text for a collaborator in Google Docs. It installs as a progressive web app and opens offline; plotcoder.com serves over HTTPS. The wall, beats, card length, groups, arrows, pan and zoom, save and open, and the agent surface are in use.
110
+ Version 0.1.37. A project of boards; sign in with your email and a password from the PlotCoder mark and your projects follow you to every device, share one with another writer by email and write it together live, or stay signed out and work on this device as before. Pages sit beside the wall: a scene's text lives on its card, measures it, paginates to the industry's rules, prints, goes out and comes in as Fountain or Final Draft, and goes out as Markdown or plain text for a collaborator in Google Docs. It installs as a progressive web app and opens offline; plotcoder.com serves over HTTPS. The wall, beats, card length, groups, arrows, pan and zoom, save and open, and the agent surface are in use.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plotcoder-board",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -982,7 +982,7 @@ const CHECK_WORDS = {
982
982
  sag: "no run out of proportion",
983
983
  empty: "no beats back to back",
984
984
  unwritten: "no card without a headline or change line",
985
- unlinked: "no card without an arrow",
985
+ unlinked: "no card without a follows arrow (a setup arrow is a claim, not a place in the story)",
986
986
  duplicate: "no two headlines alike",
987
987
  sequence: "no group too long for one sequence (act groups are not asked)",
988
988
  uncast: "nobody in the cast on no card of the project",
@@ -1345,11 +1345,13 @@ server.registerTool(
1345
1345
  rank: rankSchema.optional(),
1346
1346
  pages: pagesSchema.optional(),
1347
1347
  plants: z.boolean().optional(),
1348
- plantsWhat: z.string().optional(),
1348
+ plantsWhat: z.string().optional().describe("What the folded corner plants, in the writer's words; naming it folds the card."),
1349
1349
  after: z.string().optional().describe("Wire the new scene into the story after this card (id or headline): one call, one number under a lock. On a wall with no follows arrows yet this draws the first, so a wall can be built in order from its second card."),
1350
1350
  before: z.string().optional().describe("Or before this card (id or headline)."),
1351
1351
  location: z.string().optional(),
1352
1352
  when: z.string().optional().describe('When the scene happens, as the writer says it — "night", "day four, dawn" — printed after the place on the scene heading.'),
1353
+ locationOpen: z.string().optional().describe("The writer's words for why the place is not decided (R61's edge): the card is born with its place open, listed and not asked where, while its other questions stand."),
1354
+ whenOpen: z.string().optional().describe("The writer's words for why the when is not decided: the card is born with its when open, listed and not asked."),
1353
1355
  open: z.string().optional().describe("The writer's words for what is not decided about this card — \"whether Tom knows\" — so the card is born open: the reading lists it and asks nothing else of it until the words are cleared."),
1354
1356
  characters: z.array(z.string().min(1)).optional(),
1355
1357
  x: z.number().optional(),
@@ -1389,6 +1391,8 @@ server.registerTool(
1389
1391
  plants: args.plants,
1390
1392
  plantsWhat: args.plantsWhat,
1391
1393
  location: args.location,
1394
+ locationOpen: args.locationOpen,
1395
+ whenOpen: args.whenOpen,
1392
1396
  when: args.when,
1393
1397
  open: args.open,
1394
1398
  x: landing.x,
@@ -1433,8 +1437,8 @@ server.registerTool(
1433
1437
  result?.lengthEighths === null ? "about a page (unsized: the writer's guess until set_length)" : `${formatPages(noteEighths(result))} ${formatPages(noteEighths(result)) === "1" ? "page" : "pages"}`,
1434
1438
  result?.color ? `${result.color} paper${args.color ? "" : once("paper", " (pass color to choose)")}` : null,
1435
1439
  result?.plants ? `corner folded${result.plantsWhat ? ` — plants ${result.plantsWhat}` : ""}` : null,
1436
- result?.location ? `at ${result.location}` : `no place yet${once("place", " (location here, or set_location)")}`,
1437
- result?.when ? `when: ${result.when}` : null,
1440
+ result?.location ? `at ${result.location}` : result?.locationOpen ? `place open: "${result.locationOpen}" (listed, not asked where)` : `no place yet${once("place", " (location here, or set_location)")}`,
1441
+ result?.when ? `when: ${result.when}` : result?.whenOpen ? `when open: "${result.whenOpen}" (listed, not asked)` : null,
1438
1442
  result?.open ? `open: "${result.open}" (listed, not asked about)` : null,
1439
1443
  ].filter(Boolean).join(", ");
1440
1444
  // Where it landed matters only until the tidy, so the reply says the rule once and never the coordinates (round fourteen, entry 11).
@@ -1669,7 +1673,10 @@ server.registerTool(
1669
1673
  if (kind === "sequence" && state.groups.length === 0) return "no group too long for one sequence (not asked: no groups)";
1670
1674
  // A kind clean only because an open card is not asked says so (round eighteen, entry 18).
1671
1675
  const hiddenBy = reading.open.filter((item) => item.hides.includes(kind)).length;
1672
- if (hiddenBy) return `${CHECK_WORDS[kind]} (except ${hiddenBy} open card${hiddenBy === 1 ? "" : "s"}, not asked)`;
1676
+ // A place left open is not an open card (round twenty-one, entry 26): say which.
1677
+ const placesOpen = kind === "unplaced" ? reading.openFields.filter((field) => field.field === "location").length : 0;
1678
+ const except = [hiddenBy ? `${hiddenBy} open card${hiddenBy === 1 ? "" : "s"}` : "", placesOpen ? `${placesOpen} with ${placesOpen === 1 ? "its" : "their"} place open` : ""].filter(Boolean).join(" and ");
1679
+ if (except) return `${CHECK_WORDS[kind]} (except ${except}, not asked)`;
1673
1680
  return CHECK_WORDS[kind];
1674
1681
  }).join("; ") || "(nothing — every check found something)"}`,
1675
1682
  ];
@@ -2286,7 +2293,7 @@ server.registerTool(
2286
2293
  if (/^\.(?!\.)/.test(line) && index < ids.length) {
2287
2294
  const note = state.notes.find((item) => item.id === ids[index]);
2288
2295
  index += 1;
2289
- const standIn = note && !(note.location ?? "").trim() ? `${note.open ? " · open card" : ""} · no place: the headline stands in for the heading${note.open ? ", not a place" : ""}` : "";
2296
+ const standIn = note && !(note.location ?? "").trim() ? ((note.locationOpen ?? "").trim() ? ` · place open: the writer's words head the scene, marked, not a place` : `${note.open ? " · open card" : ""} · no place: the headline stands in for the heading${note.open ? ", not a place" : ""}`) : "";
2290
2297
  const numbered = note && pageNumbers?.get(note.id) ? ` · locked no. ${pageNumbers.get(note.id)}` : "";
2291
2298
  const mark = note ? marks.get(note.id) : null;
2292
2299
  const sourceLines = (note?.text ?? "").split("\n");
@@ -45,7 +45,7 @@ export const AGENTS = {
45
45
  firstNote: "Make these five before anything else; none depends on another, so any order is fine. list_words and list_workflows are the app's and read no project, and their first line says so; every other reading opens with the door it came through — the project it read and how many the account holds — and a write's tail says where it landed; list_projects lists the projects. read_wall, list_reminders and list_board are about the wall you will work, so after open_project or open_board make those three again, and after new_project read the wall once it holds cards. An emptied account has nothing to read: go straight to new_project: a name is enough; pages sets the target and board names the first board when the writer has them, and set_target and rename_board set them later. On an account with no project yet, read_wall has nothing to read and says so, and list_reminders gives the house principles every project starts with. No server in front of you, and no shell to take the shell door? Nothing gets you in from inside the session: say so, and ask the person to wire the server and start a new session.",
46
46
  first: [
47
47
  { tool: "list_words", why: "the room's words, the app's meaning." },
48
- { tool: "read_wall", why: "the reading: the beats, the runs, the setups, and what the wall asks. The records — every card, the cast, the places, the rows — are list_board's. A fresh folder holds a sample wall (Maya, Tom, the letter) and the reading says so only when it is the sample; it is not the writer's." },
48
+ { tool: "read_wall", why: "the reading: the beats, the runs, the setups, and what the wall asks. The records — every card, the cast, the places, the rows — are list_board's. A fresh folder holds a sample wall (Maya, Tom, the letter) and the reading says so only when it is the sample; it is not the writer's. An account can hold a wall from earlier work that is neither the sample nor what the writer means to work now: every reading names the project it read and list_projects names who is on it; ask the writer before touching it, and new_project starts theirs beside it." },
49
49
  { tool: "list_workflows", why: "what a writer can ask you for." },
50
50
  { tool: "list_reminders", why: "the house principles the app starts with, and the writer's own; read them before you change anything. Reminders live on the project and go with it." },
51
51
  { tool: "list_board", why: "the records — every card, the cast, the places, the rows — with their ids, which the reading does not carry and every write needs." },
@@ -24,10 +24,16 @@ function upper(text) {
24
24
  * A forced scene heading: the place, or the headline when the card has none,
25
25
  * and the card's when after a dash — THE PIER AT FENIT - NIGHT (R55).
26
26
  */
27
+ /** The heading's mark for a place the writer has left open (R61's edge): the words print, and never read back as a place. */
28
+ export const OPEN_PLACE_HEADING = "PLACE NOT DECIDED:";
29
+
27
30
  export function sceneHeading(note) {
28
31
  const place = typeof note.location === "string" ? note.location.trim() : "";
32
+ const placeOpen = typeof note.locationOpen === "string" ? note.locationOpen.trim() : "";
29
33
  const when = typeof note.when === "string" ? note.when.trim() : "";
30
- const words = place || note.headline || "UNTITLED";
34
+ // No place but the writer's words for why: the words head the scene, marked,
35
+ // so a reader never takes them for a place and the headline is not a slugline.
36
+ const words = place || (placeOpen ? `${OPEN_PLACE_HEADING} ${placeOpen}` : "") || note.headline || "UNTITLED";
31
37
  return `.${upper(words)}${when ? ` - ${upper(when)}` : ""}`;
32
38
  }
33
39
 
@@ -266,6 +272,8 @@ export function mergeFountain(state, parsed) {
266
272
  const headline = scene.synopsis || titleCase(scene.heading);
267
273
  const isPlace = scene.forced && Boolean(scene.synopsis);
268
274
  const parts = splitHeading(scene.heading);
275
+ // A heading that says the place is not decided comes back as an open place, not a place named that.
276
+ const openPlace = parts.place.toUpperCase().startsWith(OPEN_PLACE_HEADING) ? parts.place.slice(OPEN_PLACE_HEADING.length).trim().toLowerCase() : "";
269
277
  const id = `scene-${Math.random().toString(36).slice(2, 8)}`;
270
278
  // A marked body is an unwritten scene: its words are the change line, not a page.
271
279
  const body = unmark(scene.text);
@@ -274,7 +282,8 @@ export function mergeFountain(state, parsed) {
274
282
  id,
275
283
  headline,
276
284
  change: body.marked ? body.text || "What changes?" : scene.text ? firstSentence(scene.text) : "What changes?",
277
- location: isPlace ? titleCase(parts.place) : "",
285
+ location: isPlace && !openPlace ? titleCase(parts.place) : "",
286
+ locationOpen: isPlace && openPlace ? openPlace : "",
278
287
  when: isPlace ? parts.when.toLowerCase() : "",
279
288
  text: body.marked ? "" : scene.text,
280
289
  x: anchor ? anchor.x + 40 : 140,