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.
Files changed (35) hide show
  1. package/assets/skills/presentation/SKILL.md +124 -5
  2. package/assets/skills/presentation/WORKSHOP.md +128 -0
  3. package/assets/skills/presentation/theme-base/base.css +113 -0
  4. package/assets/skills/presentation/theme-base/layouts.css +11 -2
  5. package/assets/skills/presentation/tools/build.ts +136 -6
  6. package/assets/skills/presentation/tools/doctor.ts +106 -317
  7. package/assets/skills/presentation/tools/lib/lint-helpers.ts +150 -0
  8. package/assets/skills/presentation/tools/lib/lint-rules.ts +744 -0
  9. package/assets/skills/presentation/tools/lib/lint-types.ts +40 -0
  10. package/assets/skills/presentation/tools/new-deck.ts +9 -4
  11. package/assets/skills/presentation/vendor/reveal/plugin/highlight/github-dark.css +118 -0
  12. package/assets/skills/projects/SKILL.md +111 -0
  13. package/assets/skills/telos/SKILL.md +4 -1
  14. package/assets/templates/AGENTS.md.template +28 -7
  15. package/assets/templates/PAL/ALGORITHM.md +2 -0
  16. package/assets/templates/PAL/README.md +0 -1
  17. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
  18. package/assets/templates/pal-settings.json +2 -2
  19. package/package.json +1 -1
  20. package/src/hooks/UserPromptOrchestrator.ts +3 -1
  21. package/src/hooks/handlers/auto-graduate.ts +169 -0
  22. package/src/hooks/handlers/inject-retrieval.ts +50 -0
  23. package/src/hooks/handlers/project-touch.ts +39 -0
  24. package/src/hooks/lib/context.ts +9 -8
  25. package/src/hooks/lib/paths.ts +2 -0
  26. package/src/hooks/lib/projects.ts +270 -0
  27. package/src/hooks/lib/retrieval-index.ts +223 -0
  28. package/src/hooks/lib/retrieval.ts +170 -0
  29. package/src/hooks/lib/security.ts +2 -0
  30. package/src/hooks/lib/stop.ts +9 -1
  31. package/src/hooks/lib/text-similarity.ts +13 -9
  32. package/src/hooks/lib/wisdom.ts +155 -1
  33. package/src/tools/agent/project.ts +336 -0
  34. package/src/tools/self-model.ts +3 -3
  35. 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
- // If the user happens to run build/present from inside this deck-dir, the output
88
- // subdir lands here too. Pre-ignore it so it doesn't get accidentally committed.
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(join(target, ".gitignore"), `${slug}/\n`, "utf8");
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 → <cwd>/${slug}/${slug}.html (override with --out <dir>)`);
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 and project context management. Use when discussing goals, projects, beliefs, challenges, identity, updating telos, life context, what am I working on, adding a project, changing a goal, priorities, what do I believe, current obstacles, mission, or strategies.
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
- When you need context about any of these topics, read `~/.pal/docs/CONTEXT_ROUTING.md` for the file path:
64
-
65
- - PAL internals
66
- - The user, their life and work, etc
67
- - Your own personality and rules
68
- - Any project referenced, any work, etc.
69
- - Basically anything that's specialized
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`, `CONTEXT_ROUTING.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ }