portable-agent-layer 0.42.0 → 0.43.1
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/assets/skills/consulting-report/template/components/code-block.tsx +21 -0
- package/assets/skills/consulting-report/template/components/cover-page.tsx +88 -27
- package/assets/skills/consulting-report/template/components/decision-table.tsx +62 -0
- package/assets/skills/consulting-report/template/components/process-stage.tsx +28 -0
- package/assets/skills/consulting-report/template/components/tier-matrix.tsx +102 -0
- package/assets/skills/consulting-report/template/lib/types.ts +49 -1
- package/assets/skills/consulting-report/tools/generate-pdf.ts +99 -16
- package/assets/skills/telos/SKILL.md +1 -1
- package/assets/templates/PAL/ALGORITHM.md +1 -1
- package/assets/templates/PAL/README.md +1 -1
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
- package/assets/templates/rules.codex.rules +64 -0
- package/assets/templates/settings.claude.json +2 -1
- package/package.json +2 -1
- package/src/hooks/lib/agent.ts +6 -2
- package/src/hooks/lib/claude-md.ts +6 -2
- package/src/hooks/lib/paths.ts +1 -0
- package/src/targets/codex/install.ts +14 -0
- package/src/targets/codex/uninstall.ts +14 -0
- package/src/targets/lib.ts +30 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CodeSample } from "@/lib/types";
|
|
2
|
+
|
|
3
|
+
interface CodeBlockProps {
|
|
4
|
+
sample: CodeSample;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function CodeBlock({ sample }: CodeBlockProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="my-4 break-inside-avoid">
|
|
10
|
+
<div className="font-sans text-[0.65rem] font-bold uppercase tracking-widest text-primary mb-1">
|
|
11
|
+
{sample.language}
|
|
12
|
+
</div>
|
|
13
|
+
<pre className="rounded-lg overflow-x-auto text-[0.8rem] leading-relaxed px-5 py-4 bg-[#f6f8fa]">
|
|
14
|
+
<code>{sample.code}</code>
|
|
15
|
+
</pre>
|
|
16
|
+
{sample.caption && (
|
|
17
|
+
<p className="text-[0.78rem] text-muted italic mt-1">{sample.caption}</p>
|
|
18
|
+
)}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -1,46 +1,107 @@
|
|
|
1
|
+
interface MetadataItem {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
interface CoverPageProps {
|
|
2
|
-
clientName: string;
|
|
3
7
|
reportTitle: string;
|
|
4
8
|
reportDate: string;
|
|
5
|
-
|
|
9
|
+
clientName: string;
|
|
6
10
|
consultancyName: string;
|
|
7
11
|
preTitle?: string;
|
|
12
|
+
/** Path to consultancy logo image. Falls back to consultancyName text wordmark. */
|
|
13
|
+
consultancyLogoSrc?: string;
|
|
14
|
+
/** Path to client logo image. Falls back to clientName text wordmark. */
|
|
15
|
+
clientLogoSrc?: string;
|
|
16
|
+
/** Optional metadata rows rendered in the bottom strip (e.g. classification, version). */
|
|
17
|
+
metadata?: MetadataItem[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function LogoSlot({
|
|
21
|
+
src,
|
|
22
|
+
name,
|
|
23
|
+
align = "left",
|
|
24
|
+
}: {
|
|
25
|
+
src?: string;
|
|
26
|
+
name: string;
|
|
27
|
+
align?: "left" | "right";
|
|
28
|
+
}) {
|
|
29
|
+
if (src) {
|
|
30
|
+
return (
|
|
31
|
+
// biome-ignore lint/performance/noImgElement: Template reports are static exports; next/image is not needed for print logos.
|
|
32
|
+
<img
|
|
33
|
+
src={src}
|
|
34
|
+
alt={name}
|
|
35
|
+
className="h-10 w-auto object-contain"
|
|
36
|
+
style={{ maxWidth: 180 }}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return (
|
|
41
|
+
<span
|
|
42
|
+
className={`font-sans text-sm font-bold uppercase tracking-widest text-foreground ${
|
|
43
|
+
align === "right" ? "text-right" : ""
|
|
44
|
+
}`}
|
|
45
|
+
>
|
|
46
|
+
{name}
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
8
49
|
}
|
|
9
50
|
|
|
10
51
|
export function CoverPage({
|
|
11
|
-
clientName,
|
|
12
52
|
reportTitle,
|
|
13
|
-
|
|
14
|
-
classification,
|
|
53
|
+
clientName,
|
|
15
54
|
consultancyName,
|
|
16
|
-
preTitle
|
|
55
|
+
preTitle,
|
|
56
|
+
consultancyLogoSrc,
|
|
57
|
+
clientLogoSrc,
|
|
58
|
+
metadata,
|
|
17
59
|
}: CoverPageProps) {
|
|
18
60
|
return (
|
|
19
|
-
<div
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
].join(" ")}
|
|
25
|
-
>
|
|
26
|
-
<div className="font-sans text-sm font-semibold text-destructive uppercase tracking-[0.15em] mb-16">
|
|
27
|
-
{classification}
|
|
61
|
+
<div className="min-h-screen flex flex-col break-after-page bg-background">
|
|
62
|
+
{/* Logo strip */}
|
|
63
|
+
<div className="flex items-center justify-between px-16 py-8 border-b-2 border-primary">
|
|
64
|
+
<LogoSlot src={consultancyLogoSrc} name={consultancyName} align="left" />
|
|
65
|
+
<LogoSlot src={clientLogoSrc} name={clientName} align="right" />
|
|
28
66
|
</div>
|
|
29
67
|
|
|
30
|
-
|
|
31
|
-
|
|
68
|
+
{/* Main content — vertically centered */}
|
|
69
|
+
<div className="flex-1 flex flex-col justify-center px-16 py-20">
|
|
70
|
+
{preTitle && (
|
|
71
|
+
<p className="font-sans text-xs font-bold uppercase tracking-widest text-primary mb-5">
|
|
72
|
+
{preTitle}
|
|
73
|
+
</p>
|
|
74
|
+
)}
|
|
75
|
+
<h1 className="font-heading text-5xl font-semibold text-foreground leading-tight mb-6 tracking-tight max-w-2xl">
|
|
76
|
+
{reportTitle}
|
|
77
|
+
</h1>
|
|
78
|
+
<p className="font-heading text-xl text-muted font-normal">
|
|
79
|
+
Prepared for {clientName}
|
|
80
|
+
</p>
|
|
32
81
|
</div>
|
|
33
|
-
<h1 className="font-heading text-5xl font-semibold text-foreground leading-tight mb-4 tracking-tight">
|
|
34
|
-
{reportTitle}
|
|
35
|
-
</h1>
|
|
36
|
-
<p className="font-heading text-2xl text-muted mb-16 font-normal">
|
|
37
|
-
Prepared for {clientName}
|
|
38
|
-
</p>
|
|
39
82
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
83
|
+
{/* Metadata strip */}
|
|
84
|
+
{metadata && metadata.length > 0 && (
|
|
85
|
+
<div className="border-t border-border px-16 py-8">
|
|
86
|
+
<dl
|
|
87
|
+
className="grid gap-x-12 gap-y-4"
|
|
88
|
+
style={{
|
|
89
|
+
gridTemplateColumns: `repeat(${Math.min(metadata.length, 4)}, auto) 1fr`,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{metadata.map((item) => (
|
|
93
|
+
<div key={item.label}>
|
|
94
|
+
<dt className="font-sans text-[0.62rem] font-bold uppercase tracking-widest text-muted mb-1">
|
|
95
|
+
{item.label}
|
|
96
|
+
</dt>
|
|
97
|
+
<dd className="font-sans text-sm font-medium text-foreground">
|
|
98
|
+
{item.value}
|
|
99
|
+
</dd>
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</dl>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
44
105
|
</div>
|
|
45
106
|
);
|
|
46
107
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Decision } from "@/lib/types";
|
|
2
|
+
|
|
3
|
+
interface DecisionTableProps {
|
|
4
|
+
decisions: Decision[];
|
|
5
|
+
intro?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function StatusBadge({ status }: { status: Decision["status"] }) {
|
|
9
|
+
if (status === "adopt-default") {
|
|
10
|
+
return (
|
|
11
|
+
<span className="inline-block px-2 py-0.5 rounded font-sans text-[0.65rem] font-bold uppercase tracking-widest bg-success/10 text-success border border-success/30">
|
|
12
|
+
Adopt default
|
|
13
|
+
</span>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return (
|
|
17
|
+
<span className="inline-block px-2 py-0.5 rounded font-sans text-[0.65rem] font-bold uppercase tracking-widest bg-warning/10 text-warning border border-warning/30">
|
|
18
|
+
Confirm at sync
|
|
19
|
+
</span>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function DecisionTable({ decisions, intro }: DecisionTableProps) {
|
|
24
|
+
const thClass =
|
|
25
|
+
"font-sans text-[0.68rem] font-bold uppercase tracking-widest text-primary px-3 py-2 border-b-2 border-primary text-left";
|
|
26
|
+
const tdClass = "px-3 py-2.5 border-b border-border-subtle align-top";
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="my-4">
|
|
30
|
+
{intro && <p className="text-[0.9375rem] mb-4">{intro}</p>}
|
|
31
|
+
<table className="w-full border-collapse font-body text-[0.85rem]">
|
|
32
|
+
<thead>
|
|
33
|
+
<tr>
|
|
34
|
+
<th className={`${thClass} w-[4%]`}>#</th>
|
|
35
|
+
<th className={`${thClass} w-[28%]`}>Decision</th>
|
|
36
|
+
<th className={`${thClass} w-[44%]`}>Recommended default</th>
|
|
37
|
+
<th className={`${thClass} w-[24%]`}>Status</th>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody>
|
|
41
|
+
{decisions.map((d, i) => (
|
|
42
|
+
<tr key={d.id} className="break-inside-avoid">
|
|
43
|
+
<td className={`${tdClass} text-muted font-sans text-[0.78rem]`}>
|
|
44
|
+
{i + 1}
|
|
45
|
+
</td>
|
|
46
|
+
<td className={tdClass}>
|
|
47
|
+
<div className="font-sans font-semibold text-foreground">{d.title}</div>
|
|
48
|
+
{d.description && (
|
|
49
|
+
<div className="text-muted text-[0.82rem] mt-0.5">{d.description}</div>
|
|
50
|
+
)}
|
|
51
|
+
</td>
|
|
52
|
+
<td className={`${tdClass} text-muted`}>{d.recommendedDefault}</td>
|
|
53
|
+
<td className={tdClass}>
|
|
54
|
+
<StatusBadge status={d.status} />
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
))}
|
|
58
|
+
</tbody>
|
|
59
|
+
</table>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ProcessStage } from "@/lib/types";
|
|
2
|
+
|
|
3
|
+
interface ProcessStageProps {
|
|
4
|
+
stage: ProcessStage;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function ProcessStageBlock({ stage }: ProcessStageProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="mb-6 break-inside-avoid">
|
|
10
|
+
<div className="font-sans font-semibold text-primary text-xs uppercase tracking-widest mb-3">
|
|
11
|
+
{stage.name}
|
|
12
|
+
</div>
|
|
13
|
+
<ol className="list-decimal list-outside ml-5 space-y-2">
|
|
14
|
+
{stage.items.map((item) => (
|
|
15
|
+
<li
|
|
16
|
+
key={`${item.text}:${item.note ?? ""}`}
|
|
17
|
+
className="text-[0.9375rem] text-foreground pl-1"
|
|
18
|
+
>
|
|
19
|
+
{item.text}
|
|
20
|
+
{item.note && (
|
|
21
|
+
<span className="ml-1 text-muted text-[0.82rem] italic">({item.note})</span>
|
|
22
|
+
)}
|
|
23
|
+
</li>
|
|
24
|
+
))}
|
|
25
|
+
</ol>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import type { TierMatrixCellValue, TierMatrixRow } from "@/lib/types";
|
|
3
|
+
|
|
4
|
+
interface TierMatrixProps {
|
|
5
|
+
columns: string[];
|
|
6
|
+
rows: TierMatrixRow[];
|
|
7
|
+
caption?: string;
|
|
8
|
+
/** Alignment for data cells. Defaults to "center" for symbol matrices; use "left" for text-heavy tables. */
|
|
9
|
+
alignCells?: "left" | "center";
|
|
10
|
+
/**
|
|
11
|
+
* Column sizing mode:
|
|
12
|
+
* - "auto" (default): browser distributes space by content — no explicit widths applied.
|
|
13
|
+
* - "capped": columnWidths[i] applied as max-width — content drives size up to the cap.
|
|
14
|
+
* - "manual": columnWidths[i] applied as explicit width.
|
|
15
|
+
* columnWidths is parallel to ALL columns: [0] = layer column, [1..n] = data columns.
|
|
16
|
+
*/
|
|
17
|
+
sizing?: "auto" | "capped" | "manual";
|
|
18
|
+
columnWidths?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function CellContent({ value }: { value: TierMatrixCellValue }) {
|
|
22
|
+
if (value === "required") {
|
|
23
|
+
return <span className="text-success font-bold text-base">✓</span>;
|
|
24
|
+
}
|
|
25
|
+
if (value === "recommended") {
|
|
26
|
+
return (
|
|
27
|
+
<span className="font-sans text-[0.7rem] font-semibold uppercase tracking-widest text-warning">
|
|
28
|
+
rec.
|
|
29
|
+
</span>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (value === false) {
|
|
33
|
+
return <span className="text-muted">—</span>;
|
|
34
|
+
}
|
|
35
|
+
return <span>{value}</span>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function TierMatrix({
|
|
39
|
+
columns,
|
|
40
|
+
rows,
|
|
41
|
+
caption,
|
|
42
|
+
alignCells = "center",
|
|
43
|
+
sizing = "auto",
|
|
44
|
+
columnWidths,
|
|
45
|
+
}: TierMatrixProps) {
|
|
46
|
+
const thClass =
|
|
47
|
+
"font-sans text-[0.68rem] font-bold uppercase tracking-widest text-primary px-3 py-2 border-b-2 border-primary";
|
|
48
|
+
const tdClass = "px-3 py-2 border-b border-border-subtle align-middle";
|
|
49
|
+
const colHeaderAlign = alignCells === "left" ? "text-left" : "text-center";
|
|
50
|
+
const cellAlign = alignCells === "left" ? "text-left" : "text-center";
|
|
51
|
+
|
|
52
|
+
function colStyle(index: number): CSSProperties | undefined {
|
|
53
|
+
const val = columnWidths?.[index];
|
|
54
|
+
if (!val) return undefined;
|
|
55
|
+
if (sizing === "manual") return { width: val };
|
|
56
|
+
if (sizing === "capped") return { maxWidth: val };
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="my-4">
|
|
62
|
+
<table className="w-full border-collapse font-body text-[0.85rem] break-inside-avoid">
|
|
63
|
+
<thead>
|
|
64
|
+
<tr>
|
|
65
|
+
<th className={`${thClass} text-left`} style={colStyle(0)}>
|
|
66
|
+
Layer
|
|
67
|
+
</th>
|
|
68
|
+
{columns.map((col, i) => (
|
|
69
|
+
<th
|
|
70
|
+
key={col}
|
|
71
|
+
className={`${thClass} ${colHeaderAlign}`}
|
|
72
|
+
style={colStyle(i + 1)}
|
|
73
|
+
>
|
|
74
|
+
{col}
|
|
75
|
+
</th>
|
|
76
|
+
))}
|
|
77
|
+
</tr>
|
|
78
|
+
</thead>
|
|
79
|
+
<tbody>
|
|
80
|
+
{rows.map((row) => (
|
|
81
|
+
<tr key={`${row.layer}:${row.cells.join("|")}`}>
|
|
82
|
+
<td
|
|
83
|
+
className={`${tdClass} text-left font-sans font-medium text-foreground`}
|
|
84
|
+
>
|
|
85
|
+
{row.layer}
|
|
86
|
+
</td>
|
|
87
|
+
{row.cells.map((cell, i) => (
|
|
88
|
+
<td
|
|
89
|
+
key={`${columns[i] ?? "cell"}:${String(cell)}`}
|
|
90
|
+
className={`${tdClass} ${cellAlign}`}
|
|
91
|
+
>
|
|
92
|
+
<CellContent value={cell} />
|
|
93
|
+
</td>
|
|
94
|
+
))}
|
|
95
|
+
</tr>
|
|
96
|
+
))}
|
|
97
|
+
</tbody>
|
|
98
|
+
</table>
|
|
99
|
+
{caption && <p className="text-[0.78rem] text-muted italic mt-1">{caption}</p>}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// project (the report instance that supplies the actual data). Projects import
|
|
5
5
|
// the types they need; they do NOT edit this file.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
7
|
+
// Three families of types:
|
|
8
8
|
//
|
|
9
9
|
// 1. Strategic-assessment shapes — Finding, Recommendation, TimelinePhase,
|
|
10
10
|
// and the ReportData interface composed of them. These power the default
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
// power rubric-style deliverables (an opportunity scoring playbook is one
|
|
15
15
|
// example). Each is optional; projects use whichever apply.
|
|
16
16
|
//
|
|
17
|
+
// 3. Process-guide shapes — TierMatrixRow, ProcessStage, CodeSample, Decision.
|
|
18
|
+
// Power process documentation deliverables (tiered requirements matrices,
|
|
19
|
+
// stage-by-stage process flows, technical appendices, sign-off tables).
|
|
20
|
+
//
|
|
17
21
|
// Adding a new section type? Add its interface here so all reports share the
|
|
18
22
|
// same vocabulary.
|
|
19
23
|
|
|
@@ -188,3 +192,47 @@ export interface TuningLogEntry {
|
|
|
188
192
|
rationale: string;
|
|
189
193
|
approver: string;
|
|
190
194
|
}
|
|
195
|
+
|
|
196
|
+
// --- Process-guide shapes ---
|
|
197
|
+
|
|
198
|
+
/** Generic N-column checklist matrix (e.g. requirements by tier/level). */
|
|
199
|
+
export type TierMatrixCellValue = "required" | "recommended" | false | string;
|
|
200
|
+
export interface TierMatrixRow {
|
|
201
|
+
/** Row label (e.g. "Formatter", "Secret scan"). */
|
|
202
|
+
layer: string;
|
|
203
|
+
/** One value per column, parallel to the `columns` prop on TierMatrix. */
|
|
204
|
+
cells: TierMatrixCellValue[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** One item inside a ProcessStage. */
|
|
208
|
+
export interface ProcessStageItem {
|
|
209
|
+
text: string;
|
|
210
|
+
/** Optional inline cross-reference, e.g. "see §4.6". Renders muted. */
|
|
211
|
+
note?: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A named process stage with ordered action items. */
|
|
215
|
+
export interface ProcessStage {
|
|
216
|
+
id: string;
|
|
217
|
+
name: string;
|
|
218
|
+
items: ProcessStageItem[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** A code sample for a technical appendix. */
|
|
222
|
+
export interface CodeSample {
|
|
223
|
+
id: string;
|
|
224
|
+
title: string;
|
|
225
|
+
/** Language identifier shown as a badge, e.g. "yaml", "typescript", "markdown". */
|
|
226
|
+
language: string;
|
|
227
|
+
caption?: string;
|
|
228
|
+
code: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** A single row in a sign-off or open-decisions table. */
|
|
232
|
+
export interface Decision {
|
|
233
|
+
id: string;
|
|
234
|
+
title: string;
|
|
235
|
+
description?: string;
|
|
236
|
+
recommendedDefault: string;
|
|
237
|
+
status: "adopt-default" | "confirm-at-sync";
|
|
238
|
+
}
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
// node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts <report-dir> [--pdf <out>] [--html <out>] [--skip-build]
|
|
13
13
|
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
|
-
import { createReadStream, constants as fsConstants } from "node:fs";
|
|
16
|
-
import { access, stat } from "node:fs/promises";
|
|
15
|
+
import { createReadStream, constants as fsConstants, realpathSync } from "node:fs";
|
|
16
|
+
import { access, readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
17
17
|
import { createServer, type Server } from "node:http";
|
|
18
18
|
import { extname, join, resolve } from "node:path";
|
|
19
19
|
import { pathToFileURL } from "node:url";
|
|
20
|
+
import { PDFDocument } from "pdf-lib";
|
|
20
21
|
import { chromium } from "playwright";
|
|
21
22
|
|
|
22
23
|
interface ReportMeta {
|
|
@@ -24,6 +25,10 @@ interface ReportMeta {
|
|
|
24
25
|
reportTitle: string;
|
|
25
26
|
classification: string;
|
|
26
27
|
consultancyName: string;
|
|
28
|
+
/** Public path to consultancy logo (e.g. "/logos/konvert7.svg"). Used in the PDF footer. */
|
|
29
|
+
consultancyLogoSrc?: string;
|
|
30
|
+
/** Public path to client logo (e.g. "/logos/transcend.svg"). Used in the PDF header. */
|
|
31
|
+
clientLogoSrc?: string;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
const COLOR = {
|
|
@@ -56,6 +61,25 @@ function slugify(s: string): string {
|
|
|
56
61
|
.replace(/^-|-$/g, "");
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
/** Reads a logo file from the static export and returns a base64 data URI, or null if not found. */
|
|
65
|
+
async function logoDataUri(
|
|
66
|
+
outDir: string,
|
|
67
|
+
publicPath: string | undefined
|
|
68
|
+
): Promise<string | null> {
|
|
69
|
+
if (!publicPath) return null;
|
|
70
|
+
const filePath = join(outDir, publicPath);
|
|
71
|
+
if (!(await exists(filePath))) return null;
|
|
72
|
+
const buf = await readFile(filePath);
|
|
73
|
+
const ext = extname(filePath).toLowerCase();
|
|
74
|
+
let mime = "image/jpeg";
|
|
75
|
+
if (ext === ".svg") {
|
|
76
|
+
mime = "image/svg+xml";
|
|
77
|
+
} else if (ext === ".png") {
|
|
78
|
+
mime = "image/png";
|
|
79
|
+
}
|
|
80
|
+
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
async function loadMeta(reportDir: string): Promise<ReportMeta> {
|
|
60
84
|
const dataPath = join(reportDir, "lib", "report-data.ts");
|
|
61
85
|
if (!(await exists(dataPath))) {
|
|
@@ -150,29 +174,79 @@ async function renderPdf(
|
|
|
150
174
|
);
|
|
151
175
|
});
|
|
152
176
|
|
|
177
|
+
// Load logos as base64 data URIs — Playwright's header/footer templates
|
|
178
|
+
// run in an isolated context and cannot load external or local-path URLs.
|
|
179
|
+
const [clientUri, consultancyUri] = await Promise.all([
|
|
180
|
+
logoDataUri(outDir, meta.clientLogoSrc),
|
|
181
|
+
logoDataUri(outDir, meta.consultancyLogoSrc),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
const clientSlot = clientUri
|
|
185
|
+
? `<img src="${clientUri}" style="height:18px; width:auto; object-fit:contain; display:block;">`
|
|
186
|
+
: `<span style="font-weight:600; color:${COLOR.navy}; letter-spacing:0.05em;">${escapeHtml(meta.clientName.toUpperCase())}</span>`;
|
|
187
|
+
|
|
188
|
+
const consultancySlot = consultancyUri
|
|
189
|
+
? `<img src="${consultancyUri}" style="height:14px; width:auto; object-fit:contain; display:block;">`
|
|
190
|
+
: `<span style="color:${COLOR.navy};">${escapeHtml(meta.consultancyName)}</span>`;
|
|
191
|
+
|
|
153
192
|
const header = `
|
|
154
193
|
<div style="width:100%; font-family:Inter,'Helvetica Neue',Arial,sans-serif; font-size:7.5pt; padding:0 0.7in; display:flex; justify-content:space-between; align-items:center;">
|
|
155
|
-
|
|
194
|
+
${clientSlot}
|
|
156
195
|
<span style="color:#94a3b8;">${escapeHtml(meta.reportTitle)}</span>
|
|
157
196
|
</div>`;
|
|
158
197
|
|
|
198
|
+
// Footer: consultancy logo left, page number right — no classification (cover carries it)
|
|
159
199
|
const footer = `
|
|
160
200
|
<div style="width:100%; font-family:Inter,'Helvetica Neue',Arial,sans-serif; font-size:7.5pt; padding:0 0.7in; display:flex; justify-content:space-between; align-items:center;">
|
|
161
|
-
|
|
162
|
-
<span style="color:${COLOR.navy};">${escapeHtml(meta.consultancyName)}</span>
|
|
201
|
+
${consultancySlot}
|
|
163
202
|
<span style="color:${COLOR.navy};"><span class="pageNumber"></span></span>
|
|
164
203
|
</div>`;
|
|
165
204
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
205
|
+
const margin = { top: "0.7in", right: "0.7in", bottom: "0.7in", left: "0.7in" };
|
|
206
|
+
|
|
207
|
+
// Render cover (page 1) and body (pages 2+) separately so the cover has
|
|
208
|
+
// no header/footer and full-bleed margins, then merge with pdf-lib.
|
|
209
|
+
const tmpCover = `${pdfPath}.cover.tmp.pdf`;
|
|
210
|
+
const tmpBody = `${pdfPath}.body.tmp.pdf`;
|
|
211
|
+
|
|
212
|
+
await Promise.all([
|
|
213
|
+
page.pdf({
|
|
214
|
+
path: tmpCover,
|
|
215
|
+
format: "A4",
|
|
216
|
+
pageRanges: "1",
|
|
217
|
+
printBackground: true,
|
|
218
|
+
displayHeaderFooter: false,
|
|
219
|
+
margin: { top: "0", right: "0", bottom: "0", left: "0" },
|
|
220
|
+
preferCSSPageSize: false,
|
|
221
|
+
}),
|
|
222
|
+
page.pdf({
|
|
223
|
+
path: tmpBody,
|
|
224
|
+
format: "A4",
|
|
225
|
+
pageRanges: "2-",
|
|
226
|
+
printBackground: true,
|
|
227
|
+
displayHeaderFooter: true,
|
|
228
|
+
headerTemplate: header,
|
|
229
|
+
footerTemplate: footer,
|
|
230
|
+
margin,
|
|
231
|
+
preferCSSPageSize: false,
|
|
232
|
+
}),
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
// Merge: use body PDF as the base so its named destinations and link
|
|
236
|
+
// annotations stay intact. Insert the cover page at position 0.
|
|
237
|
+
const [coverBytes, bodyBytes] = await Promise.all([
|
|
238
|
+
readFile(tmpCover),
|
|
239
|
+
readFile(tmpBody),
|
|
240
|
+
]);
|
|
241
|
+
const [coverDoc, bodyDoc] = await Promise.all([
|
|
242
|
+
PDFDocument.load(coverBytes),
|
|
243
|
+
PDFDocument.load(bodyBytes),
|
|
244
|
+
]);
|
|
245
|
+
const [coverPage] = await bodyDoc.copyPages(coverDoc, [0]);
|
|
246
|
+
bodyDoc.insertPage(0, coverPage);
|
|
247
|
+
await writeFile(pdfPath, await bodyDoc.save());
|
|
248
|
+
|
|
249
|
+
await Promise.all([unlink(tmpCover), unlink(tmpBody)]);
|
|
176
250
|
} finally {
|
|
177
251
|
await browser.close();
|
|
178
252
|
server.close();
|
|
@@ -236,9 +310,18 @@ async function run(argv: string[] = process.argv.slice(2)): Promise<void> {
|
|
|
236
310
|
}
|
|
237
311
|
|
|
238
312
|
// Node ≥ 22.6 doesn't expose import.meta.main; gate on argv[1] instead.
|
|
313
|
+
// Use realpathSync on both sides so symlinked skill paths (e.g. ~/.pal/skills →
|
|
314
|
+
// PAL repo) match the resolved import.meta.url.
|
|
315
|
+
function realResolve(p: string): string {
|
|
316
|
+
try {
|
|
317
|
+
return realpathSync(resolve(p));
|
|
318
|
+
} catch {
|
|
319
|
+
return resolve(p);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
239
322
|
const isMain =
|
|
240
323
|
process.argv[1] &&
|
|
241
|
-
|
|
324
|
+
realResolve(process.argv[1]) === realResolve(new URL(import.meta.url).pathname);
|
|
242
325
|
if (isMain) {
|
|
243
326
|
await run();
|
|
244
327
|
}
|
|
@@ -4,7 +4,7 @@ description: Personal context management. Use when discussing goals, beliefs, ch
|
|
|
4
4
|
argument-hint: [area to view or update]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Manage the user's TELOS files —
|
|
7
|
+
Manage the user's TELOS files — from Greek τέλος (télos), meaning end/purpose/goal. The persistent personal context that orients PAL around who the user is and what they're working toward.
|
|
8
8
|
|
|
9
9
|
## TELOS Files
|
|
10
10
|
|
|
@@ -38,7 +38,7 @@ Thinking-only. No tool calls except context recovery (Grep/Glob/Read).
|
|
|
38
38
|
**0.5. ISA context** — before reverse engineering, orient against the ISA:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
# If cwd matches a registered project — read its open ISCs:
|
|
41
|
+
# If cwd matches a registered project — read its open ISCs (Ideal State Criteria):
|
|
42
42
|
bun ~/.pal/tools/project.ts list-isc <project-name>
|
|
43
43
|
|
|
44
44
|
# If this is ad-hoc work with no registered project — scaffold a task ISA:
|
|
@@ -85,7 +85,7 @@ Persistent storage across sessions:
|
|
|
85
85
|
CLI utilities: `tool:opinion` (manage opinions), `tool:reflect` (relationship reflection), `tool:analyze` (learning analysis), `pal cli usage` (token usage tracking), `pal cli export` / `pal cli import` (state portability).
|
|
86
86
|
|
|
87
87
|
### TELOS (`telos/`)
|
|
88
|
-
|
|
88
|
+
From Greek τέλος (télos) — end, purpose, goal. The persistent personal context that orients PAL around who the user is and what they're working toward: mission, goals, beliefs, challenges, strategies, ideas, learnings, mental models, narratives. Managed via the telos skill.
|
|
89
89
|
|
|
90
90
|
### Security (`SecurityValidator.ts`)
|
|
91
91
|
Hook-based security: validates Bash commands and file operations against dangerous patterns. Fail-open design — blocks known-dangerous operations without breaking legitimate work.
|
|
@@ -41,7 +41,7 @@ AI agents come and go. Your accumulated knowledge, preferences, and workflows sh
|
|
|
41
41
|
**What portability means in practice:**
|
|
42
42
|
- No agent-specific assumptions in core logic
|
|
43
43
|
- Agent-specific code isolated in `src/targets/`
|
|
44
|
-
- Skills, memory, and TELOS are agent-agnostic
|
|
44
|
+
- Skills, memory, and TELOS (Greek: τέλος — end/purpose/goal; the user's persistent life context) are agent-agnostic
|
|
45
45
|
- A single `pal cli install --<agent>` registers everything
|
|
46
46
|
|
|
47
47
|
### 2. Cross-Platform by Default
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# BEGIN PAL MANAGED CODEX RULES
|
|
2
|
+
# Managed by PAL. Install replaces this block; uninstall removes only this block.
|
|
3
|
+
prefix_rule(
|
|
4
|
+
pattern = ["bun", "~/.pal/tools/project.ts"],
|
|
5
|
+
decision = "allow",
|
|
6
|
+
justification = "PAL project state commands are part of the installed PAL workflow",
|
|
7
|
+
match = [
|
|
8
|
+
"bun ~/.pal/tools/project.ts resume portable-agent-layer",
|
|
9
|
+
"bun ~/.pal/tools/project.ts list-isc portable-agent-layer",
|
|
10
|
+
"bun ~/.pal/tools/project.ts add-next portable-agent-layer note",
|
|
11
|
+
],
|
|
12
|
+
not_match = [
|
|
13
|
+
"bun ~/.pal/tools/other.ts",
|
|
14
|
+
],
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
prefix_rule(
|
|
18
|
+
pattern = ["bun", "~/.pal/tools/algorithm-reflect.ts"],
|
|
19
|
+
decision = "allow",
|
|
20
|
+
justification = "PAL Algorithm reflection logging is part of the installed PAL workflow",
|
|
21
|
+
match = [
|
|
22
|
+
"bun ~/.pal/tools/algorithm-reflect.ts --task work --criteria 1 --passed 1 --failed 0 --sentiment 8",
|
|
23
|
+
],
|
|
24
|
+
not_match = [
|
|
25
|
+
"bun ~/.pal/tools/project.ts list",
|
|
26
|
+
],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
prefix_rule(
|
|
30
|
+
pattern = [
|
|
31
|
+
"bun",
|
|
32
|
+
[
|
|
33
|
+
"~/.pal/tools/analyze.ts",
|
|
34
|
+
"~/.pal/tools/handoff-note.ts",
|
|
35
|
+
"~/.pal/tools/relationship-note.ts",
|
|
36
|
+
"~/.pal/tools/synthesize.ts",
|
|
37
|
+
"~/.pal/tools/thread.ts",
|
|
38
|
+
"~/.pal/tools/wisdom-frame.ts",
|
|
39
|
+
],
|
|
40
|
+
],
|
|
41
|
+
decision = "allow",
|
|
42
|
+
justification = "PAL installed agent tools are safe to run without repeated approval",
|
|
43
|
+
match = [
|
|
44
|
+
"bun ~/.pal/tools/thread.ts --list",
|
|
45
|
+
"bun ~/.pal/tools/wisdom-frame.ts --help",
|
|
46
|
+
],
|
|
47
|
+
not_match = [
|
|
48
|
+
"bun ~/.pal/tools/project.ts list",
|
|
49
|
+
"bun ./local-script.ts",
|
|
50
|
+
],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
prefix_rule(
|
|
54
|
+
pattern = ["node", "--experimental-strip-types", "~/.pal/skills/consulting-report/tools/generate-pdf.ts"],
|
|
55
|
+
decision = "allow",
|
|
56
|
+
justification = "PAL consulting-report PDF generation requires Node for Playwright compatibility",
|
|
57
|
+
match = [
|
|
58
|
+
"node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts ./report",
|
|
59
|
+
],
|
|
60
|
+
not_match = [
|
|
61
|
+
"node ~/.pal/skills/consulting-report/tools/generate-pdf.ts ./report",
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
# END PAL MANAGED CODEX RULES
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"Bash(stat //*)",
|
|
21
21
|
"Bash(readlink //*)",
|
|
22
22
|
"Bash(bun ~/.pal/skills/*/tools/*.ts *)",
|
|
23
|
-
"Bash(bun ~/.pal/tools/*.ts *)"
|
|
23
|
+
"Bash(bun ~/.pal/tools/*.ts *)",
|
|
24
|
+
"Bash(node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts *)"
|
|
24
25
|
]
|
|
25
26
|
},
|
|
26
27
|
"hooks": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.1",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"@konvert7/klint": "0.4.0",
|
|
83
83
|
"adm-zip": "^0.5.17",
|
|
84
84
|
"marked": "18.0.4",
|
|
85
|
+
"pdf-lib": "1.17.1",
|
|
85
86
|
"playwright": "^1.60.0"
|
|
86
87
|
}
|
|
87
88
|
}
|
package/src/hooks/lib/agent.ts
CHANGED
|
@@ -42,7 +42,7 @@ export const isOpencode = () => getActiveAgent() === "opencode";
|
|
|
42
42
|
* Format a "block this action" response for the current agent.
|
|
43
43
|
* Claude Code: { decision: "block", reason }
|
|
44
44
|
* Cursor preToolUse: { permission: "deny", user_message }
|
|
45
|
-
* Codex PreToolUse: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny" } }
|
|
45
|
+
* Codex PreToolUse: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } }
|
|
46
46
|
*/
|
|
47
47
|
export function blockResponse(reason: string, hookEventName?: string): string {
|
|
48
48
|
if (isCursor()) {
|
|
@@ -50,7 +50,11 @@ export function blockResponse(reason: string, hookEventName?: string): string {
|
|
|
50
50
|
}
|
|
51
51
|
if (isCodex() && hookEventName === "PreToolUse") {
|
|
52
52
|
return JSON.stringify({
|
|
53
|
-
hookSpecificOutput: {
|
|
53
|
+
hookSpecificOutput: {
|
|
54
|
+
hookEventName: "PreToolUse",
|
|
55
|
+
permissionDecision: "deny",
|
|
56
|
+
permissionDecisionReason: reason,
|
|
57
|
+
},
|
|
54
58
|
});
|
|
55
59
|
}
|
|
56
60
|
return JSON.stringify({ decision: "block", reason });
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
lstatSync,
|
|
14
14
|
readdirSync,
|
|
15
15
|
readFileSync,
|
|
16
|
+
readlinkSync,
|
|
16
17
|
statSync,
|
|
17
18
|
symlinkSync,
|
|
18
19
|
unlinkSync,
|
|
@@ -54,8 +55,11 @@ function latestMtime(...filePaths: string[]): number {
|
|
|
54
55
|
function ensureOneSymlink(linkPath: string, targetPath: string): void {
|
|
55
56
|
try {
|
|
56
57
|
const stat = lstatSync(linkPath);
|
|
57
|
-
if (
|
|
58
|
-
|
|
58
|
+
if (stat.isSymbolicLink()) {
|
|
59
|
+
const currentTarget = resolve(dirname(linkPath), readlinkSync(linkPath));
|
|
60
|
+
if (currentTarget === targetPath) return;
|
|
61
|
+
}
|
|
62
|
+
unlinkSync(linkPath);
|
|
59
63
|
} catch {
|
|
60
64
|
// doesn't exist — create it
|
|
61
65
|
}
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -81,6 +81,7 @@ export const assets = {
|
|
|
81
81
|
cursorHooksTemplate: () => pkg("assets", "templates", "hooks.cursor.json"),
|
|
82
82
|
copilotHooksTemplate: () => pkg("assets", "templates", "hooks.copilot.json"),
|
|
83
83
|
codexHooksTemplate: () => pkg("assets", "templates", "hooks.codex.json"),
|
|
84
|
+
codexRulesTemplate: () => pkg("assets", "templates", "rules.codex.rules"),
|
|
84
85
|
agentTools: () => pkg("src", "tools", "agent"),
|
|
85
86
|
palDocs: () => pkg("assets", "templates", "PAL"),
|
|
86
87
|
} as const;
|
|
@@ -19,8 +19,10 @@ import {
|
|
|
19
19
|
countSkills,
|
|
20
20
|
generateSkillIndex,
|
|
21
21
|
loadCodexHooksTemplate,
|
|
22
|
+
loadCodexRulesTemplate,
|
|
22
23
|
log,
|
|
23
24
|
mergeCodexHooks,
|
|
25
|
+
mergeCodexRules,
|
|
24
26
|
readJson,
|
|
25
27
|
scaffoldPalSettings,
|
|
26
28
|
writeJson,
|
|
@@ -56,6 +58,7 @@ function enableCodexHooks(configPath: string): void {
|
|
|
56
58
|
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
57
59
|
const CODEX_DIR = platform.codexDir();
|
|
58
60
|
const HOOKS_FILE = resolve(CODEX_DIR, "hooks.json");
|
|
61
|
+
const RULES_FILE = resolve(CODEX_DIR, "rules", "default.rules");
|
|
59
62
|
|
|
60
63
|
// --- Ensure ~/.codex/ exists ---
|
|
61
64
|
mkdirSync(CODEX_DIR, { recursive: true });
|
|
@@ -73,6 +76,17 @@ const merged = mergeCodexHooks(existing, template);
|
|
|
73
76
|
writeJson(HOOKS_FILE, merged);
|
|
74
77
|
log.success("Merged PAL hooks into ~/.codex/hooks.json");
|
|
75
78
|
|
|
79
|
+
// --- Merge allowlist rules ---
|
|
80
|
+
mkdirSync(resolve(CODEX_DIR, "rules"), { recursive: true });
|
|
81
|
+
if (existsSync(RULES_FILE)) {
|
|
82
|
+
copyFileSync(RULES_FILE, `${RULES_FILE}.bak.${Date.now()}`);
|
|
83
|
+
log.info("Backed up rules/default.rules");
|
|
84
|
+
}
|
|
85
|
+
const rulesTemplate = loadCodexRulesTemplate(assets.codexRulesTemplate());
|
|
86
|
+
const existingRules = existsSync(RULES_FILE) ? readFileSync(RULES_FILE, "utf-8") : "";
|
|
87
|
+
writeFileSync(RULES_FILE, mergeCodexRules(existingRules, rulesTemplate), "utf-8");
|
|
88
|
+
log.success("Merged PAL allowlist rules into ~/.codex/rules/default.rules");
|
|
89
|
+
|
|
76
90
|
// --- Symlink skills to ~/.codex/skills/ ---
|
|
77
91
|
const codexSkillsDir = resolve(CODEX_DIR, "skills");
|
|
78
92
|
copySkills(codexSkillsDir);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
readJson,
|
|
14
14
|
removeSkills,
|
|
15
15
|
unmergeCodexHooks,
|
|
16
|
+
unmergeCodexRules,
|
|
16
17
|
writeJson,
|
|
17
18
|
} from "../lib";
|
|
18
19
|
|
|
@@ -38,6 +39,7 @@ function disableCodexHooks(configPath: string): void {
|
|
|
38
39
|
const PKG_ROOT = palPkg().replaceAll("\\", "/");
|
|
39
40
|
const CODEX_DIR = platform.codexDir();
|
|
40
41
|
const HOOKS_FILE = resolve(CODEX_DIR, "hooks.json");
|
|
42
|
+
const RULES_FILE = resolve(CODEX_DIR, "rules", "default.rules");
|
|
41
43
|
|
|
42
44
|
// --- Remove PAL hooks from hooks.json ---
|
|
43
45
|
if (existsSync(HOOKS_FILE)) {
|
|
@@ -54,6 +56,18 @@ if (existsSync(HOOKS_FILE)) {
|
|
|
54
56
|
log.info("No hooks.json found, nothing to do");
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
// --- Remove PAL allowlist rules from default.rules ---
|
|
60
|
+
if (existsSync(RULES_FILE)) {
|
|
61
|
+
copyFileSync(RULES_FILE, `${RULES_FILE}.bak.${Date.now()}`);
|
|
62
|
+
log.info("Backed up rules/default.rules");
|
|
63
|
+
|
|
64
|
+
const cleanedRules = unmergeCodexRules(readFileSync(RULES_FILE, "utf-8"));
|
|
65
|
+
writeFileSync(RULES_FILE, cleanedRules, "utf-8");
|
|
66
|
+
log.success("Removed PAL allowlist rules from ~/.codex/rules/default.rules");
|
|
67
|
+
} else {
|
|
68
|
+
log.info("No default.rules found, nothing to do");
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
// --- Remove PAL skill symlinks ---
|
|
58
72
|
const codexSkillsDir = resolve(CODEX_DIR, "skills");
|
|
59
73
|
const removed = removeSkills(codexSkillsDir);
|
package/src/targets/lib.ts
CHANGED
|
@@ -333,6 +333,36 @@ export function unmergeCodexHooks(
|
|
|
333
333
|
return result;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
// --- Codex rules (Starlark .rules file) ---
|
|
337
|
+
|
|
338
|
+
const CODEX_RULES_BEGIN = "# BEGIN PAL MANAGED CODEX RULES";
|
|
339
|
+
const CODEX_RULES_END = "# END PAL MANAGED CODEX RULES";
|
|
340
|
+
|
|
341
|
+
export function loadCodexRulesTemplate(templatePath: string): string {
|
|
342
|
+
return readFileSync(templatePath, "utf-8").trim();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function stripPalCodexRules(content: string): string {
|
|
346
|
+
const escapedBegin = CODEX_RULES_BEGIN.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
347
|
+
const escapedEnd = CODEX_RULES_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
348
|
+
const block = new RegExp(String.raw`\n?${escapedBegin}[\s\S]*?${escapedEnd}\n?`, "g");
|
|
349
|
+
return content
|
|
350
|
+
.replace(block, "\n")
|
|
351
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
352
|
+
.trim();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function mergeCodexRules(existing: string, template: string): string {
|
|
356
|
+
const preserved = stripPalCodexRules(existing);
|
|
357
|
+
const prefix = preserved ? `${preserved}\n\n` : "";
|
|
358
|
+
return `${prefix}${template.trim()}\n`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function unmergeCodexRules(existing: string): string {
|
|
362
|
+
const cleaned = stripPalCodexRules(existing);
|
|
363
|
+
return cleaned ? `${cleaned}\n` : "";
|
|
364
|
+
}
|
|
365
|
+
|
|
336
366
|
// --- TELOS scaffolding ---
|
|
337
367
|
|
|
338
368
|
/** Copy template files into telos/ without overwriting existing ones */
|