@tekmidian/pai 0.46.0 → 0.47.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-DAEC9AU7.mjs → chain-CNSa0A3G.mjs} +2 -2
- package/dist/{chain-DAEC9AU7.mjs.map → chain-CNSa0A3G.mjs.map} +1 -1
- package/dist/cli/index.mjs +4 -4
- package/dist/cli/program.mjs +4 -4
- package/dist/daemon/index.mjs +2 -2
- package/dist/{daemon-Dq2ZSdex.mjs → daemon-CYpVy_Dw.mjs} +2 -2
- package/dist/{daemon-DNBEFHaj.mjs → daemon-DZ4Q0jrs.mjs} +2 -2
- package/dist/{daemon-DNBEFHaj.mjs.map → daemon-DZ4Q0jrs.mjs.map} +1 -1
- package/dist/daemon-mcp/index.mjs +3 -3
- package/dist/{fallback-CxGDTXWt.mjs → fallback-IeR2FrFn.mjs} +6 -6
- package/dist/fallback-IeR2FrFn.mjs.map +1 -0
- package/dist/hooks/load-core-context.mjs +9 -11
- package/dist/hooks/load-core-context.mjs.map +3 -3
- package/dist/hooks/load-project-context.mjs +59 -6
- package/dist/hooks/load-project-context.mjs.map +3 -3
- package/dist/hooks/worker-status-line.mjs +23 -4
- package/dist/hooks/worker-status-line.mjs.map +3 -3
- package/dist/{planner-B4DvNpXd.mjs → planner-Cn8HmT7l.mjs} +3 -3
- package/dist/{planner-B4DvNpXd.mjs.map → planner-Cn8HmT7l.mjs.map} +1 -1
- package/dist/{program-Dk_EIMqC.mjs → program-BxpeczCx.mjs} +26 -13
- package/dist/program-BxpeczCx.mjs.map +1 -0
- package/dist/{run-BgExF8Xg.mjs → run-Cls0YGgV.mjs} +43 -6
- package/dist/run-Cls0YGgV.mjs.map +1 -0
- package/package.json +1 -1
- package/src/hooks/ts/lib/frontmatter.test.ts +19 -0
- package/src/hooks/ts/lib/frontmatter.ts +10 -0
- package/src/hooks/ts/lib/handover-budget.test.ts +124 -0
- package/src/hooks/ts/lib/handover-budget.ts +94 -0
- package/src/hooks/ts/session-start/load-core-context.ts +6 -15
- package/src/hooks/ts/session-start/load-project-context.ts +9 -6
- package/dist/fallback-CxGDTXWt.mjs.map +0 -1
- package/dist/program-Dk_EIMqC.mjs.map +0 -1
- package/dist/run-BgExF8Xg.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { stripFrontmatter } from "./frontmatter.js";
|
|
3
|
+
|
|
4
|
+
describe("stripFrontmatter", () => {
|
|
5
|
+
it("strips a leading YAML frontmatter block and trims surrounding blank lines", () => {
|
|
6
|
+
const md = `---\nname: CORE\ndescription: some skill\n---\n\nBody text here.\n`;
|
|
7
|
+
expect(stripFrontmatter(md)).toBe("Body text here.");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("leaves content unchanged when there is no frontmatter", () => {
|
|
11
|
+
const md = `Body text here.\nMore text.\n`;
|
|
12
|
+
expect(stripFrontmatter(md)).toBe("Body text here.\nMore text.");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("leaves a --- that appears inside the body (not at the top) alone", () => {
|
|
16
|
+
const md = `---\nname: CORE\n---\n\nSection one.\n\n---\n\nSection two.\n`;
|
|
17
|
+
expect(stripFrontmatter(md)).toBe("Section one.\n\n---\n\nSection two.");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip a leading YAML frontmatter block (`---` ... `---` at the very top of
|
|
3
|
+
* the file) and trim surrounding blank lines. A `---` that appears later in
|
|
4
|
+
* the body (not at the top) is left untouched.
|
|
5
|
+
*/
|
|
6
|
+
export function stripFrontmatter(md: string): string {
|
|
7
|
+
const match = md.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
8
|
+
const body = match ? md.slice(match[0].length) : md;
|
|
9
|
+
return body.replace(/^\s+|\s+$/g, '');
|
|
10
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { applyHandoverBudget, HANDOVER_CHAR_BUDGET } from "./handover-budget.js";
|
|
3
|
+
|
|
4
|
+
const SOURCE = "/tmp/TODO.md";
|
|
5
|
+
|
|
6
|
+
function field(key: string, charCount: number): string {
|
|
7
|
+
return `${key}=${"x".repeat(Math.max(0, charCount - key.length - 1))}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("applyHandoverBudget", () => {
|
|
11
|
+
it("returns the body unchanged when under budget", () => {
|
|
12
|
+
const body = "T\ng=finish the thing\nz=all good";
|
|
13
|
+
const result = applyHandoverBudget(body, SOURCE, 2400);
|
|
14
|
+
expect(result).toEqual({ body, elided: [], truncated: false });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("elides only d= when that alone brings the body under budget", () => {
|
|
18
|
+
const budget = 200;
|
|
19
|
+
const body = ["T", "g=goal line", field("d", 300), "z=state"].join("\n");
|
|
20
|
+
const result = applyHandoverBudget(body, SOURCE, budget);
|
|
21
|
+
expect(result.elided).toEqual(["d"]);
|
|
22
|
+
expect(result.truncated).toBe(false);
|
|
23
|
+
expect(result.body).toContain(`d= elided, 300 chars; full text in ${SOURCE} (## Continue)`);
|
|
24
|
+
expect(result.body).toContain("g=goal line");
|
|
25
|
+
expect(result.body).toContain("z=state");
|
|
26
|
+
expect(result.body.length).toBeLessThanOrEqual(budget);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("elides both d= and t= when d= alone is not enough", () => {
|
|
30
|
+
const budget = 200;
|
|
31
|
+
const body = ["T", "g=goal line", field("d", 300), field("t", 300), "z=state"].join("\n");
|
|
32
|
+
const result = applyHandoverBudget(body, SOURCE, budget);
|
|
33
|
+
expect(result.elided).toEqual(["d", "t"]);
|
|
34
|
+
expect(result.truncated).toBe(false);
|
|
35
|
+
expect(result.body).toContain(`d= elided, 300 chars; full text in ${SOURCE} (## Continue)`);
|
|
36
|
+
expect(result.body).toContain(`t= elided, 300 chars; full text in ${SOURCE} (## Continue)`);
|
|
37
|
+
expect(result.body).toContain("g=goal line");
|
|
38
|
+
expect(result.body).toContain("z=state");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("hard-truncates prose with no Agentish fields", () => {
|
|
42
|
+
const budget = 50;
|
|
43
|
+
const body = "This is a long plain-prose handover with no fields at all, just text. ".repeat(5);
|
|
44
|
+
const result = applyHandoverBudget(body, SOURCE, budget);
|
|
45
|
+
expect(result.truncated).toBe(true);
|
|
46
|
+
expect(result.elided).toEqual([]);
|
|
47
|
+
expect(result.body.startsWith(body.slice(0, budget))).toBe(true);
|
|
48
|
+
expect(result.body).toContain(`[handover truncated at ${budget} chars; full text in ${SOURCE} (## Continue)]`);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("hard-truncates when eliding d= and t= still leaves the body over budget", () => {
|
|
52
|
+
const budget = 60;
|
|
53
|
+
const body = ["T", "g=goal line", field("d", 300), field("t", 300), "z=state"].join("\n");
|
|
54
|
+
const result = applyHandoverBudget(body, SOURCE, budget);
|
|
55
|
+
expect(result.elided).toEqual(["d", "t"]);
|
|
56
|
+
expect(result.truncated).toBe(true);
|
|
57
|
+
expect(result.body).toContain(`[handover truncated at ${budget} chars; full text in ${SOURCE} (## Continue)]`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("keeps the preamble, g=, @n and z= lines verbatim", () => {
|
|
61
|
+
const budget = 150;
|
|
62
|
+
const body = [
|
|
63
|
+
"T",
|
|
64
|
+
"",
|
|
65
|
+
"g=finish the audit",
|
|
66
|
+
"@1=src/audit/severity.ts:190",
|
|
67
|
+
field("d", 400),
|
|
68
|
+
"z=part C in progress",
|
|
69
|
+
].join("\n");
|
|
70
|
+
const result = applyHandoverBudget(body, SOURCE, budget);
|
|
71
|
+
expect(result.body).toContain("T\n\ng=finish the audit");
|
|
72
|
+
expect(result.body).toContain("@1=src/audit/severity.ts:190");
|
|
73
|
+
expect(result.body).toContain("z=part C in progress");
|
|
74
|
+
expect(result.elided).toEqual(["d"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("drops a multi-line field (with continuation lines) as a whole when elided", () => {
|
|
78
|
+
const budget = 150;
|
|
79
|
+
const body = [
|
|
80
|
+
"T",
|
|
81
|
+
"g=goal",
|
|
82
|
+
"d=did the first thing",
|
|
83
|
+
` ${"x".repeat(100)}`,
|
|
84
|
+
` ${"y".repeat(100)}`,
|
|
85
|
+
"z=state",
|
|
86
|
+
].join("\n");
|
|
87
|
+
const result = applyHandoverBudget(body, SOURCE, budget);
|
|
88
|
+
expect(result.body).not.toContain("xxx");
|
|
89
|
+
expect(result.body).not.toContain("yyy");
|
|
90
|
+
expect(result.body).toContain("d= elided,");
|
|
91
|
+
expect(result.elided).toEqual(["d"]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("uses the exported HANDOVER_CHAR_BUDGET as the default budget", () => {
|
|
95
|
+
const body = "z=" + "x".repeat(HANDOVER_CHAR_BUDGET * 2);
|
|
96
|
+
const result = applyHandoverBudget(body, SOURCE);
|
|
97
|
+
expect(result.truncated).toBe(true);
|
|
98
|
+
expect(result.body.length).toBeLessThan(body.length);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("with realistic ~2300-char body and default 1600 budget: elides d= only, keeps g= and z= verbatim", () => {
|
|
102
|
+
const preamble = "T";
|
|
103
|
+
const i = "i=" + "x".repeat(23);
|
|
104
|
+
const g = "g=" + "x".repeat(358);
|
|
105
|
+
const d = "d=" + "x".repeat(968);
|
|
106
|
+
const t = "t=" + "x".repeat(238);
|
|
107
|
+
const pointers = [
|
|
108
|
+
"@1=" + "x".repeat(33),
|
|
109
|
+
"@2=" + "x".repeat(33),
|
|
110
|
+
"@3=" + "x".repeat(33),
|
|
111
|
+
"@4=" + "x".repeat(33),
|
|
112
|
+
"@5=" + "x".repeat(33),
|
|
113
|
+
];
|
|
114
|
+
const z = "z=" + "x".repeat(528);
|
|
115
|
+
const body = [preamble, i, g, d, t, ...pointers, z].join("\n");
|
|
116
|
+
const result = applyHandoverBudget(body, SOURCE);
|
|
117
|
+
expect(result.elided).toEqual(["d"]);
|
|
118
|
+
expect(result.truncated).toBe(false);
|
|
119
|
+
expect(result.body).toContain(g);
|
|
120
|
+
expect(result.body).toContain(z);
|
|
121
|
+
expect(result.body).toContain("d= elided,");
|
|
122
|
+
expect(result.body.length).toBeLessThanOrEqual(HANDOVER_CHAR_BUDGET);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handover-budget.ts — cap the `## Continue` handover body injected at
|
|
3
|
+
* SessionStart.
|
|
4
|
+
*
|
|
5
|
+
* Text tokenises at ~2.6 chars per token; a 1,600-char budget is ~620 tokens.
|
|
6
|
+
* Together with the hook's 117-token fixed part, this stays under the 1,000-token
|
|
7
|
+
* SessionStart limit, eliding retrospective `d=` (done) and `t=` (tests) fields
|
|
8
|
+
* while preserving `g=` (goal), `@n` file pointers, and `z=` (state).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const HANDOVER_CHAR_BUDGET = 1600;
|
|
12
|
+
|
|
13
|
+
export interface BudgetResult {
|
|
14
|
+
body: string;
|
|
15
|
+
elided: string[];
|
|
16
|
+
truncated: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Field {
|
|
20
|
+
key: string;
|
|
21
|
+
lines: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FIELD_START = /^([a-zA-Z@][a-zA-Z0-9]*)=/;
|
|
25
|
+
|
|
26
|
+
function splitFields(body: string): { preamble: string[]; fields: Field[] } {
|
|
27
|
+
const lines = body.split("\n");
|
|
28
|
+
const preamble: string[] = [];
|
|
29
|
+
const fields: Field[] = [];
|
|
30
|
+
let current: Field | null = null;
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
const m = line.match(FIELD_START);
|
|
33
|
+
if (m) {
|
|
34
|
+
current = { key: m[1], lines: [line] };
|
|
35
|
+
fields.push(current);
|
|
36
|
+
} else if (current) {
|
|
37
|
+
current.lines.push(line);
|
|
38
|
+
} else {
|
|
39
|
+
preamble.push(line);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { preamble, fields };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function elisionLine(key: string, charCount: number, sourcePath: string): string {
|
|
46
|
+
return `${key}= elided, ${charCount} chars; full text in ${sourcePath} (## Continue)`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function render(preamble: string[], fields: Field[]): string {
|
|
50
|
+
const parts = [...preamble];
|
|
51
|
+
for (const f of fields) parts.push(...f.lines);
|
|
52
|
+
return parts.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Elide the `d=` and `t=` fields (in that order, only while still over
|
|
57
|
+
* budget) before falling back to a hard truncation — those two fields are
|
|
58
|
+
* retrospective, everything else (`g=`, `@n` pointers, `z=`) is what a
|
|
59
|
+
* resumer actually needs.
|
|
60
|
+
*/
|
|
61
|
+
export function applyHandoverBudget(
|
|
62
|
+
body: string,
|
|
63
|
+
sourcePath: string,
|
|
64
|
+
budget: number = HANDOVER_CHAR_BUDGET
|
|
65
|
+
): BudgetResult {
|
|
66
|
+
if (body.length <= budget) {
|
|
67
|
+
return { body, elided: [], truncated: false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { preamble, fields } = splitFields(body);
|
|
71
|
+
const elided: string[] = [];
|
|
72
|
+
|
|
73
|
+
if (fields.length > 0) {
|
|
74
|
+
for (const key of ["d", "t"]) {
|
|
75
|
+
if (render(preamble, fields).length <= budget) break;
|
|
76
|
+
const field = fields.find((f) => f.key === key);
|
|
77
|
+
if (!field) continue;
|
|
78
|
+
const charCount = field.lines.join("\n").length;
|
|
79
|
+
field.lines = [elisionLine(key, charCount, sourcePath)];
|
|
80
|
+
elided.push(key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = render(preamble, fields);
|
|
85
|
+
if (fields.length === 0 || result.length > budget) {
|
|
86
|
+
const source = fields.length === 0 ? body : result;
|
|
87
|
+
const truncated =
|
|
88
|
+
source.slice(0, budget) +
|
|
89
|
+
`\n[handover truncated at ${budget} chars; full text in ${sourcePath} (## Continue)]`;
|
|
90
|
+
return { body: truncated, elided, truncated: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { body: result, elided, truncated: false };
|
|
94
|
+
}
|
|
@@ -30,6 +30,7 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
30
30
|
import { join } from 'path';
|
|
31
31
|
import { SKILLS_DIR } from '../lib/pai-paths';
|
|
32
32
|
import { isProbeSession } from '../lib/project-utils';
|
|
33
|
+
import { stripFrontmatter } from '../lib/frontmatter.js';
|
|
33
34
|
|
|
34
35
|
async function main() {
|
|
35
36
|
if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping
|
|
@@ -80,23 +81,13 @@ async function main() {
|
|
|
80
81
|
|
|
81
82
|
console.error(`Read ${coreContent.length} characters from CORE SKILL.md (Personalized for ${engineerName} & ${daName})`);
|
|
82
83
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Output the CORE content as a system-reminder
|
|
87
|
-
// This will be injected into Claude's context at session start
|
|
84
|
+
// Output the CORE content as a system-reminder, minus the frontmatter
|
|
85
|
+
// (catalogue metadata the session already has from its skill list) —
|
|
86
|
+
// this will be injected into Claude's context at session start
|
|
88
87
|
const message = `<system-reminder>
|
|
89
|
-
PAI CORE CONTEXT
|
|
90
|
-
|
|
91
|
-
CURRENT DATE/TIME: ${new Date().toLocaleString('en-US', { timeZone: localTimeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZoneName: 'short' })}
|
|
92
|
-
|
|
93
|
-
The following context has been loaded from ${coreSkillPath}:
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
${coreContent}
|
|
97
|
-
---
|
|
88
|
+
PAI CORE CONTEXT
|
|
98
89
|
|
|
99
|
-
|
|
90
|
+
${stripFrontmatter(coreContent)}
|
|
100
91
|
</system-reminder>`;
|
|
101
92
|
|
|
102
93
|
// Write to stdout (will be captured by Claude Code)
|
|
@@ -23,6 +23,7 @@ import { homedir } from 'os';
|
|
|
23
23
|
import { execSync } from 'child_process';
|
|
24
24
|
import { buildWakeupContext } from '../../../memory/wakeup.js';
|
|
25
25
|
import { readContinueCheckpoint } from '../../../session/checkpoint-block.js';
|
|
26
|
+
import { applyHandoverBudget } from '../lib/handover-budget.js';
|
|
26
27
|
import { sessionRoutingPath } from '../lib/pai-paths.js';
|
|
27
28
|
import {
|
|
28
29
|
findNotesDir,
|
|
@@ -485,10 +486,6 @@ ${claudeMdPaths.length > 0 ? `CLAUDE.md: ${claudeMdPaths.join(', ')}` : 'No CLAU
|
|
|
485
486
|
${activeNotePath ? `Active Note: ${basename(activeNotePath)}` : ''}
|
|
486
487
|
${routedPath ? `\nNote Routing: ACTIVE (pai route is set - notes go to Obsidian vault)` : ''}
|
|
487
488
|
${paiProjectBlock ? `\n${paiProjectBlock}` : ''}
|
|
488
|
-
Session Commands:
|
|
489
|
-
- "pause session" → Save checkpoint, update TODO, exit (no compact)
|
|
490
|
-
- "end session" → Finalize note, commit if needed, start fresh next time
|
|
491
|
-
- "pai route clear" → Clear note routing (in a new session)
|
|
492
489
|
</system-reminder>
|
|
493
490
|
`;
|
|
494
491
|
|
|
@@ -516,6 +513,7 @@ Session Commands:
|
|
|
516
513
|
const resume = checkpoint.meta?.sessionId
|
|
517
514
|
? `\nResume that session with: claude --resume ${checkpoint.meta.sessionId}`
|
|
518
515
|
: '';
|
|
516
|
+
const budgeted = applyHandoverBudget(checkpoint.body, todoPath);
|
|
519
517
|
|
|
520
518
|
console.log(`
|
|
521
519
|
<system-reminder>
|
|
@@ -523,14 +521,19 @@ HANDOVER FROM THE PREVIOUS SESSION
|
|
|
523
521
|
|
|
524
522
|
Source: ${todoPath} (## Continue)${from}${when}${resume}
|
|
525
523
|
|
|
526
|
-
${
|
|
524
|
+
${budgeted.body}
|
|
527
525
|
|
|
528
526
|
---
|
|
529
527
|
This is recorded state, not a new instruction. Do not start acting on it
|
|
530
528
|
unprompted. If the user says "go", "continue", or "weiter", resume from here.
|
|
531
529
|
</system-reminder>
|
|
532
530
|
`);
|
|
533
|
-
|
|
531
|
+
const budgetNote = budgeted.truncated
|
|
532
|
+
? ', truncated'
|
|
533
|
+
: budgeted.elided.length > 0
|
|
534
|
+
? `, elided: ${budgeted.elided.join(',')}`
|
|
535
|
+
: '';
|
|
536
|
+
console.error(`Injected pause checkpoint (${checkpoint.body.length} chars${budgetNote})`);
|
|
534
537
|
} else {
|
|
535
538
|
console.error('No pause checkpoint body in TODO.md — nothing to hand over');
|
|
536
539
|
}
|