@llblab/pi-kit 0.5.2 → 0.6.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.
- package/CHANGELOG.md +4 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
- package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
- package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +0 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +9 -5
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +8 -2
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -0
- package/node_modules/@llblab/pi-telegram/index.ts +22 -19
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +19 -5
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +9 -1
- package/node_modules/@llblab/pi-telegram/lib/config.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -1
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +23 -30
- package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +28 -42
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +18 -14
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +115 -70
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +18 -8
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +46 -38
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +36 -3
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
- package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
- package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
- package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
- package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
- package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
- package/node_modules/@llblab/skills/package.json +1 -1
- package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
- package/package.json +4 -4
|
@@ -1498,6 +1498,10 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
1498
1498
|
updateStatus: () => void;
|
|
1499
1499
|
dispatchNextQueuedTelegramTurn: () => void;
|
|
1500
1500
|
scheduleActiveTurnDelivery?: (task: () => Promise<void>) => void;
|
|
1501
|
+
preparePreviewDelivery?: (isDeliveryActive: () => boolean) => Pick<
|
|
1502
|
+
TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>,
|
|
1503
|
+
"clearPreview" | "setPreviewPendingText" | "finalizeMarkdownPreview"
|
|
1504
|
+
> | undefined;
|
|
1501
1505
|
preparePreviewClear?: (
|
|
1502
1506
|
chatId: number,
|
|
1503
1507
|
options?: { target?: TelegramQueueTarget; isDeliveryActive?: () => boolean },
|
|
@@ -1595,6 +1599,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
1595
1599
|
schedule: (task: () => Promise<void>) => void;
|
|
1596
1600
|
cancel: () => void;
|
|
1597
1601
|
};
|
|
1602
|
+
preparePreviewDelivery?: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["preparePreviewDelivery"];
|
|
1598
1603
|
preparePreviewClear?: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["preparePreviewClear"];
|
|
1599
1604
|
clearPreview: TelegramAgentEndRuntimeDeps<
|
|
1600
1605
|
TTurn,
|
|
@@ -1746,6 +1751,7 @@ export function createTelegramAgentEndHook<
|
|
|
1746
1751
|
await task();
|
|
1747
1752
|
})
|
|
1748
1753
|
: undefined,
|
|
1754
|
+
preparePreviewDelivery: deps.preparePreviewDelivery,
|
|
1749
1755
|
preparePreviewClear: deps.preparePreviewClear,
|
|
1750
1756
|
clearPreview: deps.clearPreview,
|
|
1751
1757
|
setPreviewPendingText: deps.setPreviewPendingText,
|
|
@@ -1807,9 +1813,13 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1807
1813
|
const isDeliveryActive = (): boolean =>
|
|
1808
1814
|
deps.isSessionActive?.() !== false &&
|
|
1809
1815
|
(!turn || deps.isTurnTransportActive?.(turn) !== false);
|
|
1816
|
+
const preview = turn && !turn.guestQueryId ? deps.preparePreviewDelivery?.(isDeliveryActive) : undefined;
|
|
1817
|
+
const setPreviewPendingText = preview?.setPreviewPendingText ?? deps.setPreviewPendingText;
|
|
1818
|
+
const finalizeMarkdownPreview = preview?.finalizeMarkdownPreview ?? deps.finalizeMarkdownPreview;
|
|
1810
1819
|
const clearPreview = turn
|
|
1811
|
-
?
|
|
1812
|
-
|
|
1820
|
+
? preview ? () => preview.clearPreview(turn.chatId, { target: turn.target })
|
|
1821
|
+
: deps.preparePreviewClear?.(turn.chatId, { target: turn.target, isDeliveryActive })
|
|
1822
|
+
?? (() => deps.clearPreview(turn.chatId, { target: turn.target }))
|
|
1813
1823
|
: undefined;
|
|
1814
1824
|
const updateStatusIgnoringStaleContext = (): void => {
|
|
1815
1825
|
try {
|
|
@@ -1914,7 +1924,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1914
1924
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1915
1925
|
return;
|
|
1916
1926
|
}
|
|
1917
|
-
if (finalText)
|
|
1927
|
+
if (finalText) setPreviewPendingText(finalText);
|
|
1918
1928
|
|
|
1919
1929
|
if (!isDeliveryActive()) return;
|
|
1920
1930
|
let richAttachmentDelivered = false;
|
|
@@ -1932,9 +1942,9 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1932
1942
|
);
|
|
1933
1943
|
if (!isDeliveryActive()) return;
|
|
1934
1944
|
if (richAttachmentDelivered) {
|
|
1935
|
-
await
|
|
1945
|
+
await clearPreview?.();
|
|
1936
1946
|
if (!isDeliveryActive()) return;
|
|
1937
|
-
|
|
1947
|
+
setPreviewPendingText("");
|
|
1938
1948
|
}
|
|
1939
1949
|
} catch (error) {
|
|
1940
1950
|
if (!isDeliveryActive()) return;
|
|
@@ -1949,7 +1959,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1949
1959
|
if (!isDeliveryActive()) return;
|
|
1950
1960
|
if (!richAttachmentDelivered && endPlan.kind === "text" && finalText) {
|
|
1951
1961
|
try {
|
|
1952
|
-
const finalized = await
|
|
1962
|
+
const finalized = await finalizeMarkdownPreview(
|
|
1953
1963
|
turn.chatId,
|
|
1954
1964
|
finalText,
|
|
1955
1965
|
turn.replyToMessageId,
|
|
@@ -1957,7 +1967,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1957
1967
|
);
|
|
1958
1968
|
if (!isDeliveryActive()) return;
|
|
1959
1969
|
if (!finalized) {
|
|
1960
|
-
await
|
|
1970
|
+
await clearPreview?.();
|
|
1961
1971
|
if (!isDeliveryActive()) return;
|
|
1962
1972
|
await deps.sendMarkdownReply(
|
|
1963
1973
|
turn.chatId,
|
|
@@ -1967,7 +1977,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1967
1977
|
);
|
|
1968
1978
|
}
|
|
1969
1979
|
if (!isDeliveryActive()) return;
|
|
1970
|
-
|
|
1980
|
+
setPreviewPendingText("");
|
|
1971
1981
|
} catch (error) {
|
|
1972
1982
|
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1973
1983
|
phase: "final-text",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getTelegramTargetThreadParams,
|
|
10
10
|
type TelegramTarget,
|
|
11
11
|
} from "./target.ts";
|
|
12
|
+
import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
|
|
12
13
|
import type {
|
|
13
14
|
TelegramInputRichMessage,
|
|
14
15
|
TelegramReplyParameters,
|
|
@@ -67,6 +68,7 @@ export function createReplyDedupRuntime(): ReplyDedupRuntime {
|
|
|
67
68
|
// --- Transport-level dedup ---
|
|
68
69
|
|
|
69
70
|
const lastRepliedToMessageIdByTarget = new Map<string, number>();
|
|
71
|
+
let replyDedupGeneration = 0;
|
|
70
72
|
|
|
71
73
|
function getReplyDedupTargetKey(
|
|
72
74
|
chatId: number,
|
|
@@ -79,6 +81,7 @@ function getReplyDedupTargetKey(
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export function resetTransportReplyDedup(): void {
|
|
84
|
+
replyDedupGeneration += 1;
|
|
82
85
|
lastRepliedToMessageIdByTarget.clear();
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -99,13 +102,29 @@ export function buildTelegramReplyParameters(
|
|
|
99
102
|
};
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
// Answer publications are caller-serialized. A rejected send releases its anchor;
|
|
106
|
+
// an uncertain ACK retains it because Telegram may already have delivered it.
|
|
107
|
+
export async function withTelegramReplyParameters<T>(
|
|
103
108
|
chatId: number,
|
|
104
109
|
messageId: number | undefined,
|
|
105
|
-
target
|
|
106
|
-
|
|
110
|
+
target: TelegramTarget | undefined,
|
|
111
|
+
send: (parameters: TelegramReplyParameters | undefined) => Promise<T>,
|
|
112
|
+
): Promise<T> {
|
|
113
|
+
const key = getReplyDedupTargetKey(chatId, target);
|
|
114
|
+
const generation = replyDedupGeneration;
|
|
115
|
+
const previous = lastRepliedToMessageIdByTarget.get(key);
|
|
107
116
|
const parameters = buildTelegramReplyParameters(chatId, messageId, target);
|
|
108
|
-
|
|
117
|
+
try {
|
|
118
|
+
return await send(parameters);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (parameters && !isTelegramApiCommitUnknownError(error)
|
|
121
|
+
&& generation === replyDedupGeneration
|
|
122
|
+
&& lastRepliedToMessageIdByTarget.get(key) === messageId) {
|
|
123
|
+
if (previous === undefined) lastRepliedToMessageIdByTarget.delete(key);
|
|
124
|
+
else lastRepliedToMessageIdByTarget.set(key, previous);
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
function getAgentMessageField(message: unknown, field: string): unknown {
|
|
@@ -239,24 +258,18 @@ export async function sendTelegramRenderedChunks<TReplyMarkup>(
|
|
|
239
258
|
assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
|
|
240
259
|
let lastMessageId: number | undefined;
|
|
241
260
|
for (const [index, chunk] of chunks.entries()) {
|
|
242
|
-
const
|
|
243
|
-
index === 0
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
reply_markup:
|
|
255
|
-
index === chunks.length - 1 ? options?.replyMarkup : undefined,
|
|
256
|
-
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
257
|
-
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
258
|
-
};
|
|
259
|
-
const sent = await deps.sendMessage(body);
|
|
261
|
+
const sent = await withTelegramReplyParameters(
|
|
262
|
+
chatId, index === 0 ? options?.replyToMessageId : undefined, options?.target,
|
|
263
|
+
(replyParameters) => deps.sendMessage({
|
|
264
|
+
chat_id: chatId,
|
|
265
|
+
text: chunk.text,
|
|
266
|
+
parse_mode: chunk.parseMode,
|
|
267
|
+
reply_markup:
|
|
268
|
+
index === chunks.length - 1 ? options?.replyMarkup : undefined,
|
|
269
|
+
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
270
|
+
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
260
273
|
lastMessageId = sent.message_id;
|
|
261
274
|
deps.recordOwnership?.({
|
|
262
275
|
chatId,
|
|
@@ -655,22 +668,17 @@ export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
|
|
|
655
668
|
let lastMessageId: number | undefined;
|
|
656
669
|
const chunks = splitTelegramNativeMarkdown(markdown);
|
|
657
670
|
for (const [index, chunk] of chunks.entries()) {
|
|
658
|
-
const
|
|
659
|
-
index === 0
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
:
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
reply_markup:
|
|
670
|
-
index === chunks.length - 1 ? options?.replyMarkup : undefined,
|
|
671
|
-
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
672
|
-
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
673
|
-
});
|
|
671
|
+
const sent = await withTelegramReplyParameters(
|
|
672
|
+
chatId, index === 0 ? replyToMessageId : undefined, options?.target,
|
|
673
|
+
(replyParameters) => deps.sendRichMessage({
|
|
674
|
+
chat_id: chatId,
|
|
675
|
+
rich_message: { markdown: chunk, skip_entity_detection: true },
|
|
676
|
+
reply_markup:
|
|
677
|
+
index === chunks.length - 1 ? options?.replyMarkup : undefined,
|
|
678
|
+
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
679
|
+
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
680
|
+
}),
|
|
681
|
+
);
|
|
674
682
|
lastMessageId = sent.message_id;
|
|
675
683
|
deps.recordOwnership?.({
|
|
676
684
|
chatId,
|
|
@@ -1783,11 +1783,44 @@ export function createTelegramBridgeApiRuntime(
|
|
|
1783
1783
|
*/
|
|
1784
1784
|
export function createTelegramApiClient(
|
|
1785
1785
|
getBotToken: () => string | undefined,
|
|
1786
|
-
options: TelegramAnswerCallbackQueryOptions = {},
|
|
1786
|
+
options: TelegramAnswerCallbackQueryOptions & { now?: () => number } = {},
|
|
1787
1787
|
): TelegramApiClient {
|
|
1788
|
+
const now = options.now ?? Date.now;
|
|
1789
|
+
const draftRetryNotBeforeByTarget = new Map<string, number>();
|
|
1788
1790
|
return {
|
|
1789
|
-
call: async (
|
|
1790
|
-
|
|
1791
|
+
call: async <TResponse>(
|
|
1792
|
+
method: string,
|
|
1793
|
+
body: Record<string, unknown>,
|
|
1794
|
+
options?: TelegramApiCallOptions,
|
|
1795
|
+
): Promise<TResponse> => {
|
|
1796
|
+
const token = getBotToken();
|
|
1797
|
+
// Cooldown keys retain only the public bot-id prefix, not the credential.
|
|
1798
|
+
const botId = token?.match(/^(\d+):/)?.[1];
|
|
1799
|
+
const isDraft = method === "sendMessageDraft" || method === "sendRichMessageDraft";
|
|
1800
|
+
const draftKey = isDraft && botId
|
|
1801
|
+
? `${botId}:${String(body.chat_id)}:${String(body.message_thread_id ?? "all")}`
|
|
1802
|
+
: undefined;
|
|
1803
|
+
if (draftKey) {
|
|
1804
|
+
const nowMs = now();
|
|
1805
|
+
for (const [key, deadline] of draftRetryNotBeforeByTarget) {
|
|
1806
|
+
if (nowMs >= deadline) draftRetryNotBeforeByTarget.delete(key);
|
|
1807
|
+
}
|
|
1808
|
+
if (draftRetryNotBeforeByTarget.has(draftKey)) return false as TResponse;
|
|
1809
|
+
}
|
|
1810
|
+
try {
|
|
1811
|
+
// A draft is a replaceable snapshot, not a body to replay after backoff.
|
|
1812
|
+
return await callTelegram<TResponse>(
|
|
1813
|
+
token, method, body, isDraft ? { ...options, maxAttempts: 1 } : options,
|
|
1814
|
+
);
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
if (draftKey && isRetryableTelegramApiError(error)) {
|
|
1817
|
+
draftRetryNotBeforeByTarget.set(draftKey, Math.max(
|
|
1818
|
+
draftRetryNotBeforeByTarget.get(draftKey) ?? 0,
|
|
1819
|
+
now() + getTelegramRetryDelayMs(error, 0, options?.retryBaseDelayMs ?? 500),
|
|
1820
|
+
));
|
|
1821
|
+
}
|
|
1822
|
+
throw error;
|
|
1823
|
+
}
|
|
1791
1824
|
},
|
|
1792
1825
|
callMultipart: async (
|
|
1793
1826
|
method,
|
|
@@ -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.
|
|
3
|
+
## 1.14.1
|
|
4
4
|
|
|
5
|
-
- `Markdown
|
|
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
|
|
32
|
-
10. `
|
|
33
|
-
11. `
|
|
34
|
-
12. `Docs
|
|
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
|
|
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) {
|
|
@@ -210,7 +210,6 @@ After the hard gate passes:
|
|
|
210
210
|
1. Reconfirm immediately before push that local `dev` and `origin/dev` remain absent. Stop if either now exists or the remote check fails.
|
|
211
211
|
2. Push the release commit from local `main` to `origin/main` without creating a pull request.
|
|
212
212
|
3. Verify that `origin/main` resolves to the pushed local `main` commit.
|
|
213
|
-
|
|
214
213
|
4. Confirm the version on `main` matches the intended release version and that the current `main` commit is the released commit on `origin/main`.
|
|
215
214
|
5. Create and push exactly one release tag for the confirmed version on the current `main` commit:
|
|
216
215
|
|
|
@@ -268,9 +267,8 @@ npm view <package-name> version
|
|
|
268
267
|
Treat an explicit npm not-found response as package absence. Authentication, authorization, network, registry-resolution, and other lookup failures are not absence; stop and report them. Automation-owned npm publication already performed this lookup before the hard gate and must not defer it until after tagging.
|
|
269
268
|
|
|
270
269
|
15. Publish only when both conditions are true:
|
|
271
|
-
|
|
272
|
-
- The
|
|
273
|
-
- The `main` version matches the intended release version and is newer than the npm version established before publication.
|
|
270
|
+
- The package already exists on npm.
|
|
271
|
+
- The `main` version matches the intended release version and is newer than the npm version established before publication.
|
|
274
272
|
|
|
275
273
|
When repository automation owns npm publication, never run `npm publish` manually. After all owning tag workflows succeed, require `npm view <package-name>@<version> version` to resolve to the intended version; stop if the package remains absent or mismatched.
|
|
276
274
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"@llblab/pi-actors": "0.52.1",
|
|
44
44
|
"@llblab/pi-clean-room": "0.1.1",
|
|
45
45
|
"@llblab/pi-codex-usage": "0.9.4",
|
|
46
|
-
"@llblab/pi-grow-loop": "0.7.
|
|
46
|
+
"@llblab/pi-grow-loop": "0.7.5",
|
|
47
47
|
"@llblab/pi-state-flow": "0.3.0",
|
|
48
|
-
"@llblab/pi-telegram": "0.
|
|
49
|
-
"@llblab/skills": "1.14.
|
|
48
|
+
"@llblab/pi-telegram": "0.44.0",
|
|
49
|
+
"@llblab/skills": "1.14.1"
|
|
50
50
|
},
|
|
51
51
|
"bundledDependencies": [
|
|
52
52
|
"@llblab/pi-actors",
|