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,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /instinct-export command for pi-continuous-learning.
|
|
3
|
+
* Exports instincts to a JSON file in the current directory.
|
|
4
|
+
* Optional args: scope (project|global) and domain filter.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import type { Instinct, InstinctScope } from "./types.js";
|
|
11
|
+
import { loadProjectInstincts, loadGlobalInstincts } from "./instinct-store.js";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Constants
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const COMMAND_NAME = "instinct-export";
|
|
18
|
+
const SCOPE_PROJECT: InstinctScope = "project";
|
|
19
|
+
const SCOPE_GLOBAL: InstinctScope = "global";
|
|
20
|
+
const VALID_SCOPES: readonly string[] = [SCOPE_PROJECT, SCOPE_GLOBAL];
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Arg parsing
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export interface ExportArgs {
|
|
27
|
+
scope: InstinctScope | null;
|
|
28
|
+
domain: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parses space-separated args string.
|
|
33
|
+
* If first token is "project" or "global", it is treated as scope filter.
|
|
34
|
+
* Remaining tokens (if any) are joined as domain filter.
|
|
35
|
+
*/
|
|
36
|
+
export function parseExportArgs(args: string): ExportArgs {
|
|
37
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
38
|
+
if (tokens.length === 0) {
|
|
39
|
+
return { scope: null, domain: null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const first = tokens[0] ?? "";
|
|
43
|
+
const isScope = VALID_SCOPES.includes(first);
|
|
44
|
+
|
|
45
|
+
const scope = isScope ? (first as InstinctScope) : null;
|
|
46
|
+
const domainTokens = isScope ? tokens.slice(1) : tokens;
|
|
47
|
+
const domain = domainTokens.length > 0 ? domainTokens.join(" ") : null;
|
|
48
|
+
|
|
49
|
+
return { scope, domain };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Filtering
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Filters instincts by optional scope and domain.
|
|
58
|
+
* Immutable - returns a new array.
|
|
59
|
+
*/
|
|
60
|
+
export function filterInstinctsForExport(
|
|
61
|
+
instincts: Instinct[],
|
|
62
|
+
scope: InstinctScope | null,
|
|
63
|
+
domain: string | null
|
|
64
|
+
): Instinct[] {
|
|
65
|
+
return instincts.filter((instinct) => {
|
|
66
|
+
if (scope !== null && instinct.scope !== scope) return false;
|
|
67
|
+
if (domain !== null && instinct.domain !== domain) return false;
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Filename generation
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generates an export filename with timestamp.
|
|
78
|
+
* Format: instincts-export-<YYYYMMDDTHHmmss>.json
|
|
79
|
+
*/
|
|
80
|
+
export function buildExportFilename(now: Date = new Date()): string {
|
|
81
|
+
const iso = now.toISOString(); // "2026-03-26T17:12:20.216Z"
|
|
82
|
+
// Compact: remove dashes, colons, milliseconds, and trailing Z
|
|
83
|
+
const compact = iso
|
|
84
|
+
.replace(/-/g, "")
|
|
85
|
+
.replace(/:/g, "")
|
|
86
|
+
.replace(/\.\d+Z$/, "");
|
|
87
|
+
return `instincts-export-${compact}.json`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// loadAllInstinctsForExport
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Loads instincts from disk (project + global) for export.
|
|
96
|
+
* Does NOT apply confidence filtering - export includes everything.
|
|
97
|
+
*/
|
|
98
|
+
export function loadAllInstinctsForExport(
|
|
99
|
+
projectId?: string | null,
|
|
100
|
+
baseDir?: string
|
|
101
|
+
): Instinct[] {
|
|
102
|
+
const projectInstincts =
|
|
103
|
+
projectId != null ? loadProjectInstincts(projectId, baseDir) : [];
|
|
104
|
+
const globalInstincts = loadGlobalInstincts(baseDir);
|
|
105
|
+
return [...projectInstincts, ...globalInstincts];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// handleInstinctExport
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Command handler for /instinct-export.
|
|
114
|
+
* Loads instincts, applies optional filters, writes JSON file to cwd.
|
|
115
|
+
*/
|
|
116
|
+
export async function handleInstinctExport(
|
|
117
|
+
args: string,
|
|
118
|
+
ctx: ExtensionCommandContext,
|
|
119
|
+
projectId?: string | null,
|
|
120
|
+
baseDir?: string
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const { scope, domain } = parseExportArgs(args);
|
|
123
|
+
|
|
124
|
+
const all = loadAllInstinctsForExport(projectId, baseDir);
|
|
125
|
+
const filtered = filterInstinctsForExport(all, scope, domain);
|
|
126
|
+
|
|
127
|
+
const filename = buildExportFilename();
|
|
128
|
+
const outputPath = join(ctx.cwd, filename);
|
|
129
|
+
|
|
130
|
+
writeFileSync(outputPath, JSON.stringify(filtered, null, 2), "utf-8");
|
|
131
|
+
|
|
132
|
+
ctx.ui.notify(
|
|
133
|
+
`Exported ${filtered.length} instinct${filtered.length !== 1 ? "s" : ""} to ${outputPath}`,
|
|
134
|
+
"info"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { COMMAND_NAME };
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /instinct-import command for pi-continuous-learning.
|
|
3
|
+
* Imports instincts from a JSON file into the inherited instincts directory.
|
|
4
|
+
* Destination is determined by each instinct's scope field:
|
|
5
|
+
* - scope "project" -> projects/<id>/instincts/inherited/
|
|
6
|
+
* - scope "global" -> instincts/inherited/
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, mkdirSync, existsSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import type { Instinct } from "./types.js";
|
|
13
|
+
import { saveInstinct, listInstincts } from "./instinct-store.js";
|
|
14
|
+
import {
|
|
15
|
+
getProjectInstinctsDir,
|
|
16
|
+
getGlobalInstinctsDir,
|
|
17
|
+
getBaseDir,
|
|
18
|
+
} from "./storage.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Constants
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export const COMMAND_NAME = "instinct-import";
|
|
25
|
+
|
|
26
|
+
const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
27
|
+
const REQUIRED_FIELDS = [
|
|
28
|
+
"id",
|
|
29
|
+
"title",
|
|
30
|
+
"trigger",
|
|
31
|
+
"action",
|
|
32
|
+
"confidence",
|
|
33
|
+
"domain",
|
|
34
|
+
"source",
|
|
35
|
+
"scope",
|
|
36
|
+
"created_at",
|
|
37
|
+
"updated_at",
|
|
38
|
+
"observation_count",
|
|
39
|
+
"confirmed_count",
|
|
40
|
+
"contradicted_count",
|
|
41
|
+
"inactive_count",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Validation
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export interface ValidationError {
|
|
49
|
+
index: number;
|
|
50
|
+
reason: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validates a raw JSON object as an Instinct.
|
|
55
|
+
* Returns an error string if invalid, null if valid.
|
|
56
|
+
*/
|
|
57
|
+
export function validateImportObject(
|
|
58
|
+
obj: unknown,
|
|
59
|
+
index: number
|
|
60
|
+
): ValidationError | null {
|
|
61
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
62
|
+
return { index, reason: "not an object" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const record = obj as Record<string, unknown>;
|
|
66
|
+
|
|
67
|
+
for (const field of REQUIRED_FIELDS) {
|
|
68
|
+
if (record[field] === undefined || record[field] === null) {
|
|
69
|
+
return { index, reason: `missing required field "${field}"` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const id = String(record["id"]);
|
|
74
|
+
if (!KEBAB_RE.test(id)) {
|
|
75
|
+
return { index, reason: `invalid id "${id}" - must be kebab-case` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// File loading
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
export interface LoadResult {
|
|
86
|
+
valid: Instinct[];
|
|
87
|
+
invalid: ValidationError[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reads and parses the import JSON file.
|
|
92
|
+
* Returns valid instincts and validation errors separately.
|
|
93
|
+
*/
|
|
94
|
+
export function loadImportFile(filePath: string): LoadResult {
|
|
95
|
+
const content = readFileSync(filePath, "utf-8");
|
|
96
|
+
const parsed: unknown = JSON.parse(content);
|
|
97
|
+
|
|
98
|
+
if (!Array.isArray(parsed)) {
|
|
99
|
+
throw new Error("Import file must contain a JSON array of instinct objects.");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const valid: Instinct[] = [];
|
|
103
|
+
const invalid: ValidationError[] = [];
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
106
|
+
const err = validateImportObject(parsed[i], i);
|
|
107
|
+
if (err) {
|
|
108
|
+
invalid.push(err);
|
|
109
|
+
} else {
|
|
110
|
+
valid.push(parsed[i] as Instinct);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { valid, invalid };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Duplicate detection
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
export interface PartitionResult {
|
|
122
|
+
toImport: Instinct[];
|
|
123
|
+
duplicates: string[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Partitions instincts into those to import and those skipped as duplicates.
|
|
128
|
+
* Checks existing inherited instincts in both project and global directories.
|
|
129
|
+
*/
|
|
130
|
+
export function partitionByDuplicates(
|
|
131
|
+
instincts: Instinct[],
|
|
132
|
+
projectId: string | null | undefined,
|
|
133
|
+
baseDir: string
|
|
134
|
+
): PartitionResult {
|
|
135
|
+
const existingIds = new Set<string>();
|
|
136
|
+
|
|
137
|
+
const globalDir = getGlobalInstinctsDir("inherited", baseDir);
|
|
138
|
+
for (const inst of listInstincts(globalDir)) {
|
|
139
|
+
existingIds.add(inst.id);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (projectId != null) {
|
|
143
|
+
const projectDir = getProjectInstinctsDir(projectId, "inherited", baseDir);
|
|
144
|
+
for (const inst of listInstincts(projectDir)) {
|
|
145
|
+
existingIds.add(inst.id);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const toImport: Instinct[] = [];
|
|
150
|
+
const duplicates: string[] = [];
|
|
151
|
+
|
|
152
|
+
for (const inst of instincts) {
|
|
153
|
+
if (existingIds.has(inst.id)) {
|
|
154
|
+
duplicates.push(inst.id);
|
|
155
|
+
} else {
|
|
156
|
+
toImport.push(inst);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { toImport, duplicates };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Target directory resolution
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Returns the inherited instincts directory for the given instinct.
|
|
169
|
+
* Project-scoped instincts go into the project's inherited dir.
|
|
170
|
+
* Global instincts go into the global inherited dir.
|
|
171
|
+
*/
|
|
172
|
+
export function getTargetDir(
|
|
173
|
+
instinct: Instinct,
|
|
174
|
+
projectId: string | null | undefined,
|
|
175
|
+
baseDir: string
|
|
176
|
+
): string {
|
|
177
|
+
if (instinct.scope === "project" && projectId != null) {
|
|
178
|
+
return getProjectInstinctsDir(projectId, "inherited", baseDir);
|
|
179
|
+
}
|
|
180
|
+
return getGlobalInstinctsDir("inherited", baseDir);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// handleInstinctImport
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Command handler for /instinct-import.
|
|
189
|
+
* Reads the JSON file at the given path, validates each instinct,
|
|
190
|
+
* skips duplicates, and saves valid instincts to inherited/ directories.
|
|
191
|
+
*/
|
|
192
|
+
export async function handleInstinctImport(
|
|
193
|
+
args: string,
|
|
194
|
+
ctx: ExtensionCommandContext,
|
|
195
|
+
projectId?: string | null,
|
|
196
|
+
baseDir?: string
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
const effectiveBase = baseDir ?? getBaseDir();
|
|
199
|
+
const filePath = resolve(ctx.cwd, args.trim());
|
|
200
|
+
|
|
201
|
+
if (!existsSync(filePath)) {
|
|
202
|
+
ctx.ui.notify(`Import failed: file not found: ${filePath}`, "error");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let loadResult: LoadResult;
|
|
207
|
+
try {
|
|
208
|
+
loadResult = loadImportFile(filePath);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
211
|
+
ctx.ui.notify(`Import failed: ${msg}`, "error");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const { valid, invalid } = loadResult;
|
|
216
|
+
|
|
217
|
+
const { toImport, duplicates } = partitionByDuplicates(
|
|
218
|
+
valid,
|
|
219
|
+
projectId,
|
|
220
|
+
effectiveBase
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Ensure target dirs exist before writing
|
|
224
|
+
const globalInheritedDir = getGlobalInstinctsDir("inherited", effectiveBase);
|
|
225
|
+
mkdirSync(globalInheritedDir, { recursive: true });
|
|
226
|
+
|
|
227
|
+
if (projectId != null) {
|
|
228
|
+
const projectInheritedDir = getProjectInstinctsDir(
|
|
229
|
+
projectId,
|
|
230
|
+
"inherited",
|
|
231
|
+
effectiveBase
|
|
232
|
+
);
|
|
233
|
+
mkdirSync(projectInheritedDir, { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Save each instinct to the correct inherited directory
|
|
237
|
+
for (const instinct of toImport) {
|
|
238
|
+
const targetDir = getTargetDir(instinct, projectId, effectiveBase);
|
|
239
|
+
saveInstinct(instinct, targetDir);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Build summary message
|
|
243
|
+
const lines: string[] = [
|
|
244
|
+
`Imported ${toImport.length} instinct${toImport.length !== 1 ? "s" : ""} from ${filePath}`,
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
if (duplicates.length > 0) {
|
|
248
|
+
lines.push(
|
|
249
|
+
`Skipped ${duplicates.length} duplicate${duplicates.length !== 1 ? "s" : ""}: ${duplicates.join(", ")}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (invalid.length > 0) {
|
|
254
|
+
lines.push(
|
|
255
|
+
`Skipped ${invalid.length} invalid entr${invalid.length !== 1 ? "ies" : "y"}: ${invalid.map((e) => `[${e.index}] ${e.reason}`).join("; ")}`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
260
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System prompt injection for pi-continuous-learning.
|
|
3
|
+
* Loads filtered instincts and appends them to the system prompt on each
|
|
4
|
+
* before_agent_start event so the agent benefits from learned behaviors.
|
|
5
|
+
* Also bridges injected instinct IDs to shared active-instincts state (US-023).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { BeforeAgentStartEvent, AgentEndEvent } from "./prompt-observer.js";
|
|
10
|
+
import type { Config, Instinct } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/** Subset of BeforeAgentStartEventResult used by this module. */
|
|
13
|
+
export interface InjectionResult {
|
|
14
|
+
/** Replacement system prompt to use for this turn. */
|
|
15
|
+
systemPrompt?: string;
|
|
16
|
+
}
|
|
17
|
+
import { loadAndFilterFromConfig, inferDomains } from "./instinct-loader.js";
|
|
18
|
+
import {
|
|
19
|
+
setCurrentActiveInstincts,
|
|
20
|
+
clearActiveInstincts,
|
|
21
|
+
} from "./active-instincts.js";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Constants
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export const INSTINCTS_HEADER = "## Learned Behaviors (Instincts)";
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// buildInjectionBlock
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Builds the injection block string from a list of instincts.
|
|
35
|
+
* Returns null when the list is empty (no block needed).
|
|
36
|
+
*/
|
|
37
|
+
export function buildInjectionBlock(instincts: Instinct[], maxChars?: number): string | null {
|
|
38
|
+
if (instincts.length === 0) return null;
|
|
39
|
+
|
|
40
|
+
const headerLen = `\n\n${INSTINCTS_HEADER}\n`.length;
|
|
41
|
+
const allBullets: string[] = [];
|
|
42
|
+
let charCount = headerLen;
|
|
43
|
+
let omitted = 0;
|
|
44
|
+
|
|
45
|
+
for (const i of instincts) {
|
|
46
|
+
const bullet = `- [${i.confidence.toFixed(2)}] ${i.trigger}: ${i.action}`;
|
|
47
|
+
const bulletLen = bullet.length + 1; // +1 for newline
|
|
48
|
+
|
|
49
|
+
if (maxChars && charCount + bulletLen > maxChars) {
|
|
50
|
+
omitted = instincts.length - allBullets.length;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
allBullets.push(bullet);
|
|
55
|
+
charCount += bulletLen;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (allBullets.length === 0) return null;
|
|
59
|
+
|
|
60
|
+
let result = `\n\n${INSTINCTS_HEADER}\n${allBullets.join("\n")}`;
|
|
61
|
+
if (omitted > 0) {
|
|
62
|
+
result += `\n(${omitted} lower-confidence instinct${omitted > 1 ? "s" : ""} omitted)`;
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// injectInstincts (pure, for testing)
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns a modified system prompt string with injected instincts,
|
|
73
|
+
* or null when no qualifying instincts were found.
|
|
74
|
+
* Pure function - no I/O.
|
|
75
|
+
*/
|
|
76
|
+
export function injectInstincts(
|
|
77
|
+
systemPrompt: string,
|
|
78
|
+
instincts: Instinct[]
|
|
79
|
+
): string | null {
|
|
80
|
+
const block = buildInjectionBlock(instincts);
|
|
81
|
+
if (block === null) return null;
|
|
82
|
+
return systemPrompt + block;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// handleBeforeAgentStartInjection
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handles before_agent_start events.
|
|
91
|
+
* Loads qualifying instincts, appends them to the system prompt, and stores
|
|
92
|
+
* their IDs in shared active-instincts state for observation tagging (US-023).
|
|
93
|
+
* Returns undefined when no instincts qualify (no-op).
|
|
94
|
+
*/
|
|
95
|
+
export function handleBeforeAgentStartInjection(
|
|
96
|
+
event: BeforeAgentStartEvent,
|
|
97
|
+
_ctx: ExtensionContext,
|
|
98
|
+
config: Config,
|
|
99
|
+
projectId?: string | null,
|
|
100
|
+
baseDir?: string
|
|
101
|
+
): InjectionResult | void {
|
|
102
|
+
const relevantDomains = inferDomains(event.prompt);
|
|
103
|
+
const instincts = loadAndFilterFromConfig(config, projectId, baseDir, relevantDomains);
|
|
104
|
+
|
|
105
|
+
const block = buildInjectionBlock(instincts, config.max_injection_chars);
|
|
106
|
+
if (block === null) {
|
|
107
|
+
setCurrentActiveInstincts([]);
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setCurrentActiveInstincts(instincts.map((i) => i.id));
|
|
112
|
+
return { systemPrompt: event.systemPrompt + block };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// handleAgentEndClearInstincts
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handles agent_end events.
|
|
121
|
+
* Clears active instincts state so the next prompt starts clean (US-023).
|
|
122
|
+
*/
|
|
123
|
+
export function handleAgentEndClearInstincts(
|
|
124
|
+
_event: AgentEndEvent,
|
|
125
|
+
_ctx: ExtensionContext
|
|
126
|
+
): void {
|
|
127
|
+
clearActiveInstincts();
|
|
128
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instinct loading and filtering for the injector.
|
|
3
|
+
* Loads project and global instincts, filters by confidence threshold,
|
|
4
|
+
* sorts by confidence descending, and caps to max_instincts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Instinct, Config } from "./types.js";
|
|
8
|
+
import { loadProjectInstincts, loadGlobalInstincts } from "./instinct-store.js";
|
|
9
|
+
import { DEFAULT_CONFIG } from "./config.js";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Domain inference
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const UNIVERSAL_DOMAINS = new Set(["workflow", "git"]);
|
|
16
|
+
|
|
17
|
+
const DOMAIN_KEYWORDS: Record<string, string[]> = {
|
|
18
|
+
typescript: ["typescript", ".ts", "type ", "interface ", "generic"],
|
|
19
|
+
css: ["css", "style", "tailwind", "classname", "scss", "sass"],
|
|
20
|
+
testing: ["test", "spec", "vitest", "jest", "coverage", "assert"],
|
|
21
|
+
git: ["git", "commit", "branch", "merge", "rebase", "stash"],
|
|
22
|
+
debugging: ["debug", "error", "stack trace", "exception", "breakpoint"],
|
|
23
|
+
performance: ["performance", "slow", "memory", "profil", "latency", "cache"],
|
|
24
|
+
security: ["security", "auth", "token", "secret", "csrf", "xss", "injection"],
|
|
25
|
+
documentation: ["documentation", "readme", "jsdoc", "docstring"],
|
|
26
|
+
design: ["component", "ui ", "layout", "responsive", "accessibility"],
|
|
27
|
+
workflow: ["workflow", "ci", "pipeline", "deploy", "automat"],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function inferDomains(userPrompt: string): Set<string> {
|
|
31
|
+
const lower = userPrompt.toLowerCase();
|
|
32
|
+
const matched = new Set<string>();
|
|
33
|
+
for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
|
|
34
|
+
if (keywords.some((kw) => lower.includes(kw))) {
|
|
35
|
+
matched.add(domain);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return matched;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Types
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export interface LoadInstinctsOptions {
|
|
46
|
+
/** Project ID, or undefined/null when running outside a project. */
|
|
47
|
+
projectId?: string | null;
|
|
48
|
+
/** Minimum confidence threshold (default: DEFAULT_CONFIG.min_confidence). */
|
|
49
|
+
minConfidence?: number;
|
|
50
|
+
/** Maximum number of instincts to return (default: DEFAULT_CONFIG.max_instincts). */
|
|
51
|
+
maxInstincts?: number;
|
|
52
|
+
/** Optional base directory for storage (used in tests). */
|
|
53
|
+
baseDir?: string;
|
|
54
|
+
/** Domains relevant to the current context — matched instincts sort first. */
|
|
55
|
+
relevantDomains?: Set<string>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// filterInstincts
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Filters, sorts, and caps a flat list of instincts.
|
|
64
|
+
* Pure function - no I/O.
|
|
65
|
+
*/
|
|
66
|
+
export function filterInstincts(
|
|
67
|
+
instincts: Instinct[],
|
|
68
|
+
minConfidence: number,
|
|
69
|
+
maxInstincts: number,
|
|
70
|
+
relevantDomains?: Set<string>
|
|
71
|
+
): Instinct[] {
|
|
72
|
+
const eligible = instincts.filter(
|
|
73
|
+
(i) => !i.flagged_for_removal && i.confidence >= minConfidence
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const sorted = [...eligible].sort((a, b) => {
|
|
77
|
+
// When relevantDomains are provided, prioritize domain-matched instincts
|
|
78
|
+
if (relevantDomains && relevantDomains.size > 0) {
|
|
79
|
+
const aRelevant = relevantDomains.has(a.domain) || UNIVERSAL_DOMAINS.has(a.domain);
|
|
80
|
+
const bRelevant = relevantDomains.has(b.domain) || UNIVERSAL_DOMAINS.has(b.domain);
|
|
81
|
+
if (aRelevant && !bRelevant) return -1;
|
|
82
|
+
if (!aRelevant && bRelevant) return 1;
|
|
83
|
+
}
|
|
84
|
+
return b.confidence - a.confidence;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return sorted.slice(0, maxInstincts);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// loadAndFilterInstincts
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Loads instincts from disk, filters by confidence threshold, sorts by
|
|
96
|
+
* confidence descending, and caps to max_instincts.
|
|
97
|
+
*
|
|
98
|
+
* When projectId is provided (and non-null), loads both project-scoped
|
|
99
|
+
* instincts and global instincts. Otherwise loads only global instincts.
|
|
100
|
+
*/
|
|
101
|
+
export function loadAndFilterInstincts(
|
|
102
|
+
options: LoadInstinctsOptions = {}
|
|
103
|
+
): Instinct[] {
|
|
104
|
+
const {
|
|
105
|
+
projectId,
|
|
106
|
+
minConfidence = DEFAULT_CONFIG.min_confidence,
|
|
107
|
+
maxInstincts = DEFAULT_CONFIG.max_instincts,
|
|
108
|
+
baseDir,
|
|
109
|
+
relevantDomains,
|
|
110
|
+
} = options;
|
|
111
|
+
|
|
112
|
+
const projectInstincts =
|
|
113
|
+
projectId != null
|
|
114
|
+
? loadProjectInstincts(projectId, baseDir)
|
|
115
|
+
: [];
|
|
116
|
+
|
|
117
|
+
const globalInstincts = loadGlobalInstincts(baseDir);
|
|
118
|
+
|
|
119
|
+
// Combine: project instincts first, then global (project-scoped are more specific)
|
|
120
|
+
const all = [...projectInstincts, ...globalInstincts];
|
|
121
|
+
|
|
122
|
+
return filterInstincts(all, minConfidence, maxInstincts, relevantDomains);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// loadAndFilterFromConfig
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Convenience wrapper - uses thresholds from a Config object.
|
|
131
|
+
*/
|
|
132
|
+
export function loadAndFilterFromConfig(
|
|
133
|
+
config: Config,
|
|
134
|
+
projectId?: string | null,
|
|
135
|
+
baseDir?: string,
|
|
136
|
+
relevantDomains?: Set<string>
|
|
137
|
+
): Instinct[] {
|
|
138
|
+
const opts: LoadInstinctsOptions = {
|
|
139
|
+
minConfidence: config.min_confidence,
|
|
140
|
+
maxInstincts: config.max_instincts,
|
|
141
|
+
};
|
|
142
|
+
if (projectId !== undefined) opts.projectId = projectId;
|
|
143
|
+
if (baseDir !== undefined) opts.baseDir = baseDir;
|
|
144
|
+
if (relevantDomains !== undefined) opts.relevantDomains = relevantDomains;
|
|
145
|
+
return loadAndFilterInstincts(opts);
|
|
146
|
+
}
|