pi-continuous-learning 0.3.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/LICENSE +21 -0
- package/README.md +326 -0
- package/dist/active-instincts.d.ts +4 -0
- package/dist/active-instincts.d.ts.map +1 -0
- package/dist/active-instincts.js +11 -0
- package/dist/active-instincts.js.map +1 -0
- package/dist/agents-md.d.ts +12 -0
- package/dist/agents-md.d.ts.map +1 -0
- package/dist/agents-md.js +23 -0
- package/dist/agents-md.js.map +1 -0
- package/dist/cli/analyze-prompt.d.ts +2 -0
- package/dist/cli/analyze-prompt.d.ts.map +1 -0
- package/dist/cli/analyze-prompt.js +72 -0
- package/dist/cli/analyze-prompt.js.map +1 -0
- package/dist/cli/analyze.d.ts +3 -0
- package/dist/cli/analyze.d.ts.map +1 -0
- package/dist/cli/analyze.js +214 -0
- package/dist/cli/analyze.js.map +1 -0
- package/dist/confidence.d.ts +25 -0
- package/dist/confidence.d.ts.map +1 -0
- package/dist/confidence.js +77 -0
- package/dist/confidence.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +89 -0
- package/dist/config.js.map +1 -0
- package/dist/error-logger.d.ts +34 -0
- package/dist/error-logger.d.ts.map +1 -0
- package/dist/error-logger.js +102 -0
- package/dist/error-logger.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +118 -0
- package/dist/index.js.map +1 -0
- package/dist/instinct-decay.d.ts +39 -0
- package/dist/instinct-decay.d.ts.map +1 -0
- package/dist/instinct-decay.js +93 -0
- package/dist/instinct-decay.js.map +1 -0
- package/dist/instinct-evolve.d.ts +5 -0
- package/dist/instinct-evolve.d.ts.map +1 -0
- package/dist/instinct-evolve.js +24 -0
- package/dist/instinct-evolve.js.map +1 -0
- package/dist/instinct-export.d.ts +40 -0
- package/dist/instinct-export.d.ts.map +1 -0
- package/dist/instinct-export.js +94 -0
- package/dist/instinct-export.js.map +1 -0
- package/dist/instinct-import.d.ts +50 -0
- package/dist/instinct-import.d.ts.map +1 -0
- package/dist/instinct-import.js +168 -0
- package/dist/instinct-import.js.map +1 -0
- package/dist/instinct-injector.d.ts +39 -0
- package/dist/instinct-injector.d.ts.map +1 -0
- package/dist/instinct-injector.js +89 -0
- package/dist/instinct-injector.js.map +1 -0
- package/dist/instinct-loader.d.ts +37 -0
- package/dist/instinct-loader.d.ts.map +1 -0
- package/dist/instinct-loader.js +96 -0
- package/dist/instinct-loader.js.map +1 -0
- package/dist/instinct-parser.d.ts +28 -0
- package/dist/instinct-parser.d.ts.map +1 -0
- package/dist/instinct-parser.js +143 -0
- package/dist/instinct-parser.js.map +1 -0
- package/dist/instinct-projects.d.ts +32 -0
- package/dist/instinct-projects.d.ts.map +1 -0
- package/dist/instinct-projects.js +96 -0
- package/dist/instinct-projects.js.map +1 -0
- package/dist/instinct-promote.d.ts +51 -0
- package/dist/instinct-promote.d.ts.map +1 -0
- package/dist/instinct-promote.js +169 -0
- package/dist/instinct-promote.js.map +1 -0
- package/dist/instinct-status.d.ts +39 -0
- package/dist/instinct-status.d.ts.map +1 -0
- package/dist/instinct-status.js +108 -0
- package/dist/instinct-status.js.map +1 -0
- package/dist/instinct-store.d.ts +30 -0
- package/dist/instinct-store.d.ts.map +1 -0
- package/dist/instinct-store.js +118 -0
- package/dist/instinct-store.js.map +1 -0
- package/dist/instinct-tools.d.ts +161 -0
- package/dist/instinct-tools.d.ts.map +1 -0
- package/dist/instinct-tools.js +240 -0
- package/dist/instinct-tools.js.map +1 -0
- package/dist/observations.d.ts +22 -0
- package/dist/observations.d.ts.map +1 -0
- package/dist/observations.js +62 -0
- package/dist/observations.js.map +1 -0
- package/dist/observer-guard.d.ts +3 -0
- package/dist/observer-guard.d.ts.map +1 -0
- package/dist/observer-guard.js +13 -0
- package/dist/observer-guard.js.map +1 -0
- package/dist/project.d.ts +16 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +59 -0
- package/dist/project.js.map +1 -0
- package/dist/prompt-observer.d.ts +25 -0
- package/dist/prompt-observer.d.ts.map +1 -0
- package/dist/prompt-observer.js +63 -0
- package/dist/prompt-observer.js.map +1 -0
- package/dist/prompts/analyzer-user.d.ts +38 -0
- package/dist/prompts/analyzer-user.d.ts.map +1 -0
- package/dist/prompts/analyzer-user.js +105 -0
- package/dist/prompts/analyzer-user.js.map +1 -0
- package/dist/prompts/evolve-prompt.d.ts +3 -0
- package/dist/prompts/evolve-prompt.d.ts.map +1 -0
- package/dist/prompts/evolve-prompt.js +51 -0
- package/dist/prompts/evolve-prompt.js.map +1 -0
- package/dist/scrubber.d.ts +9 -0
- package/dist/scrubber.d.ts.map +1 -0
- package/dist/scrubber.js +40 -0
- package/dist/scrubber.js.map +1 -0
- package/dist/storage.d.ts +22 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +71 -0
- package/dist/storage.js.map +1 -0
- package/dist/tool-observer.d.ts +32 -0
- package/dist/tool-observer.d.ts.map +1 -0
- package/dist/tool-observer.js +77 -0
- package/dist/tool-observer.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
- package/src/active-instincts.ts +13 -0
- package/src/agents-md.ts +23 -0
- package/src/cli/analyze-prompt.ts +71 -0
- package/src/cli/analyze.ts +286 -0
- package/src/confidence.ts +103 -0
- package/src/config.ts +111 -0
- package/src/error-logger.ts +130 -0
- package/src/index.ts +144 -0
- package/src/instinct-decay.ts +117 -0
- package/src/instinct-evolve.ts +44 -0
- package/src/instinct-export.ts +138 -0
- package/src/instinct-import.ts +260 -0
- package/src/instinct-injector.ts +128 -0
- package/src/instinct-loader.ts +146 -0
- package/src/instinct-parser.ts +171 -0
- package/src/instinct-projects.ts +119 -0
- package/src/instinct-promote.ts +231 -0
- package/src/instinct-status.ts +135 -0
- package/src/instinct-store.ts +149 -0
- package/src/instinct-tools.ts +340 -0
- package/src/observations.ts +82 -0
- package/src/observer-guard.ts +14 -0
- package/src/project.ts +70 -0
- package/src/prompt-observer.ts +92 -0
- package/src/prompts/analyzer-user.ts +156 -0
- package/src/prompts/evolve-prompt.ts +71 -0
- package/src/scrubber.ts +42 -0
- package/src/storage.ts +91 -0
- package/src/tool-observer.ts +114 -0
- package/src/types.ts +90 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure functions for instinct confidence scoring.
|
|
3
|
+
* No I/O - all functions take plain values and return plain values.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Constants
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const CLAMP_MIN = 0.1;
|
|
11
|
+
const CLAMP_MAX = 0.9;
|
|
12
|
+
|
|
13
|
+
// initialConfidence brackets
|
|
14
|
+
const INITIAL_LOW = 0.3;
|
|
15
|
+
const INITIAL_MED = 0.5;
|
|
16
|
+
const INITIAL_HIGH = 0.7;
|
|
17
|
+
const INITIAL_VERY_HIGH = 0.85;
|
|
18
|
+
|
|
19
|
+
const OBS_BRACKET_LOW_MAX = 2;
|
|
20
|
+
const OBS_BRACKET_MED_MAX = 5;
|
|
21
|
+
const OBS_BRACKET_HIGH_MAX = 10;
|
|
22
|
+
|
|
23
|
+
// adjustConfidence deltas
|
|
24
|
+
const DELTA_CONFIRMED = 0.05;
|
|
25
|
+
const DELTA_CONTRADICTED = -0.15;
|
|
26
|
+
const DELTA_INACTIVE = 0;
|
|
27
|
+
|
|
28
|
+
// applyPassiveDecay
|
|
29
|
+
const DECAY_PER_WEEK = 0.02;
|
|
30
|
+
const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Types
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export type FeedbackOutcome = "confirmed" | "contradicted" | "inactive";
|
|
37
|
+
|
|
38
|
+
export interface ConfidenceResult {
|
|
39
|
+
confidence: number;
|
|
40
|
+
flaggedForRemoval: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Helpers
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
function clamp(value: number): number {
|
|
48
|
+
return Math.max(CLAMP_MIN, Math.min(CLAMP_MAX, value));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toResult(raw: number): ConfidenceResult {
|
|
52
|
+
const flaggedForRemoval = raw < CLAMP_MIN;
|
|
53
|
+
return { confidence: clamp(raw), flaggedForRemoval };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Public API
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns the initial confidence score for a newly discovered instinct
|
|
62
|
+
* based on how many observations support it.
|
|
63
|
+
*/
|
|
64
|
+
export function initialConfidence(observationCount: number): number {
|
|
65
|
+
if (observationCount <= OBS_BRACKET_LOW_MAX) return INITIAL_LOW;
|
|
66
|
+
if (observationCount <= OBS_BRACKET_MED_MAX) return INITIAL_MED;
|
|
67
|
+
if (observationCount <= OBS_BRACKET_HIGH_MAX) return INITIAL_HIGH;
|
|
68
|
+
return INITIAL_VERY_HIGH;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Adjusts confidence based on a feedback outcome from the observer loop.
|
|
73
|
+
* Returns the clamped confidence and a flag indicating if removal is warranted.
|
|
74
|
+
*/
|
|
75
|
+
export function adjustConfidence(
|
|
76
|
+
current: number,
|
|
77
|
+
outcome: FeedbackOutcome,
|
|
78
|
+
): ConfidenceResult {
|
|
79
|
+
const deltas: Record<FeedbackOutcome, number> = {
|
|
80
|
+
confirmed: DELTA_CONFIRMED,
|
|
81
|
+
contradicted: DELTA_CONTRADICTED,
|
|
82
|
+
inactive: DELTA_INACTIVE,
|
|
83
|
+
};
|
|
84
|
+
const raw = current + deltas[outcome];
|
|
85
|
+
return toResult(raw);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Applies passive time-based decay of -0.02 per week since lastUpdated.
|
|
90
|
+
* Future lastUpdated values produce zero decay.
|
|
91
|
+
*/
|
|
92
|
+
export function applyPassiveDecay(
|
|
93
|
+
confidence: number,
|
|
94
|
+
lastUpdated: string,
|
|
95
|
+
): ConfidenceResult {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const updatedAt = new Date(lastUpdated).getTime();
|
|
98
|
+
const elapsedMs = Math.max(0, now - updatedAt);
|
|
99
|
+
const weeksElapsed = elapsedMs / MS_PER_WEEK;
|
|
100
|
+
const decay = weeksElapsed * DECAY_PER_WEEK;
|
|
101
|
+
const raw = confidence - decay;
|
|
102
|
+
return toResult(raw);
|
|
103
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration module for pi-continuous-learning.
|
|
3
|
+
* Loads user settings from ~/.pi/continuous-learning/config.json with defaults.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
10
|
+
import { Value } from "@sinclair/typebox/value";
|
|
11
|
+
import type { Config } from "./types.js";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Constants
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Maps instinct domain names to human-readable purposes.
|
|
19
|
+
* Used by findSkillShadows() to detect when an instinct is covered by an installed Pi skill.
|
|
20
|
+
*/
|
|
21
|
+
export const SKILL_DOMAINS: Record<string, string> = {
|
|
22
|
+
git: "version control and git workflows",
|
|
23
|
+
testing: "test writing and test frameworks",
|
|
24
|
+
debugging: "error analysis and debugging",
|
|
25
|
+
workflow: "development workflow and automation",
|
|
26
|
+
typescript: "TypeScript language and type system",
|
|
27
|
+
css: "CSS and styling",
|
|
28
|
+
design: "UI design and component patterns",
|
|
29
|
+
security: "security practices and vulnerability prevention",
|
|
30
|
+
performance: "performance optimization",
|
|
31
|
+
documentation: "documentation writing and standards",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const CONFIG_PATH = path.join(
|
|
35
|
+
os.homedir(),
|
|
36
|
+
".pi",
|
|
37
|
+
"continuous-learning",
|
|
38
|
+
"config.json"
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export const DEFAULT_CONFIG: Config = {
|
|
42
|
+
run_interval_minutes: 5,
|
|
43
|
+
min_observations_to_analyze: 20,
|
|
44
|
+
min_confidence: 0.5,
|
|
45
|
+
max_instincts: 20,
|
|
46
|
+
max_injection_chars: 4000,
|
|
47
|
+
model: "claude-haiku-4-5",
|
|
48
|
+
timeout_seconds: 120,
|
|
49
|
+
active_hours_start: 8,
|
|
50
|
+
active_hours_end: 23,
|
|
51
|
+
max_idle_seconds: 1800,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// TypeBox schema for partial config overrides (runtime validation)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
const PartialConfigSchema = Type.Partial(
|
|
59
|
+
Type.Object({
|
|
60
|
+
run_interval_minutes: Type.Number(),
|
|
61
|
+
min_observations_to_analyze: Type.Number(),
|
|
62
|
+
min_confidence: Type.Number(),
|
|
63
|
+
max_instincts: Type.Number(),
|
|
64
|
+
max_injection_chars: Type.Number(),
|
|
65
|
+
model: Type.String(),
|
|
66
|
+
timeout_seconds: Type.Number(),
|
|
67
|
+
active_hours_start: Type.Number(),
|
|
68
|
+
active_hours_end: Type.Number(),
|
|
69
|
+
max_idle_seconds: Type.Number(),
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
type PartialConfig = Static<typeof PartialConfigSchema>;
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// loadConfig
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Loads config from ~/.pi/continuous-learning/config.json.
|
|
81
|
+
* Returns defaults when file is absent or contains invalid JSON.
|
|
82
|
+
* Merges partial overrides with defaults (overrides win).
|
|
83
|
+
*/
|
|
84
|
+
export function loadConfig(): Config {
|
|
85
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
86
|
+
return { ...DEFAULT_CONFIG };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let raw: string;
|
|
90
|
+
try {
|
|
91
|
+
raw = fs.readFileSync(CONFIG_PATH, "utf-8") as string;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.warn(`[pi-continuous-learning] Failed to read config.json: ${String(err)}`);
|
|
94
|
+
return { ...DEFAULT_CONFIG };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let parsed: unknown;
|
|
98
|
+
try {
|
|
99
|
+
parsed = JSON.parse(raw);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn(
|
|
102
|
+
`[pi-continuous-learning] Invalid JSON in config.json: ${String(err)}. Using defaults.`
|
|
103
|
+
);
|
|
104
|
+
return { ...DEFAULT_CONFIG };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate and extract only known config fields (runtime boundary check)
|
|
108
|
+
const cleaned = Value.Clean(PartialConfigSchema, parsed) as PartialConfig;
|
|
109
|
+
|
|
110
|
+
return { ...DEFAULT_CONFIG, ...cleaned };
|
|
111
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error logging utility for pi-continuous-learning.
|
|
3
|
+
* Writes structured error entries to projects/<id>/analyzer.log.
|
|
4
|
+
* All write failures are silently swallowed - the logger must never throw.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
import { getProjectDir, getBaseDir } from "./storage.js";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Constants
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const LOG_FILENAME = "analyzer.log";
|
|
16
|
+
const PREFIX = "[pi-continuous-learning]";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Helpers
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns the absolute path to the analyzer log for the given project.
|
|
24
|
+
* Exported for testing.
|
|
25
|
+
*/
|
|
26
|
+
export function getLogPath(projectId: string, baseDir?: string): string {
|
|
27
|
+
return join(getProjectDir(projectId, baseDir ?? getBaseDir()), LOG_FILENAME);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatError(context: string, error: unknown): string {
|
|
31
|
+
const timestamp = new Date().toISOString();
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
const stack =
|
|
34
|
+
error instanceof Error && error.stack
|
|
35
|
+
? `\nStack: ${error.stack}`
|
|
36
|
+
: "";
|
|
37
|
+
return `[${timestamp}] [${context}] Error: ${message}${stack}\n`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Public API
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Logs an error to `projects/<projectId>/analyzer.log`.
|
|
46
|
+
*
|
|
47
|
+
* When `projectId` is null (e.g. session_start failed before project detection),
|
|
48
|
+
* falls back to `console.warn` only.
|
|
49
|
+
*
|
|
50
|
+
* Never throws - all I/O failures are silently swallowed.
|
|
51
|
+
*/
|
|
52
|
+
export function logError(
|
|
53
|
+
projectId: string | null,
|
|
54
|
+
context: string,
|
|
55
|
+
error: unknown,
|
|
56
|
+
baseDir?: string
|
|
57
|
+
): void {
|
|
58
|
+
const line = formatError(context, error);
|
|
59
|
+
|
|
60
|
+
if (projectId === null) {
|
|
61
|
+
console.warn(`${PREFIX} ${context}: ${line.trim()}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const logPath = getLogPath(projectId, baseDir);
|
|
66
|
+
try {
|
|
67
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
68
|
+
appendFileSync(logPath, line, "utf-8");
|
|
69
|
+
} catch {
|
|
70
|
+
// Cannot log the logger failing - fall back to console
|
|
71
|
+
console.warn(`${PREFIX} ${context}: ${line.trim()}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Logs a warning (non-error) message to the analyzer log.
|
|
77
|
+
* Used for subprocess stderr output and other non-fatal warnings.
|
|
78
|
+
*
|
|
79
|
+
* Never throws - all I/O failures are silently swallowed.
|
|
80
|
+
*/
|
|
81
|
+
export function logWarning(
|
|
82
|
+
projectId: string | null,
|
|
83
|
+
context: string,
|
|
84
|
+
message: string,
|
|
85
|
+
baseDir?: string
|
|
86
|
+
): void {
|
|
87
|
+
const timestamp = new Date().toISOString();
|
|
88
|
+
const line = `[${timestamp}] [${context}] Warning: ${message}\n`;
|
|
89
|
+
|
|
90
|
+
if (projectId === null) {
|
|
91
|
+
console.warn(`${PREFIX} ${context}: ${message}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const logPath = getLogPath(projectId, baseDir);
|
|
96
|
+
try {
|
|
97
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
98
|
+
appendFileSync(logPath, line, "utf-8");
|
|
99
|
+
} catch {
|
|
100
|
+
console.warn(`${PREFIX} ${context}: ${message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Logs an informational message to the analyzer log.
|
|
106
|
+
* Used for tracking analyzer lifecycle events (started, completed, skipped).
|
|
107
|
+
*
|
|
108
|
+
* Never throws - all I/O failures are silently swallowed.
|
|
109
|
+
*/
|
|
110
|
+
export function logInfo(
|
|
111
|
+
projectId: string | null,
|
|
112
|
+
context: string,
|
|
113
|
+
message: string,
|
|
114
|
+
baseDir?: string
|
|
115
|
+
): void {
|
|
116
|
+
const timestamp = new Date().toISOString();
|
|
117
|
+
const line = `[${timestamp}] [${context}] Info: ${message}\n`;
|
|
118
|
+
|
|
119
|
+
if (projectId === null) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const logPath = getLogPath(projectId, baseDir);
|
|
124
|
+
try {
|
|
125
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
126
|
+
appendFileSync(logPath, line, "utf-8");
|
|
127
|
+
} catch {
|
|
128
|
+
// silently swallow
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Continuous Learning Extension - Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Observes coding sessions, records events as observations, and injects
|
|
5
|
+
* learned instincts into the agent's system prompt. Background analysis
|
|
6
|
+
* runs via a separate standalone script (src/cli/analyze.ts).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { loadSkills } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
import { loadConfig } from "./config.js";
|
|
13
|
+
import { detectProject } from "./project.js";
|
|
14
|
+
import { ensureStorageLayout } from "./storage.js";
|
|
15
|
+
import { cleanOldArchives } from "./observations.js";
|
|
16
|
+
import { handleToolStart, handleToolEnd } from "./tool-observer.js";
|
|
17
|
+
import { handleBeforeAgentStart, handleAgentEnd } from "./prompt-observer.js";
|
|
18
|
+
import {
|
|
19
|
+
handleBeforeAgentStartInjection,
|
|
20
|
+
handleAgentEndClearInstincts,
|
|
21
|
+
} from "./instinct-injector.js";
|
|
22
|
+
import { handleInstinctStatus, COMMAND_NAME as STATUS_CMD } from "./instinct-status.js";
|
|
23
|
+
import { handleInstinctExport, COMMAND_NAME as EXPORT_CMD } from "./instinct-export.js";
|
|
24
|
+
import { handleInstinctImport, COMMAND_NAME as IMPORT_CMD } from "./instinct-import.js";
|
|
25
|
+
import { handleInstinctPromote, COMMAND_NAME as PROMOTE_CMD } from "./instinct-promote.js";
|
|
26
|
+
import { handleInstinctEvolve, COMMAND_NAME as EVOLVE_CMD } from "./instinct-evolve.js";
|
|
27
|
+
import { handleInstinctProjects, COMMAND_NAME as PROJECTS_CMD } from "./instinct-projects.js";
|
|
28
|
+
import { registerAllTools } from "./instinct-tools.js";
|
|
29
|
+
import { logError } from "./error-logger.js";
|
|
30
|
+
import type { Config, InstalledSkill, ProjectEntry } from "./types.js";
|
|
31
|
+
|
|
32
|
+
export default function (pi: ExtensionAPI): void {
|
|
33
|
+
let config: Config | null = null;
|
|
34
|
+
let project: ProjectEntry | null = null;
|
|
35
|
+
let installedSkills: InstalledSkill[] = [];
|
|
36
|
+
|
|
37
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
38
|
+
try {
|
|
39
|
+
config = loadConfig();
|
|
40
|
+
project = await detectProject(pi, ctx.cwd);
|
|
41
|
+
ensureStorageLayout(project);
|
|
42
|
+
cleanOldArchives(project.id);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = loadSkills({ cwd: ctx.cwd });
|
|
46
|
+
installedSkills = result.skills.map((s) => ({ name: s.name, description: s.description }));
|
|
47
|
+
} catch {
|
|
48
|
+
installedSkills = [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
registerAllTools(pi, project.id, project.name);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logError(project?.id ?? null, "session_start", err);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
pi.on("session_shutdown", (_event, _ctx) => {
|
|
58
|
+
// No cleanup needed — analyzer runs as external process
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
62
|
+
try {
|
|
63
|
+
if (!project || !config) return;
|
|
64
|
+
handleBeforeAgentStart(event, ctx, project);
|
|
65
|
+
return handleBeforeAgentStartInjection(event, ctx, config, project.id) ?? undefined;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logError(project?.id ?? null, "before_agent_start", err);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
pi.on("agent_start", (_event, _ctx) => {});
|
|
72
|
+
|
|
73
|
+
pi.on("agent_end", (event, ctx) => {
|
|
74
|
+
try {
|
|
75
|
+
if (!project) return;
|
|
76
|
+
handleAgentEnd(event, ctx, project);
|
|
77
|
+
handleAgentEndClearInstincts(event, ctx);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
logError(project?.id ?? null, "agent_end", err);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
84
|
+
try {
|
|
85
|
+
if (!project) return;
|
|
86
|
+
handleToolStart(event, ctx, project);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logError(project?.id ?? null, "tool_execution_start", err);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
93
|
+
try {
|
|
94
|
+
if (!project) return;
|
|
95
|
+
handleToolEnd(event, ctx, project);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
logError(project?.id ?? null, "tool_execution_end", err);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
pi.registerCommand(STATUS_CMD, {
|
|
102
|
+
description: "Show all instincts grouped by domain with confidence scores",
|
|
103
|
+
handler: (args: string, ctx: ExtensionCommandContext) =>
|
|
104
|
+
handleInstinctStatus(args, ctx, project?.id),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
pi.registerCommand(EXPORT_CMD, {
|
|
108
|
+
description: "Export instincts to a JSON file",
|
|
109
|
+
handler: (args: string, ctx: ExtensionCommandContext) =>
|
|
110
|
+
handleInstinctExport(args, ctx, project?.id),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
pi.registerCommand(IMPORT_CMD, {
|
|
114
|
+
description: "Import instincts from a JSON file",
|
|
115
|
+
handler: (args: string, ctx: ExtensionCommandContext) =>
|
|
116
|
+
handleInstinctImport(args, ctx, project?.id),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
pi.registerCommand(PROMOTE_CMD, {
|
|
120
|
+
description: "Promote project instincts to global scope",
|
|
121
|
+
handler: (args: string, ctx: ExtensionCommandContext) =>
|
|
122
|
+
handleInstinctPromote(args, ctx, project?.id),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
pi.registerCommand(EVOLVE_CMD, {
|
|
126
|
+
description: "Analyze instincts and suggest improvements (LLM-powered)",
|
|
127
|
+
handler: (args: string, ctx: ExtensionCommandContext) =>
|
|
128
|
+
handleInstinctEvolve(
|
|
129
|
+
args,
|
|
130
|
+
ctx,
|
|
131
|
+
pi,
|
|
132
|
+
project?.id,
|
|
133
|
+
undefined,
|
|
134
|
+
project?.root ?? null,
|
|
135
|
+
installedSkills
|
|
136
|
+
),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
pi.registerCommand(PROJECTS_CMD, {
|
|
140
|
+
description: "List all known projects and their instinct counts",
|
|
141
|
+
handler: (args: string, ctx: ExtensionCommandContext) =>
|
|
142
|
+
handleInstinctProjects(args, ctx),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passive confidence decay for instincts.
|
|
3
|
+
* Applied at the start of each analysis run to age out stale instincts.
|
|
4
|
+
*
|
|
5
|
+
* Decay: -0.02 per week since updated_at, clamped to [0.1, 0.9].
|
|
6
|
+
* Instincts dropping below 0.1 are flagged for removal.
|
|
7
|
+
*
|
|
8
|
+
* US-031: Passive Confidence Decay
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { applyPassiveDecay } from "./confidence.js";
|
|
12
|
+
import { listInstincts, saveInstinct } from "./instinct-store.js";
|
|
13
|
+
import {
|
|
14
|
+
getBaseDir,
|
|
15
|
+
getProjectInstinctsDir,
|
|
16
|
+
getGlobalInstinctsDir,
|
|
17
|
+
} from "./storage.js";
|
|
18
|
+
import type { Instinct } from "./types.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Constants
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minimum confidence change required to persist the instinct back to disk.
|
|
26
|
+
* Prevents excessive writes for negligibly small elapsed times.
|
|
27
|
+
* At 5-minute analysis intervals, elapsed decay is ~0.000002 - well below this.
|
|
28
|
+
*/
|
|
29
|
+
const DECAY_CHANGE_THRESHOLD = 0.001;
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Public API
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Applies passive decay to a single instinct.
|
|
37
|
+
*
|
|
38
|
+
* Returns the updated instinct (with adjusted confidence and refreshed
|
|
39
|
+
* updated_at) when the decay is significant, or null if no meaningful
|
|
40
|
+
* change occurred.
|
|
41
|
+
*
|
|
42
|
+
* Does not mutate the input instinct.
|
|
43
|
+
*/
|
|
44
|
+
export function applyDecayToInstinct(instinct: Instinct): Instinct | null {
|
|
45
|
+
const result = applyPassiveDecay(instinct.confidence, instinct.updated_at);
|
|
46
|
+
|
|
47
|
+
const decayAmount = Math.abs(result.confidence - instinct.confidence);
|
|
48
|
+
const flagChanged =
|
|
49
|
+
Boolean(result.flaggedForRemoval) !== Boolean(instinct.flagged_for_removal);
|
|
50
|
+
|
|
51
|
+
if (decayAmount < DECAY_CHANGE_THRESHOLD && !flagChanged) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const updated: Instinct = {
|
|
56
|
+
...instinct,
|
|
57
|
+
confidence: result.confidence,
|
|
58
|
+
updated_at: new Date().toISOString(),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Set or clear flagged_for_removal using delete pattern (exactOptionalPropertyTypes)
|
|
62
|
+
if (result.flaggedForRemoval) {
|
|
63
|
+
(updated as Partial<Instinct>).flagged_for_removal = true;
|
|
64
|
+
} else {
|
|
65
|
+
delete (updated as Partial<Instinct>).flagged_for_removal;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return updated;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Applies decay to all instincts found in a directory.
|
|
73
|
+
* Saves any instincts with meaningful confidence changes.
|
|
74
|
+
*
|
|
75
|
+
* @param dir - Directory containing .md instinct files
|
|
76
|
+
* @returns Number of instincts updated on disk
|
|
77
|
+
*/
|
|
78
|
+
export function applyDecayInDir(dir: string): number {
|
|
79
|
+
const instincts = listInstincts(dir);
|
|
80
|
+
let updatedCount = 0;
|
|
81
|
+
|
|
82
|
+
for (const instinct of instincts) {
|
|
83
|
+
const updated = applyDecayToInstinct(instinct);
|
|
84
|
+
if (updated !== null) {
|
|
85
|
+
saveInstinct(updated, dir);
|
|
86
|
+
updatedCount++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return updatedCount;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Runs a full decay pass over personal instincts for a project and globally.
|
|
95
|
+
* Called at the start of each analysis run, before the Haiku subprocess
|
|
96
|
+
* applies feedback adjustments.
|
|
97
|
+
*
|
|
98
|
+
* @param projectId - Project ID to decay (skipped when null/undefined)
|
|
99
|
+
* @param baseDir - Base storage directory (defaults to ~/.pi/continuous-learning/)
|
|
100
|
+
* @returns Total number of instincts updated across both scopes
|
|
101
|
+
*/
|
|
102
|
+
export function runDecayPass(
|
|
103
|
+
projectId?: string | null,
|
|
104
|
+
baseDir = getBaseDir()
|
|
105
|
+
): number {
|
|
106
|
+
let total = 0;
|
|
107
|
+
|
|
108
|
+
if (projectId) {
|
|
109
|
+
const projectDir = getProjectInstinctsDir(projectId, "personal", baseDir);
|
|
110
|
+
total += applyDecayInDir(projectDir);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const globalDir = getGlobalInstinctsDir("personal", baseDir);
|
|
114
|
+
total += applyDecayInDir(globalDir);
|
|
115
|
+
|
|
116
|
+
return total;
|
|
117
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { InstalledSkill } from "./types.js";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { getBaseDir } from "./storage.js";
|
|
6
|
+
import { readAgentsMd } from "./agents-md.js";
|
|
7
|
+
import { loadProjectInstincts, loadGlobalInstincts } from "./instinct-store.js";
|
|
8
|
+
import { filterInstincts } from "./instinct-loader.js";
|
|
9
|
+
import { buildEvolvePrompt } from "./prompts/evolve-prompt.js";
|
|
10
|
+
|
|
11
|
+
const MAX_EVOLVE_INSTINCTS = 100;
|
|
12
|
+
|
|
13
|
+
export const COMMAND_NAME = "instinct-evolve";
|
|
14
|
+
|
|
15
|
+
export async function handleInstinctEvolve(
|
|
16
|
+
_args: string,
|
|
17
|
+
ctx: ExtensionCommandContext,
|
|
18
|
+
pi: ExtensionAPI,
|
|
19
|
+
projectId?: string | null,
|
|
20
|
+
baseDir?: string,
|
|
21
|
+
projectRoot?: string | null,
|
|
22
|
+
installedSkills?: InstalledSkill[]
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const effectiveBase = baseDir ?? getBaseDir();
|
|
25
|
+
const projectInstincts = projectId ? loadProjectInstincts(projectId, effectiveBase) : [];
|
|
26
|
+
const globalInstincts = loadGlobalInstincts(effectiveBase);
|
|
27
|
+
const allInstincts = filterInstincts(
|
|
28
|
+
[...projectInstincts, ...globalInstincts],
|
|
29
|
+
0.1,
|
|
30
|
+
MAX_EVOLVE_INSTINCTS
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (allInstincts.length === 0) {
|
|
34
|
+
ctx.ui.notify("No instincts to analyze. Keep using pi to accumulate instincts first.", "info");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const agentsMdProject =
|
|
39
|
+
projectRoot != null ? readAgentsMd(join(projectRoot, "AGENTS.md")) : null;
|
|
40
|
+
const agentsMdGlobal = readAgentsMd(join(homedir(), ".pi", "agent", "AGENTS.md"));
|
|
41
|
+
|
|
42
|
+
const prompt = buildEvolvePrompt(allInstincts, agentsMdProject, agentsMdGlobal, installedSkills);
|
|
43
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
44
|
+
}
|