@tekmidian/pai 0.43.0 → 0.45.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/dist/{chain-CEC1a5mX.mjs → chain-DAEC9AU7.mjs} +2 -2
- package/dist/{chain-CEC1a5mX.mjs.map → chain-DAEC9AU7.mjs.map} +1 -1
- package/dist/cli/index.mjs +5 -5
- package/dist/cli/program.mjs +5 -5
- package/dist/daemon/index.mjs +4 -4
- package/dist/{daemon-BhldpFvO.mjs → daemon-DNBEFHaj.mjs} +6 -6
- package/dist/{daemon-BhldpFvO.mjs.map → daemon-DNBEFHaj.mjs.map} +1 -1
- package/dist/{daemon-QIvCXB5f.mjs → daemon-Dq2ZSdex.mjs} +4 -4
- package/dist/daemon-mcp/index.mjs +3 -3
- package/dist/{fallback-BQJ8PbCK.mjs → fallback-CxGDTXWt.mjs} +2 -2
- package/dist/{fallback-BQJ8PbCK.mjs.map → fallback-CxGDTXWt.mjs.map} +1 -1
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +28 -12
- package/dist/hooks/load-project-context.mjs.map +2 -2
- package/dist/hooks/mcp-deferred-reminder.mjs.map +1 -1
- package/dist/hooks/post-compact-inject.mjs.map +2 -2
- package/dist/{planner-BlKGgpWI.mjs → planner-B4DvNpXd.mjs} +3 -3
- package/dist/{planner-BlKGgpWI.mjs.map → planner-B4DvNpXd.mjs.map} +1 -1
- package/dist/{program-C5jRQ1kg.mjs → program-DvuMelhj.mjs} +645 -55
- package/dist/program-DvuMelhj.mjs.map +1 -0
- package/dist/{run-ByBKowN5.mjs → run-BgExF8Xg.mjs} +16 -4
- package/dist/{run-ByBKowN5.mjs.map → run-BgExF8Xg.mjs.map} +1 -1
- package/dist/{tools-DiRC5glA.mjs → tools-B77WHeVZ.mjs} +2 -2
- package/dist/{tools-DiRC5glA.mjs.map → tools-B77WHeVZ.mjs.map} +1 -1
- package/dist/{tools-CAG2sUjf.mjs → tools-NnR_n-Ja.mjs} +2 -2
- package/dist/{wakeup-BfElduEU.mjs → wakeup-UrhiW-KU.mjs} +27 -14
- package/dist/wakeup-UrhiW-KU.mjs.map +1 -0
- package/docs/commands/README.md +3 -1
- package/docs/commands/audit.md +25 -2
- package/package.json +1 -1
- package/src/hooks/ts/lib/context-fill.test.ts +40 -0
- package/src/hooks/ts/lib/context-fill.ts +12 -6
- package/src/hooks/ts/session-start/load-project-context.ts +8 -4
- package/src/hooks/ts/session-start/mcp-deferred-reminder.ts +2 -2
- package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +0 -1
- package/dist/hooks/session-commands.mjs +0 -26
- package/dist/hooks/session-commands.mjs.map +0 -7
- package/dist/program-C5jRQ1kg.mjs.map +0 -1
- package/dist/wakeup-BfElduEU.mjs.map +0 -1
- package/src/hooks/ts/session-start/session-commands.ts +0 -30
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./embeddings-DvRKcQyt.mjs";
|
|
2
2
|
import "./search-B69cQwDB.mjs";
|
|
3
|
-
import "./wakeup-
|
|
4
|
-
import { A as formatProject, C as toolProjectInfo, D as toolMemoryGet, E as combineHybridResults, O as toolMemorySearch, S as toolProjectHealth, T as toolProjectTodo, _ as toolZettelThemes, a as toolMemoryWakeup, b as toolSessionRoute, c as toolKgInvalidate, d as toolZettelConverse, f as toolZettelExplore, g as toolZettelSurprise, h as toolZettelSuggest, i as toolMemoryTaxonomy, j as lookupProjectId, k as detectProjectFromPath, l as toolKgQuery, m as toolZettelHealth, n as toolMemoryFeedback, o as toolKgAdd, p as toolZettelGodNotes, r as toolMemoryTunnels, s as toolKgContradictions, t as toolMemoryKgSearch, u as toolZettelCommunities, v as toolRegistrySearch, w as toolProjectList, x as toolProjectDetect, y as toolSessionList } from "./tools-
|
|
3
|
+
import "./wakeup-UrhiW-KU.mjs";
|
|
4
|
+
import { A as formatProject, C as toolProjectInfo, D as toolMemoryGet, E as combineHybridResults, O as toolMemorySearch, S as toolProjectHealth, T as toolProjectTodo, _ as toolZettelThemes, a as toolMemoryWakeup, b as toolSessionRoute, c as toolKgInvalidate, d as toolZettelConverse, f as toolZettelExplore, g as toolZettelSurprise, h as toolZettelSuggest, i as toolMemoryTaxonomy, j as lookupProjectId, k as detectProjectFromPath, l as toolKgQuery, m as toolZettelHealth, n as toolMemoryFeedback, o as toolKgAdd, p as toolZettelGodNotes, r as toolMemoryTunnels, s as toolKgContradictions, t as toolMemoryKgSearch, u as toolZettelCommunities, v as toolRegistrySearch, w as toolProjectList, x as toolProjectDetect, y as toolSessionList } from "./tools-B77WHeVZ.mjs";
|
|
5
5
|
|
|
6
6
|
export { toolKgAdd, toolKgContradictions, toolKgInvalidate, toolKgQuery, toolMemoryTunnels, toolZettelCommunities, toolZettelConverse, toolZettelExplore, toolZettelGodNotes, toolZettelHealth, toolZettelSuggest, toolZettelSurprise, toolZettelThemes };
|
|
@@ -128,28 +128,24 @@ function findSessionNotes(notesDir) {
|
|
|
128
128
|
function normalizeLine(line) {
|
|
129
129
|
return line.replace(/^(- \[[ x]\] |- |\* |\d+\.\s)/, "").trim().replace(/\s+/g, " ");
|
|
130
130
|
}
|
|
131
|
-
function extractKeyLines(content, maxChars, seen = /* @__PURE__ */ new Set()) {
|
|
131
|
+
function extractKeyLines(content, maxChars, seen = /* @__PURE__ */ new Set(), seenLabels = /* @__PURE__ */ new Set()) {
|
|
132
132
|
const lines = content.split("\n");
|
|
133
133
|
const selected = [];
|
|
134
134
|
let inTargetSection = false;
|
|
135
135
|
let currentSection = "";
|
|
136
136
|
let charCount = 0;
|
|
137
|
+
let pendingLabel = null;
|
|
137
138
|
for (const line of lines) {
|
|
138
139
|
const h2Match = line.match(/^## (.+)$/);
|
|
139
140
|
const h3Match = line.match(/^### (.+)$/);
|
|
140
141
|
if (h2Match) {
|
|
141
142
|
currentSection = h2Match[1];
|
|
142
143
|
inTargetSection = EXTRACT_SECTIONS.some((s) => currentSection.toLowerCase().includes(s.toLowerCase()));
|
|
144
|
+
pendingLabel = null;
|
|
143
145
|
continue;
|
|
144
146
|
}
|
|
145
147
|
if (h3Match) {
|
|
146
|
-
if (inTargetSection)
|
|
147
|
-
const label = `[${h3Match[1]}]`;
|
|
148
|
-
if (charCount + label.length < maxChars) {
|
|
149
|
-
selected.push(label);
|
|
150
|
-
charCount += label.length + 1;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
148
|
+
if (inTargetSection) pendingLabel = h3Match[1];
|
|
153
149
|
continue;
|
|
154
150
|
}
|
|
155
151
|
if (!inTargetSection) continue;
|
|
@@ -158,7 +154,19 @@ function extractKeyLines(content, maxChars, seen = /* @__PURE__ */ new Set()) {
|
|
|
158
154
|
if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || trimmed.match(/^\d+\./) || trimmed.startsWith("**")) {
|
|
159
155
|
const normalized = normalizeLine(trimmed);
|
|
160
156
|
if (seen.has(normalized)) continue;
|
|
161
|
-
|
|
157
|
+
let labelText = "";
|
|
158
|
+
let labelKey = "";
|
|
159
|
+
if (pendingLabel !== null) {
|
|
160
|
+
labelKey = pendingLabel.toLowerCase().trim();
|
|
161
|
+
if (!seenLabels.has(labelKey)) labelText = `[${pendingLabel}]`;
|
|
162
|
+
}
|
|
163
|
+
if (charCount + labelText.length + trimmed.length + 2 > maxChars) break;
|
|
164
|
+
if (labelText) {
|
|
165
|
+
selected.push(labelText);
|
|
166
|
+
seenLabels.add(labelKey);
|
|
167
|
+
charCount += labelText.length + 1;
|
|
168
|
+
}
|
|
169
|
+
pendingLabel = null;
|
|
162
170
|
seen.add(normalized);
|
|
163
171
|
selected.push(trimmed);
|
|
164
172
|
charCount += trimmed.length + 1;
|
|
@@ -184,6 +192,7 @@ function buildL1EssentialStory(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
|
184
192
|
if (noteFiles.length === 0) return "";
|
|
185
193
|
const sections = [];
|
|
186
194
|
const seen = /* @__PURE__ */ new Set();
|
|
195
|
+
const seenLabels = /* @__PURE__ */ new Set();
|
|
187
196
|
let remaining = charBudget;
|
|
188
197
|
for (const noteFile of noteFiles) {
|
|
189
198
|
if (remaining <= 50) break;
|
|
@@ -198,8 +207,9 @@ function buildL1EssentialStory(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
|
198
207
|
const dateLabel = titleMatch ? titleMatch[1] : "";
|
|
199
208
|
const titleLabel = titleMatch ? titleMatch[2] : name.replace(/^\d+ - /, "").replace(/\.md$/, "");
|
|
200
209
|
const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);
|
|
201
|
-
const extracted = extractKeyLines(content, perNoteChars, seen);
|
|
202
|
-
|
|
210
|
+
const extracted = extractKeyLines(content, perNoteChars, seen, seenLabels);
|
|
211
|
+
const hasContentLine = extracted.split("\n").some((l) => !/^\[.*\]$/.test(l));
|
|
212
|
+
if (!extracted || !hasContentLine) continue;
|
|
203
213
|
const noteBlock = `[${dateLabel} - ${titleLabel}]\n${extracted}`;
|
|
204
214
|
sections.push(noteBlock);
|
|
205
215
|
remaining -= noteBlock.length + 1;
|
|
@@ -215,10 +225,13 @@ function buildL1EssentialStory(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
|
215
225
|
*
|
|
216
226
|
* @param rootPath Project root path for L1 note lookup. Optional.
|
|
217
227
|
* @param tokenBudget L1 token budget. Default 800.
|
|
228
|
+
* @param opts.skipStory Skip the L1 essential story. Set when a handover
|
|
229
|
+
* checkpoint was already injected — the story re-summarises the same note
|
|
230
|
+
* the handover came from, at lower quality.
|
|
218
231
|
*/
|
|
219
|
-
function buildWakeupContext(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
232
|
+
function buildWakeupContext(rootPath, tokenBudget = L1_TOKEN_BUDGET, opts = {}) {
|
|
220
233
|
const identity = loadL0Identity();
|
|
221
|
-
const essentialStory = rootPath ? buildL1EssentialStory(rootPath, tokenBudget) : "";
|
|
234
|
+
const essentialStory = rootPath && !opts.skipStory ? buildL1EssentialStory(rootPath, tokenBudget) : "";
|
|
222
235
|
if (!identity && !essentialStory) return "";
|
|
223
236
|
const parts = [];
|
|
224
237
|
if (identity) parts.push(`## L0 Identity\n\n${identity}`);
|
|
@@ -228,4 +241,4 @@ function buildWakeupContext(rootPath, tokenBudget = L1_TOKEN_BUDGET) {
|
|
|
228
241
|
|
|
229
242
|
//#endregion
|
|
230
243
|
export { migrateIdentityFile as n, buildWakeupContext as t };
|
|
231
|
-
//# sourceMappingURL=wakeup-
|
|
244
|
+
//# sourceMappingURL=wakeup-UrhiW-KU.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wakeup-UrhiW-KU.mjs","names":[],"sources":["../src/memory/wakeup.ts"],"sourcesContent":["/**\n * Wake-up context system — progressive context loading inspired by mempalace.\n *\n * Layers:\n * L0 Identity (~100 tokens) — user identity from ~/.pai/identity.txt. Always loaded.\n * L1 Essential Story (~500-800t) — top session notes for the project, key lines extracted.\n * L2 On-Demand — triggered by topic queries (handled by memory_search).\n * L3 Deep Search — unlimited federated memory search (memory_search tool).\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { paiHomePath, migratePaiFile, type MigrateFileResult } from \"../config/pai-home.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Maximum tokens for the L1 essential story block. Approx 4 chars/token.\n * Measured 2026-09-20: 800 tokens was mostly stale bullets. Reduced to 300.\n */\nconst L1_TOKEN_BUDGET = 300;\nconst L1_CHAR_BUDGET = L1_TOKEN_BUDGET * 4; // ~1600 chars\n\n/** Maximum session notes to scan when building L1. Reduced from 10 to 3 (2026-09-20). */\nconst L1_MAX_NOTES = 3;\n\n/** Sections to extract from session notes (in priority order). */\nconst EXTRACT_SECTIONS = [\n \"Work Done\",\n \"Key Decisions\",\n \"Next Steps\",\n \"Checkpoint\",\n];\n\n/** Identity file location. */\nconst IDENTITY_FILE = join(homedir(), \".pai\", \"identity.txt\");\n\n// ---------------------------------------------------------------------------\n// L0: Identity\n// ---------------------------------------------------------------------------\n\n/**\n * Load L0 identity. Prefers the PAI_HOME location (identity.txt migrated by\n * `pai config migrate`), falling back to the legacy ~/.pai/identity.txt so a\n * not-yet-migrated file keeps working. Returns \"\" if neither exists. Never\n * throws.\n */\nexport function loadL0Identity(\n opts: { newPath?: string; legacyPath?: string } = {}\n): string {\n const newPath = opts.newPath ?? paiHomePath(\"identity.txt\");\n const legacyPath = opts.legacyPath ?? IDENTITY_FILE;\n const path = existsSync(newPath) ? newPath : legacyPath;\n if (!existsSync(path)) return \"\";\n try {\n return readFileSync(path, \"utf-8\").trim();\n } catch {\n return \"\";\n }\n}\n\n/**\n * Move ~/.pai/identity.txt into PAI_HOME, following the same copy /\n * verify-byte-identical / rename-aside semantics as every other per-user\n * file (see migratePaiFile). `from`/`to` are overridable for tests.\n */\nexport function migrateIdentityFile(\n opts: { dryRun?: boolean; from?: string; to?: string } = {}\n): MigrateFileResult {\n const toPath = opts.to ?? paiHomePath(\"identity.txt\");\n const fromPath = opts.from ?? IDENTITY_FILE;\n return migratePaiFile(toPath, [fromPath], { dryRun: opts.dryRun });\n}\n\n// ---------------------------------------------------------------------------\n// L1: Essential Story\n// ---------------------------------------------------------------------------\n\n/**\n * Find the Notes directory for a project given its root_path from the registry.\n * Checks local Notes/ first, then central ~/.claude/projects/... path.\n */\nfunction findNotesDirForProject(rootPath: string): string | null {\n // Check local Notes directories first\n const localCandidates = [\n join(rootPath, \"Notes\"),\n join(rootPath, \"notes\"),\n join(rootPath, \".claude\", \"Notes\"),\n ];\n for (const p of localCandidates) {\n if (existsSync(p)) return p;\n }\n\n // Fall back to central ~/.claude/projects/{encoded}/Notes\n const encoded = rootPath\n .replace(/\\//g, \"-\")\n .replace(/\\./g, \"-\")\n .replace(/ /g, \"-\");\n const centralNotes = join(\n homedir(),\n \".claude\",\n \"projects\",\n encoded,\n \"Notes\"\n );\n if (existsSync(centralNotes)) return centralNotes;\n\n return null;\n}\n\n/**\n * Recursively find all .md session note files in a Notes directory.\n * Handles both flat layout (Notes/*.md) and month-subdirectory layout\n * (Notes/YYYY/MM/*.md). Returns files sorted newest-first by filename\n * (note numbers are monotonically increasing, so lexicographic = newest-last,\n * so we reverse).\n */\nfunction findSessionNotes(notesDir: string): string[] {\n const result: string[] = [];\n\n const scanDir = (dir: string) => {\n if (!existsSync(dir)) return;\n let entries: string[];\n try {\n entries = readdirSync(dir, { withFileTypes: true } as Parameters<typeof readdirSync>[1] as any)\n .map((e: any) => ({ name: e.name, isDir: e.isDirectory() }));\n } catch {\n return;\n }\n\n for (const entry of entries as Array<{ name: string; isDir: boolean }>) {\n const fullPath = join(dir, entry.name);\n if (entry.isDir) {\n // Recurse into YYYY/MM subdirectories\n scanDir(fullPath);\n } else if (entry.name.match(/^\\d{3,4}[\\s_-].*\\.md$/)) {\n result.push(fullPath);\n }\n }\n };\n\n scanDir(notesDir);\n\n // Sort newest-first by the DATE in the filename, not the note number.\n //\n // The note number is only monotonic within one uninterrupted numbering run.\n // It restarts — per month directory, and after a registry merge renumbers a\n // project — so \"highest number\" and \"most recent\" diverge. Observed live:\n // `0184 - 2026-02-22` outranked `0008 - 2026-08-01`, and the wake-up context\n // handed a resumed session five-month-old material as its recent history.\n //\n // `NNNN - YYYY-MM-DD - Title.md` is the enforced layout, so the date is the\n // reliable key. Number breaks ties within a day; mtime is the last resort\n // for notes that predate the naming convention.\n const dateOf = (p: string): string =>\n basename(p).match(/(\\d{4}-\\d{2}-\\d{2})/)?.[1] ?? \"\";\n const numberOf = (p: string): number =>\n parseInt(basename(p).match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n const mtimeOf = (p: string): number => {\n try {\n return statSync(p).mtimeMs;\n } catch {\n return 0;\n }\n };\n\n result.sort((a, b) => {\n const dateA = dateOf(a);\n const dateB = dateOf(b);\n // Undated notes sort last rather than winning on an empty string.\n if (dateA !== dateB) {\n if (!dateA) return 1;\n if (!dateB) return -1;\n return dateB.localeCompare(dateA);\n }\n const byNumber = numberOf(b) - numberOf(a);\n if (byNumber !== 0) return byNumber;\n return mtimeOf(b) - mtimeOf(a);\n });\n\n return result;\n}\n\n/**\n * Extract the most important lines from a session note.\n * Prioritises: Work Done items, Key Decisions, Next Steps, Checkpoint headings.\n * Returns a condensed string under maxChars.\n */\nfunction normalizeLine(line: string): string {\n return line\n .replace(/^(- \\[[ x]\\] |- |\\* |\\d+\\.\\s)/, \"\")\n .trim()\n .replace(/\\s+/g, \" \");\n}\n\nfunction extractKeyLines(\n content: string,\n maxChars: number,\n seen: Set<string> = new Set(),\n seenLabels: Set<string> = new Set()\n): string {\n const lines = content.split(\"\\n\");\n const selected: string[] = [];\n let inTargetSection = false;\n let currentSection = \"\";\n let charCount = 0;\n // Heading text seen since the last line was emitted. Buffered rather than\n // pushed immediately: a heading with no surviving lines under it (every\n // line deduped against an earlier note) must not appear at all.\n let pendingLabel: string | null = null;\n\n // First pass: collect lines from priority sections\n for (const line of lines) {\n // Detect section headers\n const h2Match = line.match(/^## (.+)$/);\n const h3Match = line.match(/^### (.+)$/);\n if (h2Match) {\n currentSection = h2Match[1];\n inTargetSection = EXTRACT_SECTIONS.some((s) =>\n currentSection.toLowerCase().includes(s.toLowerCase())\n );\n pendingLabel = null;\n continue;\n }\n if (h3Match) {\n // Checkpoints / sub-sections — buffer as a candidate label.\n if (inTargetSection) {\n pendingLabel = h3Match[1];\n }\n continue;\n }\n\n if (!inTargetSection) continue;\n\n // Skip blank lines and HTML comments\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"<!--\") || trimmed === \"---\") continue;\n\n // Include checkbox items, bold text, and plain text lines\n if (\n trimmed.startsWith(\"- \") ||\n trimmed.startsWith(\"* \") ||\n trimmed.match(/^\\d+\\./) ||\n trimmed.startsWith(\"**\")\n ) {\n const normalized = normalizeLine(trimmed);\n if (seen.has(normalized)) continue;\n\n let labelText = \"\";\n let labelKey = \"\";\n if (pendingLabel !== null) {\n labelKey = pendingLabel.toLowerCase().trim();\n if (!seenLabels.has(labelKey)) {\n labelText = `[${pendingLabel}]`;\n }\n }\n\n if (charCount + labelText.length + trimmed.length + 2 > maxChars) break;\n\n if (labelText) {\n selected.push(labelText);\n seenLabels.add(labelKey);\n charCount += labelText.length + 1;\n }\n pendingLabel = null;\n\n seen.add(normalized);\n selected.push(trimmed);\n charCount += trimmed.length + 1;\n }\n }\n\n return selected.join(\"\\n\");\n}\n\n/**\n * Build the L1 essential story block.\n *\n * Reads the most recent session notes for the project and extracts the key\n * lines (Work Done, Key Decisions, Next Steps) within the token budget.\n *\n * @param rootPath The project root path (from the registry).\n * @param tokenBudget Max tokens to consume. Default 800 (~3200 chars).\n * @returns Formatted L1 block, or empty string if no notes found.\n */\nexport function buildL1EssentialStory(\n rootPath: string,\n tokenBudget = L1_TOKEN_BUDGET\n): string {\n const charBudget = tokenBudget * 4;\n const notesDir = findNotesDirForProject(rootPath);\n if (!notesDir) return \"\";\n\n const noteFiles = findSessionNotes(notesDir).slice(0, L1_MAX_NOTES);\n if (noteFiles.length === 0) return \"\";\n\n const sections: string[] = [];\n const seen = new Set<string>();\n const seenLabels = new Set<string>();\n let remaining = charBudget;\n\n for (const noteFile of noteFiles) {\n if (remaining <= 50) break;\n\n let content: string;\n try {\n content = readFileSync(noteFile, \"utf-8\");\n } catch {\n continue;\n }\n\n // Extract the note date and title from the filename\n const name = basename(noteFile);\n const titleMatch = name.match(/^\\d+ - (\\d{4}-\\d{2}-\\d{2}) - (.+)\\.md$/);\n const dateLabel = titleMatch ? titleMatch[1] : \"\";\n const titleLabel = titleMatch\n ? titleMatch[2]\n : name.replace(/^\\d+ - /, \"\").replace(/\\.md$/, \"\");\n\n // Skip if nothing useful extracted from this note\n const perNoteChars = Math.min(remaining, Math.floor(charBudget / noteFiles.length) + 200);\n const extracted = extractKeyLines(content, perNoteChars, seen, seenLabels);\n // A note whose every extracted line survived only as a label (all its\n // real lines deduped against an earlier, newer note) contributes nothing\n // — including its title.\n const hasContentLine = extracted\n .split(\"\\n\")\n .some((l) => !/^\\[.*\\]$/.test(l));\n if (!extracted || !hasContentLine) continue;\n\n const noteBlock = `[${dateLabel} - ${titleLabel}]\\n${extracted}`;\n sections.push(noteBlock);\n remaining -= noteBlock.length + 1;\n }\n\n if (sections.length === 0) return \"\";\n\n return sections.join(\"\\n\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Combined: buildWakeupContext\n// ---------------------------------------------------------------------------\n\n/**\n * Build the combined wake-up context block (L0 + L1).\n *\n * Returns a formatted string suitable for injection as a system-reminder,\n * or an empty string if both layers are empty.\n *\n * @param rootPath Project root path for L1 note lookup. Optional.\n * @param tokenBudget L1 token budget. Default 800.\n * @param opts.skipStory Skip the L1 essential story. Set when a handover\n * checkpoint was already injected — the story re-summarises the same note\n * the handover came from, at lower quality.\n */\nexport function buildWakeupContext(\n rootPath?: string,\n tokenBudget = L1_TOKEN_BUDGET,\n opts: { skipStory?: boolean } = {}\n): string {\n const identity = loadL0Identity();\n const essentialStory = rootPath && !opts.skipStory\n ? buildL1EssentialStory(rootPath, tokenBudget)\n : \"\";\n\n if (!identity && !essentialStory) return \"\";\n\n const parts: string[] = [];\n\n if (identity) {\n parts.push(`## L0 Identity\\n\\n${identity}`);\n }\n\n if (essentialStory) {\n parts.push(`## L1 Essential Story\\n\\n${essentialStory}`);\n }\n\n return parts.join(\"\\n\\n\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAsBA,MAAM,kBAAkB;AACD,kBAAkB;;AAGzC,MAAM,eAAe;;AAGrB,MAAM,mBAAmB;CACvB;CACA;CACA;CACA;CACD;;AAGD,MAAM,gBAAgB,KAAK,SAAS,EAAE,QAAQ,eAAe;;;;;;;AAY7D,SAAgB,eACd,OAAkD,EAAE,EAC5C;CACR,MAAM,UAAU,KAAK,WAAW,YAAY,eAAe;CAC3D,MAAM,aAAa,KAAK,cAAc;CACtC,MAAM,OAAO,WAAW,QAAQ,GAAG,UAAU;AAC7C,KAAI,CAAC,WAAW,KAAK,CAAE,QAAO;AAC9B,KAAI;AACF,SAAO,aAAa,MAAM,QAAQ,CAAC,MAAM;SACnC;AACN,SAAO;;;;;;;;AASX,SAAgB,oBACd,OAAyD,EAAE,EACxC;AAGnB,QAAO,eAFQ,KAAK,MAAM,YAAY,eAAe,EAEvB,CADb,KAAK,QAAQ,cACU,EAAE,EAAE,QAAQ,KAAK,QAAQ,CAAC;;;;;;AAWpE,SAAS,uBAAuB,UAAiC;CAE/D,MAAM,kBAAkB;EACtB,KAAK,UAAU,QAAQ;EACvB,KAAK,UAAU,QAAQ;EACvB,KAAK,UAAU,WAAW,QAAQ;EACnC;AACD,MAAK,MAAM,KAAK,gBACd,KAAI,WAAW,EAAE,CAAE,QAAO;CAI5B,MAAM,UAAU,SACb,QAAQ,OAAO,IAAI,CACnB,QAAQ,OAAO,IAAI,CACnB,QAAQ,MAAM,IAAI;CACrB,MAAM,eAAe,KACnB,SAAS,EACT,WACA,YACA,SACA,QACD;AACD,KAAI,WAAW,aAAa,CAAE,QAAO;AAErC,QAAO;;;;;;;;;AAUT,SAAS,iBAAiB,UAA4B;CACpD,MAAM,SAAmB,EAAE;CAE3B,MAAM,WAAW,QAAgB;AAC/B,MAAI,CAAC,WAAW,IAAI,CAAE;EACtB,IAAI;AACJ,MAAI;AACF,aAAU,YAAY,KAAK,EAAE,eAAe,MAAM,CAA6C,CAC5F,KAAK,OAAY;IAAE,MAAM,EAAE;IAAM,OAAO,EAAE,aAAa;IAAE,EAAE;UACxD;AACN;;AAGF,OAAK,MAAM,SAAS,SAAoD;GACtE,MAAM,WAAW,KAAK,KAAK,MAAM,KAAK;AACtC,OAAI,MAAM,MAER,SAAQ,SAAS;YACR,MAAM,KAAK,MAAM,wBAAwB,CAClD,QAAO,KAAK,SAAS;;;AAK3B,SAAQ,SAAS;CAajB,MAAM,UAAU,MACd,SAAS,EAAE,CAAC,MAAM,sBAAsB,GAAG,MAAM;CACnD,MAAM,YAAY,MAChB,SAAS,SAAS,EAAE,CAAC,MAAM,SAAS,GAAG,MAAM,KAAK,GAAG;CACvD,MAAM,WAAW,MAAsB;AACrC,MAAI;AACF,UAAO,SAAS,EAAE,CAAC;UACb;AACN,UAAO;;;AAIX,QAAO,MAAM,GAAG,MAAM;EACpB,MAAM,QAAQ,OAAO,EAAE;EACvB,MAAM,QAAQ,OAAO,EAAE;AAEvB,MAAI,UAAU,OAAO;AACnB,OAAI,CAAC,MAAO,QAAO;AACnB,OAAI,CAAC,MAAO,QAAO;AACnB,UAAO,MAAM,cAAc,MAAM;;EAEnC,MAAM,WAAW,SAAS,EAAE,GAAG,SAAS,EAAE;AAC1C,MAAI,aAAa,EAAG,QAAO;AAC3B,SAAO,QAAQ,EAAE,GAAG,QAAQ,EAAE;GAC9B;AAEF,QAAO;;;;;;;AAQT,SAAS,cAAc,MAAsB;AAC3C,QAAO,KACJ,QAAQ,iCAAiC,GAAG,CAC5C,MAAM,CACN,QAAQ,QAAQ,IAAI;;AAGzB,SAAS,gBACP,SACA,UACA,uBAAoB,IAAI,KAAK,EAC7B,6BAA0B,IAAI,KAAK,EAC3B;CACR,MAAM,QAAQ,QAAQ,MAAM,KAAK;CACjC,MAAM,WAAqB,EAAE;CAC7B,IAAI,kBAAkB;CACtB,IAAI,iBAAiB;CACrB,IAAI,YAAY;CAIhB,IAAI,eAA8B;AAGlC,MAAK,MAAM,QAAQ,OAAO;EAExB,MAAM,UAAU,KAAK,MAAM,YAAY;EACvC,MAAM,UAAU,KAAK,MAAM,aAAa;AACxC,MAAI,SAAS;AACX,oBAAiB,QAAQ;AACzB,qBAAkB,iBAAiB,MAAM,MACvC,eAAe,aAAa,CAAC,SAAS,EAAE,aAAa,CAAC,CACvD;AACD,kBAAe;AACf;;AAEF,MAAI,SAAS;AAEX,OAAI,gBACF,gBAAe,QAAQ;AAEzB;;AAGF,MAAI,CAAC,gBAAiB;EAGtB,MAAM,UAAU,KAAK,MAAM;AAC3B,MAAI,CAAC,WAAW,QAAQ,WAAW,OAAO,IAAI,YAAY,MAAO;AAGjE,MACE,QAAQ,WAAW,KAAK,IACxB,QAAQ,WAAW,KAAK,IACxB,QAAQ,MAAM,SAAS,IACvB,QAAQ,WAAW,KAAK,EACxB;GACA,MAAM,aAAa,cAAc,QAAQ;AACzC,OAAI,KAAK,IAAI,WAAW,CAAE;GAE1B,IAAI,YAAY;GAChB,IAAI,WAAW;AACf,OAAI,iBAAiB,MAAM;AACzB,eAAW,aAAa,aAAa,CAAC,MAAM;AAC5C,QAAI,CAAC,WAAW,IAAI,SAAS,CAC3B,aAAY,IAAI,aAAa;;AAIjC,OAAI,YAAY,UAAU,SAAS,QAAQ,SAAS,IAAI,SAAU;AAElE,OAAI,WAAW;AACb,aAAS,KAAK,UAAU;AACxB,eAAW,IAAI,SAAS;AACxB,iBAAa,UAAU,SAAS;;AAElC,kBAAe;AAEf,QAAK,IAAI,WAAW;AACpB,YAAS,KAAK,QAAQ;AACtB,gBAAa,QAAQ,SAAS;;;AAIlC,QAAO,SAAS,KAAK,KAAK;;;;;;;;;;;;AAa5B,SAAgB,sBACd,UACA,cAAc,iBACN;CACR,MAAM,aAAa,cAAc;CACjC,MAAM,WAAW,uBAAuB,SAAS;AACjD,KAAI,CAAC,SAAU,QAAO;CAEtB,MAAM,YAAY,iBAAiB,SAAS,CAAC,MAAM,GAAG,aAAa;AACnE,KAAI,UAAU,WAAW,EAAG,QAAO;CAEnC,MAAM,WAAqB,EAAE;CAC7B,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAM,6BAAa,IAAI,KAAa;CACpC,IAAI,YAAY;AAEhB,MAAK,MAAM,YAAY,WAAW;AAChC,MAAI,aAAa,GAAI;EAErB,IAAI;AACJ,MAAI;AACF,aAAU,aAAa,UAAU,QAAQ;UACnC;AACN;;EAIF,MAAM,OAAO,SAAS,SAAS;EAC/B,MAAM,aAAa,KAAK,MAAM,yCAAyC;EACvE,MAAM,YAAY,aAAa,WAAW,KAAK;EAC/C,MAAM,aAAa,aACf,WAAW,KACX,KAAK,QAAQ,WAAW,GAAG,CAAC,QAAQ,SAAS,GAAG;EAGpD,MAAM,eAAe,KAAK,IAAI,WAAW,KAAK,MAAM,aAAa,UAAU,OAAO,GAAG,IAAI;EACzF,MAAM,YAAY,gBAAgB,SAAS,cAAc,MAAM,WAAW;EAI1E,MAAM,iBAAiB,UACpB,MAAM,KAAK,CACX,MAAM,MAAM,CAAC,WAAW,KAAK,EAAE,CAAC;AACnC,MAAI,CAAC,aAAa,CAAC,eAAgB;EAEnC,MAAM,YAAY,IAAI,UAAU,KAAK,WAAW,KAAK;AACrD,WAAS,KAAK,UAAU;AACxB,eAAa,UAAU,SAAS;;AAGlC,KAAI,SAAS,WAAW,EAAG,QAAO;AAElC,QAAO,SAAS,KAAK,OAAO;;;;;;;;;;;;;;AAmB9B,SAAgB,mBACd,UACA,cAAc,iBACd,OAAgC,EAAE,EAC1B;CACR,MAAM,WAAW,gBAAgB;CACjC,MAAM,iBAAiB,YAAY,CAAC,KAAK,YACrC,sBAAsB,UAAU,YAAY,GAC5C;AAEJ,KAAI,CAAC,YAAY,CAAC,eAAgB,QAAO;CAEzC,MAAM,QAAkB,EAAE;AAE1B,KAAI,SACF,OAAM,KAAK,qBAAqB,WAAW;AAG7C,KAAI,eACF,OAAM,KAAK,4BAA4B,iBAAiB;AAG1D,QAAO,MAAM,KAAK,OAAO"}
|
package/docs/commands/README.md
CHANGED
|
@@ -45,16 +45,18 @@ Generated man pages for every `pai` command area. Read any page in the terminal
|
|
|
45
45
|
| Command | Description |
|
|
46
46
|
|---------|-------------|
|
|
47
47
|
| `pai audit` | Diagnostic audits: token-waste report and friends |
|
|
48
|
-
| `pai audit tokens` | Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue |
|
|
48
|
+
| `pai audit tokens` | Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue, subagents, MCP |
|
|
49
49
|
| `pai audit tokens daemon` | LLM spawns, KG-extraction parse failures, work-queue counts from the daemon log |
|
|
50
50
|
| `pai audit tokens env` | ANTHROPIC_BASE_URL / model-override env on every live claude process |
|
|
51
51
|
| `pai audit tokens files [paths...]` | Token count per memory file (CLAUDE.md chain, CORE skill, whisper rules, auto-memory) |
|
|
52
52
|
| `pai audit tokens hooks` | Token cost of every SessionStart / UserPromptSubmit hook |
|
|
53
53
|
| `pai audit tokens ladder` | LIVE: first-turn context at increasing headless-worker configuration (spawns real claude -p calls) |
|
|
54
|
+
| `pai audit tokens mcp` | MCP servers: configured vs. pinned vs. loaded-live vs. used in the last 30 days |
|
|
54
55
|
| `pai audit tokens schedule` | Launchd agents / crontab entries that outpace the measured prompt-cache TTL |
|
|
55
56
|
| `pai audit tokens session [path]` | Cache/input/output token split for a session transcript (default: newest) |
|
|
56
57
|
| `pai audit tokens skills` | Token cost of the SKILL.md / command / plugin catalogue, top entries, duplicate names |
|
|
57
58
|
| `pai audit tokens spawn` | Spawn-overhead comparison: Agent-tool subagents vs. pai workers |
|
|
59
|
+
| `pai audit tokens subagents` | Token cost of Claude Code subagent definitions (~/.claude/agents, <cwd>/.claude/agents) and their model pinning |
|
|
58
60
|
| `pai backup` | Backup registry, config, and Postgres database to PAI_HOME/backups/ |
|
|
59
61
|
| `pai clear-names` | Recovery: wipe corrupted iTerm2 session name state. |
|
|
60
62
|
| `pai config` | PAI_HOME namespace dir: path, migrate |
|
package/docs/commands/audit.md
CHANGED
|
@@ -14,17 +14,18 @@ pai audit <subcommand> [options]
|
|
|
14
14
|
|
|
15
15
|
| Command | Description |
|
|
16
16
|
|---------|-------------|
|
|
17
|
-
| [`pai audit tokens`](#pai-audit-tokens) | Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue |
|
|
17
|
+
| [`pai audit tokens`](#pai-audit-tokens) | Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue, subagents, MCP |
|
|
18
18
|
|
|
19
19
|
### pai audit tokens
|
|
20
20
|
|
|
21
|
-
Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue
|
|
21
|
+
Token-waste audit: memory files, hooks, session usage, spawn overhead, daemon, env, schedule, skill catalogue, subagents, MCP
|
|
22
22
|
|
|
23
23
|
**Options**
|
|
24
24
|
|
|
25
25
|
| Option | Description | Default |
|
|
26
26
|
|--------|-------------|---------|
|
|
27
27
|
| `--json` | Print JSON instead of a table | |
|
|
28
|
+
| `--record <dir>` | Write a dated, numbered markdown+JSON snapshot of the combined report to <dir> | |
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
### pai audit tokens files [paths...]
|
|
@@ -53,6 +54,12 @@ Cache/input/output token split for a session transcript (default: newest)
|
|
|
53
54
|
|----------|------|-------------|
|
|
54
55
|
| `[path]` | optional | Session JSONL path (default: newest under ~/.claude/projects) |
|
|
55
56
|
|
|
57
|
+
**Options**
|
|
58
|
+
|
|
59
|
+
| Option | Description | Default |
|
|
60
|
+
|--------|-------------|---------|
|
|
61
|
+
| `--ctx-threshold <n>` | Per-turn context size above which a turn counts as 'above threshold' (default 200000) | |
|
|
62
|
+
|
|
56
63
|
|
|
57
64
|
### pai audit tokens spawn
|
|
58
65
|
|
|
@@ -92,6 +99,22 @@ Token cost of the SKILL.md / command / plugin catalogue, top entries, duplicate
|
|
|
92
99
|
| `--top <n>` | How many top-by-tokens entries to show (default 15) | |
|
|
93
100
|
|
|
94
101
|
|
|
102
|
+
### pai audit tokens subagents
|
|
103
|
+
|
|
104
|
+
Token cost of Claude Code subagent definitions (~/.claude/agents, <cwd>/.claude/agents) and their model pinning
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
### pai audit tokens mcp
|
|
108
|
+
|
|
109
|
+
MCP servers: configured vs. pinned vs. loaded-live vs. used in the last 30 days
|
|
110
|
+
|
|
111
|
+
**Options**
|
|
112
|
+
|
|
113
|
+
| Option | Description | Default |
|
|
114
|
+
|--------|-------------|---------|
|
|
115
|
+
| `--connect` | Actually connect to every stdio MCP server to count exposed tools (side-effecting; never on by default) | |
|
|
116
|
+
|
|
117
|
+
|
|
95
118
|
### pai audit tokens ladder
|
|
96
119
|
|
|
97
120
|
LIVE: first-turn context at increasing headless-worker configuration (spawns real claude -p calls)
|
package/package.json
CHANGED
|
@@ -632,6 +632,46 @@ describe("contextFillThresholds — trigger source", () => {
|
|
|
632
632
|
});
|
|
633
633
|
});
|
|
634
634
|
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
// contextFillThresholds — the 2026-09-20 regime change (override 80 -> 20 on
|
|
637
|
+
// a 1M window). Same min(measured, configured) formula, no code change
|
|
638
|
+
// needed, but the specific numbers of that day are worth pinning down: a
|
|
639
|
+
// project's measured history (784k, from the prior 78-79% regime) is now
|
|
640
|
+
// HIGHER than the newly configured 20%-of-1M trigger (200k), so the clamp
|
|
641
|
+
// must take the configured value, not the stale measured one.
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
|
|
644
|
+
describe("contextFillThresholds — 2026-09-20 regime change (override 80 -> 20)", () => {
|
|
645
|
+
it("measured 784k + configured 200k (override=20, 1M window) -> clamps to 200k", () => {
|
|
646
|
+
const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "20" }, { measuredTrigger: 784_000 });
|
|
647
|
+
expect(t.configuredTriggerTokens).toBe(200_000);
|
|
648
|
+
expect(t.triggerSource).toBe("measured-clamped");
|
|
649
|
+
expect(t.effectiveTriggerTokens).toBe(200_000);
|
|
650
|
+
expect(t.warmupTokens).toBe(100_000); // 200,000 - 100,000
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("measured 784k + configured 800k (override=80, 1M window) -> measured wins at 784k", () => {
|
|
654
|
+
const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "80" }, { measuredTrigger: 784_000 });
|
|
655
|
+
expect(t.configuredTriggerTokens).toBe(800_000);
|
|
656
|
+
expect(t.triggerSource).toBe("measured");
|
|
657
|
+
expect(t.effectiveTriggerTokens).toBe(784_000);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("measured null + configured 200k (override=20, no compaction history yet) -> 200k", () => {
|
|
661
|
+
const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "20" }, { measuredTrigger: null });
|
|
662
|
+
expect(t.triggerSource).toBe("configured");
|
|
663
|
+
expect(t.effectiveTriggerTokens).toBe(200_000);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("measured 784k + override unset (defaults to 80, configured 800k) -> measured wins at 784k", () => {
|
|
667
|
+
const t = contextFillThresholds(readingAt(1_000_000), {}, { measuredTrigger: 784_000 });
|
|
668
|
+
expect(t.autocompactPct).toBe(80);
|
|
669
|
+
expect(t.configuredTriggerTokens).toBe(800_000);
|
|
670
|
+
expect(t.triggerSource).toBe("measured");
|
|
671
|
+
expect(t.effectiveTriggerTokens).toBe(784_000);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
635
675
|
// ---------------------------------------------------------------------------
|
|
636
676
|
// Foreign-model transcripts — a headless worker on another provider (a
|
|
637
677
|
// different context window) shares the project folder; its compactions must
|
|
@@ -263,16 +263,22 @@ export function formatContextFill(reading: ContextFillReading): FillDisplay {
|
|
|
263
263
|
// summary ahead of a compaction.
|
|
264
264
|
// ---------------------------------------------------------------------------
|
|
265
265
|
//
|
|
266
|
-
// THE CONFIGURED VALUE
|
|
267
|
-
//
|
|
268
|
-
// following regimes — the same configured number, two
|
|
266
|
+
// THE CONFIGURED VALUE PREDICTS THE TRIGGER ONLY WHILE THE PLATFORM'S FORMULA
|
|
267
|
+
// HOLDS. CLAUDE_AUTOCOMPACT_PCT_OVERRIDE was 80 throughout, on this machine,
|
|
268
|
+
// across BOTH of the following regimes — the same configured number, two
|
|
269
|
+
// different truths:
|
|
269
270
|
//
|
|
270
|
-
// 2026-08-16 .. 2026-09-10 preTokens ~ 993,096 – 1,002,500 (~100% of a 1M window)
|
|
271
|
-
// 2026-09-12 .. 2026-09-
|
|
271
|
+
// 2026-08-16 .. 2026-09-10 preTokens ~ 993,096 – 1,002,500 (~100% of a 1M window; override ignored)
|
|
272
|
+
// 2026-09-12 .. 2026-09-20 preTokens ~ 782,981 – 791,995 (1M window) and
|
|
273
|
+
// preTokens ~ 143,280 – 154,832 (200k window)
|
|
274
|
+
// = pct/100 × (window − 20,000), measured 2026-09-20
|
|
272
275
|
//
|
|
273
276
|
// A threshold derived only from the configured override was wrong for the
|
|
274
|
-
//
|
|
277
|
+
// first regime and will be wrong again the next time the formula moves — the
|
|
275
278
|
// config is not ground truth, it is what Claude Code claims it will honor.
|
|
279
|
+
// Two more facts (2026-09-20): the settings.json `env` value beats the
|
|
280
|
+
// process env, so there is no per-launch override; and headless `-p`
|
|
281
|
+
// sessions do not compact at the derived trigger at all.
|
|
276
282
|
//
|
|
277
283
|
// GROUND TRUTH IS ON DISK ALREADY: every real compaction writes a
|
|
278
284
|
// `compact_boundary` system event to the transcript with
|
|
@@ -498,15 +498,17 @@ Session Commands:
|
|
|
498
498
|
// 8.5. INJECT THE PAUSE CHECKPOINT
|
|
499
499
|
//
|
|
500
500
|
// `pai pause` writes the handover to TODO.md under `## Continue`. Until now
|
|
501
|
-
// nothing read it back:
|
|
502
|
-
//
|
|
503
|
-
//
|
|
501
|
+
// nothing read it back: CORE SKILL.md only told the model to look there *if
|
|
502
|
+
// the user typed "go"*, so a resumed session started blind unless the user
|
|
503
|
+
// knew the magic word. Writing a handover nobody delivers is not a
|
|
504
504
|
// handover. Inject it, and say plainly that it is the previous session's
|
|
505
505
|
// state rather than an instruction to act on.
|
|
506
|
+
let handoverInjected = false;
|
|
506
507
|
if (existsSync(todoPath)) {
|
|
507
508
|
try {
|
|
508
509
|
const checkpoint = readContinueCheckpoint(readFileSync(todoPath, 'utf-8'));
|
|
509
510
|
if (checkpoint) {
|
|
511
|
+
handoverInjected = true;
|
|
510
512
|
const from = checkpoint.meta?.session
|
|
511
513
|
? `\nFrom session: ${checkpoint.meta.session}`
|
|
512
514
|
: '';
|
|
@@ -579,7 +581,9 @@ THE ABOVE INSTRUCTIONS ARE MANDATORY. Follow them exactly.
|
|
|
579
581
|
wakeupRootPath = cwd;
|
|
580
582
|
}
|
|
581
583
|
|
|
582
|
-
|
|
584
|
+
// Handover supersedes the L1 story: both summarise the same recent note,
|
|
585
|
+
// and the handover is a curated message while the story is auto-extracted.
|
|
586
|
+
const wakeupBlock = buildWakeupContext(wakeupRootPath, undefined, { skipStory: handoverInjected });
|
|
583
587
|
if (wakeupBlock) {
|
|
584
588
|
const wakeupReminder = `\n<system-reminder>\nWAKEUP CONTEXT\n\n${wakeupBlock}\n</system-reminder>\n`;
|
|
585
589
|
console.log(wakeupReminder);
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* call: see ../lib/mcp-deferred-reminder.ts for the rationale and text.
|
|
12
12
|
*
|
|
13
13
|
* stdout is captured by Claude Code and injected into the session context,
|
|
14
|
-
* same channel as
|
|
15
|
-
*
|
|
14
|
+
* same channel as inject-observations. Never blocks session start: always
|
|
15
|
+
* exits 0.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { decideMcpDeferredReminder, type ReminderHookInput } from "../lib/mcp-deferred-reminder.js";
|
|
@@ -15,7 +15,6 @@ describe("session-start hooks: worker sessions get no injected context", () => {
|
|
|
15
15
|
"src/hooks/ts/session-start/load-project-context.ts",
|
|
16
16
|
"src/hooks/ts/session-start/mcp-deferred-reminder.ts",
|
|
17
17
|
"src/hooks/ts/session-start/post-compact-inject.ts",
|
|
18
|
-
"src/hooks/ts/session-start/session-commands.ts",
|
|
19
18
|
];
|
|
20
19
|
|
|
21
20
|
function runHook(entrypoint: string, env: NodeJS.ProcessEnv): string {
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createRequire as __paiCreateRequire } from 'node:module';
|
|
3
|
-
const require = __paiCreateRequire(import.meta.url);
|
|
4
|
-
if (!process.env.PAI_QUIET_NOTICES) process.env.PAI_QUIET_NOTICES = '1';
|
|
5
|
-
|
|
6
|
-
// src/hooks/ts/lib/worker-session.ts
|
|
7
|
-
function isWorkerSession(env = process.env) {
|
|
8
|
-
return env.PAI_WORKER === "1";
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// src/hooks/ts/session-start/session-commands.ts
|
|
12
|
-
if (isWorkerSession()) process.exit(0);
|
|
13
|
-
var commands = `<system-reminder>
|
|
14
|
-
SESSION COMMANDS (always available):
|
|
15
|
-
|
|
16
|
-
- "go" / "continue" / "weiter" \u2192 Read Notes/TODO.md, find ## Continue section, resume work
|
|
17
|
-
- "pause session" \u2192 Save state to TODO.md, update session note, stop (no compact)
|
|
18
|
-
- "end session" \u2192 Finalize note, commit if needed, start fresh next time
|
|
19
|
-
- "cpp" \u2192 Release workflow: version bump \u2192 build \u2192 npm publish \u2192 git add ALL \u2192 ONE commit \u2192 git push
|
|
20
|
-
|
|
21
|
-
When user says "go" with no other context, ALWAYS check TODO.md first.
|
|
22
|
-
|
|
23
|
-
IMPORTANT: Full PAI context may be truncated due to size. For complete instructions (response format, workflows, principles), READ: ~/.claude/Skills/CORE/SKILL.md
|
|
24
|
-
</system-reminder>`;
|
|
25
|
-
console.log(commands);
|
|
26
|
-
//# sourceMappingURL=session-commands.mjs.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/session-start/session-commands.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n return env.PAI_WORKER === \"1\";\n}\n", "#!/usr/bin/env node\n\n/**\n * session-commands.ts\n *\n * Tiny SessionStart hook that injects critical session commands inline.\n * Stays under 2KB to avoid persisted-output truncation.\n *\n * This runs BEFORE load-core-context so these commands are always available\n * even when the full CORE skill gets truncated.\n */\n\nimport { isWorkerSession } from \"../lib/worker-session.js\";\n\nif (isWorkerSession()) process.exit(0); // disposable worker: no per-session bookkeeping\n\nconst commands = `<system-reminder>\nSESSION COMMANDS (always available):\n\n- \"go\" / \"continue\" / \"weiter\" \u2192 Read Notes/TODO.md, find ## Continue section, resume work\n- \"pause session\" \u2192 Save state to TODO.md, update session note, stop (no compact)\n- \"end session\" \u2192 Finalize note, commit if needed, start fresh next time\n- \"cpp\" \u2192 Release workflow: version bump \u2192 build \u2192 npm publish \u2192 git add ALL \u2192 ONE commit \u2192 git push\n\nWhen user says \"go\" with no other context, ALWAYS check TODO.md first.\n\nIMPORTANT: Full PAI context may be truncated due to size. For complete instructions (response format, workflows, principles), READ: ~/.claude/Skills/CORE/SKILL.md\n</system-reminder>`;\n\nconsole.log(commands);\n"],
|
|
5
|
-
"mappings": ";;;;;;AAoBO,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;ACRA,IAAI,gBAAgB,EAAG,SAAQ,KAAK,CAAC;AAErC,IAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAajB,QAAQ,IAAI,QAAQ;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|