plotcoder-board 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plotcoder-board",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -29,6 +29,8 @@ import {
29
29
  formatPages,
30
30
  isBoardState,
31
31
  isMeasured,
32
+ NOTE_HEIGHT,
33
+ NOTE_WIDTH,
32
34
  DEFAULT_TARGET_EIGHTHS,
33
35
  normalizeState,
34
36
  noteEighths,
@@ -49,7 +51,7 @@ import { segmentBrief, WORKFLOWS } from "../src/board/workflows.js";
49
51
  import { DEFAULT_REMINDERS, titleFromBody } from "../src/board/reminders.js";
50
52
  import crypto from "node:crypto";
51
53
  import { describeRuns, describeSetups, readWall } from "../src/board/readWall.js";
52
- import { organizePoses } from "../src/board/organize.js";
54
+ import { GAP, ROW_WIDTH, organizePoses } from "../src/board/organize.js";
53
55
  import {
54
56
  addBoard,
55
57
  addStructure,
@@ -737,7 +739,16 @@ async function commit(command) {
737
739
  }
738
740
 
739
741
  /** Said once per session: that cards stack until organize (round seven, finding 11). */
740
- let saidStack = false;
742
+ /** Where a new card lands when the agent gives no position: after the last card in reading order, wrapping five wide, so cards never stack (round eleven, finding 14). */
743
+ function nextPlace(state) {
744
+ const order = readingOrder(state.notes);
745
+ const last = order[order.length - 1];
746
+ if (!last) return { x: 140, y: 140 };
747
+ const originX = Math.min(...state.notes.map((note) => note.x));
748
+ const x = last.x + NOTE_WIDTH + GAP;
749
+ if (x + NOTE_WIDTH > originX + ROW_WIDTH) return { x: originX, y: last.y + NOTE_HEIGHT + GAP };
750
+ return { x, y: last.y };
751
+ }
741
752
 
742
753
  /** Which door a read came through, for the head of a reply: the account as whom, the open app, or the file at which path. */
743
754
  function door(live, base = null) {
@@ -857,7 +868,7 @@ function summarize(state) {
857
868
  `notes: ${state.notes.length}, groups: ${state.groups.length}, arrows: ${state.arrows.length}, cast: ${state.characters.length}`,
858
869
  "cast:",
859
870
  cast || " (no one yet — add_character to start the roster)",
860
- "places (each distinct phrase is one place; near-matches sit side by side):",
871
+ "places (each phrase is its own place, and the app relates none of them — if two are one place, set_location them the same):",
861
872
  places || " (no card says where it happens yet)",
862
873
  "cards:",
863
874
  notes || " (no cards)",
@@ -884,15 +895,23 @@ const server = new McpServer({ name: "plotcoder-board", version: "0.1.0" });
884
895
 
885
896
  // A door's answer is a reply, not an error: a shut account door, or an
886
897
  // account with no project yet, says so in words from every tool alike.
898
+ // Tool calls run one at a time. Every tool reads the board, decides, and
899
+ // writes it back; two calls interleaving at those awaits would each read the
900
+ // same board and the second would write over the first — thirteen parallel
901
+ // create_note calls naming Nessa made thirteen Nessas in prospect (round
902
+ // eleven, finding 22). One lane, in the order the calls arrive.
887
903
  const registerTool = server.registerTool.bind(server);
904
+ let lane = Promise.resolve();
888
905
  server.registerTool = (name, config, handler) =>
889
- registerTool(name, config, async (...args) => {
890
- try {
891
- return await handler(...args);
892
- } catch (error) {
893
- if (error instanceof DoorReply) return ok(error.message);
894
- throw error;
895
- }
906
+ registerTool(name, config, (...args) => {
907
+ const turn = lane.then(() =>
908
+ handler(...args).catch((error) => {
909
+ if (error instanceof DoorReply) return ok(error.message);
910
+ throw error;
911
+ }),
912
+ );
913
+ lane = turn.catch(() => undefined);
914
+ return turn;
896
915
  });
897
916
 
898
917
  server.registerTool(
@@ -1028,6 +1047,7 @@ server.registerTool(
1028
1047
  },
1029
1048
  },
1030
1049
  async (args) => {
1050
+ const landing = args.x === undefined && args.y === undefined ? nextPlace((await readBoard()).state) : { x: args.x, y: args.y };
1031
1051
  let { result, live } = await commit({
1032
1052
  type: "create_note",
1033
1053
  headline: args.headline,
@@ -1041,8 +1061,8 @@ server.registerTool(
1041
1061
  lengthEighths: args.pages === undefined ? undefined : toEighths(args.pages),
1042
1062
  plants: args.plants,
1043
1063
  location: args.location,
1044
- x: args.x,
1045
- y: args.y,
1064
+ x: landing.x,
1065
+ y: landing.y,
1046
1066
  });
1047
1067
  let castLine = "";
1048
1068
  if (args.characters && args.characters.length && result?.id) {
@@ -1074,8 +1094,7 @@ server.registerTool(
1074
1094
  result?.plants ? "corner folded" : null,
1075
1095
  result?.location ? `at ${result.location}` : "no place yet (location here, or set_location)",
1076
1096
  ].filter(Boolean).join(", ");
1077
- const placed = args.x === undefined && args.y === undefined && !saidStack ? " Cards stack until organize lays them out along the arrows." : "";
1078
- if (placed) saidStack = true;
1097
+ const placed = args.x === undefined && args.y === undefined ? ` Placed after the last card in reading order (${Math.round(landing.x)},${Math.round(landing.y)}); organize lays the wall out along the arrows.` : "";
1079
1098
  return ok(`Created card ${result?.id ?? ""}: ${landed}${where(live)}.${castLine}${placed}`, result);
1080
1099
  },
1081
1100
  );
@@ -1176,6 +1195,7 @@ server.registerTool(
1176
1195
  const lines = [
1177
1196
  `PlotCoder wall (${door(live, base)})`,
1178
1197
  `logline: ${state.logline ? `"${state.logline}"` : "(none yet)"}`,
1198
+ "the cast and the places are list_board's, not the reading's",
1179
1199
  state.targetEighths === DEFAULT_TARGET_EIGHTHS
1180
1200
  ? `runtime: about ${formatPages(boardEighths(state))} pages; no target set (set_target)`
1181
1201
  : `runtime: about ${formatPages(boardEighths(state))} pages of a ${formatPages(state.targetEighths)}-page target — ${boardEighths(state) > state.targetEighths ? `${formatPages(boardEighths(state) - state.targetEighths)} over` : boardEighths(state) < state.targetEighths ? `${formatPages(state.targetEighths - boardEighths(state))} under` : "on it"}`,
@@ -1206,7 +1226,13 @@ server.registerTool(
1206
1226
  ...(reading.findings.length
1207
1227
  ? reading.findings.map((finding) => ` - [${finding.kind}] ${finding.text}${finding.ids.length ? ` (ids: ${finding.ids.join(", ")})` : ""}`)
1208
1228
  : [" (none that this reading can see)"]),
1209
- `checks: ${CHECKS.length} run — asking about ${[...new Set(reading.findings.map((finding) => finding.kind))].filter((kind) => CHECKS.includes(kind)).join(", ") || "nothing"}; checked and clean: ${CHECKS.filter((kind) => !reading.findings.some((finding) => finding.kind === kind)).map((kind) => CHECK_WORDS[kind]).join("; ") || "(nothing — every check found something)"}`,
1229
+ `checks: ${CHECKS.length} run — ${(() => {
1230
+ const asked = reading.findings.filter((finding) => CHECKS.includes(finding.kind));
1231
+ if (asked.length === 0) return "asking nothing";
1232
+ const counts = new Map();
1233
+ for (const finding of asked) counts.set(finding.kind, (counts.get(finding.kind) ?? 0) + 1);
1234
+ return `asking ${asked.length} question${asked.length === 1 ? "" : "s"} of ${counts.size} kind${counts.size === 1 ? "" : "s"}: ${[...counts.entries()].map(([kind, n]) => (n > 1 ? `${kind} ×${n}` : kind)).join(", ")}`;
1235
+ })()}; checked and clean: ${CHECKS.filter((kind) => !reading.findings.some((finding) => finding.kind === kind)).map((kind) => CHECK_WORDS[kind]).join("; ") || "(nothing — every check found something)"}`,
1210
1236
  ];
1211
1237
  if (isSampleWall(state)) lines.unshift(SAMPLE_NOTE);
1212
1238
  return ok(lines.join("\n"), { ...reading, sample: isSampleWall(state) });
@@ -1315,7 +1341,7 @@ server.registerTool(
1315
1341
  else if (note && !wrappedUnder.some((item) => item.beat === currentBeat)) wrappedUnder.push({ beat: currentBeat, first: note });
1316
1342
  }
1317
1343
  const shape = beats
1318
- ? `${opening ? `an opening row of ${opening} card(s) before the first beat, then ` : ""}${beats} row(s), one per beat${wrappedUnder.length ? `; ${wrappedUnder.map((item) => `the row of "${item.beat.headline}" wraps under from "${item.first.headline}"`).join(", ")}` : ""}`
1344
+ ? `${opening ? `an opening row of ${opening} card(s) before the first beat, then ` : ""}${beats} row(s), one per beat${wrappedUnder.length ? `; ${wrappedUnder.map((item) => `the row of "${item.beat.headline}" wraps under from "${item.first.headline}", indented under the row's first scene, never under the beat`).join(", ")}` : ""}`
1319
1345
  : `${rows} row(s) five cards wide — no beats yet, so nothing sets the rows; set_rank the turns and organize again for a row per beat`;
1320
1346
  return ok(`Organized ${poses.length} card(s) along the arrows into ${shape}${where(live)}.`, poses);
1321
1347
  },
@@ -2021,7 +2047,7 @@ server.registerTool(
2021
2047
  if (!result) return ok(`No character with id ${person.id}. Call list_board for the cast.`);
2022
2048
  return ok(`Nothing changed on ${result.name}'s page: those lines already read that way.`, result);
2023
2049
  }
2024
- const trim = (text) => (text.length > 140 ? `${text.slice(0, 137)}…` : text);
2050
+ const trim = (text) => (text.length > 400 ? `${text.slice(0, 140)} (${text.length} characters in all, every one landed)` : text);
2025
2051
  const lines = Object.keys(patch).map((field) => {
2026
2052
  const had = (person[field] ?? "").trim();
2027
2053
  const now = (result[field] ?? "").trim();
@@ -2528,7 +2554,8 @@ server.registerTool(
2528
2554
  workingProject(record.id, record.name, (account.projectCount ?? 0) + 1);
2529
2555
  joinPresence(record.id);
2530
2556
  const targetLine = target === undefined ? ` Its target is ${formatPages(state.targetEighths)} pages, the default for a feature; set_target for a pilot or a half-hour, or pass pages or minutes here.` : ` Its target is ${formatPages(state.targetEighths)} pages.`;
2531
- return ok(`Started "${record.name}" (${record.id}) and working it now, as ${account.email}.${targetLine}${oneCallHint(record)}`, { id: record.id, name: record.name, targetEighths: state.targetEighths });
2557
+ const first = record.boards[0];
2558
+ return ok(`Started "${record.name}" (${record.id}) with its first board "${first.name}" (${first.id}), and working it now, as ${account.email}.${targetLine}${oneCallHint(record)}`, { id: record.id, name: record.name, boardId: first.id, boardName: first.name, targetEighths: state.targetEighths });
2532
2559
  },
2533
2560
  );
2534
2561
 
@@ -51,7 +51,7 @@ export const AGENTS = {
51
51
  ],
52
52
  rules: [
53
53
  "Questions, not fixes, until the writer says.",
54
- "No opinions about how many beats there should be.",
54
+ "No opinions about how many beats there should be. Marking the turns a treatment plainly makes is reading it, not an opinion: mark them, say which, and let the writer strike or add.",
55
55
  "Page counts are estimates.",
56
56
  "Ask before delete_board, delete_project, empty_account, delete_account, unlock_numbers, remove_file, an import_project that replaces, or claim_account — the writer gives the email and the password; never invent one. export_project first, when something might be wanted back.",
57
57
  "Do not invent people or a logline. What the treatment states — an age, a job, a bad knee — is not invented: it goes in the person's notes. An unnamed person is named by their role — Dana's mother, the dispatcher — which is a name until the writer gives one. A scene is one place and one stretch of time; a new place or time is a new card. A beat is a whole card; a setup arrow lands on the scene's card, so a payoff never needs a card of its own. Acts are groups titled Act one, Act two, when the treatment has them; the wall never asks whether an act is a sequence. Paper colour means nothing to the app. Under target is a fact to report plainly, like over; neither is a verdict.",