portable-agent-layer 0.32.0 → 0.33.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/assets/skills/presentation/SKILL.md +124 -5
- package/assets/skills/presentation/WORKSHOP.md +128 -0
- package/assets/skills/presentation/theme-base/base.css +113 -0
- package/assets/skills/presentation/theme-base/layouts.css +11 -2
- package/assets/skills/presentation/tools/build.ts +136 -6
- package/assets/skills/presentation/tools/doctor.ts +106 -317
- package/assets/skills/presentation/tools/lib/lint-helpers.ts +150 -0
- package/assets/skills/presentation/tools/lib/lint-rules.ts +744 -0
- package/assets/skills/presentation/tools/lib/lint-types.ts +40 -0
- package/assets/skills/presentation/tools/new-deck.ts +9 -4
- package/assets/skills/presentation/vendor/reveal/plugin/highlight/github-dark.css +118 -0
- package/assets/skills/projects/SKILL.md +111 -0
- package/assets/skills/telos/SKILL.md +4 -1
- package/assets/templates/AGENTS.md.template +28 -7
- package/assets/templates/PAL/ALGORITHM.md +2 -0
- package/assets/templates/PAL/README.md +0 -1
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
- package/assets/templates/pal-settings.json +2 -2
- package/package.json +1 -1
- package/src/hooks/UserPromptOrchestrator.ts +3 -1
- package/src/hooks/handlers/auto-graduate.ts +169 -0
- package/src/hooks/handlers/inject-retrieval.ts +50 -0
- package/src/hooks/handlers/project-touch.ts +39 -0
- package/src/hooks/lib/context.ts +9 -8
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/projects.ts +270 -0
- package/src/hooks/lib/retrieval-index.ts +223 -0
- package/src/hooks/lib/retrieval.ts +170 -0
- package/src/hooks/lib/security.ts +2 -0
- package/src/hooks/lib/stop.ts +9 -1
- package/src/hooks/lib/text-similarity.ts +13 -9
- package/src/hooks/lib/wisdom.ts +155 -1
- package/src/tools/agent/project.ts +336 -0
- package/src/tools/self-model.ts +3 -3
- package/assets/templates/PAL/CONTEXT_ROUTING.md +0 -30
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// presentation skill — types shared across the lint pipeline.
|
|
2
|
+
//
|
|
3
|
+
// The doctor is structured as a rule registry: each rule is a small object
|
|
4
|
+
// with a `check` function that emits Findings. Rules are either slide-scoped
|
|
5
|
+
// (run once per slide) or deck-scoped (run once across the whole deck).
|
|
6
|
+
|
|
7
|
+
export type Severity = "E" | "W";
|
|
8
|
+
export type Finding = { rule: string; severity: Severity; msg: string };
|
|
9
|
+
export type SlideReport = { name: string; layout: string; findings: Finding[] };
|
|
10
|
+
|
|
11
|
+
export type SlideContext = {
|
|
12
|
+
name: string;
|
|
13
|
+
body: string; // raw markdown, includes Note: section
|
|
14
|
+
bodyNoNotes: string; // body with the Note: section stripped
|
|
15
|
+
layout: string;
|
|
16
|
+
deckDir: string;
|
|
17
|
+
heads1: string[];
|
|
18
|
+
heads2: string[];
|
|
19
|
+
index: number; // 0-based position in the deck
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type DeckContext = {
|
|
23
|
+
deckDir: string;
|
|
24
|
+
slides: SlideContext[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type SlideRule = {
|
|
28
|
+
name: string;
|
|
29
|
+
scope: "slide";
|
|
30
|
+
appliesTo?: (ctx: SlideContext) => boolean;
|
|
31
|
+
check: (ctx: SlideContext) => Promise<Finding[]> | Finding[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type DeckRule = {
|
|
35
|
+
name: string;
|
|
36
|
+
scope: "deck";
|
|
37
|
+
check: (ctx: DeckContext) => Promise<Finding[]> | Finding[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type Rule = SlideRule | DeckRule;
|
|
@@ -84,13 +84,18 @@ lang: en
|
|
|
84
84
|
await copyFile(join(sourceSlidesDir, f), join(slidesDir, f));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
//
|
|
88
|
-
//
|
|
87
|
+
// Build output lands in this deck-dir by default — flat (slug.html, slug.md)
|
|
88
|
+
// when --out defaults to the deck-dir, or under slug/ when --out is explicit.
|
|
89
|
+
// Pre-ignore both shapes.
|
|
89
90
|
const slug =
|
|
90
91
|
basename(target)
|
|
91
92
|
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
92
93
|
.replace(/^-+|-+$/g, "") || "deck";
|
|
93
|
-
await writeFile(
|
|
94
|
+
await writeFile(
|
|
95
|
+
join(target, ".gitignore"),
|
|
96
|
+
`${slug}.html\n${slug}.md\n${slug}/\n`,
|
|
97
|
+
"utf8"
|
|
98
|
+
);
|
|
94
99
|
|
|
95
100
|
console.log(`✓ deck scaffolded at ${target}`);
|
|
96
101
|
console.log(` template: ${templateName}`);
|
|
@@ -99,7 +104,7 @@ lang: en
|
|
|
99
104
|
console.log(`\nNext:`);
|
|
100
105
|
console.log(` $EDITOR ${slidesDir}/`);
|
|
101
106
|
console.log(` bun ~/.pal/skills/presentation/tools/build.ts ${target}`);
|
|
102
|
-
console.log(` # output →
|
|
107
|
+
console.log(` # output → ${target}/${slug}.html (override with --out <dir>)`);
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
main().catch((e) => {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
pre code.hljs {
|
|
2
|
+
display: block;
|
|
3
|
+
overflow-x: auto;
|
|
4
|
+
padding: 1em
|
|
5
|
+
}
|
|
6
|
+
code.hljs {
|
|
7
|
+
padding: 3px 5px
|
|
8
|
+
}
|
|
9
|
+
/*!
|
|
10
|
+
Theme: GitHub Dark
|
|
11
|
+
Description: Dark theme as seen on github.com
|
|
12
|
+
Author: github.com
|
|
13
|
+
Maintainer: @Hirse
|
|
14
|
+
Updated: 2021-05-15
|
|
15
|
+
|
|
16
|
+
Outdated base version: https://github.com/primer/github-syntax-dark
|
|
17
|
+
Current colors taken from GitHub's CSS
|
|
18
|
+
*/
|
|
19
|
+
.hljs {
|
|
20
|
+
color: #c9d1d9;
|
|
21
|
+
background: #0d1117
|
|
22
|
+
}
|
|
23
|
+
.hljs-doctag,
|
|
24
|
+
.hljs-keyword,
|
|
25
|
+
.hljs-meta .hljs-keyword,
|
|
26
|
+
.hljs-template-tag,
|
|
27
|
+
.hljs-template-variable,
|
|
28
|
+
.hljs-type,
|
|
29
|
+
.hljs-variable.language_ {
|
|
30
|
+
/* prettylights-syntax-keyword */
|
|
31
|
+
color: #ff7b72
|
|
32
|
+
}
|
|
33
|
+
.hljs-title,
|
|
34
|
+
.hljs-title.class_,
|
|
35
|
+
.hljs-title.class_.inherited__,
|
|
36
|
+
.hljs-title.function_ {
|
|
37
|
+
/* prettylights-syntax-entity */
|
|
38
|
+
color: #d2a8ff
|
|
39
|
+
}
|
|
40
|
+
.hljs-attr,
|
|
41
|
+
.hljs-attribute,
|
|
42
|
+
.hljs-literal,
|
|
43
|
+
.hljs-meta,
|
|
44
|
+
.hljs-number,
|
|
45
|
+
.hljs-operator,
|
|
46
|
+
.hljs-variable,
|
|
47
|
+
.hljs-selector-attr,
|
|
48
|
+
.hljs-selector-class,
|
|
49
|
+
.hljs-selector-id {
|
|
50
|
+
/* prettylights-syntax-constant */
|
|
51
|
+
color: #79c0ff
|
|
52
|
+
}
|
|
53
|
+
.hljs-regexp,
|
|
54
|
+
.hljs-string,
|
|
55
|
+
.hljs-meta .hljs-string {
|
|
56
|
+
/* prettylights-syntax-string */
|
|
57
|
+
color: #a5d6ff
|
|
58
|
+
}
|
|
59
|
+
.hljs-built_in,
|
|
60
|
+
.hljs-symbol {
|
|
61
|
+
/* prettylights-syntax-variable */
|
|
62
|
+
color: #ffa657
|
|
63
|
+
}
|
|
64
|
+
.hljs-comment,
|
|
65
|
+
.hljs-code,
|
|
66
|
+
.hljs-formula {
|
|
67
|
+
/* prettylights-syntax-comment */
|
|
68
|
+
color: #8b949e
|
|
69
|
+
}
|
|
70
|
+
.hljs-name,
|
|
71
|
+
.hljs-quote,
|
|
72
|
+
.hljs-selector-tag,
|
|
73
|
+
.hljs-selector-pseudo {
|
|
74
|
+
/* prettylights-syntax-entity-tag */
|
|
75
|
+
color: #7ee787
|
|
76
|
+
}
|
|
77
|
+
.hljs-subst {
|
|
78
|
+
/* prettylights-syntax-storage-modifier-import */
|
|
79
|
+
color: #c9d1d9
|
|
80
|
+
}
|
|
81
|
+
.hljs-section {
|
|
82
|
+
/* prettylights-syntax-markup-heading */
|
|
83
|
+
color: #1f6feb;
|
|
84
|
+
font-weight: bold
|
|
85
|
+
}
|
|
86
|
+
.hljs-bullet {
|
|
87
|
+
/* prettylights-syntax-markup-list */
|
|
88
|
+
color: #f2cc60
|
|
89
|
+
}
|
|
90
|
+
.hljs-emphasis {
|
|
91
|
+
/* prettylights-syntax-markup-italic */
|
|
92
|
+
color: #c9d1d9;
|
|
93
|
+
font-style: italic
|
|
94
|
+
}
|
|
95
|
+
.hljs-strong {
|
|
96
|
+
/* prettylights-syntax-markup-bold */
|
|
97
|
+
color: #c9d1d9;
|
|
98
|
+
font-weight: bold
|
|
99
|
+
}
|
|
100
|
+
.hljs-addition {
|
|
101
|
+
/* prettylights-syntax-markup-inserted */
|
|
102
|
+
color: #aff5b4;
|
|
103
|
+
background-color: #033a16
|
|
104
|
+
}
|
|
105
|
+
.hljs-deletion {
|
|
106
|
+
/* prettylights-syntax-markup-deleted */
|
|
107
|
+
color: #ffdcd7;
|
|
108
|
+
background-color: #67060c
|
|
109
|
+
}
|
|
110
|
+
.hljs-char.escape_,
|
|
111
|
+
.hljs-link,
|
|
112
|
+
.hljs-params,
|
|
113
|
+
.hljs-property,
|
|
114
|
+
.hljs-punctuation,
|
|
115
|
+
.hljs-tag {
|
|
116
|
+
/* purposely ignored */
|
|
117
|
+
|
|
118
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: projects
|
|
3
|
+
description: Project context management. PROACTIVE — use when the user references a project (by name or as "this repo", "current work"), asks to add/update/complete a project, says "store under <project>", "track this", "what am I working on", "my projects", "my priorities".
|
|
4
|
+
argument-hint: [list | create | resume | add-fact | add-objective | add-next | add-blocker | add-decision | add-handoff | complete | archive | pause]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Manage the user's project registry. Each project has its own state file at `~/.pal/memory/state/progress/{slug}.json`. The Stop hook auto-touches `updated` whenever the cwd resolves into a registered project — so just *being* in the project keeps it warm.
|
|
8
|
+
|
|
9
|
+
## CLI
|
|
10
|
+
|
|
11
|
+
All operations go through the canonical CLI:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun ~/.pal/tools/project.ts <command> [args]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Output is JSON.
|
|
18
|
+
|
|
19
|
+
| Command | Purpose |
|
|
20
|
+
|---------|---------|
|
|
21
|
+
| `list` | All registered projects with status, path, updated, stale flag, and counts |
|
|
22
|
+
| `create [name] [--path PATH] [--objectives "a;b;c"]` | Register a project. Defaults: name=basename(cwd), path=cwd. Slug must be `[a-z0-9_-]+` |
|
|
23
|
+
| `resume <name>` | Print the full project JSON — facts, objectives, next steps, blockers, decisions, handoff |
|
|
24
|
+
| `add-fact <name> "text"` | Append a stable fact / reference (e.g., "reference impl lives in this repo") |
|
|
25
|
+
| `add-objective <name> "text"` | Append an objective |
|
|
26
|
+
| `add-next <name> "text"` | Append a next step |
|
|
27
|
+
| `add-blocker <name> "text"` | Append a blocker |
|
|
28
|
+
| `add-decision <name> "decision" "rationale"` | Log a timestamped decision |
|
|
29
|
+
| `add-handoff <name> "text"` | Overwrite the handoff field (single-value) |
|
|
30
|
+
| `rm-fact \| rm-objective \| rm-next \| rm-blocker <name> <index>` | Remove an entry by zero-based index |
|
|
31
|
+
| `complete <name>` / `archive <name>` / `pause <name>` / `unpause <name>` | Status transitions |
|
|
32
|
+
| `rm <name>` | Delete the project state file entirely |
|
|
33
|
+
|
|
34
|
+
## Routing
|
|
35
|
+
|
|
36
|
+
| Intent | Action |
|
|
37
|
+
|--------|--------|
|
|
38
|
+
| "what am I working on", "my projects", "priorities" | `list` — summarize active and recently-touched projects |
|
|
39
|
+
| "tell me about <project>" | `resume <name>` — present current state, highlight blockers and next steps |
|
|
40
|
+
| "register this" / "track this" / cwd is unregistered work | `create` (default the name from cwd basename, confirm before writing) |
|
|
41
|
+
| "store under <project>: X" / "note on <project>: X" | Pick the field — durable reference → `add-fact`, work item → `add-next`, obstacle → `add-blocker`. If unclear, ask. |
|
|
42
|
+
| "we decided X because Y" | `add-decision <name> "X" "Y"` |
|
|
43
|
+
| "handoff for <project>" / "next session pick up at X" | `add-handoff <name> "<text>"` |
|
|
44
|
+
| "mark X complete" / "X is done" | `complete <name>` |
|
|
45
|
+
| "park <project>" / "pause <project>" | `pause <name>` |
|
|
46
|
+
| "archive <project>" | `archive <name>` |
|
|
47
|
+
|
|
48
|
+
## Proactive registration
|
|
49
|
+
|
|
50
|
+
When SessionStart context flags the current cwd as unregistered (e.g. `💡 cwd <path> is not yet registered; suggest registering if substantive work begins`) **and** the user starts substantive work (not just "hi"), surface the suggestion conversationally before the second tool call:
|
|
51
|
+
|
|
52
|
+
> "I see we're in `<basename>` and it's not registered yet — want me to add it as a project?"
|
|
53
|
+
|
|
54
|
+
- **Default name** = the FULL last path segment of cwd, lowercased. For `/repos/portable-agent-layer` → `portable-agent-layer`. Never split on `-`.
|
|
55
|
+
- **Confirm before creating.** Never auto-create without explicit user approval ("yes", "do it", "register").
|
|
56
|
+
- **Capture objectives in conversation.** If the user accepts but doesn't volunteer objectives, ask one short question, or infer from the last few messages and confirm.
|
|
57
|
+
|
|
58
|
+
### When NOT to suggest registration
|
|
59
|
+
|
|
60
|
+
- cwd has no project marker (`.git`, `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, etc.) — it's a notes folder, not a project.
|
|
61
|
+
- The user is clearly browsing or doing a one-off task.
|
|
62
|
+
- An ancestor of multiple registered projects is the cwd (e.g. a generic dev root) — that's browse mode by design.
|
|
63
|
+
- You're unsure. Err toward not registering.
|
|
64
|
+
|
|
65
|
+
## Append-as-you-go
|
|
66
|
+
|
|
67
|
+
When the user describes plans, blockers, or decisions during normal work, invoke the relevant subcommand to keep state current — that's the dynamism this system is built for. Don't invoke for fleeting comments, hypotheticals, or things the user is just thinking through. Wait for a clear declarative ("let's add X", "Z is blocking us"), not a question or musing.
|
|
68
|
+
|
|
69
|
+
## Examples
|
|
70
|
+
|
|
71
|
+
**Storing a reference under an existing project**
|
|
72
|
+
```
|
|
73
|
+
User: "store under <project> that a reference implementation exists in this repo"
|
|
74
|
+
→ Identify the project from `list` (or by name)
|
|
75
|
+
→ Durable reference, not a task → add-fact
|
|
76
|
+
→ bun ~/.pal/tools/project.ts add-fact <slug> "Reference implementation lives in this repo"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Registering the current repo**
|
|
80
|
+
```
|
|
81
|
+
User: "track this project"
|
|
82
|
+
→ Default name from cwd basename, confirm with user
|
|
83
|
+
→ bun ~/.pal/tools/project.ts create --path "$(pwd)" --objectives "first objective; second objective"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Logging a decision**
|
|
87
|
+
```
|
|
88
|
+
User: "we decided <decision> because <reason>"
|
|
89
|
+
→ bun ~/.pal/tools/project.ts add-decision <slug> "<decision>" "<reason>"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Completing a project**
|
|
93
|
+
```
|
|
94
|
+
User: "mark <project> as complete"
|
|
95
|
+
→ Confirm
|
|
96
|
+
→ bun ~/.pal/tools/project.ts complete <slug>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Anti-patterns
|
|
100
|
+
|
|
101
|
+
- **Don't dump the full JSON.** Summarize. The user can ask for the raw payload.
|
|
102
|
+
- **Don't write without confirming the field choice on ambiguous "store" requests.** A "fact" sticks forever; a "next step" implies follow-up — these are different commitments.
|
|
103
|
+
- **Don't edit the JSON files directly.** Always use the CLI — it timestamps `updated` and keeps the schema valid.
|
|
104
|
+
- **Don't re-introduce `~/.pal/telos/PROJECTS.md`.** That file and its `update-projects.ts` tool are deprecated. The legacy `telos` skill carries a deprecation notice for this reason.
|
|
105
|
+
- **Don't confuse `add-fact` with the `telos` skill's `LEARNED.md` or `IDEAS.md`.** Project facts are scoped to one project; TELOS lessons are cross-cutting.
|
|
106
|
+
|
|
107
|
+
## Rules
|
|
108
|
+
|
|
109
|
+
- Always check `list` (or `resume <name>`) before writing — match an existing project rather than spawning a near-duplicate.
|
|
110
|
+
- Slugs are `[a-z0-9_-]+`. Never rename a slug; if the display name needs to change, that's a code-side concern, not a slug change.
|
|
111
|
+
- The Stop hook handles `updated` automatically when cwd matches `path` — no manual touch needed just to mark a project alive.
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: telos
|
|
3
|
-
description: Personal
|
|
3
|
+
description: Personal context management. Use when discussing goals, beliefs, challenges, identity, updating telos, life context, changing a goal, priorities, what do I believe, current obstacles, mission, or strategies.
|
|
4
4
|
argument-hint: [area to view or update]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
> ⚠️ **DEPRECATION NOTICE — Project management has moved.**
|
|
8
|
+
> Project tracking is now handled by the `projects` skill, backed by `~/.pal/tools/project.ts` and per-project state in `~/.pal/memory/state/progress/`. **Do not use this skill for projects.** The `PROJECTS.md` references and `update-projects.ts` tool below are legacy and slated for removal — they remain only because the initial setup wizard (`src/cli/setup-telos.ts`) still depends on them. For anything project-related, invoke the `projects` skill.
|
|
9
|
+
|
|
7
10
|
Manage the user's TELOS files — the persistent personal context that drives PAL.
|
|
8
11
|
|
|
9
12
|
## TELOS Files
|
|
@@ -60,10 +60,31 @@ Start your response with the following header in this mode:
|
|
|
60
60
|
|
|
61
61
|
## Context Routing
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
Load context on-demand by reading the file at the path listed. Only load what the current task requires.
|
|
64
|
+
|
|
65
|
+
### PAL System
|
|
66
|
+
|
|
67
|
+
| Topic | Path |
|
|
68
|
+
|-------|------|
|
|
69
|
+
| PAL system overview | `~/.pal/docs/README.md` |
|
|
70
|
+
| System architecture | `~/.pal/docs/SYSTEM_ARCHITECTURE.md` |
|
|
71
|
+
| Memory format & guidelines | `~/.pal/docs/MEMORY_SYSTEM.md` |
|
|
72
|
+
| Work tracking (projects, sessions) | `~/.pal/docs/WORK_TRACKING.md` |
|
|
73
|
+
| Opinion tracking | `~/.pal/docs/OPINION_TRACKING.md` |
|
|
74
|
+
| Steering rules | `~/.pal/docs/STEERING_RULES.md` |
|
|
75
|
+
| Algorithm (complex work phases) | `~/.pal/docs/ALGORITHM.md` |
|
|
76
|
+
| Project lifecycle (when/how to register and manage user projects) | `~/.pal/skills/projects/SKILL.md` |
|
|
77
|
+
|
|
78
|
+
### User Context (TELOS)
|
|
79
|
+
|
|
80
|
+
| Topic | Path |
|
|
81
|
+
|-------|------|
|
|
82
|
+
| Goals (short/medium/long-term) | `~/.pal/telos/GOALS.md` |
|
|
83
|
+
| Beliefs & principles | `~/.pal/telos/BELIEFS.md` |
|
|
84
|
+
| Current challenges | `~/.pal/telos/CHALLENGES.md` |
|
|
85
|
+
| Mission & direction | `~/.pal/telos/MISSION.md` |
|
|
86
|
+
| Strategies & approaches | `~/.pal/telos/STRATEGIES.md` |
|
|
87
|
+
| Ideas to explore | `~/.pal/telos/IDEAS.md` |
|
|
88
|
+
| Key lessons learned | `~/.pal/telos/LEARNED.md` |
|
|
89
|
+
| Mental models | `~/.pal/telos/MODELS.md` |
|
|
90
|
+
| Narrative context | `~/.pal/telos/NARRATIVES.md` |
|
|
@@ -178,6 +178,8 @@ For EACH criterion:
|
|
|
178
178
|
|
|
179
179
|
**Capability check:** Confirm every selected capability was actually invoked via tool call. Text output alone does not count.
|
|
180
180
|
|
|
181
|
+
**Demonstrate, don't assert.** When verifying a new check / rule / behavior on a system that already passes, "existing inputs still pass" is not evidence the new logic works — the existing inputs would pass even if your code did nothing. Construct a deliberately-broken minimal example (a fake bad slide, a known-failing input, a unit test that should now fail without your change) and run it through to prove the new behavior actually fires. Show the failure happening in the verification output, not just the success case.
|
|
182
|
+
|
|
181
183
|
If any criteria failed, fix and re-verify before completing.
|
|
182
184
|
|
|
183
185
|
### ━━━ 📚 LEARN ━━━ 5/5
|
|
@@ -14,7 +14,6 @@ PAL is a persistent, cross-platform, cross-agent layer for portable AI workflows
|
|
|
14
14
|
~/.pal/ # PAL home
|
|
15
15
|
docs/ # System documentation (engine-managed)
|
|
16
16
|
ALGORITHM.md # The execution engine (4-phase)
|
|
17
|
-
CONTEXT_ROUTING.md # On-demand context routing table
|
|
18
17
|
MEMORY_SYSTEM.md # Memory guidelines
|
|
19
18
|
OPINION_TRACKING.md # Opinion system reference
|
|
20
19
|
STEERING_RULES.md # Behavioral rules
|
|
@@ -475,4 +475,4 @@ All paths resolve through `src/hooks/lib/paths.ts`:
|
|
|
475
475
|
| Library files | kebab-case | `text-similarity.ts`, `signal-trends.ts` |
|
|
476
476
|
| Tool files | kebab-case | `relationship-reflect.ts`, `token-cost.ts` |
|
|
477
477
|
| Memory files | date-prefixed | `2026-03-24.md`, `2026-03-24_weekly.md` |
|
|
478
|
-
| Template files | UPPER_SNAKE | `ALGORITHM.md`, `
|
|
478
|
+
| Template files | UPPER_SNAKE | `ALGORITHM.md`, `MEMORY_SYSTEM.md` |
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
"loadAtStartup": {
|
|
15
15
|
"_docs": "Files force-loaded into session context at startup. Injected as <system-reminder> blocks.",
|
|
16
16
|
"files": [
|
|
17
|
-
"~/.pal/docs/STEERING_RULES.md"
|
|
18
|
-
"~/.pal/telos/PROJECTS.md"
|
|
17
|
+
"~/.pal/docs/STEERING_RULES.md"
|
|
19
18
|
]
|
|
20
19
|
},
|
|
21
20
|
"dynamicContext": {
|
|
@@ -29,6 +28,7 @@
|
|
|
29
28
|
"failurePatterns": true,
|
|
30
29
|
"activeWork": true,
|
|
31
30
|
"projectHistory": true,
|
|
31
|
+
"projects": true,
|
|
32
32
|
"sessionIntelligence": true,
|
|
33
33
|
"handoff": true,
|
|
34
34
|
"selfModel": true
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - session-name: generate 4-word session headline on first prompt
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { injectRetrieval } from "./handlers/inject-retrieval";
|
|
10
11
|
import { captureRating } from "./handlers/rating";
|
|
11
12
|
import { captureSessionName } from "./handlers/session-name";
|
|
12
13
|
import { logDebug, logError } from "./lib/log";
|
|
@@ -24,9 +25,10 @@ if (!input?.prompt) process.exit(0);
|
|
|
24
25
|
const results = await Promise.allSettled([
|
|
25
26
|
captureRating(input.prompt, input.session_id),
|
|
26
27
|
captureSessionName(input.prompt, input.session_id ?? ""),
|
|
28
|
+
injectRetrieval(input.prompt),
|
|
27
29
|
]);
|
|
28
30
|
|
|
29
|
-
const handlerNames = ["rating", "session-name"];
|
|
31
|
+
const handlerNames = ["rating", "session-name", "inject-retrieval"];
|
|
30
32
|
for (let i = 0; i < results.length; i++) {
|
|
31
33
|
const r = results[i];
|
|
32
34
|
if (r.status === "rejected") {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop handler: promote graduated patterns into wisdom-frame CRYSTAL lines.
|
|
3
|
+
*
|
|
4
|
+
* Idempotency contract — N invocations in quick succession yield ≤1 promotion
|
|
5
|
+
* per pattern. Three layers:
|
|
6
|
+
*
|
|
7
|
+
* 1. TTL guard — skip if `graduated.json:lastRun` is younger than TTL_MS.
|
|
8
|
+
* Catches accidental thrashing (Stop firing many times/min).
|
|
9
|
+
*
|
|
10
|
+
* 2. State-dedup — `state.graduated[]` tracks every pattern ever promoted.
|
|
11
|
+
* A pattern with the same principle text never re-promotes, even after
|
|
12
|
+
* the TTL window closes.
|
|
13
|
+
*
|
|
14
|
+
* 3. Content-dedup — `promoteCrystal` skips if any existing CRYSTAL line in
|
|
15
|
+
* the target frame is Dice-similar (≥0.3) to the new principle. Last line
|
|
16
|
+
* of defense against state corruption / manual edits / near-misses.
|
|
17
|
+
*
|
|
18
|
+
* Past attempt at auto-graduation failed precisely because of duplicate writes;
|
|
19
|
+
* this design is structured around that lesson. See feedback memory:
|
|
20
|
+
* `feedback_graduation_idempotency.md`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { resolve } from "node:path";
|
|
25
|
+
import { analyze } from "../lib/graduation";
|
|
26
|
+
import { logDebug, logError } from "../lib/log";
|
|
27
|
+
import { ensureDir, paths } from "../lib/paths";
|
|
28
|
+
import { promoteCrystal } from "../lib/wisdom";
|
|
29
|
+
|
|
30
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 24h — matches synthesize.ts
|
|
31
|
+
const CRYSTAL_FLOOR = 85;
|
|
32
|
+
|
|
33
|
+
interface GraduatedEntry {
|
|
34
|
+
pattern: string;
|
|
35
|
+
domain: string;
|
|
36
|
+
confidence: number;
|
|
37
|
+
occurrences: number;
|
|
38
|
+
sources: string[];
|
|
39
|
+
graduatedAt: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface GraduationState {
|
|
43
|
+
lastRun: string;
|
|
44
|
+
graduated: GraduatedEntry[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function statePath(): string {
|
|
48
|
+
return resolve(ensureDir(paths.wisdomState()), "graduated.json");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readState(): GraduationState {
|
|
52
|
+
const p = statePath();
|
|
53
|
+
if (!existsSync(p)) return { lastRun: "", graduated: [] };
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(readFileSync(p, "utf-8")) as Partial<GraduationState>;
|
|
56
|
+
return {
|
|
57
|
+
lastRun: parsed.lastRun ?? "",
|
|
58
|
+
graduated: Array.isArray(parsed.graduated) ? parsed.graduated : [],
|
|
59
|
+
};
|
|
60
|
+
} catch {
|
|
61
|
+
return { lastRun: "", graduated: [] };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeState(state: GraduationState): void {
|
|
66
|
+
writeFileSync(statePath(), JSON.stringify(state, null, 2), "utf-8");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function withinTtl(state: GraduationState): boolean {
|
|
70
|
+
if (!state.lastRun) return false;
|
|
71
|
+
const last = new Date(state.lastRun).getTime();
|
|
72
|
+
if (!Number.isFinite(last)) return false;
|
|
73
|
+
return Date.now() - last < TTL_MS;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function alreadyPromoted(state: GraduationState, pattern: string): boolean {
|
|
77
|
+
return state.graduated.some((g) => g.pattern === pattern);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface AutoGraduateOptions {
|
|
81
|
+
/** Bypass the 24h TTL guard. State + content dedup still apply. */
|
|
82
|
+
force?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface AutoGraduateResult {
|
|
86
|
+
ranAnalysis: boolean;
|
|
87
|
+
candidatesAtFloor: number;
|
|
88
|
+
promoted: number;
|
|
89
|
+
skippedByState: number;
|
|
90
|
+
skippedByContent: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run auto-graduation. Safe to call as often as you like — see file header.
|
|
95
|
+
*
|
|
96
|
+
* Returns a summary of what happened so callers (handler, tests) can reason
|
|
97
|
+
* about the run without re-reading state.
|
|
98
|
+
*/
|
|
99
|
+
export async function autoGraduate(
|
|
100
|
+
opts: AutoGraduateOptions = {}
|
|
101
|
+
): Promise<AutoGraduateResult> {
|
|
102
|
+
const result: AutoGraduateResult = {
|
|
103
|
+
ranAnalysis: false,
|
|
104
|
+
candidatesAtFloor: 0,
|
|
105
|
+
promoted: 0,
|
|
106
|
+
skippedByState: 0,
|
|
107
|
+
skippedByContent: 0,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const state = readState();
|
|
111
|
+
if (!opts.force && withinTtl(state)) {
|
|
112
|
+
logDebug("auto-graduate", `skip — within TTL (last ${state.lastRun})`);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let analysis: Awaited<ReturnType<typeof analyze>>;
|
|
117
|
+
try {
|
|
118
|
+
analysis = await analyze();
|
|
119
|
+
result.ranAnalysis = true;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
logError("auto-graduate:analyze", err);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const eligible = analysis.graduated.filter((g) => g.confidence >= CRYSTAL_FLOOR);
|
|
126
|
+
result.candidatesAtFloor = eligible.length;
|
|
127
|
+
|
|
128
|
+
for (const g of eligible) {
|
|
129
|
+
if (alreadyPromoted(state, g.pattern)) {
|
|
130
|
+
result.skippedByState++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const outcome = promoteCrystal(g.domain, g.pattern, g.confidence);
|
|
134
|
+
if (outcome.skipped === "duplicate") {
|
|
135
|
+
// Frame already had a Dice-similar CRYSTAL line — record in state so we
|
|
136
|
+
// don't re-attempt next run.
|
|
137
|
+
result.skippedByContent++;
|
|
138
|
+
state.graduated.push({
|
|
139
|
+
pattern: g.pattern,
|
|
140
|
+
domain: g.domain,
|
|
141
|
+
confidence: g.confidence,
|
|
142
|
+
occurrences: g.occurrences,
|
|
143
|
+
sources: g.sources,
|
|
144
|
+
graduatedAt: new Date().toISOString(),
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
result.promoted++;
|
|
149
|
+
state.graduated.push({
|
|
150
|
+
pattern: g.pattern,
|
|
151
|
+
domain: g.domain,
|
|
152
|
+
confidence: g.confidence,
|
|
153
|
+
occurrences: g.occurrences,
|
|
154
|
+
sources: g.sources,
|
|
155
|
+
graduatedAt: new Date().toISOString(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
state.lastRun = new Date().toISOString();
|
|
160
|
+
writeState(state);
|
|
161
|
+
|
|
162
|
+
if (result.promoted > 0 || result.skippedByState > 0 || result.skippedByContent > 0) {
|
|
163
|
+
logDebug(
|
|
164
|
+
"auto-graduate",
|
|
165
|
+
`promoted=${result.promoted} skipState=${result.skippedByState} skipContent=${result.skippedByContent} candidates=${result.candidatesAtFloor}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UserPromptSubmit handler: inject the top-N matching prior lessons into the prompt.
|
|
3
|
+
*
|
|
4
|
+
* Called from UserPromptOrchestrator. Reads the retrieval index, ranks the prompt
|
|
5
|
+
* against the corpus, prints a `<system-reminder>` block to stdout (Claude Code
|
|
6
|
+
* prepends UserPromptSubmit hook stdout to the prompt). Fail-closed: any error or
|
|
7
|
+
* timeout produces empty output, never blocks the prompt.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { logDebug, logError } from "../lib/log";
|
|
11
|
+
import { runRetrieval } from "../lib/retrieval";
|
|
12
|
+
import { ensureIndex } from "../lib/retrieval-index";
|
|
13
|
+
import { isEnabled } from "../lib/settings";
|
|
14
|
+
|
|
15
|
+
const TIMEOUT_MS = 250;
|
|
16
|
+
|
|
17
|
+
function withTimeout<T>(work: () => T, ms: number): Promise<T | null> {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const timer = setTimeout(() => resolve(null), ms);
|
|
20
|
+
try {
|
|
21
|
+
const result = work();
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
resolve(result);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
logError("inject-retrieval", err);
|
|
27
|
+
resolve(null);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function injectRetrieval(prompt: string): Promise<void> {
|
|
33
|
+
if (!prompt?.trim()) return;
|
|
34
|
+
if (!isEnabled("learningInjection")) return;
|
|
35
|
+
|
|
36
|
+
const result = await withTimeout(() => {
|
|
37
|
+
const index = ensureIndex();
|
|
38
|
+
if (index.corpusSize === 0) return null;
|
|
39
|
+
return runRetrieval(prompt, index, process.cwd());
|
|
40
|
+
}, TIMEOUT_MS);
|
|
41
|
+
|
|
42
|
+
if (!result?.reminder) return;
|
|
43
|
+
|
|
44
|
+
logDebug(
|
|
45
|
+
"inject-retrieval",
|
|
46
|
+
`injected ${result.matches.length} matches; top score=${result.matches[0]?.confidence.toFixed(3)}`
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
process.stdout.write(`${result.reminder}\n`);
|
|
50
|
+
}
|