portable-agent-layer 0.70.0 → 0.71.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/README.md +5 -1
- package/assets/schema/pal-settings.schema.json +4 -0
- package/assets/skills/onboarding/SKILL.md +109 -0
- package/assets/skills/projects/SKILL.md +11 -2
- package/assets/templates/pal-settings.json +1 -0
- package/package.json +5 -1
- package/src/cli/index.ts +39 -12
- package/src/cli/personal-context.ts +67 -0
- package/src/cli/server.ts +13 -7
- package/src/cli/setup-identity.ts +13 -1
- package/src/hooks/handlers/agenda.ts +223 -0
- package/src/hooks/handlers/inject-retrieval.ts +6 -2
- package/src/hooks/lib/agenda-store.ts +41 -0
- package/src/hooks/lib/paths.ts +0 -1
- package/src/hooks/lib/projects.ts +16 -1
- package/src/hooks/lib/serves.ts +60 -0
- package/src/hooks/lib/stop.ts +14 -0
- package/src/hooks/lib/telos-goals.ts +144 -0
- package/src/hooks/lib/telos-topics.ts +68 -0
- package/src/hooks/lib/token-usage.ts +3 -1
- package/src/hooks/lib/wall-clock.ts +58 -0
- package/src/tools/agent/handoff-note.ts +38 -20
- package/src/tools/agent/project.ts +36 -4
- package/src/tools/control-room/data.ts +332 -0
- package/src/tools/control-room/matrix.ts +182 -0
- package/src/tools/control-room/server.ts +150 -0
- package/src/tools/control-room/ui/agenda.tsx +43 -0
- package/src/tools/control-room/ui/agents.tsx +67 -0
- package/src/tools/control-room/ui/app.css +857 -0
- package/src/tools/control-room/ui/app.tsx +74 -0
- package/src/tools/control-room/ui/board.tsx +82 -0
- package/src/tools/control-room/ui/format.ts +31 -0
- package/src/tools/control-room/ui/handoffs.tsx +37 -0
- package/src/tools/control-room/ui/index.html +19 -0
- package/src/tools/control-room/ui/ledger.tsx +136 -0
- package/src/tools/control-room/ui/matrix.tsx +117 -0
- package/src/tools/control-room/ui/panel.tsx +60 -0
- package/src/tools/control-room/ui/signal.tsx +161 -0
- package/assets/templates/ledger-page.html +0 -213
- package/src/cli/setup-telos.ts +0 -52
- package/src/hooks/lib/setup.ts +0 -60
- package/src/tools/ledger/server.ts +0 -111
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { DueBadge, RatingPoint, SignalView } from "../data";
|
|
2
|
+
import { percent, tenths } from "./format";
|
|
3
|
+
import { Panel, Pending, useLoaded } from "./panel";
|
|
4
|
+
|
|
5
|
+
const LOW_RATING = 3;
|
|
6
|
+
const W = 320;
|
|
7
|
+
const H = 72;
|
|
8
|
+
const PAD = 4;
|
|
9
|
+
|
|
10
|
+
function sparkPath(points: RatingPoint[]): {
|
|
11
|
+
line: string;
|
|
12
|
+
area: string;
|
|
13
|
+
xy: [number, number][];
|
|
14
|
+
} {
|
|
15
|
+
if (points.length === 0) return { line: "", area: "", xy: [] };
|
|
16
|
+
const step = points.length > 1 ? (W - PAD * 2) / (points.length - 1) : 0;
|
|
17
|
+
const xy = points.map<[number, number]>((p, i) => [
|
|
18
|
+
PAD + i * step,
|
|
19
|
+
H - PAD - ((p.rating - 1) / 9) * (H - PAD * 2),
|
|
20
|
+
]);
|
|
21
|
+
const line = xy
|
|
22
|
+
.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)} ${y.toFixed(1)}`)
|
|
23
|
+
.join(" ");
|
|
24
|
+
const area = `${line} L${xy.at(-1)?.[0].toFixed(1)} ${H} L${xy[0][0].toFixed(1)} ${H} Z`;
|
|
25
|
+
return { line, area, xy };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Sparkline({ points }: { points: RatingPoint[] }) {
|
|
29
|
+
const { line, area, xy } = sparkPath(points);
|
|
30
|
+
const last = xy.at(-1);
|
|
31
|
+
const midY = H - PAD - (4 / 9) * (H - PAD * 2);
|
|
32
|
+
return (
|
|
33
|
+
<svg
|
|
34
|
+
className="spark"
|
|
35
|
+
viewBox={`0 0 ${W} ${H}`}
|
|
36
|
+
preserveAspectRatio="none"
|
|
37
|
+
role="img"
|
|
38
|
+
aria-label="ratings"
|
|
39
|
+
>
|
|
40
|
+
<title>ratings, oldest to newest</title>
|
|
41
|
+
<defs>
|
|
42
|
+
<linearGradient id="sparkfill" x1="0" x2="0" y1="0" y2="1">
|
|
43
|
+
<stop offset="0" stopColor="#f0b14a" stopOpacity="0.28" />
|
|
44
|
+
<stop offset="1" stopColor="#f0b14a" stopOpacity="0" />
|
|
45
|
+
</linearGradient>
|
|
46
|
+
</defs>
|
|
47
|
+
<line className="rule" x1={PAD} x2={W - PAD} y1={midY} y2={midY} />
|
|
48
|
+
<path className="area" d={area} />
|
|
49
|
+
<path className="line" d={line} />
|
|
50
|
+
{points.map((p, i) =>
|
|
51
|
+
p.rating <= LOW_RATING ? (
|
|
52
|
+
<circle key={p.ts} className="low" cx={xy[i][0]} cy={xy[i][1]} r="2" />
|
|
53
|
+
) : null
|
|
54
|
+
)}
|
|
55
|
+
{last && <circle className="last" cx={last[0]} cy={last[1]} r="3" />}
|
|
56
|
+
</svg>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Figure({ value, label, tone }: { value: string; label: string; tone?: string }) {
|
|
61
|
+
return (
|
|
62
|
+
<div className="figure">
|
|
63
|
+
<div className={`value ${tone ?? ""}`}>{value}</div>
|
|
64
|
+
<div className="label">{label}</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const BADGE_LOOK: Record<DueBadge["state"], { row: string; tag: string }> = {
|
|
70
|
+
due: { row: "due", tag: "amber" },
|
|
71
|
+
clear: { row: "clear", tag: "good" },
|
|
72
|
+
"n/a": { row: "na", tag: "ghost" },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function Due({ name, badge }: { name: string; badge: DueBadge }) {
|
|
76
|
+
const look = BADGE_LOOK[badge.state];
|
|
77
|
+
return (
|
|
78
|
+
<div className={`due-row ${look.row}`}>
|
|
79
|
+
<span className={`tag ${look.tag}`}>{badge.state}</span>
|
|
80
|
+
<span className="detail">
|
|
81
|
+
{name}
|
|
82
|
+
{badge.detail ? ` — ${badge.detail}` : ""}
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ratingTone(avg: number): string {
|
|
89
|
+
if (avg < 5) return "bad";
|
|
90
|
+
if (avg >= 7) return "good";
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function Signal() {
|
|
95
|
+
const view = useLoaded<SignalView>("/api/signal");
|
|
96
|
+
return (
|
|
97
|
+
<Panel
|
|
98
|
+
index="02 · feedback"
|
|
99
|
+
title="Signal"
|
|
100
|
+
span={4}
|
|
101
|
+
order={1}
|
|
102
|
+
aside={
|
|
103
|
+
view.state === "ready" && view.data.synthesizedAt
|
|
104
|
+
? `synthesised ${view.data.synthesizedAt.slice(0, 10)}`
|
|
105
|
+
: ""
|
|
106
|
+
}
|
|
107
|
+
>
|
|
108
|
+
<Pending value={view} />
|
|
109
|
+
{view.state === "ready" && (
|
|
110
|
+
<>
|
|
111
|
+
<div className="figures">
|
|
112
|
+
<Figure
|
|
113
|
+
value={view.data.ratings ? tenths(view.data.ratings.recentAvg) : "–"}
|
|
114
|
+
label="last 10"
|
|
115
|
+
tone={view.data.ratings ? ratingTone(view.data.ratings.recentAvg) : ""}
|
|
116
|
+
/>
|
|
117
|
+
<Figure
|
|
118
|
+
value={view.data.ratings ? tenths(view.data.ratings.avg) : "–"}
|
|
119
|
+
label={`avg of ${view.data.ratings?.count ?? 0}`}
|
|
120
|
+
/>
|
|
121
|
+
<Figure
|
|
122
|
+
value={view.data.ratings ? String(view.data.ratings.lowCount) : "–"}
|
|
123
|
+
label="low (≤3)"
|
|
124
|
+
tone={view.data.ratings && view.data.ratings.lowCount > 5 ? "bad" : ""}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
{view.data.series.length > 0 ? (
|
|
128
|
+
<Sparkline points={view.data.series} />
|
|
129
|
+
) : (
|
|
130
|
+
<div className="empty">No ratings yet.</div>
|
|
131
|
+
)}
|
|
132
|
+
<div className="spark-caption">
|
|
133
|
+
<span>last {view.data.series.length} ratings</span>
|
|
134
|
+
<span>{view.data.ratings?.trend ?? ""}</span>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="figures" style={{ marginTop: 16 }}>
|
|
137
|
+
<Figure
|
|
138
|
+
value={
|
|
139
|
+
view.data.algorithm ? String(view.data.algorithm.reflectionCount) : "–"
|
|
140
|
+
}
|
|
141
|
+
label="reflections"
|
|
142
|
+
/>
|
|
143
|
+
<Figure
|
|
144
|
+
value={view.data.algorithm ? percent(view.data.algorithm.passRate) : "–"}
|
|
145
|
+
label="criteria pass"
|
|
146
|
+
/>
|
|
147
|
+
<Figure
|
|
148
|
+
value={view.data.algorithm ? tenths(view.data.algorithm.avgSentiment) : "–"}
|
|
149
|
+
label="sentiment"
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
<div className="due">
|
|
153
|
+
<Due name="learning analysis" badge={view.data.due.analysis} />
|
|
154
|
+
<Due name="algorithm review" badge={view.data.due.algorithmReview} />
|
|
155
|
+
<Due name="relationship reflect" badge={view.data.due.relationshipReflect} />
|
|
156
|
+
</div>
|
|
157
|
+
</>
|
|
158
|
+
)}
|
|
159
|
+
</Panel>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Action Ledger</title>
|
|
7
|
-
<style>
|
|
8
|
-
:root {
|
|
9
|
-
--bg: #eef0f2;
|
|
10
|
-
--panel: #ffffff;
|
|
11
|
-
--ink: #1b2026;
|
|
12
|
-
--muted: #6b7480;
|
|
13
|
-
--line: #d5dae0;
|
|
14
|
-
--accent: #0f6e6e;
|
|
15
|
-
--accent-ink: #ffffff;
|
|
16
|
-
--block: #b4471c;
|
|
17
|
-
--block-bg: #fbf0ea;
|
|
18
|
-
--ok: #2f7a3e;
|
|
19
|
-
--ok-bg: #edf6ee;
|
|
20
|
-
--pending: #8a6d00;
|
|
21
|
-
--pending-bg: #fbf5df;
|
|
22
|
-
--agent: #4a3fa3;
|
|
23
|
-
--agent-bg: #efedf9;
|
|
24
|
-
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
25
|
-
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
26
|
-
}
|
|
27
|
-
* { box-sizing: border-box; }
|
|
28
|
-
html, body { height: 100%; }
|
|
29
|
-
body { margin: 0; background: var(--bg); color: var(--ink); font: 13px/1.45 var(--sans); }
|
|
30
|
-
header {
|
|
31
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
32
|
-
padding: 10px 16px; background: var(--panel); border-bottom: 1px solid var(--line);
|
|
33
|
-
}
|
|
34
|
-
header h1 { font-size: 14px; margin: 0; font-weight: 600; letter-spacing: .01em; }
|
|
35
|
-
header .model { font-family: var(--mono); font-size: 11px; color: var(--muted); }
|
|
36
|
-
header .who { font-size: 12px; color: var(--muted); }
|
|
37
|
-
header .who b { color: var(--ink); font-weight: 600; }
|
|
38
|
-
|
|
39
|
-
main { background: var(--panel); min-height: calc(100% - 45px); }
|
|
40
|
-
h2 {
|
|
41
|
-
font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
|
|
42
|
-
margin: 0; padding: 12px 14px 8px; font-weight: 600;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.filters { display: flex; gap: 10px; padding: 0 14px 12px; flex-wrap: wrap; align-items: end; }
|
|
46
|
-
.filters label { display: block; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
|
|
47
|
-
select, input { font: inherit; padding: 4px 6px; border: 1px solid var(--line); border-radius: 3px; }
|
|
48
|
-
|
|
49
|
-
.stats { display: grid; grid-template-columns: 1.4fr repeat(4, 1fr); gap: 1px; background: var(--line); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
|
50
|
-
.stat { background: var(--panel); padding: 12px 14px; }
|
|
51
|
-
.stat label { display: block; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
|
|
52
|
-
.stat .n { font-size: 26px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.2; }
|
|
53
|
-
.stat.headline { background: var(--block-bg); }
|
|
54
|
-
.stat.headline .n { font-size: 40px; color: var(--block); }
|
|
55
|
-
.stat.applied .n { color: var(--ok); }
|
|
56
|
-
.stat.failed .n { color: var(--pending); }
|
|
57
|
-
.stat.denied .n, .stat.blocked .n { color: var(--block); }
|
|
58
|
-
.st { font-size: 11px; color: var(--muted); }
|
|
59
|
-
|
|
60
|
-
table { width: 100%; border-collapse: collapse; }
|
|
61
|
-
td, th { text-align: left; padding: 7px 14px; border-top: 1px solid var(--line); vertical-align: top; font-variant-numeric: tabular-nums; }
|
|
62
|
-
th { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); border-top: 0; font-weight: 600; }
|
|
63
|
-
td.ts { font-family: var(--mono); font-size: 11px; color: var(--muted); white-space: nowrap; }
|
|
64
|
-
td.target { font-family: var(--mono); font-size: 11px; word-break: break-all; }
|
|
65
|
-
td.diff { font-family: var(--mono); font-size: 11px; }
|
|
66
|
-
.actor { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; }
|
|
67
|
-
.actor.human { background: #eef0f2; }
|
|
68
|
-
.actor.agent { background: var(--agent-bg); color: var(--agent); }
|
|
69
|
-
.out { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; font-weight: 600; }
|
|
70
|
-
.out.applied { background: var(--ok-bg); color: var(--ok); }
|
|
71
|
-
.out.failed { background: var(--pending-bg); color: var(--pending); }
|
|
72
|
-
.out.denied { background: var(--block-bg); color: var(--block); }
|
|
73
|
-
.out.blocked { background: var(--block-bg); color: var(--block); }
|
|
74
|
-
footer { font-family: var(--mono); font-size: 10.5px; color: var(--muted); padding: 10px 14px; border-top: 1px solid var(--line); }
|
|
75
|
-
</style>
|
|
76
|
-
</head>
|
|
77
|
-
<body>
|
|
78
|
-
<header>
|
|
79
|
-
<h1>Action Ledger</h1>
|
|
80
|
-
<span class="model" id="modelLine"></span>
|
|
81
|
-
<span class="who" id="who"></span>
|
|
82
|
-
</header>
|
|
83
|
-
|
|
84
|
-
<main>
|
|
85
|
-
<h2>Window</h2>
|
|
86
|
-
<div class="filters">
|
|
87
|
-
<div><label for="project">Project</label><select id="project"><option value="">all projects</option></select></div>
|
|
88
|
-
<div><label for="since">From</label><input type="date" id="since"></div>
|
|
89
|
-
<div><label for="until">To</label><input type="date" id="until"></div>
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
<div class="stats" id="stats"></div>
|
|
93
|
-
|
|
94
|
-
<h2>Action log</h2>
|
|
95
|
-
<table>
|
|
96
|
-
<thead><tr><th>When</th><th>Actor</th><th>Action</th><th>Target</th><th>Change</th><th>Outcome</th></tr></thead>
|
|
97
|
-
<tbody id="log"></tbody>
|
|
98
|
-
</table>
|
|
99
|
-
<footer id="footer">Every row is one action an agent tried to make. The ledger saw each from both sides, before and after. Nothing here was written by hand.</footer>
|
|
100
|
-
</main>
|
|
101
|
-
|
|
102
|
-
<script>
|
|
103
|
-
const $ = id => document.getElementById(id);
|
|
104
|
-
const OUTCOMES = ['applied', 'failed', 'denied', 'blocked'];
|
|
105
|
-
let ledgerFiles = 0;
|
|
106
|
-
|
|
107
|
-
function el(tag, cls, text) {
|
|
108
|
-
const node = document.createElement(tag);
|
|
109
|
-
if (cls) node.className = cls;
|
|
110
|
-
if (text !== undefined) node.textContent = text;
|
|
111
|
-
return node;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const pad = n => String(n).padStart(2, '0');
|
|
115
|
-
function when(ts) {
|
|
116
|
-
const d = new Date(ts);
|
|
117
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const authorityWord = key => key === 'user' ? 'human' : key;
|
|
121
|
-
function tally(counts, word = k => k) {
|
|
122
|
-
const parts = Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${word(k)} ${v}`);
|
|
123
|
-
return parts.length ? parts.join(' · ') : '—';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function query() {
|
|
127
|
-
const p = new URLSearchParams();
|
|
128
|
-
if ($('project').value) p.set('project', $('project').value);
|
|
129
|
-
if ($('since').value) p.set('since', $('since').value);
|
|
130
|
-
if ($('until').value) p.set('until', `${$('until').value}T23:59:59.999Z`);
|
|
131
|
-
return p.toString();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function windowText() {
|
|
135
|
-
const from = $('since').value, to = $('until').value;
|
|
136
|
-
if (!from && !to) return 'all time';
|
|
137
|
-
return `${from || 'start'} → ${to || 'now'}`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function renderStats(s) {
|
|
141
|
-
const strip = $('stats');
|
|
142
|
-
strip.replaceChildren();
|
|
143
|
-
const headline = el('div', 'stat headline');
|
|
144
|
-
headline.append(el('label', '', 'Refusals'), el('div', 'n', String(s.refusals)),
|
|
145
|
-
el('div', 'st', `denied ${s.outcomes.denied.total} · blocked ${s.outcomes.blocked.total}`));
|
|
146
|
-
strip.append(headline);
|
|
147
|
-
for (const key of OUTCOMES) {
|
|
148
|
-
const o = s.outcomes[key];
|
|
149
|
-
const card = el('div', `stat ${key}`);
|
|
150
|
-
card.append(el('label', '', key), el('div', 'n', String(o.total)),
|
|
151
|
-
el('div', 'st', tally(o.byAuthority, authorityWord)), el('div', 'st', tally(o.byRuntime)));
|
|
152
|
-
strip.append(card);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function actorCell(r) {
|
|
157
|
-
const td = el('td');
|
|
158
|
-
const agent = r.authority === 'agent';
|
|
159
|
-
td.append(el('span', `actor ${agent ? 'agent' : 'human'}`, agent ? `agent · ${r.runtime}` : r.actor));
|
|
160
|
-
td.append(el('div', 'st', agent ? `on behalf of ${r.actor}` : `via ${r.runtime}`));
|
|
161
|
-
return td;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function changeCell(r) {
|
|
165
|
-
const td = el('td', 'diff');
|
|
166
|
-
td.append(r.reason ? el('span', 'st', r.reason) : document.createTextNode(r.change));
|
|
167
|
-
return td;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function renderRows(rows) {
|
|
171
|
-
const body = $('log');
|
|
172
|
-
body.replaceChildren();
|
|
173
|
-
if (!rows.length) {
|
|
174
|
-
const td = el('td', 'st', 'No actions in this window.');
|
|
175
|
-
td.colSpan = 6;
|
|
176
|
-
const tr = el('tr'); tr.append(td); body.append(tr);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
for (const r of rows) {
|
|
180
|
-
const tr = el('tr');
|
|
181
|
-
const outcome = el('td'); outcome.append(el('span', `out ${r.outcome}`, r.outcome));
|
|
182
|
-
tr.append(el('td', 'ts', when(r.ts)), actorCell(r), el('td', '', r.tool), el('td', 'target', r.target), changeCell(r), outcome);
|
|
183
|
-
body.append(tr);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async function load() {
|
|
188
|
-
const res = await fetch(`/api/ledger?${query()}`);
|
|
189
|
-
const body = await res.json();
|
|
190
|
-
if (!res.ok) { $('modelLine').textContent = body.error; return; }
|
|
191
|
-
renderStats(body.stats);
|
|
192
|
-
renderRows(body.rows);
|
|
193
|
-
$('modelLine').textContent = `${body.stats.total} actions · ${windowText()} · ${ledgerFiles} ledger file(s)`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function loadProjects() {
|
|
197
|
-
const list = await (await fetch('/api/projects')).json();
|
|
198
|
-
for (const { slug } of list) {
|
|
199
|
-
const o = el('option', '', slug); o.value = slug; $('project').append(o);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function loadStatus() {
|
|
204
|
-
const s = await (await fetch('/api/status')).json();
|
|
205
|
-
ledgerFiles = s.ledgerFiles;
|
|
206
|
-
$('who').replaceChildren('machine ', Object.assign(el('b', '', s.machine)));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
for (const id of ['project', 'since', 'until']) $(id).addEventListener('change', load);
|
|
210
|
-
Promise.all([loadProjects(), loadStatus()]).then(load);
|
|
211
|
-
</script>
|
|
212
|
-
</body>
|
|
213
|
-
</html>
|
package/src/cli/setup-telos.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive TELOS setup — prompts for personal context during `pal install`.
|
|
3
|
-
* Skips any step whose TELOS file already has real content.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { writeFileSync } from "node:fs";
|
|
7
|
-
import { resolve } from "node:path";
|
|
8
|
-
import * as clack from "@clack/prompts";
|
|
9
|
-
import { palHome } from "../hooks/lib/paths";
|
|
10
|
-
import { hasRealContent, SETUP_STEPS, STEP_ORDER } from "../hooks/lib/setup";
|
|
11
|
-
|
|
12
|
-
/** Prompt for missing TELOS context. Skips any step whose file already has real content. */
|
|
13
|
-
export async function promptTelos(): Promise<void> {
|
|
14
|
-
// Skip interactive prompts in non-TTY environments (tests, CI)
|
|
15
|
-
if (!process.stdin.isTTY) return;
|
|
16
|
-
|
|
17
|
-
const home = palHome();
|
|
18
|
-
const pending = STEP_ORDER.filter(
|
|
19
|
-
(key) => !hasRealContent(resolve(home, SETUP_STEPS[key].file))
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
if (pending.length === 0) {
|
|
23
|
-
clack.log.info("TELOS already configured");
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
clack.intro("Personal Context Setup");
|
|
28
|
-
clack.note(
|
|
29
|
-
"Answer in a sentence or two — you can edit the files in ~/.pal/telos/ for more detail later.",
|
|
30
|
-
"Quick setup"
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
for (const key of pending) {
|
|
34
|
-
const step = SETUP_STEPS[key];
|
|
35
|
-
const title = key.charAt(0).toUpperCase() + key.slice(1);
|
|
36
|
-
|
|
37
|
-
const answer = await clack.text({
|
|
38
|
-
message: step.question,
|
|
39
|
-
placeholder: step.hint,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
if (clack.isCancel(answer)) {
|
|
43
|
-
clack.cancel("Setup cancelled");
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const filePath = resolve(home, step.file);
|
|
48
|
-
writeFileSync(filePath, `# ${title}\n\n${answer}\n`, "utf-8");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
clack.outro("Personal context saved ✓");
|
|
52
|
-
}
|
package/src/hooks/lib/setup.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Setup state management for PAL first-run wizard.
|
|
3
|
-
*
|
|
4
|
-
* State lives in memory/state/setup.json. Each step maps to a TELOS file.
|
|
5
|
-
* The AI is instructed to mark steps done after writing each file.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
-
|
|
10
|
-
interface SetupStep {
|
|
11
|
-
done: boolean;
|
|
12
|
-
file: string;
|
|
13
|
-
question: string;
|
|
14
|
-
hint: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Ordered setup steps — defines the wizard flow */
|
|
18
|
-
export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
|
|
19
|
-
mission: {
|
|
20
|
-
file: "telos/MISSION.md",
|
|
21
|
-
question:
|
|
22
|
-
"What do you do? What's your role and core purpose? (~/.pal/telos/MISSION.md)",
|
|
23
|
-
hint: "e.g. Senior software engineer building developer tooling at Acme Corp",
|
|
24
|
-
},
|
|
25
|
-
goals: {
|
|
26
|
-
file: "telos/GOALS.md",
|
|
27
|
-
question:
|
|
28
|
-
"What are your current goals? (short-term, medium-term, long-term) (~/.pal/telos/GOALS.md)",
|
|
29
|
-
hint: "e.g. Ship v2 by Q3, learn Rust, get promoted to staff engineer",
|
|
30
|
-
},
|
|
31
|
-
beliefs: {
|
|
32
|
-
file: "telos/BELIEFS.md",
|
|
33
|
-
question: "What principles or values guide your work? (~/.pal/telos/BELIEFS.md)",
|
|
34
|
-
hint: "e.g. Simple code > clever code, ship early and iterate, always write tests",
|
|
35
|
-
},
|
|
36
|
-
challenges: {
|
|
37
|
-
file: "telos/CHALLENGES.md",
|
|
38
|
-
question: "What are your biggest current challenges? (~/.pal/telos/CHALLENGES.md)",
|
|
39
|
-
hint: "e.g. Context switching between projects, unclear requirements, work-life balance",
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export const STEP_ORDER = Object.keys(SETUP_STEPS);
|
|
44
|
-
|
|
45
|
-
/** Check if a TELOS file has real content (not just template scaffolding) */
|
|
46
|
-
export function hasRealContent(filePath: string): boolean {
|
|
47
|
-
if (!existsSync(filePath)) return false;
|
|
48
|
-
try {
|
|
49
|
-
const content = readFileSync(filePath, "utf-8").trim();
|
|
50
|
-
return content.split("\n").some((l) => {
|
|
51
|
-
if (!l.trim()) return false;
|
|
52
|
-
if (l.startsWith("#")) return false;
|
|
53
|
-
if (l.startsWith("<!--") || l.startsWith("-->")) return false;
|
|
54
|
-
if (/^\s*-\s*$/.test(l)) return false;
|
|
55
|
-
return true; // includes table rows (| ... |) — counts as real content
|
|
56
|
-
});
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A local page over the ledger, for showing the action log to someone who
|
|
3
|
-
* will not read a terminal.
|
|
4
|
-
*
|
|
5
|
-
* Loopback only, stateless, no store of its own: every request reads the
|
|
6
|
-
* ledger afresh through the same query the CLI uses, so the page and
|
|
7
|
-
* `pal cli ledger` can never disagree. The browser holds nothing but the
|
|
8
|
-
* current filter selection.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
|
-
import { loadMachine } from "../../hooks/lib/machine";
|
|
13
|
-
import { assets } from "../../hooks/lib/paths";
|
|
14
|
-
import { readAllProjects } from "../../hooks/lib/projects";
|
|
15
|
-
import { type LedgerFilter, ledgerFiles, parseSince } from "./query";
|
|
16
|
-
import { ledgerView } from "./view";
|
|
17
|
-
|
|
18
|
-
export const DEFAULT_PORT = 7250;
|
|
19
|
-
export const LOOPBACK = "127.0.0.1";
|
|
20
|
-
|
|
21
|
-
export interface ServerStatus {
|
|
22
|
-
pid: number;
|
|
23
|
-
port: number;
|
|
24
|
-
startedAt: string;
|
|
25
|
-
ledgerFiles: number;
|
|
26
|
-
machine: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function json(body: unknown, status = 200): Response {
|
|
30
|
-
return Response.json(body, { status });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** A window the page cannot parse is an error, not the whole ledger. */
|
|
34
|
-
function filterFromQuery(params: URLSearchParams): LedgerFilter | string {
|
|
35
|
-
const filter: LedgerFilter = {};
|
|
36
|
-
const project = params.get("project");
|
|
37
|
-
if (project) filter.project = project;
|
|
38
|
-
for (const key of ["since", "until"] as const) {
|
|
39
|
-
const spec = params.get(key);
|
|
40
|
-
if (!spec) continue;
|
|
41
|
-
const at = parseSince(spec);
|
|
42
|
-
if (!at) return `Unrecognised ${key}: ${spec}`;
|
|
43
|
-
filter[key] = at;
|
|
44
|
-
}
|
|
45
|
-
return filter;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function page(): Response {
|
|
49
|
-
return new Response(readFileSync(assets.ledgerPageTemplate()), {
|
|
50
|
-
headers: { "content-type": "text/html; charset=utf-8" },
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function projects(): Response {
|
|
55
|
-
const slugs = readAllProjects()
|
|
56
|
-
.map((p) => p.name)
|
|
57
|
-
.sort((a, b) => a.localeCompare(b));
|
|
58
|
-
return json(slugs.map((slug) => ({ slug })));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function ledger(url: URL): Response {
|
|
62
|
-
const filter = filterFromQuery(url.searchParams);
|
|
63
|
-
if (typeof filter === "string") return json({ error: filter }, 400);
|
|
64
|
-
return json(ledgerView(filter));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function status(port: number, startedAt: string): Response {
|
|
68
|
-
const body: ServerStatus = {
|
|
69
|
-
pid: process.pid,
|
|
70
|
-
port,
|
|
71
|
-
startedAt,
|
|
72
|
-
ledgerFiles: ledgerFiles().length,
|
|
73
|
-
machine: loadMachine().label,
|
|
74
|
-
};
|
|
75
|
-
return json(body);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function startLedgerServer(port: number = DEFAULT_PORT) {
|
|
79
|
-
const startedAt = new Date().toISOString();
|
|
80
|
-
return Bun.serve({
|
|
81
|
-
hostname: LOOPBACK,
|
|
82
|
-
port,
|
|
83
|
-
fetch(request, server) {
|
|
84
|
-
if (request.method !== "GET") return json({ error: "read only" }, 405);
|
|
85
|
-
const url = new URL(request.url);
|
|
86
|
-
switch (url.pathname) {
|
|
87
|
-
case "/":
|
|
88
|
-
return page();
|
|
89
|
-
case "/api/ledger":
|
|
90
|
-
return ledger(url);
|
|
91
|
-
case "/api/projects":
|
|
92
|
-
return projects();
|
|
93
|
-
case "/api/status":
|
|
94
|
-
return status(server.port ?? port, startedAt);
|
|
95
|
-
default:
|
|
96
|
-
return json({ error: "not found" }, 404);
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function portFromArgv(argv: string[]): number {
|
|
103
|
-
const flag = argv.find((arg) => arg.startsWith("--port="));
|
|
104
|
-
const port = flag ? Number(flag.slice("--port=".length)) : DEFAULT_PORT;
|
|
105
|
-
return Number.isInteger(port) && port >= 0 ? port : DEFAULT_PORT;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (import.meta.main) {
|
|
109
|
-
const server = startLedgerServer(portFromArgv(process.argv.slice(2)));
|
|
110
|
-
console.log(`http://${LOOPBACK}:${server.port}/`);
|
|
111
|
-
}
|