@llblab/pi-kit 0.3.1 → 0.4.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 (123) hide show
  1. package/AGENTS.md +3 -3
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +10 -0
  4. package/README.md +19 -10
  5. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -0
  6. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  7. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +16 -6
  8. package/node_modules/@llblab/pi-actors/dist/skills/music-player/SKILL.md +3 -3
  9. package/node_modules/@llblab/pi-actors/dist/skills/music-player/genapps/music-player.mjs +6 -4
  10. package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback.mjs +85 -18
  11. package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +2 -6
  12. package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/development-swarm.md +2 -31
  13. package/node_modules/@llblab/pi-actors/docs/recipe-library.md +1 -1
  14. package/node_modules/@llblab/pi-actors/package.json +1 -1
  15. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +16 -6
  16. package/node_modules/@llblab/pi-actors/skills/music-player/SKILL.md +3 -3
  17. package/node_modules/@llblab/pi-actors/skills/music-player/genapps/music-player.mjs +6 -4
  18. package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback.mjs +85 -18
  19. package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +2 -6
  20. package/node_modules/@llblab/pi-actors/skills/swarm/references/development-swarm.md +2 -31
  21. package/node_modules/@llblab/pi-clean-room/AGENTS.md +5 -0
  22. package/node_modules/@llblab/pi-clean-room/BACKLOG.md +3 -0
  23. package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +15 -0
  24. package/node_modules/@llblab/pi-clean-room/README.md +61 -0
  25. package/node_modules/@llblab/pi-clean-room/banner.jpg +0 -0
  26. package/node_modules/@llblab/pi-clean-room/index.ts +178 -0
  27. package/node_modules/@llblab/pi-clean-room/package.json +53 -0
  28. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -1
  29. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  30. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  31. package/node_modules/@llblab/pi-grow-loop/skills/grow-loop/SKILL.md +3 -17
  32. package/node_modules/@llblab/pi-state-flow/AGENTS.md +28 -0
  33. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -0
  34. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +79 -0
  35. package/node_modules/@llblab/pi-state-flow/README.md +197 -0
  36. package/node_modules/@llblab/pi-state-flow/index.ts +4 -0
  37. package/node_modules/@llblab/pi-state-flow/lib/context.ts +88 -0
  38. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +29 -0
  39. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +238 -0
  40. package/node_modules/@llblab/pi-state-flow/lib/json.ts +78 -0
  41. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +38 -0
  42. package/node_modules/@llblab/pi-state-flow/lib/session.ts +49 -0
  43. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +58 -0
  44. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +103 -0
  45. package/node_modules/@llblab/pi-state-flow/lib/state.ts +20 -0
  46. package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -0
  47. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +148 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +47 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/validation.ts +27 -0
  50. package/node_modules/@llblab/pi-state-flow/package.json +55 -0
  51. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  52. package/node_modules/@llblab/pi-telegram/BACKLOG.md +2 -0
  53. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +16 -0
  54. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  55. package/node_modules/@llblab/pi-telegram/docs/architecture.md +1 -1
  56. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +17 -5
  57. package/node_modules/@llblab/pi-telegram/docs/outbound.md +3 -3
  58. package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -0
  59. package/node_modules/@llblab/pi-telegram/index.ts +9 -0
  60. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +29 -18
  61. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -6
  62. package/node_modules/@llblab/pi-telegram/lib/bus.ts +11 -4
  63. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +34 -2
  64. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +4 -1
  65. package/node_modules/@llblab/pi-telegram/lib/routing.ts +89 -32
  66. package/node_modules/@llblab/pi-telegram/lib/sync.ts +74 -15
  67. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +32 -1
  68. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +17 -0
  69. package/node_modules/@llblab/pi-telegram/lib/threads.ts +123 -15
  70. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  71. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +1 -1
  72. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +4 -2
  73. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +40 -0
  74. package/node_modules/@llblab/skills/abcd-context/BACKLOG.md +3 -0
  75. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +19 -0
  76. package/node_modules/@llblab/skills/abcd-context/README.md +30 -0
  77. package/node_modules/@llblab/skills/abcd-context/SKILL.md +122 -0
  78. package/node_modules/@llblab/skills/abcd-context/docs/README.md +9 -0
  79. package/node_modules/@llblab/skills/abcd-context/docs/protocols.md +199 -0
  80. package/node_modules/@llblab/skills/abcd-context/docs/templates.md +194 -0
  81. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +117 -0
  82. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/AGENTS.md +10 -0
  83. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/BACKLOG.md +5 -0
  84. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/CHANGELOG.md +5 -0
  85. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/README.md +10 -0
  86. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/README.md +7 -0
  87. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/overview.md +3 -0
  88. package/node_modules/@llblab/skills/abcd-context/recipes/validate-context.json +7 -0
  89. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +244 -0
  90. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +497 -0
  91. package/node_modules/@llblab/skills/brain-storm/AGENTS.md +5 -0
  92. package/node_modules/@llblab/skills/brain-storm/SKILL.md +321 -0
  93. package/node_modules/@llblab/skills/coding-contract/SKILL.md +228 -0
  94. package/node_modules/@llblab/skills/domain-dag/AGENTS.md +6 -0
  95. package/node_modules/@llblab/skills/domain-dag/SKILL.md +257 -0
  96. package/node_modules/@llblab/skills/domain-dag/domain-dag.json +9 -0
  97. package/node_modules/@llblab/skills/domain-dag/recipes/validate-domain-dag.json +7 -0
  98. package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.mjs +771 -0
  99. package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.sh +4 -0
  100. package/node_modules/@llblab/skills/edge-tts/AGENTS.md +19 -0
  101. package/node_modules/@llblab/skills/edge-tts/SKILL.md +56 -0
  102. package/node_modules/@llblab/skills/edge-tts/recipes/say-edge.json +9 -0
  103. package/node_modules/@llblab/skills/edge-tts/scripts/say.mjs +1548 -0
  104. package/node_modules/@llblab/skills/edge-tts/scripts/say.sh +4 -0
  105. package/node_modules/@llblab/skills/extra-self/SKILL.md +229 -0
  106. package/node_modules/@llblab/skills/frontend-design/SKILL.md +78 -0
  107. package/node_modules/@llblab/skills/groq-stt/AGENTS.md +15 -0
  108. package/node_modules/@llblab/skills/groq-stt/SKILL.md +35 -0
  109. package/node_modules/@llblab/skills/groq-stt/recipes/transcribe-groq.json +10 -0
  110. package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.mjs +190 -0
  111. package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.sh +4 -0
  112. package/node_modules/@llblab/skills/mistral-stt/AGENTS.md +15 -0
  113. package/node_modules/@llblab/skills/mistral-stt/SKILL.md +35 -0
  114. package/node_modules/@llblab/skills/mistral-stt/recipes/transcribe-mistral.json +10 -0
  115. package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.mjs +191 -0
  116. package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.sh +4 -0
  117. package/node_modules/@llblab/skills/package.json +46 -0
  118. package/node_modules/@llblab/skills/re-review/SKILL.md +318 -0
  119. package/node_modules/@llblab/skills/release-flow/SKILL.md +456 -0
  120. package/node_modules/@llblab/skills/show-me/SKILL.md +144 -0
  121. package/package.json +20 -8
  122. package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback-client.mjs +0 -143
  123. package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback-client.mjs +0 -143
@@ -466,6 +466,8 @@ export interface TelegramApiClient {
466
466
  }
467
467
 
468
468
  export interface TelegramBridgeApiRuntimeDeps {
469
+ captureRequestErrorHandler?: (body: Record<string, unknown>) =>
470
+ ((error: unknown) => Promise<void>) | undefined;
469
471
  client: TelegramApiClient;
470
472
  tempDir: string;
471
473
  maxFileSizeBytes: number;
@@ -634,6 +636,11 @@ export function getTelegramApiErrorRequestTarget(
634
636
  return target ? { ...target } : undefined;
635
637
  }
636
638
 
639
+ export function isTelegramStaleTargetHttpError(error: unknown): boolean {
640
+ if (!(error instanceof TelegramApiHttpError) || error.status !== 400) return false;
641
+ return /^Telegram API \w+ failed: HTTP 400: Bad Request: (message thread not found|thread not found|topic not found|topic deleted|topic closed|thread closed|forum topic closed|message thread closed|topic_id_invalid|topic_closed)$/i.test(error.message);
642
+ }
643
+
637
644
  export function isTelegramMessageNotModifiedError(error: unknown): boolean {
638
645
  return (
639
646
  error instanceof Error && error.message.includes("message is not modified")
@@ -1438,6 +1445,7 @@ export function createTelegramAssistantDraftSender(deps: {
1438
1445
  export function createDefaultTelegramBridgeApiRuntime(deps: {
1439
1446
  getBotToken: () => string | undefined;
1440
1447
  recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
1448
+ captureRequestErrorHandler?: TelegramBridgeApiRuntimeDeps["captureRequestErrorHandler"];
1441
1449
  }): TelegramBridgeApiRuntime {
1442
1450
  return createTelegramBridgeApiRuntime({
1443
1451
  client: createTelegramApiClient(deps.getBotToken, {
@@ -1447,12 +1455,23 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
1447
1455
  maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
1448
1456
  tempFileMaxAgeMs: TELEGRAM_TEMP_FILE_MAX_AGE_MS,
1449
1457
  recordRuntimeEvent: deps.recordRuntimeEvent,
1458
+ captureRequestErrorHandler: deps.captureRequestErrorHandler,
1450
1459
  });
1451
1460
  }
1452
1461
 
1453
1462
  export function createTelegramBridgeApiRuntime(
1454
1463
  deps: TelegramBridgeApiRuntimeDeps,
1455
1464
  ): TelegramBridgeApiRuntime {
1465
+ const recoverRequestError = async (
1466
+ handler: ((error: unknown) => Promise<void>) | undefined,
1467
+ error: unknown,
1468
+ ): Promise<void> => {
1469
+ try {
1470
+ await handler?.(error);
1471
+ } catch (recoveryError) {
1472
+ deps.recordRuntimeEvent("api", recoveryError, { phase: "stale-target-recovery" });
1473
+ }
1474
+ };
1456
1475
  const now = deps.now ?? Date.now;
1457
1476
  const chatActionMinIntervalMs = Math.max(
1458
1477
  0,
@@ -1488,6 +1507,7 @@ export function createTelegramBridgeApiRuntime(
1488
1507
  body: Record<string, unknown>,
1489
1508
  options?: TelegramApiCallOptions,
1490
1509
  ): Promise<TResponse> => {
1510
+ const recoverError = deps.captureRequestErrorHandler?.(body);
1491
1511
  const chatActionKey = getChatActionKey(method, body);
1492
1512
  if (chatActionKey) {
1493
1513
  const nowMs = now();
@@ -1516,7 +1536,8 @@ export function createTelegramBridgeApiRuntime(
1516
1536
  gate.notBeforeMs = now() + chatActionMinIntervalMs;
1517
1537
  return result;
1518
1538
  })
1519
- .catch((error: unknown) => {
1539
+ .catch(async (error: unknown) => {
1540
+ await recoverRequestError(recoverError, error);
1520
1541
  if (error instanceof TelegramApiHttpError && error.status === 429) {
1521
1542
  const retryAfterMs = Math.max(
1522
1543
  chatActionMinIntervalMs,
@@ -1550,6 +1571,12 @@ export function createTelegramBridgeApiRuntime(
1550
1571
  try {
1551
1572
  return await deps.client.call<TResponse>(method, body, options);
1552
1573
  } catch (error) {
1574
+ await recoverRequestError(recoverError, error);
1575
+ if (method === "deleteMessage" && error instanceof TelegramApiHttpError &&
1576
+ error.status === 400 && error.message ===
1577
+ "Telegram API deleteMessage failed: HTTP 400: Bad Request: message to delete not found") {
1578
+ return true as TResponse;
1579
+ }
1553
1580
  deps.recordRuntimeEvent(
1554
1581
  "api",
1555
1582
  error,
@@ -1574,6 +1601,7 @@ export function createTelegramBridgeApiRuntime(
1574
1601
  fileName,
1575
1602
  options,
1576
1603
  ) => {
1604
+ const recoverError = deps.captureRequestErrorHandler?.(fields);
1577
1605
  try {
1578
1606
  return await deps.client.callMultipart(
1579
1607
  method,
@@ -1584,6 +1612,7 @@ export function createTelegramBridgeApiRuntime(
1584
1612
  options,
1585
1613
  );
1586
1614
  } catch (error) {
1615
+ await recoverRequestError(recoverError, error);
1587
1616
  deps.recordRuntimeEvent(
1588
1617
  "multipart",
1589
1618
  error,
@@ -1676,11 +1705,13 @@ export function createTelegramBridgeApiRuntime(
1676
1705
  sendRichMessageDraft: (body) =>
1677
1706
  callRecorded<boolean>("sendRichMessageDraft", body),
1678
1707
  editMessageText: async (body) => {
1708
+ const recoverError = deps.captureRequestErrorHandler?.(body);
1679
1709
  try {
1680
1710
  await deps.client.call("editMessageText", body);
1681
1711
  return "edited";
1682
1712
  } catch (error) {
1683
1713
  if (isTelegramMessageNotModifiedError(error)) return "unchanged";
1714
+ await recoverRequestError(recoverError, error);
1684
1715
  deps.recordRuntimeEvent(
1685
1716
  "api",
1686
1717
  error,
@@ -202,6 +202,7 @@ export interface ThreadReconciliationApplyResult {
202
202
  }
203
203
 
204
204
  export interface ThreadReconciliationApplyPorts {
205
+ isCleanupTargetProtected?: (target: ThreadTarget, action: ThreadReconciliationAction) => boolean;
205
206
  callApi?: <TResponse>(
206
207
  method: string,
207
208
  body: Record<string, unknown>,
@@ -576,6 +577,7 @@ export async function applyThreadReconciliationPlan(
576
577
  incompleteActions.push(action);
577
578
  continue;
578
579
  }
580
+ if (ports.isCleanupTargetProtected?.(action.target, action)) continue;
579
581
  let closeConfirmed = false;
580
582
  try {
581
583
  await ports.callApi("closeForumTopic", {
@@ -598,6 +600,7 @@ export async function applyThreadReconciliationPlan(
598
600
  incompleteActions.push(action);
599
601
  continue;
600
602
  }
603
+ if (ports.isCleanupTargetProtected?.(action.target, action)) continue;
601
604
  const changed =
602
605
  ports.markStaleByTarget?.(action.target, "closed") ?? false;
603
606
  if (changed) persistFences.push(action);
@@ -632,7 +635,12 @@ export async function applyThreadReconciliationPlan(
632
635
  continue;
633
636
  }
634
637
  let deleteConfirmed = false;
638
+ let superseded = false;
635
639
  for (const method of ["closeForumTopic", "deleteForumTopic"]) {
640
+ if (ports.isCleanupTargetProtected?.(action.target, action)) {
641
+ superseded = true;
642
+ break;
643
+ }
636
644
  if (shouldSkipForStaleLeaderEpoch(action, ports)) break;
637
645
  try {
638
646
  await ports.callApi(method, {
@@ -660,6 +668,15 @@ export async function applyThreadReconciliationPlan(
660
668
  incompleteActions.push(action);
661
669
  continue;
662
670
  }
671
+ if (superseded || ports.isCleanupTargetProtected?.(action.target, action)) {
672
+ ports.recordRuntimeEvent?.("telegram", "Cancelled cleanup of a protected Telegram target", {
673
+ phase: "thread-reconciler-cleanup-target-reused",
674
+ action: action.kind,
675
+ chatId: action.target.chatId,
676
+ threadId: action.target.threadId,
677
+ });
678
+ continue;
679
+ }
663
680
  if (!deleteConfirmed) {
664
681
  ports.recordRuntimeEvent?.(
665
682
  "telegram",
@@ -21,6 +21,7 @@ import {
21
21
  writeFile,
22
22
  } from "node:fs/promises";
23
23
  import { dirname } from "node:path";
24
+ import { isDeepStrictEqual } from "node:util";
24
25
 
25
26
  import {
26
27
  isTelegramApiCommitUnknownError,
@@ -229,6 +230,11 @@ export interface TelegramTopicTargetStore {
229
230
  /** Discard process-local projections and reload owner-published state. */
230
231
  refresh?: () => Promise<void>;
231
232
  persist: () => Promise<void>;
233
+ invalidateTarget: (
234
+ target: TelegramTarget,
235
+ isCurrent: () => boolean,
236
+ lastSyncError: string,
237
+ ) => Promise<boolean>;
232
238
  list: () => TelegramTopicTargetRecord[];
233
239
  getFollowerRecoveryHintByTarget?: (
234
240
  target: TelegramTarget,
@@ -316,6 +322,53 @@ export function reconcileTelegramFreshAllocationCursor(
316
322
  return true;
317
323
  }
318
324
 
325
+ export function createTelegramCleanupTargetProtection(
326
+ store: Pick<TelegramTopicTargetStore, "list"> & Partial<Pick<TelegramTopicTargetStore, "listReservations" | "listPendingProvisions" | "listPendingCleanups">>,
327
+ departingRecord?: TelegramTopicTargetRecord,
328
+ ): NonNullable<ThreadReconciler.ThreadReconciliationApplyPorts["isCleanupTargetProtected"]> {
329
+ const records = store.list();
330
+ const reservations = store.listReservations?.() ?? [];
331
+ const provisions = store.listPendingProvisions?.() ?? [];
332
+ const intents = store.listPendingCleanups?.() ?? [];
333
+ // Persistence may reconstruct keys in another order and omit undefined
334
+ // optional fields; neither changes the authority represented by a snapshot.
335
+ const sameSnapshot = (left: unknown, right: unknown): boolean =>
336
+ isDeepStrictEqual(JSON.parse(JSON.stringify(left)), JSON.parse(JSON.stringify(right)));
337
+ return (target, action) => {
338
+ for (const record of store.list()) {
339
+ if (!targetMatches(record.target, target)) continue;
340
+ // A persisted shutdown intent may retire only its original pre-intent
341
+ // binding. Registration/rebinding after that intent supersedes it.
342
+ const intent = action.kind === "close-delete-graceful-shutdown-topic"
343
+ ? intents.find((candidate) => candidate.id === action.cleanupIntentId && candidate.runtimeGeneration === action.runtimeGeneration)
344
+ : undefined;
345
+ const expectedDeparting = departingRecord ?? (intent && records.find((candidate) =>
346
+ candidate.instanceId === intent.instanceId && targetMatches(candidate.target, intent.target) &&
347
+ candidate.updatedAtMs <= intent.requestedAtMs));
348
+ if (expectedDeparting && "instanceId" in action &&
349
+ (action.kind === "close-delete-previous-leader-topic" || action.instanceId === expectedDeparting.instanceId) &&
350
+ sameSnapshot(record, expectedDeparting)) {
351
+ if (action.kind === "close-delete-previous-leader-topic" || action.kind === "close-stale-replaced-topic") continue;
352
+ if (action.kind === "close-delete-graceful-shutdown-topic" &&
353
+ store.listPendingCleanups?.().some((intent) => intent.id === action.cleanupIntentId &&
354
+ intent.instanceId === action.instanceId && intent.runtimeGeneration === action.runtimeGeneration &&
355
+ targetMatches(intent.target, target))) continue;
356
+ }
357
+ if (record.status === "active" || record.status === "starting" || record.status === "pending" || record.status === "probe-required") return true;
358
+ }
359
+ for (const reservation of store.listReservations?.() ?? []) {
360
+ if (!targetMatches(reservation.target, target)) continue;
361
+ if (action.kind !== "close-delete-reserved-topic" || !reservations.some((initial) => sameSnapshot(initial, reservation))) return true;
362
+ }
363
+ for (const provision of store.listPendingProvisions?.() ?? []) {
364
+ if (!provision.target || !targetMatches(provision.target, target)) continue;
365
+ if (action.kind !== "close-delete-expired-pending-provision-topic" || provision.id !== action.pendingProvisionId ||
366
+ !provisions.some((initial) => sameSnapshot(initial, provision))) return true;
367
+ }
368
+ return false;
369
+ };
370
+ }
371
+
319
372
  export interface TelegramTopicTargetStoreOptions {
320
373
  path: string | (() => string);
321
374
  getNowMs?: () => number;
@@ -1230,7 +1283,11 @@ export function createTelegramTopicTargetStore(
1230
1283
  loaded = true;
1231
1284
  return;
1232
1285
  }
1286
+ const revision = mutationRevision;
1233
1287
  const content = await readFile(path, "utf8");
1288
+ // A read begun before a local mutation must not replace the newly admitted
1289
+ // binding/cleanup state with its older disk snapshot.
1290
+ if (mutationRevision !== revision || getPath() !== path) return;
1234
1291
  const rawFile: unknown = JSON.parse(content);
1235
1292
  const file = parseTopicTargetFile(rawFile);
1236
1293
  followerRecoveryHints = parseFollowerRecoveryHints(rawFile);
@@ -1293,25 +1350,20 @@ export function createTelegramTopicTargetStore(
1293
1350
  mutationRevision += 1;
1294
1351
  };
1295
1352
 
1296
- return {
1297
- async load() {
1298
- if (dirty) return;
1299
- await loadFromDisk();
1300
- },
1301
- refresh() {
1302
- const refresh = persistQueue.then(loadFromDisk);
1303
- persistQueue = refresh.catch(() => undefined);
1304
- return refresh;
1305
- },
1306
- persist() {
1353
+ const persistSnapshot = (invalidation?: {
1354
+ target: TelegramTarget;
1355
+ isCurrent: () => boolean;
1356
+ lastSyncError: string;
1357
+ }): Promise<boolean> => {
1307
1358
  const persist = persistQueue.then(async () => {
1308
1359
  const path = getPath();
1309
1360
  if (loadedPath !== path && !dirty) resetForPath(path);
1310
1361
  if (options.canPersist && !options.canPersist()) {
1311
- await loadFromDisk();
1312
- return;
1362
+ if (!invalidation) await loadFromDisk();
1363
+ return false;
1313
1364
  }
1314
1365
  if (!dirty || !loaded) await loadFromDisk();
1366
+ if (invalidation && !invalidation.isCurrent()) return false;
1315
1367
  await mkdir(dirname(path), { recursive: true });
1316
1368
  const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
1317
1369
  const nowMs = getNowMs();
@@ -1359,6 +1411,21 @@ export function createTelegramTopicTargetStore(
1359
1411
  return serialized;
1360
1412
  }),
1361
1413
  };
1414
+ if (invalidation) {
1415
+ const record = file.threads.find((record) => targetMatches(record.target, invalidation.target));
1416
+ if (!record) return false;
1417
+ file.threads = file.threads.filter((candidate) => candidate !== record);
1418
+ file.syncObservations = file.syncObservations.filter((observation) => !targetMatches(observation.target, record.target));
1419
+ file.syncObservations.push({
1420
+ target: { ...record.target },
1421
+ syncStatus: "deleted",
1422
+ observedAtMs: nowMs,
1423
+ ...(record.instanceId ? { instanceId: record.instanceId } : {}),
1424
+ ...(record.slot ? { slot: record.slot } : {}),
1425
+ lastSyncError: invalidation.lastSyncError,
1426
+ lastReconcileAction: "mark-stale",
1427
+ });
1428
+ }
1362
1429
  let persistedSemanticSnapshot: string | undefined;
1363
1430
  try {
1364
1431
  persistedSemanticSnapshot = serializeTelegramStateSemanticSnapshot(
@@ -1375,7 +1442,7 @@ export function createTelegramTopicTargetStore(
1375
1442
  ) {
1376
1443
  loaded = true;
1377
1444
  dirty = false;
1378
- return;
1445
+ return true;
1379
1446
  }
1380
1447
  await writeFile(tempPath, `${JSON.stringify(file, null, 2)}\n`, {
1381
1448
  encoding: "utf8",
@@ -1383,6 +1450,25 @@ export function createTelegramTopicTargetStore(
1383
1450
  });
1384
1451
  await chmod(tempPath, 0o600);
1385
1452
  try {
1453
+ if (invalidation) {
1454
+ let applied = false;
1455
+ const commit = () => {
1456
+ if (getPath() !== path || mutationRevision !== persistedRevision ||
1457
+ statusRevision !== persistedStatusRevision || !invalidation.isCurrent()) return;
1458
+ // Fence and rename share one synchronous commit boundary. No stale
1459
+ // invalidation enters the live projection before durable commit.
1460
+ renameSync(tempPath, path);
1461
+ records = new Map(Array.from(records).filter(([, record]) => !targetMatches(record.target, invalidation.target)));
1462
+ syncObservations = file.syncObservations;
1463
+ mutationRevision += 1;
1464
+ dirty = false;
1465
+ applied = true;
1466
+ };
1467
+ if (options.commitPersist) options.commitPersist(commit);
1468
+ else if (!options.canPersist || options.canPersist()) commit();
1469
+ if (!applied) await unlink(tempPath).catch(() => undefined);
1470
+ return applied;
1471
+ }
1386
1472
  if (options.commitPersist) {
1387
1473
  const committed = options.commitPersist(() => {
1388
1474
  renameSync(tempPath, path);
@@ -1404,9 +1490,27 @@ export function createTelegramTopicTargetStore(
1404
1490
  }
1405
1491
  loaded = true;
1406
1492
  if (mutationRevision === persistedRevision) dirty = false;
1493
+ return true;
1407
1494
  });
1408
- persistQueue = persist.catch(() => undefined);
1495
+ persistQueue = persist.then(() => undefined, () => undefined);
1409
1496
  return persist;
1497
+ };
1498
+
1499
+ return {
1500
+ async load() {
1501
+ if (dirty) return;
1502
+ await loadFromDisk();
1503
+ },
1504
+ refresh() {
1505
+ const refresh = persistQueue.then(loadFromDisk);
1506
+ persistQueue = refresh.catch(() => undefined);
1507
+ return refresh;
1508
+ },
1509
+ async persist() {
1510
+ await persistSnapshot();
1511
+ },
1512
+ invalidateTarget(target, isCurrent, lastSyncError) {
1513
+ return persistSnapshot({ target, isCurrent, lastSyncError });
1410
1514
  },
1411
1515
  list() {
1412
1516
  return Array.from(records.values()).map(cloneRecord);
@@ -2100,6 +2204,7 @@ export async function provisionOwnBusTopic(
2100
2204
  if (typeof chatId !== "number") return undefined;
2101
2205
  await deps.store.load();
2102
2206
  const reservationCleanupPorts = {
2207
+ isCleanupTargetProtected: createTelegramCleanupTargetProtection(deps.store),
2103
2208
  callApi: deps.callApi,
2104
2209
  markStaleByTarget: (
2105
2210
  target: TelegramTarget & { threadId: number },
@@ -2253,9 +2358,11 @@ export async function provisionOwnBusTopic(
2253
2358
  continue;
2254
2359
  }
2255
2360
  const previousLeaderCleanupStartedAtMs = Date.now();
2361
+ const isCleanupTargetProtected = createTelegramCleanupTargetProtection(deps.store, record);
2256
2362
  const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(
2257
2363
  { actions: [action] },
2258
2364
  {
2365
+ isCleanupTargetProtected,
2259
2366
  callApi: deps.callApi,
2260
2367
  markStaleByTarget: (target, syncStatus, lastSyncError) =>
2261
2368
  deps.store.markStaleByTarget(target, syncStatus, lastSyncError),
@@ -2304,6 +2411,7 @@ export async function provisionOwnBusTopic(
2304
2411
  "Previous Telegram leader topic deletion was not confirmed.",
2305
2412
  );
2306
2413
  }
2414
+ if (isCleanupTargetProtected(action.target, action)) continue;
2307
2415
  deps.store.markStaleByTarget(record.target);
2308
2416
  deps.store.reserveThread({
2309
2417
  target: record.target,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.42.1",
3
+ "version": "0.42.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -39,7 +39,7 @@ Reply in concise, phone-width Telegram Rich Markdown. Use `$...$` and `$$...$$`
39
39
 
40
40
  ## Assistant Actions
41
41
 
42
- `telegram_button` and `telegram_voice` are hidden HTML comments, not tools. Emit each complete comment at column zero, outside lists, quotes, code blocks, and indentation.
42
+ `telegram_button` and `telegram_voice` are hidden HTML comments, not tools. Emit each complete action comment at column zero, outside lists, quotes, code blocks, and indentation. Telegram removes every assistant-authored HTML comment from previews and final replies regardless of owner or Markdown position, but only recognized top-level comments activate actions; comment-only output sends no text message.
43
43
 
44
44
  ### Shared Encoding Rule
45
45
 
@@ -4,11 +4,13 @@ Read this reference only when diagnosing Telegram bridge health or delivery fail
4
4
 
5
5
  Inspect in this order:
6
6
 
7
- 1. `telegram-status` for compact health.
8
- 2. `telegram-status --debug` for bounded human-readable diagnostics.
7
+ 1. `/telegram-status` for compact health in the Pi TUI.
8
+ 2. `/telegram-status --debug` for bounded human-readable diagnostics in the Pi TUI.
9
9
  3. `~/.pi/agent/tmp/telegram/state.json` and `logs.jsonl` for default-profile redacted evidence.
10
10
  4. `state.<profile>.json` and `logs.<profile>.jsonl` for a named profile.
11
11
 
12
+ These slash commands are registered Pi commands, not shell executables or agent tools. If the agent cannot invoke them through a supported Pi surface, read the diagnostic files directly; do not run them in Bash or inject terminal input.
13
+
12
14
  When `PI_CODING_AGENT_DIR` selects another compatible runtime, resolve its equivalent `tmp/telegram` directory.
13
15
 
14
16
  Do not mutate ownership files, bridge state, journals, bindings, or locks to force recovery. Use supported commands and preserve exact profile, target, transport, and session authority. Never claim successful delivery without transport evidence.
@@ -0,0 +1,40 @@
1
+ # ABCd Context Project Context
2
+
3
+ ## Meta-Protocol Principles
4
+
5
+ - `Self-Reference`: This skill validates its own context graph with its own runtimes and regression fixture.
6
+ - `Root State Separation`: Durable protocol, open work, completed delivery, README entrypoints, and subsystem docs keep distinct ownership.
7
+ - `Constraint-Driven Evolution`: Structure grows from observed coordination pressure, not template ceremony.
8
+ - `Project Neutrality`: Portable rules remain independent of repository stacks and local overlays.
9
+ - `Single Runtime`: The Node validator provides one authoritative behavior and supported execution path.
10
+ - `Cross-Platform`: Validator behavior runs wherever the supported Node runtime runs.
11
+
12
+ ## Operating Principles
13
+
14
+ - Keep `SKILL.md` as the operating kernel; place resolution, lifecycle, and consolidation detail in `docs/protocols.md`.
15
+ - Keep `BACKLOG.md` limited to real open, gated, or blocked work and `CHANGELOG.md` limited to completed outcomes.
16
+ - Keep root `README.md` connected to `AGENTS.md`, `BACKLOG.md`, `CHANGELOG.md`, and `docs/README.md` when those surfaces exist.
17
+ - Keep subtree README entrypoints reachable once they become real human starting points.
18
+ - Use `scripts/validate-context.mjs` for audits and `scripts/_self-test.mjs` for regression.
19
+ - Treat warnings as evidence requiring judgment, not as automatic failure or automatic approval.
20
+
21
+ ## Protocol Constraints
22
+
23
+ - `Farmville Guard`: Skip context mutation when it would not preserve truth, prevent drift, record meaningful delivery, or repair discoverability.
24
+ - `Activation Symmetry`: Default post-task activation performs post-task reconciliation; it must not imply retroactive pre-task ceremony.
25
+ - `Template Proportionality`: Lean projects receive lean structure; mature hierarchy requires real complexity.
26
+ - `State Ownership`: The same reality must not remain simultaneously durable, open, and completed.
27
+ - `README Continuity`: Setup, usage, topology, ownership, and same-domain entrypoint knowledge belong in the nearest relevant README.
28
+ - `Impact-Oriented History`: Delivery history records outcomes and impact rather than iteration bookkeeping.
29
+ - `Natural-Language Operation`: Do not introduce fictional YAML tracking or formalize obvious senior-engineer behavior without a non-obvious contract.
30
+ - `Local Overlay Boundary`: Project-specific release, architecture, security, and stack gates remain in local overlays.
31
+
32
+ ## Validator Constraints
33
+
34
+ - `Single Runtime`: Keep `validate-context.mjs` as the only supported validator implementation.
35
+ - `Anchor Validation`: Preserve underscores while normalizing GitHub-style heading anchors and validate line-reference bounds.
36
+ - `Path Portability`: Resolve paths through Node rather than platform-specific shell utilities.
37
+ - `Human Output`: Emit concise readable logs and a summary; `NO_COLOR=1` suppresses color.
38
+ - `Bounded Scanning`: Skip link scanning above the configured byte threshold while validating the surrounding graph.
39
+ - `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
+ - `Core Shape Flexibility`: Accept both numbered mature-project sections and compact skill-style durable sections.
@@ -0,0 +1,3 @@
1
+ # ABCd Context Backlog
2
+
3
+ No open backlog items.
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 1.5.3
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.
6
+
7
+ ## 1.5.2
8
+
9
+ - `Human-First Validation`: The validator now has one Node implementation and prints classic readable logs by default; JSON output and the Bash runtime are removed. Impact: direct local use and agent calls receive the same concise report without format switches.
10
+ - `Line-Reference Validation`: GitHub-style line-reference links now fail when their target line is absent. Impact: broken Markdown references cannot silently pass validation.
11
+
12
+ ## 1.5.1
13
+
14
+ - `JSON-First Validation`: Bash and Node validators now emit stable JSON by default while preserving explicit `--text` output and the existing `--json` alias. Impact: agent, recipe, and CI callers receive machine-readable results without extra flags.
15
+
16
+ ## 1.0.20
17
+
18
+ - `Protocol Cohesion`: Consolidated the operating kernel, deep protocols, templates, project context, and human entrypoint around one context ownership model. Impact: activation, routing, reconciliation, and validation now form one proportional workflow without duplicated ceremony.
19
+ - `Self-Reference`: Added the skill's own completed-history surface and expanded regression coverage to validate both the fixture and the skill root across Bash and Node runtimes. Impact: the skill now exercises the context graph it prescribes.
@@ -0,0 +1,30 @@
1
+ # ABCd Context
2
+
3
+ Portable context protocol for keeping durable rules, open work, completed delivery, human entrypoints, and project docs truthful without generating ceremonial churn.
4
+
5
+ ## Context Graph
6
+
7
+ - `AGENTS.md`: Durable protocol and reusable constraints.
8
+ - `BACKLOG.md`: Canonical remaining work.
9
+ - `CHANGELOG.md`: Completed outcomes and impact.
10
+ - `README.md` tree: Human entrypoints and navigation.
11
+ - `docs/README.md` + `/docs`: Indexed subsystem and contract knowledge.
12
+
13
+ ## Validation
14
+
15
+ The Node validator prints human-readable results wherever the supported Node runtime runs.
16
+
17
+ ```bash
18
+ node ./scripts/validate-context.mjs /path/to/project
19
+ node ./scripts/_self-test.mjs
20
+ ```
21
+
22
+ Warnings require review but do not fail validation. Errors return a nonzero exit.
23
+
24
+ ## Start Here
25
+
26
+ - [Skill Definition](./SKILL.md)
27
+ - [Project Context](./AGENTS.md)
28
+ - [Open Backlog](./BACKLOG.md)
29
+ - [Changelog](./CHANGELOG.md)
30
+ - [Documentation](./docs/README.md)
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: abcd-context
3
+ description: ABCd context protocol across ABC root files (AGENTS.md, BACKLOG.md, CHANGELOG.md), human entrypoint README.md files tree, and /docs. Use after meaningful project changes, backlog drift, documentation refactors, self-evolving context maintenance, or a forced context reconciliation pass.
4
+ ---
5
+
6
+ # ABCd Context
7
+
8
+ Keep project context truthful, separated, connected, and small enough to use. Treat `AGENTS.md`, `BACKLOG.md`, `CHANGELOG.md`, the README tree, and `/docs` as one context graph with distinct ownership.
9
+
10
+ ## Core Guarantee
11
+
12
+ Durable rules stay durable. Open work stays open. Completed delivery moves to history. README entrypoints remain trustworthy. Docs remain discoverable. Each fact has one authoritative home.
13
+
14
+ ## Activation Modes
15
+
16
+ - `POST_TASK` (default): Reconcile context after meaningful reality changed. Do not add pre-task ceremony retroactively.
17
+ - `ON_REQUEST`: Run a full context audit and reconciliation when the user explicitly asks.
18
+ - `ALWAYS_ON` (opt-in): Monitor context continuously only when explicitly requested; otherwise its attention cost exceeds its value.
19
+
20
+ Skip the skill for conversational work, trivial changes, or tasks that produced no durable context change.
21
+
22
+ ## Context Model
23
+
24
+ - `README.md`: Human entrypoint, setup, usage, topology, and navigation.
25
+ - Subtree `README.md`: Local human entrypoint for a real subsystem, workspace, package, or tool.
26
+ - `AGENTS.md`: Durable protocol, constraints, conventions, architecture memory, and recurring operating rules.
27
+ - `BACKLOG.md`: Canonical remaining open, gated, or blocked work. Accepted aliases: `TODO.md`, `PLAN.md`, `ROADMAP.md` when project convention already owns them.
28
+ - `CHANGELOG.md`: Completed delivery and impact, unless the project already has another canonical shipped-history surface.
29
+ - `docs/README.md`: Documentation index.
30
+ - `docs/*`: Product, subsystem, architecture, operations, and contract documentation.
31
+
32
+ Detailed resolution, adaptation, consolidation, and overlay rules live in [`docs/protocols.md`](./docs/protocols.md). Templates live in [`docs/templates.md`](./docs/templates.md).
33
+
34
+ ## Hierarchical Backlog Batching
35
+
36
+ For non-trivial work, model the plan as `outcome or epic → acceptance checkpoint → owned work item`, not one long flat sequence.
37
+
38
+ - Create a checkpoint only when several compatible changes share one meaningful integration boundary and can form a compiling, bisectable candidate. Keep one-shot work flat.
39
+ - Group by coupled invariant, ownership boundary, or acceptance evidence rather than file proximity or arbitrary item count. Keep conflicting transitions, unrelated domains, and externally gated work in separate checkpoints.
40
+ - Give every work item one focused falsification route: a named test family, filter, audit, or explicit evidence owner. A shared test names the smallest item set whose interaction it proves.
41
+ - During edits, prefer focused item validation. Run aggregate package/runtime checks once when the checkpoint candidate forms; reserve full release validation for the highest boundary that actually needs it.
42
+ - Keep dependency order inside a checkpoint only where correctness requires it. Independent items may proceed in parallel without turning the backlog into a false linear pipeline.
43
+ - Treat the checkpoint as an acceptance and context boundary, not a second source of task truth. Child items own concrete remaining work; the checkpoint owns only the shared exit condition.
44
+ - Close or narrow children as reality changes. Remove the checkpoint when its aggregate exit condition passes, and move meaningful impact to delivery history.
45
+
46
+ ## Invariants
47
+
48
+ 1. `Single source of truth`: Do not track the same state across durable protocol, open work, completed history, and docs.
49
+ 2. `Reality before prose`: Inspect the implementation, diff, plans, and existing conventions before updating context.
50
+ 3. `Constraint-driven structure`: Add hierarchy only when discovered constraints justify it; restructure inherited context when accidental shape hides truth.
51
+ 4. `Hierarchical execution`: Group compatible non-trivial work under acceptance checkpoints with attributable item-level evidence; do not force independent work into a false sequence or replay broad gates after every edit.
52
+ 5. `Human entrypoint continuity`: Keep root and relevant subtree README files reachable and current when setup, usage, topology, or ownership changes.
53
+ 6. `Backlog truth`: A completed or narrowed slice must not remain falsely open.
54
+ 7. `Impact-oriented history`: Record meaningful delivered outcomes, not bookkeeping or duplicated commit logs.
55
+ 8. `Local convention compatibility`: Cooperate with stricter project overlays; do not copy project-specific gates into this portable protocol.
56
+ 9. `Farmville guard`: If an edit would not preserve durable wisdom, correct open-work truth, record meaningful delivery, or repair discoverability, skip it. Silence is valid.
57
+
58
+ ## POST_TASK Flow
59
+
60
+ Run one proportional reconciliation pass:
61
+
62
+ 1. `Assess`: Did the task materially change behavior, architecture, public contracts, open-work truth, setup, topology, or reusable operating knowledge? If not, stop.
63
+ 2. `Inspect`: Read the touched context surfaces, relevant diff, canonical plan, nearest README entrypoints, and existing shipped-history convention.
64
+ 3. `Reconcile open work`: Close, narrow, split, retarget, defer, gate, or block affected backlog items so only real remaining work stays open.
65
+ 4. `Route knowledge`:
66
+ - Setup, usage, topology, navigation, ownership → nearest `README.md`.
67
+ - Durable reusable constraint → `AGENTS.md`.
68
+ - Remaining executable work → canonical backlog.
69
+ - Meaningful completed outcome → `CHANGELOG.md` or established equivalent.
70
+ - Product/subsystem/architecture contract → indexed `/docs` document.
71
+ 5. `Consolidate and connect`: Merge duplicates, remove stale statements, preserve one authoritative home, update navigation, and keep docs index coverage honest.
72
+ 6. `Validate`: When context files changed, run the project convention first, otherwise run `validate-context`. Review warnings rather than treating exit code alone as proof.
73
+ 7. `Report`: Name changed context files, validation result, and any unresolved drift. If no update passed the Farmville guard, report that no context mutation was needed.
74
+
75
+ Use a light pass for small scoped changes. Use the full protocol in [`docs/protocols.md`](./docs/protocols.md) for broad context refactors, architecture changes, or multi-surface drift.
76
+
77
+ ## ON_REQUEST Flow
78
+
79
+ 1. Map the current context graph and canonical files.
80
+ 2. Run both automated validation and manual truth checks.
81
+ 3. Compare claims against repository reality.
82
+ 4. Reconcile state ownership, stale content, duplicates, missing entrypoints, links, backlog drift, and docs coverage.
83
+ 5. Restructure only where current shape obstructs navigation or truth.
84
+ 6. Rerun validation and report remaining uncertainty.
85
+
86
+ ## Mutation Rules
87
+
88
+ - Read the existing format before writing and match it when it remains honest.
89
+ - Prefer targeted reconciliation over broad rewrites.
90
+ - Use templates for new/empty files and as restructuring targets for drifted files, not as mandatory ceremony.
91
+ - Keep prose unwrapped unless project tooling requires wrapping.
92
+ - Prefer label bullets over definition tables.
93
+ - Do not create missing root files automatically when project convention is ambiguous; ask or preserve the established equivalent.
94
+ - Do not claim an item completed from documentation alone; verify reality.
95
+ - Do not let generated context exceed the mistakes or navigation cost it prevents.
96
+
97
+ ## Validation
98
+
99
+ Run the Node validator from the project root or pass an explicit root:
100
+
101
+ ```bash
102
+ node "${SKILL_DIR}/scripts/validate-context.mjs" /path/to/project
103
+ ```
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.
106
+
107
+ Useful environment controls:
108
+
109
+ - `VALIDATE_CONTEXT_ROOT=/path` as an environment fallback.
110
+ - `NO_COLOR=1` for logs consumed by CI or agents.
111
+
112
+ Exit `0` means no errors; warnings may still require judgment. Exit `1` means validation errors or invalid invocation. Validator design and check semantics live in [`docs/validation-design.md`](./docs/validation-design.md).
113
+
114
+ ## Completion Checklist
115
+
116
+ - [ ] Canonical open work matches reality.
117
+ - [ ] Completed delivery left the backlog and reached the canonical history surface when meaningful.
118
+ - [ ] Durable constraints live in `AGENTS.md`, not delivery history.
119
+ - [ ] Relevant README entrypoints still explain and connect the changed area.
120
+ - [ ] Docs describe current contracts and remain indexed.
121
+ - [ ] Duplicate or stale context was removed rather than layered over.
122
+ - [ ] Automated validation ran after context mutation, and warnings were reviewed.
@@ -0,0 +1,9 @@
1
+ # Documentation Index (abcd-context)
2
+
3
+ Living index of all documentation in the `/docs` directory for the ABCd context protocol (`ABC + README tree + /docs`).
4
+
5
+ ## Documents
6
+
7
+ - [`protocols.md`](./protocols.md): Adaptation rules, lifecycle management, and validation checklist.
8
+ - [`templates.md`](./templates.md): Entry templates for index files, docs, and changelog.
9
+ - [`validation-design.md`](./validation-design.md): Design and checks of `validate-context`.