@llblab/pi-kit 0.5.2 → 0.7.0

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.
Files changed (141) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -5
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
  35. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  36. package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
  37. package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
  38. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  39. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  40. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  41. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  42. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  43. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  44. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  45. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  46. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  47. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  50. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  51. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  55. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  56. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  59. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  63. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  64. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  65. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  66. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  68. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  69. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  70. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  71. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  72. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  73. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  74. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  75. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  76. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  77. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  78. package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -4
  79. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +22 -5
  80. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  81. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  82. package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
  83. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  84. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  85. package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -7
  86. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -5
  87. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  88. package/node_modules/@llblab/pi-telegram/index.ts +4 -1415
  89. package/node_modules/@llblab/pi-telegram/lib/activity.ts +19 -5
  90. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  91. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +37 -2
  92. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  93. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  94. package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
  95. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
  96. package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
  97. package/node_modules/@llblab/pi-telegram/lib/config.ts +178 -25
  98. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
  99. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
  100. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  101. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  102. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -1
  103. package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
  104. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +154 -15
  105. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +74 -40
  106. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +28 -42
  107. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +18 -14
  108. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  109. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  110. package/node_modules/@llblab/pi-telegram/lib/preview.ts +115 -70
  111. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
  112. package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -22
  113. package/node_modules/@llblab/pi-telegram/lib/replies.ts +47 -39
  114. package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
  115. package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
  116. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  117. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +315 -7
  118. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  119. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  120. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  121. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  122. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  123. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  124. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  125. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  126. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  127. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  128. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  129. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  130. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  131. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  132. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  133. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
  134. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
  135. package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
  136. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
  137. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
  138. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
  139. package/node_modules/@llblab/skills/package.json +2 -3
  140. package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
  141. package/package.json +7 -6
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Workspace slot allocation policy
3
+ * Zones: telegram, workspace identity
4
+ * Owns bounded profile-wide letter selection and inactivity ordering.
5
+ * Excludes liveness discovery, persistence, routing, and Telegram deletion;
6
+ * a selection is a proposal, never authority to retire a binding.
7
+ */
8
+
9
+ export const TELEGRAM_WORKSPACE_SLOTS = "abcdefghijklmnopqrstuvwxyz";
10
+
11
+ export interface TelegramWorkspaceSlotOccupancy {
12
+ bindingKey: string;
13
+ slot: string;
14
+ inactiveSinceMs?: number;
15
+ protection: "eligible" | "protected" | "unknown";
16
+ }
17
+
18
+ export type TelegramWorkspaceSlotAllocation =
19
+ | { kind: "free"; slot: string }
20
+ | { kind: "reclaim"; candidate: TelegramWorkspaceSlotOccupancy }
21
+ | { kind: "blocked"; reason: "invalid-state" | "protected-capacity" };
22
+
23
+ function isSlot(slot: string): boolean {
24
+ return /^[a-z]$/u.test(slot);
25
+ }
26
+
27
+ function isValidSnapshot(
28
+ bindings: readonly TelegramWorkspaceSlotOccupancy[],
29
+ reservedSlots: readonly string[],
30
+ nowMs: number,
31
+ ): boolean {
32
+ if (!Number.isFinite(nowMs) || nowMs < 0) return false;
33
+ const slots = new Set<string>();
34
+ const keys = new Set<string>();
35
+ for (const binding of bindings) {
36
+ if (!isSlot(binding.slot) || !binding.bindingKey ||
37
+ slots.has(binding.slot) || keys.has(binding.bindingKey)) return false;
38
+ slots.add(binding.slot);
39
+ keys.add(binding.bindingKey);
40
+ }
41
+ return reservedSlots.every(isSlot);
42
+ }
43
+
44
+ function eligibleByInactivity(
45
+ bindings: readonly TelegramWorkspaceSlotOccupancy[],
46
+ reservedSlots: readonly string[],
47
+ nowMs: number,
48
+ ): TelegramWorkspaceSlotOccupancy[] {
49
+ const reserved = new Set(reservedSlots);
50
+ return bindings.filter((binding) =>
51
+ binding.protection === "eligible" &&
52
+ !reserved.has(binding.slot) &&
53
+ typeof binding.inactiveSinceMs === "number" &&
54
+ Number.isFinite(binding.inactiveSinceMs) &&
55
+ binding.inactiveSinceMs >= 0 &&
56
+ binding.inactiveSinceMs <= nowMs,
57
+ ).sort((left, right) =>
58
+ left.inactiveSinceMs! - right.inactiveSinceMs! ||
59
+ left.slot.charCodeAt(0) - right.slot.charCodeAt(0),
60
+ );
61
+ }
62
+
63
+ /** Caller must recheck exact ownership and protected work before retirement. */
64
+ export function planTelegramWorkspaceSlotAllocation(input: {
65
+ bindings: readonly TelegramWorkspaceSlotOccupancy[];
66
+ reservedSlots: readonly string[];
67
+ nowMs: number;
68
+ }): TelegramWorkspaceSlotAllocation {
69
+ const { bindings, reservedSlots, nowMs } = input;
70
+ if (!isValidSnapshot(bindings, reservedSlots, nowMs)) {
71
+ return { kind: "blocked", reason: "invalid-state" };
72
+ }
73
+ const occupied = new Set([
74
+ ...bindings.map((binding) => binding.slot),
75
+ ...reservedSlots,
76
+ ]);
77
+ for (const slot of TELEGRAM_WORKSPACE_SLOTS) {
78
+ if (!occupied.has(slot)) return { kind: "free", slot };
79
+ }
80
+ const candidate = eligibleByInactivity(bindings, reservedSlots, nowMs)[0];
81
+ return candidate
82
+ ? { kind: "reclaim", candidate: { ...candidate } }
83
+ : { kind: "blocked", reason: "protected-capacity" };
84
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.43.2",
3
+ "version": "0.45.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,83 @@
1
+ // Counts isolated registry work without sockets, configured profiles, or Telegram calls.
2
+ import assert from "node:assert/strict";
3
+ import { createTelegramBusFollowerRegistry, createTelegramBusProtocolIdentity } from "../lib/bus.ts";
4
+
5
+ const repetitions = 10;
6
+ const methods = ["get", "set", "delete", "entries", "values"];
7
+ const originals = Object.fromEntries(methods.map((name) => [name, Map.prototype[name]]));
8
+ const rows = [];
9
+ let counts;
10
+
11
+ function measure(size, operation, execute) {
12
+ const sample = { gets: 0, sets: 0, deletes: 0, entryVisits: 0, valueVisits: 0 };
13
+ let result;
14
+ counts = sample;
15
+ try {
16
+ for (let index = 0; index < repetitions; index++) result = execute(index);
17
+ } finally {
18
+ counts = undefined;
19
+ }
20
+ rows.push({ size, operation, repetitions, ...sample });
21
+ return { result, counts: sample };
22
+ }
23
+
24
+ try {
25
+ for (const [method, counter] of [["get", "gets"], ["set", "sets"], ["delete", "deletes"]]) {
26
+ Map.prototype[method] = function (...args) {
27
+ if (counts) counts[counter]++;
28
+ return originals[method].apply(this, args);
29
+ };
30
+ }
31
+ for (const [method, counter] of [["entries", "entryVisits"], ["values", "valueVisits"]]) {
32
+ Map.prototype[method] = function* (...args) {
33
+ for (const entry of originals[method].apply(this, args)) {
34
+ if (counts) counts[counter]++;
35
+ yield entry;
36
+ }
37
+ };
38
+ }
39
+ for (const size of [1, 13, 26]) {
40
+ const registry = createTelegramBusFollowerRegistry();
41
+ const protocol = createTelegramBusProtocolIdentity({ runtimeBuild: "measurement" });
42
+ const registrations = Array.from({ length: size }, (_, index) => ({
43
+ instanceId: `fixture-${index}`, profileKey: `manual:fixture-${index}`,
44
+ registrationGeneration: `generation-${index}`, connectedAtMs: 1000,
45
+ target: { chatId: 7, threadId: index + 1 }, protocol,
46
+ }));
47
+ for (const registration of registrations) registry.register(registration);
48
+ const last = registrations.at(-1);
49
+ const registered = measure(size, "reregister-last", () => registry.register(last));
50
+ assert.equal(registered.counts.entryVisits, size * repetitions);
51
+ const heartbeat = measure(size, "heartbeat-last", (index) => registry.heartbeat(last.instanceId, 2000 + index));
52
+ assert.equal(heartbeat.result.lastHeartbeatMs, 2000 + repetitions - 1);
53
+ assert.equal(heartbeat.counts.entryVisits + heartbeat.counts.valueVisits, 0);
54
+ assert.equal(heartbeat.counts.gets, repetitions);
55
+ assert.equal(heartbeat.counts.sets, repetitions);
56
+ const first = measure(size, "target-first", () => registry.getByTarget(registrations[0].target));
57
+ assert.equal(first.result.instanceId, registrations[0].instanceId);
58
+ assert.equal(first.counts.valueVisits, repetitions);
59
+ const tail = measure(size, "target-last", () => registry.getByTarget(last.target));
60
+ assert.equal(tail.result.instanceId, last.instanceId);
61
+ assert.equal(tail.counts.valueVisits, size * repetitions);
62
+ const missing = measure(size, "target-missing-chat", () => registry.getByTarget({ chatId: 8, threadId: size }));
63
+ assert.equal(missing.result, undefined);
64
+ assert.equal(missing.counts.valueVisits, size * repetitions);
65
+ const roster = measure(size, "list", () => registry.list());
66
+ assert.equal(roster.result.length, size);
67
+ assert.equal(roster.counts.valueVisits, size * repetitions);
68
+ for (const view of [registered.result, heartbeat.result, first.result, tail.result, ...roster.result]) {
69
+ const expected = registry.get(view.instanceId);
70
+ view.target.chatId = 99;
71
+ view.protocol.capabilities.push("fixture-only");
72
+ assert.deepEqual(registry.get(view.instanceId), expected, "Returned views must not mutate registry authority");
73
+ }
74
+ }
75
+ } finally {
76
+ for (const method of methods) Map.prototype[method] = originals[method];
77
+ }
78
+ console.log(JSON.stringify({
79
+ scope: "Synchronous isolated follower registry, not IPC, authentication, provisioning, or throughput",
80
+ counters: "Aggregate Map operations and visited entries for each row's repetitions; no allocation or timing claims",
81
+ copyEvidence: "Mutating returned target/protocol capability views leaves registry authority unchanged",
82
+ rows,
83
+ }, null, 2));
@@ -0,0 +1,101 @@
1
+ // Measures isolated Thread-store work; never reads configured profiles or calls Telegram.
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import { syncBuiltinESMExports } from "node:module";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { createTelegramTopicTargetStore } from "../lib/threads.ts";
8
+
9
+ const repetitions = 10;
10
+ const original = { readFile: fs.readFile, writeFile: fs.writeFile, mkdir: fs.mkdir, rename: fs.rename,
11
+ parse: JSON.parse, stringify: JSON.stringify };
12
+ let counts;
13
+ const rows = [];
14
+ const root = await fs.mkdtemp(join(tmpdir(), "pi-telegram-measure-workspace-"));
15
+
16
+ async function measure(size, operation, execute, samples = repetitions) {
17
+ counts = { reads: 0, readBytes: 0, writes: 0, writeBytes: 0, mkdirs: 0, renames: 0,
18
+ parses: 0, stringifies: 0 };
19
+ try {
20
+ for (let index = 0; index < samples; index++) await execute(index);
21
+ const result = { size, operation, repetitions: samples, ...counts };
22
+ rows.push(result);
23
+ return result;
24
+ } finally {
25
+ counts = undefined;
26
+ }
27
+ }
28
+
29
+ try {
30
+ fs.readFile = async (...args) => {
31
+ if (counts) counts.reads++;
32
+ const result = await original.readFile(...args);
33
+ if (counts) counts.readBytes += Buffer.byteLength(result);
34
+ return result;
35
+ };
36
+ fs.writeFile = async (...args) => {
37
+ if (counts) { counts.writes++; counts.writeBytes += Buffer.byteLength(args[1]); }
38
+ return original.writeFile(...args);
39
+ };
40
+ fs.mkdir = async (...args) => {
41
+ if (counts) counts.mkdirs++;
42
+ return original.mkdir(...args);
43
+ };
44
+ fs.rename = async (...args) => {
45
+ if (counts) counts.renames++;
46
+ return original.rename(...args);
47
+ };
48
+ JSON.parse = (...args) => { if (counts) counts.parses++; return original.parse(...args); };
49
+ JSON.stringify = (...args) => { if (counts) counts.stringifies++; return original.stringify(...args); };
50
+ syncBuiltinESMExports();
51
+
52
+ for (const size of [1, 13, 26]) {
53
+ const path = join(root, `${size}.json`);
54
+ const store = createTelegramTopicTargetStore({ path, getNowMs: () => 1000 });
55
+ for (let index = 0; index < size; index++) {
56
+ const instanceId = `fixture-${index}`;
57
+ const identity = store.claimWorkspaceIdentity(`/fixture/${index}`, instanceId);
58
+ assert.ok(identity);
59
+ const target = { chatId: 7, threadId: index + 1 };
60
+ assert.ok(store.upsertWorkspaceBinding({ ...identity, target, updatedAtMs: 1000 }, instanceId));
61
+ store.upsert({ profileKey: `manual:${instanceId}`, instanceId, slot: identity.slot,
62
+ target, status: "active", createdAtMs: 1000, updatedAtMs: 1000 });
63
+ }
64
+ await store.persist();
65
+ assert.equal(new Set(store.listWorkspaceBindings().map((entry) => entry.slot)).size, size);
66
+ await measure(size, "cold-load", async () => {
67
+ const reader = createTelegramTopicTargetStore({ path });
68
+ await reader.load();
69
+ assert.equal(reader.list().length, size);
70
+ assert.equal(reader.listWorkspaceBindings().length, size);
71
+ });
72
+ const lookup = await measure(size, "lookup-last", () => {
73
+ assert.equal(store.getByProfileKey(`manual:fixture-${size - 1}`)?.target.threadId, size);
74
+ });
75
+ assert.equal(lookup.reads + lookup.writes, 0);
76
+ const beforeReloadSave = JSON.parse(await fs.readFile(path, "utf8"));
77
+ const reloaded = await measure(size, "first-persist-after-reload", () => store.persist(), 1);
78
+ assert.equal(reloaded.writes + reloaded.renames + reloaded.mkdirs, 0);
79
+ assert.deepEqual(JSON.parse(await fs.readFile(path, "utf8")), beforeReloadSave,
80
+ "The first reloaded save must preserve the seeded semantic state");
81
+ const unchanged = await measure(size, "unchanged-persist", () => store.persist());
82
+ assert.equal(unchanged.writes + unchanged.renames + unchanged.mkdirs, 0);
83
+ assert.ok(unchanged.reads >= repetitions, "No-op saves still consult disk authority");
84
+ const changed = await measure(size, "diagnostic-persist", async (index) => {
85
+ store.setStatusSnapshot({ diagnostics: { measurementStep: index } });
86
+ await store.persist();
87
+ });
88
+ assert.equal(changed.writes, repetitions);
89
+ assert.equal(changed.renames, repetitions);
90
+ }
91
+ } finally {
92
+ Object.assign(fs, { readFile: original.readFile, writeFile: original.writeFile,
93
+ mkdir: original.mkdir, rename: original.rename });
94
+ JSON.parse = original.parse;
95
+ JSON.stringify = original.stringify;
96
+ syncBuiltinESMExports();
97
+ await fs.rm(root, { recursive: true, force: true });
98
+ }
99
+ console.log(JSON.stringify({ scope: "Isolated Thread store, not IPC/admission/Telegram or throughput",
100
+ counters: "Aggregate API calls and bytes for each row's repetitions; object-spread clones are not instrumented",
101
+ rows }, null, 2));
@@ -1,17 +1,37 @@
1
1
  ---
2
2
  name: show-me
3
- description: Explain the current topic or work through contextual Markdown replies, concise diagrams, code-shape sketches, and focused HTML artifacts.
3
+ description: Explain completed work, proposed changes, system structure, or ideas through truthful contextual Markdown, concise diagrams, code-shape sketches, and focused HTML artifacts. Use when the user asks to show what happened, what changed, or how something works, especially on a phone-width Telegram surface.
4
4
  ---
5
5
 
6
- Help the user understand the current topic: completed work, a proposed change, system structure, or an idea. Skip the preamble and keep prose brief. Pick the smallest view that makes the key point clear.
6
+ Help the user understand the current topic. Skip the preamble and keep prose brief. Pick the smallest view that makes the key point clear without making the underlying claim less true.
7
7
 
8
8
  ### Output Selection
9
9
 
10
10
  - Infer the subject from the conversation and honor an explicitly requested format. With plain `show me`, choose the form that best explains the subject.
11
11
  - Markdown in the reply is a complete output format for both chat and terminal surfaces. Use headings, emphasis, lists, and focused code blocks to explain outcomes, comparisons, and reasoning; add a diagram when relationships need one.
12
- - `Show me markdown` requests a rendered Markdown reply. Create a Markdown file when the user asks for a saved document or file artifact.
12
+ - `Show me markdown` requests a rendered Markdown reply. Create a Markdown file only when the user asks for a saved document or file artifact.
13
13
  - `Show me html` requests a focused HTML file. Save file artifacts in the project or filesystem as appropriate and deliver them through the active environment's file-delivery mechanism; open locally when that is the requested surface.
14
14
 
15
+ ### Surface Routing
16
+
17
+ - Infer whether the active surface is Telegram, a terminal, or another client. Keep the explanation portable and use surface-specific delivery only when the context or user request authorizes it.
18
+ - For Telegram output, read [`references/telegram-surfaces.md`](./references/telegram-surfaces.md) before selecting Markdown or HTML. Keep the immediate reply useful even when an attached artifact is the deeper view.
19
+ - When asked what changed or happened in current work, inspect the retained diff, status, and validation evidence when available. Separate this task from pre-existing changes and distinguish repository state from released or live behavior.
20
+
21
+ ### Truth And State
22
+
23
+ - Label material claims as `live`, `released`, `locally implemented`, `proposed`, or `unverified` when the distinction affects interpretation. Omit redundant labels when the state is already unambiguous.
24
+ - Preserve the exact mechanism, owner, and boundary when simplifying technical behavior. A friendly label may supplement the implementation term but must not replace it when that would merge distinct timers, states, stages, or authorities.
25
+ - Make a visual diff correspond to the actual changed symbol or contract. Show unchanged neighboring behavior when omission could imply that it changed too.
26
+ - Do not infer that an affordance is clickable, interactive, rendered, or client-visible from source or markup shape alone. Distinguish designed or expected behavior from behavior verified through the relevant renderer, transport, runtime, and client.
27
+ - Treat a mockup, diagram, or rendered artifact as explanatory evidence, not runtime proof. State the unverified boundary when the user could reasonably mistake one for the other.
28
+
29
+ A compact state line is enough when provenance matters:
30
+
31
+ ```text
32
+ State: locally implemented · validated · not released · not live
33
+ ```
34
+
15
35
  ### Visual Forms
16
36
 
17
37
  - Show logic or an algorithm as pseudocode:
@@ -127,7 +147,7 @@ function expandSkill(command: string): string {
127
147
  }
128
148
  ```
129
149
 
130
- - For a visual UI, layout, state comparison, or concept too dense for Mermaid, use a focused HTML artifact — a diagram, an infographic, or a short slide deck, whichever fits the point. Match the product's colors, type, spacing, and components; use real labels and data; support desktop and mobile.
150
+ - For a visual UI, layout, state comparison, or concept too dense for Mermaid, use a focused HTML artifact — a diagram, an infographic, or a short slide deck, whichever fits the point. Match the product's colors, type, spacing, and components; use real labels and data; support desktop and mobile. When practical, render representative narrow and wide viewports and report what was actually inspected.
131
151
 
132
152
  ### Text Rendering
133
153
 
@@ -137,8 +157,10 @@ function expandSkill(command: string): string {
137
157
  - In trees, place comments and explanations as child nodes one level below the item they describe. Keep the item's own line for its label.
138
158
  - Use prose lists for independent statuses and split larger views into meaningful sections.
139
159
 
140
- ### guidance
160
+ ### Guidance
161
+
162
+ Place each visual next to the short text it supports. Keep only the calls, files, props, states, and boundaries needed to answer the user's current question or the options to resolve the current discussion point. Prefer one primary visual and a few decision-relevant takeaways; add another form only when it explains a different necessary relationship.
141
163
 
142
- Place each visual next to the short text it supports. Keep only the calls, files, props, states, and boundaries needed to answer the user's current question or the options to resolve the current discussion point.
164
+ Use source paths, symbol names, or validation evidence when they materially anchor a claim, not as decoration. Report the strongest state the evidence supports and no stronger.
143
165
 
144
166
  You may use one of these, you may use several, it is unlikely you will use all of them. Use your judgement and don't overwhelm the user.
@@ -0,0 +1,43 @@
1
+ # Telegram Explanation Surfaces
2
+
3
+ Use this reference only when Show Me is responding through Telegram or preparing an artifact for Telegram delivery.
4
+
5
+ ## Selection
6
+
7
+ - With plain `show me`, prefer a native Markdown reply when one phone-width view can explain the point.
8
+ - Use HTML when spatial comparison, dense state, a timeline, or a visual hierarchy would become harder to understand in narrow Markdown.
9
+ - `Show me markdown` means rendered Markdown in the current reply, not a `.md` attachment, unless the user explicitly asks for a file.
10
+ - `Show me html` means one focused, self-contained `.html` artifact delivered through the active Telegram file path. The surrounding reply should say what the artifact explains and disclose its evidence state.
11
+ - Do not create both formats by habit. The second format must answer a need the first cannot.
12
+
13
+ ## Telegram Markdown
14
+
15
+ - Design for a phone before a desktop: one governing question, one primary visual, short labels, shallow nesting, and prose that wraps naturally.
16
+ - Prefer compact semantic diffs, call trees, timelines, or state transitions over raw repository diffs. A remote user needs to understand impact before file-level detail.
17
+ - Avoid wide tables, deep trees, side-by-side layouts, and Mermaid when the current Telegram renderer would expose only source text. Move genuinely spatial material to HTML.
18
+ - Put material state near the top: what changed, whether it is local or live, what was validated, and what remains unresolved.
19
+ - Keep source paths and symbol names below the explanation unless they are the explanation.
20
+ - Keep intended Telegram bot-command tokens as plain text rather than inline code. Format Pi/TUI or shell commands as code according to the host contract. Plain source shape does not prove native clickability; claim it only after the active entity-detection and client path is verified.
21
+ - Use explicit Markdown links when a destination matters rather than assuming plain URL auto-detection.
22
+
23
+ ## Telegram HTML
24
+
25
+ - Produce a single self-contained file with UTF-8 metadata and a viewport declaration. Avoid external assets, scripts, fonts, trackers, or network requirements unless the user explicitly requested them.
26
+ - Build mobile-first for roughly phone-width reading, then let the same document expand cleanly in a system or desktop browser. Text must wrap; diagrams must scroll or reflow without clipping.
27
+ - Use semantic headings, sufficient contrast, non-color status meaning, comfortable touch targets, and no hover-only information.
28
+ - Preserve real labels, values, ordering, and uncertainty. An attractive reconstruction must not invent runtime state or imply that a proposed interaction exists.
29
+ - Include a compact provenance line when state matters, such as `Local patch · validated · not released · not live`.
30
+ - When rendering tools are available, inspect at least one narrow viewport and one wider viewport. Report what was inspected; static source review is not visual proof.
31
+ - Deliver the file through the active Telegram attachment mechanism. Do not expose local paths as if the user could open them remotely.
32
+
33
+ ## Current-Work Evidence
34
+
35
+ Before explaining “what we did” or “what happened,” use the narrowest available evidence that can support the answer:
36
+
37
+ 1. Inspect retained repository status and diff for the relevant task.
38
+ 2. Identify pre-existing or unrelated changes and exclude them from the claimed task result.
39
+ 3. Name the actual changed mechanism or contract, not a friendlier neighboring concept.
40
+ 4. Separate implementation evidence from validation, release, deployment, runtime, transport, and client evidence.
41
+ 5. State unresolved causes or missing live checks instead of filling them with a cleaner story.
42
+
43
+ A visual is successful when the user can understand the outcome away from a computer without being given a stronger claim than the evidence supports.
@@ -7,7 +7,7 @@ Read this reference only for explicit local/TUI Telegram delivery, cross-target
7
7
  Use `telegram_message` only when the user explicitly requests Telegram delivery from local/TUI or names a concrete different Telegram target.
8
8
 
9
9
  - Omitted target selects the paired/default target only outside an active Telegram turn.
10
- - `chat_id` plus optional `thread_id` selects an explicit Bot API target.
10
+ - `chat_id` plus optional `thread_id` selects an explicit Bot API target. A public `@username`, or an exact negative numeric channel ID with `channel: true`, uses `chat_id` without `thread_id`; channel delivery requires the direct leader, and Telegram enforces the bot's posting permission.
11
11
  - `thread` selects another live Pi Thread by case-insensitive name or numeric id and admits one attributed turn there.
12
12
  - During an active Telegram turn, answer the current target normally; direct delivery to that same target is rejected.
13
13
  - Direct delivery requires this Pi instance to own transport or hold a live Threaded Mode registration.
@@ -36,5 +36,6 @@
36
36
  - `Path Portability`: Resolve paths through Node rather than platform-specific shell utilities.
37
37
  - `Human Output`: Emit concise readable logs and a summary; `NO_COLOR=1` suppresses color.
38
38
  - `Bounded Scanning`: Skip link scanning above the configured byte threshold while validating the surrounding graph.
39
+ - `Contiguous Lists`: Fail blank lines inside one Markdown list; list items are contiguous structural peers, not paragraphs. Ignore fenced examples and bounded-scan exclusions.
39
40
  - `Compact Tables`: Fail non-compact delimiter cells; require exactly three hyphens with one space inside each pipe, while table rows over 120 characters only warn.
40
41
  - `Core Shape Flexibility`: Accept both numbered mature-project sections and compact skill-style durable sections.
@@ -1,8 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## 1.5.3
3
+ ## 1.14.1
4
4
 
5
- - `Markdown Freedom`: LaTeX syntax is now allowed, while table checks always enforce compact three-hyphen delimiter cells and warn on rows over 120 characters without imposing a maximum. Indented backtick and tilde code fences remain excluded from table validation. Impact: context authors can use mathematical notation and fenced examples while receiving default, noise-focused table formatting guidance.
5
+ - `Contiguous Markdown Lists`: The validator now rejects blank lines inside continuous lists, including multiline and nested item boundaries, while ignoring fenced examples and bounded large-reference scans. Impact: list items remain structural peers instead of being formatted as separate paragraphs.
6
+
7
+ ## 1.13.2
8
+
9
+ - `Markdown Freedom`: LaTeX syntax is now allowed, while table checks always enforce compact three-hyphen delimiter cells and warn on rows over 120 characters without imposing a maximum. Indented backtick and tilde code fences remain excluded from table validation. Impact: context authors can use mathematical notation and fenced examples while receiving default, noise-focused table formatting guidance
6
10
 
7
11
  ## 1.5.2
8
12
 
@@ -102,7 +102,7 @@ Run the Node validator from the project root or pass an explicit root:
102
102
  node "${SKILL_DIR}/scripts/validate-context.mjs" /path/to/project
103
103
  ```
104
104
 
105
- Output is human-readable. Markdown table checks always require compact delimiter cells such as `| --- | ---: | :--- |`; non-compact delimiters fail validation, while rows longer than 120 characters only warn without imposing a maximum length.
105
+ Output is human-readable. Markdown list items must remain contiguous: blank lines inside one list fail validation, including between multiline or nested items, while fenced examples are ignored. Markdown table checks always require compact delimiter cells such as `| --- | ---: | :--- |`; non-compact delimiters fail validation, while rows longer than 120 characters only warn without imposing a maximum length.
106
106
 
107
107
  Useful environment controls:
108
108
 
@@ -28,10 +28,11 @@ A missing or non-directory root fails before validation.
28
28
  6. `README reachability — Warning`: Finds subtree README files with no inbound Markdown link.
29
29
  7. `Meta-protocol presence — Warning`: Checks the durable file for `Meta-Protocol Principles`.
30
30
  8. `Bloat signals — Warning`: Reports low information density or sparse structure in the durable file.
31
- 9. `Markdown tables — Error/Warning`: Always fails non-compact delimiter rows and warns about table rows longer than 120 characters.
32
- 10. `Freshness — Warning`: Reports durable files older than 30 days.
33
- 11. `Docs directory — Warning`: Checks for `/docs`.
34
- 12. `Docs index coverage — Warning`: Detects docs missing from `docs/README.md` and indexed files that do not exist.
31
+ 9. `Markdown lists — Error`: Fails blank lines within one continuous list, including multiline and nested items, while ignoring fenced examples and files above the bounded Markdown scan limit.
32
+ 10. `Markdown tables Error/Warning`: Always fails non-compact delimiter rows and warns about table rows longer than 120 characters.
33
+ 11. `Freshness — Warning`: Reports durable files older than 30 days.
34
+ 12. `Docs directory — Warning`: Checks for `/docs`.
35
+ 13. `Docs index coverage — Warning`: Detects docs missing from `docs/README.md` and indexed files that do not exist.
35
36
 
36
37
  ## Severity Contract
37
38
 
@@ -70,6 +71,10 @@ The validator avoids a hard file-length limit. It checks independent signals:
70
71
 
71
72
  Signals suggest consolidation; they do not replace judgment.
72
73
 
74
+ ## Markdown Lists
75
+
76
+ List checks apply to Markdown within the bounded scan size and outside fenced code blocks. A blank line fails when it separates sibling list items, a list item from its nested list, nested sibling items, or a multiline item's continuation from its next item. Paragraphs and independent blocks remain separated by blank lines normally.
77
+
73
78
  ## Markdown Tables
74
79
 
75
80
  Table checks always run and have no enabling option.
@@ -93,7 +98,8 @@ Validation prints each check and a summary by default. `NO_COLOR=1` disables ANS
93
98
  3. A missing path, which must fail clearly.
94
99
  4. The removed `--json` option, which must fail clearly.
95
100
  5. A temporary fixture with an out-of-range line reference, which must fail clearly.
96
- 6. Temporary fixtures proving that LaTeX and compact table delimiters pass, non-compact delimiters fail, and rows over 120 characters warn.
101
+ 6. Temporary fixtures proving that contiguous multiline/nested lists and fenced examples pass while blank separators inside lists fail.
102
+ 7. Temporary fixtures proving that LaTeX and compact table delimiters pass, non-compact delimiters fail, and rows over 120 characters warn.
97
103
 
98
104
  The fixture remains linked from [its README](../fixtures/abcd-project/README.md).
99
105
 
@@ -23,6 +23,7 @@ const missingPathOutput = run([path.join(fixtureRoot, "missing")], {
23
23
  const jsonOptionOutput = run(["--json"]);
24
24
  const invalidLineRefRoot = createInvalidLineRefFixture();
25
25
  const tableRoot = createTableFixture();
26
+ const listRoot = createListFixture();
26
27
  try {
27
28
  const invalidLineRefOutput = run([invalidLineRefRoot], {
28
29
  withoutRootEnv: true,
@@ -49,6 +50,12 @@ try {
49
50
  .replace("|------------|---:|", "| --- | ---: |"),
50
51
  );
51
52
  const wideTableOutput = run([tableRoot], { withoutRootEnv: true });
53
+ const tightListOutput = run([listRoot], { withoutRootEnv: true });
54
+ fs.appendFileSync(
55
+ path.join(listRoot, "docs/lists.md"),
56
+ "\n- First loose item.\n Continued detail.\n\n- Second loose item.\n - Nested item.\n\n - Nested sibling.\n",
57
+ );
58
+ const looseListOutput = run([listRoot], { withoutRootEnv: true });
52
59
 
53
60
  checkSuccess("default fixture", defaultOutput);
54
61
  checkSuccess("fixture path arg", pathOutput);
@@ -59,9 +66,12 @@ try {
59
66
  checkCompactTableAndLatex(compactTableOutput);
60
67
  checkNonCompactTable(nonCompactTableOutput);
61
68
  checkWideTable(wideTableOutput);
69
+ checkTightLists(tightListOutput);
70
+ checkLooseLists(looseListOutput);
62
71
  } finally {
63
72
  fs.rmSync(invalidLineRefRoot, { recursive: true, force: true });
64
73
  fs.rmSync(tableRoot, { recursive: true, force: true });
74
+ fs.rmSync(listRoot, { recursive: true, force: true });
65
75
  }
66
76
 
67
77
  console.log("PASS: validate-context fixture + self-reference regression");
@@ -146,6 +156,57 @@ function checkWideTable(result) {
146
156
  );
147
157
  }
148
158
 
159
+ function checkTightLists(result) {
160
+ assert(result.status === 0, "contiguous lists and fenced examples pass");
161
+ assert(
162
+ result.stdout.includes("Markdown list checks passed"),
163
+ "contiguous list check reports success",
164
+ );
165
+ }
166
+
167
+ function checkLooseLists(result) {
168
+ assert(result.status !== 0, "blank lines inside lists fail validation");
169
+ assert(
170
+ result.stdout.includes("Blank line inside Markdown list: docs/lists.md"),
171
+ "loose list failure identifies its file",
172
+ );
173
+ }
174
+
175
+ function createListFixture() {
176
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "abcd-context-list-"));
177
+ fs.mkdirSync(path.join(root, "docs"));
178
+ fs.writeFileSync(
179
+ path.join(root, "README.md"),
180
+ "# Probe\n\n[Context](./AGENTS.md) [Backlog](./BACKLOG.md) [History](./CHANGELOG.md) [Docs](./docs/README.md)\n",
181
+ );
182
+ fs.writeFileSync(
183
+ path.join(root, "AGENTS.md"),
184
+ "# Context\n\n## Meta-Protocol Principles\n\n- First rule.\n- Second rule.\n\n## Operating Principles\n\n- Rule.\n",
185
+ );
186
+ fs.writeFileSync(path.join(root, "BACKLOG.md"), "# Backlog\n\nNo open work.\n");
187
+ fs.writeFileSync(path.join(root, "CHANGELOG.md"), "# Changelog\n\nNo releases.\n");
188
+ fs.writeFileSync(path.join(root, "docs/README.md"), "# Docs\n\n- [Lists](./lists.md)\n");
189
+ fs.writeFileSync(
190
+ path.join(root, "docs/lists.md"),
191
+ [
192
+ "# Lists",
193
+ "",
194
+ "1. First item.",
195
+ "2. Second item.",
196
+ " - Nested item.",
197
+ " - Nested sibling.",
198
+ "",
199
+ "```markdown",
200
+ "- Fenced item.",
201
+ "",
202
+ "- Fenced loose item.",
203
+ "```",
204
+ "",
205
+ ].join("\n"),
206
+ );
207
+ return root;
208
+ }
209
+
149
210
  function createTableFixture() {
150
211
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "abcd-context-table-"));
151
212
  fs.mkdirSync(path.join(root, "docs"));
@@ -16,6 +16,7 @@ for (let index = 0; index < args.length; index += 1) {
16
16
  "Validates the current directory by default, VALIDATE_CONTEXT_ROOT when set,",
17
17
  "or the explicit project-root argument when provided.",
18
18
  "",
19
+ "Markdown list items must be contiguous; blank separators fail validation.",
19
20
  "Markdown table formatting is always checked. Rows over 120 characters warn.",
20
21
  ].join("\n"),
21
22
  );
@@ -167,6 +168,53 @@ function markdownLines(text) {
167
168
  });
168
169
  }
169
170
 
171
+ function markdownIndent(line) {
172
+ const leading = line.match(/^[ \t]*/)?.[0] || "";
173
+ return [...leading].reduce((width, char) => width + (char === "\t" ? 4 : 1), 0);
174
+ }
175
+
176
+ function findLooseListBlankLines(text) {
177
+ const lines = markdownLines(text);
178
+ const listIndents = [];
179
+ let pendingBlankLine;
180
+ const violations = [];
181
+ for (let index = 0; index < lines.length; index += 1) {
182
+ const { line, outsideFence } = lines[index];
183
+ if (!outsideFence) {
184
+ listIndents.length = 0;
185
+ pendingBlankLine = undefined;
186
+ continue;
187
+ }
188
+ if (/^\s*$/.test(line)) {
189
+ if (listIndents.length > 0 && pendingBlankLine === undefined) {
190
+ pendingBlankLine = index + 1;
191
+ }
192
+ continue;
193
+ }
194
+ const item = line.match(/^([ \t]*)(?:[-+*]|\d+[.)])[ \t]+\S/);
195
+ if (item) {
196
+ const indent = markdownIndent(item[1]);
197
+ if (pendingBlankLine !== undefined) violations.push(pendingBlankLine);
198
+ while (listIndents.length > 0 && listIndents.at(-1) > indent) listIndents.pop();
199
+ if (listIndents.at(-1) !== indent) listIndents.push(indent);
200
+ pendingBlankLine = undefined;
201
+ continue;
202
+ }
203
+ if (pendingBlankLine !== undefined) {
204
+ if (markdownIndent(line) > (listIndents.at(-1) ?? 0)) {
205
+ violations.push(pendingBlankLine);
206
+ pendingBlankLine = undefined;
207
+ continue;
208
+ }
209
+ listIndents.length = 0;
210
+ pendingBlankLine = undefined;
211
+ continue;
212
+ }
213
+ if (/^ {0,3}(?:#{1,6}\s|>|\|)/.test(line)) listIndents.length = 0;
214
+ }
215
+ return violations;
216
+ }
217
+
170
218
  function stripFences(text) {
171
219
  return markdownLines(text)
172
220
  .filter(({ outsideFence }) => outsideFence)
@@ -392,6 +440,25 @@ if (contextFile) {
392
440
 
393
441
  const docsDir = path.join(root, "docs");
394
442
 
443
+ progress("Checking Markdown lists...");
444
+ let listIssue = false;
445
+ for (const file of mdFiles) {
446
+ const sourceSize = fs.statSync(file).size;
447
+ if (sourceSize > markdownLinkScanMaxBytes) {
448
+ info(
449
+ `Skipped list validation for large Markdown file: ${rel(file)} (${sourceSize} bytes > ${markdownLinkScanMaxBytes})`,
450
+ );
451
+ continue;
452
+ }
453
+ for (const line of findLooseListBlankLines(read(file))) {
454
+ fail(
455
+ `Blank line inside Markdown list: ${rel(file)}:${line} (keep adjacent list items contiguous)`,
456
+ );
457
+ listIssue = true;
458
+ }
459
+ }
460
+ if (!listIssue) pass("Markdown list checks passed");
461
+
395
462
  progress("Checking Markdown tables...");
396
463
  let tableIssue = false;
397
464
  for (const file of mdFiles) {