portable-agent-layer 0.21.0 → 0.23.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 +3 -2
- package/assets/agents/gemini-researcher.md +17 -3
- package/assets/agents/grok-researcher.md +19 -5
- package/assets/agents/multi-perspective-researcher.md +16 -2
- package/assets/agents/perplexity-researcher.md +17 -3
- package/assets/skills/analyze-pdf/SKILL.md +1 -1
- package/assets/skills/analyze-youtube/SKILL.md +1 -1
- package/assets/skills/extract-entities/SKILL.md +1 -1
- package/assets/skills/fyzz-chat-api/SKILL.md +6 -6
- package/assets/skills/fyzz-chat-api/tools/fyzz-api.ts +4 -4
- package/assets/skills/reflect/SKILL.md +2 -2
- package/assets/skills/telos/SKILL.md +6 -6
- package/assets/templates/AGENTS.md.template +2 -2
- package/assets/templates/PAL/ALGORITHM.md +139 -13
- package/assets/templates/PAL/CONTEXT_ROUTING.md +17 -17
- package/assets/templates/PAL/MEMORY_SYSTEM.md +5 -5
- package/assets/templates/PAL/README.md +12 -9
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
- package/assets/templates/PAL/WORK_TRACKING.md +2 -9
- package/assets/templates/pal-settings.json +6 -3
- package/assets/templates/settings.claude.json +2 -2
- package/package.json +3 -1
- package/src/cli/index.ts +7 -14
- package/src/hooks/handlers/rating.ts +1 -1
- package/src/hooks/handlers/relationship.ts +3 -3
- package/src/hooks/handlers/session-intelligence.ts +324 -0
- package/src/hooks/handlers/session-name.ts +3 -3
- package/src/hooks/handlers/synthesis.ts +36 -0
- package/src/hooks/handlers/update-check.ts +2 -2
- package/src/hooks/handlers/work-learning.ts +1 -1
- package/src/hooks/lib/context.ts +123 -41
- package/src/hooks/lib/graduation.ts +1 -1
- package/src/hooks/lib/inference.ts +1 -1
- package/src/hooks/lib/paths.ts +4 -12
- package/src/hooks/lib/readme-sync.ts +3 -3
- package/src/hooks/lib/security.ts +41 -27
- package/src/hooks/lib/stop.ts +6 -6
- package/src/hooks/lib/token-usage.ts +1 -0
- package/src/hooks/lib/work-tracking.ts +1 -51
- package/src/targets/claude/install.ts +3 -1
- package/src/targets/cursor/install.ts +9 -1
- package/src/targets/cursor/uninstall.ts +7 -0
- package/src/targets/lib.ts +214 -111
- package/src/targets/opencode/install.ts +6 -4
- package/src/tools/agent/algorithm-reflect.ts +122 -0
- package/src/tools/agent/synthesize.ts +361 -0
- package/src/tools/agent/thread.ts +162 -0
package/src/hooks/lib/context.ts
CHANGED
|
@@ -15,13 +15,7 @@ import { readSessionNames } from "./session-names";
|
|
|
15
15
|
import { buildSetupPrompt, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
|
|
16
16
|
import { computeSignalTrends, formatTrends } from "./signal-trends";
|
|
17
17
|
import { readFramePrinciples } from "./wisdom";
|
|
18
|
-
import {
|
|
19
|
-
activeProjects,
|
|
20
|
-
readProjectHistory,
|
|
21
|
-
readSessions,
|
|
22
|
-
recentSessions,
|
|
23
|
-
staleProjects,
|
|
24
|
-
} from "./work-tracking";
|
|
18
|
+
import { readProjectHistory, readSessions, recentSessions } from "./work-tracking";
|
|
25
19
|
|
|
26
20
|
interface PalSettings {
|
|
27
21
|
loadAtStartup?: { files?: string[] };
|
|
@@ -83,44 +77,16 @@ export function loadActiveWork(): { text: string; summary: string | null } | nul
|
|
|
83
77
|
try {
|
|
84
78
|
const cwd = process.cwd();
|
|
85
79
|
const allRecent = recentSessions(48);
|
|
86
|
-
const projects = activeProjects();
|
|
87
|
-
const stale = staleProjects(7);
|
|
88
80
|
|
|
89
|
-
if (allRecent.length === 0
|
|
81
|
+
if (allRecent.length === 0) return null;
|
|
90
82
|
|
|
91
83
|
const lines: string[] = [];
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
lines.push(`- [${s.status}] ${s.name} — ${ago}${here}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (projects.length > 0) {
|
|
103
|
-
lines.push("", "### Active Projects");
|
|
104
|
-
for (const p of projects) {
|
|
105
|
-
const sessionCount = p.sessions.length;
|
|
106
|
-
const ago = formatAgo(p.updated);
|
|
107
|
-
lines.push(`- **${p.name}** (${sessionCount} sessions, last: ${ago})`);
|
|
108
|
-
if (p.nextSteps.length > 0) {
|
|
109
|
-
lines.push(` Next: ${p.nextSteps[0]}`);
|
|
110
|
-
}
|
|
111
|
-
if (p.blockers.length > 0) {
|
|
112
|
-
lines.push(` Blockers: ${p.blockers.join(", ")}`);
|
|
113
|
-
} else {
|
|
114
|
-
lines.push(" Blockers: None");
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (stale.length > 0) {
|
|
120
|
-
lines.push("", "### Stale Projects (>7d inactive)");
|
|
121
|
-
for (const p of stale) {
|
|
122
|
-
lines.push(`- **${p.name}** — last active ${formatAgo(p.updated)}`);
|
|
123
|
-
}
|
|
85
|
+
lines.push("## Recent Work (last 48h)");
|
|
86
|
+
for (const s of allRecent.slice(-10).reverse()) {
|
|
87
|
+
const ago = formatAgo(s.ts);
|
|
88
|
+
const here = s.cwd === cwd ? " *" : "";
|
|
89
|
+
lines.push(`- [${s.status}] ${s.name} — ${ago}${here}`);
|
|
124
90
|
}
|
|
125
91
|
|
|
126
92
|
// Summary from most recent session
|
|
@@ -367,6 +333,116 @@ export function loadRelationshipContext(): string {
|
|
|
367
333
|
}
|
|
368
334
|
}
|
|
369
335
|
|
|
336
|
+
/** Load session intelligence from compact synthesis state */
|
|
337
|
+
export function loadSessionIntelligence(): string {
|
|
338
|
+
try {
|
|
339
|
+
const p = resolve(paths.state(), "synthesis.json");
|
|
340
|
+
if (!existsSync(p)) return "";
|
|
341
|
+
const state = JSON.parse(readFileSync(p, "utf-8"));
|
|
342
|
+
|
|
343
|
+
const lines: string[] = ["## Session Intelligence"];
|
|
344
|
+
|
|
345
|
+
// Open Threads — project-specific first, then global
|
|
346
|
+
if (state.threads?.length > 0) {
|
|
347
|
+
const cwd = process.cwd();
|
|
348
|
+
const here = state.threads.filter((t: { cwd?: string }) => t.cwd === cwd);
|
|
349
|
+
const other = state.threads.filter((t: { cwd?: string }) => t.cwd !== cwd);
|
|
350
|
+
|
|
351
|
+
if (here.length > 0) {
|
|
352
|
+
lines.push("");
|
|
353
|
+
lines.push(`**Open threads — this project (${here.length}):**`);
|
|
354
|
+
for (const t of here) {
|
|
355
|
+
lines.push(`- ${t.title} (opened ${t.opened})`);
|
|
356
|
+
if (t.context) lines.push(` ${t.context}`);
|
|
357
|
+
}
|
|
358
|
+
lines.push("→ These are directly relevant to your current work.");
|
|
359
|
+
}
|
|
360
|
+
if (other.length > 0) {
|
|
361
|
+
lines.push("");
|
|
362
|
+
lines.push(`**Open threads — other projects (${other.length}):**`);
|
|
363
|
+
for (const t of other) {
|
|
364
|
+
lines.push(`- ${t.title} (opened ${t.opened})`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Rating Trend
|
|
370
|
+
if (state.ratings?.count > 0) {
|
|
371
|
+
const r = state.ratings;
|
|
372
|
+
lines.push("");
|
|
373
|
+
lines.push(
|
|
374
|
+
`**Rating trend:** ${r.avg}/10 avg (last 10: ${r.recentAvg}/10, ${r.trend}).${r.lowCount > 0 ? ` ${r.lowCount} low ratings.` : ""}`
|
|
375
|
+
);
|
|
376
|
+
if (r.trend === "declining") {
|
|
377
|
+
lines.push(
|
|
378
|
+
"→ Trend is declining. Be extra careful with assumptions. Confirm before acting."
|
|
379
|
+
);
|
|
380
|
+
} else if (r.trend === "improving") {
|
|
381
|
+
lines.push("→ Trend is improving. Maintain current approach.");
|
|
382
|
+
} else if (r.lowCount > 5) {
|
|
383
|
+
lines.push(
|
|
384
|
+
"→ Multiple low ratings. Slow down, verify before acting, ask when uncertain."
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Algorithm Performance
|
|
390
|
+
if (state.algorithm?.reflectionCount > 0) {
|
|
391
|
+
const a = state.algorithm;
|
|
392
|
+
lines.push("");
|
|
393
|
+
lines.push(
|
|
394
|
+
`**Algorithm:** ${a.reflectionCount} reflections, ${a.passRate}% criteria pass rate, ${a.avgSentiment}/10 sentiment.`
|
|
395
|
+
);
|
|
396
|
+
if (a.passRate < 80) {
|
|
397
|
+
lines.push(
|
|
398
|
+
"→ Criteria pass rate is low. Invest more time in OBSERVE and PLAN phases."
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (a.recentObservations?.length > 0) {
|
|
402
|
+
const cwd = process.cwd();
|
|
403
|
+
const relevant = a.recentObservations.filter(
|
|
404
|
+
(o: { cwd?: string }) => !o.cwd || o.cwd === cwd
|
|
405
|
+
);
|
|
406
|
+
if (relevant.length > 0) {
|
|
407
|
+
lines.push("Recent self-observations (this project):");
|
|
408
|
+
for (const o of relevant) {
|
|
409
|
+
lines.push(`- [${o.date}] ${o.task}: "${o.observation}"`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return lines.length > 1 ? lines.join("\n") : "";
|
|
416
|
+
} catch {
|
|
417
|
+
return "";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Load handoff state for the current project */
|
|
422
|
+
export function loadHandoff(): string {
|
|
423
|
+
try {
|
|
424
|
+
const p = resolve(paths.state(), "last-handoff.json");
|
|
425
|
+
if (!existsSync(p)) return "";
|
|
426
|
+
const handoffs = JSON.parse(readFileSync(p, "utf-8"));
|
|
427
|
+
const cwd = process.cwd();
|
|
428
|
+
const entry = handoffs[cwd];
|
|
429
|
+
if (!entry?.handoff || entry.status !== "in-progress") return "";
|
|
430
|
+
|
|
431
|
+
const age = Date.now() - new Date(entry.timestamp).getTime();
|
|
432
|
+
if (age > 7 * 24 * 60 * 60 * 1000) return ""; // stale after 7 days
|
|
433
|
+
|
|
434
|
+
return [
|
|
435
|
+
"## Pick Up Where You Left Off",
|
|
436
|
+
`*Previous session: ${entry.title}*`,
|
|
437
|
+
"",
|
|
438
|
+
entry.handoff,
|
|
439
|
+
"→ Continue this work or explicitly close it before starting something new.",
|
|
440
|
+
].join("\n");
|
|
441
|
+
} catch {
|
|
442
|
+
return "";
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
370
446
|
/**
|
|
371
447
|
* Build the <system-reminder> content for the AI.
|
|
372
448
|
*
|
|
@@ -392,10 +468,16 @@ export function buildSystemReminder(): string {
|
|
|
392
468
|
? loadSynthesisRecommendations()
|
|
393
469
|
: "";
|
|
394
470
|
const opinions = isEnabled(settings, "opinions") ? loadOpinionContext() : "";
|
|
471
|
+
const intelligence = isEnabled(settings, "sessionIntelligence")
|
|
472
|
+
? loadSessionIntelligence()
|
|
473
|
+
: "";
|
|
474
|
+
const handoff = isEnabled(settings, "handoff") ? loadHandoff() : "";
|
|
395
475
|
const parts: string[] = [];
|
|
396
476
|
if (startup) parts.push(startup);
|
|
477
|
+
if (handoff) parts.push(handoff);
|
|
397
478
|
if (wisdom) parts.push(wisdom);
|
|
398
479
|
if (opinions) parts.push(opinions);
|
|
480
|
+
if (intelligence) parts.push(intelligence);
|
|
399
481
|
if (relationship) parts.push(relationship);
|
|
400
482
|
if (projectHistory) parts.push(projectHistory);
|
|
401
483
|
if (digest) parts.push(digest);
|
|
@@ -214,7 +214,7 @@ async function generateRecommendations(
|
|
|
214
214
|
ratings: RatingsSummary | null
|
|
215
215
|
): Promise<string[]> {
|
|
216
216
|
if (candidates.length === 0 && !ratings) return [];
|
|
217
|
-
if (!process.env.
|
|
217
|
+
if (!process.env.PAL_ANTHROPIC_API_KEY) {
|
|
218
218
|
return candidates
|
|
219
219
|
.slice(0, 3)
|
|
220
220
|
.map(
|
|
@@ -21,7 +21,7 @@ export interface InferenceResult {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export async function inference(opts: InferenceOptions): Promise<InferenceResult> {
|
|
24
|
-
const apiKey = process.env.
|
|
24
|
+
const apiKey = process.env.PAL_ANTHROPIC_API_KEY;
|
|
25
25
|
if (!apiKey) return { success: false };
|
|
26
26
|
|
|
27
27
|
const {
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -12,20 +12,12 @@ export function palPkg(): string {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Root of the user's personal state (telos, memory,
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* Repo mode is detected by the presence of .palroot next to the package.
|
|
20
|
-
* This file is not included in the npm package, so it only exists in cloned repos.
|
|
15
|
+
* Root of the user's personal state (telos, memory, docs, tools, skills).
|
|
16
|
+
* Always resolves to ~/.pal/ regardless of where the package lives.
|
|
17
|
+
* Power users who want memory/telos versioned in a repo can override via PAL_HOME.
|
|
21
18
|
*/
|
|
22
19
|
export function palHome(): string {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const pkgRoot = palPkg();
|
|
26
|
-
if (existsSync(resolve(pkgRoot, ".palroot"))) return pkgRoot;
|
|
27
|
-
|
|
28
|
-
return resolve(homedir(), ".pal");
|
|
20
|
+
return process.env.PAL_HOME || resolve(homedir(), ".pal");
|
|
29
21
|
}
|
|
30
22
|
|
|
31
23
|
/** Ensure a directory exists, creating it recursively if needed */
|
|
@@ -58,12 +58,12 @@ function extractEnvVars(): string[] {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// PAL_ANTHROPIC_API_KEY from inference.ts
|
|
62
62
|
const inferenceFile = resolve(pkg, "src", "hooks", "lib", "inference.ts");
|
|
63
63
|
if (existsSync(inferenceFile)) {
|
|
64
64
|
const content = readFileSync(inferenceFile, "utf-8");
|
|
65
|
-
if (content.includes("
|
|
66
|
-
vars.add("
|
|
65
|
+
if (content.includes("PAL_ANTHROPIC_API_KEY")) {
|
|
66
|
+
vars.add("PAL_ANTHROPIC_API_KEY");
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -34,6 +34,8 @@ export const HOOK_MANAGED_FILES = [
|
|
|
34
34
|
"debug.log.prev",
|
|
35
35
|
"opinions.json",
|
|
36
36
|
"pal-settings.json",
|
|
37
|
+
"skill-index.json",
|
|
38
|
+
"algorithm-reflections.jsonl",
|
|
37
39
|
];
|
|
38
40
|
|
|
39
41
|
/** Hook-managed directories — AI must not write to or delete from these */
|
|
@@ -45,7 +47,6 @@ export const HOOK_MANAGED_DIRS = [
|
|
|
45
47
|
"memory/relationship",
|
|
46
48
|
"memory/wisdom/state",
|
|
47
49
|
"memory/projects",
|
|
48
|
-
".agents/PAL",
|
|
49
50
|
];
|
|
50
51
|
|
|
51
52
|
/** Escape a string for use in a RegExp */
|
|
@@ -60,8 +61,11 @@ export const PROTECTED_PATHS: RegExp[] = [
|
|
|
60
61
|
/^\/System\//,
|
|
61
62
|
/\.ssh\/(?!config)/,
|
|
62
63
|
/\.gnupg\//,
|
|
63
|
-
// Derived from HOOK_MANAGED_FILES
|
|
64
|
-
...HOOK_MANAGED_FILES.map(
|
|
64
|
+
// Derived from HOOK_MANAGED_FILES — scoped to managed roots only
|
|
65
|
+
...HOOK_MANAGED_FILES.map(
|
|
66
|
+
(name) =>
|
|
67
|
+
new RegExp(`[/\\\\]\\.(?:pal|claude|agents|cursor)[/\\\\].*${escapeRegExp(name)}$`)
|
|
68
|
+
),
|
|
65
69
|
];
|
|
66
70
|
|
|
67
71
|
/** Patterns that warrant a warning (logged but not blocked) */
|
|
@@ -72,38 +76,44 @@ export const WARN_COMMANDS: RegExp[] = [
|
|
|
72
76
|
/truncate\s+table/i,
|
|
73
77
|
];
|
|
74
78
|
|
|
79
|
+
/** Roots where managed files/dirs are protected (user state, not repo templates) */
|
|
80
|
+
const MANAGED_ROOTS = [".pal/", ".claude/", ".agents/", ".config/opencode/", ".cursor/"];
|
|
81
|
+
|
|
82
|
+
function isUnderManagedRoot(path: string): boolean {
|
|
83
|
+
const normalized = path.replace(/\\/g, "/");
|
|
84
|
+
return MANAGED_ROOTS.some(
|
|
85
|
+
(root) => normalized.includes(`/${root}`) || normalized.includes(`\\.${root}`)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
/** Read-only commands allowed to reference protected files */
|
|
76
90
|
const READ_ONLY_COMMANDS =
|
|
77
|
-
/^\s*(?:cat|head|tail|less|more|grep|rg|wc|diff|stat|file|ls|dir|git\s+(?:log|diff|blame|show|status)|bat)\b/;
|
|
91
|
+
/^\s*(?:cat|head|tail|less|more|grep|rg|wc|diff|stat|file|ls|dir|find|git\s+(?:log|diff|blame|show|status)|bat)\b/;
|
|
78
92
|
|
|
79
93
|
/** Check a bash command against blocked patterns. Returns reason string or null. */
|
|
80
94
|
export function checkBashCommand(cmd: string): string | null {
|
|
81
95
|
for (const [pattern, reason] of BLOCKED_COMMANDS) {
|
|
82
96
|
if (pattern.test(cmd)) return reason;
|
|
83
97
|
}
|
|
84
|
-
// If command
|
|
98
|
+
// If command references a managed file in a managed root path, block unless read-only.
|
|
99
|
+
// The filename must appear IN the same path as the managed root (e.g. .pal/.../file.json).
|
|
100
|
+
const segments = cmd.split(/[|;&&]/).map((s) => s.trim());
|
|
85
101
|
for (const name of HOOK_MANAGED_FILES) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (!allReadOnly) {
|
|
93
|
-
return `${name} is managed automatically by hooks — do not edit directly`;
|
|
94
|
-
}
|
|
102
|
+
const pattern = new RegExp(
|
|
103
|
+
`\\.(?:pal|claude|agents|cursor|config/opencode)[/\\\\]\\S*${escapeRegExp(name)}`
|
|
104
|
+
);
|
|
105
|
+
const managed = segments.filter((s) => pattern.test(s));
|
|
106
|
+
if (managed.length > 0 && !managed.every((s) => READ_ONLY_COMMANDS.test(s))) {
|
|
107
|
+
return `${name} is managed automatically by hooks — do not edit directly`;
|
|
95
108
|
}
|
|
96
109
|
}
|
|
97
|
-
// If command mentions a hook-managed directory, block unless it's read-only
|
|
98
110
|
for (const dir of HOOK_MANAGED_DIRS) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return `${dir} is managed automatically by hooks — do not edit directly`;
|
|
106
|
-
}
|
|
111
|
+
const pattern = new RegExp(
|
|
112
|
+
`\\.(?:pal|claude|agents|cursor|config/opencode)[/\\\\]\\S*${escapeRegExp(dir)}`
|
|
113
|
+
);
|
|
114
|
+
const managed = segments.filter((s) => pattern.test(s));
|
|
115
|
+
if (managed.length > 0 && !managed.every((s) => READ_ONLY_COMMANDS.test(s))) {
|
|
116
|
+
return `${dir} is managed automatically by hooks — do not edit directly`;
|
|
107
117
|
}
|
|
108
118
|
}
|
|
109
119
|
return null;
|
|
@@ -112,10 +122,14 @@ export function checkBashCommand(cmd: string): string | null {
|
|
|
112
122
|
/** Check a file path against protected patterns. Returns a reason string or null. */
|
|
113
123
|
export function checkFilePath(filePath: string): string | null {
|
|
114
124
|
const normalized = filePath.replace(/\\/g, "/");
|
|
115
|
-
// Check hook-managed files
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
// Check hook-managed files — only under managed roots (not repo templates)
|
|
126
|
+
if (isUnderManagedRoot(normalized)) {
|
|
127
|
+
const matchedFile = HOOK_MANAGED_FILES.find((name) =>
|
|
128
|
+
normalized.endsWith(`/${name}`)
|
|
129
|
+
);
|
|
130
|
+
if (matchedFile) {
|
|
131
|
+
return `${matchedFile} is managed automatically by hooks — do not edit directly`;
|
|
132
|
+
}
|
|
119
133
|
}
|
|
120
134
|
// Check hook-managed directories
|
|
121
135
|
const matchedDir = HOOK_MANAGED_DIRS.find((dir) => normalized.includes(`/${dir}/`));
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -8,10 +8,10 @@ import { resolve } from "node:path";
|
|
|
8
8
|
import { autoBackup } from "../handlers/backup";
|
|
9
9
|
import { captureFailure } from "../handlers/failure";
|
|
10
10
|
import { checkReflectTrigger } from "../handlers/reflect-trigger";
|
|
11
|
-
import {
|
|
11
|
+
import { captureSessionIntelligence } from "../handlers/session-intelligence";
|
|
12
|
+
import { runSynthesis } from "../handlers/synthesis";
|
|
12
13
|
import { resetTab } from "../handlers/tab";
|
|
13
14
|
import { updateCounts } from "../handlers/update-counts";
|
|
14
|
-
import { captureWorkLearning } from "../handlers/work-learning";
|
|
15
15
|
import { captureWorkSession } from "../handlers/work-session";
|
|
16
16
|
import { logDebug, logError } from "./log";
|
|
17
17
|
import { ensureDir, paths } from "./paths";
|
|
@@ -39,23 +39,23 @@ export async function runStopHandlers(
|
|
|
39
39
|
const results = await Promise.allSettled([
|
|
40
40
|
captureWorkSession(transcript, options.sessionId),
|
|
41
41
|
resetTab(),
|
|
42
|
-
|
|
43
|
-
captureWorkLearning(transcript, options.sessionId),
|
|
42
|
+
captureSessionIntelligence(transcript, options.sessionId),
|
|
44
43
|
checkPendingFailure(transcript),
|
|
45
44
|
updateCounts(),
|
|
46
45
|
autoBackup(),
|
|
47
46
|
checkReflectTrigger(),
|
|
47
|
+
runSynthesis(),
|
|
48
48
|
]);
|
|
49
49
|
|
|
50
50
|
const handlerNames = [
|
|
51
51
|
"work-session",
|
|
52
52
|
"tab",
|
|
53
|
-
"
|
|
54
|
-
"work-learning",
|
|
53
|
+
"session-intelligence",
|
|
55
54
|
"pending-failure",
|
|
56
55
|
"update-counts",
|
|
57
56
|
"backup",
|
|
58
57
|
"reflect-trigger",
|
|
58
|
+
"synthesis",
|
|
59
59
|
];
|
|
60
60
|
for (let i = 0; i < results.length; i++) {
|
|
61
61
|
const r = results[i];
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structured work tracking: session history +
|
|
2
|
+
* Structured work tracking: session history + per-project history.
|
|
3
3
|
* Used by both Claude Code (StopOrchestrator) and opencode (plugin).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
import { ensureDir, paths } from "./paths";
|
|
9
|
-
import { now } from "./time";
|
|
10
9
|
|
|
11
10
|
// ── Session Records ──────────────────────────────────────────────
|
|
12
11
|
|
|
@@ -179,52 +178,3 @@ export function readProjectHistory(cwd: string, limit = 15): ProjectHistoryEntry
|
|
|
179
178
|
return [];
|
|
180
179
|
}
|
|
181
180
|
}
|
|
182
|
-
|
|
183
|
-
// ── Persistent Projects ──────────────────────────────────────────
|
|
184
|
-
|
|
185
|
-
export interface Project {
|
|
186
|
-
id: string;
|
|
187
|
-
name: string;
|
|
188
|
-
created: string;
|
|
189
|
-
updated: string;
|
|
190
|
-
status: "active" | "paused" | "completed";
|
|
191
|
-
objectives: string[];
|
|
192
|
-
decisions: string[];
|
|
193
|
-
completed: string[];
|
|
194
|
-
blockers: string[];
|
|
195
|
-
nextSteps: string[];
|
|
196
|
-
handoff: string;
|
|
197
|
-
sessions: string[];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function projectsPath(): string {
|
|
201
|
-
return resolve(ensureDir(paths.state()), "projects.json");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function readProjects(): Record<string, Project> {
|
|
205
|
-
const p = projectsPath();
|
|
206
|
-
if (!existsSync(p)) return {};
|
|
207
|
-
try {
|
|
208
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
209
|
-
} catch {
|
|
210
|
-
return {};
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function writeProject(project: Project): void {
|
|
215
|
-
const projects = readProjects();
|
|
216
|
-
project.updated = now();
|
|
217
|
-
projects[project.id] = project;
|
|
218
|
-
writeFileSync(projectsPath(), JSON.stringify(projects, null, 2), "utf-8");
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function activeProjects(): Project[] {
|
|
222
|
-
return Object.values(readProjects()).filter((p) => p.status === "active");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export function staleProjects(days = 7): Project[] {
|
|
226
|
-
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
227
|
-
return Object.values(readProjects()).filter(
|
|
228
|
-
(p) => p.status === "active" && new Date(p.updated).getTime() < cutoff
|
|
229
|
-
);
|
|
230
|
-
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
countAgents,
|
|
16
16
|
countMd,
|
|
17
17
|
countSkills,
|
|
18
|
+
generateSkillIndex,
|
|
18
19
|
loadSettingsTemplate,
|
|
19
20
|
log,
|
|
20
21
|
mergeSettings,
|
|
@@ -50,13 +51,14 @@ log.success("Merged PAL settings into settings.json");
|
|
|
50
51
|
// --- Copy skills ---
|
|
51
52
|
const skillsDir = resolve(CLAUDE_DIR, "skills");
|
|
52
53
|
copySkills(skillsDir);
|
|
54
|
+
generateSkillIndex();
|
|
53
55
|
|
|
54
56
|
// --- Copy agents ---
|
|
55
57
|
copyAgents();
|
|
56
58
|
|
|
57
59
|
// --- Copy PAL system docs ---
|
|
58
60
|
const palDocsCount = copyPalDocs();
|
|
59
|
-
log.success(`Installed ${palDocsCount} PAL docs to ~/.
|
|
61
|
+
log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
|
|
60
62
|
|
|
61
63
|
// --- Scaffold PAL settings ---
|
|
62
64
|
scaffoldPalSettings();
|
|
@@ -9,9 +9,11 @@ import { resolve } from "node:path";
|
|
|
9
9
|
import { regenerateIfNeeded } from "../../hooks/lib/claude-md";
|
|
10
10
|
import { assets, palPkg, platform } from "../../hooks/lib/paths";
|
|
11
11
|
import {
|
|
12
|
+
copyAgentsForCursor,
|
|
12
13
|
copyPalDocs,
|
|
13
14
|
copySkills,
|
|
14
15
|
countSkills,
|
|
16
|
+
generateSkillIndex,
|
|
15
17
|
loadCursorHooksTemplate,
|
|
16
18
|
log,
|
|
17
19
|
mergeCursorHooks,
|
|
@@ -44,10 +46,16 @@ log.success("Merged PAL hooks into hooks.json");
|
|
|
44
46
|
// --- Symlink skills to ~/.cursor/skills/ ---
|
|
45
47
|
const cursorSkillsDir = resolve(CURSOR_DIR, "skills");
|
|
46
48
|
copySkills(cursorSkillsDir);
|
|
49
|
+
generateSkillIndex();
|
|
50
|
+
|
|
51
|
+
// --- Copy agents to ~/.cursor/agents/ ---
|
|
52
|
+
const cursorAgentsDir = resolve(CURSOR_DIR, "agents");
|
|
53
|
+
const agentCount = copyAgentsForCursor(cursorAgentsDir);
|
|
54
|
+
if (agentCount > 0) log.success(`Installed ${agentCount} agents to ~/.cursor/agents/`);
|
|
47
55
|
|
|
48
56
|
// --- Copy PAL system docs ---
|
|
49
57
|
const palDocsCount = copyPalDocs();
|
|
50
|
-
log.success(`Installed ${palDocsCount} PAL docs to ~/.
|
|
58
|
+
log.success(`Installed ${palDocsCount} PAL docs to ~/.pal/docs/`);
|
|
51
59
|
|
|
52
60
|
// --- Scaffold PAL settings ---
|
|
53
61
|
scaffoldPalSettings();
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
loadCursorHooksTemplate,
|
|
12
12
|
log,
|
|
13
13
|
readJson,
|
|
14
|
+
removeAgentsFromCursor,
|
|
14
15
|
removePalDocs,
|
|
15
16
|
removeSkills,
|
|
16
17
|
unmergeCursorHooks,
|
|
@@ -46,6 +47,12 @@ if (removed.length > 0) {
|
|
|
46
47
|
log.info("No PAL skills found");
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
// --- Remove PAL agents ---
|
|
51
|
+
const removedAgents = removeAgentsFromCursor(resolve(CURSOR_DIR, "agents"));
|
|
52
|
+
if (removedAgents.length > 0) {
|
|
53
|
+
log.success(`Removed ${removedAgents.length} agent(s): ${removedAgents.join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
// --- Remove PAL system docs ---
|
|
50
57
|
removePalDocs();
|
|
51
58
|
|