portable-agent-layer 0.69.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 -0
- 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 +45 -10
- package/src/cli/ledger.ts +1 -17
- package/src/cli/personal-context.ts +67 -0
- package/src/cli/server.ts +201 -0
- 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 +1 -0
- 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/src/tools/ledger/query.ts +18 -0
- package/src/tools/ledger/view.ts +130 -0
- package/src/cli/setup-telos.ts +0 -52
- package/src/hooks/lib/setup.ts +0 -60
package/README.md
CHANGED
|
@@ -87,8 +87,11 @@ pal cli status # check your setup
|
|
|
87
87
|
| `pal cli usage` | Summarize token usage and estimated cost |
|
|
88
88
|
| `pal cli actor [label <name>]` | Show or rename the actor — who caused a record. Travels with an export, so a shared memory can tell two people apart |
|
|
89
89
|
| `pal cli machine [label <name>]` | Show or rename this install — where a record was written. Never leaves the machine |
|
|
90
|
+
| `pal cli telos` | Which TELOS topics are answered, in interview order, and which one comes next |
|
|
91
|
+
| `pal cli timezone [<zone>]` | Show or set your timezone. IANA names only, validated before it is stored |
|
|
90
92
|
| `pal cli knowledge` | Query & manage the knowledge store (search, graph, stats, hubs, find, show, add, ls, ingest) |
|
|
91
93
|
| `pal cli ledger` | Query the action ledger — `log`, `show <id>`, `stats`, filtered by `--project`, `--since`, `--actor`, `--machine`, `--runtime`, `--outcome`, `--tool`, `--target` |
|
|
94
|
+
| `pal cli server` | The morning screen: a local page to open before a terminal — three moves for today, an urgent/important grid over every project *and* every stated goal, where you left off, and a drawer holding the signal and the action ledger — `start [--port <n>]`, `stop`, `status`. Loopback only, default port 7250. No model runs on page load |
|
|
92
95
|
| `pal cli skill link <name>` | Link a personal `~/.pal/skills/<name>/` into every installed agent so it is discoverable |
|
|
93
96
|
| `pal cli skill doctor <name>` | Evaluate a skill against the authoring best practices (folder/file-name match, name, description, body length, point-of-view, reference depth) |
|
|
94
97
|
| `pal cli subagent link <name>` | Install a personal `~/.pal/agents/<name>.md` (merged multi-platform definition) into every installed agent, split per platform |
|
|
@@ -184,6 +187,7 @@ PAL ships with built-in skills that extend your agent's capabilities:
|
|
|
184
187
|
| `frontend-design` | Build production-grade frontend interfaces |
|
|
185
188
|
| `fyzz-chat-api` | Query Fyzz Chat conversations via API |
|
|
186
189
|
| `humanize` | Rewrite text to strip AI tells and read as human |
|
|
190
|
+
| `onboarding` | Interview the user to fill empty TELOS topics and the timezone, one topic at a time |
|
|
187
191
|
| `opinion` | Confirm or contradict tracked opinions (confidence-weighted) |
|
|
188
192
|
| `pal-analyze` | Surface rating trends, failure patterns, and graduation candidates |
|
|
189
193
|
| `pal-reflect` | Promote recurring observations into tracked opinions |
|
|
@@ -222,6 +226,7 @@ Your setup should be able to travel with you.
|
|
|
222
226
|
- **Cross-agent**: full support for Claude Code, opencode, Cursor, GitHub Copilot, and Codex (Codex still lacks subagents)
|
|
223
227
|
- **Subscription-first inference**: background inference routes through whichever subscription CLI is active — no API key needed by default
|
|
224
228
|
- **Portable knowledge**: export and import accumulated knowledge
|
|
229
|
+
- **A morning screen, not a dashboard**: `pal cli server` ranks your projects *and* your written goals in one urgent/important grid. Urgency is read off the files — blockers, an unfinished handoff, a date in a next step, an important thing gone quiet. Importance comes from one fact per project: whether it serves a goal you wrote down, is a way the work could pay, or is kept for its own sake. PAL guesses that once at session stop; you correct it on the page or with `project.ts serves <name> goal|revenue|fun`, and the correction survives every later guess
|
|
225
230
|
- **TypeScript-first**: built in TypeScript from day one
|
|
226
231
|
- **Open source**: hackable, inspectable, extensible
|
|
227
232
|
- **Composable**: intended to fit into real developer workflows
|
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
"type": "boolean",
|
|
70
70
|
"description": "Prompt-time \"Potential matching skills\" hints, matched from each skill's metadata.triggers."
|
|
71
71
|
},
|
|
72
|
+
"wallClock": {
|
|
73
|
+
"type": "boolean",
|
|
74
|
+
"description": "One line of the current time with every prompt, in identity.principal.timezone, so a session resumed hours later knows the day changed."
|
|
75
|
+
},
|
|
72
76
|
"steeringTestReport": { "type": "boolean", "description": "When true, the assistant notes to the user which steering self-check fired. For the dual-live test period; ships false." }
|
|
73
77
|
}
|
|
74
78
|
},
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: onboarding
|
|
3
|
+
license: MIT
|
|
4
|
+
description: "Interview the user to fill in the personal context PAL has none of: their TELOS topics, one per pass, and their timezone. Use when the user wants to set up or flesh out that context: onboard me, fill in my telos, interview me about my goals, PAL doesn't know anything about me, help me write my mission, what should PAL know about me."
|
|
5
|
+
argument-hint: "[topic to start with]"
|
|
6
|
+
metadata:
|
|
7
|
+
source: portable-agent-layer
|
|
8
|
+
triggers:
|
|
9
|
+
- "onboarding"
|
|
10
|
+
- "onboard me"
|
|
11
|
+
- "fill in my telos"
|
|
12
|
+
- "set up my telos"
|
|
13
|
+
- "interview me about my goals"
|
|
14
|
+
- "personal context is empty"
|
|
15
|
+
- "you don't know anything about me"
|
|
16
|
+
- "help me write my mission"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
Fill the personal context by asking, one topic per pass, and write only what the user actually said.
|
|
20
|
+
|
|
21
|
+
TELOS is the context that orients every future session: purpose, goals, obstacles, methods, principles. The timezone is what makes "this morning" mean anything. A fresh install ships the TELOS files as empty scaffolds and guesses the timezone from the machine, and both stay that way until someone asks. This skill is that asking. It is deliberately not part of install: a user who has just installed a tool cannot yet say what they want from it, and answers given under that pressure are worse than no answers.
|
|
22
|
+
|
|
23
|
+
## Ask the CLI what is missing
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pal cli telos
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
It prints every topic as answered or unanswered, in interview order, and names the next unanswered one. That command owns the definition of "unanswered" — a scaffold of headings and empty bullets is not an answer. Do not open the files to judge for yourself, and do not restate the rule here: one definition, in one place, is the point.
|
|
30
|
+
|
|
31
|
+
Read a file only when you need the format of an existing entry, or when the user asks what is already on record.
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
1. Run `pal cli telos`.
|
|
36
|
+
|
|
37
|
+
2. Pick this pass's topic:
|
|
38
|
+
- The user named one in the argument → use it, answered or not.
|
|
39
|
+
- Otherwise → the `next:` topic the command printed.
|
|
40
|
+
- `next: none` → say so, list what is on file in one line each, and offer the optional topics. Do not re-interview an answered topic unless asked.
|
|
41
|
+
|
|
42
|
+
| File | The question behind it |
|
|
43
|
+
|------|------------------------|
|
|
44
|
+
| `MISSION.md` | What are you actually doing with your working life? |
|
|
45
|
+
| `GOALS.md` | What are you working toward, by when? |
|
|
46
|
+
| `CHALLENGES.md` | What is in the way right now? |
|
|
47
|
+
| `STRATEGIES.md` | How do you approach work and decisions? |
|
|
48
|
+
| `BELIEFS.md` | What principles do you refuse to trade away? |
|
|
49
|
+
| `MODELS.md` `NARRATIVES.md` `LEARNED.md` `IDEAS.md` | Optional, on request only |
|
|
50
|
+
|
|
51
|
+
3. Open the topic with one sentence saying why the answer changes how you behave in future sessions, then ask **one** question. Never ask two questions in one turn, and never present a numbered list of questions to work through.
|
|
52
|
+
|
|
53
|
+
4. Follow up until the answer is specific enough to act on. A usable answer names something concrete: a company, a role, a date, an amount, a named obstacle. A vague answer ("grow professionally", "be healthier") gets one follow-up asking for the concrete version. Stop after two follow-ups on the same point whether or not it got sharper.
|
|
54
|
+
|
|
55
|
+
5. Draft the entry in the file's existing format, show it, and ask for a yes before writing. The draft may only contain what the user said in this conversation. If they went quiet on part of it, that part is absent from the draft, not filled in by you.
|
|
56
|
+
|
|
57
|
+
6. Write it with the telos tool, never by editing the file:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bun ~/.pal/skills/telos/tools/update-telos.ts <FILE> "<content>" "<description>"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`<FILE>` is the bare filename, `<description>` is a short line naming what the entry covers and the date it was given.
|
|
64
|
+
|
|
65
|
+
7. Report what landed, name the next unanswered topic, and stop. One topic per invocation. The user resumes by invoking the skill again, whenever that is.
|
|
66
|
+
|
|
67
|
+
## The timezone
|
|
68
|
+
|
|
69
|
+
Check it on the first pass only:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pal cli timezone # show what is configured
|
|
73
|
+
pal cli timezone Europe/Budapest # set it, IANA names only
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Install prefills it from the machine, so it is usually already right and usually not worth a question. Ask only when it reads as not set, or when the user has corrected you about the date or the hour in this session. Settings are hook-protected, so this command is the only way to change it — never edit `pal-settings.json`.
|
|
77
|
+
|
|
78
|
+
## Never invent an answer
|
|
79
|
+
|
|
80
|
+
The whole value of this context is that it is the user's own words. An invented goal is worse than an empty file, because every later session treats it as fact and ranks work against it.
|
|
81
|
+
|
|
82
|
+
- Do not draft an answer from the repository, the user's projects, their git history, or anything you inferred earlier in the session.
|
|
83
|
+
- Do not offer a filled-in answer for confirmation. "Is your mission to build a small consultancy?" invites a yes that was yours, not theirs.
|
|
84
|
+
- You may offer two or three short examples of the *kind* of answer the question wants, clearly marked as examples from nobody in particular, when the user is stuck.
|
|
85
|
+
- "I don't know" is a valid answer. Say the topic stays open, write nothing, and move to the next one.
|
|
86
|
+
- If the user's answer contradicts what is already on file, show both and ask which holds. Do not merge them silently.
|
|
87
|
+
|
|
88
|
+
## Output format
|
|
89
|
+
|
|
90
|
+
Conversational, not a form. Per pass:
|
|
91
|
+
|
|
92
|
+
- One line on why this topic matters to future sessions.
|
|
93
|
+
- One question, then a real exchange.
|
|
94
|
+
- A drafted entry shown before writing, and an explicit request to confirm.
|
|
95
|
+
- After writing: one line confirming the file, and one line naming the next unanswered topic.
|
|
96
|
+
|
|
97
|
+
## When to use
|
|
98
|
+
|
|
99
|
+
- The user asks to be onboarded or interviewed, or to fill in, set up, or flesh out their personal context.
|
|
100
|
+
- The user says PAL knows nothing about them, or asks what it should know.
|
|
101
|
+
- A task needs personal context (ranking work, weighing a decision) and `pal cli telos` reports the relevant topic unanswered. Offer this skill; do not start the interview uninvited.
|
|
102
|
+
|
|
103
|
+
## Do NOT use
|
|
104
|
+
|
|
105
|
+
- To read, view, or summarize existing TELOS content, or to record a single stated change to it — that is the `telos` skill.
|
|
106
|
+
- To act on a timezone the user simply states ("we moved to Berlin") — run `pal cli timezone` and move on. No interview.
|
|
107
|
+
- To record an observation about the user made during other work — that is a relationship note.
|
|
108
|
+
- To set the assistant's name or the startup catchphrase — that is identity, and `pal cli init` asks for it.
|
|
109
|
+
- To fill several topics in one pass because the user seems willing. One topic per invocation is the design; long forms produce shallow answers.
|
|
@@ -38,7 +38,8 @@ Output is JSON.
|
|
|
38
38
|
| Command | Purpose |
|
|
39
39
|
|---------|---------|
|
|
40
40
|
| `list` | All registered projects with status, path, updated, stale flag, and counts |
|
|
41
|
-
| `create [name] [--path PATH] [--objectives "a;b;c"]` | Register a project. Defaults: name=basename(cwd), path=cwd. Slug must be `[a-z0-9_-]
|
|
41
|
+
| `create [name] [--path PATH] [--objectives "a;b;c"] [--serves KIND] [--serves-note "..."]` | Register a project. Defaults: name=basename(cwd), path=cwd. Slug must be `[a-z0-9_-]+`. KIND is `goal`, `revenue` or `fun` |
|
|
42
|
+
| `serves <name> <goal\|revenue\|fun> [note]` | Set what the project is for, after the fact |
|
|
42
43
|
| `resume <name>` | Print the full project ISA — all frontmatter and body sections |
|
|
43
44
|
| `add-next <name> "text"` | Append a next step (array, instantly appendable) |
|
|
44
45
|
| `add-blocker <name> "text"` | Append a blocker (array, instantly appendable) |
|
|
@@ -94,6 +95,7 @@ When SessionStart context flags the current cwd as unregistered (e.g. `💡 cwd
|
|
|
94
95
|
- **Default name** = the FULL last path segment of cwd, lowercased. For `/repos/portable-agent-layer` → `portable-agent-layer`. Never split on `-`.
|
|
95
96
|
- **Confirm before creating.** Never auto-create without explicit user approval ("yes", "do it", "register").
|
|
96
97
|
- **Capture context in conversation.** If the user accepts but doesn't volunteer a goal, ask one short question, or infer from the last few messages and confirm.
|
|
98
|
+
- **Ask what it serves.** One question, at registration: is this moving a stated goal forward, a way the work could pay, or kept for its own sake? Pass the answer as `--serves goal|revenue|fun` with a six-word `--serves-note`. It is the only thing that ranks the project against the others, and PAL will guess it later if you skip it — a guess the user then has to correct.
|
|
97
99
|
|
|
98
100
|
### When NOT to suggest registration
|
|
99
101
|
|
|
@@ -120,7 +122,14 @@ User: "store under <project> that a reference implementation exists in this repo
|
|
|
120
122
|
```
|
|
121
123
|
User: "track this project"
|
|
122
124
|
→ Default name from cwd basename, confirm with user
|
|
123
|
-
→
|
|
125
|
+
→ Ask what it serves: a goal, a way it could pay, or fun
|
|
126
|
+
→ bun ~/.pal/tools/project.ts create --path "$(pwd)" --objectives "first objective; second objective" --serves revenue --serves-note "could be sold as a service"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Correcting what a project is for**
|
|
130
|
+
```
|
|
131
|
+
User: "<project> isn't a toy, it's the thing I'd actually sell"
|
|
132
|
+
→ bun ~/.pal/tools/project.ts serves <slug> revenue "the one I would sell"
|
|
124
133
|
```
|
|
125
134
|
|
|
126
135
|
**Logging a decision**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -83,6 +83,8 @@
|
|
|
83
83
|
"@types/adm-zip": "^0.5.8",
|
|
84
84
|
"@types/bun": "latest",
|
|
85
85
|
"@types/node": "latest",
|
|
86
|
+
"@types/react": "^19.2.18",
|
|
87
|
+
"@types/react-dom": "^19.2.7",
|
|
86
88
|
"husky": "^9.1.7",
|
|
87
89
|
"jscpd": "^4.2.3",
|
|
88
90
|
"knip": "^6.14.1",
|
|
@@ -100,6 +102,8 @@
|
|
|
100
102
|
"marked": "18.0.4",
|
|
101
103
|
"pdf-lib": "1.17.1",
|
|
102
104
|
"playwright": "^1.60.0",
|
|
105
|
+
"react": "^19.2.8",
|
|
106
|
+
"react-dom": "^19.2.8",
|
|
103
107
|
"unpdf": "^0.12.0"
|
|
104
108
|
}
|
|
105
109
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* doctor Check prerequisites and system health
|
|
18
18
|
* usage Summarize token usage and cost
|
|
19
19
|
* ledger <sub> [filters] Query the action ledger (log · show · stats)
|
|
20
|
+
* server start|stop|status The control room, a local page over ~/.pal
|
|
20
21
|
* skill link <name> Link a personal ~/.pal/skills/<name>/ into installed agents
|
|
21
22
|
* skill doctor <name|--all> Evaluate one skill, or every installed skill, against the authoring best practices
|
|
22
23
|
* subagent link <name> Install a personal ~/.pal/agents/<name>.md into installed agents
|
|
@@ -47,7 +48,7 @@ import { DEBUG_LOG_MAX_ROTATED, logDebug } from "../hooks/lib/log";
|
|
|
47
48
|
import { ensureRegistered, writeRegistryEntry } from "../hooks/lib/machine";
|
|
48
49
|
import { palHome, palPkg, paths, platform } from "../hooks/lib/paths";
|
|
49
50
|
import { auditBindings, describeBindingIssue } from "../hooks/lib/projects";
|
|
50
|
-
import {
|
|
51
|
+
import { telosStatus } from "../hooks/lib/telos-topics";
|
|
51
52
|
import { log } from "../targets/lib";
|
|
52
53
|
import { checkPendingMigrations } from "./migrate";
|
|
53
54
|
|
|
@@ -239,6 +240,12 @@ async function runCli(command: string | undefined, args: string[]) {
|
|
|
239
240
|
if (code !== 0) process.exit(code);
|
|
240
241
|
break;
|
|
241
242
|
}
|
|
243
|
+
case "server": {
|
|
244
|
+
const { runServer } = await import("./server");
|
|
245
|
+
const code = await runServer(args);
|
|
246
|
+
if (code !== 0) process.exit(code);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
242
249
|
case "subagent": {
|
|
243
250
|
const { runSubagent } = await import("./subagent");
|
|
244
251
|
const code = await runSubagent(args);
|
|
@@ -258,6 +265,18 @@ async function runCli(command: string | undefined, args: string[]) {
|
|
|
258
265
|
if (code !== 0) process.exit(code);
|
|
259
266
|
break;
|
|
260
267
|
}
|
|
268
|
+
case "telos": {
|
|
269
|
+
const { runTelos } = await import("./personal-context");
|
|
270
|
+
const code = runTelos(args);
|
|
271
|
+
if (code !== 0) process.exit(code);
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case "timezone": {
|
|
275
|
+
const { runTimezone } = await import("./personal-context");
|
|
276
|
+
const code = runTimezone(args);
|
|
277
|
+
if (code !== 0) process.exit(code);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
261
280
|
case "debug":
|
|
262
281
|
cliDebug(args);
|
|
263
282
|
break;
|
|
@@ -284,6 +303,19 @@ function banner() {
|
|
|
284
303
|
console.log("");
|
|
285
304
|
}
|
|
286
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Install asks for identity and stops. TELOS is a conversation, not a form: a
|
|
308
|
+
* user who has just installed a tool cannot yet say what they want from it, and
|
|
309
|
+
* answers given under that pressure are worse than none.
|
|
310
|
+
*/
|
|
311
|
+
function pointAtOnboarding(): void {
|
|
312
|
+
const unanswered = telosStatus().filter((topic) => topic.priority && !topic.answered);
|
|
313
|
+
if (unanswered.length === 0) return;
|
|
314
|
+
log.info(
|
|
315
|
+
`PAL knows nothing about you yet (${unanswered.length} topics open). When you have half an hour, ask your agent to onboard you.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
287
319
|
function showHelp() {
|
|
288
320
|
console.log(`
|
|
289
321
|
Usage:
|
|
@@ -304,10 +336,13 @@ function showHelp() {
|
|
|
304
336
|
pal cli usage Summarize token usage and cost
|
|
305
337
|
pal cli actor [label <name>] Show or rename this actor (who caused a record)
|
|
306
338
|
pal cli machine [label <name>] Show or rename this install (where it was written)
|
|
339
|
+
pal cli telos Which TELOS topics are answered, in interview order
|
|
340
|
+
pal cli timezone [<zone>] Show or set your timezone (IANA name)
|
|
307
341
|
pal cli knowledge <sub> [args] Query & manage the knowledge store
|
|
308
342
|
(search · graph · stats · hubs · find · show · add · ls)
|
|
309
343
|
pal cli ledger <sub> [filters] Query the action ledger (log · show · stats)
|
|
310
344
|
e.g. ledger log --project X --since 7d
|
|
345
|
+
pal cli server start|stop|status The control room: a local page to open before a terminal
|
|
311
346
|
pal cli skill link <name> Link a personal ~/.pal/skills/<name>/ into installed agents
|
|
312
347
|
pal cli skill doctor <name|--all> Evaluate one skill, or every installed skill
|
|
313
348
|
pal cli skill author-model Print the flagship model that authors skills for the active agent
|
|
@@ -863,15 +898,16 @@ function doctor(silent = false): DoctorResult {
|
|
|
863
898
|
: fail("CLAUDE.md — missing (run 'pal cli install --claude')");
|
|
864
899
|
}
|
|
865
900
|
|
|
866
|
-
//
|
|
901
|
+
// An empty TELOS is a normal state, not a broken install: the onboarding
|
|
902
|
+
// skill fills it whenever the user is ready, which may be months from now.
|
|
867
903
|
{
|
|
868
|
-
const
|
|
869
|
-
(
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
? ok("TELOS
|
|
904
|
+
const unanswered = telosStatus(home)
|
|
905
|
+
.filter((topic) => topic.priority && !topic.answered)
|
|
906
|
+
.map((topic) => topic.key);
|
|
907
|
+
unanswered.length === 0
|
|
908
|
+
? ok("TELOS answered")
|
|
873
909
|
: warn(
|
|
874
|
-
`TELOS
|
|
910
|
+
`TELOS unanswered — ${unanswered.join(", ")} (ask your agent to onboard you)`
|
|
875
911
|
);
|
|
876
912
|
}
|
|
877
913
|
|
|
@@ -1193,13 +1229,12 @@ async function install(targets: Targets) {
|
|
|
1193
1229
|
const { scaffoldTelos, scaffoldPalSettings, copyPalDocs, generateSkillIndex } =
|
|
1194
1230
|
await import("../targets/lib");
|
|
1195
1231
|
const { promptIdentity } = await import("./setup-identity");
|
|
1196
|
-
const { promptTelos } = await import("./setup-telos");
|
|
1197
1232
|
const { promptAttribution } = await import("./setup-attribution");
|
|
1198
1233
|
scaffoldTelos();
|
|
1199
1234
|
scaffoldPalSettings();
|
|
1200
1235
|
await promptIdentity();
|
|
1201
|
-
await promptTelos();
|
|
1202
1236
|
await promptAttribution();
|
|
1237
|
+
pointAtOnboarding();
|
|
1203
1238
|
|
|
1204
1239
|
// Registers the label loadActor derives, so it travels on the next export.
|
|
1205
1240
|
const { ensureActorRegistered } = await import("../hooks/lib/actor");
|
package/src/cli/ledger.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { LedgerEntry } from "../hooks/lib/ledger";
|
|
|
16
16
|
import {
|
|
17
17
|
type ChainVerdict,
|
|
18
18
|
chainVerdict,
|
|
19
|
+
changedLines,
|
|
19
20
|
changeShape,
|
|
20
21
|
findEntry,
|
|
21
22
|
type LedgerFilter,
|
|
@@ -145,23 +146,6 @@ function shortId(id: string): string {
|
|
|
145
146
|
return id.slice(0, 11).padEnd(11);
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
function changedLines(entry: LedgerEntry): string {
|
|
149
|
-
const shape = changeShape(entry);
|
|
150
|
-
switch (shape.kind) {
|
|
151
|
-
case "redacted":
|
|
152
|
-
return "withheld";
|
|
153
|
-
case "truncated":
|
|
154
|
-
return "too large";
|
|
155
|
-
case "none":
|
|
156
|
-
return "no change";
|
|
157
|
-
default: {
|
|
158
|
-
const added = shape.delta.hunks.reduce((n, h) => n + h.insert.length, 0);
|
|
159
|
-
const removed = shape.delta.hunks.reduce((n, h) => n + h.remove, 0);
|
|
160
|
-
return `+${added} -${removed}`;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
149
|
function cmdLog(args: string[]): number {
|
|
166
150
|
const parsed = parseFilters(args);
|
|
167
151
|
if (typeof parsed === "string") return fail(parsed);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pal cli telos / pal cli timezone — the personal context PAL carries about you.
|
|
3
|
+
*
|
|
4
|
+
* pal cli telos Which TELOS topics are answered, in interview order
|
|
5
|
+
* pal cli timezone Show the configured timezone
|
|
6
|
+
* pal cli timezone <zone> Set it, if Intl recognises the name
|
|
7
|
+
*
|
|
8
|
+
* Both exist for the onboarding skill. pal-settings.json is hook-protected, so
|
|
9
|
+
* an agent cannot write the timezone itself, and a skill that restated the
|
|
10
|
+
* answered/unanswered rule in prose would drift from the code that decides it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
identity,
|
|
15
|
+
raw as readSettings,
|
|
16
|
+
write as writeSettings,
|
|
17
|
+
} from "../hooks/lib/settings";
|
|
18
|
+
import { telosStatus } from "../hooks/lib/telos-topics";
|
|
19
|
+
import { canonicalTimeZone } from "../hooks/lib/wall-clock";
|
|
20
|
+
import { log } from "../targets/lib";
|
|
21
|
+
|
|
22
|
+
export function runTelos(args: string[]): number {
|
|
23
|
+
if (args.length > 0) {
|
|
24
|
+
log.error(`Unknown telos argument: ${args[0]}`);
|
|
25
|
+
log.info("Usage: pal cli telos");
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const topics = telosStatus();
|
|
30
|
+
for (const topic of topics) {
|
|
31
|
+
const mark = topic.answered ? "answered " : "unanswered";
|
|
32
|
+
const tier = topic.priority ? "" : " (optional)";
|
|
33
|
+
console.log(` ${mark} ${topic.key.padEnd(11)} ${topic.file}${tier}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const next = topics.find((topic) => topic.priority && !topic.answered);
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log(next ? `next: ${next.key}` : "next: none — every priority topic answered");
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function runTimezone(args: string[]): number {
|
|
43
|
+
const requested = args.join(" ").trim();
|
|
44
|
+
|
|
45
|
+
if (!requested) {
|
|
46
|
+
const current = identity().principal.timezone;
|
|
47
|
+
console.log(
|
|
48
|
+
current ? `timezone: ${current}` : "timezone: not set — times read as UTC"
|
|
49
|
+
);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const zone = canonicalTimeZone(requested);
|
|
54
|
+
if (!zone) {
|
|
55
|
+
log.error(`Not a timezone Intl recognises: ${requested}`);
|
|
56
|
+
log.info("Use an IANA name — Europe/Budapest, America/New_York, Asia/Tokyo");
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const settings = { ...readSettings() };
|
|
61
|
+
settings.identity ??= {};
|
|
62
|
+
settings.identity.principal ??= {};
|
|
63
|
+
settings.identity.principal.timezone = zone;
|
|
64
|
+
writeSettings(settings);
|
|
65
|
+
log.success(`timezone: ${zone}`);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pal cli server — start, stop and inspect the local control room.
|
|
3
|
+
*
|
|
4
|
+
* The server itself is src/tools/control-room/server.ts, run detached so it
|
|
5
|
+
* outlives the shell that started it. This file only owns the lifecycle:
|
|
6
|
+
* spawning, waiting for it to answer, remembering its pid, and killing it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
12
|
+
import { spawnDetachedInference } from "../hooks/lib/detached-inference";
|
|
13
|
+
import { paths } from "../hooks/lib/paths";
|
|
14
|
+
import { DEFAULT_PORT, LOOPBACK, type ServerStatus } from "../tools/control-room/server";
|
|
15
|
+
|
|
16
|
+
interface ServerState {
|
|
17
|
+
pid: number;
|
|
18
|
+
port: number;
|
|
19
|
+
startedAt: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SERVER_SCRIPT = resolve(
|
|
23
|
+
import.meta.dir,
|
|
24
|
+
"..",
|
|
25
|
+
"tools",
|
|
26
|
+
"control-room",
|
|
27
|
+
"server.ts"
|
|
28
|
+
);
|
|
29
|
+
const STARTUP_TIMEOUT_MS = 3000;
|
|
30
|
+
const PROBE_TIMEOUT_MS = 500;
|
|
31
|
+
|
|
32
|
+
export async function runServer(args: string[]): Promise<number> {
|
|
33
|
+
const [sub, ...rest] = args;
|
|
34
|
+
switch (sub) {
|
|
35
|
+
case "start":
|
|
36
|
+
return cmdStart(rest);
|
|
37
|
+
case "stop":
|
|
38
|
+
return cmdStop();
|
|
39
|
+
case "status":
|
|
40
|
+
return cmdStatus();
|
|
41
|
+
case undefined:
|
|
42
|
+
case "help":
|
|
43
|
+
case "--help":
|
|
44
|
+
case "-h":
|
|
45
|
+
showHelp();
|
|
46
|
+
return 0;
|
|
47
|
+
default:
|
|
48
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
49
|
+
showHelp();
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function showHelp(): void {
|
|
55
|
+
console.log(`
|
|
56
|
+
Usage:
|
|
57
|
+
pal cli server <subcommand>
|
|
58
|
+
|
|
59
|
+
Subcommands:
|
|
60
|
+
start [--port <n>] Start the control room in the background (default port ${DEFAULT_PORT})
|
|
61
|
+
stop Stop it
|
|
62
|
+
status Show whether it is running, and where
|
|
63
|
+
|
|
64
|
+
The page listens on ${LOOPBACK} only.
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function url(port: number): string {
|
|
69
|
+
return `http://${LOOPBACK}:${port}/`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readState(): ServerState | null {
|
|
73
|
+
const file = paths.serverState();
|
|
74
|
+
if (!existsSync(file)) return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(readFileSync(file, "utf-8")) as ServerState;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeState(state: ServerState): void {
|
|
83
|
+
writeFileSync(paths.serverState(), JSON.stringify(state, null, 2), "utf-8");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function clearState(): void {
|
|
87
|
+
const file = paths.serverState();
|
|
88
|
+
if (existsSync(file)) unlinkSync(file);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function alive(pid: number): boolean {
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid, 0);
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function probe(port: number): Promise<ServerStatus | null> {
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch(`${url(port)}api/status`, {
|
|
103
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
104
|
+
});
|
|
105
|
+
return res.ok ? ((await res.json()) as ServerStatus) : null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function waitUntilAnswering(port: number): Promise<ServerStatus | null> {
|
|
112
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
113
|
+
while (Date.now() < deadline) {
|
|
114
|
+
const status = await probe(port);
|
|
115
|
+
if (status) return status;
|
|
116
|
+
await Bun.sleep(100);
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parsePort(args: string[]): number | string {
|
|
122
|
+
const { values } = parseArgs({ args, options: { port: { type: "string" } } });
|
|
123
|
+
if (values.port === undefined) return DEFAULT_PORT;
|
|
124
|
+
const port = Number(values.port);
|
|
125
|
+
return Number.isInteger(port) && port > 0 && port < 65536
|
|
126
|
+
? port
|
|
127
|
+
: `--port must be a port number, got ${values.port}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function cmdStart(args: string[]): Promise<number> {
|
|
131
|
+
const port = parsePort(args);
|
|
132
|
+
if (typeof port === "string") return fail(port);
|
|
133
|
+
|
|
134
|
+
const running = await runningServer();
|
|
135
|
+
if (running) {
|
|
136
|
+
console.log(`Already running at ${url(running.port)} (pid ${running.pid})`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
spawnDetachedInference(SERVER_SCRIPT, [`--port=${port}`], "control-room");
|
|
141
|
+
const status = await waitUntilAnswering(port);
|
|
142
|
+
if (!status)
|
|
143
|
+
return fail(
|
|
144
|
+
`The control room did not answer on port ${port} within ${STARTUP_TIMEOUT_MS / 1000}s. Is the port free?`
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
writeState({ pid: status.pid, port, startedAt: status.startedAt });
|
|
148
|
+
console.log(url(port));
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The state file is a claim; the process answering on that port is the fact. */
|
|
153
|
+
async function runningServer(): Promise<ServerState | null> {
|
|
154
|
+
const state = readState();
|
|
155
|
+
if (!state || !alive(state.pid)) return null;
|
|
156
|
+
return (await probe(state.port)) ? state : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function cmdStop(): Promise<number> {
|
|
160
|
+
const state = readState();
|
|
161
|
+
if (!state) {
|
|
162
|
+
console.log("Not running.");
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
if (alive(state.pid)) {
|
|
166
|
+
process.kill(state.pid);
|
|
167
|
+
console.log(`Stopped pid ${state.pid}.`);
|
|
168
|
+
} else {
|
|
169
|
+
console.log(`Pid ${state.pid} was already gone; cleared the stale record.`);
|
|
170
|
+
}
|
|
171
|
+
clearState();
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function cmdStatus(): Promise<number> {
|
|
176
|
+
const state = readState();
|
|
177
|
+
if (!state) {
|
|
178
|
+
console.log("Not running.");
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
const status = alive(state.pid) ? await probe(state.port) : null;
|
|
182
|
+
if (!status) {
|
|
183
|
+
console.log(
|
|
184
|
+
`Not running (stale record for pid ${state.pid}; run \`pal cli server stop\`).`
|
|
185
|
+
);
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
console.log(`
|
|
189
|
+
${url(status.port)}
|
|
190
|
+
pid ${status.pid}
|
|
191
|
+
started ${status.startedAt}
|
|
192
|
+
ledger ${status.ledgerFiles} file(s)
|
|
193
|
+
machine ${status.machine}
|
|
194
|
+
`);
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function fail(message: string): number {
|
|
199
|
+
console.error(message);
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
@@ -9,6 +9,17 @@ import {
|
|
|
9
9
|
raw as readSettings,
|
|
10
10
|
write as writeSettings,
|
|
11
11
|
} from "../hooks/lib/settings";
|
|
12
|
+
import { isValidTimeZone } from "../hooks/lib/wall-clock";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Empty means the machine's guess, which clack substitutes after validation runs,
|
|
16
|
+
* so pressing Enter has to pass.
|
|
17
|
+
* @lintignore exercised directly by test/setup-identity.test.ts
|
|
18
|
+
*/
|
|
19
|
+
export function timezoneProblem(input: string | undefined): string | undefined {
|
|
20
|
+
if (!input || isValidTimeZone(input)) return undefined;
|
|
21
|
+
return `Not a timezone Intl recognises: ${input}`;
|
|
22
|
+
}
|
|
12
23
|
|
|
13
24
|
/** Prompt for missing identity fields. Skips any field that already has a value. */
|
|
14
25
|
export async function promptIdentity(): Promise<void> {
|
|
@@ -80,8 +91,9 @@ export async function promptIdentity(): Promise<void> {
|
|
|
80
91
|
if (needsTimezone) {
|
|
81
92
|
const guess = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
82
93
|
const tz = await clack.text({
|
|
83
|
-
message:
|
|
94
|
+
message: `Your timezone (Enter keeps ${guess})`,
|
|
84
95
|
defaultValue: guess,
|
|
96
|
+
validate: timezoneProblem,
|
|
85
97
|
});
|
|
86
98
|
if (clack.isCancel(tz)) {
|
|
87
99
|
clack.cancel("Setup cancelled");
|